initial commit
This commit is contained in:
commit
054166e5b0
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"spellright.language": [
|
||||
"en-US-10-1."
|
||||
],
|
||||
"spellright.documentTypes": [
|
||||
"markdown",
|
||||
"latex",
|
||||
"plaintext"
|
||||
]
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Start a simple HTTP server on port 8000
|
||||
python3 -m http.server 8000
|
||||
Loading…
Reference in New Issue