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>
713 lines
28 KiB
HTML
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>
|