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

View File

@ -23,29 +23,36 @@ export class Enemies {
const spawnX = (this.x * 200) + 100 + randX;
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);
// 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);
// 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
};
// Generate unique ID for enemy
const uniqueId = Phaser.Math.Between(100000, 999999);
this.scene.enemies.add(enemy);
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
};
enemy.play(`${this.type}-side`);
this.scene.enemies.add(enemy);
enemy.play(`${this.type}-side`);
});
}
createAnim(type, start, end, repeat = -1) {

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { ENEMIES_CONFIG } from './enemiesConfig.js';
import { TOWERS_CONFIG } from './towerConfig.js';
export class TowerManager {
@ -5,27 +6,50 @@ export class TowerManager {
constructor(scene) {
this.scene = scene;
this.lastFired = {}; // Track last fire time for each tower
this.following = {};
this.createAnims();
}
createTower(type, x, y) {
const posX = this.scene.gridToLocation(x);
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 = {
'type': type,
'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);
// Draw range circle
const config = TOWERS_CONFIG[type].level1;
if (config) {
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) {
// Iterate through all towers
this.scene.towers.children.iterate((tower) => {
@ -37,6 +61,16 @@ export class TowerManager {
const level = 'level'+tower.props.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;
const range = config.range;
@ -47,7 +81,7 @@ export class TowerManager {
time - this.lastFired[tower.id] >= rate) {
// Check for enemies in range
let inRange = false;
let enemiesInRange = [];
this.scene.enemies.children.iterate((enemy) => {
const distanceTraveled = enemy.props.distanceTraveled;
const distance = Phaser.Math.Distance.Between(
@ -55,22 +89,32 @@ export class TowerManager {
enemy.x, enemy.y
);
if (distance <= range) {
inRange = true;
this.attackTarget(tower, enemy);
return false; // Stop iterating once we find one enemy
if (distance <= range && enemy.props.health > 0) {
enemiesInRange.push(enemy);
}
});
// Fire if enemies are in range
if (inRange) {
console.log('fire');
if (enemiesInRange.length > 0) {
// 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;
}
}
});
}
followTarget(tower, enemy) {
this.following[tower.id] = enemy.props.id;
}
attackTarget(tower, enemy) {
// Tower Properties
const type = tower.props.type;
@ -82,9 +126,7 @@ export class TowerManager {
const dmgLow = config.dmgLow;
const dmgHigh = config.dmgHigh;
// Enemy Information
const fullHealth = enemy.props.fullHealth;
const currentHealth = enemy.props.health;
tower.play(`${type}-${level}-fire`, true);
// Calculate damage (random between low and high)
const damage = Phaser.Math.Between(dmgLow, dmgHigh);
@ -92,17 +134,10 @@ export class TowerManager {
// Apply damage to enemy
enemy.props.health -= damage;
// Create or update health bar
if (!enemy.healthBar) {
if (enemy.props.health > 0) {
this.createHealthBar(enemy);
}
// Update health bar display
this.updateHealthBar(enemy);
// Check if enemy should be destroyed
if (enemy.props.health <= 0) {
this.destroyEnemy(enemy);
} else {
this.destroyEnemy(enemy, tower);
}
}
@ -111,9 +146,11 @@ export class TowerManager {
const barHeight = 5;
const barX = enemy.x - barWidth/2;
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
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)
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);
// Position the fill relative to background
healthFill.x = -barWidth/2 + (barWidth * (enemy.props.health / enemy.props.fullHealth)) / 2;
enemy.healthBar.add([background, healthFill]);
}
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();
if (health === 0) {
healthFill.width = 0;
} else {
healthFill.width = (health / fullHealth) * barWidth;
}
// Destroy the enemy sprite
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;
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: {
begin: 15,
basic1: 1
basic1: 20
},
3: {
begin: 30,