Files
nagi-bench/outputs/gpt-5-4/codex-xhigh/mythos-craft.html
T
2026-06-14 01:20:59 +01:00

1303 lines
58 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>神话方界</title>
<style>
:root { color-scheme: dark; font-family: Inter, "PingFang SC", "Microsoft YaHei", sans-serif; }
* { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: #0f172a; }
canvas { display: block; }
#hud { position: fixed; inset: 0; z-index: 10; pointer-events: none; color: #f8fafc; user-select: none; }
#overlay {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
background:
radial-gradient(circle at top, rgba(56, 189, 248, 0.16), transparent 42%),
linear-gradient(180deg, rgba(15, 23, 42, 0.2), rgba(15, 23, 42, 0.85));
transition: opacity 180ms ease; pointer-events: auto;
}
#overlay.hidden { opacity: 0; visibility: hidden; pointer-events: none; }
#overlayCard {
width: min(92vw, 560px); padding: 28px 30px; border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 8px;
background: rgba(15, 23, 42, 0.74); backdrop-filter: blur(12px); box-shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
}
#overlay h1 { margin: 0 0 8px; font-size: 34px; line-height: 1.05; font-weight: 800; }
#overlay p { margin: 0; color: rgba(248, 250, 252, 0.86); line-height: 1.6; font-size: 15px; }
#overlay .hint { margin-top: 18px; font-size: 13px; color: rgba(191, 219, 254, 0.9); }
#crosshair { position: absolute; left: 50%; top: 50%; width: 24px; height: 24px; transform: translate(-50%, -50%); opacity: 0.92; }
#crosshair::before, #crosshair::after {
content: ""; position: absolute; background: rgba(255, 255, 255, 0.92); box-shadow: 0 0 6px rgba(0, 0, 0, 0.45);
}
#crosshair::before { width: 2px; height: 24px; left: 11px; top: 0; }
#crosshair::after { width: 24px; height: 2px; left: 0; top: 11px; }
#topbar { position: absolute; inset: 18px 18px auto; display: grid; gap: 10px; justify-items: start; }
.panel {
display: inline-flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px; background: rgba(15, 23, 42, 0.5); backdrop-filter: blur(10px); font-size: 13px; line-height: 1.3;
color: rgba(241, 245, 249, 0.92); max-width: min(540px, calc(100vw - 36px));
}
#areaBanner {
position: absolute; left: 50%; top: 28px; transform: translateX(-50%); padding: 10px 16px; border-radius: 8px;
background: rgba(2, 8, 23, 0.6); border: 1px solid rgba(255, 255, 255, 0.12); font-size: 14px; opacity: 0;
transition: opacity 220ms ease; white-space: nowrap;
}
#areaBanner.visible { opacity: 1; }
#hotbar {
position: absolute; left: 50%; bottom: 26px; transform: translateX(-50%);
display: grid; grid-template-columns: repeat(6, 58px); gap: 8px;
}
.slot {
width: 58px; height: 58px; border: 2px solid rgba(255, 255, 255, 0.18); border-radius: 8px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.02)),
rgba(15, 23, 42, 0.58);
backdrop-filter: blur(12px); overflow: hidden; position: relative; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.slot.active {
border-color: rgba(250, 204, 21, 0.96);
box-shadow: 0 0 0 1px rgba(250, 204, 21, 0.55), 0 8px 24px rgba(250, 204, 21, 0.18);
}
.slotTexture {
position: absolute; inset: 6px; border-radius: 4px; background-size: cover; image-rendering: pixelated;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}
.slotKey {
position: absolute; right: 4px; bottom: 3px; font-size: 11px; color: rgba(255, 255, 255, 0.85);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
}
#status {
position: absolute; right: 18px; bottom: 18px; display: grid; gap: 8px; justify-items: end;
max-width: min(340px, calc(100vw - 36px));
}
#status .panel { justify-content: flex-end; text-align: right; }
@media (max-width: 720px) {
#overlay h1 { font-size: 28px; }
#hotbar { grid-template-columns: repeat(6, minmax(42px, 1fr)); width: calc(100vw - 24px); }
.slot { width: auto; aspect-ratio: 1; }
}
</style>
</head>
<body>
<div id="hud">
<div id="overlay">
<div id="overlayCard">
<h1>神话方界</h1>
<p>柏林噪声分块地形、第一人称方块交互、以及内置的神话幻想遗迹群都在这个单文件世界里。</p>
<p class="hint">点击开始 | WASD 移动 | 鼠标环视 | 空格跳跃 | 左键破坏 | 右键放置 | 滚轮或数字键切换方块 | ESC 释放鼠标</p>
</div>
</div>
<div id="areaBanner"></div>
<div id="topbar">
<div id="info" class="panel">生成中…</div>
<div class="panel">世界中包含 Asterion Citadel、Elderbloom、Dragonfall Causeway、Moonwell Steps、Skyshard Reach 等大型神话建筑群,并继续向远方程序延展。</div>
</div>
<div id="crosshair"></div>
<div id="hotbar"></div>
<div id="status">
<div id="selectedBlock" class="panel">当前方块</div>
<div id="coords" class="panel">坐标</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.159.0/build/three.min.js"></script>
<script>
(() => {
if (!window.THREE) {
document.getElementById("overlayCard").innerHTML =
"<h1>Three.js 加载失败</h1><p>这个单文件版本依赖浏览器加载 CDN 上的 Three.js。请联网打开,或在当前目录用本地 HTTP 服务访问这个 HTML。</p>";
return;
}
const overlay = document.getElementById("overlay");
const infoEl = document.getElementById("info");
const coordsEl = document.getElementById("coords");
const selectedBlockEl = document.getElementById("selectedBlock");
const hotbarEl = document.getElementById("hotbar");
const areaBannerEl = document.getElementById("areaBanner");
const BLOCK = {
AIR: 0, GRASS: 1, DIRT: 2, STONE: 3, MARBLE: 4, OBSIDIAN: 5,
GLOW: 6, BARK: 7, LEAVES: 8, CRYSTAL: 9, BONE: 10, RUNESTONE: 11, MOSS: 12
};
const CHUNK_SIZE = 16;
const WORLD_HEIGHT = 84;
const RENDER_DISTANCE = 3;
const MAX_INTERACT_DISTANCE = 8;
const TAU = Math.PI * 2;
const PLACEABLE_BLOCKS = [BLOCK.DIRT, BLOCK.GRASS, BLOCK.STONE, BLOCK.MARBLE, BLOCK.OBSIDIAN, BLOCK.CRYSTAL];
const LANDMARK_RADII = {
citadel: 34, worldTree: 38, dragonBridge: 48, moonTemple: 30,
skyArchipelago: 44, runeGate: 26, crystalGarden: 34
};
const BLOCK_INFO = {
[BLOCK.AIR]: { name: "空气", solid: false, faces: { top: -1, side: -1, bottom: -1 }, tint: [1, 1, 1] },
[BLOCK.GRASS]: { name: "草地", solid: true, faces: { top: 0, side: 1, bottom: 2 }, tint: [1, 1, 1] },
[BLOCK.DIRT]: { name: "泥土", solid: true, faces: { top: 2, side: 2, bottom: 2 }, tint: [1, 1, 1] },
[BLOCK.STONE]: { name: "石头", solid: true, faces: { top: 3, side: 3, bottom: 3 }, tint: [1, 1, 1] },
[BLOCK.MARBLE]: { name: "月白石", solid: true, faces: { top: 4, side: 4, bottom: 4 }, tint: [1, 1, 1] },
[BLOCK.OBSIDIAN]: { name: "黑曜石", solid: true, faces: { top: 5, side: 5, bottom: 5 }, tint: [0.95, 0.95, 1] },
[BLOCK.GLOW]: { name: "辉光石", solid: true, faces: { top: 6, side: 6, bottom: 6 }, tint: [1, 1, 1] },
[BLOCK.BARK]: { name: "古树木", solid: true, faces: { top: 7, side: 7, bottom: 7 }, tint: [1, 1, 1] },
[BLOCK.LEAVES]: { name: "灵叶", solid: true, faces: { top: 8, side: 8, bottom: 8 }, tint: [1, 1, 1] },
[BLOCK.CRYSTAL]: { name: "星晶", solid: true, faces: { top: 9, side: 9, bottom: 9 }, tint: [1, 1, 1] },
[BLOCK.BONE]: { name: "龙骨", solid: true, faces: { top: 10, side: 10, bottom: 10 }, tint: [1, 1, 1] },
[BLOCK.RUNESTONE]: { name: "符文石", solid: true, faces: { top: 11, side: 11, bottom: 11 }, tint: [1, 1, 1] },
[BLOCK.MOSS]: { name: "苔石", solid: true, faces: { top: 12, side: 12, bottom: 12 }, tint: [1, 1, 1] }
};
const AREA_PREFIXES = ["苍辉", "月陨", "星泪", "黑曜", "雾银", "龙眠", "天穹", "秘林", "霜灯", "神铸"];
const AREA_SUFFIXES = {
skyArchipelago: ["天岛群", "浮空坠域", "星幕裂屿"],
crystalGarden: ["晶簇庭", "星晶林", "辉棱峡湾"],
moonTemple: ["月坛", "阶神殿", "白石圣阶"],
runeGate: ["符门", "古门环", "裂环遗址"],
worldTree: ["灵树庭", "古树王座", "树冠圣域"],
dragonBridge: ["龙骸道", "骨桥", "苍龙关"],
citadel: ["星冠堡", "黑曜城", "神铸城砦"]
};
const faceDefinitions = [
{ dir: [1, 0, 0], shade: 0.84, corners: [[1,0,0],[1,1,0],[1,1,1],[1,0,1]] },
{ dir: [-1, 0, 0], shade: 0.66, corners: [[0,0,1],[0,1,1],[0,1,0],[0,0,0]] },
{ dir: [0, 1, 0], shade: 1.0, corners: [[0,1,1],[1,1,1],[1,1,0],[0,1,0]] },
{ dir: [0, -1, 0], shade: 0.54, corners: [[0,0,0],[1,0,0],[1,0,1],[0,0,1]] },
{ dir: [0, 0, 1], shade: 0.78, corners: [[1,0,1],[1,1,1],[0,1,1],[0,0,1]] },
{ dir: [0, 0, -1], shade: 0.7, corners: [[0,0,0],[0,1,0],[1,1,0],[1,0,0]] }
];
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x7cc7ff);
scene.fog = new THREE.Fog(0x7cc7ff, 36, 180);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 400);
const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: "high-performance" });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
const atlasTexture = createTextureAtlas();
atlasTexture.colorSpace = THREE.SRGBColorSpace;
atlasTexture.magFilter = THREE.NearestFilter;
atlasTexture.minFilter = THREE.NearestFilter;
atlasTexture.generateMipmaps = false;
const voxelMaterial = new THREE.MeshLambertMaterial({ map: atlasTexture, vertexColors: true });
const hemiLight = new THREE.HemisphereLight(0xcff6ff, 0x17202f, 0.9);
const sunLight = new THREE.DirectionalLight(0xfff0ca, 1.1);
sunLight.position.set(55, 72, 18);
scene.add(hemiLight, sunLight);
const selector = new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.BoxGeometry(1.02, 1.02, 1.02)),
new THREE.LineBasicMaterial({ color: 0xffffff })
);
selector.visible = false;
scene.add(selector);
const player = {
position: new THREE.Vector3(6, 24, 20),
velocityY: 0,
yaw: Math.PI,
pitch: -0.15,
onGround: false,
eyeHeight: 1.68,
speed: 6.2,
sprint: 8.3
};
const keyState = {};
const chunks = new Map();
const chunkQueue = [];
const regionLandmarkCache = new Map();
const edits = new Map();
let selectedSlot = 0;
let currentAreaName = "";
let lastChunkRequestKey = "";
let lastFrame = performance.now();
let dayClock = 0;
let activeTarget = null;
const fixedLandmarks = [
createLandmark("citadel", 0, 0, "Asterion Citadel"),
createLandmark("worldTree", 58, -42, "Elderbloom"),
createLandmark("dragonBridge", -76, 30, "Dragonfall Causeway"),
createLandmark("moonTemple", 72, 74, "Moonwell Steps"),
createLandmark("skyArchipelago", -96, -68, "Skyshard Reach"),
createLandmark("runeGate", 110, -92, "Gate of Nhalor")
];
player.position.set(6, getGroundHeight(6, 20) + 4.2, 20);
camera.rotation.order = "YXZ";
initHotbar();
updateHotbar();
updateInfo();
document.addEventListener("contextmenu", (event) => event.preventDefault());
overlay.addEventListener("click", () => renderer.domElement.requestPointerLock());
document.addEventListener("pointerlockchange", () => {
overlay.classList.toggle("hidden", document.pointerLockElement === renderer.domElement);
});
document.addEventListener("mousemove", (event) => {
if (document.pointerLockElement !== renderer.domElement) return;
player.yaw -= event.movementX * 0.0022;
player.pitch -= event.movementY * 0.0022;
player.pitch = clamp(player.pitch, -Math.PI / 2 + 0.02, Math.PI / 2 - 0.02);
});
window.addEventListener("keydown", (event) => {
keyState[event.code] = true;
if (event.code === "Space" || event.code.startsWith("Digit")) event.preventDefault();
const digitMatch = event.code.match(/^Digit([1-6])$/);
if (digitMatch) {
selectedSlot = Number(digitMatch[1]) - 1;
updateHotbar();
}
});
window.addEventListener("keyup", (event) => { keyState[event.code] = false; });
window.addEventListener("wheel", (event) => {
if (document.pointerLockElement !== renderer.domElement) return;
selectedSlot = (selectedSlot + (event.deltaY > 0 ? 1 : -1) + PLACEABLE_BLOCKS.length) % PLACEABLE_BLOCKS.length;
updateHotbar();
}, { passive: true });
renderer.domElement.addEventListener("mousedown", (event) => {
if (document.pointerLockElement !== renderer.domElement) {
renderer.domElement.requestPointerLock();
return;
}
if (event.button === 0) destroyTargetBlock();
else if (event.button === 2) placeTargetBlock();
});
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
});
ensureChunksAroundPlayer(true);
requestAnimationFrame(loop);
function loop(now) {
const dt = Math.min(0.05, (now - lastFrame) / 1000 || 0.016);
lastFrame = now;
dayClock += dt * 0.07;
updateMovement(dt);
updateCamera();
ensureChunksAroundPlayer(false);
processChunkQueue(2);
updateTargeting();
updateSky();
updateInfo();
renderer.render(scene, camera);
requestAnimationFrame(loop);
}
function updateMovement(dt) {
const moveSpeed = keyState.ShiftLeft || keyState.ShiftRight ? player.sprint : player.speed;
const moveForward = (keyState.KeyW ? 1 : 0) - (keyState.KeyS ? 1 : 0);
const moveSide = (keyState.KeyD ? 1 : 0) - (keyState.KeyA ? 1 : 0);
const moveVec = new THREE.Vector3();
if (moveForward || moveSide) {
const forward = new THREE.Vector3(Math.sin(player.yaw), 0, Math.cos(player.yaw));
const right = new THREE.Vector3(Math.cos(player.yaw), 0, -Math.sin(player.yaw));
moveVec.addScaledVector(forward, moveForward);
moveVec.addScaledVector(right, moveSide);
moveVec.normalize().multiplyScalar(moveSpeed * dt);
movePlayerHorizontally(moveVec.x, moveVec.z);
}
if (player.onGround && keyState.Space) {
player.velocityY = 7.6;
player.onGround = false;
}
player.velocityY -= 20 * dt;
movePlayerVertically(player.velocityY * dt);
}
function movePlayerHorizontally(dx, dz) {
moveAxis("x", dx);
moveAxis("z", dz);
}
function moveAxis(axis, amount) {
if (!amount) return;
const step = 0.12 * Math.sign(amount);
let remaining = amount;
while (Math.abs(remaining) > 0.0001) {
const delta = Math.abs(remaining) < Math.abs(step) ? remaining : step;
const next = player.position.clone();
next[axis] += delta;
if (!collidesPlayer(next)) {
player.position.copy(next);
} else if (player.onGround) {
const stepped = next.clone();
stepped.y += 1.05;
if (!collidesPlayer(stepped)) player.position.copy(stepped);
else break;
} else {
break;
}
remaining -= delta;
}
}
function movePlayerVertically(amount) {
player.onGround = false;
if (!amount) {
const groundedProbe = player.position.clone();
groundedProbe.y -= 0.08;
if (collidesPlayer(groundedProbe)) player.onGround = true;
return;
}
const step = 0.08 * Math.sign(amount);
let remaining = amount;
while (Math.abs(remaining) > 0.0001) {
const delta = Math.abs(remaining) < Math.abs(step) ? remaining : step;
const next = player.position.clone();
next.y += delta;
if (!collidesPlayer(next)) player.position.copy(next);
else {
if (delta < 0) player.onGround = true;
player.velocityY = 0;
break;
}
remaining -= delta;
}
}
function collidesPlayer(pos) {
const minX = Math.floor(pos.x - 0.34);
const maxX = Math.floor(pos.x + 0.34);
const minY = Math.floor(pos.y);
const maxY = Math.floor(pos.y + 1.79);
const minZ = Math.floor(pos.z - 0.34);
const maxZ = Math.floor(pos.z + 0.34);
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
for (let z = minZ; z <= maxZ; z++) {
if (isSolid(getBlockAtWorld(x, y, z))) return true;
}
}
}
return false;
}
function updateCamera() {
camera.position.set(player.position.x, player.position.y + player.eyeHeight, player.position.z);
camera.rotation.y = player.yaw;
camera.rotation.x = player.pitch;
}
function updateSky() {
const day = (Math.sin(dayClock) + 1) * 0.5;
const skyColor = new THREE.Color().lerpColors(new THREE.Color(0x081226), new THREE.Color(0x7cc7ff), day);
scene.background.copy(skyColor);
scene.fog.color.copy(skyColor);
hemiLight.intensity = 0.28 + day * 0.86;
sunLight.intensity = 0.18 + day * 1.08;
sunLight.position.set(Math.cos(dayClock) * 70, 32 + day * 50, Math.sin(dayClock) * 70);
}
function updateTargeting() {
const origin = camera.position.clone();
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
activeTarget = raycastVoxel(origin, direction, MAX_INTERACT_DISTANCE);
if (activeTarget) {
selector.visible = true;
selector.position.set(activeTarget.block.x + 0.5, activeTarget.block.y + 0.5, activeTarget.block.z + 0.5);
} else {
selector.visible = false;
}
const areaName = getAreaName(player.position.x, player.position.z);
if (areaName !== currentAreaName) {
currentAreaName = areaName;
areaBannerEl.textContent = areaName || "";
areaBannerEl.classList.toggle("visible", Boolean(areaName));
}
}
function destroyTargetBlock() {
if (!activeTarget) return;
const { x, y, z } = activeTarget.block;
if (!isSolid(getBlockAtWorld(x, y, z))) return;
setEditedBlock(x, y, z, BLOCK.AIR);
refreshChunksAroundVoxel(x, y, z);
}
function placeTargetBlock() {
if (!activeTarget || !activeTarget.previous) return;
const { x, y, z } = activeTarget.previous;
if (y < 0 || y >= WORLD_HEIGHT) return;
if (isSolid(getBlockAtWorld(x, y, z))) return;
if (voxelIntersectsPlayer(x, y, z)) return;
setEditedBlock(x, y, z, PLACEABLE_BLOCKS[selectedSlot]);
refreshChunksAroundVoxel(x, y, z);
}
function voxelIntersectsPlayer(x, y, z) {
const playerMinX = player.position.x - 0.34;
const playerMaxX = player.position.x + 0.34;
const playerMinY = player.position.y;
const playerMaxY = player.position.y + 1.79;
const playerMinZ = player.position.z - 0.34;
const playerMaxZ = player.position.z + 0.34;
return !(x + 1 <= playerMinX || x >= playerMaxX || y + 1 <= playerMinY || y >= playerMaxY || z + 1 <= playerMinZ || z >= playerMaxZ);
}
function updateInfo() {
const readyChunks = Array.from(chunks.values()).filter((chunk) => chunk.data).length;
infoEl.textContent = `区块 ${readyChunks}/${chunks.size} | 渲染半径 ${RENDER_DISTANCE} | 鼠标左键破坏 / 右键放置`;
coordsEl.textContent = `X ${player.position.x.toFixed(1)} Y ${(player.position.y + player.eyeHeight).toFixed(1)} Z ${player.position.z.toFixed(1)}`;
selectedBlockEl.textContent = `当前方块 ${selectedSlot + 1}/6 | ${BLOCK_INFO[PLACEABLE_BLOCKS[selectedSlot]].name}`;
}
function ensureChunksAroundPlayer(force) {
const playerChunkX = worldToChunk(player.position.x);
const playerChunkZ = worldToChunk(player.position.z);
const requestKey = `${playerChunkX},${playerChunkZ}`;
if (!force && requestKey === lastChunkRequestKey) {
unloadFarChunks(playerChunkX, playerChunkZ);
return;
}
lastChunkRequestKey = requestKey;
const wanted = new Set();
for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
const cx = playerChunkX + dx;
const cz = playerChunkZ + dz;
const key = chunkKey(cx, cz);
wanted.add(key);
if (!chunks.has(key)) {
const chunk = { cx, cz, data: null, mesh: null, queued: true };
chunks.set(key, chunk);
chunkQueue.push(chunk);
}
}
}
unloadFarChunks(playerChunkX, playerChunkZ, wanted);
}
function unloadFarChunks(centerX, centerZ, wanted) {
for (const [key, chunk] of chunks.entries()) {
if (wanted && wanted.has(key)) continue;
if (Math.abs(chunk.cx - centerX) > RENDER_DISTANCE + 1 || Math.abs(chunk.cz - centerZ) > RENDER_DISTANCE + 1) {
if (chunk.mesh) {
scene.remove(chunk.mesh);
chunk.mesh.geometry.dispose();
}
chunks.delete(key);
}
}
}
function processChunkQueue(maxPerFrame) {
if (!chunkQueue.length) return;
const px = worldToChunk(player.position.x);
const pz = worldToChunk(player.position.z);
chunkQueue.sort((a, b) => (Math.abs(a.cx - px) + Math.abs(a.cz - pz)) - (Math.abs(b.cx - px) + Math.abs(b.cz - pz)));
let built = 0;
while (built < maxPerFrame && chunkQueue.length) {
const chunk = chunkQueue.shift();
if (!chunk || !chunks.has(chunkKey(chunk.cx, chunk.cz))) continue;
if (chunk.data) continue;
buildChunk(chunk.cx, chunk.cz);
built++;
}
}
function buildChunk(cx, cz) {
const chunk = chunks.get(chunkKey(cx, cz));
if (!chunk) return;
chunk.data = generateChunkData(cx, cz);
if (chunk.mesh) {
scene.remove(chunk.mesh);
chunk.mesh.geometry.dispose();
}
const geometry = buildChunkGeometry(cx, cz, chunk.data);
chunk.mesh = new THREE.Mesh(geometry, voxelMaterial);
chunk.mesh.frustumCulled = true;
scene.add(chunk.mesh);
}
function refreshChunksAroundVoxel(x, y, z) {
const touched = new Set();
const candidates = [[x, z], [x - 1, z], [x + 1, z], [x, z - 1], [x, z + 1]];
for (const [vx, vz] of candidates) {
const key = chunkKey(worldToChunk(vx), worldToChunk(vz));
if (touched.has(key)) continue;
touched.add(key);
if (chunks.has(key)) buildChunk(worldToChunk(vx), worldToChunk(vz));
}
}
function generateChunkData(cx, cz) {
const data = new Uint8Array(CHUNK_SIZE * WORLD_HEIGHT * CHUNK_SIZE);
const candidates = getChunkCandidates(cx, cz);
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
const wx = cx * CHUNK_SIZE + lx;
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
const wz = cz * CHUNK_SIZE + lz;
const height = getGroundHeight(wx, wz);
const mossNoise = fbm2(wx * 0.03 + 16, wz * 0.03 - 12, 2) * 0.5 + 0.5;
for (let y = 0; y <= height; y++) {
let id = BLOCK.STONE;
if (y === height) id = mossNoise > 0.72 ? BLOCK.MOSS : BLOCK.GRASS;
else if (y > height - 3) id = mossNoise > 0.8 ? BLOCK.MOSS : BLOCK.DIRT;
else if (y < height - 18 && mossNoise > 0.85 && ((wx + y + wz) & 3) === 0) id = BLOCK.MARBLE;
data[chunkIndex(lx, y, lz)] = id;
}
const columnCandidates = candidates.filter((landmark) => Math.abs(wx - landmark.x) <= landmark.radius && Math.abs(wz - landmark.z) <= landmark.radius);
if (!columnCandidates.length && !edits.size) continue;
for (let y = 0; y < WORLD_HEIGHT; y++) {
const edited = getEditedBlock(wx, y, wz);
if (edited !== undefined) {
data[chunkIndex(lx, y, lz)] = edited;
continue;
}
if (!columnCandidates.length) continue;
const structureValue = sampleStructureVoxel(wx, y, wz, columnCandidates);
if (structureValue !== undefined) data[chunkIndex(lx, y, lz)] = structureValue;
}
}
}
return data;
}
function buildChunkGeometry(cx, cz, data) {
const positions = [];
const normals = [];
const uvs = [];
const colors = [];
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
for (let y = 0; y < WORLD_HEIGHT; y++) {
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
const id = data[chunkIndex(lx, y, lz)];
if (!isSolid(id)) continue;
const wx = cx * CHUNK_SIZE + lx;
const wz = cz * CHUNK_SIZE + lz;
const blockDef = BLOCK_INFO[id];
const baseTint = blockDef.tint;
const randomShade = 0.94 + (hashInt(wx, y, wz) & 7) * 0.008;
for (let faceIndex = 0; faceIndex < faceDefinitions.length; faceIndex++) {
const face = faceDefinitions[faceIndex];
const nx = wx + face.dir[0];
const ny = y + face.dir[1];
const nz = wz + face.dir[2];
if (isSolid(getBlockAtWorld(nx, ny, nz))) continue;
const uvRect = getAtlasUVRect(getFaceTile(id, faceIndex));
const c = face.shade * randomShade;
pushFace(positions, normals, uvs, colors, wx, y, wz, face, uvRect, baseTint[0] * c, baseTint[1] * c, baseTint[2] * c);
}
}
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute("normal", new THREE.Float32BufferAttribute(normals, 3));
geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
geometry.computeBoundingSphere();
return geometry;
}
function pushFace(positions, normals, uvs, colors, wx, wy, wz, face, uvRect, r, g, b) {
const uvPairs = [[uvRect.u0, uvRect.v1], [uvRect.u0, uvRect.v0], [uvRect.u1, uvRect.v0], [uvRect.u1, uvRect.v1]];
const order = [0, 1, 2, 0, 2, 3];
for (const index of order) {
const vertex = face.corners[index];
positions.push(wx + vertex[0], wy + vertex[1], wz + vertex[2]);
normals.push(face.dir[0], face.dir[1], face.dir[2]);
uvs.push(uvPairs[index][0], uvPairs[index][1]);
colors.push(r, g, b);
}
}
function getBlockAtWorld(x, y, z) {
if (y < 0 || y >= WORLD_HEIGHT) return BLOCK.AIR;
const cx = worldToChunk(x);
const cz = worldToChunk(z);
const chunk = chunks.get(chunkKey(cx, cz));
if (chunk && chunk.data) return chunk.data[chunkIndex(mod(x, CHUNK_SIZE), y, mod(z, CHUNK_SIZE))];
return sampleVoxel(x, y, z);
}
function sampleVoxel(x, y, z) {
const edited = getEditedBlock(x, y, z);
if (edited !== undefined) return edited;
const structureValue = sampleStructureVoxel(x, y, z, getPointCandidates(x, z));
if (structureValue !== undefined) return structureValue;
return sampleBaseVoxel(x, y, z);
}
function sampleBaseVoxel(x, y, z) {
if (y < 0 || y >= WORLD_HEIGHT) return BLOCK.AIR;
const height = getGroundHeight(x, z);
if (y > height) return BLOCK.AIR;
if (y === height) {
const mossNoise = fbm2(x * 0.03 + 16, z * 0.03 - 12, 2) * 0.5 + 0.5;
return mossNoise > 0.72 ? BLOCK.MOSS : BLOCK.GRASS;
}
if (y > height - 3) return BLOCK.DIRT;
return BLOCK.STONE;
}
function sampleStructureVoxel(x, y, z, candidates) {
for (const landmark of candidates) {
if (y < landmark.minY || y > landmark.maxY) continue;
if (Math.abs(x - landmark.x) > landmark.radius || Math.abs(z - landmark.z) > landmark.radius) continue;
let result;
switch (landmark.type) {
case "citadel": result = sampleCitadel(landmark, x, y, z); break;
case "worldTree": result = sampleWorldTree(landmark, x, y, z); break;
case "dragonBridge": result = sampleDragonBridge(landmark, x, y, z); break;
case "moonTemple": result = sampleMoonTemple(landmark, x, y, z); break;
case "skyArchipelago": result = sampleSkyArchipelago(landmark, x, y, z); break;
case "runeGate": result = sampleRuneGate(landmark, x, y, z); break;
case "crystalGarden": result = sampleCrystalGarden(landmark, x, y, z); break;
default: result = undefined;
}
if (result !== undefined) return result;
}
return undefined;
}
function sampleCitadel(landmark, x, y, z) {
const lx = x - landmark.x;
const lz = z - landmark.z;
const ly = y - landmark.baseY;
const r = Math.hypot(lx, lz);
const angle = Math.atan2(lz, lx);
const outer = 20 + Math.cos(angle * 8) * 2.2;
const inner = 11 + Math.sin(angle * 4) * 1.3;
const gate =
(Math.abs(lx) <= 2 && lz > 17 && lz < 24 && ly >= 0 && ly <= 4) ||
(Math.abs(lx) <= 2 && lz < -17 && lz > -24 && ly >= 0 && ly <= 4) ||
(Math.abs(lz) <= 2 && lx > 17 && lx < 24 && ly >= 0 && ly <= 4) ||
(Math.abs(lz) <= 2 && lx < -17 && lx > -24 && ly >= 0 && ly <= 4);
if (gate) return BLOCK.AIR;
if (ly === -1 && r < outer - 1.2) return ((Math.floor(lx) + Math.floor(lz)) & 1) === 0 ? BLOCK.MARBLE : BLOCK.RUNESTONE;
if (ly >= 0 && ly <= 8 && Math.abs(r - outer) < 1.05) return ly > 6 ? BLOCK.OBSIDIAN : BLOCK.MARBLE;
for (const tower of landmark.data.towers) {
const tr = Math.hypot(lx - tower.x, lz - tower.z);
if (tr < 2.7 && ly >= 0 && ly <= tower.h) {
if (ly === tower.h) return BLOCK.GLOW;
return tr > 2.1 ? BLOCK.MARBLE : BLOCK.OBSIDIAN;
}
}
if (ly >= 1 && ly <= 5 && Math.abs(r - inner) < 0.8) return BLOCK.RUNESTONE;
if (r < 7 && ly >= 0 && ly <= 15) {
if (ly === 15) return BLOCK.GLOW;
if (ly === 0 || r > 5.8) return BLOCK.OBSIDIAN;
}
if (ly >= 16 && ly <= 22 && Math.abs(r - 4.6) < 0.72) return BLOCK.CRYSTAL;
if (Math.abs(lx) < 1 && Math.abs(lz) < 1 && ly >= 15 && ly <= 24) return BLOCK.GLOW;
return undefined;
}
function sampleWorldTree(landmark, x, y, z) {
const lx = x - landmark.x;
const lz = z - landmark.z;
const ly = y - landmark.baseY;
if (ly === 0 && Math.hypot(lx, lz) > 6.2 && Math.hypot(lx, lz) < 7.4) return BLOCK.RUNESTONE;
if (ly >= 0 && ly < 29) {
const trunkRadius = Math.max(1.6, 4.8 - ly * 0.1);
if (lx * lx + lz * lz <= trunkRadius * trunkRadius) return BLOCK.BARK;
}
for (const root of landmark.data.roots) {
if (distanceToSegment(lx, ly, lz, root.ax, root.ay, root.az, root.bx, root.by, root.bz) < root.r) return BLOCK.BARK;
}
for (const branch of landmark.data.branches) {
if (distanceToSegment(lx, ly, lz, branch.ax, branch.ay, branch.az, branch.bx, branch.by, branch.bz) < branch.r) return BLOCK.BARK;
}
for (const fruit of landmark.data.fruits) {
const dx = lx - fruit.x;
const dy = ly - fruit.y;
const dz = lz - fruit.z;
if (dx * dx + dy * dy + dz * dz <= 1.1) return BLOCK.GLOW;
}
if (ellipsoid(lx, ly - 27, lz, 10.5, 6.6, 10.5) <= 1) return BLOCK.LEAVES;
for (const bloom of landmark.data.canopies) {
const dx = lx - bloom.x;
const dy = ly - bloom.y;
const dz = lz - bloom.z;
if (ellipsoid(dx, dy, dz, bloom.rx, bloom.ry, bloom.rz) <= 1) return BLOCK.LEAVES;
}
return undefined;
}
function sampleDragonBridge(landmark, x, y, z) {
const lx = x - landmark.x;
const lz = z - landmark.z;
const local = landmark.data.axis === "x" ? { u: lx, v: lz } : { u: lz, v: lx };
const ly = y - landmark.baseY;
const deckY = 6;
const halfLen = 24;
if (Math.abs(local.u) <= halfLen && Math.abs(local.v) <= 2 && ly === deckY) return (Math.floor(local.u + local.v) & 1) === 0 ? BLOCK.BONE : BLOCK.MARBLE;
if (Math.abs(local.u) <= halfLen && Math.abs(local.v) < 0.6 && ly < deckY) {
const ground = getGroundHeight(x, z);
if (y > ground) return BLOCK.BONE;
}
for (let rib = -20; rib <= 20; rib += 6) {
if (Math.abs(local.u - rib) < 1.25 && ly >= deckY - 1 && ly <= deckY + 8) {
const shell = Math.abs(Math.hypot(local.v * 1.12, ly - deckY) - 5.4);
if (shell < 0.72) return BLOCK.BONE;
}
}
const skullValue = ellipsoid(local.u - (halfLen + 5), ly - (deckY + 2), local.v, 4.1, 3.2, 3.1);
if (skullValue <= 1 && skullValue >= 0.48) return BLOCK.BONE;
for (const horn of landmark.data.horns) {
if (distanceToSegment(local.u, ly, local.v, horn.ax, horn.ay, horn.az, horn.bx, horn.by, horn.bz) < horn.r) return BLOCK.BONE;
}
return undefined;
}
function sampleMoonTemple(landmark, x, y, z) {
const lx = x - landmark.x;
const lz = z - landmark.z;
const ly = y - landmark.baseY;
const d = Math.max(Math.abs(lx), Math.abs(lz));
if (Math.abs(lx) < 2 && lz > 1 && lz < 17 && ly > 0 && ly < 8) return BLOCK.AIR;
if (d <= 16) {
const level = Math.floor((16 - d) / 2);
if (ly >= 0 && ly <= level) return (level & 1) === 0 ? BLOCK.MARBLE : BLOCK.MOSS;
}
if (Math.abs(lx) <= 5 && Math.abs(lz) <= 5 && ly >= 0 && ly <= 10) {
if (ly === 10 || Math.max(Math.abs(lx), Math.abs(lz)) >= 4.4 || ly === 0) return BLOCK.RUNESTONE;
}
const pillar = ((Math.abs(lx - 10) <= 1 || Math.abs(lx + 10) <= 1) && (Math.abs(lz - 10) <= 1 || Math.abs(lz + 10) <= 1));
if (pillar && ly >= 1 && ly <= 8) return BLOCK.MARBLE;
if (Math.hypot(lx, lz) < 1.75 && ly === 1) return BLOCK.GLOW;
return undefined;
}
function sampleSkyArchipelago(landmark, x, y, z) {
const lx = x - landmark.x;
const lz = z - landmark.z;
const ly = y - landmark.baseY;
for (const isle of landmark.data.isles) {
const dx = lx - isle.x;
const dy = ly - isle.y;
const dz = lz - isle.z;
const e = ellipsoid(dx, dy, dz, isle.rx, isle.ry, isle.rz);
if (e <= 1) {
if (dy > -0.2) return BLOCK.GRASS;
if (dy > -2.8) return BLOCK.DIRT;
return BLOCK.STONE;
}
if (Math.abs(dx) < 1.2 && Math.abs(dz) < 1.2 && dy < -isle.ry && dy > -isle.ry - isle.spike) return BLOCK.CRYSTAL;
}
for (const ruin of landmark.data.ruins) {
const dx = lx - ruin.x;
const dy = ly - ruin.y;
const dz = lz - ruin.z;
const column = (Math.abs(dx - 2) < 0.8 || Math.abs(dx + 2) < 0.8 || Math.abs(dz - 2) < 0.8 || Math.abs(dz + 2) < 0.8);
if (column && Math.abs(dx) < 4 && Math.abs(dz) < 4 && dy >= 0 && dy <= ruin.h) return dy === ruin.h ? BLOCK.GLOW : BLOCK.MARBLE;
if (Math.abs(dx) < 5 && Math.abs(dz) < 5 && dy === 0) return BLOCK.RUNESTONE;
}
return undefined;
}
function sampleRuneGate(landmark, x, y, z) {
const lx = x - landmark.x;
const lz = z - landmark.z;
const ly = y - landmark.baseY;
if (Math.abs(lx) < 5 && Math.abs(lz) < 5 && ly === 0) return BLOCK.MARBLE;
if (Math.abs(lz) <= 2) {
const ring = Math.hypot(lx, ly - 10.5);
if (Math.abs(ring - 8.5) < 0.9) return ((Math.round(lx + ly) & 1) === 0) ? BLOCK.RUNESTONE : BLOCK.MARBLE;
}
const pillar = (((Math.abs(lx - 8) < 1.2) || (Math.abs(lx + 8) < 1.2)) && Math.abs(lz) < 1.4 && ly >= 0 && ly <= 10);
if (pillar) return BLOCK.OBSIDIAN;
if (Math.abs(lx) < 1 && Math.abs(lz) < 1 && ly >= 18 && ly <= 24) return BLOCK.CRYSTAL;
return undefined;
}
function sampleCrystalGarden(landmark, x, y, z) {
const lx = x - landmark.x;
const lz = z - landmark.z;
const ly = y - landmark.baseY;
for (const cluster of landmark.data.clusters) {
const dx = lx - cluster.ox;
const dz = lz - cluster.oz;
const taper = 1 - clamp(ly / cluster.h, 0, 0.85);
if (ly >= 0 && ly <= cluster.h) {
const dist = distanceToSegment(dx, ly, dz, 0, 0, 0, cluster.tx, cluster.h, cluster.tz);
if (dist < cluster.r * (0.5 + taper)) return BLOCK.CRYSTAL;
}
if (ly === 0 && dx * dx + dz * dz <= 6.8) return BLOCK.OBSIDIAN;
}
return undefined;
}
function raycastVoxel(origin, direction, maxDistance) {
const pos = origin.clone();
let x = Math.floor(pos.x);
let y = Math.floor(pos.y);
let z = Math.floor(pos.z);
const stepX = Math.sign(direction.x) || 1;
const stepY = Math.sign(direction.y) || 1;
const stepZ = Math.sign(direction.z) || 1;
const invX = direction.x !== 0 ? Math.abs(1 / direction.x) : Infinity;
const invY = direction.y !== 0 ? Math.abs(1 / direction.y) : Infinity;
const invZ = direction.z !== 0 ? Math.abs(1 / direction.z) : Infinity;
let nextX = direction.x > 0 ? (x + 1 - pos.x) * invX : (pos.x - x) * invX;
let nextY = direction.y > 0 ? (y + 1 - pos.y) * invY : (pos.y - y) * invY;
let nextZ = direction.z > 0 ? (z + 1 - pos.z) * invZ : (pos.z - z) * invZ;
let distance = 0;
let previous = null;
while (distance <= maxDistance) {
const id = getBlockAtWorld(x, y, z);
if (isSolid(id)) return { block: { x, y, z, id }, previous };
previous = { x, y, z };
if (nextX < nextY && nextX < nextZ) {
x += stepX; distance = nextX; nextX += invX;
} else if (nextY < nextZ) {
y += stepY; distance = nextY; nextY += invY;
} else {
z += stepZ; distance = nextZ; nextZ += invZ;
}
}
return null;
}
function isSolid(id) { return Boolean(BLOCK_INFO[id] && BLOCK_INFO[id].solid); }
function getFaceTile(id, faceIndex) {
const faces = BLOCK_INFO[id].faces;
if (faceIndex === 2) return faces.top;
if (faceIndex === 3) return faces.bottom;
return faces.side;
}
function chunkIndex(x, y, z) { return x + CHUNK_SIZE * (z + CHUNK_SIZE * y); }
function chunkKey(cx, cz) { return `${cx},${cz}`; }
function voxelKey(x, y, z) { return `${x},${y},${z}`; }
function worldToChunk(value) { return Math.floor(value / CHUNK_SIZE); }
function mod(value, divisor) { return ((value % divisor) + divisor) % divisor; }
function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
function createLandmark(type, x, z, name) {
const seed = hashInt(x, 0, z);
const landmark = {
type, x, z, seed,
name: name || generateAreaName(type, seed),
radius: LANDMARK_RADII[type],
baseY: getGroundHeight(x, z) + 1,
data: null,
minY: 0,
maxY: WORLD_HEIGHT - 1
};
initLandmarkData(landmark);
return landmark;
}
function initLandmarkData(landmark) {
if (landmark.data) return landmark.data;
const rng = mulberry32(landmark.seed);
switch (landmark.type) {
case "citadel":
landmark.data = {
towers: [
{ x: 18, z: 0, h: 16 }, { x: -18, z: 0, h: 16 }, { x: 0, z: 18, h: 16 }, { x: 0, z: -18, h: 16 },
{ x: 13, z: 13, h: 13 }, { x: 13, z: -13, h: 13 }, { x: -13, z: 13, h: 13 }, { x: -13, z: -13, h: 13 }
]
};
landmark.minY = landmark.baseY - 2;
landmark.maxY = landmark.baseY + 26;
break;
case "worldTree":
landmark.data = {
branches: Array.from({ length: 6 }, (_, index) => {
const angle = index * (TAU / 6) + (rng() - 0.5) * 0.3;
const len = 9 + rng() * 6;
return { ax: 0, ay: 13 + index, az: 0, bx: Math.cos(angle) * len, by: 20 + rng() * 6, bz: Math.sin(angle) * len, r: 1.3 + rng() * 0.55 };
}),
roots: Array.from({ length: 5 }, (_, index) => {
const angle = index * (TAU / 5) + rng() * 0.45;
const len = 8 + rng() * 5;
return { ax: 0, ay: 1, az: 0, bx: Math.cos(angle) * len, by: -1, bz: Math.sin(angle) * len, r: 1.15 + rng() * 0.4 };
}),
canopies: Array.from({ length: 6 }, (_, index) => {
const angle = index * (TAU / 6) + rng() * 0.22;
const dist = 6 + rng() * 4;
return { x: Math.cos(angle) * dist, y: 21 + rng() * 7, z: Math.sin(angle) * dist, rx: 5 + rng() * 2.5, ry: 3.2 + rng() * 1.8, rz: 5 + rng() * 2.5 };
}),
fruits: Array.from({ length: 10 }, () => {
const angle = rng() * TAU;
const dist = 4 + rng() * 7;
return { x: Math.cos(angle) * dist, y: 19 + rng() * 10, z: Math.sin(angle) * dist };
})
};
landmark.minY = landmark.baseY - 2;
landmark.maxY = landmark.baseY + 40;
break;
case "dragonBridge":
landmark.data = {
axis: rng() > 0.5 ? "x" : "z",
horns: [
{ ax: 28, ay: 9, az: -1, bx: 32, by: 13, bz: -4, r: 0.72 },
{ ax: 28, ay: 9, az: 1, bx: 32, by: 13, bz: 4, r: 0.72 }
]
};
landmark.minY = landmark.baseY - 1;
landmark.maxY = landmark.baseY + 18;
break;
case "moonTemple":
landmark.data = {};
landmark.minY = landmark.baseY;
landmark.maxY = landmark.baseY + 15;
break;
case "skyArchipelago":
landmark.data = {
isles: Array.from({ length: 5 }, (_, index) => {
const angle = index * (TAU / 5) + rng() * 0.5;
const dist = index === 0 ? 0 : 10 + rng() * 7;
return {
x: Math.round(Math.cos(angle) * dist),
y: 18 + rng() * 7,
z: Math.round(Math.sin(angle) * dist),
rx: 6 + rng() * 3.5,
ry: 2.8 + rng() * 1.8,
rz: 6 + rng() * 3.5,
spike: 7 + rng() * 4
};
}),
ruins: Array.from({ length: 2 }, (_, index) => ({ x: index === 0 ? 0 : 10, y: 21 + index * 3, z: index === 0 ? 0 : -8, h: 4 + index * 2 }))
};
landmark.minY = landmark.baseY + 12;
landmark.maxY = landmark.baseY + 36;
break;
case "runeGate":
landmark.data = {};
landmark.minY = landmark.baseY;
landmark.maxY = landmark.baseY + 25;
break;
case "crystalGarden":
landmark.data = {
clusters: Array.from({ length: 7 }, () => {
const angle = rng() * TAU;
const dist = 3 + rng() * 15;
return { ox: Math.cos(angle) * dist, oz: Math.sin(angle) * dist, tx: (rng() - 0.5) * 3.2, tz: (rng() - 0.5) * 3.2, h: 8 + rng() * 10, r: 0.75 + rng() * 0.85 };
})
};
landmark.minY = landmark.baseY;
landmark.maxY = landmark.baseY + 18;
break;
}
return landmark.data;
}
function getChunkCandidates(cx, cz) {
const centerX = cx * CHUNK_SIZE + CHUNK_SIZE * 0.5;
const centerZ = cz * CHUNK_SIZE + CHUNK_SIZE * 0.5;
const candidates = [];
for (const landmark of fixedLandmarks) {
if (Math.abs(centerX - landmark.x) <= landmark.radius + 24 && Math.abs(centerZ - landmark.z) <= landmark.radius + 24) candidates.push(landmark);
}
const regionSize = 96;
const regionX = Math.floor(centerX / regionSize);
const regionZ = Math.floor(centerZ / regionSize);
for (let rz = regionZ - 1; rz <= regionZ + 1; rz++) {
for (let rx = regionX - 1; rx <= regionX + 1; rx++) {
const landmark = getRegionLandmark(rx, rz);
if (!landmark) continue;
if (Math.abs(centerX - landmark.x) <= landmark.radius + 24 && Math.abs(centerZ - landmark.z) <= landmark.radius + 24) candidates.push(landmark);
}
}
return candidates;
}
function getPointCandidates(x, z) {
const candidates = [];
for (const landmark of fixedLandmarks) {
if (Math.abs(x - landmark.x) <= landmark.radius && Math.abs(z - landmark.z) <= landmark.radius) candidates.push(landmark);
}
const regionSize = 96;
const regionX = Math.floor(x / regionSize);
const regionZ = Math.floor(z / regionSize);
for (let rz = regionZ - 1; rz <= regionZ + 1; rz++) {
for (let rx = regionX - 1; rx <= regionX + 1; rx++) {
const landmark = getRegionLandmark(rx, rz);
if (!landmark) continue;
if (Math.abs(x - landmark.x) <= landmark.radius && Math.abs(z - landmark.z) <= landmark.radius) candidates.push(landmark);
}
}
return candidates;
}
function getRegionLandmark(rx, rz) {
const key = `${rx},${rz}`;
if (regionLandmarkCache.has(key)) return regionLandmarkCache.get(key);
if (Math.abs(rx) <= 1 && Math.abs(rz) <= 1) {
regionLandmarkCache.set(key, null);
return null;
}
const seed = hashInt(rx, 17, rz);
const rng = mulberry32(seed);
const types = ["crystalGarden", "skyArchipelago", "moonTemple", "runeGate", "worldTree", "dragonBridge"];
const type = types[seed % types.length];
const regionSize = 96;
const x = rx * regionSize + Math.round((rng() - 0.5) * 36);
const z = rz * regionSize + Math.round((rng() - 0.5) * 36);
const landmark = createLandmark(type, x, z, generateAreaName(type, seed));
regionLandmarkCache.set(key, landmark);
return landmark;
}
function getAreaName(x, z) {
let best = null;
let bestDistance = Infinity;
const pool = [...fixedLandmarks];
const regionSize = 96;
const regionX = Math.floor(x / regionSize);
const regionZ = Math.floor(z / regionSize);
for (let rz = regionZ - 1; rz <= regionZ + 1; rz++) {
for (let rx = regionX - 1; rx <= regionX + 1; rx++) {
const landmark = getRegionLandmark(rx, rz);
if (landmark) pool.push(landmark);
}
}
for (const landmark of pool) {
const distance = Math.hypot(x - landmark.x, z - landmark.z);
if (distance < landmark.radius + 8 && distance < bestDistance) {
bestDistance = distance;
best = landmark;
}
}
return best ? best.name : "";
}
function setEditedBlock(x, y, z, id) { edits.set(voxelKey(x, y, z), id); }
function getEditedBlock(x, y, z) {
const key = voxelKey(x, y, z);
if (!edits.has(key)) return undefined;
return edits.get(key);
}
function getGroundHeight(x, z) {
const continental = fbm2(x * 0.0022, z * 0.0022, 5) * 0.5 + 0.5;
const ridges = ridge2(x * 0.0085, z * 0.0085, 4);
const hills = fbm2(x * 0.018, z * 0.018, 3) * 0.5 + 0.5;
const swells = fbm2(x * 0.0045 + 90, z * 0.0045 - 40, 2) * 0.5 + 0.5;
let height = 8 + continental * 10 + ridges * 12 + hills * 5 + swells * 3;
height += Math.sin(x * 0.011) * 1.5 + Math.cos(z * 0.013) * 1.2;
return clamp(Math.floor(height), 4, WORLD_HEIGHT - 18);
}
function generateAreaName(type, seed) {
const prefix = AREA_PREFIXES[seed % AREA_PREFIXES.length];
const suffixes = AREA_SUFFIXES[type] || ["遗迹"];
return `${prefix}${suffixes[(seed >>> 3) % suffixes.length]}`;
}
function distanceToSegment(px, py, pz, ax, ay, az, bx, by, bz) {
const abx = bx - ax, aby = by - ay, abz = bz - az;
const apx = px - ax, apy = py - ay, apz = pz - az;
const abLengthSq = abx * abx + aby * aby + abz * abz || 1;
const t = clamp((apx * abx + apy * aby + apz * abz) / abLengthSq, 0, 1);
const qx = ax + abx * t, qy = ay + aby * t, qz = az + abz * t;
return Math.hypot(px - qx, py - qy, pz - qz);
}
function ellipsoid(x, y, z, rx, ry, rz) {
return (x * x) / (rx * rx) + (y * y) / (ry * ry) + (z * z) / (rz * rz);
}
function hashInt(x, y, z) {
let h = Math.imul(x | 0, 374761393) ^ Math.imul(y | 0, 668265263) ^ Math.imul(z | 0, 2147483647);
h = (h ^ (h >>> 13)) >>> 0;
h = Math.imul(h, 1274126177) >>> 0;
return h ^ (h >>> 16);
}
function mulberry32(seed) {
let t = seed >>> 0;
return function() {
t += 0x6D2B79F5;
let r = Math.imul(t ^ (t >>> 15), 1 | t);
r ^= r + Math.imul(r ^ (r >>> 7), 61 | r);
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
};
}
class PerlinNoise {
constructor(seed) {
const p = Array.from({ length: 256 }, (_, index) => index);
const rand = mulberry32(seed);
for (let i = p.length - 1; i > 0; i--) {
const j = Math.floor(rand() * (i + 1));
const temp = p[i]; p[i] = p[j]; p[j] = temp;
}
this.perm = new Uint16Array(512);
for (let i = 0; i < 512; i++) this.perm[i] = p[i & 255];
}
fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
lerp(a, b, t) { return a + (b - a) * t; }
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);
}
noise3(x, y, z) {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
const Z = Math.floor(z) & 255;
const xf = x - Math.floor(x);
const yf = y - Math.floor(y);
const zf = z - Math.floor(z);
const u = this.fade(xf), v = this.fade(yf), w = this.fade(zf);
const p = this.perm;
const aaa = p[p[p[X] + Y] + Z];
const aba = p[p[p[X] + Y + 1] + Z];
const aab = p[p[p[X] + Y] + Z + 1];
const abb = p[p[p[X] + Y + 1] + Z + 1];
const baa = p[p[p[X + 1] + Y] + Z];
const bba = p[p[p[X + 1] + Y + 1] + Z];
const bab = p[p[p[X + 1] + Y] + Z + 1];
const bbb = p[p[p[X + 1] + Y + 1] + Z + 1];
return this.lerp(
this.lerp(
this.lerp(this.grad(aaa, xf, yf, zf), this.grad(baa, xf - 1, yf, zf), u),
this.lerp(this.grad(aba, xf, yf - 1, zf), this.grad(bba, xf - 1, yf - 1, zf), u), v
),
this.lerp(
this.lerp(this.grad(aab, xf, yf, zf - 1), this.grad(bab, xf - 1, yf, zf - 1), u),
this.lerp(this.grad(abb, xf, yf - 1, zf - 1), this.grad(bbb, xf - 1, yf - 1, zf - 1), u), v
), w
);
}
noise2(x, y) { return this.noise3(x, y, 0); }
}
const perlin = new PerlinNoise(1337);
function fbm2(x, y, octaves) {
let amplitude = 1, frequency = 1, sum = 0, max = 0;
for (let i = 0; i < octaves; i++) {
sum += perlin.noise2(x * frequency, y * frequency) * amplitude;
max += amplitude;
amplitude *= 0.5;
frequency *= 2;
}
return sum / max;
}
function ridge2(x, y, octaves) {
let amplitude = 1, frequency = 1, sum = 0, max = 0;
for (let i = 0; i < octaves; i++) {
const n = 1 - Math.abs(perlin.noise2(x * frequency, y * frequency));
sum += n * n * amplitude;
max += amplitude;
amplitude *= 0.52;
frequency *= 2.08;
}
return sum / max;
}
function createTextureAtlas() {
const tileSize = 16;
const tilesPerRow = 4;
const canvas = document.createElement("canvas");
canvas.width = tileSize * 4;
canvas.height = tileSize * 4;
const ctx = canvas.getContext("2d");
drawTile(ctx, 0, tileSize, (x, y) => blendColors([46, 112, 42], [106, 178, 62], pseudoRand(x, y, 0.35)));
drawTile(ctx, 1, tileSize, (x, y) => y < 5 ? blendColors([34, 103, 37], [109, 183, 78], pseudoRand(x, y, 0.36)) : (x % 5 === 0 && y < 8 ? [88, 142, 60] : blendColors([103, 73, 42], [134, 92, 57], pseudoRand(x, y, 0.45))));
drawTile(ctx, 2, tileSize, (x, y) => blendColors([95, 65, 38], [140, 96, 56], pseudoRand(x, y, 0.46)));
drawTile(ctx, 3, tileSize, (x, y) => blendColors([112, 116, 124], [160, 166, 173], pseudoRand(x, y, 0.48)));
drawTile(ctx, 4, tileSize, (x, y) => blendColors([196, 202, 210], [244, 247, 250], marbleRand(x, y)));
drawTile(ctx, 5, tileSize, (x, y) => ((x + y) % 7 === 0 ? [128, 94, 173] : blendColors([26, 18, 35], [66, 42, 92], pseudoRand(x, y, 0.28))));
drawTile(ctx, 6, tileSize, (x, y) => {
const center = Math.hypot(x - 7.5, y - 7.5);
const t = clamp(1 - center / 10, 0, 1);
return blendColors([166, 118, 16], [255, 239, 146], t * 0.8 + pseudoRand(x, y, 0.1));
});
drawTile(ctx, 7, tileSize, (x, y) => x % 4 === 0 ? [66, 40, 21] : blendColors([91, 58, 30], [138, 91, 48], pseudoRand(x, y, 0.38)));
drawTile(ctx, 8, tileSize, (x, y) => (x + y) % 5 === 0 ? [146, 218, 103] : blendColors([38, 103, 48], [103, 176, 82], pseudoRand(x, y, 0.42)));
drawTile(ctx, 9, tileSize, (x, y) => Math.abs(x - 8) + Math.abs(y - 8) < 4 ? [255, 255, 255] : blendColors([95, 181, 255], [233, 242, 255], pseudoRand(x, y, 0.5)));
drawTile(ctx, 10, tileSize, (x, y) => (x * 3 + y * 5) % 17 === 0 ? [167, 158, 132] : blendColors([207, 201, 180], [245, 239, 222], pseudoRand(x, y, 0.32)));
drawTile(ctx, 11, tileSize, (x, y) => (x + y) % 8 < 2 ? [94, 216, 225] : blendColors([90, 97, 112], [143, 153, 170], pseudoRand(x, y, 0.42)));
drawTile(ctx, 12, tileSize, (x, y) => (x * y) % 7 === 0 ? [130, 161, 90] : blendColors([66, 91, 54], [117, 142, 77], pseudoRand(x, y, 0.35)));
const texture = new THREE.CanvasTexture(canvas);
texture.userData.tileSize = tileSize;
texture.userData.tilesPerRow = tilesPerRow;
return texture;
}
function drawTile(ctx, index, size, colorFn) {
const x0 = (index % 4) * size;
const y0 = Math.floor(index / 4) * size;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const [r, g, b] = colorFn(x, y);
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillRect(x0 + x, y0 + y, 1, 1);
}
}
}
function pseudoRand(x, y, spread) { return ((((x * 17 + y * 31) ^ (x * 13 + y * 7)) & 255) / 255) * spread + (1 - spread) * 0.5; }
function marbleRand(x, y) {
const wave = (Math.sin((x + y) * 0.7) + 1) * 0.2;
return clamp(0.58 + pseudoRand(x, y, 0.28) + wave, 0, 1);
}
function blendColors(a, b, t) {
return [
Math.round(a[0] + (b[0] - a[0]) * t),
Math.round(a[1] + (b[1] - a[1]) * t),
Math.round(a[2] + (b[2] - a[2]) * t)
];
}
function getAtlasUVRect(tileIndex) {
const tileSize = atlasTexture.userData.tileSize;
const tilesPerRow = atlasTexture.userData.tilesPerRow;
const atlasSize = tileSize * tilesPerRow;
const padding = 0.001;
const x = (tileIndex % tilesPerRow) * tileSize;
const y = Math.floor(tileIndex / tilesPerRow) * tileSize;
return { u0: x / atlasSize + padding, v0: y / atlasSize + padding, u1: (x + tileSize) / atlasSize - padding, v1: (y + tileSize) / atlasSize - padding };
}
function initHotbar() {
hotbarEl.innerHTML = "";
for (let index = 0; index < PLACEABLE_BLOCKS.length; index++) {
const blockId = PLACEABLE_BLOCKS[index];
const slot = document.createElement("div");
slot.className = "slot";
const texture = document.createElement("div");
texture.className = "slotTexture";
texture.style.backgroundImage = `url(${getTilePreview(BLOCK_INFO[blockId].faces.top)})`;
const key = document.createElement("div");
key.className = "slotKey";
key.textContent = String(index + 1);
slot.appendChild(texture);
slot.appendChild(key);
hotbarEl.appendChild(slot);
}
}
function updateHotbar() {
hotbarEl.querySelectorAll(".slot").forEach((slot, index) => slot.classList.toggle("active", index === selectedSlot));
updateInfo();
}
function getTilePreview(tileIndex) {
const tileSize = atlasTexture.userData.tileSize;
const previewCanvas = document.createElement("canvas");
previewCanvas.width = tileSize;
previewCanvas.height = tileSize;
const ctx = previewCanvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
ctx.drawImage(atlasTexture.image, (tileIndex % 4) * tileSize, Math.floor(tileIndex / 4) * tileSize, tileSize, tileSize, 0, 0, tileSize, tileSize);
return previewCanvas.toDataURL();
}
})();
</script>
</body>
</html>