can you make there be a background grid
Sure thing! I added a subtle background grid so you can see the cell layout across your big world — it scrolls and zooms right along with everything else. Give it a look! 🔲
can you make him have a gun that shoots lasers
Whoa, laser gun for your player?! That's awesome — I gave him a blaster that shoots glowing laser bolts in the direction he's facing! Click anywhere (left mouse button while player mode is on) or press Z to fire. The lasers zoom across the world and blast through sand, wood, plants, ice and other soft stuff, and they even ignite fire-prone things. Try it out!
can you add another player that someone else can control using wasd and can you make weapons for the charecters so they can fight
Ooh, PVP battle mode! I'm adding a second player controlled with WASD (F to shoot) so you and a friend can battle it out with laser guns — lasers now hurt the OTHER player too, so watch your health! Each player gets a health bar and respawns after being hit enough times. Try it with a friend on the same keyboard!
can you make it so that there is an auto fort builder that makes forts/arenes for the players to fight in
Ooh, epic idea! I added an 'Auto Fort' button that instantly builds a cool symmetrical battle arena — with stone walls, wood platforms, and a center pit — right around both players so you two can fight it out in a real battlefield! Try clicking it, then battle away. Next we could add more fort styles to pick from, like a maze or a sky arena!
a maze
Ooh a maze arena, awesome idea! I added a '🌀 Maze Fort' button that generates a real twisty maze made of stone walls for you two to hunt each other through — way trickier than the open fort! Try it out and see who can find who first!
can you make a variety of weapons
Ooh yes, more weapons means way more fun battles! Let me add a weapon selector so each player can pick from a few different guns with their own powers.
can you make a auto tracking rocket launcher and more weapons
Heck yes — homing rockets that chase the other player! I added a 🚀 Rocket weapon that curves through the air to track your opponent and explodes on impact, plus two more new guns: a 🧊 Freeze Ray that freezes enemies in place for a bit, and a 🌊 Wave Gun that shoots a spread of bouncy projectiles. Click the weapon buttons to cycle through ALL 6 weapons now (Laser, Rapid, Shotgun, Cannon, Rocket, Freeze, Wave)! Next idea: maybe add a weapon pickup system where new guns spawn randomly in the arena to grab?
can you make a sniper that has infinite range and kills instantly make shift shoot instead of click and bombs that you throw
Ooh a sniper AND throwable bombs, nice combo! I added a 🎯 Sniper that one-shots the other player instantly with a long tracer beam, made shooting happen with Shift (P1: Right-Shift or hold Z still works, P2: Left-Shift) alongside click, and gave you throwable bombs with the B key (P1) / G key (P2) that arc through the air and explode! Try picking Sniper mode and going for an instant kill, or lob a bomb over a wall!
bombs are just making the guy explode
Oops, good catch! The bombs were exploding way too easily right where they spawn (basically right on top of you). I fixed it so bombs won't blow up immediately at birth - they need a moment to arc through the air first, and they only explode on actual impact with a wall/player or after enough time passes. Try tossing one now, it should fly further before it goes boom!
can you add more elements that you can use for building forts
Ooh, more building materials, love it! I added Brick, Reinforced Concrete, and Obsidian — all super tough wall-like blocks great for forts (Obsidian even resists lava and fire way better than regular stone!). Check the toolbar for the three new buttons. Want me to add a special 'unbreakable' block too, so you can make a totally laser-proof fort core?
can you add mounted guns and make outa generated landscapes and make it for like the game fort
Ooh big upgrade time! Mounted turrets AND auto-generated landscapes coming right up — this is going to make it feel a lot more like a real battle game! Let me build this in a couple of steps so it works great.
can you make sniper not go through blocks and the turrets cant be erased
Good catches! I fixed the sniper so it now stops at the first solid block (wall, metal, brick, concrete, obsidian) instead of shooting through everything, and turrets are now protected from the eraser tool so they'll stick around during battles. Try sniping through a wall now - it should stop right at it!
no can you make its so the turrets can be erased and add more things that are in fighting games
Got it, sorry for the mix-up! I made turrets erasable again with the Erase tool, and I'm adding classic fighting-game features: a KO announcement, a round win scoreboard for each player, and a "Round Start!" flash when you build a fort or maze. Try starting a maze battle and watch the scoreboard track your wins!
can you add monsters that you have to fight
Ooh, monster battles! I added enemy monsters (👾 purple crawlers) that spawn and actively chase whichever player is closest, biting them for damage on contact — you can smack them down with any of your weapons! Click the new '👾 Spawn Monster' button to drop one into the world, or spawn a bunch for a horde fight! Next idea: maybe give monsters their own health bar and different monster types with different attacks?
can some of the weapons have a knockback and also can you make it so the red player takes damage form the turrets
Great ideas! I added knockback to the punchier weapons (Shotgun, Cannon, and Rocket now shove players back on hit), and fixed a bug so turrets with no team (like the ones you place with 🛡️ Place Turret) will now shoot AND damage BOTH players, including P1 (red) — no more free passes! Try building a fort with turrets and see how the knockback sends you flying!
red player is still not taking damage from the turrets
Found it! The turret's targeting check was set up so team-0 (neutral) turrets skipped shooting at anyone since your team check only exempted team 1 or 2 targets — but the real bug was the turret always fired lasers with owner set opposite of its team, and for team 0 that made owner=1, which never damages player 1 (since owner!==1 check skips them). I fixed it so neutral turrets (team 0) now hurt BOTH players. Try building a fort with placed turrets now — red should finally feel the burn!
can you make diffrent abiletes that players can select like super speed lava immunite ect
Ooh, special abilities, awesome idea! I added an Ability system - each player can pick from Speed Boost (move way faster), Lava Immunity (walk through fire/lava safe), High Jump (jump way higher), and Regen (slowly heals HP over time). Click the new ability buttons to cycle through choices for each player!
can you add aspects of the game super smash bros and also sometimes the bullets dont hit
Ooh, Smash Bros vibes are awesome! I'm adding two Smash-style features: knockback now scales up the more damage you've taken (just like Smash's damage %!), and instead of dying at 0 HP, getting knocked way off the bottom/sides of the world counts as a KO (like flying off the blast zone)! I also fixed the bug where bullets sometimes miss - the laser hit-check was only testing a single point each tiny step, so fast bullets could skip past a player between steps; now it checks the whole line segment each step so hits register reliably. Try a match now and rack up damage to see the knockback grow!
<!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="ability1-btn" onclick="cycleAbility(player,'ability1-btn')" style="background:#a03030;">⭐ None</button> <button class="bar-btn" id="ability2-btn" onclick="cycleAbility(player2,'ability2-btn')" style="background:#2255aa;">⭐ None</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> <button class="bar-btn" id="monster-btn" onclick="spawnMonster()" style="background:#5e1675;">👾 Spawn Monster</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; p.damagePct = 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, knockDir, knockForce) { if(p.dead) return; p.damagePct = (p.damagePct||0) + amount*8; p.hp -= amount; p.hitFlash = 10; if(knockForce){ const scale = 1 + (p.damagePct||0)/50; p.vx += (knockDir||0) * knockForce * scale; p.vy -= knockForce*0.4*scale; } 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, knockback:3 }, cannon: { name:'☄️ Cannon', speed:9, cooldown:30, dmg:2, life:55, color:'#aa44ff', spread:0, count:1, splash:9, knockback:6 }, rocket: { name:'🚀 Rocket', speed:6, cooldown:45, dmg:3, life:90, color:'#ff6600', spread:0, count:1, splash:11, homing:true, knockback:8 }, 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'; const ABILITIES = ['none','speed','lava','jump','regen']; const ABILITY_NAMES = { none:'⭐ None', speed:'💨 Speed', lava:'🌋 Lava Immune', jump:'🦘 High Jump', regen:'💚 Regen' }; player.ability = 'none'; player2.ability = 'none'; function cycleAbility(p, btnId){ let i = ABILITIES.indexOf(p.ability); i = (i+1) % ABILITIES.length; p.ability = ABILITIES[i]; document.getElementById(btnId).textContent = ABILITY_NAMES[p.ability]; } 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, knockback: wpn.knockback||0, }); } } 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; const steps = 4; for(let s=0; s<=steps; s++){ const px = L.x - L.vx*(s/steps); const py = L.y - L.vy*(s/steps); if(px >= p.x-1 && px <= p.x + p.w+1 && py >= p.y-1 && py <= p.y + p.h+1) return true; } return false; } 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, L.owner===0 ? '🛡️ Zapped by a turret!' : '💥 Blasted by P2!', Math.sign(L.vx)||1, L.knockback); 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, L.owner===0 ? '🛡️ Zapped by a turret!' : '💥 Blasted by P1!', Math.sign(L.vx)||-1, L.knockback); if(L.splash) splashDamage(L.x, L.y, L.splash); lasers.splice(i,1); continue; } // check monster hits let hitMonster = false; for(let mi=monsters.length-1; mi>=0; mi--){ const m = monsters[mi]; if(L.x>=m.x && L.x<=m.x+m.w && L.y>=m.y && L.y<=m.y+m.h){ m.hp -= L.dmg||1; m.hitFlash = 8; if(L.splash) splashDamage(L.x, L.y, L.splash); hitMonster = true; break; } } if(hitMonster){ 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; } const speedMult = p.ability === 'speed' ? 1.8 : 1; if(keys[leftKey]) { p.vx -= 0.8*speedMult; p.facing = -1; } if(keys[rightKey]) { p.vx += 0.8*speedMult; 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; } const jumpMult = p.ability === 'jump' ? 1.6 : 1; if(gravityDir === 1) { if(wantsUp && p.onGround) { p.vy = -6*jumpMult; p.onGround = false; } } else if(gravityDir === -1) { if(wantsUp && p.onGround) { p.vy = 6*jumpMult; p.onGround = false; } } else { if(wantsUp) p.vy -= 0.6; } p.vx *= 0.75; if(p.ability === 'speed') p.vx = Math.max(-7, Math.min(7, p.vx)); 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; const BLAST_MARGIN = 60; if(p.x < -BLAST_MARGIN) { killPlayer(p, '💫 KO! Blasted off the stage!'); return; } if(p.x + p.w > COLS*CELL + BLAST_MARGIN) { killPlayer(p, '💫 KO! Blasted off the stage!'); return; } 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 < -BLAST_MARGIN) { killPlayer(p, '💫 KO! Blasted off the stage!'); return; } if(p.y + p.h > ROWS*CELL + BLAST_MARGIN) { killPlayer(p, '💫 KO! Blasted off the stage!'); return; } 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(p.ability === 'lava') { if(pt === FIRE || pt === LAVA || pt === SPARK) set(pcx, pcy, EMPTY, 0); if(pt === ACID) killPlayer(p, '🧪 Dissolved!'); } else { if(pt === FIRE || pt === LAVA || pt === SPARK) killPlayer(p, '🔥 Ouch!'); else if(pt === ACID) killPlayer(p, '🧪 Dissolved!'); } if(p.ability === 'regen' && !p.dead && p.hp < p.maxHp) { p.regenTimer = (p.regenTimer||0) + 1; if(p.regenTimer >= 40) { p.hp = Math.min(p.maxHp, p.hp + 1); p.regenTimer = 0; } } 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(); updateMonsters(); 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(); drawMonsters(); 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 = []; let monsters = []; const MONSTER_MAX_HP = 4; function spawnMonster(){ let mx = camX + Math.random()*(VIEW_W/zoom); let my = camY + 10; monsters.push({ x: mx, y: my, vx:0, vy:0, w:12, h:12, hp: MONSTER_MAX_HP, maxHp: MONSTER_MAX_HP, onGround:false, hitFlash:0, biteCooldown:0, }); } function updateMonsters(){ for(let i=monsters.length-1;i>=0;i--){ const m = monsters[i]; if(m.hitFlash>0) m.hitFlash--; if(m.biteCooldown>0) m.biteCooldown--; // find nearest active player to chase let target=null, bestDist=Infinity; for(const p of [player, player2]){ if(!p.active||p.dead) continue; const dist = Math.hypot((p.x+p.w/2)-(m.x+m.w/2), (p.y+p.h/2)-(m.y+m.h/2)); if(dist<bestDist){ bestDist=dist; target=p; } } if(target){ const dx = (target.x+target.w/2)-(m.x+m.w/2); m.vx += Math.sign(dx)*0.3; } m.vx = Math.max(-2.5, Math.min(2.5, m.vx*0.85)); m.vy += 0.35*(gravityDir||1); m.vy = Math.max(-8, Math.min(8, m.vy)); m.x += m.vx; if(m.x<0){m.x=0;m.vx=0;} if(m.x+m.w>COLS*CELL){m.x=COLS*CELL-m.w;m.vx=0;} m.y += m.vy; m.onGround=false; if(m.y<0){m.y=0;m.vy=0;} if(m.y+m.h>ROWS*CELL){m.y=ROWS*CELL-m.h;m.vy=0;m.onGround=true;} if(m.vy>0){ let gyb=Math.floor((m.y+m.h-1)/CELL); for(let gx=Math.floor(m.x/CELL); gx<=Math.floor((m.x+m.w-1)/CELL); gx++){ if(isSolid(gx,gyb)){ m.y=gyb*CELL-m.h; m.vy=0; m.onGround=true; break; } } } // bite players on contact for(const p of [player, player2]){ if(!p.active||p.dead) continue; if(m.biteCooldown<=0 && m.x<p.x+p.w && m.x+m.w>p.x && m.y<p.y+p.h && m.y+m.h>p.y){ damagePlayer(p, 1, '👾 Bitten!'); m.biteCooldown = 40; } } // die from fire/lava/laser splash area let mcx=Math.floor((m.x+m.w/2)/CELL), mcy=Math.floor((m.y+m.h/2)/CELL); let mt=get(mcx,mcy).type; if(mt===FIRE||mt===LAVA||mt===SPARK) m.hp -= 0.5; if(m.hp<=0) monsters.splice(i,1); } } function drawMonsters(){ if(monsters.length===0) return; ctx.save(); ctx.translate(-camX*zoom, -camY*zoom); ctx.scale(zoom, zoom); for(const m of monsters){ ctx.fillStyle = m.hitFlash>0 ? '#fff' : '#7d3c98'; ctx.fillRect(m.x, m.y, m.w, m.h); ctx.fillStyle='#000'; ctx.fillRect(m.x+2, m.y+3, 2, 2); ctx.fillRect(m.x+m.w-4, m.y+3, 2, 2); // health bar ctx.fillStyle='#000a'; ctx.fillRect(m.x-1, m.y-6, m.w+2, 3); ctx.fillStyle='#4f4'; ctx.fillRect(m.x-1, m.y-6, (m.w+2)*Math.max(0,m.hp/m.maxHp), 3); } ctx.restore(); } 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===1 && p===player) continue; if(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; const shooterOwner = t.team===1 ? 2 : (t.team===2 ? 1 : 0); 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: shooterOwner, 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 = []; monsters = []; } 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>