๐ŸŽฎArcadeLab

Mini Minecraft 2D

by ShadowLegend17
4715 lines244.6 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-eat" class="btn" style="bottom:94px;right:8px;width:90px;height:50px;font-size:15px;border-radius:12px;background:rgba(30,100,30,.85);border-color:rgba(80,200,80,.4);opacity:0.35;">๐ŸŽ Eat</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>
  <div id="btn-down" class="btn" style="bottom:14px;right:114px;width:80px;height:70px;display:none;background:rgba(30,30,120,.88);border-color:rgba(100,100,255,.4);">โฌ‡ Down</div>
  <div id="btn-noclip" class="btn" style="bottom:94px;left:174px;width:110px;height:50px;font-size:13px;border-radius:12px;display:none;background:rgba(120,0,120,.88);border-color:rgba(200,80,255,.5);">๐Ÿ‘ป Noclip: ON</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 id="btn-blueprint" class="btn" style="top:10px;right:120px;height:38px;padding:0 14px;border-radius:10px;font-size:12px;letter-spacing:.5px;pointer-events:all;display:none;background:rgba(80,100,220,.85);border-color:rgba(140,160,255,.6);">๐Ÿ“ Blueprint</div>
  <div id="btn-space" class="btn" style="top:10px;right:240px;height:38px;padding:0 14px;border-radius:10px;font-size:12px;letter-spacing:.5px;pointer-events:all;display:none;background:rgba(10,10,50,.9);border-color:rgba(100,120,255,.6);">๐ŸŒŒ Space</div>
  <!-- Blueprint panel -->
  <div id="bp-panel" style="display:none;position:absolute;top:58px;right:10px;z-index:12;
    background:rgba(20,20,50,.92);border:2px solid rgba(140,160,255,.5);border-radius:12px;
    padding:12px 16px;color:#fff;font-family:monospace;font-size:13px;min-width:200px;pointer-events:all;">
    <div id="bp-status" style="margin-bottom:10px;color:#aac;font-size:12px;">Tap a block for corner 1</div>
    <div style="display:flex;gap:8px;">
      <button id="bp-save" onclick="blueprintSave()" style="display:none;flex:1;padding:8px;border-radius:8px;border:none;background:rgba(80,200,100,.8);color:#fff;font-weight:bold;cursor:pointer;">๐Ÿ’พ Save Kit</button>
      <button onclick="blueprintCancel()" style="flex:1;padding:8px;border-radius:8px;border:none;background:rgba(200,80,80,.7);color:#fff;font-weight:bold;cursor:pointer;">โœ• Cancel</button>
    </div>
  </div>
  <div id="btn-menu" class="btn" style="top:56px;right:10px;height:38px;padding:0 14px;border-radius:10px;font-size:12px;pointer-events:all;background:rgba(80,40,0,.88);border-color:rgba(255,180,50,.35);">๐Ÿ’พ Menu</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="display:flex;gap:6px;margin-bottom:8px;">
        <input id="inv-search" type="text" placeholder="๐Ÿ” Search items..." oninput="filterInv(this.value)"
          style="flex:1;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.18);
          border-radius:8px;color:#fff;font-size:13px;padding:7px 10px;outline:none;
          -webkit-tap-highlight-color:transparent;">
        <button onclick="filterInv(document.getElementById('inv-search').value);document.getElementById('inv-search').blur();"
          style="background:rgba(80,180,100,.85);border:none;border-radius:8px;color:#fff;
          font-size:13px;font-weight:bold;padding:7px 14px;cursor:pointer;
          -webkit-tap-highlight-color:transparent;">Done</button>
      </div>
      <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>

<!-- Main Menu -->
<div id="menu" style="position:absolute;inset:0;z-index:100;display:flex;flex-direction:column;
  align-items:center;justify-content:flex-start;overflow-y:auto;
  background:linear-gradient(180deg,#1a6090 0%,#3390cc 35%,#87ceeb 65%,#5c9e30 75%,#8B6340 88%,#555 100%);
  font-family:monospace;padding:28px 0 40px;">
  <!-- No separate strips needed โ€” background gradient covers full sky+ground -->
  <!-- Title -->
  <div style="position:relative;z-index:2;text-align:center;margin-bottom:28px;flex-shrink:0;">
    <div style="font-size:clamp(24px,5vw,46px);font-weight:bold;color:#fff;
      text-shadow:4px 4px 0 #000,-2px -2px 0 #000,2px -2px 0 #000,-2px 2px 0 #000;
      letter-spacing:2px;line-height:1.1;">โ› Mini Minecraft 2D</div>
    <div style="color:#ffe066;font-size:14px;margin-top:8px;text-shadow:1px 1px 2px #000;">Choose your game mode</div>
  </div>
  <!-- Mode buttons -->
  <div style="position:relative;z-index:2;display:flex;flex-direction:column;gap:14px;width:min(340px,88vw);">
  <div style="padding:14px 24px;
    background:linear-gradient(135deg,rgba(5,5,30,.96),rgba(20,0,60,.96));color:#fff;
    border:3px solid rgba(100,120,255,.6);border-radius:14px;text-align:center;">
    <div style="font-size:16px;font-weight:bold;margin-bottom:10px;letter-spacing:1px;">๐ŸŒŒ Launch to Space</div>
    <div style="display:flex;gap:8px;">
      <button onclick="startSpaceGame()" style="flex:1;padding:10px 8px;border-radius:9px;border:2px solid rgba(100,200,255,.5);background:rgba(0,80,200,.4);color:#fff;font-size:13px;font-weight:bold;cursor:pointer;-webkit-tap-highlight-color:transparent;">
        ๐ŸŒŸ Creative
      </button>
      <button onclick="startSpaceSurvival()" style="flex:1;padding:10px 8px;border-radius:9px;border:2px solid rgba(255,180,80,.5);background:rgba(180,80,0,.4);color:#fff;font-size:13px;font-weight:bold;cursor:pointer;-webkit-tap-highlight-color:transparent;">
        โš”๏ธ Survival
        <div style="font-size:10px;color:rgba(255,220,150,.7);font-weight:normal;">Jetpack+Suit+Pickaxe</div>
      </button>
    </div>
  </div>
  <div id="btn-resume" style="padding:16px 24px;background:rgba(30,80,130,.95);color:#fff;
    border:3px solid rgba(80,180,255,.55);border-radius:14px;cursor:pointer;text-align:center;
    font-size:17px;font-weight:bold;letter-spacing:1px;display:none;
    box-shadow:0 4px 20px rgba(50,150,255,.25);-webkit-tap-highlight-color:transparent;">
    โ–ถ Back to World
    <div style="font-size:11px;color:rgba(255,255,255,.55);font-weight:normal;margin-top:3px;">Resume where you left off</div>
  </div>
  <div id="btn-continue" style="padding:14px 24px;background:rgba(140,100,30,.92);color:#fff;
    border:3px solid rgba(255,200,80,.5);border-radius:14px;cursor:pointer;text-align:center;
    font-size:16px;font-weight:bold;letter-spacing:1px;display:none;
    box-shadow:0 4px 20px rgba(200,150,0,.2);-webkit-tap-highlight-color:transparent;">
    ๐ŸŒ Continue World
    <div style="font-size:11px;color:rgba(255,255,255,.6);font-weight:normal;margin-top:4px;" id="save-info">Loadingโ€ฆ</div>
  </div>
    <div id="btn-survival" style="padding:18px 24px;background:rgba(60,120,60,.92);color:#fff;
      border:3px solid rgba(100,220,100,.5);border-radius:14px;cursor:pointer;text-align:center;
      font-size:18px;font-weight:bold;letter-spacing:1px;
      box-shadow:0 4px 20px rgba(0,200,0,.2);transition:all .15s;
      -webkit-tap-highlight-color:transparent;">
      โš”๏ธ Survival
      <div style="font-size:11px;color:rgba(255,255,255,.6);font-weight:normal;margin-top:4px;">Mine, craft, fight to survive</div>
    </div>
    <div id="btn-creative" style="padding:18px 24px;background:rgba(60,60,180,.92);color:#fff;
      border:3px solid rgba(100,150,255,.5);border-radius:14px;cursor:pointer;text-align:center;
      font-size:18px;font-weight:bold;letter-spacing:1px;
      box-shadow:0 4px 20px rgba(80,80,255,.2);transition:all .15s;
      -webkit-tap-highlight-color:transparent;">
      ๐ŸŒŸ Creative
      <div style="font-size:11px;color:rgba(255,255,255,.6);font-weight:normal;margin-top:4px;">Infinite items ยท Fly ยท No damage</div>
    </div>
    <div id="btn-flat" style="padding:14px 24px;background:rgba(100,60,20,.92);color:#fff;
      border:3px solid rgba(200,140,60,.5);border-radius:14px;cursor:pointer;text-align:center;
      font-size:16px;font-weight:bold;letter-spacing:1px;
      box-shadow:0 4px 20px rgba(150,100,30,.2);-webkit-tap-highlight-color:transparent;">
      ๐ŸŒ Flat World
      <div style="font-size:11px;color:rgba(255,255,255,.6);font-weight:normal;margin-top:4px;">Totally flat ยท Creative mode</div>
    </div>
    <div id="btn-night-flat" style="padding:14px 24px;background:rgba(10,10,40,.92);color:#fff;
      border:3px solid rgba(80,80,180,.5);border-radius:14px;cursor:pointer;text-align:center;
      font-size:16px;font-weight:bold;letter-spacing:1px;
      box-shadow:0 4px 20px rgba(30,30,120,.25);-webkit-tap-highlight-color:transparent;">
      ๐ŸŒ™ Night Flat World
      <div style="font-size:11px;color:rgba(255,255,255,.6);font-weight:normal;margin-top:4px;">Flat ยท Night time ยท Creative mode</div>
    </div>

    <!-- Custom World Creator -->
    <div style="width:100%;max-width:340px;">
      <input id="world-name-input" type="text" placeholder="โœ๏ธ Type a world name to create..."
        oninput="document.getElementById('world-creator-panel').style.display=this.value.trim()?'flex':'none'"
        style="width:100%;box-sizing:border-box;background:rgba(0,0,0,.5);border:2px solid rgba(255,255,255,.2);
        border-radius:12px;color:#fff;font-size:14px;padding:12px 14px;outline:none;
        -webkit-tap-highlight-color:transparent;text-align:center;">
      <div id="world-creator-panel" style="display:none;flex-direction:column;gap:10px;margin-top:10px;
        background:rgba(0,0,0,.45);border:1px solid rgba(255,255,255,.15);border-radius:14px;padding:14px;">
        <!-- Mode -->
        <div style="color:rgba(255,255,255,.6);font-size:11px;font-weight:bold;letter-spacing:1px;">MODE</div>
        <div style="display:flex;gap:8px;">
          <button id="wc-creative" onclick="wcSetMode('creative')"
            style="flex:1;padding:9px;border-radius:9px;border:2px solid rgba(100,200,255,.6);background:rgba(0,120,255,.3);color:#fff;font-size:13px;font-weight:bold;cursor:pointer;">๐ŸŒŸ Creative</button>
          <button id="wc-survival" onclick="wcSetMode('survival')"
            style="flex:1;padding:9px;border-radius:9px;border:2px solid rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);font-size:13px;font-weight:bold;cursor:pointer;">โš”๏ธ Survival</button>
        </div>
        <!-- Mobs -->
        <div style="color:rgba(255,255,255,.6);font-size:11px;font-weight:bold;letter-spacing:1px;">MOBS</div>
        <div style="display:flex;gap:8px;">
          <button id="wc-mobs-on" onclick="wcSetMobs(true)"
            style="flex:1;padding:9px;border-radius:9px;border:2px solid rgba(255,100,100,.6);background:rgba(200,50,50,.3);color:#fff;font-size:13px;font-weight:bold;cursor:pointer;">๐ŸงŸ On</button>
          <button id="wc-mobs-off" onclick="wcSetMobs(false)"
            style="flex:1;padding:9px;border-radius:9px;border:2px solid rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);font-size:13px;font-weight:bold;cursor:pointer;">๐Ÿšซ Off</button>
        </div>
        <!-- Size -->
        <div style="color:rgba(255,255,255,.6);font-size:11px;font-weight:bold;letter-spacing:1px;">WORLD SIZE</div>
        <div style="display:flex;gap:6px;">
          <button id="wc-small" onclick="wcSetSize('small')"
            style="flex:1;padding:8px 4px;border-radius:9px;border:2px solid rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);font-size:12px;font-weight:bold;cursor:pointer;">๐Ÿ—บ๏ธ Small</button>
          <button id="wc-medium" onclick="wcSetSize('medium')"
            style="flex:1;padding:8px 4px;border-radius:9px;border:2px solid rgba(100,255,100,.5);background:rgba(50,180,50,.25);color:#fff;font-size:12px;font-weight:bold;cursor:pointer;">๐ŸŒ Medium</button>
          <button id="wc-large" onclick="wcSetSize('large')"
            style="flex:1;padding:8px 4px;border-radius:9px;border:2px solid rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);font-size:12px;font-weight:bold;cursor:pointer;">๐Ÿ—ป Large</button>
        </div>
        <!-- Terrain -->
        <div style="color:rgba(255,255,255,.6);font-size:11px;font-weight:bold;letter-spacing:1px;">TERRAIN</div>
        <div style="display:flex;gap:8px;">
          <button id="wc-normal" onclick="wcSetFlat(false)"
            style="flex:1;padding:9px;border-radius:9px;border:2px solid rgba(100,255,100,.5);background:rgba(50,180,50,.25);color:#fff;font-size:13px;font-weight:bold;cursor:pointer;">๐Ÿ”๏ธ Normal</button>
          <button id="wc-flat" onclick="wcSetFlat(true)"
            style="flex:1;padding:9px;border-radius:9px;border:2px solid rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);font-size:13px;font-weight:bold;cursor:pointer;">๐ŸŸซ Super Flat</button>
        </div>
        <!-- Villages -->
        <div style="color:rgba(255,255,255,.6);font-size:11px;font-weight:bold;letter-spacing:1px;">VILLAGES</div>
        <div style="display:flex;gap:8px;">
          <button id="wc-villages-on" onclick="wcSetVillages(true)"
            style="flex:1;padding:9px;border-radius:9px;border:2px solid rgba(200,160,80,.6);background:rgba(140,100,20,.3);color:#fff;font-size:13px;font-weight:bold;cursor:pointer;">๐Ÿ˜๏ธ Generate</button>
          <button id="wc-villages-off" onclick="wcSetVillages(false)"
            style="flex:1;padding:9px;border-radius:9px;border:2px solid rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);font-size:13px;font-weight:bold;cursor:pointer;">๐Ÿšซ None</button>
        </div>
        <!-- Create button -->
        <button onclick="startCustomWorld()"
          style="padding:12px;border-radius:11px;border:none;background:linear-gradient(135deg,rgba(80,200,120,.9),rgba(40,160,80,.9));
          color:#fff;font-size:15px;font-weight:bold;cursor:pointer;letter-spacing:.5px;
          box-shadow:0 3px 12px rgba(40,180,80,.3);-webkit-tap-highlight-color:transparent;">
          โ–ถ Create World
        </button>
      </div>
    </div>
  </div>
  <!-- Footer -->
  <div style="position:relative;z-index:2;margin-top:28px;color:rgba(255,255,255,.3);font-size:10px;">
    Use creator code CHROME-MAKER-41 on arcadelab.ai
  </div>
</div>

<!-- Chest overlay -->
<div id="chest-overlay" style="display:none;position:absolute;inset:0;background:rgba(0,0,0,.82);
  align-items:center;justify-content:center;z-index:20;pointer-events:all;backdrop-filter:blur(4px);">
  <div id="chest-box" style="background:linear-gradient(150deg,#2a1a0a,#1a1008);border:2px solid rgba(200,144,26,.35);
    border-radius:18px;padding:16px;display:flex;flex-direction:column;gap:10px;
    box-shadow:0 8px 40px rgba(0,0,0,.9);max-width:96vw;max-height:90vh;overflow-y:auto;width:min(560px,96vw);">
    <div style="display:flex;align-items:center;gap:8px;">
      <span style="color:#f0c060;font-weight:bold;font-size:15px;flex:1;">๐Ÿ“ฆ Chest</span>
      <span id="chest-tip" style="color:#ffe066;font-size:11px;background:rgba(255,224,102,.1);padding:2px 8px;border-radius:5px;display:none;"></span>
      <span id="chest-close" style="color:rgba(255,255,255,.45);font-size:20px;cursor:pointer;padding:0 4px;pointer-events:all;">โœ•</span>
    </div>
    <p style="color:rgba(255,255,255,.28);font-size:9px;text-align:center;margin:0;">Tap to select ยท tap another to move or swap</p>
    <div>
      <div style="color:rgba(255,200,80,.5);font-size:10px;margin-bottom:4px;">CHEST</div>
      <div id="chest-grid" style="display:grid;grid-template-columns:repeat(6,1fr);gap:3px;"></div>
    </div>
    <div style="height:1px;background:rgba(255,255,255,.07);"></div>
    <div>
      <div style="color:rgba(255,255,255,.4);font-size:10px;margin-bottom:4px;">BACKPACK</div>
      <div id="chest-inv-grid" style="display:grid;grid-template-columns:repeat(8,1fr);gap:3px;"></div>
    </div>
    <div style="height:1px;background:rgba(255,255,255,.07);"></div>
    <div>
      <div style="color:rgba(255,255,255,.4);font-size:10px;margin-bottom:4px;">HOTBAR</div>
      <div id="chest-hot-grid" style="display:flex;gap:3px;flex-wrap:wrap;"></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=-6;
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 MAX_CREEPERS=5,CREEPER_SPD=1.5,CREEPER_HP=4,CW=16,CH=34,CREEPER_FUSE=180,CREEPER_RANGE=2.2*40;
const MAX_SKELETONS=6,SKELETON_SPD=1.3,SKELETON_HP=5,SKW=16,SKH=34;

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,STONE_BRICK:50,CHEST:51,GUNPOWDER:52,TNT:53,FLINT:54,FLINT_STEEL:55,END_STONE:56,END_PORTAL:57,OBSIDIAN:58,DRAGON_EGG:59,EYE_END:60,BREAD:61,APPLE:62,MEAT:63,STEW:64,SUPPORT:65,FENCE:66,EGG_ZOMBIE:67,EGG_CREEPER:68,LAVA:69,BARRIER:70,PISTON:71,STICKY_PISTON:72,GATE:73,TRAPDOOR:74,CONV_R:75,CONV_L:76,WATER:77,PISTOL:78,MACHINE_GUN:79,FLAMETHROWER:80,BULLET:81,TURRET:82,SPIKE:83,FAN:84,ELEVATOR:85,LADDER:86,BOMB:87,JET:88,ROCKET_LAUNCHER:89,TASER:90,ROCKET_SHIP:91,MOON_ROCK:92,MOON_DUST:93,BLAST_DOOR:94,HOLO_DOOR:95,TENT_KIT:96,FIREPLACE:97,CHIMNEY:98,
  WHITE_WALL:99,LIGHT_GRAY_WALL:100,GRAY_WALL:101,BLACK_WALL:102,
  RED_WALL:103,ORANGE_WALL:104,YELLOW_WALL:105,LIME_WALL:106,
  GREEN_WALL:107,CYAN_WALL:108,LIGHT_BLUE_WALL:109,BLUE_WALL:110,
  PURPLE_WALL:111,MAGENTA_WALL:112,PINK_WALL:113,BROWN_WALL:114,KIT:115,SPACESUIT:116,SPACE_ROCK:117,JETPACK:118,SAPLING:119,CASTLE_GATE:120};

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_BRICK,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},
  [B.STONE_BRICK]:{col:"#888",name:"Stone Brick",mt:80,drop:B.STONE_BRICK,pl:true},
  [B.CHEST]:{col:"#c8901a",name:"Chest",mt:30,drop:B.CHEST,pl:true,isChest:true},
  [B.GUNPOWDER]:{col:"#888",name:"Gunpowder",icon:"๐Ÿ’ฃ",pl:false},
  [B.TNT]:{col:"#cc2222",name:"TNT",mt:10,drop:B.TNT,pl:true,isTNT:true},
  [B.FLINT]:{col:"#555",name:"Flint",icon:"๐Ÿชจ",pl:false},
  [B.FLINT_STEEL]:{col:"#aaa",name:"Flint & Steel",icon:"๐Ÿ”ฅ",pl:false,isFlintSteel:true},
  [B.END_STONE]:{col:"#d4d48a",name:"End Stone",mt:50,drop:B.END_STONE,pl:true},
  [B.END_PORTAL]:{col:"#1a004a",name:"End Portal",mt:9999,drop:B.AIR,pl:true,isPortal:true},
  [B.OBSIDIAN]:{col:"#1a0a2e",name:"Obsidian",mt:200,drop:B.OBSIDIAN,pl:true},
  [B.DRAGON_EGG]:{col:"#220033",name:"Dragon Egg",icon:"๐Ÿฅš",pl:false},
  [B.EYE_END]:{col:"#60ff80",name:"Eye of Ender",icon:"๐Ÿ‘๏ธ",pl:false},
  [B.SUPPORT]:{col:"#8B6510",name:"Support Beam",mt:40,drop:B.WOOD,pl:false,pass:true},
  [B.FENCE]:{col:"#c8a440",name:"Fence",mt:30,drop:B.FENCE,pl:true,pass:true},
  [B.GATE]:{col:"#c8a440",name:"Fence Gate",mt:30,drop:B.GATE,pl:true,isDoor:true},
  [B.TRAPDOOR]:{col:"#c8a440",name:"Trapdoor",mt:30,drop:B.TRAPDOOR,pl:true,isTrapdoor:true},
  [B.CONV_R]:{col:"#555",name:"Conveyor (Right)",mt:40,drop:B.CONV_R,pl:true,conveyor:1},
  [B.CONV_L]:{col:"#555",name:"Conveyor (Left)",mt:40,drop:B.CONV_L,pl:true,conveyor:-1},
  [B.WATER]:{col:"#2a7ab0",name:"Water",mt:9999,drop:B.AIR,pl:false,pass:true,isWater:true},
  [B.PISTOL]:{col:"#888",name:"Pistol",icon:"๐Ÿ”ซ",pl:false,isGun:true,gunType:"pistol",gunDmg:3,gunCD:28},
  [B.MACHINE_GUN]:{col:"#555",name:"Machine Gun",icon:"๐Ÿ”ซ",pl:false,isGun:true,gunType:"mg",gunDmg:1,gunCD:5},
  [B.FLAMETHROWER]:{col:"#c63",name:"Flamethrower",icon:"๐Ÿ”ฅ",pl:false,isGun:true,gunType:"flame",gunDmg:0,gunCD:3},
  [B.BULLET]:{col:"#cc8",name:"Bullets",icon:"๐Ÿ”น",pl:false,isBullet:true},
  [B.TURRET]:{col:"#778899",name:"Turret",mt:60,drop:B.TURRET,pl:true,isTurret:true},
  [B.SPIKE]:{col:"#aaa",name:"Spike Trap",mt:40,drop:B.SPIKE,pl:true,isSpike:true},
  [B.FAN]:{col:"#889",name:"Fan",mt:40,drop:B.FAN,pl:true,isFan:true},
  [B.ELEVATOR]:{col:"#8B6340",name:"Elevator",mt:40,drop:B.ELEVATOR,pl:true,isElevator:true},
  [B.LADDER]:{col:"#c8a060",name:"Ladder",mt:15,drop:B.LADDER,pl:true,pass:true,isLadder:true},
  [B.BOMB]:{col:"#222",name:"Bomb",mt:20,drop:B.BOMB,pl:true,isBomb:true},
  [B.JET]:{col:"#667788",name:"Fighter Jet",icon:"โœˆ๏ธ",pl:false,isJet:true},
  [B.ROCKET_LAUNCHER]:{col:"#885522",name:"Rocket Launcher",icon:"๐Ÿš€",pl:false,isGun:true,gunType:"rocket",gunDmg:0,gunCD:60},
  [B.TASER]:{col:"#aacc00",name:"Taser",icon:"โšก",pl:false,isGun:true,gunType:"taser",gunDmg:0,gunCD:40},
  [B.ROCKET_SHIP]:{col:"#dde",name:"Rocket Ship",mt:80,drop:B.ROCKET_SHIP,pl:true,isRocketShip:true},
  [B.MOON_ROCK]:{col:"#888",name:"Moon Rock",mt:25,drop:B.MOON_ROCK,pl:true},
  [B.MOON_DUST]:{col:"#bbb",name:"Moon Dust",mt:8,drop:B.MOON_DUST,pl:true},
  [B.BLAST_DOOR]:{col:"#334",name:"Blast Door",mt:200,drop:B.BLAST_DOOR,pl:true,isDoor:true},
  [B.HOLO_DOOR]:{col:"#44aaff",name:"Holo Door",mt:120,drop:B.HOLO_DOOR,pl:true,isDoor:true,pass:false},
  [B.TENT_KIT]:{col:"#a07040",name:"Tent Kit",icon:"โ›บ",pl:false},
  [B.FIREPLACE]:{col:"#555",name:"Fireplace",mt:40,drop:B.FIREPLACE,pl:true,isFireplace:true,pass:true},
  [B.CHIMNEY]:{col:"#666",name:"Chimney",mt:40,drop:B.CHIMNEY,pl:true,isChimney:true},
  // Colored walls
  [B.WHITE_WALL]:{col:"#f0f0f0",name:"White Wall",mt:25,drop:B.WHITE_WALL,pl:true,isColorWall:true},
  [B.LIGHT_GRAY_WALL]:{col:"#aaaaaa",name:"Light Gray Wall",mt:25,drop:B.LIGHT_GRAY_WALL,pl:true,isColorWall:true},
  [B.GRAY_WALL]:{col:"#707070",name:"Gray Wall",mt:25,drop:B.GRAY_WALL,pl:true,isColorWall:true},
  [B.BLACK_WALL]:{col:"#1e1e1e",name:"Black Wall",mt:25,drop:B.BLACK_WALL,pl:true,isColorWall:true},
  [B.RED_WALL]:{col:"#cc2222",name:"Red Wall",mt:25,drop:B.RED_WALL,pl:true,isColorWall:true},
  [B.ORANGE_WALL]:{col:"#dd7700",name:"Orange Wall",mt:25,drop:B.ORANGE_WALL,pl:true,isColorWall:true},
  [B.YELLOW_WALL]:{col:"#ddcc00",name:"Yellow Wall",mt:25,drop:B.YELLOW_WALL,pl:true,isColorWall:true},
  [B.LIME_WALL]:{col:"#55cc22",name:"Lime Wall",mt:25,drop:B.LIME_WALL,pl:true,isColorWall:true},
  [B.GREEN_WALL]:{col:"#2a7a2a",name:"Green Wall",mt:25,drop:B.GREEN_WALL,pl:true,isColorWall:true},
  [B.CYAN_WALL]:{col:"#119999",name:"Cyan Wall",mt:25,drop:B.CYAN_WALL,pl:true,isColorWall:true},
  [B.LIGHT_BLUE_WALL]:{col:"#5599dd",name:"Light Blue Wall",mt:25,drop:B.LIGHT_BLUE_WALL,pl:true,isColorWall:true},
  [B.BLUE_WALL]:{col:"#2233bb",name:"Blue Wall",mt:25,drop:B.BLUE_WALL,pl:true,isColorWall:true},
  [B.PURPLE_WALL]:{col:"#882299",name:"Purple Wall",mt:25,drop:B.PURPLE_WALL,pl:true,isColorWall:true},
  [B.MAGENTA_WALL]:{col:"#cc2299",name:"Magenta Wall",mt:25,drop:B.MAGENTA_WALL,pl:true,isColorWall:true},
  [B.PINK_WALL]:{col:"#ee88bb",name:"Pink Wall",mt:25,drop:B.PINK_WALL,pl:true,isColorWall:true},
  [B.BROWN_WALL]:{col:"#7a3a1a",name:"Brown Wall",mt:25,drop:B.BROWN_WALL,pl:true,isColorWall:true},
  [B.KIT]:{col:"#88aaff",name:"Blueprint Kit",icon:"๐Ÿ“",pl:false,isKit:true},
  [B.SPACESUIT]:{col:"#e0e0ff",name:"Spacesuit",icon:"๐Ÿ‘จโ€๐Ÿš€",pl:false,isSpacesuit:true},
  [B.SPACE_ROCK]:{col:"#3a3a4a",name:"Space Rock",mt:30,drop:B.SPACE_ROCK,pl:true},
  [B.JETPACK]:{col:"#667788",name:"Jetpack",icon:"๐Ÿš€",pl:false,isJetpack:true},
  [B.SAPLING]:{col:"#2a9a40",name:"Sapling",mt:5,drop:B.SAPLING,pl:true,isSapling:true,pass:true},
  [B.CASTLE_GATE]:{col:"#667","name":"Castle Gate",mt:60,drop:B.CASTLE_GATE,pl:true,isCastleGate:true},
  [B.LAVA]:{col:"#ff5500",name:"Lava",mt:9999,drop:B.AIR,pl:true,pass:true,isLava:true},
  [B.BARRIER]:{col:"rgba(255,50,50,.15)",name:"Barrier",mt:9999,drop:B.AIR,pl:true,isBarrier:true},
  [B.PISTON]:{col:"#8a8a8a",name:"Piston",mt:50,drop:B.PISTON,pl:true,isPiston:true},
  [B.STICKY_PISTON]:{col:"#4a7a4a",name:"Sticky Piston",mt:50,drop:B.STICKY_PISTON,pl:true,isPiston:true,isSticky:true},
  [B.EGG_ZOMBIE]:{col:"#3a8c3a",name:"Zombie Spawn Egg",icon:"๐Ÿฅš",pl:false,spawnEgg:"zombie"},
  [B.EGG_CREEPER]:{col:"#2d7a20",name:"Creeper Spawn Egg",icon:"๐Ÿฅš",pl:false,spawnEgg:"creeper"},
  [B.BREAD]:{col:"#c8903a",name:"Bread",icon:"๐Ÿž",pl:false,food:3},
  [B.APPLE]:{col:"#dd2020",name:"Apple",icon:"๐ŸŽ",pl:false,food:2},
  [B.MEAT]:{col:"#8B2500",name:"Cooked Meat",icon:"๐Ÿ–",pl:false,food:4},
  [B.STEW]:{col:"#8B5a00",name:"Mushroom Stew",icon:"๐Ÿฒ",pl:false,food:5},
};

const PICK_SET=new Set([B.STONE,B.COAL,B.IRON,B.BEDROCK,B.REDSTONE,B.EMERALD,B.DIAMOND,B.STONE_BRICK,B.END_STONE,B.OBSIDIAN]);
// Silk Touch: maps block โ†’ what it drops with ST
const SILK_DROPS={[B.GRASS]:B.GRASS,[B.STONE]:B.STONE,[B.GLASS]:B.GLASS,[B.LEAVES]:B.LEAVES};
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:"Stone Pickaxe (Brick)",res:{b:B.STONE_PICK,q:1},ing:[{b:B.STONE_BRICK,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}]},
  {name:"TNT",res:{b:B.TNT,q:1},ing:[{b:B.SAND,q:1},{b:B.GUNPOWDER,q:1}]},
  {name:"Flint",res:{b:B.FLINT,q:1},ing:[{b:B.STONE,q:2}]},
  {name:"Flint & Steel",res:{b:B.FLINT_STEEL,q:1},ing:[{b:B.FLINT,q:1},{b:B.IRON,q:1}]},
  {name:"Eye of Ender",res:{b:B.EYE_END,q:1},ing:[{b:B.EMERALD,q:1},{b:B.REDSTONE,q:2}]},
  {name:"End Portal",res:{b:B.END_PORTAL,q:1},ing:[{b:B.EYE_END,q:4},{b:B.OBSIDIAN,q:4}]},
  {name:"Obsidian",res:{b:B.OBSIDIAN,q:2},ing:[{b:B.DIAMOND,q:1},{b:B.STONE,q:4}]},
  {name:"Bread (x2)",res:{b:B.BREAD,q:2},ing:[{b:B.WOOD,q:2},{b:B.COAL,q:1}]},
  {name:"Fence (x2)",res:{b:B.FENCE,q:2},ing:[{b:B.STICK,q:4}]},
  {name:"Gate (x2)",res:{b:B.GATE,q:2},ing:[{b:B.STICK,q:2},{b:B.PLANK,q:2}]},
  {name:"Trapdoor (x2)",res:{b:B.TRAPDOOR,q:2},ing:[{b:B.PLANK,q:3}]},
  {name:"Conveyor Right",res:{b:B.CONV_R,q:4},ing:[{b:B.IRON,q:2},{b:B.REDSTONE,q:1}]},
  {name:"Conveyor Left",res:{b:B.CONV_L,q:4},ing:[{b:B.IRON,q:2},{b:B.REDSTONE,q:1}]},
  {name:"Piston",res:{b:B.PISTON,q:1},ing:[{b:B.STONE_BRICK,q:3},{b:B.IRON,q:2},{b:B.PLANK,q:2}]},
  {name:"Sticky Piston",res:{b:B.STICKY_PISTON,q:1},ing:[{b:B.PISTON,q:1},{b:B.EMERALD,q:1}]},
  {name:"Pistol",res:{b:B.PISTOL,q:1},ing:[{b:B.IRON,q:4},{b:B.REDSTONE,q:2}]},
  {name:"Machine Gun",res:{b:B.MACHINE_GUN,q:1},ing:[{b:B.IRON,q:6},{b:B.REDSTONE,q:3}]},
  {name:"Flamethrower",res:{b:B.FLAMETHROWER,q:1},ing:[{b:B.IRON,q:4},{b:B.REDSTONE,q:2},{b:B.COAL,q:3}]},
  {name:"Bullets x32",res:{b:B.BULLET,q:32},ing:[{b:B.IRON,q:1},{b:B.REDSTONE,q:1}]},
  {name:"Turret",res:{b:B.TURRET,q:1},ing:[{b:B.IRON,q:4},{b:B.STONE_BRICK,q:2}]},
  {name:"Spike Trap x4",res:{b:B.SPIKE,q:4},ing:[{b:B.IRON,q:3},{b:B.STONE_BRICK,q:1}]},
  {name:"Fan",res:{b:B.FAN,q:1},ing:[{b:B.IRON,q:3},{b:B.REDSTONE,q:1}]},
  {name:"Elevator",res:{b:B.ELEVATOR,q:1},ing:[{b:B.IRON,q:2},{b:B.PLANK,q:4},{b:B.REDSTONE,q:1}]},
  {name:"Ladder x4",res:{b:B.LADDER,q:4},ing:[{b:B.STICK,q:4},{b:B.PLANK,q:1}]},
  {name:"Bomb",res:{b:B.BOMB,q:1},ing:[{b:B.GUNPOWDER,q:5},{b:B.IRON,q:2}]},
  {name:"Fighter Jet",res:{b:B.JET,q:1},ing:[{b:B.IRON,q:8},{b:B.DIAMOND,q:2},{b:B.REDSTONE,q:3}]},
  {name:"Rocket Launcher",res:{b:B.ROCKET_LAUNCHER,q:1},ing:[{b:B.IRON,q:4},{b:B.TNT,q:2},{b:B.REDSTONE,q:1}]},
  {name:"Taser",res:{b:B.TASER,q:1},ing:[{b:B.IRON,q:2},{b:B.REDSTONE,q:3}]},
  {name:"Rocket Ship",res:{b:B.ROCKET_SHIP,q:1},ing:[{b:B.IRON,q:10},{b:B.DIAMOND,q:4},{b:B.GUNPOWDER,q:5},{b:B.REDSTONE,q:2}]},
  {name:"Blast Door",res:{b:B.BLAST_DOOR,q:1},ing:[{b:B.IRON,q:6},{b:B.DIAMOND,q:1},{b:B.REDSTONE,q:2}]},
  {name:"Holo Door",res:{b:B.HOLO_DOOR,q:1},ing:[{b:B.DIAMOND,q:4},{b:B.REDSTONE,q:3},{b:B.EMERALD,q:1}]},
  {name:"Tent Kit",res:{b:B.TENT_KIT,q:1},ing:[{b:B.STICK,q:4},{b:B.LEAVES,q:8},{b:B.PLANK,q:6}]},
  {name:"Fireplace",res:{b:B.FIREPLACE,q:1},ing:[{b:B.STONE_BRICK,q:4},{b:B.COAL,q:2}]},
  {name:"Chimney x2",res:{b:B.CHIMNEY,q:2},ing:[{b:B.STONE_BRICK,q:4}]},
  {name:"Spacesuit",res:{b:B.SPACESUIT,q:1},ing:[{b:B.IRON,q:6},{b:B.DIAMOND,q:2}]},
  {name:"Jetpack",res:{b:B.JETPACK,q:1},ing:[{b:B.IRON,q:4},{b:B.REDSTONE,q:2},{b:B.DIAMOND,q:1}]},
  {name:"Castle Gate",res:{b:B.CASTLE_GATE,q:1},ing:[{b:B.STONE_BRICK,q:6},{b:B.IRON,q:4},{b:B.WOOD,q:2}]},
];

// โ”€โ”€ 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,enchant=null){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);if(enchant)s.enchant=enchant;r-=s.count;}}
function inLava(x,y,w,h){
  const bx1=Math.floor((x-w/2)/BS),bx2=Math.floor((x+w/2)/BS);
  const by1=Math.floor((y-h)/BS),by2=Math.floor(y/BS);
  for(let by=by1;by<=by2;by++)for(let bx=bx1;bx<=bx2;bx++){
    if(by<0||by>=WH||bx<0||bx>=WW)continue;
    if(G.world[by][bx]===B.LAVA)return true;
  }
  return false;
}
function inWater(x,y,w,h){
  const bx1=Math.floor((x-w/2)/BS),bx2=Math.floor((x+w/2)/BS);
  const by1=Math.floor((y-h)/BS),by2=Math.floor(y/BS);
  for(let by=by1;by<=by2;by++)for(let bx=bx1;bx<=bx2;bx++){
    if(by<0||by>=WH||bx<0||bx>=WW)continue;
    if(G.world[by][bx]===B.WATER)return true;
  }
  return false;
}
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;
      }
    }
  }
  // Dungeon rooms
  const dungeonTries=4;
  for(let d=0;d<dungeonTries;d++){
    const dx=4+Math.floor(rng(d,55,seed)*(WW-12));
    const depth=10+Math.floor(rng(d,56,seed)*18);
    const dy=Math.min(WH-8,surf[dx]+depth);
    const rw=7+Math.floor(rng(d,57,seed)*4); // 7-10 wide
    const rh=5;
    // Clear room
    for(let ry=dy;ry<dy+rh;ry++)for(let rx=dx;rx<dx+rw;rx++){
      if(rx<0||rx>=WW||ry<0||ry>=WH)continue;
      world[ry][rx]=B.AIR;
    }
    // Stone brick walls/floor/ceiling
    for(let ry=dy-1;ry<=dy+rh;ry++)for(let rx=dx-1;rx<=dx+rw;rx++){
      if(rx<0||rx>=WW||ry<0||ry>=WH)continue;
      if(ry===dy-1||ry===dy+rh||rx===dx-1||rx===dx+rw){
        if(world[ry][rx]!==B.AIR)world[ry][rx]=B.STONE_BRICK;
      }
    }
    // Place chest in middle
    const cx2=dx+Math.floor(rw/2),cy2=dy+rh-1;
    if(cx2>=0&&cx2<WW&&cy2>=0&&cy2<WH)world[cy2][cx2]=B.CHEST;
    // Add a torch or two
    if(dx+1<WW&&dy<WH)world[dy][dx+1]=B.TORCH;
    if(dx+rw-2<WW&&dy<WH)world[dy][dx+rw-2]=B.TORCH;
  }
  // Mineshafts - horizontal tunnels underground
  const shaftCount=2;
  for(let s=0;s<shaftCount;s++){
    const startX=5+Math.floor(rng(s,80,seed)*(WW-30));
    const depth=12+Math.floor(rng(s,81,seed)*14); // 12-26 blocks deep
    const startY=Math.min(WH-8,surf[startX]+depth);
    const len=20+Math.floor(rng(s,82,seed)*25); // 20-45 blocks long
    const dir=rng(s,83,seed)>0.5?1:-1;

    for(let i=0;i<len;i++){
      const tx=startX+i*dir;
      if(tx<1||tx>=WW-1)break;
      // Carve 2-tall tunnel
      for(let ty=startY-1;ty<=startY;ty++){
        if(ty>=0&&ty<WH)world[ty][tx]=B.AIR;
      }
      // Plank floor
      if(startY+1<WH)world[startY+1][tx]=B.PLANK;
      // Wood support beam every 6 blocks - extends down to ground, passable
      if(i%6===0&&i>0){
        // Top crossbeam (passable support)
        if(startY-2>=0&&world[startY-2][tx]===B.AIR)world[startY-2][tx]=B.SUPPORT;
        // Pillar extends down from floor until hitting solid block
        for(let py=startY-1;py<WH-1;py++){
          if(world[py][tx]!==B.AIR)break;
          world[py][tx]=B.SUPPORT;
        }
      }
      // Torch every 8 blocks on ceiling
      if(i%8===4&&startY-2>=0&&world[startY-2][tx]===B.AIR)world[startY-2][tx]=B.TORCH;
      // Chest at 1/3 and 2/3 along tunnel
      if((i===Math.floor(len/3)||i===Math.floor(2*len/3))&&startY>=0&&world[startY][tx]===B.AIR){
        world[startY][tx]=B.CHEST;
      }
    }
    // Branch tunnel perpendicular
    const branchX=startX+Math.floor(len/2)*dir;
    const branchLen=10+Math.floor(rng(s,84,seed)*12);
    for(let i=0;i<branchLen;i++){
      const ty2=branchX+i;
      if(ty2<1||ty2>=WH-1)break;
      // Note: branch goes vertically in world array (different axis)
      // Actually carve horizontally in the other direction
      const bx2=branchX,by2=startY;
      const tx2=startX+Math.floor(len/2)*dir+i-Math.floor(branchLen/2);
      if(tx2>=1&&tx2<WW-1){
        if(by2-1>=0)world[by2-1][tx2]=B.AIR;
        if(by2>=0)world[by2][tx2]=B.AIR;
        if(by2+1<WH)world[by2+1][tx2]=B.PLANK;
      }
    }
  }
  // โ”€โ”€ Cave systems - organic winding passages with rich ore walls โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  for(let c=0;c<3;c++){
    let cavX=5+Math.floor(rng(c,401,seed)*(WW-10));
    let cavY=surf[Math.max(0,Math.min(WW-1,cavX))]+8+Math.floor(rng(c,402,seed)*24);
    if(cavY>=WH-5)continue;
    const steps=50+Math.floor(rng(c,403,seed)*60);
    let cdx=rng(c,404,seed)>0.5?1:-1,cdy=0;
    for(let s=0;s<steps;s++){
      const rad=1; // narrow tunnel - always 1 block radius
      const sy2=surf[Math.max(0,Math.min(WW-1,cavX))];
      const depth=cavY-sy2;
      // Carve a small tunnel (3x3 roughly)
      for(let ry=-1;ry<=1;ry++)for(let rx=-1;rx<=1;rx++){
        if(Math.abs(rx)===1&&Math.abs(ry)===1)continue; // clip corners for rounder feel
        const tx=cavX+rx,ty=cavY+ry;
        if(tx<1||tx>=WW-1||ty<2||ty>=WH-2||world[ty][tx]===B.BEDROCK||world[ty][tx]===B.WATER)continue;
        world[ty][tx]=B.AIR;
      }
      // Ore on exposed tunnel walls (moderate density)
      for(let ry=-2;ry<=2;ry++)for(let rx=-2;rx<=2;rx++){
        const tx=cavX+rx,ty=cavY+ry;
        if(tx<1||tx>=WW-1||ty<1||ty>=WH-2)continue;
        if(world[ty][tx]!==B.STONE)continue;
        const adjAir=(world[ty-1]?.[tx]===B.AIR)||(world[ty+1]?.[tx]===B.AIR)||(world[ty][tx-1]===B.AIR)||(world[ty][tx+1]===B.AIR);
        if(!adjAir)continue;
        if(rng(tx,ty,seed+91)>0.18)continue;
        const r2=rng(tx*3,ty*7,seed+92);
        if(depth>22)      world[ty][tx]=r2<0.55?B.DIAMOND:r2<0.78?B.EMERALD:B.REDSTONE;
        else if(depth>14) world[ty][tx]=r2<0.32?B.EMERALD:r2<0.62?B.REDSTONE:B.IRON;
        else              world[ty][tx]=r2<0.55?B.COAL:B.IRON;
      }
      // Torch on floor every ~15 steps
      if(s%15===7){
        const floorY=cavY+2;
        if(floorY<WH-1&&world[floorY][cavX]!==B.AIR&&world[floorY-1][cavX]===B.AIR)
          world[floorY-1][cavX]=B.TORCH;
      }
      // Change direction randomly - mostly horizontal with occasional dips
      if(rng(c,s+406,seed)<0.28)cdx=rng(c,s+407,seed)>0.5?1:-1;
      if(rng(c,s+408,seed)<0.18)cdy=Math.floor(rng(c,s+409,seed)*3)-1;
      cavX=Math.max(2,Math.min(WW-3,cavX+cdx));
      cavY=Math.max(surf[Math.max(0,Math.min(WW-1,cavX))]+5,Math.min(WH-4,cavY+cdy));
    }
  }
  // Lava pools deep underground
  for(let x=2;x<WW-2;x++){
    if(rng(x,200,seed)<0.04){
      const lavaY=Math.min(WH-3,surf[x]+22+Math.floor(rng(x,201,seed)*12));
      const w2=2+Math.floor(rng(x,202,seed)*4);
      for(let lx=x;lx<Math.min(x+w2,WW-1);lx++){
        if(world[lavaY][lx]===B.AIR||world[lavaY][lx]===B.STONE)world[lavaY][lx]=B.LAVA;
        if(lavaY+1<WH&&(world[lavaY+1][lx]===B.AIR||world[lavaY+1][lx]===B.STONE))world[lavaY+1][lx]=B.LAVA;
      }
    }
  }
  // Ponds - shallow water on grass terrain
  const ponds=[];
  for(let i=0;i<0;i++){
    const px=10+Math.floor(rng(i,310,seed)*(WW-20));
    const pw=5+Math.floor(rng(i,311,seed)*5); // 5-9 wide
    if(px+pw>=WW-2)continue;
    const py=surf[px];
    if(world[py][px]!==B.GRASS)continue;
    // Require fairly flat terrain across pond width
    let flat=true;
    for(let lx=px;lx<px+pw;lx++){if(Math.abs(surf[lx]-py)>1){flat=false;break;}}
    if(!flat)continue;
    // Carve the pond
    for(let lx=px;lx<px+pw;lx++){
      const s=surf[lx];
      // Remove any vegetation above
      for(let ly=s-5;ly<s;ly++){if(ly>=0&&(world[ly][lx]===B.LEAVES||world[ly][lx]===B.WOOD||world[ly][lx]===B.SUPPORT))world[ly][lx]=B.AIR;}
      world[s][lx]=B.WATER;           // surface water
      if(s+1<WH-1)world[s+1][lx]=B.WATER; // one block deep
      if(s+2<WH)world[s+2][lx]=B.SAND;    // sandy bottom
    }
    ponds.push({x:px,y:py,w:pw});
  }
  // One big lake per world
  for(let attempt=0;attempt<20;attempt++){
    const lx=15+Math.floor(rng(99,700,seed)*(WW-30));
    const lw=18+Math.floor(rng(99,701,seed)*12); // 18-29 blocks wide
    const ld=4; // 4 blocks deep
    if(lx+lw>=WW-2)continue;
    // Need flat-ish grass terrain
    const ly=surf[lx];
    let ok=true;
    for(let x=lx;x<lx+lw;x++){if(world[surf[x]][x]!==B.GRASS||Math.abs(surf[x]-ly)>2){ok=false;break;}}
    if(!ok)continue;
    // Carve lake with sloped edges
    for(let x=lx;x<lx+lw;x++){
      const edge=Math.min(x-lx,lx+lw-1-x,3); // slope depth near edges (0-3)
      const depth=Math.min(ld,edge+1);
      const s2=surf[x];
      for(let ly2=s2;ly2<s2+depth&&ly2<WH-2;ly2++) world[ly2][x]=B.WATER;
      if(s2+depth<WH) world[s2+depth][x]=B.SAND;
      // Clear vegetation
      for(let ly2=s2-5;ly2<s2;ly2++){
        if(ly2>=0&&(world[ly2][x]===B.LEAVES||world[ly2][x]===B.WOOD||world[ly2][x]===B.SUPPORT))world[ly2][x]=B.AIR;
      }
    }
    break; // only one lake
  }
  // โ”€โ”€ Villages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  const villageSpawns=[];
  for(let vi=0;vi<5;vi++){
    const vx=15+Math.floor(rng(vi,600,seed)*(WW-30));
    const vy=surf[vx];
    if(world[vy][vx]!==B.GRASS)continue;
    let flat=true;
    for(let x=vx;x<vx+22&&x<WW;x++){if(Math.abs(surf[x]-vy)>1||world[surf[x]][x]===B.WATER){flat=false;break;}}
    if(!flat)continue;
    // Stone brick path
    for(let x=vx;x<vx+22&&x<WW;x++){
      if(world[vy][x]===B.GRASS||world[vy][x]===B.DIRT)world[vy][x]=B.STONE_BRICK;
    }
    // 2 houses per village
    for(let h=0;h<2;h++){
      const hx=vx+h*13,hy=vy,hw=7,hh=3;
      if(hx+hw>=WW)continue;
      // Clear area (vegetation + space for peaked roof)
      for(let x=hx-2;x<=hx+hw+1;x++)for(let y=hy-hh-7;y<hy;y++){
        if(y>=0&&x>=0&&x<WW)world[y][x]=B.AIR;
      }
      // Bottom wall row: stone corners, door center, planks
      for(let x=hx;x<hx+hw;x++){
        if(hy-1<0)continue;
        world[hy-1][x]=(x===hx||x===hx+hw-1)?B.STONE_BRICK:
                        (x===hx+Math.floor(hw/2))?B.DOOR:B.PLANK;
      }
      // Middle wall row: stone corners, glass windows, air interior
      for(let x=hx;x<hx+hw;x++){
        if(hy-2<0)continue;
        world[hy-2][x]=(x===hx||x===hx+hw-1)?B.STONE_BRICK:
                        (x===hx+1||x===hx+hw-2)?B.GLASS:B.AIR;
      }
      // Top wall row: plank frame
      for(let x=hx;x<hx+hw;x++){
        if(hy-3<0)continue;
        world[hy-3][x]=(x===hx||x===hx+hw-1||x===hx+1||x===hx+hw-2)?B.PLANK:B.AIR;
      }
      // Peaked log roof (A-frame, 5 levels)
      const roofLevels=[
        [hy-4, hx-1, hx+hw],   // eave โ€” overhangs by 1 each side
        [hy-5, hx,   hx+hw-1], // wide
        [hy-6, hx+1, hx+hw-2], // medium
        [hy-7, hx+2, hx+hw-3], // narrow
        [hy-8, hx+3, hx+3],    // peak
      ];
      roofLevels.forEach(([ry,x1,x2])=>{
        if(ry<0)return;
        for(let x=Math.max(0,x1);x<=Math.min(WW-1,x2);x++) world[ry][x]=B.WOOD;
      });
      // Chimney (chimney blocks, left side, rises above roof)
      for(let cy=hy-4;cy>=hy-8;cy--){
        const cx=hx+1;
        if(cy>=0&&cx<WW)world[cy][cx]=B.CHIMNEY;
      }
      world[hy-9]&&(world[hy-9][hx+1]=B.CHIMNEY); // chimney top above roof
      // Torch inside
      if(hx+2<WW&&hy-2>=0)world[hy-2][hx+2]=B.TORCH;
      // Spawn villagers
      const vcount=1+Math.floor(rng(vi*3+h,601,seed)*2);
      for(let v=0;v<vcount;v++){
        villageSpawns.push({x:(hx+3+v)*BS,y:hy*BS,homeX:(hx+hw/2)*BS,type:Math.floor(rng(vi*5+h+v,602,seed)*3)});
      }
    }
  }
  return{world,surf,ponds,villageSpawns};
}

// โ”€โ”€ 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 drawCreeper(ctx,cx2,cy2,fuse,flash){
  const ft=fuse/CREEPER_FUSE;
  if(flash||ft>0.5){ctx.save();ctx.globalAlpha=0.4+0.6*((Date.now()/80)%1);}
  // Shadow
  ctx.fillStyle="rgba(0,0,0,.18)";ctx.beginPath();ctx.ellipse(cx2,cy2+2,CW*.55,3,0,0,Math.PI*2);ctx.fill();
  // Legs
  ctx.fillStyle="#2a6e2a";ctx.fillRect(cx2-CW/2+1,cy2-CH*.45,CW/2-2,CH*.45);
  ctx.fillStyle="#1e5e1e";ctx.fillRect(cx2+1,cy2-CH*.45,CW/2-2,CH*.45);
  // Body
  ctx.fillStyle=ft>0.5?"#aaff66":"#3aaa3a";ctx.fillRect(cx2-CW/2,cy2-CH,CW,CH*.55);
  // Head
  ctx.fillStyle=ft>0.5?"#ccff88":"#4abf4a";ctx.fillRect(cx2-CW/2+1,cy2-CH,CW-2,CH*.38);
  // Eyes
  ctx.fillStyle="#111";ctx.fillRect(cx2-CW*.35,cy2-CH+CH*.1,5,5);ctx.fillRect(cx2+CW*.1,cy2-CH+CH*.1,5,5);
  // Mouth
  ctx.fillStyle="#111";ctx.fillRect(cx2-CW*.25,cy2-CH+CH*.28,CW*.5,3);
  if(flash||ft>0.5)ctx.restore();
  // Fuse bar
  if(fuse>0){
    const bw=30,bx=cx2-bw/2,by=cy2-CH-14;
    ctx.fillStyle="rgba(0,0,0,.5)";ctx.fillRect(bx,by,bw,6);
    ctx.fillStyle=ft>0.7?"#ff2200":ft>0.4?"#ff8800":"#ffdd00";
    ctx.fillRect(bx,by,bw*ft,6);
    // Flash symbol
    ctx.font="11px serif";ctx.textAlign="center";ctx.fillText("๐Ÿ’ฅ",cx2,by-2);ctx.textAlign="left";
  }
}
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.PISTON||type===B.STICKY_PISTON){
    const ps=G?.pistonStates?.[`${Math.floor((sx+G.camera.x)/BS)},${Math.floor((sy+G.camera.y)/BS)}`]||{dir:1,extended:false};
    const sticky=type===B.STICKY_PISTON;
    const ext=ps.extended;
    const dir=ps.dir; // 1=right,-1=left,2=down,-2=up
    // Body
    ctx.fillStyle=sticky?"#3a6a3a":"#7a7a7a";ctx.fillRect(sx+1,sy+1,BS-2,BS-2);
    ctx.fillStyle="rgba(255,255,255,.08)";ctx.fillRect(sx+1,sy+1,BS-2,3);
    ctx.fillStyle="rgba(0,0,0,.2)";ctx.fillRect(sx+1,sy+BS-4,BS-2,3);
    // Face plate
    const faceCol=sticky?"#2da82d":"#aaaaaa";
    if(dir===1){ctx.fillStyle=faceCol;ctx.fillRect(sx+BS-8,sy+4,7,BS-8);}
    else if(dir===-1){ctx.fillStyle=faceCol;ctx.fillRect(sx+1,sy+4,7,BS-8);}
    else if(dir===2){ctx.fillStyle=faceCol;ctx.fillRect(sx+4,sy+BS-8,BS-8,7);}
    else{ctx.fillStyle=faceCol;ctx.fillRect(sx+4,sy+1,BS-8,7);}
    // Arrow showing direction
    ctx.fillStyle="#fff";ctx.font=`${BS*.45}px sans-serif`;ctx.textAlign="center";
    const arrows={"1":"โ†’","-1":"โ†","2":"โ†“","-2":"โ†‘"};
    ctx.fillText(arrows[dir]||"โ†’",sx+BS/2,sy+BS*.68);ctx.textAlign="left";
    // Extended arm
    if(ext){
      ctx.fillStyle="#c8a440";
      if(dir===1)ctx.fillRect(sx+BS-2,sy+BS*.3,BS*.6,BS*.4);
      else if(dir===-1)ctx.fillRect(sx-BS*.6+2,sy+BS*.3,BS*.6,BS*.4);
      else if(dir===2)ctx.fillRect(sx+BS*.3,sy+BS-2,BS*.4,BS*.6);
      else ctx.fillRect(sx+BS*.3,sy-BS*.6+2,BS*.4,BS*.6);
      // Head
      ctx.fillStyle=sticky?"#2da82d":"#aaaaaa";
      if(dir===1)ctx.fillRect(sx+BS*.95+BS*.55,sy+BS*.2,BS*.1,BS*.6);
      else if(dir===-1)ctx.fillRect(sx-BS*.6,sy+BS*.2,BS*.1,BS*.6);
      else if(dir===2)ctx.fillRect(sx+BS*.2,sy+BS*.95+BS*.55,BS*.6,BS*.1);
      else ctx.fillRect(sx+BS*.2,sy-BS*.6,BS*.6,BS*.1);
    }
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=1;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);
    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.BARRIER){
    if(G?.creative){
      // Visible ghost outline in creative only
      ctx.fillStyle="rgba(255,50,50,.08)";ctx.fillRect(sx,sy,BS,BS);
      ctx.strokeStyle="rgba(255,80,80,.55)";ctx.lineWidth=2;
      ctx.setLineDash([5,5]);ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);ctx.setLineDash([]);
      // X mark
      ctx.strokeStyle="rgba(255,80,80,.35)";ctx.lineWidth=1.5;
      ctx.beginPath();ctx.moveTo(sx+8,sy+8);ctx.lineTo(sx+BS-8,sy+BS-8);
      ctx.moveTo(sx+BS-8,sy+8);ctx.lineTo(sx+8,sy+BS-8);ctx.stroke();
    }
    // In survival: completely invisible (nothing drawn)
    return;
  }
  if(type===B.LAVA){
    const anim=Date.now()*.002;
    // Base orange
    ctx.fillStyle="#dd3300";ctx.fillRect(sx,sy,BS,BS);
    // Animated bright patches
    for(let i=0;i<3;i++){
      const ox=8+i*10+Math.sin(anim+i)*6,oy=8+Math.cos(anim*1.3+i)*8;
      const grad=ctx.createRadialGradient(sx+ox,sy+oy,0,sx+ox,sy+oy,12);
      grad.addColorStop(0,"rgba(255,220,50,.9)");
      grad.addColorStop(1,"rgba(255,80,0,0)");
      ctx.fillStyle=grad;ctx.fillRect(sx,sy,BS,BS);
    }
    // Top glow line
    ctx.fillStyle="rgba(255,180,0,.7)";ctx.fillRect(sx,sy,BS,4);
    // Subtle grid
    ctx.strokeStyle="rgba(0,0,0,.1)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    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.WATER){
    const t2=Date.now()*.0018;
    const worldBx=Math.round((sx+G.camera.x)/BS),worldBy=Math.round((sy+G.camera.y)/BS);
    const isSurface=worldBy>0&&(!G.world[worldBy-1]?.[worldBx]||G.world[worldBy-1][worldBx]===B.AIR);
    // Deep water
    ctx.fillStyle="#1a5c8a";ctx.fillRect(sx,sy,BS,BS);
    // Mid layer
    ctx.fillStyle="#2272a8";ctx.fillRect(sx,sy,BS,isSurface?BS*0.55:BS);
    if(isSurface){
      // Animated surface wave
      const waveOff=Math.sin(t2*1.4+(sx/BS)*0.9)*3;
      ctx.fillStyle="rgba(120,200,255,0.35)";ctx.fillRect(sx,sy,BS,5+waveOff);
      ctx.fillStyle="rgba(180,230,255,0.22)";ctx.fillRect(sx,sy,BS,2+waveOff*0.5);
      // Sparkle highlights
      for(let wi=0;wi<2;wi++){
        const spx=sx+BS*(0.25+wi*0.5)+Math.sin(t2+wi*2.1)*5;
        const spy=sy+3+Math.sin(t2*1.3+wi)*2;
        ctx.fillStyle="rgba(220,245,255,0.55)";
        ctx.beginPath();ctx.arc(spx,spy,1.5,0,Math.PI*2);ctx.fill();
      }
    }
    // Caustic light dots (deep effect)
    ctx.fillStyle="rgba(100,190,255,0.18)";
    ctx.beginPath();ctx.arc(sx+BS*0.3+Math.sin(t2)*5,sy+BS*0.55+Math.cos(t2*1.2)*4,4,0,Math.PI*2);ctx.fill();
    ctx.beginPath();ctx.arc(sx+BS*0.72+Math.cos(t2*0.7)*4,sy+BS*0.7+Math.sin(t2*1.5)*3,3,0,Math.PI*2);ctx.fill();
    ctx.strokeStyle="rgba(60,140,220,0.2)";ctx.lineWidth=0.5;ctx.strokeRect(sx,sy,BS,BS);
    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.TURRET){
    const k=`${Math.round((sx+G.camera.x)/BS)},${Math.round((sy+G.camera.y)/BS)}`;
    const ts=G?.turretStates?.[k];
    const gt=ts?.gunType;
    // Stone base
    ctx.fillStyle="#777";ctx.fillRect(sx,sy+BS*.55,BS,BS*.45);
    ctx.fillStyle="#999";ctx.fillRect(sx,sy+BS*.55,BS,4);
    // Platform
    ctx.fillStyle=gt==='flame'?"#774422":gt==='mg'?"#334455":"#556677";
    ctx.beginPath();ctx.arc(sx+BS/2,sy+BS*.45,BS*.28,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="rgba(255,255,255,.12)";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS*.42,BS*.22,Math.PI,Math.PI*2);ctx.fill();
    // Barrel pointing at target angle
    if(gt){
      const ang=ts.angle??0;
      const blen=BS*.52;
      ctx.strokeStyle=gt==='flame'?"#ff6600":gt==='mg'?"#aabbcc":"#ccddee";
      ctx.lineWidth=gt==='mg'?6:4;ctx.lineCap="round";
      ctx.beginPath();
      ctx.moveTo(sx+BS/2,sy+BS*.45);
      ctx.lineTo(sx+BS/2+Math.cos(ang)*blen,sy+BS*.45+Math.sin(ang)*blen);
      ctx.stroke();ctx.lineCap="butt";
      // Muzzle ring
      ctx.strokeStyle="rgba(255,255,255,.4)";ctx.lineWidth=2;
      ctx.beginPath();ctx.arc(sx+BS/2+Math.cos(ang)*blen,sy+BS*.45+Math.sin(ang)*blen,3,0,Math.PI*2);ctx.stroke();
    }else{
      // No gun โ€” barrel pointing up, grey
      ctx.strokeStyle="#888";ctx.lineWidth=4;ctx.lineCap="round";
      ctx.beginPath();ctx.moveTo(sx+BS/2,sy+BS*.45);ctx.lineTo(sx+BS/2,sy+4);ctx.stroke();ctx.lineCap="butt";
    }
    // Status LED
    ctx.fillStyle=gt?"#00ff88":"#555";ctx.beginPath();ctx.arc(sx+BS-7,sy+BS*.6,4,0,Math.PI*2);ctx.fill();
    // Label
    ctx.font=`${BS*.22}px monospace`;ctx.fillStyle="rgba(255,255,255,.7)";ctx.textAlign="center";
    ctx.fillText(gt?`${gt==='flame'?"๐Ÿ”ฅ":gt==='mg'?"๐Ÿ”ซ":"๐Ÿ”ซ"}`:"empty",sx+BS/2,sy+BS*.92);
    ctx.textAlign="left";
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    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.SPIKE){
    // Stone base
    ctx.fillStyle="#888";ctx.fillRect(sx,sy+BS*.55,BS,BS*.45);
    ctx.fillStyle="#999";ctx.fillRect(sx,sy+BS*.52,BS,4);
    // Metal spikes
    const spikeCount=5;
    for(let i=0;i<spikeCount;i++){
      const sx2=sx+BS*(i+.5)/spikeCount;
      const tipY=sy+4;
      const baseY=sy+BS*.54;
      const hw=BS*.06;
      ctx.fillStyle="#c8c8c8";
      ctx.beginPath();ctx.moveTo(sx2,tipY);ctx.lineTo(sx2-hw,baseY);ctx.lineTo(sx2+hw,baseY);ctx.closePath();ctx.fill();
      // Highlight
      ctx.fillStyle="rgba(255,255,255,.5)";
      ctx.beginPath();ctx.moveTo(sx2,tipY+2);ctx.lineTo(sx2-hw*.5,baseY);ctx.lineTo(sx2,baseY);ctx.closePath();ctx.fill();
    }
    // Blood tint if mobs recently hit (just always show a slight red edge)
    ctx.fillStyle="rgba(180,0,0,.12)";ctx.fillRect(sx,sy+BS*.52,BS,4);
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(255,80,80,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.FAN){
    const t2=Date.now()*.004;
    // Base housing
    ctx.fillStyle="#667";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="#889";ctx.fillRect(sx+2,sy+2,BS-4,BS-4);
    ctx.fillStyle="rgba(255,255,255,.08)";ctx.fillRect(sx+2,sy+2,BS-4,4);
    // Spinning blades (4 blades)
    ctx.save();
    ctx.translate(sx+BS/2,sy+BS/2);
    ctx.rotate(t2*4);
    ctx.fillStyle="#aabbcc";
    for(let b2=0;b2<4;b2++){
      ctx.rotate(Math.PI/2);
      ctx.beginPath();
      ctx.moveTo(0,-2);
      ctx.lineTo(BS*.38,-4);
      ctx.lineTo(BS*.38,4);
      ctx.lineTo(0,2);
      ctx.closePath();ctx.fill();
    }
    // Centre hub
    ctx.fillStyle="#dde";ctx.beginPath();ctx.arc(0,0,5,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(0,0,2.5,0,Math.PI*2);ctx.fill();
    ctx.restore();
    // Animated wind lines above fan (in world space this is visual only)
    ctx.strokeStyle="rgba(180,220,255,0.35)";ctx.lineWidth=1.5;ctx.setLineDash([4,4]);
    const woff=((Date.now()*.05)%16);
    for(let wi=0;wi<3;wi++){
      const wx=sx+BS*.2+wi*BS*.3;
      ctx.beginPath();ctx.moveTo(wx,sy-woff);ctx.lineTo(wx,sy-woff-10);ctx.stroke();
    }
    ctx.setLineDash([]);
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(180,220,255,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.ELEVATOR){
    // Mechanical base housing
    ctx.fillStyle="#7a5020";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="#c8a060";ctx.fillRect(sx+3,sy+3,BS-6,BS-6);
    ctx.fillStyle="#a07040";ctx.fillRect(sx+3,sy+3,BS-6,5);
    // Vertical rails on sides
    ctx.fillStyle="#888";ctx.fillRect(sx+2,sy,4,BS);ctx.fillRect(sx+BS-6,sy,4,BS);
    ctx.fillStyle="#aaa";ctx.fillRect(sx+2,sy,2,BS);ctx.fillRect(sx+BS-6,sy,2,BS);
    // Centre button/motor
    ctx.fillStyle="#556";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS/2,8,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="#88aacc";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS/2,5,0,Math.PI*2);ctx.fill();
    // Direction arrows on button
    const es2=G?.elevatorStates?.[`${Math.round((sx+G.camera.x)/BS)},${Math.round((sy+G.camera.y)/BS)}`];
    ctx.fillStyle="#fff";ctx.font=`bold ${BS*.28}px monospace`;ctx.textAlign="center";
    ctx.fillText(es2?.vy<0?"โ†‘":"โ†•",sx+BS/2,sy+BS*.62);ctx.textAlign="left";
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(180,220,255,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.LADDER){
    // Side rails
    ctx.fillStyle="#8B5A20";ctx.fillRect(sx+3,sy,4,BS);ctx.fillRect(sx+BS-7,sy,4,BS);
    ctx.fillStyle="#c8a060";ctx.fillRect(sx+4,sy,2,BS);ctx.fillRect(sx+BS-6,sy,2,BS);
    // Rungs
    const rungCount=4;
    for(let r=0;r<rungCount;r++){
      const ry=sy+Math.round(BS*(r+0.5)/rungCount)-2;
      ctx.fillStyle="#a07030";ctx.fillRect(sx+3,ry,BS-6,5);
      ctx.fillStyle="#c8a060";ctx.fillRect(sx+3,ry,BS-6,2);
      ctx.fillStyle="rgba(0,0,0,.15)";ctx.fillRect(sx+3,ry+4,BS-6,1);
    }
    ctx.strokeStyle="rgba(0,0,0,.15)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(255,220,100,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.BOMB){
    // Round black bomb body
    ctx.fillStyle="#111";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS*.58,BS*.35,0,Math.PI*2);ctx.fill();
    // Shine highlight
    ctx.fillStyle="rgba(255,255,255,.18)";ctx.beginPath();ctx.arc(sx+BS*.38,sy+BS*.44,BS*.12,0,Math.PI*2);ctx.fill();
    // Fuse rope
    ctx.strokeStyle="#8B6340";ctx.lineWidth=2.5;ctx.lineCap="round";
    ctx.beginPath();ctx.moveTo(sx+BS/2,sy+BS*.22);ctx.bezierCurveTo(sx+BS*.62,sy+BS*.1,sx+BS*.72,sy+BS*.14,sx+BS*.68,sy+BS*.24);ctx.stroke();
    ctx.lineCap="butt";
    // Fuse tip spark (static)
    ctx.fillStyle="#ffcc00";ctx.beginPath();ctx.arc(sx+BS*.68,sy+BS*.24,3,0,Math.PI*2);ctx.fill();
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(255,80,80,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.ROCKET_SHIP){
    // Draw tall rocket extending above block
    const rx=sx+BS/2,ry=sy+BS;
    ctx.fillStyle="#dde";ctx.fillRect(sx+BS*.28,sy-BS*1.2,BS*.44,BS*2.2);// body
    ctx.fillStyle="#ccccee";ctx.fillRect(sx+BS*.28,sy-BS*1.2,BS*.44,6);// shade
    // Nose cone
    ctx.fillStyle="#eeeeff";ctx.beginPath();ctx.moveTo(rx,sy-BS*1.9);ctx.lineTo(sx+BS*.28,sy-BS*1.2);ctx.lineTo(sx+BS*.72,sy-BS*1.2);ctx.closePath();ctx.fill();
    // Red stripe
    ctx.fillStyle="#cc2222";ctx.fillRect(sx+BS*.28,sy-BS*.6,BS*.44,8);
    // Porthole
    ctx.fillStyle="#88ccff";ctx.beginPath();ctx.arc(rx,sy-BS*.1,BS*.1,0,Math.PI*2);ctx.fill();
    ctx.strokeStyle="rgba(100,100,200,.4)";ctx.lineWidth=1.5;ctx.beginPath();ctx.arc(rx,sy-BS*.1,BS*.1,0,Math.PI*2);ctx.stroke();
    // Fins
    ctx.fillStyle="#bbbbdd";
    ctx.beginPath();ctx.moveTo(sx+BS*.28,sy+BS*.6);ctx.lineTo(sx+BS*.05,sy+BS);ctx.lineTo(sx+BS*.28,sy+BS);ctx.closePath();ctx.fill();
    ctx.beginPath();ctx.moveTo(sx+BS*.72,sy+BS*.6);ctx.lineTo(sx+BS*.95,sy+BS);ctx.lineTo(sx+BS*.72,sy+BS);ctx.closePath();ctx.fill();
    // Flame when launching
    if(G?.moonLaunch){const fl=Date.now()*.008;ctx.fillStyle="rgba(255,140,0,.85)";ctx.beginPath();ctx.ellipse(rx,sy+BS,8+Math.sin(fl)*4,14+Math.sin(fl*1.3)*6,0,0,Math.PI*2);ctx.fill();ctx.fillStyle="rgba(255,255,100,.6)";ctx.beginPath();ctx.ellipse(rx,sy+BS,4,7,0,0,Math.PI*2);ctx.fill();}
    // Label
    ctx.fillStyle="#334";ctx.font=`${BS*.16}px monospace`;ctx.textAlign="center";ctx.fillText("MOON",rx,sy+BS*.4);ctx.textAlign="left";
    ctx.strokeStyle="rgba(0,0,0,.15)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(255,230,100,.9)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.MOON_ROCK){
    ctx.fillStyle="#777";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="#888";ctx.fillRect(sx,sy,BS,4);
    // Crater pits
    ctx.fillStyle="#555";ctx.beginPath();ctx.arc(sx+BS*.3,sy+BS*.4,4,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="#666";ctx.beginPath();ctx.arc(sx+BS*.3,sy+BS*.4,3,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="#555";ctx.beginPath();ctx.arc(sx+BS*.7,sy+BS*.65,3,0,Math.PI*2);ctx.fill();
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    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.MOON_DUST){
    ctx.fillStyle="#aaa";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="#bbb";ctx.fillRect(sx,sy,BS,5);
    ctx.fillStyle="#ccc";ctx.fillRect(sx,sy,BS,2);
    ctx.strokeStyle="rgba(0,0,0,.1)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    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.BLAST_DOOR||type===B.HOLO_DOOR){
    if(type===B.BLAST_DOOR){
      if(open){
        ctx.fillStyle="#223";ctx.fillRect(sx,sy,6,BS);
        ctx.fillStyle="#334";ctx.fillRect(sx,sy,4,BS);
        ctx.fillStyle="#ff6600";ctx.fillRect(sx+1,sy+6,2,BS-12);
      }else{
        ctx.fillStyle="#223";ctx.fillRect(sx,sy,BS,BS);
        ctx.fillStyle="#334";ctx.fillRect(sx+3,sy+3,BS-6,BS-6);
        ctx.fillStyle="#445";ctx.fillRect(sx+3,sy+3,BS-6,4);
        ctx.fillStyle="#556";
        [[6,6],[BS-8,6],[6,BS-8],[BS-8,BS-8]].forEach(([rx,ry])=>{ctx.beginPath();ctx.arc(sx+rx,sy+ry,3,0,Math.PI*2);ctx.fill();});
        ctx.fillStyle="#ff6600";
        for(let i=0;i<3;i++)ctx.fillRect(sx+3,sy+12+i*10,BS-6,4);
        ctx.fillStyle="#445";ctx.fillRect(sx+BS*.3,sy+BS*.5,BS*.4,BS*.3);
        ctx.fillStyle="#ff6600";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS*.65,5,0,Math.PI*2);ctx.fill();
        ctx.fillStyle="#667";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS*.65,3,0,Math.PI*2);ctx.fill();
        ctx.fillStyle="rgba(255,102,0,.6)";ctx.font=`bold ${BS*.14}px monospace`;ctx.textAlign="center";ctx.fillText("VAULT",sx+BS/2,sy+BS*.44);ctx.textAlign="left";
      }
    }else{
      const t5=Date.now()*.003;
      if(open){
        ctx.strokeStyle="rgba(80,180,255,.5)";ctx.lineWidth=2;
        ctx.strokeRect(sx+2,sy+2,4,BS-4);ctx.strokeRect(sx+BS-6,sy+2,4,BS-4);
      }else{
        const grd=ctx.createLinearGradient(sx,sy,sx+BS,sy);
        grd.addColorStop(0,"rgba(0,100,255,0.05)");grd.addColorStop(0.4,"rgba(0,180,255,0.35)");
        grd.addColorStop(0.6,"rgba(0,180,255,0.35)");grd.addColorStop(1,"rgba(0,100,255,0.05)");
        ctx.fillStyle=grd;ctx.fillRect(sx,sy,BS,BS);
        for(let sl=0;sl<8;sl++){const ly=sy+((sl*BS/8+t5*30)%BS);ctx.fillStyle="rgba(100,220,255,0.18)";ctx.fillRect(sx,ly,BS,3);}
        ctx.strokeStyle=`rgba(80,200,255,${0.6+Math.sin(t5*7)*.3})`;ctx.lineWidth=1.5;
        for(let i=0;i<3;i++){const y1=sy+BS*(i*.3+.1);ctx.beginPath();ctx.moveTo(sx+4,y1);ctx.lineTo(sx+BS*.3+Math.sin(t5*5+i)*6,y1+8);ctx.lineTo(sx+BS*.6+Math.cos(t5*4+i)*6,y1+4);ctx.lineTo(sx+BS-4,y1+10);ctx.stroke();}
        ctx.fillStyle="rgba(0,160,255,.6)";ctx.fillRect(sx,sy,4,BS);ctx.fillRect(sx+BS-4,sy,4,BS);
        ctx.fillStyle="rgba(150,230,255,.8)";ctx.fillRect(sx,sy,2,BS);ctx.fillRect(sx+BS-2,sy,2,BS);
        ctx.fillStyle=`rgba(0,160,255,${0.08+Math.sin(t5*3)*.04})`;ctx.fillRect(sx,sy,BS,BS);
      }
    }
    ctx.strokeStyle="rgba(0,0,0,.15)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle=type===B.BLAST_DOOR?"rgba(255,120,0,.8)":"rgba(100,220,255,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.FIREPLACE){
    const t6=Date.now()*.004;
    // Stone brick surround
    ctx.fillStyle="#555";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="#666";ctx.fillRect(sx+2,sy+2,BS-4,BS-4);
    // Dark firebox interior
    ctx.fillStyle="#1a1008";ctx.fillRect(sx+6,sy+6,BS-12,BS-8);
    // Animated fire (multiple layers)
    const fireH=BS-10;
    for(let fl=0;fl<3;fl++){
      const fph=fl/3;
      const fw=Math.sin(t6*3+fl*1.2)*.2;
      ctx.fillStyle=fl===0?"#ff8c00":fl===1?"#ffcc00":"rgba(255,255,200,.6)";
      ctx.beginPath();
      const fb=sy+BS-6,fby=sy+8+Math.floor(fireH*fph);
      ctx.moveTo(sx+6+fw*BS,fb);
      ctx.lineTo(sx+BS/2+Math.sin(t6*4+fl)*5,fby);
      ctx.lineTo(sx+BS-6-fw*BS,fb);
      ctx.closePath();ctx.fill();
    }
    // Glowing ember glow at base
    const grd=ctx.createRadialGradient(sx+BS/2,sy+BS-6,0,sx+BS/2,sy+BS-6,BS*.4);
    grd.addColorStop(0,"rgba(255,140,0,.35)");grd.addColorStop(1,"rgba(255,60,0,0)");
    ctx.fillStyle=grd;ctx.fillRect(sx,sy+BS*.4,BS,BS*.6);
    // Stone brick texture lines
    ctx.strokeStyle="rgba(0,0,0,.25)";ctx.lineWidth=1;
    ctx.strokeRect(sx+2,sy+2,BS-4,BS-4);
    ctx.beginPath();ctx.moveTo(sx+2,sy+BS/2);ctx.lineTo(sx+BS-2,sy+BS/2);ctx.stroke();
    ctx.beginPath();ctx.moveTo(sx+BS/2,sy+2);ctx.lineTo(sx+BS/2,sy+BS/2);ctx.stroke();
    if(hover){ctx.strokeStyle="rgba(255,200,100,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.CHIMNEY){
    // Outer stone brick walls
    ctx.fillStyle="#666";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="#777";ctx.fillRect(sx+1,sy+1,BS-2,BS-2);
    // Hollow dark interior (smoke channel)
    ctx.fillStyle="#1a1208";ctx.fillRect(sx+8,sy+2,BS-16,BS-2);
    // Stone brick texture
    ctx.strokeStyle="rgba(0,0,0,.3)";ctx.lineWidth=1;
    ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);
    ctx.beginPath();ctx.moveTo(sx+1,sy+BS/2);ctx.lineTo(sx+8,sy+BS/2);ctx.stroke();
    ctx.beginPath();ctx.moveTo(sx+BS-8,sy+BS/2);ctx.lineTo(sx+BS-1,sy+BS/2);ctx.stroke();
    ctx.beginPath();ctx.moveTo(sx+BS/2,sy+1);ctx.lineTo(sx+BS/2,sy+2);ctx.stroke();
    ctx.beginPath();ctx.moveTo(sx+BS/2,sy+BS-1);ctx.lineTo(sx+BS/2,sy+BS-2);ctx.stroke();
    // Soot marks on top rim
    ctx.fillStyle="rgba(0,0,0,.25)";ctx.fillRect(sx+8,sy+2,BS-16,4);
    // Glow from smoke inside (subtle orange tint at bottom)
    ctx.fillStyle="rgba(255,120,0,.06)";ctx.fillRect(sx+8,sy+BS-8,BS-16,6);
    ctx.strokeStyle="rgba(0,0,0,.15)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(200,180,140,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(DATA[type]?.isColorWall){
    const c=DATA[type].col;
    ctx.fillStyle=c;ctx.fillRect(sx,sy,BS,BS);
    // Subtle highlight on top-left for depth
    ctx.fillStyle="rgba(255,255,255,.18)";ctx.fillRect(sx,sy,BS,3);
    ctx.fillStyle="rgba(255,255,255,.1)";ctx.fillRect(sx,sy,3,BS);
    // Subtle shadow bottom-right
    ctx.fillStyle="rgba(0,0,0,.12)";ctx.fillRect(sx,sy+BS-3,BS,3);
    ctx.fillStyle="rgba(0,0,0,.08)";ctx.fillRect(sx+BS-3,sy,3,BS);
    ctx.strokeStyle="rgba(0,0,0,.1)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(255,255,255,.65)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.CASTLE_GATE){
    // Anchor block โ€” looks like a castle wall control mechanism
    ctx.fillStyle="#556";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="#667";ctx.fillRect(sx+2,sy+2,BS-4,BS-4);
    ctx.fillStyle="#445";ctx.fillRect(sx+4,sy+4,BS-8,BS-8);
    // Gear/crank icon
    ctx.fillStyle="#aab";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS/2,7,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="#778";ctx.beginPath();ctx.arc(sx+BS/2,sy+BS/2,4,0,Math.PI*2);ctx.fill();
    for(let i=0;i<4;i++){
      const a=i*Math.PI/2,r=10;
      ctx.fillStyle="#889";ctx.fillRect(sx+BS/2+Math.cos(a)*r-2,sy+BS/2+Math.sin(a)*r-2,4,4);
    }
    // "GATE" label
    ctx.fillStyle="rgba(200,210,255,.6)";ctx.font=`${BS*.14}px monospace`;ctx.textAlign="center";
    ctx.fillText("GATE",sx+BS/2,sy+BS*.9);ctx.textAlign="left";
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(180,200,255,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.SAPLING){
    // Tiny seedling
    ctx.fillStyle="#1a6e20";ctx.fillRect(sx+BS/2-1,sy+BS*.4,2,BS*.6); // stem
    ctx.fillStyle="#2da840";
    ctx.beginPath();ctx.arc(sx+BS/2,sy+BS*.35,BS*.18,0,Math.PI*2);ctx.fill(); // main leaf
    ctx.fillStyle="#44cc55";
    ctx.beginPath();ctx.arc(sx+BS/2-4,sy+BS*.45,BS*.1,0,Math.PI*2);ctx.fill(); // left leaf
    ctx.beginPath();ctx.arc(sx+BS/2+4,sy+BS*.45,BS*.1,0,Math.PI*2);ctx.fill(); // right leaf
    if(hover){ctx.strokeStyle="rgba(80,255,80,.7)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.CONV_R||type===B.CONV_L){
    const dir=type===B.CONV_R?1:-1;
    const spd=Date.now()*.003*dir; // animation offset
    // Base
    ctx.fillStyle="#444";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="#555";ctx.fillRect(sx,sy,BS,BS-4);
    // Animated belt stripes
    const stripeW=10,gap=14;
    ctx.save();ctx.beginPath();ctx.rect(sx+1,sy+1,BS-2,BS-2);ctx.clip();
    for(let i=-2;i<4;i++){
      const ox=((i*(stripeW+gap)+spd*60)%(stripeW+gap)*2+2*(stripeW+gap))%(stripeW+gap)*2 - (stripeW+gap);
      ctx.fillStyle="rgba(255,255,255,.12)";
      ctx.beginPath();
      ctx.moveTo(sx+ox,sy+BS-6);
      ctx.lineTo(sx+ox+stripeW,sy+BS-6);
      ctx.lineTo(sx+ox+stripeW-6,sy+2);
      ctx.lineTo(sx+ox-6,sy+2);
      ctx.closePath();ctx.fill();
    }
    ctx.restore();
    // Arrow
    ctx.fillStyle="rgba(255,200,50,.9)";
    ctx.font=`bold ${BS*.55}px sans-serif`;ctx.textAlign="center";
    ctx.fillText(dir>0?"โ†’":"โ†",sx+BS/2,sy+BS*.7);ctx.textAlign="left";
    // Edge highlight
    ctx.fillStyle="rgba(255,255,255,.1)";ctx.fillRect(sx,sy,BS,2);
    ctx.fillStyle="rgba(0,0,0,.3)";ctx.fillRect(sx,sy+BS-4,BS,4);
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    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.TRAPDOOR){
    if(open){
      // Open: thin vertical strip on the left edge
      ctx.fillStyle="#c8a440";ctx.fillRect(sx,sy,6,BS);
      ctx.fillStyle="rgba(255,255,255,.12)";ctx.fillRect(sx,sy,6,2);
      ctx.fillStyle="rgba(0,0,0,.2)";ctx.fillRect(sx+5,sy,1,BS);
      // Hinge dot
      ctx.fillStyle="#888";ctx.beginPath();ctx.arc(sx+3,sy+8,2.5,0,Math.PI*2);ctx.fill();
    }else{
      // Closed: horizontal plank filling bottom half of block
      ctx.fillStyle="#c8a440";ctx.fillRect(sx,sy+BS-10,BS,10);
      // Wood grain
      ctx.fillStyle="rgba(0,0,0,.15)";
      ctx.fillRect(sx,sy+BS-10,BS,1);
      [sx+BS*.25,sx+BS*.5,sx+BS*.75].forEach(lx=>{
        ctx.fillRect(lx,sy+BS-10,1,10);
      });
      ctx.fillStyle="rgba(255,255,255,.12)";ctx.fillRect(sx,sy+BS-10,BS,2);
      // Hinge
      ctx.fillStyle="#888";ctx.beginPath();ctx.arc(sx+8,sy+BS-5,2.5,0,Math.PI*2);ctx.fill();
      ctx.beginPath();ctx.arc(sx+BS-8,sy+BS-5,2.5,0,Math.PI*2);ctx.fill();
      // Handle
      ctx.fillStyle="#f0c030";ctx.fillRect(sx+BS/2-4,sy+BS-8,8,4);
    }
    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.GATE){
    const fc="#c8a440",fd="rgba(0,0,0,.22)";
    if(open){
      // Open gate: folded to the side - just two thin posts
      ctx.fillStyle=fc;ctx.fillRect(sx+2,sy,5,BS);ctx.fillRect(sx+BS-7,sy,5,BS);
      ctx.fillStyle=fd;ctx.fillRect(sx+6,sy,1,BS);ctx.fillRect(sx+BS-3,sy,1,BS);
    }else{
      // Closed gate: posts + horizontal rails (like fence but with latch)
      ctx.fillStyle=fc;ctx.fillRect(sx+2,sy,5,BS);ctx.fillRect(sx+BS-7,sy,5,BS);
      ctx.fillStyle=fc;ctx.fillRect(sx,sy+7,BS,5);ctx.fillRect(sx,sy+BS*0.56,BS,5);
      ctx.fillStyle=fd;ctx.fillRect(sx,sy+11,BS,1);ctx.fillRect(sx,sy+BS*0.56+4,BS,1);
      // Centre latch
      ctx.fillStyle="#f0c030";ctx.fillRect(sx+BS/2-3,sy+BS/2-4,6,8);
      ctx.fillStyle="rgba(0,0,0,.3)";ctx.fillRect(sx+BS/2-1,sy+BS/2-2,2,4);
    }
    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.FENCE){
    const fc="#c8a440",fd="rgba(0,0,0,.2)";
    // Left post
    ctx.fillStyle=fc;ctx.fillRect(sx+3,sy,5,BS);
    ctx.fillStyle=fd;ctx.fillRect(sx+7,sy,1,BS);
    // Right post
    ctx.fillStyle=fc;ctx.fillRect(sx+BS-8,sy,5,BS);
    ctx.fillStyle=fd;ctx.fillRect(sx+BS-4,sy,1,BS);
    // Top rail
    ctx.fillStyle=fc;ctx.fillRect(sx,sy+6,BS,5);
    ctx.fillStyle=fd;ctx.fillRect(sx,sy+10,BS,1);
    // Bottom rail
    ctx.fillStyle=fc;ctx.fillRect(sx,sy+BS*0.55,BS,5);
    ctx.fillStyle=fd;ctx.fillRect(sx,sy+BS*0.55+4,BS,1);
    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.SUPPORT){
    // Thin vertical beam - drawn as a centered strip
    ctx.fillStyle="#8B6510";ctx.fillRect(sx+BS*.35,sy,BS*.3,BS);
    ctx.fillStyle="rgba(255,255,255,.12)";ctx.fillRect(sx+BS*.35,sy,BS*.06,BS);
    ctx.fillStyle="rgba(0,0,0,.2)";ctx.fillRect(sx+BS*.59,sy,BS*.06,BS);
    // Wood grain lines
    ctx.strokeStyle="rgba(0,0,0,.15)";ctx.lineWidth=1;
    ctx.beginPath();ctx.moveTo(sx+BS*.42,sy+BS*.2);ctx.lineTo(sx+BS*.42,sy+BS*.8);ctx.stroke();
    ctx.beginPath();ctx.moveTo(sx+BS*.55,sy+BS*.15);ctx.lineTo(sx+BS*.55,sy+BS*.85);ctx.stroke();
    return;
  }
  if(type===B.END_STONE){
    ctx.fillStyle="#d4d48a";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="rgba(0,0,0,.08)";
    [[4,6,8,8],[18,14,10,6],[6,24,12,8]].forEach(([dx,dy,w,h])=>ctx.fillRect(sx+dx,sy+dy,w,h));
    ctx.strokeStyle="rgba(0,0,0,.09)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    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.OBSIDIAN){
    ctx.fillStyle="#1a0a2e";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="rgba(120,80,200,.2)";ctx.fillRect(sx,sy,BS,BS);
    ctx.fillStyle="rgba(255,255,255,.04)";ctx.fillRect(sx,sy,BS,2);ctx.fillRect(sx,sy,2,BS);
    ctx.strokeStyle="rgba(80,40,120,.3)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    if(hover){ctx.strokeStyle="rgba(180,100,255,.6)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.END_PORTAL){
    // Animated swirling portal
    const t2=Date.now()*.001;
    ctx.fillStyle="#0a0020";ctx.fillRect(sx,sy,BS,BS);
    for(let i=0;i<3;i++){
      const a=t2*2+i*2.1,r=BS*.28+i*4;
      ctx.fillStyle=`rgba(${80+i*40},${20+i*60},${200+i*20},.6)`;
      ctx.beginPath();ctx.arc(sx+BS/2+Math.cos(a)*r*.5,sy+BS/2+Math.sin(a)*r*.5,6-i,0,Math.PI*2);ctx.fill();
    }
    ctx.strokeStyle="rgba(120,60,255,.8)";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);
    if(hover){ctx.strokeStyle="#fff";ctx.lineWidth=2;ctx.strokeRect(sx+1,sy+1,BS-2,BS-2);}
    return;
  }
  if(type===B.STONE_BRICK){
    ctx.fillStyle="#888";ctx.fillRect(sx,sy,BS,BS);
    ctx.strokeStyle="rgba(0,0,0,.3)";ctx.lineWidth=1.5;
    ctx.strokeRect(sx+2,sy+2,BS/2-3,BS/2-3);ctx.strokeRect(sx+BS/2+1,sy+2,BS/2-3,BS/2-3);
    ctx.strokeRect(sx+2,sy+BS/2+1,BS/2-3,BS/2-3);ctx.strokeRect(sx+BS/2+1,sy+BS/2+1,BS/2-3,BS/2-3);
    ctx.fillStyle="rgba(255,255,255,.06)";ctx.fillRect(sx,sy,BS,2);ctx.fillRect(sx,sy,2,BS);
    ctx.fillStyle="rgba(0,0,0,.15)";ctx.fillRect(sx,sy+BS-2,BS,2);ctx.fillRect(sx+BS-2,sy,2,BS);
    ctx.strokeStyle="rgba(0,0,0,.08)";ctx.lineWidth=.5;ctx.strokeRect(sx,sy,BS,BS);
    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.CHEST){
    const ck=`${Math.floor((sx+G.camera.x)/BS)},${Math.floor((sy+G.camera.y)/BS)}`;const open=G?.chestContents?.[ck]&&G.chestContents[ck].some(s=>!s.count);
    ctx.fillStyle="#c8901a";ctx.fillRect(sx+2,sy+4,BS-4,BS-6);
    ctx.fillStyle="#a06010";ctx.fillRect(sx+2,sy+4,BS-4,8);
    ctx.fillStyle="#7a4a0a";ctx.fillRect(sx+2,sy+4,BS-4,2);
    // Latch
    ctx.fillStyle=open?"#555":"#f0c030";ctx.fillRect(sx+BS/2-4,sy+10,8,6);
    // Planks texture
    ctx.strokeStyle="rgba(0,0,0,.2)";ctx.lineWidth=1;
    ctx.beginPath();ctx.moveTo(sx+2,sy+BS/2);ctx.lineTo(sx+BS-2,sy+BS/2);ctx.stroke();
    ctx.fillStyle="rgba(255,255,255,.08)";ctx.fillRect(sx+2,sy+4,BS-4,2);
    if(open){ctx.fillStyle="rgba(0,0,0,.3)";ctx.fillRect(sx+4,sy+6,BS-8,BS-10);}
    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.TNT){
    // Red body
    ctx.fillStyle="#cc2222";ctx.fillRect(sx+1,sy+1,BS-2,BS-2);
    // Top & bottom stripes
    ctx.fillStyle="#e8e8e8";ctx.fillRect(sx+1,sy+1,BS-2,7);ctx.fillRect(sx+1,sy+BS-8,BS-2,7);
    // TNT text area
    ctx.fillStyle="#cc2222";ctx.fillRect(sx+2,sy+9,BS-4,BS-18);
    ctx.font=`bold ${BS*.28}px monospace`;ctx.fillStyle="#fff";
    ctx.textAlign="center";ctx.fillText("TNT",sx+BS/2,sy+BS*.58);ctx.textAlign="left";
    // Fuse dot on top
    ctx.fillStyle="#333";ctx.beginPath();ctx.arc(sx+BS/2,sy+3,2,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="rgba(255,255,255,.1)";ctx.fillRect(sx+1,sy+1,BS-2,2);ctx.fillRect(sx+1,sy+1,2,BS-2);
    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.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);
}

function drawFish(ctx,fx,fy,dir,fishType,phase){
  const cols=[
    ["#ff7030","#ffb888","#cc3300"], // orange
    ["#2288cc","#66aaee","#1155aa"], // blue
    ["#ffcc20","#ffee90","#bb9900"], // yellow
    ["#cc44aa","#ee88cc","#881188"], // pink
  ];
  const[body,belly,fin]=cols[fishType%4];
  const bob=Math.sin(Date.now()*.002+phase)*3;
  ctx.save();
  ctx.translate(Math.round(fx),Math.round(fy+bob));
  if(dir<0)ctx.scale(-1,1);
  // Tail fin
  ctx.fillStyle=fin;
  ctx.beginPath();ctx.moveTo(-10,0);ctx.lineTo(-18,-7);ctx.lineTo(-18,7);ctx.closePath();ctx.fill();
  // Body ellipse
  ctx.fillStyle=body;
  ctx.beginPath();ctx.ellipse(1,0,12,6,0,0,Math.PI*2);ctx.fill();
  // Belly lighter
  ctx.fillStyle=belly;
  ctx.beginPath();ctx.ellipse(2,2,8,3.5,-0.1,0,Math.PI*2);ctx.fill();
  // Dorsal fin
  ctx.fillStyle=fin;
  ctx.beginPath();ctx.moveTo(-1,-6);ctx.lineTo(5,-12);ctx.lineTo(9,-6);ctx.closePath();ctx.fill();
  // Eye
  ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(7,-1,2.5,0,Math.PI*2);ctx.fill();
  ctx.fillStyle="#111";ctx.beginPath();ctx.arc(7.5,-1,1.2,0,Math.PI*2);ctx.fill();
  ctx.restore();
}

function drawSkeleton(ctx,sx,sy,dir,flash,hp,bowCD){
  if(hp<=0)return;
  if(flash){ctx.save();ctx.globalAlpha=.5;}
  const bc=flash?"#ffcccc":"#e8e8d8"; // bone colour
  // Shadow
  ctx.fillStyle="rgba(0,0,0,.18)";ctx.beginPath();ctx.ellipse(sx,sy+2,SKW*.5,3,0,0,Math.PI*2);ctx.fill();
  // Legs
  ctx.fillStyle=bc;
  ctx.fillRect(sx-SKW*.38,sy-SKH*.46,5,SKH*.46);
  ctx.fillRect(sx+SKW*.1,sy-SKH*.46,5,SKH*.46);
  // Pelvis
  ctx.fillRect(sx-SKW*.42,sy-SKH*.5,SKW*.84,5);
  // Spine
  ctx.fillRect(sx-2,sy-SKH*.9,4,SKH*.42);
  // Ribs (3 pairs)
  for(let r=0;r<3;r++){ctx.fillRect(sx-SKW*.44,sy-SKH*.85+r*7,SKW*.88,3);}
  // Arms - bow arm extends further when shooting
  const shooting=bowCD<25;
  const armExt=shooting?SKH*.42:SKH*.32;
  ctx.fillRect(dir>0?sx+SKW*.2:sx-SKW*.5,sy-SKH*.88,4,armExt); // bow arm
  ctx.fillRect(dir>0?sx-SKW*.5:sx+SKW*.2,sy-SKH*.88,4,SKH*.28); // off arm
  // Skull
  ctx.fillStyle=bc;ctx.fillRect(sx-SKW*.38,sy-SKH,SKW*.76,SKH*.36);
  // Eye sockets
  ctx.fillStyle=shooting?"#ff4400":"#333";
  ctx.fillRect(sx+(dir>0?-SKW*.3:SKW*.05),sy-SKH+SKH*.08,7,6);
  ctx.fillRect(sx+(dir>0?SKW*.05:-SKW*.3),sy-SKH+SKH*.08,7,6);
  // Jaw gap
  ctx.fillStyle="#555";ctx.fillRect(sx-SKW*.28,sy-SKH+SKH*.28,SKW*.56,4);
  // Bow
  const bx2=dir>0?sx+SKW*.45:sx-SKW*.65,bowy=sy-SKH*.7;
  ctx.strokeStyle=shooting?"#c8a060":"#906830";ctx.lineWidth=2.5;
  ctx.beginPath();ctx.arc(bx2,bowy,10,dir>0?-1.9:1.2,dir>0?1.9:Math.PI+1.9);ctx.stroke();
  // Bowstring โ€” pulled back when shooting
  const pull=shooting?6:1;
  ctx.strokeStyle="#e8e0c0";ctx.lineWidth=1;
  ctx.beginPath();
  ctx.moveTo(bx2+(dir>0?-10:10),bowy-10);
  ctx.lineTo(bx2+(dir>0?pull:-pull),bowy);
  ctx.lineTo(bx2+(dir>0?-10:10),bowy+10);
  ctx.stroke();
  // Arrow on bow when charged
  if(shooting){
    ctx.strokeStyle="#c8a060";ctx.lineWidth=2;
    ctx.beginPath();ctx.moveTo(bx2+(dir>0?-10:10),bowy);ctx.lineTo(bx2+(dir>0?14:-14),bowy);ctx.stroke();
  }
  if(flash)ctx.restore();
  // Fire
  if(hp<SKELETON_HP){ctx.font='14px serif';ctx.fillText(hp<=1?'๐Ÿ’€':'',sx-8,sy-SKH-10);}
  // HP bar
  const bw=26;
  ctx.fillStyle="rgba(0,0,0,.5)";ctx.fillRect(sx-bw/2,sy-SKH-8,bw,4);
  ctx.fillStyle=`hsl(${(hp/SKELETON_HP)*120},90%,45%)`;ctx.fillRect(sx-bw/2,sy-SKH-8,bw*(hp/SKELETON_HP),4);
}

function drawJet(ctx,jx,jy,dir,vx,vy,jetHP,shootFlash){
  const angle=Math.atan2(vy,Math.abs(vx))*0.6*(dir>0?1:-1); // slight pitch
  ctx.save();
  ctx.translate(Math.round(jx),Math.round(jy));
  ctx.rotate(angle);
  if(dir<0)ctx.scale(-1,1);
  // Engine exhaust glow
  const spd=Math.abs(vx);
  if(spd>2||shootFlash){
    const grd=ctx.createRadialGradient(-38,0,0,-38,0,18);
    grd.addColorStop(0,"rgba(255,140,0,.9)");
    grd.addColorStop(0.5,"rgba(255,60,0,.5)");
    grd.addColorStop(1,"rgba(255,0,0,0)");
    ctx.fillStyle=grd;ctx.beginPath();ctx.ellipse(-38,0,16+spd,8,0,0,Math.PI*2);ctx.fill();
  }
  // Fuselage
  ctx.fillStyle="#667788";
  ctx.beginPath();ctx.moveTo(42,-5);ctx.lineTo(-30,-9);ctx.lineTo(-38,0);ctx.lineTo(-30,9);ctx.lineTo(42,5);ctx.closePath();ctx.fill();
  ctx.fillStyle="#778899";ctx.beginPath();ctx.moveTo(42,-5);ctx.lineTo(10,-9);ctx.lineTo(-10,-7);ctx.lineTo(10,5);ctx.lineTo(42,5);ctx.closePath();ctx.fill();
  // Wing (swept back)
  ctx.fillStyle="#556677";
  ctx.beginPath();ctx.moveTo(8,-9);ctx.lineTo(-18,-34);ctx.lineTo(-28,-34);ctx.lineTo(-22,-9);ctx.closePath();ctx.fill();
  ctx.fillStyle="#667788";ctx.beginPath();ctx.moveTo(8,-9);ctx.lineTo(-18,-34);ctx.lineTo(-16,-34);ctx.lineTo(-20,-9);ctx.closePath();ctx.fill();
  // Tail fin
  ctx.fillStyle="#556677";
  ctx.beginPath();ctx.moveTo(-26,-9);ctx.lineTo(-36,-22);ctx.lineTo(-38,-22);ctx.lineTo(-32,-9);ctx.closePath();ctx.fill();
  // Belly wing
  ctx.fillStyle="#445566";
  ctx.beginPath();ctx.moveTo(8,9);ctx.lineTo(-18,22);ctx.lineTo(-22,9);ctx.closePath();ctx.fill();
  // Missile pods
  ctx.fillStyle="#334455";ctx.fillRect(-20,-38,16,6);ctx.fillRect(-20,32,16,6);
  ctx.fillStyle="#ff4400";ctx.fillRect(-4,-38,5,6);ctx.fillRect(-4,32,5,6);
  // Cockpit
  ctx.fillStyle="#88ccff";ctx.beginPath();ctx.ellipse(16,-4,12,7,-.1,0,Math.PI*2);ctx.fill();
  ctx.fillStyle="rgba(200,240,255,.4)";ctx.beginPath();ctx.ellipse(16,-6,8,4,-.1,0,Math.PI*2);ctx.fill();
  // Pilot head
  ctx.fillStyle="#ffcc88";ctx.beginPath();ctx.arc(16,-4,5,0,Math.PI*2);ctx.fill();
  ctx.fillStyle="#334455";ctx.fillRect(12,-9,8,5); // helmet
  // Gun flash
  if(shootFlash){
    ctx.fillStyle="rgba(255,255,100,.9)";ctx.beginPath();ctx.arc(46,0,7,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(46,0,3,0,Math.PI*2);ctx.fill();
  }
  // HP bar (only when damaged)
  if(jetHP<20){
    ctx.fillStyle="rgba(0,0,0,.5)";ctx.fillRect(-30,-46,60,6);
    ctx.fillStyle=`hsl(${(jetHP/20)*120},90%,45%)`;ctx.fillRect(-30,-46,60*(jetHP/20),6);
  }
  ctx.restore();
}

function drawVillager(ctx,vx,vy,dir,type,flash){
  const robes=[["#8B4513","#c8860a"],["#556","#889"],["#6a0dad","#aa44cc"]]; // farmer, blacksmith, priest
  const [robe,accent]=robes[type%3];
  if(flash){ctx.save();ctx.globalAlpha=.5;}
  // Shadow
  ctx.fillStyle="rgba(0,0,0,.18)";ctx.beginPath();ctx.ellipse(vx,vy+2,10,3,0,0,Math.PI*2);ctx.fill();
  // Robe body
  ctx.fillStyle=robe;ctx.fillRect(vx-7,vy-22,14,16);
  // Robe bottom (wider)
  ctx.beginPath();ctx.moveTo(vx-9,vy-6);ctx.lineTo(vx+9,vy-6);ctx.lineTo(vx+7,vy+2);ctx.lineTo(vx-7,vy+2);ctx.closePath();ctx.fill();
  // Arms
  ctx.fillStyle="#ffcc88";
  ctx.fillRect(dir>0?vx+7:vx-11,vy-22,4,12);
  ctx.fillRect(dir>0?vx-11:vx+7,vy-22,4,12);
  // Accent trim on robe
  ctx.fillStyle=accent;ctx.fillRect(vx-7,vy-22,14,3);
  ctx.fillRect(vx-2,vy-20,4,14);
  // Head
  ctx.fillStyle="#ffcc88";ctx.fillRect(vx-6,vy-34,12,12);
  // Hat / hair by type
  if(type%3===0){ctx.fillStyle="#c8860a";ctx.fillRect(vx-7,vy-35,14,3);ctx.fillRect(vx-4,vy-38,8,4);} // straw hat
  else if(type%3===1){ctx.fillStyle="#333";ctx.fillRect(vx-6,vy-36,12,4);} // dark hair
  else{ctx.fillStyle="#5500aa";ctx.fillRect(vx-7,vy-36,14,5);} // purple hood
  // Eyes
  ctx.fillStyle="#333";
  ctx.fillRect(dir>0?vx-3:vx-1,vy-28,2,2);
  ctx.fillRect(dir>0?vx+1:vx-3,vy-28,2,2);
  // Smile
  ctx.fillStyle="#cc8866";ctx.fillRect(vx-2,vy-24,4,1);
  if(flash)ctx.restore();
}
// โ”€โ”€ HUD draw on canvas โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function drawHUD(ctx,W,H,g){
  const p=g.player,isNight=g.dayTime>=DAY_LEN;
  // Creative badge
  if(g.creative){ctx.font="bold 12px monospace";ctx.fillStyle="#ffe066";ctx.textAlign="right";ctx.fillText("๐ŸŒŸ CREATIVE",W-10,36);ctx.textAlign="left";}
  // 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||"")+(eq.enchant==="silk_touch"?" โœจ Silk Touch":"");
    ctx.font="11px monospace";ctx.fillStyle="#fff";
    const nw=ctx.measureText(nm).width;
    ctx.fillStyle=eq.enchant?"rgba(80,0,120,.75)":"rgba(0,0,0,.55)";roundRect(ctx,(W-nw-20)/2,H-100,nw+20,18,5);ctx.fill();
    ctx.fillStyle=eq.enchant?"#ddaaff":"#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();
}

// โ”€โ”€ Fish โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function spawnFishFromWorld(){
  if(!G)return;
  G.fish=[];
  // Find surface water blocks (water with air above) and spawn fish
  for(let bx=1;bx<WW-1;bx++){
    for(let by=1;by<WH-1;by++){
      if(G.world[by][bx]===B.WATER&&G.world[by+1][bx]===B.WATER&&(!G.world[by-1][bx]||G.world[by-1][bx]===B.AIR)){
        // This is a subsurface water block โ€” good swimming depth
        if(Math.random()<0.35){
          const spd=0.4+Math.random()*0.7;
          G.fish.push({
            x:(bx+0.5)*BS,y:(by+0.5)*BS,
            vx:(Math.random()>0.5?1:-1)*spd,
            dir:1,type:Math.floor(Math.random()*4),
            phase:Math.random()*Math.PI*2,
            by, // water row the fish swims in
          });
        }
      }
    }
  }
}

// โ”€โ”€ 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,down:false};

function fillCreativeInventory(){
  // Fill hotbar with placeable blocks
  const placeables=[B.DIRT,B.STONE,B.WOOD,B.PLANK,B.GLASS,B.SAND,B.STONE_BRICK,B.OBSIDIAN];
  G.hotbar=placeables.map(b=>({block:b,count:9999}));
  // ALL creative items โ€” no 32-slot cap
  const allItems=[
    // Weapons & gadgets
    B.PISTOL,B.MACHINE_GUN,B.FLAMETHROWER,B.ROCKET_LAUNCHER,B.TASER,B.BOMB,B.JET,B.BULLET,
    // Tools
    B.BOW,B.ARROW,B.DM_SWORD,B.DM_AXE,B.DM_PICK,B.IRON_PICK,B.STONE_PICK,B.IRON_SWORD,B.FLINT_STEEL,
    // Armor
    B.DM_HELM,B.DM_CHEST,B.DM_BOOT,B.IR_HELM,B.IR_CHEST,B.IR_BOOT,
    // Food
    B.BREAD,B.APPLE,B.MEAT,B.STEW,
    // Materials
    B.LEAVES,B.COAL,B.IRON,B.REDSTONE,B.EMERALD,B.DIAMOND,B.STICK,B.PLANK,B.WOOD,B.STONE,B.DIRT,B.GRASS,B.SAND,B.GRAVEL,B.GLASS,B.STONE_BRICK,B.OBSIDIAN,B.GUNPOWDER,
    // Structures & utility
    B.TORCH,B.DOOR,B.BLAST_DOOR,B.HOLO_DOOR,B.CASTLE_GATE,B.TENT_KIT,B.FIREPLACE,B.CHIMNEY,B.BED,B.CHEST,B.DISPENSER,B.FENCE,B.GATE,B.TRAPDOOR,B.LADDER,
    // Redstone & machines
    B.TNT,B.PISTON,B.STICKY_PISTON,B.CONV_R,B.CONV_L,B.TURRET,B.SPIKE,B.FAN,B.ELEVATOR,
    // Special
    B.ROCKET_SHIP,B.MOON_ROCK,B.MOON_DUST,B.LAVA,B.WATER,B.BARRIER,B.EGG_ZOMBIE,B.EGG_CREEPER,B.WHITE_WALL,B.LIGHT_GRAY_WALL,B.GRAY_WALL,B.BLACK_WALL,B.RED_WALL,B.ORANGE_WALL,B.YELLOW_WALL,B.LIME_WALL,B.GREEN_WALL,B.CYAN_WALL,B.LIGHT_BLUE_WALL,B.BLUE_WALL,B.PURPLE_WALL,B.MAGENTA_WALL,B.PINK_WALL,B.BROWN_WALL,
    B.SPACESUIT,B.SPACE_ROCK,B.JETPACK,
    B.END_PORTAL,B.EYE_END,B.END_STONE,B.DRAGON_EGG,
  ];
  G.inventory=allItems.map(b=>({block:b,count:9999}));
}
function genFlatWorld(){
  const world=Array.from({length:WH},()=>new Uint8Array(WW));
  const flatY=Math.floor(WH*0.6); // surface level
  const surf=Array(WW).fill(flatY);
  for(let x=0;x<WW;x++){
    world[flatY][x]=B.GRASS;                            // grass on top
    for(let y=flatY+1;y<flatY+4;y++) world[y][x]=B.DIRT; // 3 dirt layers
    for(let y=flatY+4;y<WH-1;y++)  world[y][x]=B.STONE; // stone below
    world[WH-1][x]=B.BEDROCK;                           // bedrock at bottom
  }
  return{world,surf};
}
function initGame(){
  const seed=Date.now()%100000;
  const{world,surf,ponds,villageSpawns=[]}=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,creepers:[],creeperTimer:0,skeletons:[],skeletonTimer:0,arrows:[],dispenserTick:0,dispenserArrows:{},chestContents:{},playerPlacedChests:new Set(),litTNT:[],litBombs:[],creative:false,flying:false,noclip:true,lastDispenserTap:null,pistonStates:{},turretStates:{},elevatorStates:{},drawbridgeStates:{},sleeping:false,sleepTimer:0,sleepMsg:"",
    dim:"over",endWorld:null,endSurf:null,overworldPos:null,
    dragon:null,endFireballs:[],dragonDefeated:false,fish:[],drops:[],jetting:false,jetHP:0,jetShootCD:0,
    moonWorld:null,moonSurf:null,preMoonPos:null,moonLaunch:null,villagers:[],noMobs:false,smoke:[],bpMode:false,bpCorner1:null,bpCorner2:null,lastKit:null,spaceWorld:null,spaceSurf:null,preSpacePos:null,oxygenT:0,saplingTimers:{},
  };
  document.getElementById('dead').classList.remove('show');
  G.villagers=villageSpawns.map(v=>({x:v.x,y:v.y,vx:0,vy:0,onGround:true,dir:1,type:v.type,homeX:v.homeX,wanderT:0,wanderDir:1,runT:0}));
  spawnFishFromWorld();
  if(G.creative)fillCreativeInventory();
  updateModeBtn();
  buildRecipeUI();
}

// โ”€โ”€ Solid / Physics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function solid(l,t,w,h){
  if(G?.creative&&G?.noclip)return false;
  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||blk===B.GATE||blk===B.TRAPDOOR||blk===B.BLAST_DOOR||blk===B.HOLO_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 hasLOS(x1,y1,x2,y2){
  // Bresenham ray - returns true if no solid block between two world points
  let bx1=Math.floor(x1/BS),by1=Math.floor(y1/BS);
  const bx2=Math.floor(x2/BS),by2=Math.floor(y2/BS);
  const dx=Math.abs(bx2-bx1),dy=Math.abs(by2-by1);
  const sx=bx1<bx2?1:-1,sy=by1<by2?1:-1;
  let err=dx-dy;
  while(bx1!==bx2||by1!==by2){
    if(bx1>=0&&bx1<WW&&by1>=0&&by1<WH){
      const blk=G.world[by1][bx1];
      if(blk&&!DATA[blk]?.pass&&blk!==B.DOOR)return false;
    }
    const e2=err*2;
    if(e2>-dy){err-=dy;bx1+=sx;}
    if(e2<dx){err+=dx;by1+=sy;}
  }
  return true;
}
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||blk===B.GATE||blk===B.TRAPDOOR||blk===B.BLAST_DOOR||blk===B.HOLO_DOOR)&&G.openDoors.has(`${bx},${by}`))continue;
      return true;
    }
  }
  return false;
}
function tryMine(bx,by){
  if(G?.bpMode){blueprintTap(bx,by);return;}
  if(bx<0||bx>=WW||by<0||by>=WH||!G.world[by][bx])return;
  if(G.world[by][bx]===B.BARRIER&&!G.creative)return; // can't mine barriers in survival
  G.mining={bx,by,t:0,max:G.creative?0:mineTime(G.world[by][bx],G.hotbar[G.slot])};
}
function tryEat(){
  if(!G)return;
  const fsl=G.hotbar[G.slot];
  if(!fsl?.count||!DATA[fsl.block]?.food){G.sleepMsg="No food in hand!";setTimeout(()=>G.sleepMsg="",1500);return;}
  if(G.player.hp>=MAX_HP){G.sleepMsg="Already at full health!";setTimeout(()=>G.sleepMsg="",1500);return;}
  const heal=DATA[fsl.block].food;
  const fname=DATA[fsl.block].name;
  G.player.hp=Math.min(MAX_HP,G.player.hp+heal);
  if(!G.creative){fsl.count--;if(!fsl.count)fsl.block=B.AIR;}
  G.sleepMsg=`+${heal} โค๏ธ ${fname}`;setTimeout(()=>G.sleepMsg="",1500);
}
function tryPlace(bx,by){
  // Spawn egg usage
  const eggSl=G.hotbar[G.slot];
  const eggType=eggSl?.count>0?DATA[eggSl.block]?.spawnEgg:null;
  if(eggType){
    const wx=(bx+.5)*BS,wy=by*BS;
    if(eggType==="zombie"){
      G.zombies.push({x:wx,y:wy,vx:0,vy:0,onGround:false,hp:ZOMBIE_HP,flash:0,dir:1,dmgT:0});
      if(!G.creative){eggSl.count--;if(!eggSl.count)eggSl.block=B.AIR;}
      G.sleepMsg="๐ŸงŸ Zombie spawned!";setTimeout(()=>G.sleepMsg="",1200);
    } else if(eggType==="creeper"){
      G.creepers.push({x:wx,y:wy,vx:0,vy:0,onGround:false,hp:CREEPER_HP,fuse:0,flash:0,dir:1,dmgT:0,burnT:0,onFire:false});
      if(!G.creative){eggSl.count--;if(!eggSl.count)eggSl.block=B.AIR;}
      G.sleepMsg="๐Ÿ’š Creeper spawned!";setTimeout(()=>G.sleepMsg="",1200);
    }
    return;
  }
  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||blk===B.BLAST_DOOR||blk===B.HOLO_DOOR){G.openDoors.has(k)?G.openDoors.delete(k):G.openDoors.add(k);}
    else if(blk===B.DISPENSER){
      const sl=G.hotbar[G.slot];
      const now=Date.now();
      const lastTap=G.lastDispenserTap;
      const isDoubleTap=lastTap&&lastTap.k===k&&(now-lastTap.t)<400;
      G.lastDispenserTap={k,t:now};
      if(sl?.count>0&&sl.block===B.ARROW){
        // Creative double-tap: load ALL arrows at once
        const loadAmt=(G.creative&&isDoubleTap)?sl.count:1;
        G.dispenserArrows[k]=(G.dispenserArrows[k]||0)+loadAmt;
        if(!G.creative){sl.count-=loadAmt;if(!sl.count)sl.block=B.AIR;}
        const t=G.dispenserArrows[k];
        G.sleepMsg=loadAmt>1?`โžœ Loaded all ${loadAmt} arrows! (${t} total)`:`โžœ ${t} arrow${t!==1?"s":""}  (tap empty-handed to remove)`;
        setTimeout(()=>G.sleepMsg="",1500);
      } else {
        // Remove ONE arrow (or ALL in creative double-tap)
        const stored=G.dispenserArrows[k]||0;
        if(stored>0){
          const removeAmt=(G.creative&&isDoubleTap)?stored:1;
          G.dispenserArrows[k]=stored-removeAmt;
          if(!G.creative)addItem(G.hotbar,G.inventory,B.ARROW,removeAmt);
          const left=G.dispenserArrows[k];
          G.sleepMsg=removeAmt>1?`Removed all ${removeAmt} arrows`: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.CHEST){
      // Init chest contents if first time
      if(!G.chestContents[k]){
        G.chestContents[k]=Array.from({length:18},()=>empty());
        // Pre-placed dungeon chests always get loot; player-crafted chests start empty
        if(!G.playerPlacedChests||!G.playerPlacedChests.has(k)){
          const lootPool=[
            {block:B.IRON,count:5},{block:B.DIAMOND,count:2},
            {block:B.EMERALD,count:3},{block:B.REDSTONE,count:8},
            {block:B.ARROW,count:12},{block:B.TORCH,count:6},
            {block:B.IRON_PICK,count:1},{block:B.IRON_SWORD,count:1},
            {block:B.COAL,count:10},{block:B.FLINT,count:3},
          ];
          // Detect mineshaft chests (plank floor below) vs dungeon chests
          const isMineshaft=by+1<WH&&G.world[by+1]?.[bx]===B.PLANK;
          if(isMineshaft){
            // Mineshaft loot: lots of food + basic supplies
            const foodPool=[{block:B.BREAD,count:3},{block:B.APPLE,count:2},{block:B.MEAT,count:2},{block:B.STEW,count:1},{block:B.BREAD,count:4}];
            const extraPool=[{block:B.TORCH,count:8},{block:B.COAL,count:6},{block:B.IRON,count:3},{block:B.ARROW,count:8},{block:B.STICK,count:10}];
            [0,1,2].forEach(si=>{const it=foodPool[Math.floor(Math.random()*foodPool.length)];G.chestContents[k][si]={block:it.block,count:it.count};});
            [4,6,8].forEach(si=>{const it=extraPool[Math.floor(Math.random()*extraPool.length)];G.chestContents[k][si]={block:it.block,count:it.count};});
          } else {
          // Dungeon chest: silk touch + rare loot
          G.chestContents[k][0]={block:B.IRON_PICK,count:1,enchant:"silk_touch"};
          [2,4,7,10,14,16].forEach(si=>{
            const it=lootPool[Math.floor(Math.random()*lootPool.length)];
            G.chestContents[k][si]={block:it.block,count:it.count};
          });
          }
        }
      }
      openChest(k);
    }
    else if(blk===B.END_PORTAL){
      if(G.dim==="over") enterEnd();
      else leaveEnd();
    }
    else if(blk===B.TNT){
      const heldSl=G.hotbar[G.slot];
      if(heldSl?.count>0&&DATA[heldSl.block]?.isFlintSteel){
        G.world[by][bx]=B.AIR;
        G.litTNT.push({x:(bx+.5)*BS,y:by*BS,vx:0,vy:0,fuse:90});
      } else {
        G.sleepMsg="Equip Flint & Steel to ignite TNT!";
        setTimeout(()=>G.sleepMsg="",2000);
      }
    }
    else if(blk===B.BOMB){
      const heldSl=G.hotbar[G.slot];
      if(heldSl?.count>0&&(DATA[heldSl.block]?.isFlintSteel||heldSl.block===B.FLINT)){
        G.world[by][bx]=B.AIR;
        G.litBombs=G.litBombs||[];
        G.litBombs.push({x:(bx+.5)*BS,y:by*BS,vx:0,vy:0,fuse:150});
        G.sleepMsg="๐Ÿ’ฃ BOMB lit โ€” RUN!";setTimeout(()=>G.sleepMsg="",2000);
      }else{
        G.sleepMsg="Use Flint & Steel to light the bomb!";setTimeout(()=>G.sleepMsg="",2000);
      }
    }
    else if(blk===B.CASTLE_GATE){toggleDrawbridge(bx,by);return;}
    else if(blk===B.ROCKET_SHIP){
      if(G.dim==="space"){leaveSpace();return;}
      if(G.dim==="moon"){leaveMoon();return;}
      // In overworld: tap once to go to moon, tap again to go to space
      if(!G.moonLaunch){
        G.moonLaunch={timer:180,dest:"moon"};
        G.sleepMsg="๐Ÿš€ Launching to Moon in 3s... tap again for Space!";setTimeout(()=>G.sleepMsg="",2500);
      }else if(G.moonLaunch.dest==="moon"){
        G.moonLaunch={timer:180,dest:"space"};
        G.sleepMsg="๐ŸŒŒ Switching to Space launch!";setTimeout(()=>G.sleepMsg="",2000);
      }else{
        G.moonLaunch=null;G.sleepMsg="Launch cancelled.";setTimeout(()=>G.sleepMsg="",1500);
      }
      return;
    }
    else if(blk===B.ELEVATOR){
      G.elevatorStates=G.elevatorStates||{};
      const es=G.elevatorStates[k];
      if(es){es.vy=-es.vy;G.sleepMsg="Elevator reversed";setTimeout(()=>G.sleepMsg="",1000);}
      return;
    }
    else if(blk===B.TURRET){
      const sl=G.hotbar[G.slot];
      G.turretStates=G.turretStates||{};
      if(sl?.count>0&&DATA[sl.block]?.isGun){
        G.turretStates[k]={gunType:DATA[sl.block].gunType,cd:0,angle:0};
        G.sleepMsg=`๐Ÿ”ซ Loaded: ${DATA[sl.block].name}`;setTimeout(()=>G.sleepMsg="",1500);
      }else{
        const loaded=G.turretStates[k]?.gunType;
        G.turretStates[k]=null;
        G.sleepMsg=loaded?"Turret unloaded":"Tap with a gun to load";setTimeout(()=>G.sleepMsg="",1500);
      }
      return;
    }
    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&&sl.block!==B.TENT_KIT&&sl.block!==B.KIT)return;
  // Place Blueprint Kit
  if(sl.block===B.KIT&&sl.kitBlocks){
    sl.kitBlocks.forEach(({dx,dy,block})=>{
      const tx=bx+dx,ty=by+dy;
      if(tx>=0&&tx<WW&&ty>=0&&ty<WH&&G.world[ty][tx]!==B.BEDROCK)G.world[ty][tx]=block;
    });
    sl.count--;if(!sl.count){sl.block=B.AIR;delete sl.kitBlocks;delete sl.kitW;delete sl.kitH;}
    G.sleepMsg="๐Ÿ“ Kit placed!";setTimeout(()=>G.sleepMsg="",1200);
    return;
  }
  // Special: Tent Kit builds a full shelter
  if(sl.block===B.TENT_KIT){
    // If tapping an existing leaf block โ€” destroy the whole tent
    if(G.world[by]&&G.world[by][bx]===B.LEAVES){
      for(let dy=-8;dy<=1;dy++)for(let dx=-6;dx<=6;dx++){
        const tx=bx+dx,ty=by+dy;
        if(tx<0||tx>=WW||ty<0||ty>=WH)continue;
        const blk=G.world[ty][tx];
        if(blk===B.LEAVES||blk===B.BED){G.world[ty][tx]=B.AIR;}
        if(blk===B.DOOR){G.world[ty][tx]=B.AIR;G.openDoors.delete(`${tx},${ty}`);}
      }
      G.sleepMsg="โ›บ Tent packed up!";setTimeout(()=>G.sleepMsg="",1500);
      return;
    }
    const dir=G.player.dir;
    const h=4; // 4 rows: base to peak
    // Triangle tent โ€” all leaves, narrows each row going up
    for(let row=0;row<h;row++){
      const ty=by-row;
      const halfW=h-1-row; // row 0 (base)=3, row 3 (peak)=0
      if(ty<0||ty>=WH)continue;
      // Clear interior of this row
      for(let dx=-(halfW-1);dx<=(halfW-1);dx++){
        const tx=bx+dx;
        if(tx>=0&&tx<WW&&G.world[ty][tx]!==B.BEDROCK)G.world[ty][tx]=B.AIR;
      }
      // Place leaf shell
      if(halfW===0){
        if(bx>=0&&bx<WW)G.world[ty][bx]=B.LEAVES; // peak
      }else if(halfW<=1){
        for(let dx=-halfW;dx<=halfW;dx++){const tx=bx+dx;if(tx>=0&&tx<WW)G.world[ty][tx]=B.LEAVES;}
      }else{
        if(bx-halfW>=0)G.world[ty][bx-halfW]=B.LEAVES; // left edge
        if(bx+halfW<WW)G.world[ty][bx+halfW]=B.LEAVES; // right edge
      }
    }
    // Door at entrance side at base row
    const doorX=bx+(dir>0?h-1:-(h-1));
    if(doorX>=0&&doorX<WW)G.world[by][doorX]=B.DOOR;
    // Bed inside the tent (sleep anywhere in the tent)
    if(by>=0&&by<WH&&bx>=0&&bx<WW)G.world[by][bx]=B.BED;
    if(!G.creative){sl.count--;if(!sl.count)sl.block=B.AIR;}
    G.sleepMsg="โ›บ Tent up! Close the door to keep zombies out.";setTimeout(()=>G.sleepMsg="",2500);
    return;
  }
  const placedBlock=sl.block;
  G.world[by][bx]=sl.block;if(!G.creative){sl.count--;if(!sl.count)sl.block=B.AIR;}
  // Start growth timer for saplings
  if(G.world[by][bx]===B.SAPLING){G.saplingTimers=G.saplingTimers||{};G.saplingTimers[`${bx},${by}`]=1200+Math.floor(Math.random()*600);}
  if(placedBlock===B.CHEST)G.playerPlacedChests.add(`${bx},${by}`);
  if(placedBlock===B.PISTON||placedBlock===B.STICKY_PISTON){
    const px2=G.player.x,py2=G.player.y;
    const adx=Math.abs(px2-(bx+.5)*BS),ady=Math.abs(py2-(by+.5)*BS);
    const dir=ady>adx?(py2<(by+.5)*BS?2:-2):(px2<(bx+.5)*BS?1:-1);
    G.pistonStates[`${bx},${by}`]={dir,extended:false};
  }
  if(sl.block===B.AIR&&G.world[by][bx]===B.DISPENSER)G.dispenserArrows[`${bx},${by}`]=0;
}
function tryAttack(){
  // Melee only โ€” bow uses startBowCharge/releaseBow; guns use tryFireGun
  const p=G.player;if(p.atkCD>0)return;
  const eq=G.hotbar[G.slot];
  if(eq?.count&&DATA[eq.block]?.isBow)return;
  if(eq?.count&&DATA[eq.block]?.isGun){tryFireGun();return;}
  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;}});
  if(G.dim==="end"&&G.dragon&&Math.hypot(G.dragon.x-p.x,G.dragon.y-p.y)<70){G.dragon.hp-=dmg;G.dragon.flashT=15;hit=true;}
  G.creepers.forEach(c=>{if(Math.abs(c.x-p.x)<range&&Math.abs(c.y-p.y)<range){c.hp-=dmg;c.fuse=Math.max(0,c.fuse-20);c.flash=8;hit=true;}});
  (G.skeletons||[]).forEach(sk=>{if(Math.abs(sk.x-p.x)<range&&Math.abs(sk.y-p.y)<range){sk.hp-=dmg;sk.flash=8;hit=true;}});
  G.zombies=G.zombies.filter(z=>z.hp>0);
  G.creepers=G.creepers.filter(c=>c.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;
}
function tryFireGun(){
  if(!G)return;
  const p=G.player;
  const eq=G.hotbar[G.slot];
  if(!eq?.count||!DATA[eq.block]?.isGun)return;
  const gun=DATA[eq.block];
  if((p.gunCD||0)>0)return;
  if(gun.gunType==='pistol'||gun.gunType==='mg'){    const bulletCount=countItem(G.hotbar,G.inventory,B.BULLET);
    if(bulletCount<=0){G.sleepMsg="No bullets! Craft: 1 Iron + 1 Redstone = 32 bullets";setTimeout(()=>G.sleepMsg="",2000);return;}
    if(!G.creative)consume(G.hotbar,G.inventory,B.BULLET,1);
    const spread=gun.gunType==='mg'?(Math.random()-.5)*0.18:0;
    const spd=gun.gunType==='mg'?20:28;
    G.bullets=G.bullets||[];
    G.bullets.push({
      x:p.x+(p.dir>0?14:-14),y:p.y-PH*.52,
      vx:p.dir*spd*Math.cos(spread),vy:Math.sin(spread)*spd*.4-0.8,
      life:gun.gunType==='mg'?22:40,dmg:gun.gunDmg,
    });
    G.muzzleFlash={x:p.x+(p.dir>0?22:-22),y:p.y-PH*.52,life:5,dir:p.dir};
  }else if(gun.gunType==='taser'){
    G.bullets=G.bullets||[];
    G.bullets.push({x:p.x+(p.dir>0?14:-14),y:p.y-PH*.52,vx:p.dir*20,vy:-0.5,life:35,dmg:0,isTaser:true});
    G.muzzleFlash={x:p.x+(p.dir>0?22:-22),y:p.y-PH*.52,life:6,dir:p.dir,taser:true};
  }else if(gun.gunType==='rocket'){    const tntCount=countItem(G.hotbar,G.inventory,B.TNT);
    if(tntCount<=0){G.sleepMsg="No TNT ammo! Add TNT to inventory.";setTimeout(()=>G.sleepMsg="",2000);return;}
    if(!G.creative)consume(G.hotbar,G.inventory,B.TNT,1);
    G.bullets=G.bullets||[];
    G.bullets.push({
      x:p.x+(p.dir>0?18:-18),y:p.y-PH*.5,
      vx:p.dir*13,vy:-1.5,
      life:120,dmg:0,isRocket:true,
    });
    G.muzzleFlash={x:p.x+(p.dir>0?24:-24),y:p.y-PH*.5,life:8,dir:p.dir,rocket:true};
  }else if(gun.gunType==='flame'){    G.flames=G.flames||[];    for(let i=0;i<5;i++){
      const sp=(Math.random()-.5)*0.65,spd2=5+Math.random()*5;
      G.flames.push({
        x:p.x+(p.dir>0?16:-16),y:p.y-PH*.45,
        vx:p.dir*spd2*Math.cos(sp),vy:Math.sin(sp)*spd2*.4-0.4,
        life:18+Math.floor(Math.random()*18),maxLife:36,size:5+Math.random()*7,
      });
    }
    G.muzzleFlash={x:p.x+(p.dir>0?20:-20),y:p.y-PH*.45,life:3,dir:p.dir,flame:true};
    // Directional heat damage in front of player every 18 frames
    G.flameDmgT=(G.flameDmgT||0)+1;
    if(G.flameDmgT>=18){
      G.flameDmgT=0;
      const fr=5*BS,fdy=2*BS;
      const inBeam=(x,y)=>p.dir>0?(x>p.x&&x<p.x+fr&&Math.abs(y-p.y)<fdy):(x<p.x&&x>p.x-fr&&Math.abs(y-p.y)<fdy);
      if(!G.creative){
        G.zombies.forEach(z=>{if(inBeam(z.x,z.y)){z.hp--;z.flash=12;}});
        G.creepers.forEach(c=>{if(inBeam(c.x,c.y)){c.hp--;c.flash=12;}});
        (G.skeletons||[]).forEach(sk=>{if(inBeam(sk.x,sk.y)){sk.hp--;sk.flash=12;}});
      }
    }
  }
  p.gunCD=gun.gunCD;
}

// โ”€โ”€ 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||chestOpen){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||chestOpen){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];if(eq?.count&&DATA[eq.block]?.isGun)tryFireGun();else if(eq?.count&&DATA[eq.block]?.isBow)startBowCharge();else 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');
holdBtn(document.getElementById('btn-down'),'down');
document.getElementById('btn-noclip').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();toggleNoclip();},{passive:false});
document.getElementById('btn-noclip').addEventListener('mousedown',e=>{e.stopPropagation();toggleNoclip();});

document.getElementById('btn-eat').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();tryEat();},{passive:false});
document.getElementById('btn-eat').addEventListener('mousedown',e=>{e.stopPropagation();tryEat();});
document.getElementById('btn-atk').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();touch.atk=true;const eq=G?.hotbar[G.slot];if(eq?.count&&DATA[eq.block]?.isGun)tryFireGun();else if(eq?.count&&DATA[eq.block]?.isBow)startBowCharge();else tryAttack();},{passive:false});
document.getElementById('btn-atk').addEventListener('touchend',e=>{e.stopPropagation();e.preventDefault();touch.atk=false;releaseBow();},{passive:false});
document.getElementById('btn-atk').addEventListener('touchcancel',e=>{e.stopPropagation();e.preventDefault();touch.atk=false;releaseBow();},{passive:false});
document.getElementById('btn-atk').addEventListener('mousedown',e=>{e.stopPropagation();touch.atk=true;const eq=G?.hotbar[G.slot];if(eq?.count&&DATA[eq.block]?.isGun)tryFireGun();else if(eq?.count&&DATA[eq.block]?.isBow)startBowCharge();else tryAttack();});
document.getElementById('btn-atk').addEventListener('mouseup',e=>{e.stopPropagation();touch.atk=false;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('btn-space').addEventListener('click',()=>G?.dim==="space"?leaveSpace():enterSpace());
document.getElementById('btn-space').addEventListener('touchend',e=>{e.preventDefault();G?.dim==="space"?leaveSpace():enterSpace();},{passive:false});
document.getElementById('btn-blueprint').addEventListener('click',blueprintStart);
document.getElementById('btn-blueprint').addEventListener('touchend',e=>{e.preventDefault();blueprintStart();},{passive:false});

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 isCreative=G?.creative;
  document.getElementById('btn-blueprint').style.display=isCreative?'flex':'none';
  document.getElementById('btn-space').style.display='flex';
  document.getElementById('btn-space').textContent=G?.dim==="space"?"๐ŸŒ Leave Space":"๐ŸŒŒ Space";
  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)";
}

// โ”€โ”€ Blueprint system โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function blueprintStart(){
  if(!G?.creative)return;
  if(G.lastKit){
    const bx=Math.floor(G.player.x/BS),by=Math.floor(G.player.y/BS);
    G.lastKit.blocks.forEach(({dx,dy,block})=>{
      const tx=bx+dx,ty=by+dy;
      if(tx>=0&&tx<WW&&ty>=0&&ty<WH&&G.world[ty][tx]!==B.BEDROCK)G.world[ty][tx]=block;
    });
    G.sleepMsg="๐Ÿ“ Kit placed!";setTimeout(()=>G.sleepMsg="",1500);
    G.lastKit=null;document.getElementById('btn-blueprint').textContent='๐Ÿ“ Blueprint';
    return;
  }
  G.bpMode=true;G.bpSelected=new Set();
  document.getElementById('bp-panel').style.display='block';
  document.getElementById('bp-save').style.display='none';
  document.getElementById('bp-status').textContent='Tap each block to select it (tap again to deselect)';
}
function blueprintCancel(){
  if(!G)return;
  G.bpMode=false;G.bpSelected=null;
  document.getElementById('bp-panel').style.display='none';
}
function blueprintTap(bx,by){
  if(!G?.bpMode)return false;
  if(!G.bpSelected)G.bpSelected=new Set();
  const key=`${bx},${by}`;
  const blk=G.world[by]?.[bx];
  if(!blk||blk===B.AIR){G.sleepMsg="Tap a non-air block!";setTimeout(()=>G.sleepMsg="",1000);return true;}
  if(G.bpSelected.has(key)){G.bpSelected.delete(key);}
  else{G.bpSelected.add(key);}
  const n=G.bpSelected.size;
  document.getElementById('bp-status').textContent=`${n} block${n!==1?'s':''} selected โ€” tap more or save`;
  document.getElementById('bp-save').style.display=n>0?'block':'none';
  return true;
}
function blueprintSave(){
  if(!G?.bpSelected||G.bpSelected.size===0){G.sleepMsg="Select some blocks first!";setTimeout(()=>G.sleepMsg="",1500);return;}
  const coords=[...G.bpSelected].map(k=>{const[x,y]=k.split(',').map(Number);return{x,y};});
  const x1=Math.min(...coords.map(c=>c.x)),y1=Math.min(...coords.map(c=>c.y));
  const x2=Math.max(...coords.map(c=>c.x)),y2=Math.max(...coords.map(c=>c.y));
  const kitBlocks=coords.map(({x,y})=>({dx:x-x1,dy:y-y1,block:G.world[y][x]})).filter(b=>b.block&&b.block!==B.AIR);
  if(!kitBlocks.length)return;
  // Remove the selected blocks from the world (cut, not copy)
  coords.forEach(({x,y})=>{if(G.world[y]&&G.world[y][x]!==B.BEDROCK)G.world[y][x]=B.AIR;});
  G.lastKit={blocks:kitBlocks,w:x2-x1+1,h:y2-y1+1};
  const kit={block:B.KIT,count:1,kitBlocks,kitW:x2-x1+1,kitH:y2-y1+1};
  const slot=G.inventory.findIndex(s=>!s.count||s.block===B.AIR);
  if(slot>=0)G.inventory[slot]=kit;
  else{const hs=G.hotbar.findIndex(s=>!s.count||s.block===B.AIR);if(hs>=0)G.hotbar[hs]=kit;}
  document.getElementById('btn-blueprint').textContent='๐Ÿ“ Place Kit';
  blueprintCancel();
  G.sleepMsg=`๐Ÿ“ ${kitBlocks.length} blocks saved! Press ๐Ÿ“ to place`;setTimeout(()=>G.sleepMsg="",2500);
  renderInv();
}

// โ”€โ”€ Inventory โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function toggleInv(force){
  invOpen=force!==undefined?force:!invOpen;
  document.getElementById('inv-overlay').classList.toggle('open',invOpen);
  if(!invOpen){const s=document.getElementById('inv-search');if(s)s.value='';}
  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',()=>area==='chest'?onChestSlotClick(area,idx):onSlotClick(area,idx));
  el.addEventListener('touchend',e=>{e.preventDefault();area==='chest'?onChestSlotClick(area,idx):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='';
  const search=(document.getElementById('inv-search')?.value||'').toLowerCase().trim();
  const isCreative=G?.creative;
  // Creative bag: scrollable, taller, shows all items
  if(isCreative){
    ig.style.cssText='display:grid;grid-template-columns:repeat(8,1fr);gap:3px;max-height:360px;overflow-y:auto;padding-right:2px;';
  }else{
    ig.style.cssText='display:grid;grid-template-columns:repeat(8,1fr);gap:3px;';
  }
  G.inventory.forEach((sl,i)=>{
    if(search&&sl.count>0){
      const name=(DATA[sl.block]?.name||'').toLowerCase();
      if(!name.includes(search)){
        const el=makeSlotEl({block:B.AIR,count:0},'inv',i);el.style.opacity='0.08';ig.appendChild(el);return;
      }
    }
    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 filterInv(query){
  if(!G)return;
  if(G.creative){
    const q=query.toLowerCase().trim();
    const allItems=[
      B.PISTOL,B.MACHINE_GUN,B.FLAMETHROWER,B.ROCKET_LAUNCHER,B.TASER,B.BOMB,B.JET,B.BULLET,
      B.BOW,B.ARROW,B.DM_SWORD,B.DM_AXE,B.DM_PICK,B.IRON_PICK,B.STONE_PICK,B.IRON_SWORD,B.FLINT_STEEL,
      B.DM_HELM,B.DM_CHEST,B.DM_BOOT,B.IR_HELM,B.IR_CHEST,B.IR_BOOT,
      B.BREAD,B.APPLE,B.MEAT,B.STEW,
      B.LEAVES,B.COAL,B.IRON,B.REDSTONE,B.EMERALD,B.DIAMOND,B.STICK,B.PLANK,B.WOOD,B.STONE,B.DIRT,B.GRASS,B.SAND,B.GRAVEL,B.GLASS,B.STONE_BRICK,B.OBSIDIAN,B.GUNPOWDER,
      B.TORCH,B.DOOR,B.BLAST_DOOR,B.HOLO_DOOR,B.CASTLE_GATE,B.TENT_KIT,B.FIREPLACE,B.CHIMNEY,B.BED,B.CHEST,B.DISPENSER,B.FENCE,B.GATE,B.TRAPDOOR,B.LADDER,
      B.TNT,B.PISTON,B.STICKY_PISTON,B.CONV_R,B.CONV_L,B.TURRET,B.SPIKE,B.FAN,B.ELEVATOR,
      B.ROCKET_SHIP,B.MOON_ROCK,B.MOON_DUST,B.LAVA,B.WATER,B.BARRIER,B.EGG_ZOMBIE,B.EGG_CREEPER,B.WHITE_WALL,B.LIGHT_GRAY_WALL,B.GRAY_WALL,B.BLACK_WALL,B.RED_WALL,B.ORANGE_WALL,B.YELLOW_WALL,B.LIME_WALL,B.GREEN_WALL,B.CYAN_WALL,B.LIGHT_BLUE_WALL,B.BLUE_WALL,B.PURPLE_WALL,B.MAGENTA_WALL,B.PINK_WALL,B.BROWN_WALL,
    B.SPACESUIT,B.SPACE_ROCK,B.JETPACK,
    B.END_PORTAL,B.EYE_END,B.END_STONE,B.DRAGON_EGG,
    ];
    const filtered=q?allItems.filter(b=>DATA[b]&&(DATA[b].name||'').toLowerCase().includes(q)):allItems;
    G.inventory=filtered.map(b=>({block:b,count:9999}));
  }
  renderInv();
}

function quickUse(area,idx){
  // Double-tap: eat food, or move to hotbar
  const arr=area==='inv'?G.inventory:G.hotbar;
  const sl=arr[idx];
  if(!sl||!sl.count)return;
  const d=DATA[sl.block];
  if(d?.food){
    // Eat it directly from inventory
    if(G.player.hp<MAX_HP){
      G.player.hp=Math.min(MAX_HP,G.player.hp+d.food);
      if(!G.creative){sl.count--;if(!sl.count)sl.block=B.AIR;}
      G.sleepMsg=`+${d.food} โค๏ธ ${d.name}`;setTimeout(()=>G.sleepMsg='',1500);
    } else {
      G.sleepMsg='Already at full health!';setTimeout(()=>G.sleepMsg='',1500);
    }
  } else if(area==='inv'){
    // Move to first available hotbar slot (empty or same block)
    let target=-1;
    for(let i=0;i<G.hotbar.length;i++)if(G.hotbar[i].block===sl.block&&G.hotbar[i].count<999){target=i;break;}
    if(target===-1)for(let i=0;i<G.hotbar.length;i++)if(!G.hotbar[i].count){target=i;break;}
    if(target!==-1){
      if(G.hotbar[target].block===sl.block){
        const add=Math.min(999-G.hotbar[target].count,sl.count);
        G.hotbar[target].count+=add;sl.count-=add;if(!sl.count)sl.block=B.AIR;
      }else{const t={...G.hotbar[target]};G.hotbar[target]={...sl};arr[idx]=t;}
      G.slot=target;
      G.sleepMsg=`โ†’ Hotbar: ${d?.name||'Item'}`;setTimeout(()=>G.sleepMsg='',1000);
    } else {
      G.sleepMsg='Hotbar is full!';setTimeout(()=>G.sleepMsg='',1200);
    }
  } else {
    // Already in hotbar โ€” just select it
    G.slot=idx;
    G.sleepMsg=d?.name||'';setTimeout(()=>G.sleepMsg='',800);
  }
  selSlot=null;document.getElementById('inv-tip').style.display='none';
  renderInv();
}

function onSlotClick(area,idx){
  const arr=area==='inv'?G.inventory:G.hotbar;
  if(!selSlot){
    if(arr[idx].count>0){
      // Double-tap same slot = quick use
      if(selSlot===null&&area===area){} // fall through
      selSlot={area,idx};
      const tip=document.getElementById('inv-tip');
      tip.textContent=(DATA[arr[idx].block]?.name||'')+'  (tap again to use/equip)';
      tip.style.display='';
    }
  }else{
    if(selSlot.area===area&&selSlot.idx===idx){
      // Second tap on same slot = quick use
      quickUse(area,idx);
      return;
    }
    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 toggleNoclip(){
  if(!G)return;
  G.noclip=!G.noclip;
  updateFlightBtns();
}
function updateFlightBtns(){
  if(!G)return;
  const downBtn=document.getElementById('btn-down');
  const jumpBtn=document.getElementById('btn-jump');
  if(!downBtn||!jumpBtn)return;
  const noclipBtn=document.getElementById('btn-noclip');
  if(G.creative){
    downBtn.style.display='flex';
    jumpBtn.textContent='โฌ† Up';
    if(noclipBtn){
      noclipBtn.style.display='flex';
      noclipBtn.textContent=G.noclip?"๐Ÿ‘ป Noclip: ON":"๐Ÿงฑ Noclip: OFF";
      noclipBtn.style.background=G.noclip?"rgba(120,0,120,.9)":"rgba(40,40,40,.9)";
      noclipBtn.style.borderColor=G.noclip?"rgba(200,80,255,.5)":"rgba(255,255,255,.2)";
    }
  } else {
    downBtn.style.display='none';
    jumpBtn.textContent='โ†‘ Jump';
    if(noclipBtn)noclipBtn.style.display='none';
  }
}
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 updateEatBtn(){
  const btn=document.getElementById('btn-eat');if(!btn||!G)return;
  const eq=G.hotbar[G.slot];
  const hasFood=eq?.count>0&&DATA[eq.block]?.food;
  btn.style.opacity=hasFood?"1":"0.3";
  if(hasFood){
    btn.textContent=DATA[eq.block].icon+' Eat';
    btn.style.background="rgba(30,120,30,.9)";
    btn.style.borderColor="rgba(80,220,80,.5)";
  }else{
    btn.textContent="๐ŸŽ Eat";
    btn.style.background="rgba(15,15,15,.7)";
    btn.style.borderColor="rgba(255,255,255,.2)";
  }
}
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)";
}

let chestOpen=false,chestKey=null,chestSel=null;

function openChest(k){
  chestKey=k;chestOpen=true;chestSel=null;
  document.getElementById('chest-overlay').style.display='flex';
  renderChest();
}
function closeChest(){
  chestOpen=false;chestKey=null;chestSel=null;
  document.getElementById('chest-overlay').style.display='none';
  document.getElementById('chest-tip').style.display='none';
}
function renderChest(){
  if(!chestKey||!G)return;
  const slots=G.chestContents[chestKey]||[];
  const cg=document.getElementById('chest-grid');cg.innerHTML='';
  slots.forEach((sl,i)=>cg.appendChild(makeSlotEl(sl,'chest',i)));
  const ig=document.getElementById('chest-inv-grid');ig.innerHTML='';
  G.inventory.forEach((sl,i)=>ig.appendChild(makeSlotEl(sl,'inv',i)));
  const hg=document.getElementById('chest-hot-grid');hg.innerHTML='';
  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 onChestSlotClick(area,idx){
  const getArr=a=>a==='chest'?G.chestContents[chestKey]:a==='inv'?G.inventory:G.hotbar;
  const arr=getArr(area);
  const tip=document.getElementById('chest-tip');
  if(!chestSel){
    if(arr[idx]?.count>0){
      chestSel={area,idx};
      tip.textContent=DATA[arr[idx].block]?.name||'';tip.style.display='';
    }
  } else {
    if(chestSel.area===area&&chestSel.idx===idx){chestSel=null;tip.style.display='none';renderChest();return;}
    const src=getArr(chestSel.area),dst=getArr(area);
    const si=chestSel.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;delete src[si].enchant;}
    } else {
      const t={...src[si]};src[si]={...dst[di]};dst[di]=t;
    }
    if(area==='hot')G.slot=idx;
    chestSel=null;tip.style.display='none';
  }
  renderChest();
}

document.getElementById('chest-close').addEventListener('click',closeChest);
document.getElementById('chest-close').addEventListener('touchend',e=>{e.preventDefault();closeChest();},{passive:false});
document.getElementById('chest-overlay').addEventListener('click',e=>{if(e.target===document.getElementById('chest-overlay'))closeChest();});


// โ”€โ”€ Drawbridge โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function toggleDrawbridge(bx,by){
  G.drawbridgeStates=G.drawbridgeStates||{};
  const k=`${bx},${by}`;
  G.drawbridgeStates[k]=G.drawbridgeStates[k]||{raised:false,blocks:[]};
  const ds=G.drawbridgeStates[k];
  // Remove previously placed drawbridge blocks
  ds.blocks.forEach(pos=>{
    const[px,py]=pos.split(',').map(Number);
    if(px>=0&&px<WW&&py>=0&&py<WH&&(G.world[py][px]===B.PLANK||G.world[py][px]===B.STONE_BRICK||G.world[py][px]===B.IRON))
      G.world[py][px]=B.AIR;
  });
  ds.blocks=[];
  if(!ds.raised){
    // RAISE: 1 block wide, 6 blocks tall above anchor
    for(let dy=1;dy<=6;dy++){
      const ty=by-dy;
      if(ty<0||ty>=WH||G.world[ty][bx]!==B.AIR)continue;
      G.world[ty][bx]=B.PLANK;
      ds.blocks.push(`${bx},${ty}`);
    }
    ds.raised=true;
    G.sleepMsg="๐Ÿฐ Gate raised!";setTimeout(()=>G.sleepMsg="",1800);
  }else{
    // LOWER: 3 blocks flat on ground as a bridge step
    const dir=G.player.x<(bx+.5)*BS?1:-1;
    for(let dx=0;dx<6;dx++){
      const tx=bx+dx*dir,ty=by;
      if(tx<0||tx>=WW||ty<0||ty>=WH||G.world[ty][tx]!==B.AIR)continue;
      G.world[ty][tx]=B.PLANK;
      ds.blocks.push(`${tx},${ty}`);
    }
    ds.raised=false;
    G.sleepMsg="๐Ÿฐ Gate lowered!";setTimeout(()=>G.sleepMsg="",1800);
  }
}

// โ”€โ”€ Moon dimension โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function genMoonWorld(){
  const world=[];for(let y=0;y<WH;y++)world.push(new Uint8Array(WW));
  const baseY=Math.floor(WH*.62);const surf=new Array(WW).fill(baseY);
  for(let x=0;x<WW;x++)for(let y=0;y<WH;y++){
    if(y===WH-1)world[y][x]=B.BEDROCK;
    else if(y>baseY)world[y][x]=B.MOON_ROCK;
    else if(y===baseY)world[y][x]=B.MOON_DUST;
    else world[y][x]=B.AIR;
  }
  for(let c=0;c<10;c++){
    const cx2=4+Math.floor(Math.random()*(WW-8)),cr=2+Math.floor(Math.random()*5),dep=1+Math.floor(Math.random()*3);
    for(let x=cx2-cr;x<=cx2+cr;x++){
      if(x<0||x>=WW)continue;
      const dip=Math.round(Math.sqrt(Math.max(0,cr*cr-(x-cx2)**2))*.6);
      for(let y=baseY-dip;y<=baseY+dep;y++){if(y>=0&&y<WH)world[y][x]=y>baseY?B.MOON_ROCK:B.AIR;}
      for(let y=0;y<WH;y++){if(world[y][x]!==B.AIR&&world[y][x]!==B.BEDROCK){surf[x]=y;break;}}
    }
  }
  const mid=Math.floor(WW/2);
  for(let x=mid-2;x<=mid+2;x++){if(surf[x]>=0&&surf[x]<WH)world[surf[x]][x]=B.MOON_DUST;}
  if(surf[mid]-1>=0)world[surf[mid]-1][mid]=B.ROCKET_SHIP;
  return{world,surf};
}
function enterMoon(){
  G.preMoonPos={x:G.player.x,y:G.player.y,world:G.world,surf:G.surf};
  if(!G.moonWorld){const r=genMoonWorld();G.moonWorld=r.world;G.moonSurf=r.surf;}
  G.world=G.moonWorld;G.surf=G.moonSurf;G.dim="moon";
  const mid=Math.floor(WW/2);
  G.player.x=(mid+.5)*BS;G.player.y=((G.moonSurf[mid]||20)-2)*BS;
  G.player.vx=0;G.player.vy=0;G.moonLaunch=null;
  G.sleepMsg="๐ŸŒ• Welcome to the Moon! Low gravity. Tap the rocket to return.";setTimeout(()=>G.sleepMsg="",4000);
}
function leaveMoon(){
  G.moonWorld=G.world;G.moonSurf=G.surf;
  G.world=G.preMoonPos.world;G.surf=G.preMoonPos.surf;
  G.player.x=G.preMoonPos.x;G.player.y=G.preMoonPos.y-BS*2;
  G.player.vx=0;G.player.vy=0;G.dim="over";G.moonLaunch=null;
  G.sleepMsg="๐ŸŒ Back on Earth!";setTimeout(()=>G.sleepMsg="",2000);
}

// โ”€โ”€ Space dimension โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const SPACE_PLANETS=[
  {cx:65,cy:12,r:8,blocks:[B.MOON_ROCK,B.MOON_DUST,B.STONE],name:"Lunaris"},
  {cx:16,cy:20,r:9,blocks:[B.GRASS,B.DIRT,B.STONE],name:"Verdania"},
  {cx:114,cy:18,r:9,blocks:[B.DIAMOND,B.GLASS,B.STONE_BRICK],name:"Crystalis"},
  {cx:8,cy:40,r:7,blocks:[B.REDSTONE,B.IRON,B.STONE],name:"Rubeus"},
  {cx:122,cy:38,r:7,blocks:[B.OBSIDIAN,B.STONE,B.COAL],name:"Darkeon"},
  {cx:85,cy:25,r:4,blocks:[B.GRASS,B.DIRT,B.STONE],name:"Sprout"},
];
function genSpaceWorld(){
  const w=[];for(let y=0;y<WH;y++)w.push(new Uint8Array(WW));
  // Place planets
  SPACE_PLANETS.forEach(({cx,cy,r,blocks})=>{
    for(let y=cy-r;y<=cy+r;y++)for(let x=cx-r;x<=cx+r;x++){
      if(x<0||x>=WW||y<0||y>=WH)continue;
      const d=Math.sqrt((x-cx)**2+(y-cy)**2);
      if(d<=r){const t=d/r;w[y][x]=t>0.65?blocks[0]:t>0.35?blocks[1]:blocks[2];}
    }
    // Chest on each planet surface
    const tx=cx,ty=cy-r-1;if(tx>=0&&tx<WW&&ty>=0&&ty<WH)w[ty][tx]=B.CHEST;
    if(cy-r>=0&&cy-r<WH&&cx>=0&&cx<WW)w[cy-r][cx]=blocks[0]; // surface top
  });

  // Tiny "Sprout" spawn planet (also in SPACE_PLANETS for gravity)
  const spx=85,spy=25,spr=4;
  for(let y=spy-spr;y<=spy+spr;y++)for(let x=spx-spr;x<=spx+spr;x++){
    if(x<0||x>=WW||y<0||y>=WH)continue;
    const d=Math.sqrt((x-spx)**2+(y-spy)**2);
    if(d<=spr){const t=d/spr;w[y][x]=t>0.6?B.GRASS:t>0.3?B.DIRT:B.STONE;}
  }
  // Little tree on top
  if(spy-spr-1>=0)w[spy-spr-1][spx]=B.WOOD;
  if(spy-spr-2>=0)w[spy-spr-2][spx]=B.LEAVES;
  if(spy-spr-3>=0){w[spy-spr-3][spx-1]=B.LEAVES;w[spy-spr-3][spx]=B.LEAVES;w[spy-spr-3][spx+1]=B.LEAVES;}

  const surf=new Array(WW).fill(WH-4);
  return{world:w,surf};
}
function enterSpace(){
  if(G.dim==="space")return;
  G.preSpacePos={x:G.player.x,y:G.player.y,world:G.world,surf:G.surf,dim:G.dim};
  if(!G.spaceWorld){const r=genSpaceWorld();G.spaceWorld=r.world;G.spaceSurf=r.surf;}
  G.world=G.spaceWorld;G.surf=G.spaceSurf;G.dim="space";
  const rc=Math.floor(WW/2),rb=WH-4;
  // Spawn above the tiny welcome planet (rc+20, rb-35, r=4) so player falls onto it
  G.player.x=(rc+20.5)*BS;G.player.y=(rb-43)*BS;
  G.player.vx=0;G.player.vy=2;
  G.sleepMsg="๐Ÿช Land on the tiny planet! Jetpack to explore the other planets and the station.";setTimeout(()=>G.sleepMsg="",5000);
}
function leaveSpace(){
  if(!G.preSpacePos)return;
  G.spaceWorld=G.world;G.spaceSurf=G.surf;
  G.world=G.preSpacePos.world;G.surf=G.preSpacePos.surf;
  G.player.x=G.preSpacePos.x;G.player.y=G.preSpacePos.y;
  G.player.vx=0;G.player.vy=0;
  G.dim=G.preSpacePos.dim||"over";G.preSpacePos=null;
  G.sleepMsg="๐ŸŒ Back home!";setTimeout(()=>G.sleepMsg="",2000);
}

// โ”€โ”€ End dimension โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const EW=80,EH=32; // end world size

function generateEnd(){
  const w=Array.from({length:EH},()=>new Uint8Array(EW));
  // Floating island 25 blocks wide at y=22
  const islandX=Math.floor(EW/2)-12,islandW=25;
  for(let x=islandX;x<islandX+islandW;x++){
    for(let y=22;y<26;y++) w[y][x]=B.END_STONE;
    w[21][x]=B.END_STONE;
  }
  // Return portal in centre of island
  const px2=Math.floor(EW/2);
  w[21][px2]=B.END_PORTAL;
  return w;
}

function enterEnd(){
  if(!G.endWorld) G.endWorld=generateEnd();
  G.overworldPos={x:G.player.x,y:G.player.y};
  G.dim="end";
  const spx=EW/2*BS,spy=20*BS;
  G.player.x=spx;G.player.y=spy;G.player.vx=0;G.player.vy=0;
  if(!G.dragonDefeated&&!G.dragon){
    G.dragon={
      x:EW/2*BS,y:4*BS,vx:3,vy:0,hp:60,maxHp:60,
      angle:0,phase:"circle",fireCD:0,flashT:0,
      diveTarget:null,diveCD:200,
    };
  }
  G.endFireballs=[];
  G.sleepMsg="โšก You entered The End!";setTimeout(()=>G.sleepMsg="",2500);
}

function leaveEnd(){
  if(G.dim!=="end")return;
  G.dim="over";
  if(G.overworldPos){G.player.x=G.overworldPos.x;G.player.y=G.overworldPos.y;}
  G.player.vx=0;G.player.vy=0;
  G.sleepMsg="๐ŸŒ Returned to the Overworld";setTimeout(()=>G.sleepMsg="",2000);
}

function solidEnd(l,t,w2,h2){
  const ew=G.endWorld;if(!ew)return false;
  for(let by=Math.floor(t/BS);by<=Math.floor((t+h2)/BS);by++)
    for(let bx=Math.floor(l/BS);bx<=Math.floor((l+w2)/BS);bx++){
      if(by<0||by>=EH||bx<0||bx>=EW)continue;
      if(ew[by][bx]&&!DATA[ew[by][bx]]?.pass&&!DATA[ew[by][bx]]?.isPortal)return true;
    }
  return false;
}

function drawDragon(ctx,dx,dy,dir,flash,W){
  if(flash){ctx.save();ctx.globalAlpha=.5;}
  const t=Date.now()*.003;
  // Wings
  const ws=40+Math.sin(t*3)*8;
  ctx.fillStyle=flash?"#cc44ff":"#4a006a";
  // Left wing
  ctx.beginPath();ctx.moveTo(dx,dy);ctx.lineTo(dx-ws,dy-18+Math.sin(t*3)*6);ctx.lineTo(dx-ws+10,dy+10);ctx.closePath();ctx.fill();
  // Right wing
  ctx.beginPath();ctx.moveTo(dx,dy);ctx.lineTo(dx+ws,dy-18+Math.sin(t*3)*6);ctx.lineTo(dx+ws-10,dy+10);ctx.closePath();ctx.fill();
  // Body
  ctx.fillStyle=flash?"#dd55ff":"#6a0090";
  ctx.beginPath();ctx.ellipse(dx,dy,28,16,0,0,Math.PI*2);ctx.fill();
  // Head
  const hx=dx+(dir>0?30:-30),hy=dy-5;
  ctx.fillStyle=flash?"#dd55ff":"#5a0080";
  ctx.beginPath();ctx.ellipse(hx,hy,14,10,0,0,Math.PI*2);ctx.fill();
  // Eyes
  ctx.fillStyle="#ff2200";ctx.beginPath();ctx.arc(hx+(dir>0?5:-5),hy-2,3,0,Math.PI*2);ctx.fill();
  ctx.fillStyle="#ffff00";ctx.beginPath();ctx.arc(hx+(dir>0?5:-5),hy-2,1.5,0,Math.PI*2);ctx.fill();
  // Tail
  ctx.strokeStyle=flash?"#cc44ff":"#4a006a";ctx.lineWidth=6;
  ctx.beginPath();ctx.moveTo(dx+(dir>0?-28:28),dy);ctx.quadraticCurveTo(dx+(dir>0?-50:50),dy+20,dx+(dir>0?-70:70),dy+5);ctx.stroke();
  if(flash)ctx.restore();
  // HP bar
  if(G.dragon){
    const bw=Math.min(300,W*.6),bx2=(W-bw)/2,by2=14;
    ctx.fillStyle="rgba(0,0,0,.7)";roundRect(ctx,bx2-2,by2-2,bw+4,20,8);ctx.fill();
    ctx.fillStyle="#6600aa";ctx.fillRect(bx2,by2,bw*(G.dragon.hp/G.dragon.maxHp),16);
    ctx.fillStyle="#aa44ff";ctx.fillRect(bx2,by2,bw*(G.dragon.hp/G.dragon.maxHp),4);
    ctx.font="bold 12px monospace";ctx.fillStyle="#fff";ctx.textAlign="center";
    ctx.fillText(`๐Ÿ‰ Ender Dragon  ${G.dragon.hp}/${G.dragon.maxHp}`,W/2,by2+12);ctx.textAlign="left";
  }
}

function renderEnd(ctx,W,H){
  const p=G.player;
  // Camera
  const cx=Math.max(0,Math.min(EW*BS-W,p.x-W/2));
  const cy=Math.max(0,Math.min(EH*BS-H,p.y-H*.5));
  // Void sky
  const sky=ctx.createLinearGradient(0,0,0,H);
  sky.addColorStop(0,"#050010");sky.addColorStop(1,"#100020");
  ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);
  // End stars
  for(let i=0;i<60;i++){
    const sx=((i*173+17)%W),sy=((i*251+31)%(H*.6));
    ctx.fillStyle=`rgba(${150+i%80},${100+i%60},255,${.4+.3*Math.sin(Date.now()*.001+i)})`;
    ctx.beginPath();ctx.arc(sx,sy,1+i%2,0,Math.PI*2);ctx.fill();
  }
  // Blocks
  const bx0=Math.max(0,Math.floor(cx/BS)-1),bx1=Math.min(EW-1,Math.ceil((cx+W)/BS)+1);
  const by0=Math.max(0,Math.floor(cy/BS)-1),by1=Math.min(EH-1,Math.ceil((cy+H)/BS)+1);
  for(let by=by0;by<=by1;by++)for(let bx=bx0;bx<=bx1;bx++){
    const blk=G.endWorld[by][bx];if(!blk)continue;
    drawBlock(ctx,bx*BS-cx,by*BS-cy,blk,0,false,false);
  }
  // Fireballs
  G.endFireballs.forEach(fb=>{
    const fx=Math.round(fb.x-cx),fy=Math.round(fb.y-cy);
    ctx.fillStyle="#ff4400";ctx.beginPath();ctx.arc(fx,fy,8,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="#ffcc00";ctx.beginPath();ctx.arc(fx,fy,4,0,Math.PI*2);ctx.fill();
  });
  // Dragon
  if(G.dragon&&G.dragon.hp>0){
    const dr=G.dragon;
    drawDragon(ctx,Math.round(dr.x-cx),Math.round(dr.y-cy),dr.vx>0?1:-1,dr.flashT>0,W);
  }
  // Player
  drawPlayer(ctx,Math.round(p.x-cx),Math.round(p.y-cy),p.dir,p.flash>0);
  // Blueprint selection โ€” highlight each individually tapped block
  if(G.bpMode&&G.bpSelected?.size>0){
    G.bpSelected.forEach(key=>{
      const[bbx,bby]=key.split(',').map(Number);
      const bsx=bbx*BS-cx,bsy=bby*BS-cy;
      ctx.fillStyle="rgba(100,160,255,.3)";ctx.fillRect(bsx,bsy,BS,BS);
      ctx.strokeStyle="rgba(120,200,255,.9)";ctx.lineWidth=2;ctx.strokeRect(bsx+1,bsy+1,BS-2,BS-2);
      ctx.fillStyle="rgba(255,255,255,.8)";ctx.font="bold 9px monospace";ctx.textAlign="center";
      ctx.fillText("โœ“",bsx+BS/2,bsy+BS/2+4);ctx.textAlign="left";
    });
  }
  // HUD
  drawHUD(ctx,W,H,G);
  if(G.dragonDefeated){
    ctx.font="bold 16px monospace";ctx.fillStyle="#aa44ff";ctx.textAlign="center";
    ctx.fillText("๐Ÿ‰ Dragon Defeated โ€” Step on the portal!",W/2,H-60);ctx.textAlign="left";
  }
  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";
  }
}

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;
  if(G.dim==='end'){G.dim='over';if(G.overworldPos){G.player.x=G.overworldPos.x;G.player.y=G.overworldPos.y;}}
  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();updateEatBtn();updateFlightBtns();}
  const sc=skyColors(cycT);

  if(G.dim==="over"){
  // Zombie spawning
  if(isNight&&G.zombies.length<MAX_ZOMBIES&&!G.noMobs){
    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 & creeper lava damage
  if(G.dim==="over"){
    G.zombies.forEach(z=>{if(inLava(z.x,z.y,ZW,ZH)){z.lavaT=(z.lavaT||0)+1;if(z.lavaT%20===0){z.hp--;z.flash=8;}}else z.lavaT=0;
    if(z.onGround){const cbx=Math.floor(z.x/BS),cby=Math.floor(z.y/BS);if(cby>=0&&cby<WH&&cbx>=0&&cbx<WW){const cv=DATA[G.world[cby][cbx]]?.conveyor;if(cv)z.x+=cv*3.5;if(G.world[cby][cbx]===B.SPIKE){z.spikeT=(z.spikeT||0)+1;if(z.spikeT%20===0){z.hp--;z.flash=10;}}else z.spikeT=0;}}
  });
    G.creepers.forEach(c=>{if(inLava(c.x,c.y,CW,CH)){c.hp--;c.flash=8;}
    if(c.onGround){const cbx=Math.floor(c.x/BS),cby=Math.floor(c.y/BS);if(cby>=0&&cby<WH&&cbx>=0&&cbx<WW){const cv=DATA[G.world[cby][cbx]]?.conveyor;if(cv)c.x+=cv*3.5;if(G.world[cby][cbx]===B.SPIKE){c.spikeT=(c.spikeT||0)+1;if(c.spikeT%20===0){c.hp--;c.flash=10;}}else c.spikeT=0;}}
  });
    G.skeletons.forEach(sk=>{if(inLava(sk.x,sk.y,SKW,SKH)){sk.hp--;sk.flash=8;}
    if(sk.onGround){const cbx=Math.floor(sk.x/BS),cby=Math.floor(sk.y/BS);if(cby>=0&&cby<WH&&cbx>=0&&cbx<WW){const cv=DATA[G.world[cby][cbx]]?.conveyor;if(cv)sk.x+=cv*3.5;if(G.world[cby][cbx]===B.SPIKE){sk.spikeT=(sk.spikeT||0)+1;if(sk.spikeT%20===0){sk.hp--;sk.flash=10;}}else sk.spikeT=0;}}
  });
  }
  // Zombie AI - burn first, then filter dead ones
  const zombieWeakBlocks=new Set([B.DIRT,B.WOOD,B.PLANK,B.LEAVES,B.SAND,B.DOOR,B.FENCE,B.GATE]);
  G.zombies.forEach(z=>{
    if((z.taseT||0)>0){z.taseT--;z.vx=0;moveEnt(z,ZW,ZH);if(z.flash>0)z.flash--;return;}
    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);
    // Break weak blocks in path (but not gate-placed blocks)
    const zbx=Math.floor((z.x+z.dir*(ZW/2+2))/BS);
    const isGateBlock=pos=>{
      if(!G.drawbridgeStates)return false;
      return Object.values(G.drawbridgeStates).some(ds=>ds.blocks.includes(pos));
    };
    for(let zby=Math.floor(z.y/BS)-1;zby<=Math.floor(z.y/BS);zby++){
      if(zbx<0||zbx>=WW||zby<0||zby>=WH)continue;
      const blk=G.world[zby][zbx];
      if(zombieWeakBlocks.has(blk)){
        if(isGateBlock(`${zbx},${zby}`))break; // gate blocks are indestructible
        z.breakT=(z.breakT||0)+1;
        z.breakBx=zbx;z.breakBy=zby;
        if(z.breakT>=90){
          G.world[zby][zbx]=B.AIR;
          G.openDoors.delete(`${zbx},${zby}`);
          z.breakT=0;z.flash=8;
        }
        break;
      }
    }
    // Reset break timer if target changed
    if(z.breakBx!==Math.floor((z.x+z.dir*(ZW/2+2))/BS))z.breakT=0;
    // 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(!G.creative&&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 โ€” drop gunpowder if killed on spikes
  G.zombies.forEach(z=>{if(z.hp<=0&&(z.spikeT||0)>0)(G.drops=G.drops||[]).push({x:z.x,y:z.y-16,block:B.GUNPOWDER,count:1+Math.floor(Math.random()*2),life:1800,bobT:Math.random()*Math.PI*2});});
  G.zombies=G.zombies.filter(z=>z.hp>0);

  // Creeper spawn (night only)
  if(isNight&&G.creepers.length<MAX_CREEPERS&&!G.noMobs){
    G.creeperTimer++;
    if(G.creeperTimer>150){G.creeperTimer=0;
      for(let a=0;a<15;a++){
        const sx2=Math.floor(Math.random()*WW),sy2=G.surf[sx2];
        const wx=(sx2+.5)*BS,wy=sy2*BS;
        if(Math.abs(wx-p.x)>25*BS){G.creepers.push({x:wx,y:wy,vx:0,vy:0,onGround:false,hp:CREEPER_HP,fuse:0,flash:0,dmgT:0});break;}
      }
    }
  }
  if(!isNight)G.creepers=[];

  // Creeper AI
  G.creepers.forEach(c=>{
    if((c.taseT||0)>0){c.taseT--;c.vx=0;c.fuse=Math.max(0,c.fuse-2);moveEnt(c,CW,CH);if(c.flash>0)c.flash--;return;}
    c.vy=Math.min(c.vy+GRAVITY,MAX_FALL);
    const dx=p.x-c.x,dist=Math.hypot(dx,p.y-c.y);
    const dir=dx>0?1:-1;
    const los=hasLOS(c.x,c.y-CH/2,p.x,p.y-PH/2);
    if(dist>CREEPER_RANGE||!los){c.vx=dir*CREEPER_SPD;c.fuse=Math.max(0,c.fuse-2);}
    else{c.vx=0;c.fuse=Math.min(CREEPER_FUSE,c.fuse+1);}
    moveEnt(c,CW,CH);
    if(c.flash>0)c.flash--;
    // Explode!
    if(c.fuse>=CREEPER_FUSE){
      // Damage player only โ€” no block destruction
      if(!G.creative){p.hp=Math.max(0,p.hp-1);p.flash=20;p.dmgCD=20;
      if(!p.hp)document.getElementById("dead").classList.add("show");}
      c.hp=0;
    }
  });
  G.creepers.forEach(c=>{if(c.hp<=0&&c.fuse<CREEPER_FUSE)addItem(G.hotbar,G.inventory,B.GUNPOWDER,1+Math.floor(Math.random()*2));});
  G.creepers.forEach(c=>{if(c.hp<=0&&(c.spikeT||0)>0)(G.drops=G.drops||[]).push({x:c.x,y:c.y-16,block:B.GUNPOWDER,count:1+Math.floor(Math.random()*2),life:1800,bobT:Math.random()*Math.PI*2});});
  G.creepers=G.creepers.filter(c=>c.hp>0);

  // Skeleton spawn (night only)
  if(isNight&&(G.skeletons||[]).length<MAX_SKELETONS&&!G.noMobs){
    G.skeletonTimer=(G.skeletonTimer||0)+1;
    if(G.skeletonTimer>120){G.skeletonTimer=0;
      for(let a=0;a<15;a++){
        const sx2=Math.floor(Math.random()*WW),sy2=G.surf[sx2];
        const wx=(sx2+.5)*BS,wy=sy2*BS;
        if(Math.abs(wx-p.x)>22*BS){(G.skeletons=G.skeletons||[]).push({x:wx,y:wy,vx:0,vy:0,onGround:false,hp:SKELETON_HP,flash:0,dir:1,bowCD:60,burnT:0,onFire:false});break;}
      }
    }
  }
  if(!isNight)G.skeletons=[];

  // Skeleton AI
  (G.skeletons||[]).forEach(sk=>{
    if((sk.taseT||0)>0){sk.taseT--;sk.vx=0;moveEnt(sk,SKW,SKH);if(sk.flash>0)sk.flash--;return;}
    sk.vy=Math.min(sk.vy+GRAVITY,MAX_FALL);
    const dx=p.x-sk.x,dist=Math.hypot(dx,p.y-sk.y);
    const dir=dx>0?1:-1;sk.dir=dir;
    const prefRange=9*BS;
    const los=hasLOS(sk.x,sk.y-SKH/2,p.x,p.y-PH/2);
    if(dist<prefRange-BS){sk.vx=-dir*SKELETON_SPD;}
    else if(dist>prefRange+BS*3||!los){sk.vx=dir*SKELETON_SPD;}
    else{sk.vx*=0.4;}
    moveEnt(sk,SKW,SKH);
    if(sk.flash>0)sk.flash--;
    if(!isNight&&!isShielded(sk.x,sk.y)){sk.burnT=(sk.burnT||0)+1;if(sk.burnT%30===0){sk.hp=Math.max(0,sk.hp-1);sk.flash=25;sk.onFire=true;}}
    else{sk.burnT=0;sk.onFire=false;}
    // Shoot arrow
    sk.bowCD=(sk.bowCD||60)-1;
    if(sk.bowCD<=0&&dist<14*BS&&los){
      sk.bowCD=80+Math.floor(Math.random()*50);
      const ang=Math.atan2((p.y-PH*.5)-(sk.y-SKH*.6),p.x-sk.x);
      G.arrows.push({x:sk.x,y:sk.y-SKH*.6,vx:Math.cos(ang)*11,vy:Math.sin(ang)*11-0.5,life:85,dmg:1,fromSkeleton:true});
    }
  });
  // Skeleton arrows hit player
  G.arrows.forEach(a=>{
    if(!a.fromSkeleton||p.dmgCD>0||a.life<=0)return;
    if(Math.abs(a.x-p.x)<PW&&Math.abs(a.y-p.y)<PH){
      if(!G.creative){p.hp=Math.max(0,p.hp-a.dmg);p.flash=14;p.dmgCD=25;if(!p.hp)document.getElementById('dead').classList.add('show');}
      a.life=0;
    }
  });
  // Player arrows hit skeletons
  G.arrows.forEach(a=>{
    if(a.fromSkeleton||a.life<=0)return;
    (G.skeletons||[]).forEach(sk=>{if(a.life>0&&Math.abs(a.x-sk.x)<SKW&&Math.abs(a.y-sk.y)<SKH){sk.hp-=(a.dmg||3);sk.flash=12;a.life=0;}});
  });
  // Drops + filter
  (G.skeletons||[]).forEach(sk=>{if(sk.hp<=0)addItem(G.hotbar,G.inventory,B.ARROW,1+Math.floor(Math.random()*3));});
  (G.skeletons||[]).forEach(sk=>{if(sk.hp<=0&&(sk.spikeT||0)>0)(G.drops=G.drops||[]).push({x:sk.x,y:sk.y-16,block:B.GUNPOWDER,count:1+Math.floor(Math.random()*2),life:1800,bobT:Math.random()*Math.PI*2});});
  G.skeletons=(G.skeletons||[]).filter(sk=>sk.hp>0);

  // Sapling growth
  if(G.saplingTimers&&G.dim==="over"){
    Object.keys(G.saplingTimers).forEach(k=>{
      G.saplingTimers[k]--;
      if(G.saplingTimers[k]<=0){
        const[sbx,sby]=k.split(',').map(Number);
        delete G.saplingTimers[k];
        if(G.world[sby]?.[sbx]!==B.SAPLING)return;
        const trunk=3+Math.floor(Math.random()*3);
        for(let i=0;i<trunk;i++){const ty=sby-i;if(ty>=0)(G.world[ty][sbx]=B.WOOD);}
        for(let dy=-2;dy<=0;dy++)for(let dx=-2;dx<=2;dx++){
          const tx=sbx+dx,ty=sby-trunk+dy;
          if(tx<0||tx>=WW||ty<0||ty>=WH)continue;
          if(G.world[ty][tx]===B.AIR&&Math.abs(dx)+Math.abs(dy)<4)G.world[ty][tx]=B.LEAVES;
        }
        G.sleepMsg="๐ŸŒฑ A sapling grew into a tree!";setTimeout(()=>G.sleepMsg="",2000);
      }
    });
  }

  // Fish AI
  if(G.fish){
    G.fish.forEach(f=>{
      f.x+=f.vx;
      // Look ahead to check if the next position is still water
      const lookX=f.x+f.vx*18;
      const lookBx=Math.floor(lookX/BS);
      const inW=lookBx>=0&&lookBx<WW&&f.by>=0&&f.by<WH&&G.world[f.by][lookBx]===B.WATER;
      if(!inW){f.vx=-f.vx;f.x+=f.vx*2;}
      f.dir=f.vx>0?1:-1;
      // Keep y centered in water row with gentle bob
      f.y=(f.by+0.5)*BS;
    });
  }

  // Fan physics โ€” push entities upward in column above
  if(G.dim==="over"){
    const fbx0=Math.max(0,Math.floor(G.camera.x/BS)-1),fbx1=Math.min(WW-1,Math.ceil((G.camera.x+1200)/BS)+1);
    const fby0=Math.max(0,Math.floor(G.camera.y/BS)-1),fby1=Math.min(WH-1,Math.ceil((G.camera.y+800)/BS)+1);
    for(let fby=fby0;fby<=fby1;fby++)for(let fbx=fbx0;fbx<=fbx1;fbx++){
      if(G.world[fby][fbx]!==B.FAN)continue;
      const fanTop=fby*BS,fanRange=7*BS;
      const fx1=fbx*BS,fx2=(fbx+1)*BS;
      const push=(ent,w)=>{
        const ex=ent.x,ey=ent.y;
        if(ex+w/2>fx1&&ex-w/2<fx2&&ey<=fanTop&&ey>fanTop-fanRange){
          ent.vy=Math.max(ent.vy-2.8,-9);ent.onGround=false;
        }
      };
      push(p,PW);
      G.zombies.forEach(z=>push(z,ZW));
      G.creepers.forEach(c=>push(c,CW));
      (G.skeletons||[]).forEach(sk=>push(sk,SKW));
    }
  }
  // Elevator physics โ€” moving platforms
  if(G.dim==="over"){
    G.elevatorStates=G.elevatorStates||{};
    const ebx0=Math.max(0,Math.floor(G.camera.x/BS)-1),ebx1=Math.min(WW-1,Math.ceil((G.camera.x+1200)/BS)+1);
    const eby0=Math.max(0,Math.floor(G.camera.y/BS)-1),eby1=Math.min(WH-1,Math.ceil((G.camera.y+800)/BS)+1);
    for(let eby=eby0;eby<=eby1;eby++)for(let ebx=ebx0;ebx<=ebx1;ebx++){
      if(G.world[eby][ebx]!==B.ELEVATOR)continue;
      const k=`${ebx},${eby}`;
      if(!G.elevatorStates[k]){
        let minY=(eby-10)*BS;
        for(let cy2=eby-1;cy2>=0;cy2--){
          const blk2=G.world[cy2]?.[ebx];
          if(blk2&&!DATA[blk2]?.pass&&blk2!==B.ELEVATOR){minY=(cy2+1)*BS;break;}
        }
        G.elevatorStates[k]={y:eby*BS-8,vy:-1.5,minY,maxY:eby*BS-8};
      }
      const es=G.elevatorStates[k];
      es.y+=es.vy;
      if(es.y<=es.minY){es.y=es.minY;es.vy=Math.abs(es.vy);}
      if(es.y>=es.maxY){es.y=es.maxY;es.vy=-Math.abs(es.vy);}
      const ex1=ebx*BS-4,ex2=(ebx+1)*BS+4,platY=es.y;
      const ride=(ent,w)=>{
        if(ent.x+w/2>ex1&&ent.x-w/2<ex2&&ent.vy>=es.vy-0.5&&ent.y>platY-6&&ent.y<=platY+Math.abs(es.vy)+3){
          ent.y=platY;ent.vy=es.vy;ent.onGround=true;
        }
      };
      ride(p,PW);
      G.zombies.forEach(z=>ride(z,ZW));
      G.creepers.forEach(c=>ride(c,CW));
      (G.skeletons||[]).forEach(sk=>ride(sk,SKW));
    }
  }
  // Fireplace smoke emission
  if(G.smoke===undefined)G.smoke=[];
  if(G.dim==="over"&&Date.now()%3===0){
    const sbx0=Math.max(0,Math.floor(G.camera.x/BS)),sbx1=Math.min(WW-1,Math.ceil((G.camera.x+1400)/BS));
    const sby0=Math.max(0,Math.floor(G.camera.y/BS)),sby1=Math.min(WH-1,Math.ceil((G.camera.y+900)/BS));
    for(let sby=sby0;sby<=sby1;sby++)for(let sbx=sbx0;sbx<=sbx1;sbx++){
      if(G.world[sby][sbx]===B.FIREPLACE&&Math.random()<0.35){
        G.smoke.push({x:(sbx+0.5)*BS+(Math.random()-.5)*8,y:sby*BS,
          vx:(Math.random()-.5)*0.4,vy:-(1.6+Math.random()*1.2),
          life:500+Math.floor(Math.random()*200),maxLife:700,size:4+Math.random()*4,
          hitCeiling:false});
      }
    }
  }
  // Smoke particle physics with ceiling detection
  G.smoke=G.smoke||[];
  G.smoke.forEach(s=>{
    const smBx=Math.floor(s.x/BS),smByUp=Math.floor((s.y-s.size)/BS);
    const blockAbove=smByUp>=0&&smByUp<WH&&smBx>=0&&smBx<WW?G.world[smByUp]?.[smBx]:0;
    const solidAbove=blockAbove&&!DATA[blockAbove]?.pass&&!DATA[blockAbove]?.isChimney;
    if(solidAbove){
      // Hit ceiling โ€” spread horizontally, filling the space
      s.hitCeiling=true;
      s.vy=Math.min(s.vy+0.04,0.08); // barely drift down
      if(Math.abs(s.vx)<0.5)s.vx=(Math.random()<0.5?1:-1)*0.7;
      // Bounce off side walls
      const smBxL=Math.floor((s.x-s.size)/BS),smBxR=Math.floor((s.x+s.size)/BS);
      const rowBy=Math.floor(s.y/BS);
      if(smBxL>=0&&smBxL<WW&&G.world[rowBy]?.[smBxL]&&!DATA[G.world[rowBy][smBxL]]?.pass)s.vx=Math.abs(s.vx)+0.2;
      if(smBxR>=0&&smBxR<WW&&G.world[rowBy]?.[smBxR]&&!DATA[G.world[rowBy][smBxR]]?.pass)s.vx=-Math.abs(s.vx)-0.2;
    }else{
      // Free air โ€” keep rising, don't slow down much
      s.hitCeiling=false;
      if(s.vy>-(0.8+Math.random()*0.3))s.vy-=0.02; // gently accelerate upward
    }
    s.x+=s.vx;s.y+=s.vy;
    if(!s.hitCeiling)s.vx*=0.98; // slight horizontal damping when rising
    else s.vx+=(Math.random()-.5)*0.08;
    s.life--;s.size+=0.015;
  });
  G.smoke=G.smoke.filter(s=>s.life>0);
  if(G.villagers&&G.dim==="over"){
    G.villagers.forEach(v=>{
      if(!v.onGround)v.vy=Math.min(v.vy+GRAVITY,MAX_FALL);
      else v.vy=0;
      let nearestZombie=null,nearestDist=8*BS;
      G.zombies.forEach(z=>{const d=Math.hypot(z.x-v.x,z.y-v.y);if(d<nearestDist){nearestDist=d;nearestZombie=z;}});
      if(nearestZombie){
        v.runT=60;const fd=v.x>nearestZombie.x?1:-1;v.vx=fd*MOVE_SPD*1.3;v.dir=fd;
      }else if((v.runT||0)>0){
        v.runT--;v.vx=v.dir*MOVE_SPD*0.8;
      }else{
        v.wanderT=(v.wanderT||0)-1;
        if(v.wanderT<=0){v.wanderT=80+Math.floor(Math.random()*120);v.wanderDir=Math.random()<0.5?1:-1;}
        const distHome=v.x-v.homeX;
        if(Math.abs(distHome)>4*BS)v.wanderDir=distHome>0?-1:1;
        v.vx=v.wanderDir*MOVE_SPD*0.45;v.dir=v.wanderDir;
      }
      moveEnt(v,18,34);
    });
  }
  if(G.turretStates){
    const tbx0=Math.max(0,Math.floor(G.camera.x/BS)-1),tbx1=Math.min(WW-1,Math.ceil((G.camera.x+1200)/BS)+1);
    const tby0=Math.max(0,Math.floor(G.camera.y/BS)-1),tby1=Math.min(WH-1,Math.ceil((G.camera.y+800)/BS)+1);
    for(let tby=tby0;tby<=tby1;tby++)for(let tbx=tbx0;tbx<=tbx1;tbx++){
      if(G.world[tby][tbx]!==B.TURRET)continue;
      const k=`${tbx},${tby}`;const ts=G.turretStates[k];
      if(!ts?.gunType)continue;
      const tx=(tbx+.5)*BS,ty=(tby+.5)*BS;
      const range=13*BS;
      let target=null,bestDist=range;
      const check=m=>{const d=Math.hypot(m.x-tx,m.y-ty);if(d<bestDist&&hasLOS(tx,ty,m.x,m.y)){bestDist=d;target=m;}};
      G.zombies.forEach(check);G.creepers.forEach(check);(G.skeletons||[]).forEach(check);
      ts.cd=(ts.cd||0)-1;
      if(!target)continue;
      ts.angle=Math.atan2((target.y-20)-ty,target.x-tx);
      if(ts.cd>0)continue;
      const fireCD=ts.gunType==='mg'?8:ts.gunType==='flame'?5:35;
      ts.cd=fireCD;
      const ang=ts.angle;
      if(ts.gunType==='flame'){
        G.flames=G.flames||[];
        for(let i=0;i<4;i++){
          const sp=(Math.random()-.5)*0.35,sp2=5+Math.random()*4;
          G.flames.push({x:tx,y:ty-8,vx:Math.cos(ang+sp)*sp2,vy:Math.sin(ang+sp)*sp2,life:22+Math.floor(Math.random()*14),maxLife:36,size:5+Math.random()*6});
        }
        const fr2=range*.55;
        G.zombies.forEach(z=>{if(Math.hypot(z.x-tx,z.y-ty)<fr2){z.hp--;z.flash=10;}});
        G.creepers.forEach(c=>{if(Math.hypot(c.x-tx,c.y-ty)<fr2){c.hp--;c.flash=10;}});
        (G.skeletons||[]).forEach(sk=>{if(Math.hypot(sk.x-tx,sk.y-ty)<fr2){sk.hp--;sk.flash=10;}});
      }else{
        const spd=ts.gunType==='mg'?18:24,dmg=ts.gunType==='mg'?1:3;
        const boffset=BS*.65; // spawn at barrel tip, outside the turret block
        G.bullets=G.bullets||[];
        G.bullets.push({x:tx+Math.cos(ang)*boffset,y:ty+Math.sin(ang)*boffset-8,vx:Math.cos(ang)*spd,vy:Math.sin(ang)*spd,life:55,dmg,fromTurret:true});
      }
    }
  }

  // Lit TNT physics & explosion
  G.litTNT.forEach(t=>{
    t.vy=Math.min(t.vy+GRAVITY,MAX_FALL);
    const ny=t.y+t.vy;
    const bby=Math.floor((ny)/BS),bbx=Math.floor(t.x/BS);
    if(t.vy>0&&bbx>=0&&bbx<WW&&bby>=0&&bby<WH&&G.world[bby][bbx]&&!DATA[G.world[bby][bbx]]?.pass){t.y=bby*BS;t.vy=0;}
    else t.y=ny;
    t.fuse--;
    if(t.fuse<=0){
      // Big explosion: 5 block radius โ€” chain-lights nearby TNT
      const ebx=Math.floor(t.x/BS),eby=Math.floor(t.y/BS);
      for(let dy=-5;dy<=5;dy++)for(let dx2=-5;dx2<=5;dx2++){
        if(dx2*dx2+dy*dy>25)continue;
        const tx=ebx+dx2,ty=eby+dy;
        if(tx<0||tx>=WW||ty<0||ty>=WH)continue;
        if(G.world[ty][tx]===B.TNT){
          G.world[ty][tx]=B.AIR;
          G.litTNT.push({x:(tx+.5)*BS,y:ty*BS,vx:0,vy:0,fuse:20+Math.floor(Math.random()*40)});
        }else if(G.world[ty][tx]!==B.BEDROCK){G.world[ty][tx]=B.AIR;}
      }
      // Damage player
      if(!G.creative&&Math.hypot(G.player.x-t.x,G.player.y-t.y)<5*BS){G.player.hp=Math.max(0,G.player.hp-4);G.player.flash=25;G.player.dmgCD=20;if(!G.player.hp)document.getElementById("dead").classList.add("show");}
      // Kill zombies & creepers
      G.zombies.forEach(z=>{if(Math.hypot(z.x-t.x,z.y-t.y)<5*BS){z.hp=0;}});
      G.creepers.forEach(c=>{if(Math.hypot(c.x-t.x,c.y-t.y)<5*BS){c.hp=0;}});
      (G.skeletons||[]).forEach(sk=>{if(Math.hypot(sk.x-t.x,sk.y-t.y)<5*BS){sk.hp=0;}});
      t.fuse=-1;
    }
  });
  G.litTNT=G.litTNT.filter(t=>t.fuse>0);

  // Lit Bomb physics & ENORMOUS explosion (radius 12)
  G.litBombs=G.litBombs||[];
  G.litBombs.forEach(b=>{
    b.vy=Math.min(b.vy+GRAVITY,MAX_FALL);
    const ny=b.y+b.vy;
    const bby=Math.floor(ny/BS),bbx=Math.floor(b.x/BS);
    if(b.vy>0&&bbx>=0&&bbx<WW&&bby>=0&&bby<WH&&G.world[bby][bbx]&&!DATA[G.world[bby][bbx]]?.pass){b.y=bby*BS;b.vy=0;}
    else b.y=ny;
    b.fuse--;
    if(b.fuse<=0){
      const ebx=Math.floor(b.x/BS),eby=Math.floor(b.y/BS);
      const BOMB_R=12;
      // Carve enormous circular crater
      for(let dy2=-BOMB_R;dy2<=BOMB_R;dy2++)for(let dx2=-BOMB_R;dx2<=BOMB_R;dx2++){
        if(dx2*dx2+dy2*dy2>BOMB_R*BOMB_R)continue;
        const tx=ebx+dx2,ty=eby+dy2;
        if(tx<0||tx>=WW||ty<0||ty>=WH)continue;
        if(G.world[ty][tx]===B.BEDROCK)continue;
        // Chain-light any TNT or bombs in radius
        if(G.world[ty][tx]===B.TNT){G.world[ty][tx]=B.AIR;G.litTNT.push({x:(tx+.5)*BS,y:ty*BS,vx:0,vy:0,fuse:10+Math.floor(Math.random()*20)});}
        else if(G.world[ty][tx]===B.BOMB){G.world[ty][tx]=B.AIR;G.litBombs.push({x:(tx+.5)*BS,y:ty*BS,vx:0,vy:0,fuse:10+Math.floor(Math.random()*20)});}
        else G.world[ty][tx]=B.AIR;
      }
      // Massive player damage
      if(!G.creative&&Math.hypot(G.player.x-b.x,G.player.y-b.y)<BOMB_R*BS){G.player.hp=Math.max(0,G.player.hp-8);G.player.flash=40;G.player.dmgCD=30;if(!G.player.hp)document.getElementById("dead").classList.add("show");}
      // Obliterate all mobs in range
      G.zombies.forEach(z=>{if(Math.hypot(z.x-b.x,z.y-b.y)<BOMB_R*BS)z.hp=0;});
      G.creepers.forEach(c=>{if(Math.hypot(c.x-b.x,c.y-b.y)<BOMB_R*BS)c.hp=0;});
      (G.skeletons||[]).forEach(sk=>{if(Math.hypot(sk.x-b.x,sk.y-b.y)<BOMB_R*BS)sk.hp=0;});
      b.fuse=-1;
    }
  });
  G.litBombs=G.litBombs.filter(b=>b.fuse>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 or creeper
    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;}
    });
    G.creepers.forEach(c=>{
      if(a.life>0&&Math.abs(a.x-c.x)<CW&&Math.abs(a.y-c.y)<CH){c.hp-=(a.dmg||3);c.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);

  // Moon/Space launch countdown
  if(G.moonLaunch){
    G.moonLaunch.timer--;
    G.sleepMsg=G.moonLaunch.timer>0?`${G.moonLaunch.dest==="space"?"๐ŸŒŒ":"๐Ÿš€"} ${Math.ceil(G.moonLaunch.timer/60)}...`:"";
    if(G.moonLaunch.timer<=0){
      if(G.moonLaunch.dest==="space")enterSpace();
      else enterMoon();
      return;
    }
  }

  // Player
  if(p.atkCD>0)p.atkCD--;if(p.flash>0)p.flash--;if(p.dmgCD>0)p.dmgCD--;
  if((p.gunCD||0)>0)p.gunCD--;
  // Auto-fire machine gun / flamethrower while atk held
  if(touch.atk&&G.dim==="over"){
    const eq=G.hotbar[G.slot];
    if(eq?.count&&DATA[eq.block]?.isGun){const gt=DATA[eq.block].gunType;if(gt==='mg'||gt==='flame')tryFireGun();}
  }else if(!touch.atk){G.flameDmgT=0;}
  // Bullet physics
  G.bullets=G.bullets||[];
  G.bullets.forEach(b=>{
    b.vy+=0.1;b.x+=b.vx;b.y+=b.vy;b.life--;
    G.zombies.forEach(z=>{if(b.life>0&&Math.abs(b.x-z.x)<ZW&&Math.abs(b.y-z.y)<ZH){if(b.isTaser){z.taseT=600;b.life=0;}else{z.hp-=b.dmg;z.flash=12;b.life=0;}}});
    G.creepers.forEach(c=>{if(b.life>0&&Math.abs(b.x-c.x)<CW&&Math.abs(b.y-c.y)<CH){if(b.isTaser){c.taseT=600;b.life=0;}else{c.hp-=b.dmg;c.flash=12;b.life=0;}}});
    (G.skeletons||[]).forEach(sk=>{if(b.life>0&&Math.abs(b.x-sk.x)<SKW&&Math.abs(b.y-sk.y)<SKH){if(b.isTaser){sk.taseT=600;b.life=0;}else{sk.hp-=b.dmg;sk.flash=12;b.life=0;}}});
    const bbx=Math.floor(b.x/BS),bby=Math.floor(b.y/BS);
    if(bbx>=0&&bbx<WW&&bby>=0&&bby<WH){const blk=G.world[bby][bbx];if(blk&&!DATA[blk]?.pass&&blk!==B.TURRET)b.life=0;}
    // Rocket explosion on impact
    if(b.isRocket&&b.life<=0){
      const ebx=Math.floor(b.x/BS),eby=Math.floor(b.y/BS);
      for(let dy=-5;dy<=5;dy++)for(let dx2=-5;dx2<=5;dx2++){
        if(dx2*dx2+dy*dy>25)continue;
        const tx=ebx+dx2,ty=eby+dy;
        if(tx<0||tx>=WW||ty<0||ty>=WH||G.world[ty][tx]===B.BEDROCK)continue;
        if(G.world[ty][tx]===B.TNT){G.world[ty][tx]=B.AIR;G.litTNT.push({x:(tx+.5)*BS,y:ty*BS,vx:0,vy:0,fuse:20+Math.floor(Math.random()*30)});}
        else G.world[ty][tx]=B.AIR;
      }
      if(!G.creative&&Math.hypot(G.player.x-b.x,G.player.y-b.y)<5*BS){G.player.hp=Math.max(0,G.player.hp-4);G.player.flash=25;G.player.dmgCD=20;if(!G.player.hp)document.getElementById("dead").classList.add("show");}
      G.zombies.forEach(z=>{if(Math.hypot(z.x-b.x,z.y-b.y)<5*BS){z.hp=0;}});
      G.creepers.forEach(c=>{if(Math.hypot(c.x-b.x,c.y-b.y)<5*BS){c.hp=0;}});
      (G.skeletons||[]).forEach(sk=>{if(Math.hypot(sk.x-b.x,sk.y-b.y)<5*BS){sk.hp=0;}});
      // Spawn flame burst
      G.flames=G.flames||[];
      for(let i=0;i<20;i++){const a=Math.random()*Math.PI*2,s=3+Math.random()*8;G.flames.push({x:b.x,y:b.y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-3,life:25+Math.floor(Math.random()*20),maxLife:45,size:8+Math.random()*10});}
    }
  });
  G.bullets=G.bullets.filter(b=>b.life>0);
  // Flame particle physics (visual only โ€” damage handled in tryFireGun)
  G.flames=G.flames||[];
  G.flames.forEach(f=>{f.vx*=0.88;f.vy-=0.06;f.x+=f.vx;f.y+=f.vy;f.life--;});
  G.flames=G.flames.filter(f=>f.life>0);
  if(G.muzzleFlash){G.muzzleFlash.life--;if(G.muzzleFlash.life<=0)G.muzzleFlash=null;}
  // Ground drops โ€” pickup when player walks over
  G.drops=G.drops||[];
  G.drops.forEach(d=>{
    d.life--;d.bobT+=0.06;
    if(Math.abs(d.x-p.x)<22&&Math.abs(d.y-p.y)<36){
      addItem(G.hotbar,G.inventory,d.block,d.count);
      G.sleepMsg=`+${d.count} ${DATA[d.block]?.name||''}`;setTimeout(()=>G.sleepMsg="",1200);
      d.life=0;
    }
  });
  G.drops=G.drops.filter(d=>d.life>0);
  // Lava damage (2 hearts/sec = every 30 frames)
  if(!G.creative&&G.dim==="over"&&inLava(p.x,p.y,PW,PH)){
    p.lavaT=(p.lavaT||0)+1;
    if(p.lavaT%30===0){p.hp=Math.max(0,p.hp-2);p.flash=10;p.dmgCD=5;
    if(!p.hp)document.getElementById("dead").classList.add("show");}
  } else p.lavaT=0;
  // Conveyor belt effect on player
  if(p.onGround&&G.dim==="over"){
    const cbx=Math.floor(p.x/BS),cby=Math.floor(p.y/BS);
    if(cby>=0&&cby<WH&&cbx>=0&&cbx<WW){
      const cvDir=DATA[G.world[cby][cbx]]?.conveyor;
      if(cvDir)p.x+=cvDir*3.5;
      if(G.world[cby][cbx]===B.SPIKE&&!G.creative){
        p.spikeT=(p.spikeT||0)+1;
        if(p.spikeT%20===0){p.hp=Math.max(0,p.hp-1);p.flash=10;p.dmgCD=5;if(!p.hp)document.getElementById('dead').classList.add('show');}
      }else p.spikeT=0;
    }
  }
  // Heal 1 heart every 10 seconds during the day
  if(!isNight&&p.hp>0&&p.hp<MAX_HP){p.regenT=(p.regenT||0)+1;if(p.regenT>=600){p.hp=Math.min(MAX_HP,p.hp+1);p.regenT=0;}}
  // 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&&!G.jetting){p.vx=-MOVE_SPD;p.dir=-1;}else if(goR&&!G.jetting){p.vx=MOVE_SPD;p.dir=1;}else if(!G.jetting)p.vx*=.7;
  if(G.creative){
    const jumpJustPressed=goJ&&!p.prevJump;
    p.prevJump=goJ;
    const goDown=keys.has("ShiftLeft")||keys.has("Shift")||touch.down;
    if(!p.onGround){
      if(goJ){p.vy=-7;p.flyHover=true;}
      else if(goDown){p.vy=5;p.flyHover=false;}
      else if(p.flyHover){p.vy=0;}
      else p.vy=Math.min(p.vy+0.18,3);
    }else{
      p.flyHover=false;
      if(goJ){p.vy=JUMP_VEL;p.onGround=false;p.flyHover=true;}
      else p.vy=Math.min(p.vy+GRAVITY,MAX_FALL);
    }
  }
  const goD=keys.has("ShiftLeft")||keys.has("Shift")||touch.down;
  // Mount jet: Jump while holding JET item on ground
  if(!G.jetting&&G.dim==="over"){
    const eq=G.hotbar[G.slot];
    if(eq?.count>0&&eq.block===B.JET&&goJ&&p.onGround){G.jetting=true;G.jetHP=20;G.jetShootCD=0;p.vy=-5;G.sleepMsg="โœˆ๏ธ Jet mounted! Attack=shoot, Down=land & exit";setTimeout(()=>G.sleepMsg="",3000);}
  }
  if(G.jetting&&G.dim==="over"){
    // Jet flight physics
    if(goL){p.vx-=1.8;p.dir=-1;}
    else if(goR){p.vx+=1.8;p.dir=1;}
    else p.vx*=0.94;
    if(goJ)p.vy=Math.max(p.vy-2,-16); // boost up
    else if(goD)p.vy=Math.min(p.vy+1.5,12); // dive
    else p.vy+=GRAVITY*0.18; // gentle glide gravity
    p.vx=Math.max(-22,Math.min(22,p.vx));
    p.vy=Math.max(-16,Math.min(12,p.vy));
    // Shoot while attack held (auto-fire)
    G.jetShootCD=(G.jetShootCD||0)-1;
    if(touch.atk&&G.jetShootCD<=0){
      G.jetShootCD=7;G.jetGunFlash=6;
      G.bullets=G.bullets||[];
      const ang2=Math.atan2(p.vy,Math.abs(p.vx)*2);
      G.bullets.push({x:p.x+p.dir*44,y:p.y-12,vx:p.dir*26+Math.cos(ang2)*4,vy:Math.sin(ang2)*6,life:55,dmg:2});
    }
    if(G.jetGunFlash>0)G.jetGunFlash--;
    // Skeleton arrows damage jet
    G.arrows.forEach(a=>{
      if(!a.fromSkeleton||a.life<=0)return;
      if(Math.abs(a.x-p.x)<44&&Math.abs(a.y-p.y)<22){G.jetHP=Math.max(0,G.jetHP-2);a.life=0;G.jetGunFlash=3;}
    });
    // Crash if jet destroyed
    if(G.jetHP<=0){G.jetting=false;G.sleepMsg="๐Ÿ’ฅ Jet destroyed!";setTimeout(()=>G.sleepMsg="",2000);p.vy=-3;}
    // Exit: press down while grounded
    if(p.onGround&&goD){G.jetting=false;G.sleepMsg="Jet landed.";setTimeout(()=>G.sleepMsg="",1500);}
  }else if(!G.jetting){
    const swimming=inWater(p.x,p.y,PW,PH)&&G.dim==="over";
    const hasJetpack=G.hotbar.some(s=>s.count>0&&s.block===B.JETPACK)||
                     G.inventory.some(s=>s.count>0&&s.block===B.JETPACK);
    // Check if player overlaps a ladder block
    const onLadder=!swimming&&G.dim==="over"&&(()=>{
      const bx1=Math.floor((p.x-PW/2+2)/BS),bx2=Math.floor((p.x+PW/2-2)/BS);
      const by1=Math.floor((p.y-PH)/BS),by2=Math.floor(p.y/BS);
      for(let by=by1;by<=by2;by++)for(let bx=bx1;bx<=bx2;bx++){
        if(by>=0&&by<WH&&bx>=0&&bx<WW&&G.world[by][bx]===B.LADDER)return true;
      }return false;
    })();
    if(onLadder){
      p.vy=0;
      if(goJ)p.vy=-3;
      else if(goD)p.vy=2.5;
      p.onGround=true;
    }else if(swimming){
      p.vy=Math.min(p.vy+GRAVITY*0.15,1.8);
      if(goJ){p.vy=-3;}
      p.vx*=0.82;
    }else if(hasJetpack&&goJ&&!p.onGround){
      // Jetpack hover/thrust โ€” hold Jump to fly
      p.vy=Math.max(p.vy-1.8,-9);
      // Emit flame particles from jetpack
      G.flames=G.flames||[];
      if(Math.random()<0.5){
        G.flames.push({x:p.x+(Math.random()-.5)*8,y:p.y+2,
          vx:(Math.random()-.5)*2,vy:2+Math.random()*2,
          life:12+Math.floor(Math.random()*10),maxLife:22,size:4+Math.random()*4});
      }
    }else{
      if(goJ&&p.onGround){p.vy=JUMP_VEL*(G.dim==="moon"?1.6:1);p.onGround=false;}
      p.vy=Math.min(p.vy+GRAVITY*(G.dim==="moon"?0.1:1),G.dim==="moon"?5: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);}}
    }
  }

  } // end G.dim==="over" block

  // โ”€โ”€ Moon physics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if(G.dim==="moon"){    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));
    if(p.atkCD>0)p.atkCD--;if(p.flash>0)p.flash--;if(p.dmgCD>0)p.dmgCD--;
    const mgoL=keys.has('ArrowLeft')||keys.has('a')||keys.has('A')||touch.left;
    const mgoR=keys.has('ArrowRight')||keys.has('d')||keys.has('D')||touch.right;
    const mgoJ=keys.has('ArrowUp')||keys.has('w')||keys.has('W')||keys.has(' ')||touch.jump;
    if(mgoL){p.vx=-MOVE_SPD;p.dir=-1;}else if(mgoR){p.vx=MOVE_SPD;p.dir=1;}else p.vx*=.8;
    if(mgoJ&&p.onGround){p.vy=JUMP_VEL*1.8;p.onGround=false;}  // huge moon jump
    p.vy=Math.min(p.vy+GRAVITY*0.1,5); // moon gravity
    moveEnt(p,PW,PH);
    if(p.y>WH*BS+200){p.x=(Math.floor(WW/2)+.5)*BS;p.y=(G.moonSurf[Math.floor(WW/2)]-2)*BS;p.vx=p.vy=0;}
  }

  // โ”€โ”€ Space physics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
  if(G.dim==="space"){
    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));
    if(p.atkCD>0)p.atkCD--;if(p.flash>0)p.flash--;if(p.dmgCD>0)p.dmgCD--;
    const sgL=keys.has('ArrowLeft')||keys.has('a')||keys.has('A')||touch.left;
    const sgR=keys.has('ArrowRight')||keys.has('d')||keys.has('D')||touch.right;
    const sgJ=keys.has('ArrowUp')||keys.has('w')||keys.has('W')||keys.has(' ')||touch.jump;
    const sgD=keys.has('ShiftLeft')||keys.has('Shift')||touch.down;

    // Find nearest planet and compute radial gravity
    let gravX=0,gravY=0.04; // default: tiny landing-pad gravity
    let nearPlanet=null,nearDist=Infinity;
    SPACE_PLANETS.forEach(pl=>{
      const d=Math.hypot(p.x-pl.cx*BS,p.y-pl.cy*BS);
      if(d<nearDist){nearDist=d;nearPlanet=pl;}
    });
    const inAtmosphere=nearPlanet&&nearDist<(nearPlanet.r+9)*BS;
    if(inAtmosphere){
      const dx=nearPlanet.cx*BS-p.x,dy=nearPlanet.cy*BS-p.y;
      const d=Math.hypot(dx,dy)||1;
      const gs=GRAVITY*1.4*(1-Math.max(0,nearDist-(nearPlanet.r*BS))/(9*BS)); // stronger closer
      gravX=dx/d*gs; gravY=dy/d*gs;
    }

    // Movement (relative to player, not planet)
    if(sgL){p.vx-=1.4;p.dir=-1;}else if(sgR){p.vx+=1.4;p.dir=1;}else p.vx*=0.88;

    // Thrust (jetpack) when far from planet surface
    const onSurface=nearPlanet&&nearDist<(nearPlanet.r+2.5)*BS;
    const hasSJetpack=G.hotbar.some(s=>s.count>0&&s.block===B.JETPACK)||G.inventory.some(s=>s.count>0&&s.block===B.JETPACK);
    const spaceThrust=hasSJetpack?3.5:2.5; // jetpack gives stronger thrust in space
    if(!onSurface){
      if(sgJ){p.vx-=gravX*4;p.vy-=gravY*4+spaceThrust;
        // Jetpack flame in space
        if(hasSJetpack){G.flames=G.flames||[];if(Math.random()<0.6)G.flames.push({x:p.x+(Math.random()-.5)*8,y:p.y+2,vx:(Math.random()-.5)*2,vy:2+Math.random()*3,life:10+Math.floor(Math.random()*8),maxLife:18,size:4+Math.random()*5});}
      }
      if(sgD&&!inAtmosphere){p.vx+=gravX*2;p.vy+=gravY*2+1;}
    }else{
      if(sgJ&&p.onGround){const jm=hasSJetpack?7:5;p.vx-=gravX/Math.max(0.01,Math.abs(gravX||1))*jm;p.vy=hasSJetpack?JUMP_VEL*1.4:JUMP_VEL;}
    }

    // Apply radial gravity
    p.vx+=gravX; p.vy+=gravY;
    const spd=Math.hypot(p.vx,p.vy);
    if(spd>MAX_FALL){p.vx=p.vx/spd*MAX_FALL;p.vy=p.vy/spd*MAX_FALL;}

    // Block collision (moveEnt handles landing on block tops + side blocking)
    moveEnt(p,PW,PH);

    // Surface detection in gravity direction: check if solid block is "below" player
    // (in the direction of gravity) to set onGround for jumps
    if(inAtmosphere){
      const checkX=p.x+gravX*BS*0.6,checkY=p.y+gravY*BS*0.6;
      const cbx=Math.floor(checkX/BS),cby=Math.floor(checkY/BS);
      if(cbx>=0&&cbx<WW&&cby>=0&&cby<WH&&G.world[cby]?.[cbx]&&!DATA[G.world[cby][cbx]]?.pass){
        p.onGround=true;
        // Cancel gravity component when touching surface (stop sinking into planet)
        const gdot=p.vx*gravX+p.vy*gravY;
        if(gdot>0){p.vx-=gravX*0.8;p.vy-=gravY*0.8;}
      }
    }

    if(p.y>WH*BS)p.y=2*BS;
    // Oxygen drain
    const hasSuit=G.hotbar.some(s=>s.count>0&&s.block===B.SPACESUIT)||
                  G.inventory.some(s=>s.count>0&&s.block===B.SPACESUIT);
    if(!hasSuit&&!G.creative){
      G.oxygenT=(G.oxygenT||0)+1;
      if(G.oxygenT%180===0){p.hp=Math.max(0,p.hp-1);p.flash=14;if(!p.hp)document.getElementById("dead").classList.add("show");}
      if(G.oxygenT%60===0)G.sleepMsg="โš ๏ธ No spacesuit! Suffocating...";
    }else G.oxygenT=0;
  }

  // โ”€โ”€ Player + camera (both dims) โ”€โ”€
  if(G.dim==="over"){
    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));
  } else if(G.dim==="end"&&G.endWorld){
    // End physics
    const goLe=keys.has('ArrowLeft')||keys.has('a')||keys.has('A')||touch.left;
    const goRe=keys.has('ArrowRight')||keys.has('d')||keys.has('D')||touch.right;
    const goJe=keys.has('ArrowUp')||keys.has('w')||keys.has('W')||keys.has(' ')||touch.jump;
    const goDe=keys.has('ShiftLeft')||keys.has('Shift')||touch.down;
    if(goLe){p.vx=-MOVE_SPD;p.dir=-1;}else if(goRe){p.vx=MOVE_SPD;p.dir=1;}else p.vx*=.7;
    if(G.creative){
      // Creative flight in End
      if(goJe){p.vy=-6;p.onGround=false;}
      else if(goDe){p.vy=5;}
      else p.vy*=0.8;
    }else{
      if(goJe&&p.onGround){p.vy=JUMP_VEL;p.onGround=false;}
      p.vy=Math.min(p.vy+GRAVITY,MAX_FALL);
    }
    const nx3=p.x+p.vx;
    if(!solidEnd(nx3-PW/2,p.y-PH+2,PW,PH-4))p.x=nx3;else p.vx=0;
    p.x=Math.max(PW/2,Math.min(EW*BS-PW/2,p.x));
    const ny3=p.y+p.vy;p.onGround=false;
    if(p.vy>0){
      if(!solidEnd(p.x-PW/2+1,ny3-.5,PW-2,1))p.y=ny3;
      else{p.y=Math.floor(ny3/BS)*BS;p.vy=0;p.onGround=true;}
    }else if(p.vy<0){
      if(!solidEnd(p.x-PW/2+1,ny3-PH,PW-2,.5))p.y=ny3;
      else{p.y=(Math.floor((ny3-PH)/BS)+1)*BS+PH;p.vy=0;}
    }
    if(p.y>EH*BS+100){p.hp=0;document.getElementById("dead").classList.add("show");}
    // Dragon AI
    if(G.dragon&&G.dragon.hp>0){
      const dr=G.dragon;dr.flashT=Math.max(0,dr.flashT-1);
      const distP=Math.hypot(p.x-dr.x,p.y-dr.y);
      const portalX=EW/2*BS,portalY=21*BS; // end portal world position
      dr.angle+=0.015;
      // Portal rush timer โ€” charges every 20 seconds
      dr.portalRushCD=(dr.portalRushCD??1200)-1;
      if(dr.portalRushCD<=0&&!dr.portalRushing){
        dr.portalRushing=true;dr.portalRushDur=90; // rush lasts 1.5s
        G.sleepMsg="๐Ÿ‰ The dragon charges the portal!";setTimeout(()=>G.sleepMsg="",2000);
      }
      if(dr.portalRushing){
        // Accelerate hard toward portal
        const pdx=portalX-dr.x,pdy=portalY-dr.y;
        const pdist=Math.hypot(pdx,pdy);
        if(pdist>1){dr.vx=pdx/pdist*14;dr.vy=pdy/pdist*14;}
        dr.portalRushDur--;
        // Reached portal โ€” switch to hovering
        if(pdist<50||dr.portalRushDur<=0){
          dr.portalRushing=false;dr.portalHovering=true;dr.portalHoverDur=600; // 10 seconds
          dr.vx=0;dr.vy=0;
          G.sleepMsg="๐Ÿ‰ The dragon guards the portal!";setTimeout(()=>G.sleepMsg="",2500);
        }
        if(!G.creative&&distP<55&&p.dmgCD<=0){p.hp=Math.max(0,p.hp-3);p.flash=20;p.dmgCD=40;if(!p.hp)document.getElementById("dead").classList.add("show");}
      }else if(dr.portalHovering){
        // Hover at portal for 10 seconds, gently bobbing
        dr.portalHoverDur--;
        const bob=Math.sin(Date.now()*.002)*20;
        dr.vx=(portalX-dr.x)*.06;
        dr.vy=((portalY-30+bob)-dr.y)*.06;
        // Deal contact damage and fire more frequently while hovering
        dr.fireCD=(dr.fireCD||0)-1;
        if(dr.fireCD<=0){dr.fireCD=60;const ang3=Math.atan2(p.y-dr.y,p.x-dr.x);G.endFireballs.push({x:dr.x,y:dr.y,vx:Math.cos(ang3)*7,vy:Math.sin(ang3)*7,life:120});}
        if(!G.creative&&distP<55&&p.dmgCD<=0){p.hp=Math.max(0,p.hp-2);p.flash=20;p.dmgCD=40;if(!p.hp)document.getElementById("dead").classList.add("show");}
        if(dr.portalHoverDur<=0){dr.portalHovering=false;dr.portalRushCD=1200;G.sleepMsg="๐Ÿ‰ The dragon returns to the sky!";setTimeout(()=>G.sleepMsg="",2000);}
      } else if(dr.hp<dr.maxHp*.6&&(dr.diveCD||200)>0){
        dr.diveCD=(dr.diveCD||200)-1;
        const tx=p.x,ty=p.y-PH*.5;
        dr.vx+=(tx-dr.x)*.008;dr.vy+=(ty-dr.y)*.008;
        const spd=Math.hypot(dr.vx,dr.vy);if(spd>8){dr.vx=dr.vx/spd*8;dr.vy=dr.vy/spd*8;}
        if(distP<40||dr.y>18*BS){dr.vx*=.6;dr.vy*=.6;dr.diveCD=300;}
      }else if(!dr.portalRushing){
        const rx=EW*BS/2+Math.cos(dr.angle)*18*BS,ry=5*BS+Math.sin(dr.angle*2)*.5*BS;
        dr.vx=(rx-dr.x)*.04;dr.vy=(ry-dr.y)*.04;
      }
      dr.x+=dr.vx;dr.y+=dr.vy;
      dr.x=Math.max(3*BS,Math.min((EW-3)*BS,dr.x));
      dr.y=Math.max(2*BS,Math.min((dr.portalRushing||dr.portalHovering)?22*BS:15*BS,dr.y));
      dr.fireCD=(dr.fireCD||0)-1;
      if(!dr.portalHovering){if(dr.fireCD<=0){dr.fireCD=150;const ang3=Math.atan2(p.y-dr.y,p.x-dr.x);G.endFireballs.push({x:dr.x,y:dr.y,vx:Math.cos(ang3)*7,vy:Math.sin(ang3)*7,life:120});}}
      if(!G.creative&&!dr.portalRushing&&!dr.portalHovering&&distP<40&&p.dmgCD<=0){p.hp=Math.max(0,p.hp-2);p.flash=20;p.dmgCD=40;if(!p.hp)document.getElementById("dead").classList.add("show");}
    }
    G.endFireballs=G.endFireballs||[];
    G.endFireballs.forEach(fb=>{
      fb.x+=fb.vx;fb.y+=fb.vy;fb.vy+=.1;fb.life--;
      if(!G.creative&&Math.hypot(fb.x-p.x,fb.y-p.y)<22&&p.dmgCD<=0){p.hp=Math.max(0,p.hp-2);p.flash=20;p.dmgCD=40;if(!p.hp)document.getElementById("dead").classList.add("show");fb.life=0;}
      if(solidEnd(fb.x-4,fb.y-4,8,8))fb.life=0;
    });
    G.endFireballs=G.endFireballs.filter(f=>f.life>0);
    G.arrows.forEach(a=>{
      if(G.dragon&&G.dragon.hp>0&&a.life>0&&Math.hypot(a.x-G.dragon.x,a.y-G.dragon.y)<40){G.dragon.hp-=(a.dmg||3);G.dragon.flashT=15;a.life=0;}
    });
    if(G.dragon&&G.dragon.hp<=0&&!G.dragonDefeated){
      G.dragonDefeated=true;G.dragon=null;
      addItem(G.hotbar,G.inventory,B.DRAGON_EGG,1);
      G.sleepMsg="๐Ÿ‰ Dragon defeated! Step on the portal to return home.";setTimeout(()=>G.sleepMsg="",5000);
    }
    G.camera.x=Math.max(0,Math.min(EW*BS-W,p.x-W/2));
    G.camera.y=Math.max(0,Math.min(EH*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,minedBlock=G.world[by][bx];
      const eq=G.hotbar[G.slot];
      const hasSilk=eq?.enchant==="silk_touch";
      const drop=hasSilk&&SILK_DROPS[minedBlock]!=null?SILK_DROPS[minedBlock]:DATA[minedBlock]?.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];}
      if(G.world[by][bx]===B.CHEST){const cs=G.chestContents[mKey];if(cs)cs.forEach(s=>{if(s.count>0)addItem(G.hotbar,G.inventory,s.block,s.count,s.enchant);});delete G.chestContents[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);
      // Drop sapling when breaking wood or leaves
      if(minedBlock===B.WOOD||(minedBlock===B.LEAVES&&Math.random()<0.2)){
        G.drops=G.drops||[];
        G.drops.push({x:(bx+.5)*BS,y:(by-.5)*BS,block:B.SAPLING,count:1,life:1800,bobT:Math.random()*Math.PI*2});
      }
      // Remove sapling timer if block was a sapling
      if(minedBlock===B.SAPLING){delete (G.saplingTimers||{})[mKey];}
    }
  }else if(!G.mouseDown)G.mining=null;

  // โ”€โ”€ Render โ”€โ”€
  const cx=G.camera.x,cy=G.camera.y;
  if(G.dim==="end"){renderEnd(ctx,W,H);raf=requestAnimationFrame(loop);return;}
  if(G.dim==="space"){
    // Deep space background
    ctx.fillStyle="#000008";ctx.fillRect(0,0,W,H);
    // Nebula clouds
    const t7=Date.now()*.0003;
    [[W*.2,H*.3,"rgba(80,0,120,.08)"],[W*.7,H*.6,"rgba(0,60,120,.07)"],[W*.5,H*.2,"rgba(120,40,0,.06)"]].forEach(([nx,ny,nc])=>{
      const ng=ctx.createRadialGradient(nx,ny,0,nx,ny,200);ng.addColorStop(0,nc);ng.addColorStop(1,"transparent");
      ctx.fillStyle=ng;ctx.fillRect(0,0,W,H);
    });
    // Dense stars
    const scx=G.camera.x,scy=G.camera.y;
    for(let i=0;i<200;i++){
      const sx2=((i*137+scx*.1)%W+W)%W,sy2=((i*251+scy*.1)%H+H)%H;
      const br=0.4+0.5*Math.sin(t7*60+i);
      ctx.fillStyle=`rgba(255,255,255,${br.toFixed(2)})`;
      ctx.beginPath();ctx.arc(sx2,sy2,i%5===0?1.5:0.7,0,Math.PI*2);ctx.fill();
    }
    // Planet name labels
    SPACE_PLANETS.forEach(pl=>{
      const px=(pl.cx+.5)*BS-scx,py=(pl.cy-pl.r-2)*BS-scy;
      if(px>-50&&px<W+50&&py>-30&&py<H+30){
        ctx.fillStyle="rgba(200,220,255,.5)";ctx.font="bold 11px monospace";ctx.textAlign="center";
        ctx.fillText(pl.name,px,py);ctx.textAlign="left";
      }
    });
    // Render space world blocks
    const mbx0=Math.max(0,Math.floor(scx/BS)),mbx1=Math.min(WW-1,Math.ceil((scx+W)/BS));
    const mby0=Math.max(0,Math.floor(scy/BS)),mby1=Math.min(WH-1,Math.ceil((scy+H)/BS));
    for(let mby=mby0;mby<=mby1;mby++)for(let mbx=mbx0;mbx<=mbx1;mbx++){
      const blk=G.world[mby][mbx];if(!blk)continue;
      drawBlock(ctx,mbx*BS-scx,mby*BS-scy,blk,0,!!(G.hover?.bx===mbx&&G.hover?.by===mby),G.openDoors.has(`${mbx},${mby}`));
    }
    // Draw player (with spacesuit tint if wearing one)
    const hasSuit=G.hotbar.some(s=>s.count>0&&s.block===B.SPACESUIT)||G.inventory.some(s=>s.count>0&&s.block===B.SPACESUIT);
    drawPlayer(ctx,Math.round(p.x-scx),Math.round(p.y-scy),p.dir,p.flash>0);
    if(hasSuit){
      // White helmet visor overlay
      ctx.fillStyle="rgba(200,230,255,.25)";ctx.fillRect(Math.round(p.x-scx)-8,Math.round(p.y-scy)-PH,16,PH);
    }
    // Oxygen bar (if no suit)
    if(!hasSuit&&!G.creative){
      const oxy=Math.max(0,1-((G.oxygenT||0)%180)/180);
      ctx.fillStyle="rgba(0,0,0,.5)";ctx.fillRect(W/2-40,10,80,8);
      ctx.fillStyle=oxy>0.4?"#00aaff":"#ff4400";ctx.fillRect(W/2-40,10,80*oxy,8);
      ctx.fillStyle="#fff";ctx.font="9px monospace";ctx.textAlign="center";ctx.fillText("Oโ‚‚",W/2,20);ctx.textAlign="left";
    }
    // "Tap ๐Ÿš€ to return" hint
    ctx.fillStyle="rgba(255,255,255,.3)";ctx.font="bold 11px monospace";ctx.textAlign="center";
    ctx.fillText("๐ŸŒŒ SPACE โ€” Tap ๐Ÿš€ to return",W/2,H-12);ctx.textAlign="left";
    drawHUD(ctx,W,H,G);
    raf=requestAnimationFrame(loop);return;
  }
  if(G.dim==="moon"){
    // Moon sky โ€” black with stars always visible
    const moonSky=ctx.createLinearGradient(0,0,0,H);moonSky.addColorStop(0,"#000005");moonSky.addColorStop(1,"#0a0a18");
    ctx.fillStyle=moonSky;ctx.fillRect(0,0,W,H);
    // Stars (always bright on moon)
    stars.forEach(s=>{const sx2=s.x-cx,sy2=s.y-cy;if(sx2<-10||sx2>W+10||sy2<-10||sy2>H)return;ctx.fillStyle="rgba(255,255,255,.9)";ctx.beginPath();ctx.arc(sx2,sy2,s.r,0,Math.PI*2);ctx.fill();});
    // Earth in sky
    const er=55,ex=W*.25,ey=H*.2;
    const egrd=ctx.createRadialGradient(ex-12,ey-12,0,ex,ey,er);
    egrd.addColorStop(0,"#3a9aff");egrd.addColorStop(0.4,"#2277dd");egrd.addColorStop(0.7,"#1a5fbb");egrd.addColorStop(1,"#0a2040");
    ctx.fillStyle=egrd;ctx.beginPath();ctx.arc(ex,ey,er,0,Math.PI*2);ctx.fill();
    // Continents
    ctx.fillStyle="rgba(60,180,60,.8)";ctx.beginPath();ctx.ellipse(ex-10,ey-5,22,15,0.3,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="rgba(60,170,60,.75)";ctx.beginPath();ctx.ellipse(ex+15,ey+10,14,10,-0.5,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="rgba(255,255,255,.25)";ctx.beginPath();ctx.ellipse(ex+8,ey-20,18,6,0.2,0,Math.PI*2);ctx.fill();
    // Earth glow
    ctx.strokeStyle="rgba(100,180,255,.3)";ctx.lineWidth=4;ctx.beginPath();ctx.arc(ex,ey,er+2,0,Math.PI*2);ctx.stroke();
    // "Moon" label and dimension indicator
    ctx.fillStyle="rgba(255,255,255,.4)";ctx.font="bold 13px monospace";ctx.textAlign="center";ctx.fillText("๐ŸŒ• MOON",W/2,20);ctx.textAlign="left";
    // Now render moon world blocks normally using existing draw code
    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));
    const mcx=G.camera.x,mcy=G.camera.y;
    const mbx0=Math.max(0,Math.floor(mcx/BS)),mbx1=Math.min(WW-1,Math.ceil((mcx+W)/BS));
    const mby0=Math.max(0,Math.floor(mcy/BS)),mby1=Math.min(WH-1,Math.ceil((mcy+H)/BS));
    for(let mby=mby0;mby<=mby1;mby++)for(let mbx=mbx0;mbx<=mbx1;mbx++){
      const blk=G.world[mby][mbx];if(!blk)continue;
      const bsx=mbx*BS-mcx,bsy=mby*BS-mcy;
      const isHov=G.hover&&G.hover.bx===mbx&&G.hover.by===mby;
      drawBlock(ctx,bsx,bsy,blk,isHov);
    }
    drawPlayer(ctx,Math.round(p.x-mcx),Math.round(p.y-mcy),p.dir,p.flash>0);
    drawHUD(ctx,W,H,G);
    raf=requestAnimationFrame(loop);return;
  }

  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}`));
  }

  // Fish (drawn before entities so they appear behind mobs)
  if(G.fish){G.fish.forEach(f=>drawFish(ctx,Math.round(f.x-cx),Math.round(f.y-cy),f.dir,f.type,f.phase));}

  // Elevator moving platforms
  if(G.elevatorStates){
    Object.entries(G.elevatorStates).forEach(([k,es])=>{
      const[ebx,eby]=k.split(',').map(Number);
      if(G.world[eby]?.[ebx]!==B.ELEVATOR)return;
      const sx2=ebx*BS-cx,platY=Math.round(es.y-cy);
      // Faint rail guides
      ctx.strokeStyle="rgba(120,100,70,.35)";ctx.lineWidth=3;ctx.setLineDash([4,6]);
      ctx.beginPath();ctx.moveTo(sx2,platY);ctx.lineTo(sx2,0);ctx.stroke();
      ctx.beginPath();ctx.moveTo(sx2+BS,platY);ctx.lineTo(sx2+BS,0);ctx.stroke();
      ctx.setLineDash([]);
      // Platform plank
      ctx.fillStyle="#7a5020";ctx.fillRect(sx2-4,platY,BS+8,10);
      ctx.fillStyle="#c8a060";ctx.fillRect(sx2-4,platY,BS+8,4);
      ctx.fillStyle="#a07040";ctx.fillRect(sx2-4,platY+4,BS+8,6);
      // Rail grip pins
      ctx.fillStyle="#999";ctx.fillRect(sx2-2,platY+2,4,6);ctx.fillRect(sx2+BS-2,platY+2,4,6);
      // Direction arrow above platform
      ctx.fillStyle="rgba(255,255,255,.85)";ctx.font="bold 13px monospace";ctx.textAlign="center";
      ctx.fillText(es.vy<0?"โ†‘":"โ†“",sx2+BS/2,platY-3);ctx.textAlign="left";
    });
  }

  // Smoke particles from fireplaces
  (G.smoke||[]).forEach(s=>{
    const t=s.life/s.maxLife;
    const alpha=Math.min(t*3,.42)*(t<0.2?t*5:1);
    ctx.fillStyle=`rgba(170,170,170,${alpha.toFixed(2)})`;
    ctx.beginPath();ctx.arc(Math.round(s.x-cx),Math.round(s.y-cy),s.size,0,Math.PI*2);ctx.fill();
  });

  // Ground drops
  (G.drops||[]).forEach(d=>{
    const dx=Math.round(d.x-cx),dy=Math.round(d.y-cy-Math.sin(d.bobT)*4);
    const fade=Math.min(1,d.life/60); // fade out in last second
    ctx.globalAlpha=fade;
    // Shadow
    ctx.fillStyle="rgba(0,0,0,.3)";ctx.beginPath();ctx.ellipse(dx,Math.round(d.y-cy)+4,9,3,0,0,Math.PI*2);ctx.fill();
    // Item square
    ctx.fillStyle=DATA[d.block]?.col||"#888";
    ctx.fillRect(dx-9,dy-9,18,18);
    ctx.fillStyle="rgba(255,255,255,.2)";ctx.fillRect(dx-9,dy-9,18,4);
    ctx.strokeStyle="rgba(255,255,255,.6)";ctx.lineWidth=1.5;ctx.strokeRect(dx-9,dy-9,18,18);
    // Icon
    if(DATA[d.block]?.icon){ctx.font="13px serif";ctx.textAlign="center";ctx.fillText(DATA[d.block].icon,dx,dy+5);ctx.textAlign="left";}
    // Count badge
    if(d.count>1){ctx.fillStyle="#fff";ctx.font="bold 8px monospace";ctx.textAlign="center";ctx.fillText(d.count,dx+7,dy+9);ctx.textAlign="left";}
    ctx.globalAlpha=1;
  });

  // Electric arcs on tased mobs
  const drawTaseEffect=(mx,my,w,h)=>{
    const t4=Date.now()*.012;
    ctx.strokeStyle="rgba(180,255,80,.9)";ctx.lineWidth=1.5;
    for(let i=0;i<5;i++){
      const a=(t4+i*1.26)%(Math.PI*2);
      const r=w*.7+Math.sin(t4*3+i)*6;
      ctx.beginPath();
      ctx.moveTo(mx+Math.cos(a)*r,my+Math.sin(a)*r*.6);
      ctx.lineTo(mx+Math.cos(a+0.5)*r*.4+Math.sin(t4+i)*8,my+Math.sin(a+0.5)*r*.4*.6);
      ctx.stroke();
    }
    ctx.fillStyle="rgba(220,255,100,.2)";ctx.beginPath();ctx.ellipse(mx,my,w*.8,h*.5,0,0,Math.PI*2);ctx.fill();
  };
  G.zombies.forEach(z=>{if((z.taseT||0)>0)drawTaseEffect(Math.round(z.x-cx),Math.round(z.y-cy-ZH/2),ZW,ZH);});
  G.creepers.forEach(c=>{if((c.taseT||0)>0)drawTaseEffect(Math.round(c.x-cx),Math.round(c.y-cy-CH/2),CW,CH);});
  (G.skeletons||[]).forEach(sk=>{if((sk.taseT||0)>0)drawTaseEffect(Math.round(sk.x-cx),Math.round(sk.y-cy-SKH/2),SKW,SKH);});

  // 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));
  G.creepers.forEach(c=>drawCreeper(ctx,Math.round(c.x-cx),Math.round(c.y-cy),c.fuse,c.flash>0));
  (G.skeletons||[]).forEach(sk=>drawSkeleton(ctx,Math.round(sk.x-cx),Math.round(sk.y-cy),sk.dir,sk.flash>0,sk.hp,sk.bowCD||60));
  (G.villagers||[]).forEach(v=>drawVillager(ctx,Math.round(v.x-cx),Math.round(v.y-cy),v.dir,v.type,false));
  // 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";}
  }
  // Draw lit TNT
  G.litTNT.forEach(t=>{
    const tx=Math.round(t.x-cx)-BS/2,ty=Math.round(t.y-cy)-BS;
    const flash=Math.floor(t.fuse/8)%2===0;
    ctx.fillStyle=flash?"#ff8888":"#cc2222";ctx.fillRect(tx,ty,BS,BS);
    ctx.fillStyle="#fff";ctx.font=`bold ${BS*.28}px monospace`;ctx.textAlign="center";
    ctx.fillText("TNT",tx+BS/2,ty+BS*.58);ctx.textAlign="left";
    // Fuse spark
    ctx.fillStyle="#ffcc00";ctx.beginPath();ctx.arc(tx+BS/2,ty-3+(Math.sin(Date.now()*.05)*3),3,0,Math.PI*2);ctx.fill();
    // Fuse countdown bar
    const bw=32,bx2=tx+BS/2-bw/2,by2=ty-14;
    ctx.fillStyle="rgba(0,0,0,.5)";ctx.fillRect(bx2,by2,bw,5);
    ctx.fillStyle="#ff4400";ctx.fillRect(bx2,by2,bw*(t.fuse/90),5);
  });
  // Draw lit bombs
  (G.litBombs||[]).forEach(b=>{
    const bx3=Math.round(b.x-cx),by3=Math.round(b.y-cy)-BS*.6;
    const flashing=b.fuse<60&&b.fuse%8<4;
    ctx.fillStyle=flashing?"#ff2200":"#111";
    ctx.beginPath();ctx.arc(bx3,by3,BS*.36,0,Math.PI*2);ctx.fill();
    if(!flashing){ctx.fillStyle="rgba(255,255,255,.18)";ctx.beginPath();ctx.arc(bx3-5,by3-6,BS*.11,0,Math.PI*2);ctx.fill();}
    // Fuse rope
    ctx.strokeStyle="#8B6340";ctx.lineWidth=2.5;ctx.lineCap="round";
    ctx.beginPath();ctx.moveTo(bx3,by3-BS*.36);ctx.bezierCurveTo(bx3+8,by3-BS*.52,bx3+13,by3-BS*.46,bx3+11,by3-BS*.34);ctx.stroke();
    ctx.lineCap="butt";
    // Sparking fuse tip
    const t3=Date.now()*.008;
    const sx3=bx3+11+Math.sin(t3*4)*2,sy3=by3-BS*.34+Math.cos(t3*3)*2;
    ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(sx3,sy3,3.5,0,Math.PI*2);ctx.fill();
    ctx.fillStyle="#ffcc00";ctx.beginPath();ctx.arc(sx3,sy3,2,0,Math.PI*2);ctx.fill();
    for(let i=0;i<5;i++){
      const sa=(t3*6+i*1.26)%(Math.PI*2),sd=3+Math.sin(t3*4+i)*3;
      ctx.fillStyle=i%2?"#ff6600":"#ffee00";
      ctx.beginPath();ctx.arc(sx3+Math.cos(sa)*sd,sy3+Math.sin(sa)*sd,1.5,0,Math.PI*2);ctx.fill();
    }
    // Countdown
    const secs=Math.ceil(b.fuse/60);
    ctx.fillStyle=flashing?"#ff4400":"rgba(255,255,255,.9)";ctx.font="bold 12px monospace";ctx.textAlign="center";
    ctx.fillText(secs+"s",bx3,by3-BS*.55);ctx.textAlign="left";
    // Fuse countdown bar
    const bw2=36,bbx=bx3-bw2/2,bby2=by3-BS*.72;
    ctx.fillStyle="rgba(0,0,0,.5)";ctx.fillRect(bbx,bby2,bw2,5);
    ctx.fillStyle=flashing?"#ff2200":"#ff8800";ctx.fillRect(bbx,bby2,bw2*(b.fuse/150),5);
  });
  if(G.jetting){
    drawJet(ctx,Math.round(p.x-cx),Math.round(p.y-cy-14),p.dir,p.vx,p.vy,G.jetHP||0,(G.jetGunFlash||0)>0);
    if(Math.abs(p.vx)>2){G.flames=G.flames||[];if(Math.random()<0.4){const ex=p.x-p.dir*38,ey=p.y-14;G.flames.push({x:ex+(Math.random()-.5)*6,y:ey+(Math.random()-.5)*6,vx:-p.dir*(3+Math.random()*5),vy:(Math.random()-.5)*2,life:12+Math.floor(Math.random()*10),maxLife:22,size:4+Math.random()*5});}}
  }else{
    drawPlayer(ctx,Math.round(p.x-cx),Math.round(p.y-cy),p.dir,p.flash>0);
    // Draw jetpack on player's back if equipped
    if(G.hotbar.some(s=>s.count>0&&s.block===B.JETPACK)||G.inventory.some(s=>s.count>0&&s.block===B.JETPACK)){
      const px3=Math.round(p.x-cx),py3=Math.round(p.y-cy);
      const bx3=p.dir>0?px3-10:px3+6;
      ctx.fillStyle="#445566";ctx.fillRect(bx3,py3-PH*.7,6,14);
      ctx.fillStyle="#778899";ctx.fillRect(bx3+1,py3-PH*.7,4,12);
      ctx.fillStyle="#aabbcc";ctx.fillRect(bx3+1,py3-PH*.65,4,3);
      // Nozzle
      ctx.fillStyle="#334";ctx.fillRect(bx3+1,py3-PH*.32,4,4);
    }
  }
  {const eq=G.hotbar[G.slot];if(eq?.count&&DATA[eq.block]?.isGun){
    const px2=Math.round(p.x-cx),py2=Math.round(p.y-cy);
    const gd=DATA[eq.block];const gx=p.dir>0?px2+8:px2-8,gy=py2-Math.round(PH*.5);
    if(gd.gunType==='flame'){
      ctx.fillStyle="#884400";ctx.fillRect(gx+(p.dir>0?-2:-12),gy-4,14,8);
      ctx.fillStyle="#cc6600";ctx.fillRect(gx+(p.dir>0?10:-14),gy-3,6,6);
      ctx.fillStyle="#ff9900";ctx.fillRect(gx+(p.dir>0?14:-18),gy-2,4,4);
    }else{
      ctx.fillStyle="#555";ctx.fillRect(gx+(p.dir>0?-2:-14),gy-4,16,7);
      ctx.fillStyle="#888";ctx.fillRect(gx+(p.dir>0?-5:-9),gy-5,8,9);
      ctx.fillStyle="#333";ctx.fillRect(gx+(p.dir>0?12:-16),gy-2,4,4);
      if(gd.gunType==='mg'){ctx.fillStyle="#666";ctx.fillRect(gx+(p.dir>0?-3:-15),gy+2,18,4);}
    }
  }}
  // Muzzle flash
  if(G.muzzleFlash){
    const mf=G.muzzleFlash,mx=Math.round(mf.x-cx),my=Math.round(mf.y-cy);
    if(mf.taser){
      ctx.strokeStyle="rgba(180,255,80,.95)";ctx.lineWidth=2;
      for(let i=0;i<6;i++){const a=i*Math.PI/3;ctx.beginPath();ctx.moveTo(mx,my);ctx.lineTo(mx+Math.cos(a)*12,my+Math.sin(a)*12);ctx.stroke();}
      ctx.fillStyle="rgba(220,255,100,.8)";ctx.beginPath();ctx.arc(mx,my,5,0,Math.PI*2);ctx.fill();
    }else if(mf.flame){
      ctx.fillStyle="rgba(255,120,0,0.9)";ctx.beginPath();ctx.arc(mx,my,12,0,Math.PI*2);ctx.fill();
      ctx.fillStyle="rgba(255,220,0,0.7)";ctx.beginPath();ctx.arc(mx,my,6,0,Math.PI*2);ctx.fill();
    }else{
      ctx.fillStyle="rgba(255,240,160,0.95)";ctx.beginPath();ctx.arc(mx,my,8,0,Math.PI*2);ctx.fill();
      ctx.fillStyle="#fff";ctx.beginPath();ctx.arc(mx,my,3,0,Math.PI*2);ctx.fill();
    }
  }
  // Flame particles
  (G.flames||[]).forEach(f=>{
    const t=f.life/f.maxLife;
    ctx.globalAlpha=Math.min(1,t*1.8)*.85;
    ctx.fillStyle=t>0.6?'#ffdd00':t>0.3?'#ff8800':'#ff3300';
    ctx.beginPath();ctx.arc(Math.round(f.x-cx),Math.round(f.y-cy),Math.max(1,f.size*t),0,Math.PI*2);ctx.fill();
  });
  ctx.globalAlpha=1;
  // Bullets + Rockets
  (G.bullets||[]).forEach(b=>{
    const bx2=Math.round(b.x-cx),by2=Math.round(b.y-cy);
    if(b.isTaser){
      // Taser bolt โ€” electric yellow-green zigzag
      const ang=Math.atan2(b.vy,b.vx);
      ctx.save();ctx.translate(bx2,by2);ctx.rotate(ang);
      ctx.strokeStyle="rgba(200,255,80,.95)";ctx.lineWidth=2;
      ctx.beginPath();ctx.moveTo(-10,0);ctx.lineTo(-4,-5);ctx.lineTo(2,5);ctx.lineTo(8,-5);ctx.lineTo(14,0);ctx.stroke();
      ctx.fillStyle="rgba(220,255,120,.8)";ctx.beginPath();ctx.arc(14,0,4,0,Math.PI*2);ctx.fill();
      ctx.restore();
    }else if(b.isRocket){      // Rocket sprite
      const ang=Math.atan2(b.vy,b.vx);
      ctx.save();ctx.translate(bx2,by2);ctx.rotate(ang);
      // Body
      ctx.fillStyle="#cc3300";ctx.fillRect(-14,-4,22,8);
      ctx.fillStyle="#ff5500";ctx.fillRect(-14,-3,8,6);
      // Nose cone
      ctx.fillStyle="#ff2200";ctx.beginPath();ctx.moveTo(8,-4);ctx.lineTo(16,0);ctx.lineTo(8,4);ctx.closePath();ctx.fill();
      // Tail fins
      ctx.fillStyle="#aa2200";
      ctx.beginPath();ctx.moveTo(-14,-4);ctx.lineTo(-20,-10);ctx.lineTo(-14,0);ctx.closePath();ctx.fill();
      ctx.beginPath();ctx.moveTo(-14,4);ctx.lineTo(-20,10);ctx.lineTo(-14,0);ctx.closePath();ctx.fill();
      // Engine glow
      ctx.fillStyle="rgba(255,160,0,.9)";ctx.beginPath();ctx.ellipse(-16,0,7,4,0,0,Math.PI*2);ctx.fill();
      ctx.fillStyle="rgba(255,255,100,.6)";ctx.beginPath();ctx.ellipse(-16,0,3,2,0,0,Math.PI*2);ctx.fill();
      ctx.restore();
      // Smoke trail
      G.flames=G.flames||[];
      if(Math.random()<0.6)G.flames.push({x:b.x-Math.cos(ang)*14,y:b.y-Math.sin(ang)*14,vx:(Math.random()-.5)*2,vy:(Math.random()-.5)*2-1,life:18+Math.floor(Math.random()*12),maxLife:30,size:6+Math.random()*6});
    }else{
      // Normal bullet
      const ang=Math.atan2(b.vy,b.vx);
      ctx.save();ctx.translate(bx2,by2);ctx.rotate(ang);
      ctx.fillStyle="#ffe060";ctx.fillRect(-9,-2,18,4);
      ctx.fillStyle="rgba(255,255,255,0.8)";ctx.fillRect(-9,-1,5,2);
      ctx.restore();
    }
  });
  if(inWater(p.x,p.y,PW,PH)&&G.dim==="over"){
    ctx.fillStyle="rgba(20,80,160,0.22)";ctx.fillRect(0,0,W,H);
    // Caustic ripple lines at top
    const t2=Date.now()*.0015;
    ctx.strokeStyle="rgba(100,200,255,0.15)";ctx.lineWidth=2;
    for(let i=0;i<4;i++){
      const wy=40+i*30+Math.sin(t2+i)*8;
      ctx.beginPath();ctx.moveTo(0,wy);
      for(let wx=0;wx<W;wx+=20)ctx.lineTo(wx,wy+Math.sin((wx/W)*6+t2*2+i)*6);
      ctx.stroke();
    }
  }

  // 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=[];G.skeletons=[];
      // Check if player slept inside a tent (leaves above the bed)
      const spx=Math.floor(G.player.x/BS),spy=Math.floor(G.player.y/BS);
      let leafCount=0;
      for(let dy=-5;dy<=0;dy++)for(let dx=-4;dx<=4;dx++){
        const tx=spx+dx,ty=spy+dy;if(tx>=0&&tx<WW&&ty>=0&&ty<WH&&G.world[ty][tx]===B.LEAVES)leafCount++;
      }
      if(leafCount>=3){
        // Randomly collapse tent leaves โ€” abandoned look
        for(let dy=-6;dy<=1;dy++)for(let dx=-5;dx<=5;dx++){
          const tx=spx+dx,ty=spy+dy;
          if(tx<0||tx>=WW||ty<0||ty>=WH)continue;
          if(G.world[ty][tx]===B.LEAVES&&Math.random()<0.55)G.world[ty][tx]=B.AIR;
        }
        // Swing door open
        for(let dy=-1;dy<=1;dy++)for(let dx=-5;dx<=5;dx++){
          const tx=spx+dx,ty=spy+dy;
          if(tx>=0&&tx<WW&&ty>=0&&ty<WH&&G.world[ty][tx]===B.DOOR)G.openDoors.add(`${tx},${ty}`);
        }
        // Remove bed
        if(G.world[spy][spx]===B.BED)G.world[spy][spx]=B.AIR;
        G.sleepMsg="The tent fell apart overnight... ๐Ÿ‚";setTimeout(()=>G.sleepMsg="",3000);
      }
    }
    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';
}
function startFlatGame(){
  document.getElementById('menu').style.display='none';
  document.getElementById('btn-resume').style.display='none';
  G=null;
  // Use flat world gen
  const{world,surf}=genFlatWorld();
  const seed=Date.now()%100000;
  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,creepers:[],creeperTimer:0,skeletons:[],skeletonTimer:0,arrows:[],dispenserTick:0,dispenserArrows:{},chestContents:{},playerPlacedChests:new Set(),litTNT:[],creative:false,flying:false,noclip:true,lastDispenserTap:null,pistonStates:{},turretStates:{},elevatorStates:{},drawbridgeStates:{},sleeping:false,sleepTimer:0,sleepMsg:"",
    dim:"over",endWorld:null,endSurf:null,overworldPos:null,
    dragon:null,endFireballs:[],dragonDefeated:false,fish:[],drops:[],
  };
  document.getElementById('dead').classList.remove('show');
  G.creative=true;
  spawnFishFromWorld();
  fillCreativeInventory();
  updateModeBtn();buildRecipeUI();updateFlightBtns();
  saveWorld();
}
function startSpaceSurvival(){
  document.getElementById('menu').style.display='none';
  document.getElementById('btn-resume').style.display='none';
  G=null;initGame();
  G.creative=false; // survival mode
  const r=genSpaceWorld();G.spaceWorld=r.world;G.spaceSurf=r.surf;
  G.preSpacePos={x:G.player.x,y:G.player.y,world:G.world,surf:G.surf,dim:"over"};
  G.world=G.spaceWorld;G.surf=G.spaceSurf;G.dim="space";
  const mid=Math.floor(WW/2),rb3=WH-4;
  G.player.x=(mid+20.5)*BS;G.player.y=(rb3-43)*BS;G.player.vx=0;G.player.vy=2;
  // Survival starter kit: spacesuit, jetpack, iron pickaxe, sword, torches
  G.hotbar[0]={block:B.SPACESUIT,count:1};
  G.hotbar[1]={block:B.JETPACK,count:1};
  G.hotbar[2]={block:B.IRON_PICK,count:1};
  G.hotbar[3]={block:B.IRON_SWORD,count:1};
  G.hotbar[4]={block:B.TORCH,count:16};
  G.hotbar[5]={block:B.BREAD,count:8};
  updateModeBtn();buildRecipeUI();updateFlightBtns();
  G.sleepMsg="โš”๏ธ Survival Space! You have a spacesuit, jetpack & pickaxe. Good luck!";setTimeout(()=>G.sleepMsg="",4000);
}
function startSpaceGame(){
  document.getElementById('menu').style.display='none';
  document.getElementById('btn-resume').style.display='none';
  G=null;initGame();
  G.creative=true; // start creative so player has items
  const r=genSpaceWorld();G.spaceWorld=r.world;G.spaceSurf=r.surf;
  G.preSpacePos={x:G.player.x,y:G.player.y,world:G.world,surf:G.surf,dim:"over"};
  G.world=G.spaceWorld;G.surf=G.spaceSurf;G.dim="space";
  const mid=Math.floor(WW/2),rb3=WH-4;
  G.player.x=(mid+20.5)*BS;G.player.y=(rb3-43)*BS;G.player.vx=0;G.player.vy=2;
  fillCreativeInventory();updateModeBtn();buildRecipeUI();updateFlightBtns();
  G.sleepMsg="๐Ÿช Land on the tiny planet! Jetpack to the station or other planets.";setTimeout(()=>G.sleepMsg="",5000);
}
function startNightFlatGame(){
  startFlatGame();
  G.dayTime=DAY_LEN; // jump straight to night
}

// โ”€โ”€ Custom World Creator โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const wcConfig={mode:'creative',mobs:true,size:'medium',flat:false,villages:true};
function wcSetMode(m){
  wcConfig.mode=m;
  document.getElementById('wc-creative').style.cssText+=m==='creative'?'border-color:rgba(100,200,255,.6);background:rgba(0,120,255,.3);color:#fff;':'border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);';
  document.getElementById('wc-survival').style.cssText+=m==='survival'?'border-color:rgba(255,200,80,.6);background:rgba(180,120,0,.3);color:#fff;':'border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);';
}
function wcSetMobs(on){
  wcConfig.mobs=on;
  document.getElementById('wc-mobs-on').style.cssText+=on?'border-color:rgba(255,100,100,.6);background:rgba(200,50,50,.3);color:#fff;':'border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);';
  document.getElementById('wc-mobs-off').style.cssText+=!on?'border-color:rgba(100,200,255,.5);background:rgba(0,120,200,.3);color:#fff;':'border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);';
}
function wcSetSize(s){
  wcConfig.size=s;
  ['small','medium','large'].forEach(n=>{
    const on=n===s;
    document.getElementById(`wc-${n}`).style.cssText+=on?'border-color:rgba(100,255,100,.5);background:rgba(50,180,50,.25);color:#fff;':'border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);';
  });
}
function wcSetFlat(flat){
  wcConfig.flat=flat;
  document.getElementById('wc-normal').style.cssText+=!flat?'border-color:rgba(100,255,100,.5);background:rgba(50,180,50,.25);color:#fff;':'border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);';
  document.getElementById('wc-flat').style.cssText+=flat?'border-color:rgba(200,160,80,.6);background:rgba(140,100,20,.35);color:#fff;':'border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);';
}
function wcSetVillages(on){
  wcConfig.villages=on;
  document.getElementById('wc-villages-on').style.cssText+=on?'border-color:rgba(200,160,80,.6);background:rgba(140,100,20,.3);color:#fff;':'border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);';
  document.getElementById('wc-villages-off').style.cssText+=!on?'border-color:rgba(100,200,255,.5);background:rgba(0,120,200,.3);color:#fff;':'border-color:rgba(255,255,255,.15);background:rgba(255,255,255,.05);color:rgba(255,255,255,.5);';
}
function startCustomWorld(){
  document.getElementById('menu').style.display='none';
  document.getElementById('btn-resume').style.display='none';
  G=null;
  if(wcConfig.flat){
    startFlatGame();
    G.creative=wcConfig.mode==='creative';
    G.noMobs=!wcConfig.mobs;
    if(wcConfig.mode==='night'){G.dayTime=DAY_LEN;}
    if(G.creative)fillCreativeInventory();else G.inventory=Array.from({length:32},()=>({block:B.AIR,count:0}));
  }else{
    initGame();
    G.creative=wcConfig.mode==='creative';
    G.noMobs=!wcConfig.mobs;
    if(wcConfig.size==='large'){
      for(let c=0;c<3;c++){
        const cx=Math.floor(Math.random()*WW),cy=G.surf[cx]+10+Math.floor(Math.random()*20);
        for(let r=-3;r<=3;r++)for(let dx=-3;dx<=3;dx++){
          if(r*r+dx*dx>9)continue;
          const tx=cx+dx,ty=cy+r;
          if(tx>=0&&tx<WW&&ty>=0&&ty<WH&&G.world[ty][tx]===B.STONE)G.world[ty][tx]=B.AIR;
        }
      }
    }
    if(G.creative)fillCreativeInventory();
  }
  // Apply size: small = narrow bedrock walls, large = extra caves
  if(wcConfig.size==='small'&&!wcConfig.flat){
    // Keep only the centre 35 blocks playable, wall the rest with bedrock
    const mid=Math.floor(WW/2),half=17;
    for(let x=0;x<WW;x++){
      if(Math.abs(x-mid)>half){
        for(let y=0;y<WH;y++)G.world[y][x]=B.BEDROCK;
      }
    }
    // Spawn player in centre of small world
    G.player.x=(mid+.5)*BS;G.player.y=(G.surf[mid]-1)*BS;
    G.spawnX=G.player.x;G.spawnY=G.player.y;
  }
  updateModeBtn();buildRecipeUI();updateFlightBtns();saveWorld();
  if(!wcConfig.villages)G.villagers=[];
  const inp=document.getElementById('world-name-input');if(inp)inp.value='';
  document.getElementById('world-creator-panel').style.display='none';
}
function startGame(creative){
  document.getElementById('menu').style.display='none';
  document.getElementById('btn-resume').style.display='none';
  G=null;
  initGame();
  G.creative=creative;
  saveWorld();// Save fresh world
  if(creative){
    fillCreativeInventory();
    G.player.hp=MAX_HP;
  }
  updateFlightBtns();
}



// โ”€โ”€ Pistons โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const CANNOT_PUSH=new Set([B.BEDROCK,B.END_PORTAL,B.OBSIDIAN]);

function pistonExtend(bx,by,dir){
  const dx=Math.abs(dir)===1?dir:0, dy=dir>1?1:dir<-1?-1:0;
  // Collect blocks in push direction (up to 5)
  const chain=[];
  let tx=bx+dx,ty=by+dy;
  while(tx>=0&&tx<WW&&ty>=0&&ty<WH&&G.world[ty][tx]!==B.AIR&&chain.length<5){
    if(CANNOT_PUSH.has(G.world[ty][tx]))return; // immovable
    chain.push({x:tx,y:ty,block:G.world[ty][tx]});
    tx+=dx;ty+=dy;
  }
  // Check destination is empty
  const ex=tx,ey=ty;
  if(ex<0||ex>=WW||ey<0||ey>=WH)return;
  // Shift blocks forward
  for(let i=chain.length-1;i>=0;i--){
    const nx2=chain[i].x+dx,ny2=chain[i].y+dy;
    G.world[ny2][nx2]=chain[i].block;
    if(G.pistonStates[`${chain[i].x},${chain[i].y}`]){
      G.pistonStates[`${nx2},${ny2}`]={...G.pistonStates[`${chain[i].x},${chain[i].y}`]};
      delete G.pistonStates[`${chain[i].x},${chain[i].y}`];
    }
  }
  G.world[by+dy][bx+dx]=B.AIR; // piston arm space (arm is visual only)
  const k=`${bx},${by}`;
  G.pistonStates[k].extended=true;
}

function pistonRetract(bx,by,dir,sticky){
  const k=`${bx},${by}`;
  const dx=Math.abs(dir)===1?dir:0, dy=dir>1?1:dir<-1?-1:0;
  G.pistonStates[k].extended=false;
  if(sticky){
    // Pull the block adjacent to arm back
    const ax=bx+dx*2,ay=by+dy*2;
    if(ax>=0&&ax<WW&&ay>=0&&ay<WH&&G.world[ay][ax]!==B.AIR&&!CANNOT_PUSH.has(G.world[ay][ax])){
      G.world[by+dy][bx+dx]=G.world[ay][ax];
      if(G.pistonStates[`${ax},${ay}`]){
        G.pistonStates[`${bx+dx},${by+dy}`]={...G.pistonStates[`${ax},${ay}`]};
        delete G.pistonStates[`${ax},${ay}`];
      }
      G.world[ay][ax]=B.AIR;
    }
  }
}

function togglePiston(bx,by){
  const k=`${bx},${by}`;
  const blk=G.world[by][bx];
  if(blk!==B.PISTON&&blk!==B.STICKY_PISTON)return;
  if(!G.pistonStates[k])G.pistonStates[k]={dir:1,extended:false};
  const ps=G.pistonStates[k];
  if(!ps.extended) pistonExtend(bx,by,ps.dir);
  else pistonRetract(bx,by,ps.dir,blk===B.STICKY_PISTON);
}

// โ”€โ”€ Save / Load โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
async function saveWorld(){
  if(!G)return;
  const data={
    world:Array.from(G.world,row=>Array.from(row)),
    surf:Array.from(G.surf),
    player:{x:G.player.x,y:G.player.y,hp:G.player.hp},
    hotbar:G.hotbar.map(s=>({...s})),
    inventory:G.inventory.map(s=>({...s})),
    chestContents:Object.fromEntries(Object.entries(G.chestContents).map(([k,v])=>[k,v.map(s=>({...s}))])),
    dispenserArrows:{...G.dispenserArrows},
    openDoors:[...G.openDoors],
    pistonStates:JSON.parse(JSON.stringify(G.pistonStates||{})),
    turretStates:JSON.parse(JSON.stringify(G.turretStates||{})),
    elevatorStates:JSON.parse(JSON.stringify(G.elevatorStates||{})),
    playerPlacedChests:[...(G.playerPlacedChests||new Set())],
    dayTime:G.dayTime,creative:G.creative,
    spawnX:G.spawnX,spawnY:G.spawnY,
    savedAt:new Date().toLocaleString(),
  };
  try{
    await window.storage.set('mc_save',JSON.stringify(data));
    G.sleepMsg="๐Ÿ’พ World saved!";setTimeout(()=>G.sleepMsg="",2000);
    checkSave();
  }catch(e){G.sleepMsg="Save failed!";setTimeout(()=>G.sleepMsg="",2000);}
}

async function loadWorld(){
  try{
    const res=await window.storage.get('mc_save');
    if(!res)return null;
    return JSON.parse(res.value);
  }catch(e){return null;}
}

async function checkSave(){
  try{
    const res=await window.storage.get('mc_save');
    if(res){
      const d=JSON.parse(res.value);
      document.getElementById('btn-continue').style.display='';
      const info=document.getElementById('save-info');
      if(info)info.textContent=(d.creative?'๐ŸŒŸ Creative':'โš”๏ธ Survival')+' ยท '+d.savedAt;
    }
  }catch(e){}
}

async function continueWorld(){
  const data=await loadWorld();
  if(!data)return;
  document.getElementById('menu').style.display='none';
  G=null;initGame();
  // Restore world
  G.world=data.world.map(row=>new Uint8Array(row));
  G.surf=data.surf;
  G.player.x=data.player.x;G.player.y=data.player.y;G.player.hp=data.player.hp||MAX_HP;
  G.hotbar=data.hotbar;G.inventory=data.inventory;
  G.chestContents=Object.fromEntries(Object.entries(data.chestContents).map(([k,v])=>[k,v]));
  G.dispenserArrows=data.dispenserArrows||{};
  G.openDoors=new Set(data.openDoors||[]);
  G.pistonStates=data.pistonStates||{};
  G.turretStates=data.turretStates||{};
  G.elevatorStates=data.elevatorStates||{};
  G.drops=[];
  G.playerPlacedChests=new Set(data.playerPlacedChests||[]);
  G.dayTime=data.dayTime||0;
  G.creative=data.creative||false;
  G.spawnX=data.spawnX;G.spawnY=data.spawnY;
  G.fish=[];spawnFishFromWorld();
  if(G.creative)fillCreativeInventory();
  updateFlightBtns();buildRecipeUI();
}

function goToMenu(){
  saveWorld();
  setTimeout(()=>{
    document.getElementById('menu').style.display='flex';
    document.getElementById('btn-resume').style.display='';
  },400);
}

document.getElementById('btn-menu').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();goToMenu();},{passive:false});
document.getElementById('btn-menu').addEventListener('mousedown',e=>{e.stopPropagation();goToMenu();});

function resumeWorld(){
  if(!G)return;
  document.getElementById('menu').style.display='none';
}
document.getElementById('btn-resume').addEventListener('click',resumeWorld);
document.getElementById('btn-resume').addEventListener('touchend',e=>{e.preventDefault();resumeWorld();},{passive:false});
document.getElementById('btn-continue').addEventListener('click',continueWorld);
document.getElementById('btn-continue').addEventListener('touchend',e=>{e.preventDefault();continueWorld();},{passive:false});
// Auto-save every 60 seconds
setInterval(()=>{if(G&&!document.getElementById('menu').style.display.includes('flex'))saveWorld();},60000);
// Check for save on load
checkSave();

document.getElementById('btn-survival').addEventListener('click',()=>startGame(false));
document.getElementById('btn-survival').addEventListener('touchend',e=>{e.preventDefault();startGame(false);},{passive:false});
document.getElementById('btn-creative').addEventListener('click',()=>startGame(true));
document.getElementById('btn-creative').addEventListener('touchend',e=>{e.preventDefault();startGame(true);},{passive:false});
document.getElementById('btn-flat').addEventListener('click',startFlatGame);
document.getElementById('btn-flat').addEventListener('touchend',e=>{e.preventDefault();startFlatGame();},{passive:false});
document.getElementById('btn-night-flat').addEventListener('click',startNightFlatGame);
document.getElementById('btn-night-flat').addEventListener('touchend',e=>{e.preventDefault();startNightFlatGame();},{passive:false});
window.addEventListener('resize',resize);
resize();
raf=requestAnimationFrame(loop); // loop starts but waits for G to be set
</script>
</body>
</html>

Game Source: Mini Minecraft 2D

Creator: ShadowLegend17

Libraries: none

Complexity: complex (4715 lines, 244.6 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-mr71mvs3" to link back to the original. Then publish at arcadelab.ai/publish.