User prompt
1. Fix the game.update Logic for Ghost Tower The primary bug is in the UI update logic. The safeCameraX and safeCameraY variables you see were likely added by your AI assistant to prevent a crash, but they don't solve the root cause. We will replace that entire section with code that uses the correct mouse position variables. Please replace the entire // --- UI UPDATE LOGIC --- section in your main game.update function with this corrected and simplified version: // --- UI UPDATE LOGIC --- // Hide all contextual UI by default each frame if (ghostTowerSprite) { ghostTowerSprite.visible = false; } buildTooltip.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; if (Game.GameplayManager.activeBuildType) { // STATE: Player has selected a tower and is choosing a location. if (ghostTowerSprite) { // Use the reliable lastMouseX/Y variables updated by the engine's move handler var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.visible = true; // --- GHOST SPRITE FIX --- // Update the texture and reset tint every frame to ensure it's correct var towerData = Game.TowerData[Game.GameplayManager.activeBuildType]; if (towerData) { ghostTowerSprite.texture = LK.assets[towerData.sprite_id].texture; ghostTowerSprite.tint = Game.GridManager.isBuildable(gridX, gridY) ? 0xFFFFFF : 0xFF5555; } // --- END FIX --- ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; } } else if (Game.GameplayManager.isConfirmingPlacement) { // STATE: Player has clicked a location and needs to confirm. confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // STATE: Player has selected an existing tower to upgrade/sell. var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId]; var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!renderComp || !towerComp) { Game.GameplayManager.selectedTowerId = -1; // Tower was sold or destroyed } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } 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 + ")"); } }
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'x')' in or related to this line: 'var mouseWorldX = LK.input.x - cameraX;' Line Number: 2051
User prompt
Finally, we need to adjust the game.update loop. It no longer needs to change the ghost's texture, only its position and visibility. Please replace the entire // --- UI UPDATE LOGIC --- section in game.update with this corrected version: // --- UI UPDATE LOGIC --- // Hide all contextual UI by default each frame if(ghostTowerSprite) ghostTowerSprite.visible = false; buildTooltip.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; if (Game.GameplayManager.activeBuildType) { // STATE: Player has selected a tower and is choosing a location. var towerData = Game.TowerData[Game.GameplayManager.activeBuildType]; if (ghostTowerSprite) { // Show ghost tower following the mouse var mouseWorldX = LK.input.x - cameraX; var mouseWorldY = LK.input.y - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.visible = true; ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable(gridX, gridY) ? 0xFFFFFF : 0xFF5555; } } else if (Game.GameplayManager.isConfirmingPlacement) { // STATE: Player has clicked a location and needs to confirm. confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // STATE: Player has selected an existing tower to upgrade/sell. var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId]; var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!renderComp || !towerComp) { Game.GameplayManager.selectedTowerId = -1; // Tower was sold or destroyed } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } 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 + ")"); } }
User prompt
Next, we will modify the build icon's down handler. It will now be responsible for creating the ghost. Please replace the entire button.down function inside the for loop with this new version: button.down = function () { // If there's an old ghost, destroy it if (ghostTowerSprite) { ghostTowerSprite.destroy(); } // Create a new ghost sprite using the correct asset ID ghostTowerSprite = gameWorld.attachAsset(this.towerType, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.visible = false; // It will be made visible in the update loop // Set the game state Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; Game.GameplayManager.lastClickedButton = this; };
User prompt
2. Fix the Ghost Tower Visuals The ghost tower isn't updating its appearance because it's being created once and its texture isn't changing correctly. The most robust fix is to destroy the old ghost and create a new one each time you select a tower. First, let's create a global variable to hold the ghost tower. Please add this line in the // === SECTION: BUILD AND CONFIRMATION UI === block, right at the beginning: var ghostTowerSprite = null;
User prompt
1. Fix the Confirmation and Build Logic The chain of events from clicking the confirmation button to actually building the tower is broken. We will fix the click handlers for the confirm/cancel buttons and ensure the correct tower type is remembered. Please find the // -- Confirmation Button Logic -- section. Delete the two empty down functions for confirmButton and cancelButton and replace them with this complete, functional block: // -- Confirmation Button Logic -- confirmButton.down = function() { if (Game.GameplayManager.isConfirmingPlacement) { var coords = Game.GameplayManager.confirmPlacementCoords; // Get the build type from the LAST button the player clicked. This is the key fix. var buildType = Game.GameplayManager.lastClickedButton ? Game.GameplayManager.lastClickedButton.towerType : null; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } // Reset all states after confirming Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; Game.GameplayManager.lastClickedButton = null; confirmPanel.visible = false; } }; cancelButton.down = function() { // Reset all states after canceling Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; Game.GameplayManager.lastClickedButton = null; confirmPanel.visible = false; };
User prompt
Please fix the bug: 'LK.gui.add is not a function' in or related to this line: 'LK.gui.add(confirmPanel); // Add to the main GUI layer to draw on top' Line Number: 1752
User prompt
1. Fix the Confirmation Panel Layering The confirmPanel is currently part of the gameWorld container, which means it gets rendered along with all the other game objects like towers and enemies. We need to move it to the main LK.gui layer so it always draws on top of everything in the game world. In the // === SECTION: BUILD AND CONFIRMATION UI === block, please find this line: gameWorld.addChild(confirmPanel); // Added to gameWorld so it pans with the camera And replace it with this line: LK.gui.add(confirmPanel); // Add to the main GUI layer to draw on top Next, because the panel is no longer affected by the camera's panning, we need to adjust how its position is calculated. Find the following lines inside the cellSprite.down function: // Position the confirmation panel at the clicked cell var worldX = gridX * 64 + 32; // Center of the cell var worldY = gridY * 64 + 32; confirmPanel.x = worldX; confirmPanel.y = worldY; And replace them with this new calculation that accounts for the camera's offset: // Position the confirmation panel relative to the screen, not the world var screenX = (gridX * 64 + 32) - cameraX; var screenY = (gridY * 64 + 32) - cameraY; confirmPanel.x = screenX; confirmPanel.y = screenY; 2. Fix the Ghost Tower Sprite The ghost tower sprite needs to be updated every frame to match the currently selected tower type. In the game.update function, inside the // --- UI UPDATE LOGIC --- section, find this line within the if (Game.GameplayManager.activeBuildType) block: ghostTowerSprite.texture = LK.assets[towerData.sprite_id].texture; The bug here is subtle: tint is applied after the asset is created, but the ghost sprite is created only once. When we just change the texture, the old tint from a previous ghost might remain. We need to reset the tint each time. Please replace that single line with these two lines: ghostTowerSprite.tint = 0xFFFFFF; // Reset tint to default white ghostTowerSprite.texture = LK.assets[towerData.sprite_id].texture; This ensures that any previous color tint is removed before the new texture is applied, making the ghost tower appear as the correct asset from the build panel.
User prompt
1. Fix the Confirmation Panel Layering The confirmPanel is currently part of the gameWorld container, which means it gets rendered along with all the other game objects like towers and enemies. We need to move it to the main LK.gui layer so it always draws on top of everything in the game world. In the // === SECTION: BUILD AND CONFIRMATION UI === block, please find this line: gameWorld.addChild(confirmPanel); // Added to gameWorld so it pans with the camera And replace it with this line: LK.gui.add(confirmPanel); // Add to the main GUI layer to draw on top Next, because the panel is no longer affected by the camera's panning, we need to adjust how its position is calculated. Find the following lines inside the cellSprite.down function: // Position the confirmation panel at the clicked cell var worldX = gridX * 64 + 32; // Center of the cell var worldY = gridY * 64 + 32; confirmPanel.x = worldX; confirmPanel.y = worldY; And replace them with this new calculation that accounts for the camera's offset: // Position the confirmation panel relative to the screen, not the world var screenX = (gridX * 64 + 32) - cameraX; var screenY = (gridY * 64 + 32) - cameraY; confirmPanel.x = screenX; confirmPanel.y = screenY; 2. Fix the Ghost Tower Sprite The ghost tower sprite needs to be updated every frame to match the currently selected tower type. In the game.update function, inside the // --- UI UPDATE LOGIC --- section, find this line within the if (Game.GameplayManager.activeBuildType) block: ghostTowerSprite.texture = LK.assets[towerData.sprite_id].texture; The bug here is subtle: tint is applied after the asset is created, but the ghost sprite is created only once. When we just change the texture, the old tint from a previous ghost might remain. We need to reset the tint each time. Please replace that single line with these two lines: ghostTowerSprite.tint = 0xFFFFFF; // Reset tint to default white ghostTowerSprite.texture = LK.assets[towerData.sprite_id].texture; This ensures that any previous color tint is removed before the new texture is applied, making the ghost tower appear as the correct asset from the build panel.
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'arrow_tower')' in or related to this line: 'ghostTowerSprite.texture = LK.assets[towerData.sprite_id].texture;' Line Number: 2055
User prompt
Step 1: Fix the Broken UI Code The provided // === SECTION: BUILD AND CONFIRMATION UI === block was malformed. Please delete that entire section from your code and replace it with this corrected version. This will properly create the visual elements for the tooltip and the confirmation panel. // === SECTION: BUILD AND CONFIRMATION UI === // -- Ghost Tower (for placement preview) -- var ghostTowerSprite = gameWorld.attachAsset('arrow_tower', {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.visible = false; // -- Build Panel (at the bottom of the screen) -- var buildPanel = new Container(); LK.gui.bottom.addChild(buildPanel); buildPanel.x = 0; buildPanel.y = -120; // -- Tooltip (appears over build icons) -- var buildTooltip = new Container(); LK.gui.add(buildTooltip); // Add to the main GUI layer so it can be positioned anywhere buildTooltip.visible = false; var tooltipBackground = new Graphics(); tooltipBackground.beginFill(0x000000, 0.8); tooltipBackground.drawRect(0, 0, 300, 80); // Adjusted size buildTooltip.addChild(tooltipBackground); var tooltipText = new Text2('Tower Info', { size: 28, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 280 }); tooltipText.anchor.set(0, 0); tooltipText.x = 10; tooltipText.y = 10; buildTooltip.addChild(tooltipText); // -- Confirmation Panel (appears on the game map) -- var confirmPanel = new Container(); gameWorld.addChild(confirmPanel); // Added to gameWorld so it pans with the camera confirmPanel.visible = false; var confirmBackground = new Graphics(); confirmBackground.beginFill(0x2c3e50, 0.9); confirmBackground.drawRect(0, 0, 180, 60); confirmBackground.anchor.set(0.5, 0.5); // Anchor background to center confirmPanel.addChild(confirmBackground); var confirmButton = new Text2('Build', { size: 35, fill: 0x27AE60 }); confirmButton.anchor.set(0.5, 0.5); confirmButton.x = -45; // Position relative to panel center confirmPanel.addChild(confirmButton); var cancelButton = new Text2('Cancel', { size: 35, fill: 0xC0392B }); cancelButton.anchor.set(0.5, 0.5); cancelButton.x = 45; // Position relative to panel center confirmPanel.addChild(cancelButton); // -- Build Button Creation Logic -- var buildButtonData = [ { type: 'ArrowTower', sprite: 'arrow_tower' }, { type: 'GeneratorTower', sprite: 'generator_tower' }, { type: 'CapacitorTower', sprite: 'capacitor_tower' }, { type: 'RockLauncher', sprite: 'rock_launcher_tower' }, { type: 'ChemicalTower', sprite: 'chemical_tower' }, { type: 'SlowTower', sprite: 'slow_tower' }, { type: 'DarkTower', sprite: 'dark_tower' }, { type: 'GraveyardTower', sprite: 'graveyard_tower' } ]; var buttonSize = 64; var buttonPadding = 10; var totalWidth = (buildButtonData.length * buttonSize) + ((buildButtonData.length - 1) * buttonPadding); var startX = -totalWidth / 2; buildButtonData.forEach(function(data, i) { var button = game.attachAsset(data.sprite, {}); button.width = buttonSize; button.height = buttonSize; button.anchor.set(0.5, 0.5); button.x = startX + (i * (buttonSize + buttonPadding)) + (buttonSize / 2); button.y = 0; button.towerType = data.type; button.down = function() { Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; Game.GameplayManager.lastClickedButton = this; }; buildPanel.addChild(button); }); // -- Confirmation Button Logic -- confirmButton.down = function() { if (Game.GameplayManager.isConfirmingPlacement) { var coords = Game.GameplayManager.confirmPlacementCoords; var buildType = Game.GameplayManager.lastClickedButton ? Game.GameplayManager.lastClickedButton.towerType : null; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } Game.GameplayManager.isConfirmingPlacement = false; } }; cancelButton.down = function() { Game.GameplayManager.isConfirmingPlacement = false; }; Step 2: Implement the UI Update Logic Now, we need to orchestrate the visibility and behavior of all these new UI elements. This logic controls when the tooltip, ghost tower, and confirmation panel appear. Please replace the entire // --- UI UPDATE LOGIC --- section in your main game.update function with this complete, corrected block. // --- UI UPDATE LOGIC --- // Hide all contextual UI by default each frame ghostTowerSprite.visible = false; buildTooltip.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; if (Game.GameplayManager.activeBuildType) { // STATE: Player has selected a tower and is choosing a location. var towerTypeKey = Game.GameplayManager.activeBuildType; var towerData = Game.TowerData[towerTypeKey]; // Show ghost tower following the mouse var mouseWorldX = LK.input.x - cameraX; var mouseWorldY = LK.input.y - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.visible = true; ghostTowerSprite.texture = LK.assets[towerData.sprite_id].texture; ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // Show tooltip for the active build tower var info = towerTypeKey + " | Cost: " + towerData.cost; var attackComp = towerData.components.find(c => c.name === 'AttackComponent'); if (attackComp) { info += "\nDMG: " + attackComp.args[0] + " | RNG: " + attackComp.args[1] + " | SPD: " + attackComp.args[2]; } tooltipText.setText(info); buildTooltip.visible = true; // Position tooltip near the mouse cursor buildTooltip.x = LK.input.x - 150; // Offset from mouse buildTooltip.y = LK.input.y - 100; } else if (Game.GameplayManager.isConfirmingPlacement) { // STATE: Player has clicked a location and needs to confirm. confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // STATE: Player has selected an existing tower to upgrade/sell. var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId]; var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!renderComp || !towerComp) { Game.GameplayManager.selectedTowerId = -1; // Tower was sold or destroyed } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } 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 + ")"); } }
User prompt
3. Modify Grid Click Logic for Confirmation Now, when a player clicks a cell whileComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = in build mode, we'll show the confirmPanel instead of building instantly. Please replace the0; i < allTowers.length; i++) { var towerId = allTowers[i entire cellSprite.down function inside drawGrid with this new version: cellSprite]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var towerGrid.down = function() { // Only process true clicks, not drags var timeSinceMouseDown = (LK.X = Math.floor(transform.x / 64); var towerGridY = Math.floorticks / 60 * 1000) - mouseDownTime; if (timeSinceMouseDown >(transform.y / 64); if (towerGridX === gridX && towerGridY === 200) { return; } var dx = Math.abs(lastMouseX - dragStartX gridY) { foundTowerId = towerId; break; } } ); var dy = Math.abs(lastMouseY - dragStartY); if (dx > 10 || dy > 10) { return; } var gridX = this.gridX;Game.GameplayManager.selectedTowerId = foundTowerId; } else { Game.GameplayManager var gridY = this.gridY; var cellState = Game.GridManager.grid[.selectedTowerId = -1; } } };
User prompt
Next, replace the entire cellSprite.down function inside drawGrid withi * (buttonSize + buttonPadding)) + (buttonSize / 2); button.y = this new logic: cellSprite.down = function() { // Ignore clicks if it0; button.towerType = data.type; button.down = function() { Game.'s part of a drag action var timeSinceMouseDown = (LK.ticks / 60 * 100GameplayManager.buildMode = this.towerType; Game.GameplayManager.selectedTowerId = -1; // Des0) - mouseDownTime; if (timeSinceMouseDown > 200) { return; } elect any existing tower Game.GameplayManager.placementConfirmation = null; // Cancel any other pending placement }; var dx = Math.abs(lastMouseX - dragStartX); var dy = Math.abs( // Show tooltip on hover (move) button.move = function() { var towerDatalastMouseY - dragStartY); if (dx > 10 || dy > 10) { = Game.TowerData[this.towerType]; tooltipText.setText(this.towerType + "\nCost return; } var gridX = this.gridX; var gridY = this.gridY: " + towerData.cost); buildTooltip.x = buildPanel.x + this.x -; // If we are currently in build mode... if (Game.GameplayManager.activeBuildType 100; // Position tooltip relative to button buildTooltip.y = buildPanel.y - ) { if (Game.GridManager.isBuildable(gridX, gridY)) { //60 - 10; // Position above the button buildTooltip.visible = true; }; ...and we click a buildable spot, show the confirmation dialog. Game.GameplayManager.isConfirmingPlacement = true; Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY buildPanel.addChild(button); }); // Hide tooltip when mouse leaves the panel area buildPanel.out = function() { buildTooltip.visible = false; }; // -- Confirmation Button Logic -- confirmButton. }; // Temporarily exit active build mode while confirming Game.GameplayManager.activeBuildType = null; down = function() { var confirmState = Game.GameplayManager.placementConfirmation; if (confirmState) } else { // Clicked an invalid spot, so cancel build mode Game.GameplayManager. { Game.Systems.TowerBuildSystem.tryBuildAt(confirmState.x, confirmState.y, confirmStateactiveBuildType = null; } } else if (!Game.GameplayManager.isConfirmingPlacement) {.type); Game.GameplayManager.placementConfirmation = null; // Clear the state } }; cancel // If not in build or confirm mode, the click is for selection. var cellState = Game.Button.down = function() { Game.GameplayManager.placementConfirmation = null; // Clear the state GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { var allTowers = Game.EntityManager.getEntitiesWithComponents(['Tower};
User prompt
3. Update Build Panel and Grid Click Logic Now, we'll modify how the build iconsTower', sprite: 'graveyard_tower' } ]; var buttonSize = 64; var and the grid cells respond to clicks. First, replace the button.down function inside the for buttonPadding = 10; var totalWidth = (buildButtonData.length * buttonSize) + ((buildButtonData.length - 1) * buttonPadding); var startX = -totalWidth / 2 loop of the // === SECTION: NEW BUILD UI === block with this: button.down; buildButtonData.forEach(function(data, i) { var button = game.attachAsset(data. = function() { Game.GameplayManager.activeBuildType = this.towerType; Game.sprite, {}); button.width = buttonSize; button.height = buttonSize; buttonGameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; // Deselect any existing tower Game.GameplayManager.lastClickedButton = this; // Remember which button was clicked.anchor.set(0.5, 0.5); button.x = startX + ( };
User prompt
2. Create the New UI Elements (Tooltip, Ghost, and Confirmation Panel) We'll create all the new visual elements needed for this system. Please add this entire new code block right after the // === SECTION: NEW BUILD UI === comment. var ghostTowerSprite = gameWorld.attachAsset('arrow_tower', {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.visible = false; var buildTooltip = new Container(); LK.gui.add(buildTooltip); // Add to the main GUI layer buildTooltip.visible = false; var tooltipBackground = new Graphics(); tooltipBackground.beginFill(0x000000, 0.8); tooltipBackground.drawRect(0, 0, 300, 100); buildTooltip.addChild(tooltipBackground); var tooltipText = new Text2('Tower Info', { size: 28, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 280 }); tooltipText.anchor.set(0, 0); tooltipText.x = 10; tooltipText.y = 10; buildTooltip.addChild that appears over the build icons and a "Confirm/Cancel" panel that appears on the map. Please **delete** the entire `// === SECTION: NEW BUILD UI ===` block and **replace** it with this new, comprehensive UI section. This replaces the old build panel and adds the new ones. ```javascript // === SECTION: BUILD AND CONFIRMATION UI === // -- Build Panel (at the bottom of the screen) -- var buildPanel = new Container(); LK.gui.bottom.addChild(buildPanel); buildPanel.x = 0; buildPanel.y = -120; // Positioned to not overlap the "Start Wave" button // -- Tooltip (appears over build icons) -- var buildTooltip = new Container(); LK.gui.bottom.addChild(buildTooltip); // Add to the same GUI layer buildTooltip.visible = false; var tooltipBackground = new Graphics(); tooltipBackground.beginFill(0x000000, 0.8); tooltipBackground.drawRect(0, 0, 200, 60); buildTooltip.addChild(tooltipBackground); var tooltipText = new Text2('', { size: 24, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 190 }); tooltipText.anchor.set(0.5, 0); tooltipText.x = 100; tooltipText.y = 5; buildTooltip.addChild(tooltipText); // -- Confirmation Panel (appears on the game map) -- var confirmPanel = new Container(); gameWorld.addChild(confirmPanel); // Added to gameWorld so it pans with the camera confirmPanel.visible = false; var confirmButton = game.attachAsset('arrow_tower', { tint: 0x2ECC71 }); // Green checkmark confirmButton.width = 48; confirmButton.height = 48; confirmButton.x = -32; confirmButton.anchor.set(0.5, 0.5); confirmPanel.addChild(confirmButton); var cancelButton = game.attachAsset('arrow_tower', { tint: 0xE74C3C }); // Red X cancelButton.width = 48;(tooltipText); var confirmPanel = new Container(); gameWorld.addChild(confirmPanel); // Add to gameWorld so it pans with the camera confirmPanel.visible = false; var confirmBackground = new Graphics(); confirmBackground.beginFill(0x2c3e50, 0.9); confirmBackground.drawRect(0, 0, 180, 60); confirmPanel.addChild(confirmBackground); var confirmButton = new Text2('Build', { size: 35, fill: 0x27AE60 }); confirmButton.anchor.set(0.5, 0.5); confirmButton cancelButton.height = 48; cancelButton.x = 32; cancelButton.anchor.set(0.5, 0.5); confirmPanel.addChild(cancelButton); // -- Build Button Creation Logic -- var buildButtonData = [ { type: 'ArrowTower', sprite: 'arrow_tower' }, { type: 'GeneratorTower', sprite: 'generator_tower' }, .x = 45; confirmButton.y = 30; confirmPanel.addChild(confirmButton); var cancelButton = new Text2('Cancel', { size: 35, fill: 0xC0392B { type: 'CapacitorTower', sprite: 'capacitor_tower' }, { type: 'RockLauncher', sprite: 'rock_launcher_tower' }, { type: 'ChemicalTower', sprite: }); cancelButton.anchor.set(0.5, 0.5); cancelButton.x = 'chemical_tower' }, { type: 'SlowTower', sprite: 'slow_tower' }, 135; cancelButton.y = 30; confirmPanel.addChild(cancelButton); { type: 'DarkTower', sprite: 'dark_tower' }, { type: 'Graveyard
User prompt
2. Create the New UI Panels (Tooltip and Confirmation) We will create two new UI elements: a small tooltip the New Workflow** We need to adjust our state variables to handle this new flow. Please replace the buildMode and pendingBuildType lines in Game.GameplayManager with the following: activeBuildType: null, // The tower type selected from the build panel (e.g., 'ArrowTower') isConfirmingPlacement: false, // True when the confirm/cancel dialog is shown confirmPlacementCoords: { x: null, y: null }, // Stores the grid coords for confirmation lastClickedButton: null, // Stores a reference to the UI button that was clicked
User prompt
1. Update GameplayManager for the New Confirmation Flow We need to change our state variables. We'll remove pendingBuildType and add a new state to hold the details for a pending placement. Please replace these two lines in Game.GameplayManager: buildMode: null, // e.g., 'ArrowTower', or null if not building pendingBuildType: null, // Tracks the tower selected for tap-to-reveal with these new lines: Generated javascript buildMode: null, // The tower type selected for building, e.g., 'ArrowTower' placementConfirmation: null, // Holds data for a pending placement, e.g., {x, y, type}
User prompt
1. Make the Minimap Interactive We will add a .down event handler to the minimapContainer object. This handler will calculate the world coordinates corresponding to the click location and move the camera. Please add the following block of code to the // === SECTION: MINIMAP UI === section, right after the line minimapContainer.addChild(minimapViewportRect);. minimapContainer.down = function(x, y, obj) { // x and y are the click coordinates relative to the minimap's top-left corner. // 1. Define map and minimap dimensions for conversion. var mapWorldWidth = 40 * 64; // 2560 var mapWorldHeight = 60 * 64; // 3840 var minimapPixelWidth = 40 * 5; // 200 var minimapPixelHeight = 60 * 5; // 300 // 2. Convert minimap click coordinates to world coordinates. // This gives us the point in the world the player wants to look at. var targetWorldX = (x / minimapPixelWidth) * mapWorldWidth; var targetWorldY = (y / minimapPixelHeight) * mapWorldHeight; // 3. Calculate the new camera position to CENTER the view on the target. var screenWidth = 2048; var screenHeight = 2732; var newCameraX = -targetWorldX + (screenWidth / 2); var newCameraY = -targetWorldY + (screenHeight / 2); // 4. Clamp the new camera position to the map boundaries. // This reuses the same clamping logic from the drag-to-pan feature. var minX = screenWidth - mapWorldWidth; var minY = screenHeight - mapWorldHeight; var maxX = 0; var maxY = 0; cameraX = Math.max(minX, Math.min(maxX, newCameraX)); cameraY = Math.max(minY, Math.min(maxY, newCameraY)); // 5. Apply the final, clamped transform to the game world. gameWorld.x = cameraX; gameWorld.y = cameraY; };
User prompt
3. Call the Update Function in the Main Loop Finally, we need to call this new function every frame. In the main game.update function, find the line updateMinimapDynamic(); and add the following line immediately after it: updateMinimapViewport();
User prompt
2. Create a Function to Update the Viewport Rectangle Next, we need a dedicated function to calculate the position and size of this rectangle every frame. Please add this new function to the same // === SECTION: MINIMAP UI === block, right after the updateMinimapDynamic function. function updateMinimapViewport() { minimapViewportRect.clear(); var tilePixelSize = 5; var worldTileSize = 64; // The cameraX/Y variables are inverse offsets. We need the true top-left coordinate. var trueCameraX = -cameraX; var trueCameraY = -cameraY; // Get screen/viewport dimensions var screenWidth = 2048; // Use the game's actual screen dimensions var screenHeight = 2732; // Convert world coordinates and dimensions to minimap scale var rectX = (trueCameraX / worldTileSize) * tilePixelSize; var rectY = (trueCameraY / worldTileSize) * tilePixelSize; var rectWidth = (screenWidth / worldTileSize) * tilePixelSize; var rectHeight = (screenHeight / worldTileSize) * tilePixelSize; minimapViewportRect.beginFill(0xFFFFFF, 0.25); // Semi-transparent white minimapViewportRect.drawRect(rectX, rectY, rectWidth, rectHeight); minimapViewportRect.endFill(); }
User prompt
1. Create the Minimap Viewport Rectangle First, we need a new visual object to represent the camera's view. Please add the following code to the // === SECTION: MINIMAP UI === block, right after minimapContainer.addChild(minimapDynamicLayer);. var minimapViewportRect = new Graphics(); minimapContainer.addChild(minimapViewportRect);
User prompt
1. Adjust the Build Panel's Position In the // === SECTION: NEW BUILD UI === block of code you just added, please find this line: Generated javascript // Position it above the bottom edge of the screen buildPanel.y = -80; Please replace that line with the following new line: Generated javascript // Position it above the bottom edge of the screen buildPanel.y = -120; Increasing the negative y value moves the panel further up from the bottom of the screen, creating the necessary space.
User prompt
Please add the following line to the Game.GameplayManager object, right after selectedTowerId: -1,: buildMode: null, // e.g., 'ArrowTower', or null if not building In the // === SECTION: TEMP BUILD UI === section, please delete the entire block of code. This includes the buildMode variable definition and all the Text2 buttons (from var buildMode = 'ArrowTower'; down to the end of graveyardTowerButton.down). 3. Modify the Grid Cell Click Logic We need to change what happens when a player clicks on the grid. It should now check if we are in a "build mode". Please replace the entire cellSprite.down function inside the drawGrid function with this new version: cellSprite.down = function() { // Only process clicks, not drags var timeSinceMouseDown = (LK.ticks / 60 * 1000) - mouseDownTime; if (timeSinceMouseDown > 200) { return; } var dx = Math.abs(lastMouseX - dragStartX); var dy = Math.abs(lastMouseY - dragStartY); if (dx > 10 || dy > 10) { return; } var gridX = this.gridX; var gridY = this.gridY; var cellState = Game.GridManager.grid[gridX][gridY]; // --- NEW BUILD MODE LOGIC --- if (Game.GameplayManager.buildMode && cellState === Game.GridManager.CELL_STATES.EMPTY) { // If in build mode, try to build the selected tower Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, Game.GameplayManager.buildMode); // Exit build mode after placing a tower Game.GameplayManager.buildMode = null; } else if (cellState === Game.GridManager.CELL_STATES.TOWER) { // If not in build mode, select an existing 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; Game.GameplayManager.buildMode = null; // Exit build mode when selecting } else { // Clicked on a path, blocked cell, or empty cell without being in build mode Game.GameplayManager.selectedTowerId = -1; Game.GameplayManager.buildMode = null; } }; // === SECTION: NEW BUILD UI === var buildPanel = new Container(); LK.gui.bottom.addChild(buildPanel); // Center the panel horizontally buildPanel.x = 0; // Position it above the bottom edge of the screen buildPanel.y = -80; var buildButtonData = [ { type: 'ArrowTower', sprite: 'arrow_tower' }, { type: 'GeneratorTower', sprite: 'generator_tower' }, { type: 'CapacitorTower', sprite: 'capacitor_tower' }, { type: 'RockLauncher', sprite: 'rock_launcher_tower' }, { type: 'ChemicalTower', sprite: 'chemical_tower' }, { type: 'SlowTower', sprite: 'slow_tower' }, { type: 'DarkTower', sprite: 'dark_tower' }, { type: 'GraveyardTower', sprite: 'graveyard_tower' } ]; var buttonSize = 64; var buttonPadding = 10; var totalWidth = (buildButtonData.length * buttonSize) + ((buildButtonData.length - 1) * buttonPadding); var startX = -totalWidth / 2; for (var i = 0; i < buildButtonData.length; i++) { var data = buildButtonData[i]; var button = game.attachAsset(data.sprite, {}); button.width = buttonSize; button.height = buttonSize; button.anchor.set(0.5, 0.5); button.x = startX + (i * (buttonSize + buttonPadding)) + (buttonSize / 2); button.y = 0; // Custom property to store tower type button.towerType = data.type; button.down = function() { // Set the game's build mode to this button's tower type Game.GameplayManager.buildMode = this.towerType; // Deselect any currently selected tower Game.GameplayManager.selectedTowerId = -1; }; buildPanel.addChild(button); } Finally, please add the following new section of code. A good place for it is immediately after the // === SECTION: DIRECT GAME INITIALIZATION === block, right before the minimap UI code.
User prompt
Finally, to prevent any conflicts, we must remove the old .down handler from the grid cells. Please replace the entire 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 = gameWorld.attachAsset('grid_cell', {}); cellSprite.x = x * 64; cellSprite.y = y * 64; cellSprite.anchor.set(0, 0); // Anchor to top-left // NOTE: The cellSprite.down handler has been completely removed. // All click/drag logic is now handled by the global game.down/move/up handlers. } } // 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', {}); pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } }
User prompt
Please replace all three global input handlers (game.down, game.move, and game.up) with the following new, corrected versions. // Add mouse event handlers for camera panning game.down = function (x, y, obj) { // Don't interact with the world if a UI element was clicked if (obj) return; isPointerDown = true; isDragging = false; // Reset dragging state on each new press dragStartX = x; dragStartY = y; cameraStartX = cameraX; cameraStartY = cameraY; }; game.move = function (x, y, obj) { if (isPointerDown) { if (!isDragging) { // Check if the mouse has moved past the drag threshold var dx = x - dragStartX; var dy = y - dragStartY; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 10) { isDragging = true; // It's officially a drag now } } if (isDragging) { var dragDeltaX = x - dragStartX; var dragDeltaY = y - dragStartY; // Update camera position based on the drag cameraX = cameraStartX + dragDeltaX; cameraY = cameraStartY + dragDeltaY; // Clamp camera to map boundaries var mapWidth = 40 * 64; var mapHeight = 60 * 64; var screenWidth = 2048; var screenHeight = 2732; var minX = Math.min(0, screenWidth - mapWidth); var minY = Math.min(0, 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 the game world container gameWorld.x = cameraX; gameWorld.y = cameraY; } } }; game.up = function (x, y, obj) { if (isPointerDown && !isDragging) { // This was a click, not a drag. // Get world coordinates from the click event, accounting for camera pan var worldX = x - cameraX; var worldY = y - cameraY; // Convert world coordinates to grid coordinates var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); // Ensure the click is within the grid 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 flags isPointerDown = false; isDragging = false; };
/**** * 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 isDragging = false; var dragStartX = 0; var dragStartY = 0; var cameraStartX = 0; var cameraStartY = 0; var lastMouseX = 0; var lastMouseY = 0; var mouseDownTime = 0; // === SECTION: GLOBAL MANAGERS === Game.ResourceManager = { gold: 500, lives: 20, powerProduction: 0, powerConsumption: 0, powerAvailable: 0, addGold: function addGold(amount) { this.gold += amount; }, spendGold: function spendGold(amount) { if (this.gold >= amount) { this.gold -= amount; return true; } ; return false; }, loseLife: function loseLife(amount) { this.lives -= amount; if (this.lives <= 0) { Game.EventBus.publish(Game.Events.GameOverEvent()); } }, updatePower: function updatePower() { // 1. Calculate total power production var totalProduction = 0; var productionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerProductionComponent']); for (var i = 0; i < productionEntities.length; i++) { var entityId = productionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerProductionComponent'][entityId]; totalProduction += powerComp.production_rate; } this.powerProduction = totalProduction; // 2. Calculate total power consumption var totalConsumption = 0; var consumptionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerConsumptionComponent']); for (var i = 0; i < consumptionEntities.length; i++) { var entityId = consumptionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerConsumptionComponent'][entityId]; totalConsumption += powerComp.drain_rate; } this.powerConsumption = totalConsumption; // 3. Calculate total storage capacity var totalCapacity = 0; var storageEntities = Game.EntityManager.getEntitiesWithComponents(['PowerStorageComponent']); for (var i = 0; i < storageEntities.length; i++) { var entityId = storageEntities[i]; var storageComp = Game.EntityManager.componentStores['PowerStorageComponent'][entityId]; totalCapacity += storageComp.capacity; } // 4. Evaluate power status and update available buffer var surplus = totalProduction - totalConsumption; if (surplus >= 0) { // We have enough or extra power this.powerAvailable += surplus; // Clamp the available power to the max capacity if (this.powerAvailable > totalCapacity) { this.powerAvailable = totalCapacity; } Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } else { // We have a deficit, drain from the buffer this.powerAvailable += surplus; // surplus is negative, so this subtracts if (this.powerAvailable < 0) { // Buffer is empty and we still have a deficit this.powerAvailable = 0; Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('deficit')); } else { // Deficit was covered by the buffer Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } } }, onEnemyKilled: function onEnemyKilled(event) { this.addGold(event.gold_value); }, onWaveCompleted: function onWaveCompleted(event) { this.addGold(50); }, init: function init() { Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('WaveCompleted', this.onWaveCompleted.bind(this)); } }; Game.GridManager = { grid: [], CELL_STATES: { EMPTY: 0, PATH: 1, BLOCKED: 2, TOWER: 3 }, init: function init(width, height) { this.grid = []; for (var x = 0; x < width; x++) { this.grid[x] = []; for (var y = 0; y < height; y++) { 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, activeBuildType: null, // The tower type selected from the build panel (e.g., 'ArrowTower') isConfirmingPlacement: false, // True when the confirm/cancel dialog is shown confirmPlacementCoords: { x: null, y: null }, // Stores the grid coords for confirmation lastClickedButton: null, // Stores a reference to the UI button that was clicked enemiesSpawnedThisWave: 0, enemiesKilledThisWave: 0, enemiesLeakedThisWave: 0, totalEnemiesThisWave: 0, init: function init() { Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('EnemyReachedEnd', this.onEnemyReachedEnd.bind(this)); }, onEnemyKilled: function onEnemyKilled(event) { this.enemiesKilledThisWave++; if (this.enemiesKilledThisWave + this.enemiesLeakedThisWave >= this.totalEnemiesThisWave) { Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1)); this.enemiesKilledThisWave = 0; this.enemiesLeakedThisWave = 0; startWaveButton.visible = true; } }, onEnemyReachedEnd: function onEnemyReachedEnd(event) { this.enemiesLeakedThisWave++; Game.ResourceManager.loseLife(1); Game.EntityManager.destroyEntity(event.enemy_id); if (this.enemiesKilledThisWave + this.enemiesLeakedThisWave >= this.totalEnemiesThisWave) { Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1)); this.enemiesKilledThisWave = 0; this.enemiesLeakedThisWave = 0; startWaveButton.visible = true; } } }; Game.GameManager = { isGameOver: false, init: function init() { Game.EventBus.subscribe('GameOver', this.onGameOver.bind(this)); }, onGameOver: function onGameOver(event) { this.isGameOver = true; LK.showGameOver(); } }; Game.Debug = { logText: null, init: function init() { this.logText = new Text2('Debug Log Initialized', { size: 40, fill: 0x00FF00 }); // Green text this.logText.anchor.set(0, 1); // Anchor to bottom-left LK.gui.bottomLeft.addChild(this.logText); this.logText.x = 20; this.logText.y = -20; }, log: function log(message) { if (this.logText) { this.logText.setText(String(message)); } } }; // === SECTION: ENTITY-COMPONENT-SYSTEM SCAFFOLDING === Game.EntityManager = { nextEntityId: 1, activeEntities: [], componentStores: {}, entitiesToDestroy: [], createEntity: function createEntity() { var entityId = this.nextEntityId++; this.activeEntities.push(entityId); return entityId; }, addComponent: function addComponent(entityId, component) { if (!this.componentStores[component.name]) { this.componentStores[component.name] = {}; } this.componentStores[component.name][entityId] = component; }, getEntitiesWithComponents: function getEntitiesWithComponents(componentNames) { var result = []; for (var i = 0; i < this.activeEntities.length; i++) { var entityId = this.activeEntities[i]; var hasAllComponents = true; for (var j = 0; j < componentNames.length; j++) { var componentName = componentNames[j]; if (!this.componentStores[componentName] || !this.componentStores[componentName][entityId]) { hasAllComponents = false; break; } } if (hasAllComponents) { result.push(entityId); } } return result; }, destroyEntity: function destroyEntity(entityId) { // Avoid adding duplicates if (this.entitiesToDestroy.indexOf(entityId) === -1) { this.entitiesToDestroy.push(entityId); } } }; Game.Components = { TransformComponent: function TransformComponent(x, y, rotation) { return { name: 'TransformComponent', x: x || 0, y: y || 0, rotation: rotation || 0 }; }, RenderComponent: function RenderComponent(sprite_id, layer, scale, is_visible) { return { name: 'RenderComponent', sprite_id: sprite_id || '', layer: layer || 0, scale: scale || 1.0, is_visible: is_visible !== undefined ? is_visible : true }; }, HealthComponent: function HealthComponent(current_hp, max_hp) { return { name: 'HealthComponent', current_hp: current_hp || 100, max_hp: max_hp || 100 }; }, AttackComponent: function AttackComponent(damage, range, attack_speed, last_attack_time, projectile_id, target_id, splash_radius) { return { name: 'AttackComponent', damage: damage || 10, range: range || 100, attack_speed: attack_speed || 1.0, last_attack_time: last_attack_time || 0, projectile_id: projectile_id || '', target_id: target_id || -1, splash_radius: splash_radius || 0 // Default to 0 (no splash) }; }, MovementComponent: function MovementComponent(speed) { return { name: 'MovementComponent', speed: speed || 50 }; }, PowerProductionComponent: function PowerProductionComponent(rate) { return { name: 'PowerProductionComponent', production_rate: rate || 0 }; }, PowerConsumptionComponent: function PowerConsumptionComponent(rate) { return { name: 'PowerConsumptionComponent', drain_rate: rate || 0 }; }, TowerComponent: function TowerComponent(cost, sellValue, builtOnWave) { return { name: 'TowerComponent', cost: cost || 50, sell_value: sellValue || 25, upgrade_level: 1, upgrade_path_A_level: 0, upgrade_path_B_level: 0, built_on_wave: builtOnWave || 0 }; }, ProjectileComponent: function ProjectileComponent(target_id, speed, damage) { return { name: 'ProjectileComponent', target_id: target_id || -1, speed: speed || 8, // Let's keep the speed lower damage: damage || 10 }; }, EnemyComponent: function EnemyComponent(goldValue, livesCost, armor_type) { return { name: 'EnemyComponent', gold_value: goldValue || 5, lives_cost: livesCost || 1, armor_type: armor_type || 'Normal' }; }, PowerStorageComponent: function PowerStorageComponent(capacity) { return { name: 'PowerStorageComponent', capacity: capacity || 0 }; }, PoisonOnImpactComponent: function PoisonOnImpactComponent(damage_per_second, duration) { return { name: 'PoisonOnImpactComponent', damage_per_second: damage_per_second || 0, duration: duration || 0 }; }, PoisonDebuffComponent: function PoisonDebuffComponent(damage_per_second, duration_remaining, last_tick_time) { return { name: 'PoisonDebuffComponent', damage_per_second: damage_per_second || 0, duration_remaining: duration_remaining || 0, last_tick_time: last_tick_time || 0 }; }, SlowOnImpactComponent: function SlowOnImpactComponent(slow_percentage, duration) { return { name: 'SlowOnImpactComponent', slow_percentage: slow_percentage || 0, duration: duration || 0 }; }, SlowDebuffComponent: function SlowDebuffComponent(slow_percentage, duration_remaining) { return { name: 'SlowDebuffComponent', slow_percentage: slow_percentage || 0, duration_remaining: duration_remaining || 0 }; }, AuraComponent: function AuraComponent(range, effect_type) { return { name: 'AuraComponent', range: range || 100, effect_type: effect_type || '' }; }, DamageBuffComponent: function DamageBuffComponent(multiplier, source_aura_id) { return { name: 'DamageBuffComponent', multiplier: multiplier || 0, source_aura_id: source_aura_id || -1 }; }, GoldGenerationComponent: function GoldGenerationComponent(gold_per_second) { return { name: 'GoldGenerationComponent', gold_per_second: gold_per_second || 0 }; }, FlyingComponent: function FlyingComponent() { return { name: 'FlyingComponent' }; }, // --- NEW COMPONENTS --- FireZoneOnImpactComponent: function FireZoneOnImpactComponent(damage_per_tick, duration, tick_rate) { return { name: 'FireZoneOnImpactComponent', damage_per_tick: damage_per_tick || 0, duration: duration || 0, tick_rate: tick_rate || 1.0 }; }, FireZoneComponent: function FireZoneComponent(damage_per_tick, duration_remaining, tick_rate, last_tick_time) { return { name: 'FireZoneComponent', damage_per_tick: damage_per_tick || 0, duration_remaining: duration_remaining || 0, tick_rate: tick_rate || 1.0, last_tick_time: last_tick_time || 0 }; } }; Game.Systems = { MovementSystem: { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); var frame_time = 1 / 60; // Assuming 60 FPS var goal = { x: 39, y: 30 }; // The goal grid cell for (var i = 0; i < movableEntities.length; i++) { var entityId = movableEntities[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][entityId]; var movement = Game.EntityManager.componentStores['MovementComponent'][entityId]; var currentSpeed = movement.speed; // Check for slow debuff (no changes here) var slowDebuff = Game.EntityManager.componentStores['SlowDebuffComponent'] ? Game.EntityManager.componentStores['SlowDebuffComponent'][entityId] : null; if (slowDebuff) { currentSpeed = movement.speed * (1 - slowDebuff.slow_percentage); slowDebuff.duration_remaining -= frame_time; if (slowDebuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['SlowDebuffComponent'][entityId]; } } // --- START OF NEW FLOW FIELD LOGIC --- // 1. Get the direction vector from the flow field at the enemy's current position. var direction = Game.PathManager.getVectorAt(transform.x, transform.y); // 2. Calculate how far to move this frame. var moveDistance = currentSpeed * frame_time; // 3. Update the enemy's position. transform.x += direction.x * moveDistance; transform.y += direction.y * moveDistance; // 4. Check if the enemy has reached the goal. var gridX = Math.floor(transform.x / 64); var gridY = Math.floor(transform.y / 64); if (gridX === goal.x && gridY === goal.y) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } // --- END OF NEW FLOW FIELD LOGIC --- } } }, RenderSystem: { displayObjects: {}, update: function update() { var renderableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'RenderComponent']); for (var i = 0; i < renderableEntities.length; i++) { var entityId = renderableEntities[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][entityId]; var render = Game.EntityManager.componentStores['RenderComponent'][entityId]; var sprite = this.displayObjects[entityId]; if (!sprite) { sprite = gameWorld.attachAsset(render.sprite_id, {}); // --- NEW ANCHOR LOGIC --- // This is the critical fix: Ensure all game entities are visually centered. sprite.anchor.set(0.5, 0.5); // --- END NEW ANCHOR LOGIC --- this.displayObjects[entityId] = sprite; } sprite.x = transform.x; sprite.y = transform.y; sprite.visible = render.is_visible; } } }, TowerBuildSystem: { tryBuildAt: function tryBuildAt(gridX, gridY, towerType) { // 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; cellSprite.down = function () { // Only process true clicks, not drags var timeSinceMouseDown = LK.ticks / 60 * 1000 - mouseDownTime; if (timeSinceMouseDown > 200) { return; } var dx = Math.abs(lastMouseX - dragStartX); var dy = Math.abs(lastMouseY - dragStartY); if (dx > 10 || dy > 10) { return; } var gridX = this.gridX; var gridY = this.gridY; var cellState = Game.GridManager.grid[gridX][gridY]; // If we are currently in build mode... if (Game.GameplayManager.activeBuildType) { if (Game.GridManager.isBuildable(gridX, gridY)) { // ...and we click a buildable spot, show the confirmation dialog. Game.GameplayManager.isConfirmingPlacement = true; Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; // Position the confirmation panel relative to the screen, not the world var screenX = gridX * 64 + 32 - cameraX; var screenY = gridY * 64 + 32 - cameraY; confirmPanel.x = screenX; confirmPanel.y = screenY; confirmPanel.visible = true; // Temporarily exit active build mode while confirming Game.GameplayManager.activeBuildType = null; } else { // Clicked an invalid spot, so cancel build mode Game.GameplayManager.activeBuildType = null; } } else if (!Game.GameplayManager.isConfirmingPlacement) { // If not in build or confirm mode, the click is for selection. 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; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else { Game.GameplayManager.selectedTowerId = -1; } } }; } } // Highlight the path cells if (Game.PathManager.currentPath) { for (var i = 0; i < Game.PathManager.currentPath.length; i++) { var pathNode = Game.PathManager.currentPath[i]; var pathSprite = gameWorld.attachAsset('path_cell', {}); // Note: PathManager coordinates are already in pixels pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } } // === SECTION: GLOBAL EVENT BUS === Game.EventBus = { listeners: {}, subscribe: function subscribe(eventType, listener) { if (!this.listeners[eventType]) { this.listeners[eventType] = []; } this.listeners[eventType].push(listener); }, unsubscribe: function unsubscribe(eventType, listener) { if (this.listeners[eventType]) { var index = this.listeners[eventType].indexOf(listener); if (index !== -1) { this.listeners[eventType].splice(index, 1); } } }, publish: function publish(event) { if (this.listeners[event.type]) { for (var i = 0; i < this.listeners[event.type].length; i++) { this.listeners[event.type][i](event); } } } }; Game.Events = { EnemyReachedEndEvent: function EnemyReachedEndEvent(enemy_id) { return { type: 'EnemyReachedEnd', enemy_id: enemy_id }; }, EnemyKilledEvent: function EnemyKilledEvent(enemy_id, gold_value) { return { type: 'EnemyKilled', enemy_id: enemy_id, gold_value: gold_value }; }, TowerPlacedEvent: function TowerPlacedEvent(tower_id, cost, position_x, position_y) { return { type: 'TowerPlaced', tower_id: tower_id, cost: cost, position_x: position_x, position_y: position_y }; }, TowerSoldEvent: function TowerSoldEvent(tower_id, refund_value) { return { type: 'TowerSold', tower_id: tower_id, refund_value: refund_value }; }, PowerStatusChangedEvent: function PowerStatusChangedEvent(new_status) { return { type: 'PowerStatusChanged', new_status: new_status }; }, WaveCompletedEvent: function WaveCompletedEvent(wave_number) { return { type: 'WaveCompleted', wave_number: wave_number }; }, GameOverEvent: function GameOverEvent() { return { type: 'GameOver' }; } }; // === SECTION: STACK-BASED GAME STATE MANAGER === // === SECTION: DIRECT GAME INITIALIZATION === // Initialize managers Game.GridManager.init(40, 60); Game.GridManager.grid[0][30] = Game.GridManager.CELL_STATES.PATH; // Set spawn point as unbuildable Game.ResourceManager.init(); Game.PathManager.init(40, 60); drawGrid(); // Create the Heads-Up Display (HUD) var goldText = new Text2('Gold: 100', { size: 40, fill: 0xFFD700 }); goldText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(goldText); goldText.x = -20; // 20px padding from the right edge goldText.y = 20; // 20px padding from the top edge var livesText = new Text2('Lives: 20', { size: 40, fill: 0xFF4136 }); livesText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(livesText); livesText.x = -20; // 20px padding from the right edge livesText.y = 70; // Position below the gold text var waveText = new Text2('Wave: 1 / 5', { size: 40, fill: 0xFFFFFF }); waveText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(waveText); waveText.x = -20; // 20px padding from the right edge waveText.y = 120; // Position below the lives text // Create Power Meter UI var powerMeterContainer = new Container(); LK.gui.topRight.addChild(powerMeterContainer); powerMeterContainer.x = -320; // Positioned in from the right edge powerMeterContainer.y = 170; // Position below wave text // Create a Graphics instance for power production bar var Graphics = function Graphics() { var graphics = new Container(); graphics.currentFill = 0xffffff; graphics.currentAlpha = 1.0; graphics.beginFill = function (color, alpha) { this.currentFill = color; this.currentAlpha = alpha !== undefined ? alpha : 1.0; }; graphics.drawRect = function (x, y, width, height) { var rect = game.attachAsset('grid_cell', {}); rect.tint = this.currentFill; rect.alpha = this.currentAlpha; rect.x = x; rect.y = y; rect.width = width; rect.height = height; this.addChild(rect); }; graphics.endFill = function () { // No-op for compatibility }; graphics.clear = function () { // Remove all children to clear the graphics while (this.children.length > 0) { var child = this.children[0]; this.removeChild(child); if (child.destroy) { child.destroy(); } } }; return graphics; }; // Background bar representing total production capacity var powerProductionBar = new Graphics(); powerProductionBar.beginFill(0x3498db, 0.5); // Semi-transparent blue powerProductionBar.drawRect(0, 0, 300, 20); // Placeholder width powerProductionBar.endFill(); powerMeterContainer.addChild(powerProductionBar); // Foreground bar representing current consumption var powerConsumptionBar = new Graphics(); powerConsumptionBar.beginFill(0x2ecc71); // Green for surplus powerConsumptionBar.drawRect(0, 0, 10, 20); // Placeholder width powerConsumptionBar.endFill(); powerMeterContainer.addChild(powerConsumptionBar); // Secondary bar for the available power buffer (capacitors) var powerAvailableBar = new Graphics(); powerAvailableBar.beginFill(0x3498db, 0.3); // Fainter blue powerAvailableBar.drawRect(0, 0, 5, 20); // Placeholder width, starts at the end of the production bar powerAvailableBar.endFill(); powerMeterContainer.addChild(powerAvailableBar); // Create Start Wave button var startWaveButton = new Text2('START NEXT WAVE', { size: 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: BUILD AND CONFIRMATION UI === // -- Ghost Tower (for placement preview) -- var ghostTowerSprite = null; // -- Build Panel (at the bottom of the screen) -- var buildPanel = new Container(); LK.gui.bottom.addChild(buildPanel); buildPanel.x = 0; buildPanel.y = -120; // Positioned to not overlap the "Start Wave" button // -- Tooltip (appears over build icons) -- var buildTooltip = new Container(); LK.gui.bottom.addChild(buildTooltip); // Add to the same GUI layer buildTooltip.visible = false; var tooltipBackground = new Graphics(); tooltipBackground.beginFill(0x000000, 0.8); tooltipBackground.drawRect(0, 0, 200, 60); buildTooltip.addChild(tooltipBackground); var tooltipText = new Text2('', { size: 24, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 190 }); tooltipText.anchor.set(0.5, 0); tooltipText.x = 100; tooltipText.y = 5; buildTooltip.addChild(tooltipText); // -- Confirmation Panel (appears on the game map) -- var confirmPanel = new Container(); LK.gui.center.addChild(confirmPanel); // Add to the center GUI layer to draw on top confirmPanel.visible = false; var confirmButton = game.attachAsset('arrow_tower', { tint: 0x2ECC71 }); // Green checkmark confirmButton.width = 48; confirmButton.height = 48; confirmButton.x = -32; confirmButton.anchor.set(0.5, 0.5); confirmPanel.addChild(confirmButton); var cancelButton = game.attachAsset('arrow_tower', { tint: 0xE74C3C }); // Red X cancelButton.width = 48; cancelButton.height = 48; cancelButton.x = 32; cancelButton.anchor.set(0.5, 0.5); confirmPanel.addChild(cancelButton); // -- Build Button Creation Logic -- var buildButtonData = [{ type: 'ArrowTower', sprite: 'arrow_tower' }, { type: 'GeneratorTower', sprite: 'generator_tower' }, { type: 'CapacitorTower', sprite: 'capacitor_tower' }, { type: 'RockLauncher', sprite: 'rock_launcher_tower' }, { type: 'ChemicalTower', sprite: 'chemical_tower' }, { type: 'SlowTower', sprite: 'slow_tower' }, { type: 'DarkTower', sprite: 'dark_tower' }, { type: 'GraveyardTower', sprite: 'graveyard_tower' }]; var buttonSize = 64; var buttonPadding = 10; var totalWidth = buildButtonData.length * buttonSize + (buildButtonData.length - 1) * buttonPadding; var startX = -totalWidth / 2; for (var i = 0; i < buildButtonData.length; i++) { var data = buildButtonData[i]; var button = game.attachAsset(data.sprite, {}); button.width = buttonSize; button.height = buttonSize; button.anchor.set(0.5, 0.5); button.x = startX + i * (buttonSize + buttonPadding) + buttonSize / 2; button.y = 0; // Custom property to store tower type button.towerType = data.type; button.down = function () { // If there's an old ghost, destroy it if (ghostTowerSprite) { ghostTowerSprite.destroy(); } // Create a new ghost sprite using the correct asset ID ghostTowerSprite = gameWorld.attachAsset(this.towerType, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.visible = false; // It will be made visible in the update loop // Set the game state Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; Game.GameplayManager.lastClickedButton = this; }; // Show tooltip on hover (move) button.move = function () { var towerData = Game.TowerData[this.towerType]; tooltipText.setText(this.towerType + "\nCost: " + towerData.cost); buildTooltip.x = buildPanel.x + this.x - 100; // Position tooltip relative to button buildTooltip.y = buildPanel.y - 60 - 10; // Position above the button buildTooltip.visible = true; }; buildPanel.addChild(button); } // Hide tooltip when mouse leaves the panel area buildPanel.out = function () { buildTooltip.visible = false; }; // -- Confirmation Button Logic -- confirmButton.down = function () { if (Game.GameplayManager.isConfirmingPlacement) { var coords = Game.GameplayManager.confirmPlacementCoords; // Get the build type from the LAST button the player clicked. This is the key fix. var buildType = Game.GameplayManager.lastClickedButton ? Game.GameplayManager.lastClickedButton.towerType : null; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } // Reset all states after confirming Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; Game.GameplayManager.lastClickedButton = null; confirmPanel.visible = false; } }; cancelButton.down = function () { // Reset all states after canceling Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; Game.GameplayManager.lastClickedButton = null; confirmPanel.visible = false; }; // === SECTION: MINIMAP UI === var minimapContainer = new Container(); LK.gui.bottomLeft.addChild(minimapContainer); minimapContainer.x = 20; // 20px padding from the left edge minimapContainer.y = -320; // Position up from bottom edge (negative value moves up in bottomLeft anchor) var minimapBackground = new Graphics(); minimapContainer.addChild(minimapBackground); // Function to draw the static parts of the minimap function drawMinimapBackground() { var mapWidth = 40; var mapHeight = 60; var tilePixelSize = 5; // Each grid cell is a 5x5 pixel block on the minimap var colors = { EMPTY: 0x3d2b1f, // Dark Brown for buildable land PATH: 0x8B4513, // SaddleBrown for the path TOWER: 0xFFFFFF, // White (will be used for dynamic tower dots later) BLOCKED: 0x111111 // Almost black for blocked areas }; minimapBackground.clear(); minimapBackground.beginFill(0x000000, 0.5); // Semi-transparent black background for the minimap minimapBackground.drawRect(0, 0, mapWidth * tilePixelSize, mapHeight * tilePixelSize); minimapBackground.endFill(); for (var x = 0; x < mapWidth; x++) { for (var y = 0; y < mapHeight; y++) { var cellState = Game.GridManager.grid[x][y]; var color; switch (cellState) { case Game.GridManager.CELL_STATES.PATH: color = colors.PATH; break; case Game.GridManager.CELL_STATES.BLOCKED: color = colors.BLOCKED; break; default: color = colors.EMPTY; break; } minimapBackground.beginFill(color); minimapBackground.drawRect(x * tilePixelSize, y * tilePixelSize, tilePixelSize, tilePixelSize); minimapBackground.endFill(); } } } // Call the function once to draw the initial state drawMinimapBackground(); var minimapDynamicLayer = new Graphics(); minimapContainer.addChild(minimapDynamicLayer); var minimapViewportRect = new Graphics(); minimapContainer.addChild(minimapViewportRect); minimapContainer.down = function (x, y, obj) { // x and y are the click coordinates relative to the minimap's top-left corner. // 1. Define map and minimap dimensions for conversion. var mapWorldWidth = 40 * 64; // 2560 var mapWorldHeight = 60 * 64; // 3840 var minimapPixelWidth = 40 * 5; // 200 var minimapPixelHeight = 60 * 5; // 300 // 2. Convert minimap click coordinates to world coordinates. // This gives us the point in the world the player wants to look at. var targetWorldX = x / minimapPixelWidth * mapWorldWidth; var targetWorldY = y / minimapPixelHeight * mapWorldHeight; // 3. Calculate the new camera position to CENTER the view on the target. var screenWidth = 2048; var screenHeight = 2732; var newCameraX = -targetWorldX + screenWidth / 2; var newCameraY = -targetWorldY + screenHeight / 2; // 4. Clamp the new camera position to the map boundaries. // This reuses the same clamping logic from the drag-to-pan feature. var minX = screenWidth - mapWorldWidth; var minY = screenHeight - mapWorldHeight; var maxX = 0; var maxY = 0; cameraX = Math.max(minX, Math.min(maxX, newCameraX)); cameraY = Math.max(minY, Math.min(maxY, newCameraY)); // 5. Apply the final, clamped transform to the game world. gameWorld.x = cameraX; gameWorld.y = cameraY; }; function updateMinimapDynamic() { minimapDynamicLayer.clear(); var tilePixelSize = 5; // Draw Towers var towers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); for (var i = 0; i < towers.length; i++) { var towerId = towers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var gridX = Math.floor(transform.x / 64); var gridY = Math.floor(transform.y / 64); minimapDynamicLayer.beginFill(0xFFFFFF); // White dot for towers minimapDynamicLayer.drawRect(gridX * tilePixelSize, gridY * tilePixelSize, tilePixelSize, tilePixelSize); minimapDynamicLayer.endFill(); } // Draw Enemies var enemies = Game.EntityManager.getEntitiesWithComponents(['EnemyComponent', 'TransformComponent']); for (var i = 0; i < enemies.length; i++) { var enemyId = enemies[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; // Use the enemy's precise world coordinates for smoother tracking on the minimap var minimapX = transform.x / 64 * tilePixelSize; var minimapY = transform.y / 64 * tilePixelSize; minimapDynamicLayer.beginFill(0xFF0000); // Red dot for enemies minimapDynamicLayer.drawRect(minimapX, minimapY, 3, 3); // Make them slightly smaller than a tile minimapDynamicLayer.endFill(); } } function updateMinimapViewport() { minimapViewportRect.clear(); var tilePixelSize = 5; var worldTileSize = 64; // The cameraX/Y variables are inverse offsets. We need the true top-left coordinate. var trueCameraX = -cameraX; var trueCameraY = -cameraY; // Get screen/viewport dimensions var screenWidth = 2048; // Use the game's actual screen dimensions var screenHeight = 2732; // Convert world coordinates and dimensions to minimap scale var rectX = trueCameraX / worldTileSize * tilePixelSize; var rectY = trueCameraY / worldTileSize * tilePixelSize; var rectWidth = screenWidth / worldTileSize * tilePixelSize; var rectHeight = screenHeight / worldTileSize * tilePixelSize; minimapViewportRect.beginFill(0xFFFFFF, 0.25); // Semi-transparent white minimapViewportRect.drawRect(rectX, rectY, rectWidth, rectHeight); minimapViewportRect.endFill(); } // Add mouse event handlers for camera panning game.down = function (x, y, obj) { isDragging = true; dragStartX = x; dragStartY = y; lastMouseX = x; lastMouseY = y; cameraStartX = cameraX; cameraStartY = cameraY; mouseDownTime = LK.ticks / 60 * 1000; // Convert to milliseconds }; game.move = function (x, y, obj) { lastMouseX = x; lastMouseY = y; if (isDragging) { var dx = x - dragStartX; var dy = y - dragStartY; // Update camera position (inverted for drag effect) cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; // 40 tiles * 64 pixels per tile var mapHeight = 60 * 64; // 60 tiles * 64 pixels per tile var screenWidth = 2048; var screenHeight = 2732; // Ensure we never see beyond the map edges var minX = screenWidth - mapWidth; var minY = screenHeight - mapHeight; var maxX = 0; var maxY = 0; cameraX = Math.max(minX, Math.min(maxX, cameraX)); cameraY = Math.max(minY, Math.min(maxY, cameraY)); // Apply camera transform to game world gameWorld.x = cameraX; gameWorld.y = cameraY; } }; game.up = function (x, y, obj) { isDragging = false; }; // Set up main game loop var powerUpdateTimer = 0; game.update = function () { if (Game.GameManager.isGameOver) { return; } 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 --- // Hide all contextual UI by default each frame if (ghostTowerSprite) { ghostTowerSprite.visible = false; } buildTooltip.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; if (Game.GameplayManager.activeBuildType) { // STATE: Player has selected a tower and is choosing a location. if (ghostTowerSprite) { // Use the reliable lastMouseX/Y variables updated by the engine's move handler var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.visible = true; // --- GHOST SPRITE FIX --- // Update the texture and reset tint every frame to ensure it's correct var towerData = Game.TowerData[Game.GameplayManager.activeBuildType]; if (towerData) { ghostTowerSprite.texture = LK.assets[towerData.sprite_id].texture; ghostTowerSprite.tint = Game.GridManager.isBuildable(gridX, gridY) ? 0xFFFFFF : 0xFF5555; } // --- END FIX --- ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; } } else if (Game.GameplayManager.isConfirmingPlacement) { // STATE: Player has clicked a location and needs to confirm. confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // STATE: Player has selected an existing tower to upgrade/sell. var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId]; var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!renderComp || !towerComp) { Game.GameplayManager.selectedTowerId = -1; // Tower was sold or destroyed } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } 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 + ")"); } } // 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(); updateMinimapViewport(); updateMinimapViewport(); // 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,10 +7,10 @@
/****
* Game Code
****/
-// === SECTION: GLOBAL NAMESPACE ===
// === SECTION: ASSETS ===
+// === SECTION: GLOBAL NAMESPACE ===
var Game = {};
// Camera state
var cameraX = 0;
var cameraY = 0;
@@ -2012,28 +2012,34 @@
Game.Systems.MovementSystem.update();
Game.Systems.RenderSystem.update();
// --- UI UPDATE LOGIC ---
// Hide all contextual UI by default each frame
- if (ghostTowerSprite) ghostTowerSprite.visible = false;
+ if (ghostTowerSprite) {
+ ghostTowerSprite.visible = false;
+ }
buildTooltip.visible = false;
confirmPanel.visible = false;
upgradePanel.visible = false;
rangeCircle.visible = false;
if (Game.GameplayManager.activeBuildType) {
// STATE: Player has selected a tower and is choosing a location.
- var towerData = Game.TowerData[Game.GameplayManager.activeBuildType];
if (ghostTowerSprite) {
- // Show ghost tower following the mouse
- var safeCameraX = typeof cameraX === "number" && !isNaN(cameraX) ? cameraX : 0;
- var safeCameraY = typeof cameraY === "number" && !isNaN(cameraY) ? cameraY : 0;
- var mouseWorldX = LK.input.x - safeCameraX;
- var mouseWorldY = LK.input.y - safeCameraY;
+ // Use the reliable lastMouseX/Y variables updated by the engine's move handler
+ var mouseWorldX = lastMouseX - cameraX;
+ var mouseWorldY = lastMouseY - cameraY;
var gridX = Math.floor(mouseWorldX / 64);
var gridY = Math.floor(mouseWorldY / 64);
ghostTowerSprite.visible = true;
+ // --- GHOST SPRITE FIX ---
+ // Update the texture and reset tint every frame to ensure it's correct
+ var towerData = Game.TowerData[Game.GameplayManager.activeBuildType];
+ if (towerData) {
+ ghostTowerSprite.texture = LK.assets[towerData.sprite_id].texture;
+ ghostTowerSprite.tint = Game.GridManager.isBuildable(gridX, gridY) ? 0xFFFFFF : 0xFF5555;
+ }
+ // --- END FIX ---
ghostTowerSprite.x = gridX * 64 + 32;
ghostTowerSprite.y = gridY * 64 + 32;
- ghostTowerSprite.tint = Game.GridManager.isBuildable(gridX, gridY) ? 0xFFFFFF : 0xFF5555;
}
} else if (Game.GameplayManager.isConfirmingPlacement) {
// STATE: Player has clicked a location and needs to confirm.
confirmPanel.visible = true;