Files
nagi-bench/outputs/claude-opus-4-8/mythos-craft.html
T

376 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<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>