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 (purefile 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 **ESmodules** (`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 copypaste 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 (centerbottom)
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 levels 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` |
| Powerups / 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 (firerate, health, enemy speed) in `config.js` or the JSON files until the difficulty feels right.
7. When youre ready to expand, follow the **Extensibility Roadmap**.
---
*This markdown file (`BASE_DEFENSE_PLAN.md`) contains the full design, filebyfile 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 (centerbottom)
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