initial commit

This commit is contained in:
Brian Fertig 2026-02-20 21:34:27 -07:00
commit 054166e5b0
13 changed files with 820 additions and 0 deletions

View File

@ -0,0 +1,19 @@
---
description: Reviews code for quality and best practices
mode: subagent
model: fertigpt/gpt-oss-120b-mxfp-GGUF
temperature: 0.1
tools:
write: false
edit: false
bash: false
---
You are in code review mode. Focus on:
- Code quality and best practices
- Potential bugs and edge cases
- Performance implications
- Security considerations
Provide constructive feedback without making direct changes.

1
.env Normal file
View File

@ -0,0 +1 @@
OPENWEBUI_API_KEY=sk-3b8d611304204df7b06d5c02d53befc0

10
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"spellright.language": [
"en-US-10-1."
],
"spellright.documentTypes": [
"markdown",
"latex",
"plaintext"
]
}

49
CLAUDE.md Normal file
View File

@ -0,0 +1,49 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a Phaser 3 HTML5 top-down space shooter game (Asteroids-inspired). The project is currently in the early implementation phase — `Software.md` is the authoritative game design specification.
## Local Development
Start the development server (serves files at `http://localhost:8000`):
```bash
./start_web.sh
# or equivalently:
python3 -m http.server 8000
```
There is no build step, bundler, or package manager. Open `index.html` directly in the browser via the HTTP server.
## Technical Constraints
- **Framework:** Phaser 3 (loaded via CDN or local file)
- **Language:** JavaScript ES6 modules using native `import`/`export`
- **No bundler** (no Webpack, Vite, Rollup, etc.) — modules must be referenced directly and served over HTTP (not `file://`)
- **Resolution:** 1600×900
- **Graphics:** Vector/placeholder graphics first; sprite replacement comes later
## Game Design Summary (from Software.md)
**Levels:** A level ends when all asteroids and alien ships are destroyed. Player always respawns at screen center on new level.
**Asteroids:**
- Spawn at level start, rotate slowly in a random direction/speed
- Large → 1–3 mid or small fragments on destruction
- Mid → 2 small fragments on destruction
- Small → fully destroyed
**Alien Ships:** Periodically attack, move toward the player, fire occasionally. Destroyed by a single player shot.
**Controls:**
- Ship rotates to follow the mouse cursor at a fixed rate (360° turn = 2 seconds)
- Left mouse button = thrust/accelerator
- `a` key or `space` = fire weapon
- Zero-gravity physics: ship accelerates over ~2 seconds to full speed, decelerates gradually when thrust stops
## Architecture Guidelines
Organize game entities as separate ES6 modules (e.g., `Player.js`, `Asteroid.js`, `AlienShip.js`, `Bullet.js`, `GameScene.js`). Each module exports its class; scenes import and instantiate them. Avoid circular dependencies — prefer a single scene that owns all game objects.

39
Software.md Normal file
View File

@ -0,0 +1,39 @@
### Build Guidelines
Create an HTML Phaser 3 video game. This game will be a top down space shooter game, similar to Asteroids but with several modifications.
## Tools and Organization
- Phaser version 3 HTML game
- Use Javascript
- Do **NOT** require a web packager. Have JS objects reference each other directly via IMPORT and EXPORT
## Basic Framework
- 1600 x 900 view
- Use termporary vector graphics that can later be replaced by sprites
## Gameplay
- There should be multiple levels in this game.
- A level ends when all asteroids and alien ships are destroyed.
- When a new level begins, the player always begins in the middle of the screen.
- Asteroids spawn at the beginning of each level.
- Asteroids rotate slowly in a random direction and a random low speed.
- When Large Asteroids are shot and destroyed, they turn into either mid sized or small asteroids (between 1 and 3 total)
- When mid sized asteroids are destroyed, they turn into two small asteroids.
- When small asteroids are destroyed, they are completely destroyed.
- Periodically alien ships will attack.
- They move towards the player's ship and fire occasionally.
- WHen the alien ship is hit with a shot from the player, it is destroyed.
## Controls
- Player's space ship follows the mouse cursor.
- The space ship can only rotate to follow the mouse at the rate of 2 seconds to turn 360 degrees.
- If the user moves the mouse the players ship will rotate automatically at this rate until it is pointing in the direction of the mouse cursor.
- The left mouse button fires the accelerator for the space ship.
- In all cases the space ship is in zero gravity.
- It takes a couple seconds to get the ship to full acceleration.
- If the player stops accelerating the ship continues to move in it's current direction until it decelerates.
- The player can use the "a" key or "space" key to fire.

16
index.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Space Shooter</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; }
canvas { display: block; }
</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.min.js"></script>
<script type="module" src="js/main.js"></script>
</body>
</html>

54
js/entities/AlienShip.js Normal file
View File

@ -0,0 +1,54 @@
const ALIEN_SPEED = 130; // pixels/sec
const ALIEN_FIRE_RATE = 2500; // ms between shots
export default class AlienShip {
constructor(scene, x, y, group) {
this.scene = scene;
this.alive = true;
this.sprite = scene.physics.add.sprite(x, y, 'alien');
group.add(this.sprite);
this.sprite.gameEntity = this;
this.sprite.body.setAllowGravity(false);
// Circular hitbox: texture is 64x48, use radius 20 centered at (32,24)
this.sprite.body.setCircle(20, 12, 4);
this.lastFired = 0;
}
update(time, player) {
if (!this.alive) return;
if (!player || !player.alive || !player.sprite.active) return;
// Move toward player
const dx = player.sprite.x - this.sprite.x;
const dy = player.sprite.y - this.sprite.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 5) {
this.sprite.setVelocity(
(dx / dist) * ALIEN_SPEED,
(dy / dist) * ALIEN_SPEED
);
}
// Fire at player periodically
if (time - this.lastFired > ALIEN_FIRE_RATE) {
this.lastFired = time;
const angle = Phaser.Math.RadToDeg(Math.atan2(dy, dx));
this.scene.spawnAlienBullet(this.sprite.x, this.sprite.y, angle);
}
// Wrap around screen
this.scene.physics.world.wrap(this.sprite, 40);
}
destroy() {
if (!this.alive) return;
this.alive = false;
if (this.sprite && this.sprite.active) {
this.sprite.destroy();
}
}
}

43
js/entities/Asteroid.js Normal file
View File

@ -0,0 +1,43 @@
// size -> { textureKey, radius, texSize, minSpeed, maxSpeed }
export const ASTEROID_CONFIG = {
large: { textureKey: 'asteroid_large', radius: 50, texSize: 120, minSpeed: 20, maxSpeed: 60 },
medium: { textureKey: 'asteroid_medium', radius: 26, texSize: 64, minSpeed: 40, maxSpeed: 100 },
small: { textureKey: 'asteroid_small', radius: 12, texSize: 32, minSpeed: 60, maxSpeed: 140 }
};
export default class Asteroid {
constructor(scene, x, y, size, group) {
this.scene = scene;
this.size = size;
this.alive = true;
const cfg = ASTEROID_CONFIG[size];
this.sprite = scene.physics.add.sprite(x, y, cfg.textureKey);
group.add(this.sprite);
this.sprite.gameEntity = this;
this.sprite.body.setAllowGravity(false);
// Circular hitbox centered in texture
const offset = cfg.texSize / 2 - cfg.radius;
this.sprite.body.setCircle(cfg.radius, offset, offset);
// Random velocity and slow spin
const speed = cfg.minSpeed + Math.random() * (cfg.maxSpeed - cfg.minSpeed);
const angle = Math.random() * Math.PI * 2;
this.sprite.setVelocity(Math.cos(angle) * speed, Math.sin(angle) * speed);
this.sprite.setAngularVelocity((Math.random() - 0.5) * 60);
}
update() {
if (!this.alive) return;
this.scene.physics.world.wrap(this.sprite, 64);
}
destroy() {
if (!this.alive) return;
this.alive = false;
if (this.sprite && this.sprite.active) {
this.sprite.destroy();
}
}
}

51
js/entities/Bullet.js Normal file
View File

@ -0,0 +1,51 @@
const BULLET_SPEED = 700; // pixels/sec
const BULLET_LIFETIME = 1800; // ms
export default class Bullet {
constructor(scene, x, y, angle, owner, group) {
this.scene = scene;
this.owner = owner; // 'player' or 'alien'
this.alive = true;
const textureKey = owner === 'player' ? 'bullet' : 'alien_bullet';
this.sprite = scene.physics.add.sprite(x, y, textureKey);
group.add(this.sprite);
this.sprite.gameEntity = this;
this.sprite.body.setAllowGravity(false);
const rad = Phaser.Math.DegToRad(angle);
this.sprite.setVelocity(
Math.cos(rad) * BULLET_SPEED,
Math.sin(rad) * BULLET_SPEED
);
this.createdAt = scene.time.now;
}
update(time) {
if (!this.alive) return false;
// Expire by lifetime
if (time - this.createdAt > BULLET_LIFETIME) {
this.destroy();
return false;
}
// Expire if far off-screen
const { x, y } = this.sprite;
if (x < -60 || x > 1660 || y < -60 || y > 960) {
this.destroy();
return false;
}
return true;
}
destroy() {
if (!this.alive) return;
this.alive = false;
if (this.sprite && this.sprite.active) {
this.sprite.destroy();
}
}
}

124
js/entities/Player.js Normal file
View File

@ -0,0 +1,124 @@
// 360 degrees per 2 seconds = 180 deg/sec
const ROTATION_SPEED = 180;
// Acceleration in pixels/sec² when thrusting
const THRUST_ACCEL = 200;
// Drag in pixels/sec² when coasting (slow deceleration)
const DRAG = 35;
const MAX_SPEED = 420;
const FIRE_RATE = 280; // ms between shots
// Nose is 24px ahead of sprite center in texture-local space
const NOSE_OFFSET = 24;
const INVINCIBLE_DURATION = 3000; // ms
export default class Player {
constructor(scene, x, y) {
this.scene = scene;
this.alive = true;
// Texture is 64x32; ship nose points right (angle=0)
this.sprite = scene.physics.add.sprite(x, y, 'player');
this.sprite.body.setAllowGravity(false);
this.sprite.body.setDrag(DRAG, DRAG);
// Hitbox: circle radius 13 centered at texture center (32,16)
this.sprite.body.setCircle(13, 19, 3);
// Start pointing up (-90°)
this.angle = -90;
this.sprite.angle = this.angle;
this.spaceKey = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
this.aKey = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A);
this.lastFired = 0;
this.invincible = false;
this.invincibleUntil = 0;
}
update(time, delta) {
if (!this.alive) return;
const dt = delta / 1000;
// Invincibility flicker
if (this.invincible) {
if (time > this.invincibleUntil) {
this.invincible = false;
this.sprite.setAlpha(1);
} else {
this.sprite.setAlpha(Math.sin(time / 80) > 0 ? 1 : 0.15);
}
}
// Rotate toward mouse cursor
const pointer = this.scene.input.activePointer;
const world = this.scene.cameras.main.getWorldPoint(pointer.x, pointer.y);
const targetAngle = Phaser.Math.RadToDeg(
Math.atan2(world.y - this.sprite.y, world.x - this.sprite.x)
);
const maxRot = ROTATION_SPEED * dt;
const diff = Phaser.Math.Angle.ShortestBetween(this.angle, targetAngle);
this.angle += Phaser.Math.Clamp(diff, -maxRot, maxRot);
this.sprite.angle = this.angle;
// Thrust on left mouse button
const thrusting = pointer.leftButtonDown();
if (thrusting) {
const rad = Phaser.Math.DegToRad(this.angle);
this.sprite.body.velocity.x += Math.cos(rad) * THRUST_ACCEL * dt;
this.sprite.body.velocity.y += Math.sin(rad) * THRUST_ACCEL * dt;
this.sprite.setTexture('player_thrust');
} else {
this.sprite.setTexture('player');
}
// Cap total speed
const speed = this.sprite.body.speed;
if (speed > MAX_SPEED) {
this.sprite.body.velocity.scale(MAX_SPEED / speed);
}
// Wrap around screen
this.scene.physics.world.wrap(this.sprite, 30);
// Fire: hold key fires at fixed rate
if (this.spaceKey.isDown || this.aKey.isDown) {
if (time - this.lastFired > FIRE_RATE) {
this.lastFired = time;
const rad = Phaser.Math.DegToRad(this.angle);
this.scene.spawnPlayerBullet(
this.sprite.x + Math.cos(rad) * NOSE_OFFSET,
this.sprite.y + Math.sin(rad) * NOSE_OFFSET,
this.angle
);
}
}
}
// Returns true if the hit should count (not invincible)
hit() {
return !this.invincible;
}
makeInvincible() {
this.invincible = true;
this.invincibleUntil = this.scene.time.now + INVINCIBLE_DURATION;
this.sprite.setAlpha(1);
}
respawn(x, y) {
this.alive = true;
this.sprite.setPosition(x, y);
this.sprite.body.setVelocity(0, 0);
this.sprite.setActive(true).setVisible(true);
this.angle = -90;
this.sprite.angle = this.angle;
this.makeInvincible();
}
destroy() {
this.alive = false;
if (this.sprite && this.sprite.active) {
this.sprite.destroy();
}
}
}

18
js/main.js Normal file
View File

@ -0,0 +1,18 @@
import GameScene from './scenes/GameScene.js';
const config = {
type: Phaser.AUTO,
width: 1600,
height: 900,
backgroundColor: '#000011',
physics: {
default: 'arcade',
arcade: {
gravity: { x: 0, y: 0 },
debug: false
}
},
scene: [GameScene]
};
new Phaser.Game(config);

392
js/scenes/GameScene.js Normal file
View File

@ -0,0 +1,392 @@
import Player from '../entities/Player.js';
import Asteroid from '../entities/Asteroid.js';
import AlienShip from '../entities/AlienShip.js';
import Bullet from '../entities/Bullet.js';
const STARTING_LIVES = 3;
export default class GameScene extends Phaser.Scene {
constructor() {
super({ key: 'GameScene' });
}
create() {
this.createTextures();
// State
this.level = 1;
this.score = 0;
this.lives = STARTING_LIVES;
this.gameOver = false;
this.levelComplete = false;
// Physics groups (used for overlap detection)
this.asteroidsGroup = this.physics.add.group();
this.aliensGroup = this.physics.add.group();
this.playerBulletsGroup = this.physics.add.group();
this.alienBulletsGroup = this.physics.add.group();
// Entity arrays for custom update logic
this.asteroids = [];
this.aliens = [];
this.playerBullets = [];
this.alienBullets = [];
this.player = new Player(this, 800, 450);
this.setupCollisions();
this.createUI();
this.startLevel();
this.alienTimer = this.time.addEvent({
delay: this.alienSpawnDelay(),
callback: this.spawnAlien,
callbackScope: this,
loop: true
});
}
// ─── Texture Generation ───────────────────────────────────────────────────
createTextures() {
// Player ship – texture 64×32, nose points right (angle 0)
// Nose: (56,16) left-wing: (12,4) right-wing: (12,28)
let g = this.make.graphics({ add: false });
g.lineStyle(2, 0x00ff44, 1);
g.strokeTriangle(56, 16, 12, 4, 12, 28);
g.generateTexture('player', 64, 32);
g.destroy();
// Player ship with thruster flame
g = this.make.graphics({ add: false });
g.lineStyle(2, 0x00ff44, 1);
g.strokeTriangle(56, 16, 12, 4, 12, 28);
g.lineStyle(2, 0xff6600, 1);
// Flame behind the ship: (12,11),(12,21) -> (0,16)
g.strokeTriangle(12, 11, 12, 21, 0, 16);
g.generateTexture('player_thrust', 64, 32);
g.destroy();
// Large asteroid – 120×120 texture, radius 50, 12-sided irregular polygon
g = this.make.graphics({ add: false });
g.lineStyle(2, 0xcccccc, 1);
g.strokePoints(this.asteroidPoints(60, 60, 50, 12,
[1.0, 0.82, 1.08, 0.88, 1.0, 0.78, 1.1, 0.9, 0.94, 1.0, 0.84, 1.06]
), true);
g.generateTexture('asteroid_large', 120, 120);
g.destroy();
// Medium asteroid – 64×64, radius 26, 10-sided
g = this.make.graphics({ add: false });
g.lineStyle(2, 0xcccccc, 1);
g.strokePoints(this.asteroidPoints(32, 32, 26, 10,
[1.0, 0.78, 1.1, 0.88, 1.0, 0.82, 1.0, 0.92, 1.06, 0.80]
), true);
g.generateTexture('asteroid_medium', 64, 64);
g.destroy();
// Small asteroid – 32×32, radius 12, 8-sided
g = this.make.graphics({ add: false });
g.lineStyle(2, 0xcccccc, 1);
g.strokePoints(this.asteroidPoints(16, 16, 12, 8,
[1.0, 0.78, 1.12, 0.84, 1.0, 0.9, 0.80, 1.04]
), true);
g.generateTexture('asteroid_small', 32, 32);
g.destroy();
// Alien saucer – 64×48, drawn as two stacked ellipses
g = this.make.graphics({ add: false });
g.lineStyle(2, 0x00ffff, 1);
g.strokeEllipse(32, 30, 52, 20); // main body
g.strokeEllipse(32, 22, 30, 18); // dome
g.generateTexture('alien', 64, 48);
g.destroy();
// Player bullet – 8×8 yellow circle
g = this.make.graphics({ add: false });
g.fillStyle(0xffff00, 1);
g.fillCircle(4, 4, 4);
g.generateTexture('bullet', 8, 8);
g.destroy();
// Alien bullet – 8×8 red circle
g = this.make.graphics({ add: false });
g.fillStyle(0xff4444, 1);
g.fillCircle(4, 4, 4);
g.generateTexture('alien_bullet', 8, 8);
g.destroy();
}
// Returns array of {x,y} points forming an irregular polygon
asteroidPoints(cx, cy, radius, numPoints, offsets) {
return offsets.map((scale, i) => {
const angle = (i / numPoints) * Math.PI * 2;
return {
x: cx + Math.cos(angle) * radius * scale,
y: cy + Math.sin(angle) * radius * scale
};
});
}
// ─── Collision Setup ─────────────────────────────────────────────────────
setupCollisions() {
this.physics.add.overlap(
this.playerBulletsGroup, this.asteroidsGroup,
this.onPlayerBulletHitAsteroid, null, this
);
this.physics.add.overlap(
this.playerBulletsGroup, this.aliensGroup,
this.onPlayerBulletHitAlien, null, this
);
this.physics.add.overlap(
this.alienBulletsGroup, this.player.sprite,
this.onAlienBulletHitPlayer, null, this
);
this.physics.add.overlap(
this.player.sprite, this.asteroidsGroup,
this.onPlayerHitAsteroid, null, this
);
this.physics.add.overlap(
this.player.sprite, this.aliensGroup,
this.onPlayerHitAlien, null, this
);
}
// ─── UI ──────────────────────────────────────────────────────────────────
createUI() {
const mono = { fontFamily: 'monospace', fill: '#ffffff' };
this.scoreText = this.add.text(16, 16, 'SCORE: 0', { ...mono, fontSize: '20px' }).setDepth(10);
this.livesText = this.add.text(16, 44, 'LIVES: 3', { ...mono, fontSize: '20px' }).setDepth(10);
this.levelText = this.add.text(800, 16, 'LEVEL 1', { ...mono, fontSize: '24px' })
.setOrigin(0.5, 0).setDepth(10);
this.msgText = this.add.text(800, 420, '', { ...mono, fontSize: '52px', align: 'center' })
.setOrigin(0.5).setDepth(10).setVisible(false);
}
updateUI() {
this.scoreText.setText(`SCORE: ${this.score}`);
this.livesText.setText(`LIVES: ${this.lives}`);
this.levelText.setText(`LEVEL ${this.level}`);
}
showMessage(text, autohideMs = 0) {
if (!text) { this.msgText.setVisible(false); return; }
this.msgText.setText(text).setVisible(true);
if (autohideMs > 0) {
this.time.delayedCall(autohideMs, () => this.msgText.setVisible(false));
}
}
// ─── Level Management ────────────────────────────────────────────────────
startLevel() {
this.levelComplete = false;
// Clear all entities
this.asteroids.forEach(a => a.destroy()); this.asteroids = [];
this.aliens.forEach(a => a.destroy()); this.aliens = [];
this.playerBullets.forEach(b => b.destroy()); this.playerBullets = [];
this.alienBullets.forEach(b => b.destroy()); this.alienBullets = [];
// Respawn player at center
this.player.respawn(800, 450);
// Spawn large asteroids – more per level
const count = 3 + this.level;
for (let i = 0; i < count; i++) {
const { x, y } = this.edgePosition();
this.asteroids.push(new Asteroid(this, x, y, 'large', this.asteroidsGroup));
}
this.updateUI();
this.showMessage(`LEVEL ${this.level}`, 2000);
}
// Random position along any screen edge, away from center
edgePosition() {
const W = 1600, H = 900;
let x, y;
do {
const edge = Phaser.Math.Between(0, 3);
switch (edge) {
case 0: x = Phaser.Math.Between(0, W); y = 0; break;
case 1: x = Phaser.Math.Between(0, W); y = H; break;
case 2: x = 0; y = Phaser.Math.Between(0, H); break;
default: x = W; y = Phaser.Math.Between(0, H); break;
}
} while (Phaser.Math.Distance.Between(x, y, 800, 450) < 200);
return { x, y };
}
alienSpawnDelay() {
return Math.max(8000, 22000 - this.level * 1500);
}
// ─── Spawning ────────────────────────────────────────────────────────────
spawnAlien() {
if (this.gameOver || this.levelComplete) return;
if (this.aliens.length >= 2) return;
const { x, y } = this.edgePosition();
this.aliens.push(new AlienShip(this, x, y, this.aliensGroup));
}
spawnPlayerBullet(x, y, angle) {
this.playerBullets.push(
new Bullet(this, x, y, angle, 'player', this.playerBulletsGroup)
);
}
spawnAlienBullet(x, y, angle) {
this.alienBullets.push(
new Bullet(this, x, y, angle, 'alien', this.alienBulletsGroup)
);
}
// ─── Collision Callbacks ─────────────────────────────────────────────────
onPlayerBulletHitAsteroid(bulletSprite, asteroidSprite) {
if (!bulletSprite.active || !asteroidSprite.active) return;
const bullet = bulletSprite.gameEntity;
const asteroid = asteroidSprite.gameEntity;
if (!bullet || !asteroid || !bullet.alive || !asteroid.alive) return;
const scoreTable = { large: 20, medium: 50, small: 100 };
this.addScore(scoreTable[asteroid.size] || 20);
this.splitAsteroid(asteroid);
bullet.destroy();
this.playerBullets = this.playerBullets.filter(b => b !== bullet);
}
onPlayerBulletHitAlien(bulletSprite, alienSprite) {
if (!bulletSprite.active || !alienSprite.active) return;
const bullet = bulletSprite.gameEntity;
const alien = alienSprite.gameEntity;
if (!bullet || !alien || !bullet.alive || !alien.alive) return;
this.addScore(200);
alien.destroy();
this.aliens = this.aliens.filter(a => a !== alien);
bullet.destroy();
this.playerBullets = this.playerBullets.filter(b => b !== bullet);
}
onAlienBulletHitPlayer(alienBulletSprite, playerSprite) {
if (!alienBulletSprite.active) return;
const bullet = alienBulletSprite.gameEntity;
if (!bullet || !bullet.alive) return;
bullet.destroy();
this.alienBullets = this.alienBullets.filter(b => b !== bullet);
this.handlePlayerHit();
}
onPlayerHitAsteroid(playerSprite, asteroidSprite) {
if (!asteroidSprite.active) return;
const asteroid = asteroidSprite.gameEntity;
if (!asteroid || !asteroid.alive) return;
this.handlePlayerHit();
}
onPlayerHitAlien(playerSprite, alienSprite) {
if (!alienSprite.active) return;
const alien = alienSprite.gameEntity;
if (!alien || !alien.alive) return;
this.handlePlayerHit();
}
handlePlayerHit() {
if (!this.player.hit()) return; // invincible – ignore
this.lives--;
this.updateUI();
if (this.lives <= 0) {
this.triggerGameOver();
} else {
this.player.respawn(800, 450);
}
}
// ─── Asteroid Splitting ───────────────────────────────────────────────────
splitAsteroid(asteroid) {
const { x, y } = asteroid.sprite;
asteroid.destroy();
this.asteroids = this.asteroids.filter(a => a !== asteroid);
if (asteroid.size === 'large') {
const count = Phaser.Math.Between(1, 3);
for (let i = 0; i < count; i++) {
const size = Math.random() < 0.5 ? 'medium' : 'small';
this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup));
}
} else if (asteroid.size === 'medium') {
for (let i = 0; i < 2; i++) {
this.asteroids.push(new Asteroid(this, x, y, 'small', this.asteroidsGroup));
}
}
// small: fully destroyed – no fragments
}
// ─── Score / Game Over ────────────────────────────────────────────────────
addScore(pts) {
this.score += pts;
this.scoreText.setText(`SCORE: ${this.score}`);
}
triggerGameOver() {
this.gameOver = true;
this.player.alive = false;
this.player.sprite.setVisible(false);
this.showMessage(`GAME OVER\n\nSCORE: ${this.score}\n\nPress R to restart`);
this.input.keyboard.once('keydown-R', () => this.scene.restart());
}
// ─── Level Completion Check ───────────────────────────────────────────────
checkLevelComplete() {
if (this.levelComplete) return;
if (this.asteroids.length > 0 || this.aliens.length > 0) return;
this.levelComplete = true;
this.showMessage('LEVEL COMPLETE!', 2000);
this.time.delayedCall(2500, () => {
this.level++;
this.startLevel();
// Reset alien spawn timer for new level
this.alienTimer.remove();
this.alienTimer = this.time.addEvent({
delay: this.alienSpawnDelay(),
callback: this.spawnAlien,
callbackScope: this,
loop: true
});
});
}
// ─── Main Update Loop ────────────────────────────────────────────────────
update(time, delta) {
if (this.gameOver) return;
this.player.update(time, delta);
this.asteroids.forEach(a => a.update());
this.aliens.forEach(a => a.update(time, this.player));
// Update bullets; keep only still-alive ones
this.playerBullets = this.playerBullets.filter(b => b.update(time));
this.alienBullets = this.alienBullets.filter(b => b.update(time));
this.checkLevelComplete();
}
}

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