Files
nagi-bench/outputs/mimo-v2-5-pro-ultraspeed/mythos-craft-2.html
T
OHCJ099 13da28bd8e Add MiMo v2.5 Pro UltraSpeed runs (mythos-craft, pelican-cycling)
Registers MiMo v2.5 Pro UltraSpeed (Xiaomi, Web · Default) with two one-shot variants each for mythos-craft and pelican-cycling. Verified: SVGs render, both Three.js voxel games run, schema validates, README tables synced (zh+en).
2026-06-16 20:20:34 +01:00

919 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<title>Mythicraft — 神话世界</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{overflow:hidden;background:#000;font-family:'Segoe UI',sans-serif;cursor:none}
canvas{display:block;width:100vw;height:100vh}
#overlay{position:fixed;inset:0;background:rgba(5,5,15,.92);display:flex;flex-direction:column;align-items:center;justify-content:center;color:#ccc;z-index:100;cursor:pointer;transition:opacity .4s}
#overlay.hidden{opacity:0;pointer-events:none}
#overlay h1{font-size:clamp(36px,6vw,64px);color:#e8c547;text-shadow:0 0 40px rgba(232,197,71,.4),0 0 80px rgba(232,197,71,.15);letter-spacing:.08em;font-weight:300;margin-bottom:8px}
#overlay .sub{font-size:15px;color:#888;margin-bottom:24px;letter-spacing:.15em}
#overlay .keys{font-size:13px;color:#777;line-height:2}
#overlay .keys b{color:#bbb}
#overlay .start{margin-top:32px;font-size:20px;color:#e8c547;animation:pulse 2.5s infinite}
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
#crosshair{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);pointer-events:none;z-index:10;display:none}
#crosshair::before,#crosshair::after{content:'';position:absolute;background:rgba(255,255,255,.85)}
#crosshair::before{width:18px;height:2px;top:-1px;left:-9px}
#crosshair::after{width:2px;height:18px;top:-9px;left:-1px}
#hotbar{position:fixed;bottom:8px;left:50%;transform:translateX(-50%);display:none;gap:3px;z-index:10}
.slot{width:48px;height:48px;border:2px solid rgba(100,100,100,.5);background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;position:relative;image-rendering:pixelated}
.slot.active{border-color:#e8c547;background:rgba(232,197,71,.15);box-shadow:0 0 12px rgba(232,197,71,.2)}
.slot canvas{width:36px;height:36px;image-rendering:pixelated}
.slot .num{position:absolute;top:2px;left:4px;font-size:10px;color:rgba(255,255,255,.4)}
#debug{position:fixed;top:8px;left:8px;color:rgba(255,255,255,.7);font-size:11px;font-family:'Consolas',monospace;z-index:10;display:none;text-shadow:0 1px 3px rgba(0,0,0,.8);line-height:1.6;pointer-events:none}
</style>
</head>
<body>
<div id="overlay">
<h1>MYTHICRAFT</h1>
<div class="sub">一个充满神话幻想的体素世界</div>
<div class="keys">
<b>W A S D</b> 移动 &nbsp; <b>空格</b> 跳跃 &nbsp; <b>Shift</b> 下潜<br>
<b>鼠标移动</b> 环视 &nbsp; <b>左键</b> 破坏方块 &nbsp; <b>右键</b> 放置方块<br>
<b>1-9 / 滚轮</b> 选择方块 &nbsp; <b>F</b> 切换飞行
</div>
<div class="start">— 点击任意处开始探险 —</div>
</div>
<div id="crosshair"></div>
<div id="hotbar"></div>
<div id="debug"></div>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script>
'use strict';
/* ======================================================
CONSTANTS
====================================================== */
const CS = 16, CH = 80, RD = 6, WL = 28;
const B = {AIR:0,GRASS:1,DIRT:2,STONE:3,SAND:4,WATER:5,WOOD:6,LEAVES:7,BEDROCK:8,GLOW:9,CRYSTAL:10,PLANKS:11,COBBLE:12,BRICK:13,OBSIDIAN:14,SNOW:15,MARBLE:16,MAGIC:17,DSTONE:18,GOLD:19};
const BF = [];
BF[B.GRASS]=[0,1,2]; BF[B.DIRT]=[2,2,2]; BF[B.STONE]=[3,3,3]; BF[B.SAND]=[4,4,4];
BF[B.WATER]=[5,5,5]; BF[B.WOOD]=[6,7,6]; BF[B.LEAVES]=[8,8,8]; BF[B.BEDROCK]=[9,9,9];
BF[B.GLOW]=[10,10,10]; BF[B.CRYSTAL]=[11,11,11]; BF[B.PLANKS]=[12,12,12];
BF[B.COBBLE]=[13,13,13]; BF[B.BRICK]=[14,14,14]; BF[B.OBSIDIAN]=[15,15,15];
BF[B.SNOW]=[16,16,16]; BF[B.MARBLE]=[17,17,17]; BF[B.MAGIC]=[18,18,18];
BF[B.DSTONE]=[19,19,19]; BF[B.GOLD]=[20,20,20];
const TRANSP = new Set([B.AIR,B.WATER,B.LEAVES,B.CRYSTAL,B.GLOW]);
const SOLID = new Set([B.GRASS,B.DIRT,B.STONE,B.SAND,B.WOOD,B.BEDROCK,B.PLANKS,B.COBBLE,B.BRICK,B.OBSIDIAN,B.SNOW,B.MARBLE,B.MAGIC,B.DSTONE,B.GOLD]);
const HOTBAR_ITEMS = [B.GRASS,B.DIRT,B.STONE,B.COBBLE,B.BRICK,B.PLANKS,B.OBSIDIAN,B.GLOW,B.CRYSTAL];
const BN = {};Object.entries(B).forEach(([k,v])=>BN[v]=k);
const AC=16,AR=2,TS=16;
const FDIR=[[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];
const FVERT=[
[[1,0,1],[1,0,0],[1,1,0],[1,1,1]],
[[0,0,0],[0,0,1],[0,1,1],[0,1,0]],
[[0,1,1],[1,1,1],[1,1,0],[0,1,0]],
[[0,0,0],[1,0,0],[1,0,1],[0,0,1]],
[[1,0,1],[0,0,1],[0,1,1],[0,1,1]],
[[0,0,0],[1,0,0],[1,1,0],[0,1,0]]
];
const FUVS=[[0,0],[1,0],[1,1],[0,1]];
/* ======================================================
SIMPLEX NOISE
====================================================== */
class SimplexNoise{
constructor(seed=42){
this.g=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]];
const p=new Uint8Array(256);for(let i=0;i<256;i++)p[i]=i;
let s=seed;for(let i=255;i>0;i--){s=(s*1103515245+12345)&0x7fffffff;const j=s%(i+1);[p[i],p[j]]=[p[j],p[i]];}
this.perm=new Uint8Array(512);this.pm12=new Uint8Array(512);
for(let i=0;i<512;i++){this.perm[i]=p[i&255];this.pm12[i]=this.perm[i]%12;}
}
n2(x,y){
const F=.5*(Math.sqrt(3)-1),G=(3-Math.sqrt(3))/6,s=(x+y)*F,i=Math.floor(x+s),j=Math.floor(y+s),t=(i+j)*G;
const x0=x-(i-t),y0=y-(j-t),i1=x0>y0?1:0,j1=x0>y0?0:1;
const x1=x0-i1+G,y1=y0-j1+G,x2=x0-1+2*G,y2=y0-1+2*G;
const ii=i&255,jj=j&255;
const d=(g,x,y)=>g[0]*x+g[1]*y;
const g0=this.pm12[ii+this.perm[jj]],g1=this.pm12[ii+i1+this.perm[jj+j1]],g2=this.pm12[ii+1+this.perm[jj+1]];
let n0,n1,n2,t0=.5-x0*x0-y0*y0;t0=t0<0?0:(t0*=t0,t0*t0*d(this.g[g0],x0,y0));
let t1=.5-x1*x1-y1*y1;t1=t1<0?0:(t1*=t1,t1*t1*d(this.g[g1],x1,y1));
let t2=.5-x2*x2-y2*y2;t2=t2<0?0:(t2*=t2,t2*t2*d(this.g[g2],x2,y2));
return 70*(t0+t1+t2);
}
n3(x,y,z){
const F=1/3,G=1/6,s=(x+y+z)*F,i=Math.floor(x+s),j=Math.floor(y+s),k=Math.floor(z+s),t=(i+j+k)*G;
const x0=x-(i-t),y0=y-(j-t),z0=z-(k-t);
let i1,j1,k1,i2,j2,k2;
if(x0>=y0){if(y0>=z0){i1=1;j1=0;k1=0;i2=1;j2=1;k2=0}else if(x0>=z0){i1=1;j1=0;k1=0;i2=1;j2=0;k2=1}else{i1=0;j1=0;k1=1;i2=1;j2=0;k2=1}}
else{if(y0<z0){i1=0;j1=0;k1=1;i2=0;j2=1;k2=1}else if(x0<z0){i1=0;j1=1;k1=0;i2=0;j2=1;k2=1}else{i1=0;j1=1;k1=0;i2=1;j2=1;k2=0}}
const x1=x0-i1+G,y1=y0-j1+G,z1=z0-k1+G,x2=x0-i2+2*G,y2=y0-j2+2*G,z2=z0-k2+2*G,x3=x0-1+3*G,y3=y0-1+3*G,z3=z0-1+3*G;
const ii=i&255,jj=j&255,kk=k&255;
const d=(g,a,b,c)=>g[0]*a+g[1]*b+g[2]*c;
const g0=this.pm12[ii+this.perm[jj+this.perm[kk]]],g1=this.pm12[ii+i1+this.perm[jj+j1+this.perm[kk+k1]]];
const g2=this.pm12[ii+i2+this.perm[jj+j2+this.perm[kk+k2]]],g3=this.pm12[ii+1+this.perm[jj+1+this.perm[kk+1]]];
let r=0,t0=.6-x0*x0-y0*y0-z0*z0;if(t0>0){t0*=t0;r+=t0*t0*d(this.g[g0],x0,y0,z0);}
let t1=.6-x1*x1-y1*y1-z1*z1;if(t1>0){t1*=t1;r+=t1*t1*d(this.g[g1],x1,y1,z1);}
let t2=.6-x2*x2-y2*y2-z2*z2;if(t2>0){t2*=t2;r+=t2*t2*d(this.g[g2],x2,y2,z2);}
let t3=.6-x3*x3-y3*y3-z3*z3;if(t3>0){t3*=t3;r+=t3*t3*d(this.g[g3],x3,y3,z3);}
return 32*r;
}
}
/* ======================================================
TEXTURE ATLAS
====================================================== */
function createAtlas(){
const c=document.createElement('canvas');c.width=AC*TS;c.height=AR*TS;
const x=c.getContext('2d');
let rng=12345;const R=()=>{rng=(rng*1103515245+12345)&0x7fffffff;return(rng/0x7fffffff);};
const h=(a,b)=>(Math.sin(a*12.9898+b*78.233)*43758.5453)%1;
function dt(i,fn){
const col=i%AC,row=i/AC|0,img=x.createImageData(TS,TS);
for(let py=0;py<TS;py++)for(let px=0;px<TS;px++){
const[r,g,b]=fn(px,py);const j=(py*TS+px)*4;
img.data[j]=Math.max(0,Math.min(255,r|0));
img.data[j+1]=Math.max(0,Math.min(255,g|0));
img.data[j+2]=Math.max(0,Math.min(255,b|0));
img.data[j+3]=255;
}
x.putImageData(img,col*TS,row*TS);
}
const n=(a,b,s=1)=>{let v=Math.sin(a*12.9898*s+b*78.233*s+43758.5453);return v-Math.floor(v);};
dt(0,(x,y)=>{const v=n(x,y)*35;return[45+v,135+v,28+v*.4]});//grass_top
dt(1,(x,y)=>y<3?[48+n(x,y,1)*25,135+n(x,y,1)*25,28+n(x,y,1)*15]:[118+n(x,y,1)*22,83+n(x,y,1)*22,48+n(x,y,1)*22]);//grass_side
dt(2,(x,y)=>{const v=n(x,y,2)*28;return[118+v,83+v,48+v]});//dirt
dt(3,(x,y)=>{const v=n(x,y,3)*32;return[108+v,108+v,108+v]});//stone
dt(4,(x,y)=>{const v=n(x,y,4)*22;return[208+v,188+v,128+v]});//sand
dt(5,(x,y)=>{const v=n(x,y,5)*18;return[28+v,75+v,195+v]});//water
dt(6,(x,y)=>{const d=Math.sqrt((x-7.5)**2+(y-7.5)**2),r=Math.sin(d*2)*18;return[138+r,98+r,48+r]});//wood_top
dt(7,(x,y)=>{const v=n(x,y,7)*22,s=Math.sin(y*.9)*12;return[98+v+s,68+v+s,33+v]});//wood_side
dt(8,(x,y)=>{const v=n(x,y,8)*45;return[18+v,95+v,12+v*.5]});//leaves
dt(9,(x,y)=>{const v=n(x,y,9)*22;return[28+v,28+v,28+v]});//bedrock
dt(10,(x,y)=>{const v=n(x,y,10)*32,g=Math.sin(x*.6+y*.4)*22;return[228+g,208+v,118+v]});//glow
dt(11,(x,y)=>{const v=n(x,y,11)*35;return[78+v,148+v,228+v*.5]});//crystal
dt(12,(x,y)=>{const v=n(x,y,12)*18,p=(y%4===0)?-35:0;return[168+v+p,128+v+p,68+v+p]});//planks
dt(13,(x,y)=>{const v=n(x,y,13)*42;return[88+v,88+v,88+v]});//cobble
dt(14,(x,y)=>{const v=n(x,y,14)*18,m=((y%4===0)||(x%8===((y<4)?0:4)))?-45:0;return[158+v+m,68+v+m,48+v+m]});//brick
dt(15,(x,y)=>{const v=n(x,y,15)*18;return[13+v,8+v,28+v]});//obsidian
dt(16,(x,y)=>{const v=n(x,y,16)*18;return[228+v,233+v,238+v]});//snow
dt(17,(x,y)=>{const v=n(x,y,17)*22,ve=Math.sin(x*.35+y*.55)*18;return[208+v+ve,208+v+ve,213+v+ve]});//marble
dt(18,(x,y)=>{const v=n(x,y,18)*22,r=Math.sin(x*1.3)*Math.cos(y*1.3)*32;return[38+v+r,18+v,78+v+r]});//magic
dt(19,(x,y)=>{const v=n(x,y,19)*28;return[48+v,43+v,53+v]});//darkstone
dt(20,(x,y)=>{const v=n(x,y,20)*22;return[208+v,178+v,48+v]});//gold
const tex=new THREE.CanvasTexture(c);tex.magFilter=THREE.NearestFilter;tex.minFilter=THREE.NearestFilter;
return{canvas:c,texture:tex};
}
/* ======================================================
CHUNK
====================================================== */
class Chunk{
constructor(cx,cz){this.cx=cx;this.cz=cz;this.blocks=new Uint8Array(CS*CH*CS);this.mesh=null;this.waterMesh=null;this.dirty=true;this.gen=false;}
idx(x,y,z){return y*CS*CS+z*CS+x;}
get(x,y,z){if(x<0||x>=CS||y<0||y>=CH||z<0||z>=CS)return 0;return this.blocks[this.idx(x,y,z)];}
set(x,y,z,t){if(x>=0&&x<CS&&y>=0&&y<CH&&z>=0&&z<CS){this.blocks[this.idx(x,y,z)]=t;this.dirty=true;}}
buildMesh(wgetBlock,mat,wmat,scene){
if(!this.dirty)return;this.dirty=false;
if(this.mesh){this.mesh.geometry.dispose();scene.remove(this.mesh);}
if(this.waterMesh){this.waterMesh.geometry.dispose();scene.remove(this.waterMesh);}
const P=[],N=[],U=[],I=[],wP=[],wN=[],wU=[],wI=[];
let vc=0,wvc=0;
const ox=this.cx*CS,oz=this.cz*CS;
for(let y=0;y<CH;y++)for(let z=0;z<CS;z++)for(let x=0;x<CS;x++){
const b=this.blocks[this.idx(x,y,z)];if(b===0)continue;
const faces=BF[b];if(!faces)continue;
const isW=b===B.WATER,wx=ox+x,wz=oz+z;
const tArr=[faces[0],faces[1],faces[1],faces[1],faces[1],faces[1]];
for(let f=0;f<6;f++){
const[dx,dy,dz]=FDIR[f],nx=x+dx,ny=y+dy,nz=z+dz;
let nb;
if(nx<0||nx>=CS||nz<0||nz>=CS)nb=wgetBlock(wx+dx,y+dy,wz+dz);
else if(ny<0||ny>=CH)nb=ny<0?B.BEDROCK:B.AIR;
else nb=this.blocks[this.idx(nx,ny,nz)];
if(isW){if(nb===B.WATER||(!TRANSP.has(nb)&&nb!==0))continue;}
else{if(!TRANSP.has(nb)||nb===b)continue;}
const ti=tArr[f],col=ti%AC,row=ti/AC|0;
const u0=col/AC,u1=(col+1)/AC,v0=1-(row+1)/AR,v1=1-row/AR;
const tp=isW?wP:P,tn=isW?wN:N,tu=isW?wU:U,ti2=isW?wI:I;
let vc2=isW?wvc:vc;
const fv=FVERT[f],fu=FUVS;
for(let v=0;v<4;v++){
tp.push(ox+x+fv[v][0],y+fv[v][1],oz+z+fv[v][2]);
tn.push(FDIR[f][0],FDIR[f][1],FDIR[f][2]);
tu.push(u0+fu[v][0]*(u1-u0),v0+fu[v][1]*(v1-v0));
}
ti2.push(vc2,vc2+1,vc2+2,vc2,vc2+2,vc2+3);
if(isW)wvc+=4;else vc+=4;
}
}
if(P.length){
const g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.Float32BufferAttribute(P,3));
g.setAttribute('normal',new THREE.Float32BufferAttribute(N,3));
g.setAttribute('uv',new THREE.Float32BufferAttribute(U,2));
g.setIndex(I);
this.mesh=new THREE.Mesh(g,mat);scene.add(this.mesh);
}
if(wP.length){
const g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.Float32BufferAttribute(wP,3));
g.setAttribute('normal',new THREE.Float32BufferAttribute(wN,3));
g.setAttribute('uv',new THREE.Float32BufferAttribute(wU,2));
g.setIndex(wI);
this.waterMesh=new THREE.Mesh(g,wmat);scene.add(this.waterMesh);
}
}
dispose(scene){
if(this.mesh){this.mesh.geometry.dispose();scene.remove(this.mesh);}
if(this.waterMesh){this.waterMesh.geometry.dispose();scene.remove(this.waterMesh);}
}
}
/* ======================================================
WORLD
====================================================== */
class World{
constructor(seed=42){this.chunks=new Map();this.noise=new SimplexNoise(seed);this.noise2=new SimplexNoise(seed+1);this.noise3=new SimplexNoise(seed+2);}
key(cx,cz){return cx+','+cz;}
getChunk(cx,cz){return this.chunks.get(this.key(cx,cz));}
getBlock(wx,wy,wz){
if(wy<0)return B.BEDROCK;if(wy>=CH)return B.AIR;
const cx=Math.floor(wx/CS),cz=Math.floor(wz/CS);
const ch=this.chunks.get(this.key(cx,cz));
if(!ch||!ch.gen)return B.AIR;
const lx=((wx%CS)+CS)%CS,lz=((wz%CS)+CS)%CS;
return ch.blocks[ch.idx(lx,wy,lz)];
}
setBlock(wx,wy,wz,type){
if(wy<0||wy>=CH)return;
const cx=Math.floor(wx/CS),cz=Math.floor(wz/CS);
const ch=this.chunks.get(this.key(cx,cz));if(!ch)return;
const lx=((wx%CS)+CS)%CS,lz=((wz%CS)+CS)%CS;
ch.blocks[ch.idx(lx,wy,lz)]=type;ch.dirty=true;
if(lx===0)this._dirty(cx-1,cz);
if(lx===CS-1)this._dirty(cx+1,cz);
if(lz===0)this._dirty(cx,cz-1);
if(lz===CS-1)this._dirty(cx,cz+1);
}
_dirty(cx,cz){const ch=this.chunks.get(this.key(cx,cz));if(ch)ch.dirty=true;}
genChunk(cx,cz){
const ch=new Chunk(cx,cz);const n=this.noise,n2=this.noise2,n3=this.noise3;
const ox=cx*CS,oz=cz*CS;
for(let lz=0;lz<CS;lz++)for(let lx=0;lx<CS;lx++){
const wx=ox+lx,wz=oz+lz;
const h1=n.n2(wx*.008,wz*.008)*25,nh2=n2.n2(wx*.025,wz*.025)*12;
const mt=Math.max(0,n.n2(wx*.004,wz*.004))*20;
const dt=n3.n2(wx*.05,wz*.05)*3;
const biome=n2.n2(wx*.005,wz*.005);
let height=Math.round(32+h1+nh2+mt+dt);
height=Math.max(1,Math.min(CH-2,height));
for(let y=0;y<CH;y++){
let b=B.AIR;
if(y===0)b=B.BEDROCK;
else if(y<height-4)b=B.STONE;
else if(y<height){
b=biome>.35?B.DIRT:B.SAND;
if(y<WL&&y>=height-1)b=biome>.35?B.DIRT:B.SAND;
}
else if(y===height){
if(biome>.35){
if(height>48)b=B.SNOW;
else if(height>WL+2)b=B.STONE;
else b=B.GRASS;
}else b=B.SAND;
}
else if(y<=WL&&y>height)b=B.WATER;
// caves
if(y>1&&y<height-1&&b!==B.AIR&&b!==B.WATER){
const cv=n3.n3(wx*.04,y*.06,wz*.04);
if(cv>.55)b=B.AIR;
}
if(b!==B.AIR)ch.blocks[ch.idx(lx,y,lz)]=b;
}
}
// trees
const treeRng=new SimplexNoise(cx*9999+cz);
for(let lz=2;lz<CS-2;lz++)for(let lx=2;lx<CS-2;lx++){
const wx=ox+lx,wz=oz+lz;
const biome=n2.n2(wx*.005,wz*.005);
if(biome<.35)continue;
const tv=treeRng.n2(wx*.5,wz*.5);
if(tv<.6)continue;
let surf=-1;
for(let y=CH-1;y>WL;y--){if(ch.blocks[ch.idx(lx,y,lz)]===B.GRASS){surf=y;break;}}
if(surf<0||surf>55)continue;
const th=4+((tv*10|0)%4);
// trunk
for(let y=surf+1;y<=surf+th;y++)if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&y<CH)ch.blocks[ch.idx(lx,y,lz)]=B.WOOD;
// leaves
const lr=th>5?3:2;
for(let dy=-2;dy<=1;dy++)for(let dx=-lr;dx<=lr;dx++)for(let dz=-lr;dz<=lr;dz++){
if(dx*dx+dz*dz+dy*dy>lr*lr+1)continue;
const tx=lx+dx,ty=surf+th+dy,tz=lz+dz;
if(tx<0||tx>=CS||tz<0||tz>=CS||ty<0||ty>=CH)continue;
if(ch.blocks[ch.idx(tx,ty,tz)]===B.AIR)ch.blocks[ch.idx(tx,ty,tz)]=B.LEAVES;
}
}
// Apply fantasy structures
this._applyStructures(ch,cx,cz);
ch.gen=true;ch.dirty=true;
this.chunks.set(this.key(cx,cz),ch);
return ch;
}
_applyStructures(ch,cx,cz){
const structures=[
{t:'tower',x:8,z:8,r:15},
{t:'fisland',x:65,z:45,r:12},
{t:'fisland',x:-55,z:75,r:10},
{t:'fisland',x:85,z:-35,r:11},
{t:'giant_tree',x:-45,z:-35,r:10},
{t:'ruins',x:105,z:65,r:18},
{t:'crystal',x:-75,z:25,r:10},
{t:'circle',x:45,z:-65,r:12},
{t:'dragon',x:-35,z:85,r:15},
{t:'portal',x:-90,z:-80,r:10},
];
const ox=cx*CS,oz=cz*CS;
for(const s of structures){
if(ox+CS<s.x-s.r||ox>s.x+s.r||oz+CS<s.z-s.r||oz>s.z+s.r)continue;
const set=(wx,wy,wz,b)=>{
const lx=wx-ox,lz=wz-oz;
if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&wy>=0&&wy<CH){
if(b!==B.AIR)ch.blocks[ch.idx(lx,wy,lz)]=b;
else{ch.blocks[ch.idx(lx,wy,lz)]=B.AIR;}
}
};
const get=(wx,wy,wz)=>{
const lx=wx-ox,lz=wz-oz;
if(lx>=0&&lx<CS&&lz>=0&&lz<CS&&wy>=0&&wy<CH)return ch.blocks[ch.idx(lx,wy,lz)];
return B.AIR;
};
if(s.t==='tower')this._tower(set,get,s.x,s.z);
else if(s.t==='fisland')this._fisland(set,get,s.x,s.z);
else if(s.t==='giant_tree')this._giantTree(set,get,s.x,s.z);
else if(s.t==='ruins')this._ruins(set,get,s.x,s.z);
else if(s.t==='crystal')this._crystal(set,get,s.x,s.z);
else if(s.t==='circle')this._circle(set,get,s.x,s.z);
else if(s.t==='dragon')this._dragon(set,get,s.x,s.z);
else if(s.t==='portal')this._portal(set,get,s.x,s.z);
}
}
_findSurface(x,z,get){
for(let y=CH-1;y>0;y--){const b=get(x,y,z);if(SOLID.has(b))return y;}
return WL;
}
_tower(set,get,cx,cz){
const base=this._findSurface(cx,cz,get);
const by=Math.max(base,WL+1);
// Base platform
for(let dx=-6;dx<=6;dx++)for(let dz=-6;dz<=6;dz++){
if(dx*dx+dz*dz>42)continue;
set(cx+dx,by,cz+dz,B.MARBLE);
for(let y=by-3;y<by;y++)set(cx+dx,y,cz+dz,B.DSTONE);
}
// Tower walls
const R=4,H=55;
for(let y=by+1;y<by+H;y++){
const taper=1-(y-by)/H*.4;
const r=R*taper;
for(let a=0;a<32;a++){
const angle=a/32*Math.PI*2;
const tx=cx+Math.round(r*Math.cos(angle)),tz=cz+Math.round(r*Math.sin(angle));
set(tx,y,tz,B.MARBLE);
}
// Windows
if((y-by)%8===4){
for(let a=0;a<32;a+=8){
const angle=a/32*Math.PI*2;
const tx=cx+Math.round((r+.5)*Math.cos(angle)),tz=cz+Math.round((r+.5)*Math.sin(angle));
set(tx,y,tz,B.GLOW);
}
}
// Spiral staircase inside
const sa=((y-by)/2%8)/8*Math.PI*2;
const sx=cx+Math.round(1.5*Math.cos(sa)),sz=cz+Math.round(1.5*Math.sin(sa));
if((y-by)%2===0)set(sx,y,sz,B.DSTONE);
}
// Top glow
const top=by+H;
for(let dx=-2;dx<=2;dx++)for(let dz=-2;dz<=2;dz++){
if(dx*dx+dz*dz>5)continue;
set(cx+dx,top,cz+dz,B.CRYSTAL);
set(cx+dx,top+1,cz+dz,B.GLOW);
}
// Floating crystal above tower
for(let y=top+3;y<top+8;y++){
set(cx,y,cz,B.CRYSTAL);
if(y%2===0){set(cx+1,y,cz,B.CRYSTAL);set(cx-1,y,cz,B.CRYSTAL);set(cx,y,cz+1,B.CRYSTAL);set(cx,y,cz-1,B.CRYSTAL);}
}
}
_fisland(set,get,cx,cz){
const iy=52+((cx*cz)%8);
const r=9;
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
const d2=dx*dx+dz*dz,d=Math.sqrt(d2);
if(d>r)continue;
const surfH=iy+Math.round(Math.cos(d/r*Math.PI/2)*3);
// Top: grass
set(cx+dx,surfH,cz+dz,B.GRASS);
// Below: dirt then stone
for(let y=surfH-3;y<surfH;y++)set(cx+dx,y,cz+dz,B.DIRT);
for(let y=iy-8;y<surfH-3;y++){
if(d<r-1)set(cx+dx,y,cz+dz,B.STONE);
}
// Bottom stalactite
const bh=iy-8-Math.round((1-d/r)*5);
for(let y=bh;y<iy-8;y++)if(d<r*.7)set(cx+dx,y,cz+dz,B.STONE);
}
// Waterfall
for(let y=iy-20;y<iy;y++){
set(cx+r-2,y,cz,B.WATER);
set(cx+r-1,y,cz,B.WATER);
}
// Small tree on top
const th=5;
for(let y=iy+1;y<=iy+th;y++)set(cx,y,cz,B.WOOD);
for(let dy=-1;dy<=1;dy++)for(let dx=-2;dx<=2;dx++)for(let dz=-2;dz<=2;dz++){
if(dx*dx+dz*dz+dy*dy>6)continue;
set(cx+dx,iy+th+dy,cz+dz,B.LEAVES);
}
// Glow crystals on bottom
for(let a=0;a<6;a++){
const ang=a/6*Math.PI*2;
set(cx+Math.round(3*Math.cos(ang)),iy-9,cz+Math.round(3*Math.sin(ang)),B.GLOW);
}
}
_giantTree(set,get,cx,cz){
const base=this._findSurface(cx,cz,get);
const by=Math.max(base,WL+1);
// Massive trunk
for(let y=by;y<by+35;y++){
const r=y<by+10?3:(y<by+25?2:1);
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
if(dx*dx+dz*dz>r*r)continue;
set(cx+dx,y,cz+dz,B.WOOD);
}
}
// Root system
for(let a=0;a<5;a++){
const ang=a/5*Math.PI*2;
for(let i=1;i<8;i++){
const rx=cx+Math.round(i*Math.cos(ang)),rz=cz+Math.round(i*Math.sin(ang));
set(rx,by-Math.round(i*.3),rz,B.WOOD);
set(rx,by-Math.round(i*.3)-1,rz,B.DIRT);
}
}
// Giant canopy
const top=by+35;
for(let dy=-5;dy<=8;dy++){
const r=dy<0?12-Math.abs(dy):Math.max(1,12-dy*1.2);
for(let dx=-r;dx<=r;dx++)for(let dz=-r;dz<=r;dz++){
if(dx*dx+dz*dz>r*r)continue;
const ty=top+dy;
if(ty<CH&&(get(cx+dx,ty,cz+dz)===B.AIR||get(cx+dx,ty,cz+dz)===B.LEAVES))
set(cx+dx,ty,cz+dz,B.LEAVES);
}
}
// Glowing fruit
const fr=new SimplexNoise(cx+cz*100);
for(let i=0;i<8;i++){
const a=i/8*Math.PI*2+fr.n2(i,i)*2;
const d=6+((i*7)%5);
set(cx+Math.round(d*Math.cos(a)),top-3+((i*3)%5),cz+Math.round(d*Math.sin(a)),B.GLOW);
}
}
_ruins(set,get,cx,cz){
const base=this._findSurface(cx,cz,get);
const by=Math.max(base,WL+1);
// Stone platform
for(let dx=-12;dx<=12;dx++)for(let dz=-8;dz<=8;dz++){
set(cx+dx,by,cz+dz,B.COBBLE);
if(Math.abs(dx)<=10&&Math.abs(dz)<=6)set(cx+dx,by-1,cz+dz,B.DSTONE);
}
// Pillars
const px=[[-10,-6],[-10,6],[10,-6],[10,6],[0,-6],[0,6]];
for(const[px2,pz]of px){
const h=8+((px2*pz)%4);
for(let y=by+1;y<by+h;y++){
set(cx+px2,y,cz+pz,B.DSTONE);
}
set(cx+px2,by+h,cz+pz,B.MARBLE);
// Lintel (broken)
if(px2===-10||px2===10){
for(let dx2=-1;dx2<=1;dx2++){
const ly=by+h-1;
set(cx+px2+dx2,ly,cz+pz,B.MARBLE);
}
}
}
// Inner magical circle
for(let a=0;a<16;a++){
const ang=a/16*Math.PI*2;
const r=4;
set(cx+Math.round(r*Math.cos(ang)),by+1,cz+Math.round(r*Math.sin(ang)),B.MAGIC);
}
// Central altar
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++){
set(cx+dx,by+1,cz+dz,B.MARBLE);
set(cx+dx,by+2,cz+dz,B.MARBLE);
}
set(cx,by+3,cz,B.GLOW);
}
_crystal(set,get,cx,cz){
const base=this._findSurface(cx,cz,get);
const by=Math.max(base,WL+1);
const cr=new SimplexNoise(cx*333+cz*777);
// Crystal spires
for(let i=0;i<7;i++){
const a=i/7*Math.PI*2+cr.n2(i,i)*.5;
const d=2+((i*5)%5);
const h=8+((i*13)%12);
const sx=cx+Math.round(d*Math.cos(a)),sz=cz+Math.round(d*Math.sin(a));
for(let y=by;y<by+h;y++){
const r=Math.max(0,1.5-(y-by)/h*1.5);
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++){
if(dx*dx+dz*dz>r*r+1)continue;
set(sx+dx,y,sz+dz,B.CRYSTAL);
}
}
}
// Glow base
for(let dx=-4;dx<=4;dx++)for(let dz=-4;dz<=4;dz++){
if(dx*dx+dz*dz>16)continue;
set(cx+dx,by,cz+dz,B.MAGIC);
if(dx*dx+dz*dz<5)set(cx+dx,by,cz+dz,B.GLOW);
}
}
_circle(set,get,cx,cz){
const base=this._findSurface(cx,cz,get);
const by=Math.max(base,WL+1);
// Outer ring
const R=8;
for(let a=0;a<24;a++){
const ang=a/24*Math.PI*2;
const sx=cx+Math.round(R*Math.cos(ang)),sz=cz+Math.round(R*Math.sin(ang));
const h=3+((a*7)%4);
for(let y=by;y<by+h;y++)set(sx,y,sz,B.COBBLE);
set(sx,by+h,sz,B.MOSS_STONE||B.COBBLE);
}
// Inner stones
for(let a=0;a<8;a++){
const ang=a/8*Math.PI*2;
const r2=4;
const sx=cx+Math.round(r2*Math.cos(ang)),sz=cz+Math.round(r2*Math.sin(ang));
for(let y=by;y<by+5;y++)set(sx,y,sz,B.DSTONE);
set(sx,by+5,sz,B.MARBLE);
}
// Center altar
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++){
set(cx+dx,by,cz+dz,B.MARBLE);
set(cx+dx,by+1,cz+dz,B.MARBLE);
}
set(cx,by+2,cz,B.GLOW);
set(cx,by+3,cz,B.CRYSTAL);
// Ground decoration
for(let dx=-R;dx<=R;dx++)for(let dz=-R;dz<=R;dz++){
const d=Math.sqrt(dx*dx+dz*dz);
if(d<R-.5&&d>R-1.5&&get(cx+dx,by,cz+dz)===B.AIR)set(cx+dx,by,cz+dz,B.MAGIC);
}
}
_dragon(set,get,cx,cz){
const base=this._findSurface(cx,cz,get);
const by=Math.max(base,WL+1);
// Spine
for(let i=0;i<20;i++){
const t=i/20;
const x=cx+Math.round(t*25-12);
const z=cz+Math.round(Math.sin(t*3)*4);
const y=by+1+Math.round(Math.sin(t*Math.PI)*6);
set(x,y,z,B.MARBLE);
set(x,y-1,z,B.MARBLE);
// Ribs
if(i%3===0){
for(let r=1;r<5;r++){
set(x,y-r,z+r,B.MARBLE);
set(x,y-r,z-r,B.MARBLE);
}
}
}
// Skull
const hx=cx-12,hy=by+7;
for(let dx=-2;dx<=2;dx++)for(let dy=-1;dy<=2;dy++)for(let dz=-2;dz<=2;dz++){
if(dx*dx+dy*dy*2+dz*dz>8)continue;
set(hx+dx,hy+dy,cz+dz,B.MARBLE);
}
// Eye sockets
set(hx-1,hy+1,cz-2,B.GLOW);
set(hx-1,hy+1,cz+2,B.GLOW);
// Jaw
for(let dx=-2;dx<=0;dx++)for(let dz=-1;dz<=1;dz++){
set(hx+dx,hy-2,cz+dz,B.MARBLE);
}
// Teeth
set(hx-2,hy-1,cz-1,B.CRYSTAL);
set(hx-2,hy-1,cz+1,B.CRYSTAL);
// Wings (simplified)
for(let i=0;i<10;i++){
const wx2=cx-2+i*1,wy=by+3+Math.round(Math.sin(i/10*Math.PI)*4);
set(wx2,wy,cz-6-i,B.MARBLE);
set(wx2,wy,cz+6+i,B.MARBLE);
if(i%2===0){set(wx2,wy+1,cz-6-i,B.CRYSTAL);set(wx2,wy+1,cz+6+i,B.CRYSTAL);}
}
}
_portal(set,get,cx,cz){
const base=this._findSurface(cx,cz,get);
const by=Math.max(base,WL+1);
// Frame
for(let y=by;y<by+10;y++){
set(cx-3,y,cz,B.OBSIDIAN);
set(cx+3,y,cz,B.OBSIDIAN);
}
for(let dx=-3;dx<=3;dx++)set(cx+dx,by+10,cz,B.OBSIDIAN);
for(let dx=-3;dx<=3;dx++)set(cx+dx,by,cz,B.OBSIDIAN);
// Inner glow
for(let y=by+1;y<by+10;y++)for(let dx=-2;dx<=2;dx++){
set(cx+dx,y,cz,B.MAGIC);
}
// Decorative corners
for(let dy=-1;dy<=11;dy++){
set(cx-4,by+dy,cz,B.DSTONE);
set(cx+4,by+dy,cz,B.DSTONE);
}
set(cx-4,by+12,cz,B.GLOW);
set(cx+4,by+12,cz,B.GLOW);
set(cx,by+11,cz,B.CRYSTAL);
}
}
/* ======================================================
RAYCAST (DDA)
====================================================== */
function raycast(ori,dir,maxD,getBlock){
let x=Math.floor(ori.x),y=Math.floor(ori.y),z=Math.floor(ori.z);
const sx=dir.x>0?1:-1,sy=dir.y>0?1:-1,sz=dir.z>0?1:-1;
const tdx=Math.abs(1/dir.x),tdy=Math.abs(1/dir.y),tdz=Math.abs(1/dir.z);
let tmx=dir.x!==0?((dir.x>0?x+1:x)-ori.x)/dir.x:1e18;
let tmy=dir.y!==0?((dir.y>0?y+1:y)-ori.y)/dir.y:1e18;
let tmz=dir.z!==0?((dir.z>0?z+1:z)-ori.z)/dir.z:1e18;
let px=x,py=y,pz=z,steps=0;
while(steps++<maxD*2){
const b=getBlock(x,y,z);
if(b!==0&&b!==B.WATER)return{x,y,z,px,py,pz,block:b};
px=x;py=y;pz=z;
if(tmx<tmy){if(tmx<tmz){x+=sx;tmx+=tdx;}else{z+=sz;tmz+=tdz;}}
else{if(tmy<tmz){y+=sy;tmy+=tdy;}else{z+=sz;tmz+=tdz;}}
}
return null;
}
/* ======================================================
MAIN GAME
====================================================== */
const atlas=createAtlas();
const world=new World(42);
// Three.js setup
const renderer=new THREE.WebGLRenderer({antialias:false});
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
renderer.setClearColor(0x7EC8E3);
document.body.appendChild(renderer.domElement);
const scene=new THREE.Scene();
scene.fog=new THREE.Fog(0x7EC8E3,CS*2,CS*RD*.9);
const camera=new THREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,.1,CS*RD*1.5);
camera.rotation.order='YXZ';
const ambLight=new THREE.AmbientLight(0xffffff,.55);scene.add(ambLight);
const hemiLight=new THREE.HemisphereLight(0x87CEEB,0x556633,.35);scene.add(hemiLight);
const dirLight=new THREE.DirectionalLight(0xFFE8C0,.75);dirLight.position.set(60,120,40);scene.add(dirLight);
const mat=new THREE.MeshLambertMaterial({map:atlas.texture,side:THREE.FrontSide});
const wmat=new THREE.MeshLambertMaterial({map:atlas.texture,transparent:true,opacity:.65,side:THREE.DoubleSide});
// Player state
const player={x:8,y:50,z:8,vx:0,vy:0,vz:0,yaw:0,pitch:0,onGround:false,flying:false};
let selectedSlot=0;
const keys={};
let gameStarted=false;
const chunkQueue=new Set();
let lastChunkKey='';
// UI
const overlay=document.getElementById('overlay');
const crosshair=document.getElementById('crosshair');
const hotbar=document.getElementById('hotbar');
const debugEl=document.getElementById('debug');
function buildHotbar(){
hotbar.innerHTML='';
HOTBAR_ITEMS.forEach((bt,i)=>{
const slot=document.createElement('div');slot.className='slot'+(i===selectedSlot?' active':'');
const num=document.createElement('span');num.className='num';num.textContent=i+1;slot.appendChild(num);
// mini preview
const tc=document.createElement('canvas');tc.width=16;tc.height=16;
const tx=tc.getContext('2d');
const faces=BF[bt];if(faces){
const ti=faces[1]||faces[0];
const col=ti%AC,row=ti/AC|0;
tx.drawImage(atlas.canvas,col*TS,row*TS,TS,TS,0,0,16,16);
}
slot.appendChild(tc);hotbar.appendChild(slot);
});
}
function updateHotbar(){
hotbar.querySelectorAll('.slot').forEach((s,i)=>s.className='slot'+(i===selectedSlot?' active':''));
}
function showDebug(){
const cx=Math.floor(player.x/CS),cz=Math.floor(player.z/CS);
debugEl.innerHTML=`XYZ: ${player.x.toFixed(1)} / ${player.y.toFixed(1)} / ${player.z.toFixed(1)}<br>Chunk: ${cx}, ${cz}<br>${player.flying?'飞行模式':'行走模式'}<br>方块: ${BN[HOTBAR_ITEMS[selectedSlot]]||'?'}`;
}
// Input
document.addEventListener('keydown',e=>{
keys[e.code]=true;
if(!gameStarted)return;
if(e.code==='KeyF'){player.flying=!player.flying;player.vy=0;}
const n=parseInt(e.key);if(n>=1&&n<=9){selectedSlot=n-1;updateHotbar();}
e.preventDefault();
});
document.addEventListener('keyup',e=>keys[e.code]=false);
document.addEventListener('wheel',e=>{
if(!gameStarted)return;
selectedSlot=(selectedSlot+(e.deltaY>0?1:-1)+HOTBAR_ITEMS.length)%HOTBAR_ITEMS.length;
updateHotbar();
});
document.addEventListener('mousemove',e=>{
if(!gameStarted||document.pointerLockElement!==renderer.domElement)return;
player.yaw-=e.movementX*.002;
player.pitch=Math.max(-Math.PI/2+.01,Math.min(Math.PI/2-.01,player.pitch-e.movementY*.002));
});
// Mouse buttons
document.addEventListener('mousedown',e=>{
if(!gameStarted)return;
if(e.button===0)breakBlock();
else if(e.button===2)placeBlock();
});
document.addEventListener('contextmenu',e=>e.preventDefault());
function getViewRay(){
const dir=new THREE.Vector3(0,0,-1);
dir.applyEuler(new THREE.Euler(player.pitch,player.yaw,0,'YXZ'));
return{origin:new THREE.Vector3(player.x,player.y+1.6,player.z),direction:dir.normalize()};
}
function breakBlock(){
const{origin,direction}=getViewRay();
const hit=raycast(origin,direction,8,(x,y,z)=>world.getBlock(x,y,z));
if(hit&&hit.block!==B.BEDROCK){world.setBlock(hit.x,hit.y,hit.z,B.AIR);}
}
function placeBlock(){
const{origin,direction}=getViewRay();
const hit=raycast(origin,direction,8,(x,y,z)=>world.getBlock(x,y,z));
if(!hit)return;
const{px,py,pz}=hit;
// Don't place inside player
const hw=.3,h=1.8;
if(px+1>player.x-hw&&px<player.x+hw&&py+1>player.y&&py<player.y+h&&pz+1>player.z-hw&&pz<player.z+hw)return;
world.setBlock(px,py,pz,HOTBAR_ITEMS[selectedSlot]);
}
// Collision
function isSolid(b){return SOLID.has(b);}
function collides(px,py,pz){
const hw=.29,h=1.79;
for(let x=Math.floor(px-hw);x<=Math.floor(px+hw);x++)
for(let y=Math.floor(py);y<=Math.floor(py+h);y++)
for(let z=Math.floor(pz-hw);z<=Math.floor(pz+hw);z++)
if(isSolid(world.getBlock(x,y,z)))return true;
return false;
}
function updatePlayer(dt){
if(!gameStarted)return;
dt=Math.min(dt,.05);
// Camera
camera.position.set(player.x,player.y+1.6,player.z);
camera.rotation.set(player.pitch,player.yaw,0);
// Movement
const fwd=new THREE.Vector3(-Math.sin(player.yaw),0,-Math.cos(player.yaw));
const rgt=new THREE.Vector3(Math.cos(player.yaw),0,-Math.sin(player.yaw));
const speed=player.flying?10:6;
let mx=0,mz=0;
if(keys['KeyW']){mx+=fwd.x;mz+=fwd.z;}
if(keys['KeyS']){mx-=fwd.x;mz-=fwd.z;}
if(keys['KeyD']){mx+=rgt.x;mz+=rgt.z;}
if(keys['KeyA']){mx-=rgt.x;mz-=rgt.z;}
const ml=Math.sqrt(mx*mx+mz*mz);
if(ml>.001){mx=mx/ml*speed;mz=mz/ml*speed;}
// Apply movement with collision (axis-separated)
const newX=player.x+mx*dt;
if(!collides(newX,player.y,player.z))player.x=newX;
const newZ=player.z+mz*dt;
if(!collides(player.x,player.y,newZ))player.z=newZ;
if(player.flying){
let vy=0;
if(keys['Space'])vy=speed;
if(keys['ShiftLeft'])vy=-speed;
const newY=player.y+vy*dt;
if(!collides(player.x,newY,player.z))player.y=newY;
player.vy=0;
}else{
// Gravity
player.vy-=25*dt;
if(player.vy<-30)player.vy=-30;
if(keys['Space']&&player.onGround)player.vy=8;
const newY=player.y+player.vy*dt;
if(!collides(player.x,newY,player.z)){
player.y=newY;player.onGround=false;
}else{
if(player.vy<0)player.onGround=true;
player.vy=0;
}
}
// Prevent falling into void
if(player.y<-10){player.y=60;player.vy=0;}
}
// Chunk management
function updateChunks(){
const pcx=Math.floor(player.x/CS),pcz=Math.floor(player.z/CS);
const pk=pcx+','+pcz;
// Queue new chunks
for(let dx=-RD;dx<=RD;dx++)for(let dz=-RD;dz<=RD;dz++){
if(dx*dx+dz*dz>RD*RD)continue;
const cx=pcx+dx,cz=pcz+dz;
const k=cx+','+cz;
if(!world.chunks.has(k))chunkQueue.add(k);
}
// Generate a few per frame
let count=0;
for(const k of chunkQueue){
if(count>=4)break;
const[cx,cz]=k.split(',').map(Number);
const ch=world.genChunk(cx,cz);
ch.buildMesh((x,y,z)=>world.getBlock(x,y,z),mat,wmat,scene);
chunkQueue.delete(k);count++;
}
// Rebuild dirty & visible chunks
let rebuilds=0;
for(const[k,ch]of world.chunks){
const dx=ch.cx-pcx,dz=ch.cz-pcz;
if(dx*dx+dz*dz>RD*RD+2){ch.dispose(scene);world.chunks.delete(k);continue;}
if(ch.dirty&&rebuilds<4){
ch.buildMesh((x,y,z)=>world.getBlock(x,y,z),mat,wmat,scene);
rebuilds++;
}
}
if(pk!==lastChunkKey){lastChunkKey=pk;showDebug();}
}
// Events
overlay.addEventListener('click',()=>{
renderer.domElement.requestPointerLock();
});
document.addEventListener('pointerlockchange',()=>{
if(document.pointerLockElement===renderer.domElement){
gameStarted=true;overlay.classList.add('hidden');
crosshair.style.display='block';hotbar.style.display='flex';debugEl.style.display='block';
buildHotbar();
}else{
if(gameStarted){overlay.classList.remove('hidden');overlay.querySelector('.start').textContent='— 点击继续探险 —';}
}
});
window.addEventListener('resize',()=>{
camera.aspect=window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
});
// Find spawn
function findSpawnY(){
for(let y=CH-1;y>0;y--){
if(isSolid(world.getBlock(8,y,8)))return y+1;
}
return 40;
}
// Init
for(let dx=-1;dx<=1;dx++)for(let dz=-1;dz<=1;dz++)world.genChunk(dx,dz);
player.y=findSpawnY();
// Game loop
let lastTime=performance.now();
function gameLoop(now){
requestAnimationFrame(gameLoop);
const dt=(now-lastTime)/1000;lastTime=now;
updatePlayer(dt);
updateChunks();
renderer.render(scene,camera);
if(gameStarted&&Math.floor(now/500)%2===0)showDebug();
}
requestAnimationFrame(gameLoop);
</script>
</body>
</html>