User prompt
At the very top of the Game Code section, please replace the block of camera state variables: // Camera state var cameraX = 0; var cameraY = 0; var isDragging = false; var dragStartX = 0; var dragStartY = 0; var cameraStartX = 0; var cameraStartY = 0; var lastMouseX = 0; var lastMouseY = 0; var mouseDownTime = 0; with this new, cleaner set of variables: // Camera state var cameraX = 0; var cameraY = 0; var isPointerDown = false; // Tracks if the mouse button is currently held down var isDragging = false; // Becomes true only after the mouse moves past a threshold var dragStartX = 0; var dragStartY = 0; var cameraStartX = 0; var cameraStartY = 0;
User prompt
In the // === SECTION: GRID DRAWING === section, please replace the entire cellSprite.down function with the following corrected version. This new code adds the necessary check to see if the mouse has moved significantly before processing the click as a build or select action. cellSprite.down = function () { // Get world coordinates from the click event, accounting for camera pan var worldX = (LK.input.mouse.x - cameraX); var worldY = (LK.input.mouse.y - cameraY); // Convert world coordinates to grid coordinates var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); // --- START OF NEW CLICK VS. DRAG LOGIC --- // Check if the mouse has moved significantly since the initial mousedown. // This prevents building a tower after panning. var dx = Math.abs(LK.input.mouse.x - dragStartX); var dy = Math.abs(LK.input.mouse.y - dragStartY); var DRAG_THRESHOLD = 10; // A 10-pixel tolerance for a "click" if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) { // The mouse moved too much, so this was a drag. Do nothing. return; } // --- END OF NEW CLICK VS. DRAG LOGIC --- // If we reach here, it was a valid click/tap. 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; } };
User prompt
Task: Implement a Camera Panning and Interaction System Please modify the provided game code to implement a complete camera control system with the following behaviors: Camera Panning: When the player presses the mouse button down on the game world (not on a UI button) and drags the cursor, the game map should move smoothly with the cursor, creating a "pulling" or "dragging" effect. The map should stop moving when the mouse button is released. Click vs. Drag Distinction: A quick press and release of the mouse button in roughly the same spot (a "tap" or "click") should not cause the camera to pan. This action should be reserved for interacting with the game world. Map Boundary Clamping: The camera should not be able to pan beyond the edges of the defined 40x60 game world. The player should never see empty black space outside the game map. The view must be constrained to always show some part of the game grid. Coordinate-Independent Interaction: Building a new tower or selecting an existing tower must work correctly regardless of how far the camera has been panned. A click on a grid cell that appears in the top-left corner of the screen must register on that correct grid cell in the world, even if the camera is panned all the way to the bottom-right of the map. Please implement all necessary logic to achieve this. Expected Outcome After your changes, the game should behave as follows: Clicking and dragging the mouse on the game grid will pan the camera smoothly across the map. The camera will stop moving when it hits the edge of the 40x60 map. A simple, quick click on an empty grid cell will build a tower at that location, regardless of the camera's position. A simple, quick click on an existing tower will select it and show the upgrade/sell panel, regardless of the camera's position.
User prompt
Task: Implement a Camera Panning and Interaction System Please modify the provided game code to implement a complete camera control system with the following behaviors: Camera Panning: When the player presses the mouse button down on the game world (not on a UI button) and drags the cursor, the game map should move smoothly with the cursor, creating a "pulling" or "dragging" effect. The map should stop moving when the mouse button is released. Click vs. Drag Distinction: A quick press and release of the mouse button in roughly the same spot (a "tap" or "click") should not cause the camera to pan. This action should be reserved for interacting with the game world. Map Boundary Clamping: The camera should not be able to pan beyond the edges of the defined 40x60 game world. The player should never see empty black space outside the game map. The view must be constrained to always show some part of the game grid. Coordinate-Independent Interaction: Building a new tower or selecting an existing tower must work correctly regardless of how far the camera has been panned. A click on a grid cell that appears in the top-left corner of the screen must register on that correct grid cell in the world, even if the camera is panned all the way to the bottom-right of the map. Please implement all necessary logic to achieve this. Expected Outcome After your changes, the game should behave as follows: Clicking and dragging the mouse on the game grid will pan the camera smoothly across the map. The camera will stop moving when it hits the edge of the 40x60 map. A simple, quick click on an empty grid cell will build a tower at that location, regardless of the camera's position. A simple, quick click on an existing tower will select it and show the upgrade/sell panel, regardless of the camera's position. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
And replace the Game.GameplayManager.init function with this version that includes the subscription: init: function init() { Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('EnemyReachedEnd', this.onEnemyReachedEnd.bind(this)); Game.EventBus.subscribe('WorldSpaceTap', this.onWorldSpaceTap.bind(this)); },
User prompt
With the old click handlers gone, we need to make the GameplayManager listen for our new WorldSpaceTapEvent. In the Game.GameplayManager object, please add this new method: onWorldSpaceTap: function onWorldSpaceTap(event) { var gridX = Math.floor(event.x / 64); var gridY = Math.floor(event.y / 64); if (gridX < 0 || gridX >= Game.GridManager.grid.length || gridY < 0 || gridY >= Game.GridManager.grid[0].length) { this.selectedTowerId = -1; return; } 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]; var towerGridX = Math.floor(transform.x / 64); var towerGridY = Math.floor(transform.y / 64); if (towerGridX === gridX && towerGridY === gridY) { foundTowerId = towerId; break; } } this.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); this.selectedTowerId = -1; } else { this.selectedTowerId = -1; } },
User prompt
Now that the input and camera state are correct, we need to make the RenderSystem actually use the camera's offset. Please replace the entire Game.Systems.RenderSystem.update function with the following. update: function update() { // Get the current camera offset from the single source of truth. var offset = Game.CameraManager.getOffset(); // Apply an inverse transformation to the main game stage. // This moves all game objects but leaves the GUI layer unaffected. game.stage.x = -offset.x; game.stage.y = -offset.y; // The existing render loop for entities remains the same. 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, {}); sprite.anchor.set(0.5, 0.5); this.displayObjects[entityId] = sprite; } sprite.x = transform.x; sprite.y = transform.y; sprite.visible = render.is_visible; } }
User prompt
Finally, let's fix the initialization. In the // === SECTION: DIRECT GAME INITIALIZATION === block, please replace these two lines: Game.CameraManager.init(null); Game.Systems.InputSystem.init(null); with these corrected lines: // The LK.Game object contains the 'view' which is the HTML Canvas element. var canvas = game.view; Game.CameraManager.init(canvas); Game.Systems.InputSystem.init(canvas);
User prompt
Now, in your Game.CameraManager object, please replace the pan function with this corrected version. This ensures that when the camera "pans," it moves in the opposite direction of the drag, creating the correct "pulling" effect. pan: function pan(delta) { var newOffset = { x: this.offset.x - delta.dx, // To "pull" left (negative delta), the camera offset moves left (subtracts) y: this.offset.y - delta.dy }; this.setOffset(newOffset); },
User prompt
First, please replace the entire Game.Systems.InputSystem object with this corrected version. This new version fixes the inverted panning logic. InputSystem: { canvas: null, currentState: 'IDLE', // IDLE, POINTER_DOWN, DRAGGING isPointerDown: false, dragStartPoint: { x: 0, y: 0 }, lastPointerPosition: { x: 0, y: 0 }, DRAG_THRESHOLD: 10, // Min pixels to move before it's a drag init: function(canvasElement) { this.canvas = canvasElement; if (this.canvas && this.canvas.addEventListener) { // Use bind to maintain 'this' context within event handlers this.canvas.addEventListener('mousedown', this.onPointerDown.bind(this)); this.canvas.addEventListener('mousemove', this.onPointerMove.bind(this)); this.canvas.addEventListener('mouseup', this.onPointerUp.bind(this)); this.canvas.addEventListener('mouseleave', this.onPointerUp.bind(this)); // Treat leaving as up // Touch events for mobile support this.canvas.addEventListener('touchstart', this.onPointerDown.bind(this)); this.canvas.addEventListener('touchmove', this.onPointerMove.bind(this)); this.canvas.addEventListener('touchend', this.onPointerUp.bind(this)); } }, getPointerCoordinates: function(event) { if (event.touches && event.touches.length > 0) { return { x: event.touches[0].clientX, y: event.touches[0].clientY }; } return { x: event.clientX, y: event.clientY }; }, onPointerDown: function(event) { event.preventDefault(); this.isPointerDown = true; this.currentState = 'POINTER_DOWN'; const coords = this.getPointerCoordinates(event); this.dragStartPoint = { ...coords }; this.lastPointerPosition = { ...coords }; }, onPointerMove: function(event) { if (!this.isPointerDown) return; event.preventDefault(); const currentPos = this.getPointerCoordinates(event); if (this.currentState === 'POINTER_DOWN') { const dx = currentPos.x - this.dragStartPoint.x; const dy = currentPos.y - this.dragStartPoint.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > this.DRAG_THRESHOLD) { this.currentState = 'DRAGGING'; } } if (this.currentState === 'DRAGGING') { const delta = { dx: currentPos.x - this.lastPointerPosition.x, dy: currentPos.y - this.lastPointerPosition.y }; // THIS IS THE FIX: We now directly call pan with the UNMODIFIED delta. // The CameraManager will handle the direction. Game.CameraManager.pan(delta); } this.lastPointerPosition = { ...currentPos }; }, onPointerUp: function(event) { if (!this.isPointerDown) return; event.preventDefault(); if (this.currentState === 'POINTER_DOWN') { // This was a tap/click const worldPos = this.screenToWorld(this.dragStartPoint); Game.EventBus.publish(Game.Events.WorldSpaceTapEvent(worldPos.x, worldPos.y)); } this.isPointerDown = false; this.currentState = 'IDLE'; }, screenToWorld: function(screenPos) { const offset = Game.CameraManager.getOffset(); return { x: screenPos.x + offset.x, y: screenPos.y + offset.y }; } },
User prompt
Next, in the Game.GameplayManager.init function, add this line to subscribe to the new event: Game.EventBus.subscribe('WorldSpaceTap', this.onWorldSpaceTap.bind(this));
User prompt
First, add this new method to the Game.GameplayManager object: onWorldSpaceTap: function onWorldSpaceTap(event) { var gridX = Math.floor(event.x / 64); var gridY = Math.floor(event.y / 64); // Check if the tap is within the grid bounds if (gridX < 0 || gridX >= Game.GridManager.grid.length || gridY < 0 || gridY >= Game.GridManager.grid[0].length) { this.selectedTowerId = -1; // Tapped outside the map, deselect return; } 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; } } this.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { // Logic to BUILD a tower Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); this.selectedTowerId = -1; // Deselect after attempting to build } else { // Clicked on a path or blocked cell, so DESELECT this.selectedTowerId = -1; } },
User prompt
Please replace the entire Game.Systems.RenderSystem.update function with the following new version. This new code wraps the existing drawing logic with the necessary transformations. update: function update() { // --- START OF NEW CAMERA LOGIC --- // 1. Get the current camera offset from the single source of truth. var offset = Game.CameraManager.getOffset(); // 2. Save the current state of the renderer. This is crucial. game.stage.x = -offset.x; game.stage.y = -offset.y; // --- END OF NEW CAMERA LOGIC --- // The existing render loop for entities remains the same. // The global stage transformation handles their final position on the screen. 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, {}); sprite.anchor.set(0.5, 0.5); this.displayObjects[entityId] = sprite; } sprite.x = transform.x; sprite.y = transform.y; sprite.visible = render.is_visible; } }
User prompt
Please fix the bug: 'Cannot read properties of null (reading 'addEventListener')' in or related to this line: 'if (this.canvas) {' Line Number: 688
User prompt
Please fix the bug: 'Cannot read properties of null (reading 'addEventListener')' in or related to this line: 'this.canvas.addEventListener('mousedown', this.onPointerDown.bind(this));' Line Number: 688
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'view')' in or related to this line: 'Game.Systems.InputSystem.init(game.app.view);' Line Number: 1878
User prompt
Finally, in the // === SECTION: DIRECT GAME INITIALIZATION === section, find the line Game.CameraManager.init(game.app.view); and add the following line immediately after it: Game.Systems.InputSystem.init(game.app.view);
User prompt
In the // === SECTION: GRID DRAWING === section, please replace the entire drawGrid function with the following version. This new version removes the .down handler, as that logic will now be handled by our new InputSystem. 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 // NOTE: The .down handler has been removed from here. } } // 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
In the // === SECTION: SYSTEMS === section of your code, please add the following new system. A good place for it is right before the Game.Systems.MovementSystem. InputSystem: { canvas: null, currentState: 'IDLE', // IDLE, POINTER_DOWN, DRAGGING isPointerDown: false, dragStartPoint: { x: 0, y: 0 }, lastPointerPosition: { x: 0, y: 0 }, DRAG_THRESHOLD: 10, // Min pixels to move before it's a drag init: function(canvasElement) { this.canvas = canvasElement; // Use bind to maintain 'this' context within event handlers this.canvas.addEventListener('mousedown', this.onPointerDown.bind(this)); this.canvas.addEventListener('mousemove', this.onPointerMove.bind(this)); this.canvas.addEventListener('mouseup', this.onPointerUp.bind(this)); this.canvas.addEventListener('mouseleave', this.onPointerUp.bind(this)); // Treat leaving canvas as mouse up // Touch events for mobile support this.canvas.addEventListener('touchstart', this.onPointerDown.bind(this)); this.canvas.addEventListener('touchmove', this.onPointerMove.bind(this)); this.canvas.addEventListener('touchend', this.onPointerUp.bind(this)); }, getPointerCoordinates: function(event) { if (event.touches && event.touches.length > 0) { // Use the first touch point for simplicity return { x: event.touches[0].clientX, y: event.touches[0].clientY }; } return { x: event.clientX, y: event.clientY }; }, onPointerDown: function(event) { event.preventDefault(); this.isPointerDown = true; this.currentState = 'POINTER_DOWN'; const coords = this.getPointerCoordinates(event); this.dragStartPoint = { ...coords }; this.lastPointerPosition = { ...coords }; }, onPointerMove: function(event) { if (!this.isPointerDown) return; event.preventDefault(); const currentPos = this.getPointerCoordinates(event); if (this.currentState === 'POINTER_DOWN') { const dx = currentPos.x - this.dragStartPoint.x; const dy = currentPos.y - this.dragStartPoint.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > this.DRAG_THRESHOLD) { this.currentState = 'DRAGGING'; } } if (this.currentState === 'DRAGGING') { const delta = { dx: currentPos.x - this.lastPointerPosition.x, dy: currentPos.y - this.lastPointerPosition.y }; // To create the "pulling the map" effect, we pan in the OPPOSITE direction of the drag. const invertedDelta = { dx: -delta.dx, dy: -delta.dy }; Game.CameraManager.pan(invertedDelta); } this.lastPointerPosition = { ...currentPos }; }, onPointerUp: function(event) { if (!this.isPointerDown) return; event.preventDefault(); if (this.currentState === 'POINTER_DOWN') { // This was a tap/click const worldPos = this.screenToWorld(this.dragStartPoint); Game.EventBus.publish(Game.Events.WorldSpaceTapEvent(worldPos.x, worldPos.y)); } // Reset state this.isPointerDown = false; this.currentState = 'IDLE'; }, screenToWorld: function(screenPos) { const offset = Game.CameraManager.getOffset(); return { x: screenPos.x + offset.x, y: screenPos.y + offset.y }; } },
User prompt
In the Game.Events object, please add the following new event definition. It can go anywhere inside the object. WorldSpaceTapEvent: function WorldSpaceTapEvent(worldX, worldY) { return { type: 'WorldSpaceTap', x: worldX, y: worldY }; },
User prompt
Please fix the bug: 'window.addEventListener is not a function' in or related to this line: 'window.addEventListener('resize', this.updateViewportSize.bind(this));' Line Number: 338
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'view')' in or related to this line: 'Game.CameraManager.init(game.app.view);' Line Number: 1742
User prompt
In the // === SECTION: DIRECT GAME INITIALIZATION === section, find the line Game.GameManager.init(); and add the following line immediately after it. We assume your canvas has the ID game-canvas. Game.CameraManager.init(game.app.view);
User prompt
In the // === SECTION: GLOBAL MANAGERS === section of your code, please add the following new code block. A good place for it is right after the Game.GameManager object. Game.CameraManager = { offset: { x: 0, y: 0 }, worldBounds: { width: 2560, height: 3840 }, // From GDD 3.4.1: 40x60 grid * 64px tiles viewportSize: { width: 0, height: 0 }, canvas: null, /** * Initializes the CameraManager. * @param {HTMLCanvasElement} canvasElement - The main game canvas. */ init: function(canvasElement) { this.canvas = canvasElement; this.updateViewportSize(); // Listen for browser window resize to keep viewport size accurate. window.addEventListener('resize', this.updateViewportSize.bind(this)); }, /** * Updates the viewport size based on the canvas element's current dimensions. */ updateViewportSize: function() { if (this.canvas) { this.viewportSize.width = this.canvas.clientWidth; this.viewportSize.height = this.canvas.clientHeight; } }, /** * Sets the camera's offset, applying boundary clamping. * @param {{x: number, y: number}} newOffset - The desired new offset. */ setOffset: function(newOffset) { // Calculate the maximum allowed top-left position for the camera const maxOffsetX = this.worldBounds.width - this.viewportSize.width; const maxOffsetY = this.worldBounds.height - this.viewportSize.height; // Ensure the calculated maximums are not negative (if viewport > world) const effectiveMaxX = Math.max(0, maxOffsetX); const effectiveMaxY = Math.max(0, maxOffsetY); // Use Math.max and Math.min to clamp the offset within the valid range. const clampedX = Math.max(0, Math.min(newOffset.x, effectiveMaxX)); const clampedY = Math.max(0, Math.min(newOffset.y, effectiveMaxY)); this.offset = { x: clampedX, y: clampedY }; }, /** * Gets the current camera offset. * @returns {{x: number, y: number}} The current offset. */ getOffset: function() { return this.offset; }, /** * Pans the camera by a given delta, creating the "pulling" effect. * @param {{dx: number, dy: number}} delta - The change in pointer position. */ pan: function(delta) { const newOffset = { x: this.offset.x + delta.dx, // Pan right, camera moves right (world moves left) y: this.offset.y + delta.dy }; this.setOffset(newOffset); } };
User prompt
Please replace this block of code: 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 with this corrected version: livesText.anchor.set(1, 0); // Align to the right UILayer.addChild(livesText); livesText.x = LK.width - 20; // Position from right edge livesText.y = 70; // Position below the gold text Prompt 3 of 6: Fix the Wave Counter Display Please replace this block of code: Generated javascript 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 with this corrected version: Generated javascript waveText.anchor.set(1, 0); // Align to the right UILayer.addChild(waveText); waveText.x = LK.width - 20; // Position from right edge waveText.y = 120; // Position below the lives text Prompt 4 of 6: Fix the Power Meter Please replace this line of code: LK.gui.topRight.addChild(powerMeterContainer); with this corrected version: UILayer.addChild(powerMeterContainer); and also replace this line: powerMeterContainer.x = -320; // Positioned in from the right edge with this one: powerMeterContainer.x = LK.width - 320; // Position from right edge Prompt 5 of 6: Fix the Start Wave Button Please replace this block of code: startWaveButton.anchor.set(0.5, 1); LK.gui.bottom.addChild(startWaveButton); startWaveButton.y = -20; startWaveButton.anchor.set(0.5, 1); UILayer.addChild(startWaveButton); startWaveButton.x = LK.width / 2; // Centered horizontally startWaveButton.y = LK.height - 20; // Positioned up from the bottom edge Prompt 6 of 6: Fix the Build Menu, Upgrade Panel, and Minimap Finally, we need to fix the remaining UI containers. We can do this in one prompt. Please perform the following three replacements: Replace LK.gui.left.addChild(arrowButton); with UILayer.addChild(arrowButton); Replace LK.gui.left.addChild(generatorButton); with UILayer.addChild(generatorButton); Replace LK.gui.left.addChild(capacitorButton); with UILayer.addChild(capacitorButton); Replace LK.gui.left.addChild(rockLauncherButton); with UILayer.addChild(rockLauncherButton); Replace LK.gui.left.addChild(chemicalTowerButton); with UILayer.addChild(chemicalTowerButton); Replace LK.gui.left.addChild(slowTowerButton); with UILayer.addChild(slowTowerButton); Replace LK.gui.left.addChild(darkTowerButton); with UILayer.addChild(darkTowerButton); Replace LK.gui.left.addChild(graveyardTowerButton); with UILayer.addChild(graveyardTowerButton); Replace LK.gui.right.addChild(upgradePanel); with UILayer.addChild(upgradePanel); Replace upgradePanel.x = -350; with upgradePanel.x = LK.width - 350; Replace LK.gui.bottomLeft.addChild(minimapContainer); with UILayer.addChild(minimapContainer); Replace minimapContainer.y = -320; with minimapContainer.y = LK.height - 20; Replace minimapContainer.anchor.set(0, 1); (if it exists) with this: minimapContainer.pivot.set(0, minimapContainer.height);
/**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // === SECTION: ASSETS === // === SECTION: GLOBAL NAMESPACE === var Game = {}; // Camera state var cameraX = 0; var cameraY = 0; var isPointerDown = false; // Tracks if the mouse button is currently held down var isDragging = false; // Becomes true only after the mouse moves past a threshold var dragStartX = 0; var dragStartY = 0; var cameraStartX = 0; var cameraStartY = 0; // === SECTION: GLOBAL MANAGERS === Game.ResourceManager = { gold: 500, lives: 20, powerProduction: 0, powerConsumption: 0, powerAvailable: 0, addGold: function addGold(amount) { this.gold += amount; }, spendGold: function spendGold(amount) { if (this.gold >= amount) { this.gold -= amount; return true; } ; return false; }, loseLife: function loseLife(amount) { this.lives -= amount; if (this.lives <= 0) { Game.EventBus.publish(Game.Events.GameOverEvent()); } }, updatePower: function updatePower() { // 1. Calculate total power production var totalProduction = 0; var productionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerProductionComponent']); for (var i = 0; i < productionEntities.length; i++) { var entityId = productionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerProductionComponent'][entityId]; totalProduction += powerComp.production_rate; } this.powerProduction = totalProduction; // 2. Calculate total power consumption var totalConsumption = 0; var consumptionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerConsumptionComponent']); for (var i = 0; i < consumptionEntities.length; i++) { var entityId = consumptionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerConsumptionComponent'][entityId]; totalConsumption += powerComp.drain_rate; } this.powerConsumption = totalConsumption; // 3. Calculate total storage capacity var totalCapacity = 0; var storageEntities = Game.EntityManager.getEntitiesWithComponents(['PowerStorageComponent']); for (var i = 0; i < storageEntities.length; i++) { var entityId = storageEntities[i]; var storageComp = Game.EntityManager.componentStores['PowerStorageComponent'][entityId]; totalCapacity += storageComp.capacity; } // 4. Evaluate power status and update available buffer var surplus = totalProduction - totalConsumption; if (surplus >= 0) { // We have enough or extra power this.powerAvailable += surplus; // Clamp the available power to the max capacity if (this.powerAvailable > totalCapacity) { this.powerAvailable = totalCapacity; } Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } else { // We have a deficit, drain from the buffer this.powerAvailable += surplus; // surplus is negative, so this subtracts if (this.powerAvailable < 0) { // Buffer is empty and we still have a deficit this.powerAvailable = 0; Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('deficit')); } else { // Deficit was covered by the buffer Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } } }, onEnemyKilled: function onEnemyKilled(event) { this.addGold(event.gold_value); }, onWaveCompleted: function onWaveCompleted(event) { this.addGold(50); }, init: function init() { Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('WaveCompleted', this.onWaveCompleted.bind(this)); } }; Game.GridManager = { grid: [], CELL_STATES: { EMPTY: 0, PATH: 1, BLOCKED: 2, TOWER: 3 }, init: function init(width, height) { this.grid = []; for (var x = 0; x < width; x++) { this.grid[x] = []; for (var y = 0; y < height; y++) { 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 = gameWorld.attachAsset(render.sprite_id, {}); // --- NEW ANCHOR LOGIC --- // This is the critical fix: Ensure all game entities are visually centered. sprite.anchor.set(0.5, 0.5); // --- END NEW ANCHOR LOGIC --- this.displayObjects[entityId] = sprite; } sprite.x = transform.x; sprite.y = transform.y; sprite.visible = render.is_visible; } } }, TowerBuildSystem: { tryBuildAt: function tryBuildAt(gridX, gridY, towerType) { // 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 === // Create a container for all game objects that will be panned var gameWorld = new Container(); game.addChild(gameWorld); function drawGrid() { // Draw the grid cells for (var x = 0; x < Game.GridManager.grid.length; x++) { for (var y = 0; y < Game.GridManager.grid[x].length; y++) { var cellSprite = gameWorld.attachAsset('grid_cell', {}); cellSprite.x = x * 64; cellSprite.y = y * 64; cellSprite.anchor.set(0, 0); // Anchor to top-left cellSprite.gridX = x; // Store grid coordinates on the sprite cellSprite.gridY = y; // --- UPDATED CLICK LOGIC --- cellSprite.down = function () { // Get world coordinates from the click event, accounting for camera pan var worldX = LK.input.mouse.x - cameraX; var worldY = LK.input.mouse.y - cameraY; // Convert world coordinates to grid coordinates var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); // --- START OF NEW CLICK VS. DRAG LOGIC --- // Check if the mouse has moved significantly since the initial mousedown. // This prevents building a tower after panning. var dx = Math.abs(LK.input.mouse.x - dragStartX); var dy = Math.abs(LK.input.mouse.y - dragStartY); var DRAG_THRESHOLD = 10; // A 10-pixel tolerance for a "click" if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) { // The mouse moved too much, so this was a drag. Do nothing. return; } // --- END OF NEW CLICK VS. DRAG LOGIC --- // If we reach here, it was a valid click/tap. 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; } }; // --- END OF UPDATED CLICK LOGIC --- } } // Highlight the path cells if (Game.PathManager.currentPath) { for (var i = 0; i < Game.PathManager.currentPath.length; i++) { var pathNode = Game.PathManager.currentPath[i]; var pathSprite = gameWorld.attachAsset('path_cell', {}); // Note: PathManager coordinates are already in pixels pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } } // === SECTION: GLOBAL EVENT BUS === Game.EventBus = { listeners: {}, subscribe: function subscribe(eventType, listener) { if (!this.listeners[eventType]) { this.listeners[eventType] = []; } this.listeners[eventType].push(listener); }, unsubscribe: function unsubscribe(eventType, listener) { if (this.listeners[eventType]) { var index = this.listeners[eventType].indexOf(listener); if (index !== -1) { this.listeners[eventType].splice(index, 1); } } }, publish: function publish(event) { if (this.listeners[event.type]) { for (var i = 0; i < this.listeners[event.type].length; i++) { this.listeners[event.type][i](event); } } } }; Game.Events = { EnemyReachedEndEvent: function EnemyReachedEndEvent(enemy_id) { return { type: 'EnemyReachedEnd', enemy_id: enemy_id }; }, EnemyKilledEvent: function EnemyKilledEvent(enemy_id, gold_value) { return { type: 'EnemyKilled', enemy_id: enemy_id, gold_value: gold_value }; }, TowerPlacedEvent: function TowerPlacedEvent(tower_id, cost, position_x, position_y) { return { type: 'TowerPlaced', tower_id: tower_id, cost: cost, position_x: position_x, position_y: position_y }; }, TowerSoldEvent: function TowerSoldEvent(tower_id, refund_value) { return { type: 'TowerSold', tower_id: tower_id, refund_value: refund_value }; }, PowerStatusChangedEvent: function PowerStatusChangedEvent(new_status) { return { type: 'PowerStatusChanged', new_status: new_status }; }, WaveCompletedEvent: function WaveCompletedEvent(wave_number) { return { type: 'WaveCompleted', wave_number: wave_number }; }, GameOverEvent: function GameOverEvent() { return { type: 'GameOver' }; } }; // === SECTION: STACK-BASED GAME STATE MANAGER === // === SECTION: DIRECT GAME INITIALIZATION === // Initialize managers Game.GridManager.init(40, 60); Game.GridManager.grid[0][30] = Game.GridManager.CELL_STATES.PATH; // Set spawn point as unbuildable Game.ResourceManager.init(); Game.PathManager.init(40, 60); drawGrid(); // Create the Heads-Up Display (HUD) var goldText = new Text2('Gold: 100', { size: 40, fill: 0xFFD700 }); goldText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(goldText); goldText.x = -20; // 20px padding from the right edge goldText.y = 20; // 20px padding from the top edge var livesText = new Text2('Lives: 20', { size: 40, fill: 0xFF4136 }); livesText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(livesText); livesText.x = -20; // 20px padding from the right edge livesText.y = 70; // Position below the gold text var waveText = new Text2('Wave: 1 / 5', { size: 40, fill: 0xFFFFFF }); waveText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(waveText); waveText.x = -20; // 20px padding from the right edge waveText.y = 120; // Position below the lives text // Create Power Meter UI var powerMeterContainer = new Container(); LK.gui.topRight.addChild(powerMeterContainer); powerMeterContainer.x = -320; // Positioned in from the right edge powerMeterContainer.y = 170; // Position below wave text // Create a Graphics instance for power production bar var Graphics = function Graphics() { var graphics = new Container(); graphics.currentFill = 0xffffff; graphics.currentAlpha = 1.0; graphics.beginFill = function (color, alpha) { this.currentFill = color; this.currentAlpha = alpha !== undefined ? alpha : 1.0; }; graphics.drawRect = function (x, y, width, height) { var rect = game.attachAsset('grid_cell', {}); rect.tint = this.currentFill; rect.alpha = this.currentAlpha; rect.x = x; rect.y = y; rect.width = width; rect.height = height; this.addChild(rect); }; graphics.endFill = function () { // No-op for compatibility }; graphics.clear = function () { // Remove all children to clear the graphics while (this.children.length > 0) { var child = this.children[0]; this.removeChild(child); if (child.destroy) { child.destroy(); } } }; return graphics; }; // Background bar representing total production capacity var powerProductionBar = new Graphics(); powerProductionBar.beginFill(0x3498db, 0.5); // Semi-transparent blue powerProductionBar.drawRect(0, 0, 300, 20); // Placeholder width powerProductionBar.endFill(); powerMeterContainer.addChild(powerProductionBar); // Foreground bar representing current consumption var powerConsumptionBar = new Graphics(); powerConsumptionBar.beginFill(0x2ecc71); // Green for surplus powerConsumptionBar.drawRect(0, 0, 10, 20); // Placeholder width powerConsumptionBar.endFill(); powerMeterContainer.addChild(powerConsumptionBar); // Secondary bar for the available power buffer (capacitors) var powerAvailableBar = new Graphics(); powerAvailableBar.beginFill(0x3498db, 0.3); // Fainter blue powerAvailableBar.drawRect(0, 0, 5, 20); // Placeholder width, starts at the end of the production bar powerAvailableBar.endFill(); powerMeterContainer.addChild(powerAvailableBar); // Create Start Wave button var startWaveButton = new Text2('START NEXT WAVE', { size: 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(); } } // Add mouse event handlers for camera panning game.down = function (x, y, obj) { isDragging = true; dragStartX = x; dragStartY = y; lastMouseX = x; lastMouseY = y; cameraStartX = cameraX; cameraStartY = cameraY; mouseDownTime = LK.ticks / 60 * 1000; // Convert to milliseconds }; game.move = function (x, y, obj) { lastMouseX = x; lastMouseY = y; if (isDragging) { var dx = x - dragStartX; var dy = y - dragStartY; // Update camera position (inverted for drag effect) cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; // 40 tiles * 64 pixels per tile var mapHeight = 60 * 64; // 60 tiles * 64 pixels per tile var screenWidth = 2048; var screenHeight = 2732; // Ensure we never see beyond the map edges var minX = screenWidth - mapWidth; var minY = screenHeight - mapHeight; var maxX = 0; var maxY = 0; cameraX = Math.max(minX, Math.min(maxX, cameraX)); cameraY = Math.max(minY, Math.min(maxY, cameraY)); // Apply camera transform to game world gameWorld.x = cameraX; gameWorld.y = cameraY; } }; game.up = function (x, y, obj) { isDragging = false; }; // Set up main game loop var powerUpdateTimer = 0; game.update = function () { if (Game.GameManager.isGameOver) { return; } 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 = gameWorld.attachAsset('range_circle', {}); gameWorld.addChild(rangeCircle); rangeCircle.anchor.set(0.5, 0.5); // Set the origin to the center of the asset rangeCircle.alpha = 0.25; rangeCircle.visible = false; // === SECTION: UPGRADE & SELL UI === var upgradePanel = new Container(); LK.gui.right.addChild(upgradePanel); // 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
@@ -7,22 +7,20 @@
/****
* Game Code
****/
-// === SECTION: GLOBAL NAMESPACE ===
// === SECTION: ASSETS ===
+// === SECTION: GLOBAL NAMESPACE ===
var Game = {};
// Camera state
var cameraX = 0;
var cameraY = 0;
-var isDragging = false;
+var isPointerDown = false; // Tracks if the mouse button is currently held down
+var isDragging = false; // Becomes true only after the mouse moves past a threshold
var dragStartX = 0;
var dragStartY = 0;
var cameraStartX = 0;
var cameraStartY = 0;
-var lastMouseX = 0;
-var lastMouseY = 0;
-var mouseDownTime = 0;
// === SECTION: GLOBAL MANAGERS ===
Game.ResourceManager = {
gold: 500,
lives: 20,