can you build a game like townscaper
Oh wow, Townscaper is such a cool game — let's build your very own town-builder right in the browser! I've made a colorful grid where you can click to place and stack buildings, pick colors, and watch your little town grow. 🏘️🎨
can you make a 3d physics envirment were you can build like in mine craft
Whoa, a Minecraft-style 3D builder — that is SO cool! 🎮 Here it is! You can walk around with WASD, look around by clicking and dragging, and place or remove blocks. Let's build something awesome!
can you make a 3d physics envirment were you can build like in mine craft
Here we go — a real 3D Minecraft-style world built right in your browser! 🎮⛏️ You can walk around, look around with your mouse, place blocks, and break them. Let's build something epic!
its not working
Sorry about that! I've completely rebuilt it from scratch with simpler, more reliable code. Click "Start Building!" then click the screen to lock your mouse, use WASD to move, Space to jump, left click to break blocks, and right click to place them! 🎮⛏️
scratch the 3d can you make it like terraria with a large auoto generating map and lots of items
Awesome idea! Terraria is one of the best games ever! 🌍⚔️ Here's your very own 2D side-scroller with a huge auto-generated world, digging, building, enemies, gravity, and a hotbar full of blocks and items. Click to start and use WASD/Arrow keys to move and jump!
the acutal map is in the sky and i keep falling through also i cant mine or place blocks
Great feedback! I've completely rewritten the physics, world generation, and block interaction from scratch to fix all those bugs — the ground is now solid, mining and placing blocks work, and you spawn right on the surface! 🌍⛏️ Let's go exploring!
more iteams
Yes! Let's load up your world with tons more items — I added new blocks like bricks, marble, obsidian, ice, mushrooms, clay, ruby, emerald, amethyst, a crafting-table look block, bookshelf, and even a healing apple and a sword you can swing for extra damage! Your hotbar now has way more to choose from. Have fun building and mining! ⛏️💎
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Terra Builder</title> <style> * { margin:0; padding:0; box-sizing:border-box; } body { background:#000; overflow:hidden; font-family:'Courier New',monospace; } canvas { display:block; } #ui { position:fixed; bottom:0; left:0; width:100%; display:flex; flex-direction:column; align-items:center; pointer-events:none; } #hotbar { display:flex; gap:3px; margin-bottom:8px; background:rgba(0,0,0,0.7); padding:6px 8px; border-radius:12px; pointer-events:all; flex-wrap:wrap; max-width:96vw; justify-content:center; } .slot { width:46px; height:46px; border:3px solid #666; border-radius:6px; cursor:pointer; display:flex; flex-direction:column; align-items:center; justify-content:center; font-size:19px; color:#fff; background:rgba(30,30,30,0.85); position:relative; } .slot span { font-size:8px; color:#aaa; margin-top:0px; } .slot.sel { border-color:#fff; background:rgba(80,80,80,0.9); } #stats { position:fixed; top:8px; left:8px; color:#fff; font-size:13px; background:rgba(0,0,0,0.55); padding:8px 12px; border-radius:8px; line-height:1.8; } #health-bar { position:fixed; top:8px; right:8px; color:#fff; font-size:13px; background:rgba(0,0,0,0.55); padding:8px 12px; border-radius:8px; text-align:right; } #hp-inner { display:flex; gap:3px; margin-top:4px; justify-content:flex-end; } .heart { font-size:16px; } #overlay { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.88); display:flex; flex-direction:column; align-items:center; justify-content:center; color:#fff; z-index:20; } #overlay h1 { font-size:3rem; color:#f90; margin-bottom:8px; } #overlay p { font-size:1rem; color:#bdf; margin-bottom:24px; text-align:center; line-height:1.8; max-width:460px; } #startBtn { background:#e67e22; color:#fff; border:none; padding:14px 44px; font-size:1.2rem; border-radius:30px; cursor:pointer; } #startBtn:hover { background:#d35400; } #controls-hint { position:fixed; bottom:96px; left:8px; color:#ccc; font-size:11px; background:rgba(0,0,0,0.5); padding:6px 10px; border-radius:8px; line-height:1.8; } #crosshair { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); color:#fff; font-size:20px; pointer-events:none; text-shadow:1px 1px 2px #000; } #msg { position:fixed; top:70px; left:50%; transform:translateX(-50%); color:#fff; background:rgba(0,0,0,0.6); padding:6px 14px; border-radius:8px; font-size:13px; pointer-events:none; opacity:0; transition:opacity 0.3s; } </style> </head> <body> <canvas id="c"></canvas> <div id="ui"> <div id="hotbar"></div> </div> <div id="stats">🌍 Terra Builder<br><span id="posText">x:0 y:0</span><br><span id="biomeText">Biome: Forest</span></div> <div id="health-bar">❤️ Health<div id="hp-inner"></div></div> <div id="controls-hint"> A/D or ←/→ — Move<br> W/Space/↑ — Jump<br> Left Click — Break/Attack<br> Right Click — Place block<br> E — Eat Apple 1–0 / Scroll — Switch item </div> <div id="crosshair">+</div> <div id="msg"></div> <div id="overlay"> <h1>🌍 Terra Builder</h1> <p> Explore a huge auto-generated world with LOTS of items!<br> Mine blocks, place structures, fight enemies, find rare gems.<br> <b>A/D</b> to move | <b>W or Space</b> to jump<br> <b>Left Click</b> = Break/Attack | <b>Right Click</b> = Place<br> <b>1–0 or Scroll</b> = Switch item | <b>E</b> = Eat apple to heal </p> <button id="startBtn">▶ Start Exploring!</button> </div> <script> const canvas = document.getElementById('c'); const ctx = canvas.getContext('2d'); function resize(){ canvas.width = window.innerWidth; canvas.height = window.innerHeight; } window.addEventListener('resize', resize); resize(); // ── BLOCK IDS ────────────────────────────────────────────────── const AIR=0, GRASS=1, DIRT=2, STONE=3, WOOD=4, LEAVES=5, SAND=6, WATER=7, COAL=8, IRON=9, GOLD=10, DIAMOND=11, TORCH=12, PLANK=13, GLASS=14, SNOW=15, CACTUS=16, BEDROCK=17, GRAVEL=18, BRICK=19, MARBLE=20, OBSIDIAN=21, ICE=22, MUSHROOM=23, CLAY=24, RUBY=25, EMERALD=26, AMETHYST=27, BOOKSHELF=28, CRAFTBENCH=29, HAY=30, ROPE=31; const BLOCK_COLOR = { [GRASS]:'#3a9e3a', [DIRT]:'#8B5E3C', [STONE]:'#888888', [WOOD]:'#7a4e28', [LEAVES]:'#2d8c2d', [SAND]:'#e8d87a', [WATER]:'#2277cc', [COAL]:'#333333', [IRON]:'#c0947a', [GOLD]:'#f5c518', [DIAMOND]:'#00e5ff', [TORCH]:'#ff9900', [PLANK]:'#c8a044', [GLASS]:'#aaddff', [SNOW]:'#eef4ff', [CACTUS]:'#2e8b2e', [BEDROCK]:'#111111', [GRAVEL]:'#777777', [BRICK]:'#a83232', [MARBLE]:'#e8e6f0', [OBSIDIAN]:'#2b1a3d', [ICE]:'#bfe8ff', [MUSHROOM]:'#d94f4f', [CLAY]:'#b06a5a', [RUBY]:'#ff2255', [EMERALD]:'#14d97a', [AMETHYST]:'#9b59d0', [BOOKSHELF]:'#6b3e1f', [CRAFTBENCH]:'#a06636', [HAY]:'#e6c94a', [ROPE]:'#c9b27a' }; const BLOCK_TOP = { [GRASS]:'#56c93a', [MUSHROOM]:'#f2a0a0' }; const BLOCK_SOLID = { [GRASS]:true,[DIRT]:true,[STONE]:true,[WOOD]:true,[LEAVES]:true, [SAND]:true,[COAL]:true,[IRON]:true,[GOLD]:true,[DIAMOND]:true, [TORCH]:false,[PLANK]:true,[GLASS]:true,[SNOW]:true, [CACTUS]:true,[BEDROCK]:true,[GRAVEL]:true,[WATER]:false, [BRICK]:true,[MARBLE]:true,[OBSIDIAN]:true,[ICE]:true, [MUSHROOM]:true,[CLAY]:true,[RUBY]:true,[EMERALD]:true, [AMETHYST]:true,[BOOKSHELF]:true,[CRAFTBENCH]:true,[HAY]:true, [ROPE]:false }; const BLOCK_ALPHA = { [WATER]:0.6, [GLASS]:0.45, [LEAVES]:0.85, [ICE]:0.75 }; const BLOCK_NAME = { [GRASS]:'Grass',[DIRT]:'Dirt',[STONE]:'Stone',[WOOD]:'Wood', [LEAVES]:'Leaves',[SAND]:'Sand',[WATER]:'Water',[COAL]:'Coal', [IRON]:'Iron',[GOLD]:'Gold',[DIAMOND]:'Diamond',[TORCH]:'Torch', [PLANK]:'Plank',[GLASS]:'Glass',[SNOW]:'Snow',[CACTUS]:'Cactus', [BEDROCK]:'Bedrock',[GRAVEL]:'Gravel',[BRICK]:'Brick', [MARBLE]:'Marble',[OBSIDIAN]:'Obsidian',[ICE]:'Ice', [MUSHROOM]:'Mushroom',[CLAY]:'Clay',[RUBY]:'Ruby', [EMERALD]:'Emerald',[AMETHYST]:'Amethyst',[BOOKSHELF]:'Books', [CRAFTBENCH]:'Bench',[HAY]:'Hay',[ROPE]:'Rope' }; const BLOCK_EMOJI = { [GRASS]:'🟩',[DIRT]:'🟫',[STONE]:'🪨',[WOOD]:'🪵',[LEAVES]:'🌿', [SAND]:'🏜️',[COAL]:'⚫',[IRON]:'🟤',[GOLD]:'🟡',[DIAMOND]:'💎', [TORCH]:'🔦',[PLANK]:'🟨',[GLASS]:'🔷',[SNOW]:'❄️',[CACTUS]:'🌵', [GRAVEL]:'🩶',[BRICK]:'🧱',[MARBLE]:'⬜',[OBSIDIAN]:'🟪', [ICE]:'🧊',[MUSHROOM]:'🍄',[CLAY]:'🏺',[RUBY]:'❤️', [EMERALD]:'💚',[AMETHYST]:'🔮',[BOOKSHELF]:'📚',[CRAFTBENCH]:'🛠️', [HAY]:'🌾',[ROPE]:'🪢' }; // Extra "item" tools (not placed as world blocks the same way) const APPLE_ITEM = 100, SWORD_ITEM = 101; const ITEM_NAME = { [APPLE_ITEM]:'Apple', [SWORD_ITEM]:'Sword' }; const ITEM_EMOJI = { [APPLE_ITEM]:'🍎', [SWORD_ITEM]:'🗡️' }; // ── WORLD ────────────────────────────────────────────────────── const BS = 24; // block size pixels const WORLD_W = 800; const WORLD_H = 200; const world = new Uint8Array(WORLD_W * WORLD_H); function wi(x,y){ return y * WORLD_W + x; } function getB(x,y){ if(x<0||x>=WORLD_W||y<0||y>=WORLD_H) return BEDROCK; return world[wi(x,y)]; } function setB(x,y,v){ if(x<0||x>=WORLD_W||y<0||y>=WORLD_H) return; world[wi(x,y)] = v; } function isSolid(x,y){ const b = getB(x,y); return BLOCK_SOLID[b] === true; } // ── NOISE ────────────────────────────────────────────────────── function hashN(n){ return (Math.sin(n * 127.1 + 311.7) * 43758.5453) % 1; } function absHash(n){ return Math.abs(hashN(n)); } function lerp(a,b,t){ return a + (b-a)*t; } function smooth(t){ return t*t*(3-2*t); } function noise1d(x){ const i = Math.floor(x); return lerp(absHash(i), absHash(i+1), smooth(x-i)); } function octaveNoise(x, octs, freq, amp){ let v=0, maxV=0, f=freq, a=amp; for(let o=0;o<octs;o++){ v += noise1d(x*f)*a; maxV += a; f *= 2; a *= 0.5; } return v/maxV; } // ── SURFACE HEIGHTS & BIOMES ─────────────────────────────────── const surfaceY = new Int32Array(WORLD_W); // block y where surface IS (ground block) const biomeArr = new Uint8Array(WORLD_W); // 0=forest,1=desert,2=snow,3=plains function generateWorld(){ // Biomes for(let x=0;x<WORLD_W;x++){ const b = octaveNoise(x*0.004+10, 3, 1, 1); if(b < 0.32) biomeArr[x] = 1; // desert else if(b < 0.5) biomeArr[x] = 3; // plains else if(b < 0.72) biomeArr[x] = 0; // forest else biomeArr[x] = 2; // snow } // Surface heights — surface block sits at surfaceY[x] const baseY = Math.floor(WORLD_H * 0.45); // ~90 blocks from top for(let x=0;x<WORLD_W;x++){ const h = octaveNoise(x*0.006, 5, 1, 1); surfaceY[x] = baseY + Math.round((h - 0.5) * 40); surfaceY[x] = Math.max(10, Math.min(WORLD_H - 30, surfaceY[x])); } // Fill blocks for(let x=0;x<WORLD_W;x++){ const sy = surfaceY[x]; const bio = biomeArr[x]; for(let y=0; y<WORLD_H; y++){ if(y === WORLD_H-1 || y === WORLD_H-2){ setB(x, y, BEDROCK); } else if(y > sy){ if(y > sy+60) setB(x, y, OBSIDIAN>0 && Math.random()<0.02 ? OBSIDIAN : STONE); else if(y > sy+3) setB(x, y, STONE); else setB(x, y, DIRT); } else if(y === sy){ if(bio===1) setB(x,y,SAND); else if(bio===2) setB(x,y,SNOW); else setB(x,y,GRASS); } else if(y === sy-1 || y === sy-2 || y === sy-3){ if(bio===1) setB(x,y,SAND); else setB(x,y,DIRT); } else { setB(x, y, AIR); } } } // Ores & special blocks function placeVein(type, count, minDepth, maxDepth, size){ for(let i=0;i<count;i++){ const ox = Math.floor(Math.random()*WORLD_W); const refY = surfaceY[ox]; const depth = minDepth + Math.floor(Math.random()*(maxDepth-minDepth)); const oy = refY + depth; for(let j=0;j<size;j++){ const bx = ox + Math.floor(Math.random()*5-2); const by = oy + Math.floor(Math.random()*3-1); if(getB(bx,by)===STONE) setB(bx,by,type); } } } placeVein(COAL, 250, 5, 60, 6); placeVein(IRON, 160, 8, 70, 5); placeVein(GOLD, 90, 15, 80, 4); placeVein(DIAMOND, 50, 25, 90, 3); placeVein(GRAVEL, 120, 3, 70, 7); placeVein(CLAY, 100, 2, 40, 5); placeVein(RUBY, 40, 30, 95, 3); placeVein(EMERALD, 40, 30, 95, 3); placeVein(AMETHYST, 35, 35, 95, 3); // Trees / cacti / mushrooms for(let x=3;x<WORLD_W-3;x++){ const sy = surfaceY[x]; const bio = biomeArr[x]; const r = Math.random(); if(bio===1 && r<0.03){ const h = 2+Math.floor(Math.random()*3); for(let t=1;t<=h;t++) setB(x, sy-t, CACTUS); } else if((bio===0||bio===3) && r<0.05){ const h = 4+Math.floor(Math.random()*4); for(let t=1;t<=h;t++) setB(x, sy-t, WOOD); for(let lx=-2;lx<=2;lx++){ for(let ly=1;ly<=3;ly++){ if(Math.abs(lx)+ly<=4) setB(x+lx, sy-h-ly+1, LEAVES); } } } else if(bio===0 && r<0.07){ setB(x, sy-1, MUSHROOM); } else if(bio===2 && r<0.04){ for(let t=1;t<=3;t++) setB(x, sy-t, WOOD); for(let lx=-1;lx<=1;lx++){ setB(x+lx, sy-3, LEAVES); setB(x+lx, sy-4, LEAVES); } } else if(bio===3 && r<0.02){ setB(x, sy-1, HAY); } } // Caves for(let c=0;c<300;c++){ let cx = Math.random()*WORLD_W; const refY = surfaceY[Math.floor(cx)]; let cy = refY + 5 + Math.random()*70; const len = 20 + Math.random()*70; let ang = Math.random()*Math.PI*2; for(let s=0;s<len;s++){ ang += (Math.random()-0.5)*0.7; cx += Math.cos(ang)*1.5; cy += Math.sin(ang)*0.9; const r = 1+Math.random()*2; for(let dx=-3;dx<=3;dx++){ for(let dy=-2;dy<=2;dy++){ if(dx*dx+dy*dy <= r*r){ const bx=Math.floor(cx+dx), by=Math.floor(cy+dy); if(getB(bx,by)!==BEDROCK) setB(bx,by,AIR); } } } } } // Random ruins (marble/brick structures) scattered on surface for(let i=0;i<25;i++){ const rx = 10+Math.floor(Math.random()*(WORLD_W-20)); const sy = surfaceY[rx]; const mats = [MARBLE, BRICK]; const mat = mats[Math.floor(Math.random()*mats.length)]; const w = 3+Math.floor(Math.random()*4); const h = 2+Math.floor(Math.random()*3); for(let dx=0;dx<w;dx++){ for(let dy=0;dy<h;dy++){ if(dx===0||dx===w-1||dy===h-1){ setB(rx+dx, sy-1-dy, mat); } } } if(Math.random()<0.5) setB(rx+Math.floor(w/2), sy-2, CRAFTBENCH); if(Math.random()<0.4) setB(rx+1, sy-2, BOOKSHELF); } // Water pools in depressions for(let x=0;x<WORLD_W;x++){ const sy = surfaceY[x]; const left = x>0 ? surfaceY[x-1] : sy; const right = x<WORLD_W-1 ? surfaceY[x+1] : sy; if(sy > left && sy > right){ setB(x, sy-1, WATER); } } // Ice patches in snow biome pools for(let x=0;x<WORLD_W;x++){ if(biomeArr[x]===2 && getB(x, surfaceY[x]-1)===WATER){ setB(x, surfaceY[x]-1, ICE); } } } generateWorld(); // ── HOTBAR ───────────────────────────────────────────────────── const HOTBAR = [ GRASS, DIRT, STONE, WOOD, PLANK, TORCH, GLASS, SAND, CACTUS, BRICK, MARBLE, OBSIDIAN, ICE, MUSHROOM, CLAY, RUBY, EMERALD, AMETHYST, BOOKSHELF, CRAFTBENCH, HAY, ROPE, SWORD_ITEM, APPLE_ITEM ]; let selSlot = 0; const hotbarEl = document.getElementById('hotbar'); function slotName(b){ return BLOCK_NAME[b] || ITEM_NAME[b] || '?'; } function slotEmoji(b){ return BLOCK_EMOJI[b] || ITEM_EMOJI[b] || '⬜'; } function buildHotbar(){ hotbarEl.innerHTML = ''; HOTBAR.forEach((b,i)=>{ const d = document.createElement('div'); d.className = 'slot' + (i===selSlot?' sel':''); d.innerHTML = `${slotEmoji(b)}<span>${slotName(b)}</span>`; d.addEventListener('click',()=>{ selSlot=i; buildHotbar(); }); hotbarEl.appendChild(d); }); } buildHotbar(); window.addEventListener('wheel',e=>{ selSlot = ((selSlot+(e.deltaY>0?1:-1))+HOTBAR.length)%HOTBAR.length; buildHotbar(); }); window.addEventListener('keydown',e=>{ if(e.code==='Digit0'){ selSlot=9; buildHotbar(); return; } const n=parseInt(e.key); if(n>=1&&n<=9){ selSlot=n-1; buildHotbar(); } }); // ── PLAYER ───────────────────────────────────────────────────── const PW = BS * 0.75; const PH = BS * 1.9; const GRAVITY = 0.6; const JUMP_VEL = -12; const MOVE_SPD = 3.5; const spawnX = Math.floor(WORLD_W / 2); const spawnSY = surfaceY[spawnX]; const player = { x: spawnX * BS + BS/2 - PW/2, y: (spawnSY - 3) * BS, vx: 0, vy: 0, onGround: false, hp: 20, maxHp: 20, facing: 1, invincible: 0, apples: 3, }; function showMsg(text){ const m = document.getElementById('msg'); m.textContent = text; m.style.opacity = 1; clearTimeout(showMsg._t); showMsg._t = setTimeout(()=>{ m.style.opacity=0; }, 1400); } function updateHUD(){ const hp = document.getElementById('hp-inner'); hp.innerHTML = ''; for(let i=0;i<player.maxHp/2;i++){ const s = document.createElement('span'); s.className = 'heart'; const full = player.hp > i*2+1; const half = player.hp === i*2+1; s.textContent = full?'❤️':half?'🧡':'🖤'; hp.appendChild(s); } } updateHUD(); // ── PHYSICS ───────────────────────────────────────────────────── function movePlayer(dt){ player.vy += GRAVITY; if(player.vy > 20) player.vy = 20; player.x += player.vx; player.x = Math.max(0, Math.min(player.x, (WORLD_W-1)*BS - PW)); resolveX(); player.y += player.vy; player.onGround = false; resolveY(); } function resolveX(){ const lbx = Math.floor(player.x / BS); const rbx = Math.floor((player.x + PW - 1) / BS); const tby = Math.floor(player.y / BS); const bby = Math.floor((player.y + PH - 1) / BS); if(player.vx > 0){ if(isSolid(rbx, tby) || isSolid(rbx, bby)){ player.x = rbx * BS - PW; player.vx = 0; } const mby = Math.floor((player.y + PH/2) / BS); if(isSolid(rbx, mby)){ player.x = rbx * BS - PW; player.vx = 0; } } else if(player.vx < 0){ if(isSolid(lbx, tby) || isSolid(lbx, bby)){ player.x = (lbx+1) * BS; player.vx = 0; } const mby = Math.floor((player.y + PH/2) / BS); if(isSolid(lbx, mby)){ player.x = (lbx+1) * BS; player.vx = 0; } } } function resolveY(){ const lbx = Math.floor(player.x / BS); const rbx = Math.floor((player.x + PW - 1) / BS); const tby = Math.floor(player.y / BS); const bby = Math.floor((player.y + PH - 1) / BS); if(player.vy > 0){ if(isSolid(lbx, bby) || isSolid(rbx, bby)){ player.y = bby * BS - PH; player.vy = 0; player.onGround = true; } } else if(player.vy < 0){ if(isSolid(lbx, tby) || isSolid(rbx, tby)){ player.y = (tby+1) * BS; player.vy = 0; } } } // ── ENEMIES ───────────────────────────────────────────────────── const enemies = []; for(let i=0;i<40;i++){ const ex = 40 + Math.floor(Math.random()*(WORLD_W-80)); const esy = surfaceY[ex]; enemies.push({ x: ex*BS, y: (esy-2)*BS, vx: (Math.random()<0.5?-1:1)*1.4, vy: 0, onGround: false, hp:6, maxHp:6, type: Math.random()<0.5?'zombie':'slime', invTimer:0, }); } function moveEnemy(e){ e.vy += GRAVITY; if(e.vy>18) e.vy=18; e.x += e.vx; e.x = Math.max(0, Math.min(e.x, (WORLD_W-1)*BS)); const elx = Math.floor(e.x/BS); const erx = Math.floor((e.x+BS*0.85)/BS); const ety = Math.floor(e.y/BS); const eby = Math.floor((e.y+BS*1.8-1)/BS); if(e.vx>0){ if(isSolid(erx,ety)||isSolid(erx,eby)){ e.x=erx*BS-BS*0.85; e.vx*=-1; } } else if(e.vx<0){ if(isSolid(elx,ety)||isSolid(elx,eby)){ e.x=(elx+1)*BS; e.vx*=-1; } } e.y += e.vy; e.onGround=false; const elx2=Math.floor(e.x/BS); const erx2=Math.floor((e.x+BS*0.85)/BS); const eby2=Math.floor((e.y+BS*1.8-1)/BS); const ety2=Math.floor(e.y/BS); if(e.vy>0){ if(isSolid(elx2,eby2)||isSolid(erx2,eby2)){ e.y=eby2*BS-BS*1.8; e.vy=0; e.onGround=true; } } else if(e.vy<0){ if(isSolid(elx2,ety2)||isSolid(erx2,ety2)){ e.y=(ety2+1)*BS; e.vy=0; } } } // ── PARTICLES ─────────────────────────────────────────────────── const particles = []; function spawnParticles(px,py,col,n=6){ for(let i=0;i<n;i++){ particles.push({ x:px, y:py, vx:(Math.random()-0.5)*5, vy:-Math.random()*5, life:1, maxLife:1, col:col||'#aaa', r:2+Math.random()*3 }); } } // ── CAMERA ────────────────────────────────────────────────────── let camX = player.x - canvas.width/2; let camY = player.y - canvas.height/2; function updateCamera(){ const tx = player.x + PW/2 - canvas.width/2; const ty = player.y + PH/2 - canvas.height/2; camX += (tx - camX) * 0.1; camY += (ty - camY) * 0.1; camX = Math.max(0, Math.min(camX, WORLD_W*BS - canvas.width)); camY = Math.max(0, Math.min(camY, WORLD_H*BS - canvas.height)); } // ── INPUT ─────────────────────────────────────────────────────── const keys = {}; window.addEventListener('keydown',e=>{ keys[e.code]=true; if(e.code==='KeyE'){ if(player.apples>0 && player.hp<player.maxHp){ player.apples--; player.hp = Math.min(player.maxHp, player.hp+4); updateHUD(); showMsg('🍎 Yum! +4 HP ('+player.apples+' apples left)'); } else if(player.apples<=0){ showMsg('No apples left! Find more 🍄 or mushrooms to trade luck.'); } } }); window.addEventListener('keyup', e=>{ keys[e.code]=false; }); const mouse = {x:0, y:0, left:false, right:false}; canvas.addEventListener('mousemove',e=>{ mouse.x=e.clientX; mouse.y=e.clientY; }); canvas.addEventListener('mousedown',e=>{ if(!started) return; if(e.button===0) mouse.left=true; if(e.button===2) mouse.right=true; }); canvas.addEventListener('mouseup',e=>{ if(e.button===0) mouse.left=false; if(e.button===2) mouse.right=false; }); canvas.addEventListener('contextmenu',e=>e.preventDefault()); // ── BLOCK INTERACTION ──────────────────────────────────────────── const REACH = 6; // blocks let breakTimer = 0, breakX=-1, breakY=-1; let placeCD = 0; function screenToBlock(sx,sy){ return { bx: Math.floor((sx + camX) / BS), by: Math.floor((sy + camY) / BS) }; } function inReach(bx,by){ const pcx = (player.x + PW/2) / BS; const pcy = (player.y + PH/2) / BS; return Math.abs(bx-pcx) < REACH && Math.abs(by-pcy) < REACH; } function breakTimeFor(b){ if(b===STONE||b===BRICK||b===MARBLE) return 0.45; if(b===OBSIDIAN) return 0.9; if(b===COAL||b===IRON) return 0.55; if(b===GOLD||b===RUBY||b===EMERALD||b===AMETHYST) return 0.6; if(b===DIAMOND) return 0.75; return 0.3; } // ── UPDATE ────────────────────────────────────────────────────── let started = false; let tick = 0; let lastTime = 0; function update(dt){ tick++; if(player.invincible>0) player.invincible -= dt; if(keys['KeyA']||keys['ArrowLeft']){ player.vx = -MOVE_SPD; player.facing = -1; } else if(keys['KeyD']||keys['ArrowRight']){ player.vx = MOVE_SPD; player.facing = 1; } else { player.vx *= 0.6; if(Math.abs(player.vx)<0.1) player.vx=0; } if((keys['Space']||keys['KeyW']||keys['ArrowUp']) && player.onGround){ player.vy = JUMP_VEL; player.onGround = false; } movePlayer(dt); if(player.y > WORLD_H*BS){ player.x = spawnX*BS; player.y = (spawnSY-3)*BS; player.vx=0; player.vy=0; } updateCamera(); const selItem = HOTBAR[selSlot]; const isSword = selItem === SWORD_ITEM; const isApple = selItem === APPLE_ITEM; // Block breaking (left click) — only if not holding apple/sword-only actions on air if(mouse.left){ const {bx,by} = screenToBlock(mouse.x, mouse.y); if(inReach(bx,by) && getB(bx,by)!==AIR && getB(bx,by)!==BEDROCK){ if(bx===breakX && by===breakY){ breakTimer += dt * (isSword?1.6:1); const need = breakTimeFor(getB(bx,by)); if(breakTimer >= need){ const col = BLOCK_COLOR[getB(bx,by)]||'#888'; spawnParticles(bx*BS-camX+BS/2, by*BS-camY+BS/2, col, 8); if(getB(bx,by)===MUSHROOM && Math.random()<0.3){ player.apples++; showMsg('Found an apple! 🍎 x'+player.apples); } setB(bx,by,AIR); breakTimer=0; breakX=-1; breakY=-1; } } else { breakX=bx; breakY=by; breakTimer=0; } } else { breakTimer=0; breakX=-1; breakY=-1; } } else { breakTimer=0; breakX=-1; breakY=-1; } // Block placing (right click) if(mouse.right && placeCD<=0 && !isSword && !isApple){ const {bx,by} = screenToBlock(mouse.x, mouse.y); if(inReach(bx,by) && getB(bx,by)===AIR){ const plx=Math.floor(player.x/BS), prx=Math.floor((player.x+PW-1)/BS); const pty=Math.floor(player.y/BS), pby=Math.floor((player.y+PH-1)/BS); const inside = bx>=plx&&bx<=prx&&by>=pty&&by<=pby; if(!inside){ setB(bx,by,selItem); placeCD = 0.2; } } } if(mouse.right && isApple && placeCD<=0){ if(player.apples>0 && player.hp<player.maxHp){ player.apples--; player.hp = Math.min(player.maxHp, player.hp+4); updateHUD(); showMsg('🍎 Yum! +4 HP'); } placeCD = 0.3; } if(placeCD>0) placeCD -= dt; // Enemies for(let i=enemies.length-1;i>=0;i--){ const e = enemies[i]; e.invTimer-=dt; const edx = player.x - e.x; if(Math.abs(edx)<canvas.width){ e.vx = Math.sign(edx) * (e.type==='slime'?1.1:1.5); if(e.onGround && Math.random()<0.015) e.vy = JUMP_VEL*0.65; } moveEnemy(e); if(player.invincible<=0){ const ox1=Math.max(player.x,e.x), ox2=Math.min(player.x+PW,e.x+BS*0.85); const oy1=Math.max(player.y,e.y), oy2=Math.min(player.y+PH,e.y+BS*1.8); if(ox1<ox2&&oy1<oy2){ player.hp--; player.invincible=1.2; player.vy=JUMP_VEL*0.4; player.vx=(player.x<e.x?-1:1)*5; updateHUD(); if(player.hp<=0){ player.hp=player.maxHp; player.x=spawnX*BS; player.y=(spawnSY-3)*BS; player.vx=0; player.vy=0; updateHUD(); } } } if(e.hp<=0){ enemies.splice(i,1); } } // Attack enemies with left click const atkRate = isSword ? 5 : 8; if(mouse.left && tick%atkRate===0){ for(let i=enemies.length-1;i>=0;i--){ const e=enemies[i]; const esx=e.x-camX+BS*0.4; const esy=e.y-camY+BS*0.9; if(Math.hypot(esx-mouse.x,esy-mouse.y)<50){ e.hp -= isSword ? 4 : 2; e.vy=-5; e.vx=(e.x<player.x?-4:4); spawnParticles(esx,esy,'#f00',5); if(e.hp<=0) enemies.splice(i,1); break; } } } // Particles for(let i=particles.length-1;i>=0;i--){ const p=particles[i]; p.x+=p.vx; p.y+=p.vy; p.vy+=0.3; p.life-=dt*1.8; if(p.life<=0) particles.splice(i,1); } // HUD text document.getElementById('posText').textContent = `x:${Math.floor(player.x/BS)} y:${Math.floor(player.y/BS)} | 🍎x${player.apples}`; const biomeNames=['Forest','Desert','Snow','Plains']; document.getElementById('biomeText').textContent = 'Biome: '+(biomeNames[biomeArr[Math.floor(player.x/BS)]]||'?'); } // ── RENDER ────────────────────────────────────────────────────── function drawBackground(){ const bio = biomeArr[Math.floor((player.x+PW/2)/BS)]||0; const skyColors = ['#5ab8f7','#e8d898','#cce8ff','#7ac8f0']; const sk = skyColors[bio]; const grad = ctx.createLinearGradient(0,0,0,canvas.height); grad.addColorStop(0, sk); grad.addColorStop(1,'#ddeeff'); ctx.fillStyle=grad; ctx.fillRect(0,0,canvas.width,canvas.height); ctx.save(); ctx.fillStyle='#ffe066'; ctx.shadowColor='#ffcc00'; ctx.shadowBlur=30; ctx.beginPath(); ctx.arc(canvas.width-90,70,30,0,Math.PI*2); ctx.fill(); ctx.restore(); ctx.fillStyle='rgba(255,255,255,0.85)'; for(let c=0;c<7;c++){ const cx = ((c*280 - camX*0.12 + WORLD_W*10) % (canvas.width+250)) - 100; const cy = 40+c*22; ctx.beginPath(); ctx.ellipse(cx,cy,55,20,0,0,Math.PI*2); ctx.fill(); ctx.ellipse(cx+30,cy-10,40,16,0,0,Math.PI*2); ctx.fill(); ctx.ellipse(cx-30,cy-6,35,14,0,0,Math.PI*2); ctx.fill(); } } function drawWorld(){ const sx0=Math.max(0,Math.floor(camX/BS)-1); const sx1=Math.min(WORLD_W,Math.ceil((camX+canvas.width)/BS)+1); const sy0=Math.max(0,Math.floor(camY/BS)-1); const sy1=Math.min(WORLD_H,Math.ceil((camY+canvas.height)/BS)+1); for(let y=sy0;y<sy1;y++){ for(let x=sx0;x<sx1;x++){ const b=getB(x,y); if(b===AIR) continue; const col=BLOCK_COLOR[b]; if(!col) continue; const px=x*BS-camX, py=y*BS-camY; ctx.save(); const alpha=BLOCK_ALPHA[b]; if(alpha!=null) ctx.globalAlpha=alpha; ctx.fillStyle=col; ctx.fillRect(px,py,BS,BS); if(BLOCK_TOP[b]){ ctx.fillStyle=BLOCK_TOP[b]; ctx.fillRect(px,py,BS,4); } if(b===DIAMOND||b===GOLD||b===RUBY||b===EMERALD||b===AMETHYST){ const glintCol = { [DIAMOND]:'rgba(255,255,255,0.45)', [GOLD]:'rgba(255,220,0,0.5)', [RUBY]:'rgba(255,255,255,0.5)', [EMERALD]:'rgba(255,255,255,0.5)', [AMETHYST]:'rgba(255,255,255,0.4)' }[b]; ctx.fillStyle=glintCol; ctx.fillRect(px+5,py+5,5,5); } if(b===TORCH){ ctx.fillStyle='#ffdd00'; ctx.shadowColor='#ff9900'; ctx.shadowBlur=10; ctx.fillRect(px+BS/2-3,py+2,6,8); ctx.shadowBlur=0; } if(b===BOOKSHELF){ ctx.fillStyle='#d9b877'; ctx.fillRect(px+3,py+4,BS-6,4); ctx.fillRect(px+3,py+12,BS-6,4); } if(b===CRAFTBENCH){ ctx.fillStyle='#5c3a1e'; ctx.fillRect(px+3,py+3,BS-6,BS-6); } ctx.globalAlpha=0.07; ctx.strokeStyle='#000'; ctx.lineWidth=0.5; ctx.strokeRect(px,py,BS,BS); ctx.restore(); } } } function drawBlockHighlight(){ const {bx,by}=screenToBlock(mouse.x,mouse.y); if(inReach(bx,by)&&getB(bx,by)!==AIR){ ctx.save(); ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.globalAlpha=0.7; ctx.strokeRect(bx*BS-camX,by*BS-camY,BS,BS); ctx.restore(); } } function drawBreakProgress(){ if(mouse.left&&breakTimer>0&&breakX>=0){ const px=breakX*BS-camX, py=breakY*BS-camY; const need = breakTimeFor(getB(breakX,breakY)); const pct=breakTimer/need; ctx.save(); ctx.globalAlpha=pct*0.5; ctx.fillStyle='#000'; ctx.fillRect(px,py,BS,BS); ctx.globalAlpha=pct*0.8; ctx.strokeStyle='#fff'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(px+3,py+3); ctx.lineTo(px+BS-3,py+BS-3); ctx.moveTo(px+BS-3,py+3); ctx.lineTo(px+3,py+BS-3); ctx.stroke(); ctx.restore(); } } function drawPlayer(){ if(player.invincible>0 && Math.floor(tick/4)%2===0) return; const x=player.x-camX, y=player.y-camY; const f=player.facing; ctx.fillStyle='#1a4e8a'; const legSwing = player.onGround ? Math.sin(tick*0.3)*3 : 0; ctx.fillRect(x+PW*0.1, y+PH*0.6, PW*0.35, PH*0.4+Math.abs(legSwing)); ctx.fillRect(x+PW*0.55, y+PH*0.6, PW*0.35, PH*0.4+Math.abs(legSwing)*0.6); ctx.fillStyle='#4a90d9'; ctx.fillRect(x+PW*0.1, y+PH*0.35, PW*0.8, PH*0.28); ctx.fillStyle='#f5c891'; ctx.fillRect(x+PW*0.1, y, PW*0.8, PH*0.36); ctx.fillStyle='#222'; if(f>0){ ctx.fillRect(x+PW*0.55, y+PH*0.1, 4, 4); } else { ctx.fillRect(x+PW*0.2, y+PH*0.1, 4, 4); } ctx.fillStyle='#f5c891'; if(f>0) ctx.fillRect(x+PW*0.85, y+PH*0.35, 7, PH*0.28); else ctx.fillRect(x-7, y+PH*0.35, 7, PH*0.28); // Show held item const held = HOTBAR[selSlot]; const emoji = slotEmoji(held); ctx.font = '16px sans-serif'; if(f>0) ctx.fillText(emoji, x+PW*0.9, y+PH*0.6); else ctx.fillText(emoji, x-14, y+PH*0.6); } function drawEnemies(){ for(const e of enemies){ const sx=e.x-camX, sy=e.y-camY; if(sx<-80||sx>canvas.width+80) continue; ctx.save(); if(e.type==='zombie'){ ctx.fillStyle='#5cb85c'; ctx.fillRect(sx+2,sy,BS*0.8,BS*0.75); ctx.fillStyle='#444'; ctx.fillRect(sx+2,sy+BS*0.75,BS*0.8,BS*0.9); ctx.fillStyle='#333'; ctx.fillRect(sx+2,sy+BS*1.65,BS*0.32,BS*0.35); ctx.fillRect(sx+BS*0.52,sy+BS*1.65,BS*0.32,BS*0.35); } else { ctx.fillStyle='#22dd88'; ctx.globalAlpha=0.85; ctx.beginPath(); ctx.ellipse(sx+BS*0.45,sy+BS*1.2,BS*0.45,BS*0.65,0,0,Math.PI*2); ctx.fill(); ctx.fillStyle='#fff'; ctx.globalAlpha=1; ctx.fillRect(sx+BS*0.2,sy+BS*0.85,5,5); ctx.fillRect(sx+BS*0.6,sy+BS*0.85,5,5); } ctx.globalAlpha=1; ctx.fillStyle='#500'; ctx.fillRect(sx,sy-8,BS,5); ctx.fillStyle='#f00'; ctx.fillRect(sx,sy-8,BS*(e.hp/e.maxHp),5); ctx.restore(); } } function drawParticles(){ for(const p of particles){ ctx.save(); ctx.globalAlpha=p.life; ctx.fillStyle=p.col; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill(); ctx.restore(); } } function drawMinimap(){ const mw=120, mh=55; const mx=canvas.width-mw-8, my=8; ctx.save(); ctx.globalAlpha=0.75; ctx.fillStyle='#111'; ctx.fillRect(mx,my,mw,mh); const scX=mw/WORLD_W, scY=mh/WORLD_H; for(let x=0;x<WORLD_W;x+=4){ const sy=surfaceY[x]; const bio=biomeArr[x]; const cols=['#3a9e3a','#e8d87a','#eef4ff','#3a9e3a']; ctx.fillStyle=cols[bio]||'#3a9e3a'; ctx.fillRect(mx+x*scX, my+sy*scY, scX*4+1, mh-(sy*scY)); ctx.fillStyle='#555'; ctx.fillRect(mx+x*scX, my+(sy+5)*scY, scX*4+1, mh-((sy+5)*scY)); } ctx.fillStyle='#fff'; ctx.fillRect(mx+(player.x/BS)*scX-1.5, my+(player.y/BS)*scY-1.5, 3, 3); ctx.globalAlpha=1; ctx.strokeStyle='#fff'; ctx.lineWidth=1; ctx.strokeRect(mx,my,mw,mh); ctx.restore(); } // ── GAME LOOP ────────────────────────────────────────────────── function render(){ ctx.clearRect(0,0,canvas.width,canvas.height); drawBackground(); drawWorld(); drawBlockHighlight(); drawBreakProgress(); drawEnemies(); drawPlayer(); drawParticles(); drawMinimap(); } function loop(t){ requestAnimationFrame(loop); const dt = Math.min((t - lastTime)/1000, 0.05); lastTime = t; if(!started) return; update(dt); render(); } requestAnimationFrame(loop); document.getElementById('startBtn').addEventListener('click',()=>{ document.getElementById('overlay').style.display='none'; started=true; }); </script> </body> </html>