Mini Minecraft 2D
by ShadowLegend174715 lines244.6 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Mini Minecraft 2D</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:monospace;user-select:none;}
#cvs{position:absolute;inset:0;width:100%;height:100%;cursor:crosshair;touch-action:none;}
#ui{position:absolute;inset:0;pointer-events:none;}
.btn{pointer-events:all;position:absolute;display:flex;align-items:center;justify-content:center;
background:rgba(15,15,15,0.88);color:#fff;border:2px solid rgba(255,255,255,0.2);
border-radius:14px;font-weight:bold;font-size:20px;cursor:pointer;touch-action:none;user-select:none;}
#inv-overlay{display:none;position:absolute;inset:0;background:rgba(0,0,0,0.82);
align-items:center;justify-content:center;z-index:20;pointer-events:all;backdrop-filter:blur(4px);}
#inv-overlay.open{display:flex;}
#inv-box{background:linear-gradient(150deg,#232333,#181828);border:2px solid rgba(255,255,255,0.12);
border-radius:18px;padding:16px;display:flex;flex-direction:column;gap:10px;
box-shadow:0 8px 40px rgba(0,0,0,0.9);max-width:96vw;max-height:90vh;overflow-y:auto;width:min(580px,96vw);}
.inv-tab{flex:1;text-align:center;padding:8px 0;font-weight:bold;font-size:13px;
background:transparent;color:rgba(255,255,255,0.38);border-radius:8px;cursor:pointer;border:none;}
.inv-tab.active{background:rgba(255,255,255,0.1);color:#fff;}
.slot{width:52px;height:52px;border:2px solid rgba(255,255,255,0.14);border-radius:8px;
cursor:pointer;position:relative;display:flex;align-items:flex-end;justify-content:flex-end;
overflow:hidden;flex-shrink:0;transition:border-color .1s;}
.slot.selected{border-color:#ffe066;box-shadow:0 0 14px rgba(255,224,102,.65);}
.slot-num{position:absolute;top:2px;left:4px;font-size:8px;color:rgba(255,255,255,.38);}
.slot-count{font-size:9px;color:#fff;font-weight:bold;text-shadow:1px 1px 2px #000;padding:1px 3px;}
.recipe-row{background:rgba(20,20,30,.45);border:1px solid rgba(255,255,255,.07);border-radius:10px;
padding:8px;display:flex;align-items:center;gap:8px;}
.recipe-row.can{background:rgba(30,60,30,.5);border-color:rgba(70,160,70,.25);}
.craft-btn{padding:7px 10px;border-radius:8px;font-size:11px;font-weight:bold;cursor:pointer;
background:rgba(40,40,45,.7);color:rgba(255,255,255,.25);border:1.5px solid rgba(255,255,255,.06);
white-space:nowrap;font-family:monospace;}
.craft-btn.can{background:rgba(45,145,45,.9);color:#fff;border-color:rgba(80,200,80,.35);}
#dead{display:none;position:absolute;inset:0;background:rgba(80,0,0,.85);flex-direction:column;
align-items:center;justify-content:center;z-index:50;pointer-events:all;backdrop-filter:blur(4px);}
#dead.show{display:flex;}
#respawn-btn{padding:14px 36px;background:rgba(50,180,50,.9);color:#fff;font-weight:bold;
font-size:18px;border-radius:12px;cursor:pointer;border:2px solid rgba(80,220,80,.5);pointer-events:all;}
</style>
</head>
<body>
<canvas id="cvs"></canvas>
<div id="ui">
<!-- Touch D-Pad left -->
<div id="btn-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.