CHAT
Fresh start! Your app is still here — tell your buddy what to build or change next. ✨
CODE
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Powder Game!</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #1a1a2e; display: flex; flex-direction: column; align-items: center; font-family: 'Comic Sans MS', cursive, sans-serif; min-height: 100vh; } h1 { color: #f5deb3; font-size: 1.5em; margin: 8px 0 5px; text-shadow: 1px 1px 0 #a07030; } #toolbar { display: flex; gap: 4px; margin-bottom: 6px; flex-wrap: wrap; justify-content: center; max-width: 800px; } .tool-btn { border: 3px solid #444; border-radius: 8px; padding: 4px 8px; font-size: 0.75em; font-family: 'Comic Sans MS', cursive; cursor: pointer; font-weight: bold; transition: transform 0.1s, border-color 0.1s; color: #fff; } .tool-btn:hover { transform: scale(1.08); border-color: #fff; } .tool-btn.active { border-color: #fff; box-shadow: 0 0 8px #fff8; } #canvas-wrap { border: 4px solid #555; border-radius: 10px; overflow: hidden; box-shadow: 0 6px 24px #0008; position: relative; width: 720px; height: 480px; max-width: 96vw; } canvas { display: block; cursor: crosshair; position: absolute; top:0; left:0; } #bottom-bar { display: flex; align-items: center; gap: 10px; margin-top: 6px; flex-wrap: wrap; justify-content: center; } #bottom-bar label { color: #ccc; font-size: 0.88em; } #size-slider { accent-color: #f5deb3; width: 80px; } .bar-btn { border-radius: 8px; padding: 5px 14px; color: #fff; font-family: 'Comic Sans MS', cursive; font-size: 0.88em; cursor: pointer; font-weight: bold; border: 2px solid #222; } #clear-btn { background: #c0392b; } #clear-btn:hover { background: #e74c3c; } #pause-btn { background: #2980b9; } #pause-btn:hover { background: #3498db; } #gravity-btn { background: #8e44ad; } #gravity-btn:hover { background: #9b59b6; } #player-btn { background: #e67e22; } #player-btn:hover { background: #f39c12; } #zoomin-btn, #zoomout-btn, #zoomreset-btn { background: #16a085; } #zoomin-btn:hover, #zoomout-btn:hover, #zoomreset-btn:hover { background: #1abc9c; } #follow-btn { background: #d35400; } #follow-btn:hover { background: #e67e22; } #grid-btn { background: #34495e; } #grid-btn:hover { background: #46637f; } #controls-hint { color: #aaa; font-size: 0.78em; margin-top: 4px; text-align: center; } #air-bar-wrap { display: none; align-items: center; gap: 6px; margin-top: 4px; } #air-bar-wrap label { color: #aef; font-size: 0.85em; } #air-bar-bg { width: 120px; height: 14px; background: #333; border-radius: 7px; overflow: hidden; border: 2px solid #555; } #air-bar { height: 100%; width: 100%; background: #3af; border-radius: 7px; transition: width 0.1s, background 0.1s; } #minimap-wrap { position: absolute; top: 8px; right: 8px; border: 2px solid #fff8; border-radius: 4px; opacity: 0.9; background: #000; } </style> </head> <body> <h1>💥 Powder Game! (Big World)</h1> <div id="toolbar"> <button class="tool-btn active" id="btn-sand" onclick="setElement('sand')" style="background:#c8a020;">🟡 Sand</button> <button class="tool-btn" id="btn-snow" onclick="setElement('snow')" style="background:#aed6f1;">❄️ Snow</button> <button class="tool-btn" id="btn-gunpowder" onclick="setElement('gunpowder')" style="background:#555;">💥 Powder</button> <button class="tool-btn" id="btn-salt" onclick="setElement('salt')" style="background:#ddd; color:#222;">🧂 Salt</button> <button class="tool-btn" id="btn-ash" onclick="setElement('ash')" style="background:#888;">🌫️ Ash</button> <button class="tool-btn" id="btn-water" onclick="setElement('water')" style="background:#2471a3;">💧 Water</button> <button class="tool-btn" id="btn-oil" onclick="setElement('oil')" style="background:#6e4c00;">🛢️ Oil</button> <button class="tool-btn" id="btn-lava" onclick="setElement('lava')" style="background:#ca3e00;">🌋 Lava</button> <button class="tool-btn" id="btn-acid" onclick="setElement('acid')" style="background:#27ae60;">🧪 Acid</button> <button class="tool-btn" id="btn-mercury" onclick="setElement('mercury')" style="background:#b0b0b0; color:#222;">🪙 Mercury</button> <button class="tool-btn" id="btn-fire" onclick="setElement('fire')" style="background:#c0392b;">🔥 Fire</button> <button class="tool-btn" id="btn-steam" onclick="setElement('steam')" style="background:#7f8c8d;">💨 Steam</button> <button class="tool-btn" id="btn-spark" onclick="setElement('spark')" style="background:#f1c40f; color:#222;">⚡ Spark</button> <button class="tool-btn" id="btn-gas" onclick="setElement('gas')" style="background:#c39bd3;">☁️ Gas</button> <button class="tool-btn" id="btn-wall" onclick="setElement('wall')" style="background:#555;">🧱 Wall</button> <button class="tool-btn" id="btn-wood" onclick="setElement('wood')" style="background:#7d5a2c;">🪵 Wood</button> <button class="tool-btn" id="btn-plant" onclick="setElement('plant')" style="background:#1e8449;">🌿 Plant</button> <button class="tool-btn" id="btn-ice" onclick="setElement('ice')" style="background:#5dade2;">🧊 Ice</button> <button class="tool-btn" id="btn-stone" onclick="setElement('stone')" style="background:#626567;">🪨 Stone</button> <button class="tool-btn" id="btn-glass" onclick="setElement('glass')" style="background:#85c1e9; color:#222;">🪟 Glass</button> <button class="tool-btn" id="btn-metal" onclick="setElement('metal')" style="background:#7f8c8d;">⚙️ Metal</button> <button class="tool-btn" id="btn-brick" onclick="setElement('brick')" style="background:#963c28;">🧱 Brick</button> <button class="tool-btn" id="btn-concrete" onclick="setElement('concrete')" style="background:#a0a09b;">🏗️ Concrete</button> <button class="tool-btn" id="btn-obsidian" onclick="setElement('obsidian')" style="background:#23142f;">🪨 Obsidian</button> <button class="tool-btn" id="btn-fuse" onclick="setElement('fuse')" style="background:#935116;">🧨 Fuse</button> <button class="tool-btn" id="btn-cloud" onclick="setElement('cloud')" style="background:#d5dbdb; color:#222;">☁️ Cloud</button> <button class="tool-btn" id="btn-virus" onclick="setElement('virus')" style="background:#7d3c98;">🦠 Virus</button> <button class="tool-btn" id="btn-clone" onclick="setElement('clone')" style="background:#117a65;">🔁 Clone</button> <button class="tool-btn" id="btn-ant" onclick="setElement('ant')" style="background:#2e4053;">🐜 Ant</button> <button class="tool-btn" id="btn-bubble" onclick="setElement('bubble')" style="background:#a9cce3; color:#222;">🫧 Bubble</button> <button class="tool-btn" id="btn-gel" onclick="setElement('gel')" style="background:#a569bd;">🫙 Gel</button> <button class="tool-btn" id="btn-bomb" onclick="setElement('bomb')" style="background:#2c3e50;">💣 Bomb</button> <button class="tool-btn" id="btn-erase" onclick="setElement('erase')" style="background:#333;">❌ Erase</button> </div> <div id="canvas-wrap"> <canvas id="c"></canvas> <canvas id="minimap-wrap"></canvas> </div> <div id="bottom-bar"> <label>Brush: <input type="range" id="size-slider" min="1" max="12" value="3"></label> <button class="bar-btn" id="pause-btn" onclick="togglePause()">⏸️ Pause</button> <button class="bar-btn" id="clear-btn" onclick="clearAll()">🗑️ Clear</button> <button class="bar-btn" id="gravity-btn" onclick="toggleGravity()">⬇️ Normal G</button> <button class="bar-btn" id="player-btn" onclick="togglePlayer()">🧑 Add Player</button> <button class="bar-btn" id="player2-btn" onclick="togglePlayer2()" style="background:#2255aa;">🧑 Add Player 2</button> <button class="bar-btn" id="weapon1-btn" onclick="cycleWeapon(player,'weapon1-btn')" style="background:#b03030;">🔫 Laser</button> <button class="bar-btn" id="weapon2-btn" onclick="cycleWeapon(player2,'weapon2-btn')" style="background:#3050b0;">🔫 Laser</button> <button class="bar-btn" id="zoomin-btn" onclick="zoomIn()">🔍+ Zoom In</button> <button class="bar-btn" id="zoomout-btn" onclick="zoomOut()">🔎- Zoom Out</button> <button class="bar-btn" id="zoomreset-btn" onclick="zoomReset()">🗺️ Reset View</button> <button class="bar-btn" id="follow-btn" onclick="toggleFollow()">🎯 Follow: Off</button> <button class="bar-btn" id="grid-btn" onclick="toggleGridLines()">🔲 Grid: On</button> <button class="bar-btn" id="fort-btn" onclick="buildFort()" style="background:#7f6000;">🏰 Auto Fort</button> <button class="bar-btn" id="maze-btn" onclick="buildMaze()" style="background:#4a235a;">🌀 Maze Fort</button> <button class="bar-btn" id="landscape-btn" onclick="generateLandscape()" style="background:#1e6b3a;">🏞️ Random Landscape</button> <button class="bar-btn" id="turret-btn" onclick="placeTurretMode=!placeTurretMode; document.getElementById('turret-btn').textContent = placeTurretMode ? '🔧 Placing Turret (click map)' : '🛡️ Place Turret';" style="background:#555;">🛡️ Place Turret</button> </div> <div id="air-bar-wrap"> <label>💨 Air:</label> <div id="air-bar-bg"><div id="air-bar"></div></div> </div> <div id="controls-hint">🧑 P1 (red): ← → move, Space/↑ jump, Z/Shift/Left-Click to shoot, B to throw bomb! | 🧑 P2 (blue): A/D move, W jump, F/Shift to shoot, G to throw bomb! | Drag with mouse (right-click or two-finger) to pan, scroll wheel or +/- buttons to zoom! 🔍</div> <script> // ============ BIG WORLD SETUP ============ const COLS=480, ROWS=320, CELL=4; // big world in cell units const VIEW_W=720, VIEW_H=480; // viewport size in CSS pixels const canvas=document.getElementById('c'); canvas.width=VIEW_W; canvas.height=VIEW_H; const ctx=canvas.getContext('2d'); const minimap=document.getElementById('minimap-wrap'); const MM_SCALE=0.12; minimap.width=Math.floor(COLS*CELL*MM_SCALE); minimap.height=Math.floor(ROWS*CELL*MM_SCALE); const mctx=minimap.getContext('2d'); let camX = (COLS*CELL - VIEW_W)/2; // top-left of camera in world pixels let camY = (ROWS*CELL - VIEW_H)/2; let zoom = 1; // 1 = 1 world px = 1 screen px const MIN_ZOOM=0.4, MAX_ZOOM=4; let followPlayerMode=false; let showGrid=true; function clampCam(){ const worldW=COLS*CELL, worldH=ROWS*CELL; const viewWorldW = VIEW_W/zoom, viewWorldH = VIEW_H/zoom; camX = Math.max(0, Math.min(worldW-viewWorldW, camX)); camY = Math.max(0, Math.min(worldH-viewWorldH, camY)); if(viewWorldW>worldW) camX=(worldW-viewWorldW)/2; if(viewWorldH>worldH) camY=(worldH-viewWorldH)/2; } function zoomIn(){ zoom=Math.min(MAX_ZOOM, zoom*1.3); clampCam(); } function zoomOut(){ zoom=Math.max(MIN_ZOOM, zoom/1.3); clampCam(); } function zoomReset(){ zoom=1; camX=(COLS*CELL-VIEW_W)/2; camY=(ROWS*CELL-VIEW_H)/2; clampCam(); } function toggleFollow(){ followPlayerMode=!followPlayerMode; document.getElementById('follow-btn').textContent = '🎯 Follow: ' + (followPlayerMode?'On':'Off'); } function toggleGridLines(){ showGrid=!showGrid; document.getElementById('grid-btn').textContent = '🔲 Grid: ' + (showGrid?'On':'Off'); } // panning with mouse drag (right button or middle) & touch two-finger let isPanning=false, panStartX=0, panStartY=0, camStartX=0, camStartY=0; canvas.addEventListener('contextmenu', e=>e.preventDefault()); canvas.addEventListener('mousedown', e=>{ if(e.button===2 || e.button===1){ isPanning=true; panStartX=e.clientX; panStartY=e.clientY; camStartX=camX; camStartY=camY; e.preventDefault(); } }); window.addEventListener('mousemove', e=>{ if(isPanning){ camX = camStartX - (e.clientX-panStartX)/zoom; camY = camStartY - (e.clientY-panStartY)/zoom; clampCam(); } }); window.addEventListener('mouseup', e=>{ if(e.button===2||e.button===1) isPanning=false; }); canvas.addEventListener('wheel', e=>{ e.preventDefault(); if(e.deltaY<0) zoomIn(); else zoomOut(); },{passive:false}); // two-finger touch pan let touchPanActive=false, tpStartX=0, tpStartY=0, tpCamX=0, tpCamY=0; let lastPinchDist=null; canvas.addEventListener('touchstart', e=>{ if(e.touches.length===2){ touchPanActive=true; tpStartX=(e.touches[0].clientX+e.touches[1].clientX)/2; tpStartY=(e.touches[0].clientY+e.touches[1].clientY)/2; tpCamX=camX; tpCamY=camY; lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX, e.touches[0].clientY-e.touches[1].clientY); e.preventDefault(); } },{passive:false}); canvas.addEventListener('touchmove', e=>{ if(touchPanActive && e.touches.length===2){ const mx=(e.touches[0].clientX+e.touches[1].clientX)/2; const my=(e.touches[0].clientY+e.touches[1].clientY)/2; camX = tpCamX - (mx-tpStartX)/zoom; camY = tpCamY - (my-tpStartY)/zoom; const dist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX, e.touches[0].clientY-e.touches[1].clientY); if(lastPinchDist){ const ratio=dist/lastPinchDist; zoom=Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom*ratio)); } lastPinchDist=dist; clampCam(); e.preventDefault(); } },{passive:false}); canvas.addEventListener('touchend', e=>{ if(e.touches.length<2){ touchPanActive=false; lastPinchDist=null; } }); // ============ ELEMENT TYPES ============ const EMPTY=0,SAND=1,WATER=2,WALL=3,FIRE=4,WOOD=5,PLANT=6,STEAM=7,SMOKE=8, OIL=9,ICE=10,STONE=11,LAVA=12,ACID=13,SNOW=14,GUNPOWDER=15,GLASS=16,CLOUD=17, SPARK=18,VIRUS=19,CLONE=20,ANT=21,BUBBLE=22,GEL=23,BOMB=24,METAL=25,FUSE=26, GAS=27,MERCURY=28,SALT=29,ASH=30,BRICK=31,CONCRETE=32,OBSIDIAN=33; const BREATHABLE = new Set([EMPTY, STEAM, SMOKE, GAS, BUBBLE]); const SUFFOCATING_SOLIDS = new Set([SAND,WATER,WALL,WOOD,PLANT,OIL,ICE,STONE,LAVA,ACID,SNOW, GUNPOWDER,GLASS,CLOUD,VIRUS,CLONE,ANT,GEL,BOMB,METAL,FUSE,MERCURY,SALT,ASH,BRICK,CONCRETE,OBSIDIAN]); const BASE_COLORS={ [EMPTY]:null, [SAND]:[200,160,30],[WATER]:[30,100,200],[WALL]:[90,90,90],[FIRE]:[220,60,10], [WOOD]:[120,80,30],[PLANT]:[30,140,50],[STEAM]:[160,160,180],[SMOKE]:[80,80,80], [OIL]:[110,76,0],[ICE]:[93,173,226],[STONE]:[98,101,103],[LAVA]:[202,62,0], [ACID]:[39,174,96],[SNOW]:[210,230,255],[GUNPOWDER]:[68,68,68],[GLASS]:[133,193,233], [CLOUD]:[213,219,219],[SPARK]:[255,230,50],[VIRUS]:[160,40,200],[CLONE]:[20,180,130], [ANT]:[50,60,80],[BUBBLE]:[180,220,255],[GEL]:[180,100,220],[BOMB]:[40,40,60], [METAL]:[120,130,140],[FUSE]:[160,90,30],[GAS]:[200,170,220],[MERCURY]:[180,180,195], [SALT]:[230,230,230],[ASH]:[130,130,130], [BRICK]:[150,60,40],[CONCRETE]:[160,160,155],[OBSIDIAN]:[35,20,50], }; let grid=[]; let paused=false; let currentElement='sand'; let brushSize=3; let mouseDown=false; let mouseX=0,mouseY=0; let gravityDir=1; let gravityMode=0; const MAX_AIR = 300; let player = { active: false, x: COLS/2 * CELL, y: 10 * CELL, vx: 0, vy: 0, w: 10, h: 16, onGround: false, dead: false, deadReason: '', air: MAX_AIR, suffocating: false, inWater: false, color: '#ff6b6b', facing: 1, hp: 5, maxHp: 5, hitFlash: 0, wins: 0, }; let player2 = { active: false, x: COLS/2 * CELL + 60, y: 10 * CELL, vx: 0, vy: 0, w: 10, h: 16, onGround: false, dead: false, deadReason: '', air: MAX_AIR, suffocating: false, inWater: false, color: '#5b8bff', facing: -1, hp: 5, maxHp: 5, hitFlash: 0, wins: 0, }; const keys = {}; window.addEventListener('keydown', e => { keys[e.code] = true; if(['ArrowLeft','ArrowRight','ArrowUp','Space','KeyZ','KeyA','KeyD','KeyW','KeyF'].includes(e.code)) e.preventDefault(); if(e.code === 'KeyZ' || e.code === 'ShiftRight') shootLaser(player, 1); if(e.code === 'KeyF' || e.code === 'ShiftLeft') shootLaser(player2, 2); if(e.code === 'KeyB') throwBomb(player, 1); if(e.code === 'KeyG') throwBomb(player2, 2); }); window.addEventListener('keyup', e => { keys[e.code] = false; }); function togglePlayer() { player.active = !player.active; if(player.active) { respawnPlayer(player); document.getElementById('player-btn').textContent = '💀 Remove Player'; document.getElementById('air-bar-wrap').style.display = 'flex'; } else { document.getElementById('player-btn').textContent = '🧑 Add Player'; document.getElementById('air-bar-wrap').style.display = 'none'; } } function togglePlayer2() { player2.active = !player2.active; if(player2.active) { respawnPlayer(player2); document.getElementById('player2-btn').textContent = '💀 Remove P2'; } else { document.getElementById('player2-btn').textContent = '🧑 Add Player 2'; } } function respawnPlayer(p) { p.x = (COLS/2) * CELL + (p===player2 ? 60 : 0); p.y = (gravityDir === -1) ? (ROWS-5)*CELL : 10*CELL; p.vx = 0; p.vy = 0; p.dead = false; p.deadReason = ''; p.air = MAX_AIR; p.suffocating = false; p.inWater = false; p.hp = p.maxHp; p.hitFlash = 0; } function killPlayer(p, reason) { if(p.dead) return; p.dead = true; p.deadReason = reason; const winner = p === player ? player2 : player; if(winner.active){ winner.wins++; showKO(winner === player ? 'P1' : 'P2'); } setTimeout(() => { respawnPlayer(p); }, 1800); } let koText = null, koTimer = 0; function showKO(winnerName){ koText = winnerName + ' WINS THE ROUND! 🏆'; koTimer = 90; } function damagePlayer(p, amount, reason) { if(p.dead) return; p.hp -= amount; p.hitFlash = 10; if(p.hp <= 0) killPlayer(p, reason); } function updateAirBar() { if(!player.active) return; const bar = document.getElementById('air-bar'); const pct = Math.max(0, player.air / MAX_AIR * 100); bar.style.width = pct + '%'; if(pct > 50) bar.style.background = '#3af'; else if(pct > 25) bar.style.background = '#fa0'; else bar.style.background = '#f33'; } function checkSuffocation(p) { if(!p.active || p.dead) return; let hasAir = false; let inWater = false; let cx1 = Math.floor(p.x / CELL); let cx2 = Math.floor((p.x + p.w - 1) / CELL); let cy1 = Math.floor(p.y / CELL); let cy2 = Math.floor((p.y + p.h - 1) / CELL); let waterCount = 0, totalCount = 0; for(let gy = cy1; gy <= cy2; gy++) { for(let gx = cx1; gx <= cx2; gx++) { if(gx < 0 || gx >= COLS || gy < 0 || gy >= ROWS) continue; totalCount++; const t = grid[idx(gx,gy)].type; if(BREATHABLE.has(t)) hasAir = true; if(t === WATER) waterCount++; } } inWater = totalCount > 0 && waterCount / totalCount > 0.5; p.inWater = inWater && !hasAir; if(!hasAir) { p.suffocating = true; let drainRate = p.inWater ? 0.4 : 1; p.air = Math.max(0, p.air - drainRate); if(p.air <= 0) { killPlayer(p, p.inWater ? '🌊 Drowned!' : '😵 Can\'t breathe!'); } } else { p.suffocating = false; p.inWater = false; p.air = Math.min(MAX_AIR, p.air + 3); } if(p === player) updateAirBar(); } // ============ LASERS ============ let lasers = []; const LASER_SPEED = 14; const LASER_MAX_LIFE = 40; const LASER_MELT = new Set([SAND,SNOW,SALT,ASH,WOOD,PLANT,ICE,GLASS,GUNPOWDER,FUSE,GEL,STONE,CLOUD,VIRUS,CLONE,ANT]); const LASER_BLOCK_EXTRA = new Set([BRICK,CONCRETE,OBSIDIAN]); let laserCooldown = 0; let laserCooldown2 = 0; const WEAPONS = { laser: { name:'🔫 Laser', speed:14, cooldown:8, dmg:1, life:40, color:'#ff2255', spread:0, count:1, splash:0 }, rapid: { name:'💫 Rapid', speed:16, cooldown:3, dmg:1, life:34, color:'#ffcc22', spread:0.12, count:1, splash:0 }, shotgun:{ name:'💥 Shotgun', speed:12, cooldown:22, dmg:1, life:22, color:'#ff8822', spread:0.5, count:5, splash:0 }, cannon: { name:'☄️ Cannon', speed:9, cooldown:30, dmg:2, life:55, color:'#aa44ff', spread:0, count:1, splash:9 }, rocket: { name:'🚀 Rocket', speed:6, cooldown:45, dmg:3, life:90, color:'#ff6600', spread:0, count:1, splash:11, homing:true }, freeze: { name:'🧊 Freeze', speed:11, cooldown:26, dmg:0, life:40, color:'#66eeff', spread:0, count:1, splash:0, freeze:true }, wave: { name:'🌊 Wave', speed:8, cooldown:20, dmg:1, life:60, color:'#22ccff', spread:0.35, count:3, splash:0, bounce:true }, sniper: { name:'🎯 Sniper', speed:0, cooldown:60, dmg:99, life:1, color:'#ffffff', spread:0, count:1, splash:0, hitscan:true }, }; player.weapon = 'laser'; player2.weapon = 'laser'; let sniperBeams = []; let bombs = []; let bombCooldown = 0, bombCooldown2 = 0; function throwBomb(p, ownerNum) { if(!p.active || p.dead) return; if(ownerNum === 1) { if(bombCooldown > 0) return; bombCooldown = 50; } else { if(bombCooldown2 > 0) return; bombCooldown2 = 50; } bombs.push({ x: p.x + p.w/2 + p.facing * (p.w/2 + 4), y: p.y + 2, vx: p.facing * 5, vy: -6 * (gravityDir||1) * -1, owner: ownerNum, life: 200, armTime: 12, }); } function updateBombs() { if(bombCooldown > 0) bombCooldown--; if(bombCooldown2 > 0) bombCooldown2--; for(let i = bombs.length - 1; i >= 0; i--) { const b = bombs[i]; if(b.armTime > 0) b.armTime--; b.vy += 0.3 * (gravityDir||1); b.x += b.vx; b.y += b.vy; b.life--; const gx = Math.floor(b.x / CELL), gy = Math.floor(b.y / CELL); let exploded = false; if(gx < 0 || gx >= COLS || gy < 0 || gy >= ROWS) exploded = true; else if(get(gx,gy).type === WALL || get(gx,gy).type === METAL || get(gx,gy).type === STONE || get(gx,gy).type === BRICK || get(gx,gy).type === CONCRETE || get(gx,gy).type === OBSIDIAN) exploded = true; if(!exploded && b.life <= 0) exploded = true; if(b.armTime <= 0 && [player, player2].some(p => p.active && !p.dead && b.x >= p.x && b.x <= p.x+p.w && b.y >= p.y && b.y <= p.y+p.h)) exploded = true; if(exploded) { splashDamage(b.x, b.y, 10); bombs.splice(i,1); } } } function drawBombs() { if(bombs.length === 0) return; ctx.save(); ctx.translate(-camX*zoom, -camY*zoom); ctx.scale(zoom, zoom); for(const b of bombs) { ctx.fillStyle = '#222'; ctx.beginPath(); ctx.arc(b.x, b.y, 3, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#ff5522'; ctx.fillRect(b.x-1, b.y-5, 2, 3); } ctx.restore(); } function drawSniperBeams() { if(sniperBeams.length === 0) return; ctx.save(); ctx.translate(-camX*zoom, -camY*zoom); ctx.scale(zoom, zoom); for(let i = sniperBeams.length - 1; i >= 0; i--) { const s = sniperBeams[i]; s.life--; ctx.strokeStyle = `rgba(255,255,255,${Math.max(0,s.life/10)})`; ctx.lineWidth = 1.5; ctx.shadowColor = '#fff'; ctx.shadowBlur = 8; ctx.beginPath(); ctx.moveTo(s.x1, s.y1); ctx.lineTo(s.x2, s.y2); ctx.stroke(); ctx.shadowBlur = 0; if(s.life <= 0) sniperBeams.splice(i,1); } ctx.restore(); } function shootLaser(p, ownerNum) { p = p || player; ownerNum = ownerNum || 1; if(!p.active || p.dead) return; const wpn = WEAPONS[p.weapon] || WEAPONS.laser; if(ownerNum === 1) { if(laserCooldown > 0) return; laserCooldown = wpn.cooldown; } else { if(laserCooldown2 > 0) return; laserCooldown2 = wpn.cooldown; } const startX = p.x + p.w/2 + p.facing * (p.w/2 + 2); const startY = p.y + 6; if(wpn.hitscan) { const target = ownerNum === 1 ? player2 : player; let endX = startX + p.facing * 2000; let endY = startY; // stop the beam at the first solid block in its path const HITSCAN_BLOCKERS = new Set([WALL, METAL, BRICK, CONCRETE, OBSIDIAN, STONE, WOOD, GLASS, ICE]); const stepPx = 2; const maxSteps = Math.floor(2000 / stepPx); let blockedX = null, blockedY = null; for(let s = 1; s <= maxSteps; s++) { const cx = startX + p.facing * s * stepPx; const cy = startY; const gx = Math.floor(cx / CELL), gy = Math.floor(cy / CELL); if(gx < 0 || gx >= COLS || gy < 0 || gy >= ROWS) { blockedX = cx; blockedY = cy; break; } if(HITSCAN_BLOCKERS.has(get(gx,gy).type)) { blockedX = cx; blockedY = cy; break; } } if(blockedX !== null) { endX = blockedX; endY = blockedY; } if(target.active && !target.dead) { const tcx = target.x + target.w/2, tcy = target.y + target.h/2; const sameDir = (tcx - startX) * p.facing > 0; const notBlocked = blockedX === null || Math.abs(tcx - startX) < Math.abs(blockedX - startX); if(sameDir && notBlocked) { damagePlayer(target, 99, ownerNum===1 ? '🎯 Sniped by P1!' : '🎯 Sniped by P2!'); endX = tcx; endY = tcy; } } sniperBeams.push({ x1: startX, y1: startY, x2: endX, y2: endY, life: 10 }); return; } for(let i=0;i<wpn.count;i++){ let ang = (Math.random()-0.5) * wpn.spread + (wpn.count>1 ? (i-(wpn.count-1)/2)*0.12 : 0); const vx = p.facing * wpn.speed * Math.cos(ang); const vy = wpn.speed * Math.sin(ang); lasers.push({ x: startX, y: startY, vx: vx, vy: vy, life: wpn.life, owner: ownerNum, dmg: wpn.dmg, color: wpn.color, splash: wpn.splash, homing: wpn.homing||false, freeze: wpn.freeze||false, bounce: wpn.bounce||false, }); } } function cycleWeapon(p, btnId){ const names = Object.keys(WEAPONS); let i = names.indexOf(p.weapon); i = (i+1) % names.length; p.weapon = names[i]; document.getElementById(btnId).textContent = WEAPONS[p.weapon].name; } function laserHitsPlayer(L, p) { if(!p.active || p.dead) return false; return L.x >= p.x && L.x <= p.x + p.w && L.y >= p.y && L.y <= p.y + p.h; } function updateLasers() { if(laserCooldown > 0) laserCooldown--; if(laserCooldown2 > 0) laserCooldown2--; for(let i = lasers.length - 1; i >= 0; i--) { const L = lasers[i]; L.life--; if(L.life <= 0) { lasers.splice(i,1); continue; } // homing rockets steer toward the enemy player if(L.homing) { const target = L.owner === 1 ? player2 : player; if(target.active && !target.dead) { const tx = target.x + target.w/2, ty = target.y + target.h/2; const dx = tx - L.x, dy = ty - L.y; const dist = Math.hypot(dx,dy) || 1; const speed = Math.hypot(L.vx, L.vy) || 6; const turnRate = 0.16; const desiredVx = (dx/dist) * speed, desiredVy = (dy/dist) * speed; L.vx += (desiredVx - L.vx) * turnRate; L.vy += (desiredVy - L.vy) * turnRate; } } // check player hits first if(L.owner !== 1 && laserHitsPlayer(L, player)) { if(L.freeze) { player.frozen = 90; } else damagePlayer(player, L.dmg||1, '💥 Blasted by P2!'); if(L.splash) splashDamage(L.x, L.y, L.splash); lasers.splice(i,1); continue; } if(L.owner !== 2 && laserHitsPlayer(L, player2)) { if(L.freeze) { player2.frozen = 90; } else damagePlayer(player2, L.dmg||1, '💥 Blasted by P1!'); if(L.splash) splashDamage(L.x, L.y, L.splash); lasers.splice(i,1); continue; } // move in small steps so it doesn't tunnel through thick blocks without effect const steps = 3; let hit = false; for(let s = 0; s < steps; s++) { L.x += L.vx/steps; L.y += L.vy/steps; const gx = Math.floor(L.x / CELL); const gy = Math.floor(L.y / CELL); if(gx < 0 || gx >= COLS || gy < 0 || gy >= ROWS) { hit = true; break; } const cell = grid[idx(gx,gy)]; const t = cell.type; if(t === WALL || t === METAL || LASER_BLOCK_EXTRA.has(t)) { if(L.bounce) { if(gx <= 0 || gx >= COLS-1) L.vx *= -1; if(gy <= 0 || gy >= ROWS-1) L.vy *= -1; const gxr = Math.floor((L.x - L.vx) / CELL); const gyr = Math.floor((L.y - L.vy) / CELL); if(get(gxr, gy).type === WALL || get(gxr,gy).type === METAL) L.vx *= -1; if(get(gx, gyr).type === WALL || get(gx,gyr).type === METAL) L.vy *= -1; continue; } hit = true; break; } if(t !== EMPTY) { if(LASER_MELT.has(t)) { set(gx, gy, EMPTY, 0); } else if(t === WATER || t === OIL || t === ACID || t === LAVA || t === MERCURY) { // passes through liquids but leaves steam sometimes if(t === WATER && Math.random() < 0.3) set(gx, gy, STEAM, 30); } else if(t === FIRE || t === SPARK || t === STEAM || t === SMOKE || t === GAS || t === BUBBLE) { // passes through freely } else { set(gx, gy, FIRE, 40); hit = true; break; } } } if(hit) { if(L.splash) splashDamage(L.x, L.y, L.splash); lasers.splice(i,1); continue; } } } function splashDamage(wx, wy, radius){ const r = radius*CELL; for(let ey=-radius;ey<=radius;ey++){ for(let ex=-radius;ex<=radius;ex++){ if(ex*ex+ey*ey>radius*radius) continue; let gx = Math.floor(wx/CELL)+ex, gy = Math.floor(wy/CELL)+ey; if(gx<0||gx>=COLS||gy<0||gy>=ROWS) continue; let ct = get(gx,gy).type; if(ct!==WALL && ct!==METAL && ct!==BRICK && ct!==CONCRETE && ct!==OBSIDIAN) set(gx,gy,FIRE,50+Math.floor(Math.random()*30)); } } [player, player2].forEach(p=>{ if(!p.active||p.dead) return; const pcx = p.x+p.w/2, pcy = p.y+p.h/2; const dist = Math.hypot(pcx-wx, pcy-wy); if(dist < r) damagePlayer(p, 2, '☄️ Caught the blast!'); }); } function drawLasers() { if(lasers.length === 0) return; ctx.save(); ctx.translate(-camX*zoom, -camY*zoom); ctx.scale(zoom, zoom); for(const L of lasers) { ctx.strokeStyle = L.color || (L.owner === 2 ? '#2299ff' : '#ff2255'); ctx.lineWidth = 2.2; ctx.shadowColor = L.color || (L.owner === 2 ? '#55bbff' : '#ff5588'); ctx.shadowBlur = 6; ctx.beginPath(); ctx.moveTo(L.x - L.vx*0.6, L.y); ctx.lineTo(L.x + L.vx*0.2, L.y); ctx.stroke(); ctx.shadowBlur = 0; } ctx.restore(); } function stepPlayer(p, leftKey, rightKey, upKey, upKey2) { if(!p.active || p.dead) return; if(p.frozen > 0) { p.frozen--; p.vx *= 0.8; p.vy *= 0.8; checkSuffocation(p); if(p.hitFlash > 0) p.hitFlash--; return; } if(keys[leftKey]) { p.vx -= 0.8; p.facing = -1; } if(keys[rightKey]) { p.vx += 0.8; p.facing = 1; } const wantsUp = keys[upKey] || (upKey2 && keys[upKey2]); if(p.inWater) { if(wantsUp) { p.vy -= 0.5 * gravityDir || -0.5; if(gravityDir === 0) p.vy -= 0.3; } p.vy -= 0.05 * gravityDir; } if(gravityDir === 1) { if(wantsUp && p.onGround) { p.vy = -6; p.onGround = false; } } else if(gravityDir === -1) { if(wantsUp && p.onGround) { p.vy = 6; p.onGround = false; } } else { if(wantsUp) p.vy -= 0.6; } p.vx *= 0.75; if(gravityDir === 1) p.vy += 0.35; else if(gravityDir === -1) p.vy -= 0.35; else p.vy *= 0.92; p.vx = Math.max(-4, Math.min(4, p.vx)); p.vy = Math.max(-10, Math.min(10, p.vy)); p.x += p.vx; if(p.x < 0) { p.x = 0; p.vx = 0; } if(p.x + p.w > COLS * CELL) { p.x = COLS * CELL - p.w; p.vx = 0; } if(p.vx > 0) { let cxr = Math.floor((p.x + p.w - 1) / CELL); for(let gy = Math.floor(p.y/CELL); gy <= Math.floor((p.y+p.h-1)/CELL); gy++) { if(isSolid(cxr, gy)) { p.x = cxr * CELL - p.w; p.vx = 0; break; } } } else if(p.vx < 0) { let cxl = Math.floor(p.x / CELL); for(let gy = Math.floor(p.y/CELL); gy <= Math.floor((p.y+p.h-1)/CELL); gy++) { if(isSolid(cxl, gy)) { p.x = (cxl+1)*CELL; p.vx = 0; break; } } } p.y += p.vy; p.onGround = false; if(p.y < 0) { p.y = 0; p.vy = 0; if(gravityDir===-1) p.onGround=true; } if(p.y + p.h > ROWS * CELL) { p.y = ROWS * CELL - p.h; p.vy = 0; if(gravityDir===1) p.onGround=true; } if(p.vy > 0) { let gyb = Math.floor((p.y + p.h - 1) / CELL); for(let gx = Math.floor(p.x/CELL); gx <= Math.floor((p.x+p.w-1)/CELL); gx++) { if(isSolid(gx, gyb)) { p.y = gyb * CELL - p.h; p.vy = 0; if(gravityDir >= 0) p.onGround = true; break; } } } else if(p.vy < 0) { let gyt = Math.floor(p.y / CELL); for(let gx = Math.floor(p.x/CELL); gx <= Math.floor((p.x+p.w-1)/CELL); gx++) { if(isSolid(gx, gyt)) { p.y = (gyt + 1) * CELL; p.vy = 0; if(gravityDir === -1) p.onGround = true; break; } } } let pcx = Math.floor((p.x + p.w/2) / CELL); let pcy = Math.floor((p.y + p.h/2) / CELL); let pt = get(pcx, pcy).type; if(pt === FIRE || pt === LAVA || pt === SPARK) killPlayer(p, '🔥 Ouch!'); else if(pt === ACID) killPlayer(p, '🧪 Dissolved!'); checkSuffocation(p); if(p.hitFlash > 0) p.hitFlash--; } function updatePlayer() { stepPlayer(player, 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'Space'); stepPlayer(player2, 'KeyA', 'KeyD', 'KeyW', null); updateLasers(); updateBombs(); updateTurrets(); if(followPlayerMode && player.active){ camX = player.x + player.w/2 - (VIEW_W/zoom)/2; camY = player.y + player.h/2 - (VIEW_H/zoom)/2; clampCam(); } } function isSolid(x, y) { if(x < 0 || x >= COLS || y < 0 || y >= ROWS) return true; const t = grid[idx(x,y)].type; return t === WALL || t === WOOD || t === STONE || t === METAL || t === GLASS || t === ICE || t === GEL || t === SAND || t === SNOW || t === GUNPOWDER || t === SALT || t === ASH || t === CLONE || t === FUSE || t === BOMB || t === BRICK || t === CONCRETE || t === OBSIDIAN; } function drawOnePlayer(player) { if(!player.active) return; ctx.save(); ctx.translate(-camX*zoom, -camY*zoom); ctx.scale(zoom, zoom); if(player.suffocating && !player.dead) { ctx.globalAlpha = 0.7 + 0.3*Math.sin(Date.now()/60); } if(player.dead) { ctx.globalAlpha = 0.5 + 0.5*Math.sin(Date.now()/80); } if(gravityDir === -1) { ctx.save(); ctx.translate(player.x + player.w/2, player.y + player.h/2); ctx.scale(1, -1); ctx.translate(-(player.x + player.w/2), -(player.y + player.h/2)); } let bodyColor = player.color; if(player.frozen > 0) bodyColor = '#aeefff'; if(player.suffocating && !player.dead) { if(player.inWater) { bodyColor = '#3aa0d0'; } else { const t = 1 - (player.air / MAX_AIR); bodyColor = `rgb(${Math.floor(255*(1-t))},${Math.floor(100*(1-t))},${Math.floor(200*t+55)})`; } } if(player.dead) bodyColor = '#ff0000'; if(player.hitFlash > 0) bodyColor = '#ffffff'; ctx.fillStyle = bodyColor; ctx.fillRect(player.x, player.y + 5, player.w, player.h - 5); ctx.fillStyle = player.suffocating ? '#a0c0ff' : '#ffe0b2'; ctx.fillRect(player.x + 1, player.y, player.w - 2, 8); ctx.fillStyle = '#222'; ctx.fillRect(player.x + 2, player.y + 2, 2, 2); ctx.fillRect(player.x + 6, player.y + 2, 2, 2); ctx.fillStyle = '#222'; if(player.suffocating) { ctx.fillRect(player.x + 3, player.y + 5, 4, 1); ctx.fillRect(player.x + 2, player.y + 6, 1, 1); ctx.fillRect(player.x + 7, player.y + 6, 1, 1); } else { ctx.fillRect(player.x + 2, player.y + 5, 6, 1); } // laser gun if(!player.dead) { ctx.fillStyle = '#333'; const gunY = player.y + 8; if(player.facing === 1) { ctx.fillRect(player.x + player.w - 2, gunY, 6, 3); } else { ctx.fillRect(player.x - 4, gunY, 6, 3); } } if(player.inWater && !player.dead) { ctx.fillStyle = 'rgba(200,230,255,0.8)'; const bt = Date.now()/200; for(let i=0;i<3;i++){ let by = player.y - 3 - ((bt*7 + i*6) % 18); let bx = player.x + player.w/2 + Math.sin(bt+i)*3; ctx.beginPath(); ctx.arc(bx, by, 1.5, 0, Math.PI*2); ctx.fill(); } } if(gravityDir === -1) ctx.restore(); // health bar above head if(!player.dead) { const barW = player.w + 2; ctx.fillStyle = '#000a'; ctx.fillRect(player.x - 1, player.y - 10, barW, 4); ctx.fillStyle = player.hp > player.maxHp*0.5 ? '#3f3' : (player.hp > player.maxHp*0.25 ? '#fa3' : '#f33'); ctx.fillRect(player.x - 1, player.y - 10, barW * Math.max(0,player.hp/player.maxHp), 4); } if(player.dead) { ctx.fillStyle = '#ff4444'; ctx.font = 'bold 13px Comic Sans MS'; ctx.fillText(player.deadReason, player.x - 20, player.y - 12); } else if(player.suffocating) { ctx.fillStyle = player.inWater ? '#5cc9ff' : '#88aaff'; ctx.font = 'bold 12px Comic Sans MS'; ctx.fillText(player.inWater ? '🌊 Swim!' : '😵 No air!', player.x - 15, player.y - 12); } ctx.restore(); } function drawPlayer() { drawOnePlayer(player); drawOnePlayer(player2); } const ANT_DIR=new Uint8Array(COLS*ROWS); function initGrid(){ grid=new Array(COLS*ROWS).fill(null).map(()=>({type:EMPTY,life:0,extra:0})); } function idx(x,y){return y*COLS+x;} function get(x,y){ if(x<0||x>=COLS||y<0||y>=ROWS) return {type:WALL,life:0,extra:0}; return grid[idx(x,y)]; } function set(x,y,type,life,extra){ if(x<0||x>=COLS||y<0||y>=ROWS) return; grid[idx(x,y)].type=type; grid[idx(x,y)].life=life||0; grid[idx(x,y)].extra=extra||0; } function isEmpty(x,y){return get(x,y).type===EMPTY;} function isType(x,y,t){return get(x,y).type===t;} function canFallInto(t){return t===EMPTY||t===WATER||t===OIL||t===ACID||t===GAS||t===STEAM||t===SMOKE||t===BUBBLE;} // ============ RENDERING (viewport only) ============ function cellColor(cell){ let r=20,g=20,b=30,a=255; if(cell.type!==EMPTY){ const base=BASE_COLORS[cell.type]; if(base){ r=base[0];g=base[1];b=base[2];a=255; const rng=Math.random(); if(cell.type===FIRE){r=base[0]+Math.floor(rng*40);g=base[1]+Math.floor(rng*80);} else if(cell.type===LAVA){r=base[0]+Math.floor(rng*40);g=base[1]+Math.floor(rng*40);} else if(cell.type===SPARK){r=255;g=200+Math.floor(rng*55);b=Math.floor(rng*80);} else if(cell.type===STEAM||cell.type===SMOKE||cell.type===GAS){a=140+Math.floor(rng*60);} else if(cell.type===GLASS){a=180;} else if(cell.type===BUBBLE){a=120+Math.floor(rng*60);} else if(cell.type===SAND){r=base[0]+(rng>0.5?10:-10);g=base[1]+(rng>0.3?8:-4);} else if(cell.type===SALT){r=220+Math.floor(rng*35);g=220+Math.floor(rng*35);b=220+Math.floor(rng*35);} else if(cell.type===BOMB){r=30+Math.floor(cell.life*0.3);g=30;b=50;} } } return [r,g,b,a]; } function drawBackgroundGrid(startCX,endCX,startCY,endCY){ if(!showGrid) return; // Only draw grid lines if zoomed in enough that they're actually visible/useful const screenCell = CELL*zoom; if(screenCell < 3) return; ctx.save(); ctx.translate(-camX*zoom, -camY*zoom); ctx.scale(zoom, zoom); ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.lineWidth = 1/zoom; // finer lines every cell ctx.beginPath(); for(let x=startCX;x<=endCX+1;x++){ ctx.moveTo(x*CELL, startCY*CELL); ctx.lineTo(x*CELL, (endCY+1)*CELL); } for(let y=startCY;y<=endCY+1;y++){ ctx.moveTo(startCX*CELL, y*CELL); ctx.lineTo((endCX+1)*CELL, y*CELL); } ctx.stroke(); // bolder lines every 10 cells to help see scale ctx.strokeStyle = 'rgba(255,255,255,0.14)'; ctx.beginPath(); const bigStart = Math.floor(startCX/10)*10; for(let x=bigStart;x<=endCX+1;x+=10){ ctx.moveTo(x*CELL, startCY*CELL); ctx.lineTo(x*CELL, (endCY+1)*CELL); } const bigStartY = Math.floor(startCY/10)*10; for(let y=bigStartY;y<=endCY+1;y+=10){ ctx.moveTo(startCX*CELL, y*CELL); ctx.lineTo((endCX+1)*CELL, y*CELL); } ctx.stroke(); ctx.restore(); } function drawGrid(){ clampCam(); const viewWorldW = VIEW_W/zoom, viewWorldH = VIEW_H/zoom; const startCX = Math.max(0, Math.floor(camX/CELL)); const endCX = Math.min(COLS-1, Math.ceil((camX+viewWorldW)/CELL)); const startCY = Math.max(0, Math.floor(camY/CELL)); const endCY = Math.min(ROWS-1, Math.ceil((camY+viewWorldH)/CELL)); ctx.fillStyle = '#141420'; ctx.fillRect(0,0,VIEW_W,VIEW_H); drawBackgroundGrid(startCX,endCX,startCY,endCY); ctx.save(); ctx.translate(-camX*zoom, -camY*zoom); ctx.scale(zoom, zoom); for(let y=startCY;y<=endCY;y++){ for(let x=startCX;x<=endCX;x++){ const cell=grid[idx(x,y)]; if(cell.type===EMPTY) continue; const [r,g,b,a]=cellColor(cell); ctx.fillStyle = a<255 ? `rgba(${r},${g},${b},${a/255})` : `rgb(${r},${g},${b})`; ctx.fillRect(x*CELL, y*CELL, CELL, CELL); } } ctx.restore(); drawPlayer(); drawLasers(); drawBombs(); drawSniperBeams(); drawTurrets(); drawMinimap(startCX,endCX,startCY,endCY); drawScoreboard(); drawKO(); } function drawScoreboard(){ if(!player.active && !player2.active) return; ctx.save(); ctx.font = 'bold 16px Comic Sans MS'; ctx.textAlign = 'center'; ctx.fillStyle = '#ff6b6b'; ctx.fillText('P1: ' + player.wins, VIEW_W/2 - 60, 24); ctx.fillStyle = '#fff'; ctx.fillText('vs', VIEW_W/2, 24); ctx.fillStyle = '#5b8bff'; ctx.fillText('P2: ' + player2.wins, VIEW_W/2 + 60, 24); ctx.restore(); } function drawKO(){ if(koTimer <= 0) return; koTimer--; ctx.save(); ctx.globalAlpha = Math.min(1, koTimer/20); ctx.font = 'bold 30px Comic Sans MS'; ctx.textAlign = 'center'; ctx.fillStyle = '#ffcc00'; ctx.strokeStyle = '#000'; ctx.lineWidth = 4; ctx.strokeText(koText, VIEW_W/2, VIEW_H/2); ctx.fillText(koText, VIEW_W/2, VIEW_H/2); ctx.restore(); } function drawMinimap(){ mctx.fillStyle='#141420'; mctx.fillRect(0,0,minimap.width,minimap.height); const step=4; // sample every few cells for perf for(let y=0;y<ROWS;y+=step){ for(let x=0;x<COLS;x+=step){ const cell=grid[idx(x,y)]; if(cell.type===EMPTY) continue; const base=BASE_COLORS[cell.type]; if(!base) continue; mctx.fillStyle=`rgb(${base[0]},${base[1]},${base[2]})`; mctx.fillRect(x*CELL*MM_SCALE, y*CELL*MM_SCALE, step*CELL*MM_SCALE, step*CELL*MM_SCALE); } } // camera box mctx.strokeStyle='#fff'; mctx.lineWidth=2; const viewWorldW = VIEW_W/zoom, viewWorldH = VIEW_H/zoom; mctx.strokeRect(camX*MM_SCALE, camY*MM_SCALE, viewWorldW*MM_SCALE, viewWorldH*MM_SCALE); // player dot if(player.active){ mctx.fillStyle= player.dead ? '#ff0000' : '#ffcc00'; mctx.beginPath(); mctx.arc(player.x*MM_SCALE, player.y*MM_SCALE, 3, 0, Math.PI*2); mctx.fill(); } if(player2.active){ mctx.fillStyle= player2.dead ? '#ff0000' : '#44aaff'; mctx.beginPath(); mctx.arc(player2.x*MM_SCALE, player2.y*MM_SCALE, 3, 0, Math.PI*2); mctx.fill(); } } let moved; function swap(x1,y1,x2,y2){ const a=grid[idx(x1,y1)],b=grid[idx(x2,y2)]; const tt=a.type,tl=a.life,te=a.extra; a.type=b.type;a.life=b.life;a.extra=b.extra; b.type=tt;b.life=tl;b.extra=te; moved[idx(x1,y1)]=1;moved[idx(x2,y2)]=1; } function moveTo(x1,y1,x2,y2){ grid[idx(x2,y2)].type=grid[idx(x1,y1)].type; grid[idx(x2,y2)].life=grid[idx(x1,y1)].life; grid[idx(x2,y2)].extra=grid[idx(x1,y1)].extra; grid[idx(x1,y1)].type=EMPTY;grid[idx(x1,y1)].life=0;grid[idx(x1,y1)].extra=0; moved[idx(x2,y2)]=1; } function down(y){return y+gravityDir;} function up(y){return y-gravityDir;} function updateSand(x,y){ let ny=down(y);if(ny<0||ny>=ROWS)return; let bt=get(x,ny).type;if(canFallInto(bt)){swap(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS){let t=get(nx,ny).type;if(canFallInto(t)){swap(x,y,nx,ny);return;}}} } function updateSalt(x,y){ for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){let nx=x+dx,ny=y+dy;if(isType(nx,ny,WATER)&&Math.random()<0.01){grid[idx(x,y)].type=EMPTY;set(nx,ny,WATER,0);return;}} let ny=down(y);if(ny<0||ny>=ROWS)return; let bt=get(x,ny).type;if(canFallInto(bt)){swap(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS){let t=get(nx,ny).type;if(canFallInto(t)){swap(x,y,nx,ny);return;}}} } function updateAsh(x,y){ let ny=down(y);if(ny<0||ny>=ROWS)return; let bt=get(x,ny).type;if(canFallInto(bt)){swap(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS){let t=get(nx,ny).type;if(canFallInto(t)){swap(x,y,nx,ny);return;}}} } function updateWater(x,y){ let ny=down(y);if(ny<0||ny>=ROWS)return; if(isEmpty(x,ny)){moveTo(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,y)){moveTo(x,y,nx,y);return;}} for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,ny)){moveTo(x,y,nx,ny);return;}} } function updateMercury(x,y){ for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){if(isType(x+dx,y+dy,SPARK)&&Math.random()<0.3){set(x,y,SPARK,5);moved[idx(x,y)]=1;return;}} let ny=down(y);if(ny<0||ny>=ROWS)return; if(isEmpty(x,ny)||isType(x,ny,WATER)||isType(x,ny,OIL)){swap(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&(isEmpty(nx,y)||isType(nx,y,WATER))){moveTo(x,y,nx,y);return;}} } function updateOil(x,y){ let ny=down(y);if(ny<0||ny>=ROWS)return; if(isEmpty(x,ny)){moveTo(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,y)){moveTo(x,y,nx,y);return;}} for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,ny)){moveTo(x,y,nx,ny);return;}} } function updateFire(x,y){ grid[idx(x,y)].life--; if(grid[idx(x,y)].life<=0){grid[idx(x,y)].type=Math.random()<0.3?SMOKE:Math.random()<0.2?ASH:EMPTY;grid[idx(x,y)].life=grid[idx(x,y)].type===SMOKE?30+Math.floor(Math.random()*30):0;return;} const neighbors=[[0,-1],[1,0],[-1,0],[0,1],[1,-1],[-1,-1],[1,1],[-1,1]]; for(let[dx,dy]of neighbors){ let nx=x+dx,ny=y+dy;if(nx<0||nx>=COLS||ny<0||ny>=ROWS)continue; let t=get(nx,ny).type; if((t===WOOD||t===PLANT||t===FUSE)&&Math.random()<0.03){set(nx,ny,FIRE,50+Math.floor(Math.random()*60));moved[idx(nx,ny)]=1;} if(t===OIL&&Math.random()<0.07){set(nx,ny,FIRE,40+Math.floor(Math.random()*40));moved[idx(nx,ny)]=1;} if(t===GUNPOWDER&&Math.random()<0.3){set(nx,ny,FIRE,120+Math.floor(Math.random()*80));moved[idx(nx,ny)]=1;} if(t===GAS&&Math.random()<0.15){set(nx,ny,FIRE,60+Math.floor(Math.random()*40));moved[idx(nx,ny)]=1;} if(t===WATER&&Math.random()<0.1){set(nx,ny,STEAM,40+Math.floor(Math.random()*30));moved[idx(nx,ny)]=1;grid[idx(x,y)].type=SMOKE;grid[idx(x,y)].life=20;return;} if(t===ICE&&Math.random()<0.05){set(nx,ny,WATER,0);moved[idx(nx,ny)]=1;} if(t===SNOW&&Math.random()<0.1){set(nx,ny,WATER,0);moved[idx(nx,ny)]=1;} if(t===METAL&&Math.random()<0.02){set(nx,ny,SPARK,8);moved[idx(nx,ny)]=1;} } let uy=up(y);if(uy>=0&&uy<ROWS&&isEmpty(x,uy)&&Math.random()<0.3)moveTo(x,y,x,uy); } function updateSteam(x,y){ grid[idx(x,y)].life--; if(grid[idx(x,y)].life<=0){grid[idx(x,y)].type=Math.random()<0.3?WATER:EMPTY;grid[idx(x,y)].life=0;return;} let uy=up(y);if(uy>=0&&uy<ROWS&&isEmpty(x,uy)){moveTo(x,y,x,uy);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&uy>=0&&uy<ROWS&&isEmpty(nx,uy)){moveTo(x,y,nx,uy);return;}if(nx>=0&&nx<COLS&&isEmpty(nx,y)){moveTo(x,y,nx,y);return;}} } function updateSmoke(x,y){ grid[idx(x,y)].life--;if(grid[idx(x,y)].life<=0){grid[idx(x,y)].type=EMPTY;return;} let uy=up(y);if(uy>=0&&uy<ROWS&&isEmpty(x,uy)&&Math.random()<0.4){moveTo(x,y,x,uy);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&uy>=0&&uy<ROWS&&isEmpty(nx,uy)){moveTo(x,y,nx,uy);return;}if(nx>=0&&nx<COLS&&isEmpty(nx,y)){moveTo(x,y,nx,y);return;}} } function updateGas(x,y){ grid[idx(x,y)].life--;if(grid[idx(x,y)].life<=0){grid[idx(x,y)].type=EMPTY;return;} let uy=up(y);if(uy>=0&&uy<ROWS&&isEmpty(x,uy)){moveTo(x,y,x,uy);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,y)){moveTo(x,y,nx,y);return;}} } function updateSpark(x,y){ grid[idx(x,y)].life--;if(grid[idx(x,y)].life<=0){grid[idx(x,y)].type=EMPTY;return;} for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]){ let nx=x+dx,ny=y+dy;if(nx<0||nx>=COLS||ny<0||ny>=ROWS)continue; let t=get(nx,ny).type; if(t===METAL&&Math.random()<0.4){set(nx,ny,SPARK,grid[idx(x,y)].life-1);moved[idx(nx,ny)]=1;} if(t===MERCURY&&Math.random()<0.5){set(nx,ny,SPARK,grid[idx(x,y)].life);moved[idx(nx,ny)]=1;} if(t===WATER&&Math.random()<0.15){set(nx,ny,SPARK,3);moved[idx(nx,ny)]=1;} if((t===WOOD||t===PLANT||t===FUSE)&&Math.random()<0.1){set(nx,ny,FIRE,60+Math.floor(Math.random()*40));moved[idx(nx,ny)]=1;} if(t===GUNPOWDER&&Math.random()<0.5){set(nx,ny,FIRE,120);moved[idx(nx,ny)]=1;} if(t===GAS&&Math.random()<0.3){set(nx,ny,FIRE,40);moved[idx(nx,ny)]=1;} if(t===BOMB){explodeBomb(nx,ny);moved[idx(nx,ny)]=1;} } if(Math.random()<0.5){let dx=Math.floor(Math.random()*3)-1,dy=Math.floor(Math.random()*3)-1;let nx=x+dx,ny=y+dy;if(nx>=0&&nx<COLS&&ny>=0&&ny<ROWS&&isEmpty(nx,ny))moveTo(x,y,nx,ny);} } function updatePlant(x,y){ if(Math.random()>0.002)return; let nearWater=false; for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){if(isType(x+dx,y+dy,WATER)){nearWater=true;break;}} if(!nearWater&&Math.random()>0.01)return; const dirs=[[0,-1],[1,0],[-1,0],[0,1]]; for(let[dx,dy]of dirs){let nx=x+dx,ny=y+dy;if(nx>=0&&nx<COLS&&ny>=0&&ny<ROWS&&isEmpty(nx,ny)){set(nx,ny,PLANT,0);moved[idx(nx,ny)]=1;break;}} } function updateIce(x,y){ if(Math.random()>0.01)return; for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]){let t=get(x+dx,y+dy).type;if(t===FIRE||t===LAVA||t===SPARK){grid[idx(x,y)].type=WATER;return;}} if(Math.random()>0.005)return; for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){let nx=x+dx,ny=y+dy;if(isType(nx,ny,WATER)){set(nx,ny,ICE,0);break;}} } function updateSnow(x,y){ for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){let t=get(x+dx,y+dy).type;if((t===FIRE||t===LAVA)&&Math.random()<0.05){grid[idx(x,y)].type=WATER;return;}} let ny=down(y);if(ny<0||ny>=ROWS)return; let bt=get(x,ny).type;if(bt===EMPTY||bt===WATER){swap(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS){let t=get(nx,ny).type;if(t===EMPTY||t===WATER){swap(x,y,nx,ny);return;}}} } function updateStone(x,y){ let ny=down(y);if(ny<0||ny>=ROWS)return; let bt=get(x,ny).type;if(bt===EMPTY||bt===WATER||bt===OIL||bt===ACID){swap(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS){let t=get(nx,ny).type;if(t===EMPTY||t===WATER||t===OIL||t===ACID){swap(x,y,nx,ny);return;}}} } function updateLava(x,y){ for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]){ let nx=x+dx,ny=y+dy;let t=get(nx,ny).type; if((t===WOOD||t===PLANT||t===OIL||t===GUNPOWDER)&&Math.random()<0.05){set(nx,ny,FIRE,60+Math.floor(Math.random()*60));} if(t===WATER&&Math.random()<0.05){grid[idx(x,y)].type=STONE;return;} if(t===ICE&&Math.random()<0.1){set(nx,ny,WATER,0);} if(t===SNOW&&Math.random()<0.1){set(nx,ny,WATER,0);} } let ny=down(y);if(ny<0||ny>=ROWS)return; if(isEmpty(x,ny)){moveTo(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,y)){moveTo(x,y,nx,y);return;}} for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,ny)){moveTo(x,y,nx,ny);return;}} } function updateAcid(x,y){ const dissolvable=[SAND,WOOD,PLANT,STONE,WALL,GUNPOWDER,GLASS,ICE,SNOW,METAL,FUSE,ASH,SALT,GEL]; for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){ let nx=x+dx,ny=y+dy;let t=get(nx,ny).type; if(dissolvable.includes(t)&&Math.random()<0.02){set(nx,ny,EMPTY,0);if(Math.random()<0.3){grid[idx(x,y)].type=EMPTY;return;}} if(t===FIRE&&Math.random()<0.1){grid[idx(x,y)].type=STEAM;grid[idx(x,y)].life=40;return;} } let ny=down(y);if(ny<0||ny>=ROWS)return; if(isEmpty(x,ny)){moveTo(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,y)){moveTo(x,y,nx,y);return;}} for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,ny)){moveTo(x,y,nx,ny);return;}} } function updateGunpowder(x,y){ for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]){ let t=get(x+dx,y+dy).type; if(t===FIRE||t===LAVA||t===SPARK){ let r=8+Math.floor(Math.random()*5); for(let ey=-r;ey<=r;ey++){for(let ex=-r;ex<=r;ex++){if(ex*ex+ey*ey>r*r)continue;let nx=x+ex,ny=y+ey;if(nx<0||nx>=COLS||ny<0||ny>=ROWS)continue;let ct=get(nx,ny).type;if(ct!==WALL)set(nx,ny,FIRE,60+Math.floor(Math.random()*60));}} return; } } let ny=down(y);if(ny<0||ny>=ROWS)return; let bt=get(x,ny).type;if(canFallInto(bt)){swap(x,y,x,ny);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS){let t=get(nx,ny).type;if(canFallInto(t)){swap(x,y,nx,ny);return;}}} } function updateFuse(x,y){ for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){let t=get(x+dx,y+dy).type;if((t===FIRE||t===SPARK||t===LAVA)&&Math.random()<0.05){grid[idx(x,y)].type=FIRE;grid[idx(x,y)].life=80+Math.floor(Math.random()*60);return;}} } function updateCloud(x,y){ if(Math.random()<0.015){let d=Math.random()>0.5?1:-1;let nx=x+d;if(nx>=0&&nx<COLS&&isEmpty(nx,y))moveTo(x,y,nx,y);} let ny=down(y);if(ny>=0&&ny<ROWS&&isEmpty(x,ny)&&Math.random()<0.006)set(x,ny,WATER,0); let uy=up(y);if(y>25&&Math.random()<0.005&&uy>=0&&uy<ROWS&&isEmpty(x,uy))moveTo(x,y,x,uy); } function updateVirus(x,y){ if(Math.random()>0.05)return; const dirs=[[0,1],[1,0],[-1,0],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]; for(let[dx,dy]of dirs){let nx=x+dx,ny=y+dy;let t=get(nx,ny).type;if(t!==EMPTY&&t!==WALL&&t!==VIRUS&&t!==CLONE&&Math.random()<0.3){set(nx,ny,VIRUS,0);moved[idx(nx,ny)]=1;return;}} } function updateClone(x,y){ if(Math.random()>0.1)return; let stored=grid[idx(x,y)].extra; if(!stored){for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){let nx=x+dx,ny=y+dy;let t=get(nx,ny).type;if(t!==EMPTY&&t!==CLONE&&t!==WALL){grid[idx(x,y)].extra=t;stored=t;break;}}} if(!stored)return; for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){let nx=x+dx,ny=y+dy;if(isEmpty(nx,ny)&&Math.random()<0.3){set(nx,ny,stored,stored===FIRE?60:stored===STEAM?40:0);moved[idx(nx,ny)]=1;return;}} } const ANT_DX=[0,1,0,-1]; const ANT_DY=[-1,0,1,0]; function updateAnt(x,y){ if(Math.random()>0.3)return; let dir=ANT_DIR[idx(x,y)]; if(Math.random()<0.2)dir=(dir+(Math.random()>0.5?1:3))%4; let nx=x+ANT_DX[dir],ny=y+ANT_DY[dir]; if(nx<0||nx>=COLS||ny<0||ny>=ROWS||!isEmpty(nx,ny)){dir=(dir+1)%4;nx=x+ANT_DX[dir];ny=y+ANT_DY[dir];} if(nx>=0&&nx<COLS&&ny>=0&&ny<ROWS&&isEmpty(nx,ny)){ if(Math.random()<0.1)set(x,y,SAND,0);else grid[idx(x,y)].type=EMPTY; set(nx,ny,ANT,0);ANT_DIR[idx(nx,ny)]=dir;moved[idx(nx,ny)]=1; }else{ANT_DIR[idx(x,y)]=(dir+1)%4;} } function updateBubble(x,y){ grid[idx(x,y)].life--;if(grid[idx(x,y)].life<=0){grid[idx(x,y)].type=STEAM;grid[idx(x,y)].life=20;return;} for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){let t=get(x+dx,y+dy).type;if(t===FIRE||t===LAVA||t===SPARK||t===ACID){grid[idx(x,y)].type=STEAM;grid[idx(x,y)].life=30;return;}} let uy=up(y);if(uy>=0&&uy<ROWS&&isEmpty(x,uy)){moveTo(x,y,x,uy);return;} let d=Math.random()>0.5?1:-1; for(let di of[d,-d]){let nx=x+di;if(nx>=0&&nx<COLS&&isEmpty(nx,y)){moveTo(x,y,nx,y);return;}} } function updateGel(x,y){ for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){let nx=x+dx,ny=y+dy;if(isType(nx,ny,WATER)&&Math.random()<0.05){set(nx,ny,EMPTY,0);grid[idx(x,y)].life=Math.min(grid[idx(x,y)].life+1,10);return;}} let ny=down(y);if(ny>=0&&ny<ROWS&&isEmpty(x,ny)&&Math.random()<0.05)moveTo(x,y,x,ny); } function updateBomb(x,y){ grid[idx(x,y)].life--; if(grid[idx(x,y)].life<=0){explodeBomb(x,y);return;} for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){let t=get(x+dx,y+dy).type;if(t===FIRE||t===SPARK||t===LAVA){explodeBomb(x,y);return;}} let ny=down(y);if(ny>=0&&ny<ROWS&&isEmpty(x,ny)){moveTo(x,y,x,ny);return;} } function explodeBomb(x,y){ let r=12+Math.floor(Math.random()*6); for(let ey=-r;ey<=r;ey++){for(let ex=-r;ex<=r;ex++){if(ex*ex+ey*ey>r*r)continue;let nx=x+ex,ny=y+ey;if(nx<0||nx>=COLS||ny<0||ny>=ROWS)continue;let ct=get(nx,ny).type;if(ct!==WALL&&ct!==METAL&&ct!==BRICK&&ct!==CONCRETE&&ct!==OBSIDIAN){set(nx,ny,FIRE,80+Math.floor(Math.random()*60));}}} set(x,y,EMPTY,0); } function loop(){ if(!paused){ moved=new Uint8Array(COLS*ROWS); for(let y=ROWS-1;y>=0;y--){ let ltr=Math.random()>0.5; for(let xi=0;xi<COLS;xi++){ let x=ltr?xi:(COLS-1-xi); if(moved[idx(x,y)])continue; const t=grid[idx(x,y)].type; switch(t){ case SAND:updateSand(x,y);break;case SALT:updateSalt(x,y);break;case ASH:updateAsh(x,y);break; case WATER:updateWater(x,y);break;case MERCURY:updateMercury(x,y);break;case FIRE:updateFire(x,y);break; case STEAM:updateSteam(x,y);break;case SMOKE:updateSmoke(x,y);break;case GAS:updateGas(x,y);break; case PLANT:updatePlant(x,y);break;case OIL:updateOil(x,y);break;case ICE:updateIce(x,y);break; case STONE:updateStone(x,y);break;case LAVA:updateLava(x,y);break;case ACID:updateAcid(x,y);break; case SNOW:updateSnow(x,y);break;case GUNPOWDER:updateGunpowder(x,y);break;case CLOUD:updateCloud(x,y);break; case SPARK:updateSpark(x,y);break;case VIRUS:updateVirus(x,y);break;case CLONE:updateClone(x,y);break; case ANT:updateAnt(x,y);break;case BUBBLE:updateBubble(x,y);break;case GEL:updateGel(x,y);break; case BOMB:updateBomb(x,y);break;case FUSE:updateFuse(x,y);break; } } } updatePlayer(); } drawGrid(); requestAnimationFrame(loop); } function paintAt(px,py){ // convert screen coords -> world coords let wx = camX + px/zoom; let wy = camY + py/zoom; let cx=Math.floor(wx/CELL),cy=Math.floor(wy/CELL); let r=brushSize; for(let dy=-r;dy<=r;dy++){ for(let dx=-r;dx<=r;dx++){ if(dx*dx+dy*dy>r*r)continue; let nx=cx+dx,ny=cy+dy; if(nx<0||nx>=COLS||ny<0||ny>=ROWS)continue; if(currentElement==='erase'){ for(let ti=turrets.length-1; ti>=0; ti--){ const t=turrets[ti]; if(Math.floor(t.x/CELL)===nx && Math.floor(t.y/CELL)===ny) turrets.splice(ti,1); } grid[idx(nx,ny)].type=EMPTY;grid[idx(nx,ny)].life=0;grid[idx(nx,ny)].extra=0; } else { const map={sand:SAND,water:WATER,wall:WALL,fire:FIRE,wood:WOOD,plant:PLANT,steam:STEAM,oil:OIL,ice:ICE,stone:STONE,lava:LAVA,acid:ACID,snow:SNOW,gunpowder:GUNPOWDER,glass:GLASS,cloud:CLOUD,spark:SPARK,virus:VIRUS,clone:CLONE,ant:ANT,bubble:BUBBLE,gel:GEL,bomb:BOMB,metal:METAL,fuse:FUSE,gas:GAS,mercury:MERCURY,salt:SALT,ash:ASH,brick:BRICK,concrete:CONCRETE,obsidian:OBSIDIAN}; const lifeMap={fire:80+Math.floor(Math.random()*40),steam:60+Math.floor(Math.random()*30),spark:8+Math.floor(Math.random()*6),gas:80+Math.floor(Math.random()*40),bubble:60+Math.floor(Math.random()*40),bomb:200+Math.floor(Math.random()*100)}; let t=map[currentElement]; if(t!==undefined){ if(grid[idx(nx,ny)].type===EMPTY||currentElement==='fire'||currentElement==='lava'||currentElement==='acid'||currentElement==='spark'||currentElement==='virus'){ set(nx,ny,t,lifeMap[currentElement]||0); } } } } } } function setElement(e){ currentElement=e; document.querySelectorAll('.tool-btn').forEach(b=>b.classList.remove('active')); const btn=document.getElementById('btn-'+e); if(btn)btn.classList.add('active'); } // ============ MOUNTED GUNS ============ let turrets = []; function placeTurret(wx, wy, team){ turrets.push({ x: wx, y: wy, angle: team===1?0:Math.PI, team: team, cooldown:0, range: 220 }); } function updateTurrets(){ for(const t of turrets){ if(t.cooldown>0) t.cooldown--; let target = null, bestDist = t.range; for(const p of [player, player2]){ if(!p.active || p.dead) continue; if(t.team && ((t.team===1 && p===player) || (t.team===2 && p===player2))) continue; const dx = (p.x+p.w/2)-t.x, dy=(p.y+p.h/2)-t.y; const dist = Math.hypot(dx,dy); if(dist < bestDist){ bestDist=dist; target=p; } } if(target){ const dx = (target.x+target.w/2)-t.x, dy=(target.y+target.h/2)-t.y; t.angle = Math.atan2(dy,dx); if(t.cooldown<=0){ t.cooldown = 26; lasers.push({ x: t.x + Math.cos(t.angle)*6, y: t.y + Math.sin(t.angle)*6, vx: Math.cos(t.angle)*11, vy: Math.sin(t.angle)*11, life: 45, owner: t.team===1?2:1, dmg:1, color:'#ffaa00', splash:0, }); } } } } function drawTurrets(){ if(turrets.length===0) return; ctx.save(); ctx.translate(-camX*zoom, -camY*zoom); ctx.scale(zoom, zoom); for(const t of turrets){ ctx.fillStyle = t.team===1 ? '#883333' : (t.team===2 ? '#334488' : '#555'); ctx.beginPath(); ctx.arc(t.x, t.y, 5, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#222'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(t.x, t.y); ctx.lineTo(t.x+Math.cos(t.angle)*9, t.y+Math.sin(t.angle)*9); ctx.stroke(); } ctx.restore(); } // ============ TERRAIN GENERATION ============ function generateLandscape(){ initGrid(); turrets = []; const heights = new Array(COLS); let h = ROWS*0.55; let hillSeed1 = Math.random()*1000, hillSeed2 = Math.random()*1000; for(let x=0;x<COLS;x++){ h += (Math.sin(x*0.02+hillSeed1)*0.6 + Math.sin(x*0.05+hillSeed2)*0.4 + (Math.random()-0.5)*0.8); h = Math.max(ROWS*0.25, Math.min(ROWS*0.8, h)); heights[x] = Math.floor(h); } for(let x=0;x<COLS;x++){ const surface = heights[x]; for(let y=surface;y<ROWS;y++){ let type = STONE; if(y < surface+3) type = SAND; else if(y < surface+10) type = STONE; else type = (Math.random()<0.02) ? METAL : STONE; set(x,y,type,0); } if(Math.random()<0.04 && surface>4){ const treeH = 4+Math.floor(Math.random()*4); for(let ty=0;ty<treeH;ty++) set(x, surface-1-ty, WOOD, 0); for(let lx=-2;lx<=2;lx++) for(let ly=-2;ly<=0;ly++){ if(Math.abs(lx)+Math.abs(ly)<=2) set(x+lx, surface-1-treeH-ly, PLANT,0); } } } // scatter a few lakes for(let i=0;i<3;i++){ const lx = 30+Math.floor(Math.random()*(COLS-60)); const surface = heights[lx]; const w = 15+Math.floor(Math.random()*20); for(let x=lx-w;x<=lx+w;x++){ if(x<0||x>=COLS) continue; const s = heights[x]; for(let y=s;y<s+6;y++) set(x,y,WATER,0); } } // place a couple of turrets on hilltops for(let i=0;i<4;i++){ const tx = Math.floor(COLS*(0.15+0.7*i/3)); const ty = heights[tx]*CELL - 8; placeTurret(tx*CELL, ty, i%2===0?1:2); } if(player.active){ player.x=(COLS*0.2)*CELL; player.y=(heights[Math.floor(COLS*0.2)]-6)*CELL; player.vx=0;player.vy=0;player.dead=false;player.hp=player.maxHp;player.air=MAX_AIR; } if(player2.active){ player2.x=(COLS*0.8)*CELL; player2.y=(heights[Math.floor(COLS*0.8)]-6)*CELL; player2.vx=0;player2.vy=0;player2.dead=false;player2.hp=player2.maxHp;player2.air=MAX_AIR; } zoomReset(); } function buildFort(){ // clear a big rectangular arena area in the middle of the world const cx = Math.floor(COLS/2); const cy = Math.floor(ROWS/2); const halfW = 90, halfH = 55; const left = cx-halfW, right = cx+halfW, top = cy-halfH, bottom = cy+halfH; for(let y=top; y<=bottom; y++){ for(let x=left; x<=right; x++){ if(x<0||x>=COLS||y<0||y>=ROWS) continue; set(x,y,EMPTY,0); } } // outer stone wall ring for(let y=top; y<=bottom; y++){ for(let x=left; x<=right; x++){ if(x<0||x>=COLS||y<0||y>=ROWS) continue; const onEdge = (x===left||x===right||y===top||y===bottom); const nearEdge = (x<=left+2||x>=right-2||y<=top+2||y>=bottom-2); if(onEdge) set(x,y,STONE,0); else if(nearEdge && Math.random()<0.5) set(x,y,WALL,0); } } // floor across the bottom of the arena for(let x=left+1; x<=right-1; x++){ set(x, bottom-1, STONE, 0); set(x, bottom-2, STONE, 0); } // symmetric wood platforms stepping up on both sides const platformRows = [bottom-10, bottom-20, bottom-30, bottom-40]; platformRows.forEach((py, i) => { const w = 16; // left platform for(let x=left+8; x<=left+8+w; x++){ set(x, py, WOOD, 0); } // right platform (mirrored) for(let x=right-8-w; x<=right-8; x++){ set(x, py, WOOD, 0); } }); // center pit hazard - a little lava pool at the very bottom middle for danger const pitW = 10; for(let x=cx-pitW; x<=cx+pitW; x++){ set(x, bottom-3, LAVA, 0); } // center dividing wall with a gap in the middle (so it's fair but has cover) const midX = cx; for(let y=bottom-30; y<=bottom-4; y++){ if(y > bottom-14 && y < bottom-9) continue; // gap to pass through set(midX, y, GLASS, 0); } // place players on opposite platforms if(player.active){ player.x = (left+16) * CELL; player.y = (top+10) * CELL; player.vx=0; player.vy=0; player.dead=false; player.hp=player.maxHp; player.air=MAX_AIR; } if(player2.active){ player2.x = (right-26) * CELL; player2.y = (top+10) * CELL; player2.vx=0; player2.vy=0; player2.dead=false; player2.hp=player2.maxHp; player2.air=MAX_AIR; } // snap camera to show the arena camX = (left-5)*CELL - (VIEW_W/zoom - (right-left+10)*CELL)/2; camY = (top-5)*CELL; clampCam(); flashRoundStart(); } function flashRoundStart(){ koText = '⚔️ ROUND START! ⚔️'; koTimer = 70; } function buildMaze(){ // build a maze using a grid of cells, each cellSize wide, carved with recursive backtracker const cx = Math.floor(COLS/2); const cy = Math.floor(ROWS/2); const mazeColsN = 21, mazeRowsN = 13; // number of maze cells (odd for nice walls) const cellSize = 7; // world cells per maze cell const mazeW = mazeColsN*cellSize, mazeH = mazeRowsN*cellSize; const left = cx - Math.floor(mazeW/2), top = cy - Math.floor(mazeH/2); // clear area first for(let y=top-2; y<=top+mazeH+2; y++){ for(let x=left-2; x<=left+mazeW+2; x++){ if(x<0||x>=COLS||y<0||y>=ROWS) continue; set(x,y,EMPTY,0); } } // fill whole maze area with stone walls, then carve paths for(let y=0; y<mazeH; y++){ for(let x=0; x<mazeW; x++){ set(left+x, top+y, STONE, 0); } } // maze cell visited grid const visited = new Array(mazeColsN*mazeRowsN).fill(false); function mIdx(cx2,cy2){ return cy2*mazeColsN+cx2; } function carveCellOpen(cx2,cy2){ // clear the cellSize x cellSize block for this maze cell (leave 1-thick border logic via walls between cells) const bx = left + cx2*cellSize + 1; const by = top + cy2*cellSize + 1; for(let yy=0; yy<cellSize-1; yy++){ for(let xx=0; xx<cellSize-1; xx++){ set(bx+xx, by+yy, EMPTY, 0); } } } function carveBetween(cx2,cy2,dx,dy){ const bx = left + cx2*cellSize + 1; const by = top + cy2*cellSize + 1; const nx2 = cx2+dx, ny2 = cy2+dy; const nbx = left + nx2*cellSize + 1; const nby = top + ny2*cellSize + 1; if(dx===1){ for(let yy=0; yy<cellSize-1; yy++) for(let xx=0; xx<2; xx++) set(bx+(cellSize-2)+xx, by+yy, EMPTY, 0); } else if(dx===-1){ for(let yy=0; yy<cellSize-1; yy++) for(let xx=0; xx<2; xx++) set(nbx+(cellSize-2)+xx, nby+yy, EMPTY, 0); } else if(dy===1){ for(let xx=0; xx<cellSize-1; xx++) for(let yy=0; yy<2; yy++) set(bx+xx, by+(cellSize-2)+yy, EMPTY, 0); } else if(dy===-1){ for(let xx=0; xx<cellSize-1; xx++) for(let yy=0; yy<2; yy++) set(nbx+xx, nby+(cellSize-2)+yy, EMPTY, 0); } } // iterative recursive-backtracker maze generation const stack = []; let startCx=0, startCy=0; visited[mIdx(startCx,startCy)] = true; carveCellOpen(startCx,startCy); stack.push([startCx,startCy]); const dirs4 = [[1,0],[-1,0],[0,1],[0,-1]]; while(stack.length){ const [ccx,ccy] = stack[stack.length-1]; const options = []; for(const [dx,dy] of dirs4){ const nx2=ccx+dx, ny2=ccy+dy; if(nx2>=0&&nx2<mazeColsN&&ny2>=0&&ny2<mazeRowsN&&!visited[mIdx(nx2,ny2)]) options.push([dx,dy,nx2,ny2]); } if(options.length===0){ stack.pop(); continue; } const [dx,dy,nx2,ny2] = options[Math.floor(Math.random()*options.length)]; carveCellOpen(nx2,ny2); carveBetween(ccx,ccy,dx,dy); visited[mIdx(nx2,ny2)] = true; stack.push([nx2,ny2]); } // add a little lava trap somewhere in the middle for fun danger const trapCx = Math.floor(mazeColsN/2), trapCy = Math.floor(mazeRowsN/2); const tbx = left + trapCx*cellSize + 2, tby = top + trapCy*cellSize + 2; for(let yy=0; yy<3; yy++) for(let xx=0; xx<3; xx++) set(tbx+xx, tby+yy, LAVA, 0); // place players at opposite corners of the maze if(player.active){ player.x = (left + cellSize + 1) * CELL; player.y = (top + cellSize + 1) * CELL; player.vx=0; player.vy=0; player.dead=false; player.hp=player.maxHp; player.air=MAX_AIR; } if(player2.active){ player2.x = (left + mazeW - cellSize*2) * CELL; player2.y = (top + mazeH - cellSize*2) * CELL; player2.vx=0; player2.vy=0; player2.dead=false; player2.hp=player2.maxHp; player2.air=MAX_AIR; } // snap camera to show the whole maze if possible zoom = Math.max(MIN_ZOOM, Math.min(1, VIEW_W/((mazeW+10)*CELL))); camX = (left-5)*CELL; camY = (top-5)*CELL; clampCam(); flashRoundStart(); } function clearAll(){ initGrid(); if(player.active) respawnPlayer(player); if(player2.active) respawnPlayer(player2); lasers = []; } function togglePause(){paused=!paused;document.getElementById('pause-btn').textContent=paused?'▶️ Resume':'⏸️ Pause';} function toggleGravity(){ gravityMode=(gravityMode+1)%3; if(gravityMode===0){ gravityDir=1; document.getElementById('gravity-btn').textContent='⬇️ Normal G'; if(player.active){ player.vy=0; } } else if(gravityMode===1){ gravityDir=-1; document.getElementById('gravity-btn').textContent='⬆️ Flip G'; if(player.active){ player.vy=0; } } else { gravityDir=0; document.getElementById('gravity-btn').textContent='🌀 Zero G'; if(player.active){ player.vy=0; player.vx=0; } } } document.getElementById('size-slider').addEventListener('input',e=>{brushSize=parseInt(e.target.value);}); let placeTurretMode=false; canvas.addEventListener('mousedown',e=>{ if(e.button!==0) return; let r=canvas.getBoundingClientRect(); if(placeTurretMode){ const wx = camX+(e.clientX-r.left)/zoom, wy = camY+(e.clientY-r.top)/zoom; placeTurret(wx, wy, 0); return; } if(player.active && !player.dead) { shootLaser(player, 1); return; } mouseDown=true;mouseX=e.clientX-r.left;mouseY=e.clientY-r.top;paintAt(mouseX,mouseY); }); canvas.addEventListener('mousemove',e=>{if(!mouseDown)return;let r=canvas.getBoundingClientRect();mouseX=e.clientX-r.left;mouseY=e.clientY-r.top;paintAt(mouseX,mouseY);}); canvas.addEventListener('mouseup',()=>mouseDown=false); canvas.addEventListener('mouseleave',()=>mouseDown=false); canvas.addEventListener('touchstart',e=>{ if(e.touches.length>1) return; e.preventDefault(); if(player.active && !player.dead) { shootLaser(player, 1); return; } let r=canvas.getBoundingClientRect(),t=e.touches[0];mouseDown=true;mouseX=t.clientX-r.left;mouseY=t.clientY-r.top;paintAt(mouseX,mouseY); },{passive:false}); canvas.addEventListener('touchmove',e=>{ if(e.touches.length>1) return; e.preventDefault();if(!mouseDown)return;let r=canvas.getBoundingClientRect(),t=e.touches[0];mouseX=t.clientX-r.left;mouseY=t.clientY-r.top;paintAt(mouseX,mouseY); },{passive:false}); canvas.addEventListener('touchend',()=>mouseDown=false); initGrid(); loop(); </script> </body> </html>
PREVIEW
PICTURES0/50
No pictures yet. Add one, then ask your buddy to put it in your app!