Files
nagi-bench/outputs/gpt-5-6-sol/codex-cli-ultra/mythos-craft-block-era.html
T
Jesse ZhangandClaude Opus 4.8 64609bd6c8 feat: add GPT-5.6-Terra and alternate takes for GPT-5.6-Sol
New model gpt-5-6-terra (Codex CLI, ultra): mythos-craft, pelican-cycling,
skeleton-watch (one-shot, unmodified).

gpt-5-6-sol-ultra gains alternate one-shot variants for existing cases:
mythos-craft 'Block Era', two more pelican SVGs, and a cleanly completed
skeleton-watch 'Calibre 01' kept alongside the interruption-recovered
AURELIUS S-01. Extra variants attributed to Nagi-ovo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:09:15 +01:00

1835 lines
65 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title>MYTHOS:方块纪元</title>
<style>
:root {
--ink: #eef2dc;
--muted: #aeb89e;
--gold: #d8b86c;
--cyan: #78e0d4;
--panel: rgba(10, 17, 24, .78);
--line: rgba(216, 184, 108, .32);
}
* { box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #09131c;
color: var(--ink);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
user-select: none;
}
canvas { display: block; }
#game {
position: fixed;
inset: 0;
background: radial-gradient(circle at 50% 35%, #233f4a 0%, #101c29 42%, #071018 100%);
}
#vignette {
pointer-events: none;
position: fixed;
inset: 0;
z-index: 5;
background:
linear-gradient(to bottom, rgba(3, 8, 14, .18), transparent 20%, transparent 72%, rgba(3, 7, 11, .42)),
radial-gradient(ellipse at center, transparent 48%, rgba(2, 7, 11, .46) 100%);
}
#hud {
position: fixed;
inset: 0;
z-index: 10;
pointer-events: none;
opacity: 0;
transition: opacity .25s ease;
}
body.playing #hud { opacity: 1; }
.hud-card {
background: linear-gradient(145deg, rgba(13, 22, 29, .84), rgba(8, 14, 21, .68));
border: 1px solid var(--line);
box-shadow: 0 14px 34px rgba(0, 0, 0, .22), inset 0 1px rgba(255, 255, 255, .04);
backdrop-filter: blur(12px);
}
#status {
position: absolute;
top: 24px;
left: 24px;
min-width: 236px;
padding: 13px 15px 12px;
border-radius: 3px 13px 3px 13px;
}
.eyebrow {
color: var(--gold);
font-size: 10px;
letter-spacing: .22em;
font-weight: 800;
text-transform: uppercase;
}
#biome {
margin-top: 6px;
font-size: 16px;
font-weight: 720;
letter-spacing: .05em;
}
#coords {
margin-top: 4px;
color: var(--muted);
font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
}
#lore {
position: absolute;
top: 24px;
right: 24px;
width: min(318px, calc(100vw - 48px));
padding: 15px 17px 16px;
border-radius: 13px 3px 13px 3px;
}
#lore-title {
margin-top: 6px;
color: #f2e7c7;
font-family: Georgia, "Noto Serif SC", serif;
font-size: 19px;
letter-spacing: .06em;
}
#lore-copy {
margin-top: 7px;
color: #b9c4b3;
font-size: 12px;
line-height: 1.7;
}
#compass {
position: absolute;
top: 24px;
left: 50%;
width: 270px;
transform: translateX(-50%);
text-align: center;
color: rgba(238, 242, 220, .78);
font: 11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: .12em;
text-shadow: 0 2px 6px #000;
}
#compass::before {
content: "";
display: block;
width: 1px;
height: 9px;
margin: 0 auto 4px;
background: var(--gold);
box-shadow: 0 0 10px var(--gold);
}
#crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 18px;
height: 18px;
transform: translate(-50%, -50%);
filter: drop-shadow(0 1px 2px #000);
}
#crosshair::before,
#crosshair::after {
content: "";
position: absolute;
background: rgba(242, 239, 213, .92);
border-radius: 1px;
}
#crosshair::before { left: 8px; top: 2px; width: 2px; height: 14px; }
#crosshair::after { left: 2px; top: 8px; width: 14px; height: 2px; }
#target-label {
position: absolute;
left: 50%;
top: calc(50% + 28px);
transform: translateX(-50%);
min-width: 210px;
color: rgba(241, 242, 221, .84);
font-size: 10px;
letter-spacing: .08em;
text-align: center;
text-shadow: 0 2px 5px #000;
}
#hotbar-wrap {
position: absolute;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
#selected-name {
color: #f2e8c7;
font-size: 11px;
font-weight: 700;
letter-spacing: .12em;
text-shadow: 0 2px 6px #000;
}
#hotbar {
display: flex;
gap: 5px;
padding: 6px;
border-radius: 10px 3px 10px 3px;
background: rgba(6, 12, 17, .72);
border: 1px solid rgba(235, 210, 139, .28);
box-shadow: 0 12px 28px rgba(0, 0, 0, .36);
backdrop-filter: blur(10px);
}
.slot {
position: relative;
width: 48px;
height: 48px;
padding: 5px;
border: 1px solid rgba(255, 255, 255, .12);
background: rgba(255, 255, 255, .035);
transition: transform .16s ease, border-color .16s ease, background .16s ease;
}
.slot.selected {
transform: translateY(-4px);
border-color: var(--gold);
background: rgba(216, 184, 108, .15);
box-shadow: 0 0 0 1px rgba(216, 184, 108, .2), 0 0 20px rgba(216, 184, 108, .16);
}
.slot-swatch {
width: 100%;
height: 100%;
image-rendering: pixelated;
background-size: cover;
box-shadow: inset -5px -5px rgba(0, 0, 0, .18), inset 3px 3px rgba(255, 255, 255, .08);
}
.slot-key {
position: absolute;
top: 2px;
left: 4px;
color: rgba(255, 255, 255, .86);
font: 9px ui-monospace, monospace;
text-shadow: 0 1px 2px #000;
}
#toast {
position: absolute;
left: 50%;
bottom: 104px;
max-width: min(440px, calc(100vw - 40px));
padding: 10px 15px;
transform: translate(-50%, 12px);
border: 1px solid rgba(120, 224, 212, .3);
border-radius: 2px 10px 2px 10px;
background: rgba(7, 18, 23, .84);
color: #d9f1e9;
font-size: 12px;
letter-spacing: .04em;
opacity: 0;
transition: opacity .25s ease, transform .25s ease;
}
#toast.show { opacity: 1; transform: translate(-50%, 0); }
#start-screen {
position: fixed;
inset: 0;
z-index: 30;
display: grid;
place-items: center;
padding: 24px;
background:
linear-gradient(90deg, rgba(5, 11, 16, .9), rgba(5, 11, 16, .54) 52%, rgba(5, 11, 16, .82)),
radial-gradient(circle at 65% 30%, rgba(54, 115, 116, .3), transparent 34%),
radial-gradient(circle at 35% 75%, rgba(111, 73, 128, .22), transparent 38%);
transition: opacity .35s ease, visibility .35s ease;
}
#start-screen.hidden { opacity: 0; visibility: hidden; }
#start-screen::before {
content: "";
position: absolute;
inset: 18px;
border: 1px solid rgba(216, 184, 108, .18);
pointer-events: none;
}
.start-panel {
position: relative;
width: min(900px, 100%);
display: grid;
grid-template-columns: 1.2fr .8fr;
gap: clamp(30px, 7vw, 100px);
align-items: end;
}
.sigil {
display: inline-flex;
align-items: center;
gap: 10px;
color: var(--gold);
font-size: 10px;
font-weight: 800;
letter-spacing: .3em;
text-transform: uppercase;
}
.sigil::before {
content: "";
width: 28px;
height: 1px;
background: currentColor;
box-shadow: 0 0 8px currentColor;
}
h1 {
margin: 18px 0 12px;
font-family: Georgia, "Noto Serif SC", serif;
font-size: clamp(52px, 8vw, 104px);
font-weight: 500;
line-height: .86;
letter-spacing: -.05em;
text-shadow: 0 8px 40px rgba(0, 0, 0, .48);
}
h1 span {
display: block;
margin-top: 18px;
color: #c8d8cb;
font-family: ui-sans-serif, system-ui, sans-serif;
font-size: .19em;
font-weight: 500;
letter-spacing: .42em;
}
.intro {
max-width: 570px;
margin: 22px 0 0;
color: #aebcae;
font-size: 14px;
line-height: 1.9;
}
.side-panel {
padding: 22px;
border-top: 1px solid rgba(216, 184, 108, .42);
border-bottom: 1px solid rgba(216, 184, 108, .2);
background: linear-gradient(90deg, rgba(12, 22, 27, .76), rgba(12, 22, 27, .2));
}
.control-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 9px 15px;
color: #a9b5a8;
font-size: 11px;
line-height: 1.4;
}
.control-grid b {
color: #e9e7d1;
font: 700 10px ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: .06em;
}
#enter {
width: 100%;
margin-top: 22px;
padding: 14px 18px;
border: 1px solid #d5bd7e;
border-radius: 0;
background: linear-gradient(100deg, #d9bd75, #b99c5c);
color: #10171a;
font: 800 11px ui-sans-serif, system-ui, sans-serif;
letter-spacing: .24em;
cursor: pointer;
box-shadow: 0 10px 28px rgba(192, 156, 82, .16);
transition: transform .18s ease, filter .18s ease;
}
#enter:hover { transform: translateY(-2px); filter: brightness(1.08); }
#load-status {
margin-top: 10px;
color: #83938a;
font: 9px ui-monospace, monospace;
letter-spacing: .08em;
text-align: center;
}
#fatal {
display: none;
position: fixed;
inset: 0;
z-index: 100;
place-items: center;
padding: 30px;
background: #081017;
color: #e6dfc7;
text-align: center;
line-height: 1.8;
}
@media (max-width: 720px) {
.start-panel { grid-template-columns: 1fr; gap: 26px; }
.intro { font-size: 12px; }
#lore { top: auto; bottom: 102px; right: 12px; width: 220px; }
#status { top: 12px; left: 12px; min-width: 190px; }
#compass { top: 95px; }
.slot { width: 40px; height: 40px; }
#hotbar-wrap { bottom: 14px; }
}
</style>
</head>
<body>
<div id="game"></div>
<div id="vignette"></div>
<div id="hud">
<div id="status" class="hud-card">
<div class="eyebrow">Realm telemetry</div>
<div id="biome">永暮高地</div>
<div id="coords">X 0 · Y 0 · Z 0</div>
</div>
<div id="compass">N · 曙光王城 · 0M</div>
<div id="lore" class="hud-card">
<div class="eyebrow" id="lore-index">Myth archive · 0/5</div>
<div id="lore-title">无名荒野</div>
<div id="lore-copy">远古地脉仍在方块之间低语。循着天际的塔影,寻找被遗忘的神话遗迹。</div>
</div>
<div id="crosshair"></div>
<div id="target-label">寻找方块</div>
<div id="hotbar-wrap">
<div id="selected-name">草方块</div>
<div id="hotbar"></div>
</div>
<div id="toast">已发现新的神话遗迹</div>
</div>
<div id="start-screen">
<div class="start-panel">
<section>
<div class="sigil">A procedural voxel myth</div>
<h1>MYTHOS<span>方块纪元</span></h1>
<p class="intro">
一座由柏林噪声与失落神话共同生成的无尽世界。越过王城的黑曜石城门,攀上浮空圣殿,
唤醒世界树的微光,并在龙骨荒原寻找通往月海的门。
</p>
</section>
<aside class="side-panel">
<div class="control-grid">
<b>W A S D</b><span>移动</span>
<b>鼠标</b><span>环视</span>
<b>空格</b><span>跳跃</span>
<b>SHIFT</b><span>疾跑</span>
<b>左键</b><span>破坏方块</span>
<b>右键</b><span>放置方块</span>
<b>1—6 / 滚轮</b><span>选择方块</span>
<b>ESC</b><span>暂停并释放鼠标</span>
</div>
<button id="enter" type="button">进入永暮世界</button>
<div id="load-status">正在编织地形 · 0%</div>
</aside>
</div>
</div>
<div id="fatal">
<div>
<strong>无法载入 3D 世界</strong><br />
请确认网络可访问 cdn.jsdelivr.net,然后刷新页面。
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
<script>
(() => {
'use strict';
if (!window.THREE) {
document.getElementById('fatal').style.display = 'grid';
return;
}
const $ = (id) => document.getElementById(id);
const game = $('game');
const startScreen = $('start-screen');
const enterButton = $('enter');
const loadStatus = $('load-status');
const biomeEl = $('biome');
const coordsEl = $('coords');
const compassEl = $('compass');
const loreTitleEl = $('lore-title');
const loreCopyEl = $('lore-copy');
const loreIndexEl = $('lore-index');
const targetLabelEl = $('target-label');
const selectedNameEl = $('selected-name');
const hotbarEl = $('hotbar');
const toastEl = $('toast');
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x102331, 0.0125);
const camera = new THREE.PerspectiveCamera(72, innerWidth / innerHeight, 0.05, 310);
camera.rotation.order = 'YXZ';
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.7));
renderer.setSize(innerWidth, innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.04;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
game.appendChild(renderer.domElement);
const sky = new THREE.Mesh(
new THREE.SphereGeometry(220, 32, 16),
new THREE.ShaderMaterial({
side: THREE.BackSide,
depthWrite: false,
uniforms: { uTime: { value: 0 } },
vertexShader: `
varying vec3 vPos;
void main() {
vPos = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec3 vPos;
uniform float uTime;
void main() {
float h = normalize(vPos).y * .5 + .5;
vec3 low = vec3(.055, .12, .16);
vec3 mid = vec3(.12, .25, .29);
vec3 high = vec3(.025, .045, .09);
vec3 col = mix(low, mid, smoothstep(.18, .46, h));
col = mix(col, high, smoothstep(.52, .92, h));
float band = sin(normalize(vPos).x * 8.0 + uTime * .035) * .5 + .5;
col += vec3(.025, .08, .07) * band * smoothstep(.52, .8, h) * (1.0 - smoothstep(.82, 1.0, h));
gl_FragColor = vec4(col, 1.0);
}
`
})
);
scene.add(sky);
const hemi = new THREE.HemisphereLight(0x99cad0, 0x182015, 1.22);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffe5bb, 1.75);
sun.position.set(-55, 88, 36);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.left = -54;
sun.shadow.camera.right = 54;
sun.shadow.camera.top = 54;
sun.shadow.camera.bottom = -54;
sun.shadow.camera.near = 1;
sun.shadow.camera.far = 180;
sun.shadow.bias = -0.00025;
scene.add(sun);
scene.add(sun.target);
const celestial = new THREE.Group();
const moon = new THREE.Mesh(
new THREE.SphereGeometry(8, 24, 16),
new THREE.MeshBasicMaterial({ color: 0xd8ebe1, fog: false })
);
moon.position.set(72, 80, -145);
celestial.add(moon);
const moonHalo = new THREE.Mesh(
new THREE.RingGeometry(10.5, 14, 48),
new THREE.MeshBasicMaterial({ color: 0x8ec5c6, transparent: true, opacity: .12, side: THREE.DoubleSide, depthWrite: false, fog: false })
);
moonHalo.position.copy(moon.position);
moonHalo.lookAt(0, 0, 0);
celestial.add(moonHalo);
const secondMoon = new THREE.Mesh(
new THREE.SphereGeometry(3.2, 18, 12),
new THREE.MeshBasicMaterial({ color: 0xbc9ed1, fog: false })
);
secondMoon.position.set(98, 62, -140);
celestial.add(secondMoon);
scene.add(celestial);
const starGeometry = new THREE.BufferGeometry();
const starPositions = [];
let starSeed = 99173;
const starRand = () => {
starSeed |= 0;
starSeed = starSeed + 0x6D2B79F5 | 0;
let t = Math.imul(starSeed ^ starSeed >>> 15, 1 | starSeed);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
for (let i = 0; i < 550; i++) {
const theta = starRand() * Math.PI * 2;
const phi = Math.acos(.08 + starRand() * .84);
const r = 198;
starPositions.push(Math.sin(phi) * Math.cos(theta) * r, Math.cos(phi) * r, Math.sin(phi) * Math.sin(theta) * r);
}
starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starPositions, 3));
const stars = new THREE.Points(starGeometry, new THREE.PointsMaterial({ color: 0xdde9dc, size: .48, transparent: true, opacity: .72, depthWrite: false, fog: false }));
celestial.add(stars);
const CHUNK = 16;
const VIEW_RADIUS = 2;
const SEA_LEVEL = 6;
const MAX_BUILD_Y = 48;
const REACH = 7;
const BLOCK = Object.freeze({
AIR: 0,
GRASS: 1,
DIRT: 2,
STONE: 3,
MARBLE: 4,
RUNE: 5,
CRYSTAL: 6,
WOOD: 7,
LEAVES: 8,
SAND: 9,
BONE: 10,
GILDED: 11
});
const BLOCK_INFO = {
[BLOCK.GRASS]: { name: '草方块', color: 0x668b45 },
[BLOCK.DIRT]: { name: '泥土', color: 0x785135 },
[BLOCK.STONE]: { name: '远古石', color: 0x687177 },
[BLOCK.MARBLE]: { name: '月白石', color: 0xd7d0b8 },
[BLOCK.RUNE]: { name: '符文石', color: 0x273c56 },
[BLOCK.CRYSTAL]: { name: '星辉晶体', color: 0x65ddd0 },
[BLOCK.WOOD]: { name: '世界树木', color: 0x735038 },
[BLOCK.LEAVES]: { name: '世界树叶', color: 0x456c55 },
[BLOCK.SAND]: { name: '月砂', color: 0xbda976 },
[BLOCK.BONE]: { name: '古龙骨', color: 0xd8cfad },
[BLOCK.GILDED]: { name: '日铸金', color: 0xc79e45 }
};
function mulberry32(seed) {
return function() {
let t = seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
class PerlinNoise {
constructor(seed) {
const random = mulberry32(seed);
const p = Array.from({ length: 256 }, (_, i) => i);
for (let i = 255; i > 0; i--) {
const j = Math.floor(random() * (i + 1));
[p[i], p[j]] = [p[j], p[i]];
}
this.p = new Uint16Array(512);
for (let i = 0; i < 512; i++) this.p[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) {
switch (hash & 7) {
case 0: return x + y;
case 1: return -x + y;
case 2: return x - y;
case 3: return -x - y;
case 4: return x;
case 5: return -x;
case 6: return y;
default: return -y;
}
}
noise(x, y) {
const xi = Math.floor(x) & 255;
const yi = Math.floor(y) & 255;
const xf = x - Math.floor(x);
const yf = y - Math.floor(y);
const u = this.fade(xf);
const v = this.fade(yf);
const aa = this.p[this.p[xi] + yi];
const ab = this.p[this.p[xi] + yi + 1];
const ba = this.p[this.p[xi + 1] + yi];
const bb = this.p[this.p[xi + 1] + yi + 1];
return this.lerp(
this.lerp(this.grad(aa, xf, yf), this.grad(ba, xf - 1, yf), u),
this.lerp(this.grad(ab, xf, yf - 1), this.grad(bb, xf - 1, yf - 1), u),
v
) * .72;
}
}
const terrainNoise = new PerlinNoise(0xA57E11);
const detailNoise = new PerlinNoise(0x51DECA);
const biomeNoise = new PerlinNoise(0xB10B1E);
function fbm(noise, x, z, octaves = 5) {
let value = 0;
let amplitude = .5;
let frequency = 1;
let total = 0;
for (let i = 0; i < octaves; i++) {
value += noise.noise(x * frequency, z * frequency) * amplitude;
total += amplitude;
amplitude *= .5;
frequency *= 2.02;
}
return value / total;
}
function hash2(x, z) {
let h = Math.imul(x | 0, 374761393) + Math.imul(z | 0, 668265263);
h = Math.imul(h ^ h >>> 13, 1274126177);
return ((h ^ h >>> 16) >>> 0) / 4294967295;
}
function hash3(x, y, z) {
return hash2(x + Math.imul(y, 193), z - Math.imul(y, 389));
}
const heightCache = new Map();
function heightAt(x, z) {
const cacheKey = x + ',' + z;
const cached = heightCache.get(cacheKey);
if (cached !== undefined) return cached;
const continent = fbm(terrainNoise, x * .009, z * .009, 5);
const ridges = 1 - Math.abs(fbm(detailNoise, x * .019, z * .019, 4));
const small = fbm(detailNoise, x * .058, z * .058, 3);
let h = Math.floor(9 + continent * 10 + Math.pow(ridges, 4) * 6 + small * 2.4);
h = Math.max(3, Math.min(27, h));
const ax = Math.abs(x);
const az = Math.abs(z);
const insideCitadel = ax <= 19 && az <= 15;
const insideMoat = ax <= 27 && az <= 23;
if (insideCitadel) h = 15;
else if (insideMoat) h = Math.min(h, SEA_LEVEL - 2);
const treeD = Math.hypot(x + 38, z + 28);
if (treeD < 13) h = Math.max(h, Math.floor(12 - treeD * .26));
const portalD = Math.hypot(x - 36, z - 30);
if (portalD < 11) h = Math.max(h, Math.floor(10 - portalD * .18));
const boneD = Math.hypot(x + 36, z - 30);
if (boneD < 13) h = Math.min(Math.max(h, 8), 10);
heightCache.set(cacheKey, h);
return h;
}
function terrainBlockAt(x, y, z) {
if (y < 0) return BLOCK.STONE;
const h = heightAt(x, z);
if (y > h) return BLOCK.AIR;
if (y === h) {
if (h <= SEA_LEVEL + 1) return BLOCK.SAND;
if (h >= 23) return BLOCK.STONE;
return BLOCK.GRASS;
}
if (y >= h - 3) return h <= SEA_LEVEL + 1 ? BLOCK.SAND : BLOCK.DIRT;
if (hash3(x, y, z) > .994 && y > 2) return BLOCK.CRYSTAL;
return BLOCK.STONE;
}
function citadelBlockAt(x, y, z) {
if (Math.abs(x) > 31 || z < -17 || z > 32 || y < 14 || y > 42) return BLOCK.AIR;
const ax = Math.abs(x);
const az = Math.abs(z);
if (z >= 13 && z <= 30) {
if (ax <= 3 && y === 16) return (z % 6 === 0 && ax <= 1) ? BLOCK.RUNE : BLOCK.MARBLE;
if (ax === 3 && y === 17 && z % 2 === 0) return BLOCK.GILDED;
}
if (ax <= 16 && az <= 12 && y === 16) {
if ((x % 7 === 0 || z % 7 === 0) && hash2(x, z) > .48) return BLOCK.RUNE;
return BLOCK.MARBLE;
}
const outerEdge = (ax === 16 && az <= 12) || (az === 12 && ax <= 16);
const southGateVoid = z === 12 && ax <= 2 && y <= 21;
const slit = y === 20 && ((ax === 16 && Math.abs(z % 5) === 1) || (az === 12 && Math.abs(x % 5) === 1));
if (outerEdge && y >= 17 && y <= 22 && !southGateVoid && !slit) {
return (y === 18 && (x + z) % 4 === 0) ? BLOCK.GILDED : BLOCK.MARBLE;
}
if (outerEdge && y === 23 && (Math.abs(x + z) % 2 === 0)) return BLOCK.RUNE;
const corners = [[16, 12], [16, -12], [-16, 12], [-16, -12]];
for (const [cx, cz] of corners) {
const dx = Math.abs(x - cx);
const dz = Math.abs(z - cz);
const d = Math.max(dx, dz);
if (d === 3 && y >= 17 && y <= 27) {
if (y === 23 && ((dx === 3 && dz === 0) || (dz === 3 && dx === 0))) return BLOCK.CRYSTAL;
return y % 5 === 0 ? BLOCK.RUNE : BLOCK.MARBLE;
}
if (d <= 3 && y === 27) return BLOCK.MARBLE;
if (d === 3 && y === 28 && ((x + z) & 1) === 0) return BLOCK.GILDED;
}
const keepX = ax;
const keepZ = Math.abs(z + 1);
const keepEdge = (keepX === 7 && keepZ <= 6) || (keepZ === 6 && keepX <= 7);
const keepGateVoid = z === 5 && ax <= 1 && y <= 21;
const keepWindow = (y === 22 || y === 25) && ((keepX === 7 && Math.abs(z % 4) === 1) || (keepZ === 6 && Math.abs(x % 4) === 1));
if (keepEdge && y >= 17 && y <= 27 && !keepGateVoid && !keepWindow) {
return y === 20 ? BLOCK.RUNE : BLOCK.MARBLE;
}
if (keepX <= 7 && keepZ <= 6 && y === 17) return BLOCK.MARBLE;
if (keepX <= 7 && keepZ <= 6 && y === 28) return ((x + z) % 3 === 0) ? BLOCK.GILDED : BLOCK.MARBLE;
if (keepX <= 6 && keepZ <= 5 && y === 29) return BLOCK.RUNE;
if (keepX <= 5 && keepZ <= 4 && y === 30) return BLOCK.MARBLE;
const spireD = Math.max(ax, Math.abs(z + 1));
if (spireD === 3 && y >= 30 && y <= 36) {
if (y === 33 && (x === 0 || z === -1)) return BLOCK.CRYSTAL;
return y % 3 === 0 ? BLOCK.RUNE : BLOCK.MARBLE;
}
if (spireD <= 3 && y === 36) return BLOCK.GILDED;
if (ax <= Math.max(0, 2 - Math.floor((y - 37) / 2)) && Math.abs(z + 1) <= Math.max(0, 2 - Math.floor((y - 37) / 2)) && y >= 37 && y <= 40) return BLOCK.CRYSTAL;
const pylons = [[10, 6], [-10, 6], [10, -6], [-10, -6]];
for (const [px, pz] of pylons) {
const pd = Math.max(Math.abs(x - px), Math.abs(z - pz));
if (pd <= 1 && y >= 17 && y <= 22) return y === 20 ? BLOCK.GILDED : BLOCK.RUNE;
if (pd === 0 && y >= 23 && y <= 25) return BLOCK.CRYSTAL;
}
if (z === 12 && ax <= 3 && y === 22) return BLOCK.GILDED;
return BLOCK.AIR;
}
function pointSegmentDistanceSq(px, py, pz, ax, ay, az, bx, by, bz) {
const abx = bx - ax;
const aby = by - ay;
const abz = bz - az;
const apx = px - ax;
const apy = py - ay;
const apz = pz - az;
const denom = abx * abx + aby * aby + abz * abz;
const t = Math.max(0, Math.min(1, (apx * abx + apy * aby + apz * abz) / denom));
const dx = px - (ax + abx * t);
const dy = py - (ay + aby * t);
const dz = pz - (az + abz * t);
return dx * dx + dy * dy + dz * dz;
}
function worldTreeBlockAt(x, y, z) {
const dx = x + 38;
const dz = z + 28;
if (Math.abs(dx) > 13 || Math.abs(dz) > 13 || y < 11 || y > 39) return BLOCK.AIR;
if (y >= 13 && y <= 29) {
const radius = y < 18 ? 2.45 : y < 25 ? 1.75 : 1.15;
if (dx * dx + dz * dz <= radius * radius + hash3(x, y, z) * .55) return BLOCK.WOOD;
}
const roots = [[9, 0], [-9, 1], [0, 9], [1, -9], [7, 7], [-7, 7], [7, -7], [-7, -7]];
for (const [rx, rz] of roots) {
if (pointSegmentDistanceSq(x + .5, y + .5, z + .5, -37.5, 14, -27.5, -37.5 + rx, 12.5, -27.5 + rz) < 1.3) return BLOCK.WOOD;
}
const branches = [
[9, 31, 1], [-8, 30, 3], [2, 34, 8], [-3, 33, -8], [7, 35, -5], [-7, 35, -4]
];
for (const [bx, by, bz] of branches) {
if (pointSegmentDistanceSq(x + .5, y + .5, z + .5, -37.5, 26.5, -27.5, -37.5 + bx, by, -27.5 + bz) < 1.15) return BLOCK.WOOD;
}
const crowns = [
[0, 33, 0, 6.4], [7, 33, 1, 4.4], [-7, 32, 3, 4.7], [2, 36, 7, 4.1], [-3, 35, -7, 4.5], [6, 37, -4, 3.7], [-6, 37, -3, 3.8]
];
for (const [cx, cy, cz, r] of crowns) {
const ex = dx - cx;
const ey = (y - cy) * 1.1;
const ez = dz - cz;
const d2 = ex * ex + ey * ey + ez * ez;
if (d2 <= r * r && hash3(x, y, z) > .12) {
if (hash3(x + 71, y - 18, z + 9) > .976 && y < cy) return BLOCK.CRYSTAL;
return BLOCK.LEAVES;
}
}
return BLOCK.AIR;
}
const floatingIslands = [
{ x: 34, z: -28, y: 32, r: 10 },
{ x: 47, z: -34, y: 28, r: 5 },
{ x: 23, z: -39, y: 35, r: 4 },
{ x: 48, z: -17, y: 38, r: 3.5 }
];
function floatingIslandBlockAt(x, y, z) {
for (const island of floatingIslands) {
const dx = x - island.x;
const dz = z - island.z;
const radial = Math.sqrt(dx * dx + dz * dz) / island.r;
if (radial <= 1) {
const top = island.y + Math.floor(detailNoise.noise(x * .21, z * .21) * 1.5);
const thickness = Math.max(1, Math.floor((1 - radial) * island.r * .78 + 1));
if (y <= top && y >= top - thickness) {
if (y === top) return BLOCK.GRASS;
if (y >= top - 2) return BLOCK.DIRT;
return hash3(x, y, z) > .94 ? BLOCK.CRYSTAL : BLOCK.STONE;
}
}
}
const dx = x - 34;
const dz = z + 28;
const r2 = dx * dx + dz * dz;
if (r2 <= 42 && y >= 32 && y <= 34) return y === 34 ? BLOCK.MARBLE : BLOCK.RUNE;
const columns = [[5, 0], [-5, 0], [0, 5], [0, -5], [4, 4], [-4, 4], [4, -4], [-4, -4]];
for (const [cx, cz] of columns) {
const d = Math.max(Math.abs(dx - cx), Math.abs(dz - cz));
if (d === 0 && y >= 35 && y <= 42) return y === 39 ? BLOCK.GILDED : BLOCK.MARBLE;
if (d <= 1 && y === 35) return BLOCK.RUNE;
}
if (r2 >= 18 && r2 <= 37 && y === 43) return (x + z) % 3 === 0 ? BLOCK.GILDED : BLOCK.MARBLE;
if (Math.abs(dx) <= 1 && Math.abs(dz) <= 1 && y >= 35 && y <= 41) return BLOCK.CRYSTAL;
if (dx === 0 && dz === 0 && y === 42) return BLOCK.GILDED;
return BLOCK.AIR;
}
function moonGateBlockAt(x, y, z) {
const dx = x - 36;
const dz = z - 30;
if (Math.abs(dx) > 10 || Math.abs(dz) > 6 || y < 9 || y > 29) return BLOCK.AIR;
const base = 11;
const d2 = dx * dx + dz * dz;
if (y === base && d2 <= 64) return d2 > 45 ? BLOCK.GILDED : BLOCK.RUNE;
if (y === base + 1 && d2 <= 25) return BLOCK.MARBLE;
if (Math.abs(dz) <= 1) {
const ny = y - 19;
const ring = Math.sqrt((dx * dx) / 49 + (ny * ny) / 81);
if (ring > .82 && ring < 1.12) {
if ((x + y) % 5 === 0) return BLOCK.CRYSTAL;
return BLOCK.RUNE;
}
}
if ((Math.abs(dx) === 8 || Math.abs(dx) === 9) && Math.abs(dz) <= 2 && y >= base && y <= base + 3) return BLOCK.MARBLE;
return BLOCK.AIR;
}
function dragonBoneBlockAt(x, y, z) {
if (x < -48 || x > -24 || z < 14 || z > 46 || y < 10 || y > 23) return BLOCK.AIR;
const spineX = -36 + Math.round(Math.sin((z - 18) * .31));
if (z >= 18 && z <= 42 && Math.abs(x - spineX) <= (z % 5 === 0 ? 1 : 0) && y === 13) return BLOCK.BONE;
const ribs = [21, 25, 29, 33, 37];
for (const rz of ribs) {
const dx = x + 36;
if (Math.abs(dx) <= 8 && Math.abs(z - rz) <= (Math.abs(dx) === 8 ? 1 : 0)) {
const arch = 13 + Math.round(Math.sqrt(Math.max(0, 1 - (dx * dx) / 64)) * 7);
if (y === arch) return BLOCK.BONE;
}
}
const sx = x + 36;
const sz = z - 16;
if (Math.abs(sx) <= 3 && sz >= -2 && sz <= 2 && y >= 12 && y <= 16) {
const shell = Math.abs(sx) === 3 || Math.abs(sz) === 2 || y === 16;
const eye = y === 14 && Math.abs(sx) === 2 && sz === -2;
if (eye) return BLOCK.RUNE;
if (shell) return BLOCK.BONE;
}
if (pointSegmentDistanceSq(x + .5, y + .5, z + .5, -38, 16, 15, -44, 21, 9) < .9) return BLOCK.BONE;
if (pointSegmentDistanceSq(x + .5, y + .5, z + .5, -34, 16, 15, -28, 21, 9) < .9) return BLOCK.BONE;
return BLOCK.AIR;
}
function shrineBlockAt(x, y, z) {
const cell = 96;
const cx = Math.floor((x + cell / 2) / cell) * cell;
const cz = Math.floor((z + cell / 2) / cell) * cell;
if (Math.abs(cx) < 70 && Math.abs(cz) < 70) return BLOCK.AIR;
if (hash2(cx / cell, cz / cell) < .48) return BLOCK.AIR;
const dx = x - cx;
const dz = z - cz;
if (Math.abs(dx) > 5 || Math.abs(dz) > 5) return BLOCK.AIR;
const base = heightAt(cx, cz) + 1;
if (y === base && Math.max(Math.abs(dx), Math.abs(dz)) <= 4) return (dx === 0 || dz === 0) ? BLOCK.RUNE : BLOCK.MARBLE;
if ((Math.abs(dx) === 3 && Math.abs(dz) === 3) && y >= base + 1 && y <= base + 6) return y === base + 4 ? BLOCK.GILDED : BLOCK.MARBLE;
if (y === base + 7 && Math.max(Math.abs(dx), Math.abs(dz)) <= 4 && (Math.abs(dx) + Math.abs(dz) >= 3)) return BLOCK.RUNE;
if (dx === 0 && dz === 0 && y >= base + 1 && y <= base + 5) return BLOCK.CRYSTAL;
return BLOCK.AIR;
}
function structureBlockAt(x, y, z) {
let block = citadelBlockAt(x, y, z);
if (block) return block;
block = worldTreeBlockAt(x, y, z);
if (block) return block;
block = floatingIslandBlockAt(x, y, z);
if (block) return block;
block = moonGateBlockAt(x, y, z);
if (block) return block;
block = dragonBoneBlockAt(x, y, z);
if (block) return block;
return shrineBlockAt(x, y, z);
}
const edits = new Map();
function blockKey(x, y, z) { return x + ',' + y + ',' + z; }
try {
const saved = JSON.parse(localStorage.getItem('mythos-voxel-edits-v1') || '[]');
for (const entry of saved) {
if (Array.isArray(entry) && entry.length === 2) edits.set(entry[0], entry[1]);
}
} catch (_) { /* A sandbox may disable storage. */ }
let saveTimer = 0;
function scheduleSave() {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
try { localStorage.setItem('mythos-voxel-edits-v1', JSON.stringify(Array.from(edits.entries()).slice(-4000))); } catch (_) {}
}, 250);
}
function getBlock(x, y, z) {
const edited = edits.get(blockKey(x, y, z));
if (edited !== undefined) return edited;
const terrain = terrainBlockAt(x, y, z);
if (terrain) return terrain;
return structureBlockAt(x, y, z);
}
const textureUrls = {};
function makePixelTexture(base, accent, painter, seed = 1) {
const canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;
const ctx = canvas.getContext('2d');
ctx.fillStyle = base;
ctx.fillRect(0, 0, 16, 16);
const random = mulberry32(seed);
for (let i = 0; i < 62; i++) {
const alpha = .07 + random() * .18;
ctx.fillStyle = accent.replace('ALPHA', alpha.toFixed(3));
ctx.fillRect(Math.floor(random() * 16), Math.floor(random() * 16), random() > .86 ? 2 : 1, 1);
}
if (painter) painter(ctx, random);
const texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.generateMipmaps = false;
texture.encoding = THREE.sRGBEncoding;
textureUrls[seed] = canvas.toDataURL();
return texture;
}
const tex = {};
tex.dirt = makePixelTexture('#775136', 'rgba(39,24,18,ALPHA)', null, 11);
tex.grassTop = makePixelTexture('#5e8c45', 'rgba(194,217,101,ALPHA)', (ctx, random) => {
for (let i = 0; i < 18; i++) {
ctx.fillStyle = random() > .5 ? '#426f3d' : '#81a64f';
ctx.fillRect(Math.floor(random() * 16), Math.floor(random() * 16), 1, 1);
}
}, 12);
tex.grassSide = makePixelTexture('#775136', 'rgba(43,27,19,ALPHA)', (ctx, random) => {
ctx.fillStyle = '#5d8b45';
ctx.fillRect(0, 0, 16, 4);
for (let x = 0; x < 16; x++) {
if (random() > .27) {
ctx.fillStyle = random() > .5 ? '#537f42' : '#70984b';
ctx.fillRect(x, 4, 1, 1 + Math.floor(random() * 3));
}
}
}, 13);
tex.stone = makePixelTexture('#6d7578', 'rgba(22,31,35,ALPHA)', (ctx) => {
ctx.fillStyle = 'rgba(178,188,183,.22)';
ctx.fillRect(2, 3, 5, 1);
ctx.fillRect(11, 9, 3, 1);
ctx.fillRect(7, 13, 4, 1);
}, 14);
tex.marble = makePixelTexture('#d5d0bd', 'rgba(73,89,94,ALPHA)', (ctx) => {
ctx.strokeStyle = 'rgba(95,110,113,.34)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, 12); ctx.lineTo(5, 9); ctx.lineTo(8, 5); ctx.lineTo(16, 3); ctx.stroke();
ctx.strokeStyle = 'rgba(255,248,219,.4)';
ctx.beginPath(); ctx.moveTo(4, 16); ctx.lineTo(9, 11); ctx.lineTo(16, 10); ctx.stroke();
}, 15);
tex.rune = makePixelTexture('#26384f', 'rgba(101,157,167,ALPHA)', (ctx) => {
ctx.strokeStyle = '#5bc4c2';
ctx.globalAlpha = .7;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(8, 2); ctx.lineTo(5, 7); ctx.lineTo(9, 7); ctx.lineTo(6, 14);
ctx.moveTo(8, 2); ctx.lineTo(11, 7); ctx.lineTo(8, 10); ctx.lineTo(11, 14);
ctx.stroke();
ctx.globalAlpha = 1;
}, 16);
tex.woodSide = makePixelTexture('#6f4a33', 'rgba(31,20,15,ALPHA)', (ctx) => {
ctx.fillStyle = 'rgba(41,25,17,.42)';
for (const x of [2, 7, 12]) ctx.fillRect(x, 0, 1, 16);
ctx.fillStyle = 'rgba(181,123,72,.25)';
ctx.fillRect(5, 0, 1, 16);
}, 17);
tex.woodTop = makePixelTexture('#906442', 'rgba(45,27,18,ALPHA)', (ctx) => {
ctx.strokeStyle = 'rgba(48,28,18,.55)';
ctx.strokeRect(3, 3, 10, 10);
ctx.strokeRect(6, 6, 4, 4);
}, 18);
tex.leaves = makePixelTexture('#466b52', 'rgba(147,205,121,ALPHA)', (ctx, random) => {
for (let i = 0; i < 17; i++) {
if (random() > .45) ctx.clearRect(Math.floor(random() * 16), Math.floor(random() * 16), 1, 1);
}
}, 19);
tex.sand = makePixelTexture('#b7a475', 'rgba(82,70,47,ALPHA)', null, 20);
tex.bone = makePixelTexture('#d7cfad', 'rgba(85,72,54,ALPHA)', (ctx) => {
ctx.fillStyle = 'rgba(248,239,200,.28)';
ctx.fillRect(2, 4, 7, 1);
ctx.fillRect(9, 11, 5, 1);
}, 21);
tex.gold = makePixelTexture('#bb913b', 'rgba(255,230,128,ALPHA)', (ctx) => {
ctx.fillStyle = 'rgba(255,235,153,.42)';
ctx.fillRect(2, 2, 12, 2);
ctx.fillRect(2, 2, 2, 12);
}, 22);
const mat = (map, options = {}) => new THREE.MeshStandardMaterial({ map, roughness: .88, metalness: 0, ...options });
const dirtMat = mat(tex.dirt);
const grassSideMat = mat(tex.grassSide);
const grassTopMat = mat(tex.grassTop);
const stoneMat = mat(tex.stone, { roughness: .96 });
const marbleMat = mat(tex.marble, { roughness: .72 });
const runeMat = mat(tex.rune, { roughness: .68, emissive: 0x0b3037, emissiveIntensity: .34 });
const crystalMat = new THREE.MeshStandardMaterial({ color: 0x77e4d8, roughness: .2, metalness: .08, emissive: 0x1ca49d, emissiveIntensity: 1.15, transparent: true, opacity: .9 });
const woodSideMat = mat(tex.woodSide);
const woodTopMat = mat(tex.woodTop);
const leavesMat = mat(tex.leaves, { color: 0x91b787, alphaTest: .36, transparent: true, side: THREE.DoubleSide });
const sandMat = mat(tex.sand, { roughness: 1 });
const boneMat = mat(tex.bone, { roughness: .85 });
const goldMat = mat(tex.gold, { roughness: .42, metalness: .62, emissive: 0x3a2305, emissiveIntensity: .25 });
const materials = {
[BLOCK.GRASS]: [grassSideMat, grassSideMat, grassTopMat, dirtMat, grassSideMat, grassSideMat],
[BLOCK.DIRT]: dirtMat,
[BLOCK.STONE]: stoneMat,
[BLOCK.MARBLE]: marbleMat,
[BLOCK.RUNE]: runeMat,
[BLOCK.CRYSTAL]: crystalMat,
[BLOCK.WOOD]: [woodSideMat, woodSideMat, woodTopMat, woodTopMat, woodSideMat, woodSideMat],
[BLOCK.LEAVES]: leavesMat,
[BLOCK.SAND]: sandMat,
[BLOCK.BONE]: boneMat,
[BLOCK.GILDED]: goldMat
};
const swatchTextures = {
[BLOCK.GRASS]: tex.grassTop.image.toDataURL(),
[BLOCK.DIRT]: tex.dirt.image.toDataURL(),
[BLOCK.STONE]: tex.stone.image.toDataURL(),
[BLOCK.MARBLE]: tex.marble.image.toDataURL(),
[BLOCK.RUNE]: tex.rune.image.toDataURL(),
[BLOCK.CRYSTAL]: (() => {
const c = document.createElement('canvas'); c.width = c.height = 16;
const g = c.getContext('2d');
const grad = g.createLinearGradient(0, 16, 16, 0);
grad.addColorStop(0, '#1e7779'); grad.addColorStop(.5, '#7de9d9'); grad.addColorStop(1, '#c7fff0');
g.fillStyle = grad; g.fillRect(0, 0, 16, 16);
g.fillStyle = 'rgba(255,255,255,.55)'; g.fillRect(4, 2, 2, 10);
return c.toDataURL();
})()
};
const geometry = new THREE.BoxGeometry(1, 1, 1);
const worldGroup = new THREE.Group();
scene.add(worldGroup);
const chunks = new Map();
let rayMeshes = [];
const buildQueue = [];
const queuedChunks = new Set();
const desiredChunks = new Set();
const instanceMatrix = new THREE.Matrix4();
function chunkKey(cx, cz) { return cx + ',' + cz; }
function chunkCoord(n) { return Math.floor(n / CHUNK); }
function isExposed(x, y, z) {
return !getBlock(x + 1, y, z) || !getBlock(x - 1, y, z) ||
!getBlock(x, y + 1, z) || !getBlock(x, y - 1, z) ||
!getBlock(x, y, z + 1) || !getBlock(x, y, z - 1);
}
function buildChunk(cx, cz) {
const key = chunkKey(cx, cz);
if (chunks.has(key)) removeChunk(key);
const buckets = new Map();
const x0 = cx * CHUNK;
const z0 = cz * CHUNK;
for (let x = x0; x < x0 + CHUNK; x++) {
for (let z = z0; z < z0 + CHUNK; z++) {
for (let y = 0; y <= MAX_BUILD_Y; y++) {
const type = getBlock(x, y, z);
if (!type || !isExposed(x, y, z)) continue;
if (!buckets.has(type)) buckets.set(type, []);
buckets.get(type).push([x, y, z]);
}
}
}
const group = new THREE.Group();
group.userData.chunkKey = key;
const meshes = [];
for (const [type, positions] of buckets) {
if (!positions.length) continue;
const mesh = new THREE.InstancedMesh(geometry, materials[type], positions.length);
mesh.userData.positions = positions;
mesh.userData.blockType = type;
for (let i = 0; i < positions.length; i++) {
const [x, y, z] = positions[i];
instanceMatrix.makeTranslation(x + .5, y + .5, z + .5);
mesh.setMatrixAt(i, instanceMatrix);
}
mesh.instanceMatrix.needsUpdate = true;
mesh.castShadow = type !== BLOCK.LEAVES;
mesh.receiveShadow = true;
mesh.frustumCulled = true;
group.add(mesh);
meshes.push(mesh);
rayMeshes.push(mesh);
}
worldGroup.add(group);
chunks.set(key, { group, meshes, cx, cz });
}
function removeChunk(key) {
const chunk = chunks.get(key);
if (!chunk) return;
worldGroup.remove(chunk.group);
const removing = new Set(chunk.meshes);
rayMeshes = rayMeshes.filter((mesh) => !removing.has(mesh));
chunks.delete(key);
}
function queueChunk(cx, cz) {
const key = chunkKey(cx, cz);
if (chunks.has(key) || queuedChunks.has(key)) return;
queuedChunks.add(key);
buildQueue.push({ cx, cz, key });
}
function refreshDesiredChunks(px, pz) {
const centerX = chunkCoord(px);
const centerZ = chunkCoord(pz);
desiredChunks.clear();
const pending = [];
for (let dx = -VIEW_RADIUS; dx <= VIEW_RADIUS; dx++) {
for (let dz = -VIEW_RADIUS; dz <= VIEW_RADIUS; dz++) {
const cx = centerX + dx;
const cz = centerZ + dz;
const key = chunkKey(cx, cz);
desiredChunks.add(key);
if (!chunks.has(key) && !queuedChunks.has(key)) pending.push({ cx, cz, d: dx * dx + dz * dz });
}
}
pending.sort((a, b) => a.d - b.d);
for (const item of pending) queueChunk(item.cx, item.cz);
for (const key of Array.from(chunks.keys())) {
if (!desiredChunks.has(key)) removeChunk(key);
}
}
function processBuildQueue() {
while (buildQueue.length) {
const next = buildQueue.shift();
queuedChunks.delete(next.key);
if (!desiredChunks.has(next.key) || chunks.has(next.key)) continue;
buildChunk(next.cx, next.cz);
break;
}
const progress = Math.min(100, Math.round(chunks.size / ((VIEW_RADIUS * 2 + 1) ** 2) * 100));
loadStatus.textContent = progress < 100 ? `正在编织地形 · ${progress}%` : '世界已生成 · 地形将随探索延展';
}
function rebuildAtBlock(x, z) {
const cx = chunkCoord(x);
const cz = chunkCoord(z);
const targets = [[cx, cz]];
if (((x % CHUNK) + CHUNK) % CHUNK === 0) targets.push([cx - 1, cz]);
if (((x % CHUNK) + CHUNK) % CHUNK === CHUNK - 1) targets.push([cx + 1, cz]);
if (((z % CHUNK) + CHUNK) % CHUNK === 0) targets.push([cx, cz - 1]);
if (((z % CHUNK) + CHUNK) % CHUNK === CHUNK - 1) targets.push([cx, cz + 1]);
for (const [tx, tz] of targets) {
if (chunks.has(chunkKey(tx, tz))) buildChunk(tx, tz);
}
}
const water = new THREE.Mesh(
new THREE.PlaneGeometry(250, 250, 1, 1),
new THREE.MeshPhysicalMaterial({
color: 0x246b76,
transparent: true,
opacity: .62,
roughness: .18,
metalness: .05,
clearcoat: 1,
clearcoatRoughness: .15,
side: THREE.DoubleSide,
depthWrite: false
})
);
water.rotation.x = -Math.PI / 2;
water.position.y = SEA_LEVEL + .58;
water.receiveShadow = true;
scene.add(water);
const portalMaterial = new THREE.ShaderMaterial({
transparent: true,
depthWrite: false,
side: THREE.DoubleSide,
uniforms: { uTime: { value: 0 } },
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
vec3 p = position;
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform float uTime;
void main() {
vec2 p = (vUv - .5) * vec2(1.0, 1.15);
float r = length(p);
float a = atan(p.y, p.x);
float wave = sin(a * 7.0 - uTime * 1.4 + r * 24.0) * .5 + .5;
float mist = sin((p.x + p.y) * 19.0 + uTime * .7) * .5 + .5;
float edge = 1.0 - smoothstep(.34, .5, r);
vec3 col = mix(vec3(.16, .45, .52), vec3(.53, .30, .72), wave);
col += vec3(.45, .9, .78) * pow(mist, 7.0) * .45;
gl_FragColor = vec4(col, edge * (.42 + wave * .26));
}
`
});
const portal = new THREE.Mesh(new THREE.PlaneGeometry(11.3, 14.2, 1, 1), portalMaterial);
portal.position.set(36.5, 19, 30.53);
scene.add(portal);
const portalLight = new THREE.PointLight(0x4ed5c8, 1.8, 24, 2);
portalLight.position.set(36.5, 18, 29);
scene.add(portalLight);
const cloudMaterial = new THREE.MeshLambertMaterial({ color: 0xe0e5d5, transparent: true, opacity: .16, depthWrite: false });
const clouds = new THREE.Group();
const cloudRandom = mulberry32(0xC10DD5);
for (let i = 0; i < 16; i++) {
const cloud = new THREE.Group();
const pieces = 3 + Math.floor(cloudRandom() * 4);
for (let j = 0; j < pieces; j++) {
const cube = new THREE.Mesh(new THREE.BoxGeometry(5 + cloudRandom() * 8, 1.8 + cloudRandom() * 2, 4 + cloudRandom() * 6), cloudMaterial);
cube.position.set(j * 4 - pieces * 2, cloudRandom() * 2, (cloudRandom() - .5) * 5);
cloud.add(cube);
}
cloud.position.set((cloudRandom() - .5) * 180, 39 + cloudRandom() * 15, (cloudRandom() - .5) * 180);
cloud.userData.speed = .18 + cloudRandom() * .2;
clouds.add(cloud);
}
scene.add(clouds);
const moteCount = 180;
const motePositions = new Float32Array(moteCount * 3);
const moteSeeds = new Float32Array(moteCount);
const moteRandom = mulberry32(0xF1AEF1);
for (let i = 0; i < moteCount; i++) {
motePositions[i * 3] = (moteRandom() - .5) * 100;
motePositions[i * 3 + 1] = 6 + moteRandom() * 34;
motePositions[i * 3 + 2] = (moteRandom() - .5) * 100;
moteSeeds[i] = moteRandom() * Math.PI * 2;
}
const moteGeometry = new THREE.BufferGeometry();
moteGeometry.setAttribute('position', new THREE.BufferAttribute(motePositions, 3));
const motes = new THREE.Points(moteGeometry, new THREE.PointsMaterial({ color: 0x8be1c4, size: .14, transparent: true, opacity: .7, depthWrite: false }));
scene.add(motes);
const outline = new THREE.LineSegments(
new THREE.EdgesGeometry(new THREE.BoxGeometry(1.018, 1.018, 1.018)),
new THREE.LineBasicMaterial({ color: 0xf4d985, transparent: true, opacity: .95, depthTest: false })
);
outline.renderOrder = 20;
outline.visible = false;
scene.add(outline);
const hotbarTypes = [BLOCK.GRASS, BLOCK.DIRT, BLOCK.STONE, BLOCK.MARBLE, BLOCK.RUNE, BLOCK.CRYSTAL];
let selectedSlot = 0;
for (let i = 0; i < hotbarTypes.length; i++) {
const type = hotbarTypes[i];
const slot = document.createElement('div');
slot.className = 'slot' + (i === 0 ? ' selected' : '');
const swatch = document.createElement('div');
swatch.className = 'slot-swatch';
swatch.style.backgroundImage = `url(${swatchTextures[type]})`;
const key = document.createElement('span');
key.className = 'slot-key';
key.textContent = String(i + 1);
slot.append(swatch, key);
hotbarEl.appendChild(slot);
}
function selectSlot(index) {
selectedSlot = (index + hotbarTypes.length) % hotbarTypes.length;
Array.from(hotbarEl.children).forEach((slot, i) => slot.classList.toggle('selected', i === selectedSlot));
selectedNameEl.textContent = BLOCK_INFO[hotbarTypes[selectedSlot]].name;
}
const player = {
position: new THREE.Vector3(.5, 17, 27.5),
velocity: new THREE.Vector3(),
radius: .31,
height: 1.78,
eye: 1.62,
grounded: false,
yaw: 0,
pitch: -.04
};
const keys = Object.create(null);
let pointerLocked = false;
function playerCollides(position) {
const minX = Math.floor(position.x - player.radius);
const maxX = Math.floor(position.x + player.radius);
const minY = Math.floor(position.y + .02);
const maxY = Math.floor(position.y + player.height - .02);
const minZ = Math.floor(position.z - player.radius);
const maxZ = Math.floor(position.z + player.radius);
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
for (let z = minZ; z <= maxZ; z++) {
if (getBlock(x, y, z)) return true;
}
}
}
return false;
}
function moveAxis(axis, amount) {
const steps = Math.max(1, Math.ceil(Math.abs(amount) / .09));
const step = amount / steps;
for (let i = 0; i < steps; i++) {
player.position[axis] += step;
if (playerCollides(player.position)) {
player.position[axis] -= step;
player.velocity[axis] = 0;
return false;
}
}
return true;
}
function wouldBlockPlayer(x, y, z) {
const minX = player.position.x - player.radius;
const maxX = player.position.x + player.radius;
const minY = player.position.y;
const maxY = player.position.y + player.height;
const minZ = player.position.z - player.radius;
const maxZ = player.position.z + player.radius;
return x < maxX && x + 1 > minX && y < maxY && y + 1 > minY && z < maxZ && z + 1 > minZ;
}
const raycaster = new THREE.Raycaster();
raycaster.far = REACH;
let currentHit = null;
let lastRaycast = 0;
function updateRaycast(force = false) {
const now = performance.now();
if (!force && now - lastRaycast < 70) return;
lastRaycast = now;
raycaster.setFromCamera({ x: 0, y: 0 }, camera);
const hits = raycaster.intersectObjects(rayMeshes, false);
currentHit = null;
for (const hit of hits) {
if (hit.distance > REACH || hit.instanceId === undefined) continue;
const pos = hit.object.userData.positions[hit.instanceId];
if (!pos) continue;
currentHit = { hit, x: pos[0], y: pos[1], z: pos[2], type: hit.object.userData.blockType };
break;
}
if (currentHit) {
outline.position.set(currentHit.x + .5, currentHit.y + .5, currentHit.z + .5);
outline.visible = true;
targetLabelEl.textContent = `${BLOCK_INFO[currentHit.type]?.name || '未知方块'} · 左键破坏 / 右键放置`;
} else {
outline.visible = false;
targetLabelEl.textContent = '探索地平线上的神话遗迹';
}
}
const effects = [];
function spawnDebris(x, y, z, type) {
const color = BLOCK_INFO[type]?.color || 0xffffff;
for (let i = 0; i < 9; i++) {
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(.1 + Math.random() * .09, .1 + Math.random() * .09, .1 + Math.random() * .09),
new THREE.MeshBasicMaterial({ color, transparent: true })
);
mesh.position.set(x + .5 + (Math.random() - .5) * .55, y + .5 + (Math.random() - .5) * .55, z + .5 + (Math.random() - .5) * .55);
scene.add(mesh);
effects.push({ mesh, velocity: new THREE.Vector3((Math.random() - .5) * 2.6, 1.5 + Math.random() * 2.2, (Math.random() - .5) * 2.6), life: .75 });
}
}
let audioContext = null;
function playTone(kind) {
try {
if (!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)();
if (audioContext.state === 'suspended') audioContext.resume();
const oscillator = audioContext.createOscillator();
const gain = audioContext.createGain();
oscillator.type = kind === 'break' ? 'square' : 'sine';
oscillator.frequency.setValueAtTime(kind === 'break' ? 125 : 210, audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(kind === 'break' ? 58 : 335, audioContext.currentTime + .09);
gain.gain.setValueAtTime(.045, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(.001, audioContext.currentTime + .1);
oscillator.connect(gain).connect(audioContext.destination);
oscillator.start();
oscillator.stop(audioContext.currentTime + .11);
} catch (_) {}
}
function breakBlock() {
updateRaycast(true);
if (!currentHit || currentHit.y <= 0) return;
const { x, y, z, type } = currentHit;
edits.set(blockKey(x, y, z), BLOCK.AIR);
spawnDebris(x, y, z, type);
playTone('break');
rebuildAtBlock(x, z);
scheduleSave();
currentHit = null;
}
function placeBlock() {
updateRaycast(true);
if (!currentHit || !currentHit.hit.face) return;
const normal = currentHit.hit.face.normal;
const x = currentHit.x + Math.round(normal.x);
const y = currentHit.y + Math.round(normal.y);
const z = currentHit.z + Math.round(normal.z);
if (getBlock(x, y, z) || wouldBlockPlayer(x, y, z) || y < 0 || y > MAX_BUILD_Y) return;
const type = hotbarTypes[selectedSlot];
edits.set(blockKey(x, y, z), type);
playTone('place');
rebuildAtBlock(x, z);
scheduleSave();
}
const landmarks = [
{
x: 0, z: 0, radius: 28,
title: '曙光王城',
copy: '天空破碎之夜,七位守望者用月白石封存第一缕火。王城中央的星辉塔至今仍为迷途者指路。'
},
{
x: -38, z: -28, radius: 21,
title: '伊尔明世界树',
copy: '它的根系穿过所有时代。树冠垂落的晶体并非果实,而是世界尚未发生的记忆。'
},
{
x: 34, z: -28, radius: 22,
title: '苍穹浮岛圣殿',
copy: '浮岛被古老引力符文托举。八根月柱围绕星核,记录着天穹诸神最后一次议会。'
},
{
x: 36, z: 30, radius: 20,
title: '月海之门',
copy: '门后的潮汐没有水,只有缓慢流动的星光。符文环每隔一个纪元便短暂苏醒。'
},
{
x: -36, z: 30, radius: 22,
title: '古龙长眠地',
copy: '最后的云脊龙在这里坠落。它的肋骨仍保持飞行姿态,双角指向消失的北辰。'
}
];
const discovered = new Set();
let toastTimer = 0;
let currentLore = null;
function showToast(message) {
toastEl.textContent = message;
toastEl.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove('show'), 3000);
}
function updateLore() {
let nearest = landmarks[0];
let nearestDistance = Infinity;
for (const landmark of landmarks) {
const distance = Math.hypot(player.position.x - landmark.x, player.position.z - landmark.z);
if (distance < nearestDistance) {
nearestDistance = distance;
nearest = landmark;
}
if (distance < landmark.radius && !discovered.has(landmark.title)) {
discovered.add(landmark.title);
showToast(`神话图鉴已更新:${landmark.title}`);
}
}
if (nearest !== currentLore) {
currentLore = nearest;
loreTitleEl.textContent = nearest.title;
loreCopyEl.textContent = nearest.copy;
}
loreIndexEl.textContent = `Myth archive · ${discovered.size}/${landmarks.length}`;
const dx = nearest.x - player.position.x;
const dz = nearest.z - player.position.z;
const targetAngle = Math.atan2(-dx, -dz);
let relative = targetAngle - player.yaw;
while (relative > Math.PI) relative -= Math.PI * 2;
while (relative < -Math.PI) relative += Math.PI * 2;
const direction = Math.abs(relative) < .22 ? '正前方' : relative > 0 ? '右前方' : '左前方';
compassEl.textContent = `${direction} · ${nearest.title} · ${Math.round(nearestDistance)}M`;
}
function biomeAt(x, z) {
if (Math.hypot(x, z) < 31) return '曙光王城领地';
if (Math.hypot(x + 38, z + 28) < 24) return '世界树荫庭';
if (Math.hypot(x - 34, z + 28) < 24) return '苍穹裂谷';
if (Math.hypot(x - 36, z - 30) < 20) return '月海门庭';
if (Math.hypot(x + 36, z - 30) < 22) return '龙骨荒原';
const moisture = biomeNoise.noise(x * .012, z * .012);
const h = heightAt(Math.floor(x), Math.floor(z));
if (h <= SEA_LEVEL + 1) return '镜潮浅滩';
if (h >= 21) return '风蚀峰脊';
return moisture > .16 ? '翡翠苔原' : moisture < -.18 ? '灰烬旷野' : '永暮高地';
}
document.addEventListener('keydown', (event) => {
keys[event.code] = true;
if (event.code === 'Space') event.preventDefault();
if (/^Digit[1-6]$/.test(event.code)) selectSlot(Number(event.code.slice(5)) - 1);
});
document.addEventListener('keyup', (event) => { keys[event.code] = false; });
document.addEventListener('mousemove', (event) => {
if (!pointerLocked) return;
player.yaw -= event.movementX * .00205;
player.pitch -= event.movementY * .00205;
player.pitch = Math.max(-Math.PI / 2 + .015, Math.min(Math.PI / 2 - .015, player.pitch));
});
renderer.domElement.addEventListener('mousedown', (event) => {
if (!pointerLocked) return;
if (event.button === 0) breakBlock();
if (event.button === 2) placeBlock();
});
renderer.domElement.addEventListener('contextmenu', (event) => event.preventDefault());
renderer.domElement.addEventListener('wheel', (event) => {
if (!pointerLocked) return;
selectSlot(selectedSlot + (event.deltaY > 0 ? 1 : -1));
}, { passive: true });
enterButton.addEventListener('click', () => {
renderer.domElement.requestPointerLock();
try {
if (!audioContext) audioContext = new (window.AudioContext || window.webkitAudioContext)();
audioContext.resume();
} catch (_) {}
});
document.addEventListener('pointerlockchange', () => {
pointerLocked = document.pointerLockElement === renderer.domElement;
startScreen.classList.toggle('hidden', pointerLocked);
document.body.classList.toggle('playing', pointerLocked);
enterButton.textContent = pointerLocked ? '已进入世界' : '继续探索';
if (!pointerLocked) {
keys.KeyW = keys.KeyA = keys.KeyS = keys.KeyD = keys.Space = false;
}
});
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.7));
});
let lastTime = performance.now();
let lastChunkX = Infinity;
let lastChunkZ = Infinity;
let hudClock = 0;
function updatePlayer(dt) {
const forwardInput = (keys.KeyW ? 1 : 0) - (keys.KeyS ? 1 : 0);
const rightInput = (keys.KeyD ? 1 : 0) - (keys.KeyA ? 1 : 0);
const length = Math.hypot(forwardInput, rightInput) || 1;
const forward = forwardInput / length;
const right = rightInput / length;
const speed = keys.ShiftLeft || keys.ShiftRight ? 9.2 : 5.7;
const desiredX = (-Math.sin(player.yaw) * forward + Math.cos(player.yaw) * right) * speed;
const desiredZ = (-Math.cos(player.yaw) * forward - Math.sin(player.yaw) * right) * speed;
const responsiveness = player.grounded ? 15 : 5;
player.velocity.x += (desiredX - player.velocity.x) * Math.min(1, responsiveness * dt);
player.velocity.z += (desiredZ - player.velocity.z) * Math.min(1, responsiveness * dt);
player.grounded = playerCollides(new THREE.Vector3(player.position.x, player.position.y - .075, player.position.z));
if (player.grounded && player.velocity.y < 0) player.velocity.y = 0;
if (player.grounded && keys.Space) {
player.velocity.y = 8.3;
player.grounded = false;
keys.Space = false;
}
player.velocity.y -= 23 * dt;
player.velocity.y = Math.max(player.velocity.y, -32);
moveAxis('x', player.velocity.x * dt);
moveAxis('z', player.velocity.z * dt);
const verticalMoved = moveAxis('y', player.velocity.y * dt);
if (!verticalMoved && player.velocity.y <= 0) player.grounded = true;
if (player.position.y < -15) {
player.position.set(.5, 17, 27.5);
player.velocity.set(0, 0, 0);
}
camera.position.set(player.position.x, player.position.y + player.eye, player.position.z);
camera.rotation.y = player.yaw;
camera.rotation.x = player.pitch;
}
function updateEffects(dt) {
for (let i = effects.length - 1; i >= 0; i--) {
const effect = effects[i];
effect.life -= dt;
effect.velocity.y -= 7 * dt;
effect.mesh.position.addScaledVector(effect.velocity, dt);
effect.mesh.rotation.x += dt * 5;
effect.mesh.rotation.y += dt * 7;
effect.mesh.material.opacity = Math.max(0, effect.life / .75);
if (effect.life <= 0) {
scene.remove(effect.mesh);
effect.mesh.geometry.dispose();
effect.mesh.material.dispose();
effects.splice(i, 1);
}
}
}
function animate(now) {
requestAnimationFrame(animate);
const dt = Math.min(.05, (now - lastTime) / 1000 || .016);
lastTime = now;
if (pointerLocked) updatePlayer(dt);
else {
camera.position.set(player.position.x, player.position.y + player.eye, player.position.z);
camera.rotation.y = player.yaw;
camera.rotation.x = player.pitch;
}
const cx = chunkCoord(player.position.x);
const cz = chunkCoord(player.position.z);
if (cx !== lastChunkX || cz !== lastChunkZ) {
lastChunkX = cx;
lastChunkZ = cz;
refreshDesiredChunks(player.position.x, player.position.z);
}
processBuildQueue();
updateRaycast();
updateEffects(dt);
hudClock += dt;
if (hudClock > .18) {
hudClock = 0;
biomeEl.textContent = biomeAt(player.position.x, player.position.z);
coordsEl.textContent = `X ${Math.floor(player.position.x)} · Y ${Math.floor(player.position.y)} · Z ${Math.floor(player.position.z)} · ${chunks.size} 区块`;
updateLore();
}
const t = now * .001;
sky.material.uniforms.uTime.value = t;
portalMaterial.uniforms.uTime.value = t;
crystalMat.emissiveIntensity = 1.02 + Math.sin(t * 2.1) * .18;
portalLight.intensity = 1.65 + Math.sin(t * 1.7) * .35;
water.material.opacity = .59 + Math.sin(t * .7) * .025;
water.position.x = Math.round(player.position.x / 20) * 20;
water.position.z = Math.round(player.position.z / 20) * 20;
sky.position.copy(camera.position);
celestial.position.set(camera.position.x, camera.position.y, camera.position.z);
sun.position.set(player.position.x - 55, 88, player.position.z + 36);
sun.target.position.set(player.position.x, 0, player.position.z);
sun.target.updateMatrixWorld();
clouds.children.forEach((cloud) => {
cloud.position.x += cloud.userData.speed * dt;
const dx = cloud.position.x - player.position.x;
const dz = cloud.position.z - player.position.z;
if (dx > 105) cloud.position.x -= 210;
if (dx < -105) cloud.position.x += 210;
if (dz > 105) cloud.position.z -= 210;
if (dz < -105) cloud.position.z += 210;
});
const pos = moteGeometry.attributes.position.array;
for (let i = 0; i < moteCount; i++) {
pos[i * 3 + 1] += Math.sin(t * 1.1 + moteSeeds[i]) * .0025;
pos[i * 3] += Math.sin(t * .43 + moteSeeds[i]) * .0015;
}
moteGeometry.attributes.position.needsUpdate = true;
motes.position.x = Math.round(player.position.x / 70) * 70;
motes.position.z = Math.round(player.position.z / 70) * 70;
renderer.render(scene, camera);
}
refreshDesiredChunks(player.position.x, player.position.z);
updateLore();
selectSlot(0);
requestAnimationFrame(animate);
})();
</script>
</body>
</html>