Mini Minecraft 2D
by ShadowLegend172403 lines122.1 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Mini Minecraft 2D</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:monospace;user-select:none;}
#cvs{position:absolute;inset:0;width:100%;height:100%;cursor:crosshair;touch-action:none;}
#ui{position:absolute;inset:0;pointer-events:none;}
.btn{pointer-events:all;position:absolute;display:flex;align-items:center;justify-content:center;
background:rgba(15,15,15,0.88);color:#fff;border:2px solid rgba(255,255,255,0.2);
border-radius:14px;font-weight:bold;font-size:20px;cursor:pointer;touch-action:none;user-select:none;}
#inv-overlay{display:none;position:absolute;inset:0;background:rgba(0,0,0,0.82);
align-items:center;justify-content:center;z-index:20;pointer-events:all;backdrop-filter:blur(4px);}
#inv-overlay.open{display:flex;}
#inv-box{background:linear-gradient(150deg,#232333,#181828);border:2px solid rgba(255,255,255,0.12);
border-radius:18px;padding:16px;display:flex;flex-direction:column;gap:10px;
box-shadow:0 8px 40px rgba(0,0,0,0.9);max-width:96vw;max-height:90vh;overflow-y:auto;width:min(580px,96vw);}
.inv-tab{flex:1;text-align:center;padding:8px 0;font-weight:bold;font-size:13px;
background:transparent;color:rgba(255,255,255,0.38);border-radius:8px;cursor:pointer;border:none;}
.inv-tab.active{background:rgba(255,255,255,0.1);color:#fff;}
.slot{width:52px;height:52px;border:2px solid rgba(255,255,255,0.14);border-radius:8px;
cursor:pointer;position:relative;display:flex;align-items:flex-end;justify-content:flex-end;
overflow:hidden;flex-shrink:0;transition:border-color .1s;}
.slot.selected{border-color:#ffe066;box-shadow:0 0 14px rgba(255,224,102,.65);}
.slot-num{position:absolute;top:2px;left:4px;font-size:8px;color:rgba(255,255,255,.38);}
.slot-count{font-size:9px;color:#fff;font-weight:bold;text-shadow:1px 1px 2px #000;padding:1px 3px;}
.recipe-row{background:rgba(20,20,30,.45);border:1px solid rgba(255,255,255,.07);border-radius:10px;
padding:8px;display:flex;align-items:center;gap:8px;}
.recipe-row.can{background:rgba(30,60,30,.5);border-color:rgba(70,160,70,.25);}
.craft-btn{padding:7px 10px;border-radius:8px;font-size:11px;font-weight:bold;cursor:pointer;
background:rgba(40,40,45,.7);color:rgba(255,255,255,.25);border:1.5px solid rgba(255,255,255,.06);
white-space:nowrap;font-family:monospace;}
.craft-btn.can{background:rgba(45,145,45,.9);color:#fff;border-color:rgba(80,200,80,.35);}
#dead{display:none;position:absolute;inset:0;background:rgba(80,0,0,.85);flex-direction:column;
align-items:center;justify-content:center;z-index:50;pointer-events:all;backdrop-filter:blur(4px);}
#dead.show{display:flex;}
#respawn-btn{padding:14px 36px;background:rgba(50,180,50,.9);color:#fff;font-weight:bold;
font-size:18px;border-radius:12px;cursor:pointer;border:2px solid rgba(80,220,80,.5);pointer-events:all;}
</style>
</head>
<body>
<canvas id="cvs"></canvas>
<div id="ui">
<!-- Touch D-Pad left -->
<div id="btn-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-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="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:center;
background:linear-gradient(180deg,#1a3a1a 0%,#0a1a0a 60%,#000 100%);
font-family:monospace;">
<!-- Pixel art sky strip -->
<div style="position:absolute;top:0;left:0;right:0;height:38%;background:linear-gradient(180deg,#1a6090 0%,#3390cc 100%);"></div>
<!-- Ground strip -->
<div style="position:absolute;bottom:0;left:0;right:0;height:30%;background:linear-gradient(180deg,#5c9e30 0%,#8B6340 30%,#555 100%);"></div>
<!-- Title -->
<div style="position:relative;z-index:2;text-align:center;margin-bottom:32px;">
<div style="font-size:clamp(28px,6vw,52px);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 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>
<!-- 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 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};
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.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}]},
];
// โโ 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 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=6;
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=4;
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;
}
}
}
// 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;
// Fill one block deep
if(lavaY+1<WH&&(world[lavaY+1][lx]===B.AIR||world[lavaY+1][lx]===B.STONE))world[lavaY+1][lx]=B.LAVA;
}
}
}
return{world,surf};
}
// โโ Sky โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function skyColors(t){
const night=t>DAY_LEN/CYCLE;
const dt=night?0:t/(DAY_LEN/CYCLE);
const nt=night?(t-DAY_LEN/CYCLE)/(NIGHT_LEN/CYCLE):0;
if(!night){
if(dt<.1){const f=dt/.1;return{top:lerpC("#1a1040","#3390cc",f),bot:lerpC("#884422","#87ceeb",f),dark:f<.5};}
if(dt>.9){const f=(dt-.9)/.1;return{top:lerpC("#3390cc","#1a1040",f),bot:lerpC("#87ceeb","#884422",f),dark:f>.5};}
return{top:"#3390cc",bot:"#87ceeb",dark:false};
}
if(nt<.05){const f=nt/.05;return{top:lerpC("#1a1040","#080818",f),bot:lerpC("#884422","#0a0828",f),dark:true};}
if(nt>.9){const f=(nt-.9)/.1;return{top:lerpC("#080818","#1a1040",f),bot:lerpC("#0a0828","#884422",f),dark:true};}
return{top:"#080818",bot:"#0a0828",dark:true};
}
// โโ Canvas Draw โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function 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.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);
}
// โโ 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();
}
// โโ 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}));
// Fill inventory with all items
const allItems=[
B.LEAVES,B.COAL,B.IRON,B.REDSTONE,B.EMERALD,B.DIAMOND,
B.TORCH,B.DOOR,B.BED,B.CHEST,B.DISPENSER,B.TNT,B.FENCE,
B.IRON_PICK,B.STONE_PICK,B.DIAMOND,B.DM_PICK,B.DM_SWORD,B.DM_AXE,
B.BOW,B.ARROW,B.BREAD,B.APPLE,B.MEAT,B.STEW,
B.LAVA,B.BARRIER,B.PISTON,B.STICKY_PISTON,B.GATE,B.TRAPDOOR,B.CONV_R,B.CONV_L,B.EGG_ZOMBIE,B.EGG_CREEPER,
B.IR_HELM,B.IR_CHEST,B.IR_BOOT,B.DM_HELM,B.DM_CHEST,B.DM_BOOT,
];
G.inventory=Array.from({length:32},(_,i)=>i<allItems.length?{block:allItems[i],count:9999}:{block:B.AIR,count:0});
}
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}=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,arrows:[],dispenserTick:0,dispenserArrows:{},chestContents:{},playerPlacedChests:new Set(),litTNT:[],creative:false,flying:false,noclip:true,lastDispenserTap:null,pistonStates:{},sleeping:false,sleepTimer:0,sleepMsg:"",
dim:"over",endWorld:null,endSurf:null,overworldPos:null,
dragon:null,endFireballs:[],dragonDefeated:false,
};
document.getElementById('dead').classList.remove('show');
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)&&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)&&G.openDoors.has(`${bx},${by}`))continue;
return true;
}
}
return false;
}
function tryMine(bx,by){
if(bx<0||bx>=WW||by<0||by>=WH||!G.world[by][bx])return;
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){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.PISTON||blk===B.STICKY_PISTON){togglePiston(bx,by);}
else if(blk===B.BED){
if(G.dayTime>=DAY_LEN){G.sleeping=true;G.sleepTimer=0;}
else{if(!G.sleepMsg){G.sleepMsg="You can only sleep at night!";setTimeout(()=>G.sleepMsg="",2500);}}
}
return;
}
const p=G.player;
if(bx>=Math.floor((p.x-PW/2)/BS)&&bx<=Math.floor((p.x+PW/2)/BS)&&by>=Math.floor((p.y-PH)/BS)&&by<=Math.floor(p.y/BS))return;
const sl=G.hotbar[G.slot];
if(!sl?.count||!DATA[sl.block]?.pl)return;
const placedBlock=sl.block;
G.world[by][bx]=sl.block;if(!G.creative){sl.count--;if(!sl.count)sl.block=B.AIR;}
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
const p=G.player;if(p.atkCD>0)return;
const eq=G.hotbar[G.slot];
if(eq?.count&&DATA[eq.block]?.isBow)return; // bow uses charge system
const dmg=(eq?.count&&DATA[eq.block]?.sdmg)||1;
const range=BS*1.8;let hit=false;
G.zombies.forEach(z=>{if(Math.abs(z.x-p.x)<range&&Math.abs(z.y-p.y)<range){z.hp-=dmg;z.flash=8;hit=true;}});
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.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;
}
// โโ 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];(eq?.count&&DATA[eq.block]?.isBow)?startBowCharge():tryAttack();}
if(['ArrowLeft','ArrowRight','ArrowUp',' '].includes(e.key))e.preventDefault();
});
window.addEventListener('keyup',e=>{keys.delete(e.key);if(e.key==='f'||e.key==='F')releaseBow();});
// โโ Touch buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function holdBtn(el,k){
el.addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();touch[k]=true;},{passive:false});
el.addEventListener('touchend',e=>{e.stopPropagation();e.preventDefault();touch[k]=false;},{passive:false});
el.addEventListener('touchcancel',e=>{e.stopPropagation();e.preventDefault();touch[k]=false;},{passive:false});
el.addEventListener('mousedown',e=>{e.stopPropagation();touch[k]=true;});
el.addEventListener('mouseup',e=>{e.stopPropagation();touch[k]=false;});
el.addEventListener('mouseleave',e=>{touch[k]=false;});
}
holdBtn(document.getElementById('btn-l'),'left');
holdBtn(document.getElementById('btn-r'),'right');
holdBtn(document.getElementById('btn-jump'),'jump');
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();const eq=G?.hotbar[G.slot];(eq?.count&&DATA[eq.block]?.isBow)?startBowCharge():tryAttack();},{passive:false});
document.getElementById('btn-atk').addEventListener('touchend',e=>{e.stopPropagation();e.preventDefault();releaseBow();},{passive:false});
document.getElementById('btn-atk').addEventListener('touchcancel',e=>{e.stopPropagation();e.preventDefault();releaseBow();},{passive:false});
document.getElementById('btn-atk').addEventListener('mousedown',e=>{e.stopPropagation();const eq=G?.hotbar[G.slot];(eq?.count&&DATA[eq.block]?.isBow)?startBowCharge():tryAttack();});
document.getElementById('btn-atk').addEventListener('mouseup',e=>{e.stopPropagation();releaseBow();});
document.getElementById('btn-mode').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();toggleMode();},{passive:false});
document.getElementById('btn-mode').addEventListener('mousedown',e=>{e.stopPropagation();toggleMode();});
document.getElementById('btn-bag').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();toggleInv();},{passive:false});
document.getElementById('btn-bag').addEventListener('mousedown',e=>{e.stopPropagation();toggleInv();});
document.getElementById('inv-close').addEventListener('click',()=>toggleInv(false));
document.getElementById('inv-overlay').addEventListener('click',e=>{if(e.target===document.getElementById('inv-overlay'))toggleInv(false);});
document.getElementById('btn-sleep').addEventListener('touchstart',e=>{e.stopPropagation();e.preventDefault();trySleep();},{passive:false});
document.getElementById('btn-sleep').addEventListener('mousedown',e=>{e.stopPropagation();trySleep();});
document.getElementById('respawn-btn').addEventListener('click',respawn);
document.getElementById('respawn-btn').addEventListener('touchend',e=>{e.preventDefault();respawn();},{passive:false});
// โโ Mode toggle โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function toggleMode(){
tmMode=tmMode==="mine"?"place":"mine";updateModeBtn();
}
function updateModeBtn(){
const btn=document.getElementById('btn-mode');
btn.textContent=tmMode==="mine"?"โ Mine":"๐งฑ Place";
btn.style.background=tmMode==="mine"?"rgba(160,48,18,.9)":"rgba(28,110,28,.9)";
}
// โโ Inventory โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function toggleInv(force){
invOpen=force!==undefined?force:!invOpen;
document.getElementById('inv-overlay').classList.toggle('open',invOpen);
if(invOpen)renderInv();
}
function switchTab(t){
activeTab=t;
document.getElementById('tab-items').classList.toggle('active',t==='items');
document.getElementById('tab-craft').classList.toggle('active',t==='craft');
document.getElementById('tab-items-content').style.display=t==='items'?'':'none';
document.getElementById('tab-craft-content').style.display=t==='craft'?'':'none';
if(t==='craft')renderCraft();
}
function makeSlotEl(sl,area,idx){
const d=sl.count>0?DATA[sl.block]:null;
const isTool=d&&!d.pl;
const el=document.createElement('div');
el.className='slot'+(selSlot&&selSlot.area===area&&selSlot.idx===idx?' selected':'');
el.style.background=isTool?'#12121e':(d?d.col:'rgba(0,0,0,.38)');
if(d){
if(isTool){const ic=document.createElement('span');ic.textContent=d.icon;ic.style.fontSize='22px';el.appendChild(ic);}
else if(d.top){const tp=document.createElement('div');tp.style.cssText=`position:absolute;top:0;left:0;right:0;height:7px;background:${d.top};`;el.appendChild(tp);}
const cnt=document.createElement('span');cnt.className='slot-count';cnt.textContent=sl.count;el.appendChild(cnt);
}
el.addEventListener('click',()=>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='';
G.inventory.forEach((sl,i)=>ig.appendChild(makeSlotEl(sl,'inv',i)));
G.hotbar.forEach((sl,i)=>{const e=makeSlotEl(sl,'hot',i);const n=document.createElement('span');n.className='slot-num';n.textContent=i+1;e.appendChild(n);hg.appendChild(e);});
}
function 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();});
// โโ 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;
}
// Central obsidian pillar
const px2=Math.floor(EW/2);
for(let y=2;y<=21;y++) w[y][px2]=B.OBSIDIAN;
w[1][px2]=B.OBSIDIAN;
// Return portal in centre of island
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);
// 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.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;}}
});
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;}}
});
}
// Zombie AI - burn first, then filter dead ones
G.zombies.forEach(z=>{
z.vy=Math.min(z.vy+GRAVITY,MAX_FALL);
const dx=p.x-z.x;z.dir=dx>0?1:-1;z.vx=z.dir*ZOMBIE_SPD*(Math.abs(dx)>5?1:0);
moveEnt(z,ZW,ZH);if(z.flash>0)z.flash--;z.dmgT=Math.max(0,z.dmgT-1);
// Burn in daylight unless sheltered by a block or door above
if(!isNight&&!isShielded(z.x,z.y)){z.burnT=(z.burnT||0)+1;if(z.burnT%30===0){z.hp=Math.max(0,z.hp-1);z.flash=25;z.onFire=true;}}
else{z.burnT=0;z.onFire=false;}
if(Math.abs(z.x-p.x)<ZW/2+PW/2+4&&Math.abs(z.y-p.y)<ZH/2+PH/2+4&&z.dmgT===0&&p.hp>0){
const totalDef=[...G.hotbar,...G.inventory].reduce((s,sl)=>s+(sl.count>0&&DATA[sl.block]?.def?DATA[sl.block].def:0),0);
const blockChance=Math.min(0.85,totalDef*0.05);
if(!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
G.zombies=G.zombies.filter(z=>z.hp>0);
// Creeper spawn (night only)
if(isNight&&G.creepers.length<MAX_CREEPERS){
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=>{
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
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");}
// Destroy blocks in radius 3
const bx=Math.floor(c.x/BS),by2=Math.floor(c.y/BS);
for(let dy=-3;dy<=3;dy++)for(let dx2=-3;dx2<=3;dx2++){
if(dx2*dx2+dy*dy>9)continue;
const tx=bx+dx2,ty=by2+dy;
if(tx>=0&&tx<WW&&ty>=0&&ty<WH&&G.world[ty][tx]!==B.BEDROCK)G.world[ty][tx]=B.AIR;
}
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=G.creepers.filter(c=>c.hp>0);
// 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;}});
t.fuse=-1;
}
});
G.litTNT=G.litTNT.filter(t=>t.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);
// Player
if(p.atkCD>0)p.atkCD--;if(p.flash>0)p.flash--;if(p.dmgCD>0)p.dmgCD--;
// 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;
}
}
// 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){p.vx=-MOVE_SPD;p.dir=-1;}else if(goR){p.vx=MOVE_SPD;p.dir=1;}else 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);
}
}else{
if(goJ&&p.onGround){p.vy=JUMP_VEL;p.onGround=false;}
p.vy=Math.min(p.vy+GRAVITY,MAX_FALL);
}
moveEnt(p,PW,PH);
if(p.y>WH*BS+200){p.x=G.spawnX;p.y=G.spawnY;p.vx=p.vy=0;}
// Sleep when standing on top of a bed
if(!G.sleeping){
const bx1=Math.floor((p.x-PW/2)/BS),bx2=Math.floor((p.x+PW/2)/BS);
const footBy=Math.floor(p.y/BS); // block directly under feet
let onBed=false;
for(let bx=bx1;bx<=bx2&&!onBed;bx++)
if(bx>=0&&bx<WW&&footBy>=0&&footBy<WH&&G.world[footBy][bx]===B.BED)onBed=true;
if(onBed&&G.hotbar[G.slot]?.block!==B.BED){
if(G.dayTime>=DAY_LEN){G.sleeping=true;G.sleepTimer=0;}
else{if(!G.sleepMsg){G.sleepMsg="You can only sleep at night!";setTimeout(()=>G.sleepMsg="",2500);}}
}
}
} // end G.dim==="over" block
// โโ 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;
if(goLe){p.vx=-MOVE_SPD;p.dir=-1;}else if(goRe){p.vx=MOVE_SPD;p.dir=1;}else p.vx*=.7;
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);
dr.angle+=0.015;
if(dr.hp<dr.maxHp*.6&&(dr.diveCD||200)>0){dr.diveCD=(dr.diveCD||200)-1;}
if(dr.hp<dr.maxHp*.6&&dr.diveCD<=0){
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{
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(15*BS,dr.y));
dr.fireCD=(dr.fireCD||0)-1;
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&&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);
}
}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;}
const sky=ctx.createLinearGradient(0,0,0,H);sky.addColorStop(0,sc.top);sky.addColorStop(1,sc.bot);
ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);
if(sc.dark){
const na=Math.min(.55,(G.dayTime-DAY_LEN)/300*.55);
// Stars
const st=Math.min(1,(G.dayTime-DAY_LEN)/60);
stars.forEach(s=>{
const sx2=s.x-cx,sy2=s.y-cy;
if(sx2<-10||sx2>W+10||sy2<-10||sy2>H*.6)return;
ctx.fillStyle=`rgba(255,255,255,${.7*st})`;ctx.beginPath();ctx.arc(sx2,sy2,s.r,0,Math.PI*2);ctx.fill();
});
// Moon
ctx.fillStyle="#e8e0c0";ctx.beginPath();ctx.arc(W*.75,H*.14,22,0,Math.PI*2);ctx.fill();
ctx.fillStyle=sc.top;ctx.beginPath();ctx.arc(W*.75+8,H*.14-4,18,0,Math.PI*2);ctx.fill();
// Darkness
ctx.fillStyle=`rgba(0,0,20,${na})`;ctx.fillRect(0,0,W,H);
// Torch glow
const bx0=Math.max(0,Math.floor(cx/BS)-1),bx1=Math.min(WW-1,Math.ceil((cx+W)/BS)+1);
const by0=Math.max(0,Math.floor(cy/BS)-1),by1=Math.min(WH-1,Math.ceil((cy+H)/BS)+1);
for(let by=by0;by<=by1;by++)for(let bx=bx0;bx<=bx1;bx++){
if(G.world[by][bx]===B.TORCH){
const lx=bx*BS+BS/2-cx,ly=by*BS+BS/2-cy;
const gr=ctx.createRadialGradient(lx,ly,0,lx,ly,BS*3.5);
gr.addColorStop(0,"rgba(255,150,30,.45)");gr.addColorStop(1,"rgba(0,0,0,0)");
ctx.fillStyle=gr;ctx.fillRect(lx-BS*3.5,ly-BS*3.5,BS*7,BS*7);
}
}
// Player ambient glow
const plx=Math.round(p.x-cx),ply=Math.round(p.y-cy);
const pg=ctx.createRadialGradient(plx,ply,0,plx,ply,BS*3);
pg.addColorStop(0,"rgba(255,160,40,.18)");pg.addColorStop(1,"rgba(0,0,0,0)");
ctx.fillStyle=pg;ctx.fillRect(plx-BS*3,ply-BS*3,BS*6,BS*6);
}else{
// Sun
const dp=G.dayTime/DAY_LEN;
const sunx=lerp(W*.05,W*.95,dp),suny=H*.1+Math.sin(dp*Math.PI)*(-H*.08);
ctx.fillStyle="#ffe060";ctx.beginPath();ctx.arc(sunx,suny,20,0,Math.PI*2);ctx.fill();
ctx.fillStyle="rgba(255,220,80,.22)";ctx.beginPath();ctx.arc(sunx,suny,32,0,Math.PI*2);ctx.fill();
// Clouds
ctx.fillStyle="rgba(255,255,255,.82)";
for(let i=0;i<5;i++){
const ox=((i*270+(ts*.008))%(W+500))-150,oy=40+i*38;
ctx.beginPath();ctx.ellipse(ox,oy,72,26,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(ox+48,oy-14,46,20,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.ellipse(ox+24,oy-20,32,16,0,0,Math.PI*2);ctx.fill();
}
}
// Blocks
const bx0=Math.max(0,Math.floor(cx/BS)-1),bx1=Math.min(WW-1,Math.ceil((cx+W)/BS)+1);
const by0=Math.max(0,Math.floor(cy/BS)-1),by1=Math.min(WH-1,Math.ceil((cy+H)/BS)+1);
for(let by=by0;by<=by1;by++)for(let bx=bx0;bx<=bx1;bx++){
const blk=G.world[by][bx];if(!blk)continue;
const mp=G.mining&&G.mining.bx===bx&&G.mining.by===by?G.mining.t/G.mining.max:0;
drawBlock(ctx,bx*BS-cx,by*BS-cy,blk,mp,!!(G.hover?.bx===bx&&G.hover?.by===by),G.openDoors.has(`${bx},${by}`));
}
// Zombies & Player
G.zombies.forEach(z=>drawZombie(ctx,Math.round(z.x-cx),Math.round(z.y-cy),z.dir,z.flash>0,z.hp,z.onFire));
G.creepers.forEach(c=>drawCreeper(ctx,Math.round(c.x-cx),Math.round(c.y-cy),c.fuse,c.flash>0));
// 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);
});
drawPlayer(ctx,Math.round(p.x-cx),Math.round(p.y-cy),p.dir,p.flash>0);
// HUD
// Sleep overlay
if(G.sleeping){
G.sleepTimer++;
const fadeIn=Math.min(1,G.sleepTimer/40);
const fadeOut=G.sleepTimer>80?Math.max(0,1-(G.sleepTimer-80)/20):1;
const a=fadeIn*fadeOut;
ctx.fillStyle=`rgba(5,5,20,${a*0.97})`;ctx.fillRect(0,0,W,H);
if(G.sleepTimer>20&&G.sleepTimer<90){
ctx.textAlign="center";ctx.font="bold 28px monospace";
ctx.fillStyle=`rgba(255,255,255,${a})`;ctx.fillText("๐ค Sleeping...",W/2,H/2);
ctx.font="16px monospace";ctx.fillStyle=`rgba(200,200,255,${a*0.7})`;
ctx.fillText("Skipping to morning",W/2,H/2+36);ctx.textAlign="left";
}
if(G.sleepTimer===60){G.dayTime=0;G.zombies=[];}
if(G.sleepTimer>=100){G.sleeping=false;G.sleepTimer=0;G.sleepMsg="โ๏ธ Good morning!";setTimeout(()=>G.sleepMsg="",2500);}
}
// Sleep/info message
if(G.sleepMsg){
ctx.textAlign="center";ctx.font="bold 14px monospace";
const tw=ctx.measureText(G.sleepMsg).width;
ctx.fillStyle="rgba(0,0,0,.7)";roundRect(ctx,(W-tw-24)/2,H/2-20,tw+24,34,10);ctx.fill();
ctx.fillStyle="#fff";ctx.fillText(G.sleepMsg,W/2,H/2+4);ctx.textAlign="left";
}
drawHUD(ctx,W,H,G);
raf=requestAnimationFrame(loop);
}
// โโ Resize & Start โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function resize(){
cvs.width=cvs.offsetWidth;cvs.height=cvs.offsetHeight;
// Reposition UI buttons for screen size
const W=cvs.offsetWidth;
document.getElementById('btn-bag').style.right='10px';
}
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,arrows:[],dispenserTick:0,dispenserArrows:{},chestContents:{},playerPlacedChests:new Set(),litTNT:[],creative:false,flying:false,noclip:true,lastDispenserTap:null,pistonStates:{},sleeping:false,sleepTimer:0,sleepMsg:"",
dim:"over",endWorld:null,endSurf:null,overworldPos:null,
dragon:null,endFireballs:[],dragonDefeated:false,
};
document.getElementById('dead').classList.remove('show');
G.creative=true;
fillCreativeInventory();
updateModeBtn();buildRecipeUI();updateFlightBtns();
saveWorld();
}
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||{})),
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.playerPlacedChests=new Set(data.playerPlacedChests||[]);
G.dayTime=data.dayTime||0;
G.creative=data.creative||false;
G.spawnX=data.spawnX;G.spawnY=data.spawnY;
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});
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 (2403 lines, 122.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: mini-minecraft-2d-shadowlegend17-mr71mvs3" to link back to the original. Then publish at arcadelab.ai/publish.