552 lines
19 KiB
JavaScript
552 lines
19 KiB
JavaScript
import { ENEMIES_CONFIG } from './enemiesConfig.js';
|
|
import { TOWERS_CONFIG } from './towerConfig.js';
|
|
import { TowerAnims } from './towerAnims.js';
|
|
|
|
export class TowerManager {
|
|
|
|
constructor(scene) {
|
|
this.scene = scene;
|
|
this.lastFired = {}; // Track last fire time for each tower
|
|
this.following = {};
|
|
this.selectedTower = null;
|
|
this.upgradeCost = null;
|
|
|
|
this.towerAnims = new TowerAnims(scene);
|
|
|
|
// Add global click handler to close upgrade menu
|
|
this.scene.input.on('pointerdown', (pointer) => {
|
|
if (this.selectedTower && !this.isPointerOverTower(pointer)) {
|
|
this.closeUpgradeMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
createTower(type, x, y) {
|
|
const posX = this.scene.gridToLocation(x);
|
|
const posY = this.scene.gridToLocation(y);
|
|
let towerBase;
|
|
let tower;
|
|
|
|
if (type === 'icbm') {
|
|
towerBase = this.scene.add.sprite(posX, posY, 'towers', TOWERS_CONFIG[type].spriteStart).setDepth(10);
|
|
tower = this.scene.add.sprite(posX, posY, 'towers', TOWERS_CONFIG[type].spriteStart+3).setDepth(11);
|
|
tower.props = {
|
|
'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
|
|
const uniqueId = Phaser.Math.Between(100000, 999999);
|
|
tower.id = uniqueId;
|
|
|
|
this.scene.towers.add(tower);
|
|
tower.setInteractive();
|
|
tower.on('pointerdown', () => {
|
|
if (!this.selectedTower) {
|
|
this.upgradeMenu(tower);
|
|
}
|
|
});
|
|
|
|
// Draw range circle
|
|
const config = TOWERS_CONFIG[type].level1;
|
|
if (config) {
|
|
const range = config.range;
|
|
tower.showRange = this.scene.add.circle(posX, posY, range, 0x00ff00, 0);
|
|
}
|
|
}
|
|
|
|
upgradeMenu(tower) {
|
|
this.selectedTower = tower;
|
|
const currentLevel = `level${tower.props.level}`;
|
|
const nextLevel = currentLevel === 'level3' ? 'level3' : `level${tower.props.level + 1}`;
|
|
|
|
this.upgradeDetails = this.scene.add.container();
|
|
|
|
const rangeCircle = this.scene.add.circle(tower.x, tower.y, TOWERS_CONFIG[tower.props.type][currentLevel].range, 0xc009900, 0.2)
|
|
.setOrigin(0.5)
|
|
.setDepth(5);
|
|
this.upgradeDetails.add(rangeCircle);
|
|
|
|
const centerToEdgeLine = this.scene.add.line(tower.x, tower.y, 0, 0, TOWERS_CONFIG[tower.props.type][currentLevel].range, 0, 0x00ff00)
|
|
.setOrigin(0)
|
|
.setDepth(6);
|
|
this.upgradeDetails.add(centerToEdgeLine);
|
|
|
|
this.scene.tweens.add({
|
|
targets: centerToEdgeLine,
|
|
angle: 360,
|
|
duration: 8000,
|
|
repeat: -1
|
|
});
|
|
|
|
const intUpgrade = this.scene.add.image(tower.x + 100, tower.y, 'intUpgrade')
|
|
.setDepth(12)
|
|
.setTint(0xAAAAAA)
|
|
.setOrigin(0, 0.5)
|
|
.setScale(0)
|
|
.setAlpha(0);
|
|
this.upgradeDetails.add(intUpgrade);
|
|
|
|
this.upgradeCost = TOWERS_CONFIG[tower.props.type][nextLevel].cost;
|
|
|
|
this.upgradeText = this.scene.add.text(tower.x +175, tower.y - 50, `Upgrade: ${this.upgradeCost}`, {
|
|
fontFamily: 'neuropol, arial',
|
|
fontSize: '32px',
|
|
fill: '#ffd900ff',
|
|
stroke: '#c48f00ff',
|
|
strokeThickness: 2,
|
|
shadow: {
|
|
offsetX: 5,
|
|
offsetY: 5,
|
|
color: '#000000',
|
|
blur: 5,
|
|
stroke: false,
|
|
fill: true
|
|
}
|
|
}).setDepth(13).setAlpha(0).setInteractive();
|
|
this.upgradeDetails.add(this.upgradeText);
|
|
|
|
if (currentLevel === 'level3') upgradeText.setText('Max Upgrade').disableInteractive();
|
|
|
|
const upgradeCancelText = this.scene.add.text(tower.x +175, tower.y + 25, `Cancel`, {
|
|
fontFamily: 'neuropol, arial',
|
|
fontSize: '32px',
|
|
fill: '#ff0000ff',
|
|
stroke: '#c40000ff',
|
|
strokeThickness: 2,
|
|
shadow: {
|
|
offsetX: 5,
|
|
offsetY: 5,
|
|
color: '#000000',
|
|
blur: 5,
|
|
stroke: false,
|
|
fill: true
|
|
}
|
|
}).setDepth(13).setAlpha(0).setInteractive();
|
|
this.upgradeDetails.add(upgradeCancelText);
|
|
|
|
upgradeCancelText.on('pointerdown', () => {
|
|
this.closeUpgradeMenu();
|
|
});
|
|
|
|
this.scene.tweens.add({
|
|
targets:[intUpgrade, this.upgradeText, upgradeCancelText],
|
|
scale: 1,
|
|
alpha: 1,
|
|
duration: 500
|
|
});
|
|
|
|
this.upgradeText.on('pointerdown', () => {
|
|
this.upgradeTower(tower);
|
|
});
|
|
|
|
if (this.scene.UIScene.interfaceManager.gold < TOWERS_CONFIG[tower.props.type][nextLevel].cost) {
|
|
this.disableUpgrade(this.upgradeText);
|
|
}
|
|
}
|
|
|
|
enableUpgrade(text) {
|
|
text.setInteractive().setTint(0xFFFFFF);
|
|
}
|
|
|
|
disableUpgrade(text) {
|
|
text.disableInteractive().setTint(0xAAAAAA);
|
|
}
|
|
|
|
isPointerOverTower(pointer) {
|
|
let isOverTower = false;
|
|
if (this.upgradeDetails.getBounds().contains(pointer.x, pointer.y)) {
|
|
isOverTower = true;
|
|
}
|
|
return isOverTower;
|
|
}
|
|
|
|
closeUpgradeMenu() {
|
|
if (this.upgradeDetails) {
|
|
this.upgradeDetails.destroy();
|
|
this.upgradeDetails = null;
|
|
}
|
|
this.selectedTower = null;
|
|
}
|
|
|
|
upgradeTower(tower) {
|
|
this.closeUpgradeMenu();
|
|
if (tower.props.level === 3) return;
|
|
tower.props.level++;
|
|
const newLevel = `level${tower.props.level}`
|
|
tower.setTexture('towers', TOWERS_CONFIG[tower.props.type][newLevel].sprite);
|
|
this.scene.UIScene.removeGold(TOWERS_CONFIG[tower.props.type][newLevel].cost);
|
|
}
|
|
|
|
update(time, delta) {
|
|
// Iterate through all towers
|
|
this.scene.towers.children.iterate((tower) => {
|
|
const towerX = tower.x;
|
|
const towerY = tower.y;
|
|
|
|
// Get tower config
|
|
const type = tower.props.type;
|
|
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
|
|
&& type !== 'icbm')
|
|
{
|
|
// 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;
|
|
const rate = config.rate;
|
|
|
|
// Check if enough time has passed since last fire
|
|
if (this.lastFired[tower.id] === undefined ||
|
|
time - this.lastFired[tower.id] >= rate) {
|
|
|
|
// Check for enemies in range
|
|
let enemiesInRange = [];
|
|
this.scene.enemies.children.iterate((enemy) => {
|
|
const distanceTraveled = enemy.props.distanceTraveled;
|
|
const distance = Phaser.Math.Distance.Between(
|
|
towerX, towerY,
|
|
enemy.x, enemy.y
|
|
);
|
|
|
|
if (distance <= range && enemy.props.health > 0) {
|
|
enemiesInRange.push(enemy);
|
|
}
|
|
});
|
|
|
|
// Fire if enemies are in range
|
|
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.following[tower.id] = furthestEnemy.props.id;
|
|
const dmgType = TOWERS_CONFIG[tower.props.type].dmgType;
|
|
if (tower.props.type === 'icbm') {
|
|
this.icbmAttackTarget(tower, furthestEnemy);
|
|
} else {
|
|
if (dmgType === 'aoe') {
|
|
this.aoeAttackTarget(tower, furthestEnemy);
|
|
} else {
|
|
this.attackTarget(tower, furthestEnemy);
|
|
}
|
|
}
|
|
|
|
this.lastFired[tower.id] = time;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (this.upgradeDetails && this.upgradeCost <= this.scene.UIScene.interfaceManager.gold) {
|
|
this.enableUpgrade(this.upgradeText);
|
|
}
|
|
}
|
|
|
|
attackTarget(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 angle = Phaser.Math.Angle.Between(tower.x, tower.y, enemy.x, enemy.y);
|
|
tower.rotation = angle;
|
|
|
|
const vid = tower.play(config.anim);
|
|
this.scene.time.delayedCall(config.duration, () => {
|
|
vid.stop();
|
|
});
|
|
|
|
// Calculate damage (random between low and high)
|
|
const damage = Phaser.Math.Between(dmgLow, dmgHigh);
|
|
|
|
// Apply damage to enemy
|
|
enemy.props.health -= damage;
|
|
|
|
if (enemy.props.health > 0) {
|
|
this.createHealthBar(enemy);
|
|
if (type === 'gun') {
|
|
const gunfire = this.scene.physics.add.sprite(enemy.x, enemy.y, 'ammo', 0);
|
|
gunfire.play('gunfire');
|
|
gunfire.setVelocity(enemy.body.velocity.x, enemy.body.velocity.y);
|
|
this.scene.time.delayedCall(config.duration, () => {
|
|
gunfire.destroy();
|
|
});
|
|
}
|
|
} else {
|
|
this.destroyEnemy(enemy, tower);
|
|
}
|
|
}
|
|
|
|
aoeAttackTarget(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 angle = Phaser.Math.Angle.Between(tower.x, tower.y, enemy.x, enemy.y);
|
|
tower.rotation = angle;
|
|
|
|
if (type === 'cannon') {
|
|
const projDistance = 50;
|
|
const projX = tower.x + Math.cos(angle) * projDistance;
|
|
const projY = tower.y + Math.sin(angle) * projDistance;
|
|
const projectile = this.scene.add.sprite(projX, projY, 'ammo', 3);
|
|
this.scene.tweens.add({
|
|
targets: projectile,
|
|
x: enemy.x,
|
|
y: enemy.y,
|
|
duration: 100,
|
|
onComplete: () => {
|
|
projectile.setTexture('ammo', 4);
|
|
this.scene.time.delayedCall(200, () => {
|
|
projectile.destroy();
|
|
});
|
|
}
|
|
});
|
|
tower.play(`${type}-${level}-fire`, { 'duration': duration });
|
|
}
|
|
|
|
if (type === 'flame') {
|
|
const projDistance = 185;
|
|
const projDistance2 = 220;
|
|
const projX = tower.x + Math.cos(angle) * projDistance;
|
|
const projY = tower.y + Math.sin(angle) * projDistance;
|
|
const proj2X = tower.x + Math.cos(angle) * projDistance2;
|
|
const proj2Y = tower.y + Math.sin(angle) * projDistance2;
|
|
const projectile = this.scene.add.sprite(projX, projY, 'towers', 23).setScale(1.5).setAlpha(.5);
|
|
projectile.rotation = angle;
|
|
projectile.play(`flamethrower`);
|
|
this.scene.tweens.add({
|
|
targets: projectile,
|
|
x: proj2X,
|
|
y: proj2Y,
|
|
duration:1000,
|
|
onComplete: () => {
|
|
projectile.destroy();
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
// Calculate damage (random between low and high)
|
|
const damage = Phaser.Math.Between(dmgLow, dmgHigh);
|
|
|
|
// Get AOE distance
|
|
const aoeDistance = config.aoe;
|
|
|
|
// Apply damage to all enemies in range
|
|
const enemiesInRange = this.scene.enemies.getChildren().filter(e => {
|
|
return Phaser.Math.Distance.Between(e.x, e.y, enemy.x, enemy.y) <= aoeDistance;
|
|
});
|
|
|
|
enemiesInRange.forEach(targetEnemy => {
|
|
targetEnemy.props.health -= damage;
|
|
|
|
if (targetEnemy.props.health > 0) {
|
|
this.createHealthBar(targetEnemy);
|
|
} else {
|
|
this.destroyEnemy(targetEnemy, tower);
|
|
}
|
|
});
|
|
}
|
|
|
|
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) {
|
|
const barWidth = 30;
|
|
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
|
|
const healthBar = this.scene.add.container(enemy.x, barY);
|
|
|
|
// Background bar (red)
|
|
const background = this.scene.add.rectangle(0, 0, barWidth, barHeight, 0xe83829);
|
|
|
|
// Health fill (green)
|
|
const healthFill = this.scene.add.rectangle(0, 0, barWidth, barHeight, 0x00ff00);
|
|
|
|
// Position the fill relative to background
|
|
if (health === 0) {
|
|
healthFill.width = 0;
|
|
} else {
|
|
healthFill.width = (health / fullHealth) * barWidth;
|
|
}
|
|
|
|
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 velocX = enemy.body.velocity.x;
|
|
const velocY = enemy.body.velocity.y;
|
|
|
|
// Add some Gold
|
|
const drop = Phaser.Math.Between(ENEMIES_CONFIG[type].dropLow, ENEMIES_CONFIG[type].dropHigh) * 5;
|
|
|
|
// Show the Gold
|
|
const cashDrop = this.scene.add.image(dieX, dieY, 'gold').setScale(0.5);
|
|
const angle = Phaser.Math.Angle.Between(tower.x, tower.y, dieX, dieY);
|
|
|
|
// Calculate end position (100px away from original position in the calculated direction)
|
|
const endX = dieX + Math.cos(angle) * 50;
|
|
const endY = dieY + Math.sin(angle) * 50;
|
|
|
|
// Get coords of Gold Interface
|
|
const cam = this.scene.cameras.main;
|
|
|
|
// Animate the cash drop moving to the new position
|
|
this.scene.tweens.add({
|
|
targets: cashDrop,
|
|
x: endX,
|
|
y: endY,
|
|
duration: 500,
|
|
ease: 'Bounce',
|
|
onComplete: () => {
|
|
cashDrop.postFX.addGlow();
|
|
this.scene.tweens.add({
|
|
targets: cashDrop,
|
|
x: 1300 + cam.scrollX,
|
|
y: 100 + cam.scrollY,
|
|
alpha: 0,
|
|
duration: 500,
|
|
onComplete: () => {
|
|
cashDrop.destroy();
|
|
this.scene.UIScene.addGold(drop);
|
|
this.scene.UIScene.interfaceManager.updateTowerAvail();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
} |