Rename Claude Code combination ids to the -cc suffix; refresh the Opus 4.8 mythos build

claude-fable-5-code -> claude-fable-5-cc, claude-opus-4-8 ->
claude-opus-4-8-cc (the entry id doubles as the outputs/ folder name).
The Opus mythos-craft artifact is a newer build of the same one-shot
provenance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jesse ZhangandClaude Fable 5 committed 2026-06-10 23:07:51 +01:00
1 parent b42022e8ce
commit 8acf16262f
7 files changed
+780 -375

No files matched your search

File renamed without changes.
File renamed without changes.
File renamed without changes.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

@@ -0,0 +1,780 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>方块秘境 · Voxel Mythos</title>
<style>
:root { --acid:#c8ff00; --night:#0c0e14; --ink:#e9f0ff; }
* { margin:0; padding:0; box-sizing:border-box; }
html, body { width:100%; height:100%; overflow:hidden; background:#0c0e14; }
body { font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; color:var(--ink); -webkit-user-select:none; user-select:none; touch-action:none; }
canvas { display:block; }
#game { position:fixed; inset:0; }
/* Crosshair */
#cross { position:fixed; left:50%; top:50%; width:22px; height:22px; transform:translate(-50%,-50%); pointer-events:none; z-index:20; mix-blend-mode:difference; opacity:.9; }
#cross::before, #cross::after { content:""; position:absolute; background:#fff; }
#cross::before { left:50%; top:0; width:2px; height:100%; transform:translateX(-50%); }
#cross::after { top:50%; left:0; height:2px; width:100%; transform:translateY(-50%); }
/* HUD top-left */
#hud { position:fixed; left:14px; top:12px; z-index:20; font-size:12px; line-height:1.55; text-shadow:0 1px 2px rgba(0,0,0,.8); pointer-events:none; }
#hud b { color:var(--acid); font-weight:700; }
#hud .dim { color:#9aa6c2; }
/* Hotbar */
#hotbar { position:fixed; left:50%; bottom:18px; transform:translateX(-50%); z-index:20; display:flex; gap:6px; padding:6px; background:rgba(8,10,18,.55); border:1px solid rgba(255,255,255,.12); border-radius:12px; backdrop-filter:blur(8px); pointer-events:none; }
.slot { position:relative; width:48px; height:48px; border-radius:8px; background:rgba(255,255,255,.04); border:2px solid rgba(255,255,255,.10); display:flex; align-items:center; justify-content:center; }
.slot.active { border-color:var(--acid); box-shadow:0 0 0 2px rgba(200,255,0,.25), 0 6px 20px rgba(200,255,0,.18); transform:translateY(-3px); }
.slot canvas { width:34px; height:34px; image-rendering:pixelated; border-radius:4px; }
.slot .key { position:absolute; top:-9px; left:-6px; font-size:9px; color:#0c0e14; background:var(--acid); border-radius:5px; padding:0 4px; font-weight:800; }
.slot .nm { position:absolute; bottom:-16px; left:50%; transform:translateX(-50%); font-size:9px; color:#cfd8ef; white-space:nowrap; opacity:0; transition:opacity .15s; }
.slot.active .nm { opacity:1; }
/* Start / pause overlay */
#veil { position:fixed; inset:0; z-index:40; display:flex; align-items:center; justify-content:center; flex-direction:column; gap:22px;
background:radial-gradient(1200px 800px at 50% 20%, rgba(40,60,120,.35), rgba(8,10,18,.92) 60%), #06070c; text-align:center; transition:opacity .4s; }
#veil.hide { opacity:0; pointer-events:none; }
#veil .badge { font-size:11px; letter-spacing:.4em; color:var(--acid); text-transform:uppercase; }
#veil h1 { font-size:clamp(34px, 7vw, 76px); line-height:.96; font-weight:800; letter-spacing:-.02em;
background:linear-gradient(180deg,#fff, #9fb6ff 70%, #6f86ff); -webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:0 0 60px rgba(120,150,255,.25); }
#veil h1 small { display:block; font-size:.34em; letter-spacing:.5em; color:#7d8bb8; -webkit-text-fill-color:#7d8bb8; margin-top:10px; font-weight:600; }
#veil .play { pointer-events:auto; cursor:pointer; font:inherit; font-weight:800; font-size:15px; letter-spacing:.18em; color:#0c0e14; background:var(--acid); border:none; padding:14px 34px; border-radius:999px; text-transform:uppercase; box-shadow:0 14px 40px rgba(200,255,0,.30); transition:transform .15s, box-shadow .15s; }
#veil .play:hover { transform:translateY(-2px); box-shadow:0 18px 50px rgba(200,255,0,.45); }
#veil .play:disabled { background:#39414f; color:#7d8bb8; box-shadow:none; cursor:default; }
#veil .keys { display:flex; gap:18px; flex-wrap:wrap; justify-content:center; max-width:680px; font-size:12px; color:#aab4d0; }
#veil .keys span b { color:#fff; background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.16); border-radius:6px; padding:2px 7px; margin-right:5px; font-weight:700; }
#veil .tip { font-size:11px; color:#6b769a; max-width:560px; line-height:1.7; }
#bar { position:fixed; left:50%; bottom:-2px; transform:translateX(-50%); width:min(420px,70vw); height:3px; background:rgba(255,255,255,.08); z-index:41; border-radius:2px; overflow:hidden; }
#bar i { display:block; height:100%; width:0%; background:var(--acid); transition:width .2s; }
#flash { position:fixed; inset:0; z-index:15; pointer-events:none; background:#fff; opacity:0; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="cross"></div>
<div id="flash"></div>
<div id="hud">
<div><b>方块秘境</b> <span class="dim">Voxel Mythos</span></div>
<div class="dim" id="stat">pos —</div>
<div class="dim" id="biome"></div>
<div class="dim" id="fps"></div>
</div>
<div id="hotbar"></div>
<div id="veil">
<div class="badge">A Hand-built Myth · 神话造物</div>
<h1>方块秘境<small>VOXEL MYTHOS</small></h1>
<button class="play" id="play" disabled>生成世界中…</button>
<div class="keys">
<span><b>W A S D</b>移动</span>
<span><b>SPACE</b>跳跃</span>
<span><b>SHIFT</b>疾跑</span>
<span><b>鼠标</b>环视</span>
<span><b>左键</b>破坏</span>
<span><b>右键</b>放置</span>
<span><b>1-9 / 滚轮</b>切换方块</span>
<span><b>F</b>飞行</span>
</div>
<div class="tip">柏林噪声生成的无限地形之上,矗立着由开发者亲手设计的「天悬神殿」与「世界之树」——光之回廊、水晶尖塔、悬浮岛屿、巨龙守卫与符文法阵环绕其间。降临神域,开始你的造物。</div>
</div>
<div id="bar"><i id="barfill"></i></div>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}}
</script>
<script type="module">
import * as THREE from 'three';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
// ======================================================================
// 方块秘境 · VOXEL MYTHOS — single-file voxel sandbox
// Perlin terrain · first-person · break/place · a hand-designed mythos
// ======================================================================
// ---------- tunables ----------
const CHUNK = 16; // chunk width/depth in blocks
const WORLD_H = 72; // world height
const SEA = 22; // sea level
const RENDER_DIST = 6; // chunk radius around player to keep loaded
const SEED = 1337;
// ---------- block ids ----------
const B = {
AIR:0, GRASS:1, DIRT:2, STONE:3, SAND:4, LOG:5, LEAVES:6, WATER:7,
PLANK:8, COBBLE:9, MARBLE:10, GLOW:11, CRYSTAL:12, GOLD:13, OBSID:14,
RUNE:15, SNOW:16, MOSS:17, LAPIS:18, BEDROCK:19
};
// transparency / pass classification
const TRANSPARENT = new Set([B.AIR, B.WATER, B.LEAVES, B.CRYSTAL]); // don't fully occlude neighbours
const EMISSIVE = new Set([B.GLOW, B.RUNE, B.LAPIS]); // full-bright pass
const GLASSY = new Set([B.WATER, B.CRYSTAL]); // translucent pass
const isAir = id => id === B.AIR;
const isOpaque = id => id !== B.AIR && !TRANSPARENT.has(id);
const isSolid = id => id !== B.AIR && id !== B.WATER; // for collision
// ---------- seeded PRNG ----------
function mulberry32(a){ return function(){ a|=0; a=a+0x6D2B79F5|0; let t=Math.imul(a^a>>>15,1|a); t=t+Math.imul(t^t>>>7,61|t)^t; return ((t^t>>>14)>>>0)/4294967296; }; }
function hash2(x,y){ let h = x*374761393 + y*668265263; h = (h^(h>>>13))*1274126177; return ((h^(h>>>16))>>>0)/4294967296; }
// ---------- Perlin noise (improved, seeded permutation) ----------
const perm = new Uint8Array(512);
(function buildPerm(){
const p = new Uint8Array(256); for(let i=0;i<256;i++) p[i]=i;
const rng = mulberry32(SEED);
for(let i=255;i>0;i--){ const j=(rng()*(i+1))|0; const t=p[i]; p[i]=p[j]; p[j]=t; }
for(let i=0;i<512;i++) perm[i]=p[i&255];
})();
const fade = t => t*t*t*(t*(t*6-15)+10);
const lerp = (a,b,t) => a+(b-a)*t;
function grad2(h,x,y){ switch(h&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; } }
function perlin2(x,y){
const X=Math.floor(x)&255, Y=Math.floor(y)&255; x-=Math.floor(x); y-=Math.floor(y);
const u=fade(x), v=fade(y);
const aa=perm[perm[X]+Y], ab=perm[perm[X]+Y+1], ba=perm[perm[X+1]+Y], bb=perm[perm[X+1]+Y+1];
return lerp( lerp(grad2(aa,x,y), grad2(ba,x-1,y), u), lerp(grad2(ab,x,y-1), grad2(bb,x-1,y-1), u), v );
}
// fractal brownian motion
function fbm(x,y,oct,lac,gain){ let f=0,amp=0.5,freq=1,norm=0; for(let i=0;i<oct;i++){ f+=amp*perlin2(x*freq,y*freq); norm+=amp; amp*=gain; freq*=lac; } return f/norm; }
// ======================================================================
// PROCEDURAL TEXTURE ATLAS — drawn pixel-by-pixel, NearestFilter
// ======================================================================
const TILE = 16, COLS = 8;
// tile indices
const T = { GRASS_TOP:0, GRASS_SIDE:1, DIRT:2, STONE:3, SAND:4, LOG_TOP:5, LOG_SIDE:6, LEAVES:7,
WATER:8, PLANK:9, COBBLE:10, MARBLE:11, GLOW:12, CRYSTAL:13, GOLD:14, OBSID:15,
RUNE:16, SNOW:17, MOSS:18, LAPIS:19, BEDROCK:20, MARBLE_GOLD:21 };
const TILE_COUNT = 22, ROWS = Math.ceil(TILE_COUNT/COLS);
const atlas = document.createElement('canvas'); atlas.width = COLS*TILE; atlas.height = ROWS*TILE;
const actx = atlas.getContext('2d');
// helpers operating on a TILE×TILE off-screen, then blitted into atlas at index
function makeTile(index, draw){
const c = document.createElement('canvas'); c.width=TILE; c.height=TILE; const g=c.getContext('2d');
const img = g.createImageData(TILE,TILE); const d = img.data;
const put=(x,y,r,gr,b,a=255)=>{ if(x<0||y<0||x>=TILE||y>=TILE)return; const i=(y*TILE+x)*4; d[i]=r;d[i+1]=gr;d[i+2]=b;d[i+3]=a; };
draw(put, mulberry32(SEED*131 + index*7919));
g.putImageData(img,0,0);
const cx=(index%COLS)*TILE, cy=((index/COLS)|0)*TILE;
actx.drawImage(c, cx, cy);
}
// colour util
function vary(base, rng, amt){ const k=(rng()*2-1)*amt; return [clamp8(base[0]+k), clamp8(base[1]+k), clamp8(base[2]+k)]; }
function clamp8(v){ return v<0?0:v>255?255:v|0; }
function mix(a,b,t){ return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t, a[2]+(b[2]-a[2])*t]; }
// ---- individual tile painters ----
function fill(put, base, rng, amt){ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const c=vary(base,rng,amt); put(x,y,c[0],c[1],c[2]); } }
makeTile(T.DIRT, (put,rng)=>{ fill(put,[134,96,67],rng,26);
for(let i=0;i<10;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; const c=vary([96,68,46],rng,18); put(x,y,c[0],c[1],c[2]); } });
makeTile(T.GRASS_TOP, (put,rng)=>{ fill(put,[86,150,58],rng,24);
for(let i=0;i<22;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; const c=rng()<.5?[64,118,42]:[110,176,70]; put(x,y,c[0],c[1],c[2]); } });
makeTile(T.GRASS_SIDE, (put,rng)=>{ // dirt body + green crown w/ dribble
fill(put,[134,96,67],rng,24);
for(let x=0;x<TILE;x++){ const top=2+((rng()*3)|0);
for(let y=0;y<top;y++){ const c=vary([78,142,52],rng,20); put(x,y,c[0],c[1],c[2]); }
if(rng()<.4){ const c=vary([70,128,46],rng,16); put(x,top,c[0],c[1],c[2]); if(rng()<.4) put(x,top+1,c[0]-8,c[1]-8,c[2]-6); } } });
makeTile(T.STONE, (put,rng)=>{ fill(put,[128,128,133],rng,16);
for(let i=0;i<6;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,96,96,102); } });
makeTile(T.COBBLE, (put,rng)=>{ const cell=4; for(let cy=0;cy<TILE;cy+=cell)for(let cx=0;cx<TILE;cx+=cell){ const b=110+((rng()*40)|0);
for(let y=0;y<cell;y++)for(let x=0;x<cell;x++){ const edge=(x===0||y===0); const v=edge?70:b+((rng()*16-8)|0); put(cx+x,cy+y,v,v,v+4); } } });
makeTile(T.SAND, (put,rng)=>{ fill(put,[219,203,140],rng,16);
for(let i=0;i<8;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,198,182,120); } });
makeTile(T.SNOW, (put,rng)=>{ fill(put,[236,242,250],rng,8);
for(let i=0;i<6;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,255,255,255); } });
makeTile(T.LOG_SIDE, (put,rng)=>{ for(let x=0;x<TILE;x++){ const streak=(x%4===0)?-18:0;
for(let y=0;y<TILE;y++){ const c=vary([101,73,42],rng,12); put(x,y,c[0]+streak,c[1]+streak,c[2]+streak); } }
for(let i=0;i<6;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,70,50,28); } });
makeTile(T.LOG_TOP, (put,rng)=>{ const c0=TILE/2-0.5; for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const d=Math.hypot(x-c0,y-c0); const ring=(Math.floor(d)%2===0); const base=ring?[176,140,95]:[150,115,75]; const c=vary(base,rng,10); put(x,y,c[0],c[1],c[2]); }
put(c0|0,c0|0,120,92,58); });
makeTile(T.LEAVES, (put,rng)=>{ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ if(rng()<.10){ put(x,y,0,0,0,0); continue; } const r=rng(); const base=r<.4?[38,82,28]:r<.8?[54,110,40]:[78,140,52]; const c=vary(base,rng,12); put(x,y,c[0],c[1],c[2]); } });
makeTile(T.MOSS, (put,rng)=>{ fill(put,[120,120,124],rng,14);
for(let i=0;i<60;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; if(rng()<.5){ const c=vary([62,116,46],rng,18); put(x,y,c[0],c[1],c[2]); } } });
makeTile(T.WATER, (put,rng)=>{ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const w=Math.sin((x+y)*0.9)*0.5+0.5; const base=mix([34,104,196],[60,140,226],w*0.6+rng()*0.2); put(x,y,base[0]|0,base[1]|0,base[2]|0,210); } });
makeTile(T.PLANK, (put,rng)=>{ fill(put,[176,134,78],rng,14);
for(let y=0;y<TILE;y++){ if(y%4===0){ for(let x=0;x<TILE;x++) put(x,y,120,86,46); } }
for(let i=0;i<10;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,150,112,62); } });
makeTile(T.MARBLE, (put,rng)=>{ fill(put,[232,230,223],rng,7);
let vx=(rng()*TILE)|0; for(let y=0;y<TILE;y++){ vx+=(rng()*3-1)|0; const c=[188,190,198]; put((vx+TILE)%TILE,y,c[0],c[1],c[2]); }
let vx2=(rng()*TILE)|0; for(let y=0;y<TILE;y++){ vx2+=(rng()*3-1)|0; put((vx2+TILE)%TILE,y,200,202,210); } });
makeTile(T.MARBLE_GOLD, (put,rng)=>{ fill(put,[232,230,223],rng,7);
for(let x=0;x<TILE;x++){ put(x,0,255,210,74); put(x,TILE-1,205,160,40); put(x,1,255,236,150); } });
makeTile(T.GLOW, (put,rng)=>{ fill(put,[255,206,107],rng,14);
for(let i=0;i<26;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,255,238,176); }
for(let i=0;i<8;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,224,150,60); } });
makeTile(T.CRYSTAL, (put,rng)=>{ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const band=((x+y)>>1)%3; const base=band===0?[210,250,255]:band===1?[150,235,255]:[96,180,228]; const c=vary(base,rng,10); put(x,y,c[0],c[1],c[2],235); }
for(let i=0;i<5;i++){ const x=(rng()*TILE)|0; put(x,(rng()*TILE)|0,255,255,255,255); } });
makeTile(T.LAPIS, (put,rng)=>{ fill(put,[40,66,176],rng,16);
for(let i=0;i<26;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; const c=rng()<.5?[92,124,232]:[24,40,120]; put(x,y,c[0],c[1],c[2]); } });
makeTile(T.GOLD, (put,rng)=>{ const half=TILE/2; for(let by=0;by<2;by++)for(let bx=0;bx<2;bx++){ for(let y=0;y<half;y++)for(let x=0;x<half;x++){ let c=[255,210,74]; if(x===0||y===0) c=[150,112,30]; else if(x<2&&y<2) c=[255,244,180]; else if(x>half-3&&y>half-3) c=[205,160,40]; const v=vary(c,rng,8); put(bx*half+x,by*half+y,v[0],v[1],v[2]); } } });
makeTile(T.OBSID, (put,rng)=>{ fill(put,[22,18,34],rng,8);
for(let i=0;i<14;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; const c=vary([74,44,98],rng,18); put(x,y,c[0],c[1],c[2]); }
for(let k=0;k<TILE;k++){ if(k%5===0) put(k,k,120,90,150); } });
makeTile(T.BEDROCK, (put,rng)=>{ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const v=30+((rng()*60)|0); put(x,y,v,v,v+3); } });
makeTile(T.RUNE, (put,rng)=>{ fill(put,[58,58,66],rng,12); // dark stone + glowing glyph
const glyph=[ "0001111000","0010000100","0100110010","0101001010","1010000101","1010000101","0101001010","0100110010","0010000100","0001111000" ];
const off=3; for(let y=0;y<10;y++)for(let x=0;x<10;x++){ if(glyph[y][x]==='1'){ const c=vary([120,240,255],rng,16); put(x+off,y+off,c[0],c[1],c[2]); } }
put(8,8,200,250,255); });
// ---------- atlas → texture ----------
const atlasTex = new THREE.CanvasTexture(atlas);
atlasTex.magFilter = THREE.NearestFilter; atlasTex.minFilter = THREE.NearestFilter;
atlasTex.generateMipmaps = false; atlasTex.flipY = false; // canvas (0,0)=top-left maps to UV(0,0)
atlasTex.colorSpace = THREE.SRGBColorSpace; atlasTex.anisotropy = 1;
const EPS = 0.0008; // inset to kill texture bleed
function tileUV(idx){ const col=idx%COLS, row=(idx/COLS)|0;
const u0=(col*TILE)/atlas.width + EPS, v0=(row*TILE)/atlas.height + EPS;
const u1=((col+1)*TILE)/atlas.width - EPS, v1=((row+1)*TILE)/atlas.height - EPS;
return [u0,v0,u1,v1]; // v0=top edge, v1=bottom edge (flipY=false)
}
// block id → face tiles {top, side, bottom}
const FACE = {};
FACE[B.GRASS]={top:T.GRASS_TOP, side:T.GRASS_SIDE, bottom:T.DIRT};
FACE[B.DIRT]={top:T.DIRT,side:T.DIRT,bottom:T.DIRT};
FACE[B.STONE]={top:T.STONE,side:T.STONE,bottom:T.STONE};
FACE[B.SAND]={top:T.SAND,side:T.SAND,bottom:T.SAND};
FACE[B.LOG]={top:T.LOG_TOP,side:T.LOG_SIDE,bottom:T.LOG_TOP};
FACE[B.LEAVES]={top:T.LEAVES,side:T.LEAVES,bottom:T.LEAVES};
FACE[B.WATER]={top:T.WATER,side:T.WATER,bottom:T.WATER};
FACE[B.PLANK]={top:T.PLANK,side:T.PLANK,bottom:T.PLANK};
FACE[B.COBBLE]={top:T.COBBLE,side:T.COBBLE,bottom:T.COBBLE};
FACE[B.MARBLE]={top:T.MARBLE,side:T.MARBLE_GOLD,bottom:T.MARBLE};
FACE[B.GLOW]={top:T.GLOW,side:T.GLOW,bottom:T.GLOW};
FACE[B.CRYSTAL]={top:T.CRYSTAL,side:T.CRYSTAL,bottom:T.CRYSTAL};
FACE[B.GOLD]={top:T.GOLD,side:T.GOLD,bottom:T.GOLD};
FACE[B.OBSID]={top:T.OBSID,side:T.OBSID,bottom:T.OBSID};
FACE[B.RUNE]={top:T.RUNE,side:T.OBSID,bottom:T.OBSID};
FACE[B.SNOW]={top:T.SNOW,side:T.SNOW,bottom:T.SNOW};
FACE[B.MOSS]={top:T.MOSS,side:T.MOSS,bottom:T.MOSS};
FACE[B.LAPIS]={top:T.LAPIS,side:T.LAPIS,bottom:T.LAPIS};
FACE[B.BEDROCK]={top:T.BEDROCK,side:T.BEDROCK,bottom:T.BEDROCK};
// ---------- materials ----------
const matOpaque = new THREE.MeshLambertMaterial({ map:atlasTex, vertexColors:true, alphaTest:0.5, side:THREE.FrontSide });
const matGlass = new THREE.MeshLambertMaterial({ map:atlasTex, vertexColors:true, transparent:true, opacity:0.82, depthWrite:false, side:THREE.DoubleSide });
const matGlow = new THREE.MeshBasicMaterial({ map:atlasTex, vertexColors:true, transparent:true, alphaTest:0.4 });
// ======================================================================
// WORLD MODEL
// - chunks store generated voxel data
// - overrides[] holds everything non-terrain: trees, the mythos, player edits
// - sampleWorld() is a stateless fallback so chunk borders never seam
// ======================================================================
const CSIZE = CHUNK*CHUNK*WORLD_H;
const chunks = new Map(); // "cx,cz" -> { data, group, dirty }
const overrides = new Map(); // "x,y,z" -> id (persistent edits/structures/trees)
const colCache = new Map(); // "x,z" -> column info
const decorated = new Set(); // chunks whose trees were stamped
const ckey = (cx,cz)=> cx+","+cz;
const bkey = (x,y,z)=> x+","+y+","+z;
const cdiv = n => Math.floor(n/CHUNK);
const localIdx = (lx,y,lz)=> (y*CHUNK + lz)*CHUNK + lx;
// write an override (force=false → first writer wins, used by trees so the mythos isn't overwritten)
function ov(x,y,z,id,force){ if(y<0||y>=WORLD_H) return; const k=bkey(x,y,z);
if(!force && overrides.has(k)) return; overrides.set(k,id); }
function columnInfo(x,z){ const k=x+","+z; let c=colCache.get(k); if(c) return c;
const base = fbm(x*0.0065, z*0.0065, 5, 2.0, 0.5); // rolling continents
const ridge = Math.pow(1 - Math.abs(fbm(x*0.012, z*0.012, 3, 2.0, 0.5)), 2); // mountain ridges
let h = SEA + Math.round(base*22) + Math.round(ridge*16) - 3;
h = Math.max(3, Math.min(WORLD_H-12, h));
const temp = perlin2(x*0.0032+50.5, z*0.0032-50.5);
const moist = perlin2(x*0.0032-80.5, z*0.0032+30.5);
const sandy = (h <= SEA+1) || (temp>0.34 && moist<-0.05);
const snowy = (h > SEA+25) || (temp < -0.42 && h>SEA+6);
let topId = sandy ? B.SAND : snowy ? B.SNOW : (h<SEA? B.DIRT : B.GRASS);
c = { h, topId, sandy, snowy }; colCache.set(k,c); return c;
}
function terrainBlock(x,y,z){
if(y<=0) return B.BEDROCK;
if(y>=WORLD_H) return B.AIR;
const info = columnInfo(x,z);
if(y > info.h){ return (y<=SEA) ? B.WATER : B.AIR; }
if(y === info.h) return info.topId;
if(y > info.h-4) return info.sandy ? B.SAND : B.DIRT;
// deep stone with rare veins for flavour
const r = hash2(x*1.7+y*3.1, z*1.3-y*2.7);
if(r < 0.010 && y < SEA-2) return B.LAPIS;
if(r > 0.992) return B.COBBLE;
return B.STONE;
}
// single authoritative lookup (used at borders & by physics/raycast for any coord)
function sampleWorld(x,y,z){ if(y<0) return B.BEDROCK; if(y>=WORLD_H) return B.AIR;
const o = overrides.get(bkey(x,y,z)); if(o!==undefined) return o; return terrainBlock(x,y,z); }
function getBlock(x,y,z){ if(y<0) return B.BEDROCK; if(y>=WORLD_H) return B.AIR;
const ch = chunks.get(ckey(cdiv(x),cdiv(z)));
if(ch){ const lx=x-cdiv(x)*CHUNK, lz=z-cdiv(z)*CHUNK; return ch.data[localIdx(lx,y,lz)]; }
return sampleWorld(x,y,z);
}
// ---------- tree decoration (writes only to overrides; first-writer-wins) ----------
function placeTree(x,by,z){ const r=mulberry32((x*73856093)^(z*19349663));
const th = 4 + ((r()*3)|0);
for(let i=0;i<th;i++) ov(x,by+i,z,B.LOG,false);
const top = by+th;
for(let dy=-2;dy<=1;dy++){ const ly=top+dy; const rad = dy<=0?2:1;
for(let dx=-rad;dx<=rad;dx++)for(let dz=-rad;dz<=rad;dz++){
if(Math.abs(dx)===rad && Math.abs(dz)===rad && r()<0.6) continue; // round the corners
if(dx===0&&dz===0&&dy<0) continue; // keep trunk clear
ov(x+dx, ly, z+dz, B.LEAVES, false);
} }
ov(x,top+1,z,B.LEAVES,false);
}
function decorate(cx,cz){ const k=ckey(cx,cz); if(decorated.has(k)) return; decorated.add(k);
for(let lx=0;lx<CHUNK;lx++)for(let lz=0;lz<CHUNK;lz++){
const x=cx*CHUNK+lx, z=cz*CHUNK+lz;
if(x*x+z*z < 52*52) continue; // keep the mythos plaza clear of forest
const info = columnInfo(x,z);
if(info.topId!==B.GRASS || info.h<=SEA) continue;
if(hash2(x*1.13+0.5, z*2.27+0.5) < 0.016) placeTree(x, info.h+1, z);
}
}
// ======================================================================
// THE MYTHOS — 天悬神殿 · 世界之树 · 巨龙守卫 · 界门 · 悬浮群岛
// A hand-authored fantasy sanctum stamped into the world at the origin.
// ======================================================================
const glowAnchors = []; // positions to seed PointLights
const PY = 38; // plaza floor top
function setb(x,y,z,id){ ov(Math.round(x),Math.round(y),Math.round(z),id,true); }
function box(x0,y0,z0,x1,y1,z1,id){ for(let x=x0;x<=x1;x++)for(let y=y0;y<=y1;y++)for(let z=z0;z<=z1;z++) setb(x,y,z,id); }
function disc(cx,cz,y,r,id,inner){ const r2=r*r, ri2=(inner||0)*(inner||0); for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){ const d=dx*dx+dz*dz; if(d<=r2 && d>=ri2) setb(cx+dx,y,cz+dz,id); } }
function ring(cx,cz,y,r,id){ for(let a=0;a<360;a+=4){ const rad=a*Math.PI/180; setb(cx+Math.round(Math.cos(rad)*r), y, cz+Math.round(Math.sin(rad)*r), id); } }
function cyl(cx,cz,y0,y1,r,id,shell){ const r2=r*r, ri2=(r-1)*(r-1); for(let y=y0;y<=y1;y++)for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){ const d=dx*dx+dz*dz; if(d<=r2 && (!shell || d>=ri2)) setb(cx+dx,y,cz+dz,id); } }
function sphere(cx,cy,cz,r,id,shell){ const r2=(r+0.4)*(r+0.4), ri2=(r-1)*(r-1); for(let dx=-r;dx<=r;dx++)for(let dy=-r;dy<=r;dy++)for(let dz=-r;dz<=r;dz++){ const d=dx*dx+dy*dy+dz*dz; if(d<=r2 && (!shell || d>=ri2)) setb(cx+dx,cy+dy,cz+dz,id); } }
function buildMythos(){
// ---- 0. clear any mountain that would intrude on the floating sanctum ----
for(let dx=-26;dx<=26;dx++)for(let dz=-26;dz<=26;dz++){ if(dx*dx+dz*dz>26*26) continue;
const info=columnInfo(dx,dz); if(info.h>=PY-1){ for(let y=PY-1;y<=info.h+2;y++) setb(dx,y,dz,B.AIR); } }
// ---- 1. great support pillars from the deep up to the platform ----
const sup=[[-14,-14],[14,-14],[-14,14],[14,14],[0,0]];
for(const s of sup){ const center=(s[0]===0&&s[1]===0); cyl(s[0],s[1], 3, PY-1, center?3:2, B.OBSID, !center); }
// ---- 2. the plaza: marble disc + gold rings + runic mandala ----
disc(0,0,PY-1,19,B.STONE); // underside course
disc(0,0,PY,19,B.MARBLE); // main floor
for(const rr of [6,12,17]) ring(0,0,PY,rr,B.GOLD);
disc(0,0,PY,5,B.RUNE); // central mandala core
ring(0,0,PY,5,B.LAPIS); ring(0,0,PY,3,B.LAPIS);
for(let a=0;a<360;a+=45){ const rad=a*Math.PI/180; for(let r=1;r<=17;r++) setb(Math.round(Math.cos(rad)*r),PY,Math.round(Math.sin(rad)*r), (r%6===0)?B.GLOW:B.RUNE); }
for(const m of [[0,17],[0,-17],[17,0],[-17,0]]){ setb(m[0],PY,m[1],B.GLOW); glowAnchors.push([m[0],PY+1,m[1]]); }
// balustrade ring (gaps for the south stair & east gate)
for(let a=0;a<360;a+=3){ const rad=a*Math.PI/180; const x=Math.round(Math.cos(rad)*18.5), z=Math.round(Math.sin(rad)*18.5);
if((z>15 && Math.abs(x)<5) || (x>15 && Math.abs(z)<4)) continue;
setb(x,PY+1,z,B.MARBLE); setb(x,PY+2,z, (a%12===0)?B.GLOW:B.MARBLE); }
// ---- 3. peristyle: 12 marble columns + architrave ----
for(let i=0;i<12;i++){ const a=i*Math.PI/6, cx=Math.round(Math.cos(a)*15), cz=Math.round(Math.sin(a)*15);
cyl(cx,cz,PY+1,PY+9,1,B.MARBLE); setb(cx,PY+1,cz,B.GOLD); disc(cx,cz,PY+10,2,B.MARBLE); setb(cx,PY+10,cz,B.GLOW); glowAnchors.push([cx,PY+11,cz]); }
for(let a=0;a<360;a+=3){ const rad=a*Math.PI/180; setb(Math.round(Math.cos(rad)*15),PY+11,Math.round(Math.sin(rad)*15),B.MARBLE); }
// ---- 4. central altar: stepped pyramid + floating crystal heart + sky beam ----
for(let s=0;s<4;s++) box(-4+s,PY+1+s,-4+s, 4-s,PY+1+s,4-s, s===3?B.GOLD:B.MARBLE);
box(-1,PY+5,-1,1,PY+5,1,B.LAPIS);
sphere(0,PY+9,0,2,B.CRYSTAL,false); setb(0,PY+9,0,B.GLOW); glowAnchors.push([0,PY+9,0]);
cyl(0,0,PY+11,WORLD_H-1,0,B.CRYSTAL); // beacon beam to the sky
setb(0,PY+6,0,B.GLOW); setb(0,PY+7,0,B.CRYSTAL); setb(0,PY+8,0,B.CRYSTAL);
// ---- 5. eight braziers ----
for(let i=0;i<8;i++){ const a=i*Math.PI/4+Math.PI/8, bx=Math.round(Math.cos(a)*10), bz=Math.round(Math.sin(a)*10);
cyl(bx,bz,PY+1,PY+2,1,B.OBSID,true); setb(bx,PY+3,bz,B.GLOW); if(i%2===0) glowAnchors.push([bx,PY+4,bz]); }
buildWorldTree(); buildGrandStair(); buildDragons(); buildGate(); buildFloatingIsles();
}
// ---- World Tree, rising through the sanctum to the north ----
function buildWorldTree(){ const tx=0, tz=-32;
cyl(tx,tz, 3, PY+22, 3, B.LOG); // colossal trunk
for(let a=0;a<360;a+=30){ const rad=a*Math.PI/180; for(let r=3;r<8;r++){ const x=tx+Math.round(Math.cos(rad)*r), z=tz+Math.round(Math.sin(rad)*r); const y=6-Math.round((r-3)*0.9); for(let yy=3;yy<=y+4;yy++) setb(x,yy,z,B.LOG); } } // buttress roots
for(let i=0;i<6;i++){ const a=i*Math.PI/3; for(let t=0;t<10;t++){ const x=tx+Math.round(Math.cos(a)*t), z=tz+Math.round(Math.sin(a)*t), y=PY+16+Math.round(Math.sin(t*0.3)*4); setb(x,y,z,B.LOG); } } // branches
const cy=PY+24; for(const lc of [[-4,8],[0,12],[4,11],[8,8],[11,5]]) sphere(tx,cy+lc[0],tz,lc[1],B.LEAVES,false); // canopy
for(let i=0;i<26;i++){ const a=hash2(i,7)*6.28, p=hash2(i,13)*3.14, r=8+hash2(i,3)*4;
const x=tx+Math.round(Math.cos(a)*Math.sin(p)*r), y=cy+Math.round(Math.cos(p)*r), z=tz+Math.round(Math.sin(a)*Math.sin(p)*r);
setb(x,y,z, i%3===0?B.GLOW:B.CRYSTAL); }
sphere(tx,cy+2,tz,2,B.GLOW,false); glowAnchors.push([tx,cy+2,tz]);
}
// ---- grand staircase descending south from the plaza ----
function buildGrandStair(){ const w=3;
for(let s=0;s<=PY-4;s++){ const z=19+s, y=PY-s;
for(let x=-w;x<=w;x++) setb(x,y,z,B.MARBLE);
setb(-w-1,y,z,B.MARBLE); setb(w+1,y,z,B.MARBLE);
if(s%4===0){ setb(-w-1,y+1,z,B.GLOW); setb(w+1,y+1,z,B.GLOW); }
for(let yy=y-1; yy>Math.max(2,y-6); yy--){ setb(-w-1,yy,z,B.STONE); setb(w+1,yy,z,B.STONE); }
}
}
// ---- twin serpent dragon guardians flanking the stair ----
function buildDragons(){ for(const side of [-1,1]){ const baseX=side*8; let hx=0,hy=0,hz=0;
for(let t=0;t<=44;t++){ const f=t/44;
const x=baseX + side*Math.round(Math.sin(f*Math.PI*3)*3);
const z=20 + t;
const y=PY - Math.round(f*(PY-6)) + Math.round(Math.cos(f*Math.PI*3)*2);
sphere(x,y,z,1,B.OBSID,false);
setb(x,y+2,z, t%6===0?B.CRYSTAL:B.GOLD); // dorsal spine + crystals
hx=x; hy=y; hz=z;
}
sphere(hx,hy+1,hz,2,B.OBSID,false); // reared head
setb(hx-side,hy+2,hz+1,B.GLOW); setb(hx+side,hy+2,hz+1,B.GLOW); glowAnchors.push([hx,hy+2,hz+1]); // eyes
setb(hx-1,hy+4,hz,B.CRYSTAL); setb(hx+1,hy+4,hz,B.CRYSTAL); // horns
box(hx-1,hy,hz+2,hx+1,hy+1,hz+3,B.OBSID); // snout
}}
// ---- Gate of Realms: obsidian arch with a crystal veil, east of plaza ----
function buildGate(){ const gz=0, gx=18;
for(let y=PY+1;y<=PY+9;y++){ setb(gx,y,gz-3,B.OBSID); setb(gx,y,gz+3,B.OBSID); }
for(let z=-3;z<=3;z++){ setb(gx,PY+9,gz+z,B.OBSID); setb(gx,PY+10,gz+z,B.GOLD); }
for(let y=PY+2;y<=PY+8;y++)for(let z=-2;z<=2;z++) setb(gx,y,gz+z, (y+z)%2===0?B.LAPIS:B.CRYSTAL);
setb(gx,PY+11,gz,B.GLOW); glowAnchors.push([gx+1,PY+6,gz]);
for(let i=1;i<=5;i++) box(gx+i,PY,gz-2,gx+i,PY,gz+2,B.MARBLE);
}
// ---- floating crystal isles linked by light bridges ----
function buildFloatingIsles(){ const isles=[[26,48,-6,5],[-28,52,8,6],[10,58,30,4],[-20,44,-26,5]];
for(const il of isles){ const ix=il[0],iy=il[1],iz=il[2],r=il[3];
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){ const d=Math.hypot(dx,dz); if(d>r) continue;
const depth=Math.round((1-d/r)*3); for(let dy=0;dy<=depth;dy++) setb(ix+dx,iy-dy,iz+dz, dy===0?(d>r-1.5?B.STONE:B.GRASS):dy<2?B.DIRT:B.STONE); }
cyl(ix,iz,iy+1,iy+5,0,B.CRYSTAL); sphere(ix,iy+6,iz,2,B.CRYSTAL,true); setb(ix,iy+4,iz,B.GLOW); glowAnchors.push([ix,iy+5,iz]);
const steps=Math.max(Math.abs(ix),Math.abs(iz));
for(let t=0;t<=steps;t++){ const f=t/steps; const x=Math.round(ix*(1-f)), z=Math.round(iz*(1-f)), y=Math.round(iy*(1-f)+(PY+1)*f);
setb(x,y-1,z,B.GLOW); setb(x,y-1,z+1,B.MARBLE); setb(x,y-1,z-1,B.MARBLE); }
}
}
// ======================================================================
// CHUNK GENERATION + MESHING (face-culled, per-vertex AO, atlas UVs)
// ======================================================================
const FACES = [
{ dir:[ 1,0,0], na:0, verts:[[1,1,0],[1,1,1],[1,0,1],[1,0,0]], uv:[[0,0],[1,0],[1,1],[0,1]], shade:0.74 },
{ dir:[-1,0,0], na:0, verts:[[0,1,1],[0,1,0],[0,0,0],[0,0,1]], uv:[[1,0],[0,0],[0,1],[1,1]], shade:0.74 },
{ dir:[0, 1,0], na:1, verts:[[0,1,1],[1,1,1],[1,1,0],[0,1,0]], uv:[[0,1],[1,1],[1,0],[0,0]], shade:1.00 },
{ dir:[0,-1,0], na:1, verts:[[0,0,0],[1,0,0],[1,0,1],[0,0,1]], uv:[[0,0],[1,0],[1,1],[0,1]], shade:0.55 },
{ dir:[0,0, 1], na:2, verts:[[0,0,1],[1,0,1],[1,1,1],[0,1,1]], uv:[[0,1],[1,1],[1,0],[0,0]], shade:0.86 },
{ dir:[0,0,-1], na:2, verts:[[1,0,0],[0,0,0],[0,1,0],[1,1,0]], uv:[[1,1],[0,1],[0,0],[1,0]], shade:0.86 },
];
const AO_LEVEL = [0.46,0.66,0.84,1.0];
function aoLevel(s1,s2,c){ if(s1&&s2) return 0; return 3-((s1?1:0)+(s2?1:0)+(c?1:0)); }
function bufferFor(id){ if(id===B.WATER) return 'glass'; if(id===B.CRYSTAL||id===B.GLOW||id===B.RUNE) return 'glow'; return 'opaque'; }
function faceTile(id,fi){ const f=FACE[id]; return fi===2?f.top : fi===3?f.bottom : f.side; }
function generateChunk(cx,cz){
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++) decorate(cx+dx,cz+dz);
const data = new Uint8Array(CSIZE);
for(let lx=0;lx<CHUNK;lx++)for(let lz=0;lz<CHUNK;lz++){ const x=cx*CHUNK+lx, z=cz*CHUNK+lz;
for(let y=0;y<WORLD_H;y++) data[localIdx(lx,y,lz)] = sampleWorld(x,y,z); }
return data;
}
function newBuf(){ return { pos:[], nor:[], uv:[], col:[], idx:[] }; }
function addMesh(group, buf, mat){ if(buf.idx.length===0) return;
const g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.Float32BufferAttribute(buf.pos,3));
g.setAttribute('normal', new THREE.Float32BufferAttribute(buf.nor,3));
g.setAttribute('uv', new THREE.Float32BufferAttribute(buf.uv,2));
g.setAttribute('color', new THREE.Float32BufferAttribute(buf.col,3));
g.setIndex(new THREE.BufferAttribute(new Uint32Array(buf.idx),1));
group.add(new THREE.Mesh(g, mat));
}
function buildChunkMeshes(cx,cz,data){
const bufs = { opaque:newBuf(), glass:newBuf(), glow:newBuf() };
for(let lx=0;lx<CHUNK;lx++)for(let lz=0;lz<CHUNK;lz++)for(let y=0;y<WORLD_H;y++){
const id = data[localIdx(lx,y,lz)]; if(id===B.AIR) continue;
const x=cx*CHUNK+lx, z=cz*CHUNK+lz; const buf=bufs[bufferFor(id)]; const selfTrans=TRANSPARENT.has(id);
for(let fi=0; fi<6; fi++){ const F=FACES[fi];
const nb = getBlock(x+F.dir[0], y+F.dir[1], z+F.dir[2]);
if(nb!==B.AIR){ if(isOpaque(nb)) continue; if(selfTrans && nb===id) continue; }
const uvr = tileUV(faceTile(id,fi)); const u0=uvr[0],v0=uvr[1],u1=uvr[2],v1=uvr[3];
const isGlow=(buf===bufs.glow), base=buf.pos.length/3;
const ta=(F.na+1)%3, tb=(F.na+2)%3;
for(let k=0;k<4;k++){ const vo=F.verts[k];
buf.pos.push(x+vo[0], y+vo[1], z+vo[2]);
buf.nor.push(F.dir[0],F.dir[1],F.dir[2]);
buf.uv.push(u0 + F.uv[k][0]*(u1-u0), v0 + F.uv[k][1]*(v1-v0));
const dA=vo[ta]===1?1:-1, dB=vo[tb]===1?1:-1;
const sa=[x+F.dir[0],y+F.dir[1],z+F.dir[2]]; sa[ta]+=dA;
const sb=[x+F.dir[0],y+F.dir[1],z+F.dir[2]]; sb[tb]+=dB;
const sc=[x+F.dir[0],y+F.dir[1],z+F.dir[2]]; sc[ta]+=dA; sc[tb]+=dB;
const lv = AO_LEVEL[aoLevel(isOpaque(getBlock(sa[0],sa[1],sa[2])), isOpaque(getBlock(sb[0],sb[1],sb[2])), isOpaque(getBlock(sc[0],sc[1],sc[2])))];
const b = isGlow ? (0.84 + 0.16*lv) : (F.shade * lv);
buf.col.push(b,b,b);
}
buf.idx.push(base,base+1,base+2, base,base+2,base+3);
}
}
const group = new THREE.Group();
addMesh(group, bufs.opaque, matOpaque);
addMesh(group, bufs.glass, matGlass);
addMesh(group, bufs.glow, matGlow);
return group;
}
// ======================================================================
// RENDERER · SCENE · SKY · LIGHTS
// ======================================================================
const canvas = document.getElementById('game');
const renderer = new THREE.WebGLRenderer({ canvas, antialias:true, powerPreference:'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio||1, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
const scene = new THREE.Scene();
// gradient sky as a screen backdrop
(function sky(){ const c=document.createElement('canvas'); c.width=4; c.height=256; const g=c.getContext('2d');
const grd=g.createLinearGradient(0,0,0,256); grd.addColorStop(0,'#3f6fc4'); grd.addColorStop(0.45,'#7ea6e6'); grd.addColorStop(0.72,'#bcd4f2'); grd.addColorStop(1,'#e6eef9');
g.fillStyle=grd; g.fillRect(0,0,4,256); const t=new THREE.CanvasTexture(c); t.colorSpace=THREE.SRGBColorSpace; scene.background=t; })();
scene.fog = new THREE.Fog(0xcadcf2, CHUNK*2.4, (RENDER_DIST-0.4)*CHUNK);
const camera = new THREE.PerspectiveCamera(74, innerWidth/innerHeight, 0.08, 1200);
scene.add(new THREE.HemisphereLight(0xbcd6ff, 0x6b6450, 0.95));
const sun = new THREE.DirectionalLight(0xfff3d4, 0.65); sun.position.set(60,120,40); scene.add(sun);
scene.add(new THREE.AmbientLight(0x3b4666, 0.35));
// ======================================================================
// PLAYER (feet-anchored AABB + voxel collision)
// ======================================================================
const HW=0.3, PH=1.8, EYE=1.62, GRAV=30, JUMP=9.0, WALK=5.6, SPRINT=9.8, FLY=15;
const controls = new PointerLockControls(camera, document.body);
scene.add(camera); // PointerLockControls drives the camera directly
const P = new THREE.Vector3(0.5, PY+1, 14.5); // feet
const vel = new THREE.Vector3();
const keys = new Set(); let flying=false, onGround=false;
function collides(fx,fy,fz){
const x0=Math.floor(fx-HW), x1=Math.floor(fx+HW), z0=Math.floor(fz-HW), z1=Math.floor(fz+HW);
const y0=Math.floor(fy+0.001), y1=Math.floor(fy+PH-0.001);
for(let x=x0;x<=x1;x++)for(let y=y0;y<=y1;y++)for(let z=z0;z<=z1;z++) if(isSolid(getBlock(x,y,z))) return true;
return false;
}
function moveResolve(dx,dy,dz){
const n=Math.max(1, Math.ceil(Math.max(Math.abs(dx),Math.abs(dy),Math.abs(dz))/0.2));
const sx=dx/n, sy=dy/n, sz=dz/n;
for(let i=0;i<n;i++){
P.x+=sx; if(collides(P.x,P.y,P.z)){ P.x-=sx; vel.x=0; }
P.y+=sy; if(collides(P.x,P.y,P.z)){ P.y-=sy; vel.y=0; }
P.z+=sz; if(collides(P.x,P.y,P.z)){ P.z-=sz; vel.z=0; }
}
}
const _f=new THREE.Vector3(), _r=new THREE.Vector3();
function updatePlayer(dt){
_f.set(0,0,-1).applyQuaternion(camera.quaternion); _f.y=0; _f.normalize();
_r.set(1,0,0).applyQuaternion(camera.quaternion); _r.y=0; _r.normalize();
let wx=0,wz=0;
if(keys.has('KeyW')){wx+=_f.x;wz+=_f.z;} if(keys.has('KeyS')){wx-=_f.x;wz-=_f.z;}
if(keys.has('KeyD')){wx+=_r.x;wz+=_r.z;} if(keys.has('KeyA')){wx-=_r.x;wz-=_r.z;}
const wl=Math.hypot(wx,wz); if(wl>0){wx/=wl;wz/=wl;}
const sprint = keys.has('ShiftLeft')||keys.has('ShiftRight');
const speed = flying ? (sprint?FLY*1.7:FLY) : (sprint?SPRINT:WALK);
const acc = flying?0.18 : (onGround?0.32:0.12);
vel.x += (wx*speed - vel.x)*acc; vel.z += (wz*speed - vel.z)*acc;
if(flying){
const up=(keys.has('Space')?1:0)-((keys.has('ShiftLeft')||keys.has('ShiftRight'))?1:0);
vel.y += (up*FLY - vel.y)*0.18;
} else {
vel.y -= GRAV*dt;
if(keys.has('Space') && onGround){ vel.y = JUMP; onGround=false; sfx('jump'); }
}
moveResolve(vel.x*dt, vel.y*dt, vel.z*dt);
onGround = !flying && collides(P.x, P.y-0.06, P.z);
if(onGround && vel.y<0) vel.y=0;
if(P.y < -40){ P.set(0.5,PY+2,14.5); vel.set(0,0,0); } // safety net
camera.position.set(P.x, P.y+EYE, P.z);
}
// ---------- voxel DDA raycast (Amanatides & Woo) ----------
const _dir=new THREE.Vector3();
function rayVoxel(maxD){
const o=camera.position; camera.getWorldDirection(_dir); const d=_dir;
let x=Math.floor(o.x), y=Math.floor(o.y), z=Math.floor(o.z);
const stepX=Math.sign(d.x), stepY=Math.sign(d.y), stepZ=Math.sign(d.z);
const tDX=d.x!==0?Math.abs(1/d.x):Infinity, tDY=d.y!==0?Math.abs(1/d.y):Infinity, tDZ=d.z!==0?Math.abs(1/d.z):Infinity;
let tMaxX = stepX!==0 ? ((stepX>0?(x+1-o.x):(o.x-x))*tDX) : Infinity;
let tMaxY = stepY!==0 ? ((stepY>0?(y+1-o.y):(o.y-y))*tDY) : Infinity;
let tMaxZ = stepZ!==0 ? ((stepZ>0?(z+1-o.z):(o.z-z))*tDZ) : Infinity;
let nx=0,ny=0,nz=0, t=0;
for(let i=0;i<256 && t<=maxD;i++){
const b=getBlock(x,y,z);
if(b!==B.AIR && b!==B.WATER) return {x,y,z,nx,ny,nz,id:b};
if(tMaxX<tMaxY && tMaxX<tMaxZ){ x+=stepX; t=tMaxX; tMaxX+=tDX; nx=-stepX; ny=0; nz=0; }
else if(tMaxY<tMaxZ){ y+=stepY; t=tMaxY; tMaxY+=tDY; nx=0; ny=-stepY; nz=0; }
else { z+=stepZ; t=tMaxZ; tMaxZ+=tDZ; nx=0; ny=0; nz=-stepZ; }
}
return null;
}
// ======================================================================
// CHUNK STREAMING
// ======================================================================
const loadQueue = [];
function disposeChunk(ch){ scene.remove(ch.group); ch.group.children.forEach(m=>m.geometry.dispose()); }
function updateChunks(){
const ncx=cdiv(P.x), ncz=cdiv(P.z);
for(const [key,ch] of chunks){ const p=key.split(','), a=+p[0], b=+p[1];
if(Math.max(Math.abs(a-ncx),Math.abs(b-ncz))>RENDER_DIST+1){ disposeChunk(ch); chunks.delete(key); } }
loadQueue.length=0;
for(let a=ncx-RENDER_DIST;a<=ncx+RENDER_DIST;a++)for(let b=ncz-RENDER_DIST;b<=ncz+RENDER_DIST;b++){
const dd=(a-ncx)*(a-ncx)+(b-ncz)*(b-ncz); if(dd>RENDER_DIST*RENDER_DIST) continue;
if(!chunks.has(ckey(a,b))) loadQueue.push([a,b,dd]); }
loadQueue.sort((p,q)=>p[2]-q[2]);
}
function processQueue(budget){ let n=0;
while(n<budget && loadQueue.length){ const it=loadQueue.shift(); const a=it[0],b=it[1]; if(chunks.has(ckey(a,b))) continue;
const data=generateChunk(a,b); const group=buildChunkMeshes(a,b,data); chunks.set(ckey(a,b),{data,group}); scene.add(group); n++; } }
function rebuildChunk(cx,cz){ const ch=chunks.get(ckey(cx,cz)); if(!ch) return;
const ng=buildChunkMeshes(cx,cz,ch.data); const old=ch.group; ch.group=ng; scene.add(ng);
if(old){ scene.remove(old); old.children.forEach(m=>m.geometry.dispose()); } }
function editBlock(x,y,z,id){
overrides.set(bkey(x,y,z), id);
const cx=cdiv(x), cz=cdiv(z), lx=x-cx*CHUNK, lz=z-cz*CHUNK;
const ch=chunks.get(ckey(cx,cz)); if(ch){ ch.data[localIdx(lx,y,lz)]=id; rebuildChunk(cx,cz); }
if(lx===0) rebuildChunk(cx-1,cz); if(lx===CHUNK-1) rebuildChunk(cx+1,cz);
if(lz===0) rebuildChunk(cx,cz-1); if(lz===CHUNK-1) rebuildChunk(cx,cz+1);
}
function playerOccupies(x,y,z){
const x0=Math.floor(P.x-HW),x1=Math.floor(P.x+HW),z0=Math.floor(P.z-HW),z1=Math.floor(P.z+HW);
const y0=Math.floor(P.y+0.001),y1=Math.floor(P.y+PH-0.001);
return x>=x0&&x<=x1&&y>=y0&&y<=y1&&z>=z0&&z<=z1;
}
// ---------- target highlight ----------
const highlight = new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.003,1.003,1.003)),
new THREE.LineBasicMaterial({ color:0x05060a, transparent:true, opacity:0.4 }));
highlight.visible=false; scene.add(highlight);
function updateHighlight(){ const h=rayVoxel(7); if(h){ highlight.visible=true; highlight.position.set(h.x+0.5,h.y+0.5,h.z+0.5);} else highlight.visible=false; }
// ---------- particles ----------
const TINT={}; TINT[B.GRASS]=0x5a9637;TINT[B.DIRT]=0x866043;TINT[B.STONE]=0x808085;TINT[B.SAND]=0xdbcb8c;TINT[B.LOG]=0x6b4f2a;TINT[B.LEAVES]=0x366e28;TINT[B.PLANK]=0xb0864e;TINT[B.COBBLE]=0x6f6f73;TINT[B.MARBLE]=0xe8e6df;TINT[B.GLOW]=0xffce6b;TINT[B.CRYSTAL]=0x96ebff;TINT[B.GOLD]=0xffd24a;TINT[B.OBSID]=0x1a1426;TINT[B.RUNE]=0x78f0ff;TINT[B.SNOW]=0xeef4fb;TINT[B.LAPIS]=0x2846b0;TINT[B.BEDROCK]=0x3a3a3a;
const pGeo=new THREE.BoxGeometry(0.16,0.16,0.16); const parts=[];
function spawnParts(x,y,z,id){ if(parts.length>70) return; const col=TINT[id]||0x888888;
for(let i=0;i<7;i++){ const m=new THREE.Mesh(pGeo, new THREE.MeshLambertMaterial({color:col}));
m.position.set(x+0.5+(Math.random()-0.5)*0.5, y+0.5+(Math.random()-0.5)*0.5, z+0.5+(Math.random()-0.5)*0.5);
scene.add(m); parts.push({m, v:new THREE.Vector3((Math.random()-0.5)*4,Math.random()*4+1,(Math.random()-0.5)*4), life:0.6}); } }
function updateParts(dt){ for(let i=parts.length-1;i>=0;i--){ const p=parts[i]; p.life-=dt; p.v.y-=22*dt;
p.m.position.addScaledVector(p.v,dt); p.m.scale.setScalar(Math.max(0.01,p.life/0.6));
if(p.life<=0){ scene.remove(p.m); p.m.material.dispose(); parts.splice(i,1); } } }
// ---------- audio ----------
let audioCtx=null;
function ensureAudio(){ if(!audioCtx){ try{ audioCtx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } }
function sfx(kind){ if(!audioCtx) return; const t=audioCtx.currentTime; const o=audioCtx.createOscillator(), g=audioCtx.createGain(); o.connect(g); g.connect(audioCtx.destination);
if(kind==='break'){ o.type='square'; o.frequency.setValueAtTime(190,t); o.frequency.exponentialRampToValueAtTime(70,t+0.12); g.gain.setValueAtTime(0.12,t); }
else if(kind==='place'){ o.type='square'; o.frequency.setValueAtTime(260,t); o.frequency.exponentialRampToValueAtTime(360,t+0.08); g.gain.setValueAtTime(0.10,t); }
else { o.type='sine'; o.frequency.setValueAtTime(420,t); g.gain.setValueAtTime(0.06,t); }
g.gain.exponentialRampToValueAtTime(0.0008,t+0.16); o.start(t); o.stop(t+0.17); }
// ======================================================================
// HOTBAR + INPUT
// ======================================================================
const palette=[ {id:B.GRASS,zh:'草地'},{id:B.DIRT,zh:'泥土'},{id:B.STONE,zh:'石头'},{id:B.COBBLE,zh:'圆石'},
{id:B.PLANK,zh:'木板'},{id:B.MARBLE,zh:'神殿石'},{id:B.GLOW,zh:'萤石'},{id:B.CRYSTAL,zh:'水晶'},{id:B.OBSID,zh:'黑曜石'} ];
let sel=0; const hotbarEl=document.getElementById('hotbar'); const slotEls=[];
palette.forEach((p,i)=>{ const slot=document.createElement('div'); slot.className='slot';
const c=document.createElement('canvas'); c.width=32;c.height=32; const g=c.getContext('2d'); g.imageSmoothingEnabled=false;
const tile=FACE[p.id].side, col=(tile%COLS)*TILE, row=((tile/COLS)|0)*TILE; g.drawImage(atlas,col,row,TILE,TILE,0,0,32,32);
slot.innerHTML='<span class="key">'+(i+1)+'</span><span class="nm">'+p.zh+'</span>'; slot.insertBefore(c,slot.firstChild);
hotbarEl.appendChild(slot); slotEls.push(slot); });
function updateHotbar(){ slotEls.forEach((s,i)=>s.classList.toggle('active', i===sel)); }
updateHotbar();
addEventListener('keydown', e=>{
if(e.code==='KeyF'){ flying=!flying; vel.y=0; }
if(/^Digit[1-9]$/.test(e.code)){ const n=+e.code.slice(5)-1; if(n<palette.length){ sel=n; updateHotbar(); } }
if(e.code==='Space' && controls.isLocked) e.preventDefault();
keys.add(e.code);
});
addEventListener('keyup', e=>keys.delete(e.code));
addEventListener('wheel', e=>{ if(!controls.isLocked) return; e.preventDefault(); sel=(sel+(e.deltaY>0?1:-1)+palette.length)%palette.length; updateHotbar(); }, {passive:false});
addEventListener('contextmenu', e=>e.preventDefault());
addEventListener('mousedown', e=>{ if(!controls.isLocked) return; const h=rayVoxel(7); if(!h) return;
if(e.button===0){ if(h.id===B.BEDROCK) return; spawnParts(h.x,h.y,h.z,h.id); editBlock(h.x,h.y,h.z,B.AIR); sfx('break'); }
else if(e.button===2){ const px=h.x+h.nx,py=h.y+h.ny,pz=h.z+h.nz; if(py<0||py>=WORLD_H) return; if(getBlock(px,py,pz)!==B.AIR) return; if(playerOccupies(px,py,pz)) return; editBlock(px,py,pz,palette[sel].id); sfx('place'); } });
// ======================================================================
// KEY LIGHTS · HUD · BOOT · LOOP
// ======================================================================
function addKeyLights(){ const L=[ [0,PY+9,0,0xffe6b0,16,24],[0,PY+26,-32,0x9fe8ff,20,34],[18.5,PY+6,0,0xbfe9ff,12,18],
[10,PY+4,0,0xffd9a0,9,14],[-10,PY+4,0,0xffd9a0,9,14],[0,PY+3,0,0xffe6b0,10,16] ];
for(const l of L){ const pl=new THREE.PointLight(l[3],l[4],l[5],1.7); pl.position.set(l[0],l[1],l[2]); scene.add(pl); } }
const elStat=document.getElementById('stat'), elBiome=document.getElementById('biome'), elFps=document.getElementById('fps');
const veil=document.getElementById('veil'), playBtn=document.getElementById('play'), barfill=document.getElementById('barfill'), bar=document.getElementById('bar'), flash=document.getElementById('flash');
function biomeName(id){ return id===B.SAND?'沙漠 · 海滩 Sand':id===B.SNOW?'雪峰 Snow':id===B.GRASS?'草甸 Meadow':'丘陵 Hills'; }
let fAcc=0, fCnt=0, hAcc=0;
function updateHUD(dt){ fAcc+=dt; fCnt++; hAcc+=dt;
if(hAcc>0.25){ const ci=columnInfo(Math.floor(P.x),Math.floor(P.z));
elStat.textContent='坐标 '+P.x.toFixed(1)+', '+P.y.toFixed(1)+', '+P.z.toFixed(1)+(flying?' ✦飞行':'');
elBiome.textContent=biomeName(ci.topId)+' · 海拔 '+ci.h;
elFps.textContent=Math.round(fCnt/fAcc)+' fps · 已加载 '+chunks.size+' 区块';
fAcc=0; fCnt=0; hAcc=0; }
}
controls.addEventListener('lock', ()=>{ running=true; ensureAudio(); veil.classList.add('hide'); bar.style.display='none'; });
controls.addEventListener('unlock', ()=>{ running=false; veil.classList.remove('hide'); playBtn.textContent='继续 · RESUME'; });
playBtn.addEventListener('click', ()=>{ if(!playBtn.disabled){ ensureAudio(); controls.lock(); } });
addEventListener('resize', ()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); });
let running=false; const clock=new THREE.Clock();
function animate(){ requestAnimationFrame(animate); const dt=Math.min(clock.getDelta(),0.05);
if(running){ updatePlayer(dt); updateChunks(); processQueue(2); updateHighlight(); updateParts(dt); updateHUD(dt); }
renderer.render(scene, camera); }
// ---- boot: stamp the mythos, stream the spawn region, then open the gate ----
function boot(){ buildMythos(); addKeyLights();
const list=[]; const r=3; for(let a=-r;a<=r;a++)for(let b=-r;b<=r;b++) list.push([a,b,a*a+b*b]); list.sort((p,q)=>p[2]-q[2]);
let bi=0;
(function step(){ const t0=performance.now();
while(bi<list.length && performance.now()-t0<22){ const a=list[bi][0], b=list[bi][1]; bi++;
const data=generateChunk(a,b); const group=buildChunkMeshes(a,b,data); chunks.set(ckey(a,b),{data,group}); scene.add(group); }
barfill.style.width=Math.round(bi/list.length*100)+'%';
if(bi<list.length){ setTimeout(step,0); }
else { camera.position.set(P.x,P.y+EYE,P.z); playBtn.disabled=false; playBtn.textContent='进入神域 · ENTER'; }
})();
animate();
}
requestAnimationFrame(boot);
</script>
</body>
</html>
File renamed without changes.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

-375
View File
@@ -1,375 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>方块秘境 · Voxel Mythos</title>
<style>
:root { --acid:#c8ff00; --night:#0c0e14; --ink:#e9f0ff; }
* { margin:0; padding:0; box-sizing:border-box; }
html, body { width:100%; height:100%; overflow:hidden; background:#0c0e14; }
body { font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; color:var(--ink); -webkit-user-select:none; user-select:none; touch-action:none; }
canvas { display:block; }
#game { position:fixed; inset:0; }
/* Crosshair */
#cross { position:fixed; left:50%; top:50%; width:22px; height:22px; transform:translate(-50%,-50%); pointer-events:none; z-index:20; mix-blend-mode:difference; opacity:.9; }
#cross::before, #cross::after { content:""; position:absolute; background:#fff; }
#cross::before { left:50%; top:0; width:2px; height:100%; transform:translateX(-50%); }
#cross::after { top:50%; left:0; height:2px; width:100%; transform:translateY(-50%); }
/* HUD top-left */
#hud { position:fixed; left:14px; top:12px; z-index:20; font-size:12px; line-height:1.55; text-shadow:0 1px 2px rgba(0,0,0,.8); pointer-events:none; }
#hud b { color:var(--acid); font-weight:700; }
#hud .dim { color:#9aa6c2; }
/* Hotbar */
#hotbar { position:fixed; left:50%; bottom:18px; transform:translateX(-50%); z-index:20; display:flex; gap:6px; padding:6px; background:rgba(8,10,18,.55); border:1px solid rgba(255,255,255,.12); border-radius:12px; backdrop-filter:blur(8px); pointer-events:none; }
.slot { position:relative; width:48px; height:48px; border-radius:8px; background:rgba(255,255,255,.04); border:2px solid rgba(255,255,255,.10); display:flex; align-items:center; justify-content:center; }
.slot.active { border-color:var(--acid); box-shadow:0 0 0 2px rgba(200,255,0,.25), 0 6px 20px rgba(200,255,0,.18); transform:translateY(-3px); }
.slot canvas { width:34px; height:34px; image-rendering:pixelated; border-radius:4px; }
.slot .key { position:absolute; top:-9px; left:-6px; font-size:9px; color:#0c0e14; background:var(--acid); border-radius:5px; padding:0 4px; font-weight:800; }
.slot .nm { position:absolute; bottom:-16px; left:50%; transform:translateX(-50%); font-size:9px; color:#cfd8ef; white-space:nowrap; opacity:0; transition:opacity .15s; }
.slot.active .nm { opacity:1; }
/* Start / pause overlay */
#veil { position:fixed; inset:0; z-index:40; display:flex; align-items:center; justify-content:center; flex-direction:column; gap:22px;
background:radial-gradient(1200px 800px at 50% 20%, rgba(40,60,120,.35), rgba(8,10,18,.92) 60%), #06070c; text-align:center; transition:opacity .4s; }
#veil.hide { opacity:0; pointer-events:none; }
#veil .badge { font-size:11px; letter-spacing:.4em; color:var(--acid); text-transform:uppercase; }
#veil h1 { font-size:clamp(34px, 7vw, 76px); line-height:.96; font-weight:800; letter-spacing:-.02em;
background:linear-gradient(180deg,#fff, #9fb6ff 70%, #6f86ff); -webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:0 0 60px rgba(120,150,255,.25); }
#veil h1 small { display:block; font-size:.34em; letter-spacing:.5em; color:#7d8bb8; -webkit-text-fill-color:#7d8bb8; margin-top:10px; font-weight:600; }
#veil .play { pointer-events:auto; cursor:pointer; font:inherit; font-weight:800; font-size:15px; letter-spacing:.18em; color:#0c0e14; background:var(--acid); border:none; padding:14px 34px; border-radius:999px; text-transform:uppercase; box-shadow:0 14px 40px rgba(200,255,0,.30); transition:transform .15s, box-shadow .15s; }
#veil .play:hover { transform:translateY(-2px); box-shadow:0 18px 50px rgba(200,255,0,.45); }
#veil .play:disabled { background:#39414f; color:#7d8bb8; box-shadow:none; cursor:default; }
#veil .keys { display:flex; gap:18px; flex-wrap:wrap; justify-content:center; max-width:680px; font-size:12px; color:#aab4d0; }
#veil .keys span b { color:#fff; background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.16); border-radius:6px; padding:2px 7px; margin-right:5px; font-weight:700; }
#veil .tip { font-size:11px; color:#6b769a; max-width:560px; line-height:1.7; }
#bar { position:fixed; left:50%; bottom:-2px; transform:translateX(-50%); width:min(420px,70vw); height:3px; background:rgba(255,255,255,.08); z-index:41; border-radius:2px; overflow:hidden; }
#bar i { display:block; height:100%; width:0%; background:var(--acid); transition:width .2s; }
#flash { position:fixed; inset:0; z-index:15; pointer-events:none; background:#fff; opacity:0; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="cross"></div>
<div id="flash"></div>
<div id="hud">
<div><b>方块秘境</b> <span class="dim">Voxel Mythos</span></div>
<div class="dim" id="stat">pos —</div>
<div class="dim" id="biome"></div>
<div class="dim" id="fps"></div>
</div>
<div id="hotbar"></div>
<div id="veil">
<div class="badge">A Hand-built Myth · 神话造物</div>
<h1>方块秘境<small>VOXEL MYTHOS</small></h1>
<button class="play" id="play" disabled>生成世界中…</button>
<div class="keys">
<span><b>W A S D</b>移动</span>
<span><b>SPACE</b>跳跃</span>
<span><b>SHIFT</b>疾跑</span>
<span><b>鼠标</b>环视</span>
<span><b>左键</b>破坏</span>
<span><b>右键</b>放置</span>
<span><b>1-9 / 滚轮</b>切换方块</span>
<span><b>F</b>飞行</span>
</div>
<div class="tip">柏林噪声生成的无限地形之上,矗立着由开发者亲手设计的「天悬神殿」与「世界之树」——光之回廊、水晶尖塔、悬浮岛屿、巨龙守卫与符文法阵环绕其间。降临神域,开始你的造物。</div>
</div>
<div id="bar"><i id="barfill"></i></div>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}}
</script>
<script type="module">
import * as THREE from 'three';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
// ======================================================================
// 方块秘境 · VOXEL MYTHOS — single-file voxel sandbox
// Perlin terrain · first-person · break/place · a hand-designed mythos
// ======================================================================
// ---------- tunables ----------
const CHUNK = 16; // chunk width/depth in blocks
const WORLD_H = 72; // world height
const SEA = 22; // sea level
const RENDER_DIST = 6; // chunk radius around player to keep loaded
const SEED = 1337;
// ---------- block ids ----------
const B = {
AIR:0, GRASS:1, DIRT:2, STONE:3, SAND:4, LOG:5, LEAVES:6, WATER:7,
PLANK:8, COBBLE:9, MARBLE:10, GLOW:11, CRYSTAL:12, GOLD:13, OBSID:14,
RUNE:15, SNOW:16, MOSS:17, LAPIS:18, BEDROCK:19
};
// transparency / pass classification
const TRANSPARENT = new Set([B.AIR, B.WATER, B.LEAVES, B.CRYSTAL]); // don't fully occlude neighbours
const EMISSIVE = new Set([B.GLOW, B.RUNE, B.LAPIS]); // full-bright pass
const GLASSY = new Set([B.WATER, B.CRYSTAL]); // translucent pass
const isAir = id => id === B.AIR;
const isOpaque = id => id !== B.AIR && !TRANSPARENT.has(id);
const isSolid = id => id !== B.AIR && id !== B.WATER; // for collision
// ---------- seeded PRNG ----------
function mulberry32(a){ return function(){ a|=0; a=a+0x6D2B79F5|0; let t=Math.imul(a^a>>>15,1|a); t=t+Math.imul(t^t>>>7,61|t)^t; return ((t^t>>>14)>>>0)/4294967296; }; }
function hash2(x,y){ let h = x*374761393 + y*668265263; h = (h^(h>>>13))*1274126177; return ((h^(h>>>16))>>>0)/4294967296; }
// ---------- Perlin noise (improved, seeded permutation) ----------
const perm = new Uint8Array(512);
(function buildPerm(){
const p = new Uint8Array(256); for(let i=0;i<256;i++) p[i]=i;
const rng = mulberry32(SEED);
for(let i=255;i>0;i--){ const j=(rng()*(i+1))|0; const t=p[i]; p[i]=p[j]; p[j]=t; }
for(let i=0;i<512;i++) perm[i]=p[i&255];
})();
const fade = t => t*t*t*(t*(t*6-15)+10);
const lerp = (a,b,t) => a+(b-a)*t;
function grad2(h,x,y){ switch(h&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; } }
function perlin2(x,y){
const X=Math.floor(x)&255, Y=Math.floor(y)&255; x-=Math.floor(x); y-=Math.floor(y);
const u=fade(x), v=fade(y);
const aa=perm[perm[X]+Y], ab=perm[perm[X]+Y+1], ba=perm[perm[X+1]+Y], bb=perm[perm[X+1]+Y+1];
return lerp( lerp(grad2(aa,x,y), grad2(ba,x-1,y), u), lerp(grad2(ab,x,y-1), grad2(bb,x-1,y-1), u), v );
}
// fractal brownian motion
function fbm(x,y,oct,lac,gain){ let f=0,amp=0.5,freq=1,norm=0; for(let i=0;i<oct;i++){ f+=amp*perlin2(x*freq,y*freq); norm+=amp; amp*=gain; freq*=lac; } return f/norm; }
// ======================================================================
// PROCEDURAL TEXTURE ATLAS — drawn pixel-by-pixel, NearestFilter
// ======================================================================
const TILE = 16, COLS = 8;
// tile indices
const T = { GRASS_TOP:0, GRASS_SIDE:1, DIRT:2, STONE:3, SAND:4, LOG_TOP:5, LOG_SIDE:6, LEAVES:7,
WATER:8, PLANK:9, COBBLE:10, MARBLE:11, GLOW:12, CRYSTAL:13, GOLD:14, OBSID:15,
RUNE:16, SNOW:17, MOSS:18, LAPIS:19, BEDROCK:20, MARBLE_GOLD:21 };
const TILE_COUNT = 22, ROWS = Math.ceil(TILE_COUNT/COLS);
const atlas = document.createElement('canvas'); atlas.width = COLS*TILE; atlas.height = ROWS*TILE;
const actx = atlas.getContext('2d');
// helpers operating on a TILE×TILE off-screen, then blitted into atlas at index
function makeTile(index, draw){
const c = document.createElement('canvas'); c.width=TILE; c.height=TILE; const g=c.getContext('2d');
const img = g.createImageData(TILE,TILE); const d = img.data;
const put=(x,y,r,gr,b,a=255)=>{ if(x<0||y<0||x>=TILE||y>=TILE)return; const i=(y*TILE+x)*4; d[i]=r;d[i+1]=gr;d[i+2]=b;d[i+3]=a; };
draw(put, mulberry32(SEED*131 + index*7919));
g.putImageData(img,0,0);
const cx=(index%COLS)*TILE, cy=((index/COLS)|0)*TILE;
actx.drawImage(c, cx, cy);
}
// colour util
function vary(base, rng, amt){ const k=(rng()*2-1)*amt; return [clamp8(base[0]+k), clamp8(base[1]+k), clamp8(base[2]+k)]; }
function clamp8(v){ return v<0?0:v>255?255:v|0; }
function mix(a,b,t){ return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t, a[2]+(b[2]-a[2])*t]; }
// ---- individual tile painters ----
function fill(put, base, rng, amt){ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const c=vary(base,rng,amt); put(x,y,c[0],c[1],c[2]); } }
makeTile(T.DIRT, (put,rng)=>{ fill(put,[134,96,67],rng,26);
for(let i=0;i<10;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; const c=vary([96,68,46],rng,18); put(x,y,c[0],c[1],c[2]); } });
makeTile(T.GRASS_TOP, (put,rng)=>{ fill(put,[86,150,58],rng,24);
for(let i=0;i<22;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; const c=rng()<.5?[64,118,42]:[110,176,70]; put(x,y,c[0],c[1],c[2]); } });
makeTile(T.GRASS_SIDE, (put,rng)=>{ // dirt body + green crown w/ dribble
fill(put,[134,96,67],rng,24);
for(let x=0;x<TILE;x++){ const top=2+((rng()*3)|0);
for(let y=0;y<top;y++){ const c=vary([78,142,52],rng,20); put(x,y,c[0],c[1],c[2]); }
if(rng()<.4){ const c=vary([70,128,46],rng,16); put(x,top,c[0],c[1],c[2]); if(rng()<.4) put(x,top+1,c[0]-8,c[1]-8,c[2]-6); } } });
makeTile(T.STONE, (put,rng)=>{ fill(put,[128,128,133],rng,16);
for(let i=0;i<6;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,96,96,102); } });
makeTile(T.COBBLE, (put,rng)=>{ const cell=4; for(let cy=0;cy<TILE;cy+=cell)for(let cx=0;cx<TILE;cx+=cell){ const b=110+((rng()*40)|0);
for(let y=0;y<cell;y++)for(let x=0;x<cell;x++){ const edge=(x===0||y===0); const v=edge?70:b+((rng()*16-8)|0); put(cx+x,cy+y,v,v,v+4); } } });
makeTile(T.SAND, (put,rng)=>{ fill(put,[219,203,140],rng,16);
for(let i=0;i<8;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,198,182,120); } });
makeTile(T.SNOW, (put,rng)=>{ fill(put,[236,242,250],rng,8);
for(let i=0;i<6;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,255,255,255); } });
makeTile(T.LOG_SIDE, (put,rng)=>{ for(let x=0;x<TILE;x++){ const streak=(x%4===0)?-18:0;
for(let y=0;y<TILE;y++){ const c=vary([101,73,42],rng,12); put(x,y,c[0]+streak,c[1]+streak,c[2]+streak); } }
for(let i=0;i<6;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,70,50,28); } });
makeTile(T.LOG_TOP, (put,rng)=>{ const c0=TILE/2-0.5; for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const d=Math.hypot(x-c0,y-c0); const ring=(Math.floor(d)%2===0); const base=ring?[176,140,95]:[150,115,75]; const c=vary(base,rng,10); put(x,y,c[0],c[1],c[2]); }
put(c0|0,c0|0,120,92,58); });
makeTile(T.LEAVES, (put,rng)=>{ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ if(rng()<.10){ put(x,y,0,0,0,0); continue; } const r=rng(); const base=r<.4?[38,82,28]:r<.8?[54,110,40]:[78,140,52]; const c=vary(base,rng,12); put(x,y,c[0],c[1],c[2]); } });
makeTile(T.MOSS, (put,rng)=>{ fill(put,[120,120,124],rng,14);
for(let i=0;i<60;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; if(rng()<.5){ const c=vary([62,116,46],rng,18); put(x,y,c[0],c[1],c[2]); } } });
makeTile(T.WATER, (put,rng)=>{ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const w=Math.sin((x+y)*0.9)*0.5+0.5; const base=mix([34,104,196],[60,140,226],w*0.6+rng()*0.2); put(x,y,base[0]|0,base[1]|0,base[2]|0,210); } });
makeTile(T.PLANK, (put,rng)=>{ fill(put,[176,134,78],rng,14);
for(let y=0;y<TILE;y++){ if(y%4===0){ for(let x=0;x<TILE;x++) put(x,y,120,86,46); } }
for(let i=0;i<10;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,150,112,62); } });
makeTile(T.MARBLE, (put,rng)=>{ fill(put,[232,230,223],rng,7);
let vx=(rng()*TILE)|0; for(let y=0;y<TILE;y++){ vx+=(rng()*3-1)|0; const c=[188,190,198]; put((vx+TILE)%TILE,y,c[0],c[1],c[2]); }
let vx2=(rng()*TILE)|0; for(let y=0;y<TILE;y++){ vx2+=(rng()*3-1)|0; put((vx2+TILE)%TILE,y,200,202,210); } });
makeTile(T.MARBLE_GOLD, (put,rng)=>{ fill(put,[232,230,223],rng,7);
for(let x=0;x<TILE;x++){ put(x,0,255,210,74); put(x,TILE-1,205,160,40); put(x,1,255,236,150); } });
makeTile(T.GLOW, (put,rng)=>{ fill(put,[255,206,107],rng,14);
for(let i=0;i<26;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,255,238,176); }
for(let i=0;i<8;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; put(x,y,224,150,60); } });
makeTile(T.CRYSTAL, (put,rng)=>{ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const band=((x+y)>>1)%3; const base=band===0?[210,250,255]:band===1?[150,235,255]:[96,180,228]; const c=vary(base,rng,10); put(x,y,c[0],c[1],c[2],235); }
for(let i=0;i<5;i++){ const x=(rng()*TILE)|0; put(x,(rng()*TILE)|0,255,255,255,255); } });
makeTile(T.LAPIS, (put,rng)=>{ fill(put,[40,66,176],rng,16);
for(let i=0;i<26;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; const c=rng()<.5?[92,124,232]:[24,40,120]; put(x,y,c[0],c[1],c[2]); } });
makeTile(T.GOLD, (put,rng)=>{ const half=TILE/2; for(let by=0;by<2;by++)for(let bx=0;bx<2;bx++){ for(let y=0;y<half;y++)for(let x=0;x<half;x++){ let c=[255,210,74]; if(x===0||y===0) c=[150,112,30]; else if(x<2&&y<2) c=[255,244,180]; else if(x>half-3&&y>half-3) c=[205,160,40]; const v=vary(c,rng,8); put(bx*half+x,by*half+y,v[0],v[1],v[2]); } } });
makeTile(T.OBSID, (put,rng)=>{ fill(put,[22,18,34],rng,8);
for(let i=0;i<14;i++){ const x=(rng()*TILE)|0,y=(rng()*TILE)|0; const c=vary([74,44,98],rng,18); put(x,y,c[0],c[1],c[2]); }
for(let k=0;k<TILE;k++){ if(k%5===0) put(k,k,120,90,150); } });
makeTile(T.BEDROCK, (put,rng)=>{ for(let y=0;y<TILE;y++)for(let x=0;x<TILE;x++){ const v=30+((rng()*60)|0); put(x,y,v,v,v+3); } });
makeTile(T.RUNE, (put,rng)=>{ fill(put,[58,58,66],rng,12); // dark stone + glowing glyph
const glyph=[ "0001111000","0010000100","0100110010","0101001010","1010000101","1010000101","0101001010","0100110010","0010000100","0001111000" ];
const off=3; for(let y=0;y<10;y++)for(let x=0;x<10;x++){ if(glyph[y][x]==='1'){ const c=vary([120,240,255],rng,16); put(x+off,y+off,c[0],c[1],c[2]); } }
put(8,8,200,250,255); });
// ---------- atlas → texture ----------
const atlasTex = new THREE.CanvasTexture(atlas);
atlasTex.magFilter = THREE.NearestFilter; atlasTex.minFilter = THREE.NearestFilter;
atlasTex.generateMipmaps = false; atlasTex.flipY = false; // canvas (0,0)=top-left maps to UV(0,0)
atlasTex.colorSpace = THREE.SRGBColorSpace; atlasTex.anisotropy = 1;
const EPS = 0.0008; // inset to kill texture bleed
function tileUV(idx){ const col=idx%COLS, row=(idx/COLS)|0;
const u0=(col*TILE)/atlas.width + EPS, v0=(row*TILE)/atlas.height + EPS;
const u1=((col+1)*TILE)/atlas.width - EPS, v1=((row+1)*TILE)/atlas.height - EPS;
return [u0,v0,u1,v1]; // v0=top edge, v1=bottom edge (flipY=false)
}
// block id → face tiles {top, side, bottom}
const FACE = {};
FACE[B.GRASS]={top:T.GRASS_TOP, side:T.GRASS_SIDE, bottom:T.DIRT};
FACE[B.DIRT]={top:T.DIRT,side:T.DIRT,bottom:T.DIRT};
FACE[B.STONE]={top:T.STONE,side:T.STONE,bottom:T.STONE};
FACE[B.SAND]={top:T.SAND,side:T.SAND,bottom:T.SAND};
FACE[B.LOG]={top:T.LOG_TOP,side:T.LOG_SIDE,bottom:T.LOG_TOP};
FACE[B.LEAVES]={top:T.LEAVES,side:T.LEAVES,bottom:T.LEAVES};
FACE[B.WATER]={top:T.WATER,side:T.WATER,bottom:T.WATER};
FACE[B.PLANK]={top:T.PLANK,side:T.PLANK,bottom:T.PLANK};
FACE[B.COBBLE]={top:T.COBBLE,side:T.COBBLE,bottom:T.COBBLE};
FACE[B.MARBLE]={top:T.MARBLE,side:T.MARBLE_GOLD,bottom:T.MARBLE};
FACE[B.GLOW]={top:T.GLOW,side:T.GLOW,bottom:T.GLOW};
FACE[B.CRYSTAL]={top:T.CRYSTAL,side:T.CRYSTAL,bottom:T.CRYSTAL};
FACE[B.GOLD]={top:T.GOLD,side:T.GOLD,bottom:T.GOLD};
FACE[B.OBSID]={top:T.OBSID,side:T.OBSID,bottom:T.OBSID};
FACE[B.RUNE]={top:T.RUNE,side:T.OBSID,bottom:T.OBSID};
FACE[B.SNOW]={top:T.SNOW,side:T.SNOW,bottom:T.SNOW};
FACE[B.MOSS]={top:T.MOSS,side:T.MOSS,bottom:T.MOSS};
FACE[B.LAPIS]={top:T.LAPIS,side:T.LAPIS,bottom:T.LAPIS};
FACE[B.BEDROCK]={top:T.BEDROCK,side:T.BEDROCK,bottom:T.BEDROCK};
// ---------- materials ----------
const matOpaque = new THREE.MeshLambertMaterial({ map:atlasTex, vertexColors:true, alphaTest:0.5, side:THREE.FrontSide });
const matGlass = new THREE.MeshLambertMaterial({ map:atlasTex, vertexColors:true, transparent:true, opacity:0.82, depthWrite:false, side:THREE.DoubleSide });
const matGlow = new THREE.MeshBasicMaterial({ map:atlasTex, vertexColors:true, transparent:true, alphaTest:0.4 });
// ======================================================================
// WORLD MODEL
// - chunks store generated voxel data
// - overrides[] holds everything non-terrain: trees, the mythos, player edits
// - sampleWorld() is a stateless fallback so chunk borders never seam
// ======================================================================
const CSIZE = CHUNK*CHUNK*WORLD_H;
const chunks = new Map(); // "cx,cz" -> { data, group, dirty }
const overrides = new Map(); // "x,y,z" -> id (persistent edits/structures/trees)
const colCache = new Map(); // "x,z" -> column info
const decorated = new Set(); // chunks whose trees were stamped
const ckey = (cx,cz)=> cx+","+cz;
const bkey = (x,y,z)=> x+","+y+","+z;
const cdiv = n => Math.floor(n/CHUNK);
const localIdx = (lx,y,lz)=> (y*CHUNK + lz)*CHUNK + lx;
// write an override (force=false → first writer wins, used by trees so the mythos isn't overwritten)
function ov(x,y,z,id,force){ if(y<0||y>=WORLD_H) return; const k=bkey(x,y,z);
if(!force && overrides.has(k)) return; overrides.set(k,id); }
function columnInfo(x,z){ const k=x+","+z; let c=colCache.get(k); if(c) return c;
const base = fbm(x*0.0065, z*0.0065, 5, 2.0, 0.5); // rolling continents
const ridge = Math.pow(1 - Math.abs(fbm(x*0.012, z*0.012, 3, 2.0, 0.5)), 2); // mountain ridges
let h = SEA + Math.round(base*22) + Math.round(ridge*16) - 3;
h = Math.max(3, Math.min(WORLD_H-12, h));
const temp = perlin2(x*0.0032+50.5, z*0.0032-50.5);
const moist = perlin2(x*0.0032-80.5, z*0.0032+30.5);
const sandy = (h <= SEA+1) || (temp>0.34 && moist<-0.05);
const snowy = (h > SEA+25) || (temp < -0.42 && h>SEA+6);
let topId = sandy ? B.SAND : snowy ? B.SNOW : (h<SEA? B.DIRT : B.GRASS);
c = { h, topId, sandy, snowy }; colCache.set(k,c); return c;
}
function terrainBlock(x,y,z){
if(y<=0) return B.BEDROCK;
if(y>=WORLD_H) return B.AIR;
const info = columnInfo(x,z);
if(y > info.h){ return (y<=SEA) ? B.WATER : B.AIR; }
if(y === info.h) return info.topId;
if(y > info.h-4) return info.sandy ? B.SAND : B.DIRT;
// deep stone with rare veins for flavour
const r = hash2(x*1.7+y*3.1, z*1.3-y*2.7);
if(r < 0.010 && y < SEA-2) return B.LAPIS;
if(r > 0.992) return B.COBBLE;
return B.STONE;
}
// single authoritative lookup (used at borders & by physics/raycast for any coord)
function sampleWorld(x,y,z){ if(y<0) return B.BEDROCK; if(y>=WORLD_H) return B.AIR;
const o = overrides.get(bkey(x,y,z)); if(o!==undefined) return o; return terrainBlock(x,y,z); }
function getBlock(x,y,z){ if(y<0) return B.BEDROCK; if(y>=WORLD_H) return B.AIR;
const ch = chunks.get(ckey(cdiv(x),cdiv(z)));
if(ch){ const lx=x-cdiv(x)*CHUNK, lz=z-cdiv(z)*CHUNK; return ch.data[localIdx(lx,y,lz)]; }
return sampleWorld(x,y,z);
}
// ---------- tree decoration (writes only to overrides; first-writer-wins) ----------
function placeTree(x,by,z){ const r=mulberry32((x*73856093)^(z*19349663));
const th = 4 + ((r()*3)|0);
for(let i=0;i<th;i++) ov(x,by+i,z,B.LOG,false);
const top = by+th;
for(let dy=-2;dy<=1;dy++){ const ly=top+dy; const rad = dy<=0?2:1;
for(let dx=-rad;dx<=rad;dx++)for(let dz=-rad;dz<=rad;dz++){
if(Math.abs(dx)===rad && Math.abs(dz)===rad && r()<0.6) continue; // round the corners
if(dx===0&&dz===0&&dy<0) continue; // keep trunk clear
ov(x+dx, ly, z+dz, B.LEAVES, false);
} }
ov(x,top+1,z,B.LEAVES,false);
}
function decorate(cx,cz){ const k=ckey(cx,cz); if(decorated.has(k)) return; decorated.add(k);
for(let lx=0;lx<CHUNK;lx++)for(let lz=0;lz<CHUNK;lz++){
const x=cx*CHUNK+lx, z=cz*CHUNK+lz;
if(x*x+z*z < 52*52) continue; // keep the mythos plaza clear of forest
const info = columnInfo(x,z);
if(info.topId!==B.GRASS || info.h<=SEA) continue;
if(hash2(x*1.13+0.5, z*2.27+0.5) < 0.016) placeTree(x, info.h+1, z);
}
}
/* @@APPEND@@ */
</script>
</body>
</html>