feat(towers): Implement ICBM tower with unique animation and explosion effects

This commit adds a new ICBM tower type with distinct visual effects and attack behavior. Key changes include:

- Added ICBM tower configuration with 3 levels, damage, range, and AOE properties
- Implemented custom animation system for tower firing sequences
- Created specialized icbmAttackTarget method with missile launch and explosion effects
- Updated tower rendering logic to handle ICBM's unique sprite setup (base + top)
- Modified enemy targeting logic to exclude ICBM from normal rotation behavior
- Added new animation definitions for ICBM explosion effects
- Adjusted interface manager to display correct sprites for ICBM tower in menus

The ICBM tower features a missile launch animation, AOE damage on impact, and explosion visual effects that distinguish it from other tower types.
This commit is contained in:
Brian Fertig 2025-09-05 14:56:07 -06:00
parent b6bdaf8645
commit b4c806cb83
7 changed files with 272 additions and 102 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

View File

@ -35,13 +35,13 @@ export class Enemies {
// Create Animations // Create Animations
if (this.type.indexOf('basic') === 0) { if (this.type.indexOf('basic') === 0) {
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+8); 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);
} }
if (this.type.indexOf('advanced') === 0) { if (this.type.indexOf('advanced') === 0) {
this.createAnim('side', ENEMIES_CONFIG[this.type].spriteStart+3, ENEMIES_CONFIG[this.type].spriteStart+5); this.createAnim('side', ENEMIES_CONFIG[this.type].spriteStart+3, ENEMIES_CONFIG[this.type].spriteStart+5);
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+8);
this.createAnim('down', ENEMIES_CONFIG[this.type].spriteStart, ENEMIES_CONFIG[this.type].spriteStart+2); this.createAnim('down', ENEMIES_CONFIG[this.type].spriteStart, ENEMIES_CONFIG[this.type].spriteStart+2);
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);
} }

View File

@ -4,7 +4,7 @@ export class InterfaceManager {
constructor(scene) { constructor(scene) {
this.scene = scene; this.scene = scene;
this.gold = 150; this.gold = 1150;
this.cores = 20; this.cores = 20;
this.interfaceOpen = false; this.interfaceOpen = false;
this.selectedTower = false; this.selectedTower = false;
@ -315,16 +315,28 @@ export class InterfaceManager {
} }
}).setOrigin(1, 0).setScrollFactor(0); }).setOrigin(1, 0).setScrollFactor(0);
slot.add(slotCost); slot.add(slotCost);
console.log(type);
if (type === 'icbm') {
const towerBase = this.scene.add.sprite(startX, startY, 'towers', TOWERS_CONFIG[type].spriteStart)
.setOrigin(0.5)
.setScrollFactor(0);
slot.add(towerBase);
const towerBase = this.scene.add.sprite(startX, startY, 'towers', 7) const towerTop = this.scene.add.sprite(startX, startY, 'towers', TOWERS_CONFIG[type].spriteStart+3)
.setOrigin(0.5) .setOrigin(0.5)
.setScrollFactor(0); .setScrollFactor(0);
slot.add(towerBase); slot.add(towerTop);
} else {
const towerBase = this.scene.add.sprite(startX, startY, 'towers', 7)
.setOrigin(0.5)
.setScrollFactor(0);
slot.add(towerBase);
const towerTop = this.scene.add.sprite(startX, startY, 'towers', TOWERS_CONFIG[type].spriteStart) const towerTop = this.scene.add.sprite(startX, startY, 'towers', TOWERS_CONFIG[type].spriteStart)
.setOrigin(0.5) .setOrigin(0.5)
.setScrollFactor(0); .setScrollFactor(0);
slot.add(towerTop); slot.add(towerTop);
}
this.towerDisplay.add(slot); this.towerDisplay.add(slot);
this.updateTowerAvail(); this.updateTowerAvail();
@ -382,17 +394,31 @@ export class InterfaceManager {
repeat: -1 repeat: -1
}); });
const towerBase = this.scene.add.sprite(0, 0, 'towers', 7) if (type === 'icbm') {
.setOrigin(0.5) const towerBase = this.scene.add.sprite(0, 0, 'towers', TOWERS_CONFIG[type].spriteStart)
.setScrollFactor(0) .setOrigin(0.5)
.setTint(0xa32a00); .setScrollFactor(0)
this.selectedTower.add(towerBase); .setTint(0xa32a00);
this.selectedTower.add(towerBase);
const towerTop = this.scene.add.sprite(0, 0, 'towers', TOWERS_CONFIG[type].spriteStart)
.setOrigin(0.5) const towerTop = this.scene.add.sprite(0, 0, 'towers', TOWERS_CONFIG[type].spriteStart+3)
.setScrollFactor(0) .setOrigin(0.5)
.setTint(0xa32a00); .setScrollFactor(0)
this.selectedTower.add(towerTop); .setTint(0xa32a00);
this.selectedTower.add(towerTop);
} else {
const towerBase = this.scene.add.sprite(0, 0, 'towers', 7)
.setOrigin(0.5)
.setScrollFactor(0)
.setTint(0xa32a00);
this.selectedTower.add(towerBase);
const towerTop = this.scene.add.sprite(0, 0, 'towers', TOWERS_CONFIG[type].spriteStart)
.setOrigin(0.5)
.setScrollFactor(0)
.setTint(0xa32a00);
this.selectedTower.add(towerTop);
}
towerInteractive.on('pointerdown', () => { towerInteractive.on('pointerdown', () => {
this.placeTower(type); this.placeTower(type);

76
src/support/towerAnims.js Normal file
View File

@ -0,0 +1,76 @@
export class TowerAnims {
constructor(scene) {
this.scene = scene;
this.createAnims();
}
createAnims() {
this.scene.anims.create({
key: 'gunfire',
frames: this.scene.anims.generateFrameNumbers('ammo', {
start:0,
end:2
}),
framerate: 15,
repeat: -1
});
this.scene.anims.create({
key: 'gun-level1-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 1,
end: 0,
}),
frameRate: 15,
repeat: -1,
});
this.scene.anims.create({
key: 'gun-level2-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 3,
end: 2,
}),
frameRate: 15,
repeat: -1,
});
this.scene.anims.create({
key: 'gun-level3-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 5,
end: 4,
}),
frameRate: 15,
repeat: -1,
yoyo: true
});
this.scene.anims.create({
key: 'cannon-level1-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 11,
end: 10
}),
frameRate: 5,
repeat: 0
});
this.scene.anims.create({
key: 'flamethrower',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 23,
end:25
}),
framerate: 4,
repeat: -1
});
this.scene.anims.create({
key: 'icbmExplosion',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 34,
end: 37,
framerate: 4,
}),
framerate: 4,
repeat: -1
})
}
}

View File

@ -9,7 +9,8 @@ export const TOWERS_CONFIG = {
'dmgHigh': 30, 'dmgHigh': 30,
'rate': 2000, 'rate': 2000,
'duration': 500, 'duration': 500,
'range': 300 'range': 300,
'anim': 'gun-level1-fire'
}, },
'level2': { 'level2': {
'sprite': 2, 'sprite': 2,
@ -18,7 +19,8 @@ export const TOWERS_CONFIG = {
'dmgHigh': 35, 'dmgHigh': 35,
'rate': 1500, 'rate': 1500,
'duration': 750, 'duration': 750,
'range': 350 'range': 350,
'anim': 'gun-level2-fire'
}, },
'level3': { 'level3': {
'sprite': 4, 'sprite': 4,
@ -27,7 +29,8 @@ export const TOWERS_CONFIG = {
'dmgHigh': 50, 'dmgHigh': 50,
'rate': 1000, 'rate': 1000,
'duration': 1000, 'duration': 1000,
'range': 400 'range': 400,
'anim': 'gun-level3-fire'
} }
}, },
'cannon': { 'cannon': {
@ -42,6 +45,7 @@ export const TOWERS_CONFIG = {
'duration': 200, 'duration': 200,
'range': 400, 'range': 400,
'aoe': 50, 'aoe': 50,
'anim': 'cannon-level1-fire'
}, },
'level2': { 'level2': {
'sprite': 12, 'sprite': 12,
@ -52,6 +56,7 @@ export const TOWERS_CONFIG = {
'duration': 500, 'duration': 500,
'range': 450, 'range': 450,
'aoe': 25, 'aoe': 25,
'anim': 'cannon-level1-fire'
}, },
'level3': { 'level3': {
'sprite': 14, 'sprite': 14,
@ -62,6 +67,7 @@ export const TOWERS_CONFIG = {
'duration': 500, 'duration': 500,
'range': 500, 'range': 500,
'aoe': 25, 'aoe': 25,
'anim': 'cannon-level1-fire'
} }
}, },
'flame': { 'flame': {
@ -76,6 +82,7 @@ export const TOWERS_CONFIG = {
'duration': 1000, 'duration': 1000,
'range': 300, 'range': 300,
'aoe': 100, 'aoe': 100,
'anim': 'flamethrower'
}, },
'level2': { 'level2': {
'sprite': 21, 'sprite': 21,
@ -86,6 +93,7 @@ export const TOWERS_CONFIG = {
'duration': 1000, 'duration': 1000,
'range': 350, 'range': 350,
'aoe': 100, 'aoe': 100,
'anim': 'flamethrower'
}, },
'level3': { 'level3': {
'sprite': 22, 'sprite': 22,
@ -96,6 +104,42 @@ export const TOWERS_CONFIG = {
'duration': 1000, 'duration': 1000,
'range': 400, 'range': 400,
'aoe': 100, 'aoe': 100,
'anim': 'flamethrower'
}
},
'icbm': {
'name': 'ICBM Launcher',
'cost': 500,
'spriteStart': 30,
'dmgType': 'aoe',
'level1': {
'dmgLow': 40,
'dmgHigh': 50,
'rate': 10000,
'duration': 2000,
'range': 800,
'aoe': 250,
'anim': 'none'
},
'level2': {
'cost': 1000,
'dmgLow': 40,
'dmgHigh': 50,
'rate': 8000,
'duration': 2000,
'range': 800,
'aoe': 250,
'anim': 'none'
},
'level3': {
'cost': 2000,
'dmgLow': 50,
'dmgHigh': 60,
'rate': 5000,
'duration': 2000,
'range': 800,
'aoe': 250,
'anim': 'none'
} }
} }
} }

View File

@ -1,5 +1,6 @@
import { ENEMIES_CONFIG } from './enemiesConfig.js'; import { ENEMIES_CONFIG } from './enemiesConfig.js';
import { TOWERS_CONFIG } from './towerConfig.js'; import { TOWERS_CONFIG } from './towerConfig.js';
import { TowerAnims } from './towerAnims.js';
export class TowerManager { export class TowerManager {
@ -10,7 +11,7 @@ export class TowerManager {
this.selectedTower = null; this.selectedTower = null;
this.upgradeCost = null; this.upgradeCost = null;
this.createAnims(); this.towerAnims = new TowerAnims(scene);
// Add global click handler to close upgrade menu // Add global click handler to close upgrade menu
this.scene.input.on('pointerdown', (pointer) => { this.scene.input.on('pointerdown', (pointer) => {
@ -23,12 +24,23 @@ export class TowerManager {
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);
let towerBase;
let tower;
const towerBase = this.scene.add.sprite(posX, posY, 'towers', 7).setDepth(10); if (type === 'icbm') {
const tower = this.scene.add.sprite(posX, posY, 'towers', TOWERS_CONFIG[type].spriteStart).setDepth(11); towerBase = this.scene.add.sprite(posX, posY, 'towers', TOWERS_CONFIG[type].spriteStart).setDepth(10);
tower.props = { tower = this.scene.add.sprite(posX, posY, 'towers', TOWERS_CONFIG[type].spriteStart+3).setDepth(11);
'type': type, tower.props = {
'level': 1 'type': type,
'level': 1
}
} else {
towerBase = this.scene.add.sprite(posX, posY, 'towers', 7).setDepth(10);
tower = this.scene.add.sprite(posX, posY, 'towers', TOWERS_CONFIG[type].spriteStart).setDepth(11);
tower.props = {
'type': type,
'level': 1
}
} }
// Generate unique ID for enemy // Generate unique ID for enemy
@ -39,7 +51,6 @@ export class TowerManager {
tower.setInteractive(); tower.setInteractive();
tower.on('pointerdown', () => { tower.on('pointerdown', () => {
if (!this.selectedTower) { if (!this.selectedTower) {
console.log('CLICK');
this.upgradeMenu(tower); this.upgradeMenu(tower);
} }
}); });
@ -188,11 +199,13 @@ export class TowerManager {
if (this.following.hasOwnProperty(tower.id)) { if (this.following.hasOwnProperty(tower.id)) {
this.scene.enemies.children.iterate((enemy) => { this.scene.enemies.children.iterate((enemy) => {
if (this.following[tower.id] === enemy.props.id) { if (this.following[tower.id] === enemy.props.id
// Rotate tower to face the enemy && type !== 'icbm')
const angle = Phaser.Math.Angle.Between(towerX, towerY, enemy.x, enemy.y); {
tower.rotation = angle; // Rotate tower to face the enemy
} const angle = Phaser.Math.Angle.Between(towerX, towerY, enemy.x, enemy.y);
tower.rotation = angle;
}
}); });
} }
@ -229,10 +242,14 @@ export class TowerManager {
this.following[tower.id] = furthestEnemy.props.id; this.following[tower.id] = furthestEnemy.props.id;
const dmgType = TOWERS_CONFIG[tower.props.type].dmgType; const dmgType = TOWERS_CONFIG[tower.props.type].dmgType;
if (dmgType === 'aoe') { if (tower.props.type === 'icbm') {
this.aoeAttackTarget(tower, furthestEnemy); this.icbmAttackTarget(tower, furthestEnemy);
} else { } else {
this.attackTarget(tower, furthestEnemy); if (dmgType === 'aoe') {
this.aoeAttackTarget(tower, furthestEnemy);
} else {
this.attackTarget(tower, furthestEnemy);
}
} }
this.lastFired[tower.id] = time; this.lastFired[tower.id] = time;
@ -259,8 +276,11 @@ export class TowerManager {
const angle = Phaser.Math.Angle.Between(tower.x, tower.y, enemy.x, enemy.y); const angle = Phaser.Math.Angle.Between(tower.x, tower.y, enemy.x, enemy.y);
tower.rotation = angle; tower.rotation = angle;
tower.play(`${type}-${level}-fire`, { 'duration': duration }); const vid = tower.play(config.anim);
this.scene.time.delayedCall(config.duration, () => {
vid.stop();
});
// 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);
@ -362,6 +382,70 @@ export class TowerManager {
}); });
} }
icbmAttackTarget(tower, enemy) {
// Tower Properties
const type = tower.props.type;
const level = 'level'+tower.props.level;
const config = TOWERS_CONFIG[type][level];
const duration = TOWERS_CONFIG[type][level].duration;
if (!config) return;
const dmgLow = config.dmgLow;
const dmgHigh = config.dmgHigh;
const fireMissle = this.scene.add.sprite(tower.x, tower.y, 'towers', 33).setDepth(12);
tower.setVisible(false);
this.scene.tweens.add({
targets: fireMissle,
x: fireMissle.x,
y: fireMissle.y - 1200,
duration: 2000,
ease: 'Cubic.easeIn',
onComplete: () => {
fireMissle.setFlipY(true);
fireMissle.x = enemy.x;
this.scene.tweens.add({
targets: fireMissle,
x: enemy.x,
y: enemy.y,
duration: 500,
ease: 'Cubic.easeIn',
onComplete: () => {
fireMissle.destroy();
tower.setVisible(true);
const damage = Phaser.Math.Between(dmgLow, dmgHigh);
const aoeDistance = config.aoe;
const enemiesInRange = this.scene.enemies.getChildren().filter(e => {
return Phaser.Math.Distance.Between(e.x, e.y, enemy.x, enemy.y) <= aoeDistance;
});
const explosion = this.scene.add.sprite(enemy.x, enemy.y, 'towers', 34).setDepth(12);
explosion.play('icbmExplosion');
this.scene.tweens.add({
targets:explosion,
duration: 1000,
scale: 3,
alpha: .2,
onComplete: () => {
explosion.destroy();
}
});
enemiesInRange.forEach(targetEnemy => {
targetEnemy.props.health -= damage;
if (targetEnemy.props.health > 0) {
this.createHealthBar(targetEnemy);
} else {
this.destroyEnemy(targetEnemy, tower);
}
});
}
});
}
});
}
createHealthBar(enemy) { createHealthBar(enemy) {
const barWidth = 30; const barWidth = 30;
const barHeight = 5; const barHeight = 5;
@ -465,64 +549,4 @@ export class TowerManager {
this.following[tower.id] = null; this.following[tower.id] = null;
} }
createAnims() {
this.scene.anims.create({
key: 'gunfire',
frames: this.scene.anims.generateFrameNumbers('ammo', {
start:0,
end:2
}),
framerate: 15,
repeat: -1
});
this.scene.anims.create({
key: 'gun-level1-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 0,
end: 1,
}),
frameRate: 15,
repeat: 6,
yoyo: true
});
this.scene.anims.create({
key: 'gun-level2-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 2,
end: 3,
}),
frameRate: 15,
repeat: 10,
yoyo: true
});
this.scene.anims.create({
key: 'gun-level3-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 4,
end: 5,
}),
frameRate: 15,
repeat: 15,
yoyo: true
});
this.scene.anims.create({
key: 'cannon-level1-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 11,
end: 10
}),
frameRate: 5,
repeat: 0
});
this.scene.anims.create({
key: 'flamethrower',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 23,
end:25
}),
framerate: 4,
repeat: -1
})
}
} }