User prompt
Copy and paste this one-line change prompt to Ava: The new systems are perfect. We just need to activate the CleanupSystem. Please add Game.Systems.CleanupSystem.update(); to the very end of the main game.update function. It should be the last system that runs each frame.
User prompt
With the A* code still stubbed out, the script is simple enough that this logic-only prompt should work. Copy and paste this prompt to Ava: The structure for the CleanupSystem is perfect. Now, please fill in its update method with the logic to actually destroy the entities. Replace the empty update method in Game.Systems.CleanupSystem with this full implementation: update: function() { for (var i = 0; i < Game.EntityManager.entitiesToDestroy.length; i++) { var entityId = Game.EntityManager.entitiesToDestroy[i]; // Remove from active entities list var index = Game.EntityManager.activeEntities.indexOf(entityId); if (index > -1) { Game.EntityManager.activeEntities.splice(index, 1); } // Remove from all component stores for (var componentName in Game.EntityManager.componentStores) { if (Game.EntityManager.componentStores[componentName][entityId]) { delete Game.EntityManager.componentStores[componentName][entityId]; } } // Remove from render system's display objects if (Game.Systems.RenderSystem.displayObjects[entityId]) { Game.Systems.RenderSystem.displayObjects[entityId].destroy(); delete Game.Systems.RenderSystem.displayObjects[entityId]; } } // Important: Clear the list for the next frame if (Game.EntityManager.entitiesToDestroy.length > 0) { Game.EntityManager.entitiesToDestroy = []; } }
User prompt
Great. Now that the code is simpler, let's add the structure for our entity cleanup system. 1. Add to EntityManager: In Game.EntityManager, add the property: entitiesToDestroy: [] In Game.EntityManager, add the method: destroyEntity: function(entityId) { if (this.entitiesToDestroy.indexOf(entityId) === -1) { this.entitiesToDestroy.push(entityId); } } 2. Create the CleanupSystem Shell: In Game.Systems, add a new CleanupSystem with an empty update method: CleanupSystem: { update: function() { // Logic to be added } }3. Update CombatSystem: In the CombatSystem.update method, find all the places where entities were being removed directly (using splice or delete) and replace those lines with a single call to Game.EntityManager.destroyEntity(entityId).
User prompt
The script is getting very large. To make it easier for you to work with, we are going to temporarily replace the A* pathfinding algorithm with a comment. Please find the Game.PathManager.findPath function and replace its entire body with a single comment, so it looks like this: findPath: function findPath(start, end) { // A* algorithm was here. Will be restored later. return null; // Return null for now }
Code edit (1 edits merged)
Please save this source code
User prompt
Please continue with our cleanup system. 1. Add to EntityManager: Add the property entitiesToDestroy: [] Add the method destroyEntity: function(entityId) { if (this.entitiesToDestroy.indexOf(entityId) === -1) { this.entitiesToDestroy.push(entityId); } }
User prompt
The projectile movement is much better. Now, let's create a robust system for cleaning up dead entities. 1. Add to EntityManager: In the Game.EntityManager object, please add the following: A new property: entitiesToDestroy: [] A new method: destroyEntity: function(entityId) { // Avoid adding duplicates if (this.entitiesToDestroy.indexOf(entityId) === -1) { this.entitiesToDestroy.push(entityId); } }
User prompt
The Targeting and Combat systems are incredibly well done. The towers are targeting and firing! We just need to fix a few small bugs to make the projectiles work correctly. Please make the following changes to Game.Systems.CombatSystem: 1. Fix Projectile Movement: Right now, projectiles are "homing" towards the target. We want them to fly in a straight line. In Game.Components, add two new properties to the ProjectileComponent: directionX and directionY. In the CombatSystem, when a tower fires, calculate the initial direction vector to the target just once. Store this normalized vector in the new directionX and directionY properties of the projectile's ProjectileComponent. In the projectile update loop, instead of recalculating the direction to the target every frame, simply move the projectile along its stored directionX and directionY vector.
User prompt
2. Create the TargetingSystem: In Game.Systems, add a new TargetingSystem. Its update method should: Find all entities with an AttackComponent (our towers). For each tower, it must find the best enemy target within its range. The best target is the one closest to the end of the path (highest path_index). Once a target is found, it should store the target's entity ID in the tower's AttackComponent.target_id. If no target is in range, it should set target_id to -1. 3. Create the CombatSystem: In Game.Systems, add a new CombatSystem. Its update method should: Find all entities with an AttackComponent that have a valid target_id (not -1). Check if the tower can fire based on its attack_speed and last_attack_time. If it can fire, it should: Create a new projectile entity. This entity will need a TransformComponent (starting at the tower's position), a RenderComponent (using 'arrow_projectile'), and a MovementComponent. Crucially, the projectile's MovementComponent will not use the main path. Instead, you'll need a new component, maybe ProjectileComponent { target_id: int, speed: float, damage: float }, to tell it how to move towards its target. The CombatSystem will then need logic to move projectiles towards their targets and apply damage on impact. 4. Update the Game Loop: In the main game.update function, add calls to update our new systems. The order is important: Game.Systems.TargetingSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update();
User prompt
Our tower building works perfectly. Now let's make the towers actually attack enemies. We will create two new systems: one for targeting and one for combat. 1. Add a Projectile Asset: In the // === SECTION: ASSETS === block, add a new asset for our tower's projectiles: LK.init.shape('arrow_projectile', {width: 10, height: 4, color: 0xf1c40f, shape: 'box'});
User prompt
The TowerBuildSystem logic is now in place. For the final step, please make the grid cells clickable so they can call this new system. Please modify the drawGrid function. Inside the first for loop that creates the cellSprite, add the click handler logic. The loop should be updated to look like this: // Inside the drawGrid function's first loop... for (var x = 0; x < Game.GridManager.grid.length; x++) { for (var y = 0; y < Game.GridManager.grid[x].length; y++) { var cellSprite = game.attachAsset('grid_cell', {}); cellSprite.x = x * 64; cellSprite.y = y * 64; cellSprite.anchor.set(0, 0); // Add these new lines to make cells interactive cellSprite.gridX = x; // Store grid coordinates on the sprite cellSprite.gridY = y; cellSprite.down = function() { Game.Systems.TowerBuildSystem.tryBuildAt(this.gridX, this.gridY); }; } } //... the rest of the function ...
User prompt
The TowerBuildSystem structure is correct. Now, please fill in the logic for the tryBuildAt method. Replace the empty tryBuildAt function with this complete implementation: tryBuildAt: function(gridX, gridY) { // 1. Check if the location is buildable if (!Game.GridManager.isBuildable(gridX, gridY)) { return; // Exit if not buildable } // 2. Check if the player can afford the tower var towerCost = 50; // We'll use a fixed cost for now if (!Game.ResourceManager.spendGold(towerCost)) { return; // Exit if not enough gold } // 3. If checks pass, create the tower entity var towerId = Game.EntityManager.createEntity(); // 4. Mark the grid cell as occupied by a tower Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.TOWER; // 5. Create and add all necessary components var transform = Game.Components.TransformComponent(gridX * 64, gridY * 64); var render = Game.Components.RenderComponent('arrow_tower', 2, 1.0, true); var tower = Game.Components.TowerComponent(towerCost); var attack = Game.Components.AttackComponent(10, 150, 1.0); // dmg, range, speed var power = Game.Components.PowerConsumptionComponent(2); Game.EntityManager.addComponent(towerId, transform); Game.EntityManager.addComponent(towerId, render); Game.EntityManager.addComponent(towerId, tower); Game.EntityManager.addComponent(towerId, attack); Game.EntityManager.addComponent(towerId, power); // 6. Announce that a tower has been placed var event = Game.Events.TowerPlacedEvent(towerId, towerCost, transform.x, transform.y); Game.EventBus.publish(event); }
User prompt
The TowerBuildSystem structure is correct. Now, please fill in the logic for the tryBuildAt method. Replace the empty tryBuildAt function with this complete implementation: tryBuildAt: function(gridX, gridY) { // 1. Check if the location is buildable if (!Game.GridManager.isBuildable(gridX, gridY)) { return; // Exit if not buildable } // 2. Check if the player can afford the tower var towerCost = 50; // We'll use a fixed cost for now if (!Game.ResourceManager.spendGold(towerCost)) { return; // Exit if not enough gold } // 3. If checks pass, create the tower entity var towerId = Game.EntityManager.createEntity(); // 4. Mark the grid cell as occupied by a tower Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.TOWER; // 5. Create and add all necessary components var transform = Game.Components.TransformComponent(gridX * 64, gridY * 64); var render = Game.Components.RenderComponent('arrow_tower', 2, 1.0, true); var tower = Game.Components.TowerComponent(towerCost); var attack = Game.Components.AttackComponent(10, 150, 1.0); // dmg, range, speed var power = Game.Components.PowerConsumptionComponent(2); Game.EntityManager.addComponent(towerId, transform); Game.EntityManager.addComponent(towerId, render); Game.EntityManager.addComponent(towerId, tower); Game.EntityManager.addComponent(towerId, attack); Game.EntityManager.addComponent(towerId, power); // 6. Announce that a tower has been placed var event = Game.Events.TowerPlacedEvent(towerId, towerCost, transform.x, transform.y); Game.EventBus.publish(event); }
User prompt
Excellent. Now, please add the structure for the TowerBuildSystem. In the Game.Systems object, add a new object named TowerBuildSystem. It should contain one empty method for now. The Game.Systems object should be modified to look like this: Game.Systems = { MovementSystem: { // ... existing MovementSystem code ... }, RenderSystem: { // ... existing RenderSystem code ... }, TowerBuildSystem: { tryBuildAt: function(gridX, gridY) { /* Logic will be added here */ } } };
User prompt
Great. Now please add the visual asset for our first tower. In the // === SECTION: ASSETS === block at the top of the code, add this new line: LK.init.shape('arrow_tower', {width: 50, height: 50, color: 0x2ecc71, shape: 'box'});
User prompt
Please add just one new component type to the Game.Components object. The new component should be: TowerComponent: function(cost, sellValue) { return { name: 'TowerComponent', cost: cost || 50, sell_value: sellValue || 25 }; }
User prompt
The HUD is working, but the platform's pause button is covering it. We need to move the Gold and Lives display to the top-right corner of the screen. Please find the code that creates the goldText and livesText UI elements and modify it as follows: Change the Anchor: For both goldText and livesText, change the anchor from (0, 0) to (1, 0). This will align the text from its right edge. Change the Parent: Add the text objects to LK.gui.topRight instead of LK.gui.topLeft. Adjust the Position: Change their x and y coordinates to add some padding from the corner. The final code for creating the HUD should look like this: // 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
User prompt
The visual grid and moving enemy are working perfectly. It's time to implement the core economic and UI systems. 1. Create New Power-Related Components: In the Game.Components object, please add the following new component types: PowerProductionComponent: function(rate) { return { name: 'PowerProductionComponent', production_rate: rate || 0 }; } PowerConsumptionComponent: function(rate) { return { name: 'PowerConsumptionComponent', drain_rate: rate || 0 }; } 2. Implement the updatePower Logic: Find the Game.ResourceManager and fill in the logic for its empty updatePower() method. This function must: Calculate total power production by finding all entities with a PowerProductionComponent and summing their production_rate values. Calculate total power consumption by finding all entities with a PowerConsumptionComponent and summing their drain_rate values. Compare production and consumption. Publish a PowerStatusChangedEvent with { new_status: 'surplus' } or { new_status: 'deficit' } accordingly. 3. Create the Heads-Up Display (HUD): In the // === SECTION: DIRECT GAME INITIALIZATION === block, create the UI text elements for our resources: var goldText = new Text2('Gold: 100', { size: 40, fill: 0xFFD700 }); var livesText = new Text2('Lives: 20', { size: 40, fill: 0xFF4136 }); Add both text objects to the top-left of the GUI: LK.gui.topLeft.addChild(goldText); and LK.gui.topLeft.addChild(livesText); Position livesText below goldText. 4. Update the HUD and Power System in the Game Loop: Modify the main game.update function. It needs to update the HUD text every frame: goldText.setText('Gold: ' + Game.ResourceManager.gold); and livesText.setText('Lives: ' + Game.ResourceManager.lives); The updatePower() function should only run once per second (i.e., every 60 frames). Add a timer variable to the game loop to manage this.
User prompt
Please fix the bug: 'game.sendToBack is not a function' in or related to this line: 'game.sendToBack(cellSprite); // Send behind everything else' Line Number: 409
User prompt
The game is running and the enemy is visible! The refactor was a success. Now, let's draw the grid and the path so we can see the world our enemy is moving in. 1. Add Grid Assets: At the top of the code, in the // === SECTION: ASSETS === block, please add two more assets: LK.init.shape('grid_cell', {width: 64, height: 64, color: 0x2c3e50}); (a dark blue-gray) LK.init.shape('path_cell', {width: 64, height: 64, color: 0x7f8c8d}); (a lighter gray) 2. Create a drawGrid Function: Add a new helper function somewhere after the Game.Systems object. Generated javascript 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 = game.attachAsset('grid_cell'); cellSprite.x = x * 64; cellSprite.y = y * 64; cellSprite.setAnchor(0, 0); // Anchor to top-left game.sendToBack(cellSprite); // Send behind everything else } } // 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 = game.attachAsset('path_cell'); // Note: PathManager coordinates are already in pixels pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.setAnchor(0, 0); game.sendToBack(pathSprite); } } } 3. Call the drawGrid Function: In the // === SECTION: DIRECT GAME INITIALIZATION === block, add a call to this new function right after all the init() calls.
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'centerOn')' in or related to this line: 'game.camera.centerOn(sprite);' Line Number: 395
User prompt
Our game logic is not running. We need to refactor the code to work more directly with the LK game engine. First, please remove the old state management code. Delete the following objects and functions entirely: The Game.GameStateManager object. The Game.MainMenuState, Game.GameplayState, and Game.PausedState classes. The Game.IGameState class. The gameLoop function at the end of the code. The lines var gameStateManager = Game.GameStateManager;, gameStateManager.push(new Game.MainMenuState());, and game.update = gameLoop;. The code should now end right after the Game.Events object definition.
User prompt
I cannot access the browser's developer console to debug. We need to create a simple, visible, on-screen text display to act as our debug log. 1. Create a Debug Manager: Under the // === SECTION: GLOBAL MANAGERS === comment, create a new object named Game.Debug: Generated javascript Game.Debug = { logText: null, init: function() { 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(message) { if (this.logText) { this.logText.setText(String(message)); } } }; 2. Initialize the Debugger: In the Game.GameplayState's enter() method, add this line at the very top: Game.Debug.init(); 3. Add a Test Log Message: In the gameLoop function, replace the console.log with our new debugger: Game.Debug.log('Game Loop Running');
User prompt
fix the game.debug code to properly display the debug console
Code edit (1 edits merged)
Please save this source code
/**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // === SECTION: GLOBAL NAMESPACE === // === SECTION: ASSETS === var Game = {}; // === SECTION: GLOBAL MANAGERS === Game.ResourceManager = { gold: 100, 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() { // 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; } // 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; } // Update power values this.powerProduction = totalProduction; this.powerConsumption = totalConsumption; this.powerAvailable = totalProduction - totalConsumption; // Publish power status event var newStatus = totalProduction >= totalConsumption ? 'surplus' : 'deficit'; Game.EventBus.publish(Game.Events.PowerStatusChangedEvent(newStatus)); }, 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++) { this.grid[x][y] = this.CELL_STATES.EMPTY; } } }, isBuildable: function isBuildable(x, y) { if (x >= 0 && x < this.grid.length && y >= 0 && y < this.grid[x].length) { return this.grid[x][y] === this.CELL_STATES.EMPTY; } return false; } }; Game.PathManager = { currentPath: [], init: function init() { Game.EventBus.subscribe('TowerPlaced', this.onTowerPlaced.bind(this)); }, onTowerPlaced: function onTowerPlaced(event) { this.recalculatePath({ x: 0, y: 30 }, { x: 39, y: 30 }); }, recalculatePath: function recalculatePath(start, end) { this.currentPath = this.findPath(start, end); if (this.currentPath) { for (var i = 0; i < this.currentPath.length; i++) { this.currentPath[i].x = this.currentPath[i].x * 64; this.currentPath[i].y = this.currentPath[i].y * 64; } } }, findPath: function findPath(start, end) { var openSet = []; var closedSet = []; var grid = Game.GridManager.grid; var gridWidth = grid.length; var gridHeight = grid[0].length; // Helper function to calculate heuristic (Manhattan distance) function heuristic(a, b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); } // Helper function to get neighbors function getNeighbors(node) { var neighbors = []; var directions = [{ x: -1, y: 0, cost: 1 }, { x: 1, y: 0, cost: 1 }, { x: 0, y: -1, cost: 1 }, { x: 0, y: 1, cost: 1 }, { x: -1, y: -1, cost: 1.4 }, { x: 1, y: -1, cost: 1.4 }, { x: -1, y: 1, cost: 1.4 }, { x: 1, y: 1, cost: 1.4 }]; for (var i = 0; i < directions.length; i++) { var dir = directions[i]; var newX = node.x + dir.x; var newY = node.y + dir.y; if (newX >= 0 && newX < gridWidth && newY >= 0 && newY < gridHeight) { var cellState = grid[newX][newY]; if (cellState !== Game.GridManager.CELL_STATES.BLOCKED && cellState !== Game.GridManager.CELL_STATES.TOWER) { neighbors.push({ x: newX, y: newY, cost: dir.cost }); } } } return neighbors; } // Helper function to find node in array function findNodeInArray(array, node) { for (var i = 0; i < array.length; i++) { if (array[i].x === node.x && array[i].y === node.y) { return array[i]; } } return null; } // Helper function to reconstruct path function reconstructPath(cameFrom, current) { var path = []; while (current) { path.unshift({ x: current.x, y: current.y }); current = cameFrom[current.x + '_' + current.y]; } return path; } // Initialize start node var startNode = { x: start.x, y: start.y, g: 0, h: heuristic(start, end), f: 0 }; startNode.f = startNode.g + startNode.h; openSet.push(startNode); var cameFrom = {}; while (openSet.length > 0) { // Find node with lowest f score var current = openSet[0]; var currentIndex = 0; for (var i = 1; i < openSet.length; i++) { if (openSet[i].f < current.f) { current = openSet[i]; currentIndex = i; } } // Remove current from openSet openSet.splice(currentIndex, 1); closedSet.push(current); // Check if we reached the goal if (current.x === end.x && current.y === end.y) { return reconstructPath(cameFrom, current); } // Check neighbors var neighbors = getNeighbors(current); for (var i = 0; i < neighbors.length; i++) { var neighbor = neighbors[i]; // Skip if in closed set if (findNodeInArray(closedSet, neighbor)) { continue; } var tentativeG = current.g + neighbor.cost; var existingNode = findNodeInArray(openSet, neighbor); if (!existingNode) { // New node var newNode = { x: neighbor.x, y: neighbor.y, g: tentativeG, h: heuristic(neighbor, end), f: 0 }; newNode.f = newNode.g + newNode.h; openSet.push(newNode); cameFrom[neighbor.x + '_' + neighbor.y] = current; } else if (tentativeG < existingNode.g) { // Better path found existingNode.g = tentativeG; existingNode.f = existingNode.g + existingNode.h; cameFrom[neighbor.x + '_' + neighbor.y] = current; } } } // No path found return null; } }; 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: {}, 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; } }; 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) { 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 }; }, MovementComponent: function MovementComponent(speed, path_index) { return { name: 'MovementComponent', speed: speed || 50, path_index: path_index || 0 }; }, 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) { return { name: 'TowerComponent', cost: cost || 50, sell_value: sellValue || 25 }; }, ProjectileComponent: function ProjectileComponent(target_id, speed, damage) { return { name: 'ProjectileComponent', target_id: target_id || -1, speed: speed || 200, damage: damage || 10 }; } }; Game.Systems = { MovementSystem: { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); for (var i = 0; i < movableEntities.length; i++) { var entityId = movableEntities[i]; var transformComponent = Game.EntityManager.componentStores['TransformComponent'][entityId]; var movementComponent = Game.EntityManager.componentStores['MovementComponent'][entityId]; if (Game.PathManager.currentPath && Game.PathManager.currentPath.length > 0) { var targetWaypoint = Game.PathManager.currentPath[movementComponent.path_index]; if (targetWaypoint) { var dx = targetWaypoint.x - transformComponent.x; var dy = targetWaypoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 1) { var moveX = dx / distance * movementComponent.speed; var moveY = dy / distance * movementComponent.speed; transformComponent.x += moveX; transformComponent.y += moveY; } else { movementComponent.path_index++; if (movementComponent.path_index >= Game.PathManager.currentPath.length) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } } } } } }, 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]; // Get the visual sprite, or create it if it doesn't exist var sprite = this.displayObjects[entityId]; if (!sprite) { sprite = game.attachAsset(render.sprite_id, {}); this.displayObjects[entityId] = sprite; } // Sync the sprite's properties with the component data sprite.x = transform.x; sprite.y = transform.y; sprite.visible = render.is_visible; } } }, TowerBuildSystem: { tryBuildAt: function tryBuildAt(gridX, gridY) { // 1. Check if the location is buildable if (!Game.GridManager.isBuildable(gridX, gridY)) { return; // Exit if not buildable } // 2. Check if the player can afford the tower var towerCost = 50; // We'll use a fixed cost for now if (!Game.ResourceManager.spendGold(towerCost)) { return; // Exit if not enough gold } // 3. If checks pass, create the tower entity var towerId = Game.EntityManager.createEntity(); // 4. Mark the grid cell as occupied by a tower Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.TOWER; // 5. Create and add all necessary components var transform = Game.Components.TransformComponent(gridX * 64, gridY * 64); var render = Game.Components.RenderComponent('arrow_tower', 2, 1.0, true); var tower = Game.Components.TowerComponent(towerCost); var attack = Game.Components.AttackComponent(10, 150, 1.0); // dmg, range, speed var power = Game.Components.PowerConsumptionComponent(2); Game.EntityManager.addComponent(towerId, transform); Game.EntityManager.addComponent(towerId, render); Game.EntityManager.addComponent(towerId, tower); Game.EntityManager.addComponent(towerId, attack); Game.EntityManager.addComponent(towerId, power); // 6. Announce that a tower has been placed var event = Game.Events.TowerPlacedEvent(towerId, towerCost, transform.x, transform.y); Game.EventBus.publish(event); } }, TargetingSystem: { update: function 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]; // Find best target (closest to end of path) 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 enemyMovement = Game.EntityManager.componentStores['MovementComponent'][enemyId]; // Check if enemy is within range 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 this enemy is closer to the end if (enemyMovement.path_index > bestPathIndex) { bestTarget = enemyId; bestPathIndex = enemyMovement.path_index; } } } // Update tower's target towerAttack.target_id = bestTarget; } } }, CombatSystem: { update: function update() { var currentTime = LK.ticks / 60; // Convert ticks to seconds 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]; // Check if tower has a valid target and can fire if (towerAttack.target_id !== -1 && currentTime - towerAttack.last_attack_time >= 1.0 / towerAttack.attack_speed) { // Create projectile var projectileId = Game.EntityManager.createEntity(); var projectileTransform = Game.Components.TransformComponent(towerTransform.x, towerTransform.y); var projectileRender = Game.Components.RenderComponent('arrow_projectile', 1, 1.0, true); var projectileComponent = Game.Components.ProjectileComponent(towerAttack.target_id, 200, towerAttack.damage); Game.EntityManager.addComponent(projectileId, projectileTransform); Game.EntityManager.addComponent(projectileId, projectileRender); Game.EntityManager.addComponent(projectileId, projectileComponent); // Update tower's last attack time towerAttack.last_attack_time = currentTime; } } // Move projectiles and check for impacts 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]; // Check if target still exists var targetExists = false; for (var j = 0; j < Game.EntityManager.activeEntities.length; j++) { if (Game.EntityManager.activeEntities[j] === projectileComp.target_id) { targetExists = true; break; } } if (!targetExists) { // Remove projectile if target no longer exists Game.EntityManager.activeEntities.splice(Game.EntityManager.activeEntities.indexOf(projectileId), 1); if (Game.Systems.RenderSystem.displayObjects[projectileId]) { Game.Systems.RenderSystem.displayObjects[projectileId].destroy(); delete Game.Systems.RenderSystem.displayObjects[projectileId]; } continue; } // Move projectile towards target var targetTransform = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id]; if (targetTransform) { var dx = targetTransform.x - projectileTransform.x; var dy = targetTransform.y - projectileTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 5) { // Hit target - apply damage and remove projectile var targetHealth = Game.EntityManager.componentStores['HealthComponent'][projectileComp.target_id]; if (targetHealth) { targetHealth.current_hp -= projectileComp.damage; if (targetHealth.current_hp <= 0) { // Enemy killed Game.EventBus.publish(Game.Events.EnemyKilledEvent(projectileComp.target_id, 10)); // Remove enemy Game.EntityManager.activeEntities.splice(Game.EntityManager.activeEntities.indexOf(projectileComp.target_id), 1); if (Game.Systems.RenderSystem.displayObjects[projectileComp.target_id]) { Game.Systems.RenderSystem.displayObjects[projectileComp.target_id].destroy(); delete Game.Systems.RenderSystem.displayObjects[projectileComp.target_id]; } } } // Remove projectile Game.EntityManager.activeEntities.splice(Game.EntityManager.activeEntities.indexOf(projectileId), 1); if (Game.Systems.RenderSystem.displayObjects[projectileId]) { Game.Systems.RenderSystem.displayObjects[projectileId].destroy(); delete Game.Systems.RenderSystem.displayObjects[projectileId]; } } else { // Move projectile var moveX = dx / distance * projectileComp.speed; var moveY = dy / distance * projectileComp.speed; projectileTransform.x += moveX; projectileTransform.y += moveY; } } } } } }; // === SECTION: GRID DRAWING === 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 = game.attachAsset('grid_cell', {}); cellSprite.x = x * 64; cellSprite.y = y * 64; cellSprite.anchor.set(0, 0); // Anchor to top-left // Add these new lines to make cells interactive cellSprite.gridX = x; // Store grid coordinates on the sprite cellSprite.gridY = y; cellSprite.down = function () { Game.Systems.TowerBuildSystem.tryBuildAt(this.gridX, this.gridY); }; } } // 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 = game.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.ResourceManager.init(); Game.PathManager.init(); Game.PathManager.recalculatePath({ x: 0, y: 30 }, { x: 39, y: 30 }); 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 // 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); var render = Game.Components.RenderComponent('enemy', 1, 1.0, true); var health = Game.Components.HealthComponent(30, 30); Game.EntityManager.addComponent(enemyId, transform); Game.EntityManager.addComponent(enemyId, movement); Game.EntityManager.addComponent(enemyId, render); Game.EntityManager.addComponent(enemyId, health); // Set up main game loop var powerUpdateTimer = 0; game.update = function () { Game.Systems.TargetingSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); // Update HUD every frame goldText.setText('Gold: ' + Game.ResourceManager.gold); livesText.setText('Lives: ' + Game.ResourceManager.lives); // Update power system once per second (every 60 frames) powerUpdateTimer++; if (powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); powerUpdateTimer = 0; } };
===================================================================
--- original.js
+++ change.js
@@ -376,8 +376,16 @@
name: 'TowerComponent',
cost: cost || 50,
sell_value: sellValue || 25
};
+ },
+ ProjectileComponent: function ProjectileComponent(target_id, speed, damage) {
+ return {
+ name: 'ProjectileComponent',
+ target_id: target_id || -1,
+ speed: speed || 200,
+ damage: damage || 10
+ };
}
};
Game.Systems = {
MovementSystem: {
@@ -459,8 +467,124 @@
// 6. Announce that a tower has been placed
var event = Game.Events.TowerPlacedEvent(towerId, towerCost, transform.x, transform.y);
Game.EventBus.publish(event);
}
+ },
+ TargetingSystem: {
+ update: function 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];
+ // Find best target (closest to end of path)
+ 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 enemyMovement = Game.EntityManager.componentStores['MovementComponent'][enemyId];
+ // Check if enemy is within range
+ 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 this enemy is closer to the end
+ if (enemyMovement.path_index > bestPathIndex) {
+ bestTarget = enemyId;
+ bestPathIndex = enemyMovement.path_index;
+ }
+ }
+ }
+ // Update tower's target
+ towerAttack.target_id = bestTarget;
+ }
+ }
+ },
+ CombatSystem: {
+ update: function update() {
+ var currentTime = LK.ticks / 60; // Convert ticks to seconds
+ 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];
+ // Check if tower has a valid target and can fire
+ if (towerAttack.target_id !== -1 && currentTime - towerAttack.last_attack_time >= 1.0 / towerAttack.attack_speed) {
+ // Create projectile
+ var projectileId = Game.EntityManager.createEntity();
+ var projectileTransform = Game.Components.TransformComponent(towerTransform.x, towerTransform.y);
+ var projectileRender = Game.Components.RenderComponent('arrow_projectile', 1, 1.0, true);
+ var projectileComponent = Game.Components.ProjectileComponent(towerAttack.target_id, 200, towerAttack.damage);
+ Game.EntityManager.addComponent(projectileId, projectileTransform);
+ Game.EntityManager.addComponent(projectileId, projectileRender);
+ Game.EntityManager.addComponent(projectileId, projectileComponent);
+ // Update tower's last attack time
+ towerAttack.last_attack_time = currentTime;
+ }
+ }
+ // Move projectiles and check for impacts
+ 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];
+ // Check if target still exists
+ var targetExists = false;
+ for (var j = 0; j < Game.EntityManager.activeEntities.length; j++) {
+ if (Game.EntityManager.activeEntities[j] === projectileComp.target_id) {
+ targetExists = true;
+ break;
+ }
+ }
+ if (!targetExists) {
+ // Remove projectile if target no longer exists
+ Game.EntityManager.activeEntities.splice(Game.EntityManager.activeEntities.indexOf(projectileId), 1);
+ if (Game.Systems.RenderSystem.displayObjects[projectileId]) {
+ Game.Systems.RenderSystem.displayObjects[projectileId].destroy();
+ delete Game.Systems.RenderSystem.displayObjects[projectileId];
+ }
+ continue;
+ }
+ // Move projectile towards target
+ var targetTransform = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id];
+ if (targetTransform) {
+ var dx = targetTransform.x - projectileTransform.x;
+ var dy = targetTransform.y - projectileTransform.y;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance < 5) {
+ // Hit target - apply damage and remove projectile
+ var targetHealth = Game.EntityManager.componentStores['HealthComponent'][projectileComp.target_id];
+ if (targetHealth) {
+ targetHealth.current_hp -= projectileComp.damage;
+ if (targetHealth.current_hp <= 0) {
+ // Enemy killed
+ Game.EventBus.publish(Game.Events.EnemyKilledEvent(projectileComp.target_id, 10));
+ // Remove enemy
+ Game.EntityManager.activeEntities.splice(Game.EntityManager.activeEntities.indexOf(projectileComp.target_id), 1);
+ if (Game.Systems.RenderSystem.displayObjects[projectileComp.target_id]) {
+ Game.Systems.RenderSystem.displayObjects[projectileComp.target_id].destroy();
+ delete Game.Systems.RenderSystem.displayObjects[projectileComp.target_id];
+ }
+ }
+ }
+ // Remove projectile
+ Game.EntityManager.activeEntities.splice(Game.EntityManager.activeEntities.indexOf(projectileId), 1);
+ if (Game.Systems.RenderSystem.displayObjects[projectileId]) {
+ Game.Systems.RenderSystem.displayObjects[projectileId].destroy();
+ delete Game.Systems.RenderSystem.displayObjects[projectileId];
+ }
+ } else {
+ // Move projectile
+ var moveX = dx / distance * projectileComp.speed;
+ var moveY = dy / distance * projectileComp.speed;
+ projectileTransform.x += moveX;
+ projectileTransform.y += moveY;
+ }
+ }
+ }
+ }
}
};
// === SECTION: GRID DRAWING ===
function drawGrid() {
@@ -599,14 +723,18 @@
var enemyId = Game.EntityManager.createEntity();
var transform = Game.Components.TransformComponent(0 * 64, 30 * 64);
var movement = Game.Components.MovementComponent(2, 0);
var render = Game.Components.RenderComponent('enemy', 1, 1.0, true);
+var health = Game.Components.HealthComponent(30, 30);
Game.EntityManager.addComponent(enemyId, transform);
Game.EntityManager.addComponent(enemyId, movement);
Game.EntityManager.addComponent(enemyId, render);
+Game.EntityManager.addComponent(enemyId, health);
// Set up main game loop
var powerUpdateTimer = 0;
game.update = function () {
+ Game.Systems.TargetingSystem.update();
+ Game.Systems.CombatSystem.update();
Game.Systems.MovementSystem.update();
Game.Systems.RenderSystem.update();
// Update HUD every frame
goldText.setText('Gold: ' + Game.ResourceManager.gold);