Files
nagi-bench/outputs/claude-haiku-4-5/claude-code-default/mythos-craft.html
T
Jesse ZhangandClaude Opus 4.8 3d5bf6967c Normalize artifact filenames and register new runs (#17)
Rename all output artifacts to their canonical case-id names
(mythos-craft.html / pelican-cycling.svg / skeleton-watch.html, with
-2 suffixes for variants) and drop the now-redundant "file" overrides
from the corresponding models/*.json runs.

Register new Claude Code runs for Opus 4.6 and 4.7 (new model JSONs),
add the GPT-5.5 Pro skeleton-watch run, and regenerate the README
registry tables. Remove stray .DS_Store files from outputs/.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:36:38 +01:00

713 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>魔幻世界 - Minecraft克隆版</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100%;
height: 100vh;
font-family: 'Arial', sans-serif;
background: #000;
overflow: hidden;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
#info {
position: absolute;
top: 10px;
left: 10px;
color: #fff;
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 8px;
font-size: 14px;
z-index: 100;
max-width: 300px;
}
#info p {
margin: 5px 0;
}
#crosshair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.7);
border-radius: 50%;
pointer-events: none;
}
#crosshair::before,
#crosshair::after {
content: '';
position: absolute;
background: rgba(255, 255, 255, 0.7);
}
#crosshair::before {
width: 8px;
height: 2px;
left: 6px;
top: 9px;
}
#crosshair::after {
width: 2px;
height: 8px;
left: 9px;
top: 6px;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="crosshair"></div>
<div id="info">
<p><strong>魔幻世界</strong></p>
<p>移动: WASD | 跳跃: 空格</p>
<p>鼠标: 环视 | 左键: 破坏 | 右键: 放置</p>
<p id="pos">位置: (0, 0, 0)</p>
<p id="block">方块: 无</p>
<p id="fps">FPS: 0</p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ==================== 柏林噪声实现 ====================
class PerlinNoise {
constructor(seed = 0) {
this.seed = seed;
this.permutation = this.buildPermutation(seed);
this.p = [...this.permutation, ...this.permutation];
}
buildPermutation(seed) {
const p = Array.from({ length: 256 }, (_, i) => i);
// Fisher-Yates shuffle with seed
for (let i = 255; i > 0; i--) {
const j = Math.floor((Math.sin(seed + i) * 10000 % 1) * (i + 1));
[p[i], p[j]] = [p[j], p[i]];
}
return p;
}
fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
lerp(t, a, b) {
return a + t * (b - a);
}
grad(hash, x, y, z) {
const h = hash & 15;
const u = h < 8 ? x : y;
const v = h < 8 ? y : z;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
noise(x, y, z) {
const xi = Math.floor(x) & 255;
const yi = Math.floor(y) & 255;
const zi = Math.floor(z) & 255;
const xf = x - Math.floor(x);
const yf = y - Math.floor(y);
const zf = z - Math.floor(z);
const u = this.fade(xf);
const v = this.fade(yf);
const w = this.fade(zf);
const aaa = this.p[this.p[this.p[xi] + yi] + zi];
const aba = this.p[this.p[this.p[xi] + (yi + 1)] + zi];
const aab = this.p[this.p[this.p[xi] + yi] + (zi + 1)];
const abb = this.p[this.p[this.p[xi] + (yi + 1)] + (zi + 1)];
const baa = this.p[this.p[this.p[(xi + 1)] + yi] + zi];
const bba = this.p[this.p[this.p[(xi + 1)] + (yi + 1)] + zi];
const bab = this.p[this.p[this.p[(xi + 1)] + yi] + (zi + 1)];
const bbb = this.p[this.p[this.p[(xi + 1)] + (yi + 1)] + (zi + 1)];
const ga = this.grad(aaa, xf, yf, zf);
const gb = this.grad(baa, xf - 1, yf, zf);
const gc = this.grad(aba, xf, yf - 1, zf);
const gd = this.grad(bba, xf - 1, yf - 1, zf);
const ge = this.grad(aab, xf, yf, zf - 1);
const gf = this.grad(bab, xf - 1, yf, zf - 1);
const gg = this.grad(abb, xf, yf - 1, zf - 1);
const gh = this.grad(bbb, xf - 1, yf - 1, zf - 1);
const p0 = this.lerp(u, ga, gb);
const p1 = this.lerp(u, gc, gd);
const p2 = this.lerp(u, ge, gf);
const p3 = this.lerp(u, gg, gh);
const p4 = this.lerp(v, p0, p1);
const p5 = this.lerp(v, p2, p3);
return this.lerp(w, p4, p5);
}
}
// ==================== 块状世界管理 ====================
const CHUNK_SIZE = 16;
const CHUNK_HEIGHT = 256;
const WORLD_HEIGHT = 64;
const LOAD_DISTANCE = 3;
const BlockType = {
AIR: 0,
GRASS: 1,
DIRT: 2,
STONE: 3,
OBSIDIAN: 4,
CRYSTAL: 5
};
const BlockColors = {
[BlockType.GRASS]: new THREE.Color(0x7cb342),
[BlockType.DIRT]: new THREE.Color(0x8b6f47),
[BlockType.STONE]: new THREE.Color(0x757575),
[BlockType.OBSIDIAN]: new THREE.Color(0x1a1a2e),
[BlockType.CRYSTAL]: new THREE.Color(0x00bcd4)
};
class World {
constructor() {
this.perlin = new PerlinNoise(42);
this.chunks = new Map();
this.scene = null;
this.meshes = new Map();
}
getBlockType(x, y, z) {
const h = this.getHeight(x, z);
if (y > h) return BlockType.AIR;
if (y === h) return BlockType.GRASS;
if (y > h - 3) return BlockType.DIRT;
return BlockType.STONE;
}
getHeight(x, z) {
let height = 20;
let scale = 1;
let amplitude = 1;
let maxAmplitude = 0;
for (let i = 0; i < 4; i++) {
const n = this.perlin.noise(x * scale * 0.05, 0, z * scale * 0.05);
height += (n * 0.5 + 0.5) * 15 * amplitude;
maxAmplitude += amplitude;
scale *= 2;
amplitude *= 0.5;
}
return Math.floor(height / maxAmplitude);
}
addMythicStructures(x, z, chunk) {
const chunkX = Math.floor(x / CHUNK_SIZE);
const chunkZ = Math.floor(z / CHUNK_SIZE);
// 魔法塔
if (chunkX === 2 && chunkZ === 2) {
this.buildMagicTower(x + 5, z + 5, chunk);
}
// 古代神庙
if (chunkX === -1 && chunkZ === 1) {
this.buildAncientTemple(x + 8, z + 8, chunk);
}
// 水晶洞
if (chunkX === 1 && chunkZ === -1) {
this.buildCrystalCave(x + 7, z + 7, chunk);
}
// 悬浮岛
if (chunkX === -2 && chunkZ === -2) {
this.buildFloatingIsland(x + 10, z + 10, chunk);
}
}
buildMagicTower(baseX, baseZ, chunk) {
const baseHeight = this.getHeight(baseX, baseZ) + 1;
// 塔底
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
const x = Math.floor(baseX + dx);
const z = Math.floor(baseZ + dz);
if (this.isInChunk(x, z, chunk)) {
const idx = this.getBlockIndex(x, z, baseHeight, chunk);
if (idx >= 0) chunk[idx] = BlockType.OBSIDIAN;
}
}
}
// 塔身
for (let h = 1; h < 15; h++) {
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
if (Math.abs(dx) === 1 || Math.abs(dz) === 1) {
const x = Math.floor(baseX + dx);
const z = Math.floor(baseZ + dz);
if (this.isInChunk(x, z, chunk)) {
const idx = this.getBlockIndex(x, z, baseHeight + h, chunk);
if (idx >= 0) chunk[idx] = h % 2 === 0 ? BlockType.OBSIDIAN : BlockType.CRYSTAL;
}
}
}
}
}
}
buildAncientTemple(baseX, baseZ, chunk) {
const baseHeight = this.getHeight(baseX, baseZ) + 1;
// 主体
for (let dx = -3; dx <= 3; dx++) {
for (let dz = -3; dz <= 3; dz++) {
const x = Math.floor(baseX + dx);
const z = Math.floor(baseZ + dz);
if (this.isInChunk(x, z, chunk)) {
const idx = this.getBlockIndex(x, z, baseHeight, chunk);
if (idx >= 0) chunk[idx] = BlockType.STONE;
}
}
}
// 柱子
for (let h = 1; h < 8; h++) {
const positions = [[-3, -3], [3, -3], [-3, 3], [3, 3]];
positions.forEach(([dx, dz]) => {
const x = Math.floor(baseX + dx);
const z = Math.floor(baseZ + dz);
if (this.isInChunk(x, z, chunk)) {
const idx = this.getBlockIndex(x, z, baseHeight + h, chunk);
if (idx >= 0) chunk[idx] = BlockType.STONE;
}
});
}
}
buildCrystalCave(baseX, baseZ, chunk) {
const baseHeight = this.getHeight(baseX, baseZ) - 5;
for (let dx = -4; dx <= 4; dx++) {
for (let dz = -4; dz <= 4; dz++) {
for (let h = 0; h < 8; h++) {
if (Math.random() < 0.3) {
const x = Math.floor(baseX + dx);
const z = Math.floor(baseZ + dz);
if (this.isInChunk(x, z, chunk)) {
const idx = this.getBlockIndex(x, z, baseHeight + h, chunk);
if (idx >= 0) chunk[idx] = BlockType.CRYSTAL;
}
}
}
}
}
}
buildFloatingIsland(baseX, baseZ, chunk) {
const baseHeight = this.getHeight(baseX, baseZ) + 30;
for (let dx = -5; dx <= 5; dx++) {
for (let dz = -5; dz <= 5; dz++) {
const dist = Math.sqrt(dx * dx + dz * dz);
if (dist < 5) {
const x = Math.floor(baseX + dx);
const z = Math.floor(baseZ + dz);
if (this.isInChunk(x, z, chunk)) {
const idx = this.getBlockIndex(x, z, baseHeight, chunk);
if (idx >= 0) chunk[idx] = BlockType.GRASS;
}
}
}
}
}
isInChunk(x, z, chunk) {
return x >= 0 && x < CHUNK_SIZE && z >= 0 && z < CHUNK_SIZE;
}
getBlockIndex(x, z, y, chunk) {
if (y < 0 || y >= CHUNK_HEIGHT) return -1;
return y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x;
}
getChunk(cx, cz) {
const key = `${cx},${cz}`;
if (!this.chunks.has(key)) {
const chunk = new Uint8Array(CHUNK_SIZE * CHUNK_SIZE * CHUNK_HEIGHT);
for (let x = 0; x < CHUNK_SIZE; x++) {
for (let z = 0; z < CHUNK_SIZE; z++) {
for (let y = 0; y < CHUNK_HEIGHT; y++) {
const wx = cx * CHUNK_SIZE + x;
const wz = cz * CHUNK_SIZE + z;
const blockType = this.getBlockType(wx, y, wz);
const idx = y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x;
chunk[idx] = blockType;
}
}
}
this.addMythicStructures(cx * CHUNK_SIZE, cz * CHUNK_SIZE, chunk);
this.chunks.set(key, chunk);
}
return this.chunks.get(key);
}
getBlock(x, y, z) {
const cx = Math.floor(x / CHUNK_SIZE);
const cz = Math.floor(z / CHUNK_SIZE);
const lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const chunk = this.getChunk(cx, cz);
if (y < 0 || y >= CHUNK_HEIGHT) return BlockType.AIR;
return chunk[y * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx];
}
setBlock(x, y, z, type) {
const cx = Math.floor(x / CHUNK_SIZE);
const cz = Math.floor(z / CHUNK_SIZE);
const lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
if (y < 0 || y >= CHUNK_HEIGHT) return;
const chunk = this.getChunk(cx, cz);
chunk[y * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx] = type;
this.updateChunkMesh(cx, cz);
// 更新相邻块
if (lx === 0) this.updateChunkMesh(cx - 1, cz);
if (lx === CHUNK_SIZE - 1) this.updateChunkMesh(cx + 1, cz);
if (lz === 0) this.updateChunkMesh(cx, cz - 1);
if (lz === CHUNK_SIZE - 1) this.updateChunkMesh(cx, cz + 1);
}
updateChunkMesh(cx, cz) {
const key = `${cx},${cz}`;
if (this.meshes.has(key)) {
this.scene.remove(this.meshes.get(key));
}
const mesh = this.createChunkMesh(cx, cz);
if (mesh) {
this.scene.add(mesh);
this.meshes.set(key, mesh);
}
}
createChunkMesh(cx, cz) {
const geometry = new THREE.BufferGeometry();
const vertices = [];
const colors = [];
const indices = [];
let indexOffset = 0;
for (let x = 0; x < CHUNK_SIZE; x++) {
for (let z = 0; z < CHUNK_SIZE; z++) {
for (let y = 0; y < CHUNK_HEIGHT; y++) {
const wx = cx * CHUNK_SIZE + x;
const wz = cz * CHUNK_SIZE + z;
const block = this.getBlock(wx, y, wz);
if (block === BlockType.AIR) continue;
const color = BlockColors[block];
let facesAdded = 0;
// 检查六个面
const faces = [
{ dir: [0, 1, 0], norm: [0, 1, 0] },
{ dir: [0, -1, 0], norm: [0, -1, 0] },
{ dir: [1, 0, 0], norm: [1, 0, 0] },
{ dir: [-1, 0, 0], norm: [-1, 0, 0] },
{ dir: [0, 0, 1], norm: [0, 0, 1] },
{ dir: [0, 0, -1], norm: [0, 0, -1] }
];
faces.forEach(face => {
if (this.getBlock(wx + face.dir[0], y + face.dir[1], wz + face.dir[2]) === BlockType.AIR) {
const v = this.getFaceVertices(x, y, z, face.dir);
const startIdx = indexOffset;
v.forEach(vertex => {
vertices.push(...vertex);
colors.push(color.r, color.g, color.b);
});
indices.push(
startIdx, startIdx + 1, startIdx + 2,
startIdx + 2, startIdx + 3, startIdx
);
indexOffset += 4;
}
});
}
}
}
if (vertices.length === 0) return null;
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), 3));
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1));
const material = new THREE.MeshLambertMaterial({
vertexColors: true,
side: THREE.DoubleSide,
flatShading: true
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(cx * CHUNK_SIZE, 0, cz * CHUNK_SIZE);
return mesh;
}
getFaceVertices(x, y, z, dir) {
const vertices = [
[x, y, z], [x + 1, y, z], [x + 1, y + 1, z], [x, y + 1, z]
];
if (dir[0] === 1) return [[x + 1, y, z], [x + 1, y, z + 1], [x + 1, y + 1, z + 1], [x + 1, y + 1, z]];
if (dir[0] === -1) return [[x, y, z + 1], [x, y, z], [x, y + 1, z], [x, y + 1, z + 1]];
if (dir[2] === 1) return [[x, y, z + 1], [x + 1, y, z + 1], [x + 1, y + 1, z + 1], [x, y + 1, z + 1]];
if (dir[2] === -1) return [[x + 1, y, z], [x, y, z], [x, y + 1, z], [x + 1, y + 1, z]];
if (dir[1] === 1) return [[x, y + 1, z], [x + 1, y + 1, z], [x + 1, y + 1, z + 1], [x, y + 1, z + 1]];
if (dir[1] === -1) return [[x, y, z + 1], [x + 1, y, z + 1], [x + 1, y, z], [x, y, z]];
return vertices;
}
updateChunks(playerX, playerZ) {
const cx = Math.floor(playerX / CHUNK_SIZE);
const cz = Math.floor(playerZ / CHUNK_SIZE);
for (let dx = -LOAD_DISTANCE; dx <= LOAD_DISTANCE; dx++) {
for (let dz = -LOAD_DISTANCE; dz <= LOAD_DISTANCE; dz++) {
const key = `${cx + dx},${cz + dz}`;
if (!this.meshes.has(key)) {
const mesh = this.createChunkMesh(cx + dx, cz + dz);
if (mesh) {
this.scene.add(mesh);
this.meshes.set(key, mesh);
}
}
}
}
// 卸载远处的块
for (const [key, mesh] of this.meshes.entries()) {
const [mcx, mcz] = key.split(',').map(Number);
if (Math.abs(mcx - cx) > LOAD_DISTANCE + 1 || Math.abs(mcz - cz) > LOAD_DISTANCE + 1) {
this.scene.remove(mesh);
this.meshes.delete(key);
}
}
}
}
// ==================== 玩家控制 ====================
class Player {
constructor(camera) {
this.camera = camera;
this.velocity = new THREE.Vector3();
this.keys = {};
this.euler = new THREE.Euler(0, 0, 0, 'YXZ');
this.raycaster = new THREE.Raycaster();
this.selectedBlock = null;
this.selectedFace = null;
document.addEventListener('keydown', (e) => this.keys[e.key.toLowerCase()] = true);
document.addEventListener('keyup', (e) => this.keys[e.key.toLowerCase()] = false);
document.addEventListener('mousemove', (e) => this.onMouseMove(e));
document.addEventListener('mousedown', (e) => this.onMouseDown(e));
document.addEventListener('mouseup', (e) => this.onMouseUp(e));
document.addEventListener('click', () => {
if (document.pointerLockElement === document.documentElement) return;
document.documentElement.requestPointerLock();
});
}
onMouseMove(e) {
if (document.pointerLockElement !== document.documentElement) return;
this.euler.setFromQuaternion(this.camera.quaternion);
this.euler.rotateY(-e.movementX * 0.005);
this.euler.rotateX(-e.movementY * 0.005);
this.euler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, this.euler.x));
this.camera.quaternion.setFromEuler(this.euler);
}
onMouseDown(e) {
if (!this.selectedBlock) return;
if (e.button === 0) this.destroyBlock();
if (e.button === 2) this.placeBlock();
}
onMouseUp(e) {}
destroyBlock() {
if (this.selectedBlock) {
const pos = this.selectedBlock;
window.world.setBlock(pos.x, pos.y, pos.z, BlockType.AIR);
}
}
placeBlock() {
if (!this.selectedBlock || !this.selectedFace) return;
const pos = this.selectedBlock;
const face = this.selectedFace;
const newX = pos.x + face[0];
const newY = pos.y + face[1];
const newZ = pos.z + face[2];
window.world.setBlock(newX, newY, newZ, BlockType.DIRT);
}
update(world, delta) {
// 移动
const speed = 6;
const forward = new THREE.Vector3();
const right = new THREE.Vector3();
this.camera.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
right.crossVectors(forward, new THREE.Vector3(0, 1, 0));
this.velocity.x = 0;
this.velocity.z = 0;
if (this.keys['w']) this.velocity.add(forward.multiplyScalar(speed));
if (this.keys['s']) this.velocity.sub(forward.multiplyScalar(speed));
if (this.keys['a']) this.velocity.sub(right.multiplyScalar(speed));
if (this.keys['d']) this.velocity.add(right.multiplyScalar(speed));
// 重力
this.velocity.y -= 9.8 * delta;
// 碰撞检测
const nextPos = this.camera.position.clone().add(this.velocity.clone().multiplyScalar(delta));
const below = Math.floor(nextPos.y - 1.6);
const blockBelow = world.getBlock(Math.floor(nextPos.x), below, Math.floor(nextPos.z));
if (blockBelow !== BlockType.AIR && this.velocity.y < 0) {
this.velocity.y = 0;
nextPos.y = below + 1.6;
if (this.keys[' ']) this.velocity.y = 8;
}
this.camera.position.copy(nextPos);
// 射线检测方块
this.raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
const intersects = this.raycaster.intersectObjects(world.scene.children);
this.selectedBlock = null;
this.selectedFace = null;
for (let intersection of intersects) {
const point = intersection.point;
const normal = intersection.face.normal;
const blockPos = {
x: Math.floor(point.x - normal.x * 0.1),
y: Math.floor(point.y - normal.y * 0.1),
z: Math.floor(point.z - normal.z * 0.1)
};
if (world.getBlock(blockPos.x, blockPos.y, blockPos.z) !== BlockType.AIR) {
this.selectedBlock = blockPos;
this.selectedFace = [normal.x, normal.y, normal.z];
break;
}
}
// 更新块加载
world.updateChunks(this.camera.position.x, this.camera.position.z);
// 更新UI
const pos = this.camera.position;
document.getElementById('pos').textContent = `位置: (${Math.floor(pos.x)}, ${Math.floor(pos.y)}, ${Math.floor(pos.z)})`;
if (this.selectedBlock) {
const block = this.selectedBlock;
const type = world.getBlock(block.x, block.y, block.z);
const typeNames = { 1: '草地', 2: '泥土', 3: '石头', 4: '黑曜石', 5: '魔法水晶' };
document.getElementById('block').textContent = `方块: ${typeNames[type] || '未知'}`;
}
}
}
// ==================== 初始化 ====================
let camera, scene, renderer, world, player;
let lastTime = Date.now();
let frameCount = 0;
let lastFpsUpdate = Date.now();
function init() {
// 场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 200, 300);
// 相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 50, 0);
// 渲染器
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('canvas'), antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
// 光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(50, 100, 50);
light.castShadow = true;
light.shadow.camera.far = 500;
light.shadow.camera.left = -200;
light.shadow.camera.right = 200;
light.shadow.camera.top = 200;
light.shadow.camera.bottom = -200;
scene.add(light);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// 世界
world = new World();
window.world = world;
world.scene = scene;
// 玩家
player = new Player(camera);
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
}
function animate() {
requestAnimationFrame(animate);
const now = Date.now();
const delta = Math.min((now - lastTime) / 1000, 0.016);
lastTime = now;
player.update(world, delta);
renderer.render(scene, camera);
frameCount++;
if (now - lastFpsUpdate >= 1000) {
document.getElementById('fps').textContent = `FPS: ${frameCount}`;
frameCount = 0;
lastFpsUpdate = now;
}
}
window.addEventListener('DOMContentLoaded', init);
window.addEventListener('contextmenu', (e) => e.preventDefault());
</script>
</body>
</html>