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:
parent
dc1eef50dc
commit
3fa2e3b596
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export const TOWERS_CONFIG = {
|
||||
'gun': {
|
||||
'spriteStart': 0,
|
||||
'level1': {
|
||||
'dmgLow': 10,
|
||||
'dmgHigh': 30,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ export const WAVE_CONFIG = {
|
|||
},
|
||||
2: {
|
||||
begin: 15,
|
||||
basic1: 1
|
||||
basic1: 20
|
||||
},
|
||||
3: {
|
||||
begin: 30,
|
||||
|
|
|
|||
Loading…
Reference in New Issue