can you bild sand dox
A sandbox sounds SO fun, Tree! 🏖️ I made you a sandbox where you can draw and play in the sand — use your mouse to dig and make patterns!
but. like sandspiel
Oh awesome idea, Tree! 🤩 I'm building you a falling sand game — like Sandspiel! Drop sand, water, fire, and more and watch them all interact. It might take a second to load, let's go!
but with more metearyles
Yes! More materials are coming right up! 🎉 I added Oil, Ice, Stone, Lava, Acid, Snow, Gunpowder, and Glass — so many cool things to mix together!
and lik the powder game
Yes!! Like The Powder Game! 🎮💥 I added tons of new stuff — Virus, Clone, Spark/Lightning, Fuse, Ant, Bubble, Gel, and more! Try mixing everything together and see what happens! 🔥⚡
can you make it have players and you cosomise them
Whoa, that app got too big for me to finish in one go! Try asking for something a little simpler, or build it up in smaller steps. 🙂
can you make it have players
Whoa, that app got too big for me to finish in one go! Try asking for something a little simpler, or build it up in smaller steps. 🙂
can you make it have players
Whoa, that app got too big for me to finish in one go! Try asking for something a little simpler, or build it up in smaller steps. 🙂
can you make it have players
Whoa, that app got too big for me to finish in one go! Try asking for something a little simpler, or build it up in smaller steps. 🙂
can you make it have players
Whoa, that app got too big for me to finish in one go! Try asking for something a little simpler, or build it up in smaller steps. 🙂
/clear
Sure thing, Tree! I'll add a little player you can move around in the powder game — use arrow keys to walk and jump! The player will interact with the sand and water around them! 🧑🎮
the 0g should afect the player
Great idea! Now when you flip gravity or go zero-G, the player floats too! 🚀 Try zero-G with fire and watch the chaos!
the 0g should afect the player
Great catch! Now the player is fully affected by gravity mode — flip it upside down or go zero-G and the player floats or falls the right way! 🚀🎮
the player should suficate
Oooh spooky! Now if the player gets buried in sand, snow, or other stuff they'll start to suffocate! 😱 Watch the player flash red when they can't breathe!
and water
Got it! Now water will suffocate the player too if they get stuck in it, but water is a little less deadly - they'll wiggle to try to swim to the surface! 💧😮
can you have a bigger woreld where you can zoom in to spesific parts
Ooh a bigger world sounds awesome, Tree! I made the whole world way bigger, and now you can zoom in and scroll around to see different parts of it! Use the zoom buttons and drag around with your mouse to explore! 🔍🗺️
No zoom. left/right keys side scroll when pause. So then when it is unpaused It is a side scrolling play space.
Got it, Tree! I made it so when the game is PAUSED, you can use Left/Right arrow keys to scroll and look around the whole big world. Then hit Resume and it becomes a side-scrolling play space where your player walks and explores! 🎮➡️
No zoom. left/right keys side scroll when pause. So then when it is unpaused It is a side scrolling play space.
Got it, Tree! I made the world simple and side-scrolling: no zoom stuff anymore. While PAUSED, use Left/Right arrow keys to scroll and look around the whole big world. Hit Resume and it becomes your side-scrolling play space - walk around with your player using the arrow keys and space to jump! 🎮➡️
<!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; } #controls-hint { color: #aaa; font-size: 0.78em; margin-top: 4px; text-align: center; max-width: 700px; } #mode-hint { color: #aef; font-size: 0.9em; margin-top: 4px; font-weight: bold; } #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-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 (Scroll Mode)</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> </div> <div id="mode-hint">▶️ PLAY MODE: Arrow keys move your player!</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">When PAUSED: ← → arrow keys scroll to look around the big world, and you can still draw with your mouse. When PLAYING (unpaused): ← → move your player, Space/↑ = jump!</div> <script> // ============ BIG WORLD SETUP (side-scroll, no zoom) ============ const COLS=480, ROWS=120, CELL=4; // big wide world, one screen tall 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 const camY = 0; // world height matches view height, no vertical scroll needed function clampCam(){ const worldW=COLS*CELL; camX = Math.max(0, Math.min(worldW-VIEW_W, camX)); } // ============ 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; const BREATHABLE = new Set([EMPTY, STEAM, SMOKE, GAS, BUBBLE]); 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], }; let grid=[]; let paused=true; 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', }; const keys = {}; window.addEventListener('keydown', e => { keys[e.code] = true; if(['ArrowLeft','ArrowRight','ArrowUp','Space'].includes(e.code)) e.preventDefault(); }); window.addEventListener('keyup', e => { keys[e.code] = false; }); function togglePlayer() { player.active = !player.active; if(player.active) { respawnPlayer(); 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 respawnPlayer() { player.x = (COLS/2) * CELL; player.y = (gravityDir === -1) ? (ROWS-5)*CELL : 10*CELL; player.vx = 0; player.vy = 0; player.dead = false; player.deadReason = ''; player.air = MAX_AIR; player.suffocating = false; player.inWater = false; } function killPlayer(reason) { if(player.dead) return; player.dead = true; player.deadReason = reason; setTimeout(() => { respawnPlayer(); }, 1800); } 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() { if(!player.active || player.dead) return; let hasAir = false; let cx1 = Math.floor(player.x / CELL); let cx2 = Math.floor((player.x + player.w - 1) / CELL); let cy1 = Math.floor(player.y / CELL); let cy2 = Math.floor((player.y + player.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++; } } let inWater = totalCount > 0 && waterCount / totalCount > 0.5; player.inWater = inWater && !hasAir; if(!hasAir) { player.suffocating = true; let drainRate = player.inWater ? 0.4 : 1; player.air = Math.max(0, player.air - drainRate); if(player.air <= 0) { killPlayer(player.inWater ? '🌊 Drowned!' : '😵 Can\'t breathe!'); } } else { player.suffocating = false; player.inWater = false; player.air = Math.min(MAX_AIR, player.air + 3); } updateAirBar(); } function updatePlayer() { if(!player.active || player.dead) return; if(keys['ArrowLeft']) player.vx -= 0.8; if(keys['ArrowRight']) player.vx += 0.8; const wantsUp = keys['ArrowUp'] || keys['Space']; if(player.inWater) { if(wantsUp) { player.vy -= 0.5 * (gravityDir || -1); } player.vy -= 0.05 * gravityDir; } if(gravityDir === 1) { if(wantsUp && player.onGround) { player.vy = -6; player.onGround = false; } } else if(gravityDir === -1) { if(wantsUp && player.onGround) { player.vy = 6; player.onGround = false; } } else { if(keys['ArrowUp']) player.vy -= 0.6; if(keys['Space']) player.vy += 0.6; } player.vx *= 0.75; if(gravityDir === 1) player.vy += 0.35; else if(gravityDir === -1) player.vy -= 0.35; else player.vy *= 0.92; player.vx = Math.max(-4, Math.min(4, player.vx)); player.vy = Math.max(-10, Math.min(10, player.vy)); player.x += player.vx; if(player.x < 0) { player.x = 0; player.vx = 0; } if(player.x + player.w > COLS * CELL) { player.x = COLS * CELL - player.w; player.vx = 0; } if(player.vx > 0) { let cxr = Math.floor((player.x + player.w - 1) / CELL); for(let gy = Math.floor(player.y/CELL); gy <= Math.floor((player.y+player.h-1)/CELL); gy++) { if(isSolid(cxr, gy)) { player.x = cxr * CELL - player.w; player.vx = 0; break; } } } else if(player.vx < 0) { let cxl = Math.floor(player.x / CELL); for(let gy = Math.floor(player.y/CELL); gy <= Math.floor((player.y+player.h-1)/CELL); gy++) { if(isSolid(cxl, gy)) { player.x = (cxl+1)*CELL; player.vx = 0; break; } } } player.y += player.vy; player.onGround = false; if(player.y < 0) { player.y = 0; player.vy = 0; if(gravityDir===-1) player.onGround=true; } if(player.y + player.h > ROWS * CELL) { player.y = ROWS * CELL - player.h; player.vy = 0; if(gravityDir===1) player.onGround=true; } if(player.vy > 0) { let gyb = Math.floor((player.y + player.h - 1) / CELL); for(let gx = Math.floor(player.x/CELL); gx <= Math.floor((player.x+player.w-1)/CELL); gx++) { if(isSolid(gx, gyb)) { player.y = gyb * CELL - player.h; player.vy = 0; if(gravityDir >= 0) player.onGround = true; break; } } } else if(player.vy < 0) { let gyt = Math.floor(player.y / CELL); for(let gx = Math.floor(player.x/CELL); gx <= Math.floor((player.x+player.w-1)/CELL); gx++) { if(isSolid(gx, gyt)) { player.y = (gyt + 1) * CELL; player.vy = 0; if(gravityDir === -1) player.onGround = true; break; } } } let pcx = Math.floor((player.x + player.w/2) / CELL); let pcy = Math.floor((player.y + player.h/2) / CELL); let pt = get(pcx, pcy).type; if(pt === FIRE || pt === LAVA || pt === SPARK) killPlayer('🔥 Ouch!'); else if(pt === ACID) killPlayer('🧪 Dissolved!'); checkSuffocation(); // camera follows player when playing camX = player.x + player.w/2 - VIEW_W/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; } function drawPlayer() { if(!player.active) return; ctx.save(); ctx.translate(-camX, -camY); 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.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'; 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); } 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(); if(player.dead) { ctx.fillStyle = '#ff4444'; ctx.font = 'bold 13px Comic Sans MS'; ctx.fillText(player.deadReason, player.x - 20, player.y - 5); } 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 - 5); } ctx.restore(); } 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 drawGrid(){ clampCam(); const startCX = Math.max(0, Math.floor(camX/CELL)); const endCX = Math.min(COLS-1, Math.ceil((camX+VIEW_W)/CELL)); ctx.fillStyle = '#141420'; ctx.fillRect(0,0,VIEW_W,VIEW_H); ctx.save(); ctx.translate(-camX, -camY); for(let y=0;y<ROWS;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(); drawMinimap(); } function drawMinimap(){ mctx.fillStyle='#141420'; mctx.fillRect(0,0,minimap.width,minimap.height); const step=4; 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); } } mctx.strokeStyle='#fff'; mctx.lineWidth=2; mctx.strokeRect(camX*MM_SCALE, camY*MM_SCALE, VIEW_W*MM_SCALE, VIEW_H*MM_SCALE); 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(); } } 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){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(); } else { // paused = scroll mode with arrow keys if(keys['ArrowLeft']) camX -= 6; if(keys['ArrowRight']) camX += 6; clampCam(); } drawGrid(); requestAnimationFrame(loop); } function paintAt(px,py){ let wx = camX + px; let wy = camY + py; 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'){ 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}; 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'); } function clearAll(){ initGrid(); if(player.active) respawnPlayer(); } function togglePause(){ paused=!paused; const btn=document.getElementById('pause-btn'); const hint=document.getElementById('mode-hint'); if(paused){ btn.textContent='▶️ Resume (Play Mode)'; hint.textContent='⏸️ SCROLL MODE: Use ← → arrow keys to look around, draw with your mouse!'; } else { btn.textContent='⏸️ Pause (Scroll Mode)'; hint.textContent='▶️ PLAY MODE: Arrow keys move your player!'; } } 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);}); canvas.addEventListener('mousedown',e=>{ if(e.button!==0) return; mouseDown=true;let r=canvas.getBoundingClientRect();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=>{ e.preventDefault();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=>{ 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>
No pictures yet. Add one, then ask your buddy to put it in your app!