Files
nagi-bench/outputs/kimi-k2-7-code/kimi-code-thinking/mythos-craft.html
T
b6b60b6e1e feat: add Kimi K2.7-Code mythos-craft run (#11)
* feat: add Kimi K2.7-Code mythos-craft run

* Move Kimi K2.7-Code mythos-craft to its artifactDir path

Conform the new run to the model/harness artifact layout merged into main
(outputs/kimi-k2-7-code/kimi-code-thinking/). Validates: Data OK, 25 models.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: OHCJ099 <OHCJ099@users.noreply.github.com>
Co-authored-by: Jesse Zhang <101612750+Nagi-ovo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:31:27 +01:00

1174 lines
39 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>
* { box-sizing: border-box; }
body {
margin: 0;
overflow: hidden;
background: #000;
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
user-select: none;
}
#ui {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
}
#crosshair {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 28px;
color: rgba(255, 255, 255, 0.9);
text-shadow: 0 0 3px #000;
line-height: 1;
}
#info {
position: absolute;
top: 10px; left: 10px;
background: rgba(0, 0, 0, 0.45);
padding: 12px 14px;
border-radius: 6px;
font-size: 13px;
line-height: 1.6;
max-width: 320px;
}
#info h1 {
margin: 0 0 6px;
font-size: 16px;
color: #ffd700;
}
#blockSelector {
position: absolute;
bottom: 18px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 5px;
background: rgba(0, 0, 0, 0.55);
padding: 7px;
border-radius: 6px;
flex-wrap: wrap;
justify-content: center;
max-width: 90vw;
}
.slot {
width: 52px; height: 52px;
background: rgba(255, 255, 255, 0.08);
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
text-align: center;
color: rgba(255, 255, 255, 0.9);
text-shadow: 0 0 2px #000;
transition: border-color 0.1s, background 0.1s;
}
.slot.selected {
border-color: #ffd700;
background: rgba(255, 215, 0, 0.25);
color: #fff;
}
#loading {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 22px;
text-shadow: 0 0 6px #000;
text-align: center;
}
.hidden { display: none !important; }
</style>
</head>
<body>
<div id="ui">
<div id="info">
<h1>神话幻想世界</h1>
<div>WASD 移动 · 空格跳跃 · 鼠标环顾</div>
<div>左键破坏方块 · 右键放置方块</div>
<div>数字键 1-9 切换方块</div>
<div>点击屏幕锁定鼠标开始探索</div>
<div id="stats">区块: 0 | FPS: 0</div>
</div>
<div id="crosshair">+</div>
<div id="blockSelector"></div>
<div id="loading">正在编织神话大地…<br><small>点击屏幕开始探索</small></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
(() => {
'use strict';
// ==================== 配置 ====================
const CHUNK_SIZE = 16;
const CHUNK_HEIGHT = 96;
const RENDER_DISTANCE = 5;
const WORLD_SEED = 12345;
// ==================== 柏林噪声 ====================
class PerlinNoise {
constructor(seed = Math.random()) {
this.p = new Uint8Array(256);
for (let i = 0; i < 256; i++) this.p[i] = i;
let s = seed;
const rand = () => {
s = (s * 16807) % 2147483647;
return s / 2147483647;
};
for (let i = 255; i > 0; i--) {
const j = Math.floor(rand() * (i + 1));
[this.p[i], this.p[j]] = [this.p[j], this.p[i]];
}
this.perm = new Uint8Array(512);
for (let i = 0; i < 512; i++) this.perm[i] = this.p[i & 255];
}
fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
lerp(a, b, t) { return a + t * (b - a); }
grad(hash, x, y, z) {
const h = hash & 15;
const u = h < 8 ? x : y;
const v = h < 4 ? y : (h === 12 || h === 14 ? x : z);
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
noise2D(x, y) {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
x -= Math.floor(x);
y -= Math.floor(y);
const u = this.fade(x);
const v = this.fade(y);
const A = this.perm[X] + Y;
const B = this.perm[X + 1] + Y;
return this.lerp(
this.lerp(this.grad(this.perm[A], x, y, 0), this.grad(this.perm[B], x - 1, y, 0), u),
this.lerp(this.grad(this.perm[A + 1], x, y - 1, 0), this.grad(this.perm[B + 1], x - 1, y - 1, 0), u),
v
);
}
noise3D(x, y, z) {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
const Z = Math.floor(z) & 255;
x -= Math.floor(x);
y -= Math.floor(y);
z -= Math.floor(z);
const u = this.fade(x);
const v = this.fade(y);
const w = this.fade(z);
const A = this.perm[X] + Y;
const AA = this.perm[A] + Z;
const AB = this.perm[A + 1] + Z;
const B = this.perm[X + 1] + Y;
const BA = this.perm[B] + Z;
const BB = this.perm[B + 1] + Z;
return this.lerp(
this.lerp(
this.lerp(this.grad(this.perm[AA], x, y, z), this.grad(this.perm[BA], x - 1, y, z), u),
this.lerp(this.grad(this.perm[AB], x, y - 1, z), this.grad(this.perm[BB], x - 1, y - 1, z), u),
v
),
this.lerp(
this.lerp(this.grad(this.perm[AA + 1], x, y, z - 1), this.grad(this.perm[BA + 1], x - 1, y, z - 1), u),
this.lerp(this.grad(this.perm[AB + 1], x, y - 1, z - 1), this.grad(this.perm[BB + 1], x - 1, y - 1, z - 1), u),
v
),
w
);
}
}
// ==================== 方块注册表 ====================
const BLOCK = {
AIR: 0,
GRASS: 1,
DIRT: 2,
STONE: 3,
LOG: 4,
LEAVES: 5,
SAND: 6,
COBBLESTONE: 7,
PLANKS: 8,
GLOWSTONE: 9,
OBSIDIAN: 10,
GOLD_ORE: 11,
DIAMOND_ORE: 12,
MOSSY_STONE: 13,
RED_CRYSTAL: 14,
WHITE_MARBLE: 15
};
const BLOCK_INFO = {
[BLOCK.AIR]: { name: '空气', solid: false, transparent: true },
[BLOCK.GRASS]: { name: '草方块', solid: true, transparent: false, textures: { top: 0, bottom: 2, side: 1 } },
[BLOCK.DIRT]: { name: '泥土', solid: true, transparent: false, textures: { all: 2 } },
[BLOCK.STONE]: { name: '石头', solid: true, transparent: false, textures: { all: 3 } },
[BLOCK.LOG]: { name: '原木', solid: true, transparent: false, textures: { top: 5, bottom: 5, side: 4 } },
[BLOCK.LEAVES]: { name: '树叶', solid: true, transparent: true, textures: { all: 6 } },
[BLOCK.SAND]: { name: '沙子', solid: true, transparent: false, textures: { all: 7 } },
[BLOCK.COBBLESTONE]: { name: '圆石', solid: true, transparent: false, textures: { all: 8 } },
[BLOCK.PLANKS]: { name: '木板', solid: true, transparent: false, textures: { all: 9 } },
[BLOCK.GLOWSTONE]: { name: '荧石', solid: true, transparent: false, emissive: true, textures: { all: 10 } },
[BLOCK.OBSIDIAN]: { name: '黑曜石', solid: true, transparent: false, textures: { all: 11 } },
[BLOCK.GOLD_ORE]: { name: '金矿石', solid: true, transparent: false, textures: { all: 12 } },
[BLOCK.DIAMOND_ORE]: { name: '钻石矿石', solid: true, transparent: false, textures: { all: 13 } },
[BLOCK.MOSSY_STONE]: { name: '苔石', solid: true, transparent: false, textures: { all: 14 } },
[BLOCK.RED_CRYSTAL]: { name: '红水晶', solid: true, transparent: true, emissive: true, textures: { all: 15 } },
[BLOCK.WHITE_MARBLE]: { name: '白大理石', solid: true, transparent: false, textures: { all: 16 } }
};
const INVENTORY = [
BLOCK.GRASS, BLOCK.DIRT, BLOCK.STONE, BLOCK.COBBLESTONE, BLOCK.PLANKS,
BLOCK.LOG, BLOCK.LEAVES, BLOCK.OBSIDIAN, BLOCK.GLOWSTONE, BLOCK.RED_CRYSTAL,
BLOCK.WHITE_MARBLE, BLOCK.SAND
];
let selectedSlot = 1; // 默认手持泥土,避免放置草方块时顶面丢失材质也能接受
// ==================== 材质图集生成 ====================
const ATLAS_COLS = 8;
const TEX_SIZE = 16;
const atlasCanvas = document.createElement('canvas');
atlasCanvas.width = ATLAS_COLS * TEX_SIZE;
atlasCanvas.height = ATLAS_COLS * TEX_SIZE;
const actx = atlasCanvas.getContext('2d');
actx.imageSmoothingEnabled = false;
function drawToCell(index, fn) {
const col = index % ATLAS_COLS;
const row = Math.floor(index / ATLAS_COLS);
actx.save();
actx.translate(col * TEX_SIZE, row * TEX_SIZE);
fn(actx, TEX_SIZE);
actx.restore();
}
function randPixels(ctx, colors, density, size) {
for (let i = 0; i < size * size * density; i++) {
const c = colors[Math.floor(Math.random() * colors.length)];
ctx.fillStyle = c;
ctx.fillRect(Math.floor(Math.random() * size), Math.floor(Math.random() * size), 1, 1);
}
}
// 0 草顶
drawToCell(0, (ctx, s) => {
ctx.fillStyle = '#4C9630';
ctx.fillRect(0, 0, s, s);
randPixels(ctx, ['#3A7A24', '#5DA838', '#6BB84A', '#3E7D26'], 0.35, s);
});
// 1 草侧面
drawToCell(1, (ctx, s) => {
ctx.fillStyle = '#5D3A1A';
ctx.fillRect(0, 0, s, s);
ctx.fillStyle = '#4C9630';
ctx.fillRect(0, 0, s, s / 2 + 1);
randPixels(ctx, ['#3A7A24', '#6BB84A'], 0.25, s);
ctx.fillStyle = '#3E7D26';
for (let x = 0; x < s; x += 2) {
if (Math.random() > 0.5) ctx.fillRect(x, s / 2 - 2, 1, 4);
}
});
// 2 泥土
drawToCell(2, (ctx, s) => {
ctx.fillStyle = '#5D3A1A';
ctx.fillRect(0, 0, s, s);
randPixels(ctx, ['#4A2E15', '#6B4221', '#3D2512'], 0.45, s);
});
// 3 石头
drawToCell(3, (ctx, s) => {
ctx.fillStyle = '#7D7D7D';
ctx.fillRect(0, 0, s, s);
randPixels(ctx, ['#656565', '#959595', '#555555'], 0.4, s);
});
// 4 原木侧面
drawToCell(4, (ctx, s) => {
ctx.fillStyle = '#3E2723';
ctx.fillRect(0, 0, s, s);
ctx.fillStyle = '#2A1B17';
for (let x = 1; x < s; x += 3) ctx.fillRect(x, 0, 1, s);
randPixels(ctx, ['#4E342E', '#281A16'], 0.2, s);
});
// 5 原木顶
drawToCell(5, (ctx, s) => {
ctx.fillStyle = '#3E2723';
ctx.fillRect(0, 0, s, s);
ctx.strokeStyle = '#2A1B17';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(s / 2, s / 2, s / 3, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.arc(s / 2, s / 2, s / 5, 0, Math.PI * 2);
ctx.stroke();
});
// 6 树叶
drawToCell(6, (ctx, s) => {
ctx.clearRect(0, 0, s, s);
ctx.fillStyle = '#2E7D32';
for (let x = 0; x < s; x++) {
for (let y = 0; y < s; y++) {
if ((x + y) % 3 !== 0 || Math.random() > 0.75) ctx.fillRect(x, y, 1, 1);
}
}
randPixels(ctx, ['#1B5E20', '#43A047', '#4CAF50'], 0.2, s);
});
// 7 沙子
drawToCell(7, (ctx, s) => {
ctx.fillStyle = '#D2B48C';
ctx.fillRect(0, 0, s, s);
randPixels(ctx, ['#C2A47C', '#E6C89C', '#B0946E'], 0.35, s);
});
// 8 圆石
drawToCell(8, (ctx, s) => {
ctx.fillStyle = '#757575';
ctx.fillRect(0, 0, s, s);
ctx.fillStyle = '#555555';
ctx.fillRect(1, 1, 5, 4);
ctx.fillRect(8, 2, 6, 5);
ctx.fillRect(2, 7, 4, 5);
ctx.fillRect(9, 9, 5, 4);
ctx.fillStyle = '#888888';
ctx.fillRect(0, 0, 6, 5);
ctx.fillRect(7, 1, 7, 6);
ctx.fillRect(1, 6, 5, 7);
ctx.fillRect(8, 8, 6, 6);
});
// 9 木板
drawToCell(9, (ctx, s) => {
ctx.fillStyle = '#8D6E63';
ctx.fillRect(0, 0, s, s);
ctx.fillStyle = '#5D4037';
for (let y = 3; y < s; y += 4) ctx.fillRect(0, y, s, 1);
ctx.fillStyle = '#795548';
for (let y = 0; y < s; y += 4) ctx.fillRect(0, y, s, 3);
});
// 10 荧石
drawToCell(10, (ctx, s) => {
ctx.fillStyle = '#F9E076';
ctx.fillRect(0, 0, s, s);
randPixels(ctx, ['#FFF9C4', '#FBC02D', '#FFF176'], 0.5, s);
});
// 11 黑曜石
drawToCell(11, (ctx, s) => {
ctx.fillStyle = '#1A0F2E';
ctx.fillRect(0, 0, s, s);
randPixels(ctx, ['#311B54', '#0D0818', '#4A2E75'], 0.3, s);
});
// 12 金矿石
drawToCell(12, (ctx, s) => {
ctx.fillStyle = '#7D7D7D';
ctx.fillRect(0, 0, s, s);
randPixels(ctx, ['#FFD700', '#B8860B'], 0.08, s);
});
// 13 钻石矿石
drawToCell(13, (ctx, s) => {
ctx.fillStyle = '#7D7D7D';
ctx.fillRect(0, 0, s, s);
randPixels(ctx, ['#00FFFF', '#008B8B'], 0.07, s);
});
// 14 苔石
drawToCell(14, (ctx, s) => {
ctx.fillStyle = '#6D6D6D';
ctx.fillRect(0, 0, s, s);
randPixels(ctx, ['#4CAF50', '#2E7D32', '#555555'], 0.25, s);
});
// 15 红水晶
drawToCell(15, (ctx, s) => {
ctx.clearRect(0, 0, s, s);
ctx.fillStyle = 'rgba(183, 28, 28, 0.92)';
ctx.fillRect(0, 0, s, s);
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.fillRect(2, 2, 3, 6);
ctx.fillRect(8, 8, 2, 4);
});
// 16 白大理石
drawToCell(16, (ctx, s) => {
ctx.fillStyle = '#F5F5F5';
ctx.fillRect(0, 0, s, s);
ctx.strokeStyle = '#BDBDBD';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, s / 2);
ctx.bezierCurveTo(s / 3, s / 3, s / 2, s * 0.7, s, s / 2);
ctx.stroke();
});
const atlasTexture = new THREE.CanvasTexture(atlasCanvas);
atlasTexture.magFilter = THREE.NearestFilter;
atlasTexture.minFilter = THREE.NearestFilter;
atlasTexture.anisotropy = 4;
// ==================== 方块面数据 ====================
// 每个顶点包含 [localX, localY, localZ, u, v]
const FACES = [
{ dir: [1, 0, 0], verts: [[1, 0, 0, 0, 0], [1, 0, 1, 1, 0], [1, 1, 1, 1, 1], [1, 1, 0, 0, 1]] }, // 右 +x
{ dir: [-1, 0, 0], verts: [[0, 0, 1, 0, 0], [0, 0, 0, 1, 0], [0, 1, 0, 1, 1], [0, 1, 1, 0, 1]] }, // 左 -x
{ dir: [0, 1, 0], verts: [[0, 1, 0, 0, 0], [1, 1, 0, 1, 0], [1, 1, 1, 1, 1], [0, 1, 1, 0, 1]] }, // 顶 +y
{ dir: [0, -1, 0], verts: [[1, 0, 0, 0, 0], [0, 0, 0, 1, 0], [0, 0, 1, 1, 1], [1, 0, 1, 0, 1]] }, // 底 -y
{ dir: [0, 0, 1], verts: [[1, 0, 1, 0, 0], [0, 0, 1, 1, 0], [0, 1, 1, 1, 1], [1, 1, 1, 0, 1]] }, // 前 +z
{ dir: [0, 0, -1], verts: [[0, 0, 0, 0, 0], [1, 0, 0, 1, 0], [1, 1, 0, 1, 1], [0, 1, 0, 0, 1]] } // 后 -z
];
function getTextureIndex(id, dir) {
const t = BLOCK_INFO[id].textures;
if (t.all !== undefined) return t.all;
if (dir[1] === 1) return t.top;
if (dir[1] === -1) return t.bottom;
return t.side;
}
function shouldRenderFace(currentId, neighborId) {
if (currentId === BLOCK.AIR) return false;
const cur = BLOCK_INFO[currentId];
const neigh = BLOCK_INFO[neighborId] || BLOCK_INFO[BLOCK.AIR];
if (neighborId === BLOCK.AIR) return true;
if (cur.transparent && neigh.transparent) return currentId !== neighborId;
if (cur.transparent !== neigh.transparent) return true;
return false;
}
function pushFace(positions, normals, uvs, bx, by, bz, face, texIndex) {
const col = texIndex % ATLAS_COLS;
const row = Math.floor(texIndex / ATLAS_COLS);
const u0 = col / ATLAS_COLS;
const v0 = 1 - (row + 1) / ATLAS_COLS;
const du = 1 / ATLAS_COLS;
const dv = 1 / ATLAS_COLS;
const idx = [0, 1, 2, 0, 2, 3];
for (const i of idx) {
const v = face.verts[i];
positions.push(bx + v[0], by + v[1], bz + v[2]);
normals.push(...face.dir);
uvs.push(u0 + v[3] * du, v0 + v[4] * dv);
}
}
// ==================== 世界 ====================
class World {
constructor(seed) {
this.seed = seed;
this.noise = new PerlinNoise(seed);
this.chunks = new Map();
this.blockMaterial = new THREE.MeshLambertMaterial({
map: atlasTexture,
alphaTest: 0.5,
transparent: true,
side: THREE.FrontSide
});
}
fract(n) { return n - Math.floor(n); }
hash2(x, z) {
return this.fract(Math.sin(x * 12.9898 + z * 78.233 + this.seed) * 43758.5453);
}
hash3(x, y, z) {
return this.fract(Math.sin(x * 12.9898 + y * 78.233 + z * 37.719 + this.seed) * 43758.5453);
}
getTerrainHeight(wx, wz) {
let h = 32;
h += this.noise.noise2D(wx * 0.01, wz * 0.01) * 16;
h += this.noise.noise2D(wx * 0.03, wz * 0.03) * 8;
h += this.noise.noise2D(wx * 0.1, wz * 0.1) * 2;
return Math.floor(h);
}
getChunk(cx, cz) {
const key = `${cx},${cz}`;
if (!this.chunks.has(key)) {
const chunk = new Chunk(this, cx, cz);
this.chunks.set(key, chunk);
chunk.generate();
const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]];
for (const [dx, dz] of dirs) {
const n = this.chunks.get(`${cx + dx},${cz + dz}`);
if (n) n.dirty = true;
}
}
return this.chunks.get(key);
}
getBlock(x, y, z) {
if (y < 0 || y >= CHUNK_HEIGHT) return BLOCK.AIR;
const cx = Math.floor(x / CHUNK_SIZE);
const cz = Math.floor(z / CHUNK_SIZE);
const chunk = this.getChunk(cx, cz);
return chunk.get(x - cx * CHUNK_SIZE, y, z - cz * CHUNK_SIZE);
}
setBlock(x, y, z, id) {
if (y < 0 || y >= CHUNK_HEIGHT) return;
const cx = Math.floor(x / CHUNK_SIZE);
const cz = Math.floor(z / CHUNK_SIZE);
const chunk = this.getChunk(cx, cz);
const lx = x - cx * CHUNK_SIZE;
const lz = z - cz * CHUNK_SIZE;
chunk.set(lx, y, lz, id);
if (lx === 0) this.getChunk(cx - 1, cz).dirty = true;
if (lx === CHUNK_SIZE - 1) this.getChunk(cx + 1, cz).dirty = true;
if (lz === 0) this.getChunk(cx, cz - 1).dirty = true;
if (lz === CHUNK_SIZE - 1) this.getChunk(cx, cz + 1).dirty = true;
}
removeChunk(chunk) {
if (chunk.mesh) {
chunk.mesh.geometry.dispose();
scene.remove(chunk.mesh);
}
this.chunks.delete(`${chunk.cx},${chunk.cz}`);
}
generateStructuresForChunk(chunk) {
const margin = 32;
const minX = chunk.cx * CHUNK_SIZE - margin;
const maxX = (chunk.cx + 1) * CHUNK_SIZE + margin;
const minZ = chunk.cz * CHUNK_SIZE - margin;
const maxZ = (chunk.cz + 1) * CHUNK_SIZE + margin;
const step = 48;
for (let wx = minX; wx < maxX; wx += step) {
for (let wz = minZ; wz < maxZ; wz += step) {
const h = this.hash2(wx, wz);
if (h < 0.18) {
const type = Math.floor(h * 10000) % 7;
const gx = wx + Math.floor(this.hash2(wx + 13, wz + 7) * step);
const gz = wz + Math.floor(this.hash2(wx + 31, wz + 17) * step);
// 保护出生点,避免玩家卡在建筑中
if (Math.abs(gx) < 24 && Math.abs(gz) < 24) continue;
this.buildStructure(type, gx, gz);
}
}
}
}
buildStructure(type, x, z) {
switch (type) {
case 0: this.buildFloatingIsland(x, 55 + Math.floor(this.noise.noise2D(x * 0.05, z * 0.05) * 15), z, 5 + Math.floor(this.hash2(x, z) * 4)); break;
case 1: this.buildTower(x, z); break;
case 2: this.buildStoneCircle(x, z); break;
case 3: this.buildGreatTree(x, z); break;
case 4: this.buildCrystalSpire(x, z); break;
case 5: this.buildMushroom(x, z); break;
case 6: this.buildRuinedTemple(x, z); break;
}
}
buildFloatingIsland(x, y, z, radius) {
const r2 = radius * radius;
for (let dx = -radius; dx <= radius; dx++) {
for (let dy = -Math.floor(radius * 0.6); dy <= Math.floor(radius * 0.5); dy++) {
for (let dz = -radius; dz <= radius; dz++) {
const d2 = dx * dx + dy * dy * 3 + dz * dz;
if (d2 < r2) {
const wx = x + dx, wy = y + dy, wz = z + dz;
if (dy >= 0 && d2 > r2 * 0.65) this.setBlock(wx, wy, wz, BLOCK.GRASS);
else if (dy < -1) this.setBlock(wx, wy, wz, BLOCK.STONE);
else this.setBlock(wx, wy, wz, BLOCK.DIRT);
}
}
}
}
// 水晶簇
for (let i = 0; i < 4; i++) {
const ox = x + Math.floor(this.hash3(x, y + i, z) * radius * 2 - radius);
const oz = z + Math.floor(this.hash3(x + i, y, z) * radius * 2 - radius);
this.setBlock(ox, y + 1, oz, BLOCK.RED_CRYSTAL);
if (this.hash3(ox, y, oz) > 0.5) this.setBlock(ox, y + 2, oz, BLOCK.RED_CRYSTAL);
}
}
buildTower(x, z) {
const ground = this.getTerrainHeight(x, z);
const y0 = ground + 1;
const height = 14 + Math.floor(this.hash2(x + 100, z) * 16);
for (let y = y0; y < y0 + height; y++) {
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
if (dx === 0 && dz === 0) continue; // 空心
const block = (y - y0) % 5 === 0 ? BLOCK.MOSSY_STONE : BLOCK.COBBLESTONE;
this.setBlock(x + dx, y, z + dz, block);
}
}
}
// 顶部平台
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
this.setBlock(x + dx, y0 + height, z + dz, BLOCK.COBBLESTONE);
}
}
this.setBlock(x, y0 + height + 1, z, BLOCK.GLOWSTONE);
}
buildStoneCircle(x, z) {
const ground = this.getTerrainHeight(x, z);
const y0 = ground + 1;
const r = 5;
for (let a = 0; a < Math.PI * 2; a += Math.PI / 4) {
const px = Math.round(x + Math.cos(a) * r);
const pz = Math.round(z + Math.sin(a) * r);
for (let h = 0; h < 4; h++) this.setBlock(px, y0 + h, pz, BLOCK.OBSIDIAN);
}
for (let h = 0; h < 7; h++) this.setBlock(x, y0 + h, z, BLOCK.RED_CRYSTAL);
// 符文苔石
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
if (dx * dx + dz * dz <= 5 && this.getBlock(x + dx, y0 - 1, z + dz) !== BLOCK.AIR) {
this.setBlock(x + dx, y0, z + dz, BLOCK.MOSSY_STONE);
}
}
}
}
buildGreatTree(x, z) {
const ground = this.getTerrainHeight(x, z);
const y0 = ground + 1;
const height = 7 + Math.floor(this.hash2(x, z + 50) * 7);
for (let y = y0; y < y0 + height; y++) {
this.setBlock(x, y, z, BLOCK.LOG);
this.setBlock(x + 1, y, z, BLOCK.LOG);
this.setBlock(x, y, z + 1, BLOCK.LOG);
this.setBlock(x + 1, y, z + 1, BLOCK.LOG);
}
const cy = y0 + height;
for (let dy = -3; dy <= 4; dy++) {
for (let dx = -4; dx <= 5; dx++) {
for (let dz = -4; dz <= 5; dz++) {
const d = Math.sqrt((dx - 0.5) * (dx - 0.5) + (dy * 1.1) * (dy * 1.1) + (dz - 0.5) * (dz - 0.5));
if (d < 4.8) this.setBlock(x + dx, cy + dy, z + dz, BLOCK.LEAVES);
}
}
}
}
buildCrystalSpire(x, z) {
const ground = this.getTerrainHeight(x, z);
const y0 = ground + 1;
const height = 10 + Math.floor(this.hash2(x + 200, z) * 12);
for (let y = y0; y < y0 + height; y++) {
this.setBlock(x, y, z, BLOCK.RED_CRYSTAL);
if (y % 3 === 0) {
this.setBlock(x + 1, y, z, BLOCK.RED_CRYSTAL);
this.setBlock(x - 1, y, z, BLOCK.RED_CRYSTAL);
this.setBlock(x, y, z + 1, BLOCK.RED_CRYSTAL);
this.setBlock(x, y, z - 1, BLOCK.RED_CRYSTAL);
}
}
this.setBlock(x, y0 + height, z, BLOCK.GLOWSTONE);
}
buildMushroom(x, z) {
const ground = this.getTerrainHeight(x, z);
const y0 = ground + 1;
const height = 4 + Math.floor(this.hash2(x, z + 300) * 3);
for (let y = y0; y < y0 + height; y++) {
this.setBlock(x, y, z, BLOCK.WHITE_MARBLE);
if (y > y0 + 1) {
this.setBlock(x + 1, y, z, BLOCK.WHITE_MARBLE);
this.setBlock(x - 1, y, z, BLOCK.WHITE_MARBLE);
this.setBlock(x, y, z + 1, BLOCK.WHITE_MARBLE);
this.setBlock(x, y, z - 1, BLOCK.WHITE_MARBLE);
}
}
const capY = y0 + height;
const capR = 4;
for (let dy = 0; dy <= 2; dy++) {
const r = capR - dy;
for (let dx = -r; dx <= r; dx++) {
for (let dz = -r; dz <= r; dz++) {
if (dx * dx + dz * dz <= r * r) this.setBlock(x + dx, capY + dy, z + dz, BLOCK.RED_CRYSTAL);
}
}
}
}
buildRuinedTemple(x, z) {
const ground = this.getTerrainHeight(x, z);
const y0 = ground + 1;
const size = 7;
for (let dx = -size; dx <= size; dx++) {
for (let dz = -size; dz <= size; dz++) {
this.setBlock(x + dx, y0, z + dz, BLOCK.WHITE_MARBLE);
if (this.hash3(x + dx, y0, z + dz) > 0.7) this.setBlock(x + dx, y0 + 1, z + dz, BLOCK.MOSSY_STONE);
}
}
const cols = [[-size, -size], [size, -size], [-size, size], [size, size], [0, -size], [0, size], [-size, 0], [size, 0]];
for (const [cx, cz] of cols) {
for (let h = 1; h < 5; h++) {
if (this.hash3(x + cx, y0 + h, z + cz) > 0.15) this.setBlock(x + cx, y0 + h, z + cz, BLOCK.WHITE_MARBLE);
}
}
this.setBlock(x, y0 + 1, z, BLOCK.GLOWSTONE);
this.setBlock(x, y0 + 2, z, BLOCK.RED_CRYSTAL);
}
buildChunkMesh(chunk) {
const positions = [];
const normals = [];
const uvs = [];
for (let y = 0; y < CHUNK_HEIGHT; y++) {
for (let z = 0; z < CHUNK_SIZE; z++) {
for (let x = 0; x < CHUNK_SIZE; x++) {
const id = chunk.get(x, y, z);
if (id === BLOCK.AIR) continue;
for (const face of FACES) {
const nx = x + face.dir[0];
const ny = y + face.dir[1];
const nz = z + face.dir[2];
let nid;
if (nx < 0 || nx >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE || ny < 0 || ny >= CHUNK_HEIGHT) {
const wx = chunk.cx * CHUNK_SIZE + nx;
const wz = chunk.cz * CHUNK_SIZE + nz;
const ncx = Math.floor(wx / CHUNK_SIZE);
const ncz = Math.floor(wz / CHUNK_SIZE);
const key = `${ncx},${ncz}`;
if (this.chunks.has(key)) {
const nc = this.chunks.get(key);
nid = nc.get(wx - ncx * CHUNK_SIZE, ny, wz - ncz * CHUNK_SIZE);
} else {
nid = BLOCK.AIR;
}
} else {
nid = chunk.get(nx, ny, nz);
}
if (shouldRenderFace(id, nid)) {
pushFace(positions, normals, uvs, x, y, z, face, getTextureIndex(id, face.dir));
}
}
}
}
}
if (chunk.mesh) {
chunk.mesh.geometry.dispose();
scene.remove(chunk.mesh);
chunk.mesh = null;
}
if (positions.length === 0) {
chunk.dirty = false;
return;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(positions), 3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(new Float32Array(normals), 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array(uvs), 2));
const mesh = new THREE.Mesh(geometry, this.blockMaterial);
mesh.position.set(chunk.cx * CHUNK_SIZE, 0, chunk.cz * CHUNK_SIZE);
scene.add(mesh);
chunk.mesh = mesh;
chunk.dirty = false;
}
updateMeshes() {
let built = 0;
for (const chunk of this.chunks.values()) {
if (chunk.dirty && built < 4) {
this.buildChunkMesh(chunk);
built++;
}
}
}
}
class Chunk {
constructor(world, cx, cz) {
this.world = world;
this.cx = cx;
this.cz = cz;
this.data = new Uint8Array(CHUNK_SIZE * CHUNK_HEIGHT * CHUNK_SIZE);
this.mesh = null;
this.dirty = true;
}
index(x, y, z) {
return y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x;
}
get(x, y, z) {
if (x < 0 || x >= CHUNK_SIZE || z < 0 || z >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT) return BLOCK.AIR;
return this.data[this.index(x, y, z)];
}
set(x, y, z, id) {
if (x < 0 || x >= CHUNK_SIZE || z < 0 || z >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT) return;
this.data[this.index(x, y, z)] = id;
this.dirty = true;
}
generate() {
const cs = CHUNK_SIZE;
for (let x = 0; x < cs; x++) {
for (let z = 0; z < cs; z++) {
const wx = this.cx * cs + x;
const wz = this.cz * cs + z;
const h = this.world.getTerrainHeight(wx, wz);
for (let y = 0; y < CHUNK_HEIGHT; y++) {
if (y > h) this.set(x, y, z, BLOCK.AIR);
else if (y === h) this.set(x, y, z, BLOCK.GRASS);
else if (y > h - 4) this.set(x, y, z, BLOCK.DIRT);
else this.set(x, y, z, BLOCK.STONE);
}
}
}
// 洞穴与矿石
for (let y = 1; y < CHUNK_HEIGHT - 1; y++) {
for (let z = 0; z < cs; z++) {
for (let x = 0; x < cs; x++) {
if (this.get(x, y, z) === BLOCK.STONE) {
const wx = this.cx * cs + x;
const wz = this.cz * cs + z;
const cave = this.world.noise.noise3D(wx * 0.05, y * 0.05, wz * 0.05);
if (cave > 0.65) this.set(x, y, z, BLOCK.AIR);
else if (this.world.hash3(wx, y, wz) < 0.02) this.set(x, y, z, BLOCK.GOLD_ORE);
else if (this.world.hash3(wx + 100, y + 50, wz) < 0.015) this.set(x, y, z, BLOCK.DIAMOND_ORE);
}
}
}
}
this.world.generateStructuresForChunk(this);
}
}
// ==================== Three.js 场景 ====================
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, RENDER_DISTANCE * CHUNK_SIZE * 0.5, RENDER_DISTANCE * CHUNK_SIZE);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.05, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
renderer.domElement.style.cssText = 'position:fixed;inset:0;z-index:0;';
const ambient = new THREE.AmbientLight(0x404060, 0.55);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xffffff, 0.75);
sun.position.set(60, 120, 40);
scene.add(sun);
// ==================== 玩家 ====================
const playerObject = new THREE.Group();
scene.add(playerObject);
playerObject.add(camera);
camera.position.set(0, 1.7, 0);
const player = {
position: new THREE.Vector3(0, 0, 0),
velocity: new THREE.Vector3(0, 0, 0),
speed: 4.3,
jumpSpeed: 9,
onGround: false,
height: 1.8,
width: 0.6
};
const world = new World(WORLD_SEED);
// ==================== 控制 ====================
const keys = {};
let isLocked = false;
const mouseSensitivity = 0.002;
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code.startsWith('Digit')) {
const n = parseInt(e.code.slice(5), 10);
if (n >= 1 && n <= 9) {
selectedSlot = Math.min(n - 1, INVENTORY.length - 1);
updateInventoryUI();
}
}
});
document.addEventListener('keyup', e => keys[e.code] = false);
document.body.addEventListener('click', async () => {
if (document.pointerLockElement !== document.body) {
try { await document.body.requestPointerLock(); } catch (e) {}
}
});
document.addEventListener('pointerlockchange', () => {
isLocked = document.pointerLockElement === document.body;
document.getElementById('loading').classList.toggle('hidden', isLocked);
});
document.addEventListener('mousemove', e => {
if (!isLocked) return;
playerObject.rotation.y -= e.movementX * mouseSensitivity;
camera.rotation.x -= e.movementY * mouseSensitivity;
camera.rotation.x = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, camera.rotation.x));
});
document.addEventListener('mousedown', e => {
if (!isLocked) return;
if (e.button === 0) breakBlock();
if (e.button === 2) placeBlock();
});
document.addEventListener('contextmenu', e => e.preventDefault());
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ==================== 物理碰撞 ====================
function isSolid(x, y, z) {
const id = world.getBlock(x, y, z);
return id !== BLOCK.AIR && BLOCK_INFO[id].solid;
}
function resolveAxis(axis) {
const p = player.position;
const minX = Math.floor(p.x - player.width / 2);
const maxX = Math.floor(p.x + player.width / 2);
const minY = Math.floor(p.y);
const maxY = Math.floor(p.y + player.height - 0.001);
const minZ = Math.floor(p.z - player.width / 2);
const maxZ = Math.floor(p.z + player.width / 2);
let collided = false;
let limit;
if (axis === 'x') {
limit = player.velocity.x > 0 ? Infinity : -Infinity;
for (let y = minY; y <= maxY; y++) {
for (let z = minZ; z <= maxZ; z++) {
for (let x = minX; x <= maxX; x++) {
if (isSolid(x, y, z)) {
collided = true;
if (player.velocity.x > 0) limit = Math.min(limit, x);
else limit = Math.max(limit, x + 1);
}
}
}
}
if (collided) {
p.x = player.velocity.x > 0 ? limit - player.width / 2 : limit + player.width / 2;
player.velocity.x = 0;
}
} else if (axis === 'z') {
limit = player.velocity.z > 0 ? Infinity : -Infinity;
for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) {
for (let z = minZ; z <= maxZ; z++) {
if (isSolid(x, y, z)) {
collided = true;
if (player.velocity.z > 0) limit = Math.min(limit, z);
else limit = Math.max(limit, z + 1);
}
}
}
}
if (collided) {
p.z = player.velocity.z > 0 ? limit - player.width / 2 : limit + player.width / 2;
player.velocity.z = 0;
}
} else if (axis === 'y') {
limit = player.velocity.y > 0 ? Infinity : -Infinity;
const falling = player.velocity.y < 0;
for (let x = minX; x <= maxX; x++) {
for (let z = minZ; z <= maxZ; z++) {
for (let y = minY; y <= maxY; y++) {
if (isSolid(x, y, z)) {
collided = true;
if (player.velocity.y > 0) limit = Math.min(limit, y);
else limit = Math.max(limit, y + 1);
}
}
}
}
if (collided) {
p.y = player.velocity.y > 0 ? limit - player.height : limit;
player.velocity.y = 0;
if (falling) player.onGround = true;
}
}
}
function updatePlayer(dt) {
const forward = new THREE.Vector3();
camera.getWorldDirection(forward);
forward.y = 0;
if (forward.lengthSq() > 0) forward.normalize();
const right = new THREE.Vector3(-forward.z, 0, forward.x);
const move = new THREE.Vector3();
if (keys.KeyW) move.add(forward);
if (keys.KeyS) move.sub(forward);
if (keys.KeyA) move.sub(right);
if (keys.KeyD) move.add(right);
if (move.lengthSq() > 0) {
move.normalize().multiplyScalar(player.speed);
}
player.velocity.x = move.x;
player.velocity.z = move.z;
if (keys.Space && player.onGround) {
player.velocity.y = player.jumpSpeed;
player.onGround = false;
}
player.velocity.y -= 28 * dt;
player.velocity.y = Math.max(player.velocity.y, -50);
player.onGround = false;
player.position.x += player.velocity.x * dt;
resolveAxis('x');
player.position.z += player.velocity.z * dt;
resolveAxis('z');
player.position.y += player.velocity.y * dt;
resolveAxis('y');
playerObject.position.copy(player.position);
updateChunks();
}
// ==================== 区块加载/卸载 ====================
function updateChunks() {
const pcx = Math.floor(player.position.x / CHUNK_SIZE);
const pcz = Math.floor(player.position.z / CHUNK_SIZE);
for (let cx = pcx - RENDER_DISTANCE; cx <= pcx + RENDER_DISTANCE; cx++) {
for (let cz = pcz - RENDER_DISTANCE; cz <= pcz + RENDER_DISTANCE; cz++) {
world.getChunk(cx, cz);
}
}
const toRemove = [];
for (const [key, chunk] of world.chunks) {
if (Math.abs(chunk.cx - pcx) > RENDER_DISTANCE + 2 || Math.abs(chunk.cz - pcz) > RENDER_DISTANCE + 2) {
toRemove.push(chunk);
}
}
for (const chunk of toRemove) world.removeChunk(chunk);
}
// ==================== 射线检测 ====================
function raycast(origin, dir, maxDist) {
let x = Math.floor(origin.x);
let y = Math.floor(origin.y);
let z = Math.floor(origin.z);
const stepX = Math.sign(dir.x);
const stepY = Math.sign(dir.y);
const stepZ = Math.sign(dir.z);
const tDeltaX = stepX !== 0 ? Math.abs(1 / dir.x) : Infinity;
const tDeltaY = stepY !== 0 ? Math.abs(1 / dir.y) : Infinity;
const tDeltaZ = stepZ !== 0 ? Math.abs(1 / dir.z) : Infinity;
let tMaxX = stepX > 0 ? tDeltaX * (1 - (origin.x - x)) : (stepX < 0 ? tDeltaX * (origin.x - x) : Infinity);
let tMaxY = stepY > 0 ? tDeltaY * (1 - (origin.y - y)) : (stepY < 0 ? tDeltaY * (origin.y - y) : Infinity);
let tMaxZ = stepZ > 0 ? tDeltaZ * (1 - (origin.z - z)) : (stepZ < 0 ? tDeltaZ * (origin.z - z) : Infinity);
let face = [0, 0, 0];
let dist = 0;
while (dist < maxDist) {
if (tMaxX < tMaxY) {
if (tMaxX < tMaxZ) {
dist = tMaxX;
x += stepX;
tMaxX += tDeltaX;
face = [-stepX, 0, 0];
} else {
dist = tMaxZ;
z += stepZ;
tMaxZ += tDeltaZ;
face = [0, 0, -stepZ];
}
} else {
if (tMaxY < tMaxZ) {
dist = tMaxY;
y += stepY;
tMaxY += tDeltaY;
face = [0, -stepY, 0];
} else {
dist = tMaxZ;
z += stepZ;
tMaxZ += tDeltaZ;
face = [0, 0, -stepZ];
}
}
const id = world.getBlock(x, y, z);
if (BLOCK_INFO[id].solid) {
return { x, y, z, face, dist };
}
}
return null;
}
function intersectsPlayer(bx, by, bz) {
return bx < player.position.x + player.width / 2 && bx + 1 > player.position.x - player.width / 2 &&
by < player.position.y + player.height && by + 1 > player.position.y &&
bz < player.position.z + player.width / 2 && bz + 1 > player.position.z - player.width / 2;
}
function breakBlock() {
const dir = new THREE.Vector3();
camera.getWorldDirection(dir);
const hit = raycast(camera.position, dir, 5);
if (hit) world.setBlock(hit.x, hit.y, hit.z, BLOCK.AIR);
}
function placeBlock() {
const dir = new THREE.Vector3();
camera.getWorldDirection(dir);
const hit = raycast(camera.position, dir, 5);
if (!hit) return;
const px = hit.x + hit.face[0];
const py = hit.y + hit.face[1];
const pz = hit.z + hit.face[2];
if (!intersectsPlayer(px, py, pz)) {
world.setBlock(px, py, pz, INVENTORY[selectedSlot]);
}
}
// ==================== UI ====================
function updateInventoryUI() {
const el = document.getElementById('blockSelector');
el.innerHTML = '';
INVENTORY.forEach((id, i) => {
const div = document.createElement('div');
div.className = 'slot' + (i === selectedSlot ? ' selected' : '');
div.textContent = BLOCK_INFO[id].name;
el.appendChild(div);
});
}
updateInventoryUI();
// ==================== 初始化出生点 ====================
function initSpawn() {
const y = world.getTerrainHeight(0, 0) + 3;
player.position.set(0, y, 0);
playerObject.position.copy(player.position);
updateChunks();
// 预生成附近区块网格
for (let i = 0; i < 50; i++) world.updateMeshes();
}
// ==================== 主循环 ====================
const clock = new THREE.Clock();
const statsEl = document.getElementById('stats');
let frames = 0;
let lastTime = performance.now();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
updateChunks();
if (isLocked) updatePlayer(dt);
world.updateMeshes();
renderer.render(scene, camera);
frames++;
const now = performance.now();
if (now - lastTime >= 1000) {
statsEl.textContent = `区块: ${world.chunks.size} | FPS: ${frames}`;
frames = 0;
lastTime = now;
}
}
initSpawn();
animate();
})();
</script>
</body>
</html>