feat(towers): Implement tower placement system with selection preview, add cannon tower type, and integrate wave progression controls

This commit introduces a comprehensive tower placement system allowing players to select and place towers on valid platform tiles. Key changes include:
- Added tower selection and preview functionality with visual feedback (green for valid, red for invalid positions)
- Implemented cannon tower type with AOE damage capabilities
- Integrated next wave button with animation effects in the UI
- Modified wave progression to be controlled via interface manager instead of automatic timing
- Enhanced tower placement logic to snap to grid and validate platform collisions
- Updated gold management system with proper removal when placing towers

The changes enable players to strategically place multiple tower types while providing visual feedback during selection and integrating seamlessly with the existing wave progression system.
This commit is contained in:
Brian Fertig 2025-09-01 13:18:20 -06:00
parent cbd1d94bcd
commit 1233c2bf76
11 changed files with 289 additions and 59 deletions

BIN
assets/nextWave.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
assets/nextWave.psd Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

View File

@ -43,9 +43,6 @@ export class Level 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.physics.add.collider(this.enemies, this.mainLayer); this.physics.add.collider(this.enemies, this.mainLayer);
this.physics.add.collider(this.enemies, this.platformsLayer); this.physics.add.collider(this.enemies, this.platformsLayer);
} }

View File

@ -7,6 +7,11 @@ export class InterfaceManager {
this.gold = 100; this.gold = 100;
this.cores = 20; this.cores = 20;
this.interfaceOpen = false; this.interfaceOpen = false;
this.selectedTower = false;
this.selectedTowerPos = {
x: 0,
y: 0
}
this.paintInterface(); this.paintInterface();
} }
@ -123,18 +128,83 @@ export class InterfaceManager {
this.interfaceUp.on('pointerdown', () => { this.interfaceUp.on('pointerdown', () => {
if (!this.interfaceOpen) { if (!this.interfaceOpen) {
this.openInterface(); this.openInterface();
this.interfaceOpen = true;
// Disable further clicks // Disable further clicks
this.interfaceUp.disableInteractive(); this.interfaceUp.disableInteractive();
} else { } else {
this.closeInterface(); this.closeInterface();
this.interfaceOpen = false;
this.interfaceUp.disableInteractive(); this.interfaceUp.disableInteractive();
} }
}); });
// Next Wave Button
this.waveStart = this.scene.add.container().setScrollFactor(0);
const nextWaveBack = this.scene.add.rectangle(1250, 200, 235, 100, 0x000000, 1).setOrigin(0, 0.5).setScrollFactor(0);
this.buttonBack = this.scene.add.sprite(1250, 200, 'nextWave', 0).setOrigin(0.5).setScrollFactor(0);
this.buttonBack.on('pointerdown', () => {
const waveManager = this.scene.levelScene.waveManager
waveManager.waveActive = true;
waveManager.nextWave();
this.hideNextWave();
})
const button = this.scene.add.sprite(1250, 200, 'nextWave', 1).setOrigin(0.5).setScrollFactor(0);
button.preFX.addGlow(0xffd900, 10);
const nextWaveText = this.scene.add.text(1300, 200, 'Start\nNext Wave', {
fontFamily: 'neuropol, arial',
fontSize: '28px',
fill: '#ffd900ff',
stroke: '#c48f00ff',
strokeThickness: 2,
shadow: {
offsetX: 5,
offsetY: 5,
color: '#000000',
blur: 5,
stroke: false,
fill: true
}
}).setOrigin(0, 0.5).setScrollFactor(0);
this.scene.tweens.add({
targets: this.buttonBack,
angle: -360,
duration: 20000,
repeat: -1
})
this.scene.tweens.add({
targets: button.preFX.list[0],
outerStrength: 0,
duration: 2000,
yoyo: true,
repeat: -1
});
this.waveStart.add(nextWaveBack);
this.waveStart.add(this.buttonBack);
this.waveStart.add(button);
this.waveStart.add(nextWaveText);
this.waveStart.setAlpha(0);
}
hideNextWave() {
this.buttonBack.disableInteractive()
this.scene.tweens.add({
targets: this.waveStart,
alpha: 0,
duration: 500
});
}
showNextWave() {
this.buttonBack.setInteractive()
this.scene.tweens.add({
targets: this.waveStart,
alpha: 1,
duration: 1000
});
} }
openInterface() { openInterface() {
this.interfaceOpen = true;
this.upLeft.setAlpha(0).setRotation(Math.PI); this.upLeft.setAlpha(0).setRotation(Math.PI);
this.upRight.setAlpha(0).setRotation(Math.PI); this.upRight.setAlpha(0).setRotation(Math.PI);
this.interfaceText.setAlpha(0).setText('Click to Close'); this.interfaceText.setAlpha(0).setText('Click to Close');
@ -154,6 +224,7 @@ export class InterfaceManager {
} }
closeInterface() { closeInterface() {
this.interfaceOpen = false;
this.upLeft.setAlpha(0).setRotation(0); this.upLeft.setAlpha(0).setRotation(0);
this.upRight.setAlpha(0).setRotation(0); this.upRight.setAlpha(0).setRotation(0);
this.interfaceText.setAlpha(0).setText('Click to Open'); this.interfaceText.setAlpha(0).setText('Click to Open');
@ -177,7 +248,7 @@ export class InterfaceManager {
this.gridAdd(0, 0, 'Gatlin Gun', 100, 'gun'); this.gridAdd(0, 0, 'Gatlin Gun', 100, 'gun');
this.gridAdd(0, 1, 'Flamethrower', 150, 'gun'); this.gridAdd(0, 1, 'Flamethrower', 150, 'gun');
this.gridAdd(1, 0, 'Laser', 200, 'gun'); this.gridAdd(1, 0, 'Laser', 200, 'gun');
this.gridAdd(1, 1, 'Cannon', 200, 'gun'); this.gridAdd(1, 1, 'Cannon', 200, 'cannon');
} }
gridAdd(x, y, text, cost, type) { gridAdd(x, y, text, cost, type) {
@ -196,6 +267,9 @@ export class InterfaceManager {
.setScrollFactor(0); .setScrollFactor(0);
slot.add(slotBack); slot.add(slotBack);
slotBack.setInteractive(); slotBack.setInteractive();
slotBack.on('pointerdown', () => {
this.selectTower(type);
});
// Add gold text display // Add gold text display
const slotText = this.scene.add.text(startX-95, startY+75, `${text}`, { const slotText = this.scene.add.text(startX-95, startY+75, `${text}`, {
@ -276,7 +350,105 @@ export class InterfaceManager {
} }
} }
update(time, delta) { selectTower(type) {
this.selectedTower = this.scene.levelScene.add.container();
const towerInteractive = this.scene.add.rectangle(0, 0, 200, 200, 0x000000, 0);
this.selectedTower.add(towerInteractive);
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', () => {
this.placeTower(type);
});
towerInteractive.setInteractive();
this.closeInterface();
}
placeTower(type) {
if (this.selectedTower.safe === true) {
//Bring up Next Wave on first tower:
if (this.scene.levelScene.towers.countActive() === 0) {
this.showNextWave();
}
// Snap to current mouse position
const mouse = this.scene.input.activePointer;
const worldX = mouse.worldX;
const worldY = mouse.worldY;
// Convert world coordinates to tile coordinates
const tileX = Math.floor(worldX / 200); // Assuming 200px tile width
const tileY = Math.floor(worldY / 200); // Assuming 200px tile height
this.scene.levelScene.towerManager.createTower(type, tileX, tileY);
this.scene.removeGold(TOWERS_CONFIG[type].cost);
console.log(this.scene.levelScene.towers.countActive());
}
// Clear Tower Selection Regardless
this.selectedTower.destroy();
this.selectedTower = false;
}
update(time, delta) {
if (this.selectedTower) {
// Snap to current mouse position
const mouse = this.scene.input.activePointer;
const worldX = mouse.worldX;
const worldY = mouse.worldY;
// Convert world coordinates to tile coordinates
const tileX = Math.floor(worldX / 200); // Assuming 200px tile width
const tileY = Math.floor(worldY / 200); // Assuming 200px tile height
if (tileX !== this.selectedTowerPos.x || tileY !== this.selectedTowerPos.y) {
this.selectedTowerPos = {
x: tileX,
y: tileY
};
// Convert back to world position for proper snapping
const snappedX = tileX * 200 + 100; // Center the tower on the tile (assuming 200px tiles)
const snappedY = tileY * 200 + 100;
this.selectedTower.setPosition(snappedX, snappedY);
const platformsLayer = this.scene.levelScene.platformsLayer;
if (platformsLayer) {
const tile = platformsLayer.getTileAt(tileX, tileY);
if (tile) {
this.selectedTower.iterate((child) => {
if (child.type !== 'Rectangle') {
child.setTint(0x89ff5b);
}
});
this.selectedTower.safe = true;
} else {
this.selectedTower.iterate((child) => {
if (child.type !== 'Rectangle') {
child.setTint(0xa32a00);
}
});
this.selectedTower.safe = false;
}
}
}
if (mouse.rightButtonDown()) {
this.selectedTower.destroy();
this.selectedTower = false;
}
}
} }
} }

View File

@ -1,6 +1,10 @@
export const TOWERS_CONFIG = { export const TOWERS_CONFIG = {
'gun': { 'gun': {
'name': 'Gatlin Gun',
'cost': 100,
'spriteStart': 0, 'spriteStart': 0,
'type': 'direct',
'aoe': 0,
'level1': { 'level1': {
'dmgLow': 10, 'dmgLow': 10,
'dmgHigh': 30, 'dmgHigh': 30,
@ -22,5 +26,33 @@ export const TOWERS_CONFIG = {
'duration': 1000, 'duration': 1000,
'range': 325 'range': 325
} }
},
'cannon': {
'name': 'Cannon',
'cost': 200,
'spriteStart': 10,
'type': 'aoe',
'aoe': 50,
'level1': {
'dmgLow': 15,
'dmgHigh': 35,
'rate': 2000,
'duration': 200,
'range': 400
},
'level2': {
'dmgLow': 25,
'dmgHigh': 45,
'rate': 2000,
'duration': 500,
'range': 450
},
'level3': {
'dmgLow': 35,
'dmgHigh': 65,
'rate': 2000,
'duration': 500,
'range': 500
}
} }
} }

View File

@ -36,39 +36,6 @@ export class TowerManager {
} }
} }
createAnims() {
this.scene.anims.create({
key: 'gun-level1-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 0,
end: 1,
}),
frameRate: 15,
repeat: 5,
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
});
}
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) => {
@ -134,6 +101,7 @@ export class TowerManager {
const type = tower.props.type; const type = tower.props.type;
const level = 'level'+tower.props.level; const level = 'level'+tower.props.level;
const config = TOWERS_CONFIG[type][level]; const config = TOWERS_CONFIG[type][level];
const duration = TOWERS_CONFIG[type][level].duration;
if (!config) return; if (!config) return;
@ -143,7 +111,7 @@ 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: 500 }); tower.play(`${type}-${level}-fire`, { 'duration': duration });
// 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);
@ -216,6 +184,9 @@ export class TowerManager {
const endX = dieX + Math.cos(angle) * 50; const endX = dieX + Math.cos(angle) * 50;
const endY = dieY + Math.sin(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 // Animate the cash drop moving to the new position
this.scene.tweens.add({ this.scene.tweens.add({
targets: cashDrop, targets: cashDrop,
@ -227,8 +198,8 @@ export class TowerManager {
cashDrop.postFX.addGlow(); cashDrop.postFX.addGlow();
this.scene.tweens.add({ this.scene.tweens.add({
targets: cashDrop, targets: cashDrop,
x: 1300, x: 1300 + cam.scrollX,
y: 100, y: 100 + cam.scrollY,
alpha: 0, alpha: 0,
duration: 500, duration: 500,
onComplete: () => { onComplete: () => {
@ -258,4 +229,47 @@ export class TowerManager {
this.following[tower.id] = null; this.following[tower.id] = null;
} }
createAnims() {
this.scene.anims.create({
key: 'gun-level1-fire',
frames: this.scene.anims.generateFrameNumbers('towers', {
start: 0,
end: 1,
}),
frameRate: 15,
repeat: 5,
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: 10,
end: 11
}),
frameRate:3,
repeat: 0,
yoyo: true
})
}
} }

View File

@ -15,13 +15,11 @@ export const WAVE_CONFIG = {
}, },
2: { 2: {
begin: 15, begin: 15,
basic1: 5, basic1: 8,
basic2: 2
}, },
3: { 3: {
begin: 30, begin: 30,
basic1: 4, basic1: 10,
basic2: 3
} }
}, },
// Wave // Wave
@ -29,15 +27,18 @@ export const WAVE_CONFIG = {
// Schedule // Schedule
1: { 1: {
begin: 0, begin: 0,
basic1: 5 basic1: 8,
basic2: 1
}, },
2: { 2: {
begin: 15, begin: 15,
basic1: 5 basic1: 5,
basic2: 2
}, },
3: { 3: {
begin: 30, begin: 30,
basic1: 5 basic1: 5,
basic2: 4
} }
} }
} }

View File

@ -6,7 +6,7 @@ export class WaveManager {
constructor(scene, level, wave) { constructor(scene, level, wave) {
this.scene = scene; this.scene = scene;
this.level = level; this.level = level;
this.wave = wave; this.wave = wave-1;
this.schedule = 0; this.schedule = 0;
this.scheduleInfo = null; this.scheduleInfo = null;
this.spawnX = WAVE_CONFIG[this.level].spawnX; this.spawnX = WAVE_CONFIG[this.level].spawnX;
@ -15,9 +15,9 @@ export class WaveManager {
this.endY = WAVE_CONFIG[this.level].endY; this.endY = WAVE_CONFIG[this.level].endY;
this.path = null; this.path = null;
this.levelActive = true; this.levelActive = true;
this.waveActive = false;
this.waveTimer = 0; this.waveTimer = 0;
this.waveScheduleStartTime();
this.placeCore(); this.placeCore();
} }
@ -28,10 +28,13 @@ export class WaveManager {
} }
nextWave() { nextWave() {
this.wave++; // Next Wave will be started in the interfaceManager
this.waveTimer = 0; if (this.waveActive === true) {
this.schedule = 0; this.wave++;
this.waveScheduleStartTime(); this.waveTimer = 0;
this.schedule = 0;
this.waveScheduleStartTime();
}
} }
placeCore() { placeCore() {
@ -146,7 +149,7 @@ export class WaveManager {
// Handle Waves and Schedules // Handle Waves and Schedules
this.waveTimer += delta; this.waveTimer += delta;
if (this.waveTimer >= this.waveStart && this.levelActive === true) { if (this.waveTimer >= this.waveStart && this.levelActive === true && this.waveActive===true) {
console.log('Wave',this.wave,'Schedule',this.schedule); console.log('Wave',this.wave,'Schedule',this.schedule);
// Make path synchronous // Make path synchronous
this.makePath().then(() => { this.makePath().then(() => {
@ -156,7 +159,9 @@ export class WaveManager {
if (WAVE_CONFIG[this.level][this.wave].hasOwnProperty(this.schedule + 1)) { if (WAVE_CONFIG[this.level][this.wave].hasOwnProperty(this.schedule + 1)) {
this.waveScheduleStartTime(); this.waveScheduleStartTime();
} else if (WAVE_CONFIG[this.level].hasOwnProperty(this.wave+1)) { } else if (WAVE_CONFIG[this.level].hasOwnProperty(this.wave+1)) {
this.nextWave(); this.waveActive = false;
this.scene.UIScene.interfaceManager.showNextWave();
//this.nextWave();
} else { } else {
console.log('LEVEL COMPLETE'); console.log('LEVEL COMPLETE');
this.levelActive = false; this.levelActive = false;

View File

@ -16,6 +16,10 @@ export class UIScene extends Phaser.Scene {
this.load.image('redArrow', 'assets/redArrow.png'); this.load.image('redArrow', 'assets/redArrow.png');
this.load.image('intTop', 'assets/intTop.png'); this.load.image('intTop', 'assets/intTop.png');
this.load.image('gold', 'assets/gold.png'); this.load.image('gold', 'assets/gold.png');
this.load.spritesheet('nextWave', 'assets/nextWave.png', {
frameHeight: 150,
frameWidth: 150
});
} }
create() { create() {
@ -33,6 +37,11 @@ export class UIScene extends Phaser.Scene {
this.updateGold(); this.updateGold();
} }
removeGold(amount) {
this.interfaceManager.gold -= amount;
this.updateGold();
}
updateGold() { updateGold() {
this.interfaceManager.goldText.setText(`${this.interfaceManager.gold}`); this.interfaceManager.goldText.setText(`${this.interfaceManager.gold}`);
} }