User prompt
Please go to the Game.TowerData object. We will add a new can_target_air property to the AttackComponent of the ArrowTower and the RockLauncher. The Arrow Tower will be our primary anti-air, while the Rock Launcher will be ground-only. In the 'ArrowTower' definition, modify the AttackComponent's args to include can_target_air: true. Replace this line: args: [10, 300, 1.0, 0, 'arrow_projectile'] With this new line: args: [10, 300, 1.0, 0, 'arrow_projectile', undefined, 0, true] // damage, range, speed, last_attack, projectile, targetId, splash, can_target_air In the 'RockLauncher' definition, modify its AttackComponent's args to include can_target_air: false. Replace this line: args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50] With this new line: args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50, false] // splash, can_target_air Finally, update the AttackComponent constructor to accept the new property. Replace this: AttackComponent: function AttackComponent(damage, range, attack_speed, last_attack_time, projectile_id, target_id, splash_radius) { return { name: 'AttackComponent', damage: damage || 10, range: range || 100, attack_speed: attack_speed || 1.0, last_attack_time: last_attack_time || 0, projectile_id: projectile_id || '', target_id: target_id || -1, splash_radius: splash_radius || 0 // Default to 0 (no splash) }; }, With this: 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 }; },
User prompt
Please go to the // === SECTION: UPGRADE & SELL UI === section. Find the line where the upgradePanelBackground draws its rectangle. Change the width value from 500 to 550. Replace this line: upgradePanelBackground.drawRect(-10, -30, 500, 280); With this new line: upgradePanelBackground.drawRect(-10, -30, 550, 280);
User prompt
Please go to the Game.GameplayState.update function. We need to modify the logic inside the else if (Game.GameplayManager.selectedTowerId !== -1) block to conditionally hide the upgrade buttons. Replace the entire else if (Game.GameplayManager.selectedTowerId !== -1) block with this new, more robust version: } else if (Game.GameplayManager.selectedTowerId !== -1) { // --- STATE: TOWER SELECTED --- 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 = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'] ? Game.EntityManager.componentStores['RenderComponent'][towerId] : null; var towerComp = Game.EntityManager.componentStores['TowerComponent'] ? Game.EntityManager.componentStores['TowerComponent'][towerId] : null; var attackComp = Game.EntityManager.componentStores['AttackComponent'] ? Game.EntityManager.componentStores['AttackComponent'][towerId] : null; var transformComp = Game.EntityManager.componentStores['TransformComponent'] ? Game.EntityManager.componentStores['TransformComponent'][towerId] : null; if (!renderComp || !towerComp || !transformComp) { Game.GameplayManager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; 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 + ")"); // --- NEW DYNAMIC VISIBILITY LOGIC --- 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; // Hide if no more upgrades } 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; // Hide if no more upgrades } // --- END NEW DYNAMIC VISIBILITY LOGIC --- var sellPrice = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { sellPrice = towerComp.cost; } sellButton.setText("Sell Tower ($" + sellPrice + ")"); }
User prompt
Please go to the // === SECTION: UPGRADE & SELL UI === section. Find the line where the upgradePanelBackground draws its rectangle. Change the width value from 450 to 500. Replace this line: upgradePanelBackground.drawRect(-10, -30, 450, 280); With this new line: upgradePanelBackground.drawRect(-10, -30, 500, 280);
User prompt
First, let's adjust the positioning logic. Go to the Game.GameplayState.update function. In the else if (Game.GameplayManager.selectedTowerId !== -1) block, replace the two lines that set the panel's position. Replace these two lines: upgradePanel.x = transformComp.x + 150; upgradePanel.y = transformComp.y; With these new lines: upgradePanel.x = transformComp.x + 100; // Move it a bit closer horizontally upgradePanel.y = transformComp.y - 140; // Offset by half the panel's height to vertically center it Next, let's add some padding to the text inside the panel for better readability. Go to the // === SECTION: UPGRADE & SELL UI === section. For each of the four text elements (towerNameText, upgradeAPathButton, upgradeBPathButton, sellButton), change the x property from 0 to 20. For example, change this: var towerNameText = new Text2('Tower Name', { size: 50, fill: 0xFFFFFF, align: 'left' }); towerNameText.anchor.set(0, 0.5); // Anchor to the left towerNameText.x = 0; upgradePanel.addChild(towerNameText); To this: 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); (Please apply this change to all four text elements in the panel.)
User prompt
Please go to the // === SECTION: UPGRADE & SELL UI === section. We will add a new block of code right after the upgradePanel is defined to create and attach its background. Find this block: var upgradePanel = new Container(); LK.gui.right.addChild(upgradePanel); upgradePanel.x = -50; // 50px padding from the right edge upgradePanel.y = 0; upgradePanel.visible = false; (Note: Even though you previously moved the addChild call, the definition of upgradePanel is still here. We will insert the new code right after upgradePanel.visible = false;) Insert the following new code immediately after that block: // -- 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, 450, 280); upgradePanelBackground.endFill(); upgradePanel.addChild(upgradePanelBackground);
User prompt
Please go to the Game.GameplayState.update function. In the else if (Game.GameplayManager.selectedTowerId !== -1) block, we will add one line of code that re-adds the panel to the container, which will move it to the end of the draw order. Find this block of code: } else { upgradePanel.visible = true; // Add these two lines to position the panel upgradePanel.x = transformComp.x + 150; upgradePanel.y = transformComp.y; if (attackComp && transformComp) { And add the new line as shown below: } else { upgradePanel.visible = true; upgradePanel.x = transformComp.x + 150; upgradePanel.y = transformComp.y; // This line moves the panel to the top of the render stack gameWorld.addChild(upgradePanel); if (attackComp && transformComp) {
User prompt
Please locate the // === SECTION: UPGRADE & SELL UI === section. Find the line where the upgradePanel is defined (var upgradePanel = new Container();) and add a new line directly below it to set its zIndex. Add the highlighted line: var upgradePanel = new Container(); upgradePanel.zIndex = 100; // A high value to ensure it renders on top LK.gui.right.addChild(upgradePanel);
User prompt
First, replace the line that adds upgradePanel to the right-side GUI with a line that adds it to the gameWorld container. Find this line in the // === SECTION: UPGRADE & SELL UI === section. LK.gui.right.addChild(upgradePanel); With this: gameWorld.addChild(upgradePanel); Next, replace the entire block of code that defines the four text elements (towerNameText, upgradeAPathButton, upgradeBPathButton, sellButton) with this new version. This changes their alignment from right to left. Replace the entire block starting from var towerNameText = ... down to upgradePanel.addChild(sellButton); with this: var towerNameText = new Text2('Tower Name', { size: 50, fill: 0xFFFFFF, align: 'left' }); towerNameText.anchor.set(0, 0.5); // Anchor to the left towerNameText.x = 0; 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 = 0; 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 = 0; 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 = 0; sellButton.y = 210; upgradePanel.addChild(sellButton); Finally, update the GameplayState.update function to dynamically position this new panel. In the else if (Game.GameplayManager.selectedTowerId !== -1) block, add two lines to set the upgradePanel's position. Find this code block inside Game.GameplayState.update: } else { upgradePanel.visible = true; if (attackComp && transformComp) { And insert these two lines right after upgradePanel.visible = true;: } else { upgradePanel.visible = true; // Add these two lines to position the panel upgradePanel.x = transformComp.x + 150; upgradePanel.y = transformComp.y; if (attackComp && transformComp) {
Code edit (1 edits merged)
Please save this source code
User prompt
Please replace the entire Game.Systems.MovementSystem object with this new version. This new logic checks if an enemy has the FlyingComponent and changes its movement behavior accordingly. 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)); } } } };
User prompt
Replace the .down handlers for the three upgrade/sell buttons: 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.selectedTowe rId !== -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'); } };
User prompt
Replace the button.down function for the tower build icons (this is inside the for loop that creates the build panel): // This is the start of the drag-and-drop 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 }; Replace the minimapContainer.down function: 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. var mapWorldWidth = 40 * 64; var mapWorldHeight = 60 * 64; var minimapPixelWidth = 40 * 5; var minimapPixelHeight = 60 * 5; var targetWorldX = x / minimapPixelWidth * mapWorldWidth; var targetWorldY = y / minimapPixelHeight * mapWorldHeight; var screenWidth = 2048; var screenHeight = 2732; var newCameraX = -targetWorldX + screenWidth / 2; var newCameraY = -targetWorldY + screenHeight / 2; 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)); gameWorld.x = cameraX; gameWorld.y = cameraY; }; Replace the global game.down, game.move, and game.up functions for camera panning: // Add mouse event handlers for camera panning game.down = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) { isDragging = false; return; } isDragging = true; dragStartX = x; dragStartY = y; lastMouseX = x; lastMouseY = y; cameraStartX = cameraX; cameraStartY = cameraY; mouseDownTime = LK.ticks / 60 * 1000; }; game.move = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; lastMouseX = x; lastMouseY = y; if (isDragging) { var dx = x - dragStartX; var dy = y - dragStartY; cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; 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.GameStateManager.currentState() !== Game.GameplayState) { isDragging = false; return; } isDragging = false; };
User prompt
Replace the cellSprite.down function (this is located inside the drawGrid function): cellSprite.down = function () { if (Game.GameStateManager.currentState() !== Game.GameplayState) return; // Only process true clicks, not drags var timeSinceMouseDown = LK.ticks / 60 * 1000 - mouseDownTime; if (timeSinceMouseDown > 200) { return; } var dx = Math.abs(lastMouseX - dragStartX); var dy = Math.abs(lastMouseY - dragStartY); if (dx > 10 || dy > 10) { return; } var gridX = this.gridX; var gridY = this.gridY; var gridCell = Game.GridManager.grid[gridX][gridY]; if (Game.GameplayManager.activeBuildType) { // If in build mode, check if the 3x3 area is buildable if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; // The center of the build is the clicked cell Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; var worldX = gridX * 64 + 32; var worldY = gridY * 64 + 32; confirmPanel.x = worldX; confirmPanel.y = worldY; } else { // Clicked an invalid spot, so cancel build mode Game.GameplayManager.activeBuildType = null; } } else if (!Game.GameplayManager.isConfirmingPlacement) { // If not in build mode, check for selection if (gridCell.state === Game.GridManager.CELL_STATES.TOWER) { // The grid cell now directly tells us which tower is selected Game.GameplayManager.selectedTowerId = gridCell.entityId; } else { Game.GameplayManager.selectedTowerId = -1; } } };
User prompt
Replace the startWaveButton.down function: 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; };
Code edit (1 edits merged)
Please save this source code
User prompt
First, replace the two LK.init.shape definitions for the UI buttons at the top of the script with these updated versions, which increase their size from 60x60 to 80x80. LK.init.shape('ui_pause_button', {width:80, height:80, color:0xecf0f1, shape:'box'}) LK.init.shape('ui_play_button', {width:80, height:80, color:0xecf0f1, shape:'box'})
User prompt
Now, add this new block of code in its place within the // === SECTION: DIRECT GAME INITIALIZATION === section. This creates the new graphical buttons and positions them correctly. // Create Pause and Play/Resume Buttons var pauseButtonAsset = LK.gui.topLeft.attachAsset('ui_pause_button', {}); pauseButtonAsset.x = 100; // 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 = 100; // 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(); }; We must update the PausedState to control the visibility of these new buttons. Please replace the entire Game.PausedState object with this new version. // The state when the game is paused. This will overlay the GameplayState. Game.PausedState = { enter: function() { 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() { 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(dt) { // Paused state does not update game logic. }, render: function(ctx) { const underlyingState = Game.GameStateManager.states[Game.GameStateManager.states.length - 2]; if (underlyingState && underlyingState.render) { underlyingState.render(ctx); } } };
User prompt
delete the entire pauseButton block of code that we added previously. It starts with // Create Pause Button and ends with the pauseButton.down function.
User prompt
First, we need to define new graphical assets for our buttons. Please add the following lines to the /**** * Assets ****/ section at the top of the script. LK.init.shape('ui_pause_button', {width:60, height:60, color:0xecf0f1, shape:'box'}) LK.init.shape('ui_play_button', {width:60, height:60, color:0xecf0f1, shape:'box'})
User prompt
First, please delete the entire pauseButton object definition and its down function from the // === SECTION: DIRECT GAME INITIALIZATION === block. Next, please delete the line pauseButton.setText('PAUSE'); from the onGameOver function inside the Game.GameManager object. Finally, add the following code to the very end of the script. This will hook our game state logic directly into the platform's pause system. // --- 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(); } };
User prompt
First, please remove the entire window.addEventListener('keydown', ...) block that we just added at the end of the script. Next, add the following code to create a UI button for pausing. Place this code within the // === SECTION: DIRECT GAME INITIALIZATION === block, near where the other HUD elements like goldText and livesText are created. // Create Pause Button var pauseButton = new Text2('PAUSE', { size: 40, fill: 0xAAAAAA }); pauseButton.anchor.set(0, 0); // Align to the top-left LK.gui.topLeft.addChild(pauseButton); pauseButton.x = 20; // 20px padding from the left edge pauseButton.y = 20; // 20px padding from the top edge pauseButton.down = function() { const currentState = Game.GameStateManager.currentState(); if (currentState === Game.GameplayState) { // If we are in gameplay, push the PausedState Game.GameStateManager.pushState(Game.PausedState); this.setText('RESUME'); // Update button text } else if (currentState === Game.PausedState) { // If we are already paused, pop back to gameplay Game.GameStateManager.popState(); this.setText('PAUSE'); // Change it back } }; Finally, we need to ensure the "RESUME" text reverts to "PAUSE" if the game ends while paused. Add the following line inside the onGameOver method within the Game.GameManager object. // Modify the onGameOver function in Game.GameManager onGameOver: function onGameOver(event) { this.isGameOver = true; // Add this line to fix the pause button text on game over pauseButton.setText('PAUSE'); LK.showGameOver(); },
User prompt
Please fix the bug: 'window.addEventListener is not a function' in or related to this line: 'window.addEventListener('keydown', function (event) {' Line Number: 2459
User prompt
Ava, let's make the confirmation buttons much larger and easier to tap. We only need to modify the game.update function. Replace the entire function with this new version, which increases the size of the confirm/cancel buttons and adjusts their spacing. game.update = function () { if (Game.GameManager.isGameOver) { return; } // Run core game systems Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); // --- UI STATE MACHINE FOR DRAG-AND-DROP --- var currentBuildType = Game.GameplayManager.activeBuildType; // Check if the player is currently dragging a tower to build if (currentBuildType && !Game.GameplayManager.isConfirmingPlacement) { // --- STATE: DRAGGING TOWER --- var towerData = Game.TowerData[currentBuildType]; // Ensure UI for other states is hidden confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update the ghost tower sprite that follows the cursor/finger if (!ghostTowerSprite || ghostTowerSprite.towerType !== currentBuildType) { if (ghostTowerSprite) ghostTowerSprite.destroy(); ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = currentBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; // Update and show the fixed info panel floatingInfoPanel.visible = true; infoText.setText(currentBuildType + "\nCost: " + towerData.cost); } else if (Game.GameplayManager.isConfirmingPlacement) { // --- STATE: AWAITING CONFIRMATION --- // Hide other UI elements if (ghostTowerSprite) ghostTowerSprite.visible = false; floatingInfoPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; // Create/update confirmation buttons if needed if (!confirmButton || confirmButton.towerType !== currentBuildType) { if (confirmButton) confirmButton.destroy(); if (cancelButton) cancelButton.destroy(); var towerData = Game.TowerData[currentBuildType]; // Create CONFIRM button confirmButton = game.attachAsset(towerData.sprite_id, { tint: 0x2ECC71 }); confirmButton.width = 120; // Increased size confirmButton.height = 120; // Increased size confirmButton.x = -65; // Adjusted spacing confirmButton.anchor.set(0.5, 0.5); confirmButton.towerType = currentBuildType; confirmButton.down = function () { var coords = Game.GameplayManager.confirmPlacementCoords; var buildType = Game.GameplayManager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(confirmButton); // Create CANCEL button cancelButton = game.attachAsset(towerData.sprite_id, { tint: 0xE74C3C }); cancelButton.width = 120; // Increased size cancelButton.height = 120; // Increased size cancelButton.x = 65; // Adjusted spacing cancelButton.anchor.set(0.5, 0.5); cancelButton.towerType = currentBuildType; cancelButton.down = function () { // Reset state Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { // --- STATE: TOWER SELECTED --- // Hide all build-related UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } confirmPanel.visible = false; floatingInfoPanel.visible = false; // Show upgrade panel var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'] ? Game.EntityManager.componentStores['RenderComponent'][towerId] : null; var towerComp = Game.EntityManager.componentStores['TowerComponent'] ? Game.EntityManager.componentStores['TowerComponent'][towerId] : null; var attackComp = Game.EntityManager.componentStores['AttackComponent'] ? Game.EntityManager.componentStores['AttackComponent'][towerId] : null; var transformComp = Game.EntityManager.componentStores['TransformComponent'] ? Game.EntityManager.componentStores['TransformComponent'][towerId] : null; if (!renderComp || !towerComp || !transformComp) { Game.GameplayManager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; if (attackComp && transformComp) { rangeCircle.visible = true; rangeCircle.x = transformComp.x; rangeCircle.y = transformComp.y; rangeCircle.width = attackComp.range * 2; rangeCircle.height = attackComp.range * 2; } else { rangeCircle.visible = false; } var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === renderComp.sprite_id) { towerType = type; break; } } towerNameText.setText(towerType + " (Lvl " + towerComp.upgrade_level + ")"); var upgradePathAData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathA'] : null; if (upgradePathAData && towerComp.upgrade_path_A_level < upgradePathAData.length) { var upgradeInfoA = upgradePathAData[towerComp.upgrade_path_A_level]; upgradeAPathButton.setText("Upgrade Path A ($" + upgradeInfoA.cost + ")"); upgradeAPathButton.visible = true; } else { upgradeAPathButton.visible = false; } var upgradePathBData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType]['PathB'] : null; if (upgradePathBData && towerComp.upgrade_path_B_level < upgradePathBData.length) { var upgradeInfoB = upgradePathBData[towerComp.upgrade_path_B_level]; upgradeBPathButton.setText("Upgrade Path B ($" + upgradeInfoB.cost + ")"); upgradeBPathButton.visible = true; } else { upgradeBPathButton.visible = false; } var sellPrice = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { sellPrice = towerComp.cost; } sellButton.setText("Sell Tower ($" + sellPrice + ")"); } } else { // --- STATE: IDLE --- // Clean up all dynamic UI if (ghostTowerSprite) { ghostTowerSprite.destroy(); ghostTowerSprite = null; } if (confirmButton) { confirmButton.destroy(); confirmButton = null; } if (cancelButton) { cancelButton.destroy(); cancelButton = null; } floatingInfoPanel.visible = false; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; } // The `game.up` event (releasing the mouse/finger) will handle the drop logic. game.up = function () { if (Game.GameplayManager.activeBuildType && !Game.GameplayManager.isConfirmingPlacement) { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); // If dropped on a valid spot, switch to confirmation state if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; confirmPanel.x = gridX * 64 + 32; confirmPanel.y = gridY * 64 + 32; } else { // Dropped on an invalid spot, cancel the build Game.GameplayManager.activeBuildType = null; } } isDragging = false; }; // Update standard HUD elements every frame goldText.setText('Gold: ' + Game.ResourceManager.gold); livesText.setText('Lives: ' + Game.ResourceManager.lives); waveText.setText('Wave: ' + (Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1) + ' / ' + Game.WaveData.length); // Update Power Meter, Minimap, etc. var p_prod = Game.ResourceManager.powerProduction; var p_cons = Game.ResourceManager.powerConsumption; var p_avail = Game.ResourceManager.powerAvailable; var isDeficit = p_cons > p_prod && p_avail <= 0; var barScale = 3; var productionWidth = p_prod * barScale; powerProductionBar.clear(); powerProductionBar.beginFill(0x5DADE2, 0.6); powerProductionBar.drawRect(0, 0, productionWidth, 20); powerProductionBar.endFill(); var consumptionWidth = Math.min(p_cons, p_prod) * barScale; var consumptionColor = 0x00FF7F; if (isDeficit) { consumptionColor = Math.floor(LK.ticks / 10) % 2 === 0 ? 0xFF4136 : 0xD9362D; } else if (p_prod > 0 && p_cons / p_prod >= 0.75) { consumptionColor = 0xFFD700; } powerConsumptionBar.clear(); powerConsumptionBar.beginFill(consumptionColor); powerConsumptionBar.drawRect(0, 0, consumptionWidth, 20); powerConsumptionBar.endFill(); var availableWidth = p_avail * barScale; powerAvailableBar.x = productionWidth; powerAvailableBar.clear(); powerAvailableBar.beginFill(0x5DADE2, 0.4); powerAvailableBar.drawRect(0, 0, availableWidth, 20); powerAvailableBar.endFill(); updateMinimapDynamic(); updateMinimapViewport(); powerUpdateTimer++; if (powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); };
User prompt
Ava, the info panel is in the right place horizontally, but it's too low and covering the build icons. Let's move it up. Find the "-- Floating Info Panel (now a fixed panel above the build bar) --" section and locate this line: floatingInfoPanel.y = -250; // Position it 250px up from the bottom edge Change the y value from -250 to -320 to move the panel higher and create a clear space: floatingInfoPanel.y = -320; // Position it 320px up from the bottom edge To ensure this is applied correctly, please replace the entire info panel block with the corrected version. Replace the entire "-- Floating Info Panel..." block with this: // -- Floating Info Panel (now a fixed panel above the build bar) -- var floatingInfoPanel = new Container(); LK.gui.bottom.addChild(floatingInfoPanel); // Attach to the bottom GUI anchor floatingInfoPanel.anchor.set(0.5, 1); // Anchor to its own bottom-center point floatingInfoPanel.x = 0; // Center horizontally floatingInfoPanel.y = -320; // Position it 320px up from the bottom edge floatingInfoPanel.visible = false; var infoBackground = new Graphics(); infoBackground.beginFill(0x000000, 0.8); infoBackground.drawRect(0, 0, 300, 80); infoBackground.endFill(); floatingInfoPanel.addChild(infoBackground); var infoText = new Text2('', { size: 30, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 290 }); infoText.anchor.set(0, 0); infoText.x = 5; infoText.y = 5; floatingInfoPanel.addChild(infoText);
/**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // === SECTION: GLOBAL NAMESPACE === // === SECTION: ASSETS === var Game = {}; // Camera state var cameraX = 0; var cameraY = 0; var isDragging = false; var dragStartX = 0; var dragStartY = 0; var cameraStartX = 0; var cameraStartY = 0; var lastMouseX = 0; var lastMouseY = 0; var mouseDownTime = 0; // === SECTION: GLOBAL MANAGERS === Game.ResourceManager = { gold: 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 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) { return { name: 'AttackComponent', damage: damage || 10, range: range || 100, attack_speed: attack_speed || 1.0, last_attack_time: last_attack_time || 0, projectile_id: projectile_id || '', target_id: target_id || -1, splash_radius: splash_radius || 0 // Default to 0 (no splash) }; }, MovementComponent: function MovementComponent(speed) { return { name: 'MovementComponent', speed: speed || 50 }; }, PowerProductionComponent: function PowerProductionComponent(rate) { return { name: 'PowerProductionComponent', production_rate: rate || 0 }; }, PowerConsumptionComponent: function PowerConsumptionComponent(rate) { return { name: 'PowerConsumptionComponent', drain_rate: rate || 0 }; }, TowerComponent: function TowerComponent(cost, sellValue, builtOnWave) { return { name: 'TowerComponent', cost: cost || 50, sell_value: sellValue || 25, upgrade_level: 1, upgrade_path_A_level: 0, upgrade_path_B_level: 0, built_on_wave: builtOnWave || 0 }; }, ProjectileComponent: function ProjectileComponent(target_id, speed, damage) { return { name: 'ProjectileComponent', target_id: target_id || -1, speed: speed || 8, // Let's keep the speed lower damage: damage || 10 }; }, EnemyComponent: function EnemyComponent(goldValue, livesCost, armor_type) { return { name: 'EnemyComponent', gold_value: goldValue || 5, lives_cost: livesCost || 1, armor_type: armor_type || 'Normal' }; }, PowerStorageComponent: function PowerStorageComponent(capacity) { return { name: 'PowerStorageComponent', capacity: capacity || 0 }; }, PoisonOnImpactComponent: function PoisonOnImpactComponent(damage_per_second, duration) { return { name: 'PoisonOnImpactComponent', damage_per_second: damage_per_second || 0, duration: duration || 0 }; }, PoisonDebuffComponent: function PoisonDebuffComponent(damage_per_second, duration_remaining, last_tick_time) { return { name: 'PoisonDebuffComponent', damage_per_second: damage_per_second || 0, duration_remaining: duration_remaining || 0, last_tick_time: last_tick_time || 0 }; }, SlowOnImpactComponent: function SlowOnImpactComponent(slow_percentage, duration) { return { name: 'SlowOnImpactComponent', slow_percentage: slow_percentage || 0, duration: duration || 0 }; }, SlowDebuffComponent: function SlowDebuffComponent(slow_percentage, duration_remaining) { return { name: 'SlowDebuffComponent', slow_percentage: slow_percentage || 0, duration_remaining: duration_remaining || 0 }; }, AuraComponent: function AuraComponent(range, effect_type) { return { name: 'AuraComponent', range: range || 100, effect_type: effect_type || '' }; }, DamageBuffComponent: function DamageBuffComponent(multiplier, source_aura_id) { return { name: 'DamageBuffComponent', multiplier: multiplier || 0, source_aura_id: source_aura_id || -1 }; }, GoldGenerationComponent: function GoldGenerationComponent(gold_per_second) { return { name: 'GoldGenerationComponent', gold_per_second: gold_per_second || 0 }; }, FlyingComponent: function FlyingComponent() { return { name: 'FlyingComponent' }; }, // --- NEW COMPONENTS --- FireZoneOnImpactComponent: function FireZoneOnImpactComponent(damage_per_tick, duration, tick_rate) { return { name: 'FireZoneOnImpactComponent', damage_per_tick: damage_per_tick || 0, duration: duration || 0, tick_rate: tick_rate || 1.0 }; }, FireZoneComponent: function FireZoneComponent(damage_per_tick, duration_remaining, tick_rate, last_tick_time) { return { name: 'FireZoneComponent', damage_per_tick: damage_per_tick || 0, duration_remaining: duration_remaining || 0, tick_rate: tick_rate || 1.0, last_tick_time: last_tick_time || 0 }; } }; Game.Systems = { MovementSystem: { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); var frame_time = 1 / 60; // Assuming 60 FPS // 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 there is a power deficit, do not assign any new targets. if (Game.Systems.CombatSystem.isPowerDeficit) { // Also, clear any existing targets to prevent towers from holding a lock. var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent']); for (var i = 0; i < towers.length; i++) { var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towers[i]]; towerAttack.target_id = -1; } return; // Stop the system's update. } var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); for (var i = 0; i < towers.length; i++) { var towerId = towers[i]; var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towerId]; var towerTransform = Game.EntityManager.componentStores['TransformComponent'][towerId]; var bestTarget = -1; var bestPathIndex = -1; var enemies = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); for (var j = 0; j < enemies.length; j++) { var enemyId = enemies[j]; var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; var dx = enemyTransform.x - towerTransform.x; var dy = enemyTransform.y - towerTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= towerAttack.range) { // Check if enemy has PathFollowerComponent (ground units) or use default targeting for flyers var pathFollower = Game.EntityManager.componentStores['PathFollowerComponent'] ? Game.EntityManager.componentStores['PathFollowerComponent'][enemyId] : null; var currentPathIndex = pathFollower ? pathFollower.path_index : 0; if (currentPathIndex > bestPathIndex) { bestTarget = enemyId; bestPathIndex = currentPathIndex; } } } towerAttack.target_id = bestTarget; } } }, CleanupSystem: { update: function update() { for (var i = 0; i < Game.EntityManager.entitiesToDestroy.length; i++) { var entityId = Game.EntityManager.entitiesToDestroy[i]; var index = Game.EntityManager.activeEntities.indexOf(entityId); if (index > -1) { Game.EntityManager.activeEntities.splice(index, 1); } for (var componentName in Game.EntityManager.componentStores) { if (Game.EntityManager.componentStores[componentName][entityId]) { delete Game.EntityManager.componentStores[componentName][entityId]; } } if (Game.Systems.RenderSystem.displayObjects[entityId]) { Game.Systems.RenderSystem.displayObjects[entityId].destroy(); delete Game.Systems.RenderSystem.displayObjects[entityId]; } } if (Game.EntityManager.entitiesToDestroy.length > 0) { Game.EntityManager.entitiesToDestroy = []; } } }, WaveSpawnerSystem: { currentWaveIndex: -1, isSpawning: false, spawnTimer: 0, subWaveIndex: 0, spawnedInSubWave: 0, startNextWave: function startNextWave() { this.currentWaveIndex++; if (this.currentWaveIndex >= Game.WaveData.length) { return; // No more waves } this.isSpawning = true; this.spawnTimer = 0; this.subWaveIndex = 0; this.spawnedInSubWave = 0; // Calculate the total number of enemies for this wave var totalEnemies = 0; var currentWave = Game.WaveData[this.currentWaveIndex]; for (var i = 0; i < currentWave.sub_waves.length; i++) { totalEnemies += currentWave.sub_waves[i].count; } Game.GameplayManager.totalEnemiesThisWave = totalEnemies; }, update: function update() { if (!this.isSpawning || this.currentWaveIndex >= Game.WaveData.length) { return; } var currentWave = Game.WaveData[this.currentWaveIndex]; if (!currentWave || this.subWaveIndex >= currentWave.sub_waves.length) { this.isSpawning = false; return; } var currentSubWave = currentWave.sub_waves[this.subWaveIndex]; if (this.spawnedInSubWave === 0 && this.spawnTimer < currentSubWave.start_delay) { this.spawnTimer += 1 / 60; return; } if (this.spawnedInSubWave > 0 && this.spawnTimer < currentSubWave.spawn_delay) { this.spawnTimer += 1 / 60; return; } if (this.spawnedInSubWave < currentSubWave.count) { var enemyData = Game.EnemyData[currentSubWave.enemy_type]; if (!enemyData) { this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; return; } var enemyId = Game.EntityManager.createEntity(); var isFlyer = enemyData.components && enemyData.components.some(function (c) { return c.name === 'FlyingComponent'; }); var renderLayer = isFlyer ? 3 : 1; var transform = Game.Components.TransformComponent(0 * 64, 30 * 64); var render = Game.Components.RenderComponent('enemy', renderLayer, 1.0, true); var movement = Game.Components.MovementComponent(enemyData.speed); var health = Game.Components.HealthComponent(enemyData.health, enemyData.health); var enemy = Game.Components.EnemyComponent(enemyData.gold_value, enemyData.lives_cost, enemyData.armor_type); Game.EntityManager.addComponent(enemyId, transform); Game.EntityManager.addComponent(enemyId, render); Game.EntityManager.addComponent(enemyId, movement); Game.EntityManager.addComponent(enemyId, health); Game.EntityManager.addComponent(enemyId, enemy); if (enemyData.components && enemyData.components.length > 0) { for (var i = 0; i < enemyData.components.length; i++) { var compData = enemyData.components[i]; var newComponent = Game.Components[compData.name].apply(null, compData.args || []); Game.EntityManager.addComponent(enemyId, newComponent); } } this.spawnedInSubWave++; this.spawnTimer = 0; Game.GameplayManager.enemiesSpawnedThisWave++; } else { this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; } } }, CombatSystem: { isPowerDeficit: false, init: function init() { Game.EventBus.subscribe('PowerStatusChanged', this.onPowerStatusChanged.bind(this)); }, onPowerStatusChanged: function onPowerStatusChanged(event) { this.isPowerDeficit = event.new_status === 'deficit'; }, update: function update() { // If there is a power deficit, do not allow towers to fire. if (this.isPowerDeficit) { return; // Stop the system's update. } var currentTime = LK.ticks / 60; var towers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); for (var i = 0; i < towers.length; i++) { var towerId = towers[i]; var towerAttack = Game.EntityManager.componentStores['AttackComponent'][towerId]; var towerTransform = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (towerAttack.target_id !== -1 && currentTime - towerAttack.last_attack_time >= 1.0 / towerAttack.attack_speed) { var targetTransform = Game.EntityManager.componentStores['TransformComponent'][towerAttack.target_id]; if (targetTransform) { var baseDamage = towerAttack.damage; var finalDamage = baseDamage; var buffComp = Game.EntityManager.componentStores['DamageBuffComponent'] ? Game.EntityManager.componentStores['DamageBuffComponent'][towerId] : null; if (buffComp) { finalDamage = baseDamage * (1 + buffComp.multiplier); } var projectileId = Game.EntityManager.createEntity(); var projectileTransform = Game.Components.TransformComponent(towerTransform.x, towerTransform.y); var projectileRender = Game.Components.RenderComponent(towerAttack.projectile_id, 1, 1.0, true); var projectileComponent = Game.Components.ProjectileComponent(towerAttack.target_id, undefined, finalDamage); projectileComponent.source_tower_id = towerId; Game.EntityManager.addComponent(projectileId, projectileTransform); Game.EntityManager.addComponent(projectileId, projectileRender); Game.EntityManager.addComponent(projectileId, projectileComponent); if (towerAttack.on_impact_effects && towerAttack.on_impact_effects.length > 0) { for (var j = 0; j < towerAttack.on_impact_effects.length; j++) { var effectData = towerAttack.on_impact_effects[j]; var newComponent = Game.Components[effectData.name].apply(null, effectData.args); Game.EntityManager.addComponent(projectileId, newComponent); } } towerAttack.last_attack_time = currentTime; } } } var projectiles = Game.EntityManager.getEntitiesWithComponents(['ProjectileComponent', 'TransformComponent']); for (var i = projectiles.length - 1; i >= 0; i--) { var projectileId = projectiles[i]; var projectileComp = Game.EntityManager.componentStores['ProjectileComponent'][projectileId]; var projectileTransform = Game.EntityManager.componentStores['TransformComponent'][projectileId]; var targetExists = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id]; if (!targetExists) { Game.EntityManager.destroyEntity(projectileId); continue; } var targetTransform = Game.EntityManager.componentStores['TransformComponent'][projectileComp.target_id]; var dx = targetTransform.x - projectileTransform.x; var dy = targetTransform.y - projectileTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 10) { var impactPoint = { x: targetTransform.x, y: targetTransform.y }; var sourceTowerAttack = Game.EntityManager.componentStores['AttackComponent'][projectileComp.source_tower_id]; if (sourceTowerAttack && sourceTowerAttack.splash_radius > 0) { var allEnemies = Game.EntityManager.getEntitiesWithComponents(['HealthComponent', 'TransformComponent', 'EnemyComponent']); for (var j = 0; j < allEnemies.length; j++) { var enemyId = allEnemies[j]; var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; var splash_dx = enemyTransform.x - impactPoint.x; var splash_dy = enemyTransform.y - impactPoint.y; var splash_dist = Math.sqrt(splash_dx * splash_dx + splash_dy * splash_dy); if (splash_dist <= sourceTowerAttack.splash_radius) { this.dealDamage(enemyId, projectileComp.damage); } } } else { this.dealDamage(projectileComp.target_id, projectileComp.damage); } var targetHealth = Game.EntityManager.componentStores['HealthComponent'][projectileComp.target_id]; if (targetHealth) { var poisonImpactComp = Game.EntityManager.componentStores['PoisonOnImpactComponent'] ? Game.EntityManager.componentStores['PoisonOnImpactComponent'][projectileId] : null; if (poisonImpactComp) { var debuff = Game.Components.PoisonDebuffComponent(poisonImpactComp.damage_per_second, poisonImpactComp.duration, currentTime); Game.EntityManager.addComponent(projectileComp.target_id, debuff); } var slowImpactComp = Game.EntityManager.componentStores['SlowOnImpactComponent'] ? Game.EntityManager.componentStores['SlowOnImpactComponent'][projectileId] : null; if (slowImpactComp) { var slowDebuff = Game.Components.SlowDebuffComponent(slowImpactComp.slow_percentage, slowImpactComp.duration); Game.EntityManager.addComponent(projectileComp.target_id, slowDebuff); } } var fireZoneImpactComp = Game.EntityManager.componentStores['FireZoneOnImpactComponent'] ? Game.EntityManager.componentStores['FireZoneOnImpactComponent'][projectileId] : null; if (fireZoneImpactComp) { var zoneId = Game.EntityManager.createEntity(); Game.EntityManager.addComponent(zoneId, Game.Components.TransformComponent(impactPoint.x, impactPoint.y)); Game.EntityManager.addComponent(zoneId, Game.Components.RenderComponent('fire_zone', 0, 1.0, true)); Game.EntityManager.addComponent(zoneId, Game.Components.FireZoneComponent(fireZoneImpactComp.damage_per_tick, fireZoneImpactComp.duration, fireZoneImpactComp.tick_rate, currentTime)); } Game.EntityManager.destroyEntity(projectileId); } else { var moveX = dx / distance * projectileComp.speed; var moveY = dy / distance * projectileComp.speed; projectileTransform.x += moveX; projectileTransform.y += moveY; } } var poisonedEnemies = Game.EntityManager.getEntitiesWithComponents(['PoisonDebuffComponent']); for (var i = poisonedEnemies.length - 1; i >= 0; i--) { var enemyId = poisonedEnemies[i]; var debuff = Game.EntityManager.componentStores['PoisonDebuffComponent'][enemyId]; if (!debuff) { continue; } if (currentTime - debuff.last_tick_time >= 1.0) { this.dealDamage(enemyId, debuff.damage_per_second); debuff.last_tick_time = currentTime; debuff.duration_remaining -= 1.0; } if (debuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['PoisonDebuffComponent'][enemyId]; } } var fireZones = Game.EntityManager.getEntitiesWithComponents(['FireZoneComponent', 'TransformComponent']); for (var i = fireZones.length - 1; i >= 0; i--) { var zoneId = fireZones[i]; var zoneComp = Game.EntityManager.componentStores['FireZoneComponent'][zoneId]; var zoneTransform = Game.EntityManager.componentStores['TransformComponent'][zoneId]; zoneComp.duration_remaining -= 1.0 / 60; if (zoneComp.duration_remaining <= 0) { Game.EntityManager.destroyEntity(zoneId); continue; } if (currentTime - zoneComp.last_tick_time >= zoneComp.tick_rate) { var allEnemies = Game.EntityManager.getEntitiesWithComponents(['HealthComponent', 'TransformComponent']); var zoneRadius = 40; for (var j = 0; j < allEnemies.length; j++) { var enemyId = allEnemies[j]; var enemyTransform = Game.EntityManager.componentStores['TransformComponent'][enemyId]; var dx = enemyTransform.x - zoneTransform.x; var dy = enemyTransform.y - zoneTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= zoneRadius) { this.dealDamage(enemyId, zoneComp.damage_per_tick); } } zoneComp.last_tick_time = currentTime; } } }, dealDamage: function dealDamage(enemyId, damage) { var targetHealth = Game.EntityManager.componentStores['HealthComponent'][enemyId]; if (!targetHealth) { return; } var finalDamage = damage; var enemyComp = Game.EntityManager.componentStores['EnemyComponent'][enemyId]; if (enemyComp && enemyComp.armor_type === 'Heavy') { finalDamage *= 0.5; } targetHealth.current_hp -= finalDamage; if (targetHealth.current_hp <= 0) { var goldValue = enemyComp ? enemyComp.gold_value : 0; Game.EventBus.publish(Game.Events.EnemyKilledEvent(enemyId, goldValue)); Game.EntityManager.destroyEntity(enemyId); } } }, AuraSystem: { update: function update() { var buffedTowers = Game.EntityManager.getEntitiesWithComponents(['DamageBuffComponent']); for (var i = 0; i < buffedTowers.length; i++) { var towerId = buffedTowers[i]; delete Game.EntityManager.componentStores['DamageBuffComponent'][towerId]; } var auraTowers = Game.EntityManager.getEntitiesWithComponents(['AuraComponent', 'TransformComponent']); if (auraTowers.length === 0) { return; } var attackTowers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); for (var i = 0; i < auraTowers.length; i++) { var auraTowerId = auraTowers[i]; var auraComponent = Game.EntityManager.componentStores['AuraComponent'][auraTowerId]; var auraTransform = Game.EntityManager.componentStores['TransformComponent'][auraTowerId]; if (auraComponent.effect_type !== 'damage_buff') { continue; } for (var j = 0; j < attackTowers.length; j++) { var attackTowerId = attackTowers[j]; if (attackTowerId === auraTowerId) { continue; } var attackTransform = Game.EntityManager.componentStores['TransformComponent'][attackTowerId]; var dx = auraTransform.x - attackTransform.x; var dy = auraTransform.y - attackTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= auraComponent.range) { var buff = Game.Components.DamageBuffComponent(0.2, auraTowerId); Game.EntityManager.addComponent(attackTowerId, buff); } } } } }, EconomySystem: { update: function update() { var goldGenerators = Game.EntityManager.getEntitiesWithComponents(['GoldGenerationComponent']); for (var i = 0; i < goldGenerators.length; i++) { var entityId = goldGenerators[i]; var goldGenComp = Game.EntityManager.componentStores['GoldGenerationComponent'][entityId]; if (goldGenComp) { Game.ResourceManager.addGold(goldGenComp.gold_per_second); } } } }, TowerSystem: { tryUpgradeTower: function tryUpgradeTower(towerId, path) { var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId]; if (!renderComp) { return; } var towerSpriteId = renderComp.sprite_id; var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === towerSpriteId) { towerType = type; break; } } if (!towerType) { return; } var upgradePathData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType][path] : null; if (!upgradePathData) { return; } var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; if (!towerComp) { return; } var nextUpgradeIndex = path === 'PathA' ? towerComp.upgrade_path_A_level : towerComp.upgrade_path_B_level; if (nextUpgradeIndex >= upgradePathData.length) { return; } var upgradeInfo = upgradePathData[nextUpgradeIndex]; if (!Game.ResourceManager.spendGold(upgradeInfo.cost)) { return; } for (var componentName in upgradeInfo.effects) { var componentToModify = Game.EntityManager.componentStores[componentName] ? Game.EntityManager.componentStores[componentName][towerId] : null; if (componentToModify) { var effectsToApply = upgradeInfo.effects[componentName]; for (var property in effectsToApply) { if (componentToModify.hasOwnProperty(property)) { componentToModify[property] += effectsToApply[property]; } } } } if (upgradeInfo.add_component_on_impact) { var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; if (attackComp) { if (!attackComp.on_impact_effects) { attackComp.on_impact_effects = []; } attackComp.on_impact_effects.push(upgradeInfo.add_component_on_impact); } } towerComp.upgrade_level++; if (path === 'PathA') { towerComp.upgrade_path_A_level++; } else { towerComp.upgrade_path_B_level++; } }, trySellTower: function trySellTower(towerId) { var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!towerComp || !transformComp) { return; } var refundAmount = towerComp.sell_value; var currentWave = Game.Systems.WaveSpawnerSystem.currentWaveIndex; if (towerComp.built_on_wave === currentWave) { refundAmount = towerComp.cost; } Game.ResourceManager.addGold(refundAmount); // Clear the tower's 3x3 footprint from the grid var gridX = Math.floor(transformComp.x / 64); var gridY = Math.floor(transformComp.y / 64); for (var dx = -1; dx <= 1; dx++) { for (var dy = -1; dy <= 1; dy++) { var x = gridX + dx; var y = gridY + dy; if (Game.GridManager.grid[x] && Game.GridManager.grid[x][y] !== undefined) { Game.GridManager.grid[x][y].state = Game.GridManager.CELL_STATES.EMPTY; Game.GridManager.grid[x][y].entityId = null; } } } Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount)); Game.GameplayManager.selectedTowerId = -1; Game.EntityManager.destroyEntity(towerId); } } }; // === SECTION: GAME DATA === Game.EnemyData = { 'Grunt': { health: 100, speed: 50, gold_value: 5, lives_cost: 1, armor_type: 'Normal', components: [] }, 'Swarmer': { health: 30, speed: 80, gold_value: 2, lives_cost: 1, armor_type: 'Light', components: [] }, 'Armored': { health: 400, speed: 30, gold_value: 15, lives_cost: 2, armor_type: 'Heavy', components: [] }, 'Flyer': { health: 80, speed: 60, gold_value: 8, lives_cost: 1, armor_type: 'Light', components: [{ name: 'FlyingComponent', args: [] }] } }; Game.WaveData = [ // Wave 1: A few basic Grunts { wave_number: 1, sub_waves: [{ enemy_type: 'Grunt', count: 10, spawn_delay: 1.5, start_delay: 0 }] }, // Wave 2: A larger group of Grunts { wave_number: 2, sub_waves: [{ enemy_type: 'Grunt', count: 15, spawn_delay: 1.2, start_delay: 0 }] }, // Wave 3: A wave of fast Swarmers { wave_number: 3, sub_waves: [{ enemy_type: 'Swarmer', count: 20, spawn_delay: 0.5, start_delay: 0 }] }, // Wave 4: Grunts supported by slow, tough Armored units { wave_number: 4, sub_waves: [{ enemy_type: 'Grunt', count: 10, spawn_delay: 1.5, start_delay: 0 }, { enemy_type: 'Armored', count: 2, spawn_delay: 3.0, start_delay: 5.0 }] }, // Wave 5: A squadron of Flyers that ignore the path { wave_number: 5, sub_waves: [{ enemy_type: 'Flyer', count: 8, spawn_delay: 1.0, start_delay: 0 }] }]; // === SECTION: TOWER DATA === Game.TowerData = { 'ArrowTower': { cost: 50, sprite_id: 'arrow_tower', color: 0x2ecc71, components: [{ name: 'TowerComponent', args: [50, 25, undefined] }, { name: 'AttackComponent', args: [10, 300, 1.0, 0, 'arrow_projectile'] }, { name: 'PowerConsumptionComponent', args: [2] }] }, 'GeneratorTower': { cost: 75, sprite_id: 'generator_tower', color: 0x3498db, components: [{ name: 'TowerComponent', args: [75, 37, undefined] }, { name: 'PowerProductionComponent', args: [5] }] }, 'CapacitorTower': { cost: 40, sprite_id: 'capacitor_tower', color: 0xf1c40f, components: [{ name: 'TowerComponent', args: [40, 20, undefined] }, { name: 'PowerStorageComponent', args: [20] }] }, 'RockLauncher': { cost: 125, sprite_id: 'rock_launcher_tower', color: 0x8d6e63, components: [{ name: 'TowerComponent', args: [125, 62, undefined] }, { name: 'AttackComponent', args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50] }, { name: 'PowerConsumptionComponent', args: [10] }] }, 'ChemicalTower': { cost: 100, sprite_id: 'chemical_tower', color: 0xb87333, components: [{ name: 'TowerComponent', args: [100, 50, undefined] }, { name: 'AttackComponent', args: [5, 150, 0.8, 0, 'chemical_projectile', -1, 0] }, { name: 'PowerConsumptionComponent', args: [8] }] }, 'SlowTower': { cost: 80, sprite_id: 'slow_tower', color: 0x5dade2, components: [{ name: 'TowerComponent', args: [80, 40, undefined] }, { name: 'AttackComponent', args: [2, 360, 1.0, 0, 'slow_projectile'] }, { name: 'PowerConsumptionComponent', args: [4] }] }, 'DarkTower': { cost: 150, sprite_id: 'dark_tower', color: 0x9b59b6, components: [{ name: 'TowerComponent', args: [150, 75, undefined] }, { name: 'AuraComponent', args: [175, 'damage_buff'] }, { name: 'PowerConsumptionComponent', args: [12] }] }, 'GraveyardTower': { cost: 200, sprite_id: 'graveyard_tower', color: 0x607d8b, components: [{ name: 'TowerComponent', args: [200, 100, undefined] }, { name: 'GoldGenerationComponent', args: [1.5] }, { name: 'PowerConsumptionComponent', args: [15] }] } }; // === SECTION: UPGRADE DATA === Game.UpgradeData = { 'ArrowTower': { 'PathA': [{ cost: 60, effects: { AttackComponent: { damage: 5 } } }, //{7R} // Level 1: +5 Dmg { cost: 110, effects: { AttackComponent: { damage: 10 } } } //{7W} // Level 2: +10 Dmg ], 'PathB': [{ cost: 75, effects: { AttackComponent: { attack_speed: 0.2 } } }, //{83} // Level 1: +0.2 AS { cost: 125, effects: { AttackComponent: { attack_speed: 0.3 } } } //{8a} // Level 2: +0.3 AS ] }, 'RockLauncher': { 'PathA': [ // Heavy Boulders - More Damage & Range { cost: 150, effects: { AttackComponent: { damage: 15, range: 20 } } }, { cost: 250, effects: { AttackComponent: { damage: 25, range: 30 } } }, // --- UPDATED UPGRADE --- { cost: 500, effects: { AttackComponent: { damage: 50, range: 50 } }, add_component_on_impact: { name: 'FireZoneOnImpactComponent', args: [10, 3, 0.5] } // dmg, duration, tick_rate }], 'PathB': [ // Lighter Munitions - Faster Attack Speed { cost: 140, effects: { AttackComponent: { damage: 5, attack_speed: 0.2 } } }, { cost: 220, effects: { AttackComponent: { damage: 10, attack_speed: 0.3 } } }, { cost: 450, effects: { AttackComponent: { damage: 15, attack_speed: 0.5 } } }] } }; // === SECTION: GRID DRAWING === // Create a container for all game objects that will be panned var gameWorld = new Container(); game.addChild(gameWorld); function drawGrid() { // Draw the grid cells for (var x = 0; x < Game.GridManager.grid.length; x++) { for (var y = 0; y < Game.GridManager.grid[x].length; y++) { var cellSprite = gameWorld.attachAsset('grid_cell', {}); cellSprite.x = x * 64; cellSprite.y = y * 64; cellSprite.anchor.set(0, 0); // Anchor to top-left cellSprite.gridX = x; // Store grid coordinates on the sprite cellSprite.gridY = y; cellSprite.down = function () { if (Game.GameStateManager.currentState() !== Game.GameplayState) { return; } // Only process true clicks, not drags var timeSinceMouseDown = LK.ticks / 60 * 1000 - mouseDownTime; if (timeSinceMouseDown > 200) { return; } var dx = Math.abs(lastMouseX - dragStartX); var dy = Math.abs(lastMouseY - dragStartY); if (dx > 10 || dy > 10) { return; } var gridX = this.gridX; var gridY = this.gridY; var gridCell = Game.GridManager.grid[gridX][gridY]; if (Game.GameplayManager.activeBuildType) { // If in build mode, check if the 3x3 area is buildable if (Game.GridManager.isBuildable3x3(gridX, gridY)) { Game.GameplayManager.isConfirmingPlacement = true; // The center of the build is the clicked cell Game.GameplayManager.confirmPlacementCoords = { x: gridX, y: gridY }; var worldX = gridX * 64 + 32; var worldY = gridY * 64 + 32; confirmPanel.x = worldX; confirmPanel.y = worldY; } else { // Clicked an invalid spot, so cancel build mode Game.GameplayManager.activeBuildType = null; } } else if (!Game.GameplayManager.isConfirmingPlacement) { // If not in build mode, check for selection if (gridCell.state === Game.GridManager.CELL_STATES.TOWER) { // The grid cell now directly tells us which tower is selected Game.GameplayManager.selectedTowerId = gridCell.entityId; } else { Game.GameplayManager.selectedTowerId = -1; } } }; } } // Highlight the path cells if (Game.PathManager.currentPath) { for (var i = 0; i < Game.PathManager.currentPath.length; i++) { var pathNode = Game.PathManager.currentPath[i]; var pathSprite = gameWorld.attachAsset('path_cell', {}); // Note: PathManager coordinates are already in pixels pathSprite.x = pathNode.x; pathSprite.y = pathNode.y; pathSprite.anchor.set(0, 0); } } } // === SECTION: GLOBAL EVENT BUS === Game.EventBus = { listeners: {}, subscribe: function subscribe(eventType, listener) { if (!this.listeners[eventType]) { this.listeners[eventType] = []; } this.listeners[eventType].push(listener); }, unsubscribe: function unsubscribe(eventType, listener) { if (this.listeners[eventType]) { var index = this.listeners[eventType].indexOf(listener); if (index !== -1) { this.listeners[eventType].splice(index, 1); } } }, publish: function publish(event) { if (this.listeners[event.type]) { for (var i = 0; i < this.listeners[event.type].length; i++) { this.listeners[event.type][i](event); } } } }; Game.Events = { EnemyReachedEndEvent: function EnemyReachedEndEvent(enemy_id) { return { type: 'EnemyReachedEnd', enemy_id: enemy_id }; }, EnemyKilledEvent: function EnemyKilledEvent(enemy_id, gold_value) { return { type: 'EnemyKilled', enemy_id: enemy_id, gold_value: gold_value }; }, TowerPlacedEvent: function TowerPlacedEvent(tower_id, cost, position_x, position_y) { return { type: 'TowerPlaced', tower_id: tower_id, cost: cost, position_x: position_x, position_y: position_y }; }, TowerSoldEvent: function TowerSoldEvent(tower_id, refund_value) { return { type: 'TowerSold', tower_id: tower_id, refund_value: refund_value }; }, PowerStatusChangedEvent: function PowerStatusChangedEvent(new_status) { return { type: 'PowerStatusChanged', new_status: new_status }; }, WaveCompletedEvent: function WaveCompletedEvent(wave_number) { return { type: 'WaveCompleted', wave_number: wave_number }; }, GameOverEvent: function GameOverEvent() { return { type: 'GameOver' }; }, 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) { // dt is not used yet, but kept for consistency if (Game.GameManager.isGameOver) { return; } // Run core game logic systems Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); // --- UI STATE MACHINE FOR DRAG-AND-DROP --- // (This entire large block of UI logic is now correctly contained within the gameplay state) var currentBuildType = Game.GameplayManager.activeBuildType; if (currentBuildType && !Game.GameplayManager.isConfirmingPlacement) { var towerData = Game.TowerData[currentBuildType]; confirmPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; if (!ghostTowerSprite || ghostTowerSprite.towerType !== currentBuildType) { if (ghostTowerSprite) { ghostTowerSprite.destroy(); } ghostTowerSprite = gameWorld.attachAsset(towerData.sprite_id, {}); ghostTowerSprite.anchor.set(0.5, 0.5); ghostTowerSprite.alpha = 0.5; ghostTowerSprite.towerType = currentBuildType; } ghostTowerSprite.visible = true; var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); ghostTowerSprite.x = gridX * 64 + 32; ghostTowerSprite.y = gridY * 64 + 32; ghostTowerSprite.tint = Game.GridManager.isBuildable3x3(gridX, gridY) ? 0xFFFFFF : 0xFF5555; floatingInfoPanel.visible = true; infoText.setText(currentBuildType + "\nCost: " + towerData.cost); } else if (Game.GameplayManager.isConfirmingPlacement) { if (ghostTowerSprite) { ghostTowerSprite.visible = false; } floatingInfoPanel.visible = false; upgradePanel.visible = false; rangeCircle.visible = false; if (!confirmButton || confirmButton.towerType !== currentBuildType) { if (confirmButton) { confirmButton.destroy(); } if (cancelButton) { cancelButton.destroy(); } var towerData = Game.TowerData[currentBuildType]; 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 = currentBuildType; confirmButton.down = function () { var coords = Game.GameplayManager.confirmPlacementCoords; var buildType = Game.GameplayManager.activeBuildType; if (coords && buildType) { Game.Systems.TowerBuildSystem.tryBuildAt(coords.x, coords.y, buildType); } Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.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 = currentBuildType; cancelButton.down = function () { Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.activeBuildType = null; }; confirmPanel.addChild(cancelButton); } confirmPanel.visible = true; } else if (Game.GameplayManager.selectedTowerId !== -1) { 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 = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'] ? Game.EntityManager.componentStores['RenderComponent'][towerId] : null; var towerComp = Game.EntityManager.componentStores['TowerComponent'] ? Game.EntityManager.componentStores['TowerComponent'][towerId] : null; var attackComp = Game.EntityManager.componentStores['AttackComponent'] ? Game.EntityManager.componentStores['AttackComponent'][towerId] : null; var transformComp = Game.EntityManager.componentStores['TransformComponent'] ? Game.EntityManager.componentStores['TransformComponent'][towerId] : null; if (!renderComp || !towerComp || !transformComp) { Game.GameplayManager.selectedTowerId = -1; upgradePanel.visible = false; rangeCircle.visible = false; } else { upgradePanel.visible = true; upgradePanel.x = transformComp.x + 100; // Move it a bit closer horizontally upgradePanel.y = transformComp.y - 140; // Offset by half the panel's height to vertically center it // This line moves the panel to the top of the render stack 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 { 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; } game.up = function () { if (Game.GameplayManager.activeBuildType && !Game.GameplayManager.isConfirmingPlacement) { var mouseWorldX = lastMouseX - cameraX; var mouseWorldY = lastMouseY - cameraY; var gridX = Math.floor(mouseWorldX / 64); var gridY = Math.floor(mouseWorldY / 64); if (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; } } isDragging = false; }; 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; } // Set the active build type to start the drag process Game.GameplayManager.activeBuildType = this.towerType; Game.GameplayManager.isConfirmingPlacement = false; Game.GameplayManager.selectedTowerId = -1; // Deselect any existing tower }; // We no longer need a 'move' handler here, as the main game loop will handle the drag. buildPanel.addChild(button); } // We no longer need the 'buildPanel.out' handler either. buildPanel.out = null; // Confirmation button logic is now handled dynamically in the game update loop // === SECTION: MINIMAP UI === var minimapContainer = new Container(); LK.gui.bottomLeft.addChild(minimapContainer); minimapContainer.x = 20; // 20px padding from the left edge minimapContainer.y = -320; // Position up from bottom edge (negative value moves up in bottomLeft anchor) var minimapBackground = new Graphics(); minimapContainer.addChild(minimapBackground); // Function to draw the static parts of the minimap function drawMinimapBackground() { var mapWidth = 40; var mapHeight = 60; var tilePixelSize = 5; // Each grid cell is a 5x5 pixel block on the minimap var colors = { EMPTY: 0x3d2b1f, // Dark Brown for buildable land PATH: 0x8B4513, // SaddleBrown for the path TOWER: 0xFFFFFF, // White (will be used for dynamic tower dots later) BLOCKED: 0x111111 // Almost black for blocked areas }; minimapBackground.clear(); minimapBackground.beginFill(0x000000, 0.5); // Semi-transparent black background for the minimap minimapBackground.drawRect(0, 0, mapWidth * tilePixelSize, mapHeight * tilePixelSize); minimapBackground.endFill(); for (var x = 0; x < mapWidth; x++) { for (var y = 0; y < mapHeight; y++) { var cellState = Game.GridManager.grid[x][y]; var color; switch (cellState) { case Game.GridManager.CELL_STATES.PATH: color = colors.PATH; break; case Game.GridManager.CELL_STATES.BLOCKED: color = colors.BLOCKED; break; default: color = colors.EMPTY; break; } minimapBackground.beginFill(color); minimapBackground.drawRect(x * tilePixelSize, y * tilePixelSize, tilePixelSize, tilePixelSize); minimapBackground.endFill(); } } } // Call the function once to draw the initial state drawMinimapBackground(); var minimapDynamicLayer = new Graphics(); minimapContainer.addChild(minimapDynamicLayer); var minimapViewportRect = new Graphics(); minimapContainer.addChild(minimapViewportRect); minimapContainer.down = function (x, y, obj) { 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(); } // Add mouse event handlers for camera panning game.down = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) { isDragging = false; return; } isDragging = true; dragStartX = x; dragStartY = y; lastMouseX = x; lastMouseY = y; cameraStartX = cameraX; cameraStartY = cameraY; mouseDownTime = LK.ticks / 60 * 1000; // Convert to milliseconds }; game.move = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) { return; } lastMouseX = x; lastMouseY = y; if (isDragging) { var dx = x - dragStartX; var dy = y - dragStartY; // Update camera position (inverted for drag effect) cameraX = cameraStartX + dx; cameraY = cameraStartY + dy; // Clamp camera to map boundaries var mapWidth = 40 * 64; // 40 tiles * 64 pixels per tile var mapHeight = 60 * 64; // 60 tiles * 64 pixels per tile var screenWidth = 2048; var screenHeight = 2732; // Ensure we never see beyond the map edges var minX = screenWidth - mapWidth; var minY = screenHeight - mapHeight; var maxX = 0; var maxY = 0; cameraX = Math.max(minX, Math.min(maxX, cameraX)); cameraY = Math.max(minY, Math.min(maxY, cameraY)); // Apply camera transform to game world gameWorld.x = cameraX; gameWorld.y = cameraY; } }; game.up = function (x, y, obj) { if (Game.GameStateManager.currentState() !== Game.GameplayState) { isDragging = false; return; } 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, 500, 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
@@ -2455,9 +2455,9 @@
// -- 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, 450, 280);
+upgradePanelBackground.drawRect(-10, -30, 500, 280);
upgradePanelBackground.endFill();
upgradePanel.addChild(upgradePanelBackground);
var towerNameText = new Text2('Tower Name', {
size: 50,