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!
<!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:4px; margin-bottom:8px; background:rgba(0,0,0,0.7); padding:6px 10px; border-radius:12px; pointer-events:all; } .slot { width:52px; height:52px; border:3px solid #666; border-radius:6px; cursor:pointer; display:flex; flex-direction:column; align-items:center; justify-content:center; font-size:22px; color:#fff; background:rgba(30,30,30,0.85); position:relative; } .slot span { font-size:9px; color:#aaa; margin-top:1px; } .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; } #hp-inner { display:flex; gap:3px; margin-top:4px; } .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:420px; } #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:90px; 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; } </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 block<br> Right Click — Place block<br> 1–9 / Scroll — Switch item </div> <div id="crosshair">+</div> <div id="overlay"> <h1>🌍 Terra Builder</h1> <p> Explore a huge auto-generated world!<br> Mine blocks, place structures, fight enemies.<br> <b>A/D</b> to move | <b>W or Space</b> to jump<br> <b>Left Click</b> = Break | <b>Right Click</b> = Place<br> <b>1–9 or Scroll</b> = Switch item </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; 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' }; const BLOCK_TOP = { [GRASS]:'#56c93a' }; 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 }; const BLOCK_ALPHA = { [WATER]:0.6, [GLASS]:0.45, [LEAVES]:0.85 }; 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' }; const BLOCK_EMOJI = { [GRASS]:'🟩',[DIRT]:'🟫',[STONE]:'🪨',[WOOD]:'🪵',[LEAVES]:'🌿', [SAND]:'🏜️',[COAL]:'⚫',[IRON]:'🟤',[GOLD]:'🟡',[DIAMOND]:'💎', [TORCH]:'🔦',[PLANK]:'🟨',[GLASS]:'🔷',[SNOW]:'❄️',[CACTUS]:'🌵', [GRAVEL]:'🩶' }; // ── 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] // We want the surface to be roughly in the MIDDLE of the world vertically 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){ // underground if(y > sy+40) setB(x, y, STONE); else if(y > sy+3) setB(x, y, STONE); else setB(x, y, DIRT); } else if(y === sy){ // surface block 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 { // above surface = air setB(x, y, AIR); } } } // Ores 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); // Trees 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){ // cactus 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){ // tree 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===2 && r<0.04){ // snow tree 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); } } } // 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); } } } } } // Water pools in depressions for(let x=0;x<WORLD_W;x++){ const sy = surfaceY[x]; // find neighbours const left = x>0 ? surfaceY[x-1] : sy; const right = x<WORLD_W-1 ? surfaceY[x+1] : sy; if(sy > left && sy > right){ // depression — fill with water above setB(x, sy-1, WATER); } } } generateWorld(); // ── HOTBAR ───────────────────────────────────────────────────── const HOTBAR = [GRASS, DIRT, STONE, WOOD, PLANK, TORCH, GLASS, SAND, CACTUS]; let selSlot = 0; const hotbarEl = document.getElementById('hotbar'); function buildHotbar(){ hotbarEl.innerHTML = ''; HOTBAR.forEach((b,i)=>{ const d = document.createElement('div'); d.className = 'slot' + (i===selSlot?' sel':''); d.innerHTML = `${BLOCK_EMOJI[b]||'⬜'}<span>${BLOCK_NAME[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=>{ 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 = { // pixel position (top-left of bounding box) x: spawnX * BS + BS/2 - PW/2, y: (spawnSY - 3) * BS, // start 3 blocks above surface vx: 0, vy: 0, onGround: false, hp: 20, maxHp: 20, facing: 1, invincible: 0, }; 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; // Move X player.x += player.vx; // Clamp to world player.x = Math.max(0, Math.min(player.x, (WORLD_W-1)*BS - PW)); // X collision resolveX(); // Move Y player.y += player.vy; player.onGround = false; resolveY(); } function resolveX(){ // left edge blocks 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){ // moving right — check right column if(isSolid(rbx, tby) || isSolid(rbx, bby)){ player.x = rbx * BS - PW; player.vx = 0; } // check middle row too 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){ // falling — check bottom if(isSolid(lbx, bby) || isSolid(rbx, bby)){ player.y = bby * BS - PH; player.vy = 0; player.onGround = true; } } else if(player.vy < 0){ // jumping — check top 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; // X 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; } } // Y 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; }); 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; } // ── UPDATE ────────────────────────────────────────────────────── let started = false; let tick = 0; let lastTime = 0; function update(dt){ tick++; if(player.invincible>0) player.invincible -= dt; // Input → velocity 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); // Clamp player vertically if(player.y > WORLD_H*BS){ // fell out — respawn player.x = spawnX*BS; player.y = (spawnSY-3)*BS; player.vx=0; player.vy=0; } updateCamera(); // Block breaking (left click) 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; if(breakTimer >= 0.35){ const col = BLOCK_COLOR[getB(bx,by)]||'#888'; spawnParticles(bx*BS-camX+BS/2, by*BS-camY+BS/2, col, 8); 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){ const {bx,by} = screenToBlock(mouse.x, mouse.y); if(inReach(bx,by) && getB(bx,by)===AIR){ // Don't place inside player 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,HOTBAR[selSlot]); placeCD = 0.2; } } } if(placeCD>0) placeCD -= dt; // Enemies for(let i=enemies.length-1;i>=0;i--){ const e = enemies[i]; e.invTimer-=dt; // Move toward player if nearby 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); // Hurt player on contact 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 if(mouse.left && tick%8===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-=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)}`; 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); // Sun 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(); // Clouds 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); // Top tint if(BLOCK_TOP[b]){ ctx.fillStyle=BLOCK_TOP[b]; ctx.fillRect(px,py,BS,4); } // Ore glint if(b===DIAMOND||b===GOLD){ ctx.fillStyle=b===DIAMOND?'rgba(255,255,255,0.45)':'rgba(255,220,0,0.5)'; ctx.fillRect(px+5,py+5,5,5); } // Torch 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; } // Grid 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 pct=breakTimer/0.35; 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; // legs 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); // body ctx.fillStyle='#4a90d9'; ctx.fillRect(x+PW*0.1, y+PH*0.35, PW*0.8, PH*0.28); // head ctx.fillStyle='#f5c891'; ctx.fillRect(x+PW*0.1, y, PW*0.8, PH*0.36); // eyes 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); } // arm 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); } 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); } // HP bar 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){ // just draw surface line 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)); } // player dot 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>
No pictures yet. Add one, then ask your buddy to put it in your app!