feat(towers): Implement tower system with targeting, firing animations, and health bars

- Added tower spritesheet loading and tower creation functionality
- Implemented tower targeting logic to follow enemies based on distance traveled
- Added firing animations for towers with proper rotation towards targets
- Enhanced enemy management with unique IDs, health bars, and death animations
- Updated enemy configurations with reduced health values
- Modified wave spawn rates to increase difficulty progression
- Improved tower attack logic to handle multiple enemies in range properly

This commit introduces the core tower defense mechanics including tower placement, targeting, firing, and enemy management systems.
This commit is contained in:
Brian Fertig 2025-08-31 12:53:42 -06:00
parent dc1eef50dc
commit 3fa2e3b596
8 changed files with 142 additions and 76 deletions

BIN
assets/towers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/towers.psd Normal file

Binary file not shown.

View File

@ -13,11 +13,14 @@ export class Level1 extends Phaser.Scene {
preload() { preload() {
this.load.tilemapTiledJSON('level1', 'assets/level1.json'); this.load.tilemapTiledJSON('level1', 'assets/level1.json');
this.load.image('terrain', 'assets/terrain.png'); this.load.image('terrain', 'assets/terrain.png');
this.load.image('josh', 'assets/josh-life.png');
this.load.spritesheet('basic-enemies', 'assets/basic-enemies.png', { this.load.spritesheet('basic-enemies', 'assets/basic-enemies.png', {
frameWidth: 50, frameWidth: 50,
frameHeight: 50 frameHeight: 50
}); });
this.load.spritesheet('towers', 'assets/towers.png', {
frameHeight: 100,
frameWidth: 100
});
} }
create() { create() {
@ -33,6 +36,7 @@ export class Level1 extends Phaser.Scene {
this.enemies = this.physics.add.group(); this.enemies = this.physics.add.group();
this.towers = this.physics.add.group(); this.towers = this.physics.add.group();
this.towerManager.createTower('gun', 2, 1);
this.towerManager.createTower('gun', 4, 2); this.towerManager.createTower('gun', 4, 2);
this.physics.add.collider(this.enemies, this.mainLayer); this.physics.add.collider(this.enemies, this.mainLayer);

View File

@ -22,30 +22,37 @@ export class Enemies {
const randSpeed = Phaser.Math.Between(this.speedLow, this.speedHigh); const randSpeed = Phaser.Math.Between(this.speedLow, this.speedHigh);
const spawnX = (this.x * 200) + 100 + randX; const spawnX = (this.x * 200) + 100 + randX;
const spawnY = (this.y * 200) + 100 + randY; const spawnY = (this.y * 200) + 100 + randY;
// Create enemy and store reference
const enemy = this.scene.add.sprite(spawnX, spawnY, ENEMIES_CONFIG[this.type].spriteSheet, ENEMIES_CONFIG[this.type].spriteStart);
// Create Animations
this.createAnim('side', ENEMIES_CONFIG[this.type].spriteStart, ENEMIES_CONFIG[this.type].spriteStart+2);
this.createAnim('up', ENEMIES_CONFIG[this.type].spriteStart+6, ENEMIES_CONFIG[this.type].spriteStart+7);
this.createAnim('down', ENEMIES_CONFIG[this.type].spriteStart+3, ENEMIES_CONFIG[this.type].spriteStart+5);
this.createAnim('die', ENEMIES_CONFIG[this.type].spriteStart+8, ENEMIES_CONFIG[this.type].spriteStart+9, 0);
enemy.props = {
'offsetX': randX,
'offsetY': randY,
'path': this.path,
'pathPhase': 0,
'speed': randSpeed,
'health': ENEMIES_CONFIG[this.type].health,
'type': this.type,
'distanceTraveled': 0
};
this.scene.enemies.add(enemy); // Randomize Spawn Time a bit
this.scene.time.delayedCall(Phaser.Math.Between(0,2000), () => {
// Create enemy and store reference
const enemy = this.scene.add.sprite(spawnX, spawnY, ENEMIES_CONFIG[this.type].spriteSheet, ENEMIES_CONFIG[this.type].spriteStart);
// Create Animations
this.createAnim('side', ENEMIES_CONFIG[this.type].spriteStart, ENEMIES_CONFIG[this.type].spriteStart+2);
this.createAnim('up', ENEMIES_CONFIG[this.type].spriteStart+6, ENEMIES_CONFIG[this.type].spriteStart+7);
this.createAnim('down', ENEMIES_CONFIG[this.type].spriteStart+3, ENEMIES_CONFIG[this.type].spriteStart+5);
this.createAnim('die', ENEMIES_CONFIG[this.type].spriteStart+8, ENEMIES_CONFIG[this.type].spriteStart+9, 0);
// Generate unique ID for enemy
const uniqueId = Phaser.Math.Between(100000, 999999);
enemy.play(`${this.type}-side`); enemy.props = {
'offsetX': randX,
'offsetY': randY,
'path': this.path,
'pathPhase': 0,
'speed': randSpeed,
'health': ENEMIES_CONFIG[this.type].health,
'type': this.type,
'distanceTraveled': 0,
'id': uniqueId
};
this.scene.enemies.add(enemy);
enemy.play(`${this.type}-side`);
});
} }
createAnim(type, start, end, repeat = -1) { createAnim(type, start, end, repeat = -1) {

View File

@ -1,8 +1,8 @@
export const ENEMIES_CONFIG = { export const ENEMIES_CONFIG = {
'basic1': { 'basic1': {
'spread': 25, 'spread': 25,
'health': 100, 'health': 25,
'fullHealth': 100, 'fullHealth': 25,
'speedLow': 25, 'speedLow': 25,
'speedHigh': 35, 'speedHigh': 35,
'spriteStart': 0, 'spriteStart': 0,
@ -12,8 +12,8 @@ export const ENEMIES_CONFIG = {
}, },
'basic2': { 'basic2': {
'spread': 0, 'spread': 0,
'health': 300, 'health': 50,
'fullHealth': 100, 'fullHealth': 50,
'speedLow': 45, 'speedLow': 45,
'speedHigh': 55, 'speedHigh': 55,
'spriteStart': 0, 'spriteStart': 0,

View File

@ -1,5 +1,6 @@
export const TOWERS_CONFIG = { export const TOWERS_CONFIG = {
'gun': { 'gun': {
'spriteStart': 0,
'level1': { 'level1': {
'dmgLow': 10, 'dmgLow': 10,
'dmgHigh': 30, 'dmgHigh': 30,

View File

@ -1,3 +1,4 @@
import { ENEMIES_CONFIG } from './enemiesConfig.js';
import { TOWERS_CONFIG } from './towerConfig.js'; import { TOWERS_CONFIG } from './towerConfig.js';
export class TowerManager { export class TowerManager {
@ -5,27 +6,50 @@ export class TowerManager {
constructor(scene) { constructor(scene) {
this.scene = scene; this.scene = scene;
this.lastFired = {}; // Track last fire time for each tower this.lastFired = {}; // Track last fire time for each tower
this.following = {};
this.createAnims();
} }
createTower(type, x, y) { createTower(type, x, y) {
const posX = this.scene.gridToLocation(x); const posX = this.scene.gridToLocation(x);
const posY = this.scene.gridToLocation(y); const posY = this.scene.gridToLocation(y);
const tower = this.scene.add.image(posX, posY, 'josh'); const tower = this.scene.add.sprite(posX, posY, 'towers', TOWERS_CONFIG[type].spriteStart);
tower.props = { tower.props = {
'type': type, 'type': type,
'level': 1 'level': 1
} }
// Generate unique ID for enemy
const uniqueId = Phaser.Math.Between(100000, 999999);
tower.id = uniqueId;
tower.setScale(1.5);
this.scene.towers.add(tower); this.scene.towers.add(tower);
// Draw range circle // Draw range circle
const config = TOWERS_CONFIG[type].level1; const config = TOWERS_CONFIG[type].level1;
if (config) { if (config) {
const range = config.range; const range = config.range;
const circle = this.scene.add.circle(posX, posY, range, 0x00ff00, 0.2); tower.showRange = this.scene.add.circle(posX, posY, range, 0x00ff00, 0);
} }
} }
createAnims() {
this.scene.anims.create({
key: 'gun-level1-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 0,
end: 1,
}),
frameRate: 15,
duration: 500,
repeat: 1,
yoyo: true
});
}
update(time, delta) { update(time, delta) {
// Iterate through all towers // Iterate through all towers
this.scene.towers.children.iterate((tower) => { this.scene.towers.children.iterate((tower) => {
@ -36,6 +60,16 @@ export class TowerManager {
const type = tower.props.type; const type = tower.props.type;
const level = 'level'+tower.props.level; const level = 'level'+tower.props.level;
const config = TOWERS_CONFIG[type][level]; const config = TOWERS_CONFIG[type][level];
if (this.following.hasOwnProperty(tower.id)) {
this.scene.enemies.children.iterate((enemy) => {
if (this.following[tower.id] === enemy.props.id) {
// Rotate tower to face the enemy
const angle = Phaser.Math.Angle.Between(towerX, towerY, enemy.x, enemy.y);
tower.rotation = angle;
}
});
}
if (!config) return; if (!config) return;
@ -47,7 +81,7 @@ export class TowerManager {
time - this.lastFired[tower.id] >= rate) { time - this.lastFired[tower.id] >= rate) {
// Check for enemies in range // Check for enemies in range
let inRange = false; let enemiesInRange = [];
this.scene.enemies.children.iterate((enemy) => { this.scene.enemies.children.iterate((enemy) => {
const distanceTraveled = enemy.props.distanceTraveled; const distanceTraveled = enemy.props.distanceTraveled;
const distance = Phaser.Math.Distance.Between( const distance = Phaser.Math.Distance.Between(
@ -55,22 +89,32 @@ export class TowerManager {
enemy.x, enemy.y enemy.x, enemy.y
); );
if (distance <= range) { if (distance <= range && enemy.props.health > 0) {
inRange = true; enemiesInRange.push(enemy);
this.attackTarget(tower, enemy);
return false; // Stop iterating once we find one enemy
} }
}); });
// Fire if enemies are in range // Fire if enemies are in range
if (inRange) { if (enemiesInRange.length > 0) {
console.log('fire');
// Find enemy with greatest distance traveled
const furthestEnemy = enemiesInRange.reduce((max, current) => {
return current.props.distanceTraveled > max.props.distanceTraveled ? current : max;
});
this.followTarget(tower, furthestEnemy);
this.attackTarget(tower, furthestEnemy);
this.lastFired[tower.id] = time; this.lastFired[tower.id] = time;
} }
} }
}); });
} }
followTarget(tower, enemy) {
this.following[tower.id] = enemy.props.id;
}
attackTarget(tower, enemy) { attackTarget(tower, enemy) {
// Tower Properties // Tower Properties
const type = tower.props.type; const type = tower.props.type;
@ -82,27 +126,18 @@ export class TowerManager {
const dmgLow = config.dmgLow; const dmgLow = config.dmgLow;
const dmgHigh = config.dmgHigh; const dmgHigh = config.dmgHigh;
// Enemy Information tower.play(`${type}-${level}-fire`, true);
const fullHealth = enemy.props.fullHealth;
const currentHealth = enemy.props.health;
// Calculate damage (random between low and high) // Calculate damage (random between low and high)
const damage = Phaser.Math.Between(dmgLow, dmgHigh); const damage = Phaser.Math.Between(dmgLow, dmgHigh);
// Apply damage to enemy // Apply damage to enemy
enemy.props.health -= damage; enemy.props.health -= damage;
// Create or update health bar if (enemy.props.health > 0) {
if (!enemy.healthBar) {
this.createHealthBar(enemy); this.createHealthBar(enemy);
} } else {
this.destroyEnemy(enemy, tower);
// Update health bar display
this.updateHealthBar(enemy);
// Check if enemy should be destroyed
if (enemy.props.health <= 0) {
this.destroyEnemy(enemy);
} }
} }
@ -111,9 +146,11 @@ export class TowerManager {
const barHeight = 5; const barHeight = 5;
const barX = enemy.x - barWidth/2; const barX = enemy.x - barWidth/2;
const barY = enemy.y - enemy.displayHeight/2 - 10; const barY = enemy.y - enemy.displayHeight/2 - 10;
const health = Math.max(enemy.props.health, 0);
const fullHealth = ENEMIES_CONFIG[enemy.props.type].fullHealth;
// Create health bar container // Create health bar container
enemy.healthBar = this.scene.add.container(enemy.x, enemy.y - enemy.displayHeight/2 - 10); const healthBar = this.scene.add.container(enemy.x, barY);
// Background bar (gray) // Background bar (gray)
const background = this.scene.add.rectangle(0, 0, barWidth, barHeight, 0x808080); const background = this.scene.add.rectangle(0, 0, barWidth, barHeight, 0x808080);
@ -122,33 +159,50 @@ export class TowerManager {
const healthFill = this.scene.add.rectangle(0, 0, barWidth, barHeight, 0x00ff00); const healthFill = this.scene.add.rectangle(0, 0, barWidth, barHeight, 0x00ff00);
// Position the fill relative to background // Position the fill relative to background
healthFill.x = -barWidth/2 + (barWidth * (enemy.props.health / enemy.props.fullHealth)) / 2; if (health === 0) {
healthFill.width = 0;
enemy.healthBar.add([background, healthFill]); } else {
} healthFill.width = (health / fullHealth) * barWidth;
updateHealthBar(enemy) {
if (!enemy.healthBar) return;
const barWidth = 30;
const healthPercentage = enemy.props.health / enemy.props.fullHealth;
// Update the fill width based on current health
enemy.healthBar.list[1].width = barWidth * healthPercentage;
// Position the container correctly
enemy.healthBar.x = enemy.x;
enemy.healthBar.y = enemy.y - enemy.displayHeight/2 - 10;
}
destroyEnemy(enemy) {
// Remove health bar if exists
if (enemy.healthBar) {
enemy.healthBar.destroy();
} }
healthBar.add([background, healthFill]);
this.scene.physics.world.enable(healthBar);
healthBar.body.setVelocity(enemy.body.velocity.x, enemy.body.velocity.y);
this.scene.tweens.add({
targets: healthBar,
delay: 1000,
alpha: 0,
onComplete: () => {
healthBar.destroy();
}
});
}
destroyEnemy(enemy, tower) {
const dieX = enemy.x;
const dieY = enemy.y;
const type = enemy.props.type;
const sprite = enemy.texture.key;
const firstFrame = enemy.texture.firstFrame;
const id = enemy.props.id;
// Destroy the enemy sprite
enemy.destroy(); enemy.destroy();
const deadEnemy = this.scene.add.sprite(dieX, dieY, sprite, firstFrame);
deadEnemy.play(`${type}-die`);
this.scene.tweens.add({
targets: deadEnemy,
delay: 5000,
duration: 2000,
alpha: 0,
onComplete: () => {
deadEnemy.destroy();
}
});
this.following[tower.id] = null;
} }
} }

View File

@ -15,7 +15,7 @@ export const WAVE_CONFIG = {
}, },
2: { 2: {
begin: 15, begin: 15,
basic1: 1 basic1: 20
}, },
3: { 3: {
begin: 30, begin: 30,