🎮ArcadeLab

Blockwalk

by LunarGalaxy87
280 lines9.0 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blockwalk</title>
<style>
  html, body { margin: 0; padding: 0; overflow: hidden; background: #4a90d9; font-family: 'Segoe UI', Arial, sans-serif; height: 100%; }
  canvas { display: block; image-rendering: pixelated; }
  #hud {
    position: fixed; top: 16px; left: 16px; color: white;
    background: rgba(0,0,0,0.4); padding: 10px 16px; border-radius: 10px;
    font-size: 14px; line-height: 1.5; pointer-events: none;
    text-shadow: 0 1px 2px rgba(0,0,0,0.5);
  }
  #startScreen {
    position: fixed; inset: 0; background: linear-gradient(#4a90d9, #7fc97f);
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    color: #1c2e1c; text-align: center; z-index: 10;
  }
  #startScreen h1 { font-size: 44px; margin: 0 0 8px; letter-spacing: 1px; }
  #startScreen p { font-size: 15px; margin: 4px 0; opacity: 0.85; }
  #startButton {
    margin-top: 22px; padding: 14px 34px; font-size: 18px; border: none;
    border-radius: 8px; background: #3a7d44; color: white; cursor: pointer;
    box-shadow: 0 4px 0 #2a5a32;
  }
  #startButton:active { box-shadow: 0 1px 0 #2a5a32; transform: translateY(3px); }
  .dpad {
    position: fixed; bottom: 24px; left: 24px; display: none;
    grid-template-columns: 56px 56px 56px;
    grid-template-rows: 56px 56px 56px;
    gap: 4px; z-index: 5;
  }
  .dpad button {
    font-size: 20px; border: none; border-radius: 8px;
    background: rgba(255,255,255,0.55); color: #223; touch-action: none;
  }
  .dpad button:active { background: rgba(255,255,255,0.9); }
</style>
</head>
<body>

<div id="startScreen">
  <h1>🧱 Blockwalk</h1>
  <p>A tiny top-down block world you can wander around.</p>
  <p>WASD / Arrow keys or the on-screen pad to move</p>
  <button id="startButton">Enter World</button>
</div>

<div id="hud" style="display:none;">WASD / Arrows: move around</div>

<div class="dpad" id="dpad">
  <div></div><button data-dir="up">▲</button><div></div>
  <button data-dir="left">◀</button><div></div><button data-dir="right">▶</button>
  <div></div><button data-dir="down">▼</button><div></div>
</div>

<canvas id="game"></canvas>

<script>
(function() {
  const canvas = document.getElementById('game');
  const ctx = canvas.getContext('2d');
  const TILE = 40;
  const WORLD_SIZE = 40;
  let world = [];
  let player = { x: WORLD_SIZE/2, y: WORLD_SIZE/2, dir: 'down', animT: 0 };
  const keys = { up:false, down:false, left:false, right:false };
  const colors = {
    water: '#3a7bd5',
    sand:  '#e0d18f',
    grass: '#5fae4a',
    grassDark: '#4f9a3d',
    tree:  '#2d6b2f',
    trunk: '#7a4a25',
    stone: '#9a9a9a'
  };

  function resize() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
  }
  window.addEventListener('resize', resize);
  resize();

  function noise2(x, y) {
    return Math.sin(x*0.35 + y*0.11) + Math.sin(x*0.09 - y*0.28) + Math.sin((x+y)*0.05);
  }

  function generateWorld() {
    for (let y = 0; y < WORLD_SIZE; y++) {
      const row = [];
      for (let x = 0; x < WORLD_SIZE; x++) {
        const n = noise2(x, y);
        let type = 'grass';
        if (n < -1.4) type = 'water';
        else if (n < -1.0) type = 'sand';
        else if (n > 1.6) type = 'stone';
        row.push({ type, tree: false, dark: (x + y) % 2 === 0 });
      }
      world.push(row);
    }
    for (let y = 0; y < WORLD_SIZE; y++) {
      for (let x = 0; x < WORLD_SIZE; x++) {
        if (world[y][x].type === 'grass' && Math.random() < 0.06) {
          world[y][x].tree = true;
        }
      }
    }
    world[Math.floor(player.y)][Math.floor(player.x)] = { type: 'grass', tree: false, dark: false };
  }

  function isWalkable(tx, ty) {
    if (tx < 0 || ty < 0 || tx >= WORLD_SIZE || ty >= WORLD_SIZE) return false;
    const tile = world[ty][tx];
    if (tile.type === 'water') return false;
    if (tile.tree) return false;
    return true;
  }

  const speed = 3.2;
  let prevTime = performance.now();

  function update(dt) {
    let dx = 0, dy = 0;
    if (keys.up) dy -= 1;
    if (keys.down) dy += 1;
    if (keys.left) dx -= 1;
    if (keys.right) dx += 1;

    if (dx !== 0 && dy !== 0) { dx *= 0.7071; dy *= 0.7071; }

    if (dx !== 0 || dy !== 0) {
      player.animT += dt;
      if (Math.abs(dx) > Math.abs(dy)) player.dir = dx > 0 ? 'right' : 'left';
      else if (dy !== 0) player.dir = dy > 0 ? 'down' : 'up';

      const nx = player.x + dx * speed * dt;
      const ny = player.y + dy * speed * dt;

      if (isWalkable(Math.floor(nx), Math.floor(player.y))) player.x = nx;
      if (isWalkable(Math.floor(player.x), Math.floor(ny))) player.y = ny;

      player.x = Math.max(0.3, Math.min(WORLD_SIZE - 0.3, player.x));
      player.y = Math.max(0.3, Math.min(WORLD_SIZE - 0.3, player.y));
    } else {
      player.animT = 0;
    }
  }

  function drawTile(px, py, tile) {
    let c = colors[tile.type];
    if (tile.type === 'grass' && tile.dark) c = colors.grassDark;
    ctx.fillStyle = c;
    ctx.fillRect(px, py, TILE, TILE);

    if (tile.type === 'water') {
      ctx.fillStyle = 'rgba(255,255,255,0.15)';
      ctx.fillRect(px, py + TILE*0.4, TILE, 3);
    }

    if (tile.tree) {
      ctx.fillStyle = colors.trunk;
      ctx.fillRect(px + TILE*0.42, py + TILE*0.55, TILE*0.16, TILE*0.4);
      ctx.fillStyle = colors.tree;
      ctx.beginPath();
      ctx.arc(px + TILE*0.5, py + TILE*0.42, TILE*0.38, 0, Math.PI*2);
      ctx.fill();
    }
  }

  function drawPlayer(px, py, bob) {
    const w = TILE * 0.5;
    const h = TILE * 0.5;
    const cx = px + TILE/2;
    const cy = py + TILE/2 - bob;

    ctx.fillStyle = 'rgba(0,0,0,0.25)';
    ctx.beginPath();
    ctx.ellipse(cx, py + TILE*0.82, TILE*0.28, TILE*0.1, 0, 0, Math.PI*2);
    ctx.fill();

    ctx.fillStyle = '#e2725b';
    ctx.fillRect(cx - w/2, cy - h/2, w, h*0.75);

    ctx.fillStyle = '#f2c49b';
    ctx.beginPath();
    ctx.arc(cx, cy - h*0.5, w*0.32, 0, Math.PI*2);
    ctx.fill();

    ctx.fillStyle = '#2b2b2b';
    let ex = 0, ey = 0;
    if (player.dir === 'left') ex = -w*0.12;
    if (player.dir === 'right') ex = w*0.12;
    if (player.dir === 'up') ey = -h*0.12;
    if (player.dir === 'down') ey = h*0.06;
    ctx.fillRect(cx - w*0.14 + ex, cy - h*0.55 + ey, 3, 3);
    ctx.fillRect(cx + w*0.10 + ex, cy - h*0.55 + ey, 3, 3);
  }

  function render() {
    ctx.imageSmoothingEnabled = false;
    ctx.fillStyle = '#264d73';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const camX = player.x * TILE - canvas.width / 2;
    const camY = player.y * TILE - canvas.height / 2;

    const startCol = Math.max(0, Math.floor(camX / TILE) - 1);
    const endCol = Math.min(WORLD_SIZE - 1, Math.ceil((camX + canvas.width) / TILE) + 1);
    const startRow = Math.max(0, Math.floor(camY / TILE) - 1);
    const endRow = Math.min(WORLD_SIZE - 1, Math.ceil((camY + canvas.height) / TILE) + 1);

    for (let y = startRow; y <= endRow; y++) {
      for (let x = startCol; x <= endCol; x++) {
        const tile = world[y][x];
        if (tile.tree) continue;
        drawTile(x*TILE - camX, y*TILE - camY, tile);
      }
    }

    for (let y = startRow; y <= endRow; y++) {
      for (let x = startCol; x <= endCol; x++) {
        const tile = world[y][x];
        if (!tile.tree) continue;
        ctx.fillStyle = tile.dark ? colors.grassDark : colors.grass;
        ctx.fillRect(x*TILE - camX, y*TILE - camY, TILE, TILE);
        drawTile(x*TILE - camX, y*TILE - camY, tile);
      }
    }

    const bob = (player.animT > 0) ? Math.abs(Math.sin(player.animT * 8)) * 3 : 0;
    drawPlayer(player.x*TILE - camX - TILE/2, player.y*TILE - camY - TILE/2, bob);
  }

  function loop(time) {
    const dt = Math.min((time - prevTime) / 1000, 0.1);
    prevTime = time;
    update(dt);
    render();
    requestAnimationFrame(loop);
  }

  function setKey(code, val) {
    switch(code) {
      case 'KeyW': case 'ArrowUp': keys.up = val; break;
      case 'KeyS': case 'ArrowDown': keys.down = val; break;
      case 'KeyA': case 'ArrowLeft': keys.left = val; break;
      case 'KeyD': case 'ArrowRight': keys.right = val; break;
    }
  }

  document.addEventListener('keydown', e => setKey(e.code, true));
  document.addEventListener('keyup', e => setKey(e.code, false));

  const dpad = document.getElementById('dpad');
  dpad.querySelectorAll('button').forEach(btn => {
    const dir = btn.getAttribute('data-dir');
    const map = { up:'up', down:'down', left:'left', right:'right' };
    const press = (v) => (e) => { e.preventDefault(); keys[map[dir]] = v; };
    btn.addEventListener('touchstart', press(true));
    btn.addEventListener('touchend', press(false));
    btn.addEventListener('mousedown', press(true));
    btn.addEventListener('mouseup', press(false));
    btn.addEventListener('mouseleave', press(false));
  });

  document.getElementById('startButton').addEventListener('click', () => {
    document.getElementById('startScreen').style.display = 'none';
    document.getElementById('hud').style.display = 'block';
    if ('ontouchstart' in window) dpad.style.display = 'grid';
    generateWorld();
    prevTime = performance.now();
    requestAnimationFrame(loop);
  });
})();
</script>
</body>
</html>

Game Source: Blockwalk

Creator: LunarGalaxy87

Libraries: none

Complexity: complex (280 lines, 9.0 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: blockwalk-lunargalaxy87" to link back to the original. Then publish at arcadelab.ai/publish.