User prompt
The code is running, but nothing is visible on screen. I suspect the camera is not looking at the right place. Please help me debug this by temporarily modifying the RenderSystem.update method. After you get the transform and sprite for the entity inside the loop, please add this line to center the game's camera on the sprite: game.camera.centerOn(sprite); The loop should now look something like this: // ... inside the RenderSystem update loop ... var sprite = this.displayObjects[entityId]; // ... sprite.x = transform.x; sprite.y = transform.y; sprite.visible = render.is_visible; // Add this debug line: game.camera.centerOn(sprite);
User prompt
The RenderSystem logic is now complete. Let's connect it to the game and give our test enemy a visual representation. Please make two changes to the Game.GameplayState: In the update() method: Add a call to the new system's update function, so it runs every frame. The method should now look like this: Generated javascript update: function () { Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); }, In the enter() method: When you create the test enemy, please add a RenderComponent to it. The enemy creation block should now look like this: Generated javascript // Create a test enemy var enemyId = Game.EntityManager.createEntity(); var transform = Game.Components.TransformComponent(0 * 64, 30 * 64); var movement = Game.Components.MovementComponent(2, 0); // Increased speed slightly var render = Game.Components.RenderComponent('enemy', 1, 1.0, true); Game.EntityManager.addComponent(enemyId, transform); Game.EntityManager.addComponent(enemyId, movement); Game.EntityManager.addComponent(enemyId, render); I've also slightly increased the movement speed in the example code so we can see it move more clearly.
Code edit (2 edits merged)
Please save this source code
User prompt
The ECS engine and the MovementSystem are perfect. The logic for handling when an enemy reaches the end of the path is an excellent addition. For the final step of building our game's foundation, let's activate the systems and spawn a test enemy. 1. Update the GameplayState: Find the Game.GameplayState object. In its update() method, add a call to run our new system: Game.Systems.MovementSystem.update(); In its enter() method, after all the init() calls, add a call to recalculate the initial path: Game.PathManager.recalculatePath({ x: 0, y: 30 }, { x: 39, y: 30 }); 2. Create a Test Enemy: Still inside the GameplayState.enter() method, let's spawn a single test enemy. Create a new entity ID: const enemyId = Game.EntityManager.createEntity(); Create its components: const transform = Game.Components.TransformComponent(0, 0); // We'll need to adjust x/y later const movement = Game.Components.MovementComponent(0.05, 0); // A very slow speed for testing Important: Set the starting transform.x and transform.y to match the pixel coordinates of the first waypoint in the path. Since our grid is 40x60 and we haven't defined a tile size, let's assume a tile size of 64 for now. The first waypoint is {x:0, y:30}. So, set transform.x = 0 * 64 and transform.y = 30 * 64. Attach the components to the entity: Game.EntityManager.addComponent(enemyId, transform); Game.EntityManager.addComponent(enemyId, movement); 3. Convert Path Coordinates: The MovementSystem expects pixel coordinates, but the PathManager returns grid coordinates. We need to convert them. Find the Game.PathManager.recalculatePath method. After this.currentPath = this.findPath(start, end);, add a check: if (this.currentPath). Inside the if block, loop through this.currentPath and multiply each x and y coordinate by a tile size of 64. This will convert the path from grid coordinates to world/pixel coordinates for the MovementSystem.
User prompt
Step 7: The First "System" and Path Following According to the ECS pattern, "Systems" are where the game's logic lives. They operate on entities that have a certain set of components. It's time to create our first real system: the MovementSystem. But first, we have a small problem to solve. Our ECS is still just scaffolding. We can create entities and define components, but there's no way to attach a component to an entity or for a system to find all entities with a specific component. We need to build this part of the ECS engine first. Copy and paste this new, two-part prompt to Ava: This A* implementation is perfect. We now have our core pathfinding. Next, we need to complete our ECS engine so we can create systems that act on entities. Then we will create our first system, the MovementSystem. Part 1: Complete the ECS Engine In the Game.EntityManager object, we need to add component storage and query functions. Add Component Storage: Add a new property: componentStores: {}. Add addComponent Method: Create a method addComponent(entityId, component). The component object will have a name property (e.g., 'TransformComponent'). If this.componentStores[component.name] doesn't exist, create it as an empty object. Store the component at this.componentStores[component.name][entityId] = component;. Modify Components: Add a name property to each of the component factory functions in Game.Components. For example, TransformComponent should return { name: 'TransformComponent', x: x, ... }. Add getEntitiesWithComponents Method: Create a method getEntitiesWithComponents(componentNames). This is the query function. It takes an array of component names (e.g., ['TransformComponent', 'MovementComponent']). It should loop through all activeEntities and return a new array containing only the IDs of entities that have all of the required components. Part 2: Create the MovementSystem Now, create our first system. In the Game.Systems object, create a new object called MovementSystem. Give it an update() method. Inside update(), get all movable entities by calling Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']). Loop through each entity ID found. For each entity: Retrieve its TransformComponent and MovementComponent from the EntityManager's stores. Look up the target waypoint in Game.PathManager.currentPath using the entity's path_index. Calculate the movement towards the target waypoint based on the entity's speed and update the x and y values in its TransformComponent. If the entity reaches the waypoint, increment its path_index.
User prompt
The GridManager is perfect. The bounds checking in isBuildable was an excellent addition. Now, let's implement the core pathfinding logic. We will create a new PathManager that will be responsible for calculating and holding the enemy path using the A* algorithm. 1. Create a PathManager: Under the // === SECTION: GLOBAL MANAGERS ===, create a new object named Game.PathManager. 2. Define PathManager Properties and Methods: Give the Game.PathManager the following: currentPath: [] (An array to store the latest calculated path). init(): An init method to set up event listeners. onTowerPlaced(event): A handler for when a tower is placed. recalculatePath(start, end): The main method to trigger a path recalculation. 3. Implement the A Pathfinding Function:* This is the most important part. Add a new method to Game.PathManager named findPath(start, end). This function is complex. It must implement the A* search algorithm. Signature: It takes a start object {x, y} and an end object {x, y}. Behavior: It should search the Game.GridManager.grid. Cost Function: Tiles marked BLOCKED or TOWER in Game.GridManager are impassable (infinite cost). Movement to adjacent orthogonal tiles (up, down, left, right) has a cost of 1. Movement to adjacent diagonal tiles has a cost of 1.4. Return Value: If a path is found, it must return an array of coordinate objects [{x, y}, {x, y}, ...]. If no path is found, it must return null. 4. Connect with the Event Bus: In the PathManager's recalculatePath(start, end) method, it should call this.findPath(start, end) and store the result in this.currentPath. The onTowerPlaced(event) handler should call this.recalculatePath(). You can use temporary hard-coded start/end points for now, like {x:0, y:30} and {x:39, y:30}. In the PathManager's init() method, subscribe to the TowerPlacedEvent: Game.EventBus.subscribe('TowerPlaced', this.onTowerPlaced.bind(this)); 5. Update GameplayState: In the Game.GameplayState's enter() method, add a call to initialize the new manager: Game.PathManager.init();
User prompt
Fantastic, the ResourceManager is fully functional. Now, let's create the foundational grid system for the battlefield as described in Section 3 of the blueprint. We will create a new manager to handle all grid-related data and logic. 1. Create a GridManager: Under the // === SECTION: GLOBAL MANAGERS ===, create a new object named Game.GridManager. 2. Define Grid Properties: Give the Game.GridManager the following properties: grid: [] (an empty array to hold the grid data). CELL_STATES: { EMPTY: 0, PATH: 1, BLOCKED: 2, TOWER: 3 } (an enumeration for cell states). 3. Create Grid Methods: Give the Game.GridManager the following methods: init(width, height): This method will create the grid. It should initialize the this.grid property as a 2D array of the given width and height. It should loop through every cell and set its initial value to this.CELL_STATES.EMPTY. isBuildable(x, y): This function should take grid coordinates x and y as input. It must return true if this.grid[x][y] is equal to this.CELL_STATES.EMPTY, and false otherwise. 4. Initialize the Grid in GameplayState: Now we need to connect this to our game state. Find the Game.GameplayState class. Modify its enter() method. Inside enter(), add a call to initialize the grid: Game.GridManager.init(40, 60);. Also in the enter() method, call the init method for our resource manager: Game.ResourceManager.init();. This ensures it starts listening for events when the game starts.
User prompt
Perfect. The EventBus and Events are exactly right. Now, let's make the Game.ResourceManager functional. We will implement its methods and subscribe it to events so it can manage the player's economy. 1. Implement ResourceManager Methods: In the Game.ResourceManager object, fill in the logic for its methods: addGold(amount): Should increase this.gold by the amount. spendGold(amount): Should check if this.gold >= amount. If true, it should subtract the amount from this.gold and return true. Otherwise, it should return false. loseLife(amount): Should decrease this.lives by the amount. Afterwards, if this.lives <= 0, it should publish a new GameOverEvent. 2. Create ResourceManager Handlers: Add two new methods to the Game.ResourceManager to handle specific events: onEnemyKilled(event): This method should call this.addGold(event.gold_value). onWaveCompleted(event): This method should call this.addGold(50). (We'll use a fixed bonus of 50 gold for now). 3. Add an init Method for Subscriptions: Add a new method to Game.ResourceManager called init(). Inside this init method, it should subscribe its handlers to the EventBus: Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('WaveCompleted', this.onWaveCompleted.bind(this)); 4. Define the GameOverEvent: In the Game.Events object, add one more event factory: GameOverEvent: function() { return { type: 'GameOver' }; }
User prompt
Excellent work. The ResourceManager and Components are implemented perfectly. Now, let's implement the logic for the Game.EventBus so our systems can communicate. We will also define the initial event data structures. 1. Implement Event Bus Logic: Find the Game.EventBus object and modify it as follows: Add a listeners property, initializing it as an empty object: listeners: {}. Implement subscribe(eventType, listener): If eventType doesn't exist in listeners, create it as an empty array. Then, push the listener function into the this.listeners[eventType] array. Implement publish(event): This method should take an event object which must have a type property. Find the array of listeners at this.listeners[event.type]. If it exists, loop through all the listener functions in that array and call each one, passing the full event object to it. Implement unsubscribe(eventType, listener): Find the array of listeners for the eventType. If it exists, find the specified listener in that array and remove it. 2. Define Initial Event Types: For better organization, first create a new empty object Game.Events = {};. Then, inside Game.Events, define the following event structures. Like the components, these should be simple functions that create data objects. EnemyReachedEndEvent { type: 'EnemyReachedEnd', enemy_id: int } EnemyKilledEvent { type: 'EnemyKilled', enemy_id: int, gold_value: int } TowerPlacedEvent { type: 'TowerPlaced', tower_id: int, cost: int, position_x: int, position_y: int } TowerSoldEvent { type: 'TowerSold', tower_id: int, refund_value: int } PowerStatusChangedEvent { type: 'PowerStatusChanged', new_status: string } WaveCompletedEvent { type: 'WaveCompleted', wave_number: int } Each event object must have a type property corresponding to its name.
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'ResourceManager')' in or related to this line: 'Game.ResourceManager = {' Line Number: 12
User prompt
This is the perfect architectural foundation. Great job. Now, let's add the first layer of game-specific data, following a strict data-oriented approach. We will implement the base Component structures and the ResourceManager. 1. Base Component Structures: In the Game.Components object, define the following data structures. They must be simple functions or objects that hold data only, with no methods or logic. TransformComponent { x: float, y: float, rotation: float } RenderComponent { sprite_id: string, layer: int, scale: float, is_visible: bool } HealthComponent { current_hp: float, max_hp: float } AttackComponent { damage: float, range: float, attack_speed: float, last_attack_time: float, projectile_id: string, target_id: int } MovementComponent { speed: float, path_index: int } 2. Global Resource Manager: Under the // === SECTION: GLOBAL MANAGERS === comment (create one if needed), create a Game.ResourceManager object. It should act as the single source of truth for the player's economy. Managed Variables: Give it the following properties with the specified initial values from the blueprint: gold: 100 lives: 20 powerProduction: 0 powerConsumption: 0 powerAvailable: 0 Core Functions: Give it the following empty methods. We will implement their logic later. addGold(amount) spendGold(amount) loseLife(amount) updatePower()
User prompt
Hello Ava. Let's restart. The previous code was a good example of a simple game, but for the complex game we are building, we need to create a very specific, modular architecture first. Please clear the previous code and start fresh with only the following instructions. Important: Do NOT add any specific gameplay logic yet. We are only building the empty scaffolding. There should be no towers, enemies, gold, or power in this first step. We will add those later. Please implement the following architectural skeleton: 1. Global Namespace: Create a single global object: const Game = {}; 2. Entity-Component-System (ECS) Scaffolding: Create a Game.EntityManager object. It needs a method createEntity() that returns a new, unique integer ID, and an array to track all active entities. Create empty placeholder objects: Game.Components = {}; and Game.Systems = {}; 3. Global Event Bus: Create a Game.EventBus object. It must have three methods with empty function bodies for now: subscribe(eventType, listener), unsubscribe(eventType, listener), and publish(event). 4. Stack-Based Game State Manager: Create a Game.GameStateManager class. It must manage a stack (an array) of game states. It needs push(state), pop(), and getCurrentState() methods. Define a base Game.IGameState class. It must have the following empty methods: enter(), exit(), update(), and render(). Create three empty state classes that inherit from Game.IGameState: Game.MainMenuState, Game.GameplayState, and Game.PausedState. 5. Main Game Loop: Create a main gameLoop function. Inside, it should call the update() and render() methods of the current state provided by the GameStateManager. Create an instance of the GameStateManager, push a new MainMenuState onto it, and start the gameLoop. Finally, please use prominent comments to delineate these five sections to keep the code organized.
Code edit (1 edits merged)
Please save this source code
User prompt
Aetherium Grid Defense
Initial prompt
Hello, Ava! We are going to build a 2D tower defense game called Aetherium Grid Defense. The game's core mechanic is managing two resources: transactional Gold and a capacity-based Power grid. To start, please set up the foundational architecture for the game in a single file. This architecture must be highly modular to make adding features easy later. Please implement the following core structures: A Global Namespace: Create a single global object called Game to hold all our game-related code. This will keepthe global scope clean. Entity-Component-System (ECS) Foundation: Create a Game.EntityManager object. It should be responsible for creating unique integer IDs for entities and tracking them. Create empty placeholder objects within the Game namespace for Game.Components, Game.Systems, and Game.Entities. We will add to these later. A Global Event Bus: Create a Game.EventBus object. It must have three methods:subscribe(eventType, listener), unsubscribe(eventType, listener), and publish(event). A Stack-Based Game State Manager: Create a Game.GameStateManager class. It should manage a stack of game states. It needs methods to push a state onto the stack, pop a state off, and get the current state (the one on top). Define a base IGameState interface or class with enter(), exit(), update(), andrender() methods. Create empty placeholder classes for MainMenuState, GameplayState, and PausedState that inherit from IGameState. The Main Game Loop: Create a mainGameLoop function that will be driven by requestAnimationFrame. Inside the loop, it should call the update() and render() methods of the current active state from the GameStateManager.Finally, please use prominent comments to delineate the major sections of the code (e.g., // === SECTION: GLOBAL MANAGERS ===, // === SECTION: GAME STATES ===, etc.) to keep the single file organized and readable.
/**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // === SECTION: GLOBAL NAMESPACE === // === SECTION: ASSETS === var Game = {}; // Camera state var cameraX = 0; var cameraY = 0; var isDragging = false; var dragStartX = 0; var dragStartY = 0; var cameraStartX = 0; var cameraStartY = 0; var lastMouseX = 0; var lastMouseY = 0; var mouseDownTime = 0; // === SECTION: GLOBAL MANAGERS === Game.ResourceManager = { gold: 500, lives: 20, powerProduction: 0, powerConsumption: 0, powerAvailable: 0, addGold: function addGold(amount) { this.gold += amount; }, spendGold: function spendGold(amount) { if (this.gold >= amount) { this.gold -= amount; return true; } ; return false; }, loseLife: function loseLife(amount) { this.lives -= amount; if (this.lives <= 0) { Game.EventBus.publish(Game.Events.GameOverEvent()); } }, updatePower: function updatePower() { // 1. Calculate total power production var totalProduction = 0; var productionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerProductionComponent']); for (var i = 0; i < productionEntities.length; i++) { var entityId = productionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerProductionComponent'][entityId]; totalProduction += powerComp.production_rate; } this.powerProduction = totalProduction; // 2. Calculate total power consumption var totalConsumption = 0; var consumptionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerConsumptionComponent']); for (var i = 0; i < consumptionEntities.length; i++) { var entityId = consumptionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerConsumptionComponent'][entityId]; totalConsumption += powerComp.drain_rate; } this.powerConsumption = totalConsumption; // 3. Calculate total storage capacity var totalCapacity = 0; var storageEntities = Game.EntityManager.getEntitiesWithComponents(['PowerStorageComponent']); for (var i = 0; i < storageEntities.length; i++) { var entityId = storageEntities[i]; var storageComp = Game.EntityManager.componentStores['PowerStorageComponent'][entityId]; totalCapacity += storageComp.capacity; } // 4. Evaluate power status and update available buffer var surplus = totalProduction - totalConsumption; if (surplus >= 0) { // We have enough or extra power this.powerAvailable += surplus; // Clamp the available power to the max capacity if (this.powerAvailable > totalCapacity) { this.powerAvailable = totalCapacity; } Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } else { // We have a deficit, drain from the buffer this.powerAvailable += surplus; // surplus is negative, so this subtracts if (this.powerAvailable < 0) { // Buffer is empty and we still have a deficit this.powerAvailable = 0; Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('deficit')); } else { // Deficit was covered by the buffer Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } } }, onEnemyKilled: function onEnemyKilled(event) { this.addGold(event.gold_value); }, onWaveCompleted: function onWaveCompleted(event) { this.addGold(50); }, init: function init() { Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('WaveCompleted', this.onWaveCompleted.bind(this)); } }; Game.GridManager = { grid: [], CELL_STATES: { EMPTY: 0, PATH: 1, BLOCKED: 2, TOWER: 3 }, init: function init(width, height) { this.grid = []; for (var x = 0; x < width; x++) { this.grid[x] = []; for (var y = 0; y < height; y++) { // Each cell is now an object storing state and entity ID this.grid[x][y] = { state: this.CELL_STATES.EMPTY, entityId: null }; } } }, isBuildable3x3: function isBuildable3x3(centerX, centerY) { // Checks a 3x3 area centered on the given coordinates for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var x = centerX + dx; var y = centerY + dy; if (x < 0 || x >= this.grid.length || y < 0 || y >= this.grid[0].length) { return false; // Out of bounds } if (this.grid[x][y].state !== this.CELL_STATES.EMPTY) { return false; // Cell is not empty } } } return true; }, isBuildable: function isBuildable(x, y) { if (x >= 0 && x < this.grid.length && y >= 0 && y < this.grid[x].length) { return this.grid[x][y].state === this.CELL_STATES.EMPTY; } return false; } }; Game.PathManager = { distanceField: [], flowField: [], init: function init(width, height) { this.distanceField = Array(width).fill(null).map(function () { return Array(height).fill(Infinity); }); this.flowField = Array(width).fill(null).map(function () { return Array(height).fill({ x: 0, y: 0 }); }); Game.EventBus.subscribe('TowerPlaced', this.generateFlowField.bind(this)); Game.EventBus.subscribe('TowerSold', this.generateFlowField.bind(this)); // Generate the initial field when the game starts this.generateFlowField(); }, generateFlowField: function generateFlowField() { console.log("Generating new flow field..."); var grid = Game.GridManager.grid; var width = grid.length; var height = grid[0].length; // 1. Reset fields this.distanceField = Array(width).fill(null).map(function () { return Array(height).fill(Infinity); }); // 2. Integration Pass (BFS) var goal = { x: 39, y: 30 }; var queue = [goal]; this.distanceField[goal.x][goal.y] = 0; var head = 0; while (head < queue.length) { var current = queue[head++]; var neighbors = [{ x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: -1 }, { x: 0, y: 1 }]; // Orthogonal neighbors for (var i = 0; i < neighbors.length; i++) { var n = neighbors[i]; var nx = current.x + n.x; var ny = current.y + n.y; // Check bounds and if traversable (checks the .state property) if (nx >= 0 && nx < width && ny >= 0 && ny < height && grid[nx][ny].state !== Game.GridManager.CELL_STATES.TOWER && this.distanceField[nx][ny] === Infinity) { this.distanceField[nx][ny] = this.distanceField[current.x][current.y] + 1; queue.push({ x: nx, y: ny }); } } } // 3. Flow Pass for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { if (this.distanceField[x][y] === Infinity) { continue; } var bestDist = Infinity; var bestVector = { x: 0, y: 0 }; var allNeighbors = [{ x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: -1 }, { x: 1, y: -1 }, { x: -1, y: 1 }, { x: 1, y: 1 }]; // Include diagonals for flow for (var i = 0; i < allNeighbors.length; i++) { var n = allNeighbors[i]; var nx = x + n.x; var ny = y + n.y; if (nx >= 0 && nx < width && ny >= 0 && ny < height) { if (this.distanceField[nx][ny] < bestDist) { bestDist = this.distanceField[nx][ny]; bestVector = n; } } } this.flowField[x][y] = bestVector; } } // 4. Announce that the field has been updated // (We will add the event publication in a later step) }, getVectorAt: function getVectorAt(worldX, worldY) { var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); if (this.flowField[gridX] && this.flowField[gridX][gridY]) { return this.flowField[gridX][gridY]; } return { x: 0, y: 0 }; // Default case } }; Game.GameplayManager = { selectedTowerId: -1, activeBuildType: null, // The tower type selected from the build panel (e.g., 'ArrowTower') isConfirmingPlacement: false, // True when the confirm/cancel dialog is shown confirmPlacementCoords: { x: null, y: null }, // Stores the grid coords for confirmation lastClickedButton: null, // Stores a reference to the UI button that was clicked enemiesSpawnedThisWave: 0, enemiesKilledThisWave: 0, enemiesLeakedThisWave: 0, totalEnemiesThisWave: 0, init: function init() { Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('EnemyReachedEnd', this.onEnemyReachedEnd.bind(this)); }, onEnemyKilled: function onEnemyKilled(event) { this.enemiesKilledThisWave++; if (this.enemiesKilledThisWave + this.enemiesLeakedThisWave >= this.totalEnemiesThisWave) { Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1)); this.enemiesKilledThisWave = 0; this.enemiesLeakedThisWave = 0; startWaveButton.visible = true; } }, onEnemyReachedEnd: function onEnemyReachedEnd(event) { this.enemiesLeakedThisWave++; Game.ResourceManager.loseLife(1); Game.EntityManager.destroyEntity(event.enemy_id); if (this.enemiesKilledThisWave + this.enemiesLeakedThisWave >= this.totalEnemiesThisWave) { Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1)); this.enemiesKilledThisWave = 0; this.enemiesLeakedThisWave = 0; startWaveButton.visible = true; } } }; Game.GameManager = { isGameOver: false, init: function init() { Game.EventBus.subscribe('GameOver', this.onGameOver.bind(this)); }, onGameOver: function onGameOver(event) { this.isGameOver = true; LK.showGameOver(); } }; Game.Debug = { logText: null, init: function init() { this.logText = new Text2('Debug Log Initialized', { size: 40, fill: 0x00FF00 }); // Green text this.logText.anchor.set(0, 1); // Anchor to bottom-left LK.gui.bottomLeft.addChild(this.logText); this.logText.x = 20; this.logText.y = -20; }, log: function log(message) { if (this.logText) { this.logText.setText(String(message)); } } }; // === SECTION: ENTITY-COMPONENT-SYSTEM SCAFFOLDING === Game.EntityManager = { nextEntityId: 1, activeEntities: [], componentStores: {}, entitiesToDestroy: [], createEntity: function createEntity() { var entityId = this.nextEntityId++; this.activeEntities.push(entityId); return entityId; }, addComponent: function addComponent(entityId, component) { if (!this.componentStores[component.name]) { this.componentStores[component.name] = {}; } this.componentStores[component.name][entityId] = component; }, getEntitiesWithComponents: function getEntitiesWithComponents(componentNames) { var result = []; for (var i = 0; i < this.activeEntities.length; i++) { var entityId = this.activeEntities[i]; var hasAllComponents = true; for (var j = 0; j < componentNames.length; j++) { var componentName = componentNames[j]; if (!this.componentStores[componentName] || !this.componentStores[componentName][entityId]) { hasAllComponents = false; break; } } if (hasAllComponents) { result.push(entityId); } } return result; }, destroyEntity: function destroyEntity(entityId) { // Avoid adding duplicates if (this.entitiesToDestroy.indexOf(entityId) === -1) { this.entitiesToDestroy.push(entityId); } } }; Game.Components = { TransformComponent: function TransformComponent(x, y, rotation) { return { name: 'TransformComponent', x: x || 0, y: y || 0, rotation: rotation || 0 }; }, RenderComponent: function RenderComponent(sprite_id, layer, scale, is_visible) { return { name: 'RenderComponent', sprite_id: sprite_id || '', layer: layer || 0, scale: scale || 1.0, is_visible: is_visible !== undefined ? is_visible : true }; }, HealthComponent: function HealthComponent(current_hp, max_hp) { return { name: 'HealthComponent', current_hp: current_hp || 100, max_hp: max_hp || 100 }; }, AttackComponent: function AttackComponent(damage, range, attack_speed, last_attack_time, projectile_id, target_id, splash_radius) { return { name: 'AttackComponent', damage: damage || 10, range: range || 100, attack_speed: attack_speed || 1.0, last_attack_time: last_attack_time || 0, projectile_id: projectile_id || '', target_id: target_id || -1, splash_radius: splash_radius || 0 // Default to 0 (no splash) }; }, MovementComponent: function MovementComponent(speed) { return { name: 'MovementComponent', speed: speed || 50 }; }, PowerProductionComponent: function PowerProductionComponent(rate) { return { name: 'PowerProductionComponent', production_rate: rate || 0 }; }, PowerConsumptionComponent: function PowerConsumptionComponent(rate) { return { name: 'PowerConsumptionComponent', drain_rate: rate || 0 }; }, TowerComponent: function TowerComponent(cost, sellValue, builtOnWave) { return { name: 'TowerComponent', cost: cost || 50, sell_value: sellValue || 25, upgrade_level: 1, upgrade_path_A_level: 0, upgrade_path_B_level: 0, built_on_wave: builtOnWave || 0 }; }, ProjectileComponent: function ProjectileComponent(target_id, speed, damage) { return { name: 'ProjectileComponent', target_id: target_id || -1, speed: speed || 8, // Let's keep the speed lower damage: damage || 10 }; }, EnemyComponent: function EnemyComponent(goldValue, livesCost, armor_type) { return { name: 'EnemyComponent', gold_value: goldValue || 5, lives_cost: livesCost || 1, armor_type: armor_type || 'Normal' }; }, PowerStorageComponent: function PowerStorageComponent(capacity) { return { name: 'PowerStorageComponent', capacity: capacity || 0 }; }, PoisonOnImpactComponent: function PoisonOnImpactComponent(damage_per_second, duration) { return { name: 'PoisonOnImpactComponent', damage_per_second: damage_per_second || 0, duration: duration || 0 }; }, PoisonDebuffComponent: function PoisonDebuffComponent(damage_per_second, duration_remaining, last_tick_time) { return { name: 'PoisonDebuffComponent', damage_per_second: damage_per_second || 0, duration_remaining: duration_remaining || 0, last_tick_time: last_tick_time || 0 }; }, SlowOnImpactComponent: function SlowOnImpactComponent(slow_percentage, duration) { return { name: 'SlowOnImpactComponent', slow_percentage: slow_percentage || 0, duration: duration || 0 }; }, SlowDebuffComponent: function SlowDebuffComponent(slow_percentage, duration_remaining) { return { name: 'SlowDebuffComponent', slow_percentage: slow_percentage || 0, duration_remaining: duration_remaining || 0 }; }, AuraComponent: function AuraComponent(range, effect_type) { return { name: 'AuraComponent', range: range || 100, effect_type: effect_type || '' }; }, DamageBuffComponent: function DamageBuffComponent(multiplier, source_aura_id) { return { name: 'DamageBuffComponent', multiplier: multiplier || 0, source_aura_id: source_aura_id || -1 }; }, GoldGenerationComponent: function GoldGenerationComponent(gold_per_second) { return { name: 'GoldGenerationComponent', gold_per_second: gold_per_second || 0 }; }, FlyingComponent: function FlyingComponent() { return { name: 'FlyingComponent' }; }, // --- NEW COMPONENTS --- FireZoneOnImpactComponent: function FireZoneOnImpactComponent(damage_per_tick, duration, tick_rate) { return { name: 'FireZoneOnImpactComponent', damage_per_tick: damage_per_tick || 0, duration: duration || 0, tick_rate: tick_rate || 1.0 }; }, FireZoneComponent: function FireZoneComponent(damage_per_tick, duration_remaining, tick_rate, last_tick_time) { return { name: 'FireZoneComponent', damage_per_tick: damage_per_tick || 0, duration_remaining: duration_remaining || 0, tick_rate: tick_rate || 1.0, last_tick_time: last_tick_time || 0 }; } }; Game.Systems = { MovementSystem: { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); var frame_time = 1 / 60; // Assuming 60 FPS var goal = { x: 39, y: 30 }; // The goal grid cell for (var i = 0; i < movableEntities.length; i++) { var entityId = movableEntities[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][entityId]; var movement = Game.EntityManager.componentStores['MovementComponent'][entityId]; var currentSpeed = movement.speed; // Check for slow debuff (no changes here) var slowDebuff = Game.EntityManager.componentStores['SlowDebuffComponent'] ? Game.EntityManager.componentStores['SlowDebuffComponent'][entityId] : null; if (slowDebuff) { currentSpeed = movement.speed * (1 - slowDebuff.slow_percentage); slowDebuff.duration_remaining -= frame_time; if (slowDebuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['SlowDebuffComponent'][entityId]; } } // --- START OF NEW FLOW FIELD LOGIC --- // 1. Get the direction vector from the flow field at the enemy's current position. var direction = Game.PathManager.getVectorAt(transform.x, transform.y); // 2. Calculate how far to move this frame. var moveDistance = currentSpeed * frame_time; // 3. Update the enemy's position. transform.x += direction.x * moveDistance; transform.y += direction.y * moveDistance; // 4. Check if the enemy has reached the goal. var gridX = Math.floor(transform.x / 64); var gridY = Math.floor(transform.y / 64); if (gridX === goal.x && gridY === goal.y) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } // --- END OF NEW FLOW FIELD LOGIC --- } } }, RenderSystem: { displayObjects: {}, update: function update() { var renderableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'RenderComponent']); for (var i = 0; i < renderableEntities.length; i++) { var entityId = renderableEntities[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][entityId]; var render = Game.EntityManager.componentStores['RenderComponent'][entityId]; var sprite = this.displayObjects[entityId]; if (!sprite) { sprite = gameWorld.attachAsset(render.sprite_id, {}); // --- NEW ANCHOR LOGIC --- // This is the critical fix: Ensure all game entities are visually centered. sprite.anchor.set(0.5, 0.5); // --- END NEW ANCHOR LOGIC --- this.displayObjects[entityId] = sprite; } sprite.x = transform.x; sprite.y = transform.y; sprite.visible = render.is_visible; } } }, TowerBuildSystem: { tryBuildAt: function tryBuildAt(gridX, gridY, towerType) { var towerData = Game.TowerData[towerType]; // 1. Use the new 3x3 build check if (!towerData || !Game.GridManager.isBuildable3x3(gridX, gridY)) { return; } if (!Game.ResourceManager.spendGold(towerData.cost)) { return; } // 2. Temporarily mark the 3x3 area on the grid for path validation for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { Game.GridManager.grid[gridX + dx][gridY + dy].state = Game.GridManager.CELL_STATES.TOWER; } } // 3. Regenerate flow field and validate path Game.PathManager.generateFlowField(); var spawnDistance = Game.PathManager.distanceField[0][30]; if (spawnDistance === Infinity) { // INVALID PLACEMENT: Revert the grid cells and refund gold for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { Game.GridManager.grid[gridX + dx][gridY + dy].state = Game.GridManager.CELL_STATES.EMPTY; } } Game.ResourceManager.addGold(towerData.cost); // Re-generate the flow field to its valid state Game.PathManager.generateFlowField(); return; } // --- VALID PLACEMENT --- var towerId = Game.EntityManager.createEntity(); // 4. Permanently mark the 3x3 grid area with the new tower's ID for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { Game.GridManager.grid[gridX + dx][gridY + dy].entityId = towerId; } } var centeredX = gridX * 64 + 32; var centeredY = gridY * 64 + 32; var transform = Game.Components.TransformComponent(centeredX, centeredY); Game.EntityManager.addComponent(towerId, transform); var render = Game.Components.RenderComponent(towerData.sprite_id, 2, 1.0, true); Game.EntityManager.addComponent(towerId, render); for (var i = 0; i < towerData.components.length; i++) { var componentDef = towerData.components[i]; var component; if (componentDef.name === 'TowerComponent') { var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; component = Game.Components.TowerComponent(componentDef.args[0], componentDef.args[1], currentWave); } else { component = Game.Components[componentDef.name].apply(null, componentDef.args); } Game.EntityManager.addComponent(towerId, component); } var event = Game.Events.TowerPlacedEvent(towerId, towerData.cost, transform.x, transform.y); Game.EventBus.publish(event); } }, TargetingSystem: { update: function update() { // If there is a power deficit, do not assign any new targets. if (Game.Systems.CombatSystem.isPowerDeficit) { // Also, clear any existing targets to prevent towers from holding a lock. var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent']); for (var i = 0; i < towers.length; i++) { var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towers[i]]; towerAttack.target_id = -1; } return; // Stop the system's update. } var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); for (var i = 0; i < towers.length; i++) { var towerId = towers[i]; var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towerId]; var towerTransform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var bestTarget = -1; var bestPathIndex = -1; var enemies = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); for (var j = 0; j < enemies.length; j++) { var enemyId = enemies[j]; var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; var dx = enemyTransform.x - towerTransform.x; var dy = enemyTransform.y - towerTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= towerAttack.range) { // Check if enemy has PathFollowerComponent (ground units) or use default targeting for flyers var pathFollower = Game.EntityManager.componentStores['PathFollowerComponent'] ? Game.EntityManager.componentStores['PathFollowerComponent'][enemyId] : null; var currentPathIndex = pathFollower ? pathFollower.path_index : 0; if (currentPathIndex > bestPathIndex) { bestTarget = enemyId; bestPathIndex = currentPathIndex; } } } towerAttack.target_id = bestTarget; } } }, CleanupSystem: { update: function update() { for (var i = 0; i < Game.EntityManager.entitiesToDestroy.length; i++) { var entityId = Game.EntityManager.entitiesToDestroy[i]; var index = Game.EntityManager.activeEntities.indexOf(entityId); if (index > -1) { Game.EntityManager.activeEntities.splice(index, 1); } for (var componentName in Game.EntityManager.componentStores) { if (Game.EntityManager.componentStores[componentName][entityId]) { delete Game.EntityManager.componentStores[componentName][entityId]; } } if (Game.Systems.RenderSystem.displayObjects[entityId]) { Game.Systems.RenderSystem.displayObjects[entityId].destroy(); delete Game.Systems.RenderSystem.displayObjects[entityId]; } } if (Game.EntityManager.entitiesToDestroy.length > 0) { Game.EntityManager.entitiesToDestroy = []; } } }, WaveSpawnerSystem: { currentWaveIndex: -1, isSpawning: false, spawnTimer: 0, subWaveIndex: 0, spawnedInSubWave: 0, startNextWave: function startNextWave() { this.currentWaveIndex++; if (this.currentWaveIndex >= Game.WaveData.length) { return; // No more waves } this.isSpawning = true; this.spawnTimer = 0; this.subWaveIndex = 0; this.spawnedInSubWave = 0; // Calculate the total number of enemies for this wave var totalEnemies = 0; var currentWave = Game.WaveData[this.currentWaveIndex]; for (var i = 0; i < currentWave.sub_waves.length; i++) { totalEnemies += currentWave.sub_waves[i].count; } Game.GameplayManager.totalEnemiesThisWave = totalEnemies; }, update: function update() { if (!this.isSpawning || this.currentWaveIndex >= Game.WaveData.length) { return; } var currentWave = Game.WaveData[this.currentWaveIndex]; if (!currentWave || this.subWaveIndex >= currentWave.sub_waves.length) { this.isSpawning = false; return; } var currentSubWave = currentWave.sub_waves[this.subWaveIndex]; if (this.spawnedInSubWave === 0 && this.spawnTimer < currentSubWave.start_delay) { this.spawnTimer += 1 / 60; return; } if (this.spawnedInSubWave > 0 && this.spawnTimer < currentSubWave.spawn_delay) { this.spawnTimer += 1 / 60; return; } if (this.spawnedInSubWave < currentSubWave.count) { var enemyData = Game.EnemyData[currentSubWave.enemy_type]; if (!enemyData) { this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; return; } var enemyId = Game.EntityManager.createEntity(); var isFlyer = enemyData.components && enemyData.components.some(function (c) { return c.name === 'FlyingComponent'; }); var renderLayer = isFlyer ? 3 : 1; var transform = Game.Components.TransformComponent(0 * 64, 30 * 64); var render = Game.Components.RenderComponent('enemy', renderLayer, 1.0, true); var movement = Game.Components.MovementComponent(enemyData.speed); var health = Game.Components.HealthComponent(enemyData.health, enemyData.health); var enemy = Game.Components.EnemyComponent(enemyData.gold_value, enemyData.lives_cost, enemyData.armor_type); Game.EntityManager.addComponent(enemyId, transform); Game.EntityManager.addComponent(enemyId, render); Game.EntityManager.addComponent(enemyId, movement); Game.EntityManager.addComponent(enemyId, health); Game.EntityManager.addComponent(enemyId, enemy); if (enemyData.components && enemyData.components.length > 0) { for (var i = 0; i < enemyData.components.length; i++) { var compData = enemyData.components[i]; var newComponent = Game.Components[compData.name].apply(null, compData.args || []); Game.EntityManager.addComponent(enemyId, newComponent); } } this.spawnedInSubWave++; this.spawnTimer = 0; Game.GameplayManager.enemiesSpawnedThisWave++; } else { this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; } } }, CombatSystem: { isPowerDeficit: false, init: function init() { Game.EventBus.subscribe('PowerStatusChanged', this.onPowerStatusChanged.bind(this)); }, onPowerStatusChanged: function onPowerStatusChanged(event) { this.isPowerDeficit = event.new_status === 'deficit'; }, update: function update() { // If there is a power deficit, do not allow towers to fire. if (this.isPowerDeficit) { return; // Stop the system's update. } var currentTime = LK.ticks / 60; var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); for (var i = 0; i < towers.length; i++) { var towerId = towers[i]; var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towerId]; var towerTransform = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (towerAttack.target_id !== -1 && currentTime - towerAttack.last_attack_time >= 1.0 / towerAttack.attack_speed) { var targetTransform = Game.EntityManager.componentStores['TransformComponent'][towerAttack.target_id]; if (targetTransform) { var baseDamage = towerAttack.damage; var finalDamage = baseDamage; var buffComp = Game.EntityManager.componentStores['DamageBuffComponent'] ? Game.EntityManager.componentStores['DamageBuffComponent'][towerId] : null; if (buffComp) { finalDamage = baseDamage * (1 + buffComp.multiplier); } var projectileId = Game.EntityManager.createEntity(); var projectileTransform = Game.Components.TransformComponent(towerTransform.x, towerTransform.y); var projectileRender = Game.Components.RenderComponent(towerAttack.projectile_id, 1, 1.0, true); var projectileComponent = Game.Components.ProjectileComponent(towerAttack.target_id, undefined, finalDamage); projectileComponent.source_tower_id = towerId; Game.EntityManager.addComponent(projectileId, projectileTransform); Game.EntityManager.addComponent(projectileId, projectileRender); Game.EntityManager.addComponent(projectileId, projectileComponent); if (towerAttack.on_impact_effects && towerAttack.on_impact_effects.length > 0) { for (var j = 0; j < towerAttack.on_impact_effects.length; j++) { var effectData = towerAttack.on_impact_effects[j]; var newComponent = Game.Components[effectData.name].apply(null, effectData.args); Game.EntityManager.addComponent(projectileId, newComponent); } } towerAttack.last_attack_time = currentTime; } } } var projectiles = Game.EntityManager.getEntitiesWithComponents(['ProjectileComponent', 'TransformComponent']); for (var i = projectiles.length - 1; i >= 0; i--) { var projectileId = projectiles[i]; var projectileComp = Game.EntityManager.componentStores['ProjectileComponent'][projectileId]; var projectileTransform = Game.EntityManager.componentStores['TransformComponent'][projectileId]; var targetExists = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id]; if (!targetExists) { Game.EntityManager.destroyEntity(projectileId); continue; } var targetTransform = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id]; var dx = targetTransform.x - projectileTransform.x; var dy = targetTransform.y - projectileTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 10) { var impactPoint = { x: targetTransform.x, y: targetTransform.y }; var sourceTowerAttack = Game.EntityManager.componentStores['AttackComponent'][projectileComp.source_tower_id]; if (sourceTowerAttack && sourceTowerAttack.splash_radius > 0) { var allEnemies = Game.EntityManager.getEntitiesWithComponents(['HealthComponent', 'TransformComponent', 'EnemyComponent']); for (var j = 0; j < allEnemies.length; j++) { var enemyId = allEnemies[j]; var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; var splash_dx = enemyTransform.x - impactPoint.x; var splash_dy = enemyTransform.y - impactPoint.y; var splash_dist = Math.sqrt(splash_dx * splash_dx + splash_dy * splash_dy); if (splash_dist <= sourceTowerAttack.splash_radius) { this.dealDamage(enemyId, projectileComp.damage); } } } else { this.dealDamage(projectileComp.target_id, projectileComp.damage); } var targetHealth = Game.EntityManager.componentStores['HealthComponent'][projectileComp.target_id]; if (targetHealth) { var poisonImpactComp = Game.EntityManager.componentStores['PoisonOnImpactComponent'] ? Game.EntityManager.componentStores['PoisonOnImpactComponent'][projectileId] : null; if (poisonImpactComp) { var debuff = Game.Components.PoisonDebuffComponent(poisonImpactComp.damage_per_second, poisonImpactComp.duration, currentTime); Game.EntityManager.addComponent(projectileComp.target_id, debuff); } var slowImpactComp = Game.EntityManager.componentStores['SlowOnImpactComponent'] ? Game.EntityManager.componentStores['SlowOnImpactComponent'][projectileId] : null; if (slowImpactComp) { var slowDebuff = Game.Components.SlowDebuffComponent(slowImpactComp.slow_percentage, slowImpactComp.duration); Game.EntityManager.addComponent(projectileComp.target_id, slowDebuff); } } var fireZoneImpactComp = Game.EntityManager.componentStores['FireZoneOnImpactComponent'] ? Game.EntityManager.componentStores['FireZoneOnImpactComponent'][projectileId] : null; if (fireZoneImpactComp) { var zoneId = Game.EntityManager.createEntity(); Game.EntityManager.addComponent(zoneId, Game.Components.TransformComponent(impactPoint.x, impactPoint.y)); Game.EntityManager.addComponent(zoneId, Game.Components.RenderComponent('fire_zone', 0, 1.0, true)); Game.EntityManager.addComponent(zoneId, Game.Components.FireZoneComponent(fireZoneImpactComp.damage_per_tick, fireZoneImpactComp.duration, fireZoneImpactComp.tick_rate, currentTime)); } Game.EntityManager.destroyEntity(projectileId); } else { var moveX = dx / distance * projectileComp.speed; var moveY = dy / distance * projectileComp.speed; projectileTransform.x += moveX; projectileTransform.y += moveY; } } var poisonedEnemies = Game.EntityManager.getEntitiesWithComponents(['PoisonDebuffComponent']); for (var i = poisonedEnemies.length - 1; i >= 0; i--) { var enemyId = poisonedEnemies[i]; var debuff = Game.EntityManager.componentStores['PoisonDebuffComponent'][enemyId]; if (!debuff) { continue; } if (currentTime - debuff.last_tick_time >= 1.0) { this.dealDamage(enemyId, debuff.damage_per_second); debuff.last_tick_time = currentTime; debuff.duration_remaining -= 1.0; } if (debuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['PoisonDebuffComponent'][enemyId]; } } var fireZones = Game.EntityManager.getEntitiesWithComponents(['FireZoneComponent', 'TransformComponent']); for (var i = fireZones.length - 1; i >= 0; i--) { var zoneId = fireZones[i]; var zoneComp = Game.EntityManager.componentStores['FireZoneComponent'][zoneId]; var zoneTransform = Game.EntityManager.componentStores['TransformComponent'][zoneId]; zoneComp.duration_remaining -= 1.0 / 60; if (zoneComp.duration_remaining <= 0) { Game.EntityManager.destroyEntity(zoneId); continue; } if (currentTime - zoneComp.last_tick_time >= zoneComp.tick_rate) { var allEnemies = Game.EntityManager.getEntitiesWithComponents(['HealthComponent', 'TransformComponent']); var zoneRadius = 40; for (var j = 0; j < allEnemies.length; j++) { var enemyId = allEnemies[j]; var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; var dx = enemyTransform.x - zoneTransform.x; var dy = enemyTransform.y - zoneTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= zoneRadius) { this.dealDamage(enemyId, zoneComp.damage_per_tick); } } zoneComp.last_tick_time = currentTime; } } }, dealDamage: function dealDamage(enemyId, damage) { var targetHealth = Game.EntityManager.componentStores['HealthComponent'][enemyId]; if (!targetHealth) { return; } var finalDamage = damage; var enemyComp = Game.EntityManager.componentStores['EnemyComponent'][enemyId]; if (enemyComp && enemyComp.armor_type === 'Heavy') { finalDamage *= 0.5; } targetHealth.current_hp -= finalDamage; if (targetHealth.current_hp <= 0) { var goldValue = enemyComp ? enemyComp.gold_value : 0; Game.EventBus.publish(Game.Events.EnemyKilledEvent(enemyId, goldValue)); Game.EntityManager.destroyEntity(enemyId); } } }, AuraSystem: { update: function update() { var buffedTowers = Game.EntityManager.getEntitiesWithComponents(['DamageBuffComponent']); for (var i = 0; i < buffedTowers.length; i++) { var towerId = buffedTowers[i]; delete Game.EntityManager.componentStores['DamageBuffComponent'][towerId]; } var auraTowers = Game.EntityManager.getEntitiesWithComponents(['AuraComponent', 'TransformComponent']); if (auraTowers.length === 0) { return; } var attackTowers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); for (var i = 0; i < auraTowers.length; i++) { var auraTowerId = auraTowers[i]; var auraComponent = Game.EntityManager.componentStores['AuraComponent'][auraTowerId]; var auraTransform = Game.EntityManager.componentStores['TransformComponent'][auraTowerId]; if (auraComponent.effect_type !== 'damage_buff') { continue; } for (var j = 0; j < attackTowers.length; j++) { var attackTowerId = attackTowers[j]; if (attackTowerId === auraTowerId) { continue; } var attackTransform = Game.EntityManager.componentStores['TransformComponent'][attackTowerId]; var dx = auraTransform.x - attackTransform.x; var dy = auraTransform.y - attackTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= auraComponent.range) { var buff = Game.Components.DamageBuffComponent(0.2, auraTowerId); Game.EntityManager.addComponent(attackTowerId, buff); } } } } }, EconomySystem: { update: function update() { var goldGenerators = Game.EntityManager.getEntitiesWithComponents(['GoldGenerationComponent']); for (var i = 0; i < goldGenerators.length; i++) { var entityId = goldGenerators[i]; var goldGenComp = Game.EntityManager.componentStores['GoldGenerationComponent'][entityId]; if (goldGenComp) { Game.ResourceManager.addGold(goldGenComp.gold_per_second); } } } }, TowerSystem: { tryUpgradeTower: function tryUpgradeTower(towerId, path) { var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId]; if (!renderComp) { return; } var towerSpriteId = renderComp.sprite_id; var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === towerSpriteId) { towerType = type; break; } } if (!towerType) { return; } var upgradePathData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType][path] : null; if (!upgradePathData) { return; } var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; if (!towerComp) { return; } var nextUpgradeIndex = path === 'PathA' ? towerComp.upgrade_path_A_level : towerComp.upgrade_path_B_level; if (nextUpgradeIndex >= upgradePathData.length) { return; } var upgradeInfo = upgradePathData[nextUpgradeIndex]; if (!Game.ResourceManager.spendGold(upgradeInfo.cost)) { return; } for (var componentName in upgradeInfo.effects) { var componentToModify = Game.EntityManager.componentStores[componentName] ? Game.EntityManager.componentStores[componentName][towerId] : null; if (componentToModify) { var effectsToApply = upgradeInfo.effects[componentName]; for (var property in effectsToApply) { if (componentToModify.hasOwnProperty(property)) { componentToModify[property] += effectsToApply[property]; } } } } if (upgradeInfo.add_component_on_impact) { var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; if (attackComp) { if (!attackComp.on_impact_effects) { attackComp.on_impact_effects = []; } attackComp.on_impact_effects.push(upgradeInfo.add_component_on_impact); } } towerComp.upgrade_level++; if (path === 'PathA') { towerComp.upgrade_path_A_level++; } else { towerComp.upgrade_path_B_level++; } }, trySellTower: function trySellTower(towerId) { var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!towerComp || !transformComp) { return; } var refundAmount = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { refundAmount = towerComp.cost; } Game.ResourceManager.addGold(refundAmount); // Clear the tower's 3x3 footprint from the grid var gridX = Math.floor(transformComp.x / 64); var gridY = Math.floor(transformComp.y / 64); for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var x = gridX + dx; var y = gridY + dy; if (Game.GridManager.grid[x] && Game.GridManager.grid[x][y] !== undefined) { Game.GridManager.grid[x][y].state = Game.GridManager.CELL_STATES.EMPTY; Game.GridManager.grid[x][y].entityId = null; } } } Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount)); Game.GameplayManager.selectedTowerId = -1; Game.EntityManager.destroyEntity(towerId); } } }; // === SECTION: GAME DATA === Game.EnemyData = { 'Grunt': { health: 100, speed: 50, gold_value: 5, lives_cost: 1, armor_type: 'Normal', components: [] }, 'Swarmer': { health: 30, speed: 80, gold_value: 2, lives_cost: 1, armor_type: 'Light', components: [] }, 'Armored': { health: 400, speed: 30, gold_value: 15, lives_cost: 2, armor_type: 'Heavy', components: [] }, 'Flyer': { health: 80, speed: 60, gold_value: 8, lives_cost: 1, armor_type: 'Light', components: [{ name: 'FlyingComponent', args: [] }] } }; Game.WaveData = [ // Wave 1: A few basic Grunts { wave_number: 1, sub_waves: [{ enemy_type: 'Grunt', count: 10, spawn_delay: 1.5, start_delay: 0 }] }, // Wave 2: A larger group of Grunts { wave_number: 2, sub_waves: [{ enemy_type: 'Grunt', count: 15, spawn_delay: 1.2, start_delay: 0 }] }, // Wave 3: A wave of fast Swarmers { wave_number: 3, sub_waves: [{ enemy_type: 'Swarmer', count: 20, spawn_delay: 0.5, start_delay: 0 }] }, // Wave 4: Grunts supported by slow, tough Armored units { wave_number: 4, sub_waves: [{ enemy_type: 'Grunt', count: 10, spawn_delay: 1.5, start_delay: 0 }, { enemy_type: 'Armored', count: 2, spawn_delay: 3.0, start_delay: 5.0 }] }, // Wave 5: A squadron of Flyers that ignore the path { wave_number: 5, sub_waves: [{ enemy_type: 'Flyer', count: 8, spawn_delay: 1.0, start_delay: 0 }] }]; // === SECTION: TOWER DATA === Game.TowerData = { 'ArrowTower': { cost: 50, sprite_id: 'arrow_tower', color: 0x2ecc71, components: [{ name: 'TowerComponent', args: [50, 25, undefined] }, { name: 'AttackComponent', args: [10, 150, 1.0, 0, 'arrow_projectile'] }, { name: 'PowerConsumptionComponent', args: [2] }] }, 'GeneratorTower': { cost: 75, sprite_id: 'generator_tower', color: 0x3498db, components: [{ name: 'TowerComponent', args: [75, 37, undefined] }, { name: 'PowerProductionComponent', args: [5] }] }, 'CapacitorTower': { cost: 40, sprite_id: 'capacitor_tower', color: 0xf1c40f, components: [{ name: 'TowerComponent', args: [40, 20, undefined] }, { name: 'PowerStorageComponent', args: [20] }] }, 'RockLauncher': { cost: 125, sprite_id: 'rock_launcher_tower', color: 0x8d6e63, components: [{ name: 'TowerComponent', args: [125, 62, undefined] }, { name: 'AttackComponent', args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50] }, { name: 'PowerConsumptionComponent', args: [10] }] }, 'ChemicalTower': { cost: 100, sprite_id: 'chemical_tower', color: 0xb87333, components: [{ name: 'TowerComponent', args: [100, 50, undefined] }, { name: 'AttackComponent', args: [5, 150, 0.8, 0, 'chemical_projectile', -1, 0] }, { name: 'PowerConsumptionComponent', args: [8] }] }, 'SlowTower': { cost: 80, sprite_id: 'slow_tower', color: 0x5dade2, components: [{ name: 'TowerComponent', args: [80, 40, undefined] }, { name: 'AttackComponent', args: [2, 160, 1.0, 0, 'slow_projectile'] }, { name: 'PowerConsumptionComponent', args: [4] }] }, 'DarkTower': { cost: 150, sprite_id: 'dark_tower', color: 0x9b59b6, components: [{ name: 'TowerComponent', args: [150, 75, undefined] }, { name: 'AuraComponent', args: [175, 'damage_buff'] }, { name: 'PowerConsumptionComponent', args: [12] }] }, 'GraveyardTower': { cost: 200, sprite_id: 'graveyard_tower', color: 0x607d8b, components: [{ name: 'TowerComponent', args: [200, 100, undefined] }, { name: 'GoldGenerationComponent', args: [1.5] }, { name: 'PowerConsumptionComponent', args: [15] }] } }; // === SECTION: UPGRADE DATA === Game.UpgradeData = { 'ArrowTower': { 'PathA': [{ cost: 60, effects: { AttackComponent: { damage: 5 } } }, //{7R} // Level 1: +5 Dmg { cost: 110, effects: { AttackComponent: { damage: 10 } } } //{7W} // Level 2: +10 Dmg ], 'PathB': [{ cost: 75, effects: { AttackComponent: { attack_speed: 0.2 } } }, //{83} // Level 1: +0.2 AS { cost: 125, effects: { AttackComponent: { attack_speed: 0.3 } } } //{8a} // Level 2: +0.3 AS ] }, 'RockLauncher': { 'PathA': [ // Heavy Boulders - More Damage & Range { cost: 150, effects: { AttackComponent: { damage: 15, range: 20 } } }, { cost: 250, effects: { AttackComponent: { damage: 25, range: 30 } } }, // --- UPDATED UPGRADE --- { cost: 500, effects: { AttackComponent: { damage: 50, range: 50 } }, add_component_on_impact: { name: 'FireZoneOnImpactComponent', args: [10, 3, 0.5] } // dmg, duration, tick_rate }], 'PathB': [ // Lighter Munitions - Faster Attack Speed { cost: 140, effects: { AttackComponent: { damage: 5, attack_speed: 0.2 } } }, { cost: 220, effects: { AttackComponent: { damage: 10, attack_speed: 0.3 } } }, { cost: 450, effects: { AttackComponent: { damage: 15, attack_speed: 0.5 } } }] } }; // === SECTION: GRID DRAWING === // Create a container for all game objects that will be panned var gameWorld = new Container(); game.addChild(gameWorld); function drawGrid() { // Draw the grid cells for (var x = 0; x < Game.GridManager.grid.length; x++) { for (var y = 0; y < Game.GridManager.grid[x].length; y++) { var cellSprite = gameWorld.attachAsset('grid_cell', {}); cellSprite.x = x * 64; cellSprite.y = y * 64; cellSprite.anchor.set(0, 0); // Anchor to top-left cellSprite.gridX = x; // Store grid coordinates on the sprite cellSprite.gridY = y; cellSprite.down = function () { // Only process true clicks, not drags var timeSinceMouseDown = LK.ticks / 60 * 1000 - mouseDownTime; if (timeSinceMouseDown > 200) { return; } var dx = Math.abs(lastMouseX - dragStartX); var dy = Math.abs(lastMouseY - dragStartY); if (dx > 10 || dy > 10) { return; } var gridX = this.gridX; var gridY = this.gridY; var gridCell = Game.GridManager.grid[gridX][gridY]; if (Game.GameplayManager.activeBuildType) { // If in build mode, check if the 3x3 area is buildable if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; // The center of the build is the clicked cell Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; var worldX = gridX * 64 + 32; var worldY = gridY * 64 + 32; confirmPanel.x = worldX; confirmPanel.y = worldY; } else { // Clicked an invalid spot, so cancel build mode Game.GameplayManager.activeBuildType = null; } } else if (!Game.GameplayManager.isConfirmingPlacement) { // If not in build mode, check for selection if (gridCell.state === Game.GridManager.CELL_STATES.TOWER) { // The grid cell now directly tells us which tower is selected Game.GameplayManager.selectedTowerId = gridCell.entityId; } else { Game.GameplayManager.selectedTowerId = -1; } } }; } } // Highlight the path cells if (Game.PathManager.currentPath) { for (var i = 0; i < Game.PathManager.currentPath.length; i++) { var pathNode = Game.PathManager.currentPath[i]; var pathSprite = gameWorld.attachAsset('path_cell', {}); // Note: PathManager coordinates are already in pixels pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } } // === SECTION: GLOBAL EVENT BUS === Game.EventBus = { listeners: {}, subscribe: function subscribe(eventType, listener) { if (!this.listeners[eventType]) { this.listeners[eventType] = []; } this.listeners[eventType].push(listener); }, unsubscribe: function unsubscribe(eventType, listener) { if (this.listeners[eventType]) { var index = this.listeners[eventType].indexOf(listener); if (index !== -1) { this.listeners[eventType].splice(index, 1); } } }, publish: function publish(event) { if (this.listeners[event.type]) { for (var i = 0; i < this.listeners[event.type].length; i++) { this.listeners[event.type][i](event); } } } }; Game.Events = { EnemyReachedEndEvent: function EnemyReachedEndEvent(enemy_id) { return { type: 'EnemyReachedEnd', enemy_id: enemy_id }; }, EnemyKilledEvent: function EnemyKilledEvent(enemy_id, gold_value) { return { type: 'EnemyKilled', enemy_id: enemy_id, gold_value: gold_value }; }, TowerPlacedEvent: function TowerPlacedEvent(tower_id, cost, position_x, position_y) { return { type: 'TowerPlaced', tower_id: tower_id, cost: cost, position_x: position_x, position_y: position_y }; }, TowerSoldEvent: function TowerSoldEvent(tower_id, refund_value) { return { type: 'TowerSold', tower_id: tower_id, refund_value: refund_value }; }, PowerStatusChangedEvent: function PowerStatusChangedEvent(new_status) { return { type: 'PowerStatusChanged', new_status: new_status }; }, WaveCompletedEvent: function WaveCompletedEvent(wave_number) { return { type: 'WaveCompleted', wave_number: wave_number }; }, GameOverEvent: function GameOverEvent() { return { type: 'GameOver' }; } }; // === SECTION: STACK-BASED GAME STATE MANAGER === // === SECTION: DIRECT GAME INITIALIZATION === // Initialize managers Game.GridManager.init(40, 60); Game.GridManager.grid[0][30] = Game.GridManager.CELL_STATES.PATH; // Set spawn point as unbuildable Game.ResourceManager.init(); Game.PathManager.init(40, 60); drawGrid(); // Create the Heads-Up Display (HUD) var goldText = new Text2('Gold: 100', { size: 40, fill: 0xFFD700 }); goldText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(goldText); goldText.x = -20; // 20px padding from the right edge goldText.y = 20; // 20px padding from the top edge var livesText = new Text2('Lives: 20', { size: 40, fill: 0xFF4136 }); livesText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(livesText); livesText.x = -20; // 20px padding from the right edge livesText.y = 70; // Position below the gold text var waveText = new Text2('Wave: 1 / 5', { size: 40, fill: 0xFFFFFF }); waveText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(waveText); waveText.x = -20; // 20px padding from the right edge waveText.y = 120; // Position below the lives text // Create Power Meter UI var powerMeterContainer = new Container(); LK.gui.topRight.addChild(powerMeterContainer); powerMeterContainer.x = -320; // Positioned in from the right edge powerMeterContainer.y = 170; // Position below wave text // Create a Graphics instance for power production bar var Graphics = function Graphics() { var graphics = new Container(); graphics.currentFill = 0xffffff; graphics.currentAlpha = 1.0; graphics.beginFill = function (color, alpha) { this.currentFill = color; this.currentAlpha = alpha !== undefined ? alpha : 1.0; }; graphics.drawRect = function (x, y, width, height) { var rect = game.attachAsset('grid_cell', {}); rect.tint = this.currentFill; rect.alpha = this.currentAlpha; rect.x = x; rect.y = y; rect.width = width; rect.height = height; this.addChild(rect); }; graphics.endFill = function () { // No-op for compatibility }; graphics.clear = function () { // Remove all children to clear the graphics while (this.children.length > 0) { var child = this.children[0]; this.removeChild(child); if (child.destroy) { child.destroy(); } } }; return graphics; }; // Background bar representing total production capacity var powerProductionBar = new Graphics(); powerProductionBar.beginFill(0x3498db, 0.5); // Semi-transparent blue powerProductionBar.drawRect(0, 0, 300, 20); // Placeholder width powerProductionBar.endFill(); powerMeterContainer.addChild(powerProductionBar); // Foreground bar representing current consumption var powerConsumptionBar = new Graphics(); powerConsumptionBar.beginFill(0x2ecc71); // Green for surplus powerConsumptionBar.drawRect(0, 0, 10, 20); // Placeholder width powerConsumptionBar.endFill(); powerMeterContainer.addChild(powerConsumptionBar); // Secondary bar for the available power buffer (capacitors) var powerAvailableBar = new Graphics(); powerAvailableBar.beginFill(0x3498db, 0.3); // Fainter blue powerAvailableBar.drawRect(0, 0, 5, 20); // Placeholder width, starts at the end of the production bar powerAvailableBar.endFill(); powerMeterContainer.addChild(powerAvailableBar); // Create Start Wave button var startWaveButton = new Text2('START NEXT WAVE', { size: 70, fill: 0x00FF00 }); startWaveButton.anchor.set(0.5, 1); LK.gui.bottom.addChild(startWaveButton); startWaveButton.y = -20; startWaveButton.visible = true; startWaveButton.down = function () { // Deselect any active tower before starting the wave Game.GameplayManager.selectedTowerId = -1; Game.Systems.WaveSpawnerSystem.startNextWave(); startWaveButton.visible = false; }; // Initialize GameplayManager Game.GameplayManager.init(); // Initialize GameManager Game.GameManager.init(); Game.Systems.CombatSystem.init(); // === SECTION: TEMP BUILD UI === // === SECTION: BUILD AND CONFIRMATION UI === // -- Ghost Tower (for placement preview) -- var ghostTowerSprite = null; // -- Build Panel (at the bottom of the screen) -- var buildPanel = new Container(); LK.gui.bottom.addChild(buildPanel); buildPanel.x = 0; buildPanel.y = -150; // Positioned further up to not overlap the "Start Wave" button // -- Floating Info Panel (now a fixed panel above the build bar) -- var floatingInfoPanel = new Container(); LK.gui.bottom.addChild(floatingInfoPanel); // Attach to the bottom GUI anchor // Container objects don't have anchor property, so position it manually to center floatingInfoPanel.x = -150; // Offset by half the panel width (300/2) to center floatingInfoPanel.y = -320; // Position it 320px up from the bottom edge floatingInfoPanel.visible = false; var infoBackground = new Graphics(); infoBackground.beginFill(0x000000, 0.8); infoBackground.drawRect(0, 0, 300, 80); infoBackground.endFill(); floatingInfoPanel.addChild(infoBackground); var infoText = new Text2('', { size: 30, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 290 }); infoText.anchor.set(0, 0); infoText.x = 5; infoText.y = 5; floatingInfoPanel.addChild(infoText); // -- Confirmation Panel (appears on the game map) -- var confirmPanel = new Container(); gameWorld.addChild(confirmPanel); // Added to gameWorld so it pans with the camera confirmPanel.visible = false; // Buttons will be created and destroyed dynamically. var confirmButton = null; var cancelButton = null; // -- Build Button Creation Logic (for Drag-and-Drop) -- var buildButtonData = [{ type: 'ArrowTower', sprite: 'arrow_tower' }, { type: 'GeneratorTower', sprite: 'generator_tower' }, { type: 'CapacitorTower', sprite: 'capacitor_tower' }, { type: 'RockLauncher', sprite: 'rock_launcher_tower' }, { type: 'ChemicalTower', sprite: 'chemical_tower' }, { type: 'SlowTower', sprite: 'slow_tower' }, { type: 'DarkTower', sprite: 'dark_tower' }, { type: 'GraveyardTower', sprite: 'graveyard_tower' }]; var buttonSize = 90; var buttonPadding = 15; var totalWidth = buildButtonData.length * buttonSize + (buildButtonData.length - 1) * buttonPadding; var startX = -totalWidth / 2; for (var i = 0; i < buildButtonData.length; i++) { var data = buildButtonData[i]; var button = game.attachAsset(data.sprite, {}); button.width = buttonSize; button.height = buttonSize; button.anchor.set(0.5, 0.5); button.x = startX + i * (buttonSize + buttonPadding) + buttonSize / 2; button.y = 0; button.towerType = data.type; // This is the start of the drag-and-drop button.down = function () { // Set the active build type to start the drag process Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; // Deselect any existing tower }; // We no longer need a 'move' handler here, as the main game loop will handle the drag. buildPanel.addChild(button); } // We no longer need the 'buildPanel.out' handler either. buildPanel.out = null; // Confirmation button logic is now handled dynamically in the game update loop // === SECTION: MINIMAP UI === var minimapContainer = new Container(); LK.gui.bottomLeft.addChild(minimapContainer); minimapContainer.x = 20; // 20px padding from the left edge minimapContainer.y = -320; // Position up from bottom edge (negative value moves up in bottomLeft anchor) var minimapBackground = new Graphics(); minimapContainer.addChild(minimapBackground); // Function to draw the static parts of the minimap function drawMinimapBackground() { var mapWidth = 40; var mapHeight = 60; var tilePixelSize = 5; // Each grid cell is a 5x5 pixel block on the minimap var colors = { EMPTY: 0x3d2b1f, // Dark Brown for buildable land PATH: 0x8B4513, // SaddleBrown for the path TOWER: 0xFFFFFF, // White (will be used for dynamic tower dots later) BLOCKED: 0x111111 // Almost black for blocked areas }; minimapBackground.clear(); minimapBackground.beginFill(0x000000, 0.5); // Semi-transparent black background for the minimap minimapBackground.drawRect(0, 0, mapWidth * tilePixelSize, mapHeight * tilePixelSize); minimapBackground.endFill(); for (var x = 0; x < mapWidth; x++) { for (var y = 0; y < mapHeight; y++) { var cellState = Game.GridManager.grid[x][y]; var color; switch (cellState) { case Game.GridManager.CELL_STATES.PATH: color = colors.PATH; break; case Game.GridManager.CELL_STATES.BLOCKED: color = colors.BLOCKED; break; default: color = colors.EMPTY; break; } minimapBackground.beginFill(color); minimapBackground.drawRect(x * tilePixelSize, y * tilePixelSize, tilePixelSize, tilePixelSize); minimapBackground.endFill(); } } } // Call the function once to draw the initial state drawMinimapBackground(); var minimapDynamicLayer = new Graphics(); minimapContainer.addChild(minimapDynamicLayer); var minimapViewportRect = new Graphics(); minimapContainer.addChild(minimapViewportRect); minimapContainer.down = function (x, y, obj) { // x and y are the click coordinates relative to the minimap's top-left corner. // 1. Define map and minimap dimensions for conversion. var mapWorldWidth = 40 * 64; // 2560 var mapWorldHeight = 60 * 64; // 3840 var minimapPixelWidth = 40 * 5; // 200 var minimapPixelHeight = 60 * 5; // 300 // 2. Convert minimap click coordinates to world coordinates. // This gives us the point in the world the player wants to look at. var targetWorldX = x / minimapPixelWidth * mapWorldWidth; var targetWorldY = y / minimapPixelHeight * mapWorldHeight; // 3. Calculate the new camera position to CENTER the view on the target. var screenWidth = 2048; var screenHeight = 2732; var newCameraX = -targetWorldX + screenWidth / 2; var newCameraY = -targetWorldY + screenHeight / 2; // 4. Clamp the new camera position to the map boundaries. // This reuses the same clamping logic from the drag-to-pan feature. var minX = screenWidth - mapWorldWidth; var minY = screenHeight - mapWorldHeight; var maxX = 0; var maxY = 0; cameraX = Math.max(minX, Math.min(maxX, newCameraX)); cameraY = Math.max(minY, Math.min(maxY, newCameraY)); // 5. Apply the final, clamped transform to the game world. gameWorld.x = cameraX; gameWorld.y = cameraY; }; function updateMinimapDynamic() { minimapDynamicLayer.clear(); var tilePixelSize = 5; // Draw Towers var towers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); for (var i = 0; i < towers.length; i++) { var towerId = towers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var gridX = Math.floor(transform.x / 64); var gridY = Math.floor(transform.y / 64); minimapDynamicLayer.beginFill(0xFFFFFF); // White dot for towers minimapDynamicLayer.drawRect(gridX * tilePixelSize, gridY * tilePixelSize, tilePixelSize, tilePixelSize); minimapDynamicLayer.endFill(); } // Draw Enemies var enemies = Game.EntityManager.getEntitiesWithComponents(['EnemyComponent', 'TransformComponent']); for (var i = 0; i < enemies.length; i++) { var enemyId = enemies[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; // Use the enemy's precise world coordinates for smoother tracking on the minimap var minimapX = transform.x / 64 * tilePixelSize; var minimapY = transform.y / 64 * tilePixelSize; minimapDynamicLayer.beginFill(0xFF0000); // Red dot for enemies minimapDynamicLayer.drawRect(minimapX, minimapY, 3, 3); // Make them slightly smaller than a tile minimapDynamicLayer.endFill(); } } function updateMinimapViewport() { minimapViewportRect.clear(); var tilePixelSize = 5; var worldTileSize = 64; // The cameraX/Y variables are inverse offsets. We need the true top-left coordinate. var trueCameraX = -cameraX; var trueCameraY = -cameraY; // Get screen/viewport dimensions var screenWidth = 2048; // Use the game's actual screen dimensions var screenHeight = 2732; // Convert world coordinates and dimensions to minimap scale var rectX = trueCameraX / worldTileSize * tilePixelSize; var rectY = trueCameraY / worldTileSize * tilePixelSize; var rectWidth = screenWidth / worldTileSize * tilePixelSize; var rectHeight = screenHeight / worldTileSize * tilePixelSize; minimapViewportRect.beginFill(0xFFFFFF, 0.25); // Semi-transparent white minimapViewportRect.drawRect(rectX, rectY, rectWidth, rectHeight); minimapViewportRect.endFill(); } // Add mouse event handlers for camera panning game.down = function (x, y, obj) { isDragging = true; dragStartX = x; dragStartY = y; lastMouseX = x; lastMouseY = y; cameraStartX = cameraX; cameraStartY = cameraY; mouseDownTime = LK.ticks / 60 * 1000; // Convert to milliseconds }; game.move = function (x, y, obj) { lastMouseX = x; lastMouseY = y; if (isDragging) { var dx = x - dragStartX; var dy = y - dragStartY; // Update camera position (inverted for drag effect) cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; // 40 tiles * 64 pixels per tile var mapHeight = 60 * 64; // 60 tiles * 64 pixels per tile var screenWidth = 2048; var screenHeight = 2732; // Ensure we never see beyond the map edges var minX = screenWidth - mapWidth; var minY = screenHeight - mapHeight; var maxX = 0; var maxY = 0; cameraX = Math.max(minX, Math.min(maxX, cameraX)); cameraY = Math.max(minY, Math.min(maxY, cameraY)); // Apply camera transform to game world gameWorld.x = cameraX; gameWorld.y = cameraY; } }; game.up = function (x, y, obj) { isDragging = false; }; // Set up main game loop var powerUpdateTimer = 0; game.update = function () { if (Game.GameManager.isGameOver) { return; } // Run core game systems Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); // --- UI STATE MACHINE FOR DRAG-AND-DROP --- var currentBuildType = Game.GameplayManager.activeBuildType; // Check if the player is currently dragging a tower to build if (currentBuildType && !Game.GameplayManager.isConfirmingPlacement) { // --- STATE: DRAGGING TOWER --- var towerData = Game.TowerData[currentBuildType]; // Ensure UI for other states is hidden confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update the ghost tower sprite that follows the cursor/finger if (!ghostTowerSprite || ghostTowerSprite.towerType !== currentBuildType) { if (ghostTowerSprite) ghostTowerSprite.destroy(); ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = currentBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // Update and show the fixed info panel floatingInfoPanel.visible = true; infoText.setText(currentBuildType + "\nCost: " + towerData.cost); // The panel's position is now fixed and no longer updated here. } else if (Game.GameplayManager.isConfirmingPlacement) { // --- STATE: AWAITING CONFIRMATION --- // Hide other UI elements if (ghostTowerSprite) ghostTowerSprite.visible = false; floatingInfoPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update confirmation buttons if needed if (!confirmButton || confirmButton.towerType !== currentBuildType) { if (confirmButton) confirmButton.destroy(); if (cancelButton) cancelButton.destroy(); var towerData = Game.TowerData[currentBuildType]; // Create CONFIRM button confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 120; // Increased size confirmButton.height = 120; // Increased size confirmButton.x = -65; // Adjusted spacing confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = currentBuildType; confirmButton.down = function () { var coords = Game.GameplayManager.confirmPlacementCoords; var buildType = Game.GameplayManager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); // Create CANCEL button cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 120; // Increased size cancelButton.height = 120; // Increased size cancelButton.x = 65; // Adjusted spacing cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = currentBuildType; cancelButton.down = function () { // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // --- STATE: TOWER SELECTED --- // Hide all build-related UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; floatingInfoPanel.visible = false; // Show upgrade panel and range circle var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'] ? Game.EntityManager.componentStores['RenderComponent'][towerId] : null; var towerComp = Game.EntityManager.componentStores['TowerComponent'] ? Game.EntityManager.componentStores['TowerComponent'][towerId] : null; var attackComp = Game.EntityManager.componentStores['AttackComponent'] ? Game.EntityManager.componentStores['AttackComponent'][towerId] : null; var transformComp = Game.EntityManager.componentStores['TransformComponent'] ? Game.EntityManager.componentStores['TransformComponent'][towerId] : null; if (!renderComp || !towerComp || !transformComp) { Game.GameplayManager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } else { rangeCircle.visible = false; } var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === renderComp.sprite_id) { towerType = type; break; } } towerNameText.setText(towerType + " (Lvl " + towerComp.upgrade_level + ")"); var upgradePathAData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathA'] : null; if (upgradePathAData && towerComp.upgrade_path_A_level < upgradePathAData.length) { var upgradeInfoA = upgradePathAData[towerComp.upgrade_path_A_level]; upgradeAPathButton.setText("Upgrade Path A ($" + upgradeInfoA.cost + ")"); upgradeAPathButton.visible = true; } else { upgradeAPathButton.visible = false; } var upgradePathBData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathB'] : null; if (upgradePathBData && towerComp.upgrade_path_B_level < upgradePathBData.length) { var upgradeInfoB = upgradePathBData[towerComp.upgrade_path_B_level]; upgradeBPathButton.setText("Upgrade Path B ($" + upgradeInfoB.cost + ")"); upgradeBPathButton.visible = true; } else { upgradeBPathButton.visible = false; } var sellPrice = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { sellPrice = towerComp.cost; } sellButton.setText("Sell Tower ($" + sellPrice + ")"); } } else { // --- STATE: IDLE --- // Clean up all dynamic UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } floatingInfoPanel.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; } // The `game.up` event (releasing the mouse/finger) will handle the drop logic. game.up = function () { if (Game.GameplayManager.activeBuildType && !Game.GameplayManager.isConfirmingPlacement) { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); // If dropped on a valid spot, switch to confirmation state if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; confirmPanel.x = gridX * 64 + 32; confirmPanel.y = gridY * 64 + 32; } else { // Dropped on an invalid spot, cancel the build Game.GameplayManager.activeBuildType = null; } } isDragging = false; }; // Update standard HUD elements every frame goldText.setText('Gold: ' + Game.ResourceManager.gold); livesText.setText('Lives: ' + Game.ResourceManager.lives); waveText.setText('Wave: ' + (Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1) + ' / ' + Game.WaveData.length); // Update Power Meter, Minimap, etc. var p_prod = Game.ResourceManager.powerProduction; var p_cons = Game.ResourceManager.powerConsumption; var p_avail = Game.ResourceManager.powerAvailable; var isDeficit = p_cons > p_prod && p_avail <= 0; var barScale = 3; var productionWidth = p_prod * barScale; powerProductionBar.clear(); powerProductionBar.beginFill(0x5DADE2, 0.6); powerProductionBar.drawRect(0, 0, productionWidth, 20); powerProductionBar.endFill(); var consumptionWidth = Math.min(p_cons, p_prod) * barScale; var consumptionColor = 0x00FF7F; if (isDeficit) { consumptionColor = Math.floor(LK.ticks / 10) % 2 === 0 ? 0xFF4136 : 0xD9362D; } else if (p_prod > 0 && p_cons / p_prod >= 0.75) { consumptionColor = 0xFFD700; } powerConsumptionBar.clear(); powerConsumptionBar.beginFill(consumptionColor); powerConsumptionBar.drawRect(0, 0, consumptionWidth, 20); powerConsumptionBar.endFill(); var availableWidth = p_avail * barScale; powerAvailableBar.x = productionWidth; powerAvailableBar.clear(); powerAvailableBar.beginFill(0x5DADE2, 0.4); powerAvailableBar.drawRect(0, 0, availableWidth, 20); powerAvailableBar.endFill(); updateMinimapDynamic(); updateMinimapViewport(); powerUpdateTimer++; if (powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); }; // === SECTION: TOWER RANGE INDICATOR UI === var rangeCircle = gameWorld.attachAsset('range_circle', {}); gameWorld.addChild(rangeCircle); rangeCircle.anchor.set(0.5, 0.5); // Set the origin to the center of the asset rangeCircle.alpha = 0.25; rangeCircle.visible = false; // === SECTION: UPGRADE & SELL UI === var upgradePanel = new Container(); LK.gui.right.addChild(upgradePanel); upgradePanel.x = -50; // 50px padding from the right edge upgradePanel.y = 0; upgradePanel.visible = false; var towerNameText = new Text2('Tower Name', { size: 50, fill: 0xFFFFFF, align: 'right' // Align text to the right }); towerNameText.anchor.set(1, 0.5); // Anchor to the right upgradePanel.addChild(towerNameText); var upgradeAPathButton = new Text2('Upgrade Path A', { size: 45, fill: 0x27AE60, align: 'right' }); upgradeAPathButton.anchor.set(1, 0.5); // Anchor to the right upgradeAPathButton.y = 70; upgradePanel.addChild(upgradeAPathButton); var upgradeBPathButton = new Text2('Upgrade Path B', { size: 45, fill: 0x2980B9, align: 'right' }); upgradeBPathButton.anchor.set(1, 0.5); // Anchor to the right upgradeBPathButton.y = 140; upgradePanel.addChild(upgradeBPathButton); var sellButton = new Text2('Sell Tower', { size: 45, fill: 0xC0392B, align: 'right' }); sellButton.anchor.set(1, 0.5); // Anchor to the right sellButton.y = 210; upgradePanel.addChild(sellButton); sellButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId); } }; upgradeAPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathA'); } }; upgradeBPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathB'); } };
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// === SECTION: GLOBAL NAMESPACE ===
// === SECTION: ASSETS ===
var Game = {};
// Camera state
var cameraX = 0;
var cameraY = 0;
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
var cameraStartX = 0;
var cameraStartY = 0;
var lastMouseX = 0;
var lastMouseY = 0;
var mouseDownTime = 0;
// === SECTION: GLOBAL MANAGERS ===
Game.ResourceManager = {
gold: 500,
lives: 20,
powerProduction: 0,
powerConsumption: 0,
powerAvailable: 0,
addGold: function addGold(amount) {
this.gold += amount;
},
spendGold: function spendGold(amount) {
if (this.gold >= amount) {
this.gold -= amount;
return true;
}
;
return false;
},
loseLife: function loseLife(amount) {
this.lives -= amount;
if (this.lives <= 0) {
Game.EventBus.publish(Game.Events.GameOverEvent());
}
},
updatePower: function updatePower() {
// 1. Calculate total power production
var totalProduction = 0;
var productionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerProductionComponent']);
for (var i = 0; i < productionEntities.length; i++) {
var entityId = productionEntities[i];
var powerComp = Game.EntityManager.componentStores['PowerProductionComponent'][entityId];
totalProduction += powerComp.production_rate;
}
this.powerProduction = totalProduction;
// 2. Calculate total power consumption
var totalConsumption = 0;
var consumptionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerConsumptionComponent']);
for (var i = 0; i < consumptionEntities.length; i++) {
var entityId = consumptionEntities[i];
var powerComp = Game.EntityManager.componentStores['PowerConsumptionComponent'][entityId];
totalConsumption += powerComp.drain_rate;
}
this.powerConsumption = totalConsumption;
// 3. Calculate total storage capacity
var totalCapacity = 0;
var storageEntities = Game.EntityManager.getEntitiesWithComponents(['PowerStorageComponent']);
for (var i = 0; i < storageEntities.length; i++) {
var entityId = storageEntities[i];
var storageComp = Game.EntityManager.componentStores['PowerStorageComponent'][entityId];
totalCapacity += storageComp.capacity;
}
// 4. Evaluate power status and update available buffer
var surplus = totalProduction - totalConsumption;
if (surplus >= 0) {
// We have enough or extra power
this.powerAvailable += surplus;
// Clamp the available power to the max capacity
if (this.powerAvailable > totalCapacity) {
this.powerAvailable = totalCapacity;
}
Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus'));
} else {
// We have a deficit, drain from the buffer
this.powerAvailable += surplus; // surplus is negative, so this subtracts
if (this.powerAvailable < 0) {
// Buffer is empty and we still have a deficit
this.powerAvailable = 0;
Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('deficit'));
} else {
// Deficit was covered by the buffer
Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus'));
}
}
},
onEnemyKilled: function onEnemyKilled(event) {
this.addGold(event.gold_value);
},
onWaveCompleted: function onWaveCompleted(event) {
this.addGold(50);
},
init: function init() {
Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this));
Game.EventBus.subscribe('WaveCompleted', this.onWaveCompleted.bind(this));
}
};
Game.GridManager = {
grid: [],
CELL_STATES: {
EMPTY: 0,
PATH: 1,
BLOCKED: 2,
TOWER: 3
},
init: function init(width, height) {
this.grid = [];
for (var x = 0; x < width; x++) {
this.grid[x] = [];
for (var y = 0; y < height; y++) {
// Each cell is now an object storing state and entity ID
this.grid[x][y] = {
state: this.CELL_STATES.EMPTY,
entityId: null
};
}
}
},
isBuildable3x3: function isBuildable3x3(centerX, centerY) {
// Checks a 3x3 area centered on the given coordinates
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var x = centerX + dx;
var y = centerY + dy;
if (x < 0 || x >= this.grid.length || y < 0 || y >= this.grid[0].length) {
return false; // Out of bounds
}
if (this.grid[x][y].state !== this.CELL_STATES.EMPTY) {
return false; // Cell is not empty
}
}
}
return true;
},
isBuildable: function isBuildable(x, y) {
if (x >= 0 && x < this.grid.length && y >= 0 && y < this.grid[x].length) {
return this.grid[x][y].state === this.CELL_STATES.EMPTY;
}
return false;
}
};
Game.PathManager = {
distanceField: [],
flowField: [],
init: function init(width, height) {
this.distanceField = Array(width).fill(null).map(function () {
return Array(height).fill(Infinity);
});
this.flowField = Array(width).fill(null).map(function () {
return Array(height).fill({
x: 0,
y: 0
});
});
Game.EventBus.subscribe('TowerPlaced', this.generateFlowField.bind(this));
Game.EventBus.subscribe('TowerSold', this.generateFlowField.bind(this));
// Generate the initial field when the game starts
this.generateFlowField();
},
generateFlowField: function generateFlowField() {
console.log("Generating new flow field...");
var grid = Game.GridManager.grid;
var width = grid.length;
var height = grid[0].length;
// 1. Reset fields
this.distanceField = Array(width).fill(null).map(function () {
return Array(height).fill(Infinity);
});
// 2. Integration Pass (BFS)
var goal = {
x: 39,
y: 30
};
var queue = [goal];
this.distanceField[goal.x][goal.y] = 0;
var head = 0;
while (head < queue.length) {
var current = queue[head++];
var neighbors = [{
x: -1,
y: 0
}, {
x: 1,
y: 0
}, {
x: 0,
y: -1
}, {
x: 0,
y: 1
}]; // Orthogonal neighbors
for (var i = 0; i < neighbors.length; i++) {
var n = neighbors[i];
var nx = current.x + n.x;
var ny = current.y + n.y;
// Check bounds and if traversable (checks the .state property)
if (nx >= 0 && nx < width && ny >= 0 && ny < height && grid[nx][ny].state !== Game.GridManager.CELL_STATES.TOWER && this.distanceField[nx][ny] === Infinity) {
this.distanceField[nx][ny] = this.distanceField[current.x][current.y] + 1;
queue.push({
x: nx,
y: ny
});
}
}
}
// 3. Flow Pass
for (var x = 0; x < width; x++) {
for (var y = 0; y < height; y++) {
if (this.distanceField[x][y] === Infinity) {
continue;
}
var bestDist = Infinity;
var bestVector = {
x: 0,
y: 0
};
var allNeighbors = [{
x: -1,
y: 0
}, {
x: 1,
y: 0
}, {
x: 0,
y: -1
}, {
x: 0,
y: 1
}, {
x: -1,
y: -1
}, {
x: 1,
y: -1
}, {
x: -1,
y: 1
}, {
x: 1,
y: 1
}]; // Include diagonals for flow
for (var i = 0; i < allNeighbors.length; i++) {
var n = allNeighbors[i];
var nx = x + n.x;
var ny = y + n.y;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
if (this.distanceField[nx][ny] < bestDist) {
bestDist = this.distanceField[nx][ny];
bestVector = n;
}
}
}
this.flowField[x][y] = bestVector;
}
}
// 4. Announce that the field has been updated
// (We will add the event publication in a later step)
},
getVectorAt: function getVectorAt(worldX, worldY) {
var gridX = Math.floor(worldX / 64);
var gridY = Math.floor(worldY / 64);
if (this.flowField[gridX] && this.flowField[gridX][gridY]) {
return this.flowField[gridX][gridY];
}
return {
x: 0,
y: 0
}; // Default case
}
};
Game.GameplayManager = {
selectedTowerId: -1,
activeBuildType: null,
// The tower type selected from the build panel (e.g., 'ArrowTower')
isConfirmingPlacement: false,
// True when the confirm/cancel dialog is shown
confirmPlacementCoords: {
x: null,
y: null
},
// Stores the grid coords for confirmation
lastClickedButton: null,
// Stores a reference to the UI button that was clicked
enemiesSpawnedThisWave: 0,
enemiesKilledThisWave: 0,
enemiesLeakedThisWave: 0,
totalEnemiesThisWave: 0,
init: function init() {
Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this));
Game.EventBus.subscribe('EnemyReachedEnd', this.onEnemyReachedEnd.bind(this));
},
onEnemyKilled: function onEnemyKilled(event) {
this.enemiesKilledThisWave++;
if (this.enemiesKilledThisWave + this.enemiesLeakedThisWave >= this.totalEnemiesThisWave) {
Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1));
this.enemiesKilledThisWave = 0;
this.enemiesLeakedThisWave = 0;
startWaveButton.visible = true;
}
},
onEnemyReachedEnd: function onEnemyReachedEnd(event) {
this.enemiesLeakedThisWave++;
Game.ResourceManager.loseLife(1);
Game.EntityManager.destroyEntity(event.enemy_id);
if (this.enemiesKilledThisWave + this.enemiesLeakedThisWave >= this.totalEnemiesThisWave) {
Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1));
this.enemiesKilledThisWave = 0;
this.enemiesLeakedThisWave = 0;
startWaveButton.visible = true;
}
}
};
Game.GameManager = {
isGameOver: false,
init: function init() {
Game.EventBus.subscribe('GameOver', this.onGameOver.bind(this));
},
onGameOver: function onGameOver(event) {
this.isGameOver = true;
LK.showGameOver();
}
};
Game.Debug = {
logText: null,
init: function init() {
this.logText = new Text2('Debug Log Initialized', {
size: 40,
fill: 0x00FF00
}); // Green text
this.logText.anchor.set(0, 1); // Anchor to bottom-left
LK.gui.bottomLeft.addChild(this.logText);
this.logText.x = 20;
this.logText.y = -20;
},
log: function log(message) {
if (this.logText) {
this.logText.setText(String(message));
}
}
};
// === SECTION: ENTITY-COMPONENT-SYSTEM SCAFFOLDING ===
Game.EntityManager = {
nextEntityId: 1,
activeEntities: [],
componentStores: {},
entitiesToDestroy: [],
createEntity: function createEntity() {
var entityId = this.nextEntityId++;
this.activeEntities.push(entityId);
return entityId;
},
addComponent: function addComponent(entityId, component) {
if (!this.componentStores[component.name]) {
this.componentStores[component.name] = {};
}
this.componentStores[component.name][entityId] = component;
},
getEntitiesWithComponents: function getEntitiesWithComponents(componentNames) {
var result = [];
for (var i = 0; i < this.activeEntities.length; i++) {
var entityId = this.activeEntities[i];
var hasAllComponents = true;
for (var j = 0; j < componentNames.length; j++) {
var componentName = componentNames[j];
if (!this.componentStores[componentName] || !this.componentStores[componentName][entityId]) {
hasAllComponents = false;
break;
}
}
if (hasAllComponents) {
result.push(entityId);
}
}
return result;
},
destroyEntity: function destroyEntity(entityId) {
// Avoid adding duplicates
if (this.entitiesToDestroy.indexOf(entityId) === -1) {
this.entitiesToDestroy.push(entityId);
}
}
};
Game.Components = {
TransformComponent: function TransformComponent(x, y, rotation) {
return {
name: 'TransformComponent',
x: x || 0,
y: y || 0,
rotation: rotation || 0
};
},
RenderComponent: function RenderComponent(sprite_id, layer, scale, is_visible) {
return {
name: 'RenderComponent',
sprite_id: sprite_id || '',
layer: layer || 0,
scale: scale || 1.0,
is_visible: is_visible !== undefined ? is_visible : true
};
},
HealthComponent: function HealthComponent(current_hp, max_hp) {
return {
name: 'HealthComponent',
current_hp: current_hp || 100,
max_hp: max_hp || 100
};
},
AttackComponent: function AttackComponent(damage, range, attack_speed, last_attack_time, projectile_id, target_id, splash_radius) {
return {
name: 'AttackComponent',
damage: damage || 10,
range: range || 100,
attack_speed: attack_speed || 1.0,
last_attack_time: last_attack_time || 0,
projectile_id: projectile_id || '',
target_id: target_id || -1,
splash_radius: splash_radius || 0 // Default to 0 (no splash)
};
},
MovementComponent: function MovementComponent(speed) {
return {
name: 'MovementComponent',
speed: speed || 50
};
},
PowerProductionComponent: function PowerProductionComponent(rate) {
return {
name: 'PowerProductionComponent',
production_rate: rate || 0
};
},
PowerConsumptionComponent: function PowerConsumptionComponent(rate) {
return {
name: 'PowerConsumptionComponent',
drain_rate: rate || 0
};
},
TowerComponent: function TowerComponent(cost, sellValue, builtOnWave) {
return {
name: 'TowerComponent',
cost: cost || 50,
sell_value: sellValue || 25,
upgrade_level: 1,
upgrade_path_A_level: 0,
upgrade_path_B_level: 0,
built_on_wave: builtOnWave || 0
};
},
ProjectileComponent: function ProjectileComponent(target_id, speed, damage) {
return {
name: 'ProjectileComponent',
target_id: target_id || -1,
speed: speed || 8,
// Let's keep the speed lower
damage: damage || 10
};
},
EnemyComponent: function EnemyComponent(goldValue, livesCost, armor_type) {
return {
name: 'EnemyComponent',
gold_value: goldValue || 5,
lives_cost: livesCost || 1,
armor_type: armor_type || 'Normal'
};
},
PowerStorageComponent: function PowerStorageComponent(capacity) {
return {
name: 'PowerStorageComponent',
capacity: capacity || 0
};
},
PoisonOnImpactComponent: function PoisonOnImpactComponent(damage_per_second, duration) {
return {
name: 'PoisonOnImpactComponent',
damage_per_second: damage_per_second || 0,
duration: duration || 0
};
},
PoisonDebuffComponent: function PoisonDebuffComponent(damage_per_second, duration_remaining, last_tick_time) {
return {
name: 'PoisonDebuffComponent',
damage_per_second: damage_per_second || 0,
duration_remaining: duration_remaining || 0,
last_tick_time: last_tick_time || 0
};
},
SlowOnImpactComponent: function SlowOnImpactComponent(slow_percentage, duration) {
return {
name: 'SlowOnImpactComponent',
slow_percentage: slow_percentage || 0,
duration: duration || 0
};
},
SlowDebuffComponent: function SlowDebuffComponent(slow_percentage, duration_remaining) {
return {
name: 'SlowDebuffComponent',
slow_percentage: slow_percentage || 0,
duration_remaining: duration_remaining || 0
};
},
AuraComponent: function AuraComponent(range, effect_type) {
return {
name: 'AuraComponent',
range: range || 100,
effect_type: effect_type || ''
};
},
DamageBuffComponent: function DamageBuffComponent(multiplier, source_aura_id) {
return {
name: 'DamageBuffComponent',
multiplier: multiplier || 0,
source_aura_id: source_aura_id || -1
};
},
GoldGenerationComponent: function GoldGenerationComponent(gold_per_second) {
return {
name: 'GoldGenerationComponent',
gold_per_second: gold_per_second || 0
};
},
FlyingComponent: function FlyingComponent() {
return {
name: 'FlyingComponent'
};
},
// --- NEW COMPONENTS ---
FireZoneOnImpactComponent: function FireZoneOnImpactComponent(damage_per_tick, duration, tick_rate) {
return {
name: 'FireZoneOnImpactComponent',
damage_per_tick: damage_per_tick || 0,
duration: duration || 0,
tick_rate: tick_rate || 1.0
};
},
FireZoneComponent: function FireZoneComponent(damage_per_tick, duration_remaining, tick_rate, last_tick_time) {
return {
name: 'FireZoneComponent',
damage_per_tick: damage_per_tick || 0,
duration_remaining: duration_remaining || 0,
tick_rate: tick_rate || 1.0,
last_tick_time: last_tick_time || 0
};
}
};
Game.Systems = {
MovementSystem: {
update: function update() {
var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']);
var frame_time = 1 / 60; // Assuming 60 FPS
var goal = {
x: 39,
y: 30
}; // The goal grid cell
for (var i = 0; i < movableEntities.length; i++) {
var entityId = movableEntities[i];
var transform = Game.EntityManager.componentStores['TransformComponent'][entityId];
var movement = Game.EntityManager.componentStores['MovementComponent'][entityId];
var currentSpeed = movement.speed;
// Check for slow debuff (no changes here)
var slowDebuff = Game.EntityManager.componentStores['SlowDebuffComponent'] ? Game.EntityManager.componentStores['SlowDebuffComponent'][entityId] : null;
if (slowDebuff) {
currentSpeed = movement.speed * (1 - slowDebuff.slow_percentage);
slowDebuff.duration_remaining -= frame_time;
if (slowDebuff.duration_remaining <= 0) {
delete Game.EntityManager.componentStores['SlowDebuffComponent'][entityId];
}
}
// --- START OF NEW FLOW FIELD LOGIC ---
// 1. Get the direction vector from the flow field at the enemy's current position.
var direction = Game.PathManager.getVectorAt(transform.x, transform.y);
// 2. Calculate how far to move this frame.
var moveDistance = currentSpeed * frame_time;
// 3. Update the enemy's position.
transform.x += direction.x * moveDistance;
transform.y += direction.y * moveDistance;
// 4. Check if the enemy has reached the goal.
var gridX = Math.floor(transform.x / 64);
var gridY = Math.floor(transform.y / 64);
if (gridX === goal.x && gridY === goal.y) {
Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId));
}
// --- END OF NEW FLOW FIELD LOGIC ---
}
}
},
RenderSystem: {
displayObjects: {},
update: function update() {
var renderableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'RenderComponent']);
for (var i = 0; i < renderableEntities.length; i++) {
var entityId = renderableEntities[i];
var transform = Game.EntityManager.componentStores['TransformComponent'][entityId];
var render = Game.EntityManager.componentStores['RenderComponent'][entityId];
var sprite = this.displayObjects[entityId];
if (!sprite) {
sprite = gameWorld.attachAsset(render.sprite_id, {});
// --- NEW ANCHOR LOGIC ---
// This is the critical fix: Ensure all game entities are visually centered.
sprite.anchor.set(0.5, 0.5);
// --- END NEW ANCHOR LOGIC ---
this.displayObjects[entityId] = sprite;
}
sprite.x = transform.x;
sprite.y = transform.y;
sprite.visible = render.is_visible;
}
}
},
TowerBuildSystem: {
tryBuildAt: function tryBuildAt(gridX, gridY, towerType) {
var towerData = Game.TowerData[towerType];
// 1. Use the new 3x3 build check
if (!towerData || !Game.GridManager.isBuildable3x3(gridX, gridY)) {
return;
}
if (!Game.ResourceManager.spendGold(towerData.cost)) {
return;
}
// 2. Temporarily mark the 3x3 area on the grid for path validation
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
Game.GridManager.grid[gridX + dx][gridY + dy].state = Game.GridManager.CELL_STATES.TOWER;
}
}
// 3. Regenerate flow field and validate path
Game.PathManager.generateFlowField();
var spawnDistance = Game.PathManager.distanceField[0][30];
if (spawnDistance === Infinity) {
// INVALID PLACEMENT: Revert the grid cells and refund gold
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
Game.GridManager.grid[gridX + dx][gridY + dy].state = Game.GridManager.CELL_STATES.EMPTY;
}
}
Game.ResourceManager.addGold(towerData.cost);
// Re-generate the flow field to its valid state
Game.PathManager.generateFlowField();
return;
}
// --- VALID PLACEMENT ---
var towerId = Game.EntityManager.createEntity();
// 4. Permanently mark the 3x3 grid area with the new tower's ID
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
Game.GridManager.grid[gridX + dx][gridY + dy].entityId = towerId;
}
}
var centeredX = gridX * 64 + 32;
var centeredY = gridY * 64 + 32;
var transform = Game.Components.TransformComponent(centeredX, centeredY);
Game.EntityManager.addComponent(towerId, transform);
var render = Game.Components.RenderComponent(towerData.sprite_id, 2, 1.0, true);
Game.EntityManager.addComponent(towerId, render);
for (var i = 0; i < towerData.components.length; i++) {
var componentDef = towerData.components[i];
var component;
if (componentDef.name === 'TowerComponent') {
var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex;
component = Game.Components.TowerComponent(componentDef.args[0], componentDef.args[1], currentWave);
} else {
component = Game.Components[componentDef.name].apply(null, componentDef.args);
}
Game.EntityManager.addComponent(towerId, component);
}
var event = Game.Events.TowerPlacedEvent(towerId, towerData.cost, transform.x, transform.y);
Game.EventBus.publish(event);
}
},
TargetingSystem: {
update: function update() {
// If there is a power deficit, do not assign any new targets.
if (Game.Systems.CombatSystem.isPowerDeficit) {
// Also, clear any existing targets to prevent towers from holding a lock.
var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent']);
for (var i = 0; i < towers.length; i++) {
var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towers[i]];
towerAttack.target_id = -1;
}
return; // Stop the system's update.
}
var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']);
for (var i = 0; i < towers.length; i++) {
var towerId = towers[i];
var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towerId];
var towerTransform = Game.EntityManager.componentStores['TransformComponent'][towerId];
var bestTarget = -1;
var bestPathIndex = -1;
var enemies = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']);
for (var j = 0; j < enemies.length; j++) {
var enemyId = enemies[j];
var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId];
var dx = enemyTransform.x - towerTransform.x;
var dy = enemyTransform.y - towerTransform.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= towerAttack.range) {
// Check if enemy has PathFollowerComponent (ground units) or use default targeting for flyers
var pathFollower = Game.EntityManager.componentStores['PathFollowerComponent'] ? Game.EntityManager.componentStores['PathFollowerComponent'][enemyId] : null;
var currentPathIndex = pathFollower ? pathFollower.path_index : 0;
if (currentPathIndex > bestPathIndex) {
bestTarget = enemyId;
bestPathIndex = currentPathIndex;
}
}
}
towerAttack.target_id = bestTarget;
}
}
},
CleanupSystem: {
update: function update() {
for (var i = 0; i < Game.EntityManager.entitiesToDestroy.length; i++) {
var entityId = Game.EntityManager.entitiesToDestroy[i];
var index = Game.EntityManager.activeEntities.indexOf(entityId);
if (index > -1) {
Game.EntityManager.activeEntities.splice(index, 1);
}
for (var componentName in Game.EntityManager.componentStores) {
if (Game.EntityManager.componentStores[componentName][entityId]) {
delete Game.EntityManager.componentStores[componentName][entityId];
}
}
if (Game.Systems.RenderSystem.displayObjects[entityId]) {
Game.Systems.RenderSystem.displayObjects[entityId].destroy();
delete Game.Systems.RenderSystem.displayObjects[entityId];
}
}
if (Game.EntityManager.entitiesToDestroy.length > 0) {
Game.EntityManager.entitiesToDestroy = [];
}
}
},
WaveSpawnerSystem: {
currentWaveIndex: -1,
isSpawning: false,
spawnTimer: 0,
subWaveIndex: 0,
spawnedInSubWave: 0,
startNextWave: function startNextWave() {
this.currentWaveIndex++;
if (this.currentWaveIndex >= Game.WaveData.length) {
return; // No more waves
}
this.isSpawning = true;
this.spawnTimer = 0;
this.subWaveIndex = 0;
this.spawnedInSubWave = 0;
// Calculate the total number of enemies for this wave
var totalEnemies = 0;
var currentWave = Game.WaveData[this.currentWaveIndex];
for (var i = 0; i < currentWave.sub_waves.length; i++) {
totalEnemies += currentWave.sub_waves[i].count;
}
Game.GameplayManager.totalEnemiesThisWave = totalEnemies;
},
update: function update() {
if (!this.isSpawning || this.currentWaveIndex >= Game.WaveData.length) {
return;
}
var currentWave = Game.WaveData[this.currentWaveIndex];
if (!currentWave || this.subWaveIndex >= currentWave.sub_waves.length) {
this.isSpawning = false;
return;
}
var currentSubWave = currentWave.sub_waves[this.subWaveIndex];
if (this.spawnedInSubWave === 0 && this.spawnTimer < currentSubWave.start_delay) {
this.spawnTimer += 1 / 60;
return;
}
if (this.spawnedInSubWave > 0 && this.spawnTimer < currentSubWave.spawn_delay) {
this.spawnTimer += 1 / 60;
return;
}
if (this.spawnedInSubWave < currentSubWave.count) {
var enemyData = Game.EnemyData[currentSubWave.enemy_type];
if (!enemyData) {
this.subWaveIndex++;
this.spawnedInSubWave = 0;
this.spawnTimer = 0;
return;
}
var enemyId = Game.EntityManager.createEntity();
var isFlyer = enemyData.components && enemyData.components.some(function (c) {
return c.name === 'FlyingComponent';
});
var renderLayer = isFlyer ? 3 : 1;
var transform = Game.Components.TransformComponent(0 * 64, 30 * 64);
var render = Game.Components.RenderComponent('enemy', renderLayer, 1.0, true);
var movement = Game.Components.MovementComponent(enemyData.speed);
var health = Game.Components.HealthComponent(enemyData.health, enemyData.health);
var enemy = Game.Components.EnemyComponent(enemyData.gold_value, enemyData.lives_cost, enemyData.armor_type);
Game.EntityManager.addComponent(enemyId, transform);
Game.EntityManager.addComponent(enemyId, render);
Game.EntityManager.addComponent(enemyId, movement);
Game.EntityManager.addComponent(enemyId, health);
Game.EntityManager.addComponent(enemyId, enemy);
if (enemyData.components && enemyData.components.length > 0) {
for (var i = 0; i < enemyData.components.length; i++) {
var compData = enemyData.components[i];
var newComponent = Game.Components[compData.name].apply(null, compData.args || []);
Game.EntityManager.addComponent(enemyId, newComponent);
}
}
this.spawnedInSubWave++;
this.spawnTimer = 0;
Game.GameplayManager.enemiesSpawnedThisWave++;
} else {
this.subWaveIndex++;
this.spawnedInSubWave = 0;
this.spawnTimer = 0;
}
}
},
CombatSystem: {
isPowerDeficit: false,
init: function init() {
Game.EventBus.subscribe('PowerStatusChanged', this.onPowerStatusChanged.bind(this));
},
onPowerStatusChanged: function onPowerStatusChanged(event) {
this.isPowerDeficit = event.new_status === 'deficit';
},
update: function update() {
// If there is a power deficit, do not allow towers to fire.
if (this.isPowerDeficit) {
return; // Stop the system's update.
}
var currentTime = LK.ticks / 60;
var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']);
for (var i = 0; i < towers.length; i++) {
var towerId = towers[i];
var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towerId];
var towerTransform = Game.EntityManager.componentStores['TransformComponent'][towerId];
if (towerAttack.target_id !== -1 && currentTime - towerAttack.last_attack_time >= 1.0 / towerAttack.attack_speed) {
var targetTransform = Game.EntityManager.componentStores['TransformComponent'][towerAttack.target_id];
if (targetTransform) {
var baseDamage = towerAttack.damage;
var finalDamage = baseDamage;
var buffComp = Game.EntityManager.componentStores['DamageBuffComponent'] ? Game.EntityManager.componentStores['DamageBuffComponent'][towerId] : null;
if (buffComp) {
finalDamage = baseDamage * (1 + buffComp.multiplier);
}
var projectileId = Game.EntityManager.createEntity();
var projectileTransform = Game.Components.TransformComponent(towerTransform.x, towerTransform.y);
var projectileRender = Game.Components.RenderComponent(towerAttack.projectile_id, 1, 1.0, true);
var projectileComponent = Game.Components.ProjectileComponent(towerAttack.target_id, undefined, finalDamage);
projectileComponent.source_tower_id = towerId;
Game.EntityManager.addComponent(projectileId, projectileTransform);
Game.EntityManager.addComponent(projectileId, projectileRender);
Game.EntityManager.addComponent(projectileId, projectileComponent);
if (towerAttack.on_impact_effects && towerAttack.on_impact_effects.length > 0) {
for (var j = 0; j < towerAttack.on_impact_effects.length; j++) {
var effectData = towerAttack.on_impact_effects[j];
var newComponent = Game.Components[effectData.name].apply(null, effectData.args);
Game.EntityManager.addComponent(projectileId, newComponent);
}
}
towerAttack.last_attack_time = currentTime;
}
}
}
var projectiles = Game.EntityManager.getEntitiesWithComponents(['ProjectileComponent', 'TransformComponent']);
for (var i = projectiles.length - 1; i >= 0; i--) {
var projectileId = projectiles[i];
var projectileComp = Game.EntityManager.componentStores['ProjectileComponent'][projectileId];
var projectileTransform = Game.EntityManager.componentStores['TransformComponent'][projectileId];
var targetExists = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id];
if (!targetExists) {
Game.EntityManager.destroyEntity(projectileId);
continue;
}
var targetTransform = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id];
var dx = targetTransform.x - projectileTransform.x;
var dy = targetTransform.y - projectileTransform.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 10) {
var impactPoint = {
x: targetTransform.x,
y: targetTransform.y
};
var sourceTowerAttack = Game.EntityManager.componentStores['AttackComponent'][projectileComp.source_tower_id];
if (sourceTowerAttack && sourceTowerAttack.splash_radius > 0) {
var allEnemies = Game.EntityManager.getEntitiesWithComponents(['HealthComponent', 'TransformComponent', 'EnemyComponent']);
for (var j = 0; j < allEnemies.length; j++) {
var enemyId = allEnemies[j];
var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId];
var splash_dx = enemyTransform.x - impactPoint.x;
var splash_dy = enemyTransform.y - impactPoint.y;
var splash_dist = Math.sqrt(splash_dx * splash_dx + splash_dy * splash_dy);
if (splash_dist <= sourceTowerAttack.splash_radius) {
this.dealDamage(enemyId, projectileComp.damage);
}
}
} else {
this.dealDamage(projectileComp.target_id, projectileComp.damage);
}
var targetHealth = Game.EntityManager.componentStores['HealthComponent'][projectileComp.target_id];
if (targetHealth) {
var poisonImpactComp = Game.EntityManager.componentStores['PoisonOnImpactComponent'] ? Game.EntityManager.componentStores['PoisonOnImpactComponent'][projectileId] : null;
if (poisonImpactComp) {
var debuff = Game.Components.PoisonDebuffComponent(poisonImpactComp.damage_per_second, poisonImpactComp.duration, currentTime);
Game.EntityManager.addComponent(projectileComp.target_id, debuff);
}
var slowImpactComp = Game.EntityManager.componentStores['SlowOnImpactComponent'] ? Game.EntityManager.componentStores['SlowOnImpactComponent'][projectileId] : null;
if (slowImpactComp) {
var slowDebuff = Game.Components.SlowDebuffComponent(slowImpactComp.slow_percentage, slowImpactComp.duration);
Game.EntityManager.addComponent(projectileComp.target_id, slowDebuff);
}
}
var fireZoneImpactComp = Game.EntityManager.componentStores['FireZoneOnImpactComponent'] ? Game.EntityManager.componentStores['FireZoneOnImpactComponent'][projectileId] : null;
if (fireZoneImpactComp) {
var zoneId = Game.EntityManager.createEntity();
Game.EntityManager.addComponent(zoneId, Game.Components.TransformComponent(impactPoint.x, impactPoint.y));
Game.EntityManager.addComponent(zoneId, Game.Components.RenderComponent('fire_zone', 0, 1.0, true));
Game.EntityManager.addComponent(zoneId, Game.Components.FireZoneComponent(fireZoneImpactComp.damage_per_tick, fireZoneImpactComp.duration, fireZoneImpactComp.tick_rate, currentTime));
}
Game.EntityManager.destroyEntity(projectileId);
} else {
var moveX = dx / distance * projectileComp.speed;
var moveY = dy / distance * projectileComp.speed;
projectileTransform.x += moveX;
projectileTransform.y += moveY;
}
}
var poisonedEnemies = Game.EntityManager.getEntitiesWithComponents(['PoisonDebuffComponent']);
for (var i = poisonedEnemies.length - 1; i >= 0; i--) {
var enemyId = poisonedEnemies[i];
var debuff = Game.EntityManager.componentStores['PoisonDebuffComponent'][enemyId];
if (!debuff) {
continue;
}
if (currentTime - debuff.last_tick_time >= 1.0) {
this.dealDamage(enemyId, debuff.damage_per_second);
debuff.last_tick_time = currentTime;
debuff.duration_remaining -= 1.0;
}
if (debuff.duration_remaining <= 0) {
delete Game.EntityManager.componentStores['PoisonDebuffComponent'][enemyId];
}
}
var fireZones = Game.EntityManager.getEntitiesWithComponents(['FireZoneComponent', 'TransformComponent']);
for (var i = fireZones.length - 1; i >= 0; i--) {
var zoneId = fireZones[i];
var zoneComp = Game.EntityManager.componentStores['FireZoneComponent'][zoneId];
var zoneTransform = Game.EntityManager.componentStores['TransformComponent'][zoneId];
zoneComp.duration_remaining -= 1.0 / 60;
if (zoneComp.duration_remaining <= 0) {
Game.EntityManager.destroyEntity(zoneId);
continue;
}
if (currentTime - zoneComp.last_tick_time >= zoneComp.tick_rate) {
var allEnemies = Game.EntityManager.getEntitiesWithComponents(['HealthComponent', 'TransformComponent']);
var zoneRadius = 40;
for (var j = 0; j < allEnemies.length; j++) {
var enemyId = allEnemies[j];
var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId];
var dx = enemyTransform.x - zoneTransform.x;
var dy = enemyTransform.y - zoneTransform.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= zoneRadius) {
this.dealDamage(enemyId, zoneComp.damage_per_tick);
}
}
zoneComp.last_tick_time = currentTime;
}
}
},
dealDamage: function dealDamage(enemyId, damage) {
var targetHealth = Game.EntityManager.componentStores['HealthComponent'][enemyId];
if (!targetHealth) {
return;
}
var finalDamage = damage;
var enemyComp = Game.EntityManager.componentStores['EnemyComponent'][enemyId];
if (enemyComp && enemyComp.armor_type === 'Heavy') {
finalDamage *= 0.5;
}
targetHealth.current_hp -= finalDamage;
if (targetHealth.current_hp <= 0) {
var goldValue = enemyComp ? enemyComp.gold_value : 0;
Game.EventBus.publish(Game.Events.EnemyKilledEvent(enemyId, goldValue));
Game.EntityManager.destroyEntity(enemyId);
}
}
},
AuraSystem: {
update: function update() {
var buffedTowers = Game.EntityManager.getEntitiesWithComponents(['DamageBuffComponent']);
for (var i = 0; i < buffedTowers.length; i++) {
var towerId = buffedTowers[i];
delete Game.EntityManager.componentStores['DamageBuffComponent'][towerId];
}
var auraTowers = Game.EntityManager.getEntitiesWithComponents(['AuraComponent', 'TransformComponent']);
if (auraTowers.length === 0) {
return;
}
var attackTowers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']);
for (var i = 0; i < auraTowers.length; i++) {
var auraTowerId = auraTowers[i];
var auraComponent = Game.EntityManager.componentStores['AuraComponent'][auraTowerId];
var auraTransform = Game.EntityManager.componentStores['TransformComponent'][auraTowerId];
if (auraComponent.effect_type !== 'damage_buff') {
continue;
}
for (var j = 0; j < attackTowers.length; j++) {
var attackTowerId = attackTowers[j];
if (attackTowerId === auraTowerId) {
continue;
}
var attackTransform = Game.EntityManager.componentStores['TransformComponent'][attackTowerId];
var dx = auraTransform.x - attackTransform.x;
var dy = auraTransform.y - attackTransform.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= auraComponent.range) {
var buff = Game.Components.DamageBuffComponent(0.2, auraTowerId);
Game.EntityManager.addComponent(attackTowerId, buff);
}
}
}
}
},
EconomySystem: {
update: function update() {
var goldGenerators = Game.EntityManager.getEntitiesWithComponents(['GoldGenerationComponent']);
for (var i = 0; i < goldGenerators.length; i++) {
var entityId = goldGenerators[i];
var goldGenComp = Game.EntityManager.componentStores['GoldGenerationComponent'][entityId];
if (goldGenComp) {
Game.ResourceManager.addGold(goldGenComp.gold_per_second);
}
}
}
},
TowerSystem: {
tryUpgradeTower: function tryUpgradeTower(towerId, path) {
var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId];
if (!renderComp) {
return;
}
var towerSpriteId = renderComp.sprite_id;
var towerType = null;
for (var type in Game.TowerData) {
if (Game.TowerData[type].sprite_id === towerSpriteId) {
towerType = type;
break;
}
}
if (!towerType) {
return;
}
var upgradePathData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType][path] : null;
if (!upgradePathData) {
return;
}
var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId];
if (!towerComp) {
return;
}
var nextUpgradeIndex = path === 'PathA' ? towerComp.upgrade_path_A_level : towerComp.upgrade_path_B_level;
if (nextUpgradeIndex >= upgradePathData.length) {
return;
}
var upgradeInfo = upgradePathData[nextUpgradeIndex];
if (!Game.ResourceManager.spendGold(upgradeInfo.cost)) {
return;
}
for (var componentName in upgradeInfo.effects) {
var componentToModify = Game.EntityManager.componentStores[componentName] ? Game.EntityManager.componentStores[componentName][towerId] : null;
if (componentToModify) {
var effectsToApply = upgradeInfo.effects[componentName];
for (var property in effectsToApply) {
if (componentToModify.hasOwnProperty(property)) {
componentToModify[property] += effectsToApply[property];
}
}
}
}
if (upgradeInfo.add_component_on_impact) {
var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId];
if (attackComp) {
if (!attackComp.on_impact_effects) {
attackComp.on_impact_effects = [];
}
attackComp.on_impact_effects.push(upgradeInfo.add_component_on_impact);
}
}
towerComp.upgrade_level++;
if (path === 'PathA') {
towerComp.upgrade_path_A_level++;
} else {
towerComp.upgrade_path_B_level++;
}
},
trySellTower: function trySellTower(towerId) {
var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId];
var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId];
if (!towerComp || !transformComp) {
return;
}
var refundAmount = towerComp.sell_value;
var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex;
if (towerComp.built_on_wave === currentWave) {
refundAmount = towerComp.cost;
}
Game.ResourceManager.addGold(refundAmount);
// Clear the tower's 3x3 footprint from the grid
var gridX = Math.floor(transformComp.x / 64);
var gridY = Math.floor(transformComp.y / 64);
for (var dx = -1; dx <= 1; dx++) {
for (var dy = -1; dy <= 1; dy++) {
var x = gridX + dx;
var y = gridY + dy;
if (Game.GridManager.grid[x] && Game.GridManager.grid[x][y] !== undefined) {
Game.GridManager.grid[x][y].state = Game.GridManager.CELL_STATES.EMPTY;
Game.GridManager.grid[x][y].entityId = null;
}
}
}
Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount));
Game.GameplayManager.selectedTowerId = -1;
Game.EntityManager.destroyEntity(towerId);
}
}
};
// === SECTION: GAME DATA ===
Game.EnemyData = {
'Grunt': {
health: 100,
speed: 50,
gold_value: 5,
lives_cost: 1,
armor_type: 'Normal',
components: []
},
'Swarmer': {
health: 30,
speed: 80,
gold_value: 2,
lives_cost: 1,
armor_type: 'Light',
components: []
},
'Armored': {
health: 400,
speed: 30,
gold_value: 15,
lives_cost: 2,
armor_type: 'Heavy',
components: []
},
'Flyer': {
health: 80,
speed: 60,
gold_value: 8,
lives_cost: 1,
armor_type: 'Light',
components: [{
name: 'FlyingComponent',
args: []
}]
}
};
Game.WaveData = [
// Wave 1: A few basic Grunts
{
wave_number: 1,
sub_waves: [{
enemy_type: 'Grunt',
count: 10,
spawn_delay: 1.5,
start_delay: 0
}]
},
// Wave 2: A larger group of Grunts
{
wave_number: 2,
sub_waves: [{
enemy_type: 'Grunt',
count: 15,
spawn_delay: 1.2,
start_delay: 0
}]
},
// Wave 3: A wave of fast Swarmers
{
wave_number: 3,
sub_waves: [{
enemy_type: 'Swarmer',
count: 20,
spawn_delay: 0.5,
start_delay: 0
}]
},
// Wave 4: Grunts supported by slow, tough Armored units
{
wave_number: 4,
sub_waves: [{
enemy_type: 'Grunt',
count: 10,
spawn_delay: 1.5,
start_delay: 0
}, {
enemy_type: 'Armored',
count: 2,
spawn_delay: 3.0,
start_delay: 5.0
}]
},
// Wave 5: A squadron of Flyers that ignore the path
{
wave_number: 5,
sub_waves: [{
enemy_type: 'Flyer',
count: 8,
spawn_delay: 1.0,
start_delay: 0
}]
}];
// === SECTION: TOWER DATA ===
Game.TowerData = {
'ArrowTower': {
cost: 50,
sprite_id: 'arrow_tower',
color: 0x2ecc71,
components: [{
name: 'TowerComponent',
args: [50, 25, undefined]
}, {
name: 'AttackComponent',
args: [10, 150, 1.0, 0, 'arrow_projectile']
}, {
name: 'PowerConsumptionComponent',
args: [2]
}]
},
'GeneratorTower': {
cost: 75,
sprite_id: 'generator_tower',
color: 0x3498db,
components: [{
name: 'TowerComponent',
args: [75, 37, undefined]
}, {
name: 'PowerProductionComponent',
args: [5]
}]
},
'CapacitorTower': {
cost: 40,
sprite_id: 'capacitor_tower',
color: 0xf1c40f,
components: [{
name: 'TowerComponent',
args: [40, 20, undefined]
}, {
name: 'PowerStorageComponent',
args: [20]
}]
},
'RockLauncher': {
cost: 125,
sprite_id: 'rock_launcher_tower',
color: 0x8d6e63,
components: [{
name: 'TowerComponent',
args: [125, 62, undefined]
}, {
name: 'AttackComponent',
args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50]
}, {
name: 'PowerConsumptionComponent',
args: [10]
}]
},
'ChemicalTower': {
cost: 100,
sprite_id: 'chemical_tower',
color: 0xb87333,
components: [{
name: 'TowerComponent',
args: [100, 50, undefined]
}, {
name: 'AttackComponent',
args: [5, 150, 0.8, 0, 'chemical_projectile', -1, 0]
}, {
name: 'PowerConsumptionComponent',
args: [8]
}]
},
'SlowTower': {
cost: 80,
sprite_id: 'slow_tower',
color: 0x5dade2,
components: [{
name: 'TowerComponent',
args: [80, 40, undefined]
}, {
name: 'AttackComponent',
args: [2, 160, 1.0, 0, 'slow_projectile']
}, {
name: 'PowerConsumptionComponent',
args: [4]
}]
},
'DarkTower': {
cost: 150,
sprite_id: 'dark_tower',
color: 0x9b59b6,
components: [{
name: 'TowerComponent',
args: [150, 75, undefined]
}, {
name: 'AuraComponent',
args: [175, 'damage_buff']
}, {
name: 'PowerConsumptionComponent',
args: [12]
}]
},
'GraveyardTower': {
cost: 200,
sprite_id: 'graveyard_tower',
color: 0x607d8b,
components: [{
name: 'TowerComponent',
args: [200, 100, undefined]
}, {
name: 'GoldGenerationComponent',
args: [1.5]
}, {
name: 'PowerConsumptionComponent',
args: [15]
}]
}
};
// === SECTION: UPGRADE DATA ===
Game.UpgradeData = {
'ArrowTower': {
'PathA': [{
cost: 60,
effects: {
AttackComponent: {
damage: 5
}
}
},
//{7R} // Level 1: +5 Dmg
{
cost: 110,
effects: {
AttackComponent: {
damage: 10
}
}
} //{7W} // Level 2: +10 Dmg
],
'PathB': [{
cost: 75,
effects: {
AttackComponent: {
attack_speed: 0.2
}
}
},
//{83} // Level 1: +0.2 AS
{
cost: 125,
effects: {
AttackComponent: {
attack_speed: 0.3
}
}
} //{8a} // Level 2: +0.3 AS
]
},
'RockLauncher': {
'PathA': [
// Heavy Boulders - More Damage & Range
{
cost: 150,
effects: {
AttackComponent: {
damage: 15,
range: 20
}
}
}, {
cost: 250,
effects: {
AttackComponent: {
damage: 25,
range: 30
}
}
},
// --- UPDATED UPGRADE ---
{
cost: 500,
effects: {
AttackComponent: {
damage: 50,
range: 50
}
},
add_component_on_impact: {
name: 'FireZoneOnImpactComponent',
args: [10, 3, 0.5]
} // dmg, duration, tick_rate
}],
'PathB': [
// Lighter Munitions - Faster Attack Speed
{
cost: 140,
effects: {
AttackComponent: {
damage: 5,
attack_speed: 0.2
}
}
}, {
cost: 220,
effects: {
AttackComponent: {
damage: 10,
attack_speed: 0.3
}
}
}, {
cost: 450,
effects: {
AttackComponent: {
damage: 15,
attack_speed: 0.5
}
}
}]
}
};
// === SECTION: GRID DRAWING ===
// Create a container for all game objects that will be panned
var gameWorld = new Container();
game.addChild(gameWorld);
function drawGrid() {
// Draw the grid cells
for (var x = 0; x < Game.GridManager.grid.length; x++) {
for (var y = 0; y < Game.GridManager.grid[x].length; y++) {
var cellSprite = gameWorld.attachAsset('grid_cell', {});
cellSprite.x = x * 64;
cellSprite.y = y * 64;
cellSprite.anchor.set(0, 0); // Anchor to top-left
cellSprite.gridX = x; // Store grid coordinates on the sprite
cellSprite.gridY = y;
cellSprite.down = function () {
// Only process true clicks, not drags
var timeSinceMouseDown = LK.ticks / 60 * 1000 - mouseDownTime;
if (timeSinceMouseDown > 200) {
return;
}
var dx = Math.abs(lastMouseX - dragStartX);
var dy = Math.abs(lastMouseY - dragStartY);
if (dx > 10 || dy > 10) {
return;
}
var gridX = this.gridX;
var gridY = this.gridY;
var gridCell = Game.GridManager.grid[gridX][gridY];
if (Game.GameplayManager.activeBuildType) {
// If in build mode, check if the 3x3 area is buildable
if (Game.GridManager.isBuildable3x3(gridX, gridY)) {
Game.GameplayManager.isConfirmingPlacement = true;
// The center of the build is the clicked cell
Game.GameplayManager.confirmPlacementCoords = {
x: gridX,
y: gridY
};
var worldX = gridX * 64 + 32;
var worldY = gridY * 64 + 32;
confirmPanel.x = worldX;
confirmPanel.y = worldY;
} else {
// Clicked an invalid spot, so cancel build mode
Game.GameplayManager.activeBuildType = null;
}
} else if (!Game.GameplayManager.isConfirmingPlacement) {
// If not in build mode, check for selection
if (gridCell.state === Game.GridManager.CELL_STATES.TOWER) {
// The grid cell now directly tells us which tower is selected
Game.GameplayManager.selectedTowerId = gridCell.entityId;
} else {
Game.GameplayManager.selectedTowerId = -1;
}
}
};
}
}
// Highlight the path cells
if (Game.PathManager.currentPath) {
for (var i = 0; i < Game.PathManager.currentPath.length; i++) {
var pathNode = Game.PathManager.currentPath[i];
var pathSprite = gameWorld.attachAsset('path_cell', {});
// Note: PathManager coordinates are already in pixels
pathSprite.x = pathNode.x;
pathSprite.y = pathNode.y;
pathSprite.anchor.set(0, 0);
}
}
}
// === SECTION: GLOBAL EVENT BUS ===
Game.EventBus = {
listeners: {},
subscribe: function subscribe(eventType, listener) {
if (!this.listeners[eventType]) {
this.listeners[eventType] = [];
}
this.listeners[eventType].push(listener);
},
unsubscribe: function unsubscribe(eventType, listener) {
if (this.listeners[eventType]) {
var index = this.listeners[eventType].indexOf(listener);
if (index !== -1) {
this.listeners[eventType].splice(index, 1);
}
}
},
publish: function publish(event) {
if (this.listeners[event.type]) {
for (var i = 0; i < this.listeners[event.type].length; i++) {
this.listeners[event.type][i](event);
}
}
}
};
Game.Events = {
EnemyReachedEndEvent: function EnemyReachedEndEvent(enemy_id) {
return {
type: 'EnemyReachedEnd',
enemy_id: enemy_id
};
},
EnemyKilledEvent: function EnemyKilledEvent(enemy_id, gold_value) {
return {
type: 'EnemyKilled',
enemy_id: enemy_id,
gold_value: gold_value
};
},
TowerPlacedEvent: function TowerPlacedEvent(tower_id, cost, position_x, position_y) {
return {
type: 'TowerPlaced',
tower_id: tower_id,
cost: cost,
position_x: position_x,
position_y: position_y
};
},
TowerSoldEvent: function TowerSoldEvent(tower_id, refund_value) {
return {
type: 'TowerSold',
tower_id: tower_id,
refund_value: refund_value
};
},
PowerStatusChangedEvent: function PowerStatusChangedEvent(new_status) {
return {
type: 'PowerStatusChanged',
new_status: new_status
};
},
WaveCompletedEvent: function WaveCompletedEvent(wave_number) {
return {
type: 'WaveCompleted',
wave_number: wave_number
};
},
GameOverEvent: function GameOverEvent() {
return {
type: 'GameOver'
};
}
};
// === SECTION: STACK-BASED GAME STATE MANAGER ===
// === SECTION: DIRECT GAME INITIALIZATION ===
// Initialize managers
Game.GridManager.init(40, 60);
Game.GridManager.grid[0][30] = Game.GridManager.CELL_STATES.PATH; // Set spawn point as unbuildable
Game.ResourceManager.init();
Game.PathManager.init(40, 60);
drawGrid();
// Create the Heads-Up Display (HUD)
var goldText = new Text2('Gold: 100', {
size: 40,
fill: 0xFFD700
});
goldText.anchor.set(1, 0); // Align to the right
LK.gui.topRight.addChild(goldText);
goldText.x = -20; // 20px padding from the right edge
goldText.y = 20; // 20px padding from the top edge
var livesText = new Text2('Lives: 20', {
size: 40,
fill: 0xFF4136
});
livesText.anchor.set(1, 0); // Align to the right
LK.gui.topRight.addChild(livesText);
livesText.x = -20; // 20px padding from the right edge
livesText.y = 70; // Position below the gold text
var waveText = new Text2('Wave: 1 / 5', {
size: 40,
fill: 0xFFFFFF
});
waveText.anchor.set(1, 0); // Align to the right
LK.gui.topRight.addChild(waveText);
waveText.x = -20; // 20px padding from the right edge
waveText.y = 120; // Position below the lives text
// Create Power Meter UI
var powerMeterContainer = new Container();
LK.gui.topRight.addChild(powerMeterContainer);
powerMeterContainer.x = -320; // Positioned in from the right edge
powerMeterContainer.y = 170; // Position below wave text
// Create a Graphics instance for power production bar
var Graphics = function Graphics() {
var graphics = new Container();
graphics.currentFill = 0xffffff;
graphics.currentAlpha = 1.0;
graphics.beginFill = function (color, alpha) {
this.currentFill = color;
this.currentAlpha = alpha !== undefined ? alpha : 1.0;
};
graphics.drawRect = function (x, y, width, height) {
var rect = game.attachAsset('grid_cell', {});
rect.tint = this.currentFill;
rect.alpha = this.currentAlpha;
rect.x = x;
rect.y = y;
rect.width = width;
rect.height = height;
this.addChild(rect);
};
graphics.endFill = function () {
// No-op for compatibility
};
graphics.clear = function () {
// Remove all children to clear the graphics
while (this.children.length > 0) {
var child = this.children[0];
this.removeChild(child);
if (child.destroy) {
child.destroy();
}
}
};
return graphics;
};
// Background bar representing total production capacity
var powerProductionBar = new Graphics();
powerProductionBar.beginFill(0x3498db, 0.5); // Semi-transparent blue
powerProductionBar.drawRect(0, 0, 300, 20); // Placeholder width
powerProductionBar.endFill();
powerMeterContainer.addChild(powerProductionBar);
// Foreground bar representing current consumption
var powerConsumptionBar = new Graphics();
powerConsumptionBar.beginFill(0x2ecc71); // Green for surplus
powerConsumptionBar.drawRect(0, 0, 10, 20); // Placeholder width
powerConsumptionBar.endFill();
powerMeterContainer.addChild(powerConsumptionBar);
// Secondary bar for the available power buffer (capacitors)
var powerAvailableBar = new Graphics();
powerAvailableBar.beginFill(0x3498db, 0.3); // Fainter blue
powerAvailableBar.drawRect(0, 0, 5, 20); // Placeholder width, starts at the end of the production bar
powerAvailableBar.endFill();
powerMeterContainer.addChild(powerAvailableBar);
// Create Start Wave button
var startWaveButton = new Text2('START NEXT WAVE', {
size: 70,
fill: 0x00FF00
});
startWaveButton.anchor.set(0.5, 1);
LK.gui.bottom.addChild(startWaveButton);
startWaveButton.y = -20;
startWaveButton.visible = true;
startWaveButton.down = function () {
// Deselect any active tower before starting the wave
Game.GameplayManager.selectedTowerId = -1;
Game.Systems.WaveSpawnerSystem.startNextWave();
startWaveButton.visible = false;
};
// Initialize GameplayManager
Game.GameplayManager.init();
// Initialize GameManager
Game.GameManager.init();
Game.Systems.CombatSystem.init();
// === SECTION: TEMP BUILD UI ===
// === SECTION: BUILD AND CONFIRMATION UI ===
// -- Ghost Tower (for placement preview) --
var ghostTowerSprite = null;
// -- Build Panel (at the bottom of the screen) --
var buildPanel = new Container();
LK.gui.bottom.addChild(buildPanel);
buildPanel.x = 0;
buildPanel.y = -150; // Positioned further up to not overlap the "Start Wave" button
// -- Floating Info Panel (now a fixed panel above the build bar) --
var floatingInfoPanel = new Container();
LK.gui.bottom.addChild(floatingInfoPanel); // Attach to the bottom GUI anchor
// Container objects don't have anchor property, so position it manually to center
floatingInfoPanel.x = -150; // Offset by half the panel width (300/2) to center
floatingInfoPanel.y = -320; // Position it 320px up from the bottom edge
floatingInfoPanel.visible = false;
var infoBackground = new Graphics();
infoBackground.beginFill(0x000000, 0.8);
infoBackground.drawRect(0, 0, 300, 80);
infoBackground.endFill();
floatingInfoPanel.addChild(infoBackground);
var infoText = new Text2('', {
size: 30,
fill: 0xFFFFFF,
wordWrap: true,
wordWrapWidth: 290
});
infoText.anchor.set(0, 0);
infoText.x = 5;
infoText.y = 5;
floatingInfoPanel.addChild(infoText);
// -- Confirmation Panel (appears on the game map) --
var confirmPanel = new Container();
gameWorld.addChild(confirmPanel); // Added to gameWorld so it pans with the camera
confirmPanel.visible = false;
// Buttons will be created and destroyed dynamically.
var confirmButton = null;
var cancelButton = null;
// -- Build Button Creation Logic (for Drag-and-Drop) --
var buildButtonData = [{
type: 'ArrowTower',
sprite: 'arrow_tower'
}, {
type: 'GeneratorTower',
sprite: 'generator_tower'
}, {
type: 'CapacitorTower',
sprite: 'capacitor_tower'
}, {
type: 'RockLauncher',
sprite: 'rock_launcher_tower'
}, {
type: 'ChemicalTower',
sprite: 'chemical_tower'
}, {
type: 'SlowTower',
sprite: 'slow_tower'
}, {
type: 'DarkTower',
sprite: 'dark_tower'
}, {
type: 'GraveyardTower',
sprite: 'graveyard_tower'
}];
var buttonSize = 90;
var buttonPadding = 15;
var totalWidth = buildButtonData.length * buttonSize + (buildButtonData.length - 1) * buttonPadding;
var startX = -totalWidth / 2;
for (var i = 0; i < buildButtonData.length; i++) {
var data = buildButtonData[i];
var button = game.attachAsset(data.sprite, {});
button.width = buttonSize;
button.height = buttonSize;
button.anchor.set(0.5, 0.5);
button.x = startX + i * (buttonSize + buttonPadding) + buttonSize / 2;
button.y = 0;
button.towerType = data.type;
// This is the start of the drag-and-drop
button.down = function () {
// Set the active build type to start the drag process
Game.GameplayManager.activeBuildType = this.towerType;
Game.GameplayManager.isConfirmingPlacement = false;
Game.GameplayManager.selectedTowerId = -1; // Deselect any existing tower
};
// We no longer need a 'move' handler here, as the main game loop will handle the drag.
buildPanel.addChild(button);
}
// We no longer need the 'buildPanel.out' handler either.
buildPanel.out = null;
// Confirmation button logic is now handled dynamically in the game update loop
// === SECTION: MINIMAP UI ===
var minimapContainer = new Container();
LK.gui.bottomLeft.addChild(minimapContainer);
minimapContainer.x = 20; // 20px padding from the left edge
minimapContainer.y = -320; // Position up from bottom edge (negative value moves up in bottomLeft anchor)
var minimapBackground = new Graphics();
minimapContainer.addChild(minimapBackground);
// Function to draw the static parts of the minimap
function drawMinimapBackground() {
var mapWidth = 40;
var mapHeight = 60;
var tilePixelSize = 5; // Each grid cell is a 5x5 pixel block on the minimap
var colors = {
EMPTY: 0x3d2b1f,
// Dark Brown for buildable land
PATH: 0x8B4513,
// SaddleBrown for the path
TOWER: 0xFFFFFF,
// White (will be used for dynamic tower dots later)
BLOCKED: 0x111111 // Almost black for blocked areas
};
minimapBackground.clear();
minimapBackground.beginFill(0x000000, 0.5); // Semi-transparent black background for the minimap
minimapBackground.drawRect(0, 0, mapWidth * tilePixelSize, mapHeight * tilePixelSize);
minimapBackground.endFill();
for (var x = 0; x < mapWidth; x++) {
for (var y = 0; y < mapHeight; y++) {
var cellState = Game.GridManager.grid[x][y];
var color;
switch (cellState) {
case Game.GridManager.CELL_STATES.PATH:
color = colors.PATH;
break;
case Game.GridManager.CELL_STATES.BLOCKED:
color = colors.BLOCKED;
break;
default:
color = colors.EMPTY;
break;
}
minimapBackground.beginFill(color);
minimapBackground.drawRect(x * tilePixelSize, y * tilePixelSize, tilePixelSize, tilePixelSize);
minimapBackground.endFill();
}
}
}
// Call the function once to draw the initial state
drawMinimapBackground();
var minimapDynamicLayer = new Graphics();
minimapContainer.addChild(minimapDynamicLayer);
var minimapViewportRect = new Graphics();
minimapContainer.addChild(minimapViewportRect);
minimapContainer.down = function (x, y, obj) {
// x and y are the click coordinates relative to the minimap's top-left corner.
// 1. Define map and minimap dimensions for conversion.
var mapWorldWidth = 40 * 64; // 2560
var mapWorldHeight = 60 * 64; // 3840
var minimapPixelWidth = 40 * 5; // 200
var minimapPixelHeight = 60 * 5; // 300
// 2. Convert minimap click coordinates to world coordinates.
// This gives us the point in the world the player wants to look at.
var targetWorldX = x / minimapPixelWidth * mapWorldWidth;
var targetWorldY = y / minimapPixelHeight * mapWorldHeight;
// 3. Calculate the new camera position to CENTER the view on the target.
var screenWidth = 2048;
var screenHeight = 2732;
var newCameraX = -targetWorldX + screenWidth / 2;
var newCameraY = -targetWorldY + screenHeight / 2;
// 4. Clamp the new camera position to the map boundaries.
// This reuses the same clamping logic from the drag-to-pan feature.
var minX = screenWidth - mapWorldWidth;
var minY = screenHeight - mapWorldHeight;
var maxX = 0;
var maxY = 0;
cameraX = Math.max(minX, Math.min(maxX, newCameraX));
cameraY = Math.max(minY, Math.min(maxY, newCameraY));
// 5. Apply the final, clamped transform to the game world.
gameWorld.x = cameraX;
gameWorld.y = cameraY;
};
function updateMinimapDynamic() {
minimapDynamicLayer.clear();
var tilePixelSize = 5;
// Draw Towers
var towers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']);
for (var i = 0; i < towers.length; i++) {
var towerId = towers[i];
var transform = Game.EntityManager.componentStores['TransformComponent'][towerId];
var gridX = Math.floor(transform.x / 64);
var gridY = Math.floor(transform.y / 64);
minimapDynamicLayer.beginFill(0xFFFFFF); // White dot for towers
minimapDynamicLayer.drawRect(gridX * tilePixelSize, gridY * tilePixelSize, tilePixelSize, tilePixelSize);
minimapDynamicLayer.endFill();
}
// Draw Enemies
var enemies = Game.EntityManager.getEntitiesWithComponents(['EnemyComponent', 'TransformComponent']);
for (var i = 0; i < enemies.length; i++) {
var enemyId = enemies[i];
var transform = Game.EntityManager.componentStores['TransformComponent'][enemyId];
// Use the enemy's precise world coordinates for smoother tracking on the minimap
var minimapX = transform.x / 64 * tilePixelSize;
var minimapY = transform.y / 64 * tilePixelSize;
minimapDynamicLayer.beginFill(0xFF0000); // Red dot for enemies
minimapDynamicLayer.drawRect(minimapX, minimapY, 3, 3); // Make them slightly smaller than a tile
minimapDynamicLayer.endFill();
}
}
function updateMinimapViewport() {
minimapViewportRect.clear();
var tilePixelSize = 5;
var worldTileSize = 64;
// The cameraX/Y variables are inverse offsets. We need the true top-left coordinate.
var trueCameraX = -cameraX;
var trueCameraY = -cameraY;
// Get screen/viewport dimensions
var screenWidth = 2048; // Use the game's actual screen dimensions
var screenHeight = 2732;
// Convert world coordinates and dimensions to minimap scale
var rectX = trueCameraX / worldTileSize * tilePixelSize;
var rectY = trueCameraY / worldTileSize * tilePixelSize;
var rectWidth = screenWidth / worldTileSize * tilePixelSize;
var rectHeight = screenHeight / worldTileSize * tilePixelSize;
minimapViewportRect.beginFill(0xFFFFFF, 0.25); // Semi-transparent white
minimapViewportRect.drawRect(rectX, rectY, rectWidth, rectHeight);
minimapViewportRect.endFill();
}
// Add mouse event handlers for camera panning
game.down = function (x, y, obj) {
isDragging = true;
dragStartX = x;
dragStartY = y;
lastMouseX = x;
lastMouseY = y;
cameraStartX = cameraX;
cameraStartY = cameraY;
mouseDownTime = LK.ticks / 60 * 1000; // Convert to milliseconds
};
game.move = function (x, y, obj) {
lastMouseX = x;
lastMouseY = y;
if (isDragging) {
var dx = x - dragStartX;
var dy = y - dragStartY;
// Update camera position (inverted for drag effect)
cameraX = cameraStartX + dx;
cameraY = cameraStartY + dy;
// Clamp camera to map boundaries
var mapWidth = 40 * 64; // 40 tiles * 64 pixels per tile
var mapHeight = 60 * 64; // 60 tiles * 64 pixels per tile
var screenWidth = 2048;
var screenHeight = 2732;
// Ensure we never see beyond the map edges
var minX = screenWidth - mapWidth;
var minY = screenHeight - mapHeight;
var maxX = 0;
var maxY = 0;
cameraX = Math.max(minX, Math.min(maxX, cameraX));
cameraY = Math.max(minY, Math.min(maxY, cameraY));
// Apply camera transform to game world
gameWorld.x = cameraX;
gameWorld.y = cameraY;
}
};
game.up = function (x, y, obj) {
isDragging = false;
};
// Set up main game loop
var powerUpdateTimer = 0;
game.update = function () {
if (Game.GameManager.isGameOver) {
return;
}
// Run core game systems
Game.Systems.WaveSpawnerSystem.update();
Game.Systems.TargetingSystem.update();
Game.Systems.AuraSystem.update();
Game.Systems.CombatSystem.update();
Game.Systems.MovementSystem.update();
Game.Systems.RenderSystem.update();
// --- UI STATE MACHINE FOR DRAG-AND-DROP ---
var currentBuildType = Game.GameplayManager.activeBuildType;
// Check if the player is currently dragging a tower to build
if (currentBuildType && !Game.GameplayManager.isConfirmingPlacement) {
// --- STATE: DRAGGING TOWER ---
var towerData = Game.TowerData[currentBuildType];
// Ensure UI for other states is hidden
confirmPanel.visible = false;
upgradePanel.visible = false;
rangeCircle.visible = false;
// Create/update the ghost tower sprite that follows the cursor/finger
if (!ghostTowerSprite || ghostTowerSprite.towerType !== currentBuildType) {
if (ghostTowerSprite) ghostTowerSprite.destroy();
ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {});
ghostTowerSprite.anchor.set(0.5, 0.5);
ghostTowerSprite.alpha = 0.5;
ghostTowerSprite.towerType = currentBuildType;
}
ghostTowerSprite.visible = true;
var mouseWorldX = lastMouseX - cameraX;
var mouseWorldY = lastMouseY - cameraY;
var gridX = Math.floor(mouseWorldX / 64);
var gridY = Math.floor(mouseWorldY / 64);
ghostTowerSprite.x = gridX * 64 + 32;
ghostTowerSprite.y = gridY * 64 + 32;
ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555;
// Update and show the fixed info panel
floatingInfoPanel.visible = true;
infoText.setText(currentBuildType + "\nCost: " + towerData.cost);
// The panel's position is now fixed and no longer updated here.
} else if (Game.GameplayManager.isConfirmingPlacement) {
// --- STATE: AWAITING CONFIRMATION ---
// Hide other UI elements
if (ghostTowerSprite) ghostTowerSprite.visible = false;
floatingInfoPanel.visible = false;
upgradePanel.visible = false;
rangeCircle.visible = false;
// Create/update confirmation buttons if needed
if (!confirmButton || confirmButton.towerType !== currentBuildType) {
if (confirmButton) confirmButton.destroy();
if (cancelButton) cancelButton.destroy();
var towerData = Game.TowerData[currentBuildType];
// Create CONFIRM button
confirmButton = game.attachAsset(towerData.sprite_id, {
tint: 0x2ECC71
});
confirmButton.width = 120; // Increased size
confirmButton.height = 120; // Increased size
confirmButton.x = -65; // Adjusted spacing
confirmButton.anchor.set(0.5, 0.5);
confirmButton.towerType = currentBuildType;
confirmButton.down = function () {
var coords = Game.GameplayManager.confirmPlacementCoords;
var buildType = Game.GameplayManager.activeBuildType;
if (coords && buildType) {
Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType);
}
// Reset state
Game.GameplayManager.isConfirmingPlacement = false;
Game.GameplayManager.activeBuildType = null;
};
confirmPanel.addChild(confirmButton);
// Create CANCEL button
cancelButton = game.attachAsset(towerData.sprite_id, {
tint: 0xE74C3C
});
cancelButton.width = 120; // Increased size
cancelButton.height = 120; // Increased size
cancelButton.x = 65; // Adjusted spacing
cancelButton.anchor.set(0.5, 0.5);
cancelButton.towerType = currentBuildType;
cancelButton.down = function () {
// Reset state
Game.GameplayManager.isConfirmingPlacement = false;
Game.GameplayManager.activeBuildType = null;
};
confirmPanel.addChild(cancelButton);
}
confirmPanel.visible = true;
} else if (Game.GameplayManager.selectedTowerId !== -1) {
// --- STATE: TOWER SELECTED ---
// Hide all build-related UI
if (ghostTowerSprite) {
ghostTowerSprite.destroy();
ghostTowerSprite = null;
}
if (confirmButton) {
confirmButton.destroy();
confirmButton = null;
}
if (cancelButton) {
cancelButton.destroy();
cancelButton = null;
}
confirmPanel.visible = false;
floatingInfoPanel.visible = false;
// Show upgrade panel and range circle
var towerId = Game.GameplayManager.selectedTowerId;
var renderComp = Game.EntityManager.componentStores['RenderComponent'] ? Game.EntityManager.componentStores['RenderComponent'][towerId] : null;
var towerComp = Game.EntityManager.componentStores['TowerComponent'] ? Game.EntityManager.componentStores['TowerComponent'][towerId] : null;
var attackComp = Game.EntityManager.componentStores['AttackComponent'] ? Game.EntityManager.componentStores['AttackComponent'][towerId] : null;
var transformComp = Game.EntityManager.componentStores['TransformComponent'] ? Game.EntityManager.componentStores['TransformComponent'][towerId] : null;
if (!renderComp || !towerComp || !transformComp) {
Game.GameplayManager.selectedTowerId = -1;
upgradePanel.visible = false;
rangeCircle.visible = false;
} else {
upgradePanel.visible = true;
if (attackComp && transformComp) {
rangeCircle.visible = true;
rangeCircle.x = transformComp.x;
rangeCircle.y = transformComp.y;
rangeCircle.width = attackComp.range * 2;
rangeCircle.height = attackComp.range * 2;
} else {
rangeCircle.visible = false;
}
var towerType = null;
for (var type in Game.TowerData) {
if (Game.TowerData[type].sprite_id === renderComp.sprite_id) {
towerType = type;
break;
}
}
towerNameText.setText(towerType + " (Lvl " + towerComp.upgrade_level + ")");
var upgradePathAData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathA'] : null;
if (upgradePathAData && towerComp.upgrade_path_A_level < upgradePathAData.length) {
var upgradeInfoA = upgradePathAData[towerComp.upgrade_path_A_level];
upgradeAPathButton.setText("Upgrade Path A ($" + upgradeInfoA.cost + ")");
upgradeAPathButton.visible = true;
} else {
upgradeAPathButton.visible = false;
}
var upgradePathBData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathB'] : null;
if (upgradePathBData && towerComp.upgrade_path_B_level < upgradePathBData.length) {
var upgradeInfoB = upgradePathBData[towerComp.upgrade_path_B_level];
upgradeBPathButton.setText("Upgrade Path B ($" + upgradeInfoB.cost + ")");
upgradeBPathButton.visible = true;
} else {
upgradeBPathButton.visible = false;
}
var sellPrice = towerComp.sell_value;
var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex;
if (towerComp.built_on_wave === currentWave) {
sellPrice = towerComp.cost;
}
sellButton.setText("Sell Tower ($" + sellPrice + ")");
}
} else {
// --- STATE: IDLE ---
// Clean up all dynamic UI
if (ghostTowerSprite) {
ghostTowerSprite.destroy();
ghostTowerSprite = null;
}
if (confirmButton) {
confirmButton.destroy();
confirmButton = null;
}
if (cancelButton) {
cancelButton.destroy();
cancelButton = null;
}
floatingInfoPanel.visible = false;
confirmPanel.visible = false;
upgradePanel.visible = false;
rangeCircle.visible = false;
}
// The `game.up` event (releasing the mouse/finger) will handle the drop logic.
game.up = function () {
if (Game.GameplayManager.activeBuildType && !Game.GameplayManager.isConfirmingPlacement) {
var mouseWorldX = lastMouseX - cameraX;
var mouseWorldY = lastMouseY - cameraY;
var gridX = Math.floor(mouseWorldX / 64);
var gridY = Math.floor(mouseWorldY / 64);
// If dropped on a valid spot, switch to confirmation state
if (Game.GridManager.isBuildable3x3(gridX, gridY)) {
Game.GameplayManager.isConfirmingPlacement = true;
Game.GameplayManager.confirmPlacementCoords = {
x: gridX,
y: gridY
};
confirmPanel.x = gridX * 64 + 32;
confirmPanel.y = gridY * 64 + 32;
} else {
// Dropped on an invalid spot, cancel the build
Game.GameplayManager.activeBuildType = null;
}
}
isDragging = false;
};
// Update standard HUD elements every frame
goldText.setText('Gold: ' + Game.ResourceManager.gold);
livesText.setText('Lives: ' + Game.ResourceManager.lives);
waveText.setText('Wave: ' + (Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1) + ' / ' + Game.WaveData.length);
// Update Power Meter, Minimap, etc.
var p_prod = Game.ResourceManager.powerProduction;
var p_cons = Game.ResourceManager.powerConsumption;
var p_avail = Game.ResourceManager.powerAvailable;
var isDeficit = p_cons > p_prod && p_avail <= 0;
var barScale = 3;
var productionWidth = p_prod * barScale;
powerProductionBar.clear();
powerProductionBar.beginFill(0x5DADE2, 0.6);
powerProductionBar.drawRect(0, 0, productionWidth, 20);
powerProductionBar.endFill();
var consumptionWidth = Math.min(p_cons, p_prod) * barScale;
var consumptionColor = 0x00FF7F;
if (isDeficit) {
consumptionColor = Math.floor(LK.ticks / 10) % 2 === 0 ? 0xFF4136 : 0xD9362D;
} else if (p_prod > 0 && p_cons / p_prod >= 0.75) {
consumptionColor = 0xFFD700;
}
powerConsumptionBar.clear();
powerConsumptionBar.beginFill(consumptionColor);
powerConsumptionBar.drawRect(0, 0, consumptionWidth, 20);
powerConsumptionBar.endFill();
var availableWidth = p_avail * barScale;
powerAvailableBar.x = productionWidth;
powerAvailableBar.clear();
powerAvailableBar.beginFill(0x5DADE2, 0.4);
powerAvailableBar.drawRect(0, 0, availableWidth, 20);
powerAvailableBar.endFill();
updateMinimapDynamic();
updateMinimapViewport();
powerUpdateTimer++;
if (powerUpdateTimer >= 60) {
Game.ResourceManager.updatePower();
if (Game.Systems.WaveSpawnerSystem.isSpawning) {
Game.Systems.EconomySystem.update();
}
powerUpdateTimer = 0;
}
Game.Systems.CleanupSystem.update();
};
// === SECTION: TOWER RANGE INDICATOR UI ===
var rangeCircle = gameWorld.attachAsset('range_circle', {});
gameWorld.addChild(rangeCircle);
rangeCircle.anchor.set(0.5, 0.5); // Set the origin to the center of the asset
rangeCircle.alpha = 0.25;
rangeCircle.visible = false;
// === SECTION: UPGRADE & SELL UI ===
var upgradePanel = new Container();
LK.gui.right.addChild(upgradePanel);
upgradePanel.x = -50; // 50px padding from the right edge
upgradePanel.y = 0;
upgradePanel.visible = false;
var towerNameText = new Text2('Tower Name', {
size: 50,
fill: 0xFFFFFF,
align: 'right' // Align text to the right
});
towerNameText.anchor.set(1, 0.5); // Anchor to the right
upgradePanel.addChild(towerNameText);
var upgradeAPathButton = new Text2('Upgrade Path A', {
size: 45,
fill: 0x27AE60,
align: 'right'
});
upgradeAPathButton.anchor.set(1, 0.5); // Anchor to the right
upgradeAPathButton.y = 70;
upgradePanel.addChild(upgradeAPathButton);
var upgradeBPathButton = new Text2('Upgrade Path B', {
size: 45,
fill: 0x2980B9,
align: 'right'
});
upgradeBPathButton.anchor.set(1, 0.5); // Anchor to the right
upgradeBPathButton.y = 140;
upgradePanel.addChild(upgradeBPathButton);
var sellButton = new Text2('Sell Tower', {
size: 45,
fill: 0xC0392B,
align: 'right'
});
sellButton.anchor.set(1, 0.5); // Anchor to the right
sellButton.y = 210;
upgradePanel.addChild(sellButton);
sellButton.down = function () {
if (Game.GameplayManager.selectedTowerId !== -1) {
Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId);
}
};
upgradeAPathButton.down = function () {
if (Game.GameplayManager.selectedTowerId !== -1) {
Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathA');
}
};
upgradeBPathButton.down = function () {
if (Game.GameplayManager.selectedTowerId !== -1) {
Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathB');
}
};