initial commit
This commit is contained in:
commit
3d9d3537b5
|
|
@ -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.*
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
@ -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.
|
|
@ -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>
|
||||
|
|
@ -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/';
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
[
|
||||
{
|
||||
"level": 1,
|
||||
"spawnInterval": 2000,
|
||||
"enemyTypes": ["grunt"],
|
||||
"enemyMultiplier": 1.0
|
||||
},
|
||||
{
|
||||
"level": 2,
|
||||
"spawnInterval": 1800,
|
||||
"enemyTypes": ["grunt", "brute"],
|
||||
"enemyMultiplier": 1.2
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
python -m http.server 8000
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Start a simple HTTP server on port 8000
|
||||
python3 -m http.server 8000
|
||||
Loading…
Reference in New Issue