User prompt
Hello Ava. Let's add new waves to test our new enemy archetypes. Please replace the entire Game.WaveData object with this expanded version. This adds Waves 4 and 5, which are designed to spawn Armored and Flyer enemies, respectively. 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 }] } ];
User prompt
Hello Ava. Let's enable the Flyer enemy type. Please replace the entire Game.EnemyData object with the following updated version. The only change is within the 'Flyer' definition, where we now properly add the FlyingComponent to its list of components. Game.EnemyData = { 'Grunt': { health: 100, speed: 50, gold_value: 5, lives_cost: 1, components: [] // No special components }, 'Swarmer': { health: 30, speed: 80, gold_value: 2, lives_cost: 1, components: [] // No special components }, 'Armored': { health: 400, speed: 30, gold_value: 15, lives_cost: 2, components: [ // We will add the ArmorComponent definition later ] }, 'Flyer': { health: 80, speed: 60, gold_value: 8, lives_cost: 1, components: [{ name: 'FlyingComponent', args: [] }] } };
User prompt
Hello Ava. Let's correct the syntax error from the last update. Please replace the entire Game.Systems object, from Game.Systems = { to its final closing brace };, with the following complete and correctly structured version. This will fix the trySellTower function's location and resolve the error. Game.Systems = { MovementSystem: { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); var frame_time = 1 / 60; // Assuming 60 FPS for (var i = 0; i < movableEntities.length; i++) { var entityId = movableEntities[i]; var transformComponent = Game.EntityManager.componentStores['TransformComponent'][entityId]; var movementComponent = Game.EntityManager.componentStores['MovementComponent'][entityId]; // 1. Get base speed and apply debuffs var currentSpeed = movementComponent.speed; var slowDebuff = Game.EntityManager.componentStores['SlowDebuffComponent'] ? Game.EntityManager.componentStores['SlowDebuffComponent'][entityId] : null; if (slowDebuff) { currentSpeed = movementComponent.speed * (1 - slowDebuff.slow_percentage); slowDebuff.duration_remaining -= frame_time; if (slowDebuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['SlowDebuffComponent'][entityId]; } } // 2. Check if the entity is a flyer var isFlyer = Game.EntityManager.componentStores['FlyingComponent'] && Game.EntityManager.componentStores['FlyingComponent'][entityId]; if (isFlyer) { // Flying Movement: Move in a straight line to the exit var exitPoint = { x: 39 * 64, y: 30 * 64 }; var dx = exitPoint.x - transformComponent.x; var dy = exitPoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); var moveDistance = currentSpeed * frame_time; if (distance > moveDistance) { transformComponent.x += dx / distance * moveDistance; transformComponent.y += dy / distance * moveDistance; } else { // Flyer reached the end Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } else { // Ground Movement: Follow the path if (Game.PathManager.currentPath && Game.PathManager.currentPath.length > 0) { var targetWaypoint = Game.PathManager.currentPath[movementComponent.path_index]; if (targetWaypoint) { var dx = targetWaypoint.x - transformComponent.x; var dy = targetWaypoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); var moveDistance = currentSpeed * frame_time; if (distance > moveDistance) { transformComponent.x += dx / distance * moveDistance; transformComponent.y += dy / distance * moveDistance; } else { transformComponent.x = targetWaypoint.x; transformComponent.y = targetWaypoint.y; movementComponent.path_index++; if (movementComponent.path_index >= Game.PathManager.currentPath.length) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } } } } } } }, RenderSystem: { displayObjects: {}, update: function update() { var renderableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'RenderComponent']); for (var i = 0; i < renderableEntities.length; i++) { var entityId = renderableEntities[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][entityId]; var render = Game.EntityManager.componentStores['RenderComponent'][entityId]; var sprite = this.displayObjects[entityId]; if (!sprite) { sprite = game.attachAsset(render.sprite_id, {}); 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]; if (!towerData) { return; } if (!Game.GridManager.isBuildable(gridX, gridY)) { return; } if (!Game.ResourceManager.spendGold(towerData.cost)) { return; } var towerId = Game.EntityManager.createEntity(); Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.TOWER; var transform = Game.Components.TransformComponent(gridX * 64, gridY * 64); 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 = 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() { 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 enemyMovement = Game.EntityManager.componentStores['MovementComponent'][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) { if (enemyMovement.path_index > bestPathIndex) { bestTarget = enemyId; bestPathIndex = enemyMovement.path_index; } } } 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++; this.isSpawning = true; this.spawnTimer = 0; this.subWaveIndex = 0; this.spawnedInSubWave = 0; }, 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 transform = Game.Components.TransformComponent(0 * 64, 30 * 64); var render = Game.Components.RenderComponent('enemy', 1, 1.0, true); var movement = Game.Components.MovementComponent(enemyData.speed, 0); 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: { update: function 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']); 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; } targetHealth.current_hp -= damage; if (targetHealth.current_hp <= 0) { var enemyComp = Game.EntityManager.componentStores['EnemyComponent'][enemyId]; 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; Game.ResourceManager.addGold(refundAmount); var gridX = Math.floor(transformComp.x / 64); var gridY = Math.floor(transformComp.y / 64); if (Game.GridManager.grid[gridX] && Game.GridManager.grid[gridX][gridY] !== undefined) { Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.EMPTY; } Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount)); Game.GameplayManager.selectedTowerId = -1; Game.EntityManager.destroyEntity(towerId); } } };
User prompt
Hello Ava. Let's fix the bug that prevents selling towers. Please replace the entire Game.Systems.TowerSystem object, and the rogue trySellTower function that follows it, with this single, correctly structured Game.Systems.TowerSystem object. TowerSystem: { tryUpgradeTower: function tryUpgradeTower(towerId, path) { // 1. Get the tower's type 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; } // 2. Get the upgrade data var upgradePathData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType][path] : null; if (!upgradePathData) { return; } var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; if (!towerComp) { return; } // 3. Determine the next upgrade level var nextUpgradeIndex = path === 'PathA' ? towerComp.upgrade_path_A_level : towerComp.upgrade_path_B_level; if (nextUpgradeIndex >= upgradePathData.length) { return; // Maxed out } var upgradeInfo = upgradePathData[nextUpgradeIndex]; // 4. Check gold if (!Game.ResourceManager.spendGold(upgradeInfo.cost)) { return; // Not enough gold } // 5. Apply stat effects 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]; } } } } // 6. Apply special "add component" effects 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); } } // 7. Update tower's level trackers towerComp.upgrade_level++; if (path === 'PathA') { towerComp.upgrade_path_A_level++; } else { towerComp.upgrade_path_B_level++; } }, trySellTower: function trySellTower(towerId) { // 1. Get the tower's components var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!towerComp || !transformComp) { return; } // 2. Refund gold var refundAmount = towerComp.sell_value; Game.ResourceManager.addGold(refundAmount); // 3. Free up the grid cell var gridX = Math.floor(transformComp.x / 64); var gridY = Math.floor(transformComp.y / 64); if (Game.GridManager.grid[gridX] && Game.GridManager.grid[gridX][gridY] !== undefined) { Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.EMPTY; } // 4. Publish a sell event Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount)); // 5. Deselect the tower and schedule it for destruction Game.GameplayManager.selectedTowerId = -1; Game.EntityManager.destroyEntity(towerId); } },
User prompt
Please fix the bug: 'Uncaught TypeError: Game.Systems.TowerSystem.trySellTower is not a function' in or related to this line: 'Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId);' Line Number: 1812
User prompt
Hello Ava. Let's fix the UI bug with the upgrade panel's position. Please replace the entire // === SECTION: UPGRADE & SELL UI === block with the following updated version. The only change is the upgradePanel.y value, which has been adjusted from -50 to -200. // === SECTION: UPGRADE & SELL UI === var upgradePanel = new Container(); LK.gui.bottom.addChild(upgradePanel); upgradePanel.x = -300; // Positioned to the right of center upgradePanel.y = -200; // Adjusted from -50 to bring the panel fully on-screen upgradePanel.visible = false; // Initially hidden var towerNameText = new Text2('Tower Name', { size: 40, fill: 0xFFFFFF }); towerNameText.anchor.set(0, 0); upgradePanel.addChild(towerNameText); var upgradeAPathButton = new Text2('Upgrade Path A', { size: 35, fill: 0x27AE60 }); upgradeAPathButton.anchor.set(0, 0); upgradeAPathButton.y = 50; upgradePanel.addChild(upgradeAPathButton); var upgradeBPathButton = new Text2('Upgrade Path B', { size: 35, fill: 0x2980B9 }); upgradeBPathButton.anchor.set(0, 0); upgradeBPathButton.y = 100; upgradePanel.addChild(upgradeBPathButton); var sellButton = new Text2('Sell Tower', { size: 35, fill: 0xC0392B }); sellButton.anchor.set(0, 0); sellButton.y = 150; upgradePanel.addChild(sellButton); sellButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId); } }; // Assign click handlers that will call the TowerSystem upgradeAPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathA'); } }; upgradeBPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathB'); } };
User prompt
Hello Ava. Let's implement the movement logic for the Flyer enemy. Please replace the entire Game.Systems.MovementSystem object with the following new version. This version adds a check for the FlyingComponent. If an entity has this component, it will move in a straight line towards the exit; otherwise, it will follow the normal path. Game.Systems.MovementSystem = { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); var frame_time = 1 / 60; // Assuming 60 FPS for (var i = 0; i < movableEntities.length; i++) { var entityId = movableEntities[i]; var transformComponent = Game.EntityManager.componentStores['TransformComponent'][entityId]; var movementComponent = Game.EntityManager.componentStores['MovementComponent'][entityId]; // 1. Get base speed and apply debuffs var currentSpeed = movementComponent.speed; var slowDebuff = Game.EntityManager.componentStores['SlowDebuffComponent'] ? Game.EntityManager.componentStores['SlowDebuffComponent'][entityId] : null; if (slowDebuff) { currentSpeed = movementComponent.speed * (1 - slowDebuff.slow_percentage); slowDebuff.duration_remaining -= frame_time; if (slowDebuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['SlowDebuffComponent'][entityId]; } } // --- START OF NEW FLYING/GROUND LOGIC --- // 2. Check if the entity is a flyer var isFlyer = Game.EntityManager.componentStores['FlyingComponent'] && Game.EntityManager.componentStores['FlyingComponent'][entityId]; if (isFlyer) { // Flying Movement: Move in a straight line to the exit var exitPoint = { x: 39 * 64, y: 30 * 64 }; var dx = exitPoint.x - transformComponent.x; var dy = exitPoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); var moveDistance = currentSpeed * frame_time; if (distance > moveDistance) { transformComponent.x += dx / distance * moveDistance; transformComponent.y += dy / distance * moveDistance; } else { // Flyer reached the end Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } else { // Ground Movement: Follow the path if (Game.PathManager.currentPath && Game.PathManager.currentPath.length > 0) { var targetWaypoint = Game.PathManager.currentPath[movementComponent.path_index]; if (targetWaypoint) { var dx = targetWaypoint.x - transformComponent.x; var dy = targetWaypoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); var moveDistance = currentSpeed * frame_time; if (distance > moveDistance) { transformComponent.x += dx / distance * moveDistance; transformComponent.y += dy / distance * moveDistance; } else { transformComponent.x = targetWaypoint.x; transformComponent.y = targetWaypoint.y; movementComponent.path_index++; if (movementComponent.path_index >= Game.PathManager.currentPath.length) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } } } } // --- END OF NEW FLYING/GROUND LOGIC --- } } };
User prompt
Hello Ava. Let's adjust the starting gold to make testing easier. Please find the Game.ResourceManager object and change the initial value of the gold property from 100 to 500. 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)); } };
User prompt
Hello Ava. Let's fix the enemy movement bug. Please replace the entire Game.Systems.MovementSystem object with the following updated and more robust version. Game.Systems.MovementSystem = { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); var frame_time = 1 / 60; // Assuming 60 FPS for now for (var i = 0; i < movableEntities.length; i++) { var entityId = movableEntities[i]; var transformComponent = Game.EntityManager.componentStores['TransformComponent'][entityId]; var movementComponent = Game.EntityManager.componentStores['MovementComponent'][entityId]; // 1. Get base speed and apply debuffs var currentSpeed = movementComponent.speed; var slowDebuff = Game.EntityManager.componentStores['SlowDebuffComponent'] ? Game.EntityManager.componentStores['SlowDebuffComponent'][entityId] : null; if (slowDebuff) { currentSpeed = movementComponent.speed * (1 - slowDebuff.slow_percentage); slowDebuff.duration_remaining -= frame_time; if (slowDebuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['SlowDebuffComponent'][entityId]; } } // 2. Check for a valid path if (Game.PathManager.currentPath && Game.PathManager.currentPath.length > 0) { var targetWaypoint = Game.PathManager.currentPath[movementComponent.path_index]; if (targetWaypoint) { var dx = targetWaypoint.x - transformComponent.x; var dy = targetWaypoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); // 3. Calculate how far to move this frame var moveDistance = currentSpeed * frame_time; // 4. Robust waypoint arrival check if (distance > moveDistance) { // If we are further than one frame's movement, move towards the waypoint transformComponent.x += dx / distance * moveDistance; transformComponent.y += dy / distance * moveDistance; } else { // Otherwise, snap to the waypoint and target the next one transformComponent.x = targetWaypoint.x; transformComponent.y = targetWaypoint.y; movementComponent.path_index++; // 5. Check if the enemy reached the end of the path if (movementComponent.path_index >= Game.PathManager.currentPath.length) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } } } } } },
User prompt
Hello Ava. Let's fix the spawning issue by updating our wave definitions to use the new enemy archetypes. Please replace the entire Game.WaveData object with this new version. We are re-defining the first three waves to use 'Grunt' and 'Swarmer' enemies. 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 }] } ];
User prompt
Hello Ava. Now that we have our enemy data and components defined, let's update the WaveSpawnerSystem to spawn the correct archetypes. Please replace the entire Game.Systems.WaveSpawnerSystem object with the following updated version. The update function has been significantly changed to be data-driven. Game.Systems.WaveSpawnerSystem = { currentWaveIndex: -1, isSpawning: false, spawnTimer: 0, subWaveIndex: 0, spawnedInSubWave: 0, startNextWave: function startNextWave() { this.currentWaveIndex++; this.isSpawning = true; this.spawnTimer = 0; this.subWaveIndex = 0; this.spawnedInSubWave = 0; }, 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]; // Handle start delay if (this.spawnedInSubWave === 0 && this.spawnTimer < currentSubWave.start_delay) { this.spawnTimer += 1 / 60; // Increment by frame time return; } // Handle spawn delay between enemies if (this.spawnedInSubWave > 0 && this.spawnTimer < currentSubWave.spawn_delay) { this.spawnTimer += 1 / 60; return; } // --- START OF NEW SPAWNING LOGIC --- if (this.spawnedInSubWave < currentSubWave.count) { // 1. Look up the enemy data based on the type from the wave definition var enemyData = Game.EnemyData[currentSubWave.enemy_type]; if (!enemyData) { // Skip this sub-wave if the enemy type is invalid this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; return; } // 2. Create the enemy entity var enemyId = Game.EntityManager.createEntity(); // 3. Add components using the data from Game.EnemyData var transform = Game.Components.TransformComponent(0 * 64, 30 * 64); var render = Game.Components.RenderComponent('enemy', 1, 1.0, true); // Still uses a generic sprite for now var movement = Game.Components.MovementComponent(enemyData.speed, 0); 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); // 4. Add any special components defined in the enemy data 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); } } // 5. Update spawner state this.spawnedInSubWave++; this.spawnTimer = 0; Game.GameplayManager.enemiesSpawnedThisWave++; } else { // Move to next sub-wave this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; } // --- END OF NEW SPAWNING LOGIC --- } };
User prompt
Hello Ava. Let's fix the flyer bug. This requires updating both the WaveSpawnerSystem and the MovementSystem. First, please replace the entire Game.Systems.WaveSpawnerSystem object with this corrected version. It now properly adds special components from Game.EnemyData when creating an enemy. Game.Systems.WaveSpawnerSystem = { currentWaveIndex: -1, isSpawning: false, spawnTimer: 0, subWaveIndex: 0, spawnedInSubWave: 0, startNextWave: function startNextWave() { this.currentWaveIndex++; this.isSpawning = true; this.spawnTimer = 0; this.subWaveIndex = 0; this.spawnedInSubWave = 0; }, 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]; // Handle start delay if (this.spawnedInSubWave === 0 && this.spawnTimer < currentSubWave.start_delay) { this.spawnTimer += 1 / 60; // Increment by frame time return; } // Handle spawn delay between enemies if (this.spawnedInSubWave > 0 && this.spawnTimer < currentSubWave.spawn_delay) { this.spawnTimer += 1 / 60; return; } // Spawn enemy if (this.spawnedInSubWave < currentSubWave.count) { var enemyType = currentSubWave.enemy_type; var enemyData = Game.EnemyData[enemyType]; if (!enemyData) { this.spawnedInSubWave++; return; } var enemyId = Game.EntityManager.createEntity(); // Add base components Game.EntityManager.addComponent(enemyId, Game.Components.TransformComponent(0 * 64, 30 * 64)); Game.EntityManager.addComponent(enemyId, Game.Components.MovementComponent(enemyData.speed, 0)); Game.EntityManager.addComponent(enemyId, Game.Components.RenderComponent('enemy', 1, 1.0, true)); Game.EntityManager.addComponent(enemyId, Game.Components.HealthComponent(enemyData.health, enemyData.health)); Game.EntityManager.addComponent(enemyId, Game.Components.EnemyComponent(enemyData.gold_value, enemyData.lives_cost)); // Add any special components from the data definition for (var i = 0; i < enemyData.components.length; i++) { var componentDef = enemyData.components[i]; var component = Game.Components[componentDef.name].apply(null, componentDef.args); Game.EntityManager.addComponent(enemyId, component); } this.spawnedInSubWave++; this.spawnTimer = 0; Game.GameplayManager.enemiesSpawnedThisWave++; } else { this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; } } }; Second, replace the entire Game.Systems.MovementSystem object with this new version. It now checks for the FlyingComponent and applies straight-line movement if present, while ground units continue to follow the path. Game.Systems.MovementSystem = { update: function update() { var movableEntities = Game.EntityManager.getEntitiesWithComponents(['TransformComponent', 'MovementComponent']); var endPoint = { x: 39 * 64, y: 30 * 64 }; // The final destination for (var i = 0; i < movableEntities.length; i++) { var entityId = movableEntities[i]; var transformComponent = Game.EntityManager.componentStores['TransformComponent'][entityId]; var movementComponent = Game.EntityManager.componentStores['MovementComponent'][entityId]; // 1. Get base speed and check for slow debuff (this logic remains the same) var currentSpeed = movementComponent.speed; var slowDebuff = Game.EntityManager.componentStores['SlowDebuffComponent'] ? Game.EntityManager.componentStores['SlowDebuffComponent'][entityId] : null; if (slowDebuff) { currentSpeed = movementComponent.speed * (1 - slowDebuff.slow_percentage); slowDebuff.duration_remaining -= 1.0 / 60; if (slowDebuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['SlowDebuffComponent'][entityId]; } } // 2. Check if the entity is a flyer var isFlying = Game.EntityManager.componentStores['FlyingComponent'] && Game.EntityManager.componentStores['FlyingComponent'][entityId]; if (isFlying) { // --- FLYING LOGIC --- // Flyers move in a straight line to the end point var dx = endPoint.x - transformComponent.x; var dy = endPoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 1) { var moveX = dx / distance * currentSpeed; var moveY = dy / distance * currentSpeed; transformComponent.x += moveX; transformComponent.y += moveY; } else { // Flyer has reached the end Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } else { // --- GROUND LOGIC (existing logic) --- // Ground units follow the path if (Game.PathManager.currentPath && Game.PathManager.currentPath.length > 0) { var targetWaypoint = Game.PathManager.currentPath[movementComponent.path_index]; if (targetWaypoint) { var dx = targetWaypoint.x - transformComponent.x; var dy = targetWaypoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 1) { var moveX = dx / distance * currentSpeed; var moveY = dy / distance * currentSpeed; transformComponent.x += moveX; transformComponent.y += moveY; } else { movementComponent.path_index++; if (movementComponent.path_index >= Game.PathManager.currentPath.length) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } } } } } } };
User prompt
Hello Ava. Let's fix the bug in the WaveSpawnerSystem that prevents it from adding special components (like the FlyingComponent) to enemies. Please replace the entire Game.Systems.WaveSpawnerSystem object with the following corrected version. The new code in the update function now properly iterates through the enemyData.components array and adds all defined components to the new enemy entity. Game.Systems.WaveSpawnerSystem = { currentWaveIndex: -1, isSpawning: false, spawnTimer: 0, subWaveIndex: 0, spawnedInSubWave: 0, startNextWave: function startNextWave() { this.currentWaveIndex++; this.isSpawning = true; this.spawnTimer = 0; this.subWaveIndex = 0; this.spawnedInSubWave = 0; }, 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]; // Handle start delay if (this.spawnedInSubWave === 0 && this.spawnTimer < currentSubWave.start_delay) { this.spawnTimer += 1 / 60; // Increment by frame time return; } // Handle spawn delay between enemies if (this.spawnedInSubWave > 0 && this.spawnTimer < currentSubWave.spawn_delay) { this.spawnTimer += 1 / 60; return; } // Spawn enemy if (this.spawnedInSubWave < currentSubWave.count) { var enemyType = currentSubWave.enemy_type; var enemyData = Game.EnemyData[enemyType]; if (!enemyData) { // Skip if the enemy type is invalid this.spawnedInSubWave++; return; } var enemyId = Game.EntityManager.createEntity(); // Add base components Game.EntityManager.addComponent(enemyId, Game.Components.TransformComponent(0 * 64, 30 * 64)); Game.EntityManager.addComponent(enemyId, Game.Components.MovementComponent(enemyData.speed, 0)); Game.EntityManager.addComponent(enemyId, Game.Components.RenderComponent('enemy', 1, 1.0, true)); Game.EntityManager.addComponent(enemyId, Game.Components.HealthComponent(enemyData.health, enemyData.health)); Game.EntityManager.addComponent(enemyId, Game.Components.EnemyComponent(enemyData.gold_value, enemyData.lives_cost)); // --- START OF NEW/CORRECTED LOGIC --- // Add any special components from the data definition for (var i = 0; i < enemyData.components.length; i++) { var componentDef = enemyData.components[i]; var component = Game.Components[componentDef.name].apply(null, componentDef.args); Game.EntityManager.addComponent(enemyId, component); } // --- END OF NEW/CORRECTED LOGIC --- this.spawnedInSubWave++; this.spawnTimer = 0; Game.GameplayManager.enemiesSpawnedThisWave++; } else { // Move to next sub-wave this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; } } };
User prompt
Hello Ava. We have a bug where players can block the path, causing enemies to get stuck. Let's fix this by implementing the path validation logic in the TowerBuildSystem. Please replace the entire Game.Systems.TowerBuildSystem object with the following updated version. This new logic temporarily marks a grid cell, checks if a path still exists, and only finalizes the build if the path is not blocked. Game.Systems.TowerBuildSystem = { tryBuildAt: function tryBuildAt(gridX, gridY, towerType) { // 0. Look up tower data var towerData = Game.TowerData[towerType]; if (!towerData) { return; } // 1. Initial checks: Is the spot buildable and can the player afford it? if (!Game.GridManager.isBuildable(gridX, gridY)) { return; } if (Game.ResourceManager.gold < towerData.cost) { return; } // 2. Temporarily change the grid to test the path Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.TOWER; // 3. Run the pathfinder on the temporary grid var tempPath = Game.PathManager.findPath({ x: 0, y: 30 }, { x: 39, y: 30 }); // 4. Check the result. If the path is blocked, revert the change and cancel the build. if (tempPath === null) { Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.EMPTY; return; // Build is invalid, so we exit. } // 5. If we reach here, the path is valid. Finalize the build. Game.ResourceManager.spendGold(towerData.cost); // Create the entity and add all its components var towerId = Game.EntityManager.createEntity(); var transform = Game.Components.TransformComponent(gridX * 64, gridY * 64); 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 = Game.Components[componentDef.name].apply(null, componentDef.args); Game.EntityManager.addComponent(towerId, component); } // Announce that a tower has been officially placed var event = Game.Events.TowerPlacedEvent(towerId, towerData.cost, transform.x, transform.y); Game.EventBus.publish(event); } };
User prompt
Hello Ava. Let's prepare the data structures needed for flying enemies. This involves three modifications. First, replace the Game.EnemyData object with this new version, which adds the FlyingComponent to the 'Flyer' archetype. Game.EnemyData = { 'Grunt': { health: 100, speed: 50, gold_value: 5, lives_cost: 1, components: [] // No special components }, 'Swarmer': { health: 30, speed: 80, gold_value: 2, lives_cost: 1, components: [] // No special components }, 'Armored': { health: 400, speed: 30, gold_value: 15, lives_cost: 2, components: [ // We will add the ArmorComponent definition later ] }, 'Flyer': { health: 80, speed: 60, gold_value: 8, lives_cost: 1, components: [{ name: 'FlyingComponent', args: [] }] } }; Second, replace the Game.Components.AttackComponent function with this updated version that includes the new can_target_air property. AttackComponent: function AttackComponent(damage, range, attack_speed, last_attack_time, projectile_id, target_id, splash_radius, can_target_air) { return { name: 'AttackComponent', damage: damage || 10, range: range || 100, attack_speed: attack_speed || 1.0, last_attack_time: last_attack_time || 0, projectile_id: projectile_id || '', target_id: target_id || -1, splash_radius: splash_radius || 0, can_target_air: can_target_air || false // Default to false }; }, Finally, replace the entire Game.TowerData object to update the attacking towers with the new can_target_air property. Game.TowerData = { 'ArrowTower': { cost: 50, sprite_id: 'arrow_tower', components: [{ name: 'TowerComponent', args: [50] }, { name: 'AttackComponent', args: [10, 150, 1.0, 0, 'arrow_projectile', -1, 0, true] }, { name: 'PowerConsumptionComponent', args: [2] }] }, 'GeneratorTower': { cost: 75, sprite_id: 'generator_tower', components: [{ name: 'TowerComponent', args: [75] }, { name: 'PowerProductionComponent', args: [5] }] }, 'CapacitorTower': { cost: 40, sprite_id: 'capacitor_tower', components: [{ name: 'TowerComponent', args: [40] }, { name: 'PowerStorageComponent', args: [20] }] }, 'RockLauncher': { cost: 125, sprite_id: 'rock_launcher_tower', components: [{ name: 'TowerComponent', args: [125] }, { name: 'AttackComponent', args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50, false] }, { name: 'PowerConsumptionComponent', args: [10] }] }, 'ChemicalTower': { cost: 100, sprite_id: 'chemical_tower', components: [{ name: 'TowerComponent', args: [100] }, { name: 'AttackComponent', args: [5, 150, 0.8, 0, 'chemical_projectile', -1, 0, false] }, { name: 'PowerConsumptionComponent', args: [8] }] }, 'SlowTower': { cost: 80, sprite_id: 'slow_tower', components: [{ name: 'TowerComponent', args: [80] }, { name: 'AttackComponent', args: [2, 160, 1.0, 0, 'slow_projectile', -1, 0, true] }, { name: 'PowerConsumptionComponent', args: [4] }] }, 'DarkTower': { cost: 150, sprite_id: 'dark_tower', components: [{ name: 'TowerComponent', args: [150] }, { name: 'AuraComponent', args: [175, 'damage_buff'] }, { name: 'PowerConsumptionComponent', args: [12] }] }, 'GraveyardTower': { cost: 200, sprite_id: 'graveyard_tower', components: [{ name: 'TowerComponent', args: [200] }, { name: 'GoldGenerationComponent', args: [1.5] }, { name: 'PowerConsumptionComponent', args: [15] }] } };
User prompt
Hello Ava. Now that we have the enemy data and components defined, let's update the WaveSpawnerSystem to use them. First, please replace the entire Game.Systems.WaveSpawnerSystem object with the following updated version. The key change is in the update method, which now looks up enemy properties from Game.EnemyData instead of using hardcoded values. Game.Systems.WaveSpawnerSystem = { currentWaveIndex: -1, isSpawning: false, spawnTimer: 0, subWaveIndex: 0, spawnedInSubWave: 0, startNextWave: function startNextWave() { this.currentWaveIndex++; this.isSpawning = true; this.spawnTimer = 0; this.subWaveIndex = 0; this.spawnedInSubWave = 0; }, 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]; // Handle start delay if (this.spawnedInSubWave === 0 && this.spawnTimer < currentSubWave.start_delay) { this.spawnTimer += 1 / 60; // Increment by frame time return; } // Handle spawn delay between enemies if (this.spawnedInSubWave > 0 && this.spawnTimer < currentSubWave.spawn_delay) { this.spawnTimer += 1 / 60; return; } // Spawn enemy if (this.spawnedInSubWave < currentSubWave.count) { // --- START OF NEW LOGIC --- var enemyType = currentSubWave.enemy_type; var enemyData = Game.EnemyData[enemyType]; if (!enemyData) { // Skip if the enemy type is invalid this.spawnedInSubWave++; return; } var enemyId = Game.EntityManager.createEntity(); Game.EntityManager.addComponent(enemyId, Game.Components.TransformComponent(0 * 64, 30 * 64)); Game.EntityManager.addComponent(enemyId, Game.Components.MovementComponent(enemyData.speed, 0)); Game.EntityManager.addComponent(enemyId, Game.Components.RenderComponent('enemy', 1, 1.0, true)); Game.EntityManager.addComponent(enemyId, Game.Components.HealthComponent(enemyData.health, enemyData.health)); Game.EntityManager.addComponent(enemyId, Game.Components.EnemyComponent(enemyData.gold_value, enemyData.lives_cost)); // armor_type defaults to 'Normal' // --- END OF NEW LOGIC --- this.spawnedInSubWave++; this.spawnTimer = 0; Game.GameplayManager.enemiesSpawnedThisWave++; } else { // Move to next sub-wave this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; } } }; Next, to make this change testable, replace the entire Game.WaveData object with this new version that uses our defined enemy archetypes. Game.WaveData = [ // Wave 1: A handful of 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: Introduce fast Swarmers { wave_number: 3, sub_waves: [{ enemy_type: 'Swarmer', count: 20, spawn_delay: 0.5, start_delay: 0 }] }, // Wave 4: A mixed wave { wave_number: 4, sub_waves: [{ enemy_type: 'Grunt', count: 10, spawn_delay: 1.5, start_delay: 0 }, { enemy_type: 'Swarmer', count: 15, spawn_delay: 0.8, start_delay: 5.0 // Swarmers start 5 seconds after the grunts }] } ];
User prompt
Hello Ava. Let's define the new components for our enemy archetypes. First, please add the new FlyingComponent to the Game.Components object. It's a simple "marker" component with no initial data. FlyingComponent: function FlyingComponent() { return { name: 'FlyingComponent' }; }, Next, find and replace the existing Game.Components.EnemyComponent function with this updated version, which now includes the armor_type property. EnemyComponent: function EnemyComponent(goldValue, livesCost, armor_type) { return { name: 'EnemyComponent', gold_value: goldValue || 5, lives_cost: livesCost || 1, armor_type: armor_type || 'Normal' }; },
User prompt
Hello Ava. Let's begin implementing the enemy archetypes. We will start by creating a new Game.EnemyData object to store their definitions. Please add the following new code block in the // === SECTION: GAME DATA === section, just above Game.WaveData. Game.EnemyData = { 'Grunt': { health: 100, speed: 50, gold_value: 5, lives_cost: 1, components: [] // No special components }, 'Swarmer': { health: 30, speed: 80, gold_value: 2, lives_cost: 1, components: [] // No special components }, 'Armored': { health: 400, speed: 30, gold_value: 15, lives_cost: 2, components: [ // We will add the ArmorComponent definition later ] }, 'Flyer': { health: 80, speed: 60, gold_value: 8, lives_cost: 1, components: [ // We will add the FlyingComponent definition later ] } };
User prompt
Hello Ava. To ensure pathing remains correct after selling a tower, we need to update the PathManager. Please replace the entire Game.PathManager object with the following updated version. The only change is in the init function, where we add a subscription to the TowerSoldEvent. This new subscription re-uses the existing onTowerPlaced handler, as the required action (recalculating the path) is identical for both events. Game.PathManager = { currentPath: [], init: function init() { Game.EventBus.subscribe('TowerPlaced', this.onTowerPlaced.bind(this)); Game.EventBus.subscribe('TowerSold', this.onTowerPlaced.bind(this)); }, onTowerPlaced: function onTowerPlaced(event) { this.recalculatePath({ x: 0, y: 30 }, { x: 39, y: 30 }); }, recalculatePath: function recalculatePath(start, end) { this.currentPath = this.findPath(start, end); if (this.currentPath) { for (var i = 0; i < this.currentPath.length; i++) { this.currentPath[i].x = this.currentPath[i].x * 64; this.currentPath[i].y = this.currentPath[i].y * 64; } } }, findPath: function findPath(start, end) { var openSet = []; var closedSet = []; var grid = Game.GridManager.grid; var gridWidth = grid.length; var gridHeight = grid[0].length; function heuristic(a, b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); } function getNeighbors(node) { var neighbors = []; var directions = [{ x: -1, y: 0, cost: 1 }, { x: 1, y: 0, cost: 1 }, { x: 0, y: -1, cost: 1 }, { x: 0, y: 1, cost: 1 }, { x: -1, y: -1, cost: 1.4 }, { x: 1, y: -1, cost: 1.4 }, { x: -1, y: 1, cost: 1.4 }, { x: 1, y: 1, cost: 1.4 }]; for (var i = 0; i < directions.length; i++) { var dir = directions[i]; var newX = node.x + dir.x; var newY = node.y + dir.y; if (newX >= 0 && newX < gridWidth && newY >= 0 && newY < gridHeight) { var cellState = grid[newX][newY]; if (cellState !== Game.GridManager.CELL_STATES.BLOCKED && cellState !== Game.GridManager.CELL_STATES.TOWER) { neighbors.push({ x: newX, y: newY, cost: dir.cost }); } } } return neighbors; } function findNodeInArray(array, node) { for (var i = 0; i < array.length; i++) { if (array[i].x === node.x && array[i].y === node.y) { return array[i]; } } return null; } function reconstructPath(cameFrom, current) { var path = []; while (current) { path.unshift({ x: current.x, y: current.y }); current = cameFrom[current.x + '_' + current.y]; } return path; } var startNode = { x: start.x, y: start.y, g: 0, h: heuristic(start, end), f: 0 }; startNode.f = startNode.g + startNode.h; openSet.push(startNode); var cameFrom = {}; while (openSet.length > 0) { var current = openSet[0]; var currentIndex = 0; for (var i = 1; i < openSet.length; i++) { if (openSet[i].f < current.f) { current = openSet[i]; currentIndex = i; } } openSet.splice(currentIndex, 1); closedSet.push(current); if (current.x === end.x && current.y === end.y) { return reconstructPath(cameFrom, current); } var neighbors = getNeighbors(current); for (var i = 0; i < neighbors.length; i++) { var neighbor = neighbors[i]; if (findNodeInArray(closedSet, neighbor)) { continue; } var tentativeG = current.g + neighbor.cost; var existingNode = findNodeInArray(openSet, neighbor); if (!existingNode) { var newNode = { x: neighbor.x, y: neighbor.y, g: tentativeG, h: heuristic(neighbor, end), f: 0 }; newNode.f = newNode.g + newNode.h; openSet.push(newNode); cameFrom[neighbor.x + '_' + neighbor.y] = current; } else if (tentativeG < existingNode.g) { existingNode.g = tentativeG; existingNode.f = existingNode.g + existingNode.h; cameFrom[neighbor.x + '_' + neighbor.y] = current; } } } return null; // No path found } };
User prompt
Hello Ava. Now that the selling logic is in place, let's wire it up to the UI. First, find the main game loop (game.update = function ()). Inside the if (Game.GameplayManager.selectedTowerId !== -1) block, add the logic to update the sellButton's text to display the tower's sell_value. // Add this inside the "if (Game.GameplayManager.selectedTowerId !== -1)" block, after the existing logic sellButton.setText("Sell Tower ($" + towerComp.sell_value + ")"); Next, find the sellButton definition in the // === SECTION: UPGRADE & SELL UI === block and assign its down handler to call the new trySellTower function. Replace this: var sellButton = new Text2('Sell Tower', { size: 35, fill: 0xC0392B }); sellButton.anchor.set(0, 0); sellButton.y = 150; upgradePanel.addChild(sellButton);With this updated version that includes the .down handler: var sellButton = new Text2('Sell Tower', { size: 35, fill: 0xC0392B }); sellButton.anchor.set(0, 0); sellButton.y = 150; upgradePanel.addChild(sellButton); sellButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId); } };
User prompt
Please replace the entire Game.Systems.TowerSystem object with the following updated version, which adds the new trySellTower method. Game.Systems.TowerSystem = { tryUpgradeTower: function tryUpgradeTower(towerId, path) { // 1. Get the tower's type 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; } // 2. Get the upgrade data var upgradePathData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType][path] : null; if (!upgradePathData) { return; } var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; if (!towerComp) { return; } // 3. Determine the next upgrade level var nextUpgradeIndex = path === 'PathA' ? towerComp.upgrade_path_A_level : towerComp.upgrade_path_B_level; if (nextUpgradeIndex >= upgradePathData.length) { return; // Maxed out } var upgradeInfo = upgradePathData[nextUpgradeIndex]; // 4. Check gold if (!Game.ResourceManager.spendGold(upgradeInfo.cost)) { return; // Not enough gold } // 5. Apply stat effects 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]; } } } } // --- NEW: Handle adding new components/abilities --- // 6. Apply special "add component" effects if (upgradeInfo.add_component_on_impact) { var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; if (attackComp) { // Initialize the array if it doesn't exist if (!attackComp.on_impact_effects) { attackComp.on_impact_effects = []; } // Add the new effect to the tower's attack data attackComp.on_impact_effects.push(upgradeInfo.add_component_on_impact); } } // --- END NEW --- // 7. Update tower's level trackers towerComp.upgrade_level++; if (path === 'PathA') { towerComp.upgrade_path_A_level++; } else { towerComp.upgrade_path_B_level++; } }, trySellTower: function trySellTower(towerId) { // 1. Get the tower's components var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!towerComp || !transformComp) { return; // Can't sell if essential components are missing } // 2. Refund gold var refundAmount = towerComp.sell_value; Game.ResourceManager.addGold(refundAmount); // 3. Free up the grid cell var gridX = Math.floor(transformComp.x / 64); var gridY = Math.floor(transformComp.y / 64); if (Game.GridManager.grid[gridX] && Game.GridManager.grid[gridX][gridY] !== undefined) { Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.EMPTY; } // 4. Publish a sell event (for future use, e.g., path recalculation) Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount)); // 5. Deselect the tower and schedule it for destruction Game.GameplayManager.selectedTowerId = -1; Game.EntityManager.destroyEntity(towerId); } };
User prompt
Please replace the entire Game.Systems.CombatSystem object with the following final version. A new "Fire Zone Processing Logic" block has been added at the end of the update function. Game.Systems.CombatSystem = { update: function update() { var currentTime = LK.ticks / 60; // Convert ticks to seconds // --- Tower Firing Logic (No changes) --- 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; } } } // --- Projectile Movement and Impact Logic (No changes) --- 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']); 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; } } // --- Poison Debuff Processing Logic (No changes) --- 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]; } } // --- NEW: Fire Zone Processing Logic --- 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; // Decrement by frame time 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; // The fire zone sprite has a width/height of 80, so radius is 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; } targetHealth.current_hp -= damage; if (targetHealth.current_hp <= 0) { var enemyComp = Game.EntityManager.componentStores['EnemyComponent'][enemyId]; var goldValue = enemyComp ? enemyComp.gold_value : 0; Game.EventBus.publish(Game.Events.EnemyKilledEvent(enemyId, goldValue)); Game.EntityManager.destroyEntity(enemyId); } } };
User prompt
Please replace the entire Game.Systems.CombatSystem object with the following updated version. The new logic is added to the "on impact" section. Game.Systems.CombatSystem = { update: function update() { var currentTime = LK.ticks / 60; // Convert ticks to seconds // --- Tower Firing Logic (No changes here) --- 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; } } } // --- Projectile Movement and Impact Logic (UPDATED TO CREATE FIRE ZONES) --- 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) { // --- ON IMPACT --- var impactPoint = { x: targetTransform.x, y: targetTransform.y }; var sourceTowerAttack = Game.EntityManager.componentStores['AttackComponent'][projectileComp.source_tower_id]; // Standard splash/single-target damage if (sourceTowerAttack && sourceTowerAttack.splash_radius > 0) { var allEnemies = Game.EntityManager.getEntitiesWithComponents(['HealthComponent', 'TransformComponent']); 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); } // Apply debuffs to the primary target 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); } } // --- NEW: Create ground effects like Fire Zone --- 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 )); } // --- END NEW --- Game.EntityManager.destroyEntity(projectileId); } else { var moveX = dx / distance * projectileComp.speed; var moveY = dy / distance * projectileComp.speed; projectileTransform.x += moveX; projectileTransform.y += moveY; } } // --- Poison Debuff Processing Logic (No changes) --- 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]; } } }, dealDamage: function dealDamage(enemyId, damage) { var targetHealth = Game.EntityManager.componentStores['HealthComponent'][enemyId]; if (!targetHealth) { return; } targetHealth.current_hp -= damage; if (targetHealth.current_hp <= 0) { var enemyComp = Game.EntityManager.componentStores['EnemyComponent'][enemyId]; var goldValue = enemyComp ? enemyComp.gold_value : 0; Game.EventBus.publish(Game.Events.EnemyKilledEvent(enemyId, goldValue)); Game.EntityManager.destroyEntity(enemyId); } } };
User prompt
Please replace the entire Game.Systems.CombatSystem object with the following updated version. The primary change is in the tower firing logic, which now intelligently adds components to projectiles based on the tower's data. Game.Systems.CombatSystem = { update: function update() { var currentTime = LK.ticks / 60; // Convert ticks to seconds // --- Tower Firing Logic (UPDATED FOR GENERIC ON-IMPACT EFFECTS) --- 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); // --- REFACTORED/NEW LOGIC: Attach all on-impact effects from the tower --- 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); } } // This replaces the old hard-coded checks for chemical/slow projectiles // --- END REFACTORED/NEW LOGIC --- towerAttack.last_attack_time = currentTime; } } } // --- Projectile Movement and Impact Logic (No changes yet for fire zone) --- 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']); 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); } } Game.EntityManager.destroyEntity(projectileId); } else { var moveX = dx / distance * projectileComp.speed; var moveY = dy / distance * projectileComp.speed; projectileTransform.x += moveX; projectileTransform.y += moveY; } } // --- Poison Debuff Processing Logic (No changes) --- 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]; } } }, dealDamage: function dealDamage(enemyId, damage) { var targetHealth = Game.EntityManager.componentStores['HealthComponent'][enemyId]; if (!targetHealth) { return; } targetHealth.current_hp -= damage; if (targetHealth.current_hp <= 0) { var enemyComp = Game.EntityManager.componentStores['EnemyComponent'][enemyId]; var goldValue = enemyComp ? enemyComp.gold_value : 0; Game.EventBus.publish(Game.Events.EnemyKilledEvent(enemyId, goldValue)); Game.EntityManager.destroyEntity(enemyId); } } };
User prompt
replace the TowerSystem object with this new version. The tryUpgradeTower function is updated to handle the add_component_on_impact property by adding a new on_impact_effects array to the tower's AttackComponent. Game.Systems.TowerSystem = { tryUpgradeTower: function tryUpgradeTower(towerId, path) { // 1. Get the tower's type 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; } // 2. Get the upgrade data var upgradePathData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType][path] : null; if (!upgradePathData) { return; } var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; if (!towerComp) { return; } // 3. Determine the next upgrade level var nextUpgradeIndex = (path === 'PathA') ? towerComp.upgrade_path_A_level : towerComp.upgrade_path_B_level; if (nextUpgradeIndex >= upgradePathData.length) { return; // Maxed out } var upgradeInfo = upgradePathData[nextUpgradeIndex]; // 4. Check gold if (!Game.ResourceManager.spendGold(upgradeInfo.cost)) { return; // Not enough gold } // 5. Apply stat effects 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]; } } } } // --- NEW: Handle adding new components/abilities --- // 6. Apply special "add component" effects if (upgradeInfo.add_component_on_impact) { var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; if (attackComp) { // Initialize the array if it doesn't exist if (!attackComp.on_impact_effects) { attackComp.on_impact_effects = []; } // Add the new effect to the tower's attack data attackComp.on_impact_effects.push(upgradeInfo.add_component_on_impact); } } // --- END NEW --- // 7. Update tower's level trackers towerComp.upgrade_level++; if (path === 'PathA') { towerComp.upgrade_path_A_level++; } else { towerComp.upgrade_path_B_level++; } } }
/**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // === SECTION: ASSETS === // === SECTION: GLOBAL NAMESPACE === var Game = {}; // === SECTION: GLOBAL MANAGERS === Game.ResourceManager = { gold: 500, lives: 20, powerProduction: 0, powerConsumption: 0, powerAvailable: 0, addGold: function addGold(amount) { this.gold += amount; }, spendGold: function spendGold(amount) { if (this.gold >= amount) { this.gold -= amount; return true; } ; return false; }, loseLife: function loseLife(amount) { this.lives -= amount; if (this.lives <= 0) { Game.EventBus.publish(Game.Events.GameOverEvent()); } }, updatePower: function updatePower() { // 1. Calculate total power production var totalProduction = 0; var productionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerProductionComponent']); for (var i = 0; i < productionEntities.length; i++) { var entityId = productionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerProductionComponent'][entityId]; totalProduction += powerComp.production_rate; } this.powerProduction = totalProduction; // 2. Calculate total power consumption var totalConsumption = 0; var consumptionEntities = Game.EntityManager.getEntitiesWithComponents(['PowerConsumptionComponent']); for (var i = 0; i < consumptionEntities.length; i++) { var entityId = consumptionEntities[i]; var powerComp = Game.EntityManager.componentStores['PowerConsumptionComponent'][entityId]; totalConsumption += powerComp.drain_rate; } this.powerConsumption = totalConsumption; // 3. Calculate total storage capacity var totalCapacity = 0; var storageEntities = Game.EntityManager.getEntitiesWithComponents(['PowerStorageComponent']); for (var i = 0; i < storageEntities.length; i++) { var entityId = storageEntities[i]; var storageComp = Game.EntityManager.componentStores['PowerStorageComponent'][entityId]; totalCapacity += storageComp.capacity; } // 4. Evaluate power status and update available buffer var surplus = totalProduction - totalConsumption; if (surplus >= 0) { // We have enough or extra power this.powerAvailable += surplus; // Clamp the available power to the max capacity if (this.powerAvailable > totalCapacity) { this.powerAvailable = totalCapacity; } Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } else { // We have a deficit, drain from the buffer this.powerAvailable += surplus; // surplus is negative, so this subtracts if (this.powerAvailable < 0) { // Buffer is empty and we still have a deficit this.powerAvailable = 0; Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('deficit')); } else { // Deficit was covered by the buffer Game.EventBus.publish(Game.Events.PowerStatusChangedEvent('surplus')); } } }, onEnemyKilled: function onEnemyKilled(event) { this.addGold(event.gold_value); }, onWaveCompleted: function onWaveCompleted(event) { this.addGold(50); }, init: function init() { Game.EventBus.subscribe('EnemyKilled', this.onEnemyKilled.bind(this)); Game.EventBus.subscribe('WaveCompleted', this.onWaveCompleted.bind(this)); } }; Game.GridManager = { grid: [], CELL_STATES: { EMPTY: 0, PATH: 1, BLOCKED: 2, TOWER: 3 }, init: function init(width, height) { this.grid = []; for (var x = 0; x < width; x++) { this.grid[x] = []; for (var y = 0; y < height; y++) { this.grid[x][y] = this.CELL_STATES.EMPTY; } } }, isBuildable: function isBuildable(x, y) { if (x >= 0 && x < this.grid.length && y >= 0 && y < this.grid[x].length) { return this.grid[x][y] === this.CELL_STATES.EMPTY; } return false; } }; Game.PathManager = { currentPath: [], init: function init() { Game.EventBus.subscribe('TowerPlaced', this.onTowerPlaced.bind(this)); Game.EventBus.subscribe('TowerSold', this.onTowerPlaced.bind(this)); }, onTowerPlaced: function onTowerPlaced(event) { this.recalculatePath({ x: 0, y: 30 }, { x: 39, y: 30 }); }, recalculatePath: function recalculatePath(start, end) { this.currentPath = this.findPath(start, end); if (this.currentPath) { for (var i = 0; i < this.currentPath.length; i++) { this.currentPath[i].x = this.currentPath[i].x * 64; this.currentPath[i].y = this.currentPath[i].y * 64; } } }, findPath: function findPath(start, end) { var openSet = []; var closedSet = []; var grid = Game.GridManager.grid; var gridWidth = grid.length; var gridHeight = grid[0].length; function heuristic(a, b) { return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); } function getNeighbors(node) { var neighbors = []; var directions = [{ x: -1, y: 0, cost: 1 }, { x: 1, y: 0, cost: 1 }, { x: 0, y: -1, cost: 1 }, { x: 0, y: 1, cost: 1 }, { x: -1, y: -1, cost: 1.4 }, { x: 1, y: -1, cost: 1.4 }, { x: -1, y: 1, cost: 1.4 }, { x: 1, y: 1, cost: 1.4 }]; for (var i = 0; i < directions.length; i++) { var dir = directions[i]; var newX = node.x + dir.x; var newY = node.y + dir.y; if (newX >= 0 && newX < gridWidth && newY >= 0 && newY < gridHeight) { var cellState = grid[newX][newY]; if (cellState !== Game.GridManager.CELL_STATES.BLOCKED && cellState !== Game.GridManager.CELL_STATES.TOWER) { neighbors.push({ x: newX, y: newY, cost: dir.cost }); } } } return neighbors; } function findNodeInArray(array, node) { for (var i = 0; i < array.length; i++) { if (array[i].x === node.x && array[i].y === node.y) { return array[i]; } } return null; } function reconstructPath(cameFrom, current) { var path = []; while (current) { path.unshift({ x: current.x, y: current.y }); current = cameFrom[current.x + '_' + current.y]; } return path; } var startNode = { x: start.x, y: start.y, g: 0, h: heuristic(start, end), f: 0 }; startNode.f = startNode.g + startNode.h; openSet.push(startNode); var cameFrom = {}; while (openSet.length > 0) { var current = openSet[0]; var currentIndex = 0; for (var i = 1; i < openSet.length; i++) { if (openSet[i].f < current.f) { current = openSet[i]; currentIndex = i; } } openSet.splice(currentIndex, 1); closedSet.push(current); if (current.x === end.x && current.y === end.y) { return reconstructPath(cameFrom, current); } var neighbors = getNeighbors(current); for (var i = 0; i < neighbors.length; i++) { var neighbor = neighbors[i]; if (findNodeInArray(closedSet, neighbor)) { continue; } var tentativeG = current.g + neighbor.cost; var existingNode = findNodeInArray(openSet, neighbor); if (!existingNode) { var newNode = { x: neighbor.x, y: neighbor.y, g: tentativeG, h: heuristic(neighbor, end), f: 0 }; newNode.f = newNode.g + newNode.h; openSet.push(newNode); cameFrom[neighbor.x + '_' + neighbor.y] = current; } else if (tentativeG < existingNode.g) { existingNode.g = tentativeG; existingNode.f = existingNode.g + existingNode.h; cameFrom[neighbor.x + '_' + neighbor.y] = current; } } } return null; // No path found } }; Game.GameplayManager = { selectedTowerId: -1, // NEW: To track the currently selected tower enemiesSpawnedThisWave: 0, enemiesKilledThisWave: 0, enemiesLeakedThisWave: 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.enemiesSpawnedThisWave) { Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1)); this.enemiesSpawnedThisWave = 0; 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.enemiesSpawnedThisWave) { Game.EventBus.publish(Game.Events.WaveCompletedEvent(Game.Systems.WaveSpawnerSystem.currentWaveIndex + 1)); this.enemiesSpawnedThisWave = 0; 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, path_index) { return { name: 'MovementComponent', speed: speed || 50, path_index: path_index || 0 }; }, 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) { return { name: 'TowerComponent', cost: cost || 50, sell_value: sellValue || 25, upgrade_level: 1, upgrade_path_A_level: 0, upgrade_path_B_level: 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 for (var i = 0; i < movableEntities.length; i++) { var entityId = movableEntities[i]; var transformComponent = Game.EntityManager.componentStores['TransformComponent'][entityId]; var movementComponent = Game.EntityManager.componentStores['MovementComponent'][entityId]; // 1. Get base speed and apply debuffs var currentSpeed = movementComponent.speed; var slowDebuff = Game.EntityManager.componentStores['SlowDebuffComponent'] ? Game.EntityManager.componentStores['SlowDebuffComponent'][entityId] : null; if (slowDebuff) { currentSpeed = movementComponent.speed * (1 - slowDebuff.slow_percentage); slowDebuff.duration_remaining -= frame_time; if (slowDebuff.duration_remaining <= 0) { delete Game.EntityManager.componentStores['SlowDebuffComponent'][entityId]; } } // --- START OF NEW FLYING/GROUND LOGIC --- // 2. Check if the entity is a flyer var isFlyer = Game.EntityManager.componentStores['FlyingComponent'] && Game.EntityManager.componentStores['FlyingComponent'][entityId]; if (isFlyer) { // Flying Movement: Move in a straight line to the exit var exitPoint = { x: 39 * 64, y: 30 * 64 }; var dx = exitPoint.x - transformComponent.x; var dy = exitPoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); var moveDistance = currentSpeed * frame_time; if (distance > moveDistance) { transformComponent.x += dx / distance * moveDistance; transformComponent.y += dy / distance * moveDistance; } else { // Flyer reached the end Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } else { // Ground Movement: Follow the path if (Game.PathManager.currentPath && Game.PathManager.currentPath.length > 0) { var targetWaypoint = Game.PathManager.currentPath[movementComponent.path_index]; if (targetWaypoint) { var dx = targetWaypoint.x - transformComponent.x; var dy = targetWaypoint.y - transformComponent.y; var distance = Math.sqrt(dx * dx + dy * dy); var moveDistance = currentSpeed * frame_time; if (distance > moveDistance) { transformComponent.x += dx / distance * moveDistance; transformComponent.y += dy / distance * moveDistance; } else { transformComponent.x = targetWaypoint.x; transformComponent.y = targetWaypoint.y; movementComponent.path_index++; if (movementComponent.path_index >= Game.PathManager.currentPath.length) { Game.EventBus.publish(Game.Events.EnemyReachedEndEvent(entityId)); } } } } } // --- END OF NEW FLYING/GROUND 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]; // Get the visual sprite, or create it if it doesn't exist var sprite = this.displayObjects[entityId]; if (!sprite) { sprite = game.attachAsset(render.sprite_id, {}); this.displayObjects[entityId] = sprite; } // Sync the sprite's properties with the component data sprite.x = transform.x; sprite.y = transform.y; sprite.visible = render.is_visible; } } }, TowerBuildSystem: { tryBuildAt: function tryBuildAt(gridX, gridY, towerType) { // 0. Look up tower data var towerData = Game.TowerData[towerType]; if (!towerData) { return; // Exit if tower type is invalid } // 1. Check if the location is buildable if (!Game.GridManager.isBuildable(gridX, gridY)) { return; // Exit if not buildable } // 2. Check if the player can afford the tower if (!Game.ResourceManager.spendGold(towerData.cost)) { return; // Exit if not enough gold } // 3. If checks pass, create the tower entity var towerId = Game.EntityManager.createEntity(); // 4. Mark the grid cell as occupied by a tower Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.TOWER; // 5. Add universal components that all towers have var transform = Game.Components.TransformComponent(gridX * 64, gridY * 64); Game.EntityManager.addComponent(towerId, transform); var render = Game.Components.RenderComponent(towerData.sprite_id, 2, 1.0, true); Game.EntityManager.addComponent(towerId, render); // 6. Add specific components from the tower's data definition for (var i = 0; i < towerData.components.length; i++) { var componentDef = towerData.components[i]; // Create the component by calling its factory function with the specified arguments var component = Game.Components[componentDef.name].apply(null, componentDef.args); Game.EntityManager.addComponent(towerId, component); } // 7. Announce that a tower has been placed var event = Game.Events.TowerPlacedEvent(towerId, towerData.cost, transform.x, transform.y); Game.EventBus.publish(event); } }, TargetingSystem: { update: function 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]; // Find best target (closest to end of path) 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 enemyMovement = Game.EntityManager.componentStores['MovementComponent'][enemyId]; // Check if enemy is within range 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 this enemy is closer to the end if (enemyMovement.path_index > bestPathIndex) { bestTarget = enemyId; bestPathIndex = enemyMovement.path_index; } } } // Update tower's target towerAttack.target_id = bestTarget; } } }, CleanupSystem: { update: function update() { for (var i = 0; i < Game.EntityManager.entitiesToDestroy.length; i++) { var entityId = Game.EntityManager.entitiesToDestroy[i]; // Remove from active entities list var index = Game.EntityManager.activeEntities.indexOf(entityId); if (index > -1) { Game.EntityManager.activeEntities.splice(index, 1); } // Remove from all component stores for (var componentName in Game.EntityManager.componentStores) { if (Game.EntityManager.componentStores[componentName][entityId]) { delete Game.EntityManager.componentStores[componentName][entityId]; } } // Remove from render system's display objects if (Game.Systems.RenderSystem.displayObjects[entityId]) { Game.Systems.RenderSystem.displayObjects[entityId].destroy(); delete Game.Systems.RenderSystem.displayObjects[entityId]; } } // Important: Clear the list for the next frame 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++; this.isSpawning = true; this.spawnTimer = 0; this.subWaveIndex = 0; this.spawnedInSubWave = 0; }, 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]; // Handle start delay if (this.spawnedInSubWave === 0 && this.spawnTimer < currentSubWave.start_delay) { this.spawnTimer += 1 / 60; // Increment by frame time return; } // Handle spawn delay between enemies if (this.spawnedInSubWave > 0 && this.spawnTimer < currentSubWave.spawn_delay) { this.spawnTimer += 1 / 60; return; } // --- START OF NEW SPAWNING LOGIC --- if (this.spawnedInSubWave < currentSubWave.count) { // 1. Look up the enemy data based on the type from the wave definition var enemyData = Game.EnemyData[currentSubWave.enemy_type]; if (!enemyData) { // Skip this sub-wave if the enemy type is invalid this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; return; } // 2. Create the enemy entity var enemyId = Game.EntityManager.createEntity(); // 3. Add components using the data from Game.EnemyData var transform = Game.Components.TransformComponent(0 * 64, 30 * 64); var render = Game.Components.RenderComponent('enemy', 1, 1.0, true); // Still uses a generic sprite for now var movement = Game.Components.MovementComponent(enemyData.speed, 0); 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); // 4. Add any special components defined in the enemy data 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); } } // 5. Update spawner state this.spawnedInSubWave++; this.spawnTimer = 0; Game.GameplayManager.enemiesSpawnedThisWave++; } else { // Move to next sub-wave this.subWaveIndex++; this.spawnedInSubWave = 0; this.spawnTimer = 0; } // --- END OF NEW SPAWNING LOGIC --- } }, CombatSystem: { update: function update() { var currentTime = LK.ticks / 60; // Convert ticks to seconds // --- Tower Firing Logic (No changes) --- 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; } } } // --- Projectile Movement and Impact Logic (No changes) --- 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']); 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; } } // --- Poison Debuff Processing Logic (No changes) --- 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]; } } // --- NEW: Fire Zone Processing Logic --- 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; // Decrement by frame time 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; // The fire zone sprite has a width/height of 80, so radius is 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; } targetHealth.current_hp -= damage; if (targetHealth.current_hp <= 0) { var enemyComp = Game.EntityManager.componentStores['EnemyComponent'][enemyId]; var goldValue = enemyComp ? enemyComp.gold_value : 0; Game.EventBus.publish(Game.Events.EnemyKilledEvent(enemyId, goldValue)); Game.EntityManager.destroyEntity(enemyId); } } }, AuraSystem: { update: function update() { // Step 1: Clean up all existing damage buffs from the previous frame. var buffedTowers = Game.EntityManager.getEntitiesWithComponents(['DamageBuffComponent']); for (var i = 0; i < buffedTowers.length; i++) { var towerId = buffedTowers[i]; delete Game.EntityManager.componentStores['DamageBuffComponent'][towerId]; } // Step 2: Find all aura-providing towers. var auraTowers = Game.EntityManager.getEntitiesWithComponents(['AuraComponent', 'TransformComponent']); if (auraTowers.length === 0) { return; // No auras to process. } // Step 3: Get all attack towers that could potentially be buffed. var attackTowers = Game.EntityManager.getEntitiesWithComponents(['AttackComponent', 'TransformComponent']); // Step 4: Iterate through each aura and apply its effect to nearby towers. 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]; // Only process damage buff auras for now if (auraComponent.effect_type !== 'damage_buff') { continue; } for (var j = 0; j < attackTowers.length; j++) { var attackTowerId = attackTowers[j]; // An aura tower cannot buff itself. if (attackTowerId === auraTowerId) { continue; } var attackTransform = Game.EntityManager.componentStores['TransformComponent'][attackTowerId]; // Check distance var dx = auraTransform.x - attackTransform.x; var dy = auraTransform.y - attackTransform.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= auraComponent.range) { // Apply the buff var buff = Game.Components.DamageBuffComponent(0.2, auraTowerId); // 20% damage bonus Game.EntityManager.addComponent(attackTowerId, buff); } } } } }, EconomySystem: { update: function update() { // Find all towers that generate gold var goldGenerators = Game.EntityManager.getEntitiesWithComponents(['GoldGenerationComponent']); // Sum up the gold from all of them for (var i = 0; i < goldGenerators.length; i++) { var entityId = goldGenerators[i]; var goldGenComp = Game.EntityManager.componentStores['GoldGenerationComponent'][entityId]; if (goldGenComp) { // Add the gold to the player's resources. // Since this runs once per second, we add the gold_per_second value directly. Game.ResourceManager.addGold(goldGenComp.gold_per_second); } } } }, TowerSystem: { tryUpgradeTower: function tryUpgradeTower(towerId, path) { // 1. Get the tower's type 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; } // 2. Get the upgrade data var upgradePathData = Game.UpgradeData[towerType] ? Game.UpgradeData[towerType][path] : null; if (!upgradePathData) { return; } var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; if (!towerComp) { return; } // 3. Determine the next upgrade level var nextUpgradeIndex = path === 'PathA' ? towerComp.upgrade_path_A_level : towerComp.upgrade_path_B_level; if (nextUpgradeIndex >= upgradePathData.length) { return; // Maxed out } var upgradeInfo = upgradePathData[nextUpgradeIndex]; // 4. Check gold if (!Game.ResourceManager.spendGold(upgradeInfo.cost)) { return; // Not enough gold } // 5. Apply stat effects 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]; } } } } // --- NEW: Handle adding new components/abilities --- // 6. Apply special "add component" effects if (upgradeInfo.add_component_on_impact) { var attackComp = Game.EntityManager.componentStores['AttackComponent'][towerId]; if (attackComp) { // Initialize the array if it doesn't exist if (!attackComp.on_impact_effects) { attackComp.on_impact_effects = []; } // Add the new effect to the tower's attack data attackComp.on_impact_effects.push(upgradeInfo.add_component_on_impact); } } // --- END NEW --- // 7. Update tower's level trackers towerComp.upgrade_level++; if (path === 'PathA') { towerComp.upgrade_path_A_level++; } else { towerComp.upgrade_path_B_level++; } } }, trySellTower: function trySellTower(towerId) { // 1. Get the tower's components var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; var transformComp = Game.EntityManager.componentStores['TransformComponent'][towerId]; if (!towerComp || !transformComp) { return; // Can't sell if essential components are missing } // 2. Refund gold var refundAmount = towerComp.sell_value; Game.ResourceManager.addGold(refundAmount); // 3. Free up the grid cell var gridX = Math.floor(transformComp.x / 64); var gridY = Math.floor(transformComp.y / 64); if (Game.GridManager.grid[gridX] && Game.GridManager.grid[gridX][gridY] !== undefined) { Game.GridManager.grid[gridX][gridY] = Game.GridManager.CELL_STATES.EMPTY; } // 4. Publish a sell event (for future use, e.g., path recalculation) Game.EventBus.publish(Game.Events.TowerSoldEvent(towerId, refundAmount)); // 5. Deselect the tower and schedule it for destruction Game.GameplayManager.selectedTowerId = -1; Game.EntityManager.destroyEntity(towerId); } }; // === SECTION: GAME DATA === Game.EnemyData = { 'Grunt': { health: 100, speed: 50, gold_value: 5, lives_cost: 1, components: [] // No special components }, 'Swarmer': { health: 30, speed: 80, gold_value: 2, lives_cost: 1, components: [] // No special components }, 'Armored': { health: 400, speed: 30, gold_value: 15, lives_cost: 2, components: [ // We will add the ArmorComponent definition later ] }, 'Flyer': { health: 80, speed: 60, gold_value: 8, lives_cost: 1, 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 }] }]; // === SECTION: TOWER DATA === Game.TowerData = { 'ArrowTower': { cost: 50, sprite_id: 'arrow_tower', components: [{ name: 'TowerComponent', args: [50] }, // cost { name: 'AttackComponent', args: [10, 150, 1.0, 0, 'arrow_projectile'] }, // dmg, range, speed, last_attack_time, projectile_id { name: 'PowerConsumptionComponent', args: [2] } // drain_rate ] }, 'GeneratorTower': { cost: 75, sprite_id: 'generator_tower', components: [{ name: 'TowerComponent', args: [75] }, // cost { name: 'PowerProductionComponent', args: [5] } // production_rate ] }, 'CapacitorTower': { cost: 40, sprite_id: 'capacitor_tower', components: [{ name: 'TowerComponent', args: [40] }, // cost { name: 'PowerStorageComponent', args: [20] } // capacity ] }, 'RockLauncher': { cost: 125, sprite_id: 'rock_launcher_tower', components: [{ name: 'TowerComponent', args: [125] }, { name: 'AttackComponent', args: [25, 200, 0.5, 0, 'rock_projectile', -1, 50] }, // dmg, range, speed, last_attack_time, projectile_id, target_id, splash_radius { name: 'PowerConsumptionComponent', args: [10] }] }, 'ChemicalTower': { cost: 100, sprite_id: 'chemical_tower', components: [{ name: 'TowerComponent', args: [100] }, { name: 'AttackComponent', args: [5, 150, 0.8, 0, 'chemical_projectile', -1, 0] }, // dmg, range, speed, last_attack_time, projectile_id, target_id, splash_radius { name: 'PowerConsumptionComponent', args: [8] }] }, 'SlowTower': { cost: 80, sprite_id: 'slow_tower', components: [{ name: 'TowerComponent', args: [80] }, { name: 'AttackComponent', args: [2, 160, 1.0, 0, 'slow_projectile'] // Low damage, good range, standard speed }, { name: 'PowerConsumptionComponent', args: [4] }] }, 'DarkTower': { cost: 150, sprite_id: 'dark_tower', components: [{ name: 'TowerComponent', args: [150] }, { name: 'AuraComponent', args: [175, 'damage_buff'] // range, effect_type }, { name: 'PowerConsumptionComponent', args: [12] }] }, // --- NEW TOWER --- 'GraveyardTower': { cost: 200, sprite_id: 'graveyard_tower', components: [{ name: 'TowerComponent', args: [200] }, { name: 'GoldGenerationComponent', args: [1.5] // gold_per_second }, { name: 'PowerConsumptionComponent', args: [15] // High power cost for economic advantage }] } }; // === SECTION: UPGRADE DATA === Game.UpgradeData = { 'ArrowTower': { 'PathA': [{ cost: 60, effects: { AttackComponent: { damage: 5 } } }, //{7R} // Level 1: +5 Dmg { cost: 110, effects: { AttackComponent: { damage: 10 } } } //{7W} // Level 2: +10 Dmg ], 'PathB': [{ cost: 75, effects: { AttackComponent: { attack_speed: 0.2 } } }, //{83} // Level 1: +0.2 AS { cost: 125, effects: { AttackComponent: { attack_speed: 0.3 } } } //{8a} // Level 2: +0.3 AS ] }, 'RockLauncher': { 'PathA': [ // Heavy Boulders - More Damage & Range { cost: 150, effects: { AttackComponent: { damage: 15, range: 20 } } }, { cost: 250, effects: { AttackComponent: { damage: 25, range: 30 } } }, // --- UPDATED UPGRADE --- { cost: 500, effects: { AttackComponent: { damage: 50, range: 50 } }, add_component_on_impact: { name: 'FireZoneOnImpactComponent', args: [10, 3, 0.5] } // dmg, duration, tick_rate }], 'PathB': [ // Lighter Munitions - Faster Attack Speed { cost: 140, effects: { AttackComponent: { damage: 5, attack_speed: 0.2 } } }, { cost: 220, effects: { AttackComponent: { damage: 10, attack_speed: 0.3 } } }, { cost: 450, effects: { AttackComponent: { damage: 15, attack_speed: 0.5 } } }] } }; // === SECTION: GRID DRAWING === 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 = game.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; // --- UPDATED CLICK LOGIC --- cellSprite.down = function () { var gridX = this.gridX; var gridY = this.gridY; var cellState = Game.GridManager.grid[gridX][gridY]; if (cellState === Game.GridManager.CELL_STATES.TOWER) { // Logic to SELECT a tower // Find the entity ID of the tower at this grid location var allTowers = Game.EntityManager.getEntitiesWithComponents(['TowerComponent', 'TransformComponent']); var foundTowerId = -1; for (var i = 0; i < allTowers.length; i++) { var towerId = allTowers[i]; var transform = Game.EntityManager.componentStores['TransformComponent'][towerId]; // Convert pixel coordinates back to grid coordinates to find a match var towerGridX = Math.floor(transform.x / 64); var towerGridY = Math.floor(transform.y / 64); if (towerGridX === gridX && towerGridY === gridY) { foundTowerId = towerId; break; } } Game.GameplayManager.selectedTowerId = foundTowerId; } else if (cellState === Game.GridManager.CELL_STATES.EMPTY) { // Logic to BUILD a tower Game.Systems.TowerBuildSystem.tryBuildAt(gridX, gridY, buildMode); Game.GameplayManager.selectedTowerId = -1; // Deselect after building } else { // Clicked on a path or blocked cell, so DESELECT Game.GameplayManager.selectedTowerId = -1; } }; // --- END OF UPDATED CLICK LOGIC --- } } // 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 = game.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.ResourceManager.init(); Game.PathManager.init(); Game.PathManager.recalculatePath({ x: 0, y: 30 }, { x: 39, y: 30 }); 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 // Create Start Wave button var startWaveButton = new Text2('START NEXT WAVE', { size: 50, fill: 0x00FF00 }); startWaveButton.anchor.set(0.5, 1); LK.gui.bottom.addChild(startWaveButton); startWaveButton.y = -20; startWaveButton.visible = false; startWaveButton.down = function () { Game.Systems.WaveSpawnerSystem.startNextWave(); startWaveButton.visible = false; }; // Initialize GameplayManager Game.GameplayManager.init(); // Initialize GameManager Game.GameManager.init(); // Start first wave Game.Systems.WaveSpawnerSystem.startNextWave(); // === SECTION: TEMP BUILD UI === var buildMode = 'ArrowTower'; // Keep this variable var arrowButton = new Text2('Build: Arrow', { size: 40, fill: 0x2ecc71 }); arrowButton.anchor.set(0, 0.5); LK.gui.left.addChild(arrowButton); arrowButton.x = 20; arrowButton.y = -350; arrowButton.down = function () { buildMode = 'ArrowTower'; }; var generatorButton = new Text2('Build: Generator', { size: 40, fill: 0x3498db }); generatorButton.anchor.set(0, 0.5); LK.gui.left.addChild(generatorButton); generatorButton.x = 20; generatorButton.y = -280; generatorButton.down = function () { buildMode = 'GeneratorTower'; }; var capacitorButton = new Text2('Build: Capacitor', { size: 40, fill: 0xf1c40f }); capacitorButton.anchor.set(0, 0.5); LK.gui.left.addChild(capacitorButton); capacitorButton.x = 20; capacitorButton.y = -210; capacitorButton.down = function () { buildMode = 'CapacitorTower'; }; var rockLauncherButton = new Text2('Build: Rock Launcher', { size: 40, fill: 0x8D6E63 }); rockLauncherButton.anchor.set(0, 0.5); LK.gui.left.addChild(rockLauncherButton); rockLauncherButton.x = 20; rockLauncherButton.y = -140; rockLauncherButton.down = function () { buildMode = 'RockLauncher'; }; var chemicalTowerButton = new Text2('Build: Chemical', { size: 40, fill: 0xb87333 }); chemicalTowerButton.anchor.set(0, 0.5); LK.gui.left.addChild(chemicalTowerButton); chemicalTowerButton.x = 20; chemicalTowerButton.y = -70; chemicalTowerButton.down = function () { buildMode = 'ChemicalTower'; }; var slowTowerButton = new Text2('Build: Slow', { size: 40, fill: 0x5DADE2 }); slowTowerButton.anchor.set(0, 0.5); LK.gui.left.addChild(slowTowerButton); slowTowerButton.x = 20; slowTowerButton.y = 0; slowTowerButton.down = function () { buildMode = 'SlowTower'; }; var darkTowerButton = new Text2('Build: Dark', { size: 40, fill: 0x9b59b6 }); darkTowerButton.anchor.set(0, 0.5); LK.gui.left.addChild(darkTowerButton); darkTowerButton.x = 20; darkTowerButton.y = 70; darkTowerButton.down = function () { buildMode = 'DarkTower'; }; // --- NEW BUTTON --- var graveyardTowerButton = new Text2('Build: Graveyard', { size: 40, fill: 0x607d8b }); graveyardTowerButton.anchor.set(0, 0.5); LK.gui.left.addChild(graveyardTowerButton); graveyardTowerButton.x = 20; graveyardTowerButton.y = 140; graveyardTowerButton.down = function () { buildMode = 'GraveyardTower'; }; // Set up main game loop var powerUpdateTimer = 0; game.update = function () { if (Game.GameManager.isGameOver) { return; } Game.Systems.WaveSpawnerSystem.update(); Game.Systems.TargetingSystem.update(); Game.Systems.AuraSystem.update(); Game.Systems.CombatSystem.update(); Game.Systems.MovementSystem.update(); Game.Systems.RenderSystem.update(); // --- UI UPDATE LOGIC --- if (Game.GameplayManager.selectedTowerId !== -1) { upgradePanel.visible = true; // --- START OF LOGIC --- var towerId = Game.GameplayManager.selectedTowerId; var renderComp = Game.EntityManager.componentStores['RenderComponent'][towerId]; var towerComp = Game.EntityManager.componentStores['TowerComponent'][towerId]; if (renderComp && towerComp) { // 1. Find the tower's type name var towerType = null; for (var type in Game.TowerData) { if (Game.TowerData[type].sprite_id === renderComp.sprite_id) { towerType = type; break; } } // 2. Update the main tower name text towerNameText.setText(towerType + " (Lvl " + towerComp.upgrade_level + ")"); // 3. Update Path A button text 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]; // --- UPDATED TEXT --- upgradeAPathButton.setText("Upgrade Path A ($" + upgradeInfoA.cost + ")"); upgradeAPathButton.visible = true; } else { upgradeAPathButton.visible = false; // Hide if path is maxed } // 4. Update Path B button text 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]; // --- UPDATED TEXT --- upgradeBPathButton.setText("Upgrade Path B ($" + upgradeInfoB.cost + ")"); upgradeBPathButton.visible = true; } else { upgradeBPathButton.visible = false; // Hide if path is maxed } // 5. Update sell button text sellButton.setText("Sell Tower ($" + towerComp.sell_value + ")"); } // --- END OF LOGIC --- } else { upgradePanel.visible = false; } // Update HUD every frame goldText.setText('Gold: ' + Game.ResourceManager.gold); livesText.setText('Lives: ' + Game.ResourceManager.lives); // Update timed systems once per second (every 60 frames) powerUpdateTimer++; if (powerUpdateTimer >= 60) { Game.ResourceManager.updatePower(); if (Game.Systems.WaveSpawnerSystem.isSpawning) { Game.Systems.EconomySystem.update(); } powerUpdateTimer = 0; } Game.Systems.CleanupSystem.update(); }; // === SECTION: UPGRADE & SELL UI === var upgradePanel = new Container(); LK.gui.bottom.addChild(upgradePanel); upgradePanel.x = -300; // Positioned to the right of center upgradePanel.y = -200; // Adjusted from -50 to bring the panel fully on-screen upgradePanel.visible = false; // Initially hidden var towerNameText = new Text2('Tower Name', { size: 40, fill: 0xFFFFFF }); towerNameText.anchor.set(0, 0); upgradePanel.addChild(towerNameText); var upgradeAPathButton = new Text2('Upgrade Path A', { size: 35, fill: 0x27AE60 }); upgradeAPathButton.anchor.set(0, 0); upgradeAPathButton.y = 50; upgradePanel.addChild(upgradeAPathButton); var upgradeBPathButton = new Text2('Upgrade Path B', { size: 35, fill: 0x2980B9 }); upgradeBPathButton.anchor.set(0, 0); upgradeBPathButton.y = 100; upgradePanel.addChild(upgradeBPathButton); var sellButton = new Text2('Sell Tower', { size: 35, fill: 0xC0392B }); sellButton.anchor.set(0, 0); sellButton.y = 150; upgradePanel.addChild(sellButton); sellButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.trySellTower(Game.GameplayManager.selectedTowerId); } }; // Assign click handlers that will call the TowerSystem upgradeAPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathA'); } }; upgradeBPathButton.down = function () { if (Game.GameplayManager.selectedTowerId !== -1) { Game.Systems.TowerSystem.tryUpgradeTower(Game.GameplayManager.selectedTowerId, 'PathB'); } };
===================================================================
--- original.js
+++ change.js
@@ -1757,9 +1757,9 @@
// === SECTION: UPGRADE & SELL UI ===
var upgradePanel = new Container();
LK.gui.bottom.addChild(upgradePanel);
upgradePanel.x = -300; // Positioned to the right of center
-upgradePanel.y = -50;
+upgradePanel.y = -200; // Adjusted from -50 to bring the panel fully on-screen
upgradePanel.visible = false; // Initially hidden
var towerNameText = new Text2('Tower Name', {
size: 40,
fill: 0xFFFFFF