Files
nagi-bench/outputs/kimi-k2-6/kimi-code-thinking/mythos-craft.html
T

1609 lines
69 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>神话世界 - MythicWorld</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #1a1a2e; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, 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: 100;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute; background: rgba(255,255,255,0.85);
}
#crosshair::before { top: 9px; left: 0; width: 20px; height: 2px; }
#crosshair::after { top: 0; left: 9px; width: 2px; height: 20px; }
/* 快捷栏 */
#hotbar {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
display: flex; gap: 4px; z-index: 100; background: rgba(0,0,0,0.5);
padding: 6px; border-radius: 8px; border: 2px solid rgba(255,255,255,0.2);
}
.slot {
width: 48px; height: 48px; background: rgba(0,0,0,0.6);
border: 2px solid rgba(255,255,255,0.3); border-radius: 4px;
display: flex; flex-direction: column; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.1s; position: relative;
}
.slot.active {
border-color: #ffd700; box-shadow: 0 0 8px rgba(255,215,0,0.4);
transform: scale(1.05);
}
.slot-preview { width: 32px; height: 32px; border-radius: 2px; }
.slot-name { font-size: 9px; color: #fff; margin-top: 2px; }
.slot-key { position: absolute; top: 2px; left: 3px; font-size: 10px; color: #ccc; }
/* 加载界面 */
#loading {
position: fixed; inset: 0; background: #0f0f23; z-index: 1000;
display: flex; flex-direction: column; align-items: center; justify-content: center;
transition: opacity 0.5s;
}
#loading h1 { color: #ffd700; font-size: 48px; margin-bottom: 20px; text-shadow: 0 0 20px rgba(255,215,0,0.3); }
#loading .subtitle { color: #aaa; font-size: 16px; margin-bottom: 40px; }
#loading-bar { width: 400px; height: 6px; background: #222; border-radius: 3px; overflow: hidden; }
#loading-fill { height: 100%; width: 0%; background: #ffd700; border-radius: 3px; transition: width 0.3s; }
#loading-text { color: #888; font-size: 13px; margin-top: 15px; }
/* 提示界面 */
#overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.75); z-index: 50;
display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #fff; text-align: center;
}
#overlay h2 { font-size: 36px; margin-bottom: 20px; color: #ffd700; }
#overlay p { font-size: 16px; margin: 6px 0; color: #ddd; line-height: 1.6; }
#overlay .start-btn {
margin-top: 30px; padding: 14px 40px; font-size: 18px;
background: #ffd700; color: #1a1a2e; border: none; border-radius: 8px;
cursor: pointer; font-weight: bold; transition: all 0.2s;
}
#overlay .start-btn:hover { background: #ffec8b; transform: scale(1.05); }
/* 坐标信息 */
#info {
position: fixed; top: 15px; left: 15px; z-index: 100;
color: #fff; font-size: 13px; line-height: 1.6;
background: rgba(0,0,0,0.5); padding: 10px 14px; border-radius: 6px;
font-family: monospace;
}
#info span { color: #ffd700; }
/* 操作提示 */
#controls-hint {
position: fixed; bottom: 85px; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.6); font-size: 12px; z-index: 100;
text-align: center; pointer-events: none;
}
/* 选中提示 */
#block-info {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -60px);
color: #fff; font-size: 14px; z-index: 100; pointer-events: none;
background: rgba(0,0,0,0.6); padding: 4px 12px; border-radius: 4px;
opacity: 0; transition: opacity 0.15s;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="loading">
<h1>神话世界</h1>
<div class="subtitle">MythicWorld - 无限创造的幻想沙盒</div>
<div id="loading-bar"><div id="loading-fill"></div></div>
<div id="loading-text">正在初始化世界...</div>
</div>
<div id="overlay">
<h2>神话世界</h2>
<p>点击屏幕进入游戏,使用鼠标环视四周</p>
<p><strong>WASD</strong> 移动 | <strong>空格</strong> 跳跃 | <strong>Shift</strong> 潜行</p>
<p><strong>鼠标左键</strong> 破坏方块 | <strong>鼠标右键</strong> 放置方块</p>
<p><strong>数字键 1-9</strong> 切换方块类型</p>
<button class="start-btn" id="startBtn">开始冒险</button>
</div>
<div id="crosshair"></div>
<div id="block-info"></div>
<div id="info">
<div>位置: <span id="pos">0, 0, 0</span></div>
<div>朝向: <span id="rot">0</span></div>
<div>方块: <span id="target">无</span></div>
</div>
<div id="controls-hint">按 ESC 暂停 | 左键破坏 | 右键放置</div>
<div id="hotbar"></div>
<script type="module">
import * as THREE from 'three';
// ==================== Perlin Noise ====================
class PerlinNoise {
constructor(seed = Math.random() * 65536) {
this.p = new Uint8Array(512);
this.perm = new Uint8Array(512);
this.seed = seed;
this._init();
}
_init() {
const p = new Uint8Array(256);
for (let i = 0; i < 256; i++) p[i] = i;
let s = this.seed;
for (let i = 255; i > 0; i--) {
s = (s * 16807 + 0) % 2147483647;
const j = s % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
}
for (let i = 0; i < 512; i++) {
this.perm[i] = p[i & 255];
this.p[i] = this.perm[i];
}
}
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);
}
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.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);
}
noise2D(x, y) {
return this.noise3D(x, y, 0);
}
fbm(x, y, z, octaves = 4, persistence = 0.5, lacunarity = 2.0) {
let total = 0, amplitude = 1, frequency = 1, maxValue = 0;
for (let i = 0; i < octaves; i++) {
total += this.noise3D(x * frequency, y * frequency, z * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return total / maxValue;
}
fbm2D(x, y, octaves = 4, persistence = 0.5, lacunarity = 2.0) {
return this.fbm(x, y, 0, octaves, persistence, lacunarity);
}
}
// ==================== 方块类型 ====================
const BLOCK_TYPES = {
AIR: 0,
GRASS: 1,
DIRT: 2,
STONE: 3,
WOOD: 4,
LEAVES: 5,
SANDSTONE: 6,
GOLD: 7,
OBSIDIAN: 8,
GLOWSTONE: 9,
BRICK: 10,
PLANKS: 11,
COBBLESTONE: 12,
MOSSY_STONE: 13,
ICE: 14,
NETHERRACK: 15,
};
const BLOCK_NAMES = {
[BLOCK_TYPES.GRASS]: '草地',
[BLOCK_TYPES.DIRT]: '泥土',
[BLOCK_TYPES.STONE]: '石头',
[BLOCK_TYPES.WOOD]: '原木',
[BLOCK_TYPES.LEAVES]: '树叶',
[BLOCK_TYPES.SANDSTONE]: '沙石',
[BLOCK_TYPES.GOLD]: '金块',
[BLOCK_TYPES.OBSIDIAN]: '黑曜石',
[BLOCK_TYPES.GLOWSTONE]: '发光石',
[BLOCK_TYPES.BRICK]: '红砖',
[BLOCK_TYPES.PLANKS]: '木板',
[BLOCK_TYPES.COBBLESTONE]: '圆石',
[BLOCK_TYPES.MOSSY_STONE]: '苔石',
[BLOCK_TYPES.ICE]: '冰块',
[BLOCK_TYPES.NETHERRACK]: '地狱岩',
};
// ==================== 程序纹理生成 ====================
function createPixelTexture(baseColor, noiseAmount = 20, pixelSize = 4) {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
const data = imgData.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const nx = Math.floor(x / pixelSize);
const ny = Math.floor(y / pixelSize);
const noise = (Math.random() - 0.5) * noiseAmount;
data[i] = Math.max(0, Math.min(255, baseColor[0] + noise));
data[i + 1] = Math.max(0, Math.min(255, baseColor[1] + noise));
data[i + 2] = Math.max(0, Math.min(255, baseColor[2] + noise));
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return tex;
}
function createGrassTopTexture() {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
const data = imgData.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const noise = (Math.random() - 0.5) * 30;
const px = Math.floor(x / 4), py = Math.floor(y / 4);
const variation = ((px + py) % 3) * 5;
data[i] = Math.max(0, Math.min(255, 90 + noise + variation));
data[i + 1] = Math.max(0, Math.min(255, 160 + noise + variation));
data[i + 2] = Math.max(0, Math.min(255, 50 + noise));
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return tex;
}
function createGrassSideTexture() {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
const data = imgData.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const isTop = y < 20 + Math.random() * 8;
const noise = (Math.random() - 0.5) * 25;
if (isTop) {
data[i] = Math.max(0, Math.min(255, 95 + noise));
data[i + 1] = Math.max(0, Math.min(255, 155 + noise));
data[i + 2] = Math.max(0, Math.min(255, 55 + noise));
} else {
data[i] = Math.max(0, Math.min(255, 130 + noise));
data[i + 1] = Math.max(0, Math.min(255, 90 + noise));
data[i + 2] = Math.max(0, Math.min(255, 60 + noise));
}
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return tex;
}
function createWoodSideTexture() {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
const data = imgData.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const ring = Math.sin(x * 0.3) * 8 + Math.cos(y * 0.2) * 6;
const bark = (x % 8 < 2) ? -15 : 0;
const noise = (Math.random() - 0.5) * 15;
data[i] = Math.max(0, Math.min(255, 110 + ring + bark + noise));
data[i + 1] = Math.max(0, Math.min(255, 85 + ring + bark + noise));
data[i + 2] = Math.max(0, Math.min(255, 55 + ring + bark + noise));
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return tex;
}
function createWoodTopTexture() {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
const data = imgData.data;
const cx = size / 2, cy = size / 2;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
const ring = Math.sin(dist * 0.5) * 12;
const noise = (Math.random() - 0.5) * 15;
data[i] = Math.max(0, Math.min(255, 160 + ring + noise));
data[i + 1] = Math.max(0, Math.min(255, 125 + ring + noise));
data[i + 2] = Math.max(0, Math.min(255, 85 + ring + noise));
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return tex;
}
function createLeavesTexture() {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
const data = imgData.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const px = Math.floor(x / 4), py = Math.floor(y / 4);
const pattern = ((px * 7 + py * 13) % 5) < 2 ? 1 : 0;
const noise = (Math.random() - 0.5) * 30;
if (pattern) {
data[i] = Math.max(0, Math.min(255, 50 + noise));
data[i + 1] = Math.max(0, Math.min(255, 110 + noise));
data[i + 2] = Math.max(0, Math.min(255, 35 + noise));
} else {
data[i] = Math.max(0, Math.min(255, 45 + noise));
data[i + 1] = Math.max(0, Math.min(255, 100 + noise));
data[i + 2] = Math.max(0, Math.min(255, 30 + noise));
}
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return tex;
}
function createGlowstoneTexture() {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
const data = imgData.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const px = Math.floor(x / 4), py = Math.floor(y / 4);
const bright = ((px + py) % 3 === 0) ? 40 : 0;
const noise = (Math.random() - 0.5) * 15;
data[i] = Math.max(0, Math.min(255, 240 + bright + noise));
data[i + 1] = Math.max(0, Math.min(255, 220 + bright + noise));
data[i + 2] = Math.max(0, Math.min(255, 160 + bright + noise));
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return tex;
}
function createBrickTexture() {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
const data = imgData.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const brickY = Math.floor(y / 16);
const brickX = Math.floor((x + (brickY % 2) * 8) / 16);
const mortar = (y % 16 < 2) || ((x + (brickY % 2) * 8) % 16 < 2);
const noise = (Math.random() - 0.5) * 20;
if (mortar) {
data[i] = Math.max(0, Math.min(255, 100 + noise));
data[i + 1] = Math.max(0, Math.min(255, 95 + noise));
data[i + 2] = Math.max(0, Math.min(255, 90 + noise));
} else {
data[i] = Math.max(0, Math.min(255, 170 + noise));
data[i + 1] = Math.max(0, Math.min(255, 75 + noise));
data[i + 2] = Math.max(0, Math.min(255, 55 + noise));
}
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return tex;
}
function createObsidianTexture() {
const size = 64;
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(size, size);
const data = imgData.data;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const px = Math.floor(x / 4), py = Math.floor(y / 4);
const crystal = ((px * 3 + py * 7) % 11) < 2 ? 25 : 0;
const noise = (Math.random() - 0.5) * 15;
data[i] = Math.max(0, Math.min(255, 30 + crystal + noise));
data[i + 1] = Math.max(0, Math.min(255, 20 + crystal + noise));
data[i + 2] = Math.max(0, Math.min(255, 45 + crystal + noise));
data[i + 3] = 255;
}
}
ctx.putImageData(imgData, 0, 0);
const tex = new THREE.CanvasTexture(canvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
return tex;
}
// ==================== 材质管理器 ====================
class MaterialManager {
constructor() {
this.materials = {};
this._createMaterials();
}
_createMaterials() {
const grassTop = createGrassTopTexture();
const grassSide = createGrassSideTexture();
const dirtTex = createPixelTexture([140, 100, 65], 20);
const stoneTex = createPixelTexture([130, 130, 130], 25);
const woodSide = createWoodSideTexture();
const woodTop = createWoodTopTexture();
const leavesTex = createLeavesTexture();
const sandstoneTex = createPixelTexture([210, 195, 155], 15);
const goldTex = createPixelTexture([255, 215, 0], 20);
const obsidianTex = createObsidianTexture();
const glowstoneTex = createGlowstoneTexture();
const brickTex = createBrickTexture();
const planksTex = createPixelTexture([185, 145, 95], 15);
const cobbleTex = createPixelTexture([115, 115, 115], 30);
const mossyTex = createPixelTexture([100, 120, 90], 25);
const iceTex = createPixelTexture([170, 210, 230], 10);
const netherrackTex = createPixelTexture([140, 60, 55], 30);
this.materials[BLOCK_TYPES.GRASS] = [
new THREE.MeshLambertMaterial({ map: grassSide }),
new THREE.MeshLambertMaterial({ map: grassSide }),
new THREE.MeshLambertMaterial({ map: grassTop }),
new THREE.MeshLambertMaterial({ map: dirtTex }),
new THREE.MeshLambertMaterial({ map: grassSide }),
new THREE.MeshLambertMaterial({ map: grassSide }),
];
this.materials[BLOCK_TYPES.DIRT] = new THREE.MeshLambertMaterial({ map: dirtTex });
this.materials[BLOCK_TYPES.STONE] = new THREE.MeshLambertMaterial({ map: stoneTex });
this.materials[BLOCK_TYPES.WOOD] = [
new THREE.MeshLambertMaterial({ map: woodSide }),
new THREE.MeshLambertMaterial({ map: woodSide }),
new THREE.MeshLambertMaterial({ map: woodTop }),
new THREE.MeshLambertMaterial({ map: woodTop }),
new THREE.MeshLambertMaterial({ map: woodSide }),
new THREE.MeshLambertMaterial({ map: woodSide }),
];
this.materials[BLOCK_TYPES.LEAVES] = new THREE.MeshLambertMaterial({ map: leavesTex, transparent: true, opacity: 0.9 });
this.materials[BLOCK_TYPES.SANDSTONE] = new THREE.MeshLambertMaterial({ map: sandstoneTex });
this.materials[BLOCK_TYPES.GOLD] = new THREE.MeshLambertMaterial({ map: goldTex });
this.materials[BLOCK_TYPES.OBSIDIAN] = new THREE.MeshLambertMaterial({ map: obsidianTex });
this.materials[BLOCK_TYPES.GLOWSTONE] = new THREE.MeshLambertMaterial({ map: glowstoneTex, emissive: 0xffcc44, emissiveIntensity: 0.5 });
this.materials[BLOCK_TYPES.BRICK] = new THREE.MeshLambertMaterial({ map: brickTex });
this.materials[BLOCK_TYPES.PLANKS] = new THREE.MeshLambertMaterial({ map: planksTex });
this.materials[BLOCK_TYPES.COBBLESTONE] = new THREE.MeshLambertMaterial({ map: cobbleTex });
this.materials[BLOCK_TYPES.MOSSY_STONE] = new THREE.MeshLambertMaterial({ map: mossyTex });
this.materials[BLOCK_TYPES.ICE] = new THREE.MeshLambertMaterial({ map: iceTex, transparent: true, opacity: 0.85 });
this.materials[BLOCK_TYPES.NETHERRACK] = new THREE.MeshLambertMaterial({ map: netherrackTex });
}
getMaterial(type) {
return this.materials[type] || this.materials[BLOCK_TYPES.STONE];
}
}
// ==================== 世界数据 ====================
class World {
constructor() {
this.blocks = new Map();
this.chunkSize = 16;
this.worldHeight = 128;
this.noise = new PerlinNoise(42);
this.noise2 = new PerlinNoise(123);
this.noise3 = new PerlinNoise(999);
}
_key(x, y, z) { return `${x},${y},${z}`; }
getBlock(x, y, z) {
return this.blocks.get(this._key(Math.round(x), Math.round(y), Math.round(z))) || BLOCK_TYPES.AIR;
}
setBlock(x, y, z, type) {
const rx = Math.round(x), ry = Math.round(y), rz = Math.round(z);
if (ry < 0 || ry >= this.worldHeight) return;
const key = this._key(rx, ry, rz);
if (type === BLOCK_TYPES.AIR) {
this.blocks.delete(key);
} else {
this.blocks.set(key, type);
}
}
hasBlock(x, y, z) {
return this.getBlock(x, y, z) !== BLOCK_TYPES.AIR;
}
generateTerrain(cx, cz) {
const chunkX = cx * this.chunkSize;
const chunkZ = cz * this.chunkSize;
for (let x = 0; x < this.chunkSize; x++) {
for (let z = 0; z < this.chunkSize; z++) {
const wx = chunkX + x;
const wz = chunkZ + z;
const scale = 0.02;
const heightNoise = this.noise.fbm2D(wx * scale, wz * scale, 6, 0.5, 2.0);
const detailNoise = this.noise2.fbm2D(wx * scale * 2, wz * scale * 2, 3, 0.5, 2.0);
const biomeNoise = this.noise3.fbm2D(wx * 0.005, wz * 0.005, 2, 0.5, 2.0);
let height = Math.floor((heightNoise * 0.7 + detailNoise * 0.3) * 30 + 40);
height = Math.max(5, Math.min(this.worldHeight - 10, height));
const biome = biomeNoise > 0.3 ? 'desert' : (biomeNoise < -0.3 ? 'mountain' : 'plains');
for (let y = 0; y <= height; y++) {
let block;
if (y === height) {
if (biome === 'desert') block = BLOCK_TYPES.SANDSTONE;
else if (biome === 'mountain') block = y > 55 ? BLOCK_TYPES.STONE : BLOCK_TYPES.GRASS;
else block = BLOCK_TYPES.GRASS;
} else if (y >= height - 3) {
if (biome === 'desert') block = BLOCK_TYPES.SANDSTONE;
else block = BLOCK_TYPES.DIRT;
} else if (y > height - 8) {
block = BLOCK_TYPES.STONE;
} else {
const caveNoise = this.noise3.noise3D(wx * 0.05, y * 0.08, wz * 0.05);
if (caveNoise > 0.15) {
continue;
}
block = BLOCK_TYPES.STONE;
}
this.setBlock(wx, y, wz, block);
}
// 水面
if (height < 35 && biome !== 'desert') {
for (let y = height + 1; y <= 35; y++) {
if (!this.hasBlock(wx, y, wz)) {
this.setBlock(wx, y, wz, BLOCK_TYPES.ICE);
}
}
}
}
}
}
generateTree(x, y, z) {
const height = 4 + Math.floor(Math.random() * 3);
for (let i = 0; i < height; i++) {
this.setBlock(x, y + i, z, BLOCK_TYPES.WOOD);
}
for (let dx = -2; dx <= 2; dx++) {
for (let dy = 0; dy <= 2; dy++) {
for (let dz = -2; dz <= 2; dz++) {
if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) <= 3) {
if (!this.hasBlock(x + dx, y + height - 1 + dy, z + dz)) {
this.setBlock(x + dx, y + height - 1 + dy, z + dz, BLOCK_TYPES.LEAVES);
}
}
}
}
}
}
generateStructures(cx, cz) {
const chunkX = cx * this.chunkSize;
const chunkZ = cz * this.chunkSize;
// 随机数种子
const seed = cx * 73856093 ^ cz * 19349663;
const rand = () => {
let s = seed;
return () => {
s = (s * 16807 + 0) % 2147483647;
return s / 2147483647;
};
};
const rng = rand();
// 生成树木
const treeCount = Math.floor(rng() * 4);
for (let i = 0; i < treeCount; i++) {
const tx = chunkX + Math.floor(rng() * this.chunkSize);
const tz = chunkZ + Math.floor(rng() * this.chunkSize);
for (let y = this.worldHeight - 1; y >= 0; y--) {
const block = this.getBlock(tx, y, tz);
if (block === BLOCK_TYPES.GRASS) {
this.generateTree(tx, y + 1, tz);
break;
}
}
}
}
}
// ==================== 神话建筑生成器 ====================
class MythicStructureGenerator {
constructor(world) {
this.world = world;
this.generatedStructures = new Set();
}
_hashPos(x, z) { return `${x},${z}`; }
generateAncientTemple(cx, cz) {
const key = this._hashPos(cx, cz);
if (this.generatedStructures.has('temple_' + key)) return;
this.generatedStructures.add('temple_' + key);
const bx = cx * 16 + 8;
const bz = cz * 16 + 8;
let by = 0;
for (let y = 80; y >= 0; y--) {
if (this.world.hasBlock(bx, y, bz)) { by = y + 1; break; }
}
// 基座
for (let y = 0; y < 4; y++) {
for (let dx = -8 + y; dx <= 8 - y; dx++) {
for (let dz = -8 + y; dz <= 8 - y; dz++) {
this.world.setBlock(bx + dx, by + y, bz + dz, BLOCK_TYPES.SANDSTONE);
}
}
}
// 主殿
for (let y = 4; y < 10; y++) {
for (let dx = -5; dx <= 5; dx++) {
for (let dz = -5; dz <= 5; dz++) {
if (Math.abs(dx) === 5 || Math.abs(dz) === 5 || y === 9) {
this.world.setBlock(bx + dx, by + y, bz + dz, BLOCK_TYPES.BRICK);
}
}
}
}
// 柱子
for (let dy = 4; dy < 12; dy++) {
this.world.setBlock(bx - 4, by + dy, bz - 4, BLOCK_TYPES.GOLD);
this.world.setBlock(bx + 4, by + dy, bz - 4, BLOCK_TYPES.GOLD);
this.world.setBlock(bx - 4, by + dy, bz + 4, BLOCK_TYPES.GOLD);
this.world.setBlock(bx + 4, by + dy, bz + 4, BLOCK_TYPES.GOLD);
}
// 祭坛
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
this.world.setBlock(bx + dx, by + 4, bz + dz, BLOCK_TYPES.OBSIDIAN);
}
}
this.world.setBlock(bx, by + 5, bz, BLOCK_TYPES.GLOWSTONE);
// 入口
for (let dx = -1; dx <= 1; dx++) {
for (let dy = 4; dy < 8; dy++) {
this.world.setBlock(bx + dx, by + dy, bz + 5, BLOCK_TYPES.AIR);
}
}
// 装饰火炬
this.world.setBlock(bx - 3, by + 5, bz + 5, BLOCK_TYPES.GLOWSTONE);
this.world.setBlock(bx + 3, by + 5, bz + 5, BLOCK_TYPES.GLOWSTONE);
}
generateSkyTower(cx, cz) {
const key = this._hashPos(cx, cz);
if (this.generatedStructures.has('tower_' + key)) return;
this.generatedStructures.add('tower_' + key);
const bx = cx * 16 + 8;
const bz = cz * 16 + 8;
let by = 0;
for (let y = 80; y >= 0; y--) {
if (this.world.hasBlock(bx, y, bz)) { by = y + 1; break; }
}
if (by < 50) by = 50;
const height = 25 + Math.floor(Math.random() * 15);
// 塔身
for (let y = 0; y < height; y++) {
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
if (Math.abs(dx) === 2 || Math.abs(dz) === 2) {
const block = y % 5 === 0 ? BLOCK_TYPES.GLOWSTONE : BLOCK_TYPES.COBBLESTONE;
this.world.setBlock(bx + dx, by + y, bz + dz, block);
}
}
}
}
// 螺旋楼梯
for (let y = 0; y < height - 2; y++) {
const angle = y * 0.8;
const sx = Math.round(Math.cos(angle) * 1.5);
const sz = Math.round(Math.sin(angle) * 1.5);
this.world.setBlock(bx + sx, by + y, bz + sz, BLOCK_TYPES.PLANKS);
}
// 观景台
for (let dx = -4; dx <= 4; dx++) {
for (let dz = -4; dz <= 4; dz++) {
if (Math.abs(dx) === 4 || Math.abs(dz) === 4) {
this.world.setBlock(bx + dx, by + height, bz + dz, BLOCK_TYPES.BRICK);
this.world.setBlock(bx + dx, by + height + 1, bz + dz, BLOCK_TYPES.BRICK);
} else {
this.world.setBlock(bx + dx, by + height, bz + dz, BLOCK_TYPES.PLANKS);
}
}
}
// 顶部光芒
this.world.setBlock(bx, by + height + 2, bz, BLOCK_TYPES.GLOWSTONE);
}
generatePortalRuins(cx, cz) {
const key = this._hashPos(cx, cz);
if (this.generatedStructures.has('portal_' + key)) return;
this.generatedStructures.add('portal_' + key);
const bx = cx * 16 + 8;
const bz = cz * 16 + 8;
let by = 0;
for (let y = 80; y >= 0; y--) {
if (this.world.hasBlock(bx, y, bz)) { by = y + 1; break; }
}
// 门框
for (let y = 0; y < 6; y++) {
for (let x = -2; x <= 2; x++) {
if (Math.abs(x) === 2 || y === 5) {
const broken = Math.random() > 0.7;
if (!broken) {
this.world.setBlock(bx + x, by + y, bz, BLOCK_TYPES.OBSIDIAN);
}
}
}
}
// 地面符文石
for (let dx = -3; dx <= 3; dx++) {
for (let dz = -3; dz <= 3; dz++) {
if (Math.random() > 0.5) {
this.world.setBlock(bx + dx, by - 1, bz + dz, BLOCK_TYPES.MOSSY_STONE);
}
}
}
// 中心传送石
this.world.setBlock(bx, by, bz, BLOCK_TYPES.GLOWSTONE);
this.world.setBlock(bx, by + 1, bz, BLOCK_TYPES.GLOWSTONE);
}
generateDragonNest(cx, cz) {
const key = this._hashPos(cx, cz);
if (this.generatedStructures.has('nest_' + key)) return;
this.generatedStructures.add('nest_' + key);
const bx = cx * 16 + 8;
const bz = cz * 16 + 8;
let by = 0;
for (let y = 80; y >= 0; y--) {
if (this.world.hasBlock(bx, y, bz)) { by = y + 1; break; }
}
// 挖空山腰
for (let dx = -5; dx <= 5; dx++) {
for (let dy = -3; dy <= 5; dy++) {
for (let dz = -5; dz <= 5; dz++) {
if (dx * dx + dy * dy + dz * dz < 20) {
this.world.setBlock(bx + dx, by + dy, bz + dz, BLOCK_TYPES.AIR);
}
}
}
}
// 金块堆积
for (let i = 0; i < 15; i++) {
const dx = Math.floor((Math.random() - 0.5) * 6);
const dy = Math.floor((Math.random() - 0.5) * 3);
const dz = Math.floor((Math.random() - 0.5) * 6);
this.world.setBlock(bx + dx, by + dy, bz + dz, BLOCK_TYPES.GOLD);
}
// 黑曜石柱
for (let i = 0; i < 4; i++) {
const angle = (i / 4) * Math.PI * 2;
const px = Math.round(Math.cos(angle) * 4);
const pz = Math.round(Math.sin(angle) * 4);
for (let h = 0; h < 4 + Math.random() * 3; h++) {
this.world.setBlock(bx + px, by + h, bz + pz, BLOCK_TYPES.OBSIDIAN);
}
}
// 中心祭坛
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
this.world.setBlock(bx + dx, by - 1, bz + dz, BLOCK_TYPES.OBSIDIAN);
}
}
}
generateFloatingIsland(cx, cz) {
const key = this._hashPos(cx, cz);
if (this.generatedStructures.has('island_' + key)) return;
this.generatedStructures.add('island_' + key);
const bx = cx * 16 + 8;
const bz = cz * 16 + 8;
const by = 70 + Math.floor(Math.random() * 20);
// 岛屿主体
for (let dx = -6; dx <= 6; dx++) {
for (let dy = -3; dy <= 2; dy++) {
for (let dz = -6; dz <= 6; dz++) {
const dist = dx * dx + dy * dy * 2 + dz * dz;
if (dist < 30) {
let block = BLOCK_TYPES.DIRT;
if (dy === 2) block = BLOCK_TYPES.GRASS;
else if (dy < -1) block = BLOCK_TYPES.STONE;
this.world.setBlock(bx + dx, by + dy, bz + dz, block);
}
}
}
}
// 树木
this.world.setBlock(bx - 3, by + 3, bz - 2, BLOCK_TYPES.WOOD);
this.world.setBlock(bx - 3, by + 4, bz - 2, BLOCK_TYPES.WOOD);
this.world.setBlock(bx - 3, by + 5, bz - 2, BLOCK_TYPES.WOOD);
for (let dx = -4; dx <= -2; dx++) {
for (let dy = 4; dy <= 6; dy++) {
for (let dz = -3; dz <= -1; dz++) {
this.world.setBlock(bx + dx, by + dy, bz + dz, BLOCK_TYPES.LEAVES);
}
}
}
// 发光石装饰
this.world.setBlock(bx + 4, by + 1, bz + 3, BLOCK_TYPES.GLOWSTONE);
this.world.setBlock(bx - 4, by + 1, bz - 3, BLOCK_TYPES.GLOWSTONE);
}
generateUndergroundMaze(cx, cz) {
const key = this._hashPos(cx, cz);
if (this.generatedStructures.has('maze_' + key)) return;
this.generatedStructures.add('maze_' + key);
const bx = cx * 16 + 8;
const bz = cz * 16 + 8;
const by = 15;
// 简单迷宫 - 十字通道
for (let i = -6; i <= 6; i++) {
for (let w = -1; w <= 1; w++) {
this.world.setBlock(bx + i, by, bz + w, BLOCK_TYPES.AIR);
this.world.setBlock(bx + i, by + 1, bz + w, BLOCK_TYPES.AIR);
this.world.setBlock(bx + w, by, bz + i, BLOCK_TYPES.AIR);
this.world.setBlock(bx + w, by + 1, bz + i, BLOCK_TYPES.AIR);
}
}
// 墙壁
for (let dx = -7; dx <= 7; dx++) {
for (let dz = -7; dz <= 7; dz++) {
if (Math.abs(dx) > 1 && Math.abs(dz) > 1) {
if (Math.random() > 0.3) {
this.world.setBlock(bx + dx, by, bz + dz, BLOCK_TYPES.MOSSY_STONE);
this.world.setBlock(bx + dx, by + 1, bz + dz, BLOCK_TYPES.MOSSY_STONE);
this.world.setBlock(bx + dx, by + 2, bz + dz, BLOCK_TYPES.MOSSY_STONE);
}
}
}
}
// 中央宝藏室
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
this.world.setBlock(bx + dx, by, bz + dz, BLOCK_TYPES.GOLD);
this.world.setBlock(bx + dx, by + 1, bz + dz, BLOCK_TYPES.AIR);
}
}
this.world.setBlock(bx, by + 1, bz, BLOCK_TYPES.GLOWSTONE);
}
generateForChunk(cx, cz) {
const hash = Math.abs(cx * 73856093 ^ cz * 19349663) % 1000;
const rand = hash / 1000;
if (rand < 0.05) this.generateAncientTemple(cx, cz);
else if (rand < 0.10) this.generateSkyTower(cx, cz);
else if (rand < 0.15) this.generatePortalRuins(cx, cz);
else if (rand < 0.20) this.generateDragonNest(cx, cz);
else if (rand < 0.25) this.generateFloatingIsland(cx, cz);
else if (rand < 0.30) this.generateUndergroundMaze(cx, cz);
}
}
// ==================== 渲染器 ====================
class ChunkRenderer {
constructor(scene, materialManager, world) {
this.scene = scene;
this.materialManager = materialManager;
this.world = world;
this.chunkMeshes = new Map();
this.geometry = new THREE.BoxGeometry(1, 1, 1);
}
_chunkKey(cx, cz) { return `${cx},${cz}`; }
updateChunk(cx, cz) {
const key = this._chunkKey(cx, cz);
const existing = this.chunkMeshes.get(key);
if (existing) {
existing.forEach(mesh => {
this.scene.remove(mesh);
mesh.geometry.dispose();
});
}
const chunkX = cx * 16;
const chunkZ = cz * 16;
const blockCounts = {};
const blockPositions = {};
for (let x = 0; x < 16; x++) {
for (let z = 0; z < 16; z++) {
for (let y = 0; y < 128; y++) {
const block = this.world.getBlock(chunkX + x, y, chunkZ + z);
if (block === BLOCK_TYPES.AIR) continue;
// 剔除被完全遮挡的方块
const wx = chunkX + x, wy = y, wz = chunkZ + z;
const hidden = this.world.hasBlock(wx + 1, wy, wz) &&
this.world.hasBlock(wx - 1, wy, wz) &&
this.world.hasBlock(wx, wy + 1, wz) &&
this.world.hasBlock(wx, wy - 1, wz) &&
this.world.hasBlock(wx, wy, wz + 1) &&
this.world.hasBlock(wx, wy, wz - 1);
if (hidden) continue;
if (!blockCounts[block]) {
blockCounts[block] = 0;
blockPositions[block] = [];
}
blockCounts[block]++;
blockPositions[block].push({ x: wx, y: wy, z: wz });
}
}
}
const meshes = [];
for (const [blockType, count] of Object.entries(blockCounts)) {
const type = parseInt(blockType);
const material = this.materialManager.getMaterial(type);
const positions = blockPositions[type];
const instancedMesh = new THREE.InstancedMesh(this.geometry, material, count);
instancedMesh.castShadow = true;
instancedMesh.receiveShadow = true;
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
const pos = positions[i];
dummy.position.set(pos.x, pos.y, pos.z);
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
this.scene.add(instancedMesh);
meshes.push(instancedMesh);
}
this.chunkMeshes.set(key, meshes);
}
removeChunk(cx, cz) {
const key = this._chunkKey(cx, cz);
const meshes = this.chunkMeshes.get(key);
if (meshes) {
meshes.forEach(mesh => {
this.scene.remove(mesh);
mesh.geometry.dispose();
});
this.chunkMeshes.delete(key);
}
}
}
// ==================== 第一人称控制器 ====================
class FirstPersonController {
constructor(camera, world, domElement) {
this.camera = camera;
this.world = world;
this.domElement = domElement;
this.enabled = false;
this.moveForward = false;
this.moveBackward = false;
this.moveLeft = false;
this.moveRight = false;
this.jump = false;
this.sneak = false;
this.velocity = new THREE.Vector3();
this.direction = new THREE.Vector3();
this.playerHeight = 1.7;
this.playerRadius = 0.3;
this.speed = 6.0;
this.jumpSpeed = 8.0;
this.gravity = 22.0;
this.onGround = false;
this.pitch = 0;
this.yaw = 0;
this._setupEvents();
}
_setupEvents() {
document.addEventListener('keydown', (e) => this._onKeyDown(e));
document.addEventListener('keyup', (e) => this._onKeyUp(e));
document.addEventListener('mousemove', (e) => this._onMouseMove(e));
this.domElement.addEventListener('click', () => {
if (!this.enabled) {
this.domElement.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
this.enabled = document.pointerLockElement === this.domElement;
});
}
_onKeyDown(e) {
switch (e.code) {
case 'KeyW': this.moveForward = true; break;
case 'KeyS': this.moveBackward = true; break;
case 'KeyA': this.moveLeft = true; break;
case 'KeyD': this.moveRight = true; break;
case 'Space': if (this.onGround) { this.jump = true; this.velocity.y = this.jumpSpeed; } break;
case 'ShiftLeft': case 'ShiftRight': this.sneak = true; break;
}
}
_onKeyUp(e) {
switch (e.code) {
case 'KeyW': this.moveForward = false; break;
case 'KeyS': this.moveBackward = false; break;
case 'KeyA': this.moveLeft = false; break;
case 'KeyD': this.moveRight = false; break;
case 'ShiftLeft': case 'ShiftRight': this.sneak = false; break;
}
}
_onMouseMove(e) {
if (!this.enabled) return;
const sensitivity = 0.002;
this.yaw -= e.movementX * sensitivity;
this.pitch -= e.movementY * sensitivity;
this.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.pitch));
}
update(dt) {
if (!this.enabled) return;
// 应用重力
this.velocity.y -= this.gravity * dt;
// 移动方向
this.direction.z = Number(this.moveForward) - Number(this.moveBackward);
this.direction.x = Number(this.moveRight) - Number(this.moveLeft);
this.direction.normalize();
const speed = this.sneak ? this.speed * 0.3 : this.speed;
if (this.moveForward || this.moveBackward) {
this.velocity.z = -this.direction.z * speed;
} else {
this.velocity.z *= 0.9;
}
if (this.moveLeft || this.moveRight) {
this.velocity.x = -this.direction.x * speed;
} else {
this.velocity.x *= 0.9;
}
// 应用旋转到速度
const forward = new THREE.Vector3(0, 0, -1);
forward.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.yaw);
const right = new THREE.Vector3(1, 0, 0);
right.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.yaw);
const moveX = forward.x * this.velocity.z + right.x * this.velocity.x;
const moveZ = forward.z * this.velocity.z + right.z * this.velocity.x;
const pos = this.camera.position.clone();
// Y轴移动和碰撞
pos.y += this.velocity.y * dt;
this.onGround = false;
if (this._checkCollision(pos.x, pos.y - this.playerHeight, pos.z)) {
if (this.velocity.y < 0) {
pos.y = Math.ceil(pos.y - this.playerHeight) + this.playerHeight;
this.velocity.y = 0;
this.onGround = true;
this.jump = false;
}
}
if (this._checkCollision(pos.x, pos.y, pos.z)) {
if (this.velocity.y > 0) {
pos.y = Math.floor(pos.y) - 0.01;
this.velocity.y = 0;
}
}
// X轴移动和碰撞
const newX = pos.x + moveX * dt;
if (!this._checkCollision(newX, pos.y - this.playerHeight * 0.5, pos.z) &&
!this._checkCollision(newX, pos.y - this.playerHeight + 0.1, pos.z)) {
pos.x = newX;
} else {
this.velocity.x = 0;
}
// Z轴移动和碰撞
const newZ = pos.z + moveZ * dt;
if (!this._checkCollision(pos.x, pos.y - this.playerHeight * 0.5, newZ) &&
!this._checkCollision(pos.x, pos.y - this.playerHeight + 0.1, newZ)) {
pos.z = newZ;
} else {
this.velocity.z = 0;
}
// 防止掉落
if (pos.y < -5) {
pos.y = 80;
this.velocity.y = 0;
}
this.camera.position.copy(pos);
this.camera.rotation.order = 'YXZ';
this.camera.rotation.y = this.yaw;
this.camera.rotation.x = this.pitch;
}
_checkCollision(x, y, z) {
const minX = Math.floor(x - this.playerRadius);
const maxX = Math.floor(x + this.playerRadius);
const minY = Math.floor(y);
const maxY = Math.floor(y + this.playerHeight);
const minZ = Math.floor(z - this.playerRadius);
const maxZ = Math.floor(z + this.playerRadius);
for (let bx = minX; bx <= maxX; bx++) {
for (let by = minY; by <= maxY; by++) {
for (let bz = minZ; bz <= maxZ; bz++) {
if (this.world.hasBlock(bx, by, bz)) return true;
}
}
}
return false;
}
}
// ==================== 方块交互 ====================
class BlockInteraction {
constructor(camera, world, scene, chunkRenderer) {
this.camera = camera;
this.world = world;
this.scene = scene;
this.chunkRenderer = chunkRenderer;
this.reach = 6;
this.selectedBlock = BLOCK_TYPES.GRASS;
this.highlightMesh = null;
this._createHighlight();
document.addEventListener('mousedown', (e) => this._onMouseDown(e));
document.addEventListener('keydown', (e) => this._onKeyDown(e));
}
_createHighlight() {
const geo = new THREE.BoxGeometry(1.01, 1.01, 1.01);
const mat = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true, transparent: true, opacity: 0.5 });
this.highlightMesh = new THREE.Mesh(geo, mat);
this.highlightMesh.visible = false;
this.scene.add(this.highlightMesh);
}
_onMouseDown(e) {
if (!document.pointerLockElement) return;
if (e.button === 0) this._breakBlock();
else if (e.button === 2) this._placeBlock();
}
_onKeyDown(e) {
const keys = ['1','2','3','4','5','6','7','8','9'];
const idx = keys.indexOf(e.key);
if (idx >= 0) {
const types = [BLOCK_TYPES.GRASS, BLOCK_TYPES.DIRT, BLOCK_TYPES.STONE, BLOCK_TYPES.WOOD,
BLOCK_TYPES.BRICK, BLOCK_TYPES.GLOWSTONE, BLOCK_TYPES.OBSIDIAN, BLOCK_TYPES.GOLD, BLOCK_TYPES.PLANKS];
if (idx < types.length) {
this.selectedBlock = types[idx];
updateHotbar(idx);
}
}
}
_getTargetBlock() {
const origin = this.camera.position.clone();
const direction = new THREE.Vector3(0, 0, -1);
direction.applyQuaternion(this.camera.quaternion);
const step = 0.05;
for (let d = 0; d < this.reach; d += step) {
const pos = origin.clone().add(direction.clone().multiplyScalar(d));
const bx = Math.round(pos.x);
const by = Math.round(pos.y);
const bz = Math.round(pos.z);
if (this.world.hasBlock(bx, by, bz)) {
const prevPos = origin.clone().add(direction.clone().multiplyScalar(Math.max(0, d - step)));
const pbx = Math.round(prevPos.x);
const pby = Math.round(prevPos.y);
const pbz = Math.round(prevPos.z);
return {
block: { x: bx, y: by, z: bz },
placePos: { x: pbx, y: pby, z: pbz },
type: this.world.getBlock(bx, by, bz)
};
}
}
return null;
}
_breakBlock() {
const target = this._getTargetBlock();
if (target) {
this.world.setBlock(target.block.x, target.block.y, target.block.z, BLOCK_TYPES.AIR);
this._updateChunks(target.block.x, target.block.z);
}
}
_placeBlock() {
const target = this._getTargetBlock();
if (target) {
const pp = target.placePos;
// 避免在玩家身上放置
const px = this.camera.position.x;
const py = this.camera.position.y - 0.85;
const pz = this.camera.position.z;
if (Math.abs(pp.x - Math.round(px)) <= 0 &&
Math.abs(pp.y - Math.round(py)) <= 0 &&
Math.abs(pp.z - Math.round(pz)) <= 0) return;
if (Math.abs(pp.x - Math.round(px)) <= 0 &&
Math.abs(pp.y - Math.round(py + 1)) <= 0 &&
Math.abs(pp.z - Math.round(pz)) <= 0) return;
this.world.setBlock(pp.x, pp.y, pp.z, this.selectedBlock);
this._updateChunks(pp.x, pp.z);
}
}
_updateChunks(x, z) {
const cx = Math.floor(x / 16);
const cz = Math.floor(z / 16);
this.chunkRenderer.updateChunk(cx, cz);
// 可能影响到相邻chunk
if (x % 16 === 0) this.chunkRenderer.updateChunk(cx - 1, cz);
if (x % 16 === 15) this.chunkRenderer.updateChunk(cx + 1, cz);
if (z % 16 === 0) this.chunkRenderer.updateChunk(cx, cz - 1);
if (z % 16 === 15) this.chunkRenderer.updateChunk(cx, cz + 1);
}
update() {
const target = this._getTargetBlock();
if (target) {
this.highlightMesh.position.set(target.block.x, target.block.y, target.block.z);
this.highlightMesh.visible = true;
document.getElementById('target').textContent = BLOCK_NAMES[target.type] || '未知';
} else {
this.highlightMesh.visible = false;
document.getElementById('target').textContent = '无';
}
}
}
// ==================== 主程序 ====================
let scene, camera, renderer, world, materialManager, chunkRenderer, controller, blockInteraction;
let loadedChunks = new Set();
let renderDistance = 6;
function init() {
// 场景
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 20, renderDistance * 16);
// 相机
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 70, 0);
// 渲染器
renderer = new THREE.WebGLRenderer({ antialias: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// 光照
const ambientLight = new THREE.AmbientLight(0x666688, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffeedd, 0.9);
dirLight.position.set(50, 100, 50);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 300;
dirLight.shadow.camera.left = -100;
dirLight.shadow.camera.right = 100;
dirLight.shadow.camera.top = 100;
dirLight.shadow.camera.bottom = -100;
scene.add(dirLight);
// 世界
world = new World();
materialManager = new MaterialManager();
chunkRenderer = new ChunkRenderer(scene, materialManager, world);
// 控制器
controller = new FirstPersonController(camera, world, renderer.domElement);
// 方块交互
blockInteraction = new BlockInteraction(camera, world, scene, chunkRenderer);
// 初始生成
generateInitialWorld();
// 窗口调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 禁用右键菜单
document.addEventListener('contextmenu', e => e.preventDefault());
}
let structureGen;
function generateInitialWorld() {
const loadingText = document.getElementById('loading-text');
const loadingFill = document.getElementById('loading-fill');
const total = (renderDistance * 2 + 1) ** 2;
let done = 0;
structureGen = new MythicStructureGenerator(world);
for (let cx = -renderDistance; cx <= renderDistance; cx++) {
for (let cz = -renderDistance; cz <= renderDistance; cz++) {
world.generateTerrain(cx, cz);
world.generateStructures(cx, cz);
structureGen.generateForChunk(cx, cz);
done++;
}
}
// 渲染所有chunk
for (let cx = -renderDistance; cx <= renderDistance; cx++) {
for (let cz = -renderDistance; cz <= renderDistance; cz++) {
chunkRenderer.updateChunk(cx, cz);
loadedChunks.add(`${cx},${cz}`);
const progress = (done++ / total) * 50 + 50;
loadingFill.style.width = progress + '%';
}
}
// 找到地面高度
let spawnY = 80;
for (let y = 80; y >= 0; y--) {
if (world.hasBlock(0, y, 0)) {
spawnY = y + 2;
break;
}
}
camera.position.set(0, spawnY, 0);
// 完成加载
setTimeout(() => {
document.getElementById('loading').style.opacity = '0';
setTimeout(() => {
document.getElementById('loading').style.display = 'none';
}, 500);
}, 500);
}
function updateChunks() {
const px = camera.position.x;
const pz = camera.position.z;
const pcx = Math.floor(px / 16);
const pcz = Math.floor(pz / 16);
const needed = new Set();
for (let dx = -renderDistance; dx <= renderDistance; dx++) {
for (let dz = -renderDistance; dz <= renderDistance; dz++) {
const cx = pcx + dx;
const cz = pcz + dz;
const key = `${cx},${cz}`;
needed.add(key);
if (!loadedChunks.has(key)) {
world.generateTerrain(cx, cz);
world.generateStructures(cx, cz);
if (structureGen) structureGen.generateForChunk(cx, cz);
chunkRenderer.updateChunk(cx, cz);
loadedChunks.add(key);
}
}
}
// 移除远处chunk
for (const key of loadedChunks) {
if (!needed.has(key)) {
const [cx, cz] = key.split(',').map(Number);
chunkRenderer.removeChunk(cx, cz);
loadedChunks.delete(key);
}
}
}
// ==================== 快捷栏 ====================
const hotbarSlots = [
{ type: BLOCK_TYPES.GRASS, color: '#5c9e3a', name: '草地' },
{ type: BLOCK_TYPES.DIRT, color: '#8c6239', name: '泥土' },
{ type: BLOCK_TYPES.STONE, color: '#7a7a7a', name: '石头' },
{ type: BLOCK_TYPES.WOOD, color: '#6b4226', name: '原木' },
{ type: BLOCK_TYPES.BRICK, color: '#a04030', name: '红砖' },
{ type: BLOCK_TYPES.GLOWSTONE, color: '#f0d040', name: '发光石' },
{ type: BLOCK_TYPES.OBSIDIAN, color: '#1a0a2e', name: '黑曜石' },
{ type: BLOCK_TYPES.GOLD, color: '#ffd700', name: '金块' },
{ type: BLOCK_TYPES.PLANKS, color: '#b8955f', name: '木板' },
];
function createHotbar() {
const hotbar = document.getElementById('hotbar');
hotbarSlots.forEach((slot, i) => {
const el = document.createElement('div');
el.className = 'slot' + (i === 0 ? ' active' : '');
el.innerHTML = `
<span class="slot-key">${i + 1}</span>
<div class="slot-preview" style="background:${slot.color}"></div>
<span class="slot-name">${slot.name}</span>
`;
el.addEventListener('click', () => {
blockInteraction.selectedBlock = slot.type;
updateHotbar(i);
});
hotbar.appendChild(el);
});
}
function updateHotbar(activeIdx) {
const slots = document.querySelectorAll('.slot');
slots.forEach((s, i) => {
s.classList.toggle('active', i === activeIdx);
});
}
// ==================== 游戏循环 ====================
const clock = new THREE.Clock();
let lastChunkUpdate = 0;
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
controller.update(dt);
blockInteraction.update();
// 更新坐标显示
const p = camera.position;
document.getElementById('pos').textContent = `${Math.round(p.x)}, ${Math.round(p.y)}, ${Math.round(p.z)}`;
document.getElementById('rot').textContent = `${Math.round(THREE.MathUtils.radToDeg(controller.yaw) % 360)}°`;
// 定期更新chunk
lastChunkUpdate += dt;
if (lastChunkUpdate > 0.5) {
updateChunks();
lastChunkUpdate = 0;
}
renderer.render(scene, camera);
}
// ==================== 启动 ====================
createHotbar();
init();
animate();
// 开始按钮
document.getElementById('startBtn').addEventListener('click', () => {
document.getElementById('overlay').style.display = 'none';
renderer.domElement.requestPointerLock();
});
</script>
</body>
</html>