04/09/26

main.js

// Sam's Phaser Side-Scroller (Expanded)

const WORLD_WIDTH = 4200;
const WORLD_HEIGHT = 540;
const PLAYER_SPEED = 260;
const RUN_SPEED = 340;
const JUMP_VELOCITY = -700;

const config = {
  type: Phaser.AUTO,
  parent: "game-container",
  width: 960,
  height: 540,
  backgroundColor: "#87ceeb",
  physics: {
    default: "arcade",
    arcade: {
      gravity: { y: 1600 },
      debug: false
    }
  },
  scene: {
    preload,
    create,
    update
  }
};

const game = new Phaser.Game(config);

let player;
let cursors;
let keys;
let platforms;
let movingPlatforms;
let coins;
let hazards;
let checkpoints;
let goalZone;

let score = 0;
let lives = 3;
let scoreText;
let livesText;
let progressText;
let messageText;

let respawnX = 120;
let respawnY = 420;
let isGameOver = false;
let isWin = false;
let invulnerableUntil = 0;

function preload() {
  // No external assets required.
}

function create() {
  resetRunState();

  this.physics.world.setBounds(0, 0, WORLD_WIDTH, WORLD_HEIGHT);
  this.cameras.main.setBounds(0, 0, WORLD_WIDTH, WORLD_HEIGHT);

  createBackground(this);
  createLevel(this);
  createPlayer(this);
  createCollectibles(this);
  createHazards(this);
  createCheckpoints(this);
  createGoal(this);
  createUI(this);
  setupCollisions(this);
  setupInput(this);

  this.cameras.main.startFollow(player, true, 0.08, 0.08);
  this.cameras.main.setDeadzone(220, 140);

  messageText.setText("Reach the green flag • Collect coins • Avoid red hazards");
  this.time.delayedCall(2200, () => {
    if (!isGameOver && !isWin) {
      messageText.setText("");
    }
  });
}

function update() {
  if ((isGameOver || isWin) && Phaser.Input.Keyboard.JustDown(keys.R)) {
    this.scene.restart();
    return;
  }

  if (!player || !player.body) return;

  updateMovingPlatforms();
  updateHUD();

  if (isGameOver || isWin) return;

  const moveLeft = cursors.left.isDown || keys.A.isDown;
  const moveRight = cursors.right.isDown || keys.D.isDown;
  const speed = keys.SHIFT.isDown ? RUN_SPEED : PLAYER_SPEED;

  if (moveLeft && !moveRight) {
    player.body.setVelocityX(-speed);
  } else if (moveRight && !moveLeft) {
    player.body.setVelocityX(speed);
  } else {
    player.body.setVelocityX(0);
  }

  const jumpPressed =
    Phaser.Input.Keyboard.JustDown(cursors.up) ||
    Phaser.Input.Keyboard.JustDown(cursors.space) ||
    Phaser.Input.Keyboard.JustDown(keys.W);

  const onGround = player.body.blocked.down || player.body.touching.down;
  if (jumpPressed && onGround) {
    player.body.setVelocityY(JUMP_VELOCITY);
  }

  if (this.time.now < invulnerableUntil) {
    player.setAlpha(Math.floor(this.time.now / 70) % 2 ? 0.35 : 1);
  } else {
    player.setAlpha(1);
  }
}

function resetRunState() {
  score = 0;
  lives = 3;
  respawnX = 120;
  respawnY = 420;
  isGameOver = false;
  isWin = false;
  invulnerableUntil = 0;
}

function createBackground(scene) {
  const sky = scene.add.rectangle(
    WORLD_WIDTH / 2,
    WORLD_HEIGHT / 2,
    WORLD_WIDTH,
    WORLD_HEIGHT,
    0x87ceeb
  );
  sky.setDepth(-50);

  const sun = scene.add.circle(220, 100, 52, 0xffec99);
  sun.setScrollFactor(0.08);
  sun.setDepth(-49);

  for (let i = 0; i < 16; i++) {
    const hill = scene.add.ellipse(i * 300 + 120, 470, 420, 220, 0x6c8cab);
    hill.setScrollFactor(0.35);
    hill.setDepth(-45);
  }

  for (let i = 0; i < 18; i++) {
    const hill = scene.add.ellipse(i * 250 + 80, 500, 320, 160, 0x5b7894);
    hill.setScrollFactor(0.55);
    hill.setDepth(-44);
  }

  for (let i = 0; i < 28; i++) {
    const cloudX = Phaser.Math.Between(80, WORLD_WIDTH - 80);
    const cloudY = Phaser.Math.Between(60, 210);
    const cloud = scene.add.ellipse(cloudX, cloudY, 90, 42, 0xffffff, 0.85);
    cloud.setScrollFactor(0.18);
    cloud.setDepth(-40);
  }

  scene.add.text(42, 438, "START", {
    fontFamily: "Arial Black",
    fontSize: "28px",
    color: "#1f2937"
  }).setDepth(9);

  scene.add.text(WORLD_WIDTH - 250, 138, "FINISH", {
    fontFamily: "Arial Black",
    fontSize: "28px",
    color: "#14532d"
  }).setDepth(9);
}

function createLevel(scene) {
  platforms = scene.physics.add.staticGroup();
  movingPlatforms = scene.physics.add.group({
    allowGravity: false,
    immovable: true
  });

  // Ground
  for (let x = 200; x <= WORLD_WIDTH - 200; x += 400) {
    addPlatform(scene, x, WORLD_HEIGHT - 20, 400, 40, 0x4e5d6c);
  }

  // Static floating platforms
  const staticPlatforms = [
    [420, 430, 180, 24],
    [700, 360, 180, 24],
    [980, 300, 180, 24],
    [1320, 260, 180, 24],
    [1700, 330, 200, 24],
    [2050, 280, 180, 24],
    [2360, 240, 180, 24],
    [2670, 280, 180, 24],
    [2980, 340, 200, 24],
    [3320, 290, 180, 24],
    [3660, 240, 180, 24]
  ];

  staticPlatforms.forEach(([x, y, w, h]) => {
    addPlatform(scene, x, y, w, h, 0x6b7280);
  });

  // Moving platforms
  addMovingPlatform(scene, 1520, 390, 140, 20, "x", 160, 95, 0x7d8597);
  addMovingPlatform(scene, 2510, 320, 140, 20, "y", 110, 75, 0x7d8597);
  addMovingPlatform(scene, 3490, 350, 130, 20, "x", 130, 85, 0x7d8597);
}

function createPlayer(scene) {
  player = scene.add.rectangle(respawnX, respawnY, 34, 50, 0x4aa3ff);
  player.setDepth(10);

  scene.physics.add.existing(player);

  player.body.setCollideWorldBounds(true);
  player.body.setMaxVelocity(500, 1200);
}

function createCollectibles(scene) {
  coins = scene.physics.add.group({
    allowGravity: false,
    immovable: true
  });

  const coinPositions = [
    [420, 390], [700, 320], [980, 260], [1320, 220],
    [1700, 290], [2050, 240], [2360, 200], [2670, 240],
    [2980, 300], [3320, 250], [3660, 200],
    [1540, 350], [2510, 270], [3490, 310],
    [1150, 455], [2250, 455], [3150, 455]
  ];

  coinPositions.forEach(([x, y]) => {
    addCoin(scene, x, y);
  });
}

function createHazards(scene) {
  hazards = scene.physics.add.staticGroup();

  const hazardBlocks = [
    [640, 490, 80, 20],
    [1160, 490, 100, 20],
    [1880, 490, 120, 20],
    [2250, 490, 90, 20],
    [2860, 490, 120, 20],
    [3480, 490, 100, 20],
    [1320, 248, 52, 12],
    [2360, 228, 52, 12]
  ];

  hazardBlocks.forEach(([x, y, w, h]) => {
    addHazard(scene, x, y, w, h);
  });
}

function createCheckpoints(scene) {
  checkpoints = scene.physics.add.staticGroup();

  addCheckpoint(scene, 1450, 460);
  addCheckpoint(scene, 3050, 460);
}

function createGoal(scene) {
  const pole = scene.add.rectangle(WORLD_WIDTH - 150, 355, 12, 290, 0x2f855a);
  pole.setDepth(8);

  const flag = scene.add.rectangle(WORLD_WIDTH - 115, 250, 72, 40, 0x00e676);
  flag.setDepth(8);

  goalZone = scene.add.rectangle(WORLD_WIDTH - 120, 355, 85, 290, 0x00e676, 0.2);
  goalZone.setDepth(7);
  scene.physics.add.existing(goalZone, true);
}

function createUI(scene) {
  const commonStyle = {
    fontFamily: "Arial",
    fontSize: "20px",
    color: "#ffffff",
    stroke: "#000000",
    strokeThickness: 4
  };

  scoreText = scene.add.text(16, 12, "Score: 0", commonStyle);
  livesText = scene.add.text(16, 40, "Lives: 3", commonStyle);

  progressText = scene.add.text(944, 12, "Progress: 0%", {
    ...commonStyle,
    fontSize: "18px"
  }).setOrigin(1, 0);

  scene.add.text(16, 510, "Move: Arrows or A/D • Jump: Up/W/Space • Run: Shift • Restart: R", {
    fontFamily: "Arial",
    fontSize: "15px",
    color: "#ffffff",
    stroke: "#000000",
    strokeThickness: 3
  });

  messageText = scene.add.text(480, 76, "", {
    fontFamily: "Arial Black",
    fontSize: "24px",
    color: "#ffffff",
    align: "center",
    stroke: "#000000",
    strokeThickness: 5
  }).setOrigin(0.5, 0);

  scoreText.setScrollFactor(0).setDepth(100);
  livesText.setScrollFactor(0).setDepth(100);
  progressText.setScrollFactor(0).setDepth(100);
  messageText.setScrollFactor(0).setDepth(100);
}

function setupCollisions(scene) {
  scene.physics.add.collider(player, platforms);
  scene.physics.add.collider(player, movingPlatforms);
  scene.physics.add.collider(player, hazards, () => loseLife(scene), null, scene);

  scene.physics.add.overlap(player, coins, collectCoin, null, scene);
  scene.physics.add.overlap(player, checkpoints, activateCheckpoint, null, scene);
  scene.physics.add.overlap(player, goalZone, reachGoal, null, scene);
}

function setupInput(scene) {
  cursors = scene.input.keyboard.createCursorKeys();
  keys = scene.input.keyboard.addKeys("W,A,D,SHIFT,R");
}

function updateMovingPlatforms() {
  movingPlatforms.children.iterate((platform) => {
    if (!platform || !platform.body) return;

    if (platform.axis === "x") {
      if (platform.x >= platform.start + platform.range) {
        platform.body.setVelocityX(-platform.speed);
      } else if (platform.x <= platform.start - platform.range) {
        platform.body.setVelocityX(platform.speed);
      }
    } else if (platform.axis === "y") {
      if (platform.y >= platform.start + platform.range) {
        platform.body.setVelocityY(-platform.speed);
      } else if (platform.y <= platform.start - platform.range) {
        platform.body.setVelocityY(platform.speed);
      }
    }
  });
}

function updateHUD() {
  const progress = Phaser.Math.Clamp((player.x / (WORLD_WIDTH - 160)) * 100, 0, 100);
  progressText.setText(`Progress: ${Math.round(progress)}%`);
}

function collectCoin(playerObj, coin) {
  if (!coin.active || isGameOver || isWin) return;

  coin.destroy();
  score += 10;
  scoreText.setText(`Score: ${score}`);

  if (coins.countActive(true) === 0) {
    score += 100;
    scoreText.setText(`Score: ${score}`);
    messageText.setText("All coins collected! +100 bonus");
    playerObj.scene.time.delayedCall(1200, () => {
      if (!isGameOver && !isWin) {
        messageText.setText("");
      }
    });
  }
}

function activateCheckpoint(playerObj, checkpoint) {
  if (checkpoint.activated || isGameOver || isWin) return;

  checkpoint.activated = true;
  checkpoint.setFillStyle(0x2ecc71);

  respawnX = checkpoint.x;
  respawnY = checkpoint.y - 90;

  messageText.setText("Checkpoint reached!");
  playerObj.scene.time.delayedCall(900, () => {
    if (!isGameOver && !isWin) {
      messageText.setText("");
    }
  });
}

function loseLife(scene) {
  if (isGameOver || isWin) return;
  if (scene.time.now < invulnerableUntil) return;

  lives -= 1;
  livesText.setText(`Lives: ${lives}`);
  scene.cameras.main.shake(160, 0.006);

  if (lives <= 0) {
    isGameOver = true;
    scene.physics.world.pause();
    player.setFillStyle(0xff6464);
    player.setAlpha(1);
    messageText.setText("Game Over\nPress R to Restart");
    return;
  }

  invulnerableUntil = scene.time.now + 1400;
  player.body.stop();
  player.setPosition(respawnX, respawnY);

  messageText.setText("Ouch! Respawning...");
  scene.time.delayedCall(900, () => {
    if (!isGameOver && !isWin) {
      messageText.setText("");
    }
  });
}

function reachGoal(playerObj) {
  if (isGameOver || isWin) return;

  const scene = playerObj.scene;
  isWin = true;
  score += 250;
  scoreText.setText(`Score: ${score}`);

  scene.physics.world.pause();
  player.setFillStyle(0x7cfc00);
  player.setAlpha(1);

  messageText.setText("You Win! 🎉\nPress R to Play Again");
}

function addPlatform(scene, x, y, w, h, color = 0x6b7280) {
  const platform = scene.add.rectangle(x, y, w, h, color);
  platform.setDepth(6);
  scene.physics.add.existing(platform, true);
  platforms.add(platform);
  return platform;
}

function addMovingPlatform(scene, x, y, w, h, axis, range, speed, color = 0x7d8597) {
  const platform = scene.add.rectangle(x, y, w, h, color);
  platform.setDepth(6);

  scene.physics.add.existing(platform);
  platform.body.setAllowGravity(false);
  platform.body.setImmovable(true);

  platform.axis = axis;
  platform.range = range;
  platform.speed = speed;
  platform.start = axis === "x" ? x : y;

  if (axis === "x") {
    platform.body.setVelocityX(speed);
  } else {
    platform.body.setVelocityY(speed);
  }

  movingPlatforms.add(platform);
  return platform;
}

function addCoin(scene, x, y) {
  const coin = scene.add.circle(x, y, 10, 0xffd166);
  coin.setDepth(11);

  scene.physics.add.existing(coin);
  coin.body.setAllowGravity(false);
  coin.body.setImmovable(true);

  coins.add(coin);
  return coin;
}

function addHazard(scene, x, y, w, h) {
  const hazard = scene.add.rectangle(x, y, w, h, 0xe63946);
  hazard.setDepth(7);

  scene.physics.add.existing(hazard, true);
  hazards.add(hazard);
  return hazard;
}

function addCheckpoint(scene, x, y) {
  const checkpoint = scene.add.rectangle(x, y, 18, 80, 0x00bcd4);
  checkpoint.setDepth(8);
  checkpoint.activated = false;

  scene.physics.add.existing(checkpoint, true);
  checkpoints.add(checkpoint);
  return checkpoint;
}

Index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Sam's Phaser Side-Scroller</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="stylesheet" href="./style.css" />
</head>
<body>
  <main class="page">
    <h1>Sam's Phaser Side-Scroller</h1>
    <p class="subtitle">
      Move: Arrow Keys or A/D • Jump: Up/W/Space • Run: Shift • Restart: R
    </p>
    <div id="game-container"></div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/phaser@3.80.1/dist/phaser.min.js"></script>
  <script src="./main.js"></script>
</body>
</html>

style.css

:root {
  --bg-top: #0b1020;
  --bg-bottom: #111827;
  --panel: #0f172a;
  --border: #334155;
  --text: #e5e7eb;
  --muted: #94a3b8;
}

* {
  box-sizing: border-box;
}

html,
body {
  width: 100%;
  height: 100%;
  margin: 0;
}

body {
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    radial-gradient(circle at 20% 15%, #1f2937 0%, transparent 45%),
    radial-gradient(circle at 80% 85%, #111827 0%, transparent 45%),
    linear-gradient(160deg, var(--bg-top), var(--bg-bottom));
  color: var(--text);
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}

.page {
  width: min(1100px, 100%);
  padding: 16px;
  text-align: center;
}

.page h1 {
  margin: 0 0 6px;
  font-size: clamp(1.15rem, 2.4vw, 1.8rem);
  letter-spacing: 0.3px;
}

.subtitle {
  margin: 0 0 12px;
  color: var(--muted);
  font-size: 0.95rem;
}

#game-container {
  display: inline-block;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: linear-gradient(180deg, #0a1224, #0a0f1b);
  box-shadow:
    0 20px 45px rgba(0, 0, 0, 0.45),
    inset 0 0 0 1px rgba(255, 255, 255, 0.03);
}

#game-container canvas {
  display: block;
  width: min(960px, 92vw);
  height: auto;
  border-radius: 10px;
  border: 2px solid #1f2937;
}

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *