Files
nagi-bench/outputs/composer-2-5/cursor-max/mythos-craft.html
T

1433 lines
54 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 { overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, sans-serif; }
canvas { display: block; }
#crosshair {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 20px; height: 20px; pointer-events: none; z-index: 10;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute; background: rgba(255,255,255,0.85);
}
#crosshair::before { width: 2px; height: 20px; left: 9px; top: 0; }
#crosshair::after { width: 20px; height: 2px; left: 0; top: 9px; }
#hud {
position: fixed; bottom: 0; left: 0; right: 0;
padding: 16px; pointer-events: none; z-index: 10;
display: flex; flex-direction: column; align-items: center; gap: 8px;
}
#hotbar {
display: flex; gap: 4px; background: rgba(0,0,0,0.5);
padding: 6px; border-radius: 4px; border: 2px solid rgba(255,255,255,0.3);
}
.slot {
width: 48px; height: 48px; border: 2px solid rgba(100,100,100,0.6);
border-radius: 2px; position: relative; background: rgba(0,0,0,0.4);
}
.slot.active { border-color: #fff; box-shadow: 0 0 8px rgba(255,255,255,0.5); }
.slot canvas { width: 100%; height: 100%; image-rendering: pixelated; }
.slot .key {
position: absolute; top: 2px; left: 4px;
font-size: 10px; color: rgba(255,255,255,0.7);
}
#info {
color: rgba(255,255,255,0.7); font-size: 12px; text-align: center;
text-shadow: 1px 1px 2px #000;
}
#overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 100; cursor: pointer;
}
#overlay h1 {
color: #e8d5a3; font-size: 42px; margin-bottom: 8px;
text-shadow: 0 0 20px rgba(232,213,163,0.5);
letter-spacing: 4px;
}
#overlay h2 {
color: #a8c8e8; font-size: 18px; margin-bottom: 32px; font-weight: 300;
}
#overlay p { color: rgba(255,255,255,0.6); font-size: 14px; line-height: 1.8; text-align: center; }
#overlay .start {
margin-top: 24px; padding: 12px 40px; font-size: 18px;
background: linear-gradient(135deg, #4a6741, #2d4a2d);
color: #e8d5a3; border: 2px solid #6b8f5e; border-radius: 4px;
cursor: pointer; letter-spacing: 2px;
}
#overlay .start:hover { background: linear-gradient(135deg, #5a7a51, #3d5a3d); }
#block-name {
position: fixed; top: 50%; left: 50%;
transform: translate(-50%, 40px);
color: rgba(255,255,255,0.8); font-size: 13px;
text-shadow: 1px 1px 3px #000; pointer-events: none; z-index: 10;
}
#minimap {
position: fixed; top: 16px; right: 16px;
width: 120px; height: 120px; border: 2px solid rgba(255,255,255,0.3);
border-radius: 4px; background: rgba(0,0,0,0.5); z-index: 10;
pointer-events: none;
}
</style>
</head>
<body>
<div id="overlay">
<h1>神话之境</h1>
<h2>Mythic Realms — 方块世界</h2>
<p>
WASD 移动 · 空格跳跃 · Shift 潜行<br>
鼠标环视 · 左键破坏 · 右键放置<br>
1-9 切换方块 · 滚轮切换物品<br>
探索柏林噪声生成的无限大地<br>
发现精灵塔、龙神殿、天空遗迹等神话建筑<br>
G 依次瞬移到三处神话景点
</p>
<div class="start">点击开始探索</div>
</div>
<div id="crosshair" style="display:none"></div>
<div id="block-name" style="display:none"></div>
<canvas id="minimap" style="display:none"></canvas>
<div id="hud" style="display:none">
<div id="hotbar"></div>
<div id="info">坐标: 0, 0, 0 | FPS: 60</div>
</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// ═══════════════════════════════════════════════════════════
// BLOCK TYPES
// ═══════════════════════════════════════════════════════════
const BLOCK = {
AIR: 0, GRASS: 1, DIRT: 2, STONE: 3, SAND: 4, WATER: 5,
WOOD: 6, LEAVES: 7, SNOW: 8, MYTHIC_STONE: 9, CRYSTAL: 10,
GOLD: 11, OBSIDIAN: 12, MOSSY_STONE: 13, BRICK: 14, GLOWSTONE: 15
};
const BLOCK_NAMES = {
[BLOCK.GRASS]: '草地', [BLOCK.DIRT]: '泥土', [BLOCK.STONE]: '石头',
[BLOCK.SAND]: '沙子', [BLOCK.WATER]: '水', [BLOCK.WOOD]: '橡木',
[BLOCK.LEAVES]: '树叶', [BLOCK.SNOW]: '雪', [BLOCK.MYTHIC_STONE]: '神话石',
[BLOCK.CRYSTAL]: '水晶', [BLOCK.GOLD]: '黄金', [BLOCK.OBSIDIAN]: '黑曜石',
[BLOCK.MOSSY_STONE]: '苔石', [BLOCK.BRICK]: '砖块', [BLOCK.GLOWSTONE]: '萤石'
};
const HOTBAR_BLOCKS = [
BLOCK.GRASS, BLOCK.DIRT, BLOCK.STONE, BLOCK.SAND, BLOCK.WOOD,
BLOCK.LEAVES, BLOCK.MYTHIC_STONE, BLOCK.CRYSTAL, BLOCK.GOLD
];
const CHUNK_SIZE = 16;
const WORLD_HEIGHT = 64;
const RENDER_DISTANCE = 4;
const GRAVITY = 28;
const JUMP_FORCE = 9;
const MOVE_SPEED = 5;
const SPRINT_MULT = 1.6;
const PLAYER_HEIGHT = 1.7;
const PLAYER_RADIUS = 0.3;
// ═══════════════════════════════════════════════════════════
// PERLIN NOISE
// ═══════════════════════════════════════════════════════════
class PerlinNoise {
constructor(seed = 42) {
this.p = new Uint8Array(512);
const perm = new Uint8Array(256);
let s = seed;
for (let i = 0; i < 256; i++) perm[i] = i;
for (let i = 255; i > 0; i--) {
s = (s * 16807 + 0) % 2147483647;
const j = s % (i + 1);
[perm[i], perm[j]] = [perm[j], perm[i]];
}
for (let i = 0; i < 512; i++) this.p[i] = perm[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) ? -u : u) + ((h & 2) ? -v : v);
}
noise3D(x, y, z) {
const X = Math.floor(x) & 255, Y = Math.floor(y) & 255, Z = Math.floor(z) & 255;
x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z);
const u = this.fade(x), v = this.fade(y), w = this.fade(z);
const A = this.p[X] + Y, AA = this.p[A] + Z, AB = this.p[A + 1] + Z;
const B = this.p[X + 1] + Y, BA = this.p[B] + Z, BB = this.p[B + 1] + Z;
return this.lerp(
this.lerp(this.lerp(this.grad(this.p[AA], x, y, z), this.grad(this.p[BA], x - 1, y, z), u),
this.lerp(this.grad(this.p[AB], x, y - 1, z), this.grad(this.p[BB], x - 1, y - 1, z), u), v),
this.lerp(this.lerp(this.grad(this.p[AA + 1], x, y, z - 1), this.grad(this.p[BA + 1], x - 1, y, z - 1), u),
this.lerp(this.grad(this.p[AB + 1], x, y - 1, z - 1), this.grad(this.p[BB + 1], x - 1, y - 1, z - 1), u), v), w);
}
octave(x, y, z, octaves, persistence, lacunarity) {
let total = 0, amp = 1, freq = 1, max = 0;
for (let i = 0; i < octaves; i++) {
total += this.noise3D(x * freq, y * freq, z * freq) * amp;
max += amp; amp *= persistence; freq *= lacunarity;
}
return total / max;
}
}
const noise = new PerlinNoise(777);
const noise2 = new PerlinNoise(1337);
// ═══════════════════════════════════════════════════════════
// TEXTURE ATLAS
// ═══════════════════════════════════════════════════════════
function createTextureAtlas() {
const ATLAS_SIZE = 256;
const TILE = 16;
const canvas = document.createElement('canvas');
canvas.width = ATLAS_SIZE;
canvas.height = ATLAS_SIZE;
const ctx = canvas.getContext('2d');
const drawTile = (index, drawFn) => {
const col = index % 16;
const row = Math.floor(index / 16);
const x = col * TILE, y = row * TILE;
ctx.save();
ctx.translate(x, y);
drawFn(ctx, TILE);
ctx.restore();
};
// Grass top
drawTile(0, (c, s) => {
c.fillStyle = '#5a9e3e'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 40; i++) {
c.fillStyle = `hsl(${100 + Math.random() * 20}, ${50 + Math.random() * 20}%, ${30 + Math.random() * 15}%)`;
c.fillRect(Math.random() * s, Math.random() * s, 1, 1);
}
});
// Grass side
drawTile(1, (c, s) => {
c.fillStyle = '#8B6914'; c.fillRect(0, 0, s, s);
c.fillStyle = '#5a9e3e'; c.fillRect(0, 0, s, 3);
for (let i = 0; i < 20; i++) {
c.fillStyle = '#4a8a32';
c.fillRect(Math.random() * s, 3 + Math.random() * 4, 1, 2);
}
});
// Dirt
drawTile(2, (c, s) => {
c.fillStyle = '#8B6914'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 30; i++) {
c.fillStyle = `hsl(30, ${30 + Math.random() * 20}%, ${25 + Math.random() * 15}%)`;
c.fillRect(Math.random() * s, Math.random() * s, 2, 1);
}
});
// Stone
drawTile(3, (c, s) => {
c.fillStyle = '#888'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 25; i++) {
const g = 120 + Math.random() * 40;
c.fillStyle = `rgb(${g},${g},${g})`;
c.fillRect(Math.random() * s, Math.random() * s, 2 + Math.random() * 3, 2);
}
});
// Sand
drawTile(4, (c, s) => {
c.fillStyle = '#dbc68a'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 20; i++) {
c.fillStyle = `hsl(45, ${40 + Math.random() * 20}%, ${70 + Math.random() * 10}%)`;
c.fillRect(Math.random() * s, Math.random() * s, 1, 1);
}
});
// Water
drawTile(5, (c, s) => {
c.fillStyle = '#3366aa'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 8; i++) {
c.fillStyle = 'rgba(100,180,255,0.3)';
c.fillRect(0, 2 + i * 2, s, 1);
}
});
// Wood (log side)
drawTile(6, (c, s) => {
c.fillStyle = '#6B4226'; c.fillRect(0, 0, s, s);
for (let i = 0; i < s; i += 2) {
c.fillStyle = '#5a3520';
c.fillRect(0, i, s, 1);
}
});
// Wood top
drawTile(7, (c, s) => {
c.fillStyle = '#8B6340'; c.fillRect(0, 0, s, s);
const cx = s / 2, cy = s / 2;
for (let r = 1; r < 6; r++) {
c.strokeStyle = `rgba(90,53,32,${0.3 + r * 0.1})`;
c.beginPath(); c.arc(cx, cy, r * 1.2, 0, Math.PI * 2); c.stroke();
}
});
// Leaves
drawTile(8, (c, s) => {
c.fillStyle = '#2d6b2d'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 35; i++) {
c.fillStyle = `hsl(${110 + Math.random() * 30}, ${50 + Math.random() * 20}%, ${25 + Math.random() * 15}%)`;
c.fillRect(Math.random() * s, Math.random() * s, 2, 2);
}
});
// Snow
drawTile(9, (c, s) => {
c.fillStyle = '#f0f0f5'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 15; i++) {
c.fillStyle = 'rgba(200,210,230,0.5)';
c.fillRect(Math.random() * s, Math.random() * s, 2, 1);
}
});
// Mythic stone
drawTile(10, (c, s) => {
c.fillStyle = '#4a3a6b'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 20; i++) {
c.fillStyle = `hsl(${260 + Math.random() * 30}, 40%, ${30 + Math.random() * 20}%)`;
c.fillRect(Math.random() * s, Math.random() * s, 2, 2);
}
c.fillStyle = 'rgba(180,140,255,0.3)';
for (let i = 0; i < 5; i++) c.fillRect(Math.random() * s, Math.random() * s, 1, 3);
});
// Crystal
drawTile(11, (c, s) => {
c.fillStyle = '#1a3a5c'; c.fillRect(0, 0, s, s);
const colors = ['#66ccff', '#88ddff', '#44aadd', '#aaddff'];
for (let i = 0; i < 12; i++) {
c.fillStyle = colors[i % colors.length];
const x = Math.random() * s, y = Math.random() * s;
c.beginPath();
c.moveTo(x, y); c.lineTo(x + 2, y - 3); c.lineTo(x + 4, y); c.closePath();
c.fill();
}
});
// Gold
drawTile(12, (c, s) => {
c.fillStyle = '#d4a520'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 15; i++) {
c.fillStyle = `hsl(45, ${70 + Math.random() * 20}%, ${45 + Math.random() * 15}%)`;
c.fillRect(Math.random() * s, Math.random() * s, 2, 1);
}
});
// Obsidian
drawTile(13, (c, s) => {
c.fillStyle = '#1a0a2e'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 10; i++) {
c.fillStyle = `hsl(270, ${20 + Math.random() * 20}%, ${8 + Math.random() * 8}%)`;
c.fillRect(Math.random() * s, Math.random() * s, 3, 2);
}
c.fillStyle = 'rgba(120,60,200,0.2)';
c.fillRect(2, 2, 4, 4); c.fillRect(9, 7, 3, 3);
});
// Mossy stone
drawTile(14, (c, s) => {
c.fillStyle = '#777'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 20; i++) {
c.fillStyle = `rgb(${110 + Math.random() * 30},${110 + Math.random() * 30},${110 + Math.random() * 30})`;
c.fillRect(Math.random() * s, Math.random() * s, 2, 2);
}
for (let i = 0; i < 12; i++) {
c.fillStyle = `hsl(${100 + Math.random() * 20}, 40%, ${25 + Math.random() * 10}%)`;
c.fillRect(Math.random() * s, Math.random() * s, 2, 2);
}
});
// Brick
drawTile(15, (c, s) => {
c.fillStyle = '#994433'; c.fillRect(0, 0, s, s);
for (let row = 0; row < 4; row++) {
const off = row % 2 === 0 ? 0 : 4;
for (let col = 0; col < 3; col++) {
c.fillStyle = `hsl(10, ${50 + Math.random() * 20}%, ${35 + Math.random() * 10}%)`;
c.fillRect(off + col * 5, row * 4, 4, 3);
}
}
});
// Glowstone
drawTile(16, (c, s) => {
c.fillStyle = '#ccaa44'; c.fillRect(0, 0, s, s);
for (let i = 0; i < 10; i++) {
c.fillStyle = `hsl(45, 80%, ${60 + Math.random() * 20}%)`;
c.fillRect(Math.random() * s, Math.random() * s, 3, 3);
}
c.fillStyle = 'rgba(255,240,150,0.4)';
c.fillRect(0, 0, s, s);
});
const texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.colorSpace = THREE.SRGBColorSpace;
return { texture, canvas };
}
const BLOCK_TEX = {
[BLOCK.GRASS]: { top: 0, bottom: 2, side: 1 },
[BLOCK.DIRT]: { top: 2, bottom: 2, side: 2 },
[BLOCK.STONE]: { top: 3, bottom: 3, side: 3 },
[BLOCK.SAND]: { top: 4, bottom: 4, side: 4 },
[BLOCK.WATER]: { top: 5, bottom: 5, side: 5 },
[BLOCK.WOOD]: { top: 7, bottom: 7, side: 6 },
[BLOCK.LEAVES]: { top: 8, bottom: 8, side: 8 },
[BLOCK.SNOW]: { top: 9, bottom: 9, side: 9 },
[BLOCK.MYTHIC_STONE]: { top: 10, bottom: 10, side: 10 },
[BLOCK.CRYSTAL]: { top: 11, bottom: 11, side: 11 },
[BLOCK.GOLD]: { top: 12, bottom: 12, side: 12 },
[BLOCK.OBSIDIAN]: { top: 13, bottom: 13, side: 13 },
[BLOCK.MOSSY_STONE]: { top: 14, bottom: 14, side: 14 },
[BLOCK.BRICK]: { top: 15, bottom: 15, side: 15 },
[BLOCK.GLOWSTONE]: { top: 16, bottom: 16, side: 16 },
};
// ═══════════════════════════════════════════════════════════
// WORLD
// ═══════════════════════════════════════════════════════════
class World {
constructor(scene) {
this.scene = scene;
this.chunks = new Map();
this.chunkMeshes = new Map();
this.modifiedBlocks = new Map();
this.structurePositions = new Set();
this.dirtyChunks = new Set();
this.atlas = createTextureAtlas();
this.material = new THREE.MeshLambertMaterial({
map: this.atlas.texture,
side: THREE.FrontSide,
transparent: false
});
this.waterMaterial = new THREE.MeshLambertMaterial({
map: this.atlas.texture,
transparent: true,
opacity: 0.75,
side: THREE.DoubleSide
});
this.glowMaterial = new THREE.MeshLambertMaterial({
map: this.atlas.texture,
emissive: new THREE.Color(0x443300),
emissiveIntensity: 0.6
});
this.crystalMaterial = new THREE.MeshLambertMaterial({
map: this.atlas.texture,
emissive: new THREE.Color(0x113366),
emissiveIntensity: 0.4
});
this.generateStructures();
}
chunkKey(cx, cz) { return `${cx},${cz}`; }
blockKey(x, y, z) { return `${x},${y},${z}`; }
getHeight(x, z) {
const scale1 = 0.008, scale2 = 0.025, scale3 = 0.06;
const h1 = noise.octave(x * scale1, 0, z * scale1, 4, 0.5, 2) * 28;
const h2 = noise2.octave(x * scale2, 100, z * scale2, 3, 0.45, 2.2) * 12;
const h3 = noise.octave(x * scale3, 200, z * scale3, 2, 0.4, 2.5) * 5;
const mountain = Math.max(0, noise.octave(x * 0.004, 50, z * 0.004, 3, 0.5, 2) * 20);
const base = 18;
return Math.floor(base + h1 + h2 + h3 + mountain);
}
getBiome(x, z) {
const temp = noise.octave(x * 0.003, 300, z * 0.003, 2, 0.5, 2);
const moisture = noise2.octave(x * 0.003, 400, z * 0.003, 2, 0.5, 2);
if (temp > 0.3) return 'desert';
if (temp < -0.2 && moisture > 0) return 'snow';
if (moisture > 0.3) return 'forest';
return 'plains';
}
getBlock(x, y, z) {
const modKey = this.blockKey(x, y, z);
if (this.modifiedBlocks.has(modKey)) return this.modifiedBlocks.get(modKey);
if (y < 0 || y >= WORLD_HEIGHT) return BLOCK.AIR;
const cx = Math.floor(x / CHUNK_SIZE), cz = Math.floor(z / CHUNK_SIZE);
const chunk = this.chunks.get(this.chunkKey(cx, cz));
if (!chunk) return BLOCK.AIR;
const lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
return chunk[lx + lz * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE];
}
setBlock(x, y, z, type) {
const modKey = this.blockKey(x, y, z);
if (type === BLOCK.AIR) {
this.modifiedBlocks.set(modKey, BLOCK.AIR);
} else {
this.modifiedBlocks.set(modKey, type);
}
const cx = Math.floor(x / CHUNK_SIZE), cz = Math.floor(z / CHUNK_SIZE);
this.rebuildChunk(cx, cz);
const lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
if (lx === 0) this.rebuildChunk(cx - 1, cz);
if (lx === CHUNK_SIZE - 1) this.rebuildChunk(cx + 1, cz);
if (lz === 0) this.rebuildChunk(cx, cz - 1);
if (lz === CHUNK_SIZE - 1) this.rebuildChunk(cx, cz + 1);
}
generateChunk(cx, cz) {
const key = this.chunkKey(cx, cz);
if (this.chunks.has(key)) return;
const chunk = new Uint8Array(CHUNK_SIZE * CHUNK_SIZE * WORLD_HEIGHT);
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
const wx = cx * CHUNK_SIZE + lx;
const wz = cz * CHUNK_SIZE + lz;
const height = this.getHeight(wx, wz);
const biome = this.getBiome(wx, wz);
const cave = noise.octave(wx * 0.05, 0, wz * 0.05, 3, 0.5, 2);
for (let y = 0; y < WORLD_HEIGHT; y++) {
let block = BLOCK.AIR;
if (y === 0) block = BLOCK.STONE;
else if (y < height - 4) block = BLOCK.STONE;
else if (y < height - 1) block = BLOCK.DIRT;
else if (y === height - 1) {
if (biome === 'desert') block = BLOCK.SAND;
else if (biome === 'snow') block = BLOCK.SNOW;
else block = BLOCK.GRASS;
} else if (y < height) block = BLOCK.DIRT;
if (y > 3 && y < height - 2 && cave > 0.55) block = BLOCK.AIR;
if (y <= 12 && block === BLOCK.AIR && y < height) block = BLOCK.WATER;
const idx = lx + lz * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
chunk[idx] = block;
}
if (biome === 'forest' && height > 14) {
const treeChance = noise.noise3D(wx * 0.1, 0, wz * 0.1);
if (treeChance > 0.6) this.placeTree(chunk, lx, lz, height);
}
}
}
this.chunks.set(key, chunk);
this.applyStructuresToChunk(cx, cz, chunk);
this.applyModificationsToChunk(cx, cz, chunk);
this.buildChunkMesh(cx, cz, chunk);
}
placeTree(chunk, lx, lz, height) {
const trunkH = 4 + Math.floor(Math.random() * 3);
for (let ty = 0; ty < trunkH; ty++) {
const y = height + ty;
if (y >= WORLD_HEIGHT) break;
const idx = lx + lz * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
chunk[idx] = BLOCK.WOOD;
}
const top = height + trunkH;
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) > 4) continue;
const nx = lx + dx, nz = lz + dz, ny = top + dy;
if (nx < 0 || nx >= CHUNK_SIZE || nz < 0 || nz >= CHUNK_SIZE || ny >= WORLD_HEIGHT) continue;
const idx = nx + nz * CHUNK_SIZE + ny * CHUNK_SIZE * CHUNK_SIZE;
if (chunk[idx] === BLOCK.AIR) chunk[idx] = BLOCK.LEAVES;
}
}
}
}
applyModificationsToChunk(cx, cz, chunk) {
for (const [key, type] of this.modifiedBlocks) {
const [bx, by, bz] = key.split(',').map(Number);
const bcx = Math.floor(bx / CHUNK_SIZE), bcz = Math.floor(bz / CHUNK_SIZE);
if (bcx !== cx || bcz !== cz) continue;
const lx = ((bx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((bz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const idx = lx + lz * CHUNK_SIZE + by * CHUNK_SIZE * CHUNK_SIZE;
chunk[idx] = type;
}
}
// ═══ MYTHIC STRUCTURES ═══
generateStructures() {
this.structures = [
{ name: '精灵螺旋塔', x: 80, z: 60, build: (w, x, z) => w.buildElvenTower(x, z) },
{ name: '龙神殿', x: -120, z: 90, build: (w, x, z) => w.buildDragonTemple(x, z) },
{ name: '天空遗迹', x: 200, z: -80, build: (w, x, z) => w.buildSkyRuins(x, z) },
{ name: '巨石阵', x: -60, z: -140, build: (w, x, z) => w.buildMonolithRing(x, z) },
{ name: '水晶洞窟', x: 150, z: 150, build: (w, x, z) => w.buildCrystalCavern(x, z) },
{ name: '巫师尖塔', x: -200, z: -60, build: (w, x, z) => w.buildWizardSpire(x, z) },
{ name: '神圣树林', x: 40, z: -180, build: (w, x, z) => w.buildSacredGrove(x, z) },
{ name: '地下神殿', x: -150, z: 180, build: (w, x, z) => w.buildUndergroundShrine(x, z) },
{ name: '黄金祭坛', x: 250, z: 30, build: (w, x, z) => w.buildGoldenAltar(x, z) },
{ name: '浮空群岛', x: -80, z: 200, build: (w, x, z) => w.buildFloatingIsles(x, z) },
];
}
applyStructuresToChunk(cx, cz, chunk) {
for (const struct of this.structures) {
const scx = Math.floor(struct.x / CHUNK_SIZE);
const scz = Math.floor(struct.z / CHUNK_SIZE);
if (Math.abs(scx - cx) > 2 || Math.abs(scz - cz) > 2) continue;
const posKey = `${struct.x},${struct.z}`;
if (!this.structurePositions.has(posKey)) {
this.structurePositions.add(posKey);
struct.build(this, struct.x, struct.z);
this.rebuildDirtyChunks();
}
}
}
setBlockDirect(x, y, z, type) {
const cx = Math.floor(x / CHUNK_SIZE), cz = Math.floor(z / CHUNK_SIZE);
const key = this.chunkKey(cx, cz);
if (!this.chunks.has(key)) {
const chunk = new Uint8Array(CHUNK_SIZE * CHUNK_SIZE * WORLD_HEIGHT);
this.chunks.set(key, chunk);
}
const chunk = this.chunks.get(key);
const lx = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const lz = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
if (y < 0 || y >= WORLD_HEIGHT) return;
const idx = lx + lz * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
chunk[idx] = type;
this.dirtyChunks.add(key);
}
rebuildDirtyChunks() {
for (const key of this.dirtyChunks) {
const [cx, cz] = key.split(',').map(Number);
const chunk = this.chunks.get(key);
if (chunk) this.buildChunkMesh(cx, cz, chunk);
}
this.dirtyChunks.clear();
}
fillBox(x1, y1, z1, x2, y2, z2, type) {
for (let x = x1; x <= x2; x++)
for (let y = y1; y <= y2; y++)
for (let z = z1; z <= z2; z++)
this.setBlockDirect(x, y, z, type);
}
fillHollowBox(x1, y1, z1, x2, y2, z2, type) {
for (let x = x1; x <= x2; x++)
for (let y = y1; y <= y2; y++)
for (let z = z1; z <= z2; z++) {
if (x > x1 && x < x2 && y > y1 && y < y2 && z > z1 && z < z2) continue;
this.setBlockDirect(x, y, z, type);
}
}
buildElvenTower(cx, cz) {
const h = this.getHeight(cx, cz);
const base = 7;
for (let y = 0; y < 28; y++) {
const r = Math.max(2, base - Math.floor(y / 5));
for (let dx = -r; dx <= r; dx++)
for (let dz = -r; dz <= r; dz++) {
if (dx * dx + dz * dz > r * r) continue;
const isWall = dx * dx + dz * dz >= (r - 1) * (r - 1);
if (isWall) this.setBlockDirect(cx + dx, h + y, cz + dz, BLOCK.MYTHIC_STONE);
}
}
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const px = Math.round(cx + Math.cos(angle) * 5);
const pz = Math.round(cz + Math.sin(angle) * 5);
for (let y = 0; y < 3; y++) this.setBlockDirect(px, h + y, pz, BLOCK.MOSSY_STONE);
}
this.setBlockDirect(cx, h + 28, cz, BLOCK.CRYSTAL);
this.setBlockDirect(cx + 1, h + 28, cz, BLOCK.CRYSTAL);
this.setBlockDirect(cx - 1, h + 28, cz, BLOCK.CRYSTAL);
this.setBlockDirect(cx, h + 28, cz + 1, BLOCK.CRYSTAL);
this.setBlockDirect(cx, h + 28, cz - 1, BLOCK.CRYSTAL);
this.setBlockDirect(cx, h + 29, cz, BLOCK.CRYSTAL);
for (let y = 0; y < 5; y++)
for (let dx = -1; dx <= 1; dx++)
for (let dz = -1; dz <= 1; dz++)
if (dx * dx + dz * dz <= 2)
this.setBlockDirect(cx + dx, h + 30 + y, cz + dz, BLOCK.LEAVES);
}
buildDragonTemple(cx, cz) {
const h = this.getHeight(cx, cz);
const size = 12;
for (let layer = 0; layer < 8; layer++) {
const r = size - layer;
for (let dx = -r; dx <= r; dx++)
for (let dz = -r; dz <= r; dz++) {
if (Math.abs(dx) + Math.abs(dz) > r) continue;
const isEdge = Math.abs(dx) + Math.abs(dz) >= r - 1;
if (isEdge) this.setBlockDirect(cx + dx, h + layer, cz + dz, BLOCK.OBSIDIAN);
else if (layer === 0) this.setBlockDirect(cx + dx, h, cz + dz, BLOCK.GOLD);
}
}
for (let y = 1; y < 6; y++) {
this.fillBox(cx - 2, h + y, cz - 2, cx + 2, h + y, cz + 2, BLOCK.BRICK);
this.setBlockDirect(cx, h + y, cz, BLOCK.AIR);
}
for (let dx = -1; dx <= 1; dx++)
this.setBlockDirect(cx + dx, h + 8, cz, BLOCK.GLOWSTONE);
this.setBlockDirect(cx, h + 9, cz, BLOCK.CRYSTAL);
for (let i = 0; i < 4; i++) {
const angle = (i / 4) * Math.PI * 2 + Math.PI / 4;
for (let s = 0; s < 6; s++) {
const px = Math.round(cx + Math.cos(angle) * (size + s));
const pz = Math.round(cz + Math.sin(angle) * (size + s));
this.setBlockDirect(px, h, pz, BLOCK.OBSIDIAN);
this.setBlockDirect(px, h + 1, pz, BLOCK.GLOWSTONE);
}
}
}
buildSkyRuins(cx, cz) {
const h = this.getHeight(cx, cz) + 20;
for (let dx = -8; dx <= 8; dx++)
for (let dz = -8; dz <= 8; dz++) {
if (dx * dx + dz * dz > 64) continue;
this.setBlockDirect(cx + dx, h, cz + dz, BLOCK.MOSSY_STONE);
}
this.fillHollowBox(cx - 5, h + 1, cz - 5, cx + 5, h + 5, cz + 5, BLOCK.MYTHIC_STONE);
for (let dx = -6; dx <= 6; dx++) {
this.setBlockDirect(cx + dx, h + 1, cz - 6, BLOCK.MOSSY_STONE);
this.setBlockDirect(cx + dx, h + 1, cz + 6, BLOCK.MOSSY_STONE);
if (dx % 3 === 0) {
this.setBlockDirect(cx + dx, h + 2, cz - 6, BLOCK.MYTHIC_STONE);
this.setBlockDirect(cx + dx, h + 2, cz + 6, BLOCK.MYTHIC_STONE);
}
}
for (let i = 0; i < 4; i++) {
const px = cx + (i < 2 ? -4 : 4);
const pz = cz + (i % 2 === 0 ? -4 : 4);
for (let y = 0; y < 8; y++) this.setBlockDirect(px, h + 1 + y, pz, BLOCK.MYTHIC_STONE);
this.setBlockDirect(px, h + 9, pz, BLOCK.CRYSTAL);
}
for (let y = 0; y < 3; y++)
this.setBlockDirect(cx, h - 1 - y, cz, BLOCK.MYTHIC_STONE);
}
buildMonolithRing(cx, cz) {
const h = this.getHeight(cx, cz);
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const px = Math.round(cx + Math.cos(angle) * 10);
const pz = Math.round(cz + Math.sin(angle) * 10);
const pillarH = 4 + (i % 3) * 2;
for (let y = 0; y < pillarH; y++)
this.setBlockDirect(px, h + y, pz, BLOCK.MYTHIC_STONE);
if (i % 2 === 0) this.setBlockDirect(px, h + pillarH, pz, BLOCK.CRYSTAL);
}
for (let i = 0; i < 8; i++) {
const a1 = (i / 8) * Math.PI * 2, a2 = ((i + 1) / 8) * Math.PI * 2;
for (let s = 0; s <= 10; s++) {
const t = s / 10;
const px = Math.round(cx + Math.cos(a1) * 10 * (1 - t) + Math.cos(a2) * 10 * t);
const pz = Math.round(cz + Math.sin(a1) * 10 * (1 - t) + Math.sin(a2) * 10 * t);
this.setBlockDirect(px, h, pz, BLOCK.MOSSY_STONE);
}
}
this.setBlockDirect(cx, h + 1, cz, BLOCK.GOLD);
this.setBlockDirect(cx, h + 2, cz, BLOCK.CRYSTAL);
}
buildCrystalCavern(cx, cz) {
const h = this.getHeight(cx, cz);
for (let dx = -6; dx <= 6; dx++)
for (let dz = -6; dz <= 6; dz++) {
if (dx * dx + dz * dz > 36) continue;
this.setBlockDirect(cx + dx, h - 1, cz + dz, BLOCK.AIR);
this.setBlockDirect(cx + dx, h - 2, cz + dz, BLOCK.AIR);
this.setBlockDirect(cx + dx, h - 3, cz + dz, BLOCK.AIR);
}
for (let dx = -5; dx <= 5; dx++)
for (let dz = -5; dz <= 5; dz++)
for (let dy = -4; dy <= -1; dy++) {
if (dx * dx + dz * dz + dy * dy > 30) continue;
if (dx * dx + dz * dz + dy * dy > 20)
this.setBlockDirect(cx + dx, h + dy, cz + dz, BLOCK.STONE);
}
const crystals = [[0, -2, 0], [3, -3, 2], [-3, -2, -2], [2, -3, -3], [-2, -3, 3]];
for (const [dx, dy, dz] of crystals) {
for (let y = 0; y < 4; y++)
this.setBlockDirect(cx + dx, h + dy + y, cz + dz, BLOCK.CRYSTAL);
}
this.setBlockDirect(cx, h - 4, cz, BLOCK.GLOWSTONE);
this.setBlockDirect(cx + 1, h - 4, cz, BLOCK.GLOWSTONE);
this.setBlockDirect(cx, h - 4, cz + 1, BLOCK.GLOWSTONE);
}
buildWizardSpire(cx, cz) {
const h = this.getHeight(cx, cz);
for (let y = 0; y < 35; y++) {
const r = y < 5 ? 4 : y < 20 ? 3 : y < 30 ? 2 : 1;
for (let dx = -r; dx <= r; dx++)
for (let dz = -r; dz <= r; dz++) {
if (Math.abs(dx) === r || Math.abs(dz) === r)
this.setBlockDirect(cx + dx, h + y, cz + dz,
y % 5 === 0 ? BLOCK.BRICK : BLOCK.MYTHIC_STONE);
}
}
for (let dx = -5; dx <= 5; dx++)
for (let dz = -5; dz <= 5; dz++)
if (dx * dx + dz * dz <= 25)
this.setBlockDirect(cx + dx, h, cz + dz, BLOCK.MOSSY_STONE);
this.setBlockDirect(cx, h + 35, cz, BLOCK.GLOWSTONE);
this.setBlockDirect(cx, h + 36, cz, BLOCK.CRYSTAL);
for (let y = 0; y < 4; y++)
this.setBlockDirect(cx + 3, h + 1 + y, cz, BLOCK.WOOD);
for (let dy = -1; dy <= 1; dy++)
for (let dx = 2; dx <= 4; dx++)
this.setBlockDirect(cx + dx, h + 4 + dy, cz, BLOCK.LEAVES);
}
buildSacredGrove(cx, cz) {
const h = this.getHeight(cx, cz);
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 2;
const px = Math.round(cx + Math.cos(angle) * 8);
const pz = Math.round(cz + Math.sin(angle) * 8);
const th = this.getHeight(px, pz);
for (let y = 0; y < 6; y++) this.setBlockDirect(px, th + y, pz, BLOCK.WOOD);
for (let dy = -2; dy <= 3; dy++)
for (let dx = -3; dx <= 3; dx++)
for (let dz = -3; dz <= 3; dz++) {
if (dx * dx + dy * dy + dz * dz > 12) continue;
this.setBlockDirect(px + dx, th + 6 + dy, pz + dz, BLOCK.LEAVES);
}
}
for (let dx = -3; dx <= 3; dx++)
for (let dz = -3; dz <= 3; dz++)
if (dx * dx + dz * dz <= 9)
this.setBlockDirect(cx + dx, h, cz + dz, BLOCK.GOLD);
this.setBlockDirect(cx, h + 1, cz, BLOCK.CRYSTAL);
this.setBlockDirect(cx, h + 2, cz, BLOCK.CRYSTAL);
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * Math.PI * 2;
const px = Math.round(cx + Math.cos(angle) * 8);
const pz = Math.round(cz + Math.sin(angle) * 8);
this.setBlockDirect(px, this.getHeight(px, pz), pz, BLOCK.MYTHIC_STONE);
}
}
buildUndergroundShrine(cx, cz) {
const h = this.getHeight(cx, cz);
for (let dx = -7; dx <= 7; dx++)
for (let dz = -7; dz <= 7; dz++) {
if (dx * dx + dz * dz > 49) continue;
for (let dy = -6; dy <= -1; dy++)
this.setBlockDirect(cx + dx, h + dy, cz + dz, BLOCK.AIR);
}
this.fillHollowBox(cx - 5, h - 6, cz - 5, cx + 5, h - 1, cz + 5, BLOCK.BRICK);
for (let dx = -3; dx <= 3; dx++)
for (let dz = -3; dz <= 3; dz++)
this.setBlockDirect(cx + dx, h - 7, cz + dz, BLOCK.OBSIDIAN);
this.setBlockDirect(cx, h - 5, cz, BLOCK.GOLD);
this.setBlockDirect(cx, h - 4, cz, BLOCK.CRYSTAL);
for (let i = 0; i < 4; i++) {
const angle = (i / 4) * Math.PI * 2;
const px = Math.round(cx + Math.cos(angle) * 4);
const pz = Math.round(cz + Math.sin(angle) * 4);
this.setBlockDirect(px, h - 5, pz, BLOCK.GLOWSTONE);
}
for (let dx = -1; dx <= 1; dx++)
for (let dz = -1; dz <= 1; dz++)
this.setBlockDirect(cx + dx, h, cz + dz, BLOCK.MOSSY_STONE);
const stairX = cx + 7;
for (let i = 0; i < 6; i++)
this.setBlockDirect(stairX, h - i, cz, BLOCK.MOSSY_STONE);
}
buildGoldenAltar(cx, cz) {
const h = this.getHeight(cx, cz);
for (let layer = 0; layer < 4; layer++) {
const r = 6 - layer;
for (let dx = -r; dx <= r; dx++)
for (let dz = -r; dz <= r; dz++) {
if (dx * dx + dz * dz > r * r) continue;
this.setBlockDirect(cx + dx, h + layer, cz + dz,
layer < 2 ? BLOCK.GOLD : BLOCK.MYTHIC_STONE);
}
}
for (let y = 0; y < 3; y++)
this.setBlockDirect(cx, h + 4 + y, cz, BLOCK.CRYSTAL);
for (let i = 0; i < 6; i++) {
const angle = (i / 6) * Math.PI * 2;
const px = Math.round(cx + Math.cos(angle) * 8);
const pz = Math.round(cz + Math.sin(angle) * 8);
for (let y = 0; y < 2; y++) this.setBlockDirect(px, h + y, pz, BLOCK.OBSIDIAN);
this.setBlockDirect(px, h + 2, pz, BLOCK.GLOWSTONE);
}
}
buildFloatingIsles(cx, cz) {
const baseH = this.getHeight(cx, cz) + 25;
const isles = [[0, 0, 6], [-12, 5, 4], [12, 3, 5], [0, -14, 4], [8, 10, 3]];
for (const [ox, oz, radius] of isles) {
const ix = cx + ox, iz = cz + oz;
const ih = baseH + (oz > 0 ? 3 : 0);
for (let dx = -radius; dx <= radius; dx++)
for (let dz = -radius; dz <= radius; dz++) {
if (dx * dx + dz * dz > radius * radius) continue;
this.setBlockDirect(ix + dx, ih, iz + dz, BLOCK.GRASS);
this.setBlockDirect(ix + dx, ih - 1, iz + dz, BLOCK.DIRT);
if (dx * dx + dz * dz > (radius - 1) * (radius - 1))
for (let y = ih - 2; y >= ih - 5; y--)
this.setBlockDirect(ix + dx, y, iz + dz, BLOCK.MYTHIC_STONE);
}
if (radius >= 5) {
for (let y = 0; y < 5; y++) this.setBlockDirect(ix, ih + 1 + y, iz, BLOCK.MYTHIC_STONE);
this.setBlockDirect(ix, ih + 6, iz, BLOCK.CRYSTAL);
}
if (radius >= 4) {
for (let y = 0; y < 3; y++) this.setBlockDirect(ix + 2, ih + 1 + y, iz, BLOCK.WOOD);
for (let dy = 0; dy <= 2; dy++)
for (let dx = -2; dx <= 2; dx++)
for (let dz = -2; dz <= 2; dz++)
if (dx * dx + dz * dz <= 4)
this.setBlockDirect(ix + 2 + dx, ih + 4 + dy, iz + dz, BLOCK.LEAVES);
}
for (let y = 0; y < 8; y++)
this.setBlockDirect(ix, ih - 6 - y, iz, BLOCK.MYTHIC_STONE);
}
}
rebuildChunk(cx, cz) {
const key = this.chunkKey(cx, cz);
const chunk = this.chunks.get(key);
if (!chunk) return;
this.applyModificationsToChunk(cx, cz, chunk);
this.buildChunkMesh(cx, cz, chunk);
}
buildChunkMesh(cx, cz, chunk) {
const key = this.chunkKey(cx, cz);
const old = this.chunkMeshes.get(key);
if (old) {
this.scene.remove(old.solid);
this.scene.remove(old.transparent);
old.solid.geometry.dispose();
if (old.transparent) old.transparent.geometry.dispose();
}
const solidData = { pos: [], norm: [], uv: [], idx: [] };
const transData = { pos: [], norm: [], uv: [], idx: [] };
const TILE_UV = 1 / 16;
const getUV = (texIndex) => {
const col = texIndex % 16, row = Math.floor(texIndex / 16);
return { u: col * TILE_UV, v: 1 - (row + 1) * TILE_UV, du: TILE_UV, dv: TILE_UV };
};
const addFace = (data, x, y, z, face, blockType) => {
const tex = BLOCK_TEX[blockType];
if (!tex) return;
let texIdx;
if (face === 0) texIdx = tex.top;
else if (face === 1) texIdx = tex.bottom;
else texIdx = tex.side;
const { u, v, du, dv } = getUV(texIdx);
const wx = cx * CHUNK_SIZE + x, wy = y, wz = cz * CHUNK_SIZE + z;
const vi = data.pos.length / 3;
const faces = [
[[0,1,1],[1,1,1],[1,1,0],[0,1,0]], // top
[[0,0,0],[1,0,0],[1,0,1],[0,0,1]], // bottom
[[0,0,1],[1,0,1],[1,1,1],[0,1,1]], // front +z
[[1,0,0],[0,0,0],[0,1,0],[1,1,0]], // back -z
[[1,0,1],[1,0,0],[1,1,0],[1,1,1]], // right +x
[[0,0,0],[0,0,1],[0,1,1],[0,1,0]], // left -x
];
const norms = [[0,1,0],[0,-1,0],[0,0,1],[0,0,-1],[1,0,0],[-1,0,0]];
const verts = faces[face];
const uvMap = [[u,v],[u+du,v],[u+du,v+dv],[u,v+dv]];
for (let i = 0; i < 4; i++) {
data.pos.push(wx + verts[i][0], wy + verts[i][1], wz + verts[i][2]);
data.norm.push(...norms[face]);
data.uv.push(uvMap[i][0], uvMap[i][1]);
}
data.idx.push(vi, vi + 1, vi + 2, vi, vi + 2, vi + 3);
};
const isTransparent = (b) => b === BLOCK.WATER || b === BLOCK.LEAVES;
const shouldDrawFace = (cur, neighbor) => {
if (neighbor === BLOCK.AIR) return true;
if (isTransparent(cur) !== isTransparent(neighbor)) return true;
if (isTransparent(cur) && isTransparent(neighbor) && cur !== neighbor) return true;
return false;
};
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
for (let y = 0; y < WORLD_HEIGHT; y++) {
const idx = lx + lz * CHUNK_SIZE + y * CHUNK_SIZE * CHUNK_SIZE;
const block = chunk[idx];
if (block === BLOCK.AIR) continue;
const wx = cx * CHUNK_SIZE + lx, wz = cz * CHUNK_SIZE + lz;
const data = isTransparent(block) ? transData : solidData;
const neighbors = [
this.getBlock(wx, y + 1, wz),
this.getBlock(wx, y - 1, wz),
this.getBlock(wx, y, wz + 1),
this.getBlock(wx, y, wz - 1),
this.getBlock(wx + 1, y, wz),
this.getBlock(wx - 1, y, wz),
];
for (let f = 0; f < 6; f++) {
if (shouldDrawFace(block, neighbors[f])) {
addFace(data, lx, y, lz, f, block);
}
}
}
}
}
const meshes = {};
if (solidData.pos.length > 0) {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(solidData.pos, 3));
geo.setAttribute('normal', new THREE.Float32BufferAttribute(solidData.norm, 3));
geo.setAttribute('uv', new THREE.Float32BufferAttribute(solidData.uv, 2));
geo.setIndex(solidData.idx);
meshes.solid = new THREE.Mesh(geo, this.material);
this.scene.add(meshes.solid);
} else {
meshes.solid = null;
}
if (transData.pos.length > 0) {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.Float32BufferAttribute(transData.pos, 3));
geo.setAttribute('normal', new THREE.Float32BufferAttribute(transData.norm, 3));
geo.setAttribute('uv', new THREE.Float32BufferAttribute(transData.uv, 2));
geo.setIndex(transData.idx);
meshes.transparent = new THREE.Mesh(geo, this.waterMaterial);
this.scene.add(meshes.transparent);
} else {
meshes.transparent = null;
}
this.chunkMeshes.set(key, meshes);
}
updateChunks(px, pz) {
const ccx = Math.floor(px / CHUNK_SIZE);
const ccz = Math.floor(pz / CHUNK_SIZE);
for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
this.generateChunk(ccx + dx, ccz + dz);
}
}
for (const [key] of this.chunkMeshes) {
const [cx, cz] = key.split(',').map(Number);
if (Math.abs(cx - ccx) > RENDER_DISTANCE + 1 || Math.abs(cz - ccz) > RENDER_DISTANCE + 1) {
const meshes = this.chunkMeshes.get(key);
if (meshes.solid) { this.scene.remove(meshes.solid); meshes.solid.geometry.dispose(); }
if (meshes.transparent) { this.scene.remove(meshes.transparent); meshes.transparent.geometry.dispose(); }
this.chunkMeshes.delete(key);
this.chunks.delete(key);
}
}
}
}
// ═══════════════════════════════════════════════════════════
// PLAYER & CONTROLS
// ═══════════════════════════════════════════════════════════
class Player {
constructor(camera, world) {
this.camera = camera;
this.world = world;
this.position = new THREE.Vector3(0, 40, 0);
this.velocity = new THREE.Vector3();
this.onGround = false;
this.yaw = 0;
this.pitch = 0;
this.sprinting = false;
this.selectedSlot = 0;
this.keys = {};
this.locked = false;
this.raycaster = new THREE.Raycaster();
this.raycaster.far = 6;
this.highlightBox = this.createHighlight();
this.world.scene.add(this.highlightBox);
}
createHighlight() {
const geo = new THREE.BoxGeometry(1.005, 1.005, 1.005);
const edges = new THREE.EdgesGeometry(geo);
const mat = new THREE.LineBasicMaterial({ color: 0x000000, linewidth: 2 });
const mesh = new THREE.LineSegments(edges, mat);
mesh.visible = false;
return mesh;
}
lock() {
document.body.requestPointerLock();
}
getTargetBlock() {
this.raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
const dir = this.raycaster.ray.direction;
const origin = this.camera.position.clone();
const step = 0.05;
let prev = null;
for (let t = 0; t < 6; t += step) {
const p = origin.clone().add(dir.clone().multiplyScalar(t));
const bx = Math.floor(p.x), by = Math.floor(p.y), bz = Math.floor(p.z);
const block = this.world.getBlock(bx, by, bz);
if (block !== BLOCK.AIR && block !== BLOCK.WATER && block !== BLOCK.LEAVES) {
return { x: bx, y: by, z: bz, block, prev };
}
if (block !== BLOCK.AIR) prev = { x: bx, y: by, z: bz };
else prev = { x: bx, y: by, z: bz };
}
return null;
}
getPlacePosition(target) {
if (!target) return null;
const dir = this.raycaster.ray.direction;
const origin = this.camera.position.clone();
const step = 0.05;
let prevPos = null;
for (let t = 0; t < 6; t += step) {
const p = origin.clone().add(dir.clone().multiplyScalar(t));
const bx = Math.floor(p.x), by = Math.floor(p.y), bz = Math.floor(p.z);
const block = this.world.getBlock(bx, by, bz);
if (block !== BLOCK.AIR && block !== BLOCK.WATER) {
return prevPos;
}
prevPos = { x: bx, y: by, z: bz };
}
return prevPos;
}
breakBlock() {
const target = this.getTargetBlock();
if (target) this.world.setBlock(target.x, target.y, target.z, BLOCK.AIR);
}
placeBlock() {
const target = this.getTargetBlock();
if (!target) return;
const pos = this.getPlacePosition(target);
if (!pos) return;
const blockType = HOTBAR_BLOCKS[this.selectedSlot];
const px = Math.floor(this.position.x), py = Math.floor(this.position.y),
pz = Math.floor(this.position.z);
const py2 = Math.floor(this.position.y + PLAYER_HEIGHT);
if (pos.x === px && pos.z === pz && (pos.y === py || pos.y === py2)) return;
this.world.setBlock(pos.x, pos.y, pos.z, blockType);
}
update(dt) {
const speed = MOVE_SPEED * (this.sprinting ? SPRINT_MULT : 1);
const forward = new THREE.Vector3(-Math.sin(this.yaw), 0, -Math.cos(this.yaw));
const right = new THREE.Vector3(Math.cos(this.yaw), 0, -Math.sin(this.yaw));
const move = new THREE.Vector3();
if (this.keys['KeyW']) move.add(forward);
if (this.keys['KeyS']) move.sub(forward);
if (this.keys['KeyA']) move.sub(right);
if (this.keys['KeyD']) move.add(right);
if (move.length() > 0) move.normalize().multiplyScalar(speed); // fix: was `speed * dt` — tryMove() multiplies by dt again, so walking ran at speed*dt^2 (~1/60 of design speed)
this.velocity.x = move.x;
this.velocity.z = move.z;
if (!this.onGround) this.velocity.y -= GRAVITY * dt;
if (this.keys['Space'] && this.onGround) {
this.velocity.y = JUMP_FORCE;
this.onGround = false;
}
this.moveWithCollision(dt);
this.camera.position.copy(this.position);
this.camera.position.y += PLAYER_HEIGHT - 0.2;
this.camera.rotation.order = 'YXZ';
this.camera.rotation.y = this.yaw;
this.camera.rotation.x = this.pitch;
const target = this.getTargetBlock();
if (target) {
this.highlightBox.position.set(target.x + 0.5, target.y + 0.5, target.z + 0.5);
this.highlightBox.visible = true;
} else {
this.highlightBox.visible = false;
}
this.world.updateChunks(this.position.x, this.position.z);
}
moveWithCollision(dt) {
const tryMove = (axis, amount) => {
this.position[axis] += amount;
const minX = Math.floor(this.position.x - PLAYER_RADIUS);
const maxX = Math.floor(this.position.x + PLAYER_RADIUS);
const minY = Math.floor(this.position.y);
const maxY = Math.floor(this.position.y + PLAYER_HEIGHT);
const minZ = Math.floor(this.position.z - PLAYER_RADIUS);
const maxZ = Math.floor(this.position.z + PLAYER_RADIUS);
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
for (let z = minZ; z <= maxZ; z++) {
const b = this.world.getBlock(x, y, z);
if (b !== BLOCK.AIR && b !== BLOCK.WATER && b !== BLOCK.LEAVES) {
this.position[axis] -= amount;
if (axis === 'y' && amount < 0) this.onGround = true;
if (axis === 'y' && amount > 0) this.velocity.y = 0;
return;
}
}
}
}
if (axis === 'y' && amount < 0) this.onGround = false;
};
tryMove('x', this.velocity.x * dt);
tryMove('z', this.velocity.z * dt);
tryMove('y', this.velocity.y * dt);
if (this.onGround) this.velocity.y = 0;
const groundY = this.getGroundHeight(this.position.x, this.position.z);
if (this.position.y < groundY + 2) {
// prevent falling through unloaded chunks
}
if (this.position.y < -10) {
this.position.set(0, 40, 0);
this.velocity.set(0, 0, 0);
}
}
getGroundHeight(x, z) {
for (let y = WORLD_HEIGHT - 1; y >= 0; y--) {
const b = this.world.getBlock(Math.floor(x), y, Math.floor(z));
if (b !== BLOCK.AIR && b !== BLOCK.WATER && b !== BLOCK.LEAVES) return y + 1;
}
return 20;
}
}
// ═══════════════════════════════════════════════════════════
// MAIN
// ═══════════════════════════════════════════════════════════
const renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 60, RENDER_DISTANCE * CHUNK_SIZE * 1.5);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 300);
const ambient = new THREE.AmbientLight(0x6688aa, 0.6);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xfff5e0, 1.0);
sun.position.set(100, 150, 80);
sun.castShadow = true;
scene.add(sun);
const world = new World(scene);
const player = new Player(camera, world);
// Spawn on ground
const spawnH = world.getHeight(0, 0) + 2;
player.position.set(0, spawnH, 0);
world.updateChunks(0, 0);
// Hotbar UI
const hotbarEl = document.getElementById('hotbar');
const { canvas: atlasCanvas } = world.atlas;
HOTBAR_BLOCKS.forEach((blockType, i) => {
const slot = document.createElement('div');
slot.className = 'slot' + (i === 0 ? ' active' : '');
const tc = document.createElement('canvas');
tc.width = 16; tc.height = 16;
const tctx = tc.getContext('2d');
const tex = BLOCK_TEX[blockType];
const TILE = 16;
const col = tex.side % 16, row = Math.floor(tex.side / 16);
tctx.drawImage(atlasCanvas, col * TILE, row * TILE, TILE, TILE, 0, 0, 16, 16);
slot.appendChild(tc);
const key = document.createElement('span');
key.className = 'key';
key.textContent = i + 1;
slot.appendChild(key);
hotbarEl.appendChild(slot);
});
function selectSlot(i) {
player.selectedSlot = i;
document.querySelectorAll('.slot').forEach((s, j) => s.classList.toggle('active', j === i));
document.getElementById('block-name').textContent = BLOCK_NAMES[HOTBAR_BLOCKS[i]] || '';
}
// Events
const overlay = document.getElementById('overlay');
overlay.addEventListener('click', () => {
overlay.style.display = 'none';
document.getElementById('crosshair').style.display = 'block';
document.getElementById('hud').style.display = 'flex';
document.getElementById('block-name').style.display = 'block';
document.getElementById('minimap').style.display = 'block';
player.lock();
selectSlot(0);
});
document.addEventListener('pointerlockchange', () => {
player.locked = document.pointerLockElement === document.body;
if (!player.locked) {
overlay.style.display = 'flex';
document.getElementById('crosshair').style.display = 'none';
}
});
document.addEventListener('mousemove', (e) => {
if (!player.locked) return;
player.yaw -= e.movementX * 0.002;
player.pitch -= e.movementY * 0.002;
player.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, player.pitch));
});
// added: G cycles teleports through the generated landmarks (the structures
// exist in world.structures but were otherwise only reachable on foot)
let landmarkIndex = 0;
document.addEventListener('keydown', (e) => {
player.keys[e.code] = true;
if (e.code === 'KeyG') {
const struct = world.structures[landmarkIndex++ % world.structures.length];
const isSky = struct.name === '天空遗迹';
// sky ruins: land on the floating deck; ground landmarks: arrive a few
// blocks out, facing the structure
const tx = struct.x + (isSky ? 6.5 : 10.5);
const tz = struct.z + (isSky ? 0.5 : 10.5);
const groundY = isSky
? world.getHeight(Math.round(struct.x), Math.round(struct.z))
: world.getHeight(Math.round(tx), Math.round(tz));
player.position.set(tx, groundY + (isSky ? 23 : 3), tz);
player.velocity.set(0, 0, 0);
player.yaw = Math.atan2(tx - struct.x, tz - struct.z);
player.pitch = 0;
}
if (e.code.startsWith('Digit') && e.code !== 'Digit0') {
const n = parseInt(e.code.replace('Digit', '')) - 1;
if (n >= 0 && n < HOTBAR_BLOCKS.length) selectSlot(n);
}
});
document.addEventListener('keyup', (e) => { player.keys[e.code] = false; });
document.addEventListener('mousedown', (e) => {
if (!player.locked) return;
if (e.button === 0) player.breakBlock();
if (e.button === 2) player.placeBlock();
});
document.addEventListener('contextmenu', (e) => e.preventDefault());
document.addEventListener('wheel', (e) => {
if (!player.locked) return;
const next = (player.selectedSlot + (e.deltaY > 0 ? 1 : -1) + HOTBAR_BLOCKS.length) % HOTBAR_BLOCKS.length;
selectSlot(next);
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Minimap
const minimapCanvas = document.getElementById('minimap');
const minimapCtx = minimapCanvas.getContext('2d');
minimapCanvas.width = 120;
minimapCanvas.height = 120;
function drawMinimap() {
minimapCtx.fillStyle = 'rgba(0,0,40,0.6)';
minimapCtx.fillRect(0, 0, 120, 120);
const px = player.position.x, pz = player.position.z;
const scale = 1.5;
for (const struct of world.structures) {
const sx = (struct.x - px) * scale + 60;
const sz = (struct.z - pz) * scale + 60;
if (sx > 0 && sx < 120 && sz > 0 && sz < 120) {
minimapCtx.fillStyle = '#e8d5a3';
minimapCtx.beginPath();
minimapCtx.arc(sx, sz, 3, 0, Math.PI * 2);
minimapCtx.fill();
}
}
minimapCtx.fillStyle = '#ff4444';
minimapCtx.beginPath();
minimapCtx.arc(60, 60, 3, 0, Math.PI * 2);
minimapCtx.fill();
const angle = -player.yaw;
minimapCtx.strokeStyle = '#ff4444';
minimapCtx.beginPath();
minimapCtx.moveTo(60, 60);
minimapCtx.lineTo(60 + Math.sin(angle) * 10, 60 + Math.cos(angle) * 10);
minimapCtx.stroke();
}
// Game loop
let lastTime = performance.now();
let frameCount = 0, fpsTime = 0, fps = 60;
function animate(now) {
requestAnimationFrame(animate);
const dt = Math.min((now - lastTime) / 1000, 0.05);
lastTime = now;
if (player.locked) {
player.sprinting = player.keys['ShiftLeft'] || player.keys['ShiftRight'];
player.update(dt);
}
frameCount++;
fpsTime += dt;
if (fpsTime >= 0.5) {
fps = Math.round(frameCount / fpsTime);
frameCount = 0;
fpsTime = 0;
const p = player.position;
document.getElementById('info').textContent =
`坐标: ${Math.floor(p.x)}, ${Math.floor(p.y)}, ${Math.floor(p.z)} | FPS: ${fps}`;
drawMinimap();
}
const t = now * 0.0001;
scene.background.setHSL(0.55 + Math.sin(t) * 0.02, 0.6, 0.65);
scene.fog.color.copy(scene.background);
renderer.render(scene, camera);
}
animate(performance.now());
</script>
</body>
</html>