Code edit (1 edits merged)
Please save this source code
User prompt
First, let's fix the tower sizes. We will change their rendered size to fit a single 64x64 grid cell, even though their sprites are larger. Go to the Game.Systems.RenderSystem.update method and add two lines to set the sprite's width and height when it's first created. Find this block: if (!sprite) { sprite = gameWorld.attachAsset(render.sprite_id, {}); sprite.anchor.set(0.5, 0.5); this.displayObjects[entityId] = sprite; } And add the new lines as shown: if (!sprite) { sprite = gameWorld.attachAsset(render.sprite_id, {}); sprite.anchor.set(0.5, 0.5); // --- ADD THESE LINES TO SET TOWER SIZE --- sprite.width = 64; sprite.height = 64; // --- END --- this.displayObjects[entityId] = sprite; } Next, let's fix the minimap sizing. In the drawMinimapBackground function, the minimap_bg asset is being used incorrectly. It should be sized to match the calculated minimap dimensions. Find this line in drawMinimapBackground: var minimapBg = minimapBackground.attachAsset('minimap_bg', {}); And add these two lines right after it: minimapBg.width = mapWidth * tilePixelSize; minimapBg.height = mapHeight * tilePixelSize; Similarly, in the updateMinimapViewport function, the viewport asset needs to be sized correctly. Find this line in updateMinimapViewport: var viewport = minimapViewportRect.attachAsset('minimap_viewport', {}); And add these two lines right after it: viewport.width = rectWidth; viewport.height = rectHeight;
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'fill')' in or related to this line: 'powerStatusText.style.fill = 0x2ECC71; // Green' Line Number: 2036
User prompt
Please fix the bug: 'Graphics is not a constructor' in or related to this line: 'var infoBackground = new Graphics();' Line Number: 2203
User prompt
First, let's delete the old, ineffective power bar UI. Go to the // === SECTION: DIRECT GAME INITIALIZATION === section and delete the entire powerMeterContainer block. Delete everything from this line... // Create Power Meter UI var powerMeterContainer = new Container(); ...down to and including this line: powerMeterContainer.addChild(powerAvailableBar); Next, in the same // === SECTION: DIRECT GAME INITIALIZATION === section, add the definitions for our new text-based power UI. A good place is right after the waveText is defined. // Create new Text-Based Power UI var powerProductionText = new Text2('Production: 0', { size: 40, fill: 0x3498DB }); // Blue powerProductionText.anchor.set(1, 0); LK.gui.topRight.addChild(powerProductionText); powerProductionText.x = -20; powerProductionText.y = 170; var powerConsumptionText = new Text2('Consumption: 0', { size: 40, fill: 0xE67E22 }); // Orange powerConsumptionText.anchor.set(1, 0); LK.gui.topRight.addChild(powerConsumptionText); powerConsumptionText.x = -20; powerConsumptionText.y = 220; var powerStatusText = new Text2('Status: OK', { size: 45, fill: 0x2ECC71 }); // Green powerStatusText.anchor.set(1, 0); LK.gui.topRight.addChild(powerStatusText); powerStatusText.x = -20; powerStatusText.y = 270; Finally, we need to update these new text elements. Go to the Game.GameplayState.update method. Inside the if (this.powerUpdateTimer >= 60) block, replace the entire power bar drawing logic with the new text update logic. Replace this entire block... // --- NEW LOCATION FOR POWER BAR LOGIC --- var p_prod = Game.ResourceManager.powerProduction; // ... (all the power bar drawing code) ... powerAvailableBar.endFill(); // --- END NEW LOCATION --- ...with this new, simpler logic: // --- Update new Power Text UI --- var production = Game.ResourceManager.powerProduction; var consumption = Game.ResourceManager.powerConsumption; powerProductionText.setText('Production: ' + production); powerConsumptionText.setText('Consumption: ' + consumption); if (consumption > production) { powerStatusText.setText('Status: DEFICIT!'); powerStatusText.style.fill = 0xE74C3C; // Red } else if (production > 0 && consumption / production >= 0.8) { powerStatusText.setText('Status: STRAINED'); powerStatusText.style.fill = 0xF1C40F; // Yellow } else { powerStatusText.setText('Status: SURPLUS'); powerStatusText.style.fill = 0x2ECC71; // Green }
User prompt
First, let's delete the old, ineffective power bar UI. Go to the // === SECTION: DIRECT GAME INITIALIZATION === section and delete the entire powerMeterContainer block. Delete everything from this line... // Create Power Meter UI var powerMeterContainer = new Container(); ...down to and including this line: powerMeterContainer.addChild(powerAvailableBar); Next, in the same // === SECTION: DIRECT GAME INITIALIZATION === section, add the definitions for our new text-based power UI. A good place is right after the waveText is defined. // Create new Text-Based Power UI var powerProductionText = new Text2('Production: 0', { size: 40, fill: 0x3498DB }); // Blue powerProductionText.anchor.set(1, 0); LK.gui.topRight.addChild(powerProductionText); powerProductionText.x = -20; powerProductionText.y = 170; var powerConsumptionText = new Text2('Consumption: 0', { size: 40, fill: 0xE67E22 }); // Orange powerConsumptionText.anchor.set(1, 0); LK.gui.topRight.addChild(powerConsumptionText); powerConsumptionText.x = -20; powerConsumptionText.y = 220; var powerStatusText = new Text2('Status: OK', { size: 45, fill: 0x2ECC71 }); // Green powerStatusText.anchor.set(1, 0); LK.gui.topRight.addChild(powerStatusText); powerStatusText.x = -20; powerStatusText.y = 270; Finally, we need to update these new text elements. Go to the Game.GameplayState.update method. Inside the if (this.powerUpdateTimer >= 60) block, replace the entire power bar drawing logic with the new text update logic. Replace this entire block... // --- NEW LOCATION FOR POWER BAR LOGIC --- var p_prod = Game.ResourceManager.powerProduction; // ... (all the power bar drawing code) ... powerAvailableBar.endFill(); // --- END NEW LOCATION --- ...with this new, simpler logic: // --- Update new Power Text UI --- var production = Game.ResourceManager.powerProduction; var consumption = Game.ResourceManager.powerConsumption; powerProductionText.setText('Production: ' + production); powerConsumptionText.setText('Consumption: ' + consumption); if (consumption > production) { powerStatusText.setText('Status: DEFICIT!'); powerStatusText.style.fill = 0xE74C3C; // Red } else if (production > 0 && consumption / production >= 0.8) { powerStatusText.setText('Status: STRAINED'); powerStatusText.style.fill = 0xF1C40F; // Yellow } else { powerStatusText.setText('Status: SURPLUS'); powerStatusText.style.fill = 0x2ECC71; // Green }
User prompt
First, go to the Game.GameplayState.render method and delete the entire block of code that updates the power meter. Delete this entire block from Game.GameplayState.render: // Update Power Meter 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(); Next, go to the Game.GameplayState.update method. Find the if (this.powerUpdateTimer >= 60) block at the end of the function. Insert the power meter drawing logic we just deleted into this block, right after Game.ResourceManager.updatePower();. Your final if (this.powerUpdateTimer >= 60) block should look like this: if (this.powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); // --- NEW LOCATION FOR POWER BAR LOGIC --- 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(); // --- END NEW LOCATION --- if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } this.powerUpdateTimer = 0; }
User prompt
Please replace the entire update: function update(dt) method inside the Game.GameplayState object with the following complete and correct version. This version re-enables the info panel logic while also correctly handling all other UI states. update: function update(dt) { if (Game.GameManager.isGameOver) { return; } // --- Core Game Logic --- Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.BossSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); // --- UI State & Rendering Logic --- var manager = Game.GameplayManager; if (manager.playerInputState === 'DRAGGING_TOWER' && !manager.isConfirmingPlacement) { // STATE: DRAGGING TOWER var towerData = Game.TowerData[manager.activeBuildType]; confirmPanel.visible = false; upgradePanel.visible = false; if (!ghostTowerSprite || ghostTowerSprite.towerType !== manager.activeBuildType) { if (ghostTowerSprite) { ghostTowerSprite.destroy(); } ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = manager.activeBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); var worldX = gridX * 64 + 32; var worldY = gridY * 64 + 32; ghostTowerSprite.x = worldX; ghostTowerSprite.y = worldY; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // --- Show range indicator on drag --- var attackCompDef = towerData.components.find(c => c.name === 'AttackComponent'); var range = attackCompDef ? attackCompDef.args[1] : 0; if (range > 0) { rangeCircle.visible = true; rangeCircle.x = worldX; rangeCircle.y = worldY; rangeCircle.width = range * 2; rangeCircle.height = range * 2; } else { rangeCircle.visible = false; } // --- THIS IS THE FIX: Restore Info Panel Logic --- floatingInfoPanel.visible = true; infoText.setText(manager.activeBuildType + "\nCost: " + towerData.cost); // --- END FIX --- } else if (manager.isConfirmingPlacement) { // STATE: AWAITING CONFIRMATION if (ghostTowerSprite) { ghostTowerSprite.visible = false; } rangeCircle.visible = false; upgradePanel.visible = false; floatingInfoPanel.visible = false; // Also hide info panel here if (!confirmButton || confirmButton.towerType !== manager.activeBuildType) { if (confirmButton) { confirmButton.destroy(); } if (cancelButton) { cancelButton.destroy(); } var towerData = Game.TowerData[manager.activeBuildType]; confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 120; confirmButton.height = 120; confirmButton.x = -65; confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = manager.activeBuildType; confirmButton.down = function () { var coords = manager.confirmPlacementCoords; var buildType = manager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } manager.isConfirmingPlacement = false; manager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 120; cancelButton.height = 120; cancelButton.x = 65; cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = manager.activeBuildType; cancelButton.down = function () { manager.isConfirmingPlacement = false; manager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (manager.selectedTowerId !== -1) { // STATE: TOWER SELECTED if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } confirmPanel.visible = false; floatingInfoPanel.visible = false; var towerId = manager.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) { manager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; upgradePanel.x = transformComp.x + 100; upgradePanel.y = transformComp.y - 140; gameWorld.addChild(upgradePanel); 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 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; } // --- System Updates --- this.powerUpdateTimer++; if (this.powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } this.powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); },
User prompt
Please add the following new wave definition to the end of the Game.WaveData array. , // Wave 6: The Juggernaut Boss { wave_number: 6, sub_waves: [{ enemy_type: 'Juggernaut', count: 1, spawn_delay: 0, start_delay: 2.0 // A small delay before it appears }] } Use code with caution. JavaScript (Note: Be sure to add the comma before the new wave object to keep the array syntax valid.)
User prompt
Please replace the entire 'Juggernaut' entry within the Game.EnemyData object with this updated version. This adds the phase transition data to its BossComponent. 'Juggernaut': { health: 5000, speed: 25, // Initial slow speed gold_value: 250, lives_cost: 20, armor_type: 'Heavy', components: [ { name: 'BossComponent', args: [ // --- NEW PHASE DATA --- [ // Start of phases array { // Phase 2: Transition at 50% HP healthThreshold: 0.5, abilitiesToTrigger: [], // We will add Stunwave later componentModifications: { MovementComponent: { speed: 75 } // Speed increases significantly } } ] // End of phases array ] } ] } Use code with caution. JavaScript Expected Outcome This is still a data-only change. There will be no visible or functional change to the game yet, as the Juggernaut has not been added to any waves. We have now successfully defined how the Juggernaut boss will behave when it transitions phases. The next steps will involve adding it to a wave so we can test it.
User prompt
Please add the following new enemy definition to the Game.EnemyData object. A good place for it is after the 'Flyer' definition. 'Juggernaut': { health: 5000, speed: 25, gold_value: 250, lives_cost: 20, armor_type: 'Heavy', components: [ { name: 'BossComponent', // We will populate the phases array in the next step. args: [ [] ] } ] }
User prompt
Please add the following new system definition to the Game.Systems object. A good place for it is after the TowerSystem. BossSystem: { update: function() { var bosses = Game.EntityManager.getEntitiesWithComponents(['BossComponent', 'HealthComponent']); for (var i = 0; i < bosses.length; i++) { var bossId = bosses[i]; var bossComp = Game.EntityManager.componentStores['BossComponent'][bossId]; var healthComp = Game.EntityManager.componentStores['HealthComponent'][bossId]; // Check if there are more phases to transition to. var nextPhaseIndex = bossComp.currentPhaseIndex + 1; if (nextPhaseIndex > bossComp.phases.length) { continue; // No more phases left. } var nextPhase = bossComp.phases[bossComp.currentPhaseIndex]; // Note: phases are sorted, so we take the current index var healthPercentage = healthComp.current_hp / healthComp.max_hp; if (healthPercentage <= nextPhase.healthThreshold) { console.log('Boss ' + bossId + ' entering phase ' + (nextPhaseIndex)); // --- 1. Apply Component Modifications --- var modifications = nextPhase.componentModifications; for (var componentName in modifications) { var targetComponent = Game.EntityManager.componentStores[componentName] ? Game.EntityManager.componentStores[componentName][bossId] : null; if (targetComponent) { var propertiesToChange = modifications[componentName]; for (var prop in propertiesToChange) { if (targetComponent.hasOwnProperty(prop)) { console.log('...Modifying ' + componentName + '.' + prop + ' to ' + propertiesToChange[prop]); targetComponent[prop] = propertiesToChange[prop]; } } } } // --- 2. Trigger One-Shot Abilities --- var abilities = nextPhase.abilitiesToTrigger; if (abilities && abilities.length > 0) { for (var j = 0; j < abilities.length; j++) { var eventName = abilities[j]; if (Game.Events[eventName]) { console.log('...Triggering ability: ' + eventName); // For now, we assume events are simple and don't require specific boss data. // This will be expanded when we implement the Juggernaut's Stunwave. Game.EventBus.publish(Game.Events[eventName]()); } } } // --- 3. Advance the phase index --- bossComp.currentPhaseIndex++; } } } } Next, we must integrate this new system into the game loop. Go to the Game.GameplayState.update method and add a call to the new system. Find this block of code: // --- Core Game Logic --- Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); And add the new line: // --- Core Game Logic --- Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.BossSystem.update(); // Add this line Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update();
User prompt
Please go to the // === SECTION: ENTITY-COMPONENT-SYSTEM SCAFFOLDING === section and add the following new component definition to the Game.Components object. BossComponent: function BossComponent(phases) { return { name: 'BossComponent', // An array of phase data objects, sorted by healthThreshold descending. // Example phase: { healthThreshold: 0.5, abilitiesToTrigger: ['StunwaveEvent'], componentModifications: {...} } phases: phases || [], // The index of the current phase the boss is in. Starts at 0 (the initial state). currentPhaseIndex: 0 }; },
User prompt
First, we must remove the conflicting input handler from the grid cells. Go to the drawGrid function and completely delete the cellSprite.down = function() { ... }; block. Next, replace all three of the global input handler functions (game.down, game.move, and game.up) with the following new, unified versions. This is the complete and correct logic for all input. // === SECTION: GLOBAL INPUT HANDLERS (UNIFIED) === game.down = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; lastMouseX = x; lastMouseY = y; mouseDownTime = LK.ticks / 60 * 1000; // If the 'obj' is a build button, its own .down handler will set the state to 'DRAGGING_TOWER'. // We do nothing else here in that case. The button's code handles it. if (Game.GameplayManager.playerInputState === 'DRAGGING_TOWER') { isDragging = true; // A tower drag has begun. return; } // If we are not dragging a tower, any other press on the screen initiates a camera pan. Game.GameplayManager.playerInputState = 'PANNING_CAMERA'; isDragging = true; dragStartX = x; dragStartY = y; cameraStartX = cameraX; cameraStartY = cameraY; }; game.move = function (x, y, obj) { lastMouseX = x; lastMouseY = y; if (Game.GameStateManager.currentState() !== Game.GameplayState || !isDragging) return; // Only pan the camera if that is our current state. if (Game.GameplayManager.playerInputState === 'PANNING_CAMERA') { var dx = x - dragStartX; var dy = y - dragStartY; cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; var mapHeight = 60 * 64; var screenWidth = 2048; var screenHeight = 2732; 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)); gameWorld.x = cameraX; gameWorld.y = cameraY; } // The ghost tower movement is handled in GameplayState.update, which is fine. }; game.up = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; var timeSinceMouseDown = (LK.ticks / 60 * 1000) - mouseDownTime; var wasAClick = timeSinceMouseDown < 200 && Math.abs(x - dragStartX) < 10 && Math.abs(y - dragStartY) < 10; if (Game.GameplayManager.playerInputState === 'DRAGGING_TOWER') { // Handle the "drop" action. var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); 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 { Game.GameplayManager.activeBuildType = null; } } else if (Game.GameplayManager.playerInputState === 'PANNING_CAMERA' && wasAClick) { // This was a click, not a drag. Check for tower selection. var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); var gridCell = Game.GridManager.grid[gridX][gridY]; if (gridCell && gridCell.entityId !== null) { Game.GameplayManager.selectedTowerId = gridCell.entityId; } else { Game.GameplayManager.selectedTowerId = -1; } } // On ANY 'up' event, reset the input states. Game.GameplayManager.playerInputState = 'IDLE'; isDragging = false; };
User prompt
First, please delete all the code related to the panningInputSprite. This includes its definition, its event handlers (.down, .move, .up), and the gameWorld.addChild(panningInputSprite) line. Next, we will correctly implement the global input handlers. Please add the following three functions back into the script, replacing any old versions that might exist. A good place is after the // === SECTION: MINIMAP UI === section. Generated javascript // === SECTION: GLOBAL INPUT HANDLERS === game.down = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; lastMouseX = x; lastMouseY = y; mouseDownTime = LK.ticks / 60 * 1000; // The 'obj' parameter is key. If it's null, the user clicked the background. if (obj === null || obj === undefined) { Game.GameplayManager.playerInputState = 'PANNING_CAMERA'; isDragging = true; dragStartX = x; dragStartY = y; cameraStartX = cameraX; cameraStartY = cameraY; } // If 'obj' is not null, it's a UI element like a build button. // That button's own .down handler will set the playerInputState correctly. }; game.move = function (x, y, obj) { lastMouseX = x; lastMouseY = y; if (Game.GameStateManager.currentState() !== Game.GameplayState) return; // Only pan the camera if the state is PANNING_CAMERA and the mouse is held down. if (Game.GameplayManager.playerInputState === 'PANNING_CAMERA' && isDragging) { var dx = x - dragStartX; var dy = y - dragStartY; cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; var mapHeight = 60 * 64; var screenWidth = 2048; var screenHeight = 2732; 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)); gameWorld.x = cameraX; gameWorld.y = cameraY; } // The ghost tower's movement is handled in the GameplayState.update loop, which is correct. }; game.up = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; // If the player was dragging a tower, this is the "drop" action. if (Game.GameplayManager.playerInputState === 'DRAGGING_TOWER') { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); 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 { // Invalid drop location, so cancel the build. Game.GameplayManager.activeBuildType = null; } } // On any 'up' event, reset the input states. Game.GameplayManager.playerInputState = 'IDLE'; isDragging = false; }; Use code with caution. JavaScript Finally, we need to ensure the .down function for the build buttons is correct. In the for loop that creates the build buttons, make sure the .down handler looks exactly like this: Generated javascript button.down = function () { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; Game.GameplayManager.playerInputState = 'DRAGGING_TOWER'; Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; }; Use code with caution. JavaScript (Note: This version is the same as the one from the last prompt, but we are confirming it is correct in the context of the other changes.)
User prompt
First, let's create the dedicated, invisible sprite that will handle our panning input. Please add the following code inside the // === SECTION: GRID DRAWING === section, right at the top, before the gameWorld container is defined. // === SECTION: GRID DRAWING === // Create a full-screen, invisible sprite to catch input for camera panning. // This is the most reliable way to handle background input vs. button input. var panningInputSprite = game.attachAsset('grid_cell', { alpha: 0.01 // Set to a very low alpha to be hittable but invisible }); panningInputSprite.width = 40 * 64; // Full world width panningInputSprite.height = 60 * 64; // Full world height panningInputSprite.x = 0; panningInputSprite.y = 0; panningInputSprite.anchor.set(0, 0); Next, add this new sprite to the gameWorld container so it pans with the world. It must be added first so it is rendered behind everything else. Find this line: var gameWorld = new Container(); And add this line immediately after it: gameWorld.addChild(panningInputSprite); Now, attach the panning logic directly to this new sprite. Add this entire block of code right after the panningInputSprite is defined. // Attach input handlers directly to the panning sprite panningInputSprite.down = function(x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; // A press on this sprite always means we start a camera pan. isDragging = true; dragStartX = x; dragStartY = y; cameraStartX = cameraX; cameraStartY = cameraY; }; panningInputSprite.move = function(x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState || !isDragging) return; var dx = x - dragStartX; var dy = y - dragStartY; cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; var mapHeight = 60 * 64; var screenWidth = 2048; var screenHeight = 2732; 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)); gameWorld.x = cameraX; gameWorld.y = cameraY; }; panningInputSprite.up = function(x, y, obj) { isDragging = false; }; Finally, to prevent any conflicts, we must completely delete the old, faulty global input handlers. Please delete these three functions from the script entirely: // Add mouse event handlers for camera panning game.down = function (x, y, obj) { ... }; game.move = function (x, y, obj) { ... }; game.up = function (x, y, obj) { ... }; (Note: The GameplayState.update logic does NOT need to be changed this time. It is already correct.)
User prompt
Please replace all three of the global input handler functions (game.down, game.move, and game.up) with the following corrected versions. // Add mouse event handlers for camera panning game.down = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; lastMouseX = x; lastMouseY = y; mouseDownTime = LK.ticks / 60 * 1000; // If the press was on the background (not a UI element), intend to pan. if (!obj) { Game.GameplayManager.playerInputState = 'PANNING_CAMERA'; isDragging = true; // Set the flag to indicate a drag action has started. dragStartX = x; dragStartY = y; cameraStartX = cameraX; cameraStartY = cameraY; } // Note: If the press was on a build button, its own .down function will set the // playerInputState to 'DRAGGING_TOWER'. }; game.move = function (x, y, obj) { lastMouseX = x; lastMouseY = y; if (Game.GameStateManager.currentState() !== Game.GameplayState) return; // Only pan the camera if the intent is to pan AND the drag action is active. if (Game.GameplayManager.playerInputState === 'PANNING_CAMERA' && isDragging) { var dx = x - dragStartX; var dy = y - dragStartY; cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; var mapHeight = 60 * 64; var screenWidth = 2048; var screenHeight = 2732; 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)); gameWorld.x = cameraX; gameWorld.y = cameraY; } }; game.up = function (x, y, obj) { // If we were dragging a tower, handle the drop logic. if (Game.GameplayManager.playerInputState === 'DRAGGING_TOWER') { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); 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 { Game.GameplayManager.activeBuildType = null; } } // CRITICAL: Reset both state variables on ANY mouse/touch release. Game.GameplayManager.playerInputState = 'IDLE'; isDragging = false; };
User prompt
Please replace the game.down function with this corrected version. This version re-introduces the isDragging = true; line for camera panning while keeping the rest of the logic intact. game.down = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; if (!obj) { // This is a press on the background, so we pan the camera. Game.GameplayManager.playerInputState = 'PANNING_CAMERA'; isDragging = true; // This was the missing line for camera panning dragStartX = x; dragStartY = y; cameraStartX = cameraX; cameraStartY = cameraY; } // If 'obj' is a tower build button, its own .down function handles the state change. lastMouseX = x; lastMouseY = y; mouseDownTime = LK.ticks / 60 * 1000; };
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'args')' in or related to this line: 'return c.name === 'AttackComponent';' Line Number: 1809
User prompt
First, let's simplify our input state tracking. Go to the Game.GameplayManager and replace the isDraggingTower: false, line with a new, more comprehensive state variable. Replace this line: isDraggingTower: false, With this new line: playerInputState: 'IDLE', // Can be 'IDLE', 'PANNING_CAMERA', or 'DRAGGING_TOWER' Next, we need to update the global input handlers (game.down, game.move, game.up) to correctly manage this new state. This is the core fix for the touch issue. Replace all three global input handlers with these new versions: // Add mouse event handlers for camera panning game.down = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; // The 'obj' parameter tells us if the press was on a specific UI element. // If 'obj' is null or undefined, it was a press on the background, so we pan the camera. if (!obj) { Game.GameplayManager.playerInputState = 'PANNING_CAMERA'; dragStartX = x; dragStartY = y; cameraStartX = cameraX; cameraStartY = cameraY; } // If 'obj' is a tower build button, its own .down function will set the state to 'DRAGGING_TOWER'. lastMouseX = x; lastMouseY = y; mouseDownTime = LK.ticks / 60 * 1000; }; game.move = function (x, y, obj) { lastMouseX = x; lastMouseY = y; if (Game.GameStateManager.currentState() !== Game.GameplayState) return; if (Game.GameplayManager.playerInputState === 'PANNING_CAMERA') { var dx = x - dragStartX; var dy = y - dragStartY; cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; var mapHeight = 60 * 64; var screenWidth = 2048; var screenHeight = 2732; 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)); gameWorld.x = cameraX; gameWorld.y = cameraY; } }; game.up = function (x, y, obj) { if (Game.GameplayManager.playerInputState === 'DRAGGING_TOWER') { // This now correctly handles the "drop" action for touch and mouse var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); 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 { Game.GameplayManager.activeBuildType = null; } } // Reset the input state regardless of what it was Game.GameplayManager.playerInputState = 'IDLE'; }; Now update the build button's .down function to use the new playerInputState. In the for loop that creates the build buttons, replace the .down function with this: button.down = function () { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; Game.GameplayManager.playerInputState = 'DRAGGING_TOWER'; Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; }; Finally, replace the Game.GameplayState.update method one last time. This version removes the complex input-checking logic and adds the range indicator visibility during the drag state. Replace the entire update method of Game.GameplayState with this streamlined version: update: function update(dt) { if (Game.GameManager.isGameOver) { return; } // --- Core Game Logic --- Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); // --- UI State & Rendering Logic --- var manager = Game.GameplayManager; if (manager.playerInputState === 'DRAGGING_TOWER' && !manager.isConfirmingPlacement) { // STATE: DRAGGING TOWER var towerData = Game.TowerData[manager.activeBuildType]; confirmPanel.visible = false; upgradePanel.visible = false; if (!ghostTowerSprite || ghostTowerSprite.towerType !== manager.activeBuildType) { if (ghostTowerSprite) { ghostTowerSprite.destroy(); } ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = manager.activeBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); var worldX = gridX * 64 + 32; var worldY = gridY * 64 + 32; ghostTowerSprite.x = worldX; ghostTowerSprite.y = worldY; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // --- NEW: Show range indicator on drag --- var attackCompArgs = towerData.components.find(c => c.name === 'AttackComponent').args; var range = attackCompArgs ? attackCompArgs[1] : 0; if (range > 0) { rangeCircle.visible = true; rangeCircle.x = worldX; rangeCircle.y = worldY; rangeCircle.width = range * 2; rangeCircle.height = range * 2; } else { rangeCircle.visible = false; } } else if (manager.isConfirmingPlacement) { // STATE: AWAITING CONFIRMATION if (ghostTowerSprite) { ghostTowerSprite.visible = false; } rangeCircle.visible = false; // Hide range circle during confirmation upgradePanel.visible = false; if (!confirmButton || confirmButton.towerType !== manager.activeBuildType) { if (confirmButton) { confirmButton.destroy(); } if (cancelButton) { cancelButton.destroy(); } var towerData = Game.TowerData[manager.activeBuildType]; confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 120; confirmButton.height = 120; confirmButton.x = -65; confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = manager.activeBuildType; confirmButton.down = function () { var coords = manager.confirmPlacementCoords; var buildType = manager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } manager.isConfirmingPlacement = false; manager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 120; cancelButton.height = 120; cancelButton.x = 65; cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = manager.activeBuildType; cancelButton.down = function () { manager.isConfirmingPlacement = false; manager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (manager.selectedTowerId !== -1) { // STATE: TOWER SELECTED if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } confirmPanel.visible = false; // Update and show upgrade panel logic (this part remains the same) // ... (rest of the tower selection logic) ... var towerId = manager.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) { manager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; upgradePanel.x = transformComp.x + 100; upgradePanel.y = transformComp.y - 140; gameWorld.addChild(upgradePanel); 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 if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; } // --- System Updates --- this.powerUpdateTimer++; if (this.powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } this.powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); },
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: 1847
User prompt
First, we need to add a new state variable to Game.GameplayManager to track if the player is actively dragging a tower icon. Add the new isDraggingTower property to the manager. Add this line inside the Game.GameplayManager object: isDraggingTower: false, Next, modify the .down function on the build panel buttons to set this new flag to true. In the for loop that creates the build buttons, replace this .down function: button.down = function () { if (Game.GameStateManager.currentState() !== Game.GameplayState) { return; } // 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 }; With this new version: button.down = function () { if (Game.GameStateManager.currentState() !== Game.GameplayState) { return; } Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; Game.GameplayManager.isDraggingTower = true; // Set the drag flag }; Finally, we will completely overhaul the UI logic in Game.GameplayState.update and remove the problematic logic from game.up. First, replace the entire game.up function with a simple version that only handles camera panning: game.up = function (x, y, obj) { isDragging = false; // This is for camera panning only now }; Second, replace the entire update method of Game.GameplayState with this new, reorganized version: update: function update(dt) { if (Game.GameManager.isGameOver) { return; } // --- Core Game Logic --- Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); // --- Input and UI State Logic --- var manager = Game.GameplayManager; // If the player releases the touch/mouse while dragging a tower if (manager.isDraggingTower && !isDragging) { // !isDragging implies mouse/touch is up var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); if (Game.GridManager.isBuildable3x3(gridX, gridY)) { // Valid drop, switch to confirming placement state manager.isConfirmingPlacement = true; manager.confirmPlacementCoords = { x: gridX, y: gridY }; confirmPanel.x = gridX * 64 + 32; confirmPanel.y = gridY * 64 + 32; } else { // Invalid drop, cancel build manager.activeBuildType = null; } manager.isDraggingTower = false; // End the drag state } // --- UI Rendering Logic --- if (manager.activeBuildType && !manager.isConfirmingPlacement) { // STATE: DRAGGING TOWER var towerData = Game.TowerData[manager.activeBuildType]; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; if (!ghostTowerSprite || ghostTowerSprite.towerType !== manager.activeBuildType) { if (ghostTowerSprite) { ghostTowerSprite.destroy(); } ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = manager.activeBuildType; } 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; floatingInfoPanel.visible = true; infoText.setText(manager.activeBuildType + "\nCost: " + towerData.cost); } else if (manager.isConfirmingPlacement) { // STATE: AWAITING CONFIRMATION if (ghostTowerSprite) { ghostTowerSprite.visible = false; } floatingInfoPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Logic for confirm/cancel buttons remains the same if (!confirmButton || confirmButton.towerType !== manager.activeBuildType) { if (confirmButton) { confirmButton.destroy(); } if (cancelButton) { cancelButton.destroy(); } var towerData = Game.TowerData[manager.activeBuildType]; confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 120; confirmButton.height = 120; confirmButton.x = -65; confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = manager.activeBuildType; confirmButton.down = function () { var coords = manager.confirmPlacementCoords; var buildType = manager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } manager.isConfirmingPlacement = false; manager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 120; cancelButton.height = 120; cancelButton.x = 65; cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = manager.activeBuildType; cancelButton.down = function () { manager.isConfirmingPlacement = false; manager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (manager.selectedTowerId !== -1) { // STATE: TOWER SELECTED (This logic block remains unchanged) // ... (rest of the tower selection logic) ... if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; floatingInfoPanel.visible = false; var towerId = manager.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) { manager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; upgradePanel.x = transformComp.x + 100; upgradePanel.y = transformComp.y - 140; gameWorld.addChild(upgradePanel); 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 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; } // --- System Updates that run every frame --- this.powerUpdateTimer++; if (this.powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } this.powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); },
User prompt
Please go to the Game.TowerData object and make the following range updates. For the 'RockLauncher', in its AttackComponent definition, replace the args line with the new one below to change its range from 200 to 350. Replace this: args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50, false] // splash, can_target_air With this: args: [25, 350, 0.5, 0, 'rock_projectile', -1, 50, false] // splash, can_target_air For the 'ChemicalTower', in its AttackComponent definition, replace the args line to change its range from 150 to 280. Replace this: args: [5, 150, 0.8, 0, 'chemical_projectile', -1, 0] With this: args: [5, 280, 0.8, 0, 'chemical_projectile', -1, 0] For the 'SlowTower', in its AttackComponent definition, replace the args line to change its range from 160 to 300. Replace this: args: [2, 160, 1.0, 0, 'slow_projectile'] With this: args: [2, 300, 1.0, 0, 'slow_projectile'] For the 'DarkTower', in its AuraComponent definition, replace the args line to change its range from 175 to 325. Replace this: args: [175, 'damage_buff'] With this: args: [325, 'damage_buff']
User prompt
Please replace only the update method inside the Game.Systems.TargetingSystem object with this new, corrected version. update: function update() { if (Game.Systems.CombatSystem.isPowerDeficit) { var towersToClear = Game.EntityManager.getEntitiesWithComponents(['AttackComponent']); for (var i = 0; i < towersToClear.length; i++) { var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towersToClear[i]]; towerAttack.target_id = -1; } return; } var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); var enemies = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent', 'EnemyComponent']); 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 bestTargetId = -1; // We want the enemy with the minimum distance to the goal (i.e., furthest along the path). var bestTargetMetric = Infinity; 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 distanceToTower = Math.sqrt(dx * dx + dy * dy); if (distanceToTower > towerAttack.range) { continue; // Skip if out of range } var isFlyer = Game.EntityManager.componentStores['FlyingComponent'] && Game.EntityManager.componentStores['FlyingComponent'][enemyId]; if (isFlyer) { // This is a flying enemy. Check if the tower can attack it. if (towerAttack.can_target_air) { var distToGoal = Math.sqrt(Math.pow(39 * 64 - enemyTransform.x, 2) + Math.pow(30 * 64 - enemyTransform.y, 2)); if (distToGoal < bestTargetMetric) { bestTargetMetric = distToGoal; bestTargetId = enemyId; } } } else { // This is a ground enemy. var gridX = Math.floor(enemyTransform.x / 64); var gridY = Math.floor(enemyTransform.y / 64); // Ensure the enemy is on a valid part of the path grid before checking distance. if (Game.PathManager.distanceField[gridX] && Game.PathManager.distanceField[gridX][gridY] !== undefined) { var distToGoal = Game.PathManager.distanceField[gridX][gridY]; if (distToGoal < bestTargetMetric) { bestTargetMetric = distToGoal; bestTargetId = enemyId; } } } } towerAttack.target_id = bestTargetId; } }
User prompt
Please replace the entire Game.Systems.TargetingSystem object with this updated version. The new logic adds a check for the FlyingComponent on enemies and the can_target_air property on towers. Game.Systems.TargetingSystem = { update: function update() { if (Game.Systems.CombatSystem.isPowerDeficit) { var towersToClear = Game.EntityManager.getEntitiesWithComponents(['AttackComponent']); for (var i = 0; i < towersToClear.length; i++) { var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towersToClear[i]]; towerAttack.target_id = -1; } return; } var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); var enemies = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent', 'EnemyComponent']); 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 bestTargetId = -1; var bestTargetMetric = -1; // For ground units, this is distance along the path. For flyers, just distance. 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) { continue; // Skip if out of range } var isFlyer = Game.EntityManager.componentStores['FlyingComponent'] && Game.EntityManager.componentStores['FlyingComponent'][enemyId]; // --- NEW TARGETING LOGIC --- if (isFlyer) { // This is a flying enemy. Check if the tower can attack it. if (towerAttack.can_target_air) { // Simple 'strongest' targeting for flyers for now (closest to goal = furthest travelled) var flyerDistToGoal = Math.sqrt(Math.pow(39*64 - enemyTransform.x, 2) + Math.pow(30*64 - enemyTransform.y, 2)); var targetMetric = 10000 - flyerDistToGoal; // Higher value is better if (targetMetric > bestTargetMetric) { bestTargetMetric = targetMetric; bestTargetId = enemyId; } } } else { // This is a ground enemy. All towers can attack ground. // Prioritize enemy furthest along the path. var gridX = Math.floor(enemyTransform.x / 64); var gridY = Math.floor(enemyTransform.y / 64); var pathDistance = Game.PathManager.distanceField[gridX] ? Game.PathManager.distanceField[gridX][gridY] : Infinity; var targetMetric = -pathDistance; // We want the smallest distance, so we maximize the negative. if (targetMetric > bestTargetMetric) { bestTargetMetric = targetMetric; bestTargetId = enemyId; } } // --- END NEW TARGETING LOGIC --- } towerAttack.target_id = bestTargetId; } } };
/**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // === SECTION: ASSETS === // === SECTION: GLOBAL NAMESPACE === var Game = {}; // Camera state var cameraX = 0; var cameraY = 0; var isDragging = false; var dragStartX = 0; var dragStartY = 0; var cameraStartX = 0; var cameraStartY = 0; var lastMouseX = 0; var lastMouseY = 0; var mouseDownTime = 0; // === SECTION: GLOBAL MANAGERS === Game.ResourceManager = { gold: 1500, 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 playerInputState: 'IDLE', // Can be 'IDLE', 'PANNING_CAMERA', or 'DRAGGING_TOWER' 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)); Game.EventBus.subscribe('CancelPlayerAction', this.cancelAllPlayerActions.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; } }, cancelAllPlayerActions: function cancelAllPlayerActions(event) { console.log("Cancelling player actions..."); this.activeBuildType = null; this.isConfirmingPlacement = false; this.selectedTowerId = -1; } }; 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, can_target_air) { 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, can_target_air: can_target_air !== undefined ? can_target_air : true // Default to true for legacy towers }; }, 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 }; }, BossComponent: function BossComponent(phases) { return { name: 'BossComponent', // An array of phase data objects, sorted by healthThreshold descending. // Example phase: { healthThreshold: 0.5, abilitiesToTrigger: ['StunwaveEvent'], componentModifications: {...} } phases: phases || [], // The index of the current phase the boss is in. Starts at 0 (the initial state). currentPhaseIndex: 0 }; } }; Game.Systems = { MovementSystem: { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); var frame_time = 1 / 60; // Assuming 60 FPS // Goal in Grid Coordinates var goalGrid = { x: 39, y: 30 }; // Goal in World Coordinates (center of the grid cell) var goalWorld = { x: goalGrid.x * 64 + 32, y: goalGrid.y * 64 + 32 }; 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]; // Apply slow debuff if present var currentSpeed = movement.speed; 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]; } } var moveDistance = currentSpeed * frame_time; var isFlyer = Game.EntityManager.componentStores['FlyingComponent'] && Game.EntityManager.componentStores['FlyingComponent'][entityId]; if (isFlyer) { // --- NEW FLYER LOGIC --- // Flyers move in a straight line to the goal. var dx = goalWorld.x - transform.x; var dy = goalWorld.y - transform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 1) { // Avoid division by zero and jittering at goal var moveX = dx / distance * moveDistance; var moveY = dy / distance * moveDistance; transform.x += moveX; transform.y += moveY; } } else { // --- EXISTING GROUND LOGIC --- // Ground units follow the flow field. var direction = Game.PathManager.getVectorAt(transform.x, transform.y); transform.x += direction.x * moveDistance; transform.y += direction.y * moveDistance; } // Check if any enemy has reached the goal. var gridX = Math.floor(transform.x / 64); var gridY = Math.floor(transform.y / 64); if (gridX === goalGrid.x && gridY === goalGrid.y) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } } }, RenderSystem: { displayObjects: {}, update: function update() { var renderableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'RenderComponent']); for (var i = 0; i < renderableEntities.length; i++) { var entityId = renderableEntities[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][entityId]; var render = Game.EntityManager.componentStores['RenderComponent'][entityId]; 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 (Game.Systems.CombatSystem.isPowerDeficit) { var towersToClear = Game.EntityManager.getEntitiesWithComponents(['AttackComponent']); for (var i = 0; i < towersToClear.length; i++) { var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towersToClear[i]]; towerAttack.target_id = -1; } return; } var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); var enemies = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent', 'EnemyComponent']); 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 bestTargetId = -1; // We want the enemy with the minimum distance to the goal (i.e., furthest along the path). var bestTargetMetric = Infinity; 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 distanceToTower = Math.sqrt(dx * dx + dy * dy); if (distanceToTower > towerAttack.range) { continue; // Skip if out of range } var isFlyer = Game.EntityManager.componentStores['FlyingComponent'] && Game.EntityManager.componentStores['FlyingComponent'][enemyId]; if (isFlyer) { // This is a flying enemy. Check if the tower can attack it. if (towerAttack.can_target_air) { var distToGoal = Math.sqrt(Math.pow(39 * 64 - enemyTransform.x, 2) + Math.pow(30 * 64 - enemyTransform.y, 2)); if (distToGoal < bestTargetMetric) { bestTargetMetric = distToGoal; bestTargetId = enemyId; } } } else { // This is a ground enemy. var gridX = Math.floor(enemyTransform.x / 64); var gridY = Math.floor(enemyTransform.y / 64); // Ensure the enemy is on a valid part of the path grid before checking distance. if (Game.PathManager.distanceField[gridX] && Game.PathManager.distanceField[gridX][gridY] !== undefined) { var distToGoal = Game.PathManager.distanceField[gridX][gridY]; if (distToGoal < bestTargetMetric) { bestTargetMetric = distToGoal; bestTargetId = enemyId; } } } } towerAttack.target_id = bestTargetId; } } }, 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); } }, BossSystem: { update: function update() { var bosses = Game.EntityManager.getEntitiesWithComponents(['BossComponent', 'HealthComponent']); for (var i = 0; i < bosses.length; i++) { var bossId = bosses[i]; var bossComp = Game.EntityManager.componentStores['BossComponent'][bossId]; var healthComp = Game.EntityManager.componentStores['HealthComponent'][bossId]; // Check if there are more phases to transition to. var nextPhaseIndex = bossComp.currentPhaseIndex + 1; if (nextPhaseIndex > bossComp.phases.length) { continue; // No more phases left. } var nextPhase = bossComp.phases[bossComp.currentPhaseIndex]; // Note: phases are sorted, so we take the current index var healthPercentage = healthComp.current_hp / healthComp.max_hp; if (healthPercentage <= nextPhase.healthThreshold) { console.log('Boss ' + bossId + ' entering phase ' + nextPhaseIndex); // --- 1. Apply Component Modifications --- var modifications = nextPhase.componentModifications; for (var componentName in modifications) { var targetComponent = Game.EntityManager.componentStores[componentName] ? Game.EntityManager.componentStores[componentName][bossId] : null; if (targetComponent) { var propertiesToChange = modifications[componentName]; for (var prop in propertiesToChange) { if (targetComponent.hasOwnProperty(prop)) { console.log('...Modifying ' + componentName + '.' + prop + ' to ' + propertiesToChange[prop]); targetComponent[prop] = propertiesToChange[prop]; } } } } // --- 2. Trigger One-Shot Abilities --- var abilities = nextPhase.abilitiesToTrigger; if (abilities && abilities.length > 0) { for (var j = 0; j < abilities.length; j++) { var eventName = abilities[j]; if (Game.Events[eventName]) { console.log('...Triggering ability: ' + eventName); // For now, we assume events are simple and don't require specific boss data. // This will be expanded when we implement the Juggernaut's Stunwave. Game.EventBus.publish(Game.Events[eventName]()); } } } // --- 3. Advance the phase index --- bossComp.currentPhaseIndex++; } } } } }; // === 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: [] }] }, 'Juggernaut': { health: 5000, speed: 25, gold_value: 250, lives_cost: 20, armor_type: 'Heavy', components: [{ name: 'BossComponent', // We will populate the phases array in the next step. 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, 300, 1.0, 0, 'arrow_projectile', undefined, 0, true] // damage, range, speed, last_attack, projectile, targetId, splash, can_target_air }, { 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, 350, 0.5, 0, 'rock_projectile', -1, 50, false] // splash, can_target_air }, { name: 'PowerConsumptionComponent', args: [10] }] }, 'ChemicalTower': { cost: 100, sprite_id: 'chemical_tower', color: 0xb87333, components: [{ name: 'TowerComponent', args: [100, 50, undefined] }, { name: 'AttackComponent', args: [5, 280, 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, 300, 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: [325, '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; // Grid cells no longer have input handlers - all input is handled by global handlers } } // Highlight the path cells if (Game.PathManager.currentPath) { for (var i = 0; i < Game.PathManager.currentPath.length; i++) { var pathNode = Game.PathManager.currentPath[i]; var pathSprite = gameWorld.attachAsset('path_cell', {}); // 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' }; }, CancelPlayerAction: function CancelPlayerAction() { return { type: 'CancelPlayerAction' }; } }; // === SECTION: STACK-BASED GAME STATE MANAGER === /** * Game.GameStateManager * Manages a stack of game states (e.g., MainMenu, Gameplay, Paused). * The state at the top of the stack is the active state. * It is responsible for calling the lifecycle methods (enter, exit, update, render) * on the states it manages, as defined by the IGameState interface. */ Game.GameStateManager = { states: [], // The stack of game states /** * Gets the state currently at the top of the stack. * @returns {Object|null} The current state object or null if the stack is empty. */ currentState: function currentState() { return this.states.length > 0 ? this.states[this.states.length - 1] : null; }, /** * Pushes a new state onto the stack, making it the active state. * Calls the enter() method on the new state. * @param {Object} state - The state object to push. */ pushState: function pushState(state) { if (state && state.enter) { state.enter(); } this.states.push(state); }, /** * Pops the current state off the stack. * Calls the exit() method on the state being popped. * @returns {Object|null} The state that was popped or null. */ popState: function popState() { if (this.states.length > 0) { var state = this.currentState(); if (state && state.exit) { state.exit(); } return this.states.pop(); } return null; }, /** * Replaces the current state with a new one by popping the old * state and pushing the new one. This correctly handles the * exit() and enter() calls. * @param {Object} state - The new state object. */ changeState: function changeState(state) { this.popState(); this.pushState(state); }, /** * Delegates the update call to the current state. * @param {number} dt - The delta time for the frame. */ update: function update(dt) { var state = this.currentState(); if (state && state.update) { state.update(dt); } }, /** * Delegates the render call to the current state. * @param {Object} ctx - The rendering context (if any). */ render: function render(ctx) { var state = this.currentState(); if (state && state.render) { state.render(ctx); } } }; /** * Game States * These objects represent the different states the game can be in. * They all adhere to the IGameState interface, which requires them * to have enter(), exit(), update(dt), and render(ctx) methods. */ // A placeholder for the main menu screen. Game.MainMenuState = { enter: function enter() { console.log("Entering MainMenuState..."); }, exit: function exit() { console.log("Exiting MainMenuState..."); }, update: function update(dt) {/* Logic for main menu update */}, render: function render(ctx) {/* Logic for main menu rendering */} }; // The state where the main tower defense gameplay occurs. Game.GameplayState = { powerUpdateTimer: 0, enter: function enter() { console.log("Entering GameplayState..."); // Initialize or re-initialize anything specific to a gameplay session this.powerUpdateTimer = 0; }, exit: function exit() { console.log("Exiting GameplayState..."); // Clean up anything from the gameplay session if needed }, update: function update(dt) { if (Game.GameManager.isGameOver) { return; } // --- Core Game Logic --- Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.BossSystem.update(); // Add this line Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); // --- UI State & Rendering Logic --- var manager = Game.GameplayManager; if (manager.playerInputState === 'DRAGGING_TOWER' && !manager.isConfirmingPlacement) { // STATE: DRAGGING TOWER var towerData = Game.TowerData[manager.activeBuildType]; confirmPanel.visible = false; upgradePanel.visible = false; if (!ghostTowerSprite || ghostTowerSprite.towerType !== manager.activeBuildType) { if (ghostTowerSprite) { ghostTowerSprite.destroy(); } ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = manager.activeBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); var worldX = gridX * 64 + 32; var worldY = gridY * 64 + 32; ghostTowerSprite.x = worldX; ghostTowerSprite.y = worldY; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // --- NEW: Show range indicator on drag --- var attackCompArgs = null; if (towerData.components) { var attackComp = towerData.components.find(function (c) { return c.name === 'AttackComponent'; }); attackCompArgs = attackComp ? attackComp.args : null; } var range = attackCompArgs ? attackCompArgs[1] : 0; if (range > 0) { rangeCircle.visible = true; rangeCircle.x = worldX; rangeCircle.y = worldY; rangeCircle.width = range * 2; rangeCircle.height = range * 2; } else { rangeCircle.visible = false; } } else if (manager.isConfirmingPlacement) { // STATE: AWAITING CONFIRMATION if (ghostTowerSprite) { ghostTowerSprite.visible = false; } rangeCircle.visible = false; // Hide range circle during confirmation upgradePanel.visible = false; if (!confirmButton || confirmButton.towerType !== manager.activeBuildType) { if (confirmButton) { confirmButton.destroy(); } if (cancelButton) { cancelButton.destroy(); } var towerData = Game.TowerData[manager.activeBuildType]; if (!towerData) { manager.isConfirmingPlacement = false; manager.activeBuildType = null; return; } confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 120; confirmButton.height = 120; confirmButton.x = -65; confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = manager.activeBuildType; confirmButton.down = function () { var coords = manager.confirmPlacementCoords; var buildType = manager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } manager.isConfirmingPlacement = false; manager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 120; cancelButton.height = 120; cancelButton.x = 65; cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = manager.activeBuildType; cancelButton.down = function () { manager.isConfirmingPlacement = false; manager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (manager.selectedTowerId !== -1) { // STATE: TOWER SELECTED if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } confirmPanel.visible = false; // Update and show upgrade panel logic (this part remains the same) // ... (rest of the tower selection logic) ... var towerId = manager.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) { manager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; upgradePanel.x = transformComp.x + 100; upgradePanel.y = transformComp.y - 140; gameWorld.addChild(upgradePanel); 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 if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; } // --- System Updates --- this.powerUpdateTimer++; if (this.powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } this.powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); }, render: function render(ctx) { // Run systems that handle visual updates Game.Systems.RenderSystem.update(); // Update HUD elements 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 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(); // Update Minimap updateMinimapDynamic(); updateMinimapViewport(); } }; // The state when the game is paused. This will overlay the GameplayState. Game.PausedState = { enter: function enter() { console.log("Entering PausedState..."); Game.EventBus.publish(Game.Events.CancelPlayerAction()); // Show pause overlay and switch button visibility pauseOverlay.visible = true; pauseText.visible = true; pauseButtonAsset.visible = false; playButtonAsset.visible = true; }, exit: function exit() { console.log("Exiting PausedState..."); // Hide pause overlay and switch button visibility back pauseOverlay.visible = false; pauseText.visible = false; pauseButtonAsset.visible = true; playButtonAsset.visible = false; }, update: function update(dt) { // Paused state does not update game logic. }, render: function render(ctx) { // CRITICAL: To create the "frozen" effect, we must first // render the state underneath this one on the stack (the GameplayState). var underlyingState = Game.GameStateManager.states[Game.GameStateManager.states.length - 2]; if (underlyingState && underlyingState.render) { underlyingState.render(ctx); } // The pauseOverlay and pauseText are already visible and will be drawn on top. } }; // The state for the "Game Over" screen. Game.GameOverState = { enter: function enter() { console.log("Entering GameOverState..."); }, exit: function exit() { console.log("Exiting GameOverState..."); }, update: function update(dt) {/* Logic for game over update */}, render: function render(ctx) {/* Logic for game over rendering */} }; // === 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 Pause and Play/Resume Buttons var pauseButtonAsset = LK.gui.topLeft.attachAsset('ui_pause_button', {}); pauseButtonAsset.x = 180; // Positioned to the right of the system's pause button pauseButtonAsset.y = 20; pauseButtonAsset.visible = true; var playButtonAsset = LK.gui.topLeft.attachAsset('ui_play_button', {}); playButtonAsset.x = 180; // Positioned in the same spot playButtonAsset.y = 20; playButtonAsset.visible = false; // Initially hidden // When the player clicks the pause button pauseButtonAsset.down = function () { Game.GameStateManager.pushState(Game.PausedState); }; // When the player clicks the resume (play) button playButtonAsset.down = function () { Game.GameStateManager.popState(); }; // 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 () { if (Game.GameStateManager.currentState() !== Game.GameplayState) { return; } // 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 () { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; Game.GameplayManager.playerInputState = 'DRAGGING_TOWER'; Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; }; // 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) { if (Game.GameStateManager.currentState() !== Game.GameplayState) { return; } // 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(); } // === SECTION: GLOBAL INPUT HANDLERS (UNIFIED) === game.down = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; lastMouseX = x; lastMouseY = y; mouseDownTime = LK.ticks / 60 * 1000; // If the 'obj' is a build button, its own .down handler will set the state to 'DRAGGING_TOWER'. // We do nothing else here in that case. The button's code handles it. if (Game.GameplayManager.playerInputState === 'DRAGGING_TOWER') { isDragging = true; // A tower drag has begun. return; } // If we are not dragging a tower, any other press on the screen initiates a camera pan. Game.GameplayManager.playerInputState = 'PANNING_CAMERA'; isDragging = true; dragStartX = x; dragStartY = y; cameraStartX = cameraX; cameraStartY = cameraY; }; game.move = function (x, y, obj) { lastMouseX = x; lastMouseY = y; if (Game.GameStateManager.currentState() !== Game.GameplayState || !isDragging) return; // Only pan the camera if that is our current state. if (Game.GameplayManager.playerInputState === 'PANNING_CAMERA') { var dx = x - dragStartX; var dy = y - dragStartY; cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; var mapHeight = 60 * 64; var screenWidth = 2048; var screenHeight = 2732; 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)); gameWorld.x = cameraX; gameWorld.y = cameraY; } // The ghost tower movement is handled in GameplayState.update, which is fine. }; game.up = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; var timeSinceMouseDown = LK.ticks / 60 * 1000 - mouseDownTime; var wasAClick = timeSinceMouseDown < 200 && Math.abs(x - dragStartX) < 10 && Math.abs(y - dragStartY) < 10; if (Game.GameplayManager.playerInputState === 'DRAGGING_TOWER') { // Handle the "drop" action. var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); 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 { Game.GameplayManager.activeBuildType = null; } } else if (Game.GameplayManager.playerInputState === 'PANNING_CAMERA' && wasAClick) { // This was a click, not a drag. Check for tower selection. var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); var gridCell = Game.GridManager.grid[gridX][gridY]; if (gridCell && gridCell.entityId !== null) { Game.GameplayManager.selectedTowerId = gridCell.entityId; } else { Game.GameplayManager.selectedTowerId = -1; } } // On ANY 'up' event, reset the input states. Game.GameplayManager.playerInputState = 'IDLE'; isDragging = false; }; // Set up main game loop game.update = function () { // The main loop now simply delegates update and render calls // to the active state managed by the GameStateManager. // The concept of 'dt' is handled internally by systems using LK.ticks. Game.GameStateManager.update(); Game.GameStateManager.render(); }; // === 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: PAUSE UI === var pauseOverlay = new Graphics(); pauseOverlay.beginFill(0x000000, 0.7); pauseOverlay.drawRect(0, 0, 2048, 2732); // Covers the whole screen pauseOverlay.endFill(); pauseOverlay.visible = false; game.addChild(pauseOverlay); // Add directly to the game stage to be on top var pauseText = new Text2('PAUSED', { size: 200, fill: 0xFFFFFF }); pauseText.anchor.set(0.5, 0.5); pauseText.x = 2048 / 2; pauseText.y = 2732 / 2; pauseText.visible = false; game.addChild(pauseText); // Add directly to the game stage to be on top // === SECTION: UPGRADE & SELL UI === var upgradePanel = new Container(); upgradePanel.zIndex = 100; // A high value to ensure it renders on top gameWorld.addChild(upgradePanel); upgradePanel.x = -50; // 50px padding from the right edge upgradePanel.y = 0; upgradePanel.visible = false; // -- Upgrade Panel Background -- var upgradePanelBackground = new Graphics(); upgradePanelBackground.beginFill(0x000000, 0.6); // Black, 60% opacity // Dimensions are chosen to comfortably fit the text options upgradePanelBackground.drawRect(-10, -30, 550, 280); upgradePanelBackground.endFill(); upgradePanel.addChild(upgradePanelBackground); var towerNameText = new Text2('Tower Name', { size: 50, fill: 0xFFFFFF, align: 'left' }); towerNameText.anchor.set(0, 0.5); // Anchor to the left towerNameText.x = 20; // Add padding upgradePanel.addChild(towerNameText); var upgradeAPathButton = new Text2('Upgrade Path A', { size: 45, fill: 0x27AE60, align: 'left' }); upgradeAPathButton.anchor.set(0, 0.5); // Anchor to the left upgradeAPathButton.x = 20; // Add padding upgradeAPathButton.y = 70; upgradePanel.addChild(upgradeAPathButton); var upgradeBPathButton = new Text2('Upgrade Path B', { size: 45, fill: 0x2980B9, align: 'left' }); upgradeBPathButton.anchor.set(0, 0.5); // Anchor to the left upgradeBPathButton.x = 20; // Add padding upgradeBPathButton.y = 140; upgradePanel.addChild(upgradeBPathButton); var sellButton = new Text2('Sell Tower', { size: 45, fill: 0xC0392B, align: 'left' }); sellButton.anchor.set(0, 0.5); // Anchor to the left sellButton.x = 20; // Add padding sellButton.y = 210; upgradePanel.addChild(sellButton); sellButton.down = function () { if (Game.GameStateManager.currentState() !== Game.GameplayState) { return; } if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId); } }; upgradeAPathButton.down = function () { if (Game.GameStateManager.currentState() !== Game.GameplayState) { return; } if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathA'); } }; upgradeBPathButton.down = function () { if (Game.GameStateManager.currentState() !== Game.GameplayState) { return; } if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathB'); } }; // --- Start the game by pushing the initial state --- Game.GameStateManager.pushState(Game.GameplayState); // --- Platform Pause/Resume Hooks --- // This code assumes the LK.Game object provides onPause and onResume // event handlers that are triggered by the platform's UI. /** * Called by the platform when its pause button is pressed. * We respond by pushing our PausedState onto the stack. */ game.onPause = function () { // Only push the pause state if we are currently in gameplay. // This prevents issues if the game is paused from a different state. if (Game.GameStateManager.currentState() === Game.GameplayState) { Game.GameStateManager.pushState(Game.PausedState); } }; /** * Called by the platform when its resume button is pressed. * We respond by popping our PausedState from the stack. */ game.onResume = function () { // Only pop the state if we are currently paused. if (Game.GameStateManager.currentState() === Game.PausedState) { Game.GameStateManager.popState(); } }; ;
===================================================================
--- original.js
+++ change.js
@@ -1271,8 +1271,20 @@
components: [{
name: 'FlyingComponent',
args: []
}]
+ },
+ 'Juggernaut': {
+ health: 5000,
+ speed: 25,
+ gold_value: 250,
+ lives_cost: 20,
+ armor_type: 'Heavy',
+ components: [{
+ name: 'BossComponent',
+ // We will populate the phases array in the next step.
+ args: [[]]
+ }]
}
};
Game.WaveData = [
// Wave 1: A few basic Grunts