First Commit

This commit is contained in:
Brian Fertig 2026-05-09 10:55:56 -06:00
commit b6002fed00
18 changed files with 1696 additions and 0 deletions

73
CLAUDE.md Normal file
View File

@ -0,0 +1,73 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Running the Game
No bundler required. Serve with any local HTTP server and open `http://localhost:8080`:
```bash
python3 -m http.server 8080
# or
npx serve .
```
## Architecture
### Entry Point & Scene Flow
`index.html` loads `src/main.js` as an ES6 module. Phaser is loaded via CDN (no bundler). Scene flow:
1. `MainMenu` — title screen, starts game
2. `GameManager` — reads `config/game.json`, randomly selects N mini-games for the current level, sequences through them, and manages global state (lives, current level)
3. Each mini-game scene — runs its level, then signals completion or failure back to `GameManager`
### Mini-Game Contract
Each mini-game is a self-contained Phaser `Scene` exported from its subdirectory. Every mini-game must implement the same interface so `GameManager` can plug it in without knowing its internals:
- Accept a `onComplete` and `onFail` callback (passed via Phaser scene data or events)
- Emit/call `onComplete` when the level is beaten; `onFail` when a life is lost
To add a new mini-game: create a subdirectory under `src/games/`, implement the Scene contract, and register it in `config/game.json`.
### Directory Structure
```
index.html
config/
game.json # level-to-game-count mapping, lives, active game pool
src/
main.js # Phaser game init, registers all scenes
scenes/
MainMenu.js
GameManager.js # sequences mini-games, tracks lives/level
games/
colorado-defense/
ColoradoDefense.js
config.json
code-bug-invaders/
CodeBugInvaders.js
config.json
dot-dude/
DotDude.js
config.json
smash-out/
SmashOut.js
config.json
```
### Configuration
- `config/game.json` — top-level settings: number of games per level, lives count, which games are in the pool
- `src/games/<name>/config.json` — per-game tuning: speeds, enemy counts, timing, etc.
## Tech Stack & Constraints
- **Phaser 3.90** — loaded via CDN, not npm
- **Vanilla JS ES6 modules**`import`/`export` only, no webpack/Vite/bundler
- **Canvas:** 1600×900 with `Phaser.Scale.FIT` for responsive scaling
- **Graphics:** Vector graphics only during initial development (no sprite sheets yet)
- **Controls:** Arrow keys + `A` to fire for all games; mouse for aiming in Colorado Defense only
- **Lives:** 3 lives global; game over on third loss
- **Level 1:** 3 randomly selected games; **Level 2:** 4 randomly selected games

8
config/game.json Normal file
View File

@ -0,0 +1,8 @@
{
"lives": 3,
"levels": {
"1": { "gameCount": 3 },
"2": { "gameCount": 4 }
},
"gamePool": ["ColoradoDefense", "CodeBugInvaders", "DotDude", "SmashOut"]
}

44
game.md Normal file
View File

@ -0,0 +1,44 @@
# Build Guidelines
Create an HTML Phaser 3.90 video game. The game will be called "Attention Retro Disorder" and will be a game that will feature several different sub-games that are all retro themed. We will start with 4 sub-games, and add on to this over time. The idea with the game will be that each sub-game will feature a short level which can be completed in under 2 minutes. After completing one of the sub-game levels the player will move onto another sub-game. Once all sub-game levels of the current level are completed, the player will Level up and move on to level 2 of the games. I want to be able to control and adjust most of the settings for the parent game and the sub games via JSON files. I would like the directory structure to have a sub-folder for each sub-game with json files I can adjust in them. Each game will be developed independently and I would like to be able to add games in the future as well, and bolt them onto the parent game easily. At the beginning of each level (starting with level 1) randomly select X number (configurable per level) of the game from the game pool. 1 level of that game will need to be completed before moving onto the next level. For all games, in the initial development of this project, use temporary vector graphics or temporary sprites (whichever is most applicable per game)
## Platform
- Phaser version 3.90 HTML game
- Use JavaScript
- Have JavaScript objects reference each other directly via IMPORT and EXPORT using ES6 standards
- Do **NOT** require a web packager.
- Create files and classes in a manner that allows future modifications and scaling at a modular level
## Basic Framework
- 1600 x 900 view
- Scale view to user's viewport.
## Gameplay
- Use Vector Graphics for all games initially
### Initial Games to develop
- Missile Command like game called "Colorado Defense"
- Space Invaders like game called "Code Bug Invaders"
- Pac Man like game called "Dot Dude"
- Akanoid like game called "Smash Out"
## Controls
- Arrow keys + 'A' to fire
- Mouse controls for aiming in "Colorado Defense" game
## Mechanics
- Three Lives -- Once three lives are lost game over
- Main Menu Screen
- Leveling
- Level 1
- 3 randomly selected games
- Level 2
- 4 randomly selected games
More Development will take place on this game after an initial structure is set up.

15
index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Attention Retro Disorder</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; }
</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.90.0/dist/phaser.min.js"></script>
<script type="module" src="src/main.js"></script>
</body>
</html>

View File

@ -0,0 +1,288 @@
export default class CodeBugInvaders extends Phaser.Scene {
constructor() { super({ key: 'CodeBugInvaders' }); }
preload() {
this.load.json('cbiConfig', 'src/games/code-bug-invaders/config.json');
}
init(data) {
this.globalLives = data.lives;
this.gameActive = false;
}
create() {
this.cfg = this.cache.json.get('cbiConfig');
this.bugs = [];
this.playerBullets = [];
this.bugBullets = [];
this.gameActive = true;
this.formationDir = 1; // 1 = right, -1 = left
this.formationX = 0;
this.formationY = 0;
this.bugFireTimer = this.cfg.bugFireInterval;
this._buildBackground();
this._buildBugs();
this._buildPlayer();
this.mainGfx = this.add.graphics();
this._buildHUD();
this._setupInput();
}
shutdown() {
// input listeners auto-cleaned by Phaser scene shutdown
}
_buildBackground() {
const bg = this.add.graphics();
bg.fillStyle(0x000011);
bg.fillRect(0, 0, 1600, 900);
for (let i = 0; i < 150; i++) {
bg.fillStyle(0xffffff, Math.random() * 0.7 + 0.1);
bg.fillRect(Phaser.Math.Between(0, 1599), Phaser.Math.Between(0, 899), 2, 2);
}
this.add.text(800, 15, 'CODE BUG INVADERS', {
fontSize: '20px', fontFamily: 'monospace', color: '#00ff88', alpha: 0.7,
}).setOrigin(0.5, 0);
}
_buildBugs() {
const { cols, rows, bugSpacingX, bugSpacingY, startX, startY } = this.cfg;
const colors = [0xff2222, 0xff8800, 0xffff00, 0x00ff44];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
this.bugs.push({
col: c, row: r,
x: startX + c * bugSpacingX,
y: startY + r * bugSpacingY,
alive: true,
color: colors[r % colors.length],
frame: 0,
});
}
}
}
_buildPlayer() {
this.player = { x: 800, y: 845, alive: true };
}
_buildHUD() {
this.livesText = this.add.text(800, 40, '♥ '.repeat(this.globalLives).trim(), {
fontSize: '24px', fontFamily: 'monospace', color: '#ff4444',
}).setOrigin(0.5, 0);
this.bugsText = this.add.text(1580, 40, '', {
fontSize: '22px', fontFamily: 'monospace', color: '#00ff88',
}).setOrigin(1, 0);
}
_setupInput() {
this.cursors = this.input.keyboard.createCursorKeys();
this.keyA = this.input.keyboard.addKey('A');
this.keyAPressed = false;
}
update(time, delta) {
if (!this.gameActive) return;
const dt = delta / 1000;
this._movePlayer(dt);
this._moveFormation(dt);
this._moveProjectiles(dt);
this._handleBugFire(delta);
this._checkCollisions();
this._checkWinLose();
this._draw();
}
_movePlayer(dt) {
if (!this.player.alive) return;
const speed = this.cfg.playerSpeed;
if (this.cursors.left.isDown) this.player.x -= speed * dt;
if (this.cursors.right.isDown) this.player.x += speed * dt;
this.player.x = Phaser.Math.Clamp(this.player.x, 50, 1550);
if (this.keyA.isDown && !this.keyAPressed) {
this.keyAPressed = true;
// Limit to 2 bullets at once
if (this.playerBullets.length < 2) {
this.playerBullets.push({ x: this.player.x, y: this.player.y - 20 });
}
}
if (!this.keyA.isDown) this.keyAPressed = false;
}
_moveFormation(dt) {
const aliveBugs = this.bugs.filter(b => b.alive);
if (aliveBugs.length === 0) return;
// Speed scales up as fewer bugs remain
const speedScale = 1 + (1 - aliveBugs.length / (this.cfg.cols * this.cfg.rows)) * 2.5;
const move = this.cfg.formationSpeed * speedScale * dt * this.formationDir;
// Check bounds first
const { rightBound, leftBound } = this.cfg;
let minX = Infinity, maxX = -Infinity;
aliveBugs.forEach(b => { minX = Math.min(minX, b.x); maxX = Math.max(maxX, b.x); });
if (maxX + move > rightBound || minX + move < leftBound) {
// Step down, reverse
this.formationDir *= -1;
aliveBugs.forEach(b => { b.y += this.cfg.formationStepDown; });
} else {
aliveBugs.forEach(b => { b.x += move; });
}
}
_moveProjectiles(dt) {
const pbSpeed = this.cfg.playerBulletSpeed * dt;
this.playerBullets = this.playerBullets.filter(b => {
b.y -= pbSpeed;
return b.y > -10;
});
const bbSpeed = this.cfg.bugBulletSpeed * dt;
this.bugBullets = this.bugBullets.filter(b => {
b.y += bbSpeed;
return b.y < 920;
});
}
_handleBugFire(delta) {
this.bugFireTimer -= delta;
if (this.bugFireTimer <= 0) {
this.bugFireTimer = this.cfg.bugFireInterval * (0.7 + Math.random() * 0.6);
// Pick a random bug from the bottom of each column
const cols = {};
this.bugs.filter(b => b.alive).forEach(b => {
if (!cols[b.col] || b.row > cols[b.col].row) cols[b.col] = b;
});
const shooters = Object.values(cols);
if (shooters.length > 0) {
const shooter = Phaser.Utils.Array.GetRandom(shooters);
this.bugBullets.push({ x: shooter.x, y: shooter.y + 20 });
}
}
}
_checkCollisions() {
// Player bullets vs bugs
for (let bi = this.playerBullets.length - 1; bi >= 0; bi--) {
const bullet = this.playerBullets[bi];
let hit = false;
for (const bug of this.bugs) {
if (!bug.alive) continue;
if (Math.abs(bullet.x - bug.x) < 28 && Math.abs(bullet.y - bug.y) < 22) {
bug.alive = false;
hit = true;
break;
}
}
if (hit) this.playerBullets.splice(bi, 1);
}
// Bug bullets vs player
if (this.player.alive) {
for (const bb of this.bugBullets) {
if (Math.abs(bb.x - this.player.x) < 30 && Math.abs(bb.y - this.player.y) < 25) {
this.player.alive = false;
this._endGame(false);
return;
}
}
}
}
_checkWinLose() {
const alive = this.bugs.filter(b => b.alive);
if (alive.length === 0) { this._endGame(true); return; }
// Bugs reach danger line
if (alive.some(b => b.y >= this.cfg.dangerY)) {
this._endGame(false);
}
}
_endGame(success) {
if (!this.gameActive) return;
this.gameActive = false;
const msg = success ? 'BUGS SQUASHED!' : 'BUGS WIN!';
const color = success ? '#00ff44' : '#ff2222';
this.add.text(800, 440, msg, {
fontSize: '72px', fontFamily: 'monospace',
color, stroke: '#000000', strokeThickness: 5,
}).setOrigin(0.5).setDepth(20);
this.time.delayedCall(2200, () => {
this.game.events.emit('miniGameResult', { success });
});
}
_drawBug(g, x, y, color, frame) {
// Simple bug shape: body + 6 legs + antenna
g.fillStyle(color);
g.fillEllipse(x, y, 36, 26);
// Head
g.fillCircle(x, y - 16, 10);
// Antennae
g.lineStyle(2, color);
g.lineBetween(x - 4, y - 24, x - 10, y - 34);
g.lineBetween(x + 4, y - 24, x + 10, y - 34);
// Legs (animated: offset based on frame)
const legOff = (frame % 2 === 0) ? 4 : -4;
g.lineBetween(x - 18, y - 4 + legOff, x - 30, y - 8);
g.lineBetween(x - 18, y + 4 - legOff, x - 30, y + 8);
g.lineBetween(x + 18, y - 4 + legOff, x + 30, y - 8);
g.lineBetween(x + 18, y + 4 - legOff, x + 30, y + 8);
// Eyes
g.fillStyle(0x000000);
g.fillCircle(x - 4, y - 16, 3);
g.fillCircle(x + 4, y - 16, 3);
g.fillStyle(0xffffff);
g.fillCircle(x - 3, y - 17, 1);
g.fillCircle(x + 5, y - 17, 1);
}
_draw() {
const g = this.mainGfx;
g.clear();
const tick = Math.floor(this.time.now / 300);
// Bugs
this.bugs.forEach(b => {
if (!b.alive) return;
this._drawBug(g, b.x, b.y, b.color, tick);
});
// Player ship
if (this.player.alive) {
const { x, y } = this.player;
g.fillStyle(0x00ff88);
g.fillTriangle(x, y - 30, x - 30, y + 15, x + 30, y + 15);
g.fillStyle(0x00aa55);
g.fillRect(x - 35, y + 10, 70, 12);
g.fillStyle(0x88ffcc);
g.fillRect(x - 6, y - 28, 12, 30);
}
// Ground line for player area
g.lineStyle(2, 0x00ff44, 0.3);
g.lineBetween(0, 870, 1600, 870);
// Player bullets
g.fillStyle(0xffffff);
this.playerBullets.forEach(b => g.fillRect(b.x - 2, b.y - 8, 4, 16));
// Bug bullets
g.fillStyle(0xff4400);
this.bugBullets.forEach(b => g.fillRect(b.x - 3, b.y - 10, 6, 18));
// Danger line
g.lineStyle(1, 0xff0000, 0.3);
g.lineBetween(0, this.cfg.dangerY, 1600, this.cfg.dangerY);
// HUD update
const aliveCount = this.bugs.filter(b => b.alive).length;
this.bugsText.setText(`BUGS: ${aliveCount}`);
}
}

View File

@ -0,0 +1,17 @@
{
"cols": 10,
"rows": 4,
"bugSpacingX": 120,
"bugSpacingY": 70,
"startX": 180,
"startY": 100,
"formationSpeed": 60,
"formationStepDown": 30,
"rightBound": 1540,
"leftBound": 60,
"bugFireInterval": 1800,
"bugBulletSpeed": 280,
"playerSpeed": 420,
"playerBulletSpeed": 700,
"dangerY": 720
}

View File

@ -0,0 +1,302 @@
export default class ColoradoDefense extends Phaser.Scene {
constructor() { super({ key: 'ColoradoDefense' }); }
preload() {
this.load.json('cdConfig', 'src/games/colorado-defense/config.json');
}
init(data) {
this.globalLives = data.lives;
this.gameActive = false;
}
create() {
this.cfg = this.cache.json.get('cdConfig');
this.cities = [];
this.missiles = [];
this.interceptors = [];
this.explosions = [];
this.wave = 0;
this.spawnTimer = 0;
this.waveSpawned = 0;
this.waveCleared = false;
this.gameActive = true;
this.mouseX = 800;
this.mouseY = 400;
this._buildBackground();
this._buildCities();
this.mainGfx = this.add.graphics();
this._buildHUD();
this._setupInput();
this._nextWave();
}
shutdown() {
this.input.off('pointerdown');
this.input.off('pointermove');
}
_buildBackground() {
const bg = this.add.graphics();
// Starfield
for (let i = 0; i < 120; i++) {
const alpha = Math.random() * 0.6 + 0.1;
bg.fillStyle(0xffffff, alpha);
bg.fillRect(Phaser.Math.Between(0, 1599), Phaser.Math.Between(0, 860), 2, 2);
}
// Ground
bg.fillStyle(0x003300);
bg.fillRect(0, 865, 1600, 35);
bg.lineStyle(2, 0x00bb00);
bg.lineBetween(0, 865, 1600, 865);
// Title banner
this.add.text(800, 15, 'COLORADO DEFENSE', {
fontSize: '20px', fontFamily: 'monospace', color: '#00ff00', alpha: 0.7,
}).setOrigin(0.5, 0);
}
_buildCities() {
const xs = [110, 300, 490, 1110, 1300, 1490];
xs.forEach((x, i) => {
this.cities.push({ x, y: 865, alive: true, index: i });
});
}
_buildHUD() {
this.waveText = this.add.text(20, 40, 'WAVE 1', {
fontSize: '24px', fontFamily: 'monospace', color: '#00ff00',
});
this.livesText = this.add.text(800, 40, '♥ '.repeat(this.globalLives).trim(), {
fontSize: '24px', fontFamily: 'monospace', color: '#ff4444',
}).setOrigin(0.5, 0);
this.statusText = this.add.text(1580, 40, 'CITIES: 6/6', {
fontSize: '24px', fontFamily: 'monospace', color: '#4488ff',
}).setOrigin(1, 0);
}
_setupInput() {
this.input.on('pointermove', p => { this.mouseX = p.x; this.mouseY = p.y; });
this.input.on('pointerdown', p => this._fire(p.x, p.y));
this.keyA = this.input.keyboard.addKey('A');
this.keyAPressed = false;
}
_nextWave() {
this.wave++;
if (this.wave > this.cfg.waves.length) {
this._endGame(true);
return;
}
this.currentWave = this.cfg.waves[this.wave - 1];
this.waveSpawned = 0;
this.waveCleared = false;
this.spawnTimer = 800; // small delay before first spawn
this.waveText.setText(`WAVE ${this.wave}`);
const waveLabel = this.add.text(800, 450, `WAVE ${this.wave}`, {
fontSize: '72px', fontFamily: 'monospace', color: '#ffff00',
stroke: '#885500', strokeThickness: 4,
}).setOrigin(0.5).setDepth(10);
this.time.delayedCall(1200, () => waveLabel.destroy());
}
_fire(tx, ty) {
if (!this.gameActive || ty >= 855) return;
this.interceptors.push({
sx: 800, sy: 855,
tx, ty,
dx: tx - 800, dy: ty - 855,
dist: Math.max(1, Math.hypot(tx - 800, ty - 855)),
progress: 0,
speed: this.cfg.interceptorSpeed,
});
}
update(time, delta) {
if (!this.gameActive) return;
const dt = delta / 1000;
if (this.keyA.isDown && !this.keyAPressed) {
this.keyAPressed = true;
this._fire(this.mouseX, this.mouseY);
}
if (!this.keyA.isDown) this.keyAPressed = false;
// Spawn enemy missiles
if (this.waveSpawned < this.currentWave.count) {
this.spawnTimer -= delta;
if (this.spawnTimer <= 0) {
this._spawnMissile();
this.waveSpawned++;
this.spawnTimer = this.currentWave.spawnInterval;
}
}
// Move interceptors
this.interceptors = this.interceptors.filter(i => {
i.progress += (i.speed / i.dist) * dt;
i.x = i.sx + i.dx * i.progress;
i.y = i.sy + i.dy * i.progress;
if (i.progress >= 1) {
this.explosions.push({
x: i.tx, y: i.ty, r: 0,
maxR: this.cfg.explosionRadius,
growing: true,
});
return false;
}
return true;
});
// Move enemy missiles
this.missiles = this.missiles.filter(m => {
if (m.dead) return false;
m.progress += (m.speed / m.dist) * dt;
m.x = m.sx + m.dx * m.progress;
m.y = m.sy + m.dy * m.progress;
if (m.y >= 865) {
this._cityHit(m.x);
return false;
}
return true;
});
// Update explosions + kill missiles inside
this.explosions = this.explosions.filter(e => {
if (e.growing) {
e.r += e.maxR * 2.8 * dt;
if (e.r >= e.maxR) e.growing = false;
} else {
e.r -= e.maxR * 2.2 * dt;
}
this.missiles.forEach(m => {
if (!m.dead && Math.hypot(m.x - e.x, m.y - e.y) < e.r) m.dead = true;
});
return e.r > 0;
});
// Check wave clear
if (!this.waveCleared &&
this.waveSpawned >= this.currentWave.count &&
this.missiles.length === 0 &&
this.interceptors.length === 0) {
this.waveCleared = true;
this.time.delayedCall(1800, () => this._nextWave());
}
// Check city deaths
const dead = this.cities.filter(c => !c.alive).length;
if (dead >= this.cfg.maxCityDeaths) this._endGame(false);
this.statusText.setText(`CITIES: ${6 - dead}/6`);
this._draw();
}
_spawnMissile() {
const target = this._pickTarget();
const sx = Phaser.Math.Between(80, 1520);
const sy = -10;
const dist = Math.max(1, Math.hypot(target.x - sx, target.y - sy));
this.missiles.push({
sx, sy, dx: target.x - sx, dy: target.y - sy,
dist, speed: this.currentWave.speed,
progress: 0, x: sx, y: sy, dead: false,
});
}
_pickTarget() {
const alive = this.cities.filter(c => c.alive);
if (alive.length > 0 && Math.random() < 0.65) {
return Phaser.Utils.Array.GetRandom(alive);
}
return { x: Phaser.Math.Between(100, 1500), y: 865 };
}
_cityHit(mx) {
for (const c of this.cities) {
if (c.alive && Math.abs(c.x - mx) < 70) {
c.alive = false;
// Explosion on city
this.explosions.push({ x: c.x, y: c.y - 15, r: 5, maxR: 50, growing: true });
break;
}
}
}
_endGame(success) {
if (!this.gameActive) return;
this.gameActive = false;
const msg = success ? 'COLORADO DEFENDED!' : 'CITIES DESTROYED!';
const color = success ? '#00ff00' : '#ff2200';
this.add.text(800, 440, msg, {
fontSize: '64px', fontFamily: 'monospace',
color, stroke: '#000000', strokeThickness: 5,
}).setOrigin(0.5).setDepth(20);
this.time.delayedCall(2200, () => {
this.game.events.emit('miniGameResult', { success });
});
}
_draw() {
const g = this.mainGfx;
g.clear();
// Turret
g.fillStyle(0x00ff00);
g.fillTriangle(785, 865, 815, 865, 800, 840);
g.fillStyle(0x00aa00);
g.fillRect(794, 850, 12, 15);
// Crosshair
g.lineStyle(1, 0x00ff00, 0.6);
g.strokeCircle(this.mouseX, this.mouseY, 16);
g.lineBetween(this.mouseX - 22, this.mouseY, this.mouseX + 22, this.mouseY);
g.lineBetween(this.mouseX, this.mouseY - 22, this.mouseX, this.mouseY + 22);
// Cities
this.cities.forEach(c => {
if (c.alive) {
g.fillStyle(0x0055ff);
g.fillRect(c.x - 30, c.y - 30, 60, 30);
g.fillStyle(0x3388ff);
g.fillRect(c.x - 22, c.y - 50, 14, 22);
g.fillRect(c.x + 6, c.y - 44, 12, 16);
g.fillRect(c.x - 5, c.y - 56, 10, 8);
} else {
g.fillStyle(0x553311);
g.fillRect(c.x - 30, c.y - 8, 60, 8);
g.fillStyle(0x884422);
for (let i = 0; i < 4; i++) {
g.fillRect(c.x - 25 + i * 14, c.y - 16, 10, 10);
}
}
});
// Enemy missiles
this.missiles.forEach(m => {
if (m.dead) return;
g.lineStyle(2, 0xff4400, 0.9);
g.lineBetween(m.sx, m.sy, m.x, m.y);
g.fillStyle(0xff6600);
g.fillCircle(m.x, m.y, 5);
});
// Interceptors
this.interceptors.forEach(i => {
g.lineStyle(2, 0x00ffff, 0.9);
g.lineBetween(800, 855, i.x, i.y);
g.fillStyle(0xffffff);
g.fillCircle(i.x, i.y, 4);
});
// Explosions
this.explosions.forEach(e => {
const alpha = e.growing ? 0.9 : e.r / e.maxR * 0.9;
g.lineStyle(3, 0xffff00, alpha);
g.strokeCircle(e.x, e.y, e.r);
g.lineStyle(2, 0xff8800, alpha * 0.7);
g.strokeCircle(e.x, e.y, e.r * 0.65);
});
}
}

View File

@ -0,0 +1,10 @@
{
"waves": [
{ "count": 6, "speed": 55, "spawnInterval": 2800 },
{ "count": 9, "speed": 80, "spawnInterval": 2000 },
{ "count": 14, "speed": 110, "spawnInterval": 1400 }
],
"interceptorSpeed": 420,
"explosionRadius": 75,
"maxCityDeaths": 3
}

View File

@ -0,0 +1,342 @@
const LAYOUT = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,1],
[1,0,1,1,0,1,0,1,1,0,0,1,1,0,1,0,1,1,0,1],
[1,0,1,1,0,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,1,1,0,1,1,1,1,0,1,1,0,1,1,0,1],
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
[1,0,1,1,0,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
const ROWS = 15;
const COLS = 20;
export default class DotDude extends Phaser.Scene {
constructor() { super({ key: 'DotDude' }); }
preload() {
this.load.json('ddConfig', 'src/games/dot-dude/config.json');
}
init(data) {
this.globalLives = data.lives;
this.gameActive = false;
}
create() {
this.cfg = this.cache.json.get('ddConfig');
this.TW = 80;
this.TH = 60;
// Dots grid: copy of open cells
this.dots = LAYOUT.map(row => row.map(cell => cell === 0 ? 1 : 0));
this.totalDots = this.dots.flat().filter(d => d === 1).length;
this.dotsEaten = 0;
this._initPlayer();
this._initGhosts();
this.mazeGfx = this.add.graphics();
this.mainGfx = this.add.graphics();
this.gameActive = true;
this.mouthAngle = 0.4;
this.mouthDir = -1;
this._drawMaze();
this._buildHUD();
this._setupInput();
}
_initPlayer() {
const startCol = 10, startRow = 12;
this.player = {
x: (startCol + 0.5) * 80,
y: (startRow + 0.5) * 60,
dir: { dx: 0, dy: 0 },
wantDir: { dx: 1, dy: 0 },
};
}
_initGhosts() {
const ghostCols = [7, 10, 13];
const ghostRow = 6;
const colors = [0xff4444, 0xff88ff, 0x44ffff];
this.ghosts = ghostCols.map((col, i) => ({
x: (col + 0.5) * 80,
y: (ghostRow + 0.5) * 60,
dir: { dx: i === 1 ? -1 : 1, dy: 0 },
color: colors[i],
active: false,
}));
this.cfg.ghostStartDelay.forEach((delay, i) => {
this.time.delayedCall(delay, () => {
if (this.gameActive && this.ghosts[i]) this.ghosts[i].active = true;
});
});
}
_buildHUD() {
this.livesText = this.add.text(800, 6, '♥ '.repeat(this.globalLives).trim(), {
fontSize: '20px', fontFamily: 'monospace', color: '#ff4444',
}).setOrigin(0.5, 0).setDepth(10);
this.dotsText = this.add.text(1580, 6, `DOTS: ${this.totalDots}`, {
fontSize: '20px', fontFamily: 'monospace', color: '#ffff00',
}).setOrigin(1, 0).setDepth(10);
this.add.text(20, 6, 'DOT DUDE', {
fontSize: '20px', fontFamily: 'monospace', color: '#ffaa00',
}).setDepth(10);
}
_setupInput() {
this.cursors = this.input.keyboard.createCursorKeys();
}
_wallAt(col, row) {
if (col < 0 || col >= COLS || row < 0 || row >= ROWS) return true;
return LAYOUT[row][col] === 1;
}
_canMoveTo(px, py) {
const r = 15;
const tileOf = (x, y) => ({
col: Math.floor(x / this.TW),
row: Math.floor(y / this.TH),
});
const corners = [
tileOf(px - r + 1, py - r + 1),
tileOf(px + r - 1, py - r + 1),
tileOf(px - r + 1, py + r - 1),
tileOf(px + r - 1, py + r - 1),
];
return corners.every(({ col, row }) => !this._wallAt(col, row));
}
_canGhostMoveTo(px, py) {
const r = 13;
const tileOf = (x, y) => ({
col: Math.floor(x / this.TW),
row: Math.floor(y / this.TH),
});
const corners = [
tileOf(px - r + 1, py - r + 1),
tileOf(px + r - 1, py - r + 1),
tileOf(px - r + 1, py + r - 1),
tileOf(px + r - 1, py + r - 1),
];
return corners.every(({ col, row }) => !this._wallAt(col, row));
}
update(time, delta) {
if (!this.gameActive) return;
const dt = delta / 1000;
// Input
if (this.cursors.left.isDown) this.player.wantDir = { dx: -1, dy: 0 };
else if (this.cursors.right.isDown) this.player.wantDir = { dx: 1, dy: 0 };
else if (this.cursors.up.isDown) this.player.wantDir = { dx: 0, dy: -1 };
else if (this.cursors.down.isDown) this.player.wantDir = { dx: 0, dy: 1 };
const speed = this.cfg.playerSpeed;
const { wantDir, dir } = this.player;
// Try wanted direction
const wx = this.player.x + wantDir.dx * speed * dt;
const wy = this.player.y + wantDir.dy * speed * dt;
if (this._canMoveTo(wx, wy)) {
this.player.dir = { ...wantDir };
this.player.x = wx;
this.player.y = wy;
} else {
// Keep current direction
const dx2 = this.player.x + dir.dx * speed * dt;
const dy2 = this.player.y + dir.dy * speed * dt;
if (this._canMoveTo(dx2, dy2)) {
this.player.x = dx2;
this.player.y = dy2;
}
}
// Eat dot
const col = Math.floor(this.player.x / this.TW);
const row = Math.floor(this.player.y / this.TH);
if (row >= 0 && row < ROWS && col >= 0 && col < COLS && this.dots[row][col] === 1) {
this.dots[row][col] = 0;
this.dotsEaten++;
this.dotsText.setText(`DOTS: ${this.totalDots - this.dotsEaten}`);
}
// Update ghosts
this.ghosts.forEach(g => this._updateGhost(g, dt));
// Ghost collision
for (const g of this.ghosts) {
if (g.active && Math.hypot(g.x - this.player.x, g.y - this.player.y) < 26) {
this._endGame(false);
return;
}
}
// Win check
if (this.dotsEaten >= this.totalDots) {
this._endGame(true);
return;
}
// Mouth animation
this.mouthAngle += this.mouthDir * 3 * dt;
if (this.mouthAngle < 0.02) { this.mouthAngle = 0.02; this.mouthDir = 1; }
if (this.mouthAngle > 0.45) { this.mouthAngle = 0.45; this.mouthDir = -1; }
this._drawDynamic();
}
_updateGhost(ghost, dt) {
if (!ghost.active) return;
const speed = this.cfg.ghostSpeed;
const { dir } = ghost;
const nx = ghost.x + dir.dx * speed * dt;
const ny = ghost.y + dir.dy * speed * dt;
if (this._canGhostMoveTo(nx, ny)) {
ghost.x = nx;
ghost.y = ny;
}
// Near tile center: rechoose direction
const col = Math.floor(ghost.x / this.TW);
const row = Math.floor(ghost.y / this.TH);
const cx = (col + 0.5) * this.TW;
const cy = (row + 0.5) * this.TH;
const snapD = speed * dt * 4;
if (Math.abs(ghost.x - cx) < snapD && Math.abs(ghost.y - cy) < snapD) {
ghost.x = cx;
ghost.y = cy;
ghost.dir = this._chooseGhostDir(ghost, col, row);
}
}
_chooseGhostDir(ghost, col, row) {
const dirs = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }];
const reverse = { dx: -ghost.dir.dx, dy: -ghost.dir.dy };
const pCol = Math.floor(this.player.x / this.TW);
const pRow = Math.floor(this.player.y / this.TH);
let best = null, bestScore = Infinity;
for (const d of dirs) {
if (d.dx === reverse.dx && d.dy === reverse.dy) continue;
const nc = col + d.dx, nr = row + d.dy;
if (this._wallAt(nc, nr)) continue;
const score = Math.abs(nc - pCol) + Math.abs(nr - pRow);
if (score < bestScore) { bestScore = score; best = d; }
}
return best || reverse;
}
_drawMaze() {
const g = this.mazeGfx;
g.clear();
// Background
g.fillStyle(0x000022);
g.fillRect(0, 0, 1600, 900);
// Walls
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (LAYOUT[r][c] === 1) {
const x = c * this.TW;
const y = r * this.TH;
g.fillStyle(0x0000aa);
g.fillRect(x + 1, y + 1, this.TW - 2, this.TH - 2);
g.lineStyle(2, 0x4444ff);
g.strokeRect(x + 2, y + 2, this.TW - 4, this.TH - 4);
}
}
}
}
_drawDynamic() {
const g = this.mainGfx;
g.clear();
// Dots
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (this.dots[r][c] === 1) {
const x = (c + 0.5) * this.TW;
const y = (r + 0.5) * this.TH;
g.fillStyle(0xffd700);
g.fillCircle(x, y, this.cfg.dotRadius);
}
}
}
// Ghosts
this.ghosts.forEach(gh => {
if (!gh.active) return;
this._drawGhost(g, gh.x, gh.y, gh.color);
});
// Player
this._drawPlayer(g, this.player.x, this.player.y, this.player.dir, this.mouthAngle);
}
_drawPlayer(g, x, y, dir, mouthAngle) {
const r = 18;
// Yellow circle
g.fillStyle(0xffff00);
g.fillCircle(x, y, r);
// Black mouth triangle cutout
const rot = Math.atan2(dir.dy, dir.dx);
g.fillStyle(0x000022);
g.fillTriangle(
x, y,
x + Math.cos(rot - mouthAngle) * (r + 2), y + Math.sin(rot - mouthAngle) * (r + 2),
x + Math.cos(rot + mouthAngle) * (r + 2), y + Math.sin(rot + mouthAngle) * (r + 2)
);
// Eye
g.fillStyle(0x000000);
g.fillCircle(x + Math.cos(rot - 0.8) * 8, y + Math.sin(rot - 0.8) * 8, 3);
}
_drawGhost(g, x, y, color) {
// Body
g.fillStyle(color);
g.fillCircle(x, y - 6, 16);
g.fillRect(x - 16, y - 6, 32, 20);
// Wavy bottom
g.fillTriangle(x - 16, y + 14, x - 8, y + 20, x, y + 14);
g.fillTriangle(x, y + 14, x + 8, y + 20, x + 16, y + 14);
// Eyes
g.fillStyle(0xffffff);
g.fillCircle(x - 6, y - 8, 5);
g.fillCircle(x + 6, y - 8, 5);
g.fillStyle(0x0000cc);
g.fillCircle(x - 5, y - 8, 2);
g.fillCircle(x + 7, y - 8, 2);
}
_endGame(success) {
if (!this.gameActive) return;
this.gameActive = false;
const msg = success ? 'ALL DOTS EATEN!' : 'GHOST GOT YOU!';
const color = success ? '#ffff00' : '#ff4444';
this.add.text(800, 440, msg, {
fontSize: '72px', fontFamily: 'monospace',
color, stroke: '#000000', strokeThickness: 5,
}).setOrigin(0.5).setDepth(20);
this.time.delayedCall(2200, () => {
this.game.events.emit('miniGameResult', { success });
});
}
}

View File

@ -0,0 +1,7 @@
{
"playerSpeed": 155,
"ghostSpeed": 105,
"ghostStartDelay": [2000, 3500, 5000],
"dotRadius": 5,
"powerDotRadius": 10
}

View File

@ -0,0 +1,257 @@
const BLOCK_COLORS = [0xff2222, 0xff8800, 0xffff00, 0x00ff44, 0x00aaff];
export default class SmashOut extends Phaser.Scene {
constructor() { super({ key: 'SmashOut' }); }
preload() {
this.load.json('soConfig', 'src/games/smash-out/config.json');
}
init(data) {
this.globalLives = data.lives;
this.gameActive = false;
}
create() {
this.cfg = this.cache.json.get('soConfig');
this.blocks = [];
this.gameActive = true;
this.ballLaunched = false;
this._buildBackground();
this._buildBlocks();
this._buildPaddle();
this._buildBall();
this.mainGfx = this.add.graphics();
this._buildHUD();
this._setupInput();
}
_buildBackground() {
const bg = this.add.graphics();
bg.fillStyle(0x050510);
bg.fillRect(0, 0, 1600, 900);
// Grid lines for retro feel
bg.lineStyle(1, 0x111133, 0.4);
for (let x = 0; x < 1600; x += 80) bg.lineBetween(x, 0, x, 900);
for (let y = 0; y < 900; y += 60) bg.lineBetween(0, y, 1600, y);
this.add.text(800, 15, 'SMASH OUT', {
fontSize: '20px', fontFamily: 'monospace', color: '#aaaaff', alpha: 0.7,
}).setOrigin(0.5, 0);
}
_buildBlocks() {
const { blockCols, blockRows, blockWidth, blockHeight, blockGapX, blockGapY, blocksStartX, blocksStartY } = this.cfg;
for (let r = 0; r < blockRows; r++) {
for (let c = 0; c < blockCols; c++) {
const x = blocksStartX + c * (blockWidth + blockGapX);
const y = blocksStartY + r * (blockHeight + blockGapY);
this.blocks.push({ x, y, w: blockWidth, h: blockHeight, alive: true, color: BLOCK_COLORS[r] });
}
}
}
_buildPaddle() {
const { paddleWidth, paddleHeight, paddleY } = this.cfg;
this.paddle = { x: 800, y: paddleY, w: paddleWidth, h: paddleHeight };
}
_buildBall() {
const r = this.cfg.ballRadius;
this.ball = {
x: this.paddle.x,
y: this.paddle.y - this.paddle.h / 2 - r - 1,
vx: 0, vy: 0,
r,
launched: false,
};
}
_buildHUD() {
this.livesText = this.add.text(800, 40, '♥ '.repeat(this.globalLives).trim(), {
fontSize: '24px', fontFamily: 'monospace', color: '#ff4444',
}).setOrigin(0.5, 0).setDepth(10);
this.blocksText = this.add.text(1580, 40, '', {
fontSize: '22px', fontFamily: 'monospace', color: '#aaaaff',
}).setOrigin(1, 0).setDepth(10);
this.launchHint = this.add.text(800, 820, 'PRESS A TO LAUNCH', {
fontSize: '24px', fontFamily: 'monospace', color: '#ffffff',
}).setOrigin(0.5, 0).setDepth(10);
this.tweens.add({ targets: this.launchHint, alpha: 0, duration: 600, yoyo: true, repeat: -1 });
}
_setupInput() {
this.cursors = this.input.keyboard.createCursorKeys();
this.keyA = this.input.keyboard.addKey('A');
this.keyAPressed = false;
}
update(time, delta) {
if (!this.gameActive) return;
const dt = delta / 1000;
// Move paddle
const pSpeed = this.cfg.paddleSpeed;
const halfW = this.paddle.w / 2;
if (this.cursors.left.isDown) this.paddle.x -= pSpeed * dt;
if (this.cursors.right.isDown) this.paddle.x += pSpeed * dt;
this.paddle.x = Phaser.Math.Clamp(this.paddle.x, halfW, 1600 - halfW);
// Launch
if (!this.ball.launched) {
this.ball.x = this.paddle.x;
if (this.keyA.isDown && !this.keyAPressed) {
this.keyAPressed = true;
this._launch();
}
if (!this.keyA.isDown) this.keyAPressed = false;
} else {
this._moveBall(dt);
}
this._draw();
}
_launch() {
this.ball.launched = true;
this.launchHint.setVisible(false);
const angle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI / 3;
const s = this.cfg.ballSpeed;
this.ball.vx = Math.cos(angle) * s;
this.ball.vy = Math.sin(angle) * s;
}
_moveBall(dt) {
const ball = this.ball;
const r = ball.r;
ball.x += ball.vx * dt;
ball.y += ball.vy * dt;
// Wall bounces
if (ball.x - r < 0) { ball.x = r; ball.vx = Math.abs(ball.vx); }
if (ball.x + r > 1600) { ball.x = 1600 - r; ball.vx = -Math.abs(ball.vx); }
if (ball.y - r < 0) { ball.y = r; ball.vy = Math.abs(ball.vy); }
// Paddle bounce
const { paddle } = this;
const pTop = paddle.y - paddle.h / 2;
if (ball.vy > 0 &&
ball.y + r >= pTop && ball.y + r <= pTop + paddle.h + 2 &&
ball.x >= paddle.x - paddle.w / 2 - r &&
ball.x <= paddle.x + paddle.w / 2 + r) {
const hitOffset = (ball.x - paddle.x) / (paddle.w / 2);
const angle = -Math.PI / 2 + hitOffset * (Math.PI / 3);
const speed = Math.hypot(ball.vx, ball.vy);
ball.vx = Math.cos(angle) * speed;
ball.vy = -Math.abs(Math.sin(angle) * speed);
ball.y = pTop - r - 1;
}
// Block collisions
this._checkBlockCollisions();
// Ball lost
if (ball.y - r > 900) {
this._endGame(false);
}
}
_checkBlockCollisions() {
const ball = this.ball;
const r = ball.r;
for (const block of this.blocks) {
if (!block.alive) continue;
const bLeft = block.x;
const bRight = block.x + block.w;
const bTop = block.y;
const bBottom = block.y + block.h;
// AABB vs circle
const nearX = Phaser.Math.Clamp(ball.x, bLeft, bRight);
const nearY = Phaser.Math.Clamp(ball.y, bTop, bBottom);
const dist = Math.hypot(ball.x - nearX, ball.y - nearY);
if (dist < r) {
block.alive = false;
// Determine bounce axis: use which edge the ball is closest to
const overlapX = (ball.x < bLeft || ball.x > bRight)
? Math.min(Math.abs(ball.x - bLeft), Math.abs(ball.x - bRight))
: 0;
const overlapY = (ball.y < bTop || ball.y > bBottom)
? Math.min(Math.abs(ball.y - bTop), Math.abs(ball.y - bBottom))
: 0;
// overlapX==0 → ball center inside block horizontally → top/bottom hit
if (overlapX === 0 || (overlapY !== 0 && overlapX > overlapY)) {
ball.vy = -ball.vy;
} else {
ball.vx = -ball.vx;
}
break; // one block per frame
}
}
// Win check
if (this.blocks.every(b => !b.alive)) {
this._endGame(true);
}
}
_endGame(success) {
if (!this.gameActive) return;
this.gameActive = false;
const msg = success ? 'SMASHED IT!' : 'BALL LOST!';
const color = success ? '#00ff44' : '#ff2222';
this.add.text(800, 440, msg, {
fontSize: '80px', fontFamily: 'monospace',
color, stroke: '#000000', strokeThickness: 5,
}).setOrigin(0.5).setDepth(20);
this.time.delayedCall(2200, () => {
this.game.events.emit('miniGameResult', { success });
});
}
_draw() {
const g = this.mainGfx;
g.clear();
// Blocks
const alive = this.blocks.filter(b => b.alive).length;
this.blocksText.setText(`BLOCKS: ${alive}`);
this.blocks.forEach(b => {
if (!b.alive) return;
g.fillStyle(b.color);
g.fillRect(b.x, b.y, b.w, b.h);
g.lineStyle(2, 0xffffff, 0.4);
g.strokeRect(b.x + 1, b.y + 1, b.w - 2, b.h - 2);
// Highlight
g.lineStyle(2, 0xffffff, 0.6);
g.lineBetween(b.x + 3, b.y + 3, b.x + b.w - 3, b.y + 3);
});
// Paddle
const { paddle } = this;
g.fillStyle(0xaaaaff);
g.fillRoundedRect(paddle.x - paddle.w / 2, paddle.y - paddle.h / 2, paddle.w, paddle.h, 6);
g.lineStyle(2, 0xffffff, 0.8);
g.lineBetween(paddle.x - paddle.w / 2 + 6, paddle.y - paddle.h / 2 + 4, paddle.x + paddle.w / 2 - 6, paddle.y - paddle.h / 2 + 4);
// Ball
const { ball } = this;
g.fillStyle(0xffffff);
g.fillCircle(ball.x, ball.y, ball.r);
g.fillStyle(0xccccff);
g.fillCircle(ball.x - 3, ball.y - 3, ball.r * 0.4);
// Walls
g.lineStyle(2, 0x333366, 0.6);
g.lineBetween(0, 0, 0, 900);
g.lineBetween(1600, 0, 1600, 900);
g.lineBetween(0, 0, 1600, 0);
}
}

View File

@ -0,0 +1,16 @@
{
"paddleWidth": 200,
"paddleHeight": 18,
"paddleY": 855,
"paddleSpeed": 480,
"ballRadius": 12,
"ballSpeed": 530,
"blockCols": 8,
"blockRows": 5,
"blockWidth": 140,
"blockHeight": 30,
"blockGapX": 20,
"blockGapY": 12,
"blocksStartX": 170,
"blocksStartY": 70
}

20
src/main.js Normal file
View File

@ -0,0 +1,20 @@
import MainMenu from './scenes/MainMenu.js';
import GameManager from './scenes/GameManager.js';
import GameOver from './scenes/GameOver.js';
import LevelComplete from './scenes/LevelComplete.js';
import ColoradoDefense from './games/colorado-defense/ColoradoDefense.js';
import CodeBugInvaders from './games/code-bug-invaders/CodeBugInvaders.js';
import DotDude from './games/dot-dude/DotDude.js';
import SmashOut from './games/smash-out/SmashOut.js';
new Phaser.Game({
type: Phaser.AUTO,
width: 1600,
height: 900,
backgroundColor: '#000000',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
scene: [MainMenu, GameManager, ColoradoDefense, CodeBugInvaders, DotDude, SmashOut, GameOver, LevelComplete],
});

137
src/scenes/GameManager.js Normal file
View File

@ -0,0 +1,137 @@
export default class GameManager extends Phaser.Scene {
constructor() {
super({ key: 'GameManager' });
}
init(data) {
this.config = data.config;
this.level = data.level || 1;
this.lives = data.lives !== undefined ? data.lives : this.config.lives;
this.queue = [];
this.queueIndex = 0;
}
create() {
this.announceTween = null;
this.announceTimer = null;
this._buildQueue();
this._showAnnouncement();
this.game.events.on('miniGameResult', this._onResult, this);
}
shutdown() {
this.game.events.off('miniGameResult', this._onResult, this);
}
_buildQueue() {
const pool = [...this.config.gamePool];
const count = this.config.levels[String(this.level)]?.gameCount ?? 3;
Phaser.Utils.Array.Shuffle(pool);
this.queue = pool.slice(0, count);
this.queueIndex = 0;
}
_showAnnouncement() {
// Clean up any in-flight tween/timer from a previous announcement
if (this.announceTween) { this.announceTween.remove(); this.announceTween = null; }
if (this.announceTimer) { this.announceTimer.remove(); this.announceTimer = null; }
this.cameras.main.setBackgroundColor('#000000');
if (this.announcementGroup) this.announcementGroup.destroy(true);
this.announcementGroup = this.add.group();
const cx = 800, cy = 450;
const gameNum = this.queueIndex + 1;
const total = this.queue.length;
const gameName = this._displayName(this.queue[this.queueIndex]);
const levelText = this.add.text(cx, 320, `LEVEL ${this.level} — GAME ${gameNum} OF ${total}`, {
fontSize: '32px', fontFamily: 'monospace', color: '#888888',
}).setOrigin(0.5);
const nameText = this.add.text(cx, 420, gameName, {
fontSize: '72px', fontFamily: 'monospace', color: '#00ff00',
stroke: '#005500', strokeThickness: 4,
}).setOrigin(0.5);
const livesText = this.add.text(cx, 530, '♥ '.repeat(this.lives).trim(), {
fontSize: '40px', fontFamily: 'monospace', color: '#ff4444',
}).setOrigin(0.5);
const goText = this.add.text(cx, 640, 'GET READY!', {
fontSize: '48px', fontFamily: 'monospace', color: '#ffff00',
}).setOrigin(0.5).setAlpha(0);
this.announcementGroup.addMultiple([levelText, nameText, livesText, goText]);
this.announceTween = this.tweens.add({
targets: goText,
alpha: 1,
duration: 400,
delay: 800,
onComplete: () => {
this.announceTimer = this.time.delayedCall(1000, () => this._launchCurrent());
},
});
}
_launchCurrent() {
this.announceTween = null;
this.announceTimer = null;
if (this.announcementGroup) {
this.announcementGroup.destroy(true);
this.announcementGroup = null;
}
this.cameras.main.setBackgroundColor('#000000');
const key = this.queue[this.queueIndex];
this.scene.launch(key, { lives: this.lives, level: this.level });
this.scene.bringToTop(key);
}
_onResult({ success }) {
const key = this.queue[this.queueIndex];
if (this.scene.isActive(key)) {
this.scene.stop(key);
}
if (!success) {
this.lives -= 1;
if (this.lives <= 0) {
this.scene.start('GameOver', { level: this.level });
return;
}
// Retry same mini-game
this._showAnnouncement();
return;
}
this.queueIndex += 1;
if (this.queueIndex >= this.queue.length) {
// All games in this level done
const nextLevel = this.level + 1;
const hasNext = !!this.config.levels[String(nextLevel)];
this.scene.start('LevelComplete', {
config: this.config,
level: this.level,
lives: this.lives,
hasNextLevel: hasNext,
});
return;
}
this._showAnnouncement();
}
_displayName(key) {
const names = {
ColoradoDefense: 'COLORADO DEFENSE',
CodeBugInvaders: 'CODE BUG INVADERS',
DotDude: 'DOT DUDE',
SmashOut: 'SMASH OUT',
};
return names[key] || key;
}
}

31
src/scenes/GameOver.js Normal file
View File

@ -0,0 +1,31 @@
export default class GameOver extends Phaser.Scene {
constructor() {
super({ key: 'GameOver' });
}
init(data) {
this.level = data.level || 1;
}
create() {
const cx = 800, cy = 450;
this.add.text(cx, 340, 'GAME OVER', {
fontSize: '96px', fontFamily: 'monospace',
color: '#ff0000', stroke: '#550000', strokeThickness: 6,
}).setOrigin(0.5);
this.add.text(cx, 470, `You reached Level ${this.level}`, {
fontSize: '36px', fontFamily: 'monospace', color: '#aaaaaa',
}).setOrigin(0.5);
const prompt = this.add.text(cx, 580, 'PRESS SPACE OR CLICK TO RESTART', {
fontSize: '28px', fontFamily: 'monospace', color: '#ffffff',
}).setOrigin(0.5);
this.tweens.add({ targets: prompt, alpha: 0, duration: 600, yoyo: true, repeat: -1 });
this.input.keyboard.once('keydown-SPACE', () => this.scene.start('MainMenu'));
this.input.once('pointerdown', () => this.scene.start('MainMenu'));
}
}

View File

@ -0,0 +1,59 @@
export default class LevelComplete extends Phaser.Scene {
constructor() {
super({ key: 'LevelComplete' });
}
init(data) {
this.config = data.config;
this.level = data.level;
this.lives = data.lives;
this.hasNextLevel = data.hasNextLevel;
}
create() {
const cx = 800, cy = 450;
this.add.text(cx, 300, `LEVEL ${this.level} COMPLETE!`, {
fontSize: '80px', fontFamily: 'monospace',
color: '#00ff00', stroke: '#005500', strokeThickness: 4,
}).setOrigin(0.5);
this.add.text(cx, 420, '♥ '.repeat(this.lives).trim(), {
fontSize: '48px', fontFamily: 'monospace', color: '#ff4444',
}).setOrigin(0.5);
if (this.hasNextLevel) {
this.add.text(cx, 530, `PREPARE FOR LEVEL ${this.level + 1}`, {
fontSize: '40px', fontFamily: 'monospace', color: '#ffff00',
}).setOrigin(0.5);
const prompt = this.add.text(cx, 650, 'PRESS SPACE OR CLICK TO CONTINUE', {
fontSize: '28px', fontFamily: 'monospace', color: '#ffffff',
}).setOrigin(0.5);
this.tweens.add({ targets: prompt, alpha: 0, duration: 600, yoyo: true, repeat: -1 });
this.input.keyboard.once('keydown-SPACE', () => this._nextLevel());
this.input.once('pointerdown', () => this._nextLevel());
} else {
this.add.text(cx, 530, 'YOU BEAT THE GAME!', {
fontSize: '48px', fontFamily: 'monospace', color: '#ffff00',
}).setOrigin(0.5);
const prompt = this.add.text(cx, 650, 'PRESS SPACE OR CLICK TO RESTART', {
fontSize: '28px', fontFamily: 'monospace', color: '#ffffff',
}).setOrigin(0.5);
this.tweens.add({ targets: prompt, alpha: 0, duration: 600, yoyo: true, repeat: -1 });
this.input.keyboard.once('keydown-SPACE', () => this.scene.start('MainMenu'));
this.input.once('pointerdown', () => this.scene.start('MainMenu'));
}
}
_nextLevel() {
this.scene.start('GameManager', {
config: this.config,
level: this.level + 1,
lives: this.lives,
});
}
}

66
src/scenes/MainMenu.js Normal file
View File

@ -0,0 +1,66 @@
export default class MainMenu extends Phaser.Scene {
constructor() {
super({ key: 'MainMenu' });
}
preload() {
this.load.json('gameConfig', 'config/game.json');
}
create() {
const cx = 800, cy = 450;
// Title
this.add.text(cx, 280, 'ATTENTION RETRO DISORDER', {
fontSize: '64px',
fontFamily: 'monospace',
color: '#00ff00',
stroke: '#005500',
strokeThickness: 4,
}).setOrigin(0.5);
this.add.text(cx, 370, 'A MULTI-GAME RETRO EXPERIENCE', {
fontSize: '28px',
fontFamily: 'monospace',
color: '#00aa00',
}).setOrigin(0.5);
// Game list
const games = [
'★ COLORADO DEFENSE (Missile Command)',
'★ CODE BUG INVADERS (Space Invaders)',
'★ DOT DUDE (Pac-Man)',
'★ SMASH OUT (Arkanoid)',
];
games.forEach((txt, i) => {
this.add.text(cx, 470 + i * 38, txt, {
fontSize: '22px',
fontFamily: 'monospace',
color: '#ffff00',
}).setOrigin(0.5);
});
// Blink "Press SPACE"
const prompt = this.add.text(cx, 730, 'PRESS SPACE OR CLICK TO START', {
fontSize: '32px',
fontFamily: 'monospace',
color: '#ffffff',
}).setOrigin(0.5);
this.tweens.add({
targets: prompt,
alpha: 0,
duration: 600,
yoyo: true,
repeat: -1,
});
this.input.keyboard.once('keydown-SPACE', () => this.startGame());
this.input.once('pointerdown', () => this.startGame());
}
startGame() {
const cfg = this.cache.json.get('gameConfig');
this.scene.start('GameManager', { config: cfg, level: 1, lives: cfg.lives });
}
}

4
start_web.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
# Start a simple HTTP server on port 8000
python3 -m http.server 8000