initial commit

This commit is contained in:
Brian Fertig 2026-02-14 11:19:48 -07:00
commit 3d9d3537b5
24 changed files with 910 additions and 0 deletions

495
BASE_DEFENSE_PLAN.md Normal file
View File

@ -0,0 +1,495 @@
# Base Defense Game – Design & Implementation Plan
## 1️⃣ Project Layout (pure‑file hierarchy)
```
/project-root
│
├─ index.html # entry point, loads Phaser CDN + main.js
│
├─ /js
│ ├─ main.js # creates the Phaser.Game instance
│ ├─ config.js # global constants (resolution, asset paths)
│ │
│ ├─ /scenes
│ │ ├─ MenuScene.js
│ │ ├─ GameScene.js # core gameplay
│ │ └─ GameOverScene.js
│ │
│ ├─ /objects
│ │ ├─ Base.js # player base (has health, shoots)
│ │ ├─ Enemy.js # enemy unit
│ │ └─ Bullet.js # projectile fired by the base
│ │
│ ├─ /managers
│ │ ├─ LevelManager.js # loads level JSON, handles progression
│ │ ├─ XPManager.js # tracks XP, emits “levelup”
│ │ ├─ SpawnManager.js # spawns enemies based on LevelManager data
│ │ └─ UIManager.js # health bar, XP bar, level display
│ │
│ └─ /data
│ ├─ levels.json # array of level definitions
│ └─ units.json # definitions for enemy types, bullet stats, etc.
│
└─ /assets
├─ /sprites
│ ├─ base.png
│ ├─ base_spritesheet.png # optional animation frames
│ ├─ enemy_spritesheet.png
│ └─ bullet.png
└─ /audio
├─ shoot.wav
└─ explosion.wav
```
All JavaScript files are **ES‑modules** (`export` / `import`). The only external library is Phaser, pulled from a CDN.
---
## 2️⃣ Core Files – Skeleton Code
Below are minimal implementations for each file. They are ready to copy‑paste into the structure above.
### 2.1 `index.html`
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dystopian Base Defense</title>
<style>
body { margin:0; overflow:hidden; background:#000; }
canvas { display:block; margin:0 auto; }
</style>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.55.2/dist/phaser.min.js"></script>
</head>
<body>
<script type="module" src="./js/main.js"></script>
</body>
</html>
```
---
### 2.2 `js/config.js`
```js
export const GAME_WIDTH = 1600;
export const GAME_HEIGHT = 900;
export const BASE_HEALTH = 100; // starting health
export const BASE_FIRE_RATE = 2000; // ms between shots
export const BASE_DAMAGE = 20; // damage per bullet
export const ASSET_PATH = './assets/';
```
---
### 2.3 `js/main.js`
```js
import { GAME_WIDTH, GAME_HEIGHT } from './config.js';
import MenuScene from './scenes/MenuScene.js';
import GameScene from './scenes/GameScene.js';
import GameOverScene from './scenes/GameOverScene.js';
const config = {
type: Phaser.AUTO,
width: GAME_WIDTH,
height: GAME_HEIGHT,
physics: {
default: 'arcade',
arcade: { debug: false }
},
scene: [MenuScene, GameScene, GameOverScene]
};
new Phaser.Game(config);
```
---
### 2.4 `js/scenes/GameScene.js`
```js
import Base from '../objects/Base.js';
import Bullet from '../objects/Bullet.js';
import Enemy from '../objects/Enemy.js';
import LevelManager from '../managers/LevelManager.js';
import XPManager from '../managers/XPManager.js';
import SpawnManager from '../managers/SpawnManager.js';
import UIManager from '../managers/UIManager.js';
import { ASSET_PATH } from '../config.js';
export default class GameScene extends Phaser.Scene {
constructor() { super('Game'); }
preload() {
// Spritesheets
this.load.spritesheet('base', `${ASSET_PATH}sprites/base_spritesheet.png`, { frameWidth:128, frameHeight:128 });
this.load.spritesheet('enemy', `${ASSET_PATH}sprites/enemy_spritesheet.png`, { frameWidth:96, frameHeight:96 });
this.load.image('bullet', `${ASSET_PATH}sprites/bullet.png`);
// Audio
this.load.audio('shoot', `${ASSET_PATH}audio/shoot.wav`);
this.load.audio('explosion', `${ASSET_PATH}audio/explosion.wav`);
// JSON data
this.load.json('levels', `${ASSET_PATH}data/levels.json`);
this.load.json('units', `${ASSET_PATH}data/units.json`);
}
create() {
// Managers
this.xpManager = new XPManager(this);
this.levelManager = new LevelManager(this, this.cache.json.get('levels'));
this.spawnManager = new SpawnManager(this, this.cache.json.get('units'));
this.uiManager = new UIManager(this, this.xpManager);
// Groups
this.enemies = this.physics.add.group();
this.bullets = this.physics.add.group();
// Base (center‑bottom)
const baseX = this.scale.width / 2;
const baseY = this.scale.height - 80;
this.base = new Base(this, baseX, baseY, 'base');
this.add.existing(this.base);
// Collisions
this.physics.add.overlap(this.bullets, this.enemies, this.handleBulletHit, null, this);
this.physics.add.overlap(this.enemies, this.base, this.handleEnemyReachBase, null, this);
// Start first level
this.levelManager.startLevel(1);
this.spawnManager.startSpawning(this.levelManager.currentConfig.spawnInterval);
}
update(time, delta) {
this.base.update(time, delta);
}
handleBulletHit(bullet, enemy) {
bullet.destroy();
enemy.takeDamage(bullet.damage);
if (enemy.isDead()) {
this.xpManager.addXP(enemy.xpValue);
enemy.destroy();
}
}
handleEnemyReachBase(enemy, base) {
base.takeDamage(enemy.collisionDamage || 20);
enemy.destroy();
if (base.isDestroyed()) {
this.scene.start('GameOver');
}
}
}
```
---
### 2.5 `js/objects/Base.js`
```js
import { BASE_FIRE_RATE, BASE_DAMAGE, BASE_HEALTH } from '../config.js';
import Bullet from './Bullet.js';
export default class Base extends Phaser.GameObjects.Sprite {
constructor(scene, x, y, texture) {
super(scene, x, y, texture, 0);
this.scene = scene;
this.health = BASE_HEALTH;
this.lastShot = 0;
this.setOrigin(0.5, 0.5);
this.setDepth(1);
}
update(time, delta) {
if (time > this.lastShot + BASE_FIRE_RATE) {
const target = this.findNearestEnemy();
if (target) {
this.shoot(target);
this.lastShot = time;
}
}
}
findNearestEnemy() {
const enemies = this.scene.enemies.getChildren();
if (!enemies.length) return null;
let nearest = null;
let minDist = Infinity;
enemies.forEach(e => {
const d = Phaser.Math.Distance.Between(this.x, this.y, e.x, e.y);
if (d < minDist) { minDist = d; nearest = e; }
});
return nearest;
}
shoot(target) {
const bullet = new Bullet(this.scene, this.x, this.y, 'bullet', target);
bullet.damage = BASE_DAMAGE;
this.scene.bullets.add(bullet);
this.scene.sound.play('shoot');
}
takeDamage(amount) {
this.health -= amount;
this.scene.uiManager.updateBaseHealth(this.health);
}
isDestroyed() { return this.health <= 0; }
}
```
---
### 2.6 `js/objects/Enemy.js`
```js
export default class Enemy extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y, texture, cfg) {
super(scene, x, y, texture, 0);
this.scene = scene;
this.health = cfg.health;
this.speed = cfg.speed;
this.xpValue = cfg.xp;
this.collisionDamage = cfg.collisionDamage || 20;
scene.physics.world.enable(this);
scene.add.existing(this);
this.setOrigin(0.5, 0.5);
}
preUpdate(time, delta) {
super.preUpdate(time, delta);
const base = this.scene.base;
const angle = Phaser.Math.Angle.Between(this.x, this.y, base.x, base.y);
this.scene.physics.velocityFromRotation(angle, this.speed, this.body.velocity);
}
takeDamage(amount) { this.health -= amount; }
isDead() { return this.health <= 0; }
}
```
---
### 2.7 `js/objects/Bullet.js`
```js
export default class Bullet extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y, texture, target) {
super(scene, x, y, texture);
scene.physics.world.enable(this);
scene.add.existing(this);
this.setDepth(2);
this.speed = 600;
const angle = Phaser.Math.Angle.Between(x, y, target.x, target.y);
scene.physics.velocityFromRotation(angle, this.speed, this.body.velocity);
this.body.setAllowGravity(false);
this.body.setCollideWorldBounds(false);
}
}
```
---
### 2.8 `js/managers/LevelManager.js`
```js
export default class LevelManager {
constructor(scene, levelData) {
this.scene = scene;
this.levels = levelData; // array of level objects
this.currentLevel = 1;
this.currentConfig = this.levels[0];
}
startLevel(num) {
this.currentLevel = num;
this.currentConfig = this.levels[num - 1];
// could broadcast an event for UI if desired
}
levelUp() {
const next = this.currentLevel + 1;
if (next <= this.levels.length) {
this.startLevel(next);
this.scene.spawnManager.updateSpawnInterval(this.currentConfig.spawnInterval);
}
}
}
```
---
### 2.9 `js/managers/XPManager.js`
```js
export default class XPManager extends Phaser.Events.EventEmitter {
constructor(scene) {
super();
this.scene = scene;
this.xp = 0;
this.level = 1;
this.nextLevelXP = this.calculateNextThreshold();
}
calculateNextThreshold() {
return Math.round(100 * Math.pow(this.level, 1.5)); // simple exponential curve
}
addXP(amount) {
this.xp += amount;
if (this.xp >= this.nextLevelXP) {
this.level++;
this.nextLevelXP = this.calculateNextThreshold();
this.emit('levelup', this.level);
}
this.scene.uiManager.updateXP(this.xp, this.nextLevelXP);
}
}
```
---
### 2.10 `js/managers/SpawnManager.js`
```js
import Enemy from '../objects/Enemy.js';
export default class SpawnManager {
constructor(scene, unitData) {
this.scene = scene;
this.unitDefs = unitData; // object keyed by enemy type
this.timer = null;
}
startSpawning(interval) {
this.timer = this.scene.time.addEvent({
delay: interval,
loop: true,
callback: this.spawnEnemy,
callbackScope: this
});
}
updateSpawnInterval(newInterval) {
if (this.timer) { this.timer.remove(); }
this.startSpawning(newInterval);
}
spawnEnemy() {
const levelCfg = this.scene.levelManager.currentConfig;
const type = Phaser.Utils.Array.GetRandom(levelCfg.enemyTypes);
const def = this.unitDefs[type];
const x = Phaser.Math.Between(50, this.scene.scale.width - 50);
const y = -def.height; // just above screen
const enemy = new Enemy(this.scene, x, y, 'enemy', def);
this.scene.enemies.add(enemy);
}
}
```
---
### 2.11 `js/managers/UIManager.js`
```js
export default class UIManager {
constructor(scene, xpManager) {
this.scene = scene;
this.xpManager = xpManager;
// Base health bar
this.healthBar = this.scene.add.graphics();
this.updateBaseHealth(100);
// XP text
this.xpText = this.scene.add.text(20, 20, 'XP: 0 / 100', { font: '20px Arial', fill: '#fff' });
// Level display
this.levelText = this.scene.add.text(20, 50, 'Level: 1', { font: '20px Arial', fill: '#fff' });
this.xpManager.on('levelup', lvl => this.levelText.setText(`Level: ${lvl}`));
}
updateBaseHealth(current) {
const max = 100; // same as BASE_HEALTH; could be dynamic later
const width = 300, height = 20;
const percent = Phaser.Math.Clamp(current / max, 0, 1);
const y = this.scene.scale.height - 40;
this.healthBar.clear();
this.healthBar.fillStyle(0x555555);
this.healthBar.fillRect(20, y, width, height);
this.healthBar.fillStyle(0xff0000);
this.healthBar.fillRect(20, y, width * percent, height);
}
updateXP(current, next) {
this.xpText.setText(`XP: ${current} / ${next}`);
}
}
```
---
### 2.12 Example JSON files
#### `js/data/levels.json`
```json
[
{
"level": 1,
"spawnInterval": 2000,
"enemyTypes": ["grunt"],
"enemyMultiplier": 1.0
},
{
"level": 2,
"spawnInterval": 1800,
"enemyTypes": ["grunt", "brute"],
"enemyMultiplier": 1.2
}
]
```
#### `js/data/units.json`
```json
{
"grunt": {
"health": 30,
"speed": 80,
"xp": 10,
"collisionDamage": 15,
"width": 96,
"height": 96
},
"brute": {
"health": 80,
"speed": 60,
"xp": 25,
"collisionDamage": 30,
"width": 96,
"height": 96
}
}
```
---
## 3️⃣ Game Flow Summary
1. Browser loads `index.html` → Phaser CDN → `js/main.js`.
2. `Phaser.Game` starts with the **MenuScene** (you can add a simple “Press Space to Start”).
3. On start, **GameScene** preloads assets and JSON data, creates managers, the base, and the enemy/bullet groups.
4. `SpawnManager` begins spawning enemies according to the current level’s config.
5. The base fires every `BASE_FIRE_RATE` ms at the nearest enemy.
6. Bullets damage enemies; dead enemies grant XP via `XPManager`.
7. When XP reaches the next threshold, `XPManager` emits `levelup`; `LevelManager` loads the next level, which may change spawn interval, enemy speed, etc.
8. If an enemy reaches the base, the base loses health. When health ≤ 0, the **GameOverScene** is launched.
9. UI (health bar, XP, level) updates in real time via `UIManager`.
---
## 4️⃣ Extensibility Roadmap (future features)
| Feature | Where to add | Files to modify |
|---------|--------------|-----------------|
| Enemy projectiles | New `EnemyBullet` class + collision with base | `objects/EnemyBullet.js`, update `Enemy.js` |
| Power‑ups / upgrades | New `PowerUp` class + manager | `objects/PowerUp.js`, `managers/PowerUpManager.js` |
| Multiple base turrets | Extend `Base` to hold turret objects | `objects/Turret.js` |
| Animated sprites | Define animations in `GameScene.create` using `this.anims.create` | `GameScene.js` |
| Soundtrack / ambient music | Load audio in `preload` and play loop in `create` | `GameScene.js` |
| Different difficulty curves | Add `difficulty.json` and let `LevelManager` read it | `managers/LevelManager.js` |
| Responsive scaling | Use `this.scale.resize` and adjust UI positions | `GameScene.js`, `UIManager.js` |
| Mobile touch controls | Add pointer events for aiming / UI | `GameScene.js` |
Because everything is modular, you can drop new files into the appropriate folder and import them without touching the core loop.
---
## 5️⃣ Getting Started Checklist
1. **Create the folder structure** exactly as shown.
2. **Copy each code block** into its corresponding file.
3. **Add your own art assets** (spritesheets, background, UI icons) into `/assets`.
4. **Populate `levels.json` and `units.json`** with the enemy types you want for the first few levels.
5. Open `index.html` in a modern desktop browser – you should see the base at the bottom, enemies spawning from the top, and the base shooting at the nearest enemy.
6. Tweak numbers (fire‑rate, health, enemy speed) in `config.js` or the JSON files until the difficulty feels right.
7. When you’re ready to expand, follow the **Extensibility Roadmap**.
---
*This markdown file (`BASE_DEFENSE_PLAN.md`) contains the full design, file‑by‑file skeleton, data format, game flow, and a roadmap for future features. Feel free to edit or extend any section as your project evolves.*

BIN
assets/audio/electro.mp3 Normal file

Binary file not shown.

BIN
assets/audio/shoot.mp3 Normal file

Binary file not shown.

20
assets/data/levels.json Normal file
View File

@ -0,0 +1,20 @@
[
{
"level": 1,
"spawnInterval": 2000,
"enemyTypes": ["grunt"],
"enemyMultiplier": 1.0
},
{
"level": 2,
"spawnInterval": 1800,
"enemyTypes": ["grunt", "brute"],
"enemyMultiplier": 1.2
},
{
"level": 3,
"spawnInterval": 1600,
"enemyTypes": ["grunt", "brute", "tank"],
"enemyMultiplier": 1.5
}
]

28
assets/data/units.json Normal file
View File

@ -0,0 +1,28 @@
{
"grunt": {
"health": 30,
"speed": 80,
"xp": 10,
"collisionDamage": 15,
"width": 96,
"height": 96
},
"brute": {
"health": 80,
"speed": 60,
"xp": 25,
"collisionDamage": 30,
"width": 96,
"height": 96
},
"tank": {
"health": 150,
"speed": 40,
"xp": 50,
"collisionDamage": 45,
"width": 128,
"height": 128
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

15
index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dystopian Base Defense</title>
<style>
body { margin:0; overflow:hidden; background:#000; }
canvas { display:block; margin:0 auto; }
</style>
<script src="https://cdn.jsdelivr.net/npm/phaser@3.55.2/dist/phaser.min.js"></script>
</head>
<body>
<script type="module" src="./js/main.js"></script>
</body>
</html>

6
js/config.js Normal file
View File

@ -0,0 +1,6 @@
export const GAME_WIDTH = 1600;
export const GAME_HEIGHT = 900;
export const BASE_HEALTH = 100; // starting health
export const BASE_FIRE_RATE = 2000; // ms between shots
export const BASE_DAMAGE = 20; // damage per bullet
export const ASSET_PATH = './assets/';

14
js/data/levels.json Normal file
View File

@ -0,0 +1,14 @@
[
{
"level": 1,
"spawnInterval": 2000,
"enemyTypes": ["grunt"],
"enemyMultiplier": 1.0
},
{
"level": 2,
"spawnInterval": 1800,
"enemyTypes": ["grunt", "brute"],
"enemyMultiplier": 1.2
}
]

18
js/data/units.json Normal file
View File

@ -0,0 +1,18 @@
{
"grunt": {
"health": 30,
"speed": 80,
"xp": 10,
"collisionDamage": 15,
"width": 96,
"height": 96
},
"brute": {
"health": 80,
"speed": 60,
"xp": 25,
"collisionDamage": 30,
"width": 96,
"height": 96
}
}

17
js/main.js Normal file
View File

@ -0,0 +1,17 @@
import { GAME_WIDTH, GAME_HEIGHT } from './config.js';
import MenuScene from './scenes/MenuScene.js';
import GameScene from './scenes/GameScene.js';
import GameOverScene from './scenes/GameOverScene.js';
const config = {
type: Phaser.AUTO,
width: GAME_WIDTH,
height: GAME_HEIGHT,
physics: {
default: 'arcade',
arcade: { debug: false }
},
scene: [MenuScene, GameScene, GameOverScene]
};
new Phaser.Game(config);

View File

@ -0,0 +1,22 @@
export default class LevelManager {
constructor(scene, levelData) {
this.scene = scene;
this.levels = levelData; // array of level objects
this.currentLevel = 1;
this.currentConfig = this.levels[0];
}
startLevel(num) {
this.currentLevel = num;
this.currentConfig = this.levels[num - 1];
// could broadcast an event for UI if desired
}
levelUp() {
const next = this.currentLevel + 1;
if (next <= this.levels.length) {
this.startLevel(next);
this.scene.spawnManager.updateSpawnInterval(this.currentConfig.spawnInterval);
}
}
}

View File

@ -0,0 +1,33 @@
import Enemy from '../objects/Enemy.js';
export default class SpawnManager {
constructor(scene, unitData) {
this.scene = scene;
this.unitDefs = unitData; // object keyed by enemy type
this.timer = null;
}
startSpawning(interval) {
this.timer = this.scene.time.addEvent({
delay: interval,
loop: true,
callback: this.spawnEnemy,
callbackScope: this
});
}
updateSpawnInterval(newInterval) {
if (this.timer) { this.timer.remove(); }
this.startSpawning(newInterval);
}
spawnEnemy() {
const levelCfg = this.scene.levelManager.currentConfig;
const type = Phaser.Utils.Array.GetRandom(levelCfg.enemyTypes);
const def = this.unitDefs[type];
const x = Phaser.Math.Between(50, this.scene.scale.width - 50);
const y = -def.height; // just above screen
const enemy = new Enemy(this.scene, x, y, 'enemy', def);
this.scene.enemies.add(enemy);
}
}

34
js/managers/UIManager.js Normal file
View File

@ -0,0 +1,34 @@
export default class UIManager {
constructor(scene, xpManager) {
this.scene = scene;
this.xpManager = xpManager;
// Base health bar
this.healthBar = this.scene.add.graphics();
this.updateBaseHealth(100);
// XP text
this.xpText = this.scene.add.text(20, 20, 'XP: 0 / 100', { font: '20px Arial', fill: '#fff' });
// Level display
this.levelText = this.scene.add.text(20, 50, 'Level: 1', { font: '20px Arial', fill: '#fff' });
this.xpManager.on('levelup', lvl => this.levelText.setText(`Level: ${lvl}`));
}
updateBaseHealth(current) {
const max = 100; // same as BASE_HEALTH; could be dynamic later
const width = 300, height = 20;
const percent = Phaser.Math.Clamp(current / max, 0, 1);
const y = this.scene.scale.height - 40;
this.healthBar.clear();
this.healthBar.fillStyle(0x555555);
this.healthBar.fillRect(20, y, width, height);
this.healthBar.fillStyle(0xff0000);
this.healthBar.fillRect(20, y, width * percent, height);
}
updateXP(current, next) {
this.xpText.setText(`XP: ${current} / ${next}`);
}
}

23
js/managers/XPManager.js Normal file
View File

@ -0,0 +1,23 @@
export default class XPManager extends Phaser.Events.EventEmitter {
constructor(scene) {
super();
this.scene = scene;
this.xp = 0;
this.level = 1;
this.nextLevelXP = this.calculateNextThreshold();
}
calculateNextThreshold() {
return Math.round(100 * Math.pow(this.level, 1.5)); // simple exponential curve
}
addXP(amount) {
this.xp += amount;
if (this.xp >= this.nextLevelXP) {
this.level++;
this.nextLevelXP = this.calculateNextThreshold();
this.emit('levelup', this.level);
}
this.scene.uiManager.updateXP(this.xp, this.nextLevelXP);
}
}

49
js/objects/Base.js Normal file
View File

@ -0,0 +1,49 @@
import { BASE_FIRE_RATE, BASE_DAMAGE, BASE_HEALTH } from '../config.js';
import Bullet from './Bullet.js';
export default class Base extends Phaser.GameObjects.Sprite {
constructor(scene, x, y, texture) {
super(scene, x, y, texture, 0);
this.scene = scene;
this.health = BASE_HEALTH;
this.lastShot = 0;
this.setOrigin(0.5, 0.5);
this.setDepth(1);
}
update(time, delta) {
if (time > this.lastShot + BASE_FIRE_RATE) {
const target = this.findNearestEnemy();
if (target) {
this.shoot(target);
this.lastShot = time;
}
}
}
findNearestEnemy() {
const enemies = this.scene.enemies.getChildren();
if (!enemies.length) return null;
let nearest = null;
let minDist = Infinity;
enemies.forEach(e => {
const d = Phaser.Math.Distance.Between(this.x, this.y, e.x, e.y);
if (d < minDist) { minDist = d; nearest = e; }
});
return nearest;
}
shoot(target) {
const bullet = new Bullet(this.scene, this.x, this.y, 'bullet', target);
bullet.damage = BASE_DAMAGE;
this.scene.bullets.add(bullet);
this.scene.sound.play('shoot');
}
takeDamage(amount) {
this.health -= amount;
this.scene.uiManager.updateBaseHealth(this.health);
}
isDestroyed() { return this.health <= 0; }
}

13
js/objects/Bullet.js Normal file
View File

@ -0,0 +1,13 @@
export default class Bullet extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y, texture, target) {
super(scene, x, y, texture);
scene.physics.world.enable(this);
scene.add.existing(this);
this.setDepth(2);
this.speed = 600;
const angle = Phaser.Math.Angle.Between(x, y, target.x, target.y);
scene.physics.velocityFromRotation(angle, this.speed, this.body.velocity);
this.body.setAllowGravity(false);
this.body.setCollideWorldBounds(false);
}
}

23
js/objects/Enemy.js Normal file
View File

@ -0,0 +1,23 @@
export default class Enemy extends Phaser.Physics.Arcade.Sprite {
constructor(scene, x, y, texture, cfg) {
super(scene, x, y, texture, 0);
this.scene = scene;
this.health = cfg.health;
this.speed = cfg.speed;
this.xpValue = cfg.xp;
this.collisionDamage = cfg.collisionDamage || 20;
scene.physics.world.enable(this);
scene.add.existing(this);
this.setOrigin(0.5, 0.5);
}
preUpdate(time, delta) {
super.preUpdate(time, delta);
const base = this.scene.base;
const angle = Phaser.Math.Angle.Between(this.x, this.y, base.x, base.y);
this.scene.physics.velocityFromRotation(angle, this.speed, this.body.velocity);
}
takeDamage(amount) { this.health -= amount; }
isDead() { return this.health <= 0; }
}

View File

@ -0,0 +1,10 @@
export default class GameOverScene extends Phaser.Scene {
constructor() { super('GameOver'); }
create() {
const { width, height } = this.scale;
this.add.text(width/2, height/2, 'Game Over\nPress SPACE to Restart', { font: '32px Arial', fill: '#f00', align: 'center' }).setOrigin(0.5);
this.input.keyboard.once('keydown-SPACE', () => {
this.scene.start('Game');
});
}
}

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

@ -0,0 +1,74 @@
import Base from '../objects/Base.js';
import Bullet from '../objects/Bullet.js';
import Enemy from '../objects/Enemy.js';
import LevelManager from '../managers/LevelManager.js';
import XPManager from '../managers/XPManager.js';
import SpawnManager from '../managers/SpawnManager.js';
import UIManager from '../managers/UIManager.js';
import { ASSET_PATH } from '../config.js';
export default class GameScene extends Phaser.Scene {
constructor() { super('Game'); }
preload() {
// Spritesheets
this.load.spritesheet('base', `${ASSET_PATH}sprites/base_spritesheet.png`, { frameWidth:128, frameHeight:128 });
this.load.spritesheet('enemy', `${ASSET_PATH}sprites/enemy_spritesheet.png`, { frameWidth:96, frameHeight:96 });
this.load.image('bullet', `${ASSET_PATH}sprites/bullet.png`);
// Audio
this.load.audio('shoot', `${ASSET_PATH}audio/shoot.mp3`);
this.load.audio('explosion', `${ASSET_PATH}audio/explosion.wav`);
// JSON data
this.load.json('levels', `${ASSET_PATH}data/levels.json`);
this.load.json('units', `${ASSET_PATH}data/units.json`);
}
create() {
// Managers
this.xpManager = new XPManager(this);
this.levelManager = new LevelManager(this, this.cache.json.get('levels'));
this.spawnManager = new SpawnManager(this, this.cache.json.get('units'));
this.uiManager = new UIManager(this, this.xpManager);
// Groups
this.enemies = this.physics.add.group();
this.bullets = this.physics.add.group();
// Base (center‑bottom)
const baseX = this.scale.width / 2;
const baseY = this.scale.height - 80;
this.base = new Base(this, baseX, baseY, 'base');
this.add.existing(this.base);
// Collisions
this.physics.add.overlap(this.bullets, this.enemies, this.handleBulletHit, null, this);
this.physics.add.overlap(this.enemies, this.base, this.handleEnemyReachBase, null, this);
// Start first level
this.levelManager.startLevel(1);
this.spawnManager.startSpawning(this.levelManager.currentConfig.spawnInterval);
}
update(time, delta) {
this.base.update(time, delta);
}
handleBulletHit(bullet, enemy) {
bullet.destroy();
enemy.takeDamage(bullet.damage);
if (enemy.isDead()) {
this.xpManager.addXP(enemy.xpValue);
enemy.destroy();
}
}
handleEnemyReachBase(enemy, base) {
base.takeDamage(enemy.collisionDamage || 20);
enemy.destroy();
if (base.isDestroyed()) {
this.scene.start('GameOver');
}
}
}

11
js/scenes/MenuScene.js Normal file
View File

@ -0,0 +1,11 @@
export default class MenuScene extends Phaser.Scene {
constructor() { super('Menu'); }
preload() {}
create() {
const { width, height } = this.scale;
this.add.text(width/2, height/2, 'Press SPACE to Start', { font: '32px Arial', fill: '#fff' }).setOrigin(0.5);
this.input.keyboard.once('keydown-SPACE', () => {
this.scene.start('Game');
});
}
}

1
start_web.bat Normal file
View File

@ -0,0 +1 @@
python -m http.server 8000

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