CHAT
TREE
can you copy noita
BUDDY
Noita is a big, complex game, so let's start with a fun mini version: a sandbox where you draw different materials like sand, water, fire, and stone, and watch them fall and mix like real physics! Try it out, then tell me what to add next, like plants or explosions!
TREE
no noita
BUDDY
Ha, got it! We already made an "Element Sandbox" - it's kind of like a mini Noita where sand falls, water flows, and fire burns wood into smoke. Want me to add more Noita-style stuff to it, like explosions, acid, or oil? Or would you rather try building something totally different?
CODE
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Sand Physics</title> <style> body { margin: 0; background: #111; color: white; font-family: sans-serif; display: flex; flex-direction: column; align-items: center; touch-action: none; user-select: none; } h1 { margin: 8px 0 4px 0; font-size: 22px; } #toolbar { display: flex; gap: 6px; margin-bottom: 6px; flex-wrap: wrap; justify-content: center; max-width: 480px; } button { padding: 8px 12px; border-radius: 8px; border: 2px solid #444; font-weight: bold; cursor: pointer; font-size: 14px; } button.active { outline: 3px solid white; } #sand { background: #d2b48c; color: #333; } #water { background: #3aa0ff; color: #002; } #fire { background: #ff5500; color: #fff; } #stone { background: #888; color: #fff; } #wood { background: #8b5a2b; color: #fff; } #eraser { background: #222; color: #fff; } #clearBtn { background: #700; color: white; } canvas { background: black; border: 3px solid #555; border-radius: 8px; image-rendering: pixelated; touch-action: none; } #brushRow { margin: 6px 0; display: flex; align-items: center; gap: 8px; } </style> </head> <body> <h1>🔥 Element Sandbox 🔥</h1> <div id="toolbar"> <button id="sand" class="active">Sand</button> <button id="water">Water</button> <button id="fire">Fire</button> <button id="stone">Stone</button> <button id="wood">Wood</button> <button id="eraser">Eraser</button> <button id="clearBtn">Clear</button> </div> <div id="brushRow"> <label for="brush">Brush size:</label> <input type="range" id="brush" min="1" max="8" value="3"> </div> <canvas id="game" width="160" height="120"></canvas> <script> const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); const W = canvas.width, H = canvas.height; canvas.style.width = "480px"; canvas.style.height = "360px"; // element types const EMPTY = 0, SAND = 1, WATER = 2, STONE = 3, WOOD = 4, FIRE = 5, SMOKE = 6; const colors = {}; colors[EMPTY] = null; colors[SAND] = [237, 201, 175]; colors[WATER] = [58, 160, 255]; colors[STONE] = [136, 136, 136]; colors[WOOD] = [139, 90, 43]; colors[FIRE] = [255, 100, 20]; colors[SMOKE] = [90, 90, 90]; let grid = new Uint8Array(W * H); let fireLife = new Uint8Array(W * H); // how long fire/smoke has left function idx(x, y) { return y * W + x; } function inBounds(x, y) { return x >= 0 && x < W && y >= 0 && y < H; } let currentTool = SAND; let brushSize = 3; const toolButtons = { sand: SAND, water: WATER, fire: FIRE, stone: STONE, wood: WOOD, eraser: EMPTY }; for (const id in toolButtons) { document.getElementById(id).addEventListener('click', () => { currentTool = toolButtons[id]; document.querySelectorAll('#toolbar button').forEach(b => b.classList.remove('active')); if (id !== 'clearBtn') document.getElementById(id).classList.add('active'); }); } document.getElementById('clearBtn').addEventListener('click', () => { grid = new Uint8Array(W * H); fireLife = new Uint8Array(W * H); }); document.getElementById('brush').addEventListener('input', (e) => { brushSize = parseInt(e.target.value); }); // drawing with mouse / touch let drawing = false; function getCanvasPos(e) { const rect = canvas.getBoundingClientRect(); let clientX, clientY; if (e.touches && e.touches.length > 0) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const x = Math.floor((clientX - rect.left) / rect.width * W); const y = Math.floor((clientY - rect.top) / rect.height * H); return { x, y }; } function paintAt(x, y) { const r = brushSize; for (let dx = -r; dx <= r; dx++) { for (let dy = -r; dy <= r; dy++) { if (dx*dx + dy*dy <= r*r) { const nx = x + dx, ny = y + dy; if (inBounds(nx, ny)) { const i = idx(nx, ny); if (currentTool === EMPTY) { grid[i] = EMPTY; fireLife[i] = 0; } else { grid[i] = currentTool; if (currentTool === FIRE) fireLife[i] = 25 + Math.floor(Math.random()*15); } } } } } } function startDraw(e) { drawing = true; const p = getCanvasPos(e); paintAt(p.x, p.y); e.preventDefault(); } function moveDraw(e) { if (!drawing) return; const p = getCanvasPos(e); paintAt(p.x, p.y); e.preventDefault(); } function endDraw(e) { drawing = false; } canvas.addEventListener('mousedown', startDraw); canvas.addEventListener('mousemove', moveDraw); window.addEventListener('mouseup', endDraw); canvas.addEventListener('touchstart', startDraw, {passive:false}); canvas.addEventListener('touchmove', moveDraw, {passive:false}); canvas.addEventListener('touchend', endDraw); function swap(i, j) { const t = grid[i]; grid[i] = grid[j]; grid[j] = t; const t2 = fireLife[i]; fireLife[i] = fireLife[j]; fireLife[j] = t2; } function update() { // go bottom to top so things fall properly for (let y = H - 2; y >= 0; y--) { // randomize x direction each row for natural look const leftToRight = Math.random() < 0.5; for (let xi = 0; xi < W; xi++) { const x = leftToRight ? xi : (W - 1 - xi); const i = idx(x, y); const cell = grid[i]; if (cell === SAND) { const below = idx(x, y+1); if (grid[below] === EMPTY || grid[below] === WATER) { swap(i, below); } else { const dir = Math.random() < 0.5 ? -1 : 1; const bx1 = x + dir, bx2 = x - dir; if (inBounds(bx1, y+1) && (grid[idx(bx1,y+1)] === EMPTY || grid[idx(bx1,y+1)] === WATER)) { swap(i, idx(bx1, y+1)); } else if (inBounds(bx2, y+1) && (grid[idx(bx2,y+1)] === EMPTY || grid[idx(bx2,y+1)] === WATER)) { swap(i, idx(bx2, y+1)); } } } else if (cell === WATER) { const below = idx(x, y+1); if (grid[below] === EMPTY) { swap(i, below); } else { const dir = Math.random() < 0.5 ? -1 : 1; const bx1 = x + dir, bx2 = x - dir; if (inBounds(bx1, y+1) && grid[idx(bx1,y+1)] === EMPTY) { swap(i, idx(bx1, y+1)); } else if (inBounds(bx2, y+1) && grid[idx(bx2,y+1)] === EMPTY) { swap(i, idx(bx2, y+1)); } else if (inBounds(bx1, y) && grid[idx(bx1,y)] === EMPTY) { swap(i, idx(bx1, y)); } else if (inBounds(bx2, y) && grid[idx(bx2,y)] === EMPTY) { swap(i, idx(bx2, y)); } } } else if (cell === FIRE) { fireLife[i]--; // spread fire to wood neighbors const neighbors = [idx(x-1,y), idx(x+1,y), idx(x,y-1), idx(x,y+1)]; for (const n of neighbors) { if (n >= 0 && n < W*H && grid[n] === WOOD && Math.random() < 0.1) { grid[n] = FIRE; fireLife[n] = 25 + Math.floor(Math.random()*15); } if (n >= 0 && n < W*H && grid[n] === WATER) { grid[i] = SMOKE; fireLife[i] = 20; } } if (fireLife[i] <= 0) { grid[i] = Math.random() < 0.5 ? SMOKE : EMPTY; fireLife[i] = 15; } else { // fire rises a bit randomly if (Math.random() < 0.3 && inBounds(x, y-1) && grid[idx(x,y-1)] === EMPTY) { swap(i, idx(x, y-1)); } } } else if (cell === SMOKE) { fireLife[i]--; if (fireLife[i] <= 0) { grid[i] = EMPTY; } else if (inBounds(x, y-1) && grid[idx(x,y-1)] === EMPTY) { swap(i, idx(x, y-1)); } else { const dir = Math.random() < 0.5 ? -1 : 1; if (inBounds(x+dir, y-1) && grid[idx(x+dir,y-1)] === EMPTY) { swap(i, idx(x+dir, y-1)); } } } } } } function render() { const imgData = ctx.createImageData(W, H); for (let i = 0; i < W*H; i++) { const c = colors[grid[i]]; const o = i * 4; if (c) { imgData.data[o] = c[0]; imgData.data[o+1] = c[1]; imgData.data[o+2] = c[2]; imgData.data[o+3] = 255; } else { imgData.data[o] = 0; imgData.data[o+1] = 0; imgData.data[o+2] = 0; imgData.data[o+3] = 255; } } ctx.putImageData(imgData, 0, 0); } function loop() { update(); render(); requestAnimationFrame(loop); } loop(); </script> </body> </html>
PREVIEW
PICTURES0/50
No pictures yet. Add one, then ask your buddy to put it in your app!