Meta Description" name="description" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crystal Depths</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{overflow:hidden;background:#000;cursor:crosshair}
canvas{display:block}
</style>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
</head>
<body>
<canvas id="game"></canvas>
<script>
const C=document.getElementById('game'),ctx=C.getContext('2d');
function resize(){C.width=innerWidth;C.height=innerHeight}
addEventListener('resize',resize);resize();
/* ===== TILE IDS ===== */
const AIR=0,GRASS=1,DIRT=2,STONE=3,SAND=4,SANDSTONE=5,WOOD=6,LEAVES=7,
CACTUS=8,COAL=9,COPPER=10,IRON=11,GOLD=12,CRYSTAL=13,RUBY=14,
EMERALD=15,MITHRIL=16,PLANKS=17,BENCH=18,TORCH=19,BRICK=20,
OBSIDIAN=21,MUSH_BLK=22,CHEST=23,SPAWNER=24;
/* ===== ITEM IDS ===== */
const W_PICK=100,S_PICK=101,CU_PICK=102,IR_PICK=103,CR_PICK=104,MT_PICK=105;
const W_SWORD=110,S_SWORD=111,CU_SWORD=112,IR_SWORD=113,CR_SWORD=114,MT_SWORD=115;
const BERRY=150,CK_MEAT=151,H_POT=152,M_SOUP=153;
/* ===== STATE ===== */
let WW=250,WH=120,SEED=12345,DIFF=1,wSeed=12345;
let GRAV=0.42,JUMP_V=-7.5,SPD=2.8,REACH=5.5;
let world=[],surfY=[],skyExp=[],biomes=[];
let pl={x:0,y:0,vx:0,vy:0,w:10,h:16,hp:100,maxHp:100,dir:1,gr:false,fr:0,invT:0,atkCd:0,hunger:100};
let inv=new Array(30).fill(null),hSlot=0,cursor=null,curFrom=-1;
let mng={on:false,tx:0,ty:0,pr:0};
let enemies=[],particles=[],spTimers=new Map();
let cam={x:0,y:0},shake=0;
let keys={},ms={x:0,y:0,l:false,r:false};
let dayT=0.35,invOpen=false,titleScr=true;
let spawnX=0,spawnY=0;
let tSpr={},tIc={};
let darkCvs,darkCtx2,torchLCvs;
let titleParts=[];
let startSel={sz:1,df:1,seed:Math.floor(Math.random()*99999)};
/* ===== TILE DATA ===== */
const TD={};
function dT(id,s,hp,dr,c,n){TD[id]={s,hp,dr,c,n}}
dT(GRASS,1,3,DIRT,'#4ade80','Grass');dT(DIRT,1,2,DIRT,'#92700a','Dirt');
dT(STONE,1,5,STONE,'#7a7a7a','Stone');dT(SAND,1,1,SAND,'#d4a853','Sand');
dT(SANDSTONE,1,4,SANDSTONE,'#c49a42','Sandstone');dT(WOOD,1,3,WOOD,'#8B5E14','Wood');
dT(LEAVES,0,1,LEAVES,'#22a855','Leaves');dT(CACTUS,1,3,CACTUS,'#16a34a','Cactus');
dT(COAL,1,5,COAL,'#333','Coal');dT(COPPER,1,6,COPPER,'#d97706','Copper');
dT(IRON,1,8,IRON,'#b0b0b0','Iron');dT(GOLD,1,10,GOLD,'#facc15','Gold');
dT(CRYSTAL,1,12,CRYSTAL,'#22d3ee','Crystal');dT(RUBY,1,14,RUBY,'#e11d48','Ruby');
dT(EMERALD,1,14,EMERALD,'#10b981','Emerald');dT(MITHRIL,1,18,MITHRIL,'#a78bfa','Mithril');
dT(PLANKS,1,3,PLANKS,'#c49a5c','Planks');dT(BENCH,1,5,BENCH,'#a06020','Workbench');
dT(TORCH,0,1,TORCH,'#fbbf24','Torch');dT(BRICK,1,8,BRICK,'#b45309','Brick');
dT(OBSIDIAN,1,20,OBSIDIAN,'#1e1b2e','Obsidian');dT(MUSH_BLK,1,2,MUSH_BLK,'#c084fc','Mushroom');
dT(CHEST,1,3,null,'#92400e','Chest');dT(SPAWNER,1,15,null,'#374151','Spawner');
/* ===== ITEM DATA ===== */
const ID={};
function dI(id,n,t,v){ID[id]={n,t,v}}
dI(W_PICK,'Wood Pick','pick',1);dI(S_PICK,'Stone Pick','pick',2.2);
dI(CU_PICK,'Copper Pick','pick',3.5);dI(IR_PICK,'Iron Pick','pick',5);
dI(CR_PICK,'Crystal Pick','pick',7);dI(MT_PICK,'Mithril Pick','pick',10);
dI(W_SWORD,'Wood Sword','sword',8);dI(S_SWORD,'Stone Sword','sword',15);
dI(CU_SWORD,'Copper Sword','sword',22);dI(IR_SWORD,'Iron Sword','sword',32);
dI(CR_SWORD,'Crystal Sword','sword',45);dI(MT_SWORD,'Mithril Sword','sword',60);
dI(BERRY,'Berry','food',{h:15,heal:5});dI(CK_MEAT,'Cooked Meat','food',{h:35,heal:10});
dI(H_POT,'Health Potion','food',{h:0,heal:50});dI(M_SOUP,'Mush Soup','food',{h:25,heal:20});
/* ===== RECIPES ===== */
const recipes=[
{r:PLANKS,c:4,i:[{id:WOOD,c:4}],n:'Planks'},
{r:BENCH,c:1,i:[{id:PLANKS,c:10}],n:'Workbench'},
{r:TORCH,c:4,i:[{id:WOOD,c:1},{id:COAL,c:1}],n:'Torch',b:1},
{r:S_PICK,c:1,i:[{id:WOOD,c:3},{id:STONE,c:8}],n:'Stone Pick',b:1},
{r:S_SWORD,c:1,i:[{id:WOOD,c:2},{id:STONE,c:6}],n:'Stone Sword',b:1},
{r:CU_PICK,c:1,i:[{id:WOOD,c:3},{id:COPPER,c:8}],n:'Copper Pick',b:1},
{r:CU_SWORD,c:1,i:[{id:WOOD,c:2},{id:COPPER,c:6}],n:'Copper Sword',b:1},
{r:IR_PICK,c:1,i:[{id:WOOD,c:3},{id:IRON,c:8}],n:'Iron Pick',b:1},
{r:IR_SWORD,c:1,i:[{id:WOOD,c:2},{id:IRON,c:6}],n:'Iron Sword',b:1},
{r:CR_PICK,c:1,i:[{id:WOOD,c:3},{id:CRYSTAL,c:8}],n:'Crystal Pick',b:1},
{r:CR_SWORD,c:1,i:[{id:WOOD,c:2},{id:CRYSTAL,c:6}],n:'Crystal Sword',b:1},
{r:MT_PICK,c:1,i:[{id:IRON,c:3},{id:MITHRIL,c:8}],n:'Mithril Pick',b:1},
{r:MT_SWORD,c:1,i:[{id:IRON,c:2},{id:MITHRIL,c:6}],n:'Mithril Sword',b:1},
{r:H_POT,c:2,i:[{id:CRYSTAL,c:2},{id:LEAVES,c:4}],n:'Health Potion',b:1},
{r:M_SOUP,c:2,i:[{id:MUSH_BLK,c:3},{id:LEAVES,c:2}],n:'Mush Soup',b:1},
{r:CK_MEAT,c:1,i:[{id:LEAVES,c:3},{id:COAL,c:1}],n:'Cooked Meat',b:1},
{r:BRICK,c:4,i:[{id:STONE,c:4},{id:COAL,c:2}],n:'Brick',b:1},
{r:OBSIDIAN,c:1,i:[{id:STONE,c:5},{id:CRYSTAL,c:3}],n:'Obsidian',b:1}
];
/* ===== NOISE ===== */
function hash(n){n=((n>>16)^n)*45679+wSeed|0;n=((n>>16)^n)*45679|0;n=(n>>16)^n;return(n&0xffff)/0xffff}
function hash2(x,y){return hash(x*374761+y*668265)}
function n1(x){const i=Math.floor(x),f=x-i,t=f*f*(3-2*f);return hash(i)*(1-t)+hash(i+1)*t}
function fbm(x){return n1(x*0.014)*14+n1(x*0.04+100)*6+n1(x*0.09+200)*2.5}
function n2(x,y){const ix=Math.floor(x),iy=Math.floor(y),fx=x-ix,fy=y-iy;const tx=fx*fx*(3-2*fx),ty=fy*fy*(3-2*fy);const a=hash2(ix,iy),b=hash2(ix+1,iy),c=hash2(ix,iy+1),d=hash2(ix+1,iy+1);return(a*(1-tx)+b*tx)*(1-ty)+(c*(1-tx)+d*tx)*ty}
function inB(x,y){return x>=0&&x<WW&&y>=0&&y<WH}
function isSol(tx,ty){if(!inB(tx,ty))return ty>=WH;return TD[world[ty][tx]]?.s||false}
function lC(a,b,t){return[Math.round(a[0]+(b[0]-a[0])*t),Math.round(a[1]+(b[1]-a[1])*t),Math.round(a[2]+(b[2]-a[2])*t)]}
/* ===== WORLD GEN ===== */
function getBiome(x){const v=n1(x*0.008+wSeed*0.001);if(v<0.28)return'desert';if(v>0.72)return'rocky';return'forest'}
function genWorld(){
world=[];surfY=[];skyExp=[];biomes=[];
for(let y=0;y<WH;y++){world[y]=new Uint8Array(WW);skyExp[y]=new Uint8Array(WW)}
for(let x=0;x<WW;x++){biomes[x]=getBiome(x);surfY[x]=Math.max(20,Math.min(WH*0.4,Math.floor(WH*0.27+fbm(x))))}
// Fill terrain by biome
for(let x=0;x<WW;x++){
const sy=surfY[x],bio=biomes[x];
for(let y=sy;y<WH;y++){
if(y===sy){world[y][x]=bio==='desert'?SAND:bio==='rocky'?STONE:GRASS}
else if(y<sy+4+Math.floor(n1(x*0.3+500)*3)){world[y][x]=bio==='desert'?SANDSTONE:DIRT}
else world[y][x]=STONE;
}
}
// Caves
for(let y=0;y<WH;y++)for(let x=0;x<WW;x++){
if(y>surfY[x]+5){const v=n2(x*0.065,y*0.065)+n2(x*0.03+50,y*0.03+50)*0.5;if(v>0.82)world[y][x]=AIR}
}
// Ores
for(let y=0;y<WH;y++)for(let x=0;x<WW;x++){
if(world[y][x]===STONE){
const r=n2(x*0.15+1e3,y*0.15+1e3);
if(y>35&&r>0.81)world[y][x]=COAL;
else if(y>44&&r>0.84)world[y][x]=COPPER;
else if(y>55&&r>0.87)world[y][x]=IRON;
else if(y>68&&r>0.90)world[y][x]=GOLD;
else if(y>80&&r>0.92)world[y][x]=CRYSTAL;
else if(y>85&&r>0.93)world[y][x]=RUBY;
else if(y>88&&r>0.935)world[y][x]=EMERALD;
else if(y>95&&r>0.94)world[y][x]=MITHRIL;
}
}
// Mushroom zone underground
for(let y=Math.floor(WH*0.7);y<WH;y++)for(let x=0;x<WW;x++){
if(world[y][x]===STONE&&n2(x*0.1+2e3,y*0.1+2e3)>0.88)world[y][x]=MUSH_BLK;
}
// Trees / Cacti
for(let x=3;x<WW-3;x++){
const sy=surfY[x],bio=biomes[x];
if(bio==='desert'){
if(world[sy][x]===SAND&&hash(x*17+33)<0.06){
for(let dy=1;dy<=3+Math.floor(hash(x*19)*2);dy++)if(sy-dy>=0)world[sy-dy][x]=CACTUS;
x+=3;
}
}else if(bio==='forest'){
if(world[sy][x]===GRASS&&hash(x*7+99)<0.16){
const h=4+Math.floor(hash(x*13+77)*3);
for(let dy=1;dy<=h;dy++)if(sy-dy>=0)world[sy-dy][x]=WOOD;
const top=sy-h;
for(let ly=top-2;ly<=top;ly++){const w=ly===top-2?1:2;for(let lx=-w;lx<=w;lx++){const tx2=x+lx,ty2=ly;if(inB(tx2,ty2)&&world[ty2][tx2]===AIR)world[ty2][tx2]=LEAVES}}
x+=3;
}
}else{
if(world[sy][x]===STONE&&hash(x*7+99)<0.06){
const h=3+Math.floor(hash(x*13+77)*2);
for(let dy=1;dy<=h;dy++)if(sy-dy>=0)world[sy-dy][x]=WOOD;
x+=4;
}
}
}
// Dungeons
const nDng=3+Math.floor(hash(wSeed+999)*5);
for(let d=0;d<nDng;d++){
const dw=8+Math.floor(hash(wSeed+d*100)*12),dh=6+Math.floor(hash(wSeed+d*200)*8);
const dx=10+Math.floor(hash(wSeed+d*300)*(WW-dw-20)),dy=Math.floor(WH*0.4)+Math.floor(hash(wSeed+d*400)*(WH*0.5-dh));
for(let y=dy;y<dy+dh&&y<WH;y++)for(let x=dx;x<dx+dw&&x<WW;x++){
if(x===dx||x===dx+dw-1||y===dy||y===dy+dh-1)world[y][x]=BRICK;
else world[y][x]=AIR;
}
// Entrance
const ex=dx+Math.floor(dw/2);
for(let y=dy-1;y<=dy+1&&y<WH;y++)if(y>=0&&inB(ex,y))world[y][ex]=AIR;
// Chest
const cx=dx+2+Math.floor(hash(wSeed+d*500)*(dw-4)),cy=dy+1+Math.floor(hash(wSeed+d*600)*(dh-3));
if(inB(cx,cy)&&cy<WH)world[cy][cx]=CHEST;
// Spawner
const sx2=dx+2+Math.floor(hash(wSeed+d*700)*(dw-4)),sy2=dy+1+Math.floor(hash(wSeed+d*800)*(dh-3));
if(inB(sx2,sy2)&&sy2<WH&&world[sy2][sx2]===AIR)world[sy2][sx2]=SPAWNER;
}
// Sky exposure
for(let x=0;x<WW;x++)calcSky(x);
// Spawn
spawnX=Math.floor(WW/2);spawnY=surfY[spawnX]-2;
pl.x=spawnX*T+T/2;pl.y=spawnY*T;
cam.x=pl.x-C.width/2;cam.y=pl.y-C.height/2;
}
function calcSky(x){let bl=false;for(let y=0;y<WH;y++){skyExp[y][x]=bl?0:1;if(TD[world[y][x]]?.s)bl=true}}
/* ===== SPRITES ===== */
function initSprites(){
for(const id of[GRASS,DIRT,STONE,SAND,SANDSTONE,WOOD,LEAVES,CACTUS,COAL,COPPER,IRON,GOLD,CRYSTAL,RUBY,EMERALD,MITHRIL,PLANKS,BENCH,TORCH,BRICK,OBSIDIAN,MUSH_BLK,CHEST,SPAWNER]){
const c=document.createElement('canvas');c.width=c.height=T;drawTS(c.getContext('2d'),id);tSpr[id]=c;
}
for(const id in ID){const c=document.createElement('canvas');c.width=c.height=28;drawTI(c.getContext('2d'),+id,28);tIc[id]=c}
// Torch light sprite
const r=T*8;torchLCvs=document.createElement('canvas');torchLCvs.width=torchLCvs.height=r*2;
const tc=torchLCvs.getContext('2d'),g=tc.createRadialGradient(r,r,0,r,r,r);
g.addColorStop(0,'rgba(0,0,0,0.97)');g.addColorStop(0.3,'rgba(0,0,0,0.7)');g.addColorStop(0.7,'rgba(0,0,0,0.25)');g.addColorStop(1,'rgba(0,0,0,0)');
tc.fillStyle=g;tc.fillRect(0,0,r*2,r*2);
// Dark canvas
darkCvs=document.createElement('canvas');darkCvs.width=C.width;darkCvs.height=C.height;
darkCtx2=darkCvs.getContext('2d');
}
function drawTS(x,id){
switch(id){
case GRASS:x.fillStyle='#8B6914';x.fillRect(0,0,T,T);x.fillStyle='#6d5510';x.fillRect(3,8,2,2);x.fillRect(10,12,2,1);x.fillStyle='#4ade80';x.fillRect(0,0,T,5);x.fillStyle='#22c55e';for(let i=0;i<5;i++)x.fillRect(i*3+1,1,1,-1-((i*7)%3));break;
case DIRT:x.fillStyle='#92700a';x.fillRect(0,0,T,T);x.fillStyle='#7a5c10';x.fillRect(2,3,2,2);x.fillRect(9,8,2,2);break;
case STONE:x.fillStyle='#7a7a7a';x.fillRect(0,0,T,T);x.fillStyle='#666';x.fillRect(0,7,T,1);x.fillRect(8,0,1,7);x.fillStyle='#8a8a8a';x.fillRect(3,2,2,2);break;
case SAND:x.fillStyle='#d4a853';x.fillRect(0,0,T,T);x.fillStyle='#c49a42';x.fillRect(2,4,1,1);x.fillRect(8,2,1,1);x.fillRect(13,9,1,1);break;
case SANDSTONE:x.fillStyle='#c49a42';x.fillRect(0,0,T,T);x.fillStyle='#b08530';x.fillRect(0,4,T,1);x.fillRect(0,10,T,1);x.fillStyle='#d4a853';x.fillRect(3,6,3,2);break;
case WOOD:x.fillStyle='#8B5E14';x.fillRect(0,0,T,T);x.fillStyle='#6d4a10';x.fillRect(4,0,1,T);x.fillRect(10,0,1,T);break;
case LEAVES:x.fillStyle='#22a855';x.fillRect(0,0,T,T);x.fillStyle='#1a8a44';x.fillRect(2,2,3,3);x.fillRect(9,5,4,3);x.fillStyle='#2ecc60';x.fillRect(0,5,2,2);x.fillRect(12,1,2,2);break;
case CACTUS:x.fillStyle='#16a34a';x.fillRect(5,0,6,T);x.fillStyle='#15803d';x.fillRect(0,4,5,3);x.fillRect(11,7,5,3);x.fillStyle='#22c55e';x.fillRect(6,1,4,1);break;
case COAL:case COPPER:case IRON:case GOLD:case CRYSTAL:case RUBY:case EMERALD:case MITHRIL:
x.fillStyle='#7a7a7a';x.fillRect(0,0,T,T);x.fillStyle='#666';x.fillRect(0,7,T,1);
x.fillStyle={[COAL]:'#222',[COPPER]:'#d97706',[IRON]:'#b0b0b0',[GOLD]:'#facc15',[CRYSTAL]:'#22d3ee',[RUBY]:'#e11d48',[EMERALD]:'#10b981',[MITHRIL]:'#a78bfa'}[id];
[[2,3],[8,2],[12,9],[5,11],[1,8],[10,5]].forEach(([sx,sy])=>x.fillRect(sx,sy,2+((sx+sy)%2),2));break;
case PLANKS:x.fillStyle='#c49a5c';x.fillRect(0,0,T,T);x.fillStyle='#a67c42';x.fillRect(0,3,T,1);x.fillRect(0,7,T,1);x.fillRect(0,11,T,1);x.fillRect(8,0,1,T);break;
case BENCH:x.fillStyle='#a06020';x.fillRect(0,0,T,T);x.fillStyle='#7a4a18';x.fillRect(0,4,T,1);x.fillRect(0,8,T,1);x.fillRect(0,12,T,1);x.fillStyle='#c07030';x.fillRect(2,1,4,3);x.fillRect(10,1,4,3);x.fillRect(2,5,4,3);x.fillRect(10,5,4,3);break;
case TORCH:x.fillStyle='#8B5E14';x.fillRect(7,6,2,10);x.fillStyle='#fbbf24';x.fillRect(6,2,4,5);x.fillStyle='#fff7a0';x.fillRect(7,3,2,3);break;
case BRICK:x.fillStyle='#92400e';x.fillRect(0,0,T,T);x.fillStyle='#b45309';x.fillRect(0,0,7,7);x.fillRect(9,0,7,7);x.fillRect(4,8,7,7);x.fillRect(0,8,3,7);x.fillRect(12,8,4,7);x.fillStyle='#78350f';x.fillRect(7,0,2,7);x.fillRect(3,8,1,7);x.fillRect(11,8,1,7);break;
case OBSIDIAN:x.fillStyle='#1e1b2e';x.fillRect(0,0,T,T);x.fillStyle='#2d2640';x.fillRect(2,2,4,3);x.fillRect(10,8,4,3);x.fillStyle='#151225';x.fillRect(6,10,3,3);break;
case MUSH_BLK:x.fillStyle='#7c3aed';x.fillRect(0,0,T,T);x.fillStyle='#c084fc';x.beginPath();x.arc(5,4,4,Math.PI,0);x.fill();x.beginPath();x.arc(12,3,3,Math.PI,0);x.fill();x.fillStyle='#a855f7';x.fillRect(4,6,8,5);break;
case CHEST:x.fillStyle='#78350f';x.fillRect(2,4,12,10);x.fillStyle='#92400e';x.fillRect(1,2,14,4);x.fillStyle='#fbbf24';x.fillRect(6,5,4,3);x.fillStyle='#f59e0b';x.fillRect(7,6,2,1);break;
case SPAWNER:x.fillStyle='#1f2937';x.fillRect(0,0,T,T);x.strokeStyle='#7f1d1d';x.lineWidth=1;x.strokeRect(3,3,10,10);x.fillStyle='#991b1b';x.fillRect(7,6,2,2);x.fillRect(5,9,2,2);x.fillRect(9,9,2,2);break;
}
}
function drawTI(x,id,s){
const m=s/2;x.save();x.translate(m,m);x.rotate(-Math.PI/4);
if(id>=W_PICK&&id<=MT_PICK){
x.fillStyle='#8B6914';x.fillRect(-1.5,-s*0.3,3,s*0.65);
const cols={[W_PICK]:'#aaa',[S_PICK]:'#888',[CU_PICK]:'#d97706',[IR_PICK]:'#b0b0b0',[CR_PICK]:'#22d3ee',[MT_PICK]:'#a78bfa'};
x.fillStyle=cols[id];x.fillRect(-s*0.28,-s*0.35,s*0.56,5);
}else if(id>=W_SWORD&&id<=MT_SWORD){
x.fillStyle='#8B6914';x.fillRect(-1.5,s*0.05,3,s*0.3);x.fillRect(-s*0.13,0,s*0.26,3);
const cols={[W_SWORD]:'#bbb',[S_SWORD]:'#999',[CU_SWORD]:'#d97706',[IR_SWORD]:'#b0b0b0',[CR_SWORD]:'#22d3ee',[MT_SWORD]:'#a78bfa'};
x.fillStyle=cols[id];x.fillRect(-2.5,-s*0.38,5,s*0.4);
x.beginPath();x.moveTo(-2.5,-s*0.38);x.lineTo(0,-s*0.48);x.lineTo(2.5,-s*0.38);x.fill();
}else if(id>=BERRY){
x.setTransform(1,0,0,1,0,0);
const fc={[BERRY]:'#ef4444',[CK_MEAT]:'#a0522d',[H_POT]:'#166534',[M_SOUP]:'#7c3aed'}[id]||'#888';
x.fillStyle=fc;x.fillRect(m-5,m-3,10,8);
if(id===H_POT){x.fillStyle='#15803d';x.fillRect(m-2,m-6,4,4);x.fillStyle='#92400e';x.fillRect(m-2,m-8,4,3)}
if(id===M_SOUP){x.fillStyle='#c084fc';x.fillRect(m-3,m-5,6,3)}
x.fillStyle='rgba(255,255,255,0.25)';x.fillRect(m-3,m-1,3,5);
}
x.restore();
}
/* ===== INVENTORY ===== */
function addItem(id,count){
if(id<100)for(let i=0;i<30;i++)if(inv[i]&&inv[i].id===id){inv[i].c+=count;return true}
for(let i=0;i<30;i++)if(!inv[i]){inv[i]={id,c:count};return true}
return false;
}
function cntItem(id){let t=0;for(let i=0;i<30;i++)if(inv[i]&&inv[i].id===id)t+=inv[i].c;return t}
function rmItems(id,count){let rem=count;for(let i=0;i<30;i++){if(inv[i]&&inv[i].id===id){const tk=Math.min(inv[i].c,rem);inv[i].c-=tk;rem-=tk;if(inv[i].c<=0)inv[i]=null;if(rem<=0)return true}}return rem<=0}
function nearBench(){const px=Math.floor(pl.x/T),py=Math.floor(pl.y/T);for(let dy=-3;dy<=3;dy++)for(let dx=-3;dx<=3;dx++){const tx=px+dx,ty=py+dy;if(inB(tx,ty)&&world[ty][tx]===BENCH)return true}return false}
function canCraft(r){for(const it of r.i)if(cntItem(it.id)<it.c)return false;if(r.b&&!nearBench())return false;return true}
function doCraft(r){if(!canCraft(r))return;for(const it of r.i)rmItems(it.id,it.c);addItem(r.r,r.c);spawnP(pl.x,pl.y-pl.h/2,'#4ade80',5)}
function chestLoot(depth){
const items=[{id:TORCH,c:3+Math.floor(Math.random()*5)}];
if(depth>20)items.push({id:COPPER,c:3+Math.floor(Math.random()*5)});
if(depth>35)items.push({id:IRON,c:2+Math.floor(Math.random()*4)});
if(depth>50)items.push({id:GOLD,c:1+Math.floor(Math.random()*3)});
if(depth>65)items.push({id:CRYSTAL,c:1+Math.floor(Math.random()*3)});
if(depth>75&&Math.random()>0.4)items.push({id:RUBY,c:1+Math.floor(Math.random()*2)});
if(depth>80&&Math.random()>0.5)items.push({id:EMERALD,c:1+Math.floor(Math.random()*2)});
if(depth>90&&Math.random()>0.6)items.push({id:MITHRIL,c:1+Math.floor(Math.random()*2)});
items.push({id:Math.random()>0.5?H_POT:CK_MEAT,c:1+Math.floor(Math.random()*2)});
return items;
}
function itemName(id){if(ID[id])return ID[id].n;if(id===200)return'Cu Sword';return TD[id]?.n||'???'}
/* ===== PARTICLES ===== */
function spawnP(x,y,col,n){for(let i=0;i<n;i++)particles.push({x,y,vx:(Math.random()-0.5)*4,vy:(Math.random()-0.5)*4-1,life:20+Math.random()*15,ml:35,col,sz:1.5+Math.random()*2})}
function updParts(){for(let i=particles.length-1;i>=0;i--){const p=particles[i];p.x+=p.vx;p.y+=p.vy;p.vy+=0.15;p.life--;if(p.life<=0)particles.splice(i,1)}}
/* ===== ENEMIES ===== */
function spawnEnemyAt(tx,ty){
if(enemies.length>15)return;
const depth=ty-(surfY[Math.min(tx,WW-1)]||35);
let type='green',hp=18,dmg=8,col='#4ade80',spd=0.8;
const dm=[0.5,1,1.8][DIFF];
if(depth>20||getDayLight()<0.5){type='blue';hp=30;dmg=14;col='#3b82f6';spd=1.2}
if(depth>40){type='crystal';hp=50;dmg=22;col='#22d3ee';spd=1.5}
if(depth>65){type='shadow';hp=70;dmg=30;col='#a855f7';spd=1.3}
enemies.push({x:tx*T+T/2,y:ty*T+T,vx:0,vy:0,w:14,h:12,hp,mhp:hp,dir:1,jt:30,type,col,dmg:dmg*dm,spd,gr:false,fr:0});
}
function spawnEnemy(){
if(enemies.length>12)return;
const dl=getDayLight();const sr=[0.003,0.015,0.03][DIFF];
if(dl>0.75&&Math.random()>sr)return;
const dir=Math.random()<0.5?-1:1;
const ex=Math.floor(pl.x/T)+dir*(14+Math.random()*10);
if(ex<2||ex>=WW-2)return;
let ey=-1;for(let y=0;y<WH-1;y++){if(TD[world[y][ex]]?.s&&y+1<WH&&world[y+1][ex]===AIR){ey=y;break}}
if(ey<0)return;
spawnEnemyAt(ex,ey);
}
function updEnemies(){
for(let i=enemies.length-1;i>=0;i--){
const e=enemies[i];
const dx=pl.x-e.x,dy=pl.y-e.y,dist=Math.sqrt(dx*dx+dy*dy);
if(dist>300){enemies.splice(i,1);continue}
if(dist<180){e.dir=dx>0?1:-1;e.vx=e.dir*e.spd;e.jt--;if(e.jt<=0&&e.gr){e.vy=e.type==='shadow'?-7:-5.5;e.jt=20+Math.random()*25}}
else e.vx*=0.9;
e.vy+=GRAV;if(e.vy>10)e.vy=10;
e.x+=e.vx;rColE(e,'x');e.y+=e.vy;e.gr=false;rColE(e,'y');
if(pl.invT<=0&&Math.abs(pl.x-e.x)<(pl.w+e.w)/2&&Math.abs((pl.y-pl.h/2)-(e.y-e.h/2))<(pl.h+e.h)/2){
pl.hp-=e.dmg;pl.invT=50;pl.vx=(pl.x>e.x?1:-1)*5;pl.vy=-4;shake=8;spawnP(pl.x,pl.y-pl.h/2,'#ef4444',6);
}
if(e.y>WH*T+100)enemies.splice(i,1);
e.fr++;
}
}
function rColE(e,axis){
const l=Math.floor((e.x-e.w/2)/T),r=Math.floor((e.x+e.w/2-0.1)/T);
const t=Math.floor((e.y-e.h)/T),b=Math.floor((e.y-0.1)/T);
if(axis==='x'){for(let y=t;y<=b;y++){if(e.vx>0&&isSol(r,y)){e.x=r*T-e.w/2;e.vx=0}if(e.vx<0&&isSol(l,y)){e.x=(l+1)*T+e.w/2;e.vx=0}}}
else{for(let x=l;x<=r;x++){if(e.vy>0&&isSol(x,b)){e.y=b*T;e.vy=0;e.gr=true}if(e.vy<0&&isSol(x,t)){e.y=(t+1)*T+e.h;e.vy=0}}}
}
/* ===== SPAWNERS ===== */
function updSpawners(){
const px=Math.floor(pl.x/T),py=Math.floor(pl.y/T);
for(let dy=-12;dy<=12;dy++)for(let dx=-12;dx<=12;dx++){
const tx=px+dx,ty=py+dy;
if(!inB(tx,ty)||world[ty][tx]!==SPAWNER)continue;
const k=tx+','+ty;if(Math.abs(dx)+Math.abs(dy)>10)continue;
if(!spTimers.has(k))spTimers.set(k,0);
spTimers.set(k,spTimers.get(k)-1);
if(spTimers.get(k)<=0&&enemies.length<15){
if(ty-1>=0&&world[ty-1][tx]===AIR)spawnEnemyAt(tx,ty-1);
spTimers.set(k,90+Math.floor(Math.random()*90));
}
}
}
/* ===== PLAYER ===== */
function updPlayer(){
pl.vx=0;
if(keys['KeyA']||keys['ArrowLeft']){pl.vx=-SPD;pl.dir=-1}
if(keys['KeyD']||keys['ArrowRight']){pl.vx=SPD;pl.dir=1}
if((keys['KeyW']||keys['ArrowUp']||keys['Space'])&&pl.gr)pl.vy=JUMP_V;
pl.vy+=GRAV;if(pl.vy>12)pl.vy=12;
pl.x+=pl.vx;
let pl2=Math.floor((pl.x-pl.w/2)/T),pr2=Math.floor((pl.x+pl.w/2-0.1)/T);
let pt2=Math.floor((pl.y-pl.h)/T),pb2=Math.floor((pl.y-0.1)/T);
for(let y=pt2;y<=pb2;y++){if(pl.vx>0&&isSol(pr2,y)){pl.x=pr2*T-pl.w/2;pl.vx=0}if(pl.vx<0&&isSol(pl2,y)){pl.x=(pl2+1)*T+pl.w/2;pl.vx=0}}
pl.y+=pl.vy;pl.gr=false;
pl2=Math.floor((pl.x-pl.w/2)/T);pr2=Math.floor((pl.x+pl.w/2-0.1)/T);
pt2=Math.floor((pl.y-pl.h)/T);pb2=Math.floor((pl.y-0.1)/T);
for(let x=pl2;x<=pr2;x++){if(pl.vy>0&&isSol(x,pb2)){pl.y=pb2*T;pl.vy=0;pl.gr=true}if(pl.vy<0&&isSol(x,pt2)){pl.y=(pt2+1)*T+pl.h;pl.vy=0}}
pl.x=Math.max(T,Math.min(WW*T-T,pl.x));
if(pl.y>WH*T+50)pl.hp=0;
if(Math.abs(pl.vx)>0.1)pl.fr++;
if(pl.invT>0)pl.invT--;if(pl.atkCd>0)pl.atkCd--;
// Hunger
const hr=[0.015,0.04,0.09][DIFF];
pl.hunger=Math.max(0,pl.hunger-hr);
// HP regen/drain
if(pl.hunger>60&&pl.hp<pl.maxHp)pl.hp=Math.min(pl.maxHp,pl.hp+0.03);
else if(pl.hunger<=0)pl.hp-=0.03;
if(pl.hp<=0){pl.hp=pl.maxHp;pl.hunger=80;pl.x=spawnX*T+T/2;pl.y=spawnY*T;pl.vx=0;pl.vy=0;pl.invT=90;shake=12}
}
/* ===== MINING & PLACE ===== */
function updMining(){
if(invOpen||titleScr){mng.on=false;return}
const wx=ms.x+cam.x,wy=ms.y+cam.y;
const tx=Math.floor(wx/T),ty=Math.floor(wy/T);
const px=pl.x/T,py=(pl.y-pl.h/2)/T;
const dist=Math.sqrt((tx+0.5-px)**2+(ty+0.5-py)**2);
if(!ms.l||dist>REACH||!inB(tx,ty)||world[ty][tx]===AIR){mng.on=false;return}
const tt=world[ty][tx],held=inv[hSlot];
// Sword attack
if(held&&ID[held.id]?.t==='sword'){
if(pl.atkCd<=0){for(const e of enemies){const ed=Math.sqrt((e.x-pl.x)**2+(e.y-e.h/2-(pl.y-pl.h/2))**2);if(ed/T<2.5){e.hp-=ID[held.id].v;e.vx=(e.x>pl.x?1:-1)*6;e.vy=-3;spawnP(e.x,e.y-e.h/2,e.col,5);shake=3}}pl.atkCd=15}
mng.on=false;return;
}
// Food use
if(held&&ID[held.id]?.t==='food'){
const fd=ID[held.id].v;
if(pl.hp<pl.maxHp||pl.hunger<100){
pl.hp=Math.min(pl.maxHp,pl.hp+(fd.heal||0));
pl.hunger=Math.min(100,pl.hunger+(fd.h||0));
held.c--;if(held.c<=0)inv[hSlot]=null;
spawnP(pl.x,pl.y-pl.h/2,'#4ade80',8);ms.l=false;
}
mng.on=false;return;
}
// Mining
if(!mng.on||mng.tx!==tx||mng.ty!==ty){mng.on=true;mng.tx=tx;mng.ty=ty;mng.pr=0}
let pw=0.4;if(held&&ID[held.id]?.t==='pick')pw=ID[held.id].v;
mng.pr+=pw;
const td=TD[tt];
if(td&&mng.pr>=td.hp){
if(tt===CHEST){const depth=ty-(surfY[Math.min(tx,WW-1)]||35);chestLoot(depth).forEach(it=>addItem(it.id,it.c));spawnP(tx*T+T/2,ty*T+T/2,'#fbbf24',10)}
else if(tt===SPAWNER){spawnP(tx*T+T/2,ty*T+T/2,'#ef4444',12)}
else if(td.dr!==null)addItem(td.dr,1);
world[ty][tx]=AIR;calcSky(tx);spawnP(tx*T+T/2,ty*T+T/2,td.c,6);mng.on=false;
}
}
function handlePlace(){
if(invOpen||titleScr)return;
const wx=ms.x+cam.x,wy=ms.y+cam.y;
const tx=Math.floor(wx/T),ty=Math.floor(wy/T);
const px=pl.x/T,py=(pl.y-pl.h/2)/T;
const dist=Math.sqrt((tx+0.5-px)**2+(ty+0.5-py)**2);
if(!ms.r||dist>REACH||!inB(tx,ty)||world[ty][tx]!==AIR)return;
const pl2=Math.floor((pl.x-pl.w/2)/T),pr2=Math.floor((pl.x+pl.w/2-0.1)/T);
const pt2=Math.floor((pl.y-pl.h)/T),pb2=Math.floor((pl.y-0.1)/T);
if(tx>=pl2&&tx<=pr2&&ty>=pt2&&ty<=pb2)return;
const held=inv[hSlot];
if(held&&held.id<100&&TD[held.id]){world[ty][tx]=held.id;held.c--;if(held.c<=0)inv[hSlot]=null;calcSky(tx);ms.r=false}
}
/* ===== DAY/NIGHT ===== */
function getDayLight(){return 0.65+0.35*Math.cos((dayT-0.5)*Math.PI*2)}
/* ===== UPDATE ===== */
function update(){
if(titleScr)return;
dayT+=0.0001;if(dayT>=1)dayT-=1;
updPlayer();updMining();handlePlace();updEnemies();updSpawners();updParts();
if(Math.random()<(0.01+DIFF*0.008))spawnEnemy();
const tx=pl.x-C.width/2,ty=pl.y-C.height/2;
cam.x+=(tx-cam.x)*0.12;cam.y+=(ty-cam.y)*0.12;
cam.x=Math.max(0,Math.min(WW*T-C.width,cam.x));
cam.y=Math.max(0,Math.min(WH*T-C.height,cam.y));
if(shake>0){shake*=0.85;if(shake<0.3)shake=0}
// Resize dark canvas if needed
if(darkCvs.width!==C.width||darkCvs.height!==C.height){darkCvs.width=C.width;darkCvs.height=C.height}
}
/* ===== RENDER ===== */
function rr(ctx,x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.lineTo(x+w-r,y);ctx.quadraticCurveTo(x+w,y,x+w,y+r);ctx.lineTo(x+w,y+h-r);ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);ctx.lineTo(x+r,y+h);ctx.quadraticCurveTo(x,y+h,x,y+h-r);ctx.lineTo(x,y+r);ctx.quadraticCurveTo(x,y,x+r,y);ctx.closePath()}
function drawBg(){
const dl=getDayLight();
const top=lC([10,15,20],[45,74,62],dl),bot=lC([15,25,30],[220,155,110],dl);
const g=ctx.createLinearGradient(0,0,0,C.height);
g.addColorStop(0,`rgb(${top})`);g.addColorStop(1,`rgb(${bot})`);
ctx.fillStyle=g;ctx.fillRect(0,0,C.width,C.height);
if(dl<0.6){const sa=Math.min(1,(0.6-dl)/0.25);ctx.fillStyle=`rgba(255,255,220,${sa})`;for(let i=0;i<80;i++){const sx=hash(i*3+10)*C.width,sy=hash(i*3+11)*C.height*0.5,ss=hash(i*3+12)*1.8+0.5;ctx.fillRect(sx,sy,ss,ss)}}
ctx.fillStyle=`rgba(${lC([12,22,28],[30,55,45],dl)},0.7)`;drawMtns(cam.x*0.08,0.55,90);
ctx.fillStyle=`rgba(${lC([18,28,32],[38,65,50],dl)},0.85)`;drawMtns(cam.x*0.18,0.5,65);
}
function drawMtns(ox,yF,amp){ctx.beginPath();ctx.moveTo(0,C.height);for(let x=0;x<=C.width;x+=3){const wx=x+ox,yy=C.height*yF-amp*n1(wx*0.003)-amp*0.5*n1(wx*0.008+50);ctx.lineTo(x,yy)}ctx.lineTo(C.width,C.height);ctx.closePath();ctx.fill()}
function drawPlayer(x,y){
const bob=pl.gr&&Math.abs(pl.vx)>0.1?Math.sin(pl.fr*0.35)*1.5:0;
if(pl.invT>0&&Math.floor(pl.invT/4)%2===0)ctx.globalAlpha=0.5;
ctx.save();ctx.translate(x,y+bob);if(pl.dir<0)ctx.scale(-1,1);
ctx.fillStyle='rgba(0,0,0,0.25)';ctx.fillRect(-6,0,12,2);
ctx.fillStyle='#c2410c';ctx.fillRect(-4,-4,3,4);ctx.fillRect(1,-4,3,4);
ctx.fillStyle='#ef4444';ctx.beginPath();ctx.ellipse(0,-10,7,6,0,Math.PI,0);ctx.fill();
ctx.fillStyle='#dc2626';ctx.fillRect(-5,-10,10,4);
ctx.fillStyle='#fbbf24';ctx.beginPath();ctx.arc(-3,-14,1.8,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(2,-12,1.5,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fef3c7';ctx.fillRect(-5,-10,10,5);
ctx.fillStyle='#1e293b';ctx.fillRect(-3,-9,2,2);ctx.fillRect(2,-9,2,2);
ctx.fillStyle='#fff';ctx.fillRect(-3,-9,1,1);ctx.fillRect(2,-9,1,1);
ctx.fillStyle='#92400e';ctx.fillRect(-1,-6,2,1);
const held=inv[hSlot];
if(held){ctx.save();ctx.translate(6,-8);ctx.rotate(0.3);if(held.id>=100&&tIc[held.id])ctx.drawImage(tIc[held.id],-10,-10,20,20);else if(tSpr[held.id])ctx.drawImage(tSpr[held.id],-8,-8,16,16);ctx.restore()}
ctx.restore();ctx.globalAlpha=1;
}
function render(){
ctx.clearRect(0,0,C.width,C.height);
ctx.save();
if(shake>0)ctx.translate((Math.random()-0.5)*shake,(Math.random()-0.5)*shake);
drawBg();
const sx=Math.floor(cam.x/T)-1,ex=Math.ceil((cam.x+C.width)/T)+1;
const sy=Math.floor(cam.y/T)-1,ey=Math.ceil((cam.y+C.height)/T)+1;
// Collect torches for glow
const torches=[];
for(let y=Math.max(0,sy-9);y<=Math.min(WH-1,ey+9);y+=2)for(let x=Math.max(0,sx-9);x<=Math.min(WW-1,ex+9);x+=2){if(world[y]&&world[y][x]===TORCH)torches.push({x,y})}
// Draw tiles
for(let ty=Math.max(0,sy);ty<=Math.min(WH-1,ey);ty++){
for(let tx=Math.max(0,sx);tx<=Math.min(WW-1,ex);tx++){
const tt=world[ty][tx];if(tt===AIR)continue;
const dx=tx*T-cam.x,dy=ty*T-cam.y;
ctx.drawImage(tSpr[tt],dx,dy);
// Torch ambient glow
if(tt===TORCH){ctx.fillStyle='rgba(251,191,36,0.04)';ctx.fillRect(dx-T*3,dy-T*3,T*7,T*7)}
// Mining cracks
if(mng.on&&mng.tx===tx&&mng.ty===ty&&TD[tt]){
const prog=mng.pr/TD[tt].hp;
ctx.strokeStyle=`rgba(0,0,0,${0.4+prog*0.5})`;ctx.lineWidth=1.5;
const nc=Math.floor(prog*6)+1;
for(let i=0;i<nc;i++){ctx.beginPath();ctx.moveTo(dx+T/2,dy+T/2);const a=(i/nc)*Math.PI*2+0.3;ctx.lineTo(dx+T/2+Math.cos(a)*T*0.55,dy+T/2+Math.sin(a)*T*0.55);ctx.stroke()}
}
}
}
// Enemies
for(const e of enemies){
const ex2=e.x-cam.x,ey2=e.y-cam.y;
if(ex2<-30||ex2>C.width+30||ey2<-30||ey2>C.height+30)continue;
const sq=e.gr?1+Math.sin(e.fr*0.15)*0.08:1;
ctx.save();ctx.translate(ex2,ey2);ctx.scale(e.dir,1);ctx.scale(1/sq,sq);
ctx.fillStyle=e.col;ctx.beginPath();ctx.ellipse(0,-e.h/2,e.w/2,e.h/2,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.15)';ctx.beginPath();ctx.ellipse(-2,-e.h/2-2,e.w/4,e.h/4,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.fillRect(1,-e.h/2-2,3,3);ctx.fillStyle='#111';ctx.fillRect(2,-e.h/2-1,2,2);
if(e.type==='shadow'){ctx.fillStyle='rgba(168,85,247,0.3)';ctx.beginPath();ctx.ellipse(0,-e.h/2,e.w/2+3,e.h/2+3,0,0,Math.PI*2);ctx.fill()}
ctx.restore();
if(e.hp<e.mhp){const bw=16,bx=ex2-bw/2,by=ey2-e.h-6;ctx.fillStyle='#333';ctx.fillRect(bx,by,bw,3);ctx.fillStyle='#ef4444';ctx.fillRect(bx,by,bw*(e.hp/e.mhp),3)}
}
drawPlayer(pl.x-cam.x,pl.y-cam.y);
// Particles
for(const p of particles){const px2=p.x-cam.x,py2=p.y-cam.y;ctx.globalAlpha=p.life/p.ml;ctx.fillStyle=p.col;ctx.fillRect(px2-p.sz/2,py2-p.sz/2,p.sz,p.sz)}
ctx.globalAlpha=1;
// LIGHTING - dark canvas approach
const dl=getDayLight();
darkCtx2.fillStyle='rgba(0,0,0,0.93)';darkCtx2.fillRect(0,0,C.width,C.height);
darkCtx2.globalCompositeOperation='destination-out';
// Sky light per column
darkCtx2.fillStyle=`rgba(0,0,0,${dl})`;
for(let tx=Math.max(0,sx);tx<=Math.min(WW-1,ex);tx++){
let startTy=-1;
for(let ty=Math.max(0,sy);ty<=Math.min(WH-1,ey);ty++){
if(skyExp[ty][tx]){if(startTy===-1)startTy=ty}
else{if(startTy!==-1){const dx=tx*T-cam.x,dy=startTy*T-cam.y,h=(ty-startTy)*T;darkCtx2.fillRect(dx,dy,T,h);startTy=-1}}
}
if(startTy!==-1){const dx=tx*T-cam.x,dy=startTy*T-cam.y,h=(Math.min(WH-1,ey)-startTy+1)*T;darkCtx2.fillRect(dx,dy,T,h)}
}
// Torch light
const tr=T*8;
for(const t of torches){
const dx=(t.x+0.5)*T-cam.x,dy=(t.y+0.5)*T-cam.y;
darkCtx2.drawImage(torchLCvs,dx-tr,dy-tr);
}
darkCtx2.globalCompositeOperation='source-over';
ctx.drawImage(darkCvs,0,0);
// Cursor item in inventory
if(invOpen&&cursor){
const isT=cursor.id>=100;
if(isT)ctx.drawImage(tIc[cursor.id],ms.x-14,ms.y-14);
else if(tSpr[cursor.id])ctx.drawImage(tSpr[cursor.id],ms.x-12,ms.y-12,24,24);
if(cursor.c>1){ctx.fillStyle='#fff';ctx.font='8px "Press Start 2P"';ctx.fillText(cursor.c,ms.x+8,ms.y+12)}
}
ctx.restore();
drawHUD();
if(invOpen)drawInvUI();
if(titleScr)drawTitle();
}
/* ===== HUD ===== */
function drawHUD(){
const slotS=40,gap=2,hbW=10*(slotS+gap)-gap;
const hbX=(C.width-hbW)/2,hbY=C.height-slotS-10;
ctx.fillStyle='rgba(0,0,0,0.6)';rr(ctx,hbX-4,hbY-4,hbW+8,slotS+8,6);ctx.fill();
ctx.strokeStyle='rgba(255,255,255,0.15)';ctx.lineWidth=1;rr(ctx,hbX-4,hbY-4,hbW+8,slotS+8,6);ctx.stroke();
for(let i=0;i<10;i++){
const sx2=hbX+i*(slotS+gap),sy2=hbY;
ctx.fillStyle=i===hSlot?'rgba(255,255,255,0.15)':'rgba(30,30,30,0.7)';ctx.fillRect(sx2,sy2,slotS,slotS);
ctx.strokeStyle=i===hSlot?'rgba(255,200,50,0.7)':'rgba(255,255,255,0.1)';ctx.strokeRect(sx2,sy2,slotS,slotS);
const item=inv[i];if(item){
if(item.id>=100&&tIc[item.id])ctx.drawImage(tIc[item.id],sx2+6,sy2+6,28,28);
else if(tSpr[item.id])ctx.drawImage(tSpr[item.id],sx2+8,sy2+8,24,24);
if(item.c>1){ctx.fillStyle='#fff';ctx.font='7px "Press Start 2P"';ctx.fillText(item.c,sx2+2,sy2+slotS-4)}
}
ctx.fillStyle='rgba(255,255,255,0.3)';ctx.font='6px "Press Start 2P"';ctx.fillText(i===9?'0':(i+1)+'',sx2+2,sy2+10);
}
// Tooltip for hovered hotbar slot
if(!invOpen){
for(let i=0;i<10;i++){
const sx2=hbX+i*(slotS+gap),sy2=hbY;
if(ms.x>=sx2&&ms.x<sx2+slotS&&ms.y>=sy2&&ms.y<sy2+slotS&&inv[i]){
const nm=itemName(inv[i].id);
ctx.fillStyle='rgba(0,0,0,0.8)';const tw=ctx.measureText(nm).width+12;ctx.fillRect(ms.x+10,ms.y-20,tw,18);
ctx.fillStyle='#fbbf24';ctx.font='7px "Press Start 2P"';ctx.fillText(nm,ms.x+16,ms.y-8);
break;
}
}
}
// HP bar
const hpW=150,hpH=14,hpX=10,hpY=10;
ctx.fillStyle='rgba(0,0,0,0.5)';rr(ctx,hpX-2,hpY-2,hpW+4,hpH+4,4);ctx.fill();
ctx.fillStyle='#1c1917';ctx.fillRect(hpX,hpY,hpW,hpH);
const hpP=Math.max(0,pl.hp/pl.maxHp);
ctx.fillStyle=hpP>0.5?'#22c55e':hpP>0.25?'#eab308':'#ef4444';
rr(ctx,hpX,hpY,hpW*hpP,hpH,3);ctx.fill();
ctx.fillStyle='#fff';ctx.font='7px "Press Start 2P"';ctx.fillText(Math.ceil(pl.hp)+'/'+pl.maxHp,hpX+4,hpY+10);
// Hunger bar
const huW=120,huH=10,huX=10,huY=hpY+hpH+6;
ctx.fillStyle='rgba(0,0,0,0.5)';rr(ctx,huX-2,huY-2,huW+4,huH+4,4);ctx.fill();
ctx.fillStyle='#1c1917';ctx.fillRect(huX,huY,huW,huH);
const huP=pl.hunger/100;
ctx.fillStyle=huP>0.5?'#d97706':huP>0.25?'#b45309':'#92400e';
rr(ctx,huX,huY,huW*huP,huH,3);ctx.fill();
ctx.fillStyle='rgba(255,255,255,0.7)';ctx.font='6px "Press Start 2P"';ctx.fillText('HUNGER',huX+2,huY+8);
// Day/night
const dl=getDayLight();
ctx.fillStyle=dl>0.7?'#fbbf24':'#94a3b8';ctx.font='7px "Press Start 2P"';
ctx.fillText(dl>0.7?'DAY':'NIGHT',C.width-55,18);
// Depth
const depth=Math.max(0,Math.floor(pl.y/T)-surfY[Math.min(Math.floor(pl.x/T),WW-1)]);
if(depth>0){ctx.fillStyle='rgba(255,255,255,0.5)';ctx.fillText('Depth: '+depth,C.width-100,36)}
// Difficulty label
ctx.fillStyle=['#4ade80','#fbbf24','#ef4444'][DIFF];ctx.font='6px "Press Start 2P"';
ctx.fillText(['Peaceful','Normal','Hard'][DIFF],C.width-70,C.height-slotS-20);
// Near bench
if(nearBench()){ctx.fillStyle='#fbbf24';ctx.font='7px "Press Start 2P"';ctx.textAlign='center';ctx.fillText('Near Workbench',C.width/2,20);ctx.textAlign='left'}
// Controls
ctx.fillStyle='rgba(255,255,255,0.2)';ctx.font='6px "Press Start 2P"';
ctx.fillText('WASD Move | LMB Mine/Attack/Eat | RMB Place | E Inventory | Scroll Hotbar',10,C.height-slotS-20);
}
/* ===== INVENTORY UI ===== */
function drawInvUI(){
const slotS=40,gap=3,cols=10,rows=3;
const panW=cols*(slotS+gap)-gap,panH=rows*(slotS+gap)-gap+30;
const panX=(C.width-panW)/2,panY=(C.height-panH)/2-40;
ctx.fillStyle='rgba(10,10,10,0.9)';rr(ctx,panX-8,panY-28,panW+16,panH+36,8);ctx.fill();
ctx.strokeStyle='rgba(255,200,50,0.3)';ctx.lineWidth=1;rr(ctx,panX-8,panY-28,panW+16,panH+36,8);ctx.stroke();
ctx.fillStyle='#fbbf24';ctx.font='9px "Press Start 2P"';ctx.fillText('Inventory',panX,panY-12);
for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){
const i=r*cols+c,sx2=panX+c*(slotS+gap),sy2=panY+r*(slotS+gap)+18;
ctx.fillStyle=i===hSlot&&r===0?'rgba(255,200,50,0.15)':'rgba(30,30,30,0.8)';ctx.fillRect(sx2,sy2,slotS,slotS);
ctx.strokeStyle=i===hSlot&&r===0?'rgba(255,200,50,0.5)':'rgba(255,255,255,0.08)';ctx.strokeRect(sx2,sy2,slotS,slotS);
const item=inv[i];if(item){
if(item.id>=100&&tIc[item.id])ctx.drawImage(tIc[item.id],sx2+6,sy2+6,28,28);
else if(tSpr[item.id])ctx.drawImage(tSpr[item.id],sx2+8,sy2+8,24,24);
if(item.c>1){ctx.fillStyle='#fff';ctx.font='7px "Press Start 2P"';ctx.fillText(item.c,sx2+2,sy2+slotS-4)}
}
}
// Crafting
const cpY=panY+panH+12,cpX=panX,cpW=panW;
const crH=recipes.length*34+34;
ctx.fillStyle='rgba(10,10,10,0.9)';rr(ctx,cpX-8,cpY-24,cpW+16,crH,8);ctx.fill();
ctx.strokeStyle='rgba(100,200,150,0.3)';rr(ctx,cpX-8,cpY-24,cpW+16,crH,8);ctx.stroke();
ctx.fillStyle='#4ade80';ctx.font='9px "Press Start 2P"';ctx.fillText('Crafting',cpX,cpY-8);
const bn=nearBench();
for(let ri=0;ri<recipes.length;ri++){
const r=recipes[ri],ry=cpY+ri*34;const cc=canCraft(r);
ctx.fillStyle=cc?'rgba(34,197,94,0.12)':'rgba(50,50,50,0.5)';ctx.fillRect(cpX,ry,cpW-4,30);
ctx.strokeStyle=cc?'rgba(34,197,94,0.4)':'rgba(255,255,255,0.05)';ctx.strokeRect(cpX,ry,cpW-4,30);
const rIsT=r.r>=100;
if(rIsT&&tIc[r.r])ctx.drawImage(tIc[r.r],cpX+4,ry+3,24,24);
else if(tSpr[r.r])ctx.drawImage(tSpr[r.r],cpX+4,ry+3,24,24);
ctx.fillStyle=cc?'#4ade80':'#555';ctx.font='7px "Press Start 2P"';ctx.fillText(r.n+(r.c>1?' x'+r.c:''),cpX+32,ry+13);
ctx.font='6px "Press Start 2P"';let ix=cpX+32;
for(const it of r.i){ctx.fillStyle=cntItem(it.id)>=it.c?'rgba(255,255,255,0.5)':'#ef4444';ctx.fillText(itemName(it.id)+':'+cntItem(it.id)+'/'+it.c,ix,ry+25);ix+=ctx.measureText(itemName(it.id)+':'+cntItem(it.id)+'/'+it.c).width+10}
if(r.b){ctx.fillStyle=bn?'#4ade80':'#ef4444';ctx.font='6px "Press Start 2P"';ctx.fillText(bn?'[Bench OK]':'[Need Bench]',cpX+cpW-90,ry+13)}
}
}
/* ===== TITLE SCREEN ===== */
function drawTitle(){
ctx.fillStyle='rgba(5,8,12,0.92)';ctx.fillRect(0,0,C.width,C.height);
// Background particles
if(titleParts.length<50)titleParts.push({x:Math.random()*C.width,y:C.height+10,vy:-0.3-Math.random()*0.7,sz:1+Math.random()*2,life:300+Math.random()*200});
for(let i=titleParts.length-1;i>=0;i--){const p=titleParts[i];p.y+=p.vy;p.life--;if(p.life<=0||p.y<-10){titleParts.splice(i,1);continue}const a=Math.min(1,p.life/50)*0.4;ctx.fillStyle=`rgba(34,211,238,${a})`;ctx.fillRect(p.x,p.y,p.sz,p.sz)}
// Title
const pulse=1+Math.sin(Date.now()*0.003)*0.02;
ctx.save();ctx.translate(C.width/2,C.height*0.15);ctx.scale(pulse,pulse);
ctx.fillStyle='#22d3ee';ctx.font='32px "Press Start 2P"';ctx.textAlign='center';ctx.fillText('CRYSTAL',0,-22);
ctx.fillStyle='#ef4444';ctx.fillText('DEPTHS',0,22);ctx.restore();ctx.textAlign='left';
ctx.fillStyle='rgba(255,255,255,0.4)';ctx.font='8px "Press Start 2P"';ctx.textAlign='center';
ctx.fillText('A Sandbox Survival Adventure',C.width/2,C.height*0.15+55);ctx.textAlign='left';
// Mushroom character
ctx.save();ctx.translate(C.width/2,C.height*0.33);ctx.scale(3.5,3.5);
ctx.fillStyle='#c2410c';ctx.fillRect(-4,-4,3,4);ctx.fillRect(1,-4,3,4);
ctx.fillStyle='#ef4444';ctx.beginPath();ctx.ellipse(0,-10,7,6,0,Math.PI,0);ctx.fill();
ctx.fillStyle='#dc2626';ctx.fillRect(-5,-10,10,4);
ctx.fillStyle='#fbbf24';ctx.beginPath();ctx.arc(-3,-14,1.8,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(2,-12,1.5,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#fef3c7';ctx.fillRect(-5,-10,10,5);
ctx.fillStyle='#1e293b';ctx.fillRect(-3,-9,2,2);ctx.fillRect(2,-9,2,2);
ctx.fillStyle='#fff';ctx.fillRect(-3,-9,1,1);ctx.fillRect(2,-9,1,1);
ctx.fillStyle='#92400e';ctx.fillRect(-1,-6,2,1);
ctx.restore();
// Parameter selection
const baseY=C.height*0.48;
ctx.fillStyle='rgba(255,255,255,0.6)';ctx.font='8px "Press Start 2P"';ctx.textAlign='center';
ctx.fillText('WORLD SIZE',C.width/2,baseY);
const sizes=['Small','Medium','Large'],sizeVals=[150,250,350];
for(let i=0;i<3;i++){
const bx=C.width/2-150+i*110,by=baseY+8,bw=100,bh=28;
const hov=ms.x>=bx&&ms.x<bx+bw&&ms.y>=by&&ms.y<by+bh;
const sel=startSel.sz===i;
ctx.fillStyle=sel?'rgba(34,211,238,0.3)':hov?'rgba(255,255,255,0.1)':'rgba(30,30,30,0.7)';
ctx.fillRect(bx,by,bw,bh);ctx.strokeStyle=sel?'#22d3ee':'rgba(255,255,255,0.2)';ctx.strokeRect(bx,by,bw,bh);
ctx.fillStyle=sel?'#22d3ee':'#ccc';ctx.font='7px "Press Start 2P"';ctx.fillText(sizes[i]+' ('+sizeVals]+'x'+Math.floor(sizeVals[i]*0.48)+')',bx+bw/2,by+18);
}
const dfY=baseY+55;
ctx.fillStyle='rgba(255,255,255,0.6)';ctx.font='8px "Press Start 2P"';ctx.fillText('DIFFICULTY',C.width/2,dfY);
const diffs=['Peaceful','Normal','Hard'],dfCols=['#4ade80','#fbbf24','#ef4444'];
for(let i=0;i<3;i++){
const bx=C.width/2-150+i*110,by=dfY+8,bw=100,bh=28;
const hov=ms.x>=bx&&ms.x<bx+bw&&ms.y>=by&&ms.y<by+bh;
const sel=startSel.df===i;
ctx.fillStyle=sel?dfCols[i]+'44':hov?'rgba(255,255,255,0.1)':'rgba(30,30,30,0.7)';
ctx.fillRect(bx,by,bw,bh);ctx.strokeStyle=sel?dfCols[i]:'rgba(255,255,255,0.2)';ctx.strokeRect(bx,by,bw,bh);
ctx.fillStyle=sel?dfCols[i]:'#ccc';ctx.font='7px "Press Start 2P"';ctx.fillText(diffs[i],bx+bw/2,by+18);
}
const sdY=dfY+55;
ctx.fillStyle='rgba(255,255,255,0.6)';ctx.font='8px "Press Start 2P"';ctx.fillText('SEED',C.width/2,sdY);
const rbx=C.width/2-30,rby=sdY+6,rbw=60,rbh=24;
const rHov=ms.x>=rbx&&ms.x<rbx+rbw&&ms.y>=rby&&ms.y<rby+rbh;
ctx.fillStyle=rHov?'rgba(255,255,255,0.15)':'rgba(30,30,30,0.7)';ctx.fillRect(rbx,rby,rbw,rbh);
ctx.strokeStyle='rgba(255,255,255,0.2)';ctx.strokeRect(rbx,rby,rbw,rbh);
ctx.fillStyle='#ccc';ctx.font='6px "Press Start 2P"';ctx.fillText('Random',C.width/2,rby+16);
ctx.fillStyle='#fbbf24';ctx.font='10px "Press Start 2P"';ctx.fillText(startSel.seed,C.width/2,sdY+48);
// Start button
const stx=C.width/2-100,sty=sdY+65,stw=200,sth=40;
const stHov=ms.x>=stx&&ms.x<stx+stw&&ms.y>=sty&&ms.y<sty+sth;
ctx.fillStyle=stHov?'rgba(239,68,68,0.4)':'rgba(239,68,68,0.2)';ctx.fillRect(stx,sty,stw,sth);
ctx.strokeStyle='#ef4444';ctx.lineWidth=2;ctx.strokeRect(stx,sty,stw,sth);ctx.lineWidth=1;
ctx.fillStyle='#fff';ctx.font='11px "Press Start 2P"';ctx.fillText('START GAME',C.width/2,sty+26);
ctx.textAlign='left';
// Controls
ctx.fillStyle='rgba(255,255,255,0.25)';ctx.font='6px "Press Start 2P"';ctx.textAlign='center';
ctx.fillText('WASD: Move & Jump | Mouse: Mine & Place | E: Inventory | Scroll: Hotbar',C.width/2,C.height-30);
ctx.fillText('Explore caves, find dungeons, mine ores, craft gear, survive!',C.width/2,C.height-16);
ctx.textAlign='left';
}
/* ===== INPUT ===== */
addEventListener('keydown',e=>{
keys[e.code]=true;
if(e.code==='KeyE'&&!titleScr){
invOpen=!invOpen;
if(!invOpen){if(curFrom>=0&&cursor){inv[curFrom]=cursor;cursor=null;curFrom=-1}}
cursor=null;curFrom=-1;
}
if(e.code>='Digit1'&&e.code<='Digit9')hSlot=parseInt(e.code[5])-1;
if(e.code==='Digit0')hSlot=9;
if(e.code==='Escape'&&invOpen){invOpen=false;if(curFrom>=0&&cursor){inv[curFrom]=cursor;cursor=null;curFrom=-1}cursor=null;curFrom=-1}
e.preventDefault();
});
addEventListener('keyup',e=>{keys[e.code]=false});
addEventListener('mousemove',e=>{ms.x=e.clientX;ms.y=e.clientY});
addEventListener('mousedown',e=>{
ms.x=e.clientX;ms.y=e.clientY;
if(titleScr){
// Check button clicks
const baseY=C.height*0.48;
// Size buttons
for(let i=0;i<3;i++){const bx=C.width/2-150+i*110,by=baseY+8;if(ms.x>=bx&&ms.x<bx+100&&ms.y>=by&&ms.y<by+28)startSel.sz=i}
// Difficulty buttons
const dfY=baseY+55;
for(let i=0;i<3;i++){const bx=C.width/2-150+i*110,by=dfY+8;if(ms.x>=bx&&ms.x<bx+100&&ms.y>=by&&ms.y<by+28)startSel.df=i}
// Random seed
const sdY=dfY+55;const rbx=C.width/2-30,rby=sdY+6;
if(ms.x>=rbx&&ms.x<rbx+60&&ms.y>=rby&&ms.y<rby+24)startSel.seed=Math.floor(Math.random()*99999);
// Start button
const sty=sdY+65;if(ms.x>=C.width/2-100&&ms.x<C.width/2+100&&ms.y>=sty&&ms.y<sty+40)startGame();
return;
}
if(e.button===0)ms.l=true;if(e.button===2)ms.r=true;
// Inventory click
if(invOpen&&e.button===0){
const slotS=40,gap=3,cols=10,rows=3;
const panW=cols*(slotS+gap)-gap,panH=rows*(slotS+gap)-gap+30;
const panX=(C.width-panW)/2,panY=(C.height-panH)/2-40;
for(let r=0;r<rows;r++)for(let c=0;c<cols;c++){
const i=r*cols+c,sx2=panX+c*(slotS+gap),sy2=panY+r*(slotS+gap)+18;
if(ms.x>=sx2&&ms.x<sx2+slotS&&ms.y>=sy2&&ms.y<sy2+slotS){
if(cursor){if(!inv[i]){inv[i]=cursor;cursor=null;curFrom=-1}else if(inv[i].id===cursor.id){inv[i].c+=cursor.c;cursor=null;curFrom=-1}else{const tmp=inv[i];inv[i]=cursor;cursor=tmp;curFrom=i}}
else if(inv[i]){cursor=inv[i];inv[i]=null;curFrom=i}
}
}
// Crafting click
const cpY=panY+panH+12;
for(let ri=0;ri<recipes.length;ri++){
const ry=cpY+ri*34;
if(ms.x>=panX&&ms.x<panX+panW-4&&ms.y>=ry&&ms.y<ry+30){doCraft(recipes[ri]);break}
}
}
});
addEventListener('mouseup',e=>{if(e.button===0)ms.l=false;if(e.button===2)ms.r=false});
addEventListener('wheel',e=>{hSlot=(hSlot+(e.deltaY>0?1:-1)+10)%10;e.preventDefault()},{passive:false});
addEventListener('contextmenu',e=>e.preventDefault());
/* ===== START GAME ===== */
function startGame(){
const sizeVals=[150,250,350];
WW=sizeVals[startSel.sz];WH=Math.floor(WW*0.48);
SEED=startSel.seed;wSeed=SEED;DIFF=startSel.df;
const dm=[0.8,1,1.2][DIFF];
GRAV=0.42;JUMP_V=-7.5;SPD=2.8;REACH=52
2
45KB
46KB
156.0ms
112.0ms
156.0ms