๐ŸŽฎArcadeLab

Mini Minecraft 2D

by ShadowLegend17
1064 lines58.1 KB
โ–ถ Play
<!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-sleep" class="btn" style="bottom:94px;left:14px;width:152px;height:50px;font-size:16px;border-radius:12px;opacity:0.35;">๐Ÿ˜ด Sleep</div>
  <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:70px;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=4200,NIGHT_LEN=2800,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,REDSTONE:22,EMERALD:23,DIAMOND:24,RS_PICK:25,RS_AXE:26,RS_SWORD:27,EM_PICK:28,EM_AXE:29,EM_SWORD:30,DM_PICK:31,DM_AXE:32,DM_SWORD:33,IR_HELM:34,IR_CHEST:35,IR_BOOT:36,RS_HELM:37,RS_CHEST:38,RS_BOOT:39,EM_HELM:40,EM_CHEST:41,EM_BOOT:42,DM_HELM:43,DM_CHEST:44,DM_BOOT:45,BED:46,BOW:47,ARROW:48,DISPENSER:49};

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.LEAVES,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.REDSTONE]:{col:"#6a1818",name:"Redstone Ore",mt:100,drop:B.REDSTONE,pl:true},
  [B.EMERALD]:{col:"#1a5c2a",name:"Emerald Ore",mt:140,drop:B.EMERALD,pl:true},
  [B.DIAMOND]:{col:"#1a5a7a",name:"Diamond Ore",mt:200,drop:B.DIAMOND,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.BED]:{col:"#d04040",name:"Bed",mt:20,drop:B.BED,pl:true,isBed: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},
  [B.RS_PICK]:{col:"#cc2200",name:"Redstone Pickaxe",icon:"โ›",pl:false,tc:"pick",tm:12},
  [B.RS_AXE]:{col:"#cc2200",name:"Redstone Axe",icon:"๐Ÿช“",pl:false,tc:"axe",tm:12},
  [B.RS_SWORD]:{col:"#cc2200",name:"Redstone Sword",icon:"โš”๏ธ",pl:false,tc:"sword",tm:1,sdmg:3},
  [B.EM_PICK]:{col:"#20aa50",name:"Emerald Pickaxe",icon:"โ›",pl:false,tc:"pick",tm:18},
  [B.EM_AXE]:{col:"#20aa50",name:"Emerald Axe",icon:"๐Ÿช“",pl:false,tc:"axe",tm:18},
  [B.EM_SWORD]:{col:"#20aa50",name:"Emerald Sword",icon:"โš”๏ธ",pl:false,tc:"sword",tm:1,sdmg:4},
  [B.DM_PICK]:{col:"#20c8e8",name:"Diamond Pickaxe",icon:"โ›",pl:false,tc:"pick",tm:28},
  [B.DM_AXE]:{col:"#20c8e8",name:"Diamond Axe",icon:"๐Ÿช“",pl:false,tc:"axe",tm:28},
  [B.DM_SWORD]:{col:"#20c8e8",name:"Diamond Sword",icon:"โš”๏ธ",pl:false,tc:"sword",tm:1,sdmg:6},
  [B.IR_HELM]:{col:"#c0b8a8",name:"Iron Helmet",icon:"๐Ÿช–",pl:false,def:1},
  [B.IR_CHEST]:{col:"#c0b8a8",name:"Iron Chestplate",icon:"๐Ÿ›ก๏ธ",pl:false,def:2},
  [B.IR_BOOT]:{col:"#c0b8a8",name:"Iron Boots",icon:"๐Ÿ‘Ÿ",pl:false,def:1},
  [B.RS_HELM]:{col:"#cc2200",name:"Redstone Helmet",icon:"๐Ÿช–",pl:false,def:2},
  [B.RS_CHEST]:{col:"#cc2200",name:"Redstone Chestplate",icon:"๐Ÿ›ก๏ธ",pl:false,def:4},
  [B.RS_BOOT]:{col:"#cc2200",name:"Redstone Boots",icon:"๐Ÿ‘Ÿ",pl:false,def:2},
  [B.EM_HELM]:{col:"#20aa50",name:"Emerald Helmet",icon:"๐Ÿช–",pl:false,def:3},
  [B.EM_CHEST]:{col:"#20aa50",name:"Emerald Chestplate",icon:"๐Ÿ›ก๏ธ",pl:false,def:6},
  [B.EM_BOOT]:{col:"#20aa50",name:"Emerald Boots",icon:"๐Ÿ‘Ÿ",pl:false,def:3},
  [B.DM_HELM]:{col:"#20c8e8",name:"Diamond Helmet",icon:"๐Ÿช–",pl:false,def:5},
  [B.DM_CHEST]:{col:"#20c8e8",name:"Diamond Chestplate",icon:"๐Ÿ›ก๏ธ",pl:false,def:10},
  [B.DM_BOOT]:{col:"#20c8e8",name:"Diamond Boots",icon:"๐Ÿ‘Ÿ",pl:false,def:5},
  [B.BOW]:{col:"#a07040",name:"Bow",icon:"๐Ÿน",pl:false,isBow:true},
  [B.ARROW]:{col:"#c8a060",name:"Arrow",icon:"โžถ",pl:false,isArrow:true},
  [B.DISPENSER]:{col:"#606060",name:"Dispenser",mt:60,drop:B.DISPENSER,pl:true,isDispenser:true},
};

const PICK_SET=new Set([B.STONE,B.COAL,B.IRON,B.BEDROCK,B.REDSTONE,B.EMERALD,B.DIAMOND]);
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}]},
  {name:"Redstone Pickaxe",res:{b:B.RS_PICK,q:1},ing:[{b:B.REDSTONE,q:3},{b:B.STICK,q:2}]},
  {name:"Redstone Axe",res:{b:B.RS_AXE,q:1},ing:[{b:B.REDSTONE,q:2},{b:B.STICK,q:2}]},
  {name:"Redstone Sword",res:{b:B.RS_SWORD,q:1},ing:[{b:B.REDSTONE,q:2},{b:B.STICK,q:1}]},
  {name:"Emerald Pickaxe",res:{b:B.EM_PICK,q:1},ing:[{b:B.EMERALD,q:3},{b:B.STICK,q:2}]},
  {name:"Emerald Axe",res:{b:B.EM_AXE,q:1},ing:[{b:B.EMERALD,q:2},{b:B.STICK,q:2}]},
  {name:"Emerald Sword",res:{b:B.EM_SWORD,q:1},ing:[{b:B.EMERALD,q:2},{b:B.STICK,q:1}]},
  {name:"Diamond Pickaxe",res:{b:B.DM_PICK,q:1},ing:[{b:B.DIAMOND,q:3},{b:B.STICK,q:2}]},
  {name:"Diamond Axe",res:{b:B.DM_AXE,q:1},ing:[{b:B.DIAMOND,q:2},{b:B.STICK,q:2}]},
  {name:"Diamond Sword",res:{b:B.DM_SWORD,q:1},ing:[{b:B.DIAMOND,q:2},{b:B.STICK,q:1}]},
  {name:"Iron Helmet",res:{b:B.IR_HELM,q:1},ing:[{b:B.IRON,q:3}]},
  {name:"Iron Chestplate",res:{b:B.IR_CHEST,q:1},ing:[{b:B.IRON,q:5}]},
  {name:"Iron Boots",res:{b:B.IR_BOOT,q:1},ing:[{b:B.IRON,q:2}]},
  {name:"Redstone Helmet",res:{b:B.RS_HELM,q:1},ing:[{b:B.REDSTONE,q:3}]},
  {name:"Redstone Chestplate",res:{b:B.RS_CHEST,q:1},ing:[{b:B.REDSTONE,q:5}]},
  {name:"Redstone Boots",res:{b:B.RS_BOOT,q:1},ing:[{b:B.REDSTONE,q:2}]},
  {name:"Emerald Helmet",res:{b:B.EM_HELM,q:1},ing:[{b:B.EMERALD,q:3}]},
  {name:"Emerald Chestplate",res:{b:B.EM_CHEST,q:1},ing:[{b:B.EMERALD,q:5}]},
  {name:"Emerald Boots",res:{b:B.EM_BOOT,q:1},ing:[{b:B.EMERALD,q:2}]},
  {name:"Diamond Helmet",res:{b:B.DM_HELM,q:1},ing:[{b:B.DIAMOND,q:3}]},
  {name:"Diamond Chestplate",res:{b:B.DM_CHEST,q:1},ing:[{b:B.DIAMOND,q:5}]},
  {name:"Diamond Boots",res:{b:B.DM_BOOT,q:1},ing:[{b:B.DIAMOND,q:2}]},
  {name:"Bed",res:{b:B.BED,q:1},ing:[{b:B.PLANK,q:3},{b:B.LEAVES,q:3}]},
  {name:"Bow",res:{b:B.BOW,q:1},ing:[{b:B.STICK,q:3},{b:B.LEAVES,q:3}]},
  {name:"Arrow x4",res:{b:B.ARROW,q:4},ing:[{b:B.STICK,q:1},{b:B.PLANK,q:1},{b:B.LEAVES,q:1}]},
  {name:"Dispenser",res:{b:B.DISPENSER,q:1},ing:[{b:B.STONE,q:9}]},
];

// โ”€โ”€ 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),depth=y-surf[x];if(depth>20&&r<.012)world[y][x]=B.DIAMOND;else if(depth>13&&r<.022)world[y][x]=B.EMERALD;else if(depth>7&&r<.04)world[y][x]=B.REDSTONE;else if(r<.045)world[y][x]=B.COAL;else if(r<.068)world[y][x]=B.IRON;else world[y][x]=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*.58)?B.SAND:B.GRASS;
  }
  for(let x=2;x<WW-2;x++){
    if(world[surf[x]][x]===B.GRASS&&rng(x,9999,seed)<.18){
      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;
  }
  if(type===B.DISPENSER){
    ctx.fillStyle="#606060";ctx.fillRect(sx,sy,BS,BS);
    // Stone texture
    ctx.fillStyle="rgba(255,255,255,.08)";ctx.fillRect(sx,sy,BS,2);ctx.fillRect(sx,sy,2,BS);
    ctx.fillStyle="rgba(0,0,0,.2)";ctx.fillRect(sx,sy+BS-3,BS,3);ctx.fillRect(sx+BS-3,sy,3,BS);
    ctx.strokeStyle="rgba(0,0,0,.15)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    // Arrow slot (center hole)
    ctx.fillStyle="#222";ctx.fillRect(sx+BS*.3,sy+BS*.25,BS*.4,BS*.5);
    // Arrow pointing right
    ctx.fillStyle="#c8a060";ctx.fillRect(sx+BS*.35,sy+BS*.44,BS*.25,5);
    ctx.fillStyle="#e0e0a0";ctx.beginPath();ctx.moveTo(sx+BS*.6,sy+BS*.38);ctx.lineTo(sx+BS*.72,sy+BS*.5);ctx.lineTo(sx+BS*.6,sy+BS*.62);ctx.fill();
    // Arrow count
    const stored=(G&&G.dispenserArrows)?G.dispenserArrows[`${Math.floor(sx/BS+G.camera.x/BS)},${Math.floor(sy/BS+G.camera.y/BS)}`]||0:0;
    ctx.font=`bold ${BS*0.22}px monospace`;ctx.fillStyle=stored>0?"#ffe066":"#888";
    ctx.textAlign="center";ctx.fillText(stored>0?`โžถ${stored}`:"empty",sx+BS/2,sy+BS*0.92);ctx.textAlign="left";
    if(hover){ctx.strokeStyle="rgba(255,255,255,.55)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.BED){
    // Frame
    ctx.fillStyle="#7a4010";ctx.fillRect(sx+1,sy+BS*0.35,BS-2,BS*0.6);
    // Mattress
    ctx.fillStyle="#d04040";ctx.fillRect(sx+3,sy+BS*0.38,BS-6,BS*0.55);
    // Pillow
    ctx.fillStyle="#f0e0e0";ctx.fillRect(sx+4,sy+BS*0.4,BS*0.35,BS*0.4);
    // Blanket stripe
    ctx.fillStyle="#b03030";ctx.fillRect(sx+BS*0.5,sy+BS*0.4,BS*0.46,BS*0.4);
    if(hover){ctx.strokeStyle="rgba(255,255,255,.55)";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);
  const oreCol={[B.COAL]:"#111",[B.IRON]:"#e8e0d0",[B.REDSTONE]:"#ff3020",[B.EMERALD]:"#30ff70",[B.DIAMOND]:"#50e8ff"};
  if(oreCol[type]){ctx.fillStyle=oreCol[type];[[7,9,7,7],[21,19,6,6],[12,27,8,8]].forEach(([dx,dy,w,h])=>ctx.fillRect(sx+dx,sy+dy,w,h));
  if(type===B.DIAMOND||type===B.EMERALD){ctx.fillStyle="rgba(255,255,255,.45)";ctx.fillRect(sx+10,sy+11,3,3);ctx.fillRect(sx+23,sy+21,2,2);}}
  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,onFire){
  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();
  // Fire emoji when burning
  if(onFire){ctx.font='16px serif';ctx.fillText('๐Ÿ”ฅ',zx-8,zy-ZH-14);}
  const bw=28,bx=zx-bw/2,by=zy-ZH-(onFire?26: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;
  // Armor bar
  const totalDef2=[...g.hotbar,...g.inventory].reduce((s,sl)=>s+(sl.count>0&&DATA[sl.block]?.def?DATA[sl.block].def:0),0);
  if(totalDef2>0){
    ctx.font='14px serif';ctx.globalAlpha=1;
    ctx.fillStyle='rgba(0,0,0,.45)';roundRect(ctx,10,42,totalDef2*14+16,22,6);ctx.fill();
    for(let i=0;i<Math.min(totalDef2,20);i++){ctx.globalAlpha=1;ctx.fillText('๐Ÿ›ก๏ธ',14+i*14,58);}
    ctx.globalAlpha=1;
  }
  // 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,bowCharge:0,bowCharging:false},
    camera:{x:0,y:0},
    hotbar:Array.from({length:8},()=>empty()),
    inventory:Array.from({length:32},()=>empty()),
    slot:0,mining:null,hover:null,mouseDown:false,openDoors:new Set(),
    dayTime:0,zombies:[],zombieTimer:0,arrows:[],dispenserTick:0,dispenserArrows:{},sleeping:false,sleepTimer:0,sleepMsg:"",
  };
  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 decayLeaves(){
  // Scan every leaf in the world - remove any with no wood within 5 blocks
  for(let y=0;y<WH;y++)for(let x=0;x<WW;x++){
    if(G.world[y][x]!==B.LEAVES)continue;
    let wood=false;
    for(let sy=-5;sy<=5&&!wood;sy++)for(let sx=-5;sx<=5&&!wood;sx++){
      const wx=x+sx,wy=y+sy;
      if(wx>=0&&wx<WW&&wy>=0&&wy<WH&&G.world[wy][wx]===B.WOOD)wood=true;
    }
    if(!wood)G.world[y][x]=B.AIR;
  }
}
function isShielded(zx,zy){
  // Check if any solid block exists above the zombie's head (open doors don't count)
  const bx=Math.floor(zx/BS);
  for(let by=Math.floor((zy-ZH)/BS)-1;by>=0;by--){
    if(bx<0||bx>=WW)break;
    const blk=G.world[by][bx];
    if(blk&&!DATA[blk]?.pass){
      if(blk===B.DOOR&&G.openDoors.has(`${bx},${by}`))continue;
      return true;
    }
  }
  return false;
}
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]){
    const blk=G.world[by][bx];
    const k=`${bx},${by}`;
    if(blk===B.DOOR){G.openDoors.has(k)?G.openDoors.delete(k):G.openDoors.add(k);}
    else if(blk===B.DISPENSER){
      const sl=G.hotbar[G.slot];
      if(sl?.count>0&&sl.block===B.ARROW){
        // Add ONE arrow into dispenser
        G.dispenserArrows[k]=(G.dispenserArrows[k]||0)+1;
        sl.count--;if(!sl.count)sl.block=B.AIR;
        const t=G.dispenserArrows[k];
        G.sleepMsg=`โžœ ${t} arrow${t!==1?"s":""}  (tap empty-handed to remove)`;
        setTimeout(()=>G.sleepMsg="",1500);
      } else {
        // Remove ONE arrow from dispenser
        const stored=G.dispenserArrows[k]||0;
        if(stored>0){
          G.dispenserArrows[k]=stored-1;
          addItem(G.hotbar,G.inventory,B.ARROW,1);
          const left=stored-1;
          G.sleepMsg=left>0?`โžœ ${left} arrow${left!==1?"s":""}  (tap empty-handed to remove)`:"Dispenser empty";
        } else {
          G.sleepMsg="Dispenser empty โ€” hold arrows & tap to load";
        }
        setTimeout(()=>G.sleepMsg="",1500);
      }
    }
    else if(blk===B.BED){
      if(G.dayTime>=DAY_LEN){G.sleeping=true;G.sleepTimer=0;}
      else{if(!G.sleepMsg){G.sleepMsg="You can only sleep at night!";setTimeout(()=>G.sleepMsg="",2500);}}
    }
    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;
  if(sl.block===B.AIR&&G.world[by][bx]===B.DISPENSER)G.dispenserArrows[`${bx},${by}`]=0;
}
function tryAttack(){
  // Melee only โ€” bow uses startBowCharge/releaseBow
  const p=G.player;if(p.atkCD>0)return;
  const eq=G.hotbar[G.slot];
  if(eq?.count&&DATA[eq.block]?.isBow)return; // bow uses charge system
  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;
}
function startBowCharge(){
  const eq=G.hotbar[G.slot];
  if(!G||!eq?.count||!DATA[eq.block]?.isBow)return;
  G.player.bowCharging=true;
}
function releaseBow(){
  const p=G.player;if(!p.bowCharging)return;
  const charge=p.bowCharge/60;
  if(charge>0.1){
    // Check for arrow ammo
    const arrowCount=countItem(G.hotbar,G.inventory,B.ARROW);
    if(arrowCount<=0){G.sleepMsg="No arrows!";setTimeout(()=>G.sleepMsg="",1500);p.bowCharging=false;p.bowCharge=0;return;}
    consume(G.hotbar,G.inventory,B.ARROW,1);
    const spd=6+charge*14;
    const dmg=Math.round(1+charge*4);
    G.arrows.push({x:p.x,y:p.y-PH*0.6,vx:p.dir*spd,vy:-charge*3,life:Math.round(50+charge*60),dmg});
  }
  p.bowCharging=false;p.bowCharge=0;
}

// โ”€โ”€ 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;releaseBow();});
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};
  const eq=G.hotbar[G.slot];
  if(eq?.count&&DATA[eq.block]?.isBow){startBowCharge();return;}
  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;releaseBow();},{passive:false});
cvs.addEventListener('touchcancel',e=>{e.preventDefault();G.mouseDown=false;G.mining=null;releaseBow();},{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'){const eq=G?.hotbar[G.slot];(eq?.count&&DATA[eq.block]?.isBow)?startBowCharge():tryAttack();}
  if(['ArrowLeft','ArrowRight','ArrowUp',' '].includes(e.key))e.preventDefault();
});
window.addEventListener('keyup',e=>{keys.delete(e.key);if(e.key==='f'||e.key==='F')releaseBow();});

// โ”€โ”€ 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();const eq=G?.hotbar[G.slot];(eq?.count&&DATA[eq.block]?.isBow)?startBowCharge():tryAttack();},{passive:false});
document.getElementById('btn-atk').addEventListener('touchend',e=>{e.stopPropagation();e.preventDefault();releaseBow();},{passive:false});
document.getElementById('btn-atk').addEventListener('touchcancel',e=>{e.stopPropagation();e.preventDefault();releaseBow();},{passive:false});
document.getElementById('btn-atk').addEventListener('mousedown',e=>{e.stopPropagation();const eq=G?.hotbar[G.slot];(eq?.count&&DATA[eq.block]?.isBow)?startBowCharge():tryAttack();});
document.getElementById('btn-atk').addEventListener('mouseup',e=>{e.stopPropagation();releaseBow();});

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('btn-sleep').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();trySleep();},{passive:false});
document.getElementById('btn-sleep').addEventListener('mousedown',e=>{e.stopPropagation();trySleep();});
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 trySleep(){
  if(!G)return;
  if(G.dayTime>=DAY_LEN){G.sleeping=true;G.sleepTimer=0;}
  else{G.sleepMsg="You can only sleep at night!";setTimeout(()=>G.sleepMsg="",2500);}
}
function updateAtkBtn(){
  const btn=document.getElementById('btn-atk');if(!G||!btn)return;
  const eq=G.hotbar[G.slot];
  const isBow=eq?.count&&DATA[eq.block]?.isBow;
  btn.textContent=isBow?"๐Ÿน":"โš”๏ธ";
  btn.title=isBow?"Hold to charge, release to fire":"Melee attack";
}
function updateSleepBtn(){
  const btn=document.getElementById('btn-sleep');
  if(!btn||!G)return;
  // Show bright only at night and when bed is in inventory/hotbar or placed nearby
  const hasBed=[...G.hotbar,...G.inventory].some(s=>s.count>0&&s.block===B.BED);
  const night=G.dayTime>=DAY_LEN;
  btn.style.opacity=night&&hasBed?"1":"0.3";
  btn.style.background=night&&hasBed?"rgba(80,0,120,0.9)":"rgba(15,15,15,0.7)";
  btn.style.borderColor=night&&hasBed?"rgba(180,100,255,0.6)":"rgba(255,255,255,0.2)";
}
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;
  if(G.dayTime%30===0){updateSleepBtn();updateAtkBtn();}
  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;}
      }
    }
  }
  // Zombie AI - burn first, then filter dead ones
  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);
    // Burn in daylight unless sheltered by a block or door above
    if(!isNight&&!isShielded(z.x,z.y)){z.burnT=(z.burnT||0)+1;if(z.burnT%30===0){z.hp=Math.max(0,z.hp-1);z.flash=25;z.onFire=true;}}
    else{z.burnT=0;z.onFire=false;}
    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){
      const totalDef=[...G.hotbar,...G.inventory].reduce((s,sl)=>s+(sl.count>0&&DATA[sl.block]?.def?DATA[sl.block].def:0),0);
      const blockChance=Math.min(0.85,totalDef*0.05);
      if(Math.random()>blockChance){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');
    }
  });
  // Remove dead zombies after processing
  G.zombies=G.zombies.filter(z=>z.hp>0);

  // Dispenser firing (every 90 frames ~1.5s)
  G.dispenserTick++;
  if(G.dispenserTick>=90){
    G.dispenserTick=0;
    for(let dy=Math.max(0,Math.floor(G.camera.y/BS)-2);dy<=Math.min(WH-1,Math.ceil((G.camera.y+800)/BS)+2);dy++){
      for(let dx=0;dx<WW;dx++){
        if(G.world[dy][dx]!==B.DISPENSER)continue;
        const wx=(dx+.5)*BS,wy=(dy+.5)*BS;
        // Find nearest zombie within 20 blocks
        let nearest=null,bestDist=20*BS;
        G.zombies.forEach(z=>{const d=Math.hypot(z.x-wx,z.y-wy);if(d<bestDist){bestDist=d;nearest=z;}});
        const dk=`${dx},${dy}`;
        const stored=G.dispenserArrows[dk]||0;
        if(nearest&&stored>0){
          G.dispenserArrows[dk]=stored-1;
          const ang=Math.atan2(nearest.y-wy,nearest.x-wx);
          G.arrows.push({x:wx,y:wy,vx:Math.cos(ang)*13,vy:Math.sin(ang)*13,life:90,dmg:2,fromDispenser:true});
        }
      }
    }
  }
  // Arrow physics & collision
  G.arrows.forEach(a=>{
    a.vy+=0.25;a.x+=a.vx;a.y+=a.vy;a.life--;
    // Hit zombie
    G.zombies.forEach(z=>{
      if(a.life>0&&Math.abs(a.x-z.x)<ZW&&Math.abs(a.y-z.y)<ZH){z.hp-=(a.dmg||3);z.flash=12;a.life=0;}
    });
    // Hit solid block
    const bx=Math.floor(a.x/BS),by=Math.floor(a.y/BS);
    if(bx>=0&&bx<WW&&by>=0&&by<WH){
      const blk=G.world[by][bx];
      if(blk&&!DATA[blk]?.pass&&!(blk===B.DOOR&&G.openDoors.has(`${bx},${by}`)))a.life=0;
    }
  });
  G.zombies=G.zombies.filter(z=>z.hp>0);
  G.arrows=G.arrows.filter(a=>a.life>0);

  // Player
  if(p.atkCD>0)p.atkCD--;if(p.flash>0)p.flash--;if(p.dmgCD>0)p.dmgCD--;
  // Bow charging
  const equippedBow=G.hotbar[G.slot]?.count>0&&DATA[G.hotbar[G.slot].block]?.isBow;
  if(p.bowCharging&&equippedBow)p.bowCharge=Math.min(60,p.bowCharge+1);
  else if(!equippedBow){p.bowCharging=false;p.bowCharge=0;}
  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;}
  // Sleep when standing on top of a bed
  if(!G.sleeping){
    const bx1=Math.floor((p.x-PW/2)/BS),bx2=Math.floor((p.x+PW/2)/BS);
    const footBy=Math.floor(p.y/BS); // block directly under feet
    let onBed=false;
    for(let bx=bx1;bx<=bx2&&!onBed;bx++)
      if(bx>=0&&bx<WW&&footBy>=0&&footBy<WH&&G.world[footBy][bx]===B.BED)onBed=true;
    if(onBed&&G.hotbar[G.slot]?.block!==B.BED){
      if(G.dayTime>=DAY_LEN){G.sleeping=true;G.sleepTimer=0;}
      else{if(!G.sleepMsg){G.sleepMsg="You can only sleep at night!";setTimeout(()=>G.sleepMsg="",2500);}}
    }
  }

  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;
      const wasWood=G.world[by][bx]===B.WOOD;
      const mKey=`${bx},${by}`;
      if(G.world[by][bx]===B.DISPENSER){const stored=G.dispenserArrows[mKey]||0;if(stored>0)addItem(G.hotbar,G.inventory,B.ARROW,stored);delete G.dispenserArrows[mKey];}
      G.world[by][bx]=B.AIR;G.openDoors.delete(mKey);G.mining=null;
      if(wasWood)decayLeaves();
      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,z.onFire));
  // Draw arrows
  G.arrows.forEach(a=>{
    const ax=Math.round(a.x-cx),ay=Math.round(a.y-cy);
    const angle=Math.atan2(a.vy,a.vx);
    ctx.save();ctx.translate(ax,ay);ctx.rotate(angle);
    ctx.fillStyle="#a07040";ctx.fillRect(-10,-2,20,3);
    ctx.fillStyle="#e0e0a0";ctx.beginPath();ctx.moveTo(10,-3);ctx.lineTo(14,0);ctx.lineTo(10,3);ctx.fill();
    ctx.restore();
  });
  // Bow charge bar above player
  if(p.bowCharging&&p.bowCharge>0){
    const px2=Math.round(p.x-cx),py2=Math.round(p.y-cy);
    const t=p.bowCharge/60;
    const bw=36,bx2=px2-bw/2,by2=py2-PH-18;
    ctx.fillStyle="rgba(0,0,0,.6)";ctx.fillRect(bx2-1,by2-1,bw+2,10);
    const col=t<0.5?`rgb(255,${Math.round(200*t*2)},0)`:"rgb(255,200,0)";
    if(t>=1){ctx.fillStyle="#fff";ctx.fillRect(bx2-1,by2-1,bw+2,10);}
    ctx.fillStyle=col;ctx.fillRect(bx2,by2,bw*t,8);
    if(t>=1){ctx.font="12px serif";ctx.textAlign="center";ctx.fillText("๐Ÿน",px2,by2-4);ctx.textAlign="left";}
  }
  drawPlayer(ctx,Math.round(p.x-cx),Math.round(p.y-cy),p.dir,p.flash>0);

  // HUD
  // Sleep overlay
  if(G.sleeping){
    G.sleepTimer++;
    const fadeIn=Math.min(1,G.sleepTimer/40);
    const fadeOut=G.sleepTimer>80?Math.max(0,1-(G.sleepTimer-80)/20):1;
    const a=fadeIn*fadeOut;
    ctx.fillStyle=`rgba(5,5,20,${a*0.97})`;ctx.fillRect(0,0,W,H);
    if(G.sleepTimer>20&&G.sleepTimer<90){
      ctx.textAlign="center";ctx.font="bold 28px monospace";
      ctx.fillStyle=`rgba(255,255,255,${a})`;ctx.fillText("๐Ÿ’ค Sleeping...",W/2,H/2);
      ctx.font="16px monospace";ctx.fillStyle=`rgba(200,200,255,${a*0.7})`;
      ctx.fillText("Skipping to morning",W/2,H/2+36);ctx.textAlign="left";
    }
    if(G.sleepTimer===60){G.dayTime=0;G.zombies=[];}
    if(G.sleepTimer>=100){G.sleeping=false;G.sleepTimer=0;G.sleepMsg="โ˜€๏ธ Good morning!";setTimeout(()=>G.sleepMsg="",2500);}
  }
  // Sleep/info message
  if(G.sleepMsg){
    ctx.textAlign="center";ctx.font="bold 14px monospace";
    const tw=ctx.measureText(G.sleepMsg).width;
    ctx.fillStyle="rgba(0,0,0,.7)";roundRect(ctx,(W-tw-24)/2,H/2-20,tw+24,34,10);ctx.fill();
    ctx.fillStyle="#fff";ctx.fillText(G.sleepMsg,W/2,H/2+4);ctx.textAlign="left";
  }
  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 (1064 lines, 58.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.