← tower-of-time-game
// You can write more code here
/* START OF COMPILED CODE */
import Player from "../prefabs/Player";
import Goal from "../prefabs/Goal";
import EnemySpawner from "../prefabs/EnemySpawner";
/* START-USER-IMPORTS */
import { MenuItemData } from "../prefabs/PlayerMenu";
import { TowerManager, TowerType } from "../systems/BuildingSystem";
import InputManager from "../components/InputManager";
import { TimeMode } from "../components/RewindableSprite";
import { DEBUG } from "../main";
import { EnemyState } from "../prefabs/Enemy";
import Enemy from "../prefabs/Enemy";
import Bullet from "../prefabs/Bullet";
import EnergySystem from "../systems/EnergySystem";
import EnergyBar from "../ui/EnergyBar";
import EnergyCrystal from "../prefabs/EnergyCrystal";
import GoalHPSystem from "../systems/GoalHPSystem";
import GoalHPBar from "../ui/GoalHPBar";
import GameOverOverlay from "../ui/GameOverOverlay";
import WaveStartOverlay from "../ui/WaveStartOverlay";
import CongratulationsOverlay from "../ui/CongratulationsOverlay";
import ConfigSystem from "../systems/ConfigSystem";
import StatusBar from "../ui/StatusBar";
import TutorialSystem, { TutorialStep } from "../systems/TutorialSystem";
import DialogBox from "../ui/DialogBox";
import GlobalMusicManager from "../utils/GlobalMusicManager";
/* END-USER-IMPORTS */
export default class Level extends Phaser.Scene {
constructor() {
super("Level");
/* START-USER-CTR-CODE */
// Write your code here.
/* END-USER-CTR-CODE */
}
editorCreate(): void {
// editabletilemap
this.cache.tilemap.add(
"editabletilemap_a29028ea-14dc-427e-a9a4-61c80dae566b",
{
format: 1,
data: {
width: 20,
height: 14,
orientation: "orthogonal",
tilewidth: 32,
tileheight: 32,
tilesets: [
{
columns: 4,
margin: 0,
spacing: 0,
tilewidth: 32,
tileheight: 32,
tilecount: 16,
firstgid: 1,
image: "tiles_ground",
name: "tiles_ground",
imagewidth: 128,
imageheight: 128,
},
{
columns: 8,
margin: 0,
spacing: 0,
tilewidth: 32,
tileheight: 32,
tilecount: 64,
firstgid: 17,
image: "tiles",
name: "tiles",
imagewidth: 256,
imageheight: 256,
},
],
layers: [
{
type: "tilelayer",
name: "ground",
width: 20,
height: 14,
opacity: 1,
data: [
19, 19, 19, 19, 19, 25, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20,
57, 58, 59, 65, 42, 43, 42, 43, 44, 28, 54, 44, 42, 43, 42, 43,
42, 43, 42, 28, 65, 73, 67, 57, 22, 22, 22, 22, 23, 26, 21, 22,
22, 22, 22, 22, 22, 22, 23, 28, 58, 58, 73, 65, 46, 46, 40, 30,
31, 26, 29, 30, 38, 18, 19, 19, 53, 39, 31, 28, 67, 73, 73, 58,
19, 20, 39, 27, 31, 26, 29, 27, 31, 26, 54, 42, 42, 37, 31, 28,
57, 58, 65, 66, 60, 28, 29, 27, 31, 49, 29, 27, 31, 28, 21, 22,
22, 45, 31, 26, 58, 66, 58, 13, 57, 28, 29, 27, 31, 43, 37, 27,
31, 26, 29, 18, 25, 19, 19, 33, 19, 19, 19, 20, 73, 28, 29, 30,
56, 22, 45, 30, 31, 26, 29, 26, 26, 54, 44, 42, 42, 42, 43, 28,
65, 28, 48, 46, 46, 46, 46, 46, 64, 26, 29, 26, 26, 21, 22, 22,
22, 22, 23, 28, 73, 34, 19, 19, 19, 19, 19, 19, 19, 51, 29, 26,
26, 29, 38, 18, 20, 39, 31, 28, 66, 60, 61, 60, 60, 61, 60, 61,
60, 28, 29, 34, 36, 29, 31, 26, 28, 29, 31, 28, 58, 68, 68, 68,
68, 68, 68, 68, 68, 28, 29, 42, 42, 37, 31, 28, 26, 29, 31, 28,
65, 58, 59, 58, 66, 67, 65, 73, 73, 28, 29, 22, 22, 45, 31, 26,
28, 48, 64, 28, 67, 66, 58, 65, 73, 65, 73, 65, 67, 34, 19, 19,
19, 19, 19, 33, 33, 35, 35, 36,
],
},
{
type: "tilelayer",
name: "path",
width: 20,
height: 14,
opacity: 1,
data: [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 27,
0, 0, 0, 0, 0, 27, 27, 27, 27, 27, 27, 27, 27, 0, 0, 0, 0, 0,
27, 27, 27, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 27, 27, 0, 0, 0, 0,
0, 0, 0, 27, 0, 0, 0, 0, 27, 0, 0, 0, 0, 0, 27, 27, 0, 0, 0, 0,
0, 0, 0, 27, 0, 0, 0, 0, 27, 0, 0, 27, 27, 27, 27, 27, 0, 0, 0,
0, 0, 0, 0, 27, 0, 0, 0, 0, 27, 0, 0, 27, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 27, 27, 27, 27, 27, 27, 0, 0, 27, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 27, 27, 27, 27,
27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 27, 27, 0, 0,
27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 27, 27, 0, 0,
27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 0, 0, 27, 27, 0, 0,
27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 27, 27, 27, 27, 0,
0, 27, 27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0,
],
},
{
type: "tilelayer",
name: "walls",
width: 20,
height: 14,
opacity: 1,
data: [
19, 19, 19, 19, 19, 25, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20,
0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0,
0, 0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0, 0,
0, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 18, 19, 19, 53, 0, 0, 28, 0,
0, 0, 0, 19, 20, 0, 0, 0, 28, 0, 0, 0, 28, 0, 0, 0, 0, 0, 28, 0,
0, 0, 0, 0, 28, 0, 0, 0, 49, 0, 0, 0, 26, 0, 0, 0, 0, 0, 26, 0,
0, 0, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 28, 0, 18, 25, 19, 19, 33,
19, 19, 19, 20, 0, 28, 0, 0, 0, 0, 0, 0, 0, 28, 0, 28, 26, 0, 0,
0, 0, 0, 0, 28, 0, 28, 0, 0, 0, 0, 0, 0, 0, 28, 0, 28, 28, 0, 0,
0, 0, 0, 0, 26, 0, 34, 19, 19, 19, 19, 19, 19, 19, 51, 0, 26,
28, 0, 0, 18, 20, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 0,
34, 36, 0, 0, 26, 28, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 26,
0, 0, 0, 0, 0, 28, 26, 0, 0, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28,
0, 0, 0, 0, 0, 26, 28, 0, 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34,
19, 19, 19, 19, 19, 33, 33, 35, 35, 36,
],
},
],
},
}
);
const editabletilemap = this.add.tilemap(
"editabletilemap_a29028ea-14dc-427e-a9a4-61c80dae566b"
);
editabletilemap.addTilesetImage("tiles_ground");
editabletilemap.addTilesetImage("tiles");
// ground
editabletilemap.createLayer("ground", ["tiles", "tiles_ground"], 0, 0);
// path
const path = editabletilemap.createLayer("path", ["tiles"], 0, 0)!;
path.blendMode = Phaser.BlendModes.SCREEN;
// walls
editabletilemap.createLayer("walls", ["tiles"], 0, 0);
// player
const player = new Player(this, 241, 252);
this.add.existing(player);
// goal
const goal = new Goal(this, 576, 391);
this.add.existing(goal);
// enemySpawner
const enemySpawner = new EnemySpawner(this, 26, 96);
this.add.existing(enemySpawner);
enemySpawner.alpha = 0.5;
this.editabletilemap = editabletilemap;
this.events.emit("scene-awake");
}
private editabletilemap!: Phaser.Tilemaps.Tilemap;
/* START-USER-CODE */
private player!: Player;
private tileHighlight!: Phaser.GameObjects.Graphics;
private currentHighlightTileX: number = -1;
private currentHighlightTileY: number = -1;
private towerManager!: TowerManager;
private inputManager!: InputManager;
private wasRewindPressed: boolean = false;
private rewindIndicator!: Phaser.GameObjects.Graphics;
private energySystem!: EnergySystem;
private energyBar!: EnergyBar;
private goalHPSystem!: GoalHPSystem;
private goalHPBar!: GoalHPBar;
private gameOverOverlay!: GameOverOverlay;
private waveStartOverlay: WaveStartOverlay | null = null;
private congratulationsOverlay!: CongratulationsOverlay;
private statusBar!: StatusBar;
private goal!: Goal;
private enemySpawner!: EnemySpawner;
private isGameOver: boolean = false;
private configSystem!: ConfigSystem;
private tutorialSystem!: TutorialSystem;
private dialogBox!: DialogBox;
private isTutorialMode: boolean = true; // Start in tutorial mode by default
private tutorialTileHighlight!: Phaser.GameObjects.Sprite;
// Display layers for proper rendering order (bottom to top):
// groundLayer: Background elements, terrain decorations
// buildingLayer: Buildings, towers, structures
// effectLayer: Visual effects, highlights, particles
// uiLayer: Player, UI elements, always on top
private groundLayer!: Phaser.GameObjects.Group;
private buildingLayer!: Phaser.GameObjects.Group;
private effectLayer!: Phaser.GameObjects.Group;
private uiLayer!: Phaser.GameObjects.Group;
// Depth constants for proper layering
private static readonly DEPTH_GROUND = 0;
private static readonly DEPTH_WALLS = 75;
private static readonly DEPTH_BUILDINGS = 100;
private static readonly DEPTH_EFFECTS = 200;
private static readonly DEPTH_UI = 300;
// Write your code here
create() {
this.editorCreate();
// Reset game over state on scene restart
this.isGameOver = false;
// Initialize input manager
this.inputManager = new InputManager(this);
// Initialize configuration system
this.configSystem = new ConfigSystem();
// Start level music using global music manager
GlobalMusicManager.playMusicForScene(this, "Level");
// Create Tutorial System early (before enemy spawner initialization)
this.tutorialSystem = new TutorialSystem(this);
// Create Dialog Box early so tutorial can use it
this.dialogBox = new DialogBox(this, this.inputManager);
this.add.existing(this.dialogBox);
// Setup tutorial event handlers immediately after creation
this.setupTutorialEventHandlers();
// Create display layers in proper order (bottom to top)
this.groundLayer = this.add.group();
this.buildingLayer = this.add.group();
this.effectLayer = this.add.group();
this.uiLayer = this.add.group();
// Find and organize existing objects into layers
const children = this.children.getAll();
let goalObject: Goal | undefined;
let spawnerObject: EnemySpawner | undefined;
// Make the path layer invisible
const pathLayer = this.editabletilemap.getLayer("path");
if (pathLayer) {
pathLayer.tilemapLayer.visible = false;
}
children.forEach((child) => {
if (child instanceof Player) {
this.player = child;
// Player should be in the top layer (uiLayer) with highest depth
this.uiLayer.add(child);
child.setDepth(Level.DEPTH_UI);
// Configure player speed from config
const playerConfig = this.configSystem.getPlayerConfig();
child.setSpeed(playerConfig.speed);
} else if (child instanceof Enemy) {
// Set enemy to building depth and give it a target to demonstrate walking
child.setDepth(Level.DEPTH_BUILDINGS);
this.buildingLayer.add(child);
// Set a target position for the enemy to walk to (example: walk to player's starting position)
child.setTarget(child.x, child.y);
} else if (child instanceof Goal) {
// Store reference to goal for spawner initialization and HP system
goalObject = child;
this.goal = child;
// Set goal to building depth
child.setDepth(Level.DEPTH_BUILDINGS);
this.buildingLayer.add(child);
} else if (child instanceof EnemySpawner) {
// Store reference to enemy spawner for initialization
spawnerObject = child;
// Set spawner to ground layer (it's invisible anyway)
child.setDepth(Level.DEPTH_GROUND);
this.groundLayer.add(child);
} else if (
child instanceof Phaser.GameObjects.Image &&
child.texture.key === "tower1"
) {
// Set existing tower to building depth
child.setDepth(Level.DEPTH_BUILDINGS);
this.buildingLayer.add(child);
}
});
// Initialize enemy spawner if both goal and spawner exist
if (goalObject && spawnerObject) {
const pathLayer = this.editabletilemap.getLayer("path");
const wallsLayer = this.editabletilemap.getLayer("walls");
if (wallsLayer) {
wallsLayer.tilemapLayer.setDepth(Level.DEPTH_WALLS);
}
if (pathLayer) {
spawnerObject.initialize(
goalObject,
pathLayer.tilemapLayer,
this.buildingLayer
);
// Store reference to spawner for updates
this.enemySpawner = spawnerObject;
// Ensure Wave Start overlay exists and is bound to the current scene
if (this.waveStartOverlay && this.waveStartOverlay.scene !== this) {
// The overlay belongs to a previous scene instance – destroy it so we can create a fresh one
this.waveStartOverlay.destroy();
this.waveStartOverlay = null;
}
if (!this.waveStartOverlay) {
this.waveStartOverlay = new WaveStartOverlay(this);
this.add.existing(this.waveStartOverlay);
this.uiLayer.add(this.waveStartOverlay);
}
console.log("EnemySpawner initialized with wave system");
} else {
console.warn("Path layer not found - enemy spawner cannot pathfind");
}
} else {
if (!goalObject) console.warn("Goal not found - enemy spawner disabled");
if (!spawnerObject) console.warn("EnemySpawner not found");
}
// Create tile highlight in effect layer (between buildings and player)
this.tileHighlight = this.add.graphics();
this.tileHighlight.fillStyle(0xffffff, 0.2);
this.tileHighlight.fillRect(0, 0, 32, 32);
this.tileHighlight.setVisible(false);
this.tileHighlight.setDepth(Level.DEPTH_EFFECTS);
this.effectLayer.add(this.tileHighlight);
// Create rewind indicator
this.createRewindIndicator();
// Initialize energy system
const energyConfig = this.configSystem.getEnergyConfig();
this.energySystem = new EnergySystem(this, energyConfig);
// Disable energy regeneration during tutorial until enemies spawn
if (this.isTutorialMode) {
this.energySystem.setTutorialRegenerationDisabled(true);
}
// Create energy bar UI with label (bottom left, stacked vertically)
const baseX = 10;
const baseY = 400;
const labelOffset = 50; // Space for label text
const barSpacing = 23; // Vertical spacing between bars
// Energy label
const energyLabel = this.add.text(baseX, baseY, "Energy", {
fontSize: "10px",
color: "#ffffff",
fontFamily: "Arial",
stroke: "#000000",
strokeThickness: 1,
});
energyLabel.setDepth(EnergyBar.ENERGY_BAR_DEPTH);
energyLabel.setScrollFactor(0);
this.uiLayer.add(energyLabel);
// Energy bar
this.energyBar = new EnergyBar(
this,
baseX + labelOffset,
baseY - 2,
this.energySystem
);
this.add.existing(this.energyBar);
this.uiLayer.add(this.energyBar);
// Initialize Goal HP system
const goalConfig = this.configSystem.getGoalConfig();
this.goalHPSystem = new GoalHPSystem({
maxHP: goalConfig.maxHP,
initialHP: goalConfig.initialHP,
});
// Goal HP label
const goalHPLabel = this.add.text(baseX, baseY + barSpacing, "Goal HP", {
fontSize: "10px",
color: "#ffffff",
fontFamily: "Arial",
stroke: "#000000",
strokeThickness: 1,
});
goalHPLabel.setDepth(GoalHPBar.GOAL_HP_BAR_DEPTH);
goalHPLabel.setScrollFactor(0);
this.uiLayer.add(goalHPLabel);
// Goal HP bar
this.goalHPBar = new GoalHPBar(
this,
baseX + labelOffset,
baseY + barSpacing - 2,
this.goalHPSystem
);
this.add.existing(this.goalHPBar);
this.uiLayer.add(this.goalHPBar);
// Connect Goal HP system to the goal
if (this.goal) {
this.goal.setHPSystem(this.goalHPSystem);
}
// Create Game Over overlay
this.gameOverOverlay = new GameOverOverlay(this, this.inputManager);
this.add.existing(this.gameOverOverlay);
this.uiLayer.add(this.gameOverOverlay);
// Create Congratulations overlay
this.congratulationsOverlay = new CongratulationsOverlay(this);
this.add.existing(this.congratulationsOverlay);
this.uiLayer.add(this.congratulationsOverlay);
// Create Status Bar
this.statusBar = new StatusBar(this);
this.add.existing(this.statusBar);
this.uiLayer.add(this.statusBar);
// Dialog Box already created earlier, just add to UI layer
this.uiLayer.add(this.dialogBox);
// Create tutorial-specific tile highlight
this.tutorialTileHighlight = this.add.sprite(0, 0, "tile-highlight");
this.tutorialTileHighlight.setVisible(false);
this.tutorialTileHighlight.setDepth(Level.DEPTH_EFFECTS + 10); // Above normal tile highlight
this.effectLayer.add(this.tutorialTileHighlight);
// Subscribe to game over events
this.goalHPSystem.onGameOver(this.handleGameOver.bind(this));
// Initialize tower manager with tower layer and energy system
this.towerManager = new TowerManager(
this,
this.editabletilemap,
this.buildingLayer,
this.energySystem
);
// Initialize the player menu with building system
const menuItems: MenuItemData[] = [
{
id: "build",
icon: "tower",
submenu: [
{
id: "build_basic",
icon: "tower1",
action: () => this.handleBuildTowerAction(TowerType.BASIC_TOWER),
},
{
id: "build_sniper",
icon: "tower2",
action: () => this.handleBuildTowerAction(TowerType.SNIPER_TOWER),
},
{
id: "build_slowdown",
icon: "tower3",
action: () => this.handleBuildTowerAction(TowerType.SLOWDOWN_TOWER),
},
{
id: "build_splash",
icon: "tower4",
action: () => this.handleBuildTowerAction(TowerType.SPLASH_TOWER),
},
],
},
{
id: "sell",
icon: "sell",
action: () => this.handleSellAction(),
},
];
this.player.initializeMenu(menuItems);
// Ensure player menu is in the UI layer with highest depth
const playerMenu = this.player.getMenu();
if (playerMenu) {
this.uiLayer.add(playerMenu);
// Menu already has high depth set in its constructor
// Connect status bar and config system to player menu
playerMenu.setStatusBar(this.statusBar);
playerMenu.setConfigSystem(this.configSystem);
}
// Check if tutorial should be skipped
if (this.configSystem.getSkipTutorial()) {
console.log("Skipping tutorial - starting waves immediately");
this.isTutorialMode = false;
// Ensure energy regeneration is enabled when skipping tutorial
if (this.energySystem) {
this.energySystem.setTutorialRegenerationDisabled(false);
}
this.loadDefaultWaves();
} else {
this.startTutorial();
}
this.events.emit("scene-awake");
}
/**
* Setup tutorial system event handlers
*/
private setupTutorialEventHandlers(): void {
// Dialog system integration
this.tutorialSystem.onDialogShow((text: string, isLast: boolean) => {
console.log(
`Tutorial dialog show event received: "${text}" (isLast: ${isLast})`
);
this.dialogBox.show(text, () => {
console.log("Tutorial dialog confirmed by player");
this.tutorialSystem.onDialogConfirmed();
});
});
// Add a separate handler for when dialog sequences complete
this.tutorialSystem.onStepChange((step: TutorialStep) => {
// Hide dialog when moving to non-dialog steps
if (
step !== TutorialStep.INTRO_DIALOG &&
step !== TutorialStep.CRYSTAL_DIALOG &&
step !== TutorialStep.SHOW_BUILD_LOCATION &&
step !== TutorialStep.POST_BUILD_DIALOG &&
step !== TutorialStep.ENEMY_WARNING &&
step !== TutorialStep.REWIND_INSTRUCTION &&
step !== TutorialStep.FINAL_DIALOG
) {
console.log(`Step changed to ${step} - hiding dialog`);
this.dialogBox.hide();
}
});
// Tile highlighting integration
this.tutorialSystem.onTileHighlight(
(x: number, y: number, show: boolean) => {
if (show) {
// Center the 36x36 sprite on the 32x32 tile
const worldX = x * 32 + 16; // Tile center X
const worldY = y * 32 + 16; // Tile center Y
this.tutorialTileHighlight.setPosition(worldX, worldY);
this.tutorialTileHighlight.setVisible(true);
// Start pulsing effect
this.tweens.add({
targets: this.tutorialTileHighlight,
scaleX: 1.2,
scaleY: 1.2,
duration: 800,
ease: "Sine.easeInOut",
yoyo: true,
repeat: -1,
});
} else {
this.tutorialTileHighlight.setVisible(false);
// Stop pulsing effect
this.tweens.killTweensOf(this.tutorialTileHighlight);
this.tutorialTileHighlight.setScale(1, 1);
}
}
);
// Player menu control integration
this.tutorialSystem.onMenuControl(
(enabled: boolean, restrictedItems?: string[]) => {
const playerMenu = this.player.getMenu();
if (playerMenu) {
if (enabled && restrictedItems) {
// Enable menu with restricted items
playerMenu.setRestrictedMode(restrictedItems);
} else if (enabled) {
// Enable full menu
console.log("Level onMenuControl: enabling full menu");
playerMenu.setRestrictedMode(undefined);
} else {
// Disable menu completely
playerMenu.setDisabled(true);
}
}
}
);
// Screen flash integration
this.tutorialSystem.onScreenFlash((color: number, count: number) => {
this.createScreenFlash(color, count);
});
// Enemy spawn integration
this.tutorialSystem.onEnemySpawn((type: string, count: number) => {
this.spawnTutorialEnemies(type, count);
// Enable energy regeneration when enemies spawn during tutorial
if (this.energySystem && this.isTutorialMode) {
this.energySystem.setTutorialRegenerationDisabled(false);
}
});
// Tutorial completion integration
this.tutorialSystem.onTutorialComplete(() => {
this.isTutorialMode = false;
// Ensure energy regeneration is enabled when tutorial completes
if (this.energySystem) {
this.energySystem.setTutorialRegenerationDisabled(false);
}
this.loadDefaultWaves();
});
// Pause gameplay integration (for dialog boxes)
this.tutorialSystem.onPauseGameplay((paused: boolean) => {
console.log(`Tutorial requesting gameplay pause: ${paused}`);
this.pauseGameplayForDialog(paused);
});
}
/**
* Pause or resume gameplay for dialog boxes
*/
private pauseGameplayForDialog(paused: boolean): void {
console.log(`Pausing gameplay for dialog: ${paused}`);
// Pause/resume enemy spawning
const spawner = this.getEnemySpawner();
if (spawner) {
if (paused) {
spawner.pauseSpawning();
} else {
spawner.resumeSpawning();
}
}
// Pause/resume all towers using the TowerManager
if (this.towerManager) {
this.towerManager.setPaused(paused);
}
// Pause/resume all enemy movement
const enemies = this.getAllEnemies();
enemies.forEach((enemy) => {
if (paused) {
if (typeof enemy.pauseMovement === "function") {
enemy.pauseMovement();
}
} else {
if (typeof enemy.resumeMovement === "function") {
enemy.resumeMovement();
}
}
});
}
/**
* Start the tutorial sequence
*/
private startTutorial(): void {
console.log("Starting tutorial mode");
this.tutorialSystem.startTutorial();
}
/**
* Update tutorial player position detection
*/
private updateTutorialPlayerPosition(): void {
if (!this.player || !this.tutorialSystem) return;
const targetTile = this.tutorialSystem.getTargetTile();
if (!targetTile) return;
// Get player center position
const playerCenterX = this.player.x;
const playerCenterY = this.player.y;
// Convert world position to tile coordinates
const playerTileX = Math.floor(playerCenterX / 32);
const playerTileY = Math.floor(playerCenterY / 32);
// Check if player is at target tile
if (playerTileX === targetTile.x && playerTileY === targetTile.y) {
this.tutorialSystem.onPlayerAtTargetTile();
}
}
/**
* Update tutorial enemy detection
*/
private updateTutorialEnemyDetection(): void {
if (!this.tutorialSystem) return;
const currentStep = this.tutorialSystem.getCurrentStep();
if (currentStep === "WAIT_FOR_ENEMIES_AT_TILE") {
this.checkEnemiesAtTargetTile();
} else if (currentStep === "WAIT_FOR_ENEMIES_KILLED") {
this.checkAllEnemiesKilled();
}
}
/**
* Check if any enemy has reached the target tile (10, 6)
*/
private checkEnemiesAtTargetTile(): void {
if (!this.buildingLayer) return;
const targetTileX = 10;
const targetTileY = 6;
const targetWorldX = targetTileX * 32;
const targetWorldY = targetTileY * 32;
const detectionRadius = 32; // One tile radius
this.buildingLayer.children.entries.forEach((child) => {
if (child instanceof Enemy && !child.isDead_() && child.visible) {
const distance = Phaser.Math.Distance.Between(
child.x,
child.y,
targetWorldX,
targetWorldY
);
if (distance <= detectionRadius) {
console.log(
`Enemy reached target tile (${targetTileX}, ${targetTileY})`
);
this.tutorialSystem.onEnemyAtTargetTile();
return; // Only need one enemy to trigger this
}
}
});
}
/**
* Check if all enemies are killed
*/
private checkAllEnemiesKilled(): void {
if (!this.buildingLayer) return;
let livingEnemyCount = 0;
this.buildingLayer.children.entries.forEach((child) => {
if (child instanceof Enemy && !child.isDead_() && child.visible) {
livingEnemyCount++;
}
});
if (livingEnemyCount === 0) {
console.log("All enemies killed - tutorial can advance");
this.tutorialSystem.onAllEnemiesKilled();
}
}
/**
* Create screen flash effect for tutorial
*/
private createScreenFlash(color: number, count: number): void {
let flashesRemaining = count;
const flash = () => {
if (flashesRemaining <= 0) return;
// Create flash overlay
const flashOverlay = this.add.rectangle(
this.scale.width / 2,
this.scale.height / 2,
this.scale.width,
this.scale.height,
color,
0.7
);
flashOverlay.setDepth(2000); // Above everything
this.uiLayer.add(flashOverlay);
// Flash animation - adjusted timing to fit 3-second red-alert sound
this.tweens.add({
targets: flashOverlay,
alpha: 0,
duration: 500,
ease: "Power2",
onComplete: () => {
flashOverlay.destroy();
flashesRemaining--;
if (flashesRemaining > 0) {
// Wait longer between flashes to spread them over 3 seconds
this.time.delayedCall(400, flash);
}
},
});
};
flash();
}
/**
* Spawn tutorial enemies
*/
private spawnTutorialEnemies(type: string, count: number): void {
if (!this.enemySpawner) {
console.warn("Enemy spawner not available for tutorial");
return;
}
console.log(`Spawning ${count} ${type} enemies for tutorial`);
// Spawn enemies one by one with a small delay
for (let i = 0; i < count; i++) {
this.time.delayedCall(i * 500, () => {
// Use the enemy spawner's spawnSpecificType method
const enemy = this.enemySpawner.spawnSpecificType(type);
if (enemy) {
console.log(
`Tutorial enemy ${i + 1} spawned: ${enemy.getEnemyConfig().name}`
);
} else {
console.warn(`Failed to spawn tutorial enemy ${i + 1}`);
}
});
}
}
/**
* Create the rewind indicator (double triangles facing left)
*/
private createRewindIndicator(): void {
this.rewindIndicator = this.add.graphics();
// Set position in bottom right corner (with some padding)
const padding = 5;
const cameraWidth = this.cameras.main.width;
const cameraHeight = this.cameras.main.height;
this.rewindIndicator.setPosition(
cameraWidth - padding - 40,
cameraHeight - padding - 20
);
// Set high depth so it appears on top of everything
this.rewindIndicator.setDepth(Level.DEPTH_UI + 100);
this.rewindIndicator.setScrollFactor(0); // Keep it fixed on screen
// Draw double triangles facing left (rewind symbol)
this.rewindIndicator.fillStyle(0xffffff, 0.9); // White with slight transparency
// First triangle (left one)
this.rewindIndicator.beginPath();
this.rewindIndicator.moveTo(0, 10); // Top point
this.rewindIndicator.lineTo(15, 0); // Bottom right
this.rewindIndicator.lineTo(15, 20); // Top right
this.rewindIndicator.closePath();
this.rewindIndicator.fillPath();
// Second triangle (right one, slightly overlapping)
this.rewindIndicator.beginPath();
this.rewindIndicator.moveTo(12, 10); // Top point
this.rewindIndicator.lineTo(27, 0); // Bottom right
this.rewindIndicator.lineTo(27, 20); // Top right
this.rewindIndicator.closePath();
this.rewindIndicator.fillPath();
// Add a subtle outline
this.rewindIndicator.lineStyle(1, 0x000000, 0.3);
this.rewindIndicator.strokePath();
// Initially hidden
this.rewindIndicator.setVisible(false);
// Add to UI layer
this.uiLayer.add(this.rewindIndicator);
}
private updateTileHighlight(): void {
if (!this.player || !this.tileHighlight) return;
// Get player center position
const playerCenterX = this.player.x;
const playerCenterY = this.player.y;
// Convert world position to tile coordinates
const tileX = Math.floor(playerCenterX / 32);
const tileY = Math.floor(playerCenterY / 32);
// Only update if we're on a different tile
if (
tileX !== this.currentHighlightTileX ||
tileY !== this.currentHighlightTileY
) {
this.currentHighlightTileX = tileX;
this.currentHighlightTileY = tileY;
// Check if there's a tile on the path layer at this position
const pathTile = this.editabletilemap.getTileAt(
tileX,
tileY,
false,
"walls"
);
// Only show highlight for tiles on the walls layer
const shouldShowHighlight = !!pathTile;
if (shouldShowHighlight) {
// Position the highlight on the tile
const worldX = tileX * 32;
const worldY = tileY * 32;
this.tileHighlight.setPosition(worldX, worldY);
this.tileHighlight.setVisible(true);
} else {
this.tileHighlight.setVisible(false);
}
}
}
update(time: number, delta: number): void {
// Always update game over overlay for input handling
if (this.gameOverOverlay) {
this.gameOverOverlay.update();
}
// Skip all game logic if game is over
if (this.isGameOver) {
return;
}
// Update input manager
this.inputManager.update();
// Handle rewind input
this.handleRewindInput();
// Update energy system
if (this.energySystem) {
this.energySystem.update();
}
// Update energy bar UI
if (this.energyBar) {
this.energyBar.update();
}
// Update Goal HP bar UI
if (this.goalHPBar) {
this.goalHPBar.update();
}
// Update goal (for attack detection)
if (this.goal) {
this.goal.update();
}
// Update enemy spawner (for wave system)
if (this.enemySpawner) {
this.enemySpawner.update();
}
if (this.player) {
// Only update player if no dialog is showing (to prevent menu opening during dialogs)
const isDialogShowing =
this.dialogBox && this.dialogBox.isDialogVisible();
if (!isDialogShowing) {
this.player.update(time, delta);
this.updateTileHighlight();
// Update tutorial player position detection only when not in dialog
if (this.tutorialSystem && this.tutorialSystem.isInTutorial()) {
this.updateTutorialPlayerPosition();
this.updateTutorialEnemyDetection();
}
}
// Always update tutorial system (for timing-based steps)
if (this.tutorialSystem && this.tutorialSystem.isInTutorial()) {
this.tutorialSystem.update();
}
}
// Update dialog box if it exists
if (this.dialogBox) {
this.dialogBox.update();
// Check for energy crystal collection
this.checkEnergyCrystalCollection();
}
// Update all enemies
const enemies = this.getAllEnemies();
enemies.forEach((enemy) => {
enemy.update(time, delta);
});
// Update all bullets
const bullets = this.getAllBullets();
bullets.forEach((bullet) => {
// Only update active bullets that haven't been destroyed
if (bullet.active && bullet.scene) {
bullet.update(time, delta);
}
});
// Clean up dead enemies and bullets periodically
this.cleanupDeadObjects();
}
/**
* Clean up dead enemies and bullets that have been invisible for a while
* This prevents memory leaks and improves performance
*/
private cleanupDeadObjects(): void {
// Don't clean up during rewind mode to avoid interfering with rewind system
const isRewindPressed = this.inputManager.isActionPressed("REWIND");
if (isRewindPressed) {
return;
}
const currentTime = Date.now();
const CLEANUP_DELAY = 5000; // 5 seconds after death before cleanup
// Clean up dead enemies
const enemies = this.getAllEnemies();
const enemiesToCleanup: Enemy[] = [];
enemies.forEach((enemy) => {
if (this.shouldCleanupDeadObject(enemy, currentTime, CLEANUP_DELAY)) {
enemiesToCleanup.push(enemy);
}
});
// Clean up dead bullets
const bullets = this.getAllBullets();
const bulletsToCleanup: Bullet[] = [];
bullets.forEach((bullet) => {
if (this.shouldCleanupDeadObject(bullet, currentTime, CLEANUP_DELAY)) {
bulletsToCleanup.push(bullet);
}
});
// Perform cleanup outside of iteration to avoid modifying collection during iteration
enemiesToCleanup.forEach((enemy) => {
console.debug("Cleaning up dead enemy");
enemy.destroy();
});
bulletsToCleanup.forEach((bullet) => {
console.debug("Cleaning up dead bullet");
bullet.destroy();
});
// Log cleanup stats if any objects were cleaned up
if (enemiesToCleanup.length > 0 || bulletsToCleanup.length > 0) {
console.debug(
`Cleaned up ${enemiesToCleanup.length} enemies and ${bulletsToCleanup.length} bullets`
);
}
}
/**
* Check if a dead object should be cleaned up
* Uses death time tracking to determine if enough time has passed
*/
private shouldCleanupDeadObject(
obj: Enemy | Bullet,
currentTime: number,
delay: number
): boolean {
// Get the death time from the object
const deathTime = obj.getDeathTime();
// If no death time recorded, don't clean up yet
if (deathTime === 0) {
return false;
}
// Check if enough time has passed since death
return currentTime - deathTime >= delay;
}
/**
* Handle rewind input logic
*/
private handleRewindInput(): void {
// Check if rewind should be disabled during tutorial
if (this.tutorialSystem && this.tutorialSystem.isInTutorial()) {
const currentStep = this.tutorialSystem.getCurrentStep();
// Disable rewind until we reach the rewind instruction step
if (
currentStep !== TutorialStep.REWIND_INSTRUCTION &&
currentStep !== TutorialStep.WAIT_FOR_ENEMIES_KILLED &&
currentStep !== TutorialStep.FINAL_DIALOG &&
currentStep !== TutorialStep.TUTORIAL_COMPLETE
) {
return; // Exit early, don't process rewind input
}
}
const isRewindPressed = this.inputManager.isActionPressed("REWIND");
const enemies = this.getAllEnemies();
const bullets = this.getAllBullets();
const towers = this.getAllTowers();
const spawner = this.getEnemySpawner();
// Check if rewind button was just pressed
if (isRewindPressed && !this.wasRewindPressed) {
// Set energy system to rewind mode (stops regeneration)
if (this.energySystem) {
this.energySystem.setRewindMode(true);
}
// Switch all enemies to REWIND mode
enemies.forEach((enemy) => {
enemy.setTimeMode(TimeMode.REWIND);
});
// Switch all bullets to REWIND mode
bullets.forEach((bullet) => {
if (bullet.active && bullet.scene) {
bullet.setTimeMode(TimeMode.REWIND);
}
});
// Pause all towers during rewind
towers.forEach((tower) => {
if (tower.pauseTower) {
tower.pauseTower();
}
});
// Pause enemy spawning during rewind
if (spawner) {
spawner.pauseSpawning();
}
// Show rewind indicator
this.rewindIndicator.setVisible(true);
// Switch to rewind music track
GlobalMusicManager.switchToRewindTrack("Level");
console.log("Rewind mode activated for all enemies, bullets, and towers");
}
// Check if rewind button was just released
if (!isRewindPressed && this.wasRewindPressed) {
// Set energy system back to forward mode (resumes regeneration)
if (this.energySystem) {
this.energySystem.setRewindMode(false);
}
// Switch all enemies back to FORWARD mode
enemies.forEach((enemy) => {
enemy.setTimeMode(TimeMode.FORWARD);
});
// Switch all bullets back to FORWARD mode
bullets.forEach((bullet) => {
if (bullet.active && bullet.scene) {
bullet.setTimeMode(TimeMode.FORWARD);
}
});
// Resume all towers when exiting rewind
towers.forEach((tower) => {
if (tower.resumeTower) {
tower.resumeTower();
}
});
// Resume enemy spawning when returning to forward mode
if (spawner) {
spawner.resumeSpawning();
}
// Hide rewind indicator
this.rewindIndicator.setVisible(false);
// Switch back to normal music track
GlobalMusicManager.switchToNormalTrack("Level");
console.log("Forward mode restored for all enemies, bullets, and towers");
}
// While rewind is held, rewind 1 step per frame and consume energy
if (isRewindPressed) {
const energyConfig = this.configSystem.getEnergyConfig();
const cost = energyConfig.rewindCostPerTick;
console.log("Rewind cost: ", cost);
if (this.energySystem && this.energySystem.hasEnergy(cost)) {
this.energySystem.consumeEnergy(cost, "rewind");
enemies.forEach((enemy) => {
enemy.rewindTime(1);
});
bullets.forEach((bullet) => {
if (bullet.active && bullet.scene) {
bullet.rewindTime(1);
}
});
} else {
// Not enough energy - force exit rewind mode
if (this.energySystem) {
console.log("Not enough energy to continue rewinding");
// This will trigger the rewind release logic on next frame
this.wasRewindPressed = false;
}
}
}
// Update previous state
this.wasRewindPressed = isRewindPressed;
}
/**
* Get all enemies in the scene
*/
private getAllEnemies(): Enemy[] {
return this.children
.getAll()
.filter((child) => child instanceof Enemy) as Enemy[];
}
/**
* Get all bullets in the scene
*/
private getAllBullets(): Bullet[] {
return this.children
.getAll()
.filter((child) => child instanceof Bullet && child.active) as Bullet[];
}
/**
* Get all living (non-dead) bullets in the scene
*/
private getAllLivingBullets(): Bullet[] {
return this.children
.getAll()
.filter(
(child) => child instanceof Bullet && child.active && !child.isDead_()
) as Bullet[];
}
/**
* Get all energy crystals in the scene
*/
private getAllEnergyCrystals(): EnergyCrystal[] {
return this.children
.getAll()
.filter(
(child) => child instanceof EnergyCrystal && child.canBeCollected()
) as EnergyCrystal[];
}
/**
* Check for collision between player and energy crystals
*/
private checkEnergyCrystalCollection(): void {
if (!this.player || !this.energySystem) return;
const energyCrystals = this.getAllEnergyCrystals();
const playerBounds = this.player.getBounds();
energyCrystals.forEach((crystal) => {
const crystalBounds = crystal.getBounds();
// Check if player and crystal bounds overlap
if (Phaser.Geom.Rectangle.Overlaps(playerBounds, crystalBounds)) {
// Collect the crystal and add energy
const energyGained = crystal.collect();
if (energyGained > 0) {
this.energySystem.addEnergy(energyGained, "crystal_collection");
console.log(
`Player collected energy crystal! Gained ${energyGained} energy`
);
}
}
});
}
/**
* Get all towers in the scene
*/
private getAllTowers(): any[] {
// Get towers from tower manager
const towers = this.towerManager?.getAllTowers() || [];
return towers.map((tower) => tower.gameObject);
}
/**
* Get the enemy spawner in the scene
*/
private getEnemySpawner(): EnemySpawner | null {
const spawner = this.children
.getAll()
.find((child) => child instanceof EnemySpawner) as
| EnemySpawner
| undefined;
return spawner || null;
}
/**
* Handle build tower action from player menu submenu
*/
private handleBuildTowerAction(towerType: TowerType): void {
if (!this.player || !this.towerManager) return;
// Get the tile below the player (same as highlighted tile)
const playerCenterX = this.player.x;
const playerCenterY = this.player.y;
const tileX = Math.floor(playerCenterX / 32);
const tileY = Math.floor(playerCenterY / 32);
// Check if we can build at this location
if (!this.towerManager.canBuildAt(tileX, tileY)) {
console.log("Cannot build at this location!");
return;
}
// Place the specified tower type
const tower = this.towerManager.placeTower(towerType, tileX, tileY);
if (tower) {
console.log(`Built ${tower.config.name} at tile (${tileX}, ${tileY})`);
const menuDepth = this.player.getMenu()?.depth || "undefined";
console.log(
`Depths - Tower: ${(tower.gameObject as any).depth}, Player: ${
this.player.depth
}, Menu: ${menuDepth}`
);
// Notify tutorial system if active
if (this.tutorialSystem && this.tutorialSystem.isInTutorial()) {
this.tutorialSystem.onTowerPlaced();
}
} else {
console.log("Failed to build tower!");
}
}
/**
* Handle sell action from player menu
*/
private handleSellAction(): void {
if (!this.player || !this.towerManager) return;
// Get the tower at the player's current position
const tower = this.towerManager.getTowerAtPlayerPosition(
this.player.x,
this.player.y
);
if (!tower) {
console.log("No tower to sell at this location!");
return;
}
// Remove the tower
const success = this.towerManager.removeTower(tower.tileX, tower.tileY);
if (success) {
const energyGained = tower.config.energyCost / 2;
console.log(`Sold ${tower.config.name} for ${energyGained} energy`);
this.energySystem.addEnergy(energyGained, "sell");
} else {
console.log("Failed to sell tower!");
}
}
/**
* Get the tower at the player's current position
* Public method for external access
*/
public getTowerAtPlayerPosition(): any {
if (!this.player || !this.towerManager) return null;
return this.towerManager.getTowerAtPlayerPosition(
this.player.x,
this.player.y
);
}
/**
* Check if the player can build at their current position
* Public method for external access
*/
public canBuildAtPlayerPosition(): boolean {
if (!this.player || !this.towerManager) return false;
const tileX = Math.floor(this.player.x / 32);
const tileY = Math.floor(this.player.y / 32);
return this.towerManager.canBuildAt(tileX, tileY);
}
/**
* Check if the player menu should be available at their current position
* Menu should open if player can either build OR sell at current position
* Public method for external access
*/
public canOpenMenuAtPlayerPosition(): boolean {
if (!this.player || !this.towerManager) return false;
const tileX = Math.floor(this.player.x / 32);
const tileY = Math.floor(this.player.y / 32);
// Menu should open if player can build OR if there's a tower to sell
return (
this.towerManager.canBuildAt(tileX, tileY) ||
this.towerManager.getTowerAtTile(tileX, tileY) !== null
);
}
/**
* Get the energy system instance
* Public method for external access
*/
public getEnergySystem(): EnergySystem | null {
return this.energySystem || null;
}
/**
* Get the Goal HP system instance
* Public method for external access
*/
public getGoalHPSystem(): GoalHPSystem | null {
return this.goalHPSystem || null;
}
/**
* Get the configuration system instance
* Public method for external access
*/
public getConfigSystem(): ConfigSystem | null {
return this.configSystem || null;
}
/**
* Load default wave configuration and start wave system
*/
private loadDefaultWaves(): void {
if (!this.enemySpawner) {
console.warn("Enemy spawner not initialized - cannot load waves");
return;
}
// Load waves from configuration
const wavesConfig = this.configSystem.getWavesConfig();
this.enemySpawner.loadWaves(wavesConfig);
// Subscribe to wave start events to show overlay BEFORE starting waves
const waveSystem = this.enemySpawner.getWaveSystem();
if (waveSystem && this.waveStartOverlay) {
waveSystem.onWaveStart(
(waveIndex: number, waveName?: string, totalWaves?: number) => {
console.log(
`Wave start event received: Wave ${waveIndex}${
waveName ? ` (${waveName})` : ""
}${totalWaves ? ` (${waveIndex}/${totalWaves})` : ""}`
);
this.waveStartOverlay!.showWave(waveIndex, waveName, totalWaves);
}
);
// Subscribe to all waves complete event to show congratulations
waveSystem.onAllWavesComplete(() => {
console.log(
"All waves complete event received - showing congratulations"
);
this.congratulationsOverlay.show();
});
}
// Start the waves (this will trigger the first wave start event)
this.enemySpawner.startWaves();
console.log("Default waves loaded and started");
}
/**
* Load and start wave configuration
* Call from console: game.scene.getScene('Level').loadWaves()
*/
public loadWaves(): void {
if (!this.enemySpawner) {
console.warn("Enemy spawner not initialized - cannot load waves");
return;
}
const wavesConfig = this.configSystem.getWavesConfig();
this.enemySpawner.stopWaves();
this.enemySpawner.loadWaves(wavesConfig);
// Subscribe to wave start events to show overlay BEFORE starting waves
const waveSystem = this.enemySpawner.getWaveSystem();
if (waveSystem && this.waveStartOverlay) {
waveSystem.onWaveStart(
(waveIndex: number, waveName?: string, totalWaves?: number) => {
console.log(
`Wave start event received: Wave ${waveIndex}${
waveName ? ` (${waveName})` : ""
}${totalWaves ? ` (${waveIndex}/${totalWaves})` : ""}`
);
this.waveStartOverlay!.showWave(waveIndex, waveName, totalWaves);
}
);
// Subscribe to all waves complete event to show congratulations
waveSystem.onAllWavesComplete(() => {
console.log(
"All waves complete event received - showing congratulations"
);
this.congratulationsOverlay.show();
});
}
this.enemySpawner.startWaves();
console.log("Waves loaded and started");
}
/**
* Test the wave start overlay
* Call from console: game.scene.getScene('Level').testWaveOverlay(waveNumber, waveName, totalWaves)
*/
public testWaveOverlay(
waveNumber: number = 1,
waveName?: string,
totalWaves?: number
): void {
if (this.waveStartOverlay) {
this.waveStartOverlay.showWave(waveNumber, waveName, totalWaves);
console.log(
`Showing test wave overlay: Wave ${waveNumber}${
waveName ? ` (${waveName})` : ""
}${totalWaves ? ` (${waveNumber}/${totalWaves})` : ""}`
);
} else {
console.log("Wave start overlay not initialized");
}
}
/**
* Configure wave start delay
* Call from console: game.scene.getScene('Level').setWaveStartDelay(3000)
*/
public setWaveStartDelay(delay: number): void {
if (this.enemySpawner) {
this.enemySpawner.setWaveStartDelay(delay);
console.log(`Wave start delay set to ${delay}ms`);
} else {
console.log("Enemy spawner not initialized");
}
}
/**
* Test the congratulations overlay
* Call from console: game.scene.getScene('Level').testCongratulations()
*/
public testCongratulations(): void {
if (this.congratulationsOverlay) {
this.congratulationsOverlay.show();
console.log("Showing test congratulations overlay");
} else {
console.log("Congratulations overlay not initialized");
}
}
/**
* Handle game over event
*/
private handleGameOver(): void {
if (this.isGameOver) return; // Already handled
this.isGameOver = true;
console.log("Game Over! Goal has been destroyed.");
// Show game over overlay
if (this.gameOverOverlay) {
this.gameOverOverlay.show();
}
// Stop all enemy spawning
const spawner = this.getEnemySpawner();
if (spawner) {
spawner.pauseSpawning();
}
// Stop all enemies
const enemies = this.getAllEnemies();
enemies.forEach((enemy) => {
enemy.stopMovement();
enemy.setEnemyState(EnemyState.IDLE);
});
}
/**
* Get display layers for external access
* Useful for adding objects to specific layers
*/
public getLayers() {
return {
ground: this.groundLayer,
building: this.buildingLayer,
effect: this.effectLayer,
ui: this.uiLayer,
};
}
/**
* Debug method to log depth information
*/
public debugDepths(): void {
console.log("=== DEPTH DEBUG ===");
console.log(`Player depth: ${this.player?.depth || "undefined"}`);
console.log(
`Tile highlight depth: ${this.tileHighlight?.depth || "undefined"}`
);
// Log menu depth
const playerMenu = this.player?.getMenu();
console.log(`Player menu depth: ${playerMenu?.depth || "undefined"}`);
// Log tower depths
const towers = this.towerManager?.getAllTowers() || [];
towers.forEach((tower, index) => {
const depth = (tower.gameObject as any).depth;
console.log(`Tower ${index + 1} depth: ${depth}`);
});
console.log("Expected depths:");
console.log(`- Ground: ${Level.DEPTH_GROUND}`);
console.log(`- Buildings: ${Level.DEPTH_BUILDINGS}`);
console.log(`- Effects: ${Level.DEPTH_EFFECTS}`);
console.log(`- UI/Player: ${Level.DEPTH_UI}`);
console.log(`- Menu: 1000 (highest)`);
}
/**
* Debug method to check object counts in the scene
*/
public debugObjectCounts(): void {
const enemies = this.getAllEnemies();
const bullets = this.getAllBullets();
const livingEnemies = enemies.filter((e) => !e.isDead_());
const deadEnemies = enemies.filter((e) => e.isDead_());
const livingBullets = bullets.filter((b) => !b.isDead_());
const deadBullets = bullets.filter((b) => b.isDead_());
console.log("=== OBJECT COUNT DEBUG ===");
console.log(
`Total enemies: ${enemies.length} (living: ${livingEnemies.length}, dead: ${deadEnemies.length})`
);
console.log(
`Total bullets: ${bullets.length} (living: ${livingBullets.length}, dead: ${deadBullets.length})`
);
console.log(`Total scene children: ${this.children.getAll().length}`);
}
/**
* Debug method to log tilemap information
*/
public debugTilemap(): void {
console.log("=== TILEMAP DEBUG ===");
console.log(
`Tilemap size: ${this.editabletilemap.width}x${this.editabletilemap.height}`
);
console.log(
`Tile size: ${this.editabletilemap.tileWidth}x${this.editabletilemap.tileHeight}`
);
console.log(
`World size: ${this.editabletilemap.widthInPixels}x${this.editabletilemap.heightInPixels}`
);
const playerTileX = Math.floor(this.player.x / 32);
const playerTileY = Math.floor(this.player.y / 32);
console.log(`Player position: (${this.player.x}, ${this.player.y})`);
console.log(`Player tile: (${playerTileX}, ${playerTileY})`);
console.log(
`Can build at player position: ${this.canBuildAtPlayerPosition()}`
);
}
/**
* Debug method to test enemy movement
* Call from console: game.scene.getScene('Level').testEnemyMovement(x, y)
*/
public testEnemyMovement(x: number, y: number): void {
const enemies = this.children
.getAll()
.filter((child) => child instanceof Enemy) as Enemy[];
if (enemies.length > 0) {
const enemy = enemies[0];
console.log(`Setting enemy target to (${x}, ${y})`);
enemy.setTarget(x, y);
} else {
console.log("No enemies found in scene");
}
}
/**
* Debug method to make enemy follow player
* Call from console: game.scene.getScene('Level').makeEnemyFollowPlayer()
*/
public makeEnemyFollowPlayer(): void {
const enemies = this.children
.getAll()
.filter((child) => child instanceof Enemy) as Enemy[];
enemies.forEach((enemy) => {
console.log("Enemy will now follow player");
// Update enemy target to player position every second
const followInterval = setInterval(() => {
if (enemy && this.player) {
enemy.setTarget(this.player.x, this.player.y);
} else {
clearInterval(followInterval);
}
}, 250);
});
}
/**
* Debug method to test pathfinding
* Call from console: game.scene.getScene('Level').testPathfinding(x, y)
*/
public testPathfinding(x: number, y: number): void {
const enemies = this.children
.getAll()
.filter((child) => child instanceof Enemy) as Enemy[];
if (enemies.length > 0) {
const enemy = enemies[0];
const pathLayer = this.editabletilemap.getLayer("path");
if (pathLayer) {
console.log(`Setting enemy pathfinding target to (${x}, ${y})`);
console.log(`Enemy current state: ${enemy.getState()}`);
console.log(`Enemy current position: (${enemy.x}, ${enemy.y})`);
console.log(
`DEBUG mode: ${DEBUG} - Path visualization ${
DEBUG ? "enabled" : "disabled"
}`
);
enemy.setTargetWithPath(x, y, pathLayer.tilemapLayer);
console.log(`Enemy new state: ${enemy.getState()}`);
// Give it a moment to compute the path, then log details
this.time.delayedCall(100, () => {
console.log(`Path computed: ${enemy.getCurrentPath().length} tiles`);
console.log(`Current path index: ${enemy.getCurrentPathIndex()}`);
console.log(`Path destination:`, enemy.getPathDestination());
});
} else {
console.log("Path layer not found!");
}
} else {
console.log("No enemies found in scene");
}
}
/**
* Debug method to inspect current pathfinding state
* Call from console: game.scene.getScene('Level').debugPathfindingState()
*/
public debugPathfindingState(): void {
const enemies = this.children
.getAll()
.filter((child) => child instanceof Enemy) as Enemy[];
if (enemies.length > 0) {
const enemy = enemies[0];
console.log("=== PATHFINDING STATE DEBUG ===");
console.log(`Enemy position: (${enemy.x}, ${enemy.y})`);
console.log(`Enemy state: ${enemy.getState()}`);
console.log(
`Target position: (${enemy.getTarget().x}, ${enemy.getTarget().y})`
);
console.log(`Path destination:`, enemy.getPathDestination());
console.log(`Current path length: ${enemy.getCurrentPath().length}`);
console.log(`Current path index: ${enemy.getCurrentPathIndex()}`);
console.log(`Has reached target: ${enemy.hasReachedTarget()}`);
const currentPath = enemy.getCurrentPath();
if (currentPath.length > 0) {
console.log("Path tiles:");
currentPath.forEach((tile, index) => {
const marker =
index === enemy.getCurrentPathIndex() ? " <- CURRENT TARGET" : "";
console.log(` ${index}: (${tile.x}, ${tile.y})${marker}`);
});
} else {
console.log("No current path");
}
} else {
console.log("No enemies found in scene");
}
}
/**
* Debug method to make enemy pathfind to player
* Call from console: game.scene.getScene('Level').makeEnemyPathfindToPlayer()
*/
public makeEnemyPathfindToPlayer(): void {
const enemies = this.children
.getAll()
.filter((child) => child instanceof Enemy) as Enemy[];
enemies.forEach((enemy) => {
const pathLayer = this.editabletilemap.getLayer("path");
if (pathLayer) {
console.log(
"Enemy will now continuously pathfind to player using path layer"
);
console.log(`Player position: (${this.player.x}, ${this.player.y})`);
console.log(`Enemy current state: ${enemy.getState()}`);
console.log(
"Visual path debugging is enabled when DEBUG = true in main.ts"
);
// Helper function to check if enemy and player are on the same tile
const areOnSameTile = (
enemyX: number,
enemyY: number,
playerX: number,
playerY: number
): boolean => {
const enemyTileX = Math.floor(enemyX / 32);
const enemyTileY = Math.floor(enemyY / 32);
const playerTileX = Math.floor(playerX / 32);
const playerTileY = Math.floor(playerY / 32);
return enemyTileX === playerTileX && enemyTileY === playerTileY;
};
// Update enemy pathfinding target to player position every 500ms
const pathfindInterval = setInterval(() => {
if (enemy && this.player) {
// Only pathfind if not already on the same tile and not currently pathfinding to the right location
const currentDestination = enemy.getPathDestination();
const playerMoved =
!currentDestination ||
Math.abs(currentDestination.x - this.player.x) > 16 ||
Math.abs(currentDestination.y - this.player.y) > 16;
if (
!areOnSameTile(enemy.x, enemy.y, this.player.x, this.player.y) &&
playerMoved
) {
console.log(
`Pathfinding to player at (${this.player.x}, ${this.player.y})`
);
enemy.setTargetWithPath(
this.player.x,
this.player.y,
pathLayer.tilemapLayer
);
} else if (
areOnSameTile(enemy.x, enemy.y, this.player.x, this.player.y)
) {
console.log("Enemy reached player tile, stopping pathfinding");
enemy.setEnemyState(EnemyState.IDLE);
}
} else {
console.log(
"Enemy or player no longer exists, stopping pathfinding"
);
clearInterval(pathfindInterval);
}
}, 250);
// Initial pathfind
if (!areOnSameTile(enemy.x, enemy.y, this.player.x, this.player.y)) {
enemy.setTargetWithPath(
this.player.x,
this.player.y,
pathLayer.tilemapLayer
);
}
console.log(`Enemy new state: ${enemy.getState()}`);
console.log(
"Enemy will update pathfinding every 500ms. Use enemy.setEnemyState('IDLE') to stop."
);
} else {
console.log("Path layer not found!");
}
});
}
/**
* Debug method to test pathfinding to multiple locations
* Call from console: game.scene.getScene('Level').testPathfindingDemo()
*/
public testPathfindingDemo(): void {
const enemies = this.children
.getAll()
.filter((child) => child instanceof Enemy) as Enemy[];
if (enemies.length > 0) {
const enemy = enemies[0];
const pathLayer = this.editabletilemap.getLayer("path");
if (pathLayer) {
console.log(
"Starting pathfinding demo - enemy will move to several locations"
);
console.log(
"Visual path debugging is enabled when DEBUG = true in main.ts"
);
// Test locations on the path
const testLocations = [
{ x: 200, y: 100 },
{ x: 400, y: 200 },
{ x: 300, y: 300 },
{ x: 100, y: 250 },
];
let currentLocationIndex = 0;
const moveToNextLocation = () => {
if (currentLocationIndex < testLocations.length) {
const location = testLocations[currentLocationIndex];
console.log(
`Moving to location ${currentLocationIndex + 1}: (${
location.x
}, ${location.y})`
);
enemy.setTargetWithPath(
location.x,
location.y,
pathLayer.tilemapLayer
);
currentLocationIndex++;
// Schedule next move
this.time.delayedCall(3000, moveToNextLocation);
} else {
console.log("Pathfinding demo completed!");
}
};
moveToNextLocation();
} else {
console.log("Path layer not found!");
}
} else {
console.log("No enemies found in scene");
}
}
/**
* Debug method to stop enemy behavior
* Call from console: game.scene.getScene('Level').stopEnemyBehavior()
*/
public stopEnemyBehavior(): void {
const enemies = this.children
.getAll()
.filter((child) => child instanceof Enemy) as Enemy[];
if (enemies.length > 0) {
const enemy = enemies[0];
console.log("Stopping enemy behavior and setting to IDLE");
enemy.setEnemyState(EnemyState.IDLE);
enemy.stopMovement();
enemy.debugClearVisualization();
console.log(`Enemy state set to: ${enemy.getState()}`);
console.log(
"Note: This will stop pathfinding intervals, but won't clear setInterval calls. Refresh page if needed."
);
} else {
console.log("No enemies found in scene");
}
}
/**
* Debug method to manually spawn an enemy from the spawner
* Call from console: game.scene.getScene('Level').testSpawnEnemy(enemyType)
*/
public testSpawnEnemy(enemyType: string = "BASIC"): void {
const spawner = this.children
.getAll()
.find((child) => child instanceof EnemySpawner) as
| EnemySpawner
| undefined;
if (spawner) {
const enemy = spawner.spawnSpecificType(enemyType);
if (enemy) {
console.log(
`Manually spawned ${enemy.getEnemyConfig().name} enemy:`,
enemy
);
console.log(`Enemy spawned at: (${enemy.x}, ${enemy.y})`);
console.log(
`Enemy target: (${enemy.getTarget().x}, ${enemy.getTarget().y})`
);
console.log(`Enemy state: ${enemy.getState()}`);
} else {
console.log("Failed to spawn enemy - check spawner initialization");
}
} else {
console.log("No enemy spawner found in scene");
}
}
/**
* Debug method to control enemy spawner wave system
* Call from console: game.scene.getScene('Level').controlSpawner('start'|'stop'|'status')
*/
public controlSpawner(action: "start" | "stop" | "status"): void {
const spawner = this.children
.getAll()
.find((child) => child instanceof EnemySpawner) as
| EnemySpawner
| undefined;
if (!spawner) {
console.log("No enemy spawner found in scene");
return;
}
switch (action) {
case "start":
spawner.startWaves();
console.log("Enemy wave spawning started");
break;
case "stop":
spawner.stopWaves();
console.log("Enemy wave spawning stopped");
break;
case "status":
console.log(`Spawner active: ${spawner.isSpawning()}`);
console.log(`Goal buffer: ${spawner.getGoalBuffer()}px`);
const waveSystem = spawner.getWaveSystem();
if (waveSystem) {
console.log(`Wave system: ${waveSystem.getDebugInfo()}`);
}
break;
default:
console.log("Invalid action. Use 'start', 'stop', or 'status'");
}
}
/**
* Debug method to test tower defense system
* Call from console: game.scene.getScene('Level').testTowerDefense()
*/
public testTowerDefense(): void {
console.log("=== TOWER DEFENSE TEST ===");
// Count existing towers
const towers = this.towerManager?.getAllTowers() || [];
console.log(`Existing towers: ${towers.length}`);
// Count enemies
const enemies = this.getAllEnemies();
console.log(`Enemies in scene: ${enemies.length}`);
console.log(
`Living enemies: ${enemies.filter((e) => e.canBeTargeted()).length}`
);
console.log(`Dead enemies: ${enemies.filter((e) => e.isDead_()).length}`);
// Count bullets
const bullets = this.getAllBullets();
const livingBullets = this.getAllLivingBullets();
console.log(`Bullets in scene: ${bullets.length}`);
console.log(`Living bullets: ${livingBullets.length}`);
console.log(`Dead bullets: ${bullets.length - livingBullets.length}`);
// Show tower details
towers.forEach((tower, index) => {
const towerObj = tower.gameObject as any;
console.log(`Tower ${index + 1}:`);
console.log(` Position: (${tower.tileX * 32}, ${tower.tileY * 32})`);
console.log(` Range: ${towerObj.getRange?.() || "unknown"}px`);
console.log(
` Shooting interval: ${
towerObj.getShootingInterval?.() || "unknown"
}ms`
);
console.log(` Total bullets: ${towerObj.getBullets?.()?.length || 0}`);
console.log(
` Living bullets: ${towerObj.getLivingBullets?.()?.length || 0}`
);
console.log(` Is paused: ${towerObj.isPaused_?.() || false}`);
});
if (towers.length === 0) {
console.log(
"TIP: Build a tower using the player menu (P key to toggle), then stand on an empty tile and select 'build'"
);
}
if (enemies.length === 0) {
console.log(
"TIP: Start enemy spawning with: game.scene.getScene('Level').controlSpawner('start')"
);
}
if (livingBullets.length === 0 && enemies.length > 0 && towers.length > 0) {
console.log(
"TIP: Make sure enemies are within tower range for bullets to be fired"
);
}
}
/**
* Debug method to configure tower properties
* Call from console: game.scene.getScene('Level').configureTowers(range, interval)
*/
public configureTowers(range?: number, shootingInterval?: number): void {
const towers = this.towerManager?.getAllTowers() || [];
if (towers.length === 0) {
console.log("No towers found in scene");
return;
}
towers.forEach((tower, index) => {
const towerObj = tower.gameObject as any;
if (range !== undefined && towerObj.setRange) {
towerObj.setRange(range);
console.log(`Tower ${index + 1}: Range set to ${range}px`);
}
if (shootingInterval !== undefined && towerObj.setShootingInterval) {
towerObj.setShootingInterval(shootingInterval);
console.log(
`Tower ${index + 1}: Shooting interval set to ${shootingInterval}ms`
);
}
});
console.log(`Configured ${towers.length} towers`);
}
/**
* Debug method to spawn enemies quickly for testing
* Call from console: game.scene.getScene('Level').spawnTestEnemies(count, enemyType)
*/
public spawnTestEnemies(
count: number = 5,
enemyType: string = "BASIC"
): void {
const spawner = this.children
.getAll()
.find((child) => child instanceof EnemySpawner) as
| EnemySpawner
| undefined;
if (!spawner) {
console.log("No enemy spawner found in scene");
return;
}
console.log(`Spawning ${count} ${enemyType} test enemies...`);
for (let i = 0; i < count; i++) {
const enemy = spawner.spawnSpecificType(enemyType);
if (enemy) {
console.log(
`Spawned ${enemy.getEnemyConfig().name} enemy ${i + 1} at (${
enemy.x
}, ${enemy.y})`
);
} else {
console.log(`Failed to spawn enemy ${i + 1}`);
}
}
}
/**
* Debug method to test bullet collision manually
* Call from console: game.scene.getScene('Level').testBulletCollision()
*/
public testBulletCollision(): void {
const enemies = this.getAllEnemies().filter((e) => e.canBeTargeted());
if (enemies.length === 0) {
console.log("No living enemies found for bullet collision test");
return;
}
const testEnemy = enemies[0];
console.log(
`Testing bullet collision with enemy at (${testEnemy.x}, ${testEnemy.y})`
);
// Create a test bullet near the enemy
const bullet = new Bullet(this, testEnemy.x - 20, testEnemy.y);
this.add.existing(bullet);
bullet.setTarget(testEnemy.x, testEnemy.y, testEnemy);
console.log("Test bullet created - watch for collision!");
}
/**
* Debug method to kill all enemies (for testing rewind)
* Call from console: game.scene.getScene('Level').killAllEnemies()
*/
public killAllEnemies(): void {
const enemies = this.getAllEnemies().filter((e) => e.canBeTargeted());
console.log(`Killing ${enemies.length} enemies...`);
enemies.forEach((enemy, index) => {
enemy.markAsDead();
console.log(`Killed enemy ${index + 1}`);
});
console.log("All enemies killed. Use rewind (R key) to bring them back!");
}
/**
* Debug method to count and show bullet details
* Call from console: game.scene.getScene('Level').debugBullets()
*/
public debugBullets(): void {
const bullets = this.getAllBullets();
const livingBullets = this.getAllLivingBullets();
console.log("=== BULLET DEBUG ===");
console.log(`Total bullets in scene: ${bullets.length}`);
console.log(`Living bullets: ${livingBullets.length}`);
console.log(`Dead bullets: ${bullets.length - livingBullets.length}`);
if (bullets.length > 0) {
console.log("Bullet details:");
bullets.forEach((bullet, index) => {
const velocity = bullet.getVelocity();
const target = bullet.getTargetEnemy();
const isDead = bullet.isDead_();
console.log(` Bullet ${index + 1}: ${isDead ? "[DEAD]" : "[ALIVE]"}`);
console.log(
` Position: (${Math.round(bullet.x)}, ${Math.round(bullet.y)})`
);
console.log(
` Velocity: (${Math.round(velocity.x)}, ${Math.round(velocity.y)})`
);
console.log(` Has hit target: ${bullet.hasHit()}`);
console.log(
` Target enemy: ${
target
? `(${Math.round(target.x)}, ${Math.round(target.y)})`
: "none"
}`
);
console.log(` Active: ${bullet.active}`);
console.log(` Visible: ${bullet.visible}`);
console.log(` Dead: ${isDead}`);
});
} else {
console.log(
"No bullets found. Ensure towers are built and enemies are in range."
);
}
console.log(
"\nTIP: Use rewind (R key) to bring dead bullets back to life!"
);
}
/**
* Debug method to test tower pause/resume functionality
* Call from console: game.scene.getScene('Level').testTowerPause(true/false)
*/
public testTowerPause(shouldPause: boolean): void {
const towers = this.getAllTowers();
console.log("=== TOWER PAUSE TEST ===");
console.log(`Found ${towers.length} towers`);
if (towers.length === 0) {
console.log("No towers found. Build some towers first.");
return;
}
towers.forEach((tower, index) => {
if (shouldPause) {
if (tower.pauseTower) {
tower.pauseTower();
console.log(`Tower ${index + 1}: PAUSED`);
}
} else {
if (tower.resumeTower) {
tower.resumeTower();
console.log(`Tower ${index + 1}: RESUMED`);
}
}
});
console.log(
`\nAll towers ${
shouldPause ? "paused" : "resumed"
}. Check with testTowerDefense() to see status.`
);
}
/**
* Clean up when scene is shut down
*/
shutdown() {
// No need to destroy music system - it's managed globally
}
/* END-USER-CODE */
}
/* END OF COMPILED CODE */
// You can write more code here