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>
This commit is contained in:
1 parent
dd67d6fda5
commit
3d5bf6967c
37 files changed
+7577
-24
No files matched your search
@@ -0,0 +1,712 @@
|
||||
<!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>
|
||||
Reference in new issue
Block a user