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. 🙂
<!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; } canvas { display: block; cursor: crosshair; } #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; } </style> </head> <body> <h1>💥 Powder Game!</h1> <div id="toolbar"> <!-- Powders --> <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> <!-- Liquids --> <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> <!-- Gases --> <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> <!-- Solids --> <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> <!-- Special --> <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> </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()">🔄 Gravity</button> </div> <script> const COLS=180, ROWS=120, CELL=4; const canvas=document.getElementById('c'); canvas.width=COLS*CELL; canvas.height=ROWS*CELL; const ctx=canvas.getContext('2d'); // Element IDs 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 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 imageData; let paused=false; let currentElement='sand'; let brushSize=3; let mouseDown=false; let mouseX=0,mouseY=0; let gravityDir=1; // 1=down, -1=up, 0=zero-g let gravityMode=0; // 0=down,1=up,2=zero // Ant state const ANT_DIR=new Uint8Array(COLS*ROWS); // direction 0-3 function initGrid(){ grid=new Array(COLS*ROWS).fill(null).map(()=>({type:EMPTY,life:0,extra:0})); imageData=ctx.createImageData(canvas.width,canvas.height); imageData.data.fill(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 isLiquid(x,y){const t=get(x,y).type;return t===WATER||t===OIL||t===ACID||t===LAVA||t===MERCURY;} function isPowder(x,y){const t=get(x,y).type;return t===SAND||t===SNOW||t===GUNPOWDER||t===SALT||t===ASH;} function isGas(x,y){const t=get(x,y).type;return t===STEAM||t===SMOKE||t===SPARK||t===GAS||t===BUBBLE;} function drawGrid(){ const data=imageData.data; const bg=[20,20,30]; for(let y=0;y<ROWS;y++){ for(let x=0;x<COLS;x++){ const cell=grid[idx(x,y)]; let r=bg[0],g=bg[1],b=bg[2],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===VIRUS){r=base[0]+Math.floor(rng*40);g=base[1];b=base[2]+Math.floor(rng*40);} else if(cell.type===CLONE){r=20+Math.floor(rng*30);g=160+Math.floor(rng*60);b=110+Math.floor(rng*40);} else if(cell.type===SAND){r=base[0]+(x%2===y%2?10:-10);g=base[1]+(x%3===0?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;} } } for(let dy=0;dy<CELL;dy++){ for(let dx=0;dx<CELL;dx++){ const pi=4*((y*CELL+dy)*canvas.width+(x*CELL+dx)); data[pi]=r;data[pi+1]=g;data[pi+2]=b;data[pi+3]=a; } } } } ctx.putImageData(imageData,0,0); } 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; } // Gravity helpers function down(y){return y+gravityDir;} function up(y){return y-gravityDir;} function canFallInto(t){return t===EMPTY||t===WATER||t===OIL||t===ACID||t===GAS||t===STEAM||t===SMOKE||t===BUBBLE;} // --- SAND --- 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;} } } } // --- SALT (dissolves in water) --- function updateSalt(x,y){ // Check for water nearby -> dissolve 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; // salt water is just slightly different water 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;} } } } // --- ASH (falls like sand, comes from fire) --- 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;} } } } // --- WATER --- 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;} } } // --- MERCURY (heavy liquid, conducts spark) --- function updateMercury(x,y){ // Conducts spark 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;} } } // --- OIL --- 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;} } } // --- FIRE --- 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); } // --- STEAM --- 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;} } } // --- SMOKE --- 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;} } } // --- GAS (flammable, floats up) --- 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;} } } // --- SPARK / LIGHTNING --- function updateSpark(x,y){ grid[idx(x,y)].life--; if(grid[idx(x,y)].life<=0){grid[idx(x,y)].type=EMPTY;return;} // Spread to conductors and ignite things 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;} } // Move randomly 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); } } // --- PLANT --- 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;} } } // --- ICE --- 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;} } } // --- SNOW --- 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;} } } } // --- STONE (falls heavy) --- 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;} } } } // --- LAVA --- 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;} } } // --- ACID --- 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;} } } // --- GUNPOWDER --- 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;} } } } // --- FUSE (burns slowly, conducts fire) --- function updateFuse(x,y){ // burns when touching fire/spark/lava 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; } } } // --- METAL (conducts spark, doesn't burn) --- function updateMetal(x,y){ // Just static - spark passes through for(let[dx,dy]of[[0,1],[1,0],[-1,0],[0,-1]]){ if(isType(x+dx,y+dy,SPARK)){ // spark already handles metal conduction } } } // --- CLOUD --- 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); // Stay high let uy=up(y); if(y>25&&Math.random()<0.005&&uy>=0&&uy<ROWS&&isEmpty(x,uy)) moveTo(x,y,x,uy); } // --- VIRUS (converts neighbors to virus) --- 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; } } } // --- CLONE (copies whatever touches it) --- function updateClone(x,y){ if(Math.random()>0.1) return; let stored=grid[idx(x,y)].extra; // Pick up nearby material 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; // Spawn stored material 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; } } } // --- ANT (walks around, leaves sand) --- 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)]; // Turn randomly 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)){ // leave sand behind 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; } } // --- BUBBLE (floats up, pops when touching fire or wall) --- 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;} } } // --- GEL (stretchy, absorbs water, slows things) --- function updateGel(x,y){ // Absorb water 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; } } // Sticky - barely moves let ny=down(y); if(ny>=0&&ny<ROWS&&isEmpty(x,ny)&&Math.random()<0.05){moveTo(x,y,x,ny);} } // --- BOMB --- function updateBomb(x,y){ grid[idx(x,y)].life--; if(grid[idx(x,y)].life<=0){explodeBomb(x,y);return;} // Explode if touched by fire/spark/lava 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;} } // Falls 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; case METAL: updateMetal(x,y);break; } } } } drawGrid(); requestAnimationFrame(loop); } function paintAt(px,py){ let cx=Math.floor(px/CELL),cy=Math.floor(py/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();} 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';} else if(gravityMode===1){gravityDir=-1;document.getElementById('gravity-btn').textContent='⬆️ Flip G';} else{gravityDir=0;document.getElementById('gravity-btn').textContent='🌀 Zero G';} } document.getElementById('size-slider').addEventListener('input',e=>{brushSize=parseInt(e.target.value);}); canvas.addEventListener('mousedown',e=>{ 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!