
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>MineJS — Minecraft Clone</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box;user-select:none}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'Press Start 2P',monospace}
canvas{display:block}
#gameCanvas{position:absolute;inset:0}
.pixel{image-rendering:pixelated;image-rendering:crisp-edges}
/* ---------- HUD ---------- */
#hud{position:absolute;inset:0;pointer-events:none;display:none}
#crosshair{position:absolute;left:50%;top:50%;width:20px;height:20px;transform:translate(-50%,-50%);mix-blend-mode:difference}
#crosshair:before,#crosshair:after{content:'';position:absolute;background:#fff}
#crosshair:before{left:9px;top:0;width:2px;height:20px}
#crosshair:after{top:9px;left:0;width:20px;height:2px}
#hotbar{position:absolute;bottom:8px;left:50%;transform:translateX(-50%);display:flex;gap:0;background:rgba(0,0,0,.45);border:2px solid #1a1a1a;outline:2px solid rgba(255,255,255,.25)}
.slot{width:46px;height:46px;border:2px solid #555;background:rgba(40,40,40,.6);display:flex;align-items:center;justify-content:center;position:relative}
.slot.sel{border:2px solid #fff;background:rgba(90,90,90,.7);box-shadow:0 0 6px rgba(255,255,255,.6) inset}
.slot canvas{width:36px;height:36px}
#hearts{position:absolute;bottom:62px;left:50%;transform:translateX(-50%);font-size:15px;letter-spacing:2px;text-shadow:2px 2px 0 #000;font-family:Arial}
#itemname{position:absolute;bottom:92px;left:50%;transform:translateX(-50%);color:#fff;font-size:10px;text-shadow:2px 2px #000;opacity:0;transition:opacity .5s}
#debug{position:absolute;top:6px;left:6px;color:#fff;font-size:8px;line-height:1.8;text-shadow:1px 1px #000;background:rgba(0,0,0,.25);padding:6px}
#vignette{position:absolute;inset:0;background:radial-gradient(ellipse at center,transparent 55%,rgba(0,0,0,.35) 100%)}
#damageFlash{position:absolute;inset:0;background:rgba(255,0,0,.35);opacity:0;transition:opacity .4s}
#waterOverlay{position:absolute;inset:0;background:rgba(20,60,160,.35);display:none}
/* ---------- Screens ---------- */
.screen{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;text-align:center;z-index:10}
#titleScreen{background:linear-gradient(#3a7bd5 0%,#79a7e8 45%,#3b7a2a 45.2%,#2a5c1e 100%)}
#titleScreen:before{content:'';position:absolute;inset:0;background:repeating-conic-gradient(rgba(0,0,0,.06) 0 25%,transparent 0 50%) 0 0/32px 32px;opacity:.6}
h1{font-size:42px;color:#fff;text-shadow:4px 4px 0 #3f3f3f, 8px 8px 0 rgba(0,0,0,.3);margin-bottom:8px;letter-spacing:4px;position:relative}
.sub{color:#ffff55;font-size:11px;text-shadow:2px 2px #3f3f00;margin-bottom:34px;transform:rotate(-4deg);animation:pulse 1s infinite;position:relative}
@keyframes pulse{50%{transform:rotate(-4deg) scale(1.08)}}
.btn{font-family:inherit;font-size:12px;color:#fff;background:#6f6f6f;border:2px solid #000;box-shadow:inset 2px 2px 0 rgba(255,255,255,.45),inset -2px -2px 0 rgba(0,0,0,.45);padding:14px 40px;margin:6px;cursor:pointer;position:relative}
.btn:hover{background:#7f8fbf}
.controls{font-size:8px;line-height:2.2;color:#ddd;margin-top:28px;text-shadow:1px 1px #000;position:relative}
#pauseScreen,#deathScreen{background:rgba(0,0,0,.55);display:none}
#deathScreen{background:rgba(120,0,0,.5)}
#invScreen{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:#c6c6c6;border:4px solid #555;box-shadow:inset 3px 3px 0 #fff,inset -3px -3px 0 #888,0 0 0 3px #000;padding:14px;display:none;z-index:20}
#invScreen h3{font-size:10px;color:#404040;margin-bottom:10px}
#invGrid{display:grid;grid-template-columns:repeat(9,44px);gap:2px}
.invSlot{width:44px;height:44px;background:#8b8b8b;box-shadow:inset 2px 2px 0 #373737,inset -2px -2px 0 #fff;display:flex;align-items:center;justify-content:center;cursor:pointer}
.invSlot:hover{background:#a8a8c0}
.invSlot canvas{width:32px;height:32px}
#loadingText{font-size:10px;margin-top:20px;color:#fff;text-shadow:2px 2px #000;position:relative}
</style>
</head>
<body>
<div id="hud">
<div id="vignette"></div>
<div id="waterOverlay"></div>
<div id="damageFlash"></div>
<div id="crosshair"></div>
<div id="debug"></div>
<div id="hearts"></div>
<div id="itemname"></div>
<div id="hotbar"></div>
</div>
<div id="invScreen"><h3>Select Block (E / Esc to close)</h3><div id="invGrid"></div></div>
<div id="titleScreen" class="screen">
<h1>MINEJS</h1>
<div class="sub">Now in JavaScript!</div>
<button class="btn" id="playBtn" disabled>Generating World...</button>
<button class="btn" id="newWorldBtn">New World (delete save)</button>
<div class="controls">
WASD move SPACE jump SHIFT sneak double-W sprint<br>
LMB break RMB place MMB pick block WHEEL/1-9 hotbar<br>
E inventory F fly N skip day/night G/H spawn pig/zombie
</div>
<div id="loadingText"></div>
</div>
<div id="pauseScreen" class="screen"><h1 style="font-size:24px">PAUSED</h1>
<button class="btn" id="resumeBtn">Back to Game</button>
<button class="btn" id="resetBtn">Delete World & Restart</button></div>
<div id="deathScreen" class="screen"><h1 style="font-size:24px">You Died!</h1>
<button class="btn" id="respawnBtn">Respawn</button></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
/* ============================================================
MineJS — a Minecraft clone in one file
============================================================ */
'use strict';
const $=id=>document.getElementById(id);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const smooth=(a,b,x)=>{const t=clamp((x-a)/(b-a),0,1);return t*t*(3-2*t);};
/* ---------------- RNG + Perlin noise ---------------- */
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;};}
let SEED;
function makePerlin(seed){
const p=new Uint8Array(512),perm=[];for(let i=0;i<256;i++)perm[i]=i;
const rng=mulberry32(seed);
for(let i=255;i>0;i--){const j=(rng()*(i+1))|0;[perm[i],perm[j]]=[perm[j],perm[i]];}
for(let i=0;i<512;i++)p[i]=perm[i&255];
const fade=t=>t*t*t*(t*(t*6-15)+10);
const grad=(h,x,y,z)=>{const u=h<8?x:y,v=h<4?y:(h===12||h===14?x:z);return((h&1)?-u:u)+((h&2)?-v:v);};
function n3(x,y,z){
const X=Math.floor(x)&255,Y=Math.floor(y)&255,Z=Math.floor(z)&255;
x-=Math.floor(x);y-=Math.floor(y);z-=Math.floor(z);
const u=fade(x),v=fade(y),w=fade(z);
const A=p[X]+Y,AA=p[A]+Z,AB=p[A+1]+Z,B=p[X+1]+Y,BA=p[B]+Z,BB=p[B+1]+Z;
return lerp(lerp(lerp(grad(p[AA]&15,x,y,z),grad(p[BA]&15,x-1,y,z),u),
lerp(grad(p[AB]&15,x,y-1,z),grad(p[BB]&15,x-1,y-1,z),u),v),
lerp(lerp(grad(p[AA+1]&15,x,y,z-1),grad(p[BA+1]&15,x-1,y,z-1),u),
lerp(grad(p[AB+1]&15,x,y-1,z-1),grad(p[BB+1]&15,x-1,y-1,z-1),u),v),w);
}
return{n3,n2:(x,y)=>n3(x,y,0)};
}
let perlin;
function fbm2(x,y,oct){let v=0,a=1,f=1,m=0;for(let i=0;i<oct;i++){v+=perlin.n2(x*f,y*f)*a;m+=a;a*=.5;f*=2;}return v/m;}
function h3(x,y,z){let n=(x|0)*73856093^(y|0)*19349663^(z|0)*83492791^SEED;n=Math.imul(n^(n>>>13),1274126177);n^=n>>>16;return(n>>>0)/4294967296;}
/* ---------------- Block definitions ---------------- */
const B={AIR:0,GRASS:1,DIRT:2,STONE:3,COBBLE:4,PLANKS:5,LOG:6,LEAVES:7,SAND:8,SANDSTONE:9,GRAVEL:10,BRICK:11,GLASS:12,GLOW:13,SNOWGRASS:14,SNOW:15,WATER:16,COAL:17,IRON:18,GOLD:19,DIAMOND:20,BEDROCK:21,CACTUS:22,FLOWER_R:23,FLOWER_Y:24,TALLGRASS:25};
const T={GRASS_TOP:0,GRASS_SIDE:1,DIRT:2,STONE:3,SAND:4,WATER:5,LOG_SIDE:6,LOG_TOP:7,LEAVES:8,PLANKS:9,COBBLE:10,GLASS:11,COAL:12,IRON:13,GOLD:14,DIAMOND:15,BEDROCK:16,SNOW_TOP:17,SNOW_SIDE:18,GRAVEL:19,BRICK:20,FLOWER_R:21,FLOWER_Y:22,TALLGRASS:23,CACTUS_SIDE:24,CACTUS_TOP:25,SANDSTONE:26,GLOW:27,CRACK0:32};
const D=[];// block defs
function def(id,name,top,side,bottom,o){D[id]=Object.assign({name,top,side,bottom,solid:true,transparent:false,cross:false,cullSame:false,hard:1,icon:side},o||{});}
def(B.AIR,'Air',0,0,0,{solid:false,transparent:true,hard:0});
def(B.GRASS,'Grass Block',T.GRASS_TOP,T.GRASS_SIDE,T.DIRT,{hard:.6});
def(B.DIRT,'Dirt',T.DIRT,T.DIRT,T.DIRT,{hard:.5});
def(B.STONE,'Stone',T.STONE,T.STONE,T.STONE,{hard:1.5});
def(B.COBBLE,'Cobblestone',T.COBBLE,T.COBBLE,T.COBBLE,{hard:1.6});
def(B.PLANKS,'Oak Planks',T.PLANKS,T.PLANKS,T.PLANKS,{hard:1});
def(B.LOG,'Oak Log',T.LOG_TOP,T.LOG_SIDE,T.LOG_TOP,{hard:1});
def(B.LEAVES,'Oak Leaves',T.LEAVES,T.LEAVES,T.LEAVES,{hard:.25,transparent:true});
def(B.SAND,'Sand',T.SAND,T.SAND,T.SAND,{hard:.5});
def(B.SANDSTONE,'Sandstone',T.SANDSTONE,T.SANDSTONE,T.SANDSTONE,{hard:1.3});
def(B.GRAVEL,'Gravel',T.GRAVEL,T.GRAVEL,T.GRAVEL,{hard:.6});
def(B.BRICK,'Bricks',T.BRICK,T.BRICK,T.BRICK,{hard:1.6});
def(B.GLASS,'Glass',T.GLASS,T.GLASS,T.GLASS,{hard:.3,transparent:true,cullSame:true});
def(B.GLOW,'Glowstone',T.GLOW,T.GLOW,T.GLOW,{hard:.4});
def(B.SNOWGRASS,'Snowy Grass',T.SNOW_TOP,T.SNOW_SIDE,T.DIRT,{hard:.6});
def(B.SNOW,'Snow Block',T.SNOW_TOP,T.SNOW_TOP,T.SNOW_TOP,{hard:.4});
def(B.WATER,'Water',T.WATER,T.WATER,T.WATER,{solid:false,transparent:true,cullSame:true,liquid:true,hard:0});
def(B.COAL,'Coal Ore',T.COAL,T.COAL,T.COAL,{hard:2});
def(B.IRON,'Iron Ore',T.IRON,T.IRON,T.IRON,{hard:2.2});
def(B.GOLD,'Gold Ore',T.GOLD,T.GOLD,T.GOLD,{hard:2.2});
def(B.DIAMOND,'Diamond Ore',T.DIAMOND,T.DIAMOND,T.DIAMOND,{hard:2.5});
def(B.BEDROCK,'Bedrock',T.BEDROCK,T.BEDROCK,T.BEDROCK,{hard:-1});
def(B.CACTUS,'Cactus',T.CACTUS_TOP,T.CACTUS_SIDE,T.CACTUS_TOP,{hard:.4});
def(B.FLOWER_R,'Rose',T.FLOWER_R,T.FLOWER_R,T.FLOWER_R,{solid:false,transparent:true,cross:true,hard:.05});
def(B.FLOWER_Y,'Dandelion',T.FLOWER_Y,T.FLOWER_Y,T.FLOWER_Y,{solid:false,transparent:true,cross:true,hard:.05});
def(B.TALLGRASS,'Tall Grass',T.TALLGRASS,T.TALLGRASS,T.TALLGRASS,{solid:false,transparent:true,cross:true,hard:.05});
D[B.GRASS].icon=T.GRASS_SIDE; D[B.LOG].icon=T.LOG_SIDE;
/* ---------------- Texture atlas (procedural pixel art) ---------------- */
const atlas=document.createElement('canvas');atlas.width=atlas.height=256;
const A=atlas.getContext('2d');
function tCtx(t){return{ox:(t%16)*16,oy:((t/16)|0)*16};}
function px(t,x,y,c){const o=tCtx(t);A.fillStyle=c;A.fillRect(o.ox+x,o.oy+y,1,1);}
function hsl(h,s,l,a){return a===undefined?`hsl(${h},${s}%,${l}%)`:`hsla(${h},${s}%,${l}%,${a})`;}
function noiseFill(t,h,s,l,v,rng){for(let y=0;y<16;y++)for(let x=0;x<16;x++)px(t,x,y,hsl(h,s,l+(rng()-.5)*v));}
function genTextures(){
const R=t=>mulberry32(0xC0FFEE+t*7919);
let r;
r=R(1);noiseFill(T.GRASS_TOP,100,42,42,16,r);
for(let i=0;i<26;i++)px(T.GRASS_TOP,(r()*16)|0,(r()*16)|0,hsl(100,48,30+r()*8));
r=R(2);noiseFill(T.DIRT,28,38,36,14,r);
for(let i=0;i<14;i++)px(T.DIRT,(r()*16)|0,(r()*16)|0,hsl(28,30,24));
r=R(3);noiseFill(T.GRASS_SIDE,28,38,36,14,r);
for(let y=0;y<3;y++)for(let x=0;x<16;x++)px(T.GRASS_SIDE,x,y,hsl(100,45,40+(r()-.5)*14));
for(let x=0;x<16;x++)if(r()<.6)px(T.GRASS_SIDE,x,3,hsl(100,45,38+(r()-.5)*10));
r=R(4);noiseFill(T.STONE,220,3,47,11,r);
for(let i=0;i<7;i++){let x=(r()*13)|0,y=(r()*15)|0,len=2+(r()*4)|0;for(let k=0;k<len;k++)px(T.STONE,x+k,y,hsl(220,3,35));}
r=R(5);noiseFill(T.SAND,50,42,73,8,r);
for(let i=0;i<10;i++)px(T.SAND,(r()*16)|0,(r()*16)|0,hsl(48,40,62));
r=R(6);for(let y=0;y<16;y++)for(let x=0;x<16;x++)px(T.WATER,x,y,hsl(218,72,40+(r()-.5)*10+((y%4===0&&r()<.5)?9:0),.85));
r=R(7);for(let x=0;x<16;x++){const base=(x%4<2)?31:23;for(let y=0;y<16;y++)px(T.LOG_SIDE,x,y,hsl(30,40,base+(r()-.5)*7));}
r=R(8);for(let y=0;y<16;y++)for(let x=0;x<16;x++){const d=Math.max(Math.abs(x-7.5),Math.abs(y-7.5));px(T.LOG_TOP,x,y,hsl(33,42,(d|0)%2?40:28+(r()-.5)*6));}
r=R(9);for(let y=0;y<16;y++)for(let x=0;x<16;x++){if(r()<.16)continue;px(T.LEAVES,x,y,hsl(108,48,22+r()*18));}
r=R(10);for(let y=0;y<16;y++)for(let x=0;x<16;x++){let l=46+(r()-.5)*8;if(y%4===3)l=30;if((y<4&&x===7)||(y>=4&&y<8&&x===3)||(y>=8&&y<12&&x===11)||(y>=12&&x===5))l=30;px(T.PLANKS,x,y,hsl(33,42,l));}
r=R(11);noiseFill(T.COBBLE,220,4,46,20,r);
for(let i=0;i<14;i++){let x=(r()*16)|0,y=(r()*16)|0;for(let k=0;k<6;k++){px(T.COBBLE,x&15,y&15,hsl(220,4,24));x+=(r()*3-1)|0;y+=(r()*3-1)|0;if(x<0||y<0||x>15||y>15)break;}}
r=R(12);A.fillStyle='rgba(180,220,255,0.10)';const g=tCtx(T.GLASS);A.fillRect(g.ox,g.oy,16,16);
for(let i=0;i<16;i++){px(T.GLASS,i,0,hsl(0,0,88,.95));px(T.GLASS,i,15,hsl(0,0,88,.95));px(T.GLASS,0,i,hsl(0,0,88,.95));px(T.GLASS,15,i,hsl(0,0,88,.95));}
for(let i=0;i<5;i++){px(T.GLASS,3+i,8-i,hsl(0,0,95,.9));px(T.GLASS,8+i,13-i,hsl(0,0,95,.9));}
function ore(t,color){const rr=R(t+40);noiseFill(t,220,3,47,11,rr);for(let i=0;i<5;i++){const x=1+(rr()*13)|0,y=1+(rr()*13)|0;px(t,x,y,color);px(t,x+1,y,color);px(t,x,y+1,color);if(rr()<.6)px(t,x+1,y+1,color);}}
ore(T.COAL,'#1c1c1c');ore(T.IRON,hsl(20,45,65));ore(T.GOLD,hsl(48,90,55));ore(T.DIAMOND,hsl(180,80,62));
r=R(13);for(let y=0;y<16;y++)for(let x=0;x<16;x++)px(T.BEDROCK,x,y,hsl(0,0,r()<.5?14+r()*10:34+r()*14));
r=R(14);noiseFill(T.SNOW_TOP,210,12,92,6,r);
r=R(15);noiseFill(T.SNOW_SIDE,28,38,36,14,r);
for(let y=0;y<4;y++)for(let x=0;x<16;x++)if(y<3||r()<.5)px(T.SNOW_SIDE,x,y,hsl(210,12,90+(r()-.5)*6));
r=R(16);for(let y=0;y<16;y++)for(let x=0;x<16;x++){const c=r();px(T.GRAVEL,x,y,c<.3?hsl(28,12,38):c<.6?hsl(220,4,52):hsl(220,4,40));}
r=R(17);for(let y=0;y<16;y++)for(let x=0;x<16;x++){const row=(y/4)|0,off=row%2?4:0,mortar=(y%4===3)||(((x+off)%8)===7);px(T.BRICK,x,y,mortar?hsl(20,8,70):hsl(5,55,38+(r()-.5)*9));}
function flower(t,petal,center){const rr=R(t+60);for(let y=8;y<16;y++)px(t,7+(y%3===0?1:0)-(y%5===0?1:0)? 7:7,y,hsl(110,50,30));
for(let y=9;y<16;y++)px(t,7,y,hsl(110,55,28+rr()*8));px(t,6,11,hsl(110,55,30));px(t,8,13,hsl(110,55,30));
const cx=7,cy=5;[[0,0],[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,-1],[1,-1],[-1,1]].forEach(o=>px(t,cx+o[0],cy+o[1],petal));px(t,cx,cy,center);}
flower(T.FLOWER_R,hsl(0,75,48),hsl(0,80,32));flower(T.FLOWER_Y,hsl(52,95,55),hsl(40,95,45));
r=R(18);for(let i=0;i<7;i++){let x=2+i*2,h=5+(r()*6)|0;for(let y=15;y>15-h;y--){px(T.TALLGRASS,x+((y%4===0)?(r()<.5?1:-1):0),y,hsl(105,48,28+r()*16));}}
r=R(19);for(let y=0;y<16;y++)for(let x=0;x<16;x++){let l=34+(r()-.5)*8;if(x%4===0)l=24;if(x%4===2&&y%4===1)l=55;px(T.CACTUS_SIDE,x,y,hsl(95,52,l));}
r=R(20);for(let y=0;y<16;y++)for(let x=0;x<16;x++){const b=x===0||y===0||x===15||y===15;px(T.CACTUS_TOP,x,y,hsl(95,52,b?26:42+(r()-.5)*8));}
r=R(21);for(let y=0;y<16;y++)for(let x=0;x<16;x++){let l=70+(r()-.5)*7;if(y===0||y===15)l=58;if(y>3&&y<12&&r()<.08)l=60;px(T.SANDSTONE,x,y,hsl(48,38,l));}
r=R(22);noiseFill(T.GLOW,45,85,55,25,r);for(let i=0;i<9;i++){const x=(r()*14)|0,y=(r()*14)|0;px(T.GLOW,x,y,hsl(48,95,78));px(T.GLOW,x+1,y,hsl(48,95,72));px(T.GLOW,x,y+1,hsl(48,95,72));}
for(let s=0;s<8;s++){const t=T.CRACK0+s,rr=mulberry32(777);A.fillStyle='rgba(0,0,0,0.8)';
const cracks=2+s;for(let c=0;c<cracks;c++){let x=4+(rr()*8)|0,y=4+(rr()*8)|0;const steps=3+s*2;
for(let k=0;k<steps;k++){const o=tCtx(t);A.fillRect(o.ox+(x&15),o.oy+(y&15),1,1);x+=(rr()*3-1)|0;y+=(rr()*3-1)|0;}}}
}
/* ---------------- World ---------------- */
const CH=16,H=64,SEA=28;
let RD=5;
const chunks=new Map(),dirty=new Set(),editsByChunk=new Map();
const ckey=(cx,cz)=>cx+','+cz;
const bidx=(x,y,z)=>(x*16+z)*H+y;
let worldTime=0;const DAY=480;
function biomeAt(x,z){const t=fbm2(x*.0035+900,z*.0035-700,3);if(t>.34)return'desert';if(t<-.42)return'snow';return fbm2(x*.012+33,z*.012-71,3)>.06?'forest':'plains';}
function groundH(x,z){
const cont=fbm2(x*.0032,z*.0032,4),hills=fbm2(x*.014+50,z*.014+50,3);
let m=fbm2(x*.007+200,z*.007+200,4);m=Math.max(0,m);
let h=30+cont*12+hills*5+m*m*38;
return clamp(h|0,4,H-10);
}
function genChunk(cx,cz){
const blocks=new Uint8Array(16*H*16);
const hs=new Int16Array(256),bs=[];
for(let lx=0;lx<16;lx++)for(let lz=0;lz<16;lz++){
const wx=cx*16+lx,wz=cz*16+lz,h=groundH(wx,wz),bio=biomeAt(wx,wz);
hs[lx*16+lz]=h;bs[lx*16+lz]=bio;
for(let y=0;y<H;y++){
let id=B.AIR;
if(y===0)id=B.BEDROCK;
else if(y<h-3){
id=B.STONE;const r=h3(wx,y,wz);
if(r<.0016&&y<14)id=B.DIAMOND;else if(r<.004&&y<22)id=B.GOLD;else if(r<.011&&y<34)id=B.IRON;else if(r<.022&&y<44)id=B.COAL;else if(r>.992)id=B.GRAVEL;
}else if(y<h)id=(bio==='desert'||h<=SEA+1)?B.SAND:B.DIRT;
else if(y===h){
if(h<=SEA+1)id=B.SAND;
else if(bio==='desert')id=B.SAND;
else if(bio==='snow')id=B.SNOWGRASS;
else id=B.GRASS;
}else if(y<=SEA)id=B.WATER;
// caves
if(id!==B.AIR&&id!==B.BEDROCK&&id!==B.WATER&&y>1){
const canBreach=h>SEA?y<=h:y<h-3;
if(canBreach&&perlin.n3(wx*.065,y*.105,wz*.065)>.44)id=B.AIR;
}
blocks[bidx(lx,y,lz)]=id;
}
}
// decorations
const rng=mulberry32((cx*341873128+cz*132897987^SEED)>>>0);
function top(lx,lz){for(let y=H-1;y>0;y--){const b=blocks[bidx(lx,y,lz)];if(b!==B.AIR)return y;}return 0;}
for(let lx=0;lx<16;lx++)for(let lz=0;lz<16;lz++){
const bio=bs[lx*16+lz],wy=top(lx,lz),tb=blocks[bidx(lx,wy,lz)];
if(wy<=SEA)continue;
if((tb===B.GRASS||tb===B.SNOWGRASS)&&lx>=2&&lx<=13&&lz>=2&&lz<=13){
const tc=bio==='forest'?.045:bio==='plains'?.006:bio==='snow'?.015:0;
if(rng()<tc){
const th=4+((rng()*3)|0);
for(let dy=th-2;dy<=th+1;dy++){const ly=wy+dy;if(ly>=H)break;const rad=dy>th-1?1:2;
for(let dx=-rad;dx<=rad;dx++)for(let dz=-rad;dz<=rad;dz++){
if(Math.abs(dx)===rad&&Math.abs(dz)===rad&&rng()<.5)continue;
const i=bidx(lx+dx,ly,lz+dz);if(blocks[i]===B.AIR)blocks[i]=B.LEAVES;}}
for(let dy=1;dy<=th&&wy+dy<H;dy++)blocks[bidx(lx,wy+dy,lz)]=B.LOG;
continue;
}
}
if(tb===B.GRASS&&wy+1<H){
const r=rng();
if(r<.05)blocks[bidx(lx,wy+1,lz)]=B.TALLGRASS;
else if(r<.062)blocks[bidx(lx,wy+1,lz)]=rng()<.5?B.FLOWER_R:B.FLOWER_Y;
}
if(bio==='desert'&&tb===B.SAND&&rng()<.004){
const ch2=2+((rng()*2)|0);for(let dy=1;dy<=ch2&&wy+dy<H;dy++)blocks[bidx(lx,wy+dy,lz)]=B.CACTUS;
}
}
// apply saved edits
const ed=editsByChunk.get(ckey(cx,cz));
if(ed)for(const k in ed){const[lx,y,lz]=k.split(',').map(Number);blocks[bidx(lx,y,lz)]=ed[k];}
const chunk={cx,cz,blocks,meshO:null,meshW:null};
chunks.set(ckey(cx,cz),chunk);
dirty.add(ckey(cx,cz));
[[1,0],[-1,0],[0,1],[0,-1]].forEach(o=>{if(chunks.has(ckey(cx+o[0],cz+o[1])))dirty.add(ckey(cx+o[0],cz+o[1]));});
return chunk;
}
function getBlock(x,y,z){
if(y<0)return B.BEDROCK;if(y>=H)return B.AIR;
const c=chunks.get(ckey(Math.floor(x/16),Math.floor(z/16)));
if(!c)return B.AIR;
return c.blocks[bidx(((x%16)+16)%16,y,((z%16)+16)%16)];
}
function setBlock(x,y,z,id,record=true){
if(y<0||y>=H)return;
const cx=Math.floor(x/16),cz=Math.floor(z/16),c=chunks.get(ckey(cx,cz));
if(!c)return;
const lx=((x%16)+16)%16,lz=((z%16)+16)%16;
c.blocks[bidx(lx,y,lz)]=id;
if(record){let ed=editsByChunk.get(ckey(cx,cz));if(!ed){ed={};editsByChunk.set(ckey(cx,cz),ed);}ed[lx+','+y+','+lz]=id;}
dirty.add(ckey(cx,cz));
if(lx===0)dirty.add(ckey(cx-1,cz));if(lx===15)dirty.add(ckey(cx+1,cz));
if(lz===0)dirty.add(ckey(cx,cz-1));if(lz===15)dirty.add(ckey(cx,cz+1));
}
/* ---------------- Meshing ---------------- */
const FACES=[
{dir:[-1,0,0],corners:[{pos:[0,1,0],uv:[0,1]},{pos:[0,0,0],uv:[0,0]},{pos:[0,1,1],uv:[1,1]},{pos:[0,0,1],uv:[1,0]}],shade:.6},
{dir:[1,0,0], corners:[{pos:[1,1,1],uv:[0,1]},{pos:[1,0,1],uv:[0,0]},{pos:[1,1,0],uv:[1,1]},{pos:[1,0,0],uv:[1,0]}],shade:.6},
{dir:[0,-1,0],corners:[{pos:[1,0,1],uv:[1,0]},{pos:[0,0,1],uv:[0,0]},{pos:[1,0,0],uv:[1,1]},{pos:[0,0,0],uv:[0,1]}],shade:.5},
{dir:[0,1,0], corners:[{pos:[0,1,1],uv:[1,1]},{pos:[1,1,1],uv:[0,1]},{pos:[0,1,0],uv:[1,0]},{pos:[1,1,0],uv:[0,0]}],shade:1},
{dir:[0,0,-1],corners:[{pos:[1,0,0],uv:[0,0]},{pos:[0,0,0],uv:[1,0]},{pos:[1,1,0],uv:[0,1]},{pos:[0,1,0],uv:[1,1]}],shade:.8},
{dir:[0,0,1], corners:[{pos:[0,0,1],uv:[0,0]},{pos:[1,0,1],uv:[1,0]},{pos:[0,1,1],uv:[0,1]},{pos:[1,1,1],uv:[1,1]}],shade:.8}];
const TS=1/16,PAD=.6/256;
const AOF=[.45,.62,.8,1];
let matOpaque,matWater,atlasTex;
function tileUV(t,ux,uy){const col=t%16,row=(t/16)|0;return[col*TS+PAD+ux*(TS-2*PAD),1-(row+1)*TS+PAD+uy*(TS-2*PAD)];}
function meshChunk(c){
if(c.meshO){scene.remove(c.meshO);c.meshO.geometry.dispose();c.meshO=null;}
if(c.meshW){scene.remove(c.meshW);c.meshW.geometry.dispose();c.meshW=null;}
const pO=[],uO=[],cO=[],iO=[],pW=[],uW=[],cW=[],iW=[];
const ox=c.cx*16,oz=c.cz*16;
function gb(x,y,z){
if(y<0)return B.BEDROCK;if(y>=H)return B.AIR;
const cc=chunks.get(ckey(Math.floor(x/16),Math.floor(z/16)));
if(!cc)return B.STONE;
return cc.blocks[bidx(((x%16)+16)%16,y,((z%16)+16)%16)];
}
const occ=(x,y,z)=>{const b=gb(x,y,z),d=D[b];return b!==B.AIR&&d.solid&&!d.transparent;};
for(let lx=0;lx<16;lx++)for(let lz=0;lz<16;lz++)for(let y=0;y<H;y++){
const id=c.blocks[bidx(lx,y,lz)];if(id===B.AIR)continue;
const dd=D[id],wx=ox+lx,wz=oz+lz;
if(dd.cross){ // X-shaped plant
const t=dd.side,quads=[[[.15,0,.15],[.85,0,.85],[.15,1,.15],[.85,1,.85]],[[.85,0,.15],[.15,0,.85],[.85,1,.15],[.15,1,.85]]];
for(const q of quads)for(const flip of[0,1]){
const n=pO.length/3;
const ord=flip?[1,0,3,2]:[0,1,2,3];
const uvs=[[0,0],[1,0],[0,1],[1,1]];
for(let k=0;k<4;k++){const v=q[ord[k]];pO.push(lx+v[0],y+v[1],lz+v[2]);const u=tileUV(t,uvs[k][0],uvs[k][1]);uO.push(u[0],u[1]);cO.push(.95,.95,.95);}
iO.push(n,n+1,n+2,n+2,n+1,n+3);
}
continue;
}
const isW=id===B.WATER;
const topY=(isW&&gb(wx,y+1,wz)!==B.WATER)?.875:1;
for(let f=0;f<6;f++){
const F=FACES[f],nx=wx+F.dir[0],ny=y+F.dir[1],nz=wz+F.dir[2];
const nid=gb(nx,ny,nz),nd=D[nid];
const visible=nid===B.AIR||nd.cross||(nd.transparent&&(nid!==id||!dd.cullSame));
if(!visible)continue;
const tile=F.dir[1]===1?dd.top:F.dir[1]===-1?dd.bottom:dd.side;
const[P,U,C,I]=isW?[pW,uW,cW,iW]:[pO,uO,cO,iO];
const n=P.length/3,ao=[1,1,1,1];
const a=F.dir[0]?0:F.dir[1]?1:2,p1=(a+1)%3,p2=(a+2)%3;
for(let k=0;k<4;k++){
const cr=F.corners[k];
let yy=cr.pos[1]===1?topY:cr.pos[1];
P.push(lx+cr.pos[0],y+yy,lz+cr.pos[2]);
const u=tileUV(tile,cr.uv[0],cr.uv[1]);U.push(u[0],u[1]);
let aoV=1;
if(!isW){
const bp=[nx,ny,nz],s=[0,0,0],t2=[0,0,0];
s[p1]=cr.pos[p1]===1?1:-1;t2[p2]=cr.pos[p2]===1?1:-1;
const s1=occ(bp[0]+s[0],bp[1]+s[1],bp[2]+s[2])?1:0;
const s2=occ(bp[0]+t2[0],bp[1]+t2[1],bp[2]+t2[2])?1:0;
const co=occ(bp[0]+s[0]+t2[0],bp[1]+s[1]+t2[1],bp[2]+s[2]+t2[2])?1:0;
aoV=AOF[(s1&&s2)?0:3-s1-s2-co];
}
ao[k]=aoV;const sh=F.shade*aoV;C.push(sh,sh,sh);
}
if(ao[0]+ao[3]<ao[1]+ao[2])I.push(n,n+1,n+3,n,n+3,n+2);
else I.push(n,n+1,n+2,n+2,n+1,n+3);
}
}
function build(pos,uv,col,idx,mat,ro){
if(!idx.length)return null;
const g=new THREE.BufferGeometry();
g.setAttribute('position',new THREE.Float32BufferAttribute(pos,3));
g.setAttribute('uv',new THREE.Float32BufferAttribute(uv,2));
g.setAttribute('color',new THREE.Float32BufferAttribute(col,3));
g.setIndex(idx);
const m=new THREE.Mesh(g,mat);
m.position.set(ox,0,oz);m.matrixAutoUpdate=false;m.updateMatrix();m.renderOrder=ro;
scene.add(m);return m;
}
c.meshO=build(pO,uO,cO,iO,matOpaque,0);
c.meshW=build(pW,uW,cW,iW,matWater,2);
}
/* ---------------- Three.js setup ---------------- */
let scene,camera,renderer,sunLight,ambLight,skyPivot,sunMesh,moonMesh,stars,cloudGroup;
let highlight,crackMesh,handGroup,handMesh;
function setupScene(){
scene=new THREE.Scene();
scene.background=new THREE.Color(0x87b1ff);
scene.fog=new THREE.Fog(0x87b1ff,RD*16*.55,RD*16*.95);
camera=new THREE.PerspectiveCamera(75,innerWidth/innerHeight,.1,1000);
camera.rotation.order='YXZ';scene.add(camera);
renderer=new THREE.WebGLRenderer({antialias:false});
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
renderer.setSize(innerWidth,innerHeight);
renderer.domElement.id='gameCanvas';
document.body.appendChild(renderer.domElement);
atlasTex=new THREE.CanvasTexture(atlas);
atlasTex.magFilter=atlasTex.minFilter=THREE.NearestFilter;atlasTex.generateMipmaps=false;
matOpaque=new THREE.MeshBasicMaterial({map:atlasTex,vertexColors:true,alphaTest:.5,side:THREE.FrontSide});
matWater=new THREE.MeshBasicMaterial({map:atlasTex,vertexColors:true,transparent:true,opacity:.78,depthWrite:false,side:THREE.DoubleSide});
ambLight=new THREE.AmbientLight(0xffffff,.7);scene.add(ambLight);
sunLight=new THREE.DirectionalLight(0xffffff,.7);scene.add(sunLight);scene.add(sunLight.target);
// sky
skyPivot=new THREE.Group();scene.add(skyPivot);
const sg=new THREE.PlaneGeometry(46,46);
sunMesh=new THREE.Mesh(sg,new THREE.MeshBasicMaterial({color:0xffe14d,fog:false}));
sunMesh.position.set(420,0,0);skyPivot.add(sunMesh);
moonMesh=new THREE.Mesh(new THREE.PlaneGeometry(30,30),new THREE.MeshBasicMaterial({color:0xd8dce8,fog:false}));
moonMesh.position.set(-420,0,0);skyPivot.add(moonMesh);
const starPos=[];const srng=mulberry32(42);
for(let i=0;i<450;i++){const v=new THREE.Vector3(srng()*2-1,srng()*2-1,srng()*2-1).normalize().multiplyScalar(400);starPos.push(v.x,v.y,v.z);}
const stg=new THREE.BufferGeometry();stg.setAttribute('position',new THREE.Float32BufferAttribute(starPos,3));
stars=new THREE.Points(stg,new THREE.PointsMaterial({color:0xffffff,size:1.6,fog:false,transparent:true,opacity:0}));
skyPivot.add(stars);
// clouds
cloudGroup=new THREE.Group();scene.add(cloudGroup);
const crng=mulberry32(7);
for(let i=0;i<26;i++){
const m=new THREE.Mesh(new THREE.BoxGeometry(1,1,1),new THREE.MeshBasicMaterial({color:0xffffff,transparent:true,opacity:.5}));
m.scale.set(12+crng()*26,3.2,10+crng()*20);
m.position.set((crng()-.5)*420,70+crng()*8,(crng()-.5)*420);
cloudGroup.add(m);
}
// block highlight + crack overlay
highlight=new THREE.LineSegments(new THREE.EdgesGeometry(new THREE.BoxGeometry(1.002,1.002,1.002)),new THREE.LineBasicMaterial({color:0x000000,transparent:true,opacity:.7}));
highlight.visible=false;scene.add(highlight);
crackMesh=new THREE.Mesh(new THREE.BoxGeometry(1.004,1.004,1.004),new THREE.MeshBasicMaterial({map:atlasTex,transparent:true,depthWrite:false,polygonOffset:true,polygonOffsetFactor:-2}));
crackMesh.visible=false;crackMesh.renderOrder=1;scene.add(crackMesh);
// held block
handGroup=new THREE.Group();camera.add(handGroup);
handMesh=new THREE.Mesh(new THREE.BoxGeometry(.35,.35,.35),matOpaque);
handMesh.position.set(.42,-.42,-.65);handMesh.rotation.set(.2,Math.PI/5,0);
handGroup.add(handMesh);
addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight);});
}
function setCubeUV(geom,tiles){ // tiles: [px,nx,py,ny,pz,nz]
const uv=geom.attributes.uv;
for(let face=0;face<6;face++){const t=tiles[face];
for(let v=0;v<4;v++){const i=face*4+v;
const u0=tileUV(t,uv.getX(i)>.5?1:0,uv.getY(i)>.5?1:0);
uv.setXY(i,u0[0],u0[1]);}}
uv.needsUpdate=true;
}
function updateHandMesh(){
const id=hotbar[hotSel],dd=D[id];
setCubeUV(handMesh.geometry,[dd.side,dd.side,dd.top,dd.bottom,dd.side,dd.side]);
}
function setCrackStage(s){setCubeUV(crackMesh.geometry,Array(6).fill(T.CRACK0+clamp(s,0,7)));}
/* ---------------- Audio ---------------- */
let AC=null;
function ac(){if(!AC)AC=new(window.AudioContext||window.webkitAudioContext)();if(AC.state==='suspended')AC.resume();return AC;}
function sfx(f1,f2,dur,type,vol){
try{const a=ac(),o=a.createOscillator(),g=a.createGain();
o.type=type||'square';o.frequency.setValueAtTime(f1,a.currentTime);
o.frequency.exponentialRampToValueAtTime(Math.max(20,f2||f1),a.currentTime+dur);
g.gain.setValueAtTime(vol||.15,a.currentTime);
g.gain.exponentialRampToValueAtTime(.001,a.currentTime+dur);
o.connect(g).connect(a.destination);o.start();o.stop(a.currentTime+dur);}catch(e){}
}
const sndDig=id=>{const d=D[id];if(id===B.STONE||id===B.COBBLE||d.hard>1.2)sfx(95,55,.12,'square',.18);else if(id===B.SAND||id===B.GRAVEL)sfx(180,90,.1,'triangle',.2);else sfx(150,80,.1,'triangle',.18);};
const sndPlace=()=>sfx(220,140,.08,'square',.14);
const sndHurt=()=>sfx(280,90,.25,'sawtooth',.2);
const sndPop=()=>sfx(400,900,.12,'square',.15);
/* ---------------- Particles ---------------- */
const particles=[];const tileColorCache={};
function tileColor(t){
if(tileColorCache[t])return tileColorCache[t];
const o=tCtx(t),d=A.getImageData(o.ox,o.oy,16,16).data;
let r=0,g=0,b=0,n=0;
for(let i=0;i<d.length;i+=4)if(d[i+3]>100){r+=d[i];g+=d[i+1];b+=d[i+2];n++;}
const c=n?new THREE.Color(r/n/255,g/n/255,b/n/255):new THREE.Color(.5,.5,.5);
return tileColorCache[t]=c;
}
const partGeo=new THREE.BoxGeometry(.1,.1,.1);const partMats={};
function spawnParticles(x,y,z,tile,count){
const c=tileColor(tile),key=c.getHexString();
if(!partMats[key])partMats[key]=new THREE.MeshBasicMaterial({color:c});
for(let i=0;i<count;i++){
const m=new THREE.Mesh(partGeo,partMats[key]);
m.position.set(x+Math.random(),y+Math.random(),z+Math.random());
scene.add(m);
particles.push({m,vx:(Math.random()-.5)*4,vy:2+Math.random()*3,vz:(Math.random()-.5)*4,life:.5+Math.random()*.3});
}
}
function updateParticles(dt){
for(let i=particles.length-1;i>=0;i--){const p=particles[i];
p.life-=dt;p.vy-=18*dt;
p.m.position.x+=p.vx*dt;p.m.position.y+=p.vy*dt;p.m.position.z+=p.vz*dt;
if(p.life<=0){scene.remove(p.m);particles.splice(i,1);}}
}
/* ---------------- Mobs ---------------- */
const mobs=[];
function lambBox(w,h,d,color,x,y,z,parent,pivotTop){
const g=new THREE.BoxGeometry(w,h,d);
if(pivotTop)g.translate(0,-h/2,0);
const m=new THREE.Mesh(g,new THREE.MeshLambertMaterial({color}));
m.position.set(x,y,z);parent.add(m);return m;
}
class Mob{
constructor(type,x,y,z){
this.type=type;this.x=x;this.y=y;this.z=z;this.vy=0;this.dir=Math.random()*Math.PI*2;
this.state='idle';this.timer=1+Math.random()*3;this.animT=0;this.onGround=false;
this.attackCd=0;this.legs=[];this.g=new THREE.Group();
if(type==='pig'){this.hp=10;this.speed=1.2;
const c=0xeb9c9c;lambBox(.62,.5,.95,c,0,.62,0,this.g);
const head=lambBox(.48,.48,.42,0xf0a8a8,0,.72,.62,this.g);
lambBox(.24,.16,.06,0xd87f7f,0,-.04,.24,head);
lambBox(.07,.07,.02,0x202020,-.13,.1,.22,head);lambBox(.07,.07,.02,0x202020,.13,.1,.22,head);
[[-.2,-.32],[.2,-.32],[-.2,.32],[.2,.32]].forEach(o=>this.legs.push(lambBox(.18,.38,.18,0xdb8e8e,o[0],.38,o[1],this.g,true)));
}else if(type==='sheep'){this.hp=8;this.speed=1;
lambBox(.7,.62,1.05,0xe8e8e8,0,.78,0,this.g);
const head=lambBox(.4,.4,.35,0xd8c5b8,0,.95,.65,this.g);
lambBox(.06,.06,.02,0x202020,-.1,.05,.18,head);lambBox(.06,.06,.02,0x202020,.1,.05,.18,head);
[[-.22,-.35],[.22,-.35],[-.22,.35],[.22,.35]].forEach(o=>this.legs.push(lambBox(.17,.48,.17,0xcfcfcf,o[0],.48,o[1],this.g,true)));
}else{ // zombie
this.hp=20;this.speed=1.7;
const skin=0x57a04b;
[[-.13,0],[.13,0]].forEach(o=>this.legs.push(lambBox(.22,.72,.22,0x2e6b8a,o[0],.72,o[1],this.g,true)));
lambBox(.52,.68,.3,0x3a7ca5,0,1.06,0,this.g);
const head=lambBox(.48,.48,.48,skin,0,1.64,0,this.g);
lambBox(.08,.08,.02,0x111111,-.11,.05,.25,head);lambBox(.08,.08,.02,0x111111,.11,.05,.25,head);
this.arms=[lambBox(.18,.18,.62,skin,-.35,1.28,.28,this.g),lambBox(.18,.18,.62,skin,.35,1.28,.28,this.g)];
}
this.g.position.set(x,y,z);scene.add(this.g);
}
solidAt(x,y,z){const b=getBlock(Math.floor(x),Math.floor(y),Math.floor(z));return D[b].solid;}
damage(n,kx,kz){
this.hp-=n;this.vy=5;this.x+=kx*.3;this.z+=kz*.3;
this.g.traverse(o=>{if(o.material)o.material.emissive=new THREE.Color(0x880000);});
setTimeout(()=>this.g.traverse(o=>{if(o.material)o.material.emissive=new THREE.Color(0)}),140);
sfx(200,80,.15,'sawtooth',.15);
if(this.hp<=0){
spawnParticles(this.x-.5,this.y+.4,this.z-.5,this.type==='zombie'?T.LEAVES:T.FLOWER_R,14);
sndPop();this.dead=true;
}
}
update(dt){
this.timer-=dt;this.attackCd-=dt;
const dx=player.x-this.x,dz=player.z-this.z,distP=Math.hypot(dx,dz);
if(this.type==='zombie'&&distP<14){this.state='walk';this.dir=Math.atan2(dx,dz);
if(distP<1.3&&this.attackCd<=0&&Math.abs(player.y-this.y)<2){this.attackCd=1;hurt(3,dx/distP,dz/distP);}
}else if(this.timer<=0){
this.timer=1.5+Math.random()*4;
this.state=Math.random()<.55?'walk':'idle';
if(this.state==='walk')this.dir=Math.random()*Math.PI*2;
}
let spd=this.state==='walk'?this.speed:0;
if(this.type==='zombie'&&distP<14)spd=2.4;
if(spd>0){
const fx=Math.sin(this.dir),fz=Math.cos(this.dir);
const nx=this.x+fx*spd*dt,nz=this.z+fz*spd*dt;
const lx=nx+fx*.35,lz=nz+fz*.35;
const blocked=this.solidAt(lx,this.y+.1,lz)||this.solidAt(lx,this.y+1.1,lz);
if(blocked){
if(this.onGround&&!this.solidAt(lx,this.y+1.4,lz)&&!this.solidAt(this.x,this.y+2.2,this.z))this.vy=7;
else this.dir+=Math.PI+(Math.random()-.5);
}else{this.x=nx;this.z=nz;}
this.animT+=dt*spd*3.4;
}
this.vy-=22*dt;
const inW=D[getBlock(Math.floor(this.x),Math.floor(this.y+.3),Math.floor(this.z))].liquid;
if(inW){this.vy=Math.max(this.vy,-1);this.vy+=26*dt;this.vy=Math.min(this.vy,2.2);}
this.y+=this.vy*dt;this.onGround=false;
if(this.vy<=0&&this.solidAt(this.x,this.y,this.z)){this.y=Math.floor(this.y)+1;this.vy=0;this.onGround=true;}
if(this.vy>0&&this.solidAt(this.x,this.y+1.8,this.z))this.vy=0;
if(this.y<-10)this.dead=true;
const sw=Math.sin(this.animT)*.7;
this.legs.forEach((l,i)=>l.rotation.x=i%2?sw:-sw);
this.g.position.set(this.x,this.y,this.z);
this.g.rotation.y=this.dir;
if(distP>70)this.dead=true;
if(this.type==='zombie'&&dayFactor>.6&&Math.random()<dt*.4){spawnParticles(this.x-.5,this.y+1.5,this.z-.5,T.GLOW,3);this.dead=true;}
}
}
function topSolidY(x,z){
if(!chunks.has(ckey(Math.floor(x/16),Math.floor(z/16))))return-1;
for(let y=H-1;y>0;y--){const b=getBlock(x,y,z);if(b===B.WATER)return-1;if(D[b].solid)return y;}
return-1;
}
let mobTimer=0;
function updateMobs(dt){
mobTimer-=dt;
if(mobTimer<=0){
mobTimer=1.5;
const night=sunElev<-.05;
const zCount=mobs.filter(m=>m.type==='zombie').length;
const pCount=mobs.length-zCount;
const ang=Math.random()*Math.PI*2,r=18+Math.random()*22;
const x=Math.floor(player.x+Math.sin(ang)*r),z=Math.floor(player.z+Math.cos(ang)*r);
const y=topSolidY(x,z);
if(y>0&&y<H-3){
const tb=getBlock(x,y,z);
if((tb===B.GRASS||tb===B.SAND||tb===B.SNOWGRASS)&&!D[getBlock(x,y+1,z)].solid){
if(night&&zCount<8&&Math.random()<.75)mobs.push(new Mob('zombie',x+.5,y+1,z+.5));
else if(pCount<10)mobs.push(new Mob(Math.random()<.5?'pig':'sheep',x+.5,y+1,z+.5));
}
}
}
for(let i=mobs.length-1;i>=0;i--){
const m=mobs[i];m.update(dt);
if(m.dead){scene.remove(m.g);mobs.splice(i,1);}
}
}
/* ---------------- Player ---------------- */
const player={x:8,y:40,z:8,vx:0,vy:0,vz:0,yaw:0,pitch:0,onGround:false,fly:false,hp:20,peakY:0,bobT:0,sneak:false,sprint:false,inWater:false};
let spawnPoint={x:8,y:40,z:8};
const keys={};let dead=false,invOpen=false,playing=false;
let lastHurtT=-99,regenT=0,lastWTap=0;
const HALF=.3,PH=1.8,EYE=1.62;
function collides(){
const x0=Math.floor(player.x-HALF),x1=Math.floor(player.x+HALF);
const y0=Math.floor(player.y),y1=Math.floor(player.y+PH);
const z0=Math.floor(player.z-HALF),z1=Math.floor(player.z+HALF);
for(let x=x0;x<=x1;x++)for(let y=y0;y<=y1;y++)for(let z=z0;z<=z1;z++)
if(D[getBlock(x,y,z)].solid)return true;
return false;
}
function hurt(n,kx,kz){
if(dead||n<=0)return;
player.hp-=n;lastHurtT=worldTime;
if(kx!==undefined){player.vx+=kx*7;player.vz+=kz*7;player.vy=Math.max(player.vy,4.5);}
sndHurt();
const f=$('damageFlash');f.style.transition='none';f.style.opacity=.5;
requestAnimationFrame(()=>{f.style.transition='opacity .4s';f.style.opacity=0;});
updateHearts();
if(player.hp<=0){dead=true;document.exitPointerLock();$('deathScreen').style.display='flex';}
}
function updatePlayer(dt){
const steps=Math.max(1,Math.ceil(dt/.0333));const sdt=dt/steps;
for(let s=0;s<steps;s++)stepPlayer(sdt);
// camera
const sneakOff=player.sneak&&player.onGround?-.15:0;
let bobO=0;
const hSpeed=Math.hypot(player.vx,player.vz);
if(player.onGround&&hSpeed>.5){player.bobT+=dt*hSpeed*1.7;bobO=Math.sin(player.bobT*4)*.05;}
camera.position.set(player.x,player.y+EYE+sneakOff+bobO,player.z);
camera.rotation.set(player.pitch,player.yaw,0);
const tFov=player.sprint&&hSpeed>4?84:75;
camera.fov=lerp(camera.fov,tFov,dt*8);camera.updateProjectionMatrix();
// water check
const eyeB=getBlock(Math.floor(player.x),Math.floor(player.y+EYE+sneakOff),Math.floor(player.z));
$('waterOverlay').style.display=D[eyeB].liquid?'block':'none';
if(D[eyeB].liquid){scene.fog.near=2;scene.fog.far=24;}
// regen
regenT+=dt;
if(regenT>3){regenT=0;if(player.hp<20&&worldTime-lastHurtT>6){player.hp++;updateHearts();}}
// hand swing
if(swingT>0){swingT-=dt*5;handGroup.rotation.x=-Math.sin(Math.max(0,swingT)*Math.PI)*.7;handGroup.position.y=-Math.sin(Math.max(0,swingT)*Math.PI)*.15;}
}
function stepPlayer(dt){
const fwd=[-Math.sin(player.yaw),-Math.cos(player.yaw)],right=[Math.cos(player.yaw),-Math.sin(player.yaw)];
let mx=0,mz=0;
if(keys.KeyW){mx+=fwd[0];mz+=fwd[1];}
if(keys.KeyS){mx-=fwd[0];mz-=fwd[1];}
if(keys.KeyD){mx+=right[0];mz+=right[1];}
if(keys.KeyA){mx-=right[0];mz-=right[1];}
const ml=Math.hypot(mx,mz);if(ml>0){mx/=ml;mz/=ml;}
player.sneak=!!keys.ShiftLeft&&!player.fly;
if(!keys.KeyW)player.sprint=false;
const feetB=D[getBlock(Math.floor(player.x),Math.floor(player.y+.2),Math.floor(player.z))];
const bodyB=D[getBlock(Math.floor(player.x),Math.floor(player.y+1),Math.floor(player.z))];
player.inWater=feetB.liquid||bodyB.liquid;
let speed=player.fly?11:player.inWater?2.6:player.sneak?1.4:player.sprint?5.6:4.3;
const acc=player.fly?40:(player.onGround?55:12);
player.vx+=clamp(mx*speed-player.vx,-acc*dt,acc*dt);
player.vz+=clamp(mz*speed-player.vz,-acc*dt,acc*dt);
if(player.fly){
let ty=0;if(keys.Space)ty=9;if(keys.ShiftLeft)ty=-9;
player.vy+=clamp(ty-player.vy,-50*dt,50*dt);
}else if(player.inWater){
player.vy-=8*dt;player.vy=Math.max(player.vy,-2.6);
if(keys.Space)player.vy+=clamp(3.2-player.vy,0,30*dt);
player.peakY=player.y;
}else{
player.vy-=26*dt;player.vy=Math.max(player.vy,-50);
if(keys.Space&&player.onGround){player.vy=8.2;player.onGround=false;}
}
// Y
player.y+=player.vy*dt;
const wasFalling=player.vy<0;
if(collides()){
if(player.vy<0){player.y=Math.floor(player.y)+1+1e-4;
if(wasFalling){player.onGround=true;
const fall=player.peakY-player.y;
if(fall>3.5&&!player.inWater&&!player.fly)hurt(Math.floor(fall-3));
player.peakY=player.y;}
}else player.y=Math.floor(player.y+PH)-PH-1e-4;
player.vy=0;
}else{player.onGround=false;}
if(player.onGround||player.fly)player.peakY=player.y;
else player.peakY=Math.max(player.peakY,player.y);
// X
player.x+=player.vx*dt;
if(collides()){
if(player.vx>0)player.x=Math.floor(player.x+HALF)-HALF-1e-4;
else player.x=Math.floor(player.x-HALF)+1+HALF+1e-4;
player.vx=0;
}
// Z
player.z+=player.vz*dt;
if(collides()){
if(player.vz>0)player.z=Math.floor(player.z+HALF)-HALF-1e-4;
else player.z=Math.floor(player.z-HALF)+1+HALF+1e-4;
player.vz=0;
}
if(player.y<-12){hurt(100);}
}
/* ---------------- Raycasting + block interaction ---------------- */
function raycast(maxD){
const o=camera.position,d=camera.getWorldDirection(new THREE.Vector3());
let x=Math.floor(o.x),y=Math.floor(o.y),z=Math.floor(o.z);
const stX=d.x>0?1:-1,stY=d.y>0?1:-1,stZ=d.z>0?1:-1;
const tdX=Math.abs(1/d.x),tdY=Math.abs(1/d.y),tdZ=Math.abs(1/d.z);
let tmX=d.x!==0?((x+(stX>0?1:0))-o.x)/d.x:1e30;
let tmY=d.y!==0?((y+(stY>0?1:0))-o.y)/d.y:1e30;
let tmZ=d.z!==0?((z+(stZ>0?1:0))-o.z)/d.z:1e30;
let nx=0,ny=0,nz=0,t=0;
for(let i=0;i<120;i++){
const b=getBlock(x,y,z),dd=D[b];
if(b!==B.AIR&&(dd.solid||dd.cross))return{x,y,z,nx,ny,nz,id:b,t};
if(tmX<tmY&&tmX<tmZ){x+=stX;t=tmX;tmX+=tdX;nx=-stX;ny=0;nz=0;}
else if(tmY<tmZ){y+=stY;t=tmY;tmY+=tdY;nx=0;ny=-stY;nz=0;}
else{z+=stZ;t=tmZ;tmZ+=tdZ;nx=0;ny=0;nz=-stZ;}
if(t>maxD)return null;
}
return null;
}
let mouseL=false,mouseR=false,breakTarget=null,breakProgress=0,placeTimer=0,swingT=0;
function tryHitMob(){
const o=camera.position,d=camera.getWorldDirection(new THREE.Vector3());
let best=null,bestT=4.2;
for(const m of mobs){
const c=new THREE.Vector3(m.x,m.y+.9,m.z).sub(o);
const t=c.dot(d);
if(t>0&&t<bestT){
const perp=c.clone().addScaledVector(d,-t).length();
if(perp<.85){best=m;bestT=t;}
}
}
if(best){
const dx=best.x-player.x,dz=best.z-player.z,l=Math.hypot(dx,dz)||1;
best.damage(5,dx/l,dz/l);return true;
}
return false;
}
function updateBreaking(dt){
const hit=raycast(5);
if(hit){highlight.visible=true;highlight.position.set(hit.x+.5,hit.y+.5,hit.z+.5);}
else highlight.visible=false;
if(mouseL&&hit&&!invOpen){
const dd=D[hit.id];
if(dd.hard>=0){
const same=breakTarget&&breakTarget.x===hit.x&&breakTarget.y===hit.y&&breakTarget.z===hit.z;
if(!same){breakTarget={x:hit.x,y:hit.y,z:hit.z};breakProgress=0;}
breakProgress+=dt/Math.max(.05,dd.hard);
swingT=1;
if(breakProgress>=1){
setBlock(hit.x,hit.y,hit.z,B.AIR);
spawnParticles(hit.x,hit.y,hit.z,dd.icon,12);
sndDig(hit.id);
breakTarget=null;breakProgress=0;
}
}
}else{breakTarget=null;breakProgress=0;}
if(breakTarget&&breakProgress>0){
crackMesh.visible=true;
crackMesh.position.set(breakTarget.x+.5,breakTarget.y+.5,breakTarget.z+.5);
setCrackStage(Math.floor(breakProgress*8));
}else crackMesh.visible=false;
placeTimer-=dt;
if(mouseR&&!invOpen&&placeTimer<=0&&hit){
placeTimer=.22;
const px2=hit.x+hit.nx,py2=hit.y+hit.ny,pz2=hit.z+hit.nz;
const cur=getBlock(px2,py2,pz2);
if(cur===B.AIR||cur===B.WATER||cur===B.TALLGRASS){
const id=hotbar[hotSel],dd=D[id];
let blocked=false;
if(dd.solid){
const ox=Math.abs(player.x-(px2+.5)),oz=Math.abs(player.z-(pz2+.5));
if(ox<HALF+.5&&oz<HALF+.5&&player.y+PH>py2&&player.y<py2+1)blocked=true;
}
if(!blocked){setBlock(px2,py2,pz2,id);sndPlace();swingT=1;}
}
}
}
/* ---------------- Day/Night ---------------- */
let sunElev=1,dayFactor=1;
const colNight=new THREE.Color(.04,.05,.12),colDay=new THREE.Color(.49,.66,1),colSet=new THREE.Color(1,.55,.28);
function updateDayNight(dt){
worldTime+=dt;
const tod=(worldTime/DAY)%1;
const ang=tod*Math.PI*2;
sunElev=Math.sin(ang);
dayFactor=smooth(-.08,.16,sunElev);
skyPivot.position.copy(camera.position);
skyPivot.rotation.z=ang;
sunMesh.lookAt(camera.position);moonMesh.lookAt(camera.position);
stars.material.opacity=1-dayFactor;
const sky=colNight.clone().lerp(colDay,dayFactor);
const sunsetF=clamp(1-Math.abs(sunElev)*5,0,1)*.55;
sky.lerp(colSet,sunsetF);
scene.background.copy(sky);scene.fog.color.copy(sky);
scene.fog.near=RD*16*.55;scene.fog.far=RD*16*.95;
const bright=.28+.72*dayFactor;
matOpaque.color.setScalar(bright);matWater.color.setScalar(bright);
ambLight.intensity=.35+.45*dayFactor;
sunLight.intensity=.15+.65*dayFactor;
const sd=new THREE.Vector3(Math.cos(ang),Math.abs(Math.sin(ang))*.8+.2,.3).normalize();
sunLight.position.copy(camera.position).addScaledVector(sd,80);
sunLight.target.position.copy(camera.position);
// clouds drift
cloudGroup.children.forEach(c=>{
c.position.x+=1.4*dt;
if(c.position.x-player.x>240)c.position.x-=480;
if(c.position.x-player.x<-240)c.position.x+=480;
if(c.position.z-player.z>240)c.position.z-=480;
if(c.position.z-player.z<-240)c.position.z+=480;
c.material.opacity=.18+.34*dayFactor;
});
}
/* ---------------- Chunk streaming ---------------- */
function updateStream(){
const pcx=Math.floor(player.x/16),pcz=Math.floor(player.z/16);
const want=[];
for(let dx=-RD;dx<=RD;dx++)for(let dz=-RD;dz<=RD;dz++){
const cx=pcx+dx,cz=pcz+dz;
if(!chunks.has(ckey(cx,cz)))want.push([cx,cz,dx*dx+dz*dz]);
}
want.sort((a,b)=>a[2]-b[2]);
for(let i=0;i<Math.min(2,want.length);i++)genChunk(want[i][0],want[i][1]);
if(dirty.size){
const list=[...dirty].map(k=>{const[cx,cz]=k.split(',').map(Number);return[k,(cx-pcx)**2+(cz-pcz)**2];}).sort((a,b)=>a[1]-b[1]);
let n=0;
for(const[k]of list){
const c=chunks.get(k);dirty.delete(k);
if(c){meshChunk(c);if(++n>=3)break;}
}
}
if(frame%180===0){
for(const[k,c]of chunks){
const[cx,cz]=k.split(',').map(Number);
if(Math.abs(cx-pcx)>RD+2||Math.abs(cz-pcz)>RD+2){
if(c.meshO){scene.remove(c.meshO);c.meshO.geometry.dispose();}
if(c.meshW){scene.remove(c.meshW);c.meshW.geometry.dispose();}
chunks.delete(k);
}
}
}
}
/* ---------------- UI ---------------- */
let hotbar=[B.GRASS,B.DIRT,B.STONE,B.LOG,B.PLANKS,B.COBBLE,B.GLASS,B.SAND,B.BRICK],hotSel=0;
const INV_ITEMS=[B.GRASS,B.DIRT,B.STONE,B.COBBLE,B.PLANKS,B.LOG,B.LEAVES,B.SAND,B.SANDSTONE,B.GRAVEL,B.BRICK,B.GLASS,B.GLOW,B.SNOWGRASS,B.SNOW,B.COAL,B.IRON,B.GOLD,B.DIAMOND,B.CACTUS,B.FLOWER_R,B.FLOWER_Y,B.TALLGRASS,B.WATER,B.BEDROCK];
function drawIcon(cv,id){
const ctx=cv.getContext('2d');ctx.imageSmoothingEnabled=false;
ctx.clearRect(0,0,cv.width,cv.height);
const t=D[id].icon,o=tCtx(t);
ctx.drawImage(atlas,o.ox,o.oy,16,16,0,0,cv.width,cv.height);
}
function buildHotbar(){
const hb=$('hotbar');hb.innerHTML='';
for(let i=0;i<9;i++){
const s=document.createElement('div');s.className='slot'+(i===hotSel?' sel':'');
const cv=document.createElement('canvas');cv.width=cv.height=32;cv.className='pixel';
drawIcon(cv,hotbar[i]);s.appendChild(cv);hb.appendChild(s);
}
}
function selectSlot(i){
hotSel=((i%9)+9)%9;
document.querySelectorAll('#hotbar .slot').forEach((s,k)=>s.classList.toggle('sel',k===hotSel));
updateHandMesh();showItemName();
}
let nameTimer;
function showItemName(){
const el=$('itemname');el.textContent=D[hotbar[hotSel]].name;el.style.opacity=1;
clearTimeout(nameTimer);nameTimer=setTimeout(()=>el.style.opacity=0,1300);
}
function updateHearts(){
const n=Math.ceil(clamp(player.hp,0,20)/2);let s='';
for(let i=0;i<10;i++)s+=`<span style="color:${i<n?'#e3340b':'#3a3a3a'}">♥</span>`;
$('hearts').innerHTML=s;
}
function buildInventory(){
const g=$('invGrid');g.innerHTML='';
INV_ITEMS.forEach(id=>{
const s=document.createElement('div');s.className='invSlot';s.title=D[id].name;
const cv=document.createElement('canvas');cv.width=cv.height=32;cv.className='pixel';
drawIcon(cv,id);s.appendChild(cv);
s.onclick=()=>{hotbar[hotSel]=id;buildHotbar();updateHandMesh();showItemName();closeInv();};
g.appendChild(s);
});
}
function openInv(){invOpen=true;$('invScreen').style.display='block';document.exitPointerLock();}
function closeInv(){invOpen=false;$('invScreen').style.display='none';if(playing&&!dead)renderer.domElement.requestPointerLock();}
/* ---------------- Save / Load ---------------- */
const SAVE_KEY='minejs_save_v1';
function save(){
if(!playing)return;
const ed={};for(const[k,v]of editsByChunk)ed[k]=v;
try{localStorage.setItem(SAVE_KEY,JSON.stringify({seed:SEED,edits:ed,time:worldTime,hotbar,
p:{x:player.x,y:player.y,z:player.z,yaw:player.yaw,pitch:player.pitch,hp:player.hp,fly:player.fly}}));}catch(e){}
}
function load(){
try{
const s=JSON.parse(localStorage.getItem(SAVE_KEY));
if(!s)return null;
return s;
}catch(e){return null;}
}
/* ---------------- Input ---------------- */
function setupInput(){
const canvas=renderer.domElement;
document.addEventListener('keydown',e=>{
if(e.code==='F5'||e.code==='F12')return;
keys[e.code]=true;
if(!playing)return;
if(e.code.startsWith('Digit')){const n=parseInt(e.code.slice(5));if(n>=1&&n<=9)selectSlot(n-1);}
if(e.code==='KeyW'){ // double-tap sprint
const now=performance.now();
if(now-lastWTap<280)player.sprint=true;
lastWTap=now;
}
if(e.code==='KeyF'){player.fly=!player.fly;player.vy=0;showMsg(player.fly?'Flight: ON':'Flight: OFF');}
if(e.code==='KeyE'){if(invOpen)closeInv();else if(!dead)openInv();}
if(e.code==='KeyN'){worldTime+=DAY/2;showMsg('Time skipped');}
if(e.code==='KeyG'||e.code==='KeyH'){
const hit=raycast(20);
if(hit){const t=e.code==='KeyG'?(Math.random()<.5?'pig':'sheep'):'zombie';
mobs.push(new Mob(t,hit.x+.5,hit.y+1,hit.z+.5));sndPop();}
}
if(e.code==='BracketLeft'){RD=clamp(RD-1,3,8);showMsg('Render distance: '+RD);}
if(e.code==='BracketRight'){RD=clamp(RD+1,3,8);showMsg('Render distance: '+RD);}
if(e.code==='Space')e.preventDefault();
});
document.addEventListener('keyup',e=>{keys[e.code]=false;});
document.addEventListener('mousemove',e=>{
if(document.pointerLockElement!==canvas)return;
player.yaw-=e.movementX*.0022;
player.pitch=clamp(player.pitch-e.movementY*.0022,-Math.PI/2+.01,Math.PI/2-.01);
});
canvas.addEventListener('mousedown',e=>{
ac();
if(document.pointerLockElement!==canvas)return;
if(e.button===0){
if(tryHitMob()){swingT=1;}
mouseL=true;
}
if(e.button===2)mouseR=true;
if(e.button===1){
e.preventDefault();
const hit=raycast(5);
if(hit&&hit.id!==B.AIR){hotbar[hotSel]=hit.id;buildHotbar();updateHandMesh();showItemName();}
}
});
document.addEventListener('mouseup',e=>{
if(e.button===0)mouseL=false;
if(e.button===2)mouseR=false;
});
document.addEventListener('wheel',e=>{
if(!playing||invOpen)return;
selectSlot(hotSel+(e.deltaY>0?1:-1));
},{passive:true});
document.addEventListener('contextmenu',e=>e.preventDefault());
document.addEventListener('pointerlockchange',()=>{
if(document.pointerLockElement===canvas){
$('pauseScreen').style.display='none';
$('titleScreen').style.display='none';
$('hud').style.display='block';
playing=true;
}else{
mouseL=mouseR=false;keys.KeyW=keys.KeyA=keys.KeyS=keys.KeyD=keys.Space=keys.ShiftLeft=false;
if(playing&&!invOpen&&!dead)$('pauseScreen').style.display='flex';
}
});
$('playBtn').onclick=()=>{ac();canvas.requestPointerLock();};
$('resumeBtn').onclick=()=>canvas.requestPointerLock();
$('newWorldBtn').onclick=()=>{localStorage.removeItem(SAVE_KEY);location.reload();};
$('resetBtn').onclick=()=>{localStorage.removeItem(SAVE_KEY);location.reload();};
$('respawnBtn').onclick=()=>{
dead=false;player.hp=20;updateHearts();
player.x=spawnPoint.x;player.y=spawnPoint.y;player.z=spawnPoint.z;
player.vx=player.vy=player.vz=0;player.peakY=player.y;
$('deathScreen').style.display='none';
canvas.requestPointerLock();
};
}
let msgTimer;
function showMsg(t){
const el=$('itemname');el.textContent=t;el.style.opacity=1;
clearTimeout(msgTimer);msgTimer=setTimeout(()=>el.style.opacity=0,1400);
}
/* ---------------- Debug ---------------- */
let fps=0,fpsAcc=0,fpsN=0;
function updateDebug(dt){
fpsAcc+=dt;fpsN++;
if(fpsAcc>.5){fps=Math.round(fpsN/fpsAcc);fpsAcc=0;fpsN=0;}
const tod=((worldTime/DAY)%1*24+6)%24;
$('debug').innerHTML=
`MineJS ${fps} fps<br>`+
`XYZ: ${player.x.toFixed(1)} / ${player.y.toFixed(1)} / ${player.z.toFixed(1)}<br>`+
`Biome: ${biomeAt(Math.floor(player.x),Math.floor(player.z))} Time: ${tod|0}:${(''+((tod%1*60)|0)).padStart(2,'0')}<br>`+
`Chunks: ${chunks.size} Mobs: ${mobs.length} Seed: ${SEED}`;
}
/* ---------------- Init + Main loop ---------------- */
let frame=0;
const clock=new THREE.Clock();
function findSpawn(){
for(let r=0;r<200;r+=4){
for(let a=0;a<Math.PI*2;a+=.7){
const x=Math.floor(Math.sin(a)*r)+8,z=Math.floor(Math.cos(a)*r)+8;
const h=groundH(x,z);
if(h>SEA+1)return{x:x+.5,y:h+2,z:z+.5};
}
}
return{x:8.5,y:45,z:8.5};
}
function init(){
const saveData=load();
if(saveData&&saveData.seed!==undefined){
SEED=saveData.seed;
worldTime=saveData.time||DAY*.06;
if(saveData.hotbar)hotbar=saveData.hotbar;
for(const k in saveData.edits)editsByChunk.set(k,saveData.edits[k]);
}else{
SEED=(Math.random()*0x7fffffff)|0;
worldTime=DAY*.06;
}
perlin=makePerlin(SEED);
genTextures();
setupScene();
setupInput();
buildHotbar();buildInventory();updateHearts();updateHandMesh();
// spawn position
spawnPoint=findSpawn();
if(saveData&&saveData.p){
Object.assign(player,{x:saveData.p.x,y:saveData.p.y,z:saveData.p.z,
yaw:saveData.p.yaw,pitch:saveData.p.pitch,hp:saveData.p.hp??20,fly:!!saveData.p.fly});
}else{
player.x=spawnPoint.x;player.y=spawnPoint.y;player.z=spawnPoint.z;
}
player.peakY=player.y;updateHearts();
// pre-generate spawn area synchronously
$('loadingText').textContent='Generating terrain...';
const pcx=Math.floor(player.x/16),pcz=Math.floor(player.z/16);
setTimeout(()=>{
for(let dx=-2;dx<=2;dx++)for(let dz=-2;dz<=2;dz++)
if(!chunks.has(ckey(pcx+dx,pcz+dz)))genChunk(pcx+dx,pcz+dz);
for(const k of[...dirty]){const c=chunks.get(k);if(c)meshChunk(c);dirty.delete(k);}
// make sure player isn't inside terrain
while(collides())player.y+=1;
$('loadingText').textContent='World ready! Seed: '+SEED;
const pb=$('playBtn');pb.disabled=false;pb.textContent=saveData?'Continue World':'Play';
clock.getDelta();
animate();
},50);
setInterval(save,15000);
addEventListener('beforeunload',save);
}
function animate(){
requestAnimationFrame(animate);
frame++;
const dt=clamp(clock.getDelta(),0,.1);
if(playing&&!dead&&!invOpen&&document.pointerLockElement===renderer.domElement){
updatePlayer(dt);
updateBreaking(dt);
updateMobs(dt);
}
if(playing){
updateStream();
updateParticles(dt);
updateDayNight(dt);
updateDebug(dt);
}else{
// gentle camera pan on title screen
updateDayNight(dt*0);
camera.position.set(player.x,player.y+EYE+6,player.z);
camera.rotation.set(-.4,worldTime*.01+frame*.0005,0);
updateStream();
}
renderer.render(scene,camera);
}
init();
</script>
</body>
</html>
1 个帖子 - 1 位参与者