Mini Minecraft 2D
by ShadowLegend17765 lines41.1 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Mini Minecraft 2D</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:monospace;user-select:none;}
#cvs{position:absolute;inset:0;width:100%;height:100%;cursor:crosshair;touch-action:none;}
#ui{position:absolute;inset:0;pointer-events:none;}
.btn{pointer-events:all;position:absolute;display:flex;align-items:center;justify-content:center;
background:rgba(15,15,15,0.88);color:#fff;border:2px solid rgba(255,255,255,0.2);
border-radius:14px;font-weight:bold;font-size:20px;cursor:pointer;touch-action:none;user-select:none;}
#inv-overlay{display:none;position:absolute;inset:0;background:rgba(0,0,0,0.82);
align-items:center;justify-content:center;z-index:20;pointer-events:all;backdrop-filter:blur(4px);}
#inv-overlay.open{display:flex;}
#inv-box{background:linear-gradient(150deg,#232333,#181828);border:2px solid rgba(255,255,255,0.12);
border-radius:18px;padding:16px;display:flex;flex-direction:column;gap:10px;
box-shadow:0 8px 40px rgba(0,0,0,0.9);max-width:96vw;max-height:90vh;overflow-y:auto;width:min(580px,96vw);}
.inv-tab{flex:1;text-align:center;padding:8px 0;font-weight:bold;font-size:13px;
background:transparent;color:rgba(255,255,255,0.38);border-radius:8px;cursor:pointer;border:none;}
.inv-tab.active{background:rgba(255,255,255,0.1);color:#fff;}
.slot{width:52px;height:52px;border:2px solid rgba(255,255,255,0.14);border-radius:8px;
cursor:pointer;position:relative;display:flex;align-items:flex-end;justify-content:flex-end;
overflow:hidden;flex-shrink:0;transition:border-color .1s;}
.slot.selected{border-color:#ffe066;box-shadow:0 0 14px rgba(255,224,102,.65);}
.slot-num{position:absolute;top:2px;left:4px;font-size:8px;color:rgba(255,255,255,.38);}
.slot-count{font-size:9px;color:#fff;font-weight:bold;text-shadow:1px 1px 2px #000;padding:1px 3px;}
.recipe-row{background:rgba(20,20,30,.45);border:1px solid rgba(255,255,255,.07);border-radius:10px;
padding:8px;display:flex;align-items:center;gap:8px;}
.recipe-row.can{background:rgba(30,60,30,.5);border-color:rgba(70,160,70,.25);}
.craft-btn{padding:7px 10px;border-radius:8px;font-size:11px;font-weight:bold;cursor:pointer;
background:rgba(40,40,45,.7);color:rgba(255,255,255,.25);border:1.5px solid rgba(255,255,255,.06);
white-space:nowrap;font-family:monospace;}
.craft-btn.can{background:rgba(45,145,45,.9);color:#fff;border-color:rgba(80,200,80,.35);}
#dead{display:none;position:absolute;inset:0;background:rgba(80,0,0,.85);flex-direction:column;
align-items:center;justify-content:center;z-index:50;pointer-events:all;backdrop-filter:blur(4px);}
#dead.show{display:flex;}
#respawn-btn{padding:14px 36px;background:rgba(50,180,50,.9);color:#fff;font-weight:bold;
font-size:18px;border-radius:12px;cursor:pointer;border:2px solid rgba(80,220,80,.5);pointer-events:all;}
</style>
</head>
<body>
<canvas id="cvs"></canvas>
<div id="ui">
<!-- Touch D-Pad left -->
<div id="btn-l" class="btn" style="bottom:14px;left:14px;width:70px;height:70px;">โ</div>
<div id="btn-r" class="btn" style="bottom:14px;left:94px;width:70px;height:70px;">โถ</div>
<!-- Right buttons -->
<div id="btn-atk" class="btn" style="bottom:14px;right:220px;width:60px;height:70px;background:rgba(120,20,20,.9);border-color:rgba(255,80,80,.4);">โ๏ธ</div>
<div id="btn-mode" class="btn" style="bottom:14px;right:112px;width:100px;height:70px;background:rgba(160,48,18,.9);">โ Mine</div>
<div id="btn-jump" class="btn" style="bottom:14px;right:8px;width:98px;height:70px;">โ Jump</div>
<!-- Bag -->
<div id="btn-bag" class="btn" style="top:10px;right:10px;height:38px;padding:0 14px;border-radius:10px;font-size:12px;letter-spacing:.5px;pointer-events:all;">๐ Bag [E]</div>
</div>
<!-- Inventory Overlay -->
<div id="inv-overlay">
<div id="inv-box">
<div style="display:flex;align-items:center;gap:8px;">
<span style="color:#fff;font-weight:bold;font-size:15px;flex:1;">๐ Inventory & Crafting</span>
<span id="inv-tip" style="color:#ffe066;font-size:11px;background:rgba(255,224,102,.1);padding:2px 8px;border-radius:5px;display:none;"></span>
<span id="inv-close" style="color:rgba(255,255,255,.45);font-size:20px;cursor:pointer;padding:0 4px;pointer-events:all;">โ</span>
</div>
<div style="display:flex;gap:3px;background:rgba(0,0,0,.3);padding:4px;border-radius:10px;">
<button class="inv-tab active" id="tab-items" onclick="switchTab('items')">๐ Items</button>
<button class="inv-tab" id="tab-craft" onclick="switchTab('craft')">โ๏ธ Craft</button>
</div>
<div id="tab-items-content">
<p style="color:rgba(255,255,255,.28);font-size:9px;text-align:center;margin-bottom:6px;">Tap to select ยท tap another to move/swap</p>
<div style="color:rgba(255,255,255,.4);font-size:10px;margin-bottom:4px;">BACKPACK</div>
<div id="inv-grid" style="display:grid;grid-template-columns:repeat(8,1fr);gap:3px;"></div>
<div style="height:1px;background:rgba(255,255,255,.07);margin:8px 0;"></div>
<div style="color:rgba(255,255,255,.4);font-size:10px;margin-bottom:4px;">HOTBAR</div>
<div id="hot-grid" style="display:flex;gap:3px;flex-wrap:wrap;"></div>
</div>
<div id="tab-craft-content" style="display:none;">
<div id="craft-msg" style="display:none;background:rgba(50,160,50,.18);border:1px solid rgba(70,180,70,.3);color:#8ddd50;font-size:12px;padding:6px 10px;border-radius:8px;text-align:center;margin-bottom:6px;"></div>
<div id="recipe-list" style="display:flex;flex-direction:column;gap:5px;"></div>
</div>
</div>
</div>
<!-- Death screen -->
<div id="dead">
<div style="font-size:52px;margin-bottom:12px;">๐</div>
<div style="color:#ff4444;font-weight:bold;font-size:28px;margin-bottom:8px;text-shadow:0 0 20px #f00;">YOU DIED</div>
<div style="color:rgba(255,255,255,.5);font-size:14px;margin-bottom:28px;">The zombies got you!</div>
<div id="respawn-btn">โฉ Respawn</div>
</div>
<script>
// โโ Constants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const BS=40,WW=130,WH=64,PW=20,PH=38;
const GRAVITY=.45,MAX_FALL=16,MOVE_SPD=4.2,JUMP_VEL=-11;
const DAY_LEN=1800,NIGHT_LEN=1200,CYCLE=DAY_LEN+NIGHT_LEN;
const MAX_HP=10,MAX_ZOMBIES=10,ZOMBIE_SPD=1.8,ZOMBIE_HP=6,ZW=18,ZH=36;
const B={AIR:0,GRASS:1,DIRT:2,STONE:3,WOOD:4,LEAVES:5,SAND:6,COAL:7,BEDROCK:8,PLANK:9,GLASS:10,IRON:11,TORCH:12,DOOR:13,STICK:14,WOOD_PICK:15,STONE_PICK:16,IRON_PICK:17,WOOD_AXE:18,STONE_AXE:19,IRON_AXE:20,IRON_SWORD:21};
const DATA={
[B.GRASS]:{col:"#5c9e30",top:"#6ecf35",name:"Grass",mt:22,drop:B.DIRT,pl:true},
[B.DIRT]:{col:"#8B6340",name:"Dirt",mt:22,drop:B.DIRT,pl:true},
[B.STONE]:{col:"#787878",name:"Stone",mt:80,drop:B.STONE,pl:true},
[B.WOOD]:{col:"#8B6510",top:"#c8a020",name:"Wood",mt:55,drop:B.WOOD,pl:true},
[B.LEAVES]:{col:"#2d7a20",name:"Leaves",mt:8,drop:B.AIR,pl:true},
[B.SAND]:{col:"#d4c47a",name:"Sand",mt:22,drop:B.SAND,pl:true},
[B.COAL]:{col:"#484848",name:"Coal",mt:90,drop:B.COAL,pl:true},
[B.BEDROCK]:{col:"#1c1c1c",name:"Bedrock",mt:9999,drop:B.AIR,pl:true},
[B.PLANK]:{col:"#c8a440",name:"Plank",mt:40,drop:B.PLANK,pl:true},
[B.GLASS]:{col:"#90d0e8",name:"Glass",mt:15,drop:B.AIR,pl:true},
[B.IRON]:{col:"#c0b8a8",name:"Iron Ore",mt:110,drop:B.IRON,pl:true},
[B.TORCH]:{col:"#ff9020",name:"Torch",mt:5,drop:B.TORCH,pl:true,pass:true},
[B.DOOR]:{col:"#a07040",name:"Door",mt:30,drop:B.DOOR,pl:true,isDoor:true},
[B.STICK]:{col:"#a07040",name:"Stick",icon:"๐ชต",pl:false},
[B.WOOD_PICK]:{col:"#c8a060",name:"Wood Pickaxe",icon:"โ",pl:false,tc:"pick",tm:2},
[B.STONE_PICK]:{col:"#909090",name:"Stone Pickaxe",icon:"โ",pl:false,tc:"pick",tm:4},
[B.IRON_PICK]:{col:"#d0e0f0",name:"Iron Pickaxe",icon:"โ",pl:false,tc:"pick",tm:8},
[B.WOOD_AXE]:{col:"#c8a060",name:"Wood Axe",icon:"๐ช",pl:false,tc:"axe",tm:2},
[B.STONE_AXE]:{col:"#909090",name:"Stone Axe",icon:"๐ช",pl:false,tc:"axe",tm:4},
[B.IRON_AXE]:{col:"#d0e0f0",name:"Iron Axe",icon:"๐ช",pl:false,tc:"axe",tm:8},
[B.IRON_SWORD]:{col:"#d0e0f0",name:"Iron Sword",icon:"โ๏ธ",pl:false,tc:"sword",tm:1,sdmg:2},
};
const PICK_SET=new Set([B.STONE,B.COAL,B.IRON,B.BEDROCK]);
const AXE_SET=new Set([B.WOOD,B.LEAVES,B.PLANK,B.DOOR]);
const RECIPES=[
{name:"Planks",res:{b:B.PLANK,q:4},ing:[{b:B.WOOD,q:1}]},
{name:"Sticks",res:{b:B.STICK,q:4},ing:[{b:B.PLANK,q:2}]},
{name:"Torches x4",res:{b:B.TORCH,q:4},ing:[{b:B.COAL,q:1},{b:B.STICK,q:1}]},
{name:"Door x2",res:{b:B.DOOR,q:2},ing:[{b:B.PLANK,q:6}]},
{name:"Wood Pickaxe",res:{b:B.WOOD_PICK,q:1},ing:[{b:B.PLANK,q:3},{b:B.STICK,q:2}]},
{name:"Stone Pickaxe",res:{b:B.STONE_PICK,q:1},ing:[{b:B.STONE,q:3},{b:B.STICK,q:2}]},
{name:"Iron Pickaxe",res:{b:B.IRON_PICK,q:1},ing:[{b:B.IRON,q:3},{b:B.STICK,q:2}]},
{name:"Wood Axe",res:{b:B.WOOD_AXE,q:1},ing:[{b:B.PLANK,q:2},{b:B.STICK,q:2}]},
{name:"Stone Axe",res:{b:B.STONE_AXE,q:1},ing:[{b:B.STONE,q:2},{b:B.STICK,q:2}]},
{name:"Iron Axe",res:{b:B.IRON_AXE,q:1},ing:[{b:B.IRON,q:2},{b:B.STICK,q:2}]},
{name:"Iron Sword",res:{b:B.IRON_SWORD,q:1},ing:[{b:B.IRON,q:2},{b:B.STICK,q:1}]},
];
// โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const empty=()=>({block:B.AIR,count:0});
function lerp(a,b,t){return a+(b-a)*t;}
function lerpC(c1,c2,t){
const p=(s,i)=>parseInt(s.slice(i,i+2),16);
return`rgb(${Math.round(lerp(p(c1,1),p(c2,1),t))},${Math.round(lerp(p(c1,3),p(c2,3),t))},${Math.round(lerp(p(c1,5),p(c2,5),t))})`;
}
function rng(x,y,s){let h=s^(x*374761393)^(y*668265263);h=Math.imul(h^(h>>>13),1274126177);return((h^(h>>>16))>>>0)/4294967296;}
function s1d(x,s){const xi=Math.floor(x),f=x-xi,t=f*f*(3-2*f);return rng(xi,0,s)*(1-t)+rng(xi+1,0,s)*t;}
function frac(x,s){let v=0,a=1,f=1,m=0;for(let i=0;i<4;i++){v+=s1d(x*f*.025,s+i*77)*a;m+=a;a*=.5;f*=2;}return v/m;}
function countItem(hot,inv,b){return[...hot,...inv].reduce((s,sl)=>s+(sl.block===b?sl.count:0),0);}
function consume(hot,inv,b,n){let r=n;for(const s of[...hot,...inv]){if(s.block===b&&r>0){const t=Math.min(s.count,r);s.count-=t;r-=t;if(!s.count)s.block=B.AIR;}}}
function addItem(hot,inv,b,n=1){let r=n;for(const s of[...hot,...inv])if(s.block===b&&s.count>0&&s.count<999&&r>0){const a=Math.min(999-s.count,r);s.count+=a;r-=a;}for(const s of[...inv,...hot])if(!s.count&&r>0){s.block=b;s.count=Math.min(r,999);r-=s.count;}}
function mineTime(bt,eq){const base=DATA[bt]?.mt??20;if(!eq?.count)return base;const d=DATA[eq.block];if(!d?.tc)return base;const ok=d.tc==="pick"?PICK_SET.has(bt):d.tc==="axe"?AXE_SET.has(bt):false;return ok?Math.max(2,Math.floor(base/d.tm)):base;}
// โโ World Gen โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function genWorld(seed){
const world=Array.from({length:WH},()=>new Uint8Array(WW)),surf=[];
for(let x=0;x<WW;x++)surf[x]=Math.round(WH*.45+frac(x,seed)*14);
for(let x=0;x<WW;x++)for(let y=0;y<WH;y++){
if(y===WH-1){world[y][x]=B.BEDROCK;continue;}
if(y>surf[x]+4){const r=rng(x,y,seed+1);world[y][x]=r<.045?B.COAL:r<.068?B.IRON:B.STONE;}
else if(y>surf[x])world[y][x]=B.DIRT;
else if(y===surf[x])world[y][x]=surf[x]>Math.round(WH*.52)?B.SAND:B.GRASS;
}
for(let x=2;x<WW-2;x++){
if(world[surf[x]][x]===B.GRASS&&rng(x,9999,seed)<.1){
const h=4+Math.floor(rng(x,9998,seed)*3);
for(let ty=surf[x]-h;ty<surf[x];ty++)if(ty>=0)world[ty][x]=B.WOOD;
for(let ly=surf[x]-h-2;ly<=surf[x]-h+1;ly++)for(let lx=x-2;lx<=x+2;lx++){
if(ly<0||lx<0||lx>=WW)continue;
if(Math.abs(ly-(surf[x]-h-1))+Math.abs(lx-x)<=3&&!world[ly][lx])world[ly][lx]=B.LEAVES;
}
}
}
return{world,surf};
}
// โโ Sky โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function skyColors(t){
const night=t>DAY_LEN/CYCLE;
const dt=night?0:t/(DAY_LEN/CYCLE);
const nt=night?(t-DAY_LEN/CYCLE)/(NIGHT_LEN/CYCLE):0;
if(!night){
if(dt<.1){const f=dt/.1;return{top:lerpC("#1a1040","#3390cc",f),bot:lerpC("#884422","#87ceeb",f),dark:f<.5};}
if(dt>.9){const f=(dt-.9)/.1;return{top:lerpC("#3390cc","#1a1040",f),bot:lerpC("#87ceeb","#884422",f),dark:f>.5};}
return{top:"#3390cc",bot:"#87ceeb",dark:false};
}
if(nt<.05){const f=nt/.05;return{top:lerpC("#1a1040","#080818",f),bot:lerpC("#884422","#0a0828",f),dark:true};}
if(nt>.9){const f=(nt-.9)/.1;return{top:lerpC("#080818","#1a1040",f),bot:lerpC("#0a0828","#884422",f),dark:true};}
return{top:"#080818",bot:"#0a0828",dark:true};
}
// โโ Canvas Draw โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawBlock(ctx,sx,sy,type,mine,hover,open){
const d=DATA[type];if(!d)return;
if(type===B.TORCH){
ctx.fillStyle="#7a5020";ctx.fillRect(sx+BS/2-2,sy+BS*.4,4,BS*.6);
ctx.fillStyle="#ff5500";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS*.3,7,0,Math.PI*2);ctx.fill();
ctx.fillStyle="#ffcc00";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS*.22,4,0,Math.PI*2);ctx.fill();
if(hover){ctx.strokeStyle="rgba(255,255,255,.45)";ctx.lineWidth=1.5;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
return;
}
if(type===B.DOOR){
if(open){ctx.fillStyle="#8B6340";ctx.fillRect(sx+1,sy,5,BS);}
else{
ctx.fillStyle=d.col;ctx.fillRect(sx+2,sy+1,BS-4,BS-2);
ctx.fillStyle="rgba(0,0,0,.15)";ctx.fillRect(sx+5,sy+4,BS-10,Math.floor(BS*.43));
ctx.fillRect(sx+5,sy+Math.floor(BS*.52),BS-10,Math.floor(BS*.43));
ctx.strokeStyle="#7a5020";ctx.lineWidth=1.5;ctx.strokeRect(sx+2,sy+1,BS-4,BS-2);
ctx.fillStyle="#f0c030";ctx.fillRect(sx+Math.floor(BS*.7),sy+Math.floor(BS*.42),5,10);
}
if(hover){ctx.strokeStyle="rgba(255,255,255,.5)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
return;
}
ctx.fillStyle=d.col;ctx.fillRect(sx,sy,BS,BS);
if(d.top){ctx.fillStyle=d.top;ctx.fillRect(sx,sy,BS,6);}
ctx.fillStyle="rgba(255,255,255,.11)";ctx.fillRect(sx,sy,BS,2);ctx.fillRect(sx,sy,2,BS);
ctx.fillStyle="rgba(0,0,0,.18)";ctx.fillRect(sx,sy+BS-3,BS,3);ctx.fillRect(sx+BS-3,sy,3,BS);
if(type===B.COAL||type===B.IRON){ctx.fillStyle=type===B.COAL?"#1a1a1a":"#e8e0d0";[[8,10,6,6],[22,20,5,5],[13,28,7,7]].forEach(([dx,dy,w,h])=>ctx.fillRect(sx+dx,sy+dy,w,h));}
if(type===B.GLASS){ctx.fillStyle="rgba(255,255,255,.28)";ctx.fillRect(sx,sy,BS,BS);}
if(type===B.WOOD){ctx.strokeStyle="#5a4010";ctx.lineWidth=1.5;ctx.beginPath();ctx.ellipse(sx+BS/2,sy+BS/2,BS*.32,BS*.1,0,0,Math.PI*2);ctx.stroke();}
ctx.strokeStyle="rgba(0,0,0,.09)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
if(mine>0){
ctx.fillStyle=`rgba(0,0,0,${mine*.6})`;ctx.fillRect(sx,sy,BS,BS);
ctx.strokeStyle=`rgba(255,255,255,${mine*.85})`;ctx.lineWidth=1.5;ctx.beginPath();
if(mine>.1){ctx.moveTo(sx+BS*.4,sy+BS*.1);ctx.lineTo(sx+BS*.5,sy+BS*.5);ctx.lineTo(sx+BS*.8,sy+BS*.3);}
if(mine>.4){ctx.moveTo(sx+BS*.1,sy+BS*.6);ctx.lineTo(sx+BS*.5,sy+BS*.5);ctx.lineTo(sx+BS*.6,sy+BS*.9);}
if(mine>.7){ctx.moveTo(sx+BS*.2,sy+BS*.2);ctx.lineTo(sx+BS*.5,sy+BS*.5);}
ctx.stroke();
}
if(hover){ctx.strokeStyle="rgba(255,255,255,.55)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
}
function drawPlayer(ctx,px,py,dir,flash){
if(flash){ctx.save();ctx.globalAlpha=.65;}
ctx.fillStyle="rgba(0,0,0,.18)";ctx.beginPath();ctx.ellipse(px,py+2,PW*.55,4,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle=flash?"#cc3333":"#2060a0";ctx.fillRect(px-PW/2+1,py-PH*.52,PW/2-2,PH*.52);
ctx.fillStyle=flash?"#aa2222":"#1a5090";ctx.fillRect(px+1,py-PH*.52,PW/2-2,PH*.52);
ctx.fillStyle=flash?"#ff5555":"#4a8fd9";ctx.fillRect(px-PW/2,py-PH,PW,PH*.48);
ctx.fillStyle="#f5c895";ctx.fillRect(px-PW/2+1,py-PH,PW-2,PH*.42);
ctx.fillStyle="#7a4515";ctx.fillRect(px-PW/2+1,py-PH,PW-2,5);
ctx.fillStyle="#333";const ex=dir>0?px+2:px-8;ctx.fillRect(ex,py-PH+8,5,5);
ctx.fillStyle="#fff";ctx.fillRect(ex+1,py-PH+9,2,2);
ctx.fillStyle=flash?"#ff4444":"#3a7fc9";ctx.fillRect(dir>0?px-PW/2-5:px+PW/2,py-PH+2,5,PH*.38);
if(flash)ctx.restore();
}
function drawZombie(ctx,zx,zy,dir,flash,hp){
if(hp<=0)return;
if(flash){ctx.save();ctx.globalAlpha=.5;}
ctx.fillStyle="rgba(0,0,0,.2)";ctx.beginPath();ctx.ellipse(zx,zy+2,ZW*.55,3,0,0,Math.PI*2);ctx.fill();
ctx.fillStyle="#2a5e2a";ctx.fillRect(zx-ZW/2+1,zy-ZH*.48,ZW/2-2,ZH*.48);
ctx.fillStyle="#1e4e1e";ctx.fillRect(zx+1,zy-ZH*.48,ZW/2-2,ZH*.48);
ctx.fillStyle="#3a8c3a";ctx.fillRect(zx-ZW/2,zy-ZH,ZW,ZH*.52);
ctx.fillStyle="#5abf5a";ctx.fillRect(zx-ZW/2+1,zy-ZH,ZW-2,ZH*.4);
ctx.fillStyle="#cc2222";const ez=dir>0?zx+1:zx-7;ctx.fillRect(ez,zy-ZH+6,4,4);
ctx.fillStyle="#ff4444";ctx.fillRect(ez+1,zy-ZH+7,2,2);
ctx.fillStyle="#3a8c3a";ctx.fillRect(dir>0?zx-ZW/2-7:zx+ZW/2,zy-ZH+4,7,ZH*.35);
if(flash)ctx.restore();
const bw=28,bx=zx-bw/2,by=zy-ZH-8;
ctx.fillStyle="rgba(0,0,0,.5)";ctx.fillRect(bx,by,bw,4);
ctx.fillStyle=`hsl(${(hp/ZOMBIE_HP)*120},90%,45%)`;ctx.fillRect(bx,by,bw*(hp/ZOMBIE_HP),4);
}
// โโ HUD draw on canvas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function drawHUD(ctx,W,H,g){
const p=g.player,isNight=g.dayTime>=DAY_LEN;
// HP hearts
for(let i=0;i<MAX_HP;i++){
ctx.font="18px serif";ctx.globalAlpha=i<p.hp?1:.2;
ctx.fillText("โค๏ธ",12+i*22,36);
}
ctx.globalAlpha=1;
// Day/Night badge
const badgeTxt=isNight?"๐ NIGHT":"โ๏ธ Day";
const badgeCol=isNight?"rgba(40,0,0,.88)":"rgba(40,30,0,.78)";
const tw=ctx.measureText(badgeTxt).width;
ctx.font="bold 13px monospace";
const bw=tw+24,bx=(W-bw)/2;
ctx.fillStyle=badgeCol;roundRect(ctx,bx,10,bw,28,14);ctx.fill();
ctx.fillStyle=isNight?"#ff6060":"#ffe066";ctx.fillText(badgeTxt,bx+12,29);
// Hotbar
const hs=46,hgap=4,hotW=g.hotbar.length*(hs+hgap)-hgap;
const hx=(W-hotW)/2,hy=H-90;
ctx.fillStyle="rgba(0,0,0,.72)";roundRect(ctx,hx-8,hy-6,hotW+16,hs+12,10);ctx.fill();
g.hotbar.forEach((sl,i)=>{
const sx=hx+i*(hs+hgap),sy=hy;
const d=DATA[sl.block];
ctx.fillStyle=sl.count>0&&d&&d.pl?d.col:"#181818";
roundRect(ctx,sx,sy,hs,hs,7);ctx.fill();
if(i===g.slot){ctx.strokeStyle="#fff";ctx.lineWidth=2.5;}
else{ctx.strokeStyle="rgba(255,255,255,.15)";ctx.lineWidth=2;}
roundRect(ctx,sx,sy,hs,hs,7);ctx.stroke();
if(sl.count>0&&d){
if(d.top&&d.pl){ctx.fillStyle=d.top;roundRect(ctx,sx,sy,hs,7,7);ctx.fill();}
if(!d.pl){ctx.font="24px serif";ctx.fillText(d.icon,sx+hs*.5-10,sy+hs*.5+8);}
ctx.font="bold 9px monospace";ctx.fillStyle="#fff";ctx.shadowColor="#000";ctx.shadowBlur=3;
ctx.fillText(sl.count,sx+hs-18,sy+hs-4);ctx.shadowBlur=0;
}
ctx.font="8px monospace";ctx.fillStyle="rgba(255,255,255,.38)";ctx.fillText(i+1,sx+4,sy+12);
});
// Selected item name
const eq=g.hotbar[g.slot];
if(eq?.count>0){
const nm=DATA[eq.block]?.name||"";
ctx.font="11px monospace";ctx.fillStyle="#fff";
const nw=ctx.measureText(nm).width;
ctx.fillStyle="rgba(0,0,0,.55)";roundRect(ctx,(W-nw-20)/2,H-100,nw+20,18,5);ctx.fill();
ctx.fillStyle="#fff";ctx.fillText(nm,(W-nw)/2,H-86);
}
}
function roundRect(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();
}
// โโ Game State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let G=null,stars=[],tmMode="mine",invOpen=false,activeTab="items",selSlot=null,raf=null;
const keys=new Set(),touch={left:false,right:false,jump:false};
function initGame(){
const seed=Date.now()%100000;
const{world,surf}=genWorld(seed);
const sx=Math.floor(WW/2);
stars=Array.from({length:80},(_,i)=>({x:rng(i,0,seed+42)*WW*BS,y:rng(i,1,seed+42)*WH*.4*BS,r:1+rng(i,2,seed+42)*2}));
G={world,surf,
spawnX:(sx+.5)*BS,spawnY:surf[sx]*BS,
player:{x:(sx+.5)*BS,y:surf[sx]*BS,vx:0,vy:0,onGround:false,dir:1,hp:MAX_HP,dmgCD:0,flash:0,atkCD:0},
camera:{x:0,y:0},
hotbar:[
{block:B.DIRT,count:20},{block:B.STONE,count:20},{block:B.WOOD,count:20},
{block:B.PLANK,count:20},{block:B.GLASS,count:20},{block:B.SAND,count:20},
{block:B.LEAVES,count:20},{block:B.COAL,count:10},
],
inventory:Array.from({length:32},(_,i)=>{
const st=[{block:B.IRON,count:12},{block:B.STONE,count:15},{block:B.DIRT,count:10},{block:B.WOOD,count:12}];
return i<4?{...st[i]}:empty();
}),
slot:0,mining:null,hover:null,mouseDown:false,openDoors:new Set(),
dayTime:0,zombies:[],zombieTimer:0,
};
document.getElementById('dead').classList.remove('show');
updateModeBtn();
buildRecipeUI();
}
// โโ Solid / Physics โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function solid(l,t,w,h){
for(let by=Math.floor(t/BS);by<=Math.floor((t+h)/BS);by++)
for(let bx=Math.floor(l/BS);bx<=Math.floor((l+w)/BS);bx++){
if(by<0||by>=WH||bx<0||bx>=WW)continue;
const blk=G.world[by][bx];if(!blk||DATA[blk]?.pass)continue;
if(blk===B.DOOR&&G.openDoors.has(`${bx},${by}`))continue;
return true;
}
return false;
}
function moveEnt(e,w,h){
const nx=e.x+e.vx;
if(!solid(nx-w/2,e.y-h+2,w,h-4))e.x=nx;
else{e.vx=0;if(e.onGround)e.vy=JUMP_VEL;}
e.x=Math.max(w/2,Math.min(WW*BS-w/2,e.x));
const ny=e.y+e.vy;e.onGround=false;
if(e.vy>0){
if(!solid(e.x-w/2+1,ny-.5,w-2,1))e.y=ny;
else{e.y=Math.floor(ny/BS)*BS;e.vy=0;e.onGround=true;}
}else if(e.vy<0){
if(!solid(e.x-w/2+1,ny-h,w-2,.5))e.y=ny;
else{e.y=(Math.floor((ny-h)/BS)+1)*BS+h;e.vy=0;}
}
}
// โโ Mine / Place โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function tryMine(bx,by){
if(bx<0||bx>=WW||by<0||by>=WH||!G.world[by][bx])return;
G.mining={bx,by,t:0,max:mineTime(G.world[by][bx],G.hotbar[G.slot])};
}
function tryPlace(bx,by){
if(bx<0||bx>=WW||by<0||by>=WH)return;
if(G.world[by][bx]){
if(G.world[by][bx]===B.DOOR){const k=`${bx},${by}`;G.openDoors.has(k)?G.openDoors.delete(k):G.openDoors.add(k);}
return;
}
const p=G.player;
if(bx>=Math.floor((p.x-PW/2)/BS)&&bx<=Math.floor((p.x+PW/2)/BS)&&by>=Math.floor((p.y-PH)/BS)&&by<=Math.floor(p.y/BS))return;
const sl=G.hotbar[G.slot];
if(!sl?.count||!DATA[sl.block]?.pl)return;
G.world[by][bx]=sl.block;sl.count--;if(!sl.count)sl.block=B.AIR;
}
function tryAttack(){
const p=G.player;if(p.atkCD>0)return;
const eq=G.hotbar[G.slot];
const dmg=(eq?.count&&DATA[eq.block]?.sdmg)||1;
const range=BS*1.8;let hit=false;
G.zombies.forEach(z=>{if(Math.abs(z.x-p.x)<range&&Math.abs(z.y-p.y)<range){z.hp-=dmg;z.flash=8;hit=true;}});
G.zombies=G.zombies.filter(z=>z.hp>0);
if(hit)p.atkCD=20;
}
// โโ Canvas coords โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const cvs=document.getElementById('cvs');
function toBlk(cx,cy){
const r=cvs.getBoundingClientRect();
return{bx:Math.floor((cx-r.left+G.camera.x)/BS),by:Math.floor((cy-r.top+G.camera.y)/BS)};
}
// โโ Mouse โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
cvs.addEventListener('mousedown',e=>{
if(invOpen)return;
G.mouseDown=true;const{bx,by}=toBlk(e.clientX,e.clientY);
e.button===0?tryMine(bx,by):tryPlace(bx,by);
});
cvs.addEventListener('mousemove',e=>{
if(invOpen||!G)return;
const{bx,by}=toBlk(e.clientX,e.clientY);G.hover={bx,by};
if(G.mouseDown&&G.mining&&(G.mining.bx!==bx||G.mining.by!==by))tryMine(bx,by);
});
cvs.addEventListener('mouseup',()=>{G.mouseDown=false;G.mining=null;});
cvs.addEventListener('contextmenu',e=>e.preventDefault());
// โโ Touch on canvas โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
cvs.addEventListener('touchstart',e=>{
if(invOpen){e.preventDefault();return;}e.preventDefault();
const t=e.changedTouches[0],{bx,by}=toBlk(t.clientX,t.clientY);
G.hover={bx,by};
if(tmMode==="mine"){tryMine(bx,by);G.mouseDown=true;}else tryPlace(bx,by);
},{passive:false});
cvs.addEventListener('touchmove',e=>{
if(invOpen){e.preventDefault();return;}e.preventDefault();
const t=e.changedTouches[0],{bx,by}=toBlk(t.clientX,t.clientY);
G.hover={bx,by};
if(tmMode==="mine"&&G.mouseDown&&G.mining&&(G.mining.bx!==bx||G.mining.by!==by))tryMine(bx,by);
},{passive:false});
cvs.addEventListener('touchend',e=>{e.preventDefault();G.mouseDown=false;G.mining=null;},{passive:false});
cvs.addEventListener('touchcancel',e=>{e.preventDefault();G.mouseDown=false;G.mining=null;},{passive:false});
// โโ Keyboard โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
window.addEventListener('keydown',e=>{
keys.add(e.key);
if(e.key>='1'&&e.key<='8'){G.slot=+e.key-1;}
if(e.key==='e'||e.key==='E')toggleInv();
if(e.key==='f'||e.key==='F')tryAttack();
if(['ArrowLeft','ArrowRight','ArrowUp',' '].includes(e.key))e.preventDefault();
});
window.addEventListener('keyup',e=>keys.delete(e.key));
// โโ Touch buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function holdBtn(el,k){
el.addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();touch[k]=true;},{passive:false});
el.addEventListener('touchend',e=>{e.stopPropagation();e.preventDefault();touch[k]=false;},{passive:false});
el.addEventListener('touchcancel',e=>{e.stopPropagation();e.preventDefault();touch[k]=false;},{passive:false});
el.addEventListener('mousedown',e=>{e.stopPropagation();touch[k]=true;});
el.addEventListener('mouseup',e=>{e.stopPropagation();touch[k]=false;});
el.addEventListener('mouseleave',e=>{touch[k]=false;});
}
holdBtn(document.getElementById('btn-l'),'left');
holdBtn(document.getElementById('btn-r'),'right');
holdBtn(document.getElementById('btn-jump'),'jump');
document.getElementById('btn-atk').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();tryAttack();},{passive:false});
document.getElementById('btn-atk').addEventListener('mousedown',e=>{e.stopPropagation();tryAttack();});
document.getElementById('btn-mode').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();toggleMode();},{passive:false});
document.getElementById('btn-mode').addEventListener('mousedown',e=>{e.stopPropagation();toggleMode();});
document.getElementById('btn-bag').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();toggleInv();},{passive:false});
document.getElementById('btn-bag').addEventListener('mousedown',e=>{e.stopPropagation();toggleInv();});
document.getElementById('inv-close').addEventListener('click',()=>toggleInv(false));
document.getElementById('inv-overlay').addEventListener('click',e=>{if(e.target===document.getElementById('inv-overlay'))toggleInv(false);});
document.getElementById('respawn-btn').addEventListener('click',respawn);
document.getElementById('respawn-btn').addEventListener('touchend',e=>{e.preventDefault();respawn();},{passive:false});
// โโ Mode toggle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function toggleMode(){
tmMode=tmMode==="mine"?"place":"mine";updateModeBtn();
}
function updateModeBtn(){
const btn=document.getElementById('btn-mode');
btn.textContent=tmMode==="mine"?"โ Mine":"๐งฑ Place";
btn.style.background=tmMode==="mine"?"rgba(160,48,18,.9)":"rgba(28,110,28,.9)";
}
// โโ Inventory โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function toggleInv(force){
invOpen=force!==undefined?force:!invOpen;
document.getElementById('inv-overlay').classList.toggle('open',invOpen);
if(invOpen)renderInv();
}
function switchTab(t){
activeTab=t;
document.getElementById('tab-items').classList.toggle('active',t==='items');
document.getElementById('tab-craft').classList.toggle('active',t==='craft');
document.getElementById('tab-items-content').style.display=t==='items'?'':'none';
document.getElementById('tab-craft-content').style.display=t==='craft'?'':'none';
if(t==='craft')renderCraft();
}
function makeSlotEl(sl,area,idx){
const d=sl.count>0?DATA[sl.block]:null;
const isTool=d&&!d.pl;
const el=document.createElement('div');
el.className='slot'+(selSlot&&selSlot.area===area&&selSlot.idx===idx?' selected':'');
el.style.background=isTool?'#12121e':(d?d.col:'rgba(0,0,0,.38)');
if(d){
if(isTool){const ic=document.createElement('span');ic.textContent=d.icon;ic.style.fontSize='22px';el.appendChild(ic);}
else if(d.top){const tp=document.createElement('div');tp.style.cssText=`position:absolute;top:0;left:0;right:0;height:7px;background:${d.top};`;el.appendChild(tp);}
const cnt=document.createElement('span');cnt.className='slot-count';cnt.textContent=sl.count;el.appendChild(cnt);
}
el.addEventListener('click',()=>onSlotClick(area,idx));
el.addEventListener('touchend',e=>{e.preventDefault();onSlotClick(area,idx);},{passive:false});
return el;
}
function renderInv(){
const ig=document.getElementById('inv-grid');ig.innerHTML='';
const hg=document.getElementById('hot-grid');hg.innerHTML='';
G.inventory.forEach((sl,i)=>ig.appendChild(makeSlotEl(sl,'inv',i)));
G.hotbar.forEach((sl,i)=>{const e=makeSlotEl(sl,'hot',i);const n=document.createElement('span');n.className='slot-num';n.textContent=i+1;e.appendChild(n);hg.appendChild(e);});
}
function onSlotClick(area,idx){
const arr=area==='inv'?G.inventory:G.hotbar;
if(!selSlot){
if(arr[idx].count>0){selSlot={area,idx};const tip=document.getElementById('inv-tip');tip.textContent=DATA[arr[idx].block]?.name||'';tip.style.display='';}
}else{
if(selSlot.area===area&&selSlot.idx===idx){selSlot=null;document.getElementById('inv-tip').style.display='none';}
else{
const src=selSlot.area==='inv'?G.inventory:G.hotbar;
const dst=area==='inv'?G.inventory:G.hotbar;
const si=selSlot.idx,di=idx;
if(src[si].block===dst[di].block&&dst[di].count>0){
const add=Math.min(999-dst[di].count,src[si].count);dst[di].count+=add;src[si].count-=add;
if(!src[si].count)src[si].block=B.AIR;
}else{const t={...src[si]};src[si]={...dst[di]};dst[di]=t;}
if(area==='hot')G.slot=di;
selSlot=null;document.getElementById('inv-tip').style.display='none';
}
}
renderInv();
}
// โโ Crafting UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function buildRecipeUI(){
const list=document.getElementById('recipe-list');list.innerHTML='';
RECIPES.forEach((rec,ri)=>{
const rd=DATA[rec.res.b];
const row=document.createElement('div');row.className='recipe-row';row.id=`rec-${ri}`;
// Icon
const ic=document.createElement('div');
ic.style.cssText=`width:38px;height:38px;flex-shrink:0;border-radius:6px;overflow:hidden;position:relative;background:${rd?.pl?rd.col:'#12121e'};border:1px solid rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;`;
if(rd?.icon){const s=document.createElement('span');s.textContent=rd.icon;s.style.fontSize='20px';ic.appendChild(s);}
row.appendChild(ic);
// Info
const info=document.createElement('div');info.style.flex='1';
const nm=document.createElement('div');nm.style.cssText='color:rgba(255,255,255,.4);font-size:11px;font-weight:bold;margin-bottom:4px;';
nm.textContent=rec.name+(rec.res.q>1?` ร${rec.res.q}`:'');info.appendChild(nm);
const ings=document.createElement('div');ings.style.cssText='display:flex;flex-wrap:wrap;gap:3px;';
rec.ing.forEach(ing=>{
const id=DATA[ing.b];const sp=document.createElement('span');
sp.style.cssText='font-size:9px;padding:1px 5px;border-radius:4px;';
sp.textContent=`${id?.icon||''}${ing.q}ร ${id?.name}`;
sp.dataset.ingB=ing.b;sp.dataset.ingQ=ing.q;ings.appendChild(sp);
});
info.appendChild(ings);row.appendChild(info);
// Craft button
const btn=document.createElement('button');btn.className='craft-btn';btn.textContent='โ';
btn.addEventListener('click',()=>doCraft(ri));btn.addEventListener('touchend',e=>{e.preventDefault();doCraft(ri);},{passive:false});
btn.id=`cbtn-${ri}`;row.appendChild(btn);
list.appendChild(row);
});
renderCraft();
}
function renderCraft(){
RECIPES.forEach((rec,ri)=>{
const can=rec.ing.every(i=>countItem(G.hotbar,G.inventory,i.b)>=i.q);
const row=document.getElementById(`rec-${ri}`);if(!row)return;
row.classList.toggle('can',can);
const nm=row.querySelector('div>div');if(nm)nm.style.color=can?'#fff':'rgba(255,255,255,.4)';
const ings=row.querySelectorAll('span[data-ing-b]');
ings.forEach(sp=>{
const b=+sp.dataset.ingB,q=+sp.dataset.ingQ;
const have=countItem(G.hotbar,G.inventory,b);
sp.style.color=have>=q?'#7ddd60':'#dd6050';
sp.style.background=have>=q?'rgba(60,200,60,.1)':'rgba(200,60,60,.1)';
sp.textContent=`${DATA[b]?.icon||''}${q}ร ${DATA[b]?.name} (${have})`;
});
const btn=document.getElementById(`cbtn-${ri}`);if(!btn)return;
btn.className='craft-btn'+(can?' can':'');btn.textContent=can?'โ Craft':'โ';
});
}
function doCraft(ri){
const rec=RECIPES[ri];
if(!rec.ing.every(i=>countItem(G.hotbar,G.inventory,i.b)>=i.q))return;
rec.ing.forEach(i=>consume(G.hotbar,G.inventory,i.b,i.q));
addItem(G.hotbar,G.inventory,rec.res.b,rec.res.q);
const msg=document.getElementById('craft-msg');
msg.textContent=`โ Crafted ${rec.res.q>1?rec.res.q+'ร ':''}${rec.name}!`;
msg.style.display='';setTimeout(()=>msg.style.display='none',2500);
renderCraft();renderInv();
}
function respawn(){
G.player.x=G.spawnX;G.player.y=G.spawnY;G.player.vx=G.player.vy=0;
G.player.hp=MAX_HP;G.player.dmgCD=G.player.flash=0;
G.zombies=[];G.dayTime=0;
document.getElementById('dead').classList.remove('show');
}
// โโ Main Loop โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function loop(ts){
const canvas=cvs,W=canvas.width,H=canvas.height;
if(!G){raf=requestAnimationFrame(loop);return;}
const ctx=canvas.getContext('2d'),p=G.player;
// Day/Night
G.dayTime=(G.dayTime+1)%CYCLE;
const cycT=G.dayTime/CYCLE,isNight=G.dayTime>=DAY_LEN;
const sc=skyColors(cycT);
// Zombie spawning
if(isNight&&G.zombies.length<MAX_ZOMBIES){
G.zombieTimer++;
if(G.zombieTimer>90){G.zombieTimer=0;
for(let a=0;a<15;a++){
const sx=Math.floor(Math.random()*WW),sy=G.surf[sx];
const wx=(sx+.5)*BS,wy=sy*BS;
if(Math.abs(wx-p.x)>20*BS){G.zombies.push({x:wx,y:wy,vx:0,vy:0,onGround:false,hp:ZOMBIE_HP,flash:0,dir:1,dmgT:0});break;}
}
}
}
if(!isNight&&G.zombies.length&&G.dayTime===DAY_LEN-5)G.zombies=[];
// Zombie AI
G.zombies.forEach(z=>{
z.vy=Math.min(z.vy+GRAVITY,MAX_FALL);
const dx=p.x-z.x;z.dir=dx>0?1:-1;z.vx=z.dir*ZOMBIE_SPD*(Math.abs(dx)>5?1:0);
moveEnt(z,ZW,ZH);if(z.flash>0)z.flash--;z.dmgT=Math.max(0,z.dmgT-1);
if(Math.abs(z.x-p.x)<ZW/2+PW/2+4&&Math.abs(z.y-p.y)<ZH/2+PH/2+4&&z.dmgT===0&&p.hp>0){
p.hp=Math.max(0,p.hp-1);p.dmgCD=20;p.flash=12;z.dmgT=90;
p.vx=(p.x>z.x?1:-1)*5;p.vy=-4;
if(p.hp===0)document.getElementById('dead').classList.add('show');
}
});
// Player
if(p.atkCD>0)p.atkCD--;if(p.flash>0)p.flash--;if(p.dmgCD>0)p.dmgCD--;
const goL=keys.has('ArrowLeft')||keys.has('a')||keys.has('A')||touch.left;
const goR=keys.has('ArrowRight')||keys.has('d')||keys.has('D')||touch.right;
const goJ=keys.has('ArrowUp')||keys.has('w')||keys.has('W')||keys.has(' ')||touch.jump;
if(goL){p.vx=-MOVE_SPD;p.dir=-1;}else if(goR){p.vx=MOVE_SPD;p.dir=1;}else p.vx*=.7;
if(goJ&&p.onGround){p.vy=JUMP_VEL;p.onGround=false;}
p.vy=Math.min(p.vy+GRAVITY,MAX_FALL);
moveEnt(p,PW,PH);
if(p.y>WH*BS+200){p.x=G.spawnX;p.y=G.spawnY;p.vx=p.vy=0;}
G.camera.x=Math.max(0,Math.min(WW*BS-W,p.x-W/2));
G.camera.y=Math.max(0,Math.min(WH*BS-H,p.y-H*.5));
// Mining tick
if(G.mining&&G.mouseDown){
G.mining.t++;
if(G.mining.t>=G.mining.max){
const{bx,by}=G.mining,drop=DATA[G.world[by][bx]]?.drop;
G.world[by][bx]=B.AIR;G.openDoors.delete(`${bx},${by}`);G.mining=null;
if(drop&&drop!==B.AIR)addItem(G.hotbar,G.inventory,drop,1);
}
}else if(!G.mouseDown)G.mining=null;
// โโ Render โโ
const cx=G.camera.x,cy=G.camera.y;
const sky=ctx.createLinearGradient(0,0,0,H);sky.addColorStop(0,sc.top);sky.addColorStop(1,sc.bot);
ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);
if(sc.dark){
const na=Math.min(.55,(G.dayTime-DAY_LEN)/300*.55);
// Stars
const st=Math.min(1,(G.dayTime-DAY_LEN)/60);
stars.forEach(s=>{
const sx2=s.x-cx,sy2=s.y-cy;
if(sx2<-10||sx2>W+10||sy2<-10||sy2>H*.6)return;
ctx.fillStyle=`rgba(255,255,255,${.7*st})`;ctx.beginPath();ctx.arc(sx2,sy2,s.r,0,Math.PI*2);ctx.fill();
});
// Moon
ctx.fillStyle="#e8e0c0";ctx.beginPath();ctx.arc(W*.75,H*.14,22,0,Math.PI*2);ctx.fill();
ctx.fillStyle=sc.top;ctx.beginPath();ctx.arc(W*.75+8,H*.14-4,18,0,Math.PI*2);ctx.fill();
// Darkness
ctx.fillStyle=`rgba(0,0,20,${na})`;ctx.fillRect(0,0,W,H);
// Torch glow
const bx0=Math.max(0,Math.floor(cx/BS)-1),bx1=Math.min(WW-1,Math.ceil((cx+W)/BS)+1);
const by0=Math.max(0,Math.floor(cy/BS)-1),by1=Math.min(WH-1,Math.ceil((cy+H)/BS)+1);
for(let by=by0;by<=by1;by++)for(let bx=bx0;bx<=bx1;bx++){
if(G.world[by][bx]===B.TORCH){
const lx=bx*BS+BS/2-cx,ly=by*BS+BS/2-cy;
const gr=ctx.createRadialGradient(lx,ly,0,lx,ly,BS*3.5);
gr.addColorStop(0,"rgba(255,150,30,.45)");gr.addColorStop(1,"rgba(0,0,0,0)");
ctx.fillStyle=gr;ctx.fillRect(lx-BS*3.5,ly-BS*3.5,BS*7,BS*7);
}
}
// Player ambient glow
const plx=Math.round(p.x-cx),ply=Math.round(p.y-cy);
const pg=ctx.createRadialGradient(plx,ply,0,plx,ply,BS*3);
pg.addColorStop(0,"rgba(255,160,40,.18)");pg.addColorStop(1,"rgba(0,0,0,0)");
ctx.fillStyle=pg;ctx.fillRect(plx-BS*3,ply-BS*3,BS*6,BS*6);
}else{
// Sun
const dp=G.dayTime/DAY_LEN;
const sunx=lerp(W*.05,W*.95,dp),suny=H*.1+Math.sin(dp*Math.PI)*(-H*.08);
ctx.fillStyle="#ffe060";ctx.beginPath();ctx.arc(sunx,suny,20,0,Math.PI*2);ctx.fill();
ctx.fillStyle="rgba(255,220,80,.22)";ctx.beginPath();ctx.arc(sunx,suny,32,0,Math.PI*2);ctx.fill();
// Clouds
ctx.fillStyle="rgba(255,255,255,.82)";
for(let i=0;i<5;i++){
const ox=((i*270+(ts*.008))%(W+500))-150,oy=40+i*38;
ctx.beginPath();ctx.ellipse(ox,oy,72,26,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(ox+48,oy-14,46,20,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(ox+24,oy-20,32,16,0,0,Math.PI*2);ctx.fill();
}
}
// Blocks
const bx0=Math.max(0,Math.floor(cx/BS)-1),bx1=Math.min(WW-1,Math.ceil((cx+W)/BS)+1);
const by0=Math.max(0,Math.floor(cy/BS)-1),by1=Math.min(WH-1,Math.ceil((cy+H)/BS)+1);
for(let by=by0;by<=by1;by++)for(let bx=bx0;bx<=bx1;bx++){
const blk=G.world[by][bx];if(!blk)continue;
const mp=G.mining&&G.mining.bx===bx&&G.mining.by===by?G.mining.t/G.mining.max:0;
drawBlock(ctx,bx*BS-cx,by*BS-cy,blk,mp,!!(G.hover?.bx===bx&&G.hover?.by===by),G.openDoors.has(`${bx},${by}`));
}
// Zombies & Player
G.zombies.forEach(z=>drawZombie(ctx,Math.round(z.x-cx),Math.round(z.y-cy),z.dir,z.flash>0,z.hp));
drawPlayer(ctx,Math.round(p.x-cx),Math.round(p.y-cy),p.dir,p.flash>0);
// HUD
drawHUD(ctx,W,H,G);
raf=requestAnimationFrame(loop);
}
// โโ Resize & Start โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function resize(){
cvs.width=cvs.offsetWidth;cvs.height=cvs.offsetHeight;
// Reposition UI buttons for screen size
const W=cvs.offsetWidth;
document.getElementById('btn-bag').style.right='10px';
}
window.addEventListener('resize',resize);
resize();
initGame();
raf=requestAnimationFrame(loop);
</script>
</body>
</html>Game Source: Mini Minecraft 2D
Creator: ShadowLegend17
Libraries: none
Complexity: complex (765 lines, 41.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: mini-minecraft-2d-shadowlegend17" to link back to the original. Then publish at arcadelab.ai/publish.