🎮ArcadeLab

DayZ 2D - Winter Survival v2

by SparkLegend68
797 lines20.2 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DayZ 2D - Winter Survival v2</title>
<style>
  html, body {
    margin: 0;
    padding: 0;
    background: #000;
    overflow: hidden;
    font-family: Arial, sans-serif;
  }
  #game {
    display: block;
  }
  #ui {
    position: fixed;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
  }
  .screen {
    pointer-events: auto;
    background: rgba(0,0,0,0.85);
    padding: 20px 40px;
    border-radius: 8px;
    border: 2px solid #555;
    text-align: center;
    color: #eee;
  }
  button {
    padding: 10px 20px;
    margin: 10px;
    background: #222;
    color: #eee;
    border: 1px solid #444;
    cursor: pointer;
  }
  button:hover { background: #333; }
</style>
</head>
<body>

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

<div id="ui">
  <div id="startScreen" class="screen">
    <h1>DayZ 2D</h1>
    <p>Winter pixel survival prototype</p>
    <button id="playBtn">Play</button>
    <button id="mpBtn">Multiplayer (Coming Soon)</button>
  </div>

  <div id="gameOverScreen" class="screen" style="display:none;">
    <h2>You Died</h2>
    <p>Press restart to try again.</p>
    <button id="restartBtn">Restart</button>
  </div>
</div>

<script>
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");

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

// UI
const startScreen = document.getElementById("startScreen");
const gameOverScreen = document.getElementById("gameOverScreen");
const playBtn = document.getElementById("playBtn");
const restartBtn = document.getElementById("restartBtn");
const mpBtn = document.getElementById("mpBtn");

mpBtn.onclick = () => {
  alert("Multiplayer placeholder.\nLater you can add lobbies, servers, or WebSockets here.");
};

// World / camera
const worldWidth = 4000;
const worldHeight = 4000;
let camX = 0;
let camY = 0;

// Input
const keys = {};
window.addEventListener("keydown", e => {
  keys[e.key.toLowerCase()] = true;
});
window.addEventListener("keyup", e => {
  keys[e.key.toLowerCase()] = false;
});

// Mouse for aiming + shooting
let mouseX = 0;
let mouseY = 0;
let shooting = false;

canvas.addEventListener("mousemove", e => {
  const rect = canvas.getBoundingClientRect();
  mouseX = e.clientX - rect.left;
  mouseY = e.clientY - rect.top;
});

canvas.addEventListener("mousedown", e => {
  if (e.button === 0) shooting = true;
});

canvas.addEventListener("mouseup", e => {
  if (e.button === 0) shooting = false;
});

// Game state
let running = false;
let lastTime = 0;

let player;
let zombies = [];
let bullets = [];
let pickups = []; // ammo + food + weapons
let trees = [];
let buildings = [];

let shootCooldown = 0;

// Weapons definition
const weapons = {
  ak:   { name: "AK",     magSize: 30, fireRate: 0.15, bulletSpeed: 420, ammoType: "rifle",  reloadTime: 1.2 },
  m4:   { name: "M4",     magSize: 35, fireRate: 0.12, bulletSpeed: 460, ammoType: "rifle",  reloadTime: 1.1 },
  sniper:{name: "Sniper", magSize: 5,  fireRate: 0.8,  bulletSpeed: 700, ammoType: "sniper", reloadTime: 1.8 },
  lmg:  { name: "LMG",    magSize: 60, fireRate: 0.08, bulletSpeed: 400, ammoType: "lmg",    reloadTime: 2.0 },
  rpg:  { name: "RPG",    magSize: 1,  fireRate: 1.2,  bulletSpeed: 260, ammoType: "rocket", reloadTime: 2.5 }
};

function randomInRange(min, max) {
  return Math.random() * (max - min) + min;
}

function generateWorldDecor() {
  trees = [];
  buildings = [];

  // Trees
  for (let i = 0; i < 260; i++) {
    trees.push({
      x: Math.random() * worldWidth,
      y: Math.random() * worldHeight
    });
  }

  // More buildings
  for (let i = 0; i < 16; i++) {
    const w = randomInRange(120, 260);
    const h = randomInRange(100, 220);
    buildings.push({
      x: Math.random() * (worldWidth - w),
      y: Math.random() * (worldHeight - h),
      w,
      h
    });
  }
}

function resetGame() {
  generateWorldDecor();

  player = {
    x: worldWidth / 2,
    y: worldHeight / 2,
    baseSpeed: 100,
    speed: 100,
    hp: 100,
    hunger: 100,
    stamina: 100,
    dirX: 1,
    dirY: 0,
    currentWeaponKey: "ak",
    ammoInMag: 30,
    ammoReserve: {
      rifle: 90,
      sniper: 20,
      lmg: 120,
      rocket: 5
    },
    reloading: false,
    reloadTimer: 0,
    walkAnim: 0 // for walking animation
  };

  const w = weapons[player.currentWeaponKey];
  player.magSize = w.magSize;
  player.reloadTime = w.reloadTime;

  zombies = [];
  bullets = [];
  pickups = [];

  for (let i = 0; i < 30; i++) {
    zombies.push(spawnZombie());
  }

  // Ammo pickups
  for (let i = 0; i < 24; i++) {
    pickups.push(spawnPickup("ammo"));
  }
  // Food pickups
  for (let i = 0; i < 14; i++) {
    pickups.push(spawnPickup("food"));
  }
  // Weapon pickups
  const weaponTypes = ["m4", "sniper", "lmg", "rpg"];
  weaponTypes.forEach(type => {
    for (let i = 0; i < 3; i++) {
      pickups.push(spawnWeaponPickup(type));
    }
  });

  lastTime = performance.now();
  shootCooldown = 0;
}

function spawnZombie() {
  const edge = Math.floor(Math.random() * 4);
  let x, y;
  if (edge === 0) { // top
    x = Math.random() * worldWidth;
    y = -40;
  } else if (edge === 1) { // right
    x = worldWidth + 40;
    y = Math.random() * worldHeight;
  } else if (edge === 2) { // bottom
    x = Math.random() * worldWidth;
    y = worldHeight + 40;
  } else { // left
    x = -40;
    y = Math.random() * worldHeight;
  }
  return {
    x,
    y,
    speed: 50 + Math.random() * 20,
    aggro: false,
    walkAnim: 0
  };
}

function spawnPickup(type) {
  return {
    x: Math.random() * worldWidth,
    y: Math.random() * worldHeight,
    type
  };
}

function spawnWeaponPickup(weaponKey) {
  return {
    x: Math.random() * worldWidth,
    y: Math.random() * worldHeight,
    type: "weapon",
    weaponKey
  };
}

function tryReload() {
  if (player.reloading) return;
  const w = weapons[player.currentWeaponKey];
  if (player.ammoInMag >= w.magSize) return;

  const ammoType = w.ammoType;
  if (!player.ammoReserve[ammoType] || player.ammoReserve[ammoType] <= 0) return;

  player.reloading = true;
  player.reloadTimer = w.reloadTime;
}

function finishReload() {
  const w = weapons[player.currentWeaponKey];
  const ammoType = w.ammoType;
  const needed = w.magSize - player.ammoInMag;
  const available = player.ammoReserve[ammoType] || 0;
  const taken = Math.min(needed, available);
  player.ammoInMag += taken;
  player.ammoReserve[ammoType] -= taken;
  player.reloading = false;
}

function update(dt) {
  // Hunger drain
  player.hunger -= 2 * dt;
  if (player.hunger < 0) player.hunger = 0;
  if (player.hunger <= 0) {
    player.hp -= 5 * dt; // starving damage
  }

  // Stamina & sprint (Shift)
  const sprinting = keys["shift"];
  if (sprinting && player.stamina > 0) {
    player.speed = player.baseSpeed * 1.5;
    player.stamina -= 25 * dt;
    if (player.stamina < 0) player.stamina = 0;
  } else {
    player.speed = player.baseSpeed;
    player.stamina += 15 * dt;
    if (player.stamina > 100) player.stamina = 100;
  }

  // Movement
  let dx = 0, dy = 0;
  if (keys["w"] || keys["arrowup"]) dy -= 1;
  if (keys["s"] || keys["arrowdown"]) dy += 1;
  if (keys["a"] || keys["arrowleft"]) dx -= 1;
  if (keys["d"] || keys["arrowright"]) dx += 1;

  const moving = (dx !== 0 || dy !== 0);

  if (moving) {
    const len = Math.hypot(dx, dy) || 1;
    dx /= len;
    dy /= len;
    player.dirX = dx;
    player.dirY = dy;
    player.walkAnim += dt * 8; // speed of walk animation
  } else {
    // slowly return to neutral
    player.walkAnim *= 0.9;
  }

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

  // Clamp to world
  player.x = Math.max(0, Math.min(worldWidth, player.x));
  player.y = Math.max(0, Math.min(worldHeight, player.y));

  // Camera follow
  camX = player.x - canvas.width / 2;
  camY = player.y - canvas.height / 2;
  camX = Math.max(0, Math.min(worldWidth - canvas.width, camX));
  camY = Math.max(0, Math.min(worldHeight - canvas.height, camY));

  // Aim direction from player to mouse (convert mouse to world coords)
  const worldMouseX = camX + mouseX;
  const worldMouseY = camY + mouseY;
  const ax = worldMouseX - player.x;
  const ay = worldMouseY - player.y;
  const ad = Math.hypot(ax, ay) || 1;
  const aimX = ax / ad;
  const aimY = ay / ad;

  if (ad > 4) {
    player.dirX = aimX;
    player.dirY = aimY;
  }

  // Reload logic
  if (keys["r"]) {
    tryReload();
  }
  if (player.reloading) {
    player.reloadTimer -= dt;
    if (player.reloadTimer <= 0) {
      finishReload();
    }
  }

  const w = weapons[player.currentWeaponKey];

  // Shooting
  shootCooldown -= dt;
  if (shooting && shootCooldown <= 0 && !player.reloading) {
    if (player.ammoInMag > 0) {
      shootCooldown = w.fireRate;
      player.ammoInMag--;

      const speed = w.bulletSpeed;
      const isRocket = (w.ammoType === "rocket");

      bullets.push({
        x: player.x + aimX * 14,
        y: player.y + aimY * 10,
        vx: aimX * speed,
        vy: aimY * speed,
        life: isRocket ? 2.0 : 1.2,
        rocket: isRocket
      });
    }
  }

  // Update bullets
  bullets.forEach(b => {
    b.x += b.vx * dt;
    b.y += b.vy * dt;
    b.life -= dt;
  });
  bullets = bullets.filter(b =>
    b.life > 0 &&
    b.x > -100 && b.x < worldWidth + 100 &&
    b.y > -100 && b.y < worldHeight + 100
  );

  // Zombies: only aggro when close, with simple walk animation
  zombies.forEach(z => {
    const zx = player.x - z.x;
    const zy = player.y - z.y;
    const dist = Math.hypot(zx, zy) || 1;

    // Aggro radius
    if (!z.aggro && dist < 260) {
      z.aggro = true;
    }
    // De-aggro if far away
    if (z.aggro && dist > 420) {
      z.aggro = false;
    }

    if (z.aggro) {
      const vx = (zx / dist) * z.speed * dt;
      const vy = (zy / dist) * z.speed * dt;
      z.x += vx;
      z.y += vy;
      z.walkAnim += dt * 6;
    } else {
      // idle shuffle
      z.walkAnim *= 0.9;
    }

    // Collision damage only if very close
    if (dist < 24) {
      player.hp -= 15 * dt;
    }
  });

  // Bullet–zombie collisions (rockets have bigger radius)
  bullets.forEach(b => {
    zombies.forEach(z => {
      const d = Math.hypot(b.x - z.x, b.y - z.y);
      const hitRadius = b.rocket ? 40 : 18;
      if (d < hitRadius && !z.dead) {
        z.dead = true;
        if (b.rocket) {
          b.life = 0;
        } else {
          b.life = 0;
        }
      }
    });
  });

  // Remove dead zombies and respawn
  zombies = zombies.filter(z => !z.dead);
  while (zombies.length < 30) {
    zombies.push(spawnZombie());
  }

  // Pickup collisions
  pickups.forEach(p => {
    const d = Math.hypot(player.x - p.x, player.y - p.y);
    if (d < 26 && !p.collected) {
      p.collected = true;
      if (p.type === "ammo") {
        // random ammo type
        const types = ["rifle", "sniper", "lmg", "rocket"];
        const t = types[Math.floor(Math.random() * types.length)];
        const amount = (t === "rocket") ? 2 : 30;
        player.ammoReserve[t] = (player.ammoReserve[t] || 0) + amount;
      } else if (p.type === "food") {
        player.hunger += 35;
        if (player.hunger > 100) player.hunger = 100;
        player.hp += 15;
        if (player.hp > 100) player.hp = 100;
      } else if (p.type === "weapon") {
        player.currentWeaponKey = p.weaponKey;
        const nw = weapons[p.weaponKey];
        player.magSize = nw.magSize;
        player.reloadTime = nw.reloadTime;
        // auto-fill mag if ammo exists
        const ammoType = nw.ammoType;
        const have = player.ammoReserve[ammoType] || 0;
        const needed = nw.magSize;
        const taken = Math.min(needed, have);
        player.ammoInMag = taken;
        player.ammoReserve[ammoType] = have - taken;
      }
    }
  });
  pickups = pickups.filter(p => !p.collected);

  // Keep some pickups around
  while (pickups.filter(p => p.type === "ammo").length < 24) {
    pickups.push(spawnPickup("ammo"));
  }
  while (pickups.filter(p => p.type === "food").length < 14) {
    pickups.push(spawnPickup("food"));
  }

  if (player.hp <= 0) {
    running = false;
    gameOverScreen.style.display = "block";
  }
}

// Pixel-art helpers
function drawPixelRect(x, y, w, h, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, w, h);
}

// Draw soldier with gun (top-down pixel art) + walk animation
function drawPlayer(px, py, dirX, dirY) {
  const size = 24;
  const half = size / 2;
  const baseX = px - half - camX;
  const baseY = py - half - camY;

  const legOffset = Math.sin(player.walkAnim) * 2;

  // Body (armor)
  drawPixelRect(baseX + 6, baseY + 8, 12, 12, "#2b3b2b"); // torso
  // Head
  drawPixelRect(baseX + 8, baseY + 2, 8, 8, "#c9b28a");   // face
  drawPixelRect(baseX + 8, baseY + 0, 8, 4, "#3b5b3b");   // helmet
  // Legs with simple animation
  drawPixelRect(baseX + 6, baseY + 20 + legOffset, 4, 6, "#1f2525");
  drawPixelRect(baseX + 14, baseY + 20 - legOffset, 4, 6, "#1f2525");
  // Arms
  drawPixelRect(baseX + 2, baseY + 10, 4, 8, "#c9b28a");
  drawPixelRect(baseX + 18, baseY + 10, 4, 8, "#c9b28a");

  // Gun style by weapon
  const w = weapons[player.currentWeaponKey];
  const gunLenMap = {
    ak: 16,
    m4: 18,
    sniper: 22,
    lmg: 20,
    rpg: 24
  };
  const gunLen = gunLenMap[player.currentWeaponKey] || 16;
  const gunThick = 4;
  const gx = px - camX + dirX * 10;
  const gy = py - camY + dirY * 6;

  ctx.save();
  ctx.translate(gx, gy);
  const angle = Math.atan2(dirY, dirX);
  ctx.rotate(angle);

  if (player.currentWeaponKey === "rpg") {
    // RPG tube
    drawPixelRect(-6, -gunThick, gunLen + 8, gunThick * 2, "#555");
    drawPixelRect(gunLen - 2, -gunThick - 2, 6, gunThick * 2 + 4, "#777");
  } else {
    // Stock
    drawPixelRect(-4, -gunThick / 2, 6, gunThick, "#3b2b1b");
    // Barrel
    drawPixelRect(2, -gunThick / 2, gunLen, gunThick, "#444");
    // Magazine
    drawPixelRect(6, gunThick / 2, 4, 6, "#222");
  }

  ctx.restore();
}

// Draw zombie (pixel art) with walk animation
function drawZombie(zx, zy, walkAnim) {
  const size = 22;
  const half = size / 2;
  const baseX = zx - half - camX;
  const baseY = zy - half - camY;

  const legOffset = Math.sin(walkAnim) * 2;

  // Body
  drawPixelRect(baseX + 6, baseY + 8, 10, 12, "#305030");
  // Head
  drawPixelRect(baseX + 6, baseY + 0, 10, 8, "#6fbf6f");
  // Eyes
  drawPixelRect(baseX + 7, baseY + 2, 2, 2, "#000");
  drawPixelRect(baseX + 13, baseY + 2, 2, 2, "#000");
  // Mouth
  drawPixelRect(baseX + 8, baseY + 5, 6, 2, "#900");
  // Arms
  drawPixelRect(baseX + 2, baseY + 10, 4, 8, "#6fbf6f");
  drawPixelRect(baseX + 16, baseY + 10, 4, 8, "#6fbf6f");
  // Legs with animation
  drawPixelRect(baseX + 6, baseY + 20 + legOffset, 4, 6, "#203020");
  drawPixelRect(baseX + 12, baseY + 20 - legOffset, 4, 6, "#203020");
}

// Draw ammo pickup (pixel box)
function drawAmmoPickup(px, py) {
  const size = 16;
  const half = size / 2;
  const x = px - half - camX;
  const y = py - half - camY;

  drawPixelRect(x, y, size, size, "#c9a23b"); // box
  drawPixelRect(x + 2, y + 4, size - 4, 4, "#e6d27a"); // stripe
  drawPixelRect(x + 4, y + 10, size - 8, 3, "#7a5b1b"); // label
}

// Draw food pickup (pixel can)
function drawFoodPickup(px, py) {
  const w = 12;
  const h = 18;
  const x = px - w / 2 - camX;
  const y = py - h / 2 - camY;

  drawPixelRect(x, y, w, h, "#b03030"); // can body
  drawPixelRect(x, y, w, 3, "#d8d8d8"); // top rim
  drawPixelRect(x, y + h - 3, w, 3, "#d8d8d8"); // bottom rim
  drawPixelRect(x + 3, y + 7, 6, 4, "#f5e08a"); // label
}

// Draw weapon pickup (small gun icon)
function drawWeaponPickup(px, py, weaponKey) {
  const x = px - camX;
  const y = py - camY;

  ctx.save();
  ctx.translate(x, y);

  if (weaponKey === "rpg") {
    drawPixelRect(-10, -3, 20, 6, "#555");
    drawPixelRect(6, -5, 6, 10, "#777");
  } else if (weaponKey === "sniper") {
    drawPixelRect(-12, -2, 24, 4, "#444");
    drawPixelRect(-4, -4, 6, 2, "#222");
  } else if (weaponKey === "lmg") {
    drawPixelRect(-10, -3, 20, 6, "#444");
    drawPixelRect(-4, 3, 8, 4, "#222");
  } else if (weaponKey === "m4") {
    drawPixelRect(-8, -2, 16, 4, "#555");
    drawPixelRect(-2, 2, 4, 4, "#222");
  }

  ctx.restore();
}

// Winter background
function drawBackground() {
  ctx.fillStyle = "#e0e5f0";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  // Trees
  trees.forEach(t => {
    const x = t.x - camX;
    const y = t.y - camY;
    if (x < -40 || x > canvas.width + 40 || y < -40 || y > canvas.height + 40) return;

    // trunk
    drawPixelRect(x - 2, y, 4, 10, "#5b4630");
    // snow crown
    drawPixelRect(x - 8, y - 10, 16, 10, "#f5f7fb");
    drawPixelRect(x - 6, y - 16, 12, 6, "#f5f7fb");
  });

  // Buildings
  buildings.forEach(b => {
    const x = b.x - camX;
    const y = b.y - camY;
    if (x > canvas.width || y > canvas.height || x + b.w < 0 || y + b.h < 0) return;

    drawPixelRect(x, y, b.w, b.h, "#c0c4cc");
    drawPixelRect(x, y, b.w, 6, "#a0a4ac");
    // door
    drawPixelRect(x + b.w / 2 - 8, y + b.h - 24, 16, 24, "#4b3b2b");
  });
}

function drawBullets() {
  bullets.forEach(b => {
    const x = b.x - camX;
    const y = b.y - camY;
    if (b.rocket) {
      ctx.fillStyle = "#ffb74d";
      ctx.beginPath();
      ctx.arc(x, y, 5, 0, Math.PI * 2);
      ctx.fill();
    } else {
      ctx.fillStyle = "#f5e08a";
      ctx.fillRect(x - 2, y - 2, 4, 4);
    }
  });
}

function drawPickups() {
  pickups.forEach(p => {
    if (p.type === "ammo") {
      drawAmmoPickup(p.x, p.y);
    } else if (p.type === "food") {
      drawFoodPickup(p.x, p.y);
    } else if (p.type === "weapon") {
      drawWeaponPickup(p.x, p.y, p.weaponKey);
    }
  });
}

function drawBars() {
  const barWidth = 220;
  const barHeight = 12;
  const x = 20;

  // HP
  ctx.fillStyle = "#444";
  ctx.fillRect(x, 60, barWidth, barHeight);
  ctx.fillStyle = "#4caf50";
  ctx.fillRect(x, 60, barWidth * (player.hp / 100), barHeight);
  ctx.fillStyle = "#fff";
  ctx.font = "12px Arial";
  ctx.fillText("HP", x + 4, 70);

  // Hunger
  ctx.fillStyle = "#444";
  ctx.fillRect(x, 80, barWidth, barHeight);
  ctx.fillStyle = "#ffb74d";
  ctx.fillRect(x, 80, barWidth * (player.hunger / 100), barHeight);
  ctx.fillStyle = "#fff";
  ctx.fillText("Hunger", x + 4, 90);

  // Stamina
  ctx.fillStyle = "#444";
  ctx.fillRect(x, 100, barWidth, barHeight);
  ctx.fillStyle = "#64b5f6";
  ctx.fillRect(x, 100, barWidth * (player.stamina / 100), barHeight);
  ctx.fillStyle = "#fff";
  ctx.fillText("Stamina (Shift to sprint)", x + 4, 110);
}

function drawHUD() {
  ctx.fillStyle = "#fff";
  ctx.font = "16px Arial";
  ctx.fillText("DayZ 2D - Winter Survival", 20, 30);

  const w = weapons[player.currentWeaponKey];
  const ammoType = w.ammoType;
  const reserve = player.ammoReserve[ammoType] || 0;
  const ammoText = `${w.name} | Ammo: ${player.ammoInMag}/${w.magSize} | Reserve (${ammoType}): ${reserve}` +
                   (player.reloading ? " (Reloading...)" : "");
  ctx.fillText(ammoText, 20, 50);

  drawBars();
}

function draw() {
  drawBackground();

  // Pickups
  drawPickups();

  // Player
  drawPlayer(player.x, player.y, player.dirX, player.dirY);

  // Zombies
  zombies.forEach(z => {
    drawZombie(z.x, z.y, z.walkAnim);
  });

  // Bullets
  drawBullets();

  // HUD
  drawHUD();
}

function loop(timestamp) {
  if (!running) return;
  const dt = (timestamp - lastTime) / 1000;
  lastTime = timestamp;

  update(dt);
  draw();

  requestAnimationFrame(loop);
}

function startGame() {
  resetGame();
  startScreen.style.display = "none";
  gameOverScreen.style.display = "none";
  running = true;
  requestAnimationFrame(loop);
}

playBtn.onclick = startGame;
restartBtn.onclick = startGame;
</script>
</body>
</html>

Game Source: DayZ 2D - Winter Survival v2

Creator: SparkLegend68

Libraries: none

Complexity: complex (797 lines, 20.2 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: dayz-2d-winter-survival-v2-sparklegend68" to link back to the original. Then publish at arcadelab.ai/publish.