User prompt
Ava, let's make the confirmation buttons much larger and easier to tap. We only need to modify the game.update function. Replace the entire function with this new version, which increases the size of the confirm/cancel buttons and adjusts their spacing. game.update = function () { if (Game.GameManager.isGameOver) { return; } // Run core game systems Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); // --- UI STATE MACHINE FOR DRAG-AND-DROP --- var currentBuildType = Game.GameplayManager.activeBuildType; // Check if the player is currently dragging a tower to build if (currentBuildType && !Game.GameplayManager.isConfirmingPlacement) { // --- STATE: DRAGGING TOWER --- var towerData = Game.TowerData[currentBuildType]; // Ensure UI for other states is hidden confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update the ghost tower sprite that follows the cursor/finger if (!ghostTowerSprite || ghostTowerSprite.towerType !== currentBuildType) { if (ghostTowerSprite) ghostTowerSprite.destroy(); ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = currentBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // Update and show the fixed info panel floatingInfoPanel.visible = true; infoText.setText(currentBuildType + "\nCost: " + towerData.cost); } else if (Game.GameplayManager.isConfirmingPlacement) { // --- STATE: AWAITING CONFIRMATION --- // Hide other UI elements if (ghostTowerSprite) ghostTowerSprite.visible = false; floatingInfoPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update confirmation buttons if needed if (!confirmButton || confirmButton.towerType !== currentBuildType) { if (confirmButton) confirmButton.destroy(); if (cancelButton) cancelButton.destroy(); var towerData = Game.TowerData[currentBuildType]; // Create CONFIRM button confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 120; // Increased size confirmButton.height = 120; // Increased size confirmButton.x = -65; // Adjusted spacing confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = currentBuildType; confirmButton.down = function () { var coords = Game.GameplayManager.confirmPlacementCoords; var buildType = Game.GameplayManager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); // Create CANCEL button cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 120; // Increased size cancelButton.height = 120; // Increased size cancelButton.x = 65; // Adjusted spacing cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = currentBuildType; cancelButton.down = function () { // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // --- STATE: TOWER SELECTED --- // Hide all build-related UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; floatingInfoPanel.visible = false; // Show upgrade panel var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'] ? Game.EntityManager.componentStores['RenderComponent'][towerId] : null; var towerComp = Game.EntityManager.componentStores['TowerComponent'] ? Game.EntityManager.componentStores['TowerComponent'][towerId] : null; var attackComp = Game.EntityManager.componentStores['AttackComponent'] ? Game.EntityManager.componentStores['AttackComponent'][towerId] : null; var transformComp = Game.EntityManager.componentStores['TransformComponent'] ? Game.EntityManager.componentStores['TransformComponent'][towerId] : null; if (!renderComp || !towerComp || !transformComp) { Game.GameplayManager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } else { rangeCircle.visible = false; } var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === renderComp.sprite_id) { towerType = type; break; } } towerNameText.setText(towerType + " (Lvl " + towerComp.upgrade_level + ")"); var upgradePathAData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathA'] : null; if (upgradePathAData && towerComp.upgrade_path_A_level < upgradePathAData.length) { var upgradeInfoA = upgradePathAData[towerComp.upgrade_path_A_level]; upgradeAPathButton.setText("Upgrade Path A ($" + upgradeInfoA.cost + ")"); upgradeAPathButton.visible = true; } else { upgradeAPathButton.visible = false; } var upgradePathBData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathB'] : null; if (upgradePathBData && towerComp.upgrade_path_B_level < upgradePathBData.length) { var upgradeInfoB = upgradePathBData[towerComp.upgrade_path_B_level]; upgradeBPathButton.setText("Upgrade Path B ($" + upgradeInfoB.cost + ")"); upgradeBPathButton.visible = true; } else { upgradeBPathButton.visible = false; } var sellPrice = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { sellPrice = towerComp.cost; } sellButton.setText("Sell Tower ($" + sellPrice + ")"); } } else { // --- STATE: IDLE --- // Clean up all dynamic UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } floatingInfoPanel.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; } // The `game.up` event (releasing the mouse/finger) will handle the drop logic. game.up = function () { if (Game.GameplayManager.activeBuildType && !Game.GameplayManager.isConfirmingPlacement) { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); // If dropped on a valid spot, switch to confirmation state if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; confirmPanel.x = gridX * 64 + 32; confirmPanel.y = gridY * 64 + 32; } else { // Dropped on an invalid spot, cancel the build Game.GameplayManager.activeBuildType = null; } } isDragging = false; }; // Update standard HUD elements every frame goldText.setText('Gold: ' + Game.ResourceManager.gold); livesText.setText('Lives: ' + Game.ResourceManager.lives); waveText.setText('Wave: ' + (Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1) + ' / ' + Game.WaveData.length); // Update Power Meter, Minimap, etc. var p_prod = Game.ResourceManager.powerProduction; var p_cons = Game.ResourceManager.powerConsumption; var p_avail = Game.ResourceManager.powerAvailable; var isDeficit = p_cons > p_prod && p_avail <= 0; var barScale = 3; var productionWidth = p_prod * barScale; powerProductionBar.clear(); powerProductionBar.beginFill(0x5DADE2, 0.6); powerProductionBar.drawRect(0, 0, productionWidth, 20); powerProductionBar.endFill(); var consumptionWidth = Math.min(p_cons, p_prod) * barScale; var consumptionColor = 0x00FF7F; if (isDeficit) { consumptionColor = Math.floor(LK.ticks / 10) % 2 === 0 ? 0xFF4136 : 0xD9362D; } else if (p_prod > 0 && p_cons / p_prod >= 0.75) { consumptionColor = 0xFFD700; } powerConsumptionBar.clear(); powerConsumptionBar.beginFill(consumptionColor); powerConsumptionBar.drawRect(0, 0, consumptionWidth, 20); powerConsumptionBar.endFill(); var availableWidth = p_avail * barScale; powerAvailableBar.x = productionWidth; powerAvailableBar.clear(); powerAvailableBar.beginFill(0x5DADE2, 0.4); powerAvailableBar.drawRect(0, 0, availableWidth, 20); powerAvailableBar.endFill(); updateMinimapDynamic(); updateMinimapViewport(); powerUpdateTimer++; if (powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); };
User prompt
Ava, the info panel is in the right place horizontally, but it's too low and covering the build icons. Let's move it up. Find the "-- Floating Info Panel (now a fixed panel above the build bar) --" section and locate this line: floatingInfoPanel.y = -250; // Position it 250px up from the bottom edge Change the y value from -250 to -320 to move the panel higher and create a clear space: floatingInfoPanel.y = -320; // Position it 320px up from the bottom edge To ensure this is applied correctly, please replace the entire info panel block with the corrected version. Replace the entire "-- Floating Info Panel..." block with this: // -- Floating Info Panel (now a fixed panel above the build bar) -- var floatingInfoPanel = new Container(); LK.gui.bottom.addChild(floatingInfoPanel); // Attach to the bottom GUI anchor floatingInfoPanel.anchor.set(0.5, 1); // Anchor to its own bottom-center point floatingInfoPanel.x = 0; // Center horizontally floatingInfoPanel.y = -320; // Position it 320px up from the bottom edge floatingInfoPanel.visible = false; var infoBackground = new Graphics(); infoBackground.beginFill(0x000000, 0.8); infoBackground.drawRect(0, 0, 300, 80); infoBackground.endFill(); floatingInfoPanel.addChild(infoBackground); var infoText = new Text2('', { size: 30, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 290 }); infoText.anchor.set(0, 0); infoText.x = 5; infoText.y = 5; floatingInfoPanel.addChild(infoText);
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'set')' in or related to this line: 'floatingInfoPanel.anchor.set(0.5, 1); // Anchor to its own bottom-center point' Line Number: 1734
User prompt
Ava, we're making two final visual adjustments to polish the user experience. First, let's make the enemies larger so they are more prominent. Find this line of code at the top of your file: LK.init.image('enemy', {width:32, height:32, id:'686404d09478e64db129f435'}) And change the width and height from 32 to 50: LK.init.image('enemy', {width:50, height:50, id:'686404d09478e64db129f435'}) Second, let's move the tower info panel to a more intuitive, fixed location: centered, directly above the tower build bar at the bottom of the screen. Find the "-- Floating Info Panel" section and replace it entirely with this new version: // -- Floating Info Panel (now a fixed panel above the build bar) -- var floatingInfoPanel = new Container(); LK.gui.bottom.addChild(floatingInfoPanel); // Attach to the bottom GUI anchor floatingInfoPanel.anchor.set(0.5, 1); // Anchor to its own bottom-center point floatingInfoPanel.x = 0; // Center horizontally floatingInfoPanel.y = -250; // Position it 250px up from the bottom edge floatingInfoPanel.visible = false; var infoBackground = new Graphics(); infoBackground.beginFill(0x000000, 0.8); infoBackground.drawRect(0, 0, 300, 80); infoBackground.endFill(); floatingInfoPanel.addChild(infoBackground); var infoText = new Text2('', { size: 30, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 290 }); infoText.anchor.set(0, 0); infoText.x = 5; infoText.y = 5; floatingInfoPanel.addChild(infoText);
User prompt
Next, we must remove the code from the main game loop that was making the panel move. This will make its position static. Replace the entire game.update function with this final version. The only change is the removal of the two lines that updated the floatingInfoPanel's position. game.update = function () { if (Game.GameManager.isGameOver) { return; } // Run core game systems Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); // --- UI STATE MACHINE FOR DRAG-AND-DROP --- var currentBuildType = Game.GameplayManager.activeBuildType; // Check if the player is currently dragging a tower to build if (currentBuildType && !Game.GameplayManager.isConfirmingPlacement) { // --- STATE: DRAGGING TOWER --- var towerData = Game.TowerData[currentBuildType]; // Ensure UI for other states is hidden confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update the ghost tower sprite that follows the cursor/finger if (!ghostTowerSprite || ghostTowerSprite.towerType !== currentBuildType) { if (ghostTowerSprite) ghostTowerSprite.destroy(); ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = currentBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // Update and show the fixed info panel floatingInfoPanel.visible = true; infoText.setText(currentBuildType + "\nCost: " + towerData.cost); // The panel's position is now fixed and no longer updated here. } else if (Game.GameplayManager.isConfirmingPlacement) { // --- STATE: AWAITING CONFIRMATION --- // Hide other UI elements if (ghostTowerSprite) ghostTowerSprite.visible = false; floatingInfoPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update confirmation buttons if needed if (!confirmButton || confirmButton.towerType !== currentBuildType) { if (confirmButton) confirmButton.destroy(); if (cancelButton) cancelButton.destroy(); var towerData = Game.TowerData[currentBuildType]; // Create CONFIRM button confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 80; confirmButton.height = 80; confirmButton.x = -50; confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = currentBuildType; confirmButton.down = function () { var coords = Game.GameplayManager.confirmPlacementCoords; var buildType = Game.GameplayManager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); // Create CANCEL button cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 80; cancelButton.height = 80; cancelButton.x = 50; cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = currentBuildType; cancelButton.down = function () { // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // --- STATE: TOWER SELECTED --- // Hide all build-related UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; floatingInfoPanel.visible = false; // Show upgrade panel var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'] ? Game.EntityManager.componentStores['RenderComponent'][towerId] : null; var towerComp = Game.EntityManager.componentStores['TowerComponent'] ? Game.EntityManager.componentStores['TowerComponent'][towerId] : null; var attackComp = Game.EntityManager.componentStores['AttackComponent'] ? Game.EntityManager.componentStores['AttackComponent'][towerId] : null; var transformComp = Game.EntityManager.componentStores['TransformComponent'] ? Game.EntityManager.componentStores['TransformComponent'][towerId] : null; if (!renderComp || !towerComp || !transformComp) { Game.GameplayManager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } else { rangeCircle.visible = false; } var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === renderComp.sprite_id) { towerType = type; break; } } towerNameText.setText(towerType + " (Lvl " + towerComp.upgrade_level + ")"); var upgradePathAData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathA'] : null; if (upgradePathAData && towerComp.upgrade_path_A_level < upgradePathAData.length) { var upgradeInfoA = upgradePathAData[towerComp.upgrade_path_A_level]; upgradeAPathButton.setText("Upgrade Path A ($" + upgradeInfoA.cost + ")"); upgradeAPathButton.visible = true; } else { upgradeAPathButton.visible = false; } var upgradePathBData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathB'] : null; if (upgradePathBData && towerComp.upgrade_path_B_level < upgradePathBData.length) { var upgradeInfoB = upgradePathBData[towerComp.upgrade_path_B_level]; upgradeBPathButton.setText("Upgrade Path B ($" + upgradeInfoB.cost + ")"); upgradeBPathButton.visible = true; } else { upgradeBPathButton.visible = false; } var sellPrice = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { sellPrice = towerComp.cost; } sellButton.setText("Sell Tower ($" + sellPrice + ")"); } } else { // --- STATE: IDLE --- // Clean up all dynamic UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } floatingInfoPanel.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; } // The `game.up` event (releasing the mouse/finger) will handle the drop logic. game.up = function () { if (Game.GameplayManager.activeBuildType && !Game.GameplayManager.isConfirmingPlacement) { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); // If dropped on a valid spot, switch to confirmation state if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; confirmPanel.x = gridX * 64 + 32; confirmPanel.y = gridY * 64 + 32; } else { // Dropped on an invalid spot, cancel the build Game.GameplayManager.activeBuildType = null; } } isDragging = false; }; // Update standard HUD elements every frame goldText.setText('Gold: ' + Game.ResourceManager.gold); livesText.setText('Lives: ' + Game.ResourceManager.lives); waveText.setText('Wave: ' + (Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1) + ' / ' + Game.WaveData.length); // Update Power Meter, Minimap, etc. var p_prod = Game.ResourceManager.powerProduction; var p_cons = Game.ResourceManager.powerConsumption; var p_avail = Game.ResourceManager.powerAvailable; var isDeficit = p_cons > p_prod && p_avail <= 0; var barScale = 3; var productionWidth = p_prod * barScale; powerProductionBar.clear(); powerProductionBar.beginFill(0x5DADE2, 0.6); powerProductionBar.drawRect(0, 0, productionWidth, 20); powerProductionBar.endFill(); var consumptionWidth = Math.min(p_cons, p_prod) * barScale; var consumptionColor = 0x00FF7F; if (isDeficit) { consumptionColor = Math.floor(LK.ticks / 10) % 2 === 0 ? 0xFF4136 : 0xD9362D; } else if (p_prod > 0 && p_cons / p_prod >= 0.75) { consumptionColor = 0xFFD700; } powerConsumptionBar.clear(); powerConsumptionBar.beginFill(consumptionColor); powerConsumptionBar.drawRect(0, 0, consumptionWidth, 20); powerConsumptionBar.endFill(); var availableWidth = p_avail * barScale; powerAvailableBar.x = productionWidth; powerAvailableBar.clear(); powerAvailableBar.beginFill(0x5DADE2, 0.4); powerAvailableBar.drawRect(0, 0, availableWidth, 20); powerAvailableBar.endFill(); updateMinimapDynamic(); updateMinimapViewport(); powerUpdateTimer++; if (powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); };
User prompt
Ava, let's make one final UI polish. The floating info panel is distracting; we will convert it into a fixed panel in the top-left corner. First, let's change how the panel is created. We will attach it to the top-left UI anchor and give it a fixed position. Find the "-- Floating Info Panel" section and replace it entirely with this new code block: // -- Floating Info Panel (now a fixed panel in the top-left) -- var floatingInfoPanel = new Container(); LK.gui.topLeft.addChild(floatingInfoPanel); // Attach to the top-left GUI anchor floatingInfoPanel.x = 120; // Position it to the right of the pause button floatingInfoPanel.y = 20; // Position it near the top floatingInfoPanel.visible = false; var infoBackground = new Graphics(); infoBackground.beginFill(0x000000, 0.8); infoBackground.drawRect(0, 0, 300, 80); // Made slightly wider for readability infoBackground.endFill(); floatingInfoPanel.addChild(infoBackground); var infoText = new Text2('', { size: 30, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 290 }); infoText.anchor.set(0, 0); infoText.x = 5; infoText.y = 5; floatingInfoPanel.addChild(infoText);
User prompt
Second, we will fix the laggy floating info panel. This requires a small but critical change to the main game loop to ensure the panel's position is calculated correctly, independent of the camera. Replace the entire game.update function with this final version. The only change is to the positioning logic for the floatingInfoPanel. game.update = function () { if (Game.GameManager.isGameOver) { return; } // Run core game systems Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); // --- UI STATE MACHINE FOR DRAG-AND-DROP --- var currentBuildType = Game.GameplayManager.activeBuildType; // Check if the player is currently dragging a tower to build if (currentBuildType && !Game.GameplayManager.isConfirmingPlacement) { // --- STATE: DRAGGING TOWER --- var towerData = Game.TowerData[currentBuildType]; // Ensure UI for other states is hidden confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update the ghost tower sprite that follows the cursor/finger if (!ghostTowerSprite || ghostTowerSprite.towerType !== currentBuildType) { if (ghostTowerSprite) ghostTowerSprite.destroy(); ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = currentBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // Update and show the floating info panel floatingInfoPanel.visible = true; infoText.setText(currentBuildType + "\nCost: " + towerData.cost); // --- FIX: Position the panel relative to the raw mouse coordinates (lastMouseX/Y) --- // This makes it independent of the camera and prevents the lag/offset. floatingInfoPanel.x = lastMouseX - floatingInfoPanel.width / 2; floatingInfoPanel.y = lastMouseY - floatingInfoPanel.height - 20; } else if (Game.GameplayManager.isConfirmingPlacement) { // --- STATE: AWAITING CONFIRMATION --- // Hide other UI elements if (ghostTowerSprite) ghostTowerSprite.visible = false; floatingInfoPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update confirmation buttons if needed if (!confirmButton || confirmButton.towerType !== currentBuildType) { if (confirmButton) confirmButton.destroy(); if (cancelButton) cancelButton.destroy(); var towerData = Game.TowerData[currentBuildType]; // Create CONFIRM button confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 80; // Larger button confirmButton.height = 80; // Larger button confirmButton.x = -50; // Spaced further apart confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = currentBuildType; confirmButton.down = function () { var coords = Game.GameplayManager.confirmPlacementCoords; var buildType = Game.GameplayManager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); // Create CANCEL button cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 80; // Larger button cancelButton.height = 80; // Larger button cancelButton.x = 50; // Spaced further apart cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = currentBuildType; cancelButton.down = function () { // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // --- STATE: TOWER SELECTED --- // Hide all build-related UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; floatingInfoPanel.visible = false; // Show upgrade panel and range circle var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'] ? Game.EntityManager.componentStores['RenderComponent'][towerId] : null; var towerComp = Game.EntityManager.componentStores['TowerComponent'] ? Game.EntityManager.componentStores['TowerComponent'][towerId] : null; var attackComp = Game.EntityManager.componentStores['AttackComponent'] ? Game.EntityManager.componentStores['AttackComponent'][towerId] : null; var transformComp = Game.EntityManager.componentStores['TransformComponent'] ? Game.EntityManager.componentStores['TransformComponent'][towerId] : null; if (!renderComp || !towerComp || !transformComp) { Game.GameplayManager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } else { rangeCircle.visible = false; } var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === renderComp.sprite_id) { towerType = type; break; } } towerNameText.setText(towerType + " (Lvl " + towerComp.upgrade_level + ")"); var upgradePathAData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathA'] : null; if (upgradePathAData && towerComp.upgrade_path_A_level < upgradePathAData.length) { var upgradeInfoA = upgradePathAData[towerComp.upgrade_path_A_level]; upgradeAPathButton.setText("Upgrade Path A ($" + upgradeInfoA.cost + ")"); upgradeAPathButton.visible = true; } else { upgradeAPathButton.visible = false; } var upgradePathBData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathB'] : null; if (upgradePathBData && towerComp.upgrade_path_B_level < upgradePathBData.length) { var upgradeInfoB = upgradePathBData[towerComp.upgrade_path_B_level]; upgradeBPathButton.setText("Upgrade Path B ($" + upgradeInfoB.cost + ")"); upgradeBPathButton.visible = true; } else { upgradeBPathButton.visible = false; } var sellPrice = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { sellPrice = towerComp.cost; } sellButton.setText("Sell Tower ($" + sellPrice + ")"); } } else { // --- STATE: IDLE --- // Clean up all dynamic UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } floatingInfoPanel.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; } // The `game.up` event (releasing the mouse/finger) will handle the drop logic. game.up = function () { if (Game.GameplayManager.activeBuildType && !Game.GameplayManager.isConfirmingPlacement) { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); // If dropped on a valid spot, switch to confirmation state if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; confirmPanel.x = gridX * 64 + 32; confirmPanel.y = gridY * 64 + 32; } else { // Dropped on an invalid spot, cancel the build Game.GameplayManager.activeBuildType = null; } } isDragging = false; }; // Update standard HUD elements every frame goldText.setText('Gold: ' + Game.ResourceManager.gold); livesText.setText('Lives: ' + Game.ResourceManager.lives); waveText.setText('Wave: ' + (Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1) + ' / ' + Game.WaveData.length); // Update Power Meter, Minimap, etc. var p_prod = Game.ResourceManager.powerProduction; var p_cons = Game.ResourceManager.powerConsumption; var p_avail = Game.ResourceManager.powerAvailable; var isDeficit = p_cons > p_prod && p_avail <= 0; var barScale = 3; var productionWidth = p_prod * barScale; powerProductionBar.clear(); powerProductionBar.beginFill(0x5DADE2, 0.6); powerProductionBar.drawRect(0, 0, productionWidth, 20); powerProductionBar.endFill(); var consumptionWidth = Math.min(p_cons, p_prod) * barScale; var consumptionColor = 0x00FF7F; if (isDeficit) { consumptionColor = Math.floor(LK.ticks / 10) % 2 === 0 ? 0xFF4136 : 0xD9362D; } else if (p_prod > 0 && p_cons / p_prod >= 0.75) { consumptionColor = 0xFFD700; } powerConsumptionBar.clear(); powerConsumptionBar.beginFill(consumptionColor); powerConsumptionBar.drawRect(0, 0, consumptionWidth, 20); powerConsumptionBar.endFill(); var availableWidth = p_avail * barScale; powerAvailableBar.x = productionWidth; powerAvailableBar.clear(); powerAvailableBar.beginFill(0x5DADE2, 0.4); powerAvailableBar.drawRect(0, 0, availableWidth, 20); powerAvailableBar.endFill(); updateMinimapDynamic(); updateMinimapViewport(); powerUpdateTimer++; if (powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); };
User prompt
Ava, the drag-and-drop is a great improvement, but the UI needs two final adjustments to be perfect. First, let's fix the Upgrade & Sell panel so it's fully visible. Find the // === SECTION: UPGRADE & SELL UI === section and replace the entire upgradePanel container creation with this new version. It changes the anchor point so it correctly positions itself from its right edge. // === SECTION: UPGRADE & SELL UI === var upgradePanel = new Container(); LK.gui.right.addChild(upgradePanel); upgradePanel.x = -50; // 50px padding from the right edge upgradePanel.y = 0; upgradePanel.visible = false; var towerNameText = new Text2('Tower Name', { size: 50, fill: 0xFFFFFF, align: 'right' // Align text to the right }); towerNameText.anchor.set(1, 0.5); // Anchor to the right upgradePanel.addChild(towerNameText); var upgradeAPathButton = new Text2('Upgrade Path A', { size: 45, fill: 0x27AE60, align: 'right' }); upgradeAPathButton.anchor.set(1, 0.5); // Anchor to the right upgradeAPathButton.y = 70; upgradePanel.addChild(upgradeAPathButton); var upgradeBPathButton = new Text2('Upgrade Path B', { size: 45, fill: 0x2980B9, align: 'right' }); upgradeBPathButton.anchor.set(1, 0.5); // Anchor to the right upgradeBPathButton.y = 140; upgradePanel.addChild(upgradeBPathButton); var sellButton = new Text2('Sell Tower', { size: 45, fill: 0xC0392B, align: 'right' }); sellButton.anchor.set(1, 0.5); // Anchor to the right sellButton.y = 210; upgradePanel.addChild(sellButton); sellButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId); } }; upgradeAPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathA'); } }; upgradeBPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathB'); } };
User prompt
Finally, we must replace the main game loop. This new game.update function is completely rewritten to handle the new drag-and-drop state, including showing the floating info panel, handling the drop to show the confirmation, and cleaning up correctly. Replace the entire game.update function with this new, definitive version: game.update = function () { if (Game.GameManager.isGameOver) { return; } // Run core game systems Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); // --- UI STATE MACHINE FOR DRAG-AND-DROP --- var currentBuildType = Game.GameplayManager.activeBuildType; // Check if the player is currently dragging a tower to build if (currentBuildType && !Game.GameplayManager.isConfirmingPlacement) { // --- STATE: DRAGGING TOWER --- var towerData = Game.TowerData[currentBuildType]; // Ensure UI for other states is hidden confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update the ghost tower sprite that follows the cursor/finger if (!ghostTowerSprite || ghostTowerSprite.towerType !== currentBuildType) { if (ghostTowerSprite) ghostTowerSprite.destroy(); ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = currentBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // Update and show the floating info panel floatingInfoPanel.visible = true; infoText.setText(currentBuildType + "\nCost: " + towerData.cost); // Position the panel above the cursor/finger floatingInfoPanel.x = lastMouseX - floatingInfoPanel.width / 2; floatingInfoPanel.y = lastMouseY - floatingInfoPanel.height - 20; } else if (Game.GameplayManager.isConfirmingPlacement) { // --- STATE: AWAITING CONFIRMATION --- // Hide other UI elements if (ghostTowerSprite) ghostTowerSprite.visible = false; floatingInfoPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update confirmation buttons if needed if (!confirmButton || confirmButton.towerType !== currentBuildType) { if (confirmButton) confirmButton.destroy(); if (cancelButton) cancelButton.destroy(); var towerData = Game.TowerData[currentBuildType]; // Create CONFIRM button confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 80; // Larger button confirmButton.height = 80; // Larger button confirmButton.x = -50; // Spaced further apart confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = currentBuildType; confirmButton.down = function () { var coords = Game.GameplayManager.confirmPlacementCoords; var buildType = Game.GameplayManager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); // Create CANCEL button cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 80; // Larger button cancelButton.height = 80; // Larger button cancelButton.x = 50; // Spaced further apart cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = currentBuildType; cancelButton.down = function () { // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // --- STATE: TOWER SELECTED --- // Hide all build-related UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; floatingInfoPanel.visible = false; // Show upgrade panel and range circle // (This logic remains the same) } else { // --- STATE: IDLE --- // Clean up all dynamic UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } floatingInfoPanel.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; } // The `game.up` event (releasing the mouse/finger) will handle the drop logic. game.up = function() { if (Game.GameplayManager.activeBuildType && !Game.GameplayManager.isConfirmingPlacement) { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); // If dropped on a valid spot, switch to confirmation state if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; confirmPanel.x = gridX * 64 + 32; confirmPanel.y = gridY * 64 + 32; } else { // Dropped on an invalid spot, cancel the build Game.GameplayManager.activeBuildType = null; } } isDragging = false; }; // Update standard HUD elements every frame goldText.setText('Gold: ' + Game.ResourceManager.gold); livesText.setText('Lives: ' + Game.ResourceManager.lives); waveText.setText('Wave: ' + (Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1) + ' / ' + Game.WaveData.length); // Update Power Meter, Minimap, etc. // ... (This logic remains the same) ... };
User prompt
Now, let's implement the core drag-and-drop logic. We'll replace the old build button logic with a new version that initiates the drag. Replace the entire "-- Build Button Creation Logic --" section with this new code: // -- Build Button Creation Logic (for Drag-and-Drop) -- var buildButtonData = [{ type: 'ArrowTower', sprite: 'arrow_tower' }, { type: 'GeneratorTower', sprite: 'generator_tower' }, { type: 'CapacitorTower', sprite: 'capacitor_tower' }, { type: 'RockLauncher', sprite: 'rock_launcher_tower' }, { type: 'ChemicalTower', sprite: 'chemical_tower' }, { type: 'SlowTower', sprite: 'slow_tower' }, { type: 'DarkTower', sprite: 'dark_tower' }, { type: 'GraveyardTower', sprite: 'graveyard_tower' }]; var buttonSize = 90; var buttonPadding = 15; var totalWidth = buildButtonData.length * buttonSize + (buildButtonData.length - 1) * buttonPadding; var startX = -totalWidth / 2; for (let i = 0; i < buildButtonData.length; i++) { const data = buildButtonData[i]; const button = game.attachAsset(data.sprite, {}); button.width = buttonSize; button.height = buttonSize; button.anchor.set(0.5, 0.5); button.x = startX + i * (buttonSize + buttonPadding) + buttonSize / 2; button.y = 0; button.towerType = data.type; // This is the start of the drag-and-drop button.down = function () { // Set the active build type to start the drag process Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; // Deselect any existing tower }; // We no longer need a 'move' handler here, as the main game loop will handle the drag. buildPanel.addChild(button); } // We no longer need the 'buildPanel.out' handler either. buildPanel.out = null;
User prompt
Please fix the bug: 'ReferenceError: buildTooltip is not defined' in or related to this line: 'buildTooltip.visible = false;' Line Number: 2153
User prompt
Next, we need to adapt our UI elements for the new drag-and-drop system. The "tooltip" will become a floating info panel. Find the "-- Tooltip" section and replace it entirely with this new "-- Floating Info Panel" section: // -- Floating Info Panel (replaces tooltip for drag-and-drop) -- var floatingInfoPanel = new Container(); LK.gui.addChild(floatingInfoPanel); // Add to the main GUI layer to float over everything floatingInfoPanel.visible = false; var infoBackground = new Graphics(); infoBackground.beginFill(0x000000, 0.8); infoBackground.drawRect(0, 0, 250, 80); // Made slightly larger infoBackground.endFill(); floatingInfoPanel.addChild(infoBackground); var infoText = new Text2('', { size: 30, // Larger font fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 240 }); infoText.anchor.set(0, 0); infoText.x = 5; infoText.y = 5; floatingInfoPanel.addChild(infoText);
User prompt
Ava, we are changing the build mechanic to a more intuitive drag-and-drop system. We'll also resize the towers to 150x150. First, update the tower sprite sizes. Replace the entire block of LK.init.image calls at the top of your code with this new block: LK.init.image('arrow_tower', {width:150, height:150, id:'6870070998c9580ed2873c36'}) LK.init.image('capacitor_tower', {width:150, height:150, id:'687007b598c9580ed2873c52'}) LK.init.image('chemical_tower', {width:150, height:150, id:'68700aba98c9580ed2873c83'}) LK.init.image('dark_tower', {width:150, height:150, id:'68700b5698c9580ed2873c99'}) LK.init.image('enemy', {width:32, height:32, id:'686404d09478e64db129f435'}) LK.init.image('generator_tower', {width:150, height:150, id:'6870077a98c9580ed2873c45'}) LK.init.image('graveyard_tower', {width:150, height:150, id:'68700b9e98c9580ed2873ca4'}) LK.init.image('rock_launcher_tower', {width:150, height:150, id:'687007f498c9580ed2873c61'}) LK.init.image('slow_tower', {width:150, height:150, id:'68700b0e98c9580ed2873c8c'})
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'sprite_id')' in or related to this line: 'confirmButton = game.attachAsset(towerData.sprite_id, {' Line Number: 2037
User prompt
Finally, let's adjust the UI element positions to fix the overlaps. Find the "-- Build Panel" section and change the buildPanel.y value: // -- Build Panel (at the bottom of the screen) -- var buildPanel = new Container(); LK.gui.bottom.addChild(buildPanel); buildPanel.x = 0; buildPanel.y = -150; // Positioned further up to not overlap the "Start Wave" button Now find the "-- Build Button Creation Logic --" section and replace the button.move function inside the for loop with this version, which places the tooltip higher: // Show tooltip on hover (move) button.move = function () { const 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 - 90 - 25; // Positioned higher to avoid overlap buildTooltip.visible = true; };
User prompt
Now we update the grid-clicking logic to work with the new grid system. This is much simpler now. Replace the entire cellSprite.down function inside the drawGrid function with this: cellSprite.down = function () { // Only process true clicks, not drags var timeSinceMouseDown = LK.ticks / 60 * 1000 - mouseDownTime; if (timeSinceMouseDown > 200) { return; } var dx = Math.abs(lastMouseX - dragStartX); var dy = Math.abs(lastMouseY - dragStartY); if (dx > 10 || dy > 10) { return; } var gridX = this.gridX; var gridY = this.gridY; var gridCell = Game.GridManager.grid[gridX][gridY]; if (Game.GameplayManager.activeBuildType) { // If in build mode, check if the 3x3 area is buildable if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; // The center of the build is the clicked cell Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; var worldX = gridX * 64 + 32; var worldY = gridY * 64 + 32; confirmPanel.x = worldX; confirmPanel.y = worldY; } else { // Clicked an invalid spot, so cancel build mode Game.GameplayManager.activeBuildType = null; } } else if (!Game.GameplayManager.isConfirmingPlacement) { // If not in build mode, check for selection if (gridCell.state === Game.GridManager.CELL_STATES.TOWER) { // The grid cell now directly tells us which tower is selected Game.GameplayManager.selectedTowerId = gridCell.entityId; } else { Game.GameplayManager.selectedTowerId = -1; } } };
User prompt
Now, we will update the build and sell systems to work with the 3x3 tower footprints. Replace the entire Game.Systems.TowerBuildSystem and Game.Systems.TowerSystem objects with these new versions: Game.Systems.TowerBuildSystem = { tryBuildAt: function tryBuildAt(gridX, gridY, towerType) { var towerData = Game.TowerData[towerType]; // 1. Use the new 3x3 build check if (!towerData || !Game.GridManager.isBuildable3x3(gridX, gridY)) { return; } if (!Game.ResourceManager.spendGold(towerData.cost)) { return; } // 2. Temporarily mark the 3x3 area on the grid for path validation for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { Game.GridManager.grid[gridX + dx][gridY + dy].state = Game.GridManager.CELL_STATES.TOWER; } } // 3. Regenerate flow field and validate path Game.PathManager.generateFlowField(); var spawnDistance = Game.PathManager.distanceField[0][30]; if (spawnDistance === Infinity) { // INVALID PLACEMENT: Revert the grid cells and refund gold for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { Game.GridManager.grid[gridX + dx][gridY + dy].state = Game.GridManager.CELL_STATES.EMPTY; } } Game.ResourceManager.addGold(towerData.cost); // Re-generate the flow field to its valid state Game.PathManager.generateFlowField(); return; } // --- VALID PLACEMENT --- var towerId = Game.EntityManager.createEntity(); // 4. Permanently mark the 3x3 grid area with the new tower's ID for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { Game.GridManager.grid[gridX + dx][gridY + dy].entityId = towerId; } } var centeredX = gridX * 64 + 32; var centeredY = gridY * 64 + 32; var transform = Game.Components.TransformComponent(centeredX, centeredY); Game.EntityManager.addComponent(towerId, transform); var render = Game.Components.RenderComponent(towerData.sprite_id, 2, 1.0, true); Game.EntityManager.addComponent(towerId, render); for (var i = 0; i < towerData.components.length; i++) { var componentDef = towerData.components[i]; var component; if (componentDef.name === 'TowerComponent') { var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; component = Game.Components.TowerComponent(componentDef.args[0], componentDef.args[1], currentWave); } else { component = Game.Components[componentDef.name].apply(null, componentDef.args); } Game.EntityManager.addComponent(towerId, component); } var event = Game.Events.TowerPlacedEvent(towerId, towerData.cost, transform.x, transform.y); Game.EventBus.publish(event); } }, Game.Systems.TowerSystem = { tryUpgradeTower: function tryUpgradeTower(towerId, path) { var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId]; if (!renderComp) { return; } var towerSpriteId = renderComp.sprite_id; var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === towerSpriteId) { towerType = type; break; } } if (!towerType) { return; } var upgradePathData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType][path] : null; if (!upgradePathData) { return; } var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; if (!towerComp) { return; } var nextUpgradeIndex = path === 'PathA' ? towerComp.upgrade_path_A_level : towerComp.upgrade_path_B_level; if (nextUpgradeIndex >= upgradePathData.length) { return; } var upgradeInfo = upgradePathData[nextUpgradeIndex]; if (!Game.ResourceManager.spendGold(upgradeInfo.cost)) { return; } for (var componentName in upgradeInfo.effects) { var componentToModify = Game.EntityManager.componentStores[componentName] ? Game.EntityManager.componentStores[componentName][towerId] : null; if (componentToModify) { var effectsToApply = upgradeInfo.effects[componentName]; for (var property in effectsToApply) { if (componentToModify.hasOwnProperty(property)) { componentToModify[property] += effectsToApply[property]; } } } } if (upgradeInfo.add_component_on_impact) { var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; if (attackComp) { if (!attackComp.on_impact_effects) { attackComp.on_impact_effects = []; } attackComp.on_impact_effects.push(upgradeInfo.add_component_on_impact); } } towerComp.upgrade_level++; if (path === 'PathA') { towerComp.upgrade_path_A_level++; } else { towerComp.upgrade_path_B_level++; } }, trySellTower: function trySellTower(towerId) { var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!towerComp || !transformComp) { return; } var refundAmount = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { refundAmount = towerComp.cost; } Game.ResourceManager.addGold(refundAmount); // Clear the tower's 3x3 footprint from the grid var gridX = Math.floor(transformComp.x / 64); var gridY = Math.floor(transformComp.y / 64); for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var x = gridX + dx; var y = gridY + dy; if (Game.GridManager.grid[x] && Game.GridManager.grid[x][y] !== undefined) { Game.GridManager.grid[x][y].state = Game.GridManager.CELL_STATES.EMPTY; Game.GridManager.grid[x][y].entityId = null; } } } Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount)); Game.GameplayManager.selectedTowerId = -1; Game.EntityManager.destroyEntity(towerId); } }
User prompt
Because we changed how the grid stores data, we must update the pathfinding system to read the new data structure. Replace the generateFlowField function inside Game.PathManager with this updated version: generateFlowField: function generateFlowField() { console.log("Generating new flow field..."); var grid = Game.GridManager.grid; var width = grid.length; var height = grid[0].length; // 1. Reset fields this.distanceField = Array(width).fill(null).map(function () { return Array(height).fill(Infinity); }); // 2. Integration Pass (BFS) var goal = { x: 39, y: 30 }; var queue = [goal]; this.distanceField[goal.x][goal.y] = 0; var head = 0; while (head < queue.length) { var current = queue[head++]; var neighbors = [{ x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: -1 }, { x: 0, y: 1 }]; // Orthogonal neighbors for (var i = 0; i < neighbors.length; i++) { var n = neighbors[i]; var nx = current.x + n.x; var ny = current.y + n.y; // Check bounds and if traversable (checks the .state property) if (nx >= 0 && nx < width && ny >= 0 && ny < height && grid[nx][ny].state !== Game.GridManager.CELL_STATES.TOWER && this.distanceField[nx][ny] === Infinity) { this.distanceField[nx][ny] = this.distanceField[current.x][current.y] + 1; queue.push({ x: nx, y: ny }); } } } // 3. Flow Pass for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { if (this.distanceField[x][y] === Infinity) { continue; } var bestDist = Infinity; var bestVector = { x: 0, y: 0 }; var allNeighbors = [{ x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: -1 }, { x: 1, y: -1 }, { x: -1, y: 1 }, { x: 1, y: 1 }]; // Include diagonals for flow for (var i = 0; i < allNeighbors.length; i++) { var n = allNeighbors[i]; var nx = x + n.x; var ny = y + n.y; if (nx >= 0 && nx < width && ny >= 0 && ny < height) { if (this.distanceField[nx][ny] < bestDist) { bestDist = this.distanceField[nx][ny]; bestVector = n; } } } this.flowField[x][y] = bestVector; } } }
User prompt
Next, to handle the larger tower footprints, we must upgrade the GridManager. It will now store both the state and the ID of any entity in a cell. This also includes a new function to check if a 3x3 area is buildable. Replace the entire Game.GridManager object with this new version: Game.GridManager = { grid: [], CELL_STATES: { EMPTY: 0, PATH: 1, BLOCKED: 2, TOWER: 3 }, init: function init(width, height) { this.grid = []; for (var x = 0; x < width; x++) { this.grid[x] = []; for (var y = 0; y < height; y++) { // Each cell is now an object storing state and entity ID this.grid[x][y] = { state: this.CELL_STATES.EMPTY, entityId: null }; } } }, isBuildable3x3: function isBuildable3x3(centerX, centerY) { // Checks a 3x3 area centered on the given coordinates for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var x = centerX + dx; var y = centerY + dy; if (x < 0 || x >= this.grid.length || y < 0 || y >= this.grid[0].length) { return false; // Out of bounds } if (this.grid[x][y].state !== this.CELL_STATES.EMPTY) { return false; // Cell is not empty } } } return true; } };
User prompt
Ava, we're making the game much more touch-friendly and fixing the pathing. This is a large update that refactors several core systems. First, let's make the tower sprites much larger, as requested. Replace the entire block of LK.init.image calls at the top of your code with this new block: LK.init.image('arrow_tower', {width:175, height:175, id:'6870070998c9580ed2873c36'}) LK.init.image('capacitor_tower', {width:175, height:175, id:'687007b598c9580ed2873c52'}) LK.init.image('chemical_tower', {width:175, height:175, id:'68700aba98c9580ed2873c83'}) LK.init.image('dark_tower', {width:175, height:175, id:'68700b5698c9580ed2873c99'}) LK.init.image('enemy', {width:32, height:32, id:'686404d09478e64db129f435'}) LK.init.image('generator_tower', {width:175, height:175, id:'6870077a98c9580ed2873c45'}) LK.init.image('graveyard_tower', {width:175, height:175, id:'68700b9e98c9580ed2873ca4'}) LK.init.image('rock_launcher_tower', {width:175, height:175, id:'687007f498c9580ed2873c61'}) LK.init.image('slow_tower', {width:175, height:175, id:'68700b0e98c9580ed2873c8c'})
/**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // === SECTION: GLOBAL NAMESPACE === // === SECTION: ASSETS === var Game = {}; // Camera state var cameraX = 0; var cameraY = 0; var isDragging = false; var dragStartX = 0; var dragStartY = 0; var cameraStartX = 0; var cameraStartY = 0; var lastMouseX = 0; var lastMouseY = 0; var mouseDownTime = 0; // === SECTION: GLOBAL MANAGERS === Game.ResourceManager = { gold: 500, lives: 20, powerProduction: 0, powerConsumption: 0, powerAvailable: 0, addGold: function addGold(amount) { this.gold += amount; }, spendGold: function spendGold(amount) { if (this.gold >= amount) { this.gold -= amount; return true; } ; return false; }, loseLife: function loseLife(amount) { this.lives -= amount; if (this.lives <= 0) { Game.EventBus.publish(Game.Events.GameOverEvent()); } }, updatePower: function updatePower() { // 1. Calculate total power production var totalProduction = 0; var productionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerProductionComponent']); for (var i = 0; i < productionEntities.length; i++) { var entityId = productionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerProductionComponent'][entityId]; totalProduction += powerComp.production_rate; } this.powerProduction = totalProduction; // 2. Calculate total power consumption var totalConsumption = 0; var consumptionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerConsumptionComponent']); for (var i = 0; i < consumptionEntities.length; i++) { var entityId = consumptionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerConsumptionComponent'][entityId]; totalConsumption += powerComp.drain_rate; } this.powerConsumption = totalConsumption; // 3. Calculate total storage capacity var totalCapacity = 0; var storageEntities = Game.EntityManager.getEntitiesWithComponents(['PowerStorageComponent']); for (var i = 0; i < storageEntities.length; i++) { var entityId = storageEntities[i]; var storageComp = Game.EntityManager.componentStores['PowerStorageComponent'][entityId]; totalCapacity += storageComp.capacity; } // 4. Evaluate power status and update available buffer var surplus = totalProduction - totalConsumption; if (surplus >= 0) { // We have enough or extra power this.powerAvailable += surplus; // Clamp the available power to the max capacity if (this.powerAvailable > totalCapacity) { this.powerAvailable = totalCapacity; } Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } else { // We have a deficit, drain from the buffer this.powerAvailable += surplus; // surplus is negative, so this subtracts if (this.powerAvailable < 0) { // Buffer is empty and we still have a deficit this.powerAvailable = 0; Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('deficit')); } else { // Deficit was covered by the buffer Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } } }, onEnemyKilled: function onEnemyKilled(event) { this.addGold(event.gold_value); }, onWaveCompleted: function onWaveCompleted(event) { this.addGold(50); }, init: function init() { Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('WaveCompleted', this.onWaveCompleted.bind(this)); } }; Game.GridManager = { grid: [], CELL_STATES: { EMPTY: 0, PATH: 1, BLOCKED: 2, TOWER: 3 }, init: function init(width, height) { this.grid = []; for (var x = 0; x < width; x++) { this.grid[x] = []; for (var y = 0; y < height; y++) { // Each cell is now an object storing state and entity ID this.grid[x][y] = { state: this.CELL_STATES.EMPTY, entityId: null }; } } }, isBuildable3x3: function isBuildable3x3(centerX, centerY) { // Checks a 3x3 area centered on the given coordinates for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var x = centerX + dx; var y = centerY + dy; if (x < 0 || x >= this.grid.length || y < 0 || y >= this.grid[0].length) { return false; // Out of bounds } if (this.grid[x][y].state !== this.CELL_STATES.EMPTY) { return false; // Cell is not empty } } } return true; }, isBuildable: function isBuildable(x, y) { if (x >= 0 && x < this.grid.length && y >= 0 && y < this.grid[x].length) { return this.grid[x][y].state === this.CELL_STATES.EMPTY; } return false; } }; Game.PathManager = { distanceField: [], flowField: [], init: function init(width, height) { this.distanceField = Array(width).fill(null).map(function () { return Array(height).fill(Infinity); }); this.flowField = Array(width).fill(null).map(function () { return Array(height).fill({ x: 0, y: 0 }); }); Game.EventBus.subscribe('TowerPlaced', this.generateFlowField.bind(this)); Game.EventBus.subscribe('TowerSold', this.generateFlowField.bind(this)); // Generate the initial field when the game starts this.generateFlowField(); }, generateFlowField: function generateFlowField() { console.log("Generating new flow field..."); var grid = Game.GridManager.grid; var width = grid.length; var height = grid[0].length; // 1. Reset fields this.distanceField = Array(width).fill(null).map(function () { return Array(height).fill(Infinity); }); // 2. Integration Pass (BFS) var goal = { x: 39, y: 30 }; var queue = [goal]; this.distanceField[goal.x][goal.y] = 0; var head = 0; while (head < queue.length) { var current = queue[head++]; var neighbors = [{ x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: -1 }, { x: 0, y: 1 }]; // Orthogonal neighbors for (var i = 0; i < neighbors.length; i++) { var n = neighbors[i]; var nx = current.x + n.x; var ny = current.y + n.y; // Check bounds and if traversable (checks the .state property) if (nx >= 0 && nx < width && ny >= 0 && ny < height && grid[nx][ny].state !== Game.GridManager.CELL_STATES.TOWER && this.distanceField[nx][ny] === Infinity) { this.distanceField[nx][ny] = this.distanceField[current.x][current.y] + 1; queue.push({ x: nx, y: ny }); } } } // 3. Flow Pass for (var x = 0; x < width; x++) { for (var y = 0; y < height; y++) { if (this.distanceField[x][y] === Infinity) { continue; } var bestDist = Infinity; var bestVector = { x: 0, y: 0 }; var allNeighbors = [{ x: -1, y: 0 }, { x: 1, y: 0 }, { x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: -1 }, { x: 1, y: -1 }, { x: -1, y: 1 }, { x: 1, y: 1 }]; // Include diagonals for flow for (var i = 0; i < allNeighbors.length; i++) { var n = allNeighbors[i]; var nx = x + n.x; var ny = y + n.y; if (nx >= 0 && nx < width && ny >= 0 && ny < height) { if (this.distanceField[nx][ny] < bestDist) { bestDist = this.distanceField[nx][ny]; bestVector = n; } } } this.flowField[x][y] = bestVector; } } // 4. Announce that the field has been updated // (We will add the event publication in a later step) }, getVectorAt: function getVectorAt(worldX, worldY) { var gridX = Math.floor(worldX / 64); var gridY = Math.floor(worldY / 64); if (this.flowField[gridX] && this.flowField[gridX][gridY]) { return this.flowField[gridX][gridY]; } return { x: 0, y: 0 }; // Default case } }; Game.GameplayManager = { selectedTowerId: -1, activeBuildType: null, // The tower type selected from the build panel (e.g., 'ArrowTower') isConfirmingPlacement: false, // True when the confirm/cancel dialog is shown confirmPlacementCoords: { x: null, y: null }, // Stores the grid coords for confirmation lastClickedButton: null, // Stores a reference to the UI button that was clicked enemiesSpawnedThisWave: 0, enemiesKilledThisWave: 0, enemiesLeakedThisWave: 0, totalEnemiesThisWave: 0, init: function init() { Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('EnemyReachedEnd', this.onEnemyReachedEnd.bind(this)); }, onEnemyKilled: function onEnemyKilled(event) { this.enemiesKilledThisWave++; if (this.enemiesKilledThisWave + this.enemiesLeakedThisWave >= this.totalEnemiesThisWave) { Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1)); this.enemiesKilledThisWave = 0; this.enemiesLeakedThisWave = 0; startWaveButton.visible = true; } }, onEnemyReachedEnd: function onEnemyReachedEnd(event) { this.enemiesLeakedThisWave++; Game.ResourceManager.loseLife(1); Game.EntityManager.destroyEntity(event.enemy_id); if (this.enemiesKilledThisWave + this.enemiesLeakedThisWave >= this.totalEnemiesThisWave) { Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1)); this.enemiesKilledThisWave = 0; this.enemiesLeakedThisWave = 0; startWaveButton.visible = true; } } }; Game.GameManager = { isGameOver: false, init: function init() { Game.EventBus.subscribe('GameOver', this.onGameOver.bind(this)); }, onGameOver: function onGameOver(event) { this.isGameOver = true; LK.showGameOver(); } }; Game.Debug = { logText: null, init: function init() { this.logText = new Text2('Debug Log Initialized', { size: 40, fill: 0x00FF00 }); // Green text this.logText.anchor.set(0, 1); // Anchor to bottom-left LK.gui.bottomLeft.addChild(this.logText); this.logText.x = 20; this.logText.y = -20; }, log: function log(message) { if (this.logText) { this.logText.setText(String(message)); } } }; // === SECTION: ENTITY-COMPONENT-SYSTEM SCAFFOLDING === Game.EntityManager = { nextEntityId: 1, activeEntities: [], componentStores: {}, entitiesToDestroy: [], createEntity: function createEntity() { var entityId = this.nextEntityId++; this.activeEntities.push(entityId); return entityId; }, addComponent: function addComponent(entityId, component) { if (!this.componentStores[component.name]) { this.componentStores[component.name] = {}; } this.componentStores[component.name][entityId] = component; }, getEntitiesWithComponents: function getEntitiesWithComponents(componentNames) { var result = []; for (var i = 0; i < this.activeEntities.length; i++) { var entityId = this.activeEntities[i]; var hasAllComponents = true; for (var j = 0; j < componentNames.length; j++) { var componentName = componentNames[j]; if (!this.componentStores[componentName] || !this.componentStores[componentName][entityId]) { hasAllComponents = false; break; } } if (hasAllComponents) { result.push(entityId); } } return result; }, destroyEntity: function destroyEntity(entityId) { // Avoid adding duplicates if (this.entitiesToDestroy.indexOf(entityId) === -1) { this.entitiesToDestroy.push(entityId); } } }; Game.Components = { TransformComponent: function TransformComponent(x, y, rotation) { return { name: 'TransformComponent', x: x || 0, y: y || 0, rotation: rotation || 0 }; }, RenderComponent: function RenderComponent(sprite_id, layer, scale, is_visible) { return { name: 'RenderComponent', sprite_id: sprite_id || '', layer: layer || 0, scale: scale || 1.0, is_visible: is_visible !== undefined ? is_visible : true }; }, HealthComponent: function HealthComponent(current_hp, max_hp) { return { name: 'HealthComponent', current_hp: current_hp || 100, max_hp: max_hp || 100 }; }, AttackComponent: function AttackComponent(damage, range, attack_speed, last_attack_time, projectile_id, target_id, splash_radius) { return { name: 'AttackComponent', damage: damage || 10, range: range || 100, attack_speed: attack_speed || 1.0, last_attack_time: last_attack_time || 0, projectile_id: projectile_id || '', target_id: target_id || -1, splash_radius: splash_radius || 0 // Default to 0 (no splash) }; }, MovementComponent: function MovementComponent(speed) { return { name: 'MovementComponent', speed: speed || 50 }; }, PowerProductionComponent: function PowerProductionComponent(rate) { return { name: 'PowerProductionComponent', production_rate: rate || 0 }; }, PowerConsumptionComponent: function PowerConsumptionComponent(rate) { return { name: 'PowerConsumptionComponent', drain_rate: rate || 0 }; }, TowerComponent: function TowerComponent(cost, sellValue, builtOnWave) { return { name: 'TowerComponent', cost: cost || 50, sell_value: sellValue || 25, upgrade_level: 1, upgrade_path_A_level: 0, upgrade_path_B_level: 0, built_on_wave: builtOnWave || 0 }; }, ProjectileComponent: function ProjectileComponent(target_id, speed, damage) { return { name: 'ProjectileComponent', target_id: target_id || -1, speed: speed || 8, // Let's keep the speed lower damage: damage || 10 }; }, EnemyComponent: function EnemyComponent(goldValue, livesCost, armor_type) { return { name: 'EnemyComponent', gold_value: goldValue || 5, lives_cost: livesCost || 1, armor_type: armor_type || 'Normal' }; }, PowerStorageComponent: function PowerStorageComponent(capacity) { return { name: 'PowerStorageComponent', capacity: capacity || 0 }; }, PoisonOnImpactComponent: function PoisonOnImpactComponent(damage_per_second, duration) { return { name: 'PoisonOnImpactComponent', damage_per_second: damage_per_second || 0, duration: duration || 0 }; }, PoisonDebuffComponent: function PoisonDebuffComponent(damage_per_second, duration_remaining, last_tick_time) { return { name: 'PoisonDebuffComponent', damage_per_second: damage_per_second || 0, duration_remaining: duration_remaining || 0, last_tick_time: last_tick_time || 0 }; }, SlowOnImpactComponent: function SlowOnImpactComponent(slow_percentage, duration) { return { name: 'SlowOnImpactComponent', slow_percentage: slow_percentage || 0, duration: duration || 0 }; }, SlowDebuffComponent: function SlowDebuffComponent(slow_percentage, duration_remaining) { return { name: 'SlowDebuffComponent', slow_percentage: slow_percentage || 0, duration_remaining: duration_remaining || 0 }; }, AuraComponent: function AuraComponent(range, effect_type) { return { name: 'AuraComponent', range: range || 100, effect_type: effect_type || '' }; }, DamageBuffComponent: function DamageBuffComponent(multiplier, source_aura_id) { return { name: 'DamageBuffComponent', multiplier: multiplier || 0, source_aura_id: source_aura_id || -1 }; }, GoldGenerationComponent: function GoldGenerationComponent(gold_per_second) { return { name: 'GoldGenerationComponent', gold_per_second: gold_per_second || 0 }; }, FlyingComponent: function FlyingComponent() { return { name: 'FlyingComponent' }; }, // --- NEW COMPONENTS --- FireZoneOnImpactComponent: function FireZoneOnImpactComponent(damage_per_tick, duration, tick_rate) { return { name: 'FireZoneOnImpactComponent', damage_per_tick: damage_per_tick || 0, duration: duration || 0, tick_rate: tick_rate || 1.0 }; }, FireZoneComponent: function FireZoneComponent(damage_per_tick, duration_remaining, tick_rate, last_tick_time) { return { name: 'FireZoneComponent', damage_per_tick: damage_per_tick || 0, duration_remaining: duration_remaining || 0, tick_rate: tick_rate || 1.0, last_tick_time: last_tick_time || 0 }; } }; Game.Systems = { MovementSystem: { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); var frame_time = 1 / 60; // Assuming 60 FPS var goal = { x: 39, y: 30 }; // The goal grid cell for (var i = 0; i < movableEntities.length; i++) { var entityId = movableEntities[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][entityId]; var movement = Game.EntityManager.componentStores['MovementComponent'][entityId]; var currentSpeed = movement.speed; // Check for slow debuff (no changes here) var slowDebuff = Game.EntityManager.componentStores['SlowDebuffComponent'] ? Game.EntityManager.componentStores['SlowDebuffComponent'][entityId] : null; if (slowDebuff) { currentSpeed = movement.speed * (1 - slowDebuff.slow_percentage); slowDebuff.duration_remaining -= frame_time; if (slowDebuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['SlowDebuffComponent'][entityId]; } } // --- START OF NEW FLOW FIELD LOGIC --- // 1. Get the direction vector from the flow field at the enemy's current position. var direction = Game.PathManager.getVectorAt(transform.x, transform.y); // 2. Calculate how far to move this frame. var moveDistance = currentSpeed * frame_time; // 3. Update the enemy's position. transform.x += direction.x * moveDistance; transform.y += direction.y * moveDistance; // 4. Check if the enemy has reached the goal. var gridX = Math.floor(transform.x / 64); var gridY = Math.floor(transform.y / 64); if (gridX === goal.x && gridY === goal.y) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } // --- END OF NEW FLOW FIELD LOGIC --- } } }, RenderSystem: { displayObjects: {}, update: function update() { var renderableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'RenderComponent']); for (var i = 0; i < renderableEntities.length; i++) { var entityId = renderableEntities[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][entityId]; var render = Game.EntityManager.componentStores['RenderComponent'][entityId]; var sprite = this.displayObjects[entityId]; if (!sprite) { sprite = gameWorld.attachAsset(render.sprite_id, {}); // --- NEW ANCHOR LOGIC --- // This is the critical fix: Ensure all game entities are visually centered. sprite.anchor.set(0.5, 0.5); // --- END NEW ANCHOR LOGIC --- this.displayObjects[entityId] = sprite; } sprite.x = transform.x; sprite.y = transform.y; sprite.visible = render.is_visible; } } }, TowerBuildSystem: { tryBuildAt: function tryBuildAt(gridX, gridY, towerType) { var towerData = Game.TowerData[towerType]; // 1. Use the new 3x3 build check if (!towerData || !Game.GridManager.isBuildable3x3(gridX, gridY)) { return; } if (!Game.ResourceManager.spendGold(towerData.cost)) { return; } // 2. Temporarily mark the 3x3 area on the grid for path validation for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { Game.GridManager.grid[gridX + dx][gridY + dy].state = Game.GridManager.CELL_STATES.TOWER; } } // 3. Regenerate flow field and validate path Game.PathManager.generateFlowField(); var spawnDistance = Game.PathManager.distanceField[0][30]; if (spawnDistance === Infinity) { // INVALID PLACEMENT: Revert the grid cells and refund gold for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { Game.GridManager.grid[gridX + dx][gridY + dy].state = Game.GridManager.CELL_STATES.EMPTY; } } Game.ResourceManager.addGold(towerData.cost); // Re-generate the flow field to its valid state Game.PathManager.generateFlowField(); return; } // --- VALID PLACEMENT --- var towerId = Game.EntityManager.createEntity(); // 4. Permanently mark the 3x3 grid area with the new tower's ID for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { Game.GridManager.grid[gridX + dx][gridY + dy].entityId = towerId; } } var centeredX = gridX * 64 + 32; var centeredY = gridY * 64 + 32; var transform = Game.Components.TransformComponent(centeredX, centeredY); Game.EntityManager.addComponent(towerId, transform); var render = Game.Components.RenderComponent(towerData.sprite_id, 2, 1.0, true); Game.EntityManager.addComponent(towerId, render); for (var i = 0; i < towerData.components.length; i++) { var componentDef = towerData.components[i]; var component; if (componentDef.name === 'TowerComponent') { var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; component = Game.Components.TowerComponent(componentDef.args[0], componentDef.args[1], currentWave); } else { component = Game.Components[componentDef.name].apply(null, componentDef.args); } Game.EntityManager.addComponent(towerId, component); } var event = Game.Events.TowerPlacedEvent(towerId, towerData.cost, transform.x, transform.y); Game.EventBus.publish(event); } }, TargetingSystem: { update: function update() { // If there is a power deficit, do not assign any new targets. if (Game.Systems.CombatSystem.isPowerDeficit) { // Also, clear any existing targets to prevent towers from holding a lock. var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent']); for (var i = 0; i < towers.length; i++) { var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towers[i]]; towerAttack.target_id = -1; } return; // Stop the system's update. } var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); for (var i = 0; i < towers.length; i++) { var towerId = towers[i]; var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towerId]; var towerTransform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var bestTarget = -1; var bestPathIndex = -1; var enemies = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); for (var j = 0; j < enemies.length; j++) { var enemyId = enemies[j]; var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; var dx = enemyTransform.x - towerTransform.x; var dy = enemyTransform.y - towerTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= towerAttack.range) { // Check if enemy has PathFollowerComponent (ground units) or use default targeting for flyers var pathFollower = Game.EntityManager.componentStores['PathFollowerComponent'] ? Game.EntityManager.componentStores['PathFollowerComponent'][enemyId] : null; var currentPathIndex = pathFollower ? pathFollower.path_index : 0; if (currentPathIndex > bestPathIndex) { bestTarget = enemyId; bestPathIndex = currentPathIndex; } } } towerAttack.target_id = bestTarget; } } }, CleanupSystem: { update: function update() { for (var i = 0; i < Game.EntityManager.entitiesToDestroy.length; i++) { var entityId = Game.EntityManager.entitiesToDestroy[i]; var index = Game.EntityManager.activeEntities.indexOf(entityId); if (index > -1) { Game.EntityManager.activeEntities.splice(index, 1); } for (var componentName in Game.EntityManager.componentStores) { if (Game.EntityManager.componentStores[componentName][entityId]) { delete Game.EntityManager.componentStores[componentName][entityId]; } } if (Game.Systems.RenderSystem.displayObjects[entityId]) { Game.Systems.RenderSystem.displayObjects[entityId].destroy(); delete Game.Systems.RenderSystem.displayObjects[entityId]; } } if (Game.EntityManager.entitiesToDestroy.length > 0) { Game.EntityManager.entitiesToDestroy = []; } } }, WaveSpawnerSystem: { currentWaveIndex: -1, isSpawning: false, spawnTimer: 0, subWaveIndex: 0, spawnedInSubWave: 0, startNextWave: function startNextWave() { this.currentWaveIndex++; if (this.currentWaveIndex >= Game.WaveData.length) { return; // No more waves } this.isSpawning = true; this.spawnTimer = 0; this.subWaveIndex = 0; this.spawnedInSubWave = 0; // Calculate the total number of enemies for this wave var totalEnemies = 0; var currentWave = Game.WaveData[this.currentWaveIndex]; for (var i = 0; i < currentWave.sub_waves.length; i++) { totalEnemies += currentWave.sub_waves[i].count; } Game.GameplayManager.totalEnemiesThisWave = totalEnemies; }, update: function update() { if (!this.isSpawning || this.currentWaveIndex >= Game.WaveData.length) { return; } var currentWave = Game.WaveData[this.currentWaveIndex]; if (!currentWave || this.subWaveIndex >= currentWave.sub_waves.length) { this.isSpawning = false; return; } var currentSubWave = currentWave.sub_waves[this.subWaveIndex]; if (this.spawnedInSubWave === 0 && this.spawnTimer < currentSubWave.start_delay) { this.spawnTimer += 1 / 60; return; } if (this.spawnedInSubWave > 0 && this.spawnTimer < currentSubWave.spawn_delay) { this.spawnTimer += 1 / 60; return; } if (this.spawnedInSubWave < currentSubWave.count) { var enemyData = Game.EnemyData[currentSubWave.enemy_type]; if (!enemyData) { this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; return; } var enemyId = Game.EntityManager.createEntity(); var isFlyer = enemyData.components && enemyData.components.some(function (c) { return c.name === 'FlyingComponent'; }); var renderLayer = isFlyer ? 3 : 1; var transform = Game.Components.TransformComponent(0 * 64, 30 * 64); var render = Game.Components.RenderComponent('enemy', renderLayer, 1.0, true); var movement = Game.Components.MovementComponent(enemyData.speed); var health = Game.Components.HealthComponent(enemyData.health, enemyData.health); var enemy = Game.Components.EnemyComponent(enemyData.gold_value, enemyData.lives_cost, enemyData.armor_type); Game.EntityManager.addComponent(enemyId, transform); Game.EntityManager.addComponent(enemyId, render); Game.EntityManager.addComponent(enemyId, movement); Game.EntityManager.addComponent(enemyId, health); Game.EntityManager.addComponent(enemyId, enemy); if (enemyData.components && enemyData.components.length > 0) { for (var i = 0; i < enemyData.components.length; i++) { var compData = enemyData.components[i]; var newComponent = Game.Components[compData.name].apply(null, compData.args || []); Game.EntityManager.addComponent(enemyId, newComponent); } } this.spawnedInSubWave++; this.spawnTimer = 0; Game.GameplayManager.enemiesSpawnedThisWave++; } else { this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; } } }, CombatSystem: { isPowerDeficit: false, init: function init() { Game.EventBus.subscribe('PowerStatusChanged', this.onPowerStatusChanged.bind(this)); }, onPowerStatusChanged: function onPowerStatusChanged(event) { this.isPowerDeficit = event.new_status === 'deficit'; }, update: function update() { // If there is a power deficit, do not allow towers to fire. if (this.isPowerDeficit) { return; // Stop the system's update. } var currentTime = LK.ticks / 60; var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); for (var i = 0; i < towers.length; i++) { var towerId = towers[i]; var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towerId]; var towerTransform = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (towerAttack.target_id !== -1 && currentTime - towerAttack.last_attack_time >= 1.0 / towerAttack.attack_speed) { var targetTransform = Game.EntityManager.componentStores['TransformComponent'][towerAttack.target_id]; if (targetTransform) { var baseDamage = towerAttack.damage; var finalDamage = baseDamage; var buffComp = Game.EntityManager.componentStores['DamageBuffComponent'] ? Game.EntityManager.componentStores['DamageBuffComponent'][towerId] : null; if (buffComp) { finalDamage = baseDamage * (1 + buffComp.multiplier); } var projectileId = Game.EntityManager.createEntity(); var projectileTransform = Game.Components.TransformComponent(towerTransform.x, towerTransform.y); var projectileRender = Game.Components.RenderComponent(towerAttack.projectile_id, 1, 1.0, true); var projectileComponent = Game.Components.ProjectileComponent(towerAttack.target_id, undefined, finalDamage); projectileComponent.source_tower_id = towerId; Game.EntityManager.addComponent(projectileId, projectileTransform); Game.EntityManager.addComponent(projectileId, projectileRender); Game.EntityManager.addComponent(projectileId, projectileComponent); if (towerAttack.on_impact_effects && towerAttack.on_impact_effects.length > 0) { for (var j = 0; j < towerAttack.on_impact_effects.length; j++) { var effectData = towerAttack.on_impact_effects[j]; var newComponent = Game.Components[effectData.name].apply(null, effectData.args); Game.EntityManager.addComponent(projectileId, newComponent); } } towerAttack.last_attack_time = currentTime; } } } var projectiles = Game.EntityManager.getEntitiesWithComponents(['ProjectileComponent', 'TransformComponent']); for (var i = projectiles.length - 1; i >= 0; i--) { var projectileId = projectiles[i]; var projectileComp = Game.EntityManager.componentStores['ProjectileComponent'][projectileId]; var projectileTransform = Game.EntityManager.componentStores['TransformComponent'][projectileId]; var targetExists = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id]; if (!targetExists) { Game.EntityManager.destroyEntity(projectileId); continue; } var targetTransform = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id]; var dx = targetTransform.x - projectileTransform.x; var dy = targetTransform.y - projectileTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 10) { var impactPoint = { x: targetTransform.x, y: targetTransform.y }; var sourceTowerAttack = Game.EntityManager.componentStores['AttackComponent'][projectileComp.source_tower_id]; if (sourceTowerAttack && sourceTowerAttack.splash_radius > 0) { var allEnemies = Game.EntityManager.getEntitiesWithComponents(['HealthComponent', 'TransformComponent', 'EnemyComponent']); for (var j = 0; j < allEnemies.length; j++) { var enemyId = allEnemies[j]; var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; var splash_dx = enemyTransform.x - impactPoint.x; var splash_dy = enemyTransform.y - impactPoint.y; var splash_dist = Math.sqrt(splash_dx * splash_dx + splash_dy * splash_dy); if (splash_dist <= sourceTowerAttack.splash_radius) { this.dealDamage(enemyId, projectileComp.damage); } } } else { this.dealDamage(projectileComp.target_id, projectileComp.damage); } var targetHealth = Game.EntityManager.componentStores['HealthComponent'][projectileComp.target_id]; if (targetHealth) { var poisonImpactComp = Game.EntityManager.componentStores['PoisonOnImpactComponent'] ? Game.EntityManager.componentStores['PoisonOnImpactComponent'][projectileId] : null; if (poisonImpactComp) { var debuff = Game.Components.PoisonDebuffComponent(poisonImpactComp.damage_per_second, poisonImpactComp.duration, currentTime); Game.EntityManager.addComponent(projectileComp.target_id, debuff); } var slowImpactComp = Game.EntityManager.componentStores['SlowOnImpactComponent'] ? Game.EntityManager.componentStores['SlowOnImpactComponent'][projectileId] : null; if (slowImpactComp) { var slowDebuff = Game.Components.SlowDebuffComponent(slowImpactComp.slow_percentage, slowImpactComp.duration); Game.EntityManager.addComponent(projectileComp.target_id, slowDebuff); } } var fireZoneImpactComp = Game.EntityManager.componentStores['FireZoneOnImpactComponent'] ? Game.EntityManager.componentStores['FireZoneOnImpactComponent'][projectileId] : null; if (fireZoneImpactComp) { var zoneId = Game.EntityManager.createEntity(); Game.EntityManager.addComponent(zoneId, Game.Components.TransformComponent(impactPoint.x, impactPoint.y)); Game.EntityManager.addComponent(zoneId, Game.Components.RenderComponent('fire_zone', 0, 1.0, true)); Game.EntityManager.addComponent(zoneId, Game.Components.FireZoneComponent(fireZoneImpactComp.damage_per_tick, fireZoneImpactComp.duration, fireZoneImpactComp.tick_rate, currentTime)); } Game.EntityManager.destroyEntity(projectileId); } else { var moveX = dx / distance * projectileComp.speed; var moveY = dy / distance * projectileComp.speed; projectileTransform.x += moveX; projectileTransform.y += moveY; } } var poisonedEnemies = Game.EntityManager.getEntitiesWithComponents(['PoisonDebuffComponent']); for (var i = poisonedEnemies.length - 1; i >= 0; i--) { var enemyId = poisonedEnemies[i]; var debuff = Game.EntityManager.componentStores['PoisonDebuffComponent'][enemyId]; if (!debuff) { continue; } if (currentTime - debuff.last_tick_time >= 1.0) { this.dealDamage(enemyId, debuff.damage_per_second); debuff.last_tick_time = currentTime; debuff.duration_remaining -= 1.0; } if (debuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['PoisonDebuffComponent'][enemyId]; } } var fireZones = Game.EntityManager.getEntitiesWithComponents(['FireZoneComponent', 'TransformComponent']); for (var i = fireZones.length - 1; i >= 0; i--) { var zoneId = fireZones[i]; var zoneComp = Game.EntityManager.componentStores['FireZoneComponent'][zoneId]; var zoneTransform = Game.EntityManager.componentStores['TransformComponent'][zoneId]; zoneComp.duration_remaining -= 1.0 / 60; if (zoneComp.duration_remaining <= 0) { Game.EntityManager.destroyEntity(zoneId); continue; } if (currentTime - zoneComp.last_tick_time >= zoneComp.tick_rate) { var allEnemies = Game.EntityManager.getEntitiesWithComponents(['HealthComponent', 'TransformComponent']); var zoneRadius = 40; for (var j = 0; j < allEnemies.length; j++) { var enemyId = allEnemies[j]; var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; var dx = enemyTransform.x - zoneTransform.x; var dy = enemyTransform.y - zoneTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= zoneRadius) { this.dealDamage(enemyId, zoneComp.damage_per_tick); } } zoneComp.last_tick_time = currentTime; } } }, dealDamage: function dealDamage(enemyId, damage) { var targetHealth = Game.EntityManager.componentStores['HealthComponent'][enemyId]; if (!targetHealth) { return; } var finalDamage = damage; var enemyComp = Game.EntityManager.componentStores['EnemyComponent'][enemyId]; if (enemyComp && enemyComp.armor_type === 'Heavy') { finalDamage *= 0.5; } targetHealth.current_hp -= finalDamage; if (targetHealth.current_hp <= 0) { var goldValue = enemyComp ? enemyComp.gold_value : 0; Game.EventBus.publish(Game.Events.EnemyKilledEvent(enemyId, goldValue)); Game.EntityManager.destroyEntity(enemyId); } } }, AuraSystem: { update: function update() { var buffedTowers = Game.EntityManager.getEntitiesWithComponents(['DamageBuffComponent']); for (var i = 0; i < buffedTowers.length; i++) { var towerId = buffedTowers[i]; delete Game.EntityManager.componentStores['DamageBuffComponent'][towerId]; } var auraTowers = Game.EntityManager.getEntitiesWithComponents(['AuraComponent', 'TransformComponent']); if (auraTowers.length === 0) { return; } var attackTowers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); for (var i = 0; i < auraTowers.length; i++) { var auraTowerId = auraTowers[i]; var auraComponent = Game.EntityManager.componentStores['AuraComponent'][auraTowerId]; var auraTransform = Game.EntityManager.componentStores['TransformComponent'][auraTowerId]; if (auraComponent.effect_type !== 'damage_buff') { continue; } for (var j = 0; j < attackTowers.length; j++) { var attackTowerId = attackTowers[j]; if (attackTowerId === auraTowerId) { continue; } var attackTransform = Game.EntityManager.componentStores['TransformComponent'][attackTowerId]; var dx = auraTransform.x - attackTransform.x; var dy = auraTransform.y - attackTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= auraComponent.range) { var buff = Game.Components.DamageBuffComponent(0.2, auraTowerId); Game.EntityManager.addComponent(attackTowerId, buff); } } } } }, EconomySystem: { update: function update() { var goldGenerators = Game.EntityManager.getEntitiesWithComponents(['GoldGenerationComponent']); for (var i = 0; i < goldGenerators.length; i++) { var entityId = goldGenerators[i]; var goldGenComp = Game.EntityManager.componentStores['GoldGenerationComponent'][entityId]; if (goldGenComp) { Game.ResourceManager.addGold(goldGenComp.gold_per_second); } } } }, TowerSystem: { tryUpgradeTower: function tryUpgradeTower(towerId, path) { var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId]; if (!renderComp) { return; } var towerSpriteId = renderComp.sprite_id; var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === towerSpriteId) { towerType = type; break; } } if (!towerType) { return; } var upgradePathData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType][path] : null; if (!upgradePathData) { return; } var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; if (!towerComp) { return; } var nextUpgradeIndex = path === 'PathA' ? towerComp.upgrade_path_A_level : towerComp.upgrade_path_B_level; if (nextUpgradeIndex >= upgradePathData.length) { return; } var upgradeInfo = upgradePathData[nextUpgradeIndex]; if (!Game.ResourceManager.spendGold(upgradeInfo.cost)) { return; } for (var componentName in upgradeInfo.effects) { var componentToModify = Game.EntityManager.componentStores[componentName] ? Game.EntityManager.componentStores[componentName][towerId] : null; if (componentToModify) { var effectsToApply = upgradeInfo.effects[componentName]; for (var property in effectsToApply) { if (componentToModify.hasOwnProperty(property)) { componentToModify[property] += effectsToApply[property]; } } } } if (upgradeInfo.add_component_on_impact) { var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; if (attackComp) { if (!attackComp.on_impact_effects) { attackComp.on_impact_effects = []; } attackComp.on_impact_effects.push(upgradeInfo.add_component_on_impact); } } towerComp.upgrade_level++; if (path === 'PathA') { towerComp.upgrade_path_A_level++; } else { towerComp.upgrade_path_B_level++; } }, trySellTower: function trySellTower(towerId) { var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!towerComp || !transformComp) { return; } var refundAmount = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { refundAmount = towerComp.cost; } Game.ResourceManager.addGold(refundAmount); // Clear the tower's 3x3 footprint from the grid var gridX = Math.floor(transformComp.x / 64); var gridY = Math.floor(transformComp.y / 64); for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var x = gridX + dx; var y = gridY + dy; if (Game.GridManager.grid[x] && Game.GridManager.grid[x][y] !== undefined) { Game.GridManager.grid[x][y].state = Game.GridManager.CELL_STATES.EMPTY; Game.GridManager.grid[x][y].entityId = null; } } } Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount)); Game.GameplayManager.selectedTowerId = -1; Game.EntityManager.destroyEntity(towerId); } } }; // === SECTION: GAME DATA === Game.EnemyData = { 'Grunt': { health: 100, speed: 50, gold_value: 5, lives_cost: 1, armor_type: 'Normal', components: [] }, 'Swarmer': { health: 30, speed: 80, gold_value: 2, lives_cost: 1, armor_type: 'Light', components: [] }, 'Armored': { health: 400, speed: 30, gold_value: 15, lives_cost: 2, armor_type: 'Heavy', components: [] }, 'Flyer': { health: 80, speed: 60, gold_value: 8, lives_cost: 1, armor_type: 'Light', components: [{ name: 'FlyingComponent', args: [] }] } }; Game.WaveData = [ // Wave 1: A few basic Grunts { wave_number: 1, sub_waves: [{ enemy_type: 'Grunt', count: 10, spawn_delay: 1.5, start_delay: 0 }] }, // Wave 2: A larger group of Grunts { wave_number: 2, sub_waves: [{ enemy_type: 'Grunt', count: 15, spawn_delay: 1.2, start_delay: 0 }] }, // Wave 3: A wave of fast Swarmers { wave_number: 3, sub_waves: [{ enemy_type: 'Swarmer', count: 20, spawn_delay: 0.5, start_delay: 0 }] }, // Wave 4: Grunts supported by slow, tough Armored units { wave_number: 4, sub_waves: [{ enemy_type: 'Grunt', count: 10, spawn_delay: 1.5, start_delay: 0 }, { enemy_type: 'Armored', count: 2, spawn_delay: 3.0, start_delay: 5.0 }] }, // Wave 5: A squadron of Flyers that ignore the path { wave_number: 5, sub_waves: [{ enemy_type: 'Flyer', count: 8, spawn_delay: 1.0, start_delay: 0 }] }]; // === SECTION: TOWER DATA === Game.TowerData = { 'ArrowTower': { cost: 50, sprite_id: 'arrow_tower', color: 0x2ecc71, components: [{ name: 'TowerComponent', args: [50, 25, undefined] }, { name: 'AttackComponent', args: [10, 150, 1.0, 0, 'arrow_projectile'] }, { name: 'PowerConsumptionComponent', args: [2] }] }, 'GeneratorTower': { cost: 75, sprite_id: 'generator_tower', color: 0x3498db, components: [{ name: 'TowerComponent', args: [75, 37, undefined] }, { name: 'PowerProductionComponent', args: [5] }] }, 'CapacitorTower': { cost: 40, sprite_id: 'capacitor_tower', color: 0xf1c40f, components: [{ name: 'TowerComponent', args: [40, 20, undefined] }, { name: 'PowerStorageComponent', args: [20] }] }, 'RockLauncher': { cost: 125, sprite_id: 'rock_launcher_tower', color: 0x8d6e63, components: [{ name: 'TowerComponent', args: [125, 62, undefined] }, { name: 'AttackComponent', args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50] }, { name: 'PowerConsumptionComponent', args: [10] }] }, 'ChemicalTower': { cost: 100, sprite_id: 'chemical_tower', color: 0xb87333, components: [{ name: 'TowerComponent', args: [100, 50, undefined] }, { name: 'AttackComponent', args: [5, 150, 0.8, 0, 'chemical_projectile', -1, 0] }, { name: 'PowerConsumptionComponent', args: [8] }] }, 'SlowTower': { cost: 80, sprite_id: 'slow_tower', color: 0x5dade2, components: [{ name: 'TowerComponent', args: [80, 40, undefined] }, { name: 'AttackComponent', args: [2, 160, 1.0, 0, 'slow_projectile'] }, { name: 'PowerConsumptionComponent', args: [4] }] }, 'DarkTower': { cost: 150, sprite_id: 'dark_tower', color: 0x9b59b6, components: [{ name: 'TowerComponent', args: [150, 75, undefined] }, { name: 'AuraComponent', args: [175, 'damage_buff'] }, { name: 'PowerConsumptionComponent', args: [12] }] }, 'GraveyardTower': { cost: 200, sprite_id: 'graveyard_tower', color: 0x607d8b, components: [{ name: 'TowerComponent', args: [200, 100, undefined] }, { name: 'GoldGenerationComponent', args: [1.5] }, { name: 'PowerConsumptionComponent', args: [15] }] } }; // === SECTION: UPGRADE DATA === Game.UpgradeData = { 'ArrowTower': { 'PathA': [{ cost: 60, effects: { AttackComponent: { damage: 5 } } }, //{7R} // Level 1: +5 Dmg { cost: 110, effects: { AttackComponent: { damage: 10 } } } //{7W} // Level 2: +10 Dmg ], 'PathB': [{ cost: 75, effects: { AttackComponent: { attack_speed: 0.2 } } }, //{83} // Level 1: +0.2 AS { cost: 125, effects: { AttackComponent: { attack_speed: 0.3 } } } //{8a} // Level 2: +0.3 AS ] }, 'RockLauncher': { 'PathA': [ // Heavy Boulders - More Damage & Range { cost: 150, effects: { AttackComponent: { damage: 15, range: 20 } } }, { cost: 250, effects: { AttackComponent: { damage: 25, range: 30 } } }, // --- UPDATED UPGRADE --- { cost: 500, effects: { AttackComponent: { damage: 50, range: 50 } }, add_component_on_impact: { name: 'FireZoneOnImpactComponent', args: [10, 3, 0.5] } // dmg, duration, tick_rate }], 'PathB': [ // Lighter Munitions - Faster Attack Speed { cost: 140, effects: { AttackComponent: { damage: 5, attack_speed: 0.2 } } }, { cost: 220, effects: { AttackComponent: { damage: 10, attack_speed: 0.3 } } }, { cost: 450, effects: { AttackComponent: { damage: 15, attack_speed: 0.5 } } }] } }; // === SECTION: GRID DRAWING === // Create a container for all game objects that will be panned var gameWorld = new Container(); game.addChild(gameWorld); function drawGrid() { // Draw the grid cells for (var x = 0; x < Game.GridManager.grid.length; x++) { for (var y = 0; y < Game.GridManager.grid[x].length; y++) { var cellSprite = gameWorld.attachAsset('grid_cell', {}); cellSprite.x = x * 64; cellSprite.y = y * 64; cellSprite.anchor.set(0, 0); // Anchor to top-left cellSprite.gridX = x; // Store grid coordinates on the sprite cellSprite.gridY = y; cellSprite.down = function () { // Only process true clicks, not drags var timeSinceMouseDown = LK.ticks / 60 * 1000 - mouseDownTime; if (timeSinceMouseDown > 200) { return; } var dx = Math.abs(lastMouseX - dragStartX); var dy = Math.abs(lastMouseY - dragStartY); if (dx > 10 || dy > 10) { return; } var gridX = this.gridX; var gridY = this.gridY; var gridCell = Game.GridManager.grid[gridX][gridY]; if (Game.GameplayManager.activeBuildType) { // If in build mode, check if the 3x3 area is buildable if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; // The center of the build is the clicked cell Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; var worldX = gridX * 64 + 32; var worldY = gridY * 64 + 32; confirmPanel.x = worldX; confirmPanel.y = worldY; } else { // Clicked an invalid spot, so cancel build mode Game.GameplayManager.activeBuildType = null; } } else if (!Game.GameplayManager.isConfirmingPlacement) { // If not in build mode, check for selection if (gridCell.state === Game.GridManager.CELL_STATES.TOWER) { // The grid cell now directly tells us which tower is selected Game.GameplayManager.selectedTowerId = gridCell.entityId; } else { Game.GameplayManager.selectedTowerId = -1; } } }; } } // Highlight the path cells if (Game.PathManager.currentPath) { for (var i = 0; i < Game.PathManager.currentPath.length; i++) { var pathNode = Game.PathManager.currentPath[i]; var pathSprite = gameWorld.attachAsset('path_cell', {}); // Note: PathManager coordinates are already in pixels pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } } // === SECTION: GLOBAL EVENT BUS === Game.EventBus = { listeners: {}, subscribe: function subscribe(eventType, listener) { if (!this.listeners[eventType]) { this.listeners[eventType] = []; } this.listeners[eventType].push(listener); }, unsubscribe: function unsubscribe(eventType, listener) { if (this.listeners[eventType]) { var index = this.listeners[eventType].indexOf(listener); if (index !== -1) { this.listeners[eventType].splice(index, 1); } } }, publish: function publish(event) { if (this.listeners[event.type]) { for (var i = 0; i < this.listeners[event.type].length; i++) { this.listeners[event.type][i](event); } } } }; Game.Events = { EnemyReachedEndEvent: function EnemyReachedEndEvent(enemy_id) { return { type: 'EnemyReachedEnd', enemy_id: enemy_id }; }, EnemyKilledEvent: function EnemyKilledEvent(enemy_id, gold_value) { return { type: 'EnemyKilled', enemy_id: enemy_id, gold_value: gold_value }; }, TowerPlacedEvent: function TowerPlacedEvent(tower_id, cost, position_x, position_y) { return { type: 'TowerPlaced', tower_id: tower_id, cost: cost, position_x: position_x, position_y: position_y }; }, TowerSoldEvent: function TowerSoldEvent(tower_id, refund_value) { return { type: 'TowerSold', tower_id: tower_id, refund_value: refund_value }; }, PowerStatusChangedEvent: function PowerStatusChangedEvent(new_status) { return { type: 'PowerStatusChanged', new_status: new_status }; }, WaveCompletedEvent: function WaveCompletedEvent(wave_number) { return { type: 'WaveCompleted', wave_number: wave_number }; }, GameOverEvent: function GameOverEvent() { return { type: 'GameOver' }; } }; // === SECTION: STACK-BASED GAME STATE MANAGER === // === SECTION: DIRECT GAME INITIALIZATION === // Initialize managers Game.GridManager.init(40, 60); Game.GridManager.grid[0][30] = Game.GridManager.CELL_STATES.PATH; // Set spawn point as unbuildable Game.ResourceManager.init(); Game.PathManager.init(40, 60); drawGrid(); // Create the Heads-Up Display (HUD) var goldText = new Text2('Gold: 100', { size: 40, fill: 0xFFD700 }); goldText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(goldText); goldText.x = -20; // 20px padding from the right edge goldText.y = 20; // 20px padding from the top edge var livesText = new Text2('Lives: 20', { size: 40, fill: 0xFF4136 }); livesText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(livesText); livesText.x = -20; // 20px padding from the right edge livesText.y = 70; // Position below the gold text var waveText = new Text2('Wave: 1 / 5', { size: 40, fill: 0xFFFFFF }); waveText.anchor.set(1, 0); // Align to the right LK.gui.topRight.addChild(waveText); waveText.x = -20; // 20px padding from the right edge waveText.y = 120; // Position below the lives text // Create Power Meter UI var powerMeterContainer = new Container(); LK.gui.topRight.addChild(powerMeterContainer); powerMeterContainer.x = -320; // Positioned in from the right edge powerMeterContainer.y = 170; // Position below wave text // Create a Graphics instance for power production bar var Graphics = function Graphics() { var graphics = new Container(); graphics.currentFill = 0xffffff; graphics.currentAlpha = 1.0; graphics.beginFill = function (color, alpha) { this.currentFill = color; this.currentAlpha = alpha !== undefined ? alpha : 1.0; }; graphics.drawRect = function (x, y, width, height) { var rect = game.attachAsset('grid_cell', {}); rect.tint = this.currentFill; rect.alpha = this.currentAlpha; rect.x = x; rect.y = y; rect.width = width; rect.height = height; this.addChild(rect); }; graphics.endFill = function () { // No-op for compatibility }; graphics.clear = function () { // Remove all children to clear the graphics while (this.children.length > 0) { var child = this.children[0]; this.removeChild(child); if (child.destroy) { child.destroy(); } } }; return graphics; }; // Background bar representing total production capacity var powerProductionBar = new Graphics(); powerProductionBar.beginFill(0x3498db, 0.5); // Semi-transparent blue powerProductionBar.drawRect(0, 0, 300, 20); // Placeholder width powerProductionBar.endFill(); powerMeterContainer.addChild(powerProductionBar); // Foreground bar representing current consumption var powerConsumptionBar = new Graphics(); powerConsumptionBar.beginFill(0x2ecc71); // Green for surplus powerConsumptionBar.drawRect(0, 0, 10, 20); // Placeholder width powerConsumptionBar.endFill(); powerMeterContainer.addChild(powerConsumptionBar); // Secondary bar for the available power buffer (capacitors) var powerAvailableBar = new Graphics(); powerAvailableBar.beginFill(0x3498db, 0.3); // Fainter blue powerAvailableBar.drawRect(0, 0, 5, 20); // Placeholder width, starts at the end of the production bar powerAvailableBar.endFill(); powerMeterContainer.addChild(powerAvailableBar); // Create Start Wave button var startWaveButton = new Text2('START NEXT WAVE', { size: 70, fill: 0x00FF00 }); startWaveButton.anchor.set(0.5, 1); LK.gui.bottom.addChild(startWaveButton); startWaveButton.y = -20; startWaveButton.visible = true; startWaveButton.down = function () { // Deselect any active tower before starting the wave Game.GameplayManager.selectedTowerId = -1; Game.Systems.WaveSpawnerSystem.startNextWave(); startWaveButton.visible = false; }; // Initialize GameplayManager Game.GameplayManager.init(); // Initialize GameManager Game.GameManager.init(); Game.Systems.CombatSystem.init(); // === SECTION: TEMP BUILD UI === // === SECTION: BUILD AND CONFIRMATION UI === // -- Ghost Tower (for placement preview) -- var ghostTowerSprite = null; // -- Build Panel (at the bottom of the screen) -- var buildPanel = new Container(); LK.gui.bottom.addChild(buildPanel); buildPanel.x = 0; buildPanel.y = -150; // Positioned further up to not overlap the "Start Wave" button // -- Floating Info Panel (now a fixed panel above the build bar) -- var floatingInfoPanel = new Container(); LK.gui.bottom.addChild(floatingInfoPanel); // Attach to the bottom GUI anchor // Container objects don't have anchor property, so position it manually to center floatingInfoPanel.x = -150; // Offset by half the panel width (300/2) to center floatingInfoPanel.y = -320; // Position it 320px up from the bottom edge floatingInfoPanel.visible = false; var infoBackground = new Graphics(); infoBackground.beginFill(0x000000, 0.8); infoBackground.drawRect(0, 0, 300, 80); infoBackground.endFill(); floatingInfoPanel.addChild(infoBackground); var infoText = new Text2('', { size: 30, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 290 }); infoText.anchor.set(0, 0); infoText.x = 5; infoText.y = 5; floatingInfoPanel.addChild(infoText); // -- Confirmation Panel (appears on the game map) -- var confirmPanel = new Container(); gameWorld.addChild(confirmPanel); // Added to gameWorld so it pans with the camera confirmPanel.visible = false; // Buttons will be created and destroyed dynamically. var confirmButton = null; var cancelButton = null; // -- Build Button Creation Logic (for Drag-and-Drop) -- var buildButtonData = [{ type: 'ArrowTower', sprite: 'arrow_tower' }, { type: 'GeneratorTower', sprite: 'generator_tower' }, { type: 'CapacitorTower', sprite: 'capacitor_tower' }, { type: 'RockLauncher', sprite: 'rock_launcher_tower' }, { type: 'ChemicalTower', sprite: 'chemical_tower' }, { type: 'SlowTower', sprite: 'slow_tower' }, { type: 'DarkTower', sprite: 'dark_tower' }, { type: 'GraveyardTower', sprite: 'graveyard_tower' }]; var buttonSize = 90; var buttonPadding = 15; var totalWidth = buildButtonData.length * buttonSize + (buildButtonData.length - 1) * buttonPadding; var startX = -totalWidth / 2; for (var i = 0; i < buildButtonData.length; i++) { var data = buildButtonData[i]; var button = game.attachAsset(data.sprite, {}); button.width = buttonSize; button.height = buttonSize; button.anchor.set(0.5, 0.5); button.x = startX + i * (buttonSize + buttonPadding) + buttonSize / 2; button.y = 0; button.towerType = data.type; // This is the start of the drag-and-drop button.down = function () { // Set the active build type to start the drag process Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; // Deselect any existing tower }; // We no longer need a 'move' handler here, as the main game loop will handle the drag. buildPanel.addChild(button); } // We no longer need the 'buildPanel.out' handler either. buildPanel.out = null; // Confirmation button logic is now handled dynamically in the game update loop // === SECTION: MINIMAP UI === var minimapContainer = new Container(); LK.gui.bottomLeft.addChild(minimapContainer); minimapContainer.x = 20; // 20px padding from the left edge minimapContainer.y = -320; // Position up from bottom edge (negative value moves up in bottomLeft anchor) var minimapBackground = new Graphics(); minimapContainer.addChild(minimapBackground); // Function to draw the static parts of the minimap function drawMinimapBackground() { var mapWidth = 40; var mapHeight = 60; var tilePixelSize = 5; // Each grid cell is a 5x5 pixel block on the minimap var colors = { EMPTY: 0x3d2b1f, // Dark Brown for buildable land PATH: 0x8B4513, // SaddleBrown for the path TOWER: 0xFFFFFF, // White (will be used for dynamic tower dots later) BLOCKED: 0x111111 // Almost black for blocked areas }; minimapBackground.clear(); minimapBackground.beginFill(0x000000, 0.5); // Semi-transparent black background for the minimap minimapBackground.drawRect(0, 0, mapWidth * tilePixelSize, mapHeight * tilePixelSize); minimapBackground.endFill(); for (var x = 0; x < mapWidth; x++) { for (var y = 0; y < mapHeight; y++) { var cellState = Game.GridManager.grid[x][y]; var color; switch (cellState) { case Game.GridManager.CELL_STATES.PATH: color = colors.PATH; break; case Game.GridManager.CELL_STATES.BLOCKED: color = colors.BLOCKED; break; default: color = colors.EMPTY; break; } minimapBackground.beginFill(color); minimapBackground.drawRect(x * tilePixelSize, y * tilePixelSize, tilePixelSize, tilePixelSize); minimapBackground.endFill(); } } } // Call the function once to draw the initial state drawMinimapBackground(); var minimapDynamicLayer = new Graphics(); minimapContainer.addChild(minimapDynamicLayer); var minimapViewportRect = new Graphics(); minimapContainer.addChild(minimapViewportRect); minimapContainer.down = function (x, y, obj) { // x and y are the click coordinates relative to the minimap's top-left corner. // 1. Define map and minimap dimensions for conversion. var mapWorldWidth = 40 * 64; // 2560 var mapWorldHeight = 60 * 64; // 3840 var minimapPixelWidth = 40 * 5; // 200 var minimapPixelHeight = 60 * 5; // 300 // 2. Convert minimap click coordinates to world coordinates. // This gives us the point in the world the player wants to look at. var targetWorldX = x / minimapPixelWidth * mapWorldWidth; var targetWorldY = y / minimapPixelHeight * mapWorldHeight; // 3. Calculate the new camera position to CENTER the view on the target. var screenWidth = 2048; var screenHeight = 2732; var newCameraX = -targetWorldX + screenWidth / 2; var newCameraY = -targetWorldY + screenHeight / 2; // 4. Clamp the new camera position to the map boundaries. // This reuses the same clamping logic from the drag-to-pan feature. var minX = screenWidth - mapWorldWidth; var minY = screenHeight - mapWorldHeight; var maxX = 0; var maxY = 0; cameraX = Math.max(minX, Math.min(maxX, newCameraX)); cameraY = Math.max(minY, Math.min(maxY, newCameraY)); // 5. Apply the final, clamped transform to the game world. gameWorld.x = cameraX; gameWorld.y = cameraY; }; function updateMinimapDynamic() { minimapDynamicLayer.clear(); var tilePixelSize = 5; // Draw Towers var towers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); for (var i = 0; i < towers.length; i++) { var towerId = towers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var gridX = Math.floor(transform.x / 64); var gridY = Math.floor(transform.y / 64); minimapDynamicLayer.beginFill(0xFFFFFF); // White dot for towers minimapDynamicLayer.drawRect(gridX * tilePixelSize, gridY * tilePixelSize, tilePixelSize, tilePixelSize); minimapDynamicLayer.endFill(); } // Draw Enemies var enemies = Game.EntityManager.getEntitiesWithComponents(['EnemyComponent', 'TransformComponent']); for (var i = 0; i < enemies.length; i++) { var enemyId = enemies[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; // Use the enemy's precise world coordinates for smoother tracking on the minimap var minimapX = transform.x / 64 * tilePixelSize; var minimapY = transform.y / 64 * tilePixelSize; minimapDynamicLayer.beginFill(0xFF0000); // Red dot for enemies minimapDynamicLayer.drawRect(minimapX, minimapY, 3, 3); // Make them slightly smaller than a tile minimapDynamicLayer.endFill(); } } function updateMinimapViewport() { minimapViewportRect.clear(); var tilePixelSize = 5; var worldTileSize = 64; // The cameraX/Y variables are inverse offsets. We need the true top-left coordinate. var trueCameraX = -cameraX; var trueCameraY = -cameraY; // Get screen/viewport dimensions var screenWidth = 2048; // Use the game's actual screen dimensions var screenHeight = 2732; // Convert world coordinates and dimensions to minimap scale var rectX = trueCameraX / worldTileSize * tilePixelSize; var rectY = trueCameraY / worldTileSize * tilePixelSize; var rectWidth = screenWidth / worldTileSize * tilePixelSize; var rectHeight = screenHeight / worldTileSize * tilePixelSize; minimapViewportRect.beginFill(0xFFFFFF, 0.25); // Semi-transparent white minimapViewportRect.drawRect(rectX, rectY, rectWidth, rectHeight); minimapViewportRect.endFill(); } // Add mouse event handlers for camera panning game.down = function (x, y, obj) { isDragging = true; dragStartX = x; dragStartY = y; lastMouseX = x; lastMouseY = y; cameraStartX = cameraX; cameraStartY = cameraY; mouseDownTime = LK.ticks / 60 * 1000; // Convert to milliseconds }; game.move = function (x, y, obj) { lastMouseX = x; lastMouseY = y; if (isDragging) { var dx = x - dragStartX; var dy = y - dragStartY; // Update camera position (inverted for drag effect) cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; // 40 tiles * 64 pixels per tile var mapHeight = 60 * 64; // 60 tiles * 64 pixels per tile var screenWidth = 2048; var screenHeight = 2732; // Ensure we never see beyond the map edges var minX = screenWidth - mapWidth; var minY = screenHeight - mapHeight; var maxX = 0; var maxY = 0; cameraX = Math.max(minX, Math.min(maxX, cameraX)); cameraY = Math.max(minY, Math.min(maxY, cameraY)); // Apply camera transform to game world gameWorld.x = cameraX; gameWorld.y = cameraY; } }; game.up = function (x, y, obj) { isDragging = false; }; // Set up main game loop var powerUpdateTimer = 0; game.update = function () { if (Game.GameManager.isGameOver) { return; } // Run core game systems Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); // --- UI STATE MACHINE FOR DRAG-AND-DROP --- var currentBuildType = Game.GameplayManager.activeBuildType; // Check if the player is currently dragging a tower to build if (currentBuildType && !Game.GameplayManager.isConfirmingPlacement) { // --- STATE: DRAGGING TOWER --- var towerData = Game.TowerData[currentBuildType]; // Ensure UI for other states is hidden confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update the ghost tower sprite that follows the cursor/finger if (!ghostTowerSprite || ghostTowerSprite.towerType !== currentBuildType) { if (ghostTowerSprite) ghostTowerSprite.destroy(); ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = currentBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // Update and show the fixed info panel floatingInfoPanel.visible = true; infoText.setText(currentBuildType + "\nCost: " + towerData.cost); // The panel's position is now fixed and no longer updated here. } else if (Game.GameplayManager.isConfirmingPlacement) { // --- STATE: AWAITING CONFIRMATION --- // Hide other UI elements if (ghostTowerSprite) ghostTowerSprite.visible = false; floatingInfoPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update confirmation buttons if needed if (!confirmButton || confirmButton.towerType !== currentBuildType) { if (confirmButton) confirmButton.destroy(); if (cancelButton) cancelButton.destroy(); var towerData = Game.TowerData[currentBuildType]; // Create CONFIRM button confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 120; // Increased size confirmButton.height = 120; // Increased size confirmButton.x = -65; // Adjusted spacing confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = currentBuildType; confirmButton.down = function () { var coords = Game.GameplayManager.confirmPlacementCoords; var buildType = Game.GameplayManager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); // Create CANCEL button cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 120; // Increased size cancelButton.height = 120; // Increased size cancelButton.x = 65; // Adjusted spacing cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = currentBuildType; cancelButton.down = function () { // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // --- STATE: TOWER SELECTED --- // Hide all build-related UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; floatingInfoPanel.visible = false; // Show upgrade panel and range circle var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'] ? Game.EntityManager.componentStores['RenderComponent'][towerId] : null; var towerComp = Game.EntityManager.componentStores['TowerComponent'] ? Game.EntityManager.componentStores['TowerComponent'][towerId] : null; var attackComp = Game.EntityManager.componentStores['AttackComponent'] ? Game.EntityManager.componentStores['AttackComponent'][towerId] : null; var transformComp = Game.EntityManager.componentStores['TransformComponent'] ? Game.EntityManager.componentStores['TransformComponent'][towerId] : null; if (!renderComp || !towerComp || !transformComp) { Game.GameplayManager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } else { rangeCircle.visible = false; } var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === renderComp.sprite_id) { towerType = type; break; } } towerNameText.setText(towerType + " (Lvl " + towerComp.upgrade_level + ")"); var upgradePathAData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathA'] : null; if (upgradePathAData && towerComp.upgrade_path_A_level < upgradePathAData.length) { var upgradeInfoA = upgradePathAData[towerComp.upgrade_path_A_level]; upgradeAPathButton.setText("Upgrade Path A ($" + upgradeInfoA.cost + ")"); upgradeAPathButton.visible = true; } else { upgradeAPathButton.visible = false; } var upgradePathBData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathB'] : null; if (upgradePathBData && towerComp.upgrade_path_B_level < upgradePathBData.length) { var upgradeInfoB = upgradePathBData[towerComp.upgrade_path_B_level]; upgradeBPathButton.setText("Upgrade Path B ($" + upgradeInfoB.cost + ")"); upgradeBPathButton.visible = true; } else { upgradeBPathButton.visible = false; } var sellPrice = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { sellPrice = towerComp.cost; } sellButton.setText("Sell Tower ($" + sellPrice + ")"); } } else { // --- STATE: IDLE --- // Clean up all dynamic UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } floatingInfoPanel.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; } // The `game.up` event (releasing the mouse/finger) will handle the drop logic. game.up = function () { if (Game.GameplayManager.activeBuildType && !Game.GameplayManager.isConfirmingPlacement) { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); // If dropped on a valid spot, switch to confirmation state if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; confirmPanel.x = gridX * 64 + 32; confirmPanel.y = gridY * 64 + 32; } else { // Dropped on an invalid spot, cancel the build Game.GameplayManager.activeBuildType = null; } } isDragging = false; }; // Update standard HUD elements every frame goldText.setText('Gold: ' + Game.ResourceManager.gold); livesText.setText('Lives: ' + Game.ResourceManager.lives); waveText.setText('Wave: ' + (Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1) + ' / ' + Game.WaveData.length); // Update Power Meter, Minimap, etc. var p_prod = Game.ResourceManager.powerProduction; var p_cons = Game.ResourceManager.powerConsumption; var p_avail = Game.ResourceManager.powerAvailable; var isDeficit = p_cons > p_prod && p_avail <= 0; var barScale = 3; var productionWidth = p_prod * barScale; powerProductionBar.clear(); powerProductionBar.beginFill(0x5DADE2, 0.6); powerProductionBar.drawRect(0, 0, productionWidth, 20); powerProductionBar.endFill(); var consumptionWidth = Math.min(p_cons, p_prod) * barScale; var consumptionColor = 0x00FF7F; if (isDeficit) { consumptionColor = Math.floor(LK.ticks / 10) % 2 === 0 ? 0xFF4136 : 0xD9362D; } else if (p_prod > 0 && p_cons / p_prod >= 0.75) { consumptionColor = 0xFFD700; } powerConsumptionBar.clear(); powerConsumptionBar.beginFill(consumptionColor); powerConsumptionBar.drawRect(0, 0, consumptionWidth, 20); powerConsumptionBar.endFill(); var availableWidth = p_avail * barScale; powerAvailableBar.x = productionWidth; powerAvailableBar.clear(); powerAvailableBar.beginFill(0x5DADE2, 0.4); powerAvailableBar.drawRect(0, 0, availableWidth, 20); powerAvailableBar.endFill(); updateMinimapDynamic(); updateMinimapViewport(); powerUpdateTimer++; if (powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); }; // === SECTION: TOWER RANGE INDICATOR UI === var rangeCircle = gameWorld.attachAsset('range_circle', {}); gameWorld.addChild(rangeCircle); rangeCircle.anchor.set(0.5, 0.5); // Set the origin to the center of the asset rangeCircle.alpha = 0.25; rangeCircle.visible = false; // === SECTION: UPGRADE & SELL UI === var upgradePanel = new Container(); LK.gui.right.addChild(upgradePanel); upgradePanel.x = -50; // 50px padding from the right edge upgradePanel.y = 0; upgradePanel.visible = false; var towerNameText = new Text2('Tower Name', { size: 50, fill: 0xFFFFFF, align: 'right' // Align text to the right }); towerNameText.anchor.set(1, 0.5); // Anchor to the right upgradePanel.addChild(towerNameText); var upgradeAPathButton = new Text2('Upgrade Path A', { size: 45, fill: 0x27AE60, align: 'right' }); upgradeAPathButton.anchor.set(1, 0.5); // Anchor to the right upgradeAPathButton.y = 70; upgradePanel.addChild(upgradeAPathButton); var upgradeBPathButton = new Text2('Upgrade Path B', { size: 45, fill: 0x2980B9, align: 'right' }); upgradeBPathButton.anchor.set(1, 0.5); // Anchor to the right upgradeBPathButton.y = 140; upgradePanel.addChild(upgradeBPathButton); var sellButton = new Text2('Sell Tower', { size: 45, fill: 0xC0392B, align: 'right' }); sellButton.anchor.set(1, 0.5); // Anchor to the right sellButton.y = 210; upgradePanel.addChild(sellButton); sellButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId); } }; upgradeAPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathA'); } }; upgradeBPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathB'); } };
===================================================================
--- original.js
+++ change.js
@@ -2012,11 +2012,11 @@
// Create CONFIRM button
confirmButton = game.attachAsset(towerData.sprite_id, {
tint: 0x2ECC71
});
- confirmButton.width = 80; // Larger button
- confirmButton.height = 80; // Larger button
- confirmButton.x = -50; // Spaced further apart
+ confirmButton.width = 120; // Increased size
+ confirmButton.height = 120; // Increased size
+ confirmButton.x = -65; // Adjusted spacing
confirmButton.anchor.set(0.5, 0.5);
confirmButton.towerType = currentBuildType;
confirmButton.down = function () {
var coords = Game.GameplayManager.confirmPlacementCoords;
@@ -2032,11 +2032,11 @@
// Create CANCEL button
cancelButton = game.attachAsset(towerData.sprite_id, {
tint: 0xE74C3C
});
- cancelButton.width = 80; // Larger button
- cancelButton.height = 80; // Larger button
- cancelButton.x = 50; // Spaced further apart
+ cancelButton.width = 120; // Increased size
+ cancelButton.height = 120; // Increased size
+ cancelButton.x = 65; // Adjusted spacing
cancelButton.anchor.set(0.5, 0.5);
cancelButton.towerType = currentBuildType;
cancelButton.down = function () {
// Reset state