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;
}