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