User prompt
Please replace this block of code: 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 with this corrected version: goldText.anchor.set(1, 0); // Align to the right UILayer.addChild(goldText); goldText.x = LK.width - 20; // Position from right edge goldText.y = 20; // Position from top edge
User prompt
Please add the following code block to the very top of your script, right after the var game = new LK.Game(...) block. // === SECTION: UI LAYER === var UILayer = new Container(); LK.stage.addChild(UILayer);
User prompt
Please replace the game.move function that Ava implemented with the following updated version. This version includes the clamping logic. game.move = function (x, y, obj) { if (cameraControl.startX !== undefined && cameraControl.startY !== undefined) { // Calculate distance from start position var deltaX = x - cameraControl.startX; var deltaY = y - cameraControl.startY; var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // If we've moved beyond the threshold, start dragging if (distance > cameraControl.dragThreshold) { cameraControl.isDragging = true; } // If we're dragging, update camera position if (cameraControl.isDragging) { var moveX = x - cameraControl.lastX; var moveY = y - cameraControl.lastY; // Update game container position (this pans the camera) game.x += moveX; game.y += moveY; // --- START: NEW CLAMPING LOGIC --- var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); // --- END: NEW CLAMPING LOGIC --- // Update last position cameraControl.lastX = x; cameraControl.lastY = y; } } };
User prompt
Ava, I need to implement a "tap-and-drag" camera control system for a 2D map. The map itself is a container called game which holds all the world objects. The user should be able to press down on the map, drag to pan the camera (by changing the x and y of the game container), and release to stop panning. A press and release without dragging should be treated as a "tap" for later use. Please provide a complete, minimal code example demonstrating the correct, idiomatic way to implement this tap-and-drag functionality in the LK engine. The example should include setting up the necessary interactive objects and all required event handlers (down, move, and up).
User prompt
Please add the following code block to the very end of your script, after the last }. // === SECTION: STAGE-LEVEL DIAGNOSTIC INPUT (PANNING ONLY) === var isPanning = false; var lastPanPos = null; var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; // Make the absolute root stage interactive. LK.stage.interactive = true; LK.stage.down = function(e) { // Ignore clicks that land on a GUI element. if (e.target && e.target.isGui) { return; } isPanning = true; lastPanPos = { x: e.x, y: e.y }; }; LK.stage.move = function(e) { if (isPanning) { var currentPos = { x: e.x, y: e.y }; var dx = currentPos.x - lastPanPos.x; var dy = currentPos.y - lastPanPos.y; // Move the 'game' container based on the drag. game.x += dx; game.y += dy; // Clamp camera position. var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); lastPanPos = currentPos; } }; LK.stage.up = function(e) { isPanning = false; lastPanPos = null; };
User prompt
Please replace the entire function drawGrid() { ... } function with this simplified version: 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); // NOTE: We are deliberately NOT making the cells interactive for this test. } } // 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', {}); pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } }
User prompt
Please replace the entire // === SECTION: UNIFIED INPUT HANDLING === section (and all the code within it from the last failed attempt) with the following new, simplified block: // === SECTION: DIAGNOSTIC INPUT TEST (PANNING ONLY) === var isPanning = false; var lastPanPos = null; var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; game.interactive = true; game.down = function(e) { // Ignore clicks on UI elements if (e.target && e.target.isGui) { return; } isPanning = true; lastPanPos = { x: e.x, y: e.y }; }; game.move = function(e) { if (isPanning) { var currentPos = { x: e.x, y: e.y }; var dx = currentPos.x - lastPanPos.x; var dy = currentPos.y - lastPanPos.y; game.x += dx; game.y += dy; // Clamp camera position var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); lastPanPos = currentPos; } }; game.up = function(e) { isPanning = false; lastPanPos = null; };
User prompt
In the // === SECTION: DIRECT GAME INITIALIZATION === section, please find the line that says drawGrid(); and add the following code block immediately after it: // --- DIAGNOSTIC STEP: Disable interactivity on all grid cells --- for (var i = 0; i < game.children.length; i++) { var child = game.children[i]; // Identify grid cells by their asset ID and make them non-interactive. if (child.assetId === 'grid_cell') { child.interactive = false; } }
User prompt
Now, with the old code gone, we will add the final, correct implementation. This single block contains everything needed for both grid drawing and the input logic that handles both tapping and panning. Please replace the // Input and Grid Drawing sections temporarily removed for clean replacement. line you just added with the following complete code block: // === SECTION: INPUT AND CAMERA STATE === var isPointerDown = false; var isDragging = false; var lastPointerPos = null; // === 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); cellSprite.gridX = x; cellSprite.gridY = y; // No event handlers here; they are all managed by the 'game' container. } } // 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', {}); pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } } // === SECTION: UNIFIED INPUT HANDLING === var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; // Use the 'game' container itself as the listener for all input. // This is the correct target as it contains all game world objects. game.interactive = true; game.down = function(e) { // Ignore any clicks that land on a GUI element. if (e.target && e.target.isGui) { return; } isPointerDown = true; isDragging = false; lastPointerPos = { x: e.x, y: e.y }; }; game.move = function(e) { if (!isPointerDown) { return; } var currentPos = { x: e.x, y: e.y }; var dx = currentPos.x - lastPointerPos.x; var dy = currentPos.y - lastPointerPos.y; if (!isDragging) { // If we move more than 5 pixels, it's a drag. if (Math.sqrt(dx*dx + dy*dy) > 5) { isDragging = true; Game.GameplayManager.selectedTowerId = -1; // Deselect when drag starts } } if (isDragging) { // Pan the camera by moving the 'game' container. game.x += dx; game.y += dy; // Clamp camera position to map boundaries. var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); } lastPointerPos = currentPos; }; game.up = function(e) { // If the pointer was down but we didn't drag, it was a "tap". if (isPointerDown && !isDragging) { // Convert the tap's screen coordinates to world coordinates. var worldX = e.x - game.x; var worldY = e.y - game.y; var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); if (gridX >= 0 && gridX < 40 && gridY >= 0 && gridY < 60) { var cellState = Game.GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { var allTowers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = 0; i < allTowers.length; i++) { var towerId = allTowers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (Math.floor(transform.x / 64) === gridX && Math.floor(transform.y / 64) === gridY) { foundTowerId = towerId; break; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); Game.GameplayManager.selectedTowerId = -1; } else { Game.GameplayManager.selectedTowerId = -1; } } else { Game.GameplayManager.selectedTowerId = -1; } } // Reset all state flags. isPointerDown = false; isDragging = false; lastPointerPos = null; };
User prompt
Please replace the entire block of code starting from // === SECTION: GRID DRAWING === down to the end of the // === SECTION: CAMERA CONTROLS === section with this single line: // Input and Grid Drawing sections temporarily removed for clean replacement.
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot read properties of undefined (reading 'isGui')' in or related to this line: 'if (e.target !== LK.stage && e.target.isGui) {' Line Number: 1817
User prompt
Now, with a clean slate, we will add the one, true, correct input handling section. This code will live in one place and manage everything. Please replace the // === SECTION: CAMERA CONTROLS === (REMOVED) line with this new, final block of code: // === SECTION: UNIFIED INPUT HANDLING === var isPointerDown = false; var isDragging = false; var lastPos = null; var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; // Use the main stage for all events to guarantee they are always captured. LK.stage.interactive = true; LK.stage.down = function(e) { // If we clicked on a UI element, ignore the press. if (e.target !== LK.stage && e.target.isGui) { return; } isPointerDown = true; isDragging = false; lastPos = { x: e.x, y: e.y }; }; LK.stage.move = function(e) { if (isPointerDown) { var currentPos = { x: e.x, y: e.y }; var dx = currentPos.x - lastPos.x; var dy = currentPos.y - lastPos.y; // If not dragging yet, check if the drag threshold has been met. if (!isDragging) { if (Math.sqrt(dx*dx + dy*dy) > 5) { isDragging = true; Game.GameplayManager.selectedTowerId = -1; // Deselect tower when pan starts } } // If we are dragging, pan the camera. if (isDragging) { game.x += dx; game.y += dy; // Clamp camera position. var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); } lastPos = currentPos; } }; LK.stage.up = function(e) { // If the pointer was down but we didn't drag, it was a "tap". if (isPointerDown && !isDragging) { // Convert screen coordinates to world coordinates. var worldX = e.x - game.x; var worldY = e.y - game.y; var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); if (gridX >= 0 && gridX < 40 && gridY >= 0 && gridY < 60) { var cellState = Game.GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { var allTowers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = 0; i < allTowers.length; i++) { var towerId = allTowers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (Math.floor(transform.x / 64) === gridX && Math.floor(transform.y / 64) === gridY) { foundTowerId = towerId; break; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); Game.GameplayManager.selectedTowerId = -1; } else { Game.GameplayManager.selectedTowerId = -1; } } else { Game.GameplayManager.selectedTowerId = -1; } } // Reset all flags. isPointerDown = false; isDragging = false; lastPos = null; };
User prompt
Please replace the entire function drawGrid() { ... } function with the following clean version that has no event handlers: 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); cellSprite.gridX = x; cellSprite.gridY = y; // All input handlers are now managed globally. } } // 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', {}); pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } }
User prompt
Now, we will create the global handlers on the game object that will do all the real work. Please replace the // === SECTION: CAMERA CONTROLS === (REMOVED) line with the following new, fully functional code block: // === SECTION: CAMERA CONTROLS === var isReadyToDrag = false; // Flag to know if a press started on a grid cell var isDragging = false; // Flag to know if movement exceeded the tap threshold var lastDragPos = null; var tappedCell = null; // Reference to the specific cell that was pressed var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; // Attach global listeners to the 'game' object. game.interactive = true; game.up = function(e) { // If a press happened on a cell but we didn't drag, it's a "tap". if (isReadyToDrag && !isDragging) { var gridX = tappedCell.gridX; var gridY = tappedCell.gridY; var cellState = Game.GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { // Logic to SELECT a tower var allTowers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = 0; i < allTowers.length; i++) { var towerId = allTowers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (Math.floor(transform.x / 64) === gridX && Math.floor(transform.y / 64) === gridY) { foundTowerId = towerId; break; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { // Logic to BUILD a tower Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); Game.GameplayManager.selectedTowerId = -1; } else { Game.GameplayManager.selectedTowerId = -1; } } // Reset all states on mouse up, regardless. isReadyToDrag = false; isDragging = false; lastDragPos = null; tappedCell = null; }; game.move = function(e) { if (isReadyToDrag) { var currentPos = { x: e.x, y: e.y }; var dx = currentPos.x - lastDragPos.x; var dy = currentPos.y - lastDragPos.y; // If we haven't started dragging yet, check the threshold. if (!isDragging && Math.sqrt(dx*dx + dy*dy) > 5) { isDragging = true; Game.GameplayManager.selectedTowerId = -1; // Deselect tower when pan starts } if (isDragging) { game.x += dx; game.y += dy; // Clamp camera position var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); lastDragPos = currentPos; } } };
User prompt
First, we need to replace the drawGrid function to add the simple down handler to each grid cell, as instructed. Please replace the entire function drawGrid() { ... } function with the following corrected version: 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); cellSprite.gridX = x; cellSprite.gridY = y; // Per Ava's instructions, this ONLY initiates the drag/tap state. cellSprite.down = function(e) { isDragging = false; // It's not a drag yet. isReadyToDrag = true; // But it might become one. lastDragPos = { x: e.x, y: e.y }; // Store the cell that was clicked to handle the "tap" action later. tappedCell = this; }; } } // Highlight the path cells (no change here) 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', {}); pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } }
User prompt
Next, we will put brand new, corrected logic into the drawGrid function. This new code will handle both tapping and dragging properly, all within the grid cell's event handlers. Please replace the entire function drawGrid() { ... } function with this new, comprehensive version: function drawGrid() { // These variables will be shared by all grid cell handlers var isDragging = false; var lastDragPos = null; var hasDragged = false; // A flag to distinguish a drag from a tap // 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); cellSprite.gridX = x; cellSprite.gridY = y; cellSprite.down = function(e) { isDragging = true; hasDragged = false; // Reset drag flag on new press lastDragPos = { x: e.x, y: e.y }; }; cellSprite.move = function(e) { if (isDragging) { var currentPos = { x: e.x, y: e.y }; var deltaX = currentPos.x - lastDragPos.x; var deltaY = currentPos.y - lastDragPos.y; // If we move more than a few pixels, it's officially a drag if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 5) { hasDragged = true; } if(hasDragged) { // Pan the camera (the 'game' container) game.x += deltaX; game.y += deltaY; // Clamp camera position var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); lastDragPos = currentPos; } } }; cellSprite.up = function() { // If the mouse was released WITHOUT significant movement, it's a tap. if (isDragging && !hasDragged) { var gridX = this.gridX; var gridY = this.gridY; var cellState = Game.GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { // Logic to SELECT a tower var allTowers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = 0; i < allTowers.length; i++) { var towerId = allTowers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var towerGridX = Math.floor(transform.x / 64); var towerGridY = Math.floor(transform.y / 64); if (towerGridX === gridX && towerGridY === gridY) { foundTowerId = towerId; break; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { // Logic to BUILD a tower Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); Game.GameplayManager.selectedTowerId = -1; } else { Game.GameplayManager.selectedTowerId = -1; } } // Reset all flags isDragging = false; lastDragPos = null; hasDragged = false; }; } } // Highlight the path cells (no change here) 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', {}); pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } }
User prompt
First, we must completely remove the non-functional camera control code. Please replace the entire // === SECTION: CAMERA CONTROLS === section with the following single line to clear it: // === SECTION: CAMERA CONTROLS === (REMOVED)
User prompt
Please replace the // === SECTION: CAMERA CONTROLS === (Temporarily Stubbed) line with the following complete and corrected code block: // === SECTION: CAMERA CONTROLS === var isDragging = false; var lastDragPos = null; var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; // Attach listeners to the main stage, which does NOT move. LK.stage.interactive = true; LK.stage.down = function(e) { // Record the starting position of any potential drag using the correct event properties. lastDragPos = { x: e.x, y: e.y }; }; LK.stage.up = function(e) { // If we were not dragging, this was a tap. if (!isDragging) { // Convert screen coordinates to world coordinates by subtracting the camera's offset. var worldX = e.x - game.x; var worldY = e.y - game.y; var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); // Check if the tap was within the grid bounds if (gridX >= 0 && gridX < 40 && gridY >= 0 && gridY < 60) { var cellState = Game.GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { // Logic to SELECT a tower var allTowers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = 0; i < allTowers.length; i++) { var towerId = allTowers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var towerGridX = Math.floor(transform.x / 64); var towerGridY = Math.floor(transform.y / 64); if (towerGridX === gridX && towerGridY === gridY) { foundTowerId = towerId; break; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { // Logic to BUILD a tower Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); Game.GameplayManager.selectedTowerId = -1; // Deselect after building } else { // Clicked on a path or blocked cell, so DESELECT Game.GameplayManager.selectedTowerId = -1; } } else { // Clicked outside the grid, so DESELECT Game.GameplayManager.selectedTowerId = -1; } } // Reset dragging state regardless isDragging = false; lastDragPos = null; }; LK.stage.move = function(e) { if (lastDragPos) { // If a 'down' event has occurred var currentPos = { x: e.x, y: e.y }; var dx = currentPos.x - lastDragPos.x; var dy = currentPos.y - lastDragPos.y; // If it's not a drag yet, check if it should become one if (!isDragging) { // Check if mouse has moved more than a small threshold if (Math.sqrt(dx*dx + dy*dy) > 5) { isDragging = true; } } // If it is a drag, pan the camera (the 'game' container) if (isDragging) { game.x += dx; game.y += dy; // Clamp camera position var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); // Update last drag position for the next frame lastDragPos = { x: currentPos.x, y: currentPos.y }; } } };
User prompt
Please replace the entire code block starting from // === SECTION: CAMERA CONTROLS === to the end of the LK.stage.move function with the following single line: // === SECTION: CAMERA CONTROLS === (Temporarily Stubbed)
User prompt
Please replace the entire // === SECTION: CAMERA CONTROLS === section with the following corrected and finalized code. This version attaches the listeners to the correct, non-moving LK.stage object. // === SECTION: CAMERA CONTROLS === var isDragging = false; var lastDragPos = null; var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; // Attach listeners to the main stage, which does NOT move. This is the critical fix. LK.stage.interactive = true; LK.stage.down = function(e) { // Record the starting position of any potential drag. lastDragPos = { x: e.data.global.x, y: e.data.global.y }; }; LK.stage.up = function(e) { // If we were not dragging, this was a tap. if (!isDragging) { // Convert screen coordinates to world coordinates by subtracting the camera's offset. var worldX = e.data.global.x - game.x; var worldY = e.data.global.y - game.y; var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); // Check if the tap was within the grid bounds if (gridX >= 0 && gridX < 40 && gridY >= 0 && gridY < 60) { var cellState = Game.GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { // Logic to SELECT a tower var allTowers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = 0; i < allTowers.length; i++) { var towerId = allTowers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var towerGridX = Math.floor(transform.x / 64); var towerGridY = Math.floor(transform.y / 64); if (towerGridX === gridX && towerGridY === gridY) { foundTowerId = towerId; break; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { // Logic to BUILD a tower Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); Game.GameplayManager.selectedTowerId = -1; // Deselect after building } else { // Clicked on a path or blocked cell, so DESELECT Game.GameplayManager.selectedTowerId = -1; } } else { // Clicked outside the grid, so DESELECT Game.GameplayManager.selectedTowerId = -1; } } // Reset dragging state regardless isDragging = false; lastDragPos = null; }; LK.stage.move = function(e) { if (lastDragPos) { // If a 'down' event has occurred var currentPos = e.data.global; var dx = currentPos.x - lastDragPos.x; var dy = currentPos.y - lastDragPos.y; // If it's not a drag yet, check if it should become one if (!isDragging) { // Check if mouse has moved more than a small threshold if (Math.sqrt(dx*dx + dy*dy) > 5) { isDragging = true; } } // If it is a drag, pan the camera (the 'game' container) if (isDragging) { game.x += dx; game.y += dy; // Clamp camera position var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); // Update last drag position for the next frame lastDragPos = { x: currentPos.x, y: currentPos.y }; } } };
User prompt
Please replace the entire game.up = function() { ... }; block in the // === SECTION: CAMERA CONTROLS === with this new, more intelligent version: game.up = function(e) { // If we were not dragging, this was a tap. if (!isDragging) { // We need to figure out which grid cell was tapped. // Convert screen coordinates to world coordinates by subtracting the camera's offset. var worldX = e.x - game.x; var worldY = e.y - game.y; var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); // Check if the tap was within the grid bounds if (gridX >= 0 && gridX < 40 && gridY >= 0 && gridY < 60) { var cellState = Game.GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { // Logic to SELECT a tower var allTowers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = 0; i < allTowers.length; i++) { var towerId = allTowers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var towerGridX = Math.floor(transform.x / 64); var towerGridY = Math.floor(transform.y / 64); if (towerGridX === gridX && towerGridY === gridY) { foundTowerId = towerId; break; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { // Logic to BUILD a tower Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); Game.GameplayManager.selectedTowerId = -1; // Deselect after building } else { // Clicked on a path or blocked cell, so DESELECT Game.GameplayManager.selectedTowerId = -1; } } else { // Clicked outside the grid, so DESELECT Game.GameplayManager.selectedTowerId = -1; } } // Reset dragging state regardless isDragging = false; lastDragPos = null; };
User prompt
Please replace the entire (currently stubbed) function drawGrid() { ... } block with the following complete and corrected version: 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 cellSprite.gridX = x; // Store grid coordinates on the sprite cellSprite.gridY = y; // This handler is now simplified. The logic to differentiate // a tap from a drag will be handled on the 'up' event. cellSprite.down = function (e) { // We simply let the main 'up' handler decide what to do. }; } } // 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); } } }
User prompt
Please replace the entire function drawGrid() { ... } block with the following stubbed version: function drawGrid() { // Function temporarily stubbed to allow for a targeted replacement. }
User prompt
let's change the game.move handler. It will now be responsible for setting isDragging = true but only after the mouse has moved a small amount. Please replace the entire game.move = function(e) { ... }; block with this new version: game.move = function(e) { if (lastDragPos) { // If a 'down' event has occurred var dx = e.x - lastDragPos.x; var dy = e.y - lastDragPos.y; // If it's not a drag yet, check if it should become one if (!isDragging) { // Check if mouse has moved more than a small threshold if (Math.sqrt(dx*dx + dy*dy) > 5) { isDragging = true; } } // If it is a drag, pan the camera if (isDragging) { game.x += dx; game.y += dy; // Clamp camera position var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); // Update last drag position for the next frame lastDragPos = { x: e.x, y: e.y }; } } };
User prompt
First, let's modify the game.down handler to record the initial position but not immediately set isDragging to true. Please replace the entire game.down = function(e) { ... }; block in the // === SECTION: CAMERA CONTROLS === with this new version: game.down = function(e) { // We don't set isDragging to true yet. // We just record the starting position of any potential drag. lastDragPos = { x: e.x, y: e.y }; };
/**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // === SECTION: UI LAYER === var UILayer = new Container(); LK.stage.addChild(UILayer); // === SECTION: GLOBAL NAMESPACE === // === SECTION: CAMERA CONTROL === var cameraControl = { isDragging: false, lastX: 0, lastY: 0, startX: 0, startY: 0, dragThreshold: 10 // Minimum distance to consider it a drag vs tap }; // === SECTION: ASSETS === var Game = {}; // === 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++) { 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 = { 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 if (nx >= 0 && nx < width && ny >= 0 && ny < height && grid[nx][ny] !== 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, 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 = game.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) { // 1. Look up tower data and check if location is buildable var towerData = Game.TowerData[towerType]; if (!towerData || !Game.GridManager.isBuildable(gridX, gridY)) { return; } // 2. Temporarily spend the gold. We'll refund it if the build is invalid. if (!Game.ResourceManager.spendGold(towerData.cost)) { return; // Exit if not enough gold } // 3. Temporarily "place" the tower on the grid for path validation Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.TOWER; // 4. Regenerate the flow field and check if a valid path exists Game.PathManager.generateFlowField(); // Check if there's a valid path by verifying the distance field at the spawn point var spawnDistance = Game.PathManager.distanceField[0][30]; if (spawnDistance === Infinity) { // INVALID PLACEMENT: No path exists from spawn to goal // Revert the grid cell to its original state Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.EMPTY; // Refund the player's gold Game.ResourceManager.addGold(towerData.cost); // (Optional: Play an error sound/show a message in the future) return; // Stop the build process } // --- If we reach here, the placement is VALID --- var towerId = Game.EntityManager.createEntity(); // --- UPDATED POSITIONING LOGIC --- // Place the tower in the CENTER of the grid cell. var centeredX = gridX * 64 + 32; var centeredY = gridY * 64 + 32; var transform = Game.Components.TransformComponent(centeredX, centeredY); // --- END UPDATED LOGIC --- Game.EntityManager.addComponent(towerId, transform); var render = Game.Components.RenderComponent(towerData.sprite_id, 2, 1.0, true); Game.EntityManager.addComponent(towerId, render); // 6. Add all necessary components, now including the current wave index for (var i = 0; i < towerData.components.length; i++) { var componentDef = towerData.components[i]; var component; // If we're creating the TowerComponent, add the current wave index 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); } // 7. Announce that a tower has been successfully placed 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; } // --- START OF NEW REFUND LOGIC --- // 1. Determine the correct refund amount var refundAmount = towerComp.sell_value; // Default to partial refund var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; // If the tower was built during the current build phase, give a full refund if (towerComp.built_on_wave === currentWave) { refundAmount = towerComp.cost; } // 2. Refund the calculated amount Game.ResourceManager.addGold(refundAmount); // --- END OF NEW REFUND LOGIC --- // 3. Free up the grid cell var gridX = Math.floor(transformComp.x / 64); var gridY = Math.floor(transformComp.y / 64); if (Game.GridManager.grid[gridX] && Game.GridManager.grid[gridX][gridY] !== undefined) { Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.EMPTY; } // 4. Publish a sell event Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount)); // 5. Deselect the tower and schedule it for destruction 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', components: [{ name: 'TowerComponent', args: [50, 25, undefined] }, // cost { name: 'AttackComponent', args: [10, 150, 1.0, 0, 'arrow_projectile'] }, // dmg, range, speed, last_attack_time, projectile_id { name: 'PowerConsumptionComponent', args: [2] } // drain_rate ] }, 'GeneratorTower': { cost: 75, sprite_id: 'generator_tower', components: [{ name: 'TowerComponent', args: [75, 37, undefined] }, // cost { name: 'PowerProductionComponent', args: [5] } // production_rate ] }, 'CapacitorTower': { cost: 40, sprite_id: 'capacitor_tower', components: [{ name: 'TowerComponent', args: [40, 20, undefined] }, // cost { name: 'PowerStorageComponent', args: [20] } // capacity ] }, 'RockLauncher': { cost: 125, sprite_id: 'rock_launcher_tower', components: [{ name: 'TowerComponent', args: [125, 62, undefined] }, { name: 'AttackComponent', args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50] }, // dmg, range, speed, last_attack_time, projectile_id, target_id, splash_radius { name: 'PowerConsumptionComponent', args: [10] }] }, 'ChemicalTower': { cost: 100, sprite_id: 'chemical_tower', components: [{ name: 'TowerComponent', args: [100, 50, undefined] }, { name: 'AttackComponent', args: [5, 150, 0.8, 0, 'chemical_projectile', -1, 0] }, // dmg, range, speed, last_attack_time, projectile_id, target_id, splash_radius { name: 'PowerConsumptionComponent', args: [8] }] }, 'SlowTower': { cost: 80, sprite_id: 'slow_tower', components: [{ name: 'TowerComponent', args: [80, 40, undefined] }, { name: 'AttackComponent', args: [2, 160, 1.0, 0, 'slow_projectile'] // Low damage, good range, standard speed }, { name: 'PowerConsumptionComponent', args: [4] }] }, 'DarkTower': { cost: 150, sprite_id: 'dark_tower', components: [{ name: 'TowerComponent', args: [150, 75, undefined] }, { name: 'AuraComponent', args: [175, 'damage_buff'] // range, effect_type }, { name: 'PowerConsumptionComponent', args: [12] }] }, // --- NEW TOWER --- 'GraveyardTower': { cost: 200, sprite_id: 'graveyard_tower', components: [{ name: 'TowerComponent', args: [200, 100, undefined] }, { name: 'GoldGenerationComponent', args: [1.5] // gold_per_second }, { name: 'PowerConsumptionComponent', args: [15] // High power cost for economic advantage }] } }; // === 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 === 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 cellSprite.gridX = x; // Store grid coordinates on the sprite cellSprite.gridY = y; // Click handling is now managed by the camera control system } } // 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.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 UILayer.addChild(goldText); goldText.x = LK.width - 20; // Position from right edge goldText.y = 20; // Position from 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: 50, 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 === var buildMode = 'ArrowTower'; // Keep this variable var arrowButton = new Text2('Build: Arrow', { size: 40, fill: 0x2ecc71 }); arrowButton.anchor.set(0, 0.5); LK.gui.left.addChild(arrowButton); arrowButton.x = 20; arrowButton.y = -350; arrowButton.down = function () { buildMode = 'ArrowTower'; }; var generatorButton = new Text2('Build: Generator', { size: 40, fill: 0x3498db }); generatorButton.anchor.set(0, 0.5); LK.gui.left.addChild(generatorButton); generatorButton.x = 20; generatorButton.y = -280; generatorButton.down = function () { buildMode = 'GeneratorTower'; }; var capacitorButton = new Text2('Build: Capacitor', { size: 40, fill: 0xf1c40f }); capacitorButton.anchor.set(0, 0.5); LK.gui.left.addChild(capacitorButton); capacitorButton.x = 20; capacitorButton.y = -210; capacitorButton.down = function () { buildMode = 'CapacitorTower'; }; var rockLauncherButton = new Text2('Build: Rock Launcher', { size: 40, fill: 0x8D6E63 }); rockLauncherButton.anchor.set(0, 0.5); LK.gui.left.addChild(rockLauncherButton); rockLauncherButton.x = 20; rockLauncherButton.y = -140; rockLauncherButton.down = function () { buildMode = 'RockLauncher'; }; var chemicalTowerButton = new Text2('Build: Chemical', { size: 40, fill: 0xb87333 }); chemicalTowerButton.anchor.set(0, 0.5); LK.gui.left.addChild(chemicalTowerButton); chemicalTowerButton.x = 20; chemicalTowerButton.y = -70; chemicalTowerButton.down = function () { buildMode = 'ChemicalTower'; }; var slowTowerButton = new Text2('Build: Slow', { size: 40, fill: 0x5DADE2 }); slowTowerButton.anchor.set(0, 0.5); LK.gui.left.addChild(slowTowerButton); slowTowerButton.x = 20; slowTowerButton.y = 0; slowTowerButton.down = function () { buildMode = 'SlowTower'; }; var darkTowerButton = new Text2('Build: Dark', { size: 40, fill: 0x9b59b6 }); darkTowerButton.anchor.set(0, 0.5); LK.gui.left.addChild(darkTowerButton); darkTowerButton.x = 20; darkTowerButton.y = 70; darkTowerButton.down = function () { buildMode = 'DarkTower'; }; // --- NEW BUTTON --- var graveyardTowerButton = new Text2('Build: Graveyard', { size: 40, fill: 0x607d8b }); graveyardTowerButton.anchor.set(0, 0.5); LK.gui.left.addChild(graveyardTowerButton); graveyardTowerButton.x = 20; graveyardTowerButton.y = 140; graveyardTowerButton.down = function () { buildMode = 'GraveyardTower'; }; // === 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); 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(); } } // === SECTION: CAMERA CONTROL HANDLERS === game.down = function (x, y, obj) { // Store initial touch/click position cameraControl.startX = x; cameraControl.startY = y; cameraControl.lastX = x; cameraControl.lastY = y; cameraControl.isDragging = false; }; game.move = function (x, y, obj) { if (cameraControl.startX !== undefined && cameraControl.startY !== undefined) { // Calculate distance from start position var deltaX = x - cameraControl.startX; var deltaY = y - cameraControl.startY; var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); // If we've moved beyond the threshold, start dragging if (distance > cameraControl.dragThreshold) { cameraControl.isDragging = true; } // If we're dragging, update camera position if (cameraControl.isDragging) { var moveX = x - cameraControl.lastX; var moveY = y - cameraControl.lastY; // Update game container position (this pans the camera) game.x += moveX; game.y += moveY; // --- START: NEW CLAMPING LOGIC --- var MAP_WIDTH_PIXELS = 40 * 64; var MAP_HEIGHT_PIXELS = 60 * 64; var minX = LK.width - MAP_WIDTH_PIXELS; var minY = LK.height - MAP_HEIGHT_PIXELS; game.x = Math.max(minX, Math.min(0, game.x)); game.y = Math.max(minY, Math.min(0, game.y)); // --- END: NEW CLAMPING LOGIC --- // Update last position cameraControl.lastX = x; cameraControl.lastY = y; } } }; game.up = function (x, y, obj) { // If we weren't dragging, treat this as a tap if (!cameraControl.isDragging) { // Convert screen coordinates to game world coordinates var gameX = x - game.x; var gameY = y - game.y; // Convert to grid coordinates var gridX = Math.floor(gameX / 64); var gridY = Math.floor(gameY / 64); // Only process tap if it's within valid grid bounds if (gridX >= 0 && gridX < 40 && gridY >= 0 && gridY < 60) { var cellState = Game.GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { // Logic to SELECT a tower var allTowers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = 0; i < allTowers.length; i++) { var towerId = allTowers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var towerGridX = Math.floor(transform.x / 64); var towerGridY = Math.floor(transform.y / 64); if (towerGridX === gridX && towerGridY === gridY) { foundTowerId = towerId; break; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { // Logic to BUILD a tower Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); Game.GameplayManager.selectedTowerId = -1; // Deselect after building } else { // Clicked on a path or blocked cell, so DESELECT Game.GameplayManager.selectedTowerId = -1; } } } // Reset drag state cameraControl.isDragging = false; cameraControl.startX = undefined; cameraControl.startY = undefined; cameraControl.lastX = 0; cameraControl.lastY = 0; }; // Set up main game loop var powerUpdateTimer = 0; game.update = function () { if (Game.GameManager.isGameOver) { return; } 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 UPDATE LOGIC --- if (Game.GameplayManager.selectedTowerId !== -1) { var towerId = Game.GameplayManager.selectedTowerId; // Validate that the tower still exists and has required components 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 tower doesn't exist anymore, deselect it if (!renderComp || !towerComp || !transformComp) { Game.GameplayManager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; // --- START OF UPDATED RANGE INDICATOR LOGIC --- if (attackComp && transformComp) { rangeCircle.visible = true; // Position the circle's center directly on the tower's center rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } else { rangeCircle.visible = false; } // --- END OF UPDATED RANGE INDICATOR LOGIC --- 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 { upgradePanel.visible = false; rangeCircle.visible = false; // Hide indicator when no tower is selected } // Update HUD 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 UI var p_prod = Game.ResourceManager.powerProduction; var p_cons = Game.ResourceManager.powerConsumption; var p_avail = Game.ResourceManager.powerAvailable; // Determine the grid's status for coloring var isDeficit = p_cons > p_prod && p_avail <= 0; var barScale = 3; // 1 unit of power = 3 pixels of bar width // --- 1. Update Production Bar (The blue background) --- // Using a brighter cyan-blue with good visibility var productionWidth = p_prod * barScale; powerProductionBar.clear(); powerProductionBar.beginFill(0x5DADE2, 0.6); // Brighter cyan-blue, slightly more opaque powerProductionBar.drawRect(0, 0, productionWidth, 20); powerProductionBar.endFill(); // --- 2. Update Consumption Bar (The colored foreground) --- var consumptionWidth = Math.min(p_cons, p_prod) * barScale; var consumptionColor = 0x00FF7F; // Default: Bright Spring Green if (isDeficit) { // Flashing Vibrant Red for a deficit state consumptionColor = Math.floor(LK.ticks / 10) % 2 === 0 ? 0xFF4136 : 0xD9362D; } else if (p_prod > 0 && p_cons / p_prod >= 0.75) { consumptionColor = 0xFFD700; // Bright Gold for warning } powerConsumptionBar.clear(); powerConsumptionBar.beginFill(consumptionColor); // Now fully opaque powerConsumptionBar.drawRect(0, 0, consumptionWidth, 20); powerConsumptionBar.endFill(); // --- 3. Update Available Buffer Bar (Extends beyond the main bar) --- var availableWidth = p_avail * barScale; powerAvailableBar.x = productionWidth; // Position it right after the production bar powerAvailableBar.clear(); powerAvailableBar.beginFill(0x5DADE2, 0.4); // Same cyan-blue, but more transparent for distinction powerAvailableBar.drawRect(0, 0, availableWidth, 20); powerAvailableBar.endFill(); updateMinimapDynamic(); // Update timed systems once per second (every 60 frames) 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 = game.attachAsset('range_circle', {}); game.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); // Attaching to the right side upgradePanel.x = -350; // Positioned in from the right edge upgradePanel.y = 0; // Centered vertically upgradePanel.visible = false; // Initially hidden var towerNameText = new Text2('Tower Name', { size: 40, fill: 0xFFFFFF }); towerNameText.anchor.set(0, 0); upgradePanel.addChild(towerNameText); var upgradeAPathButton = new Text2('Upgrade Path A', { size: 35, fill: 0x27AE60 }); upgradeAPathButton.anchor.set(0, 0); upgradeAPathButton.y = 50; upgradePanel.addChild(upgradeAPathButton); var upgradeBPathButton = new Text2('Upgrade Path B', { size: 35, fill: 0x2980B9 }); upgradeBPathButton.anchor.set(0, 0); upgradeBPathButton.y = 100; upgradePanel.addChild(upgradeBPathButton); var sellButton = new Text2('Sell Tower', { size: 35, fill: 0xC0392B }); sellButton.anchor.set(0, 0); sellButton.y = 150; upgradePanel.addChild(sellButton); sellButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId); } }; // Assign click handlers that will call the TowerSystem 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'); } };
===================================================================
--- original.js
+++ change.js
@@ -1529,11 +1529,11 @@
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
+UILayer.addChild(goldText);
+goldText.x = LK.width - 20; // Position from right edge
+goldText.y = 20; // Position from top edge
var livesText = new Text2('Lives: 20', {
size: 40,
fill: 0xFF4136
});