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() {
|
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);
|
||||||
|
|
|
||||||
|
|
@ -23,29 +23,36 @@ export class Enemies {
|
||||||
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
|
// Randomize Spawn Time a bit
|
||||||
const enemy = this.scene.add.sprite(spawnX, spawnY, ENEMIES_CONFIG[this.type].spriteSheet, ENEMIES_CONFIG[this.type].spriteStart);
|
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
|
// Create Animations
|
||||||
this.createAnim('side', ENEMIES_CONFIG[this.type].spriteStart, ENEMIES_CONFIG[this.type].spriteStart+2);
|
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('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('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);
|
this.createAnim('die', ENEMIES_CONFIG[this.type].spriteStart+8, ENEMIES_CONFIG[this.type].spriteStart+9, 0);
|
||||||
|
|
||||||
enemy.props = {
|
// Generate unique ID for enemy
|
||||||
'offsetX': randX,
|
const uniqueId = Phaser.Math.Between(100000, 999999);
|
||||||
'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);
|
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) {
|
createAnim(type, start, end, repeat = -1) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
@ -37,6 +61,16 @@ export class TowerManager {
|
||||||
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;
|
||||||
|
|
||||||
const range = config.range;
|
const range = config.range;
|
||||||
|
|
@ -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,9 +126,7 @@ 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);
|
||||||
|
|
@ -92,17 +134,10 @@ export class TowerManager {
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
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: {
|
2: {
|
||||||
begin: 15,
|
begin: 15,
|
||||||
basic1: 1
|
basic1: 20
|
||||||
},
|
},
|
||||||
3: {
|
3: {
|
||||||
begin: 30,
|
begin: 30,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue