User prompt
The reward will appear a little further away from the character's location.
User prompt
Let the bullet spin according to the direction it goes βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add nine more new enemy types
User prompt
oyun devam ederken sanki bir melodi koy
User prompt
Make the green object available for pickup and increase the bullet size by 3 percent and the damage radius by 1 percent
User prompt
bullet speed 50
User prompt
set the physical diameter of the character to the image
User prompt
add character redirection feature
User prompt
add redirection by clicking on character
User prompt
bullet speed 20
User prompt
bullet speed 5
User prompt
bullet speed 1.5
Code edit (1 edits merged)
Please save this source code
User prompt
Castle Defense: Auto-Fire Tower
Initial prompt
The AI needs to be able to monitor and track the following for its decision-making and game simulation: Player State: Player_Health: Integer. Represents the castle's current health. Game ends when this reaches 0. Current_Bullets_Per_Shot: Integer. The number of projectiles fired in a single shot. Starts at 1. Increases by 1 for each collected Reward_Object. Time_Since_Last_Shot: Float. Tracks the time elapsed since the player's last shot. Used to determine when the next shot can be fired based on the Firing_Rate. Player_Score: Integer. Optional, for tracking performance. Enemy State (List of Enemy Objects): Each enemy object will have: Enemy_ID: Unique identifier. Enemy_Type: String/Enum (e.g., "Basic_Grunt", "Fast_Scout", "Heavy_Armored"). Different types may have different Health, Speed, and Damage_Dealt. Enemy_Health: Integer. Current health of the individual enemy. Enemy_Position: (X, Y) Coordinates. Represents the enemy's current location on the game map. Enemy_Speed: Float. How many units per second the enemy moves. Distance_To_Castle: Float. Calculated distance from the enemy's current position to the castle's fixed position. This is crucial for target prioritization. Reward Object State (List of Reward Objects): Each reward object will have: Reward_ID: Unique identifier. Reward_Position: (X, Y) Coordinates. Current location on the game map. Time_To_Despawn: Float. If rewards have a limited lifespan. Global Game State: Game_Time: Float. Total time elapsed since the start of the round. Wave_Number: Integer. Current wave of enemies. Increases as waves are cleared. Next_Enemy_Spawn_Time: Float. Time at which the next enemy will spawn. Next_Reward_Spawn_Time: Float. Time at which the next reward will spawn. Castle_Position: (X, Y) Coordinates. Fixed position of the castle on the map. Map_Boundaries: (Min_X, Max_X, Min_Y, Max_Y). Defines the playable area. II. Game Mechanics (Rules for AI Simulation): The AI needs to understand how the game evolves and how its actions affect the state. Spawning: Enemies: Spawn at a designated Spawn_Point (e.g., top of the screen/map). Spawn Gradually_and_Randomly: This implies a Spawn_Rate (e.g., every X seconds) and a Random_Enemy_Type_Selection (from a predefined pool of enemy types). The Spawn_Rate can decrease and the variety/strength of enemies can increase with Wave_Number. Movement: Enemies move towards the Castle_Position at their Enemy_Speed. Rewards: Spawn In_Between enemy spawns, also Randomly in terms of position within a designated area. Spawn Reward_Rate: Defines how often rewards appear. Movement: Rewards do not move unless specified (e.g., they might float down slowly). They may have a Time_To_Despawn. Player Actions (or lack thereof, since it's automated): Shooting: Automatic_Targeting: The player (castle) does not need to aim. Firing_Rate: The castle fires automatically at a fixed interval (e.g., every 1 second). This is a constant Time_Between_Shots. Projectile_Spread: When Current_Bullets_Per_Shot is greater than 1, the bullets are dispersed around the automatically selected target. This could be: Cone Spread: Bullets fire within an angular cone centered on the primary target. Random Spread: Bullets deviate randomly by a certain radius from the primary target. Closest Target Spread: Each additional bullet targets the next closest enemy within a certain range of the primary target. (This is more complex and usually explicitly stated). For simplicity, assume a cone spread for initial setup. Bullet_Damage: Each individual bullet deals a fixed amount of damage (e.g., 1 damage per bullet). Bullet_Travel_Time: Bullets have a speed and take time to reach their targets. Interactions: Bullet-Enemy Collision: When a bullet hits an enemy: Enemy_Health is reduced by Bullet_Damage. If Enemy_Health <= 0, the enemy is destroyed. Optionally, Player_Score increases. Enemy-Castle Collision: If an enemy reaches the Castle_Position: Player_Health is reduced by Enemy_Damage_Dealt. The enemy is removed from the game (destroyed). Player-Reward Collision (Collection): When a reward object comes within a certain Collection_Radius of the castle (or the castle's "front" area): Current_Bullets_Per_Shot increases by 1. The Reward_Object is removed from the game. Game End Conditions: Player_Health <= 0: Game Over (Player loses). (Optional) All waves cleared: Game Win (Player wins). III. AI's Role (Setup for AI's Internal Logic): The AI needs to understand its role within this simulation. Given the description, the "player" (the castle) is largely automated. The AI's setup would involve: Simulating the Game World: The AI needs to be able to run the game mechanics forward in time based on the defined rules and current game state variables. This is crucial for: Predicting Enemy Movement: Where will enemies be in the next frame/tick? Predicting Bullet Trajectories: Where will bullets land? Predicting Collisions: When will bullets hit enemies? When will enemies hit the castle? When will rewards be collected? Targeting Logic (Pre-defined for this game): The primary target for the automatic shot is the closest enemy to the castle. This is a critical rule for the AI to implement. If multiple enemies are equidistant, a tie-breaking rule (e.g., highest health, lowest ID) might be needed. Reward Prioritization (Implicitly Handled): Rewards contribute directly to player power. The AI needs to understand that collecting rewards is beneficial as it increases Current_Bullets_Per_Shot, leading to more damage output. The game's design passively handles reward collection by making them spawn in accessible areas. Performance Metrics for AI Evaluation: Survival Time: How long did the castle last? Enemies Destroyed: How many enemies were defeated? Max Bullets Per Shot Achieved: What was the highest Current_Bullets_Per_Shot reached? Score: If a scoring system is implemented. IV. Data Structures for AI (How to represent the above): Game Map: A grid or continuous coordinate system. Entity Lists: Python lists or similar data structures to hold Enemy_Objects and Reward_Objects. Each object in the list is a dictionary or custom class with its attributes. Constants: Defined values for Firing_Rate, Bullet_Damage, Spawn_Rates, Castle_Position, Map_Boundaries, etc. V. Example Scenario for AI Understanding: Imagine the AI is observing the game: Initial State: Player_Health = 100, Current_Bullets_Per_Shot = 1. No enemies or rewards. Time Passes: Game_Time increases. Enemy Spawns: An Enemy_Object (e.g., Enemy_ID=1, Type="Basic_Grunt", Health=5, Position=(X_spawn, Y_spawn)) appears. Enemy Moves: AI updates Enemy_Position based on Enemy_Speed and Game_Time. Shooting Interval Reached: Time_Since_Last_Shot > Firing_Rate. Target Selection: AI identifies Enemy_ID=1 as the Closest_Enemy_To_Castle. Bullet Fired: A single bullet is spawned from Castle_Position towards Enemy_ID=1. Bullet Travels: AI updates Bullet_Position. Bullet Hits: Bullet_Position intersects Enemy_ID=1's collision box. Enemy_Health for Enemy_ID=1 becomes 4. Reward Spawns: A Reward_Object appears. Reward Reaches Castle: Reward_Position is within Collection_Radius of Castle_Position. Current_Bullets_Per_Shot becomes 2. Reward_Object is removed. Next Shot: When the next firing interval is reached, Current_Bullets_Per_Shot = 2, so two bullets will be fired. The AI needs to calculate the spread based on the primary target.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { scrapMetal: 0, fireRateLevel: 1, movementSpeedLevel: 1, healthLevel: 1, damageLevel: 1, bulletCountLevel: 1, shieldLevel: 1, shockwaveLevel: 1, maxUpgradeTier: 10, fireRateBaseCost: 50, movementSpeedBaseCost: 75, healthBaseCost: 100, damageBaseCost: 80, bulletCountBaseCost: 120, shieldBaseCost: 150, shockwaveBaseCost: 200, highScore: 0, bestWave: 1, longestSurvivalTime: 0, totalGamesPlayed: 0, totalEnemiesDefeated: 0, achievementUnlocked: false }); /**** * Classes ****/ var AdaptiveEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); enemyGraphics.tint = 0x8A2BE2; // Purple for adaptive enemy self.health = 4; self.maxHealth = 4; self.speed = 1.2; self.damage = 12; self.adaptationLevel = 0; self.adaptationTimer = 0; self.adaptationInterval = 300; // Adapt every 5 seconds self.lastX = 0; self.lastY = 0; self.lastCastleDistanceSquared = Infinity; // Create health bar for this enemy self.healthBar = new EnemyHealthBar(); self.healthBar.y = -50; // Position above enemy enemyHealthBars.push(self.healthBar); self.addChild(self.healthBar); self.update = function () { // Adaptation logic - enemy becomes stronger over time self.adaptationTimer++; if (self.adaptationTimer >= self.adaptationInterval && self.adaptationLevel < 3) { self.adaptationLevel++; self.adaptationTimer = 0; // Increase stats based on adaptation level self.speed += 0.3; self.damage += 3; self.maxHealth += 2; self.health = Math.min(self.maxHealth, self.health + 2); // Visual feedback for adaptation LK.effects.flashObject(self, 0x8A2BE2, 500); tween(self, { scaleX: 1.3, scaleY: 1.3 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn }); } }); } // Move toward castle var dx = castle.x - self.x; var dy = castle.y - self.y; var distanceSquared = dx * dx + dy * dy; if (distanceSquared > 3600) { var distance = Math.sqrt(distanceSquared); var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } // Check if reached castle if (self.lastCastleDistanceSquared > 3600 && distanceSquared <= 3600) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistanceSquared = distanceSquared; }; self.takeDamage = function (damage) { // Adaptive enemy takes less damage as it adapts var damagereduction = self.adaptationLevel * 0.1; var actualDamage = damage * (1 - damagereduction); self.health -= actualDamage; LK.effects.flashObject(self, 0xFFFFFF, 200); // Update health bar if (self.healthBar) { self.healthBar.updateHealth(self.health, self.maxHealth); } if (self.health <= 0) { LK.setScore(LK.getScore() + 25 + self.adaptationLevel * 10); // More points for adapted enemies scoreText.setText(LK.getScore()); LK.getSound('hit').play(); // Drop more scrap metal based on adaptation level if (Math.random() < 0.7 + self.adaptationLevel * 0.1) { var scrap = new ScrapMetalDrop(); scrap.x = self.x; scrap.y = self.y; scrap.baseY = scrap.y; scrap.value = 5 + self.adaptationLevel * 2; // More valuable based on adaptation scrapMetalDrops.push(scrap); game.addChild(scrap); } self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var Bomb = Container.expand(function () { var self = Container.call(this); var bombGraphics = self.attachAsset('bomb', { anchorX: 0.5, anchorY: 0.5 }); bombGraphics.tint = 0xFF0000; self.explosionRadius = 1000; self.fuseTime = 120; // 2 seconds at 60fps self.blinkSpeed = 10; // Start with small scale self.scaleX = 0.3; self.scaleY = 0.3; self.update = function () { self.fuseTime--; // Blinking effect that gets faster as explosion approaches var blinkInterval = Math.max(5, Math.floor(self.fuseTime / 10)); if (LK.ticks % blinkInterval === 0) { bombGraphics.alpha = bombGraphics.alpha === 1 ? 0.3 : 1; } // Scale pulsing effect var pulseScale = 1 + Math.sin(LK.ticks * 0.3) * 0.1; self.scaleX = pulseScale; self.scaleY = pulseScale; // Explode when fuse runs out if (self.fuseTime <= 0) { self.explode(); } }; self.explode = function () { // Visual explosion effect LK.effects.flashScreen(0xFF4400, 600); // Scale up rapidly during explosion tween(self, { scaleX: 3, scaleY: 3 }, { duration: 300, easing: tween.easeOut }); // Damage enemies in radius for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.explosionRadius) { // Instant kill for enemies in blast radius enemy.takeDamage(999); } } // Remove bomb from game after explosion animation LK.setTimeout(function () { self.destroy(); for (var i = bombs.length - 1; i >= 0; i--) { if (bombs[i] === self) { bombs.splice(i, 1); break; } } }, 300); }; self.activateShield = function () { if (self.shieldCooldown <= 0 && !self.shield) { // Create shield self.shield = new Shield(); self.shield.x = self.x; self.shield.y = self.y; game.addChild(self.shield); // Start cooldown var cooldownReduction = getUpgradeBonus('shield', storage.shieldLevel); self.shieldCooldown = Math.floor(self.shieldMaxCooldown * (1 - cooldownReduction)); // Visual feedback LK.effects.flashScreen(0x0088FF, 300); } }; self.activateShockwave = function () { if (self.shockwaveCooldown <= 0) { // Create shockwave var shockwave = new Shockwave(); shockwave.x = self.x; shockwave.y = self.y; // Increase damage based on upgrade level shockwave.damage = 5 + getUpgradeBonus('shockwave', storage.shockwaveLevel) * 20; // +2 damage per level (0.1 * 20 = 2) shockwaves.push(shockwave); game.addChild(shockwave); // Start cooldown var cooldownReduction = getUpgradeBonus('shockwave', storage.shockwaveLevel); self.shockwaveCooldown = Math.floor(self.shockwaveMaxCooldown * (1 - cooldownReduction)); // Visual feedback LK.effects.flashScreen(0xFF4400, 500); } }; self.update = function () { // Update ability cooldowns if (self.shieldCooldown > 0) { self.shieldCooldown--; } if (self.shockwaveCooldown > 0) { self.shockwaveCooldown--; } // Update shield position if active if (self.shield) { self.shield.x = self.x; self.shield.y = self.y; } if (self.shootCooldown > 0) { self.shootCooldown--; } // Auto-fire with priority targeting system if (self.shootCooldown <= 0 && enemies.length > 0) { var targetEnemy = null; var bestPriority = -1; var bestDistance = Infinity; // Priority levels: Higher number = higher priority var priorityMap = { 'SniperEnemy': 5, // Highest priority - long range damage 'BomberEnemy': 4, // High priority - explosive threat 'BossEnemy': 4, // High priority - powerful enemy 'FastEnemy': 3, // Medium-high priority - quick threat 'HealerEnemy': 3, // Medium-high priority - supports others 'TeleporterEnemy': 2, // Medium priority - unpredictable 'SplitterEnemy': 2, // Medium priority - creates more enemies 'TankEnemy': 1, // Low priority - slow but tough 'ShieldEnemy': 1, // Low priority - tough but manageable 'Enemy': 0 // Lowest priority - basic enemy }; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Determine enemy type by checking constructor name var enemyType = 'Enemy'; // Default if (enemy.constructor === SniperEnemy) enemyType = 'SniperEnemy';else if (enemy.constructor === BomberEnemy) enemyType = 'BomberEnemy';else if (enemy.constructor === BossEnemy) enemyType = 'BossEnemy';else if (enemy.constructor === FastEnemy) enemyType = 'FastEnemy';else if (enemy.constructor === HealerEnemy) enemyType = 'HealerEnemy';else if (enemy.constructor === TeleporterEnemy) enemyType = 'TeleporterEnemy';else if (enemy.constructor === SplitterEnemy) enemyType = 'SplitterEnemy';else if (enemy.constructor === TankEnemy) enemyType = 'TankEnemy';else if (enemy.constructor === ShieldEnemy) enemyType = 'ShieldEnemy'; var priority = priorityMap[enemyType] || 0; // Select target based on priority first, then distance if (priority > bestPriority || priority === bestPriority && distance < bestDistance) { bestPriority = priority; bestDistance = distance; targetEnemy = enemy; } } if (targetEnemy) { self.fireAtTarget(targetEnemy); self.shootCooldown = self.shootInterval; } } }; return self; }); var BombReward = Container.expand(function () { var self = Container.call(this); var rewardGraphics = self.attachAsset('bombReward', { anchorX: 0.5, anchorY: 0.5 }); // Make it visually distinct with orange tint rewardGraphics.tint = 0xFF8800; self.bobOffset = Math.random() * Math.PI * 2; self.baseY = 0; self.lifetime = 420; // 7 seconds at 60fps self.update = function () { // Gentle bobbing animation self.y = self.baseY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 5; // Decrease lifetime self.lifetime--; // Start fading when close to expiring if (self.lifetime <= 120) { var alpha = self.lifetime / 120; rewardGraphics.alpha = alpha; // Add flashing animation when close to expiring var flashInterval = Math.max(5, Math.floor(self.lifetime / 20)); if (LK.ticks % flashInterval === 0) { tween(rewardGraphics, { scaleX: 1.3, scaleY: 1.3 }, { duration: flashInterval * 8, easing: tween.easeOut, onFinish: function onFinish() { tween(rewardGraphics, { scaleX: 1, scaleY: 1 }, { duration: flashInterval * 8, easing: tween.easeIn }); } }); } } // Remove when lifetime expires if (self.lifetime <= 0) { tween(self, { scaleX: 0, scaleY: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); for (var i = bombRewards.length - 1; i >= 0; i--) { if (bombRewards[i] === self) { bombRewards.splice(i, 1); break; } } } }); } }; self.down = function (x, y, obj) { // Schedule bomb to appear at character position after 3 seconds var bombX = castle.x; var bombY = castle.y; LK.setTimeout(function () { var bomb = new Bomb(); bomb.x = bombX; bomb.y = bombY; bombs.push(bomb); game.addChild(bomb); }, 3000); LK.getSound('award').play(); LK.effects.flashObject(castle, 0xFF8800, 500); self.destroy(); for (var i = bombRewards.length - 1; i >= 0; i--) { if (bombRewards[i] === self) { bombRewards.splice(i, 1); break; } } }; return self; }); var BomberEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('bomberEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 3; self.maxHealth = 3; self.speed = 1.2; self.damage = 12; self.explosionRadius = 100; self.lastX = 0; self.lastY = 0; self.lastCastleDistance = Infinity; self.update = function () { var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 60) { var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } var currentDistance = Math.sqrt(dx * dx + dy * dy); if (self.lastCastleDistance > 60 && currentDistance <= 60) { castle.takeDamage(self.damage); self.explode(); } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistance = currentDistance; }; self.explode = function () { LK.effects.flashScreen(0xFF8800, 300); // Damage nearby enemies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy !== self) { var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.explosionRadius) { enemy.takeDamage(2); } } } self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } }; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFFFFFF, 200); if (self.health <= 0) { LK.setScore(LK.getScore() + 30); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); self.explode(); } }; return self; }); var BossEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('bossEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 15; self.maxHealth = 15; self.speed = 0.6; self.damage = 30; self.spawnCooldown = 0; self.spawnInterval = 300; // Spawns minions every 5 seconds self.lastX = 0; self.lastY = 0; self.lastCastleDistance = Infinity; self.update = function () { var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 60) { var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } // Spawn minion enemies if (self.spawnCooldown > 0) { self.spawnCooldown--; } if (self.spawnCooldown <= 0 && enemies.length < 15) { var minion = new Enemy(); var angle = Math.random() * Math.PI * 2; minion.x = self.x + Math.cos(angle) * 60; minion.y = self.y + Math.sin(angle) * 60; minion.lastX = minion.x; minion.lastY = minion.y; var dx2 = castle.x - minion.x; var dy2 = castle.y - minion.y; minion.lastCastleDistance = Math.sqrt(dx2 * dx2 + dy2 * dy2); enemies.push(minion); game.addChild(minion); self.spawnCooldown = self.spawnInterval; } var currentDistance = Math.sqrt(dx * dx + dy * dy); if (self.lastCastleDistance > 60 && currentDistance <= 60) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistance = currentDistance; }; self.takeDamage = function (damage) { var reducedDamage = damage * 0.7; // Boss takes 30% less damage self.health -= reducedDamage; LK.effects.flashObject(self, 0xFFFFFF, 200); if (self.health <= 0) { LK.setScore(LK.getScore() + 100); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); LK.effects.flashScreen(0x440044, 1000); // Drop multiple scrap metal (guaranteed) for (var c = 0; c < 3; c++) { var scrap = new ScrapMetalDrop(); var angle = Math.random() * Math.PI * 2; var distance = 20 + Math.random() * 40; scrap.x = self.x + Math.cos(angle) * distance; scrap.y = self.y + Math.sin(angle) * distance; scrap.baseY = scrap.y; scrap.value = 15; // High value for boss scrapMetalDrops.push(scrap); game.addChild(scrap); } self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var Bullet = Container.expand(function () { var self = Container.call(this); var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 50; self.velocityX = 0; self.velocityY = 0; self.damage = 1.01; self.lastX = 0; self.lastY = 0; self.update = function () { self.lastX = self.x; self.lastY = self.y; self.x += self.velocityX; self.y += self.velocityY; // Add spinning animation self.rotation += 0.2; // Remove if off screen if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782) { self.destroy(); for (var i = bullets.length - 1; i >= 0; i--) { if (bullets[i] === self) { bullets.splice(i, 1); break; } } } }; return self; }); var Castle = Container.expand(function () { var self = Container.call(this); var castleGraphics = self.attachAsset('castle', { anchorX: 0.5, anchorY: 0.5 }); // Set physical diameter to match image dimensions self.width = castleGraphics.width; self.height = castleGraphics.height; self.health = 100; self.maxHealth = 100; self.bulletsPerShot = 1; self.shootCooldown = 0; self.shootInterval = 60; // Fire every second at 60fps // Special abilities self.shield = null; self.shieldCooldown = 0; self.shieldMaxCooldown = 900; // 15 seconds at 60fps self.shockwaveCooldown = 0; self.shockwaveMaxCooldown = 1200; // 20 seconds at 60fps self.update = function () { if (self.shootCooldown > 0) { self.shootCooldown--; } // Auto-fire with priority targeting system if (self.shootCooldown <= 0 && enemies.length > 0) { var targetEnemy = null; var bestPriority = -1; var bestDistance = Infinity; // Priority levels: Higher number = higher priority var priorityMap = { 'EliteEnemy': 6, // Highest priority - extremely dangerous 'SniperEnemy': 5, // Highest priority - long range damage 'AdaptiveEnemy': 5, // Highest priority - becomes stronger over time 'BomberEnemy': 4, // High priority - explosive threat 'BossEnemy': 4, // High priority - powerful enemy 'RegenEnemy': 4, // High priority - heals over time 'FastEnemy': 3, // Medium-high priority - quick threat 'HealerEnemy': 3, // Medium-high priority - supports others 'TeleporterEnemy': 2, // Medium priority - unpredictable 'SplitterEnemy': 2, // Medium priority - creates more enemies 'TankEnemy': 1, // Low priority - slow but tough 'ShieldEnemy': 1, // Low priority - tough but manageable 'Enemy': 0 // Lowest priority - basic enemy }; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Determine enemy type by checking constructor name var enemyType = 'Enemy'; // Default if (enemy.constructor === SniperEnemy) enemyType = 'SniperEnemy';else if (enemy.constructor === BomberEnemy) enemyType = 'BomberEnemy';else if (enemy.constructor === BossEnemy) enemyType = 'BossEnemy';else if (enemy.constructor === FastEnemy) enemyType = 'FastEnemy';else if (enemy.constructor === HealerEnemy) enemyType = 'HealerEnemy';else if (enemy.constructor === TeleporterEnemy) enemyType = 'TeleporterEnemy';else if (enemy.constructor === SplitterEnemy) enemyType = 'SplitterEnemy';else if (enemy.constructor === TankEnemy) enemyType = 'TankEnemy';else if (enemy.constructor === ShieldEnemy) enemyType = 'ShieldEnemy';else if (enemy.constructor === AdaptiveEnemy) enemyType = 'AdaptiveEnemy';else if (enemy.constructor === RegenEnemy) enemyType = 'RegenEnemy';else if (enemy.constructor === EliteEnemy) enemyType = 'EliteEnemy'; var priority = priorityMap[enemyType] || 0; // Select target based on priority first, then distance if (priority > bestPriority || priority === bestPriority && distance < bestDistance) { bestPriority = priority; bestDistance = distance; targetEnemy = enemy; } } if (targetEnemy) { self.fireAtTarget(targetEnemy); self.shootCooldown = self.shootInterval; } } }; self.fireAtTarget = function (target) { var baseAngle = Math.atan2(target.y - self.y, target.x - self.x); var spreadAngle = Math.PI / 6; // 30 degrees spread for (var i = 0; i < self.bulletsPerShot; i++) { var bullet = new Bullet(); bullet.x = self.x; bullet.y = self.y; // Apply upgraded damage (always use upgraded damage) bullet.damage = self.upgradedBulletDamage || bullet.damage; var offset = 0; if (self.bulletsPerShot > 1) { offset = (i - (self.bulletsPerShot - 1) / 2) * (spreadAngle / Math.max(1, self.bulletsPerShot - 1)); } var angle = baseAngle + offset; bullet.velocityX = Math.cos(angle) * bullet.speed; bullet.velocityY = Math.sin(angle) * bullet.speed; // Set bullet rotation to match direction bullet.rotation = angle; bullets.push(bullet); game.addChild(bullet); } LK.getSound('shoot').play(); }; self.takeDamage = function (damage) { // Check if shield is active if (self.shield) { // Shield blocks damage and flashes LK.effects.flashObject(self.shield, 0x0088FF, 200); return; // No damage taken } self.health -= damage; LK.effects.flashObject(self, 0xFF0000, 300); // Update health bar if (castleHealthBar) { castleHealthBar.updateHealth(self.health, self.maxHealth); } if (self.health <= 0) { // Save collected scrap metal to persistent storage storage.scrapMetal += currentScrapMetal; // Check and update high scores var currentScore = LK.getScore(); var isNewHighScore = false; var isNewWaveRecord = false; var isNewTimeRecord = false; if (currentScore > storage.highScore) { storage.highScore = currentScore; isNewHighScore = true; } if (waveNumber > storage.bestWave) { storage.bestWave = waveNumber; isNewWaveRecord = true; } if (survivalTime > storage.longestSurvivalTime) { storage.longestSurvivalTime = survivalTime; isNewTimeRecord = true; } // Show achievement notifications if (isNewHighScore || isNewWaveRecord || isNewTimeRecord) { var achievementText = 'NEW RECORD!\n'; if (isNewHighScore) achievementText += 'HIGH SCORE: ' + currentScore + '\n'; if (isNewWaveRecord) achievementText += 'BEST WAVE: ' + waveNumber + '\n'; if (isNewTimeRecord) { var minutes = Math.floor(survivalTime / 3600); var seconds = Math.floor(survivalTime % 3600 / 60); achievementText += 'LONGEST TIME: ' + minutes + ':' + (seconds < 10 ? '0' : '') + seconds; } var recordText = new Text2(achievementText, { size: 55, fill: 0xFF00FF }); recordText.anchor.set(0.5, 0.5); recordText.x = 1024; recordText.y = 800; game.addChild(recordText); LK.effects.flashScreen(0xFF00FF, 1500); tween(recordText, { scaleX: 1.3, scaleY: 1.3 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { tween(recordText, { alpha: 0 }, { duration: 2000, onFinish: function onFinish() { recordText.destroy(); } }); } }); } LK.showGameOver(); } }; return self; }); var EliteEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('tankEnemy', { anchorX: 0.5, anchorY: 0.5 }); enemyGraphics.tint = 0xFFD700; // Gold for elite enemy self.health = 12; self.maxHealth = 12; self.speed = 1.1; self.damage = 25; self.armor = 0.3; // Takes 30% less damage self.specialAbilityCooldown = 0; self.specialAbilityInterval = 240; // Special ability every 4 seconds self.lastX = 0; self.lastY = 0; self.lastCastleDistanceSquared = Infinity; // Create health bar for this enemy self.healthBar = new EnemyHealthBar(); self.healthBar.y = -50; // Position above enemy enemyHealthBars.push(self.healthBar); self.addChild(self.healthBar); self.update = function () { // Special ability - damage reduction boost self.specialAbilityCooldown++; if (self.specialAbilityCooldown >= self.specialAbilityInterval) { // Temporary damage reduction boost self.armor = Math.min(0.6, self.armor + 0.1); self.specialAbilityCooldown = 0; LK.effects.flashObject(self, 0xFFD700, 400); // Create temporary shield effect tween(self, { scaleX: 1.2, scaleY: 1.2 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeIn }); } }); } // Move toward castle var dx = castle.x - self.x; var dy = castle.y - self.y; var distanceSquared = dx * dx + dy * dy; if (distanceSquared > 3600) { var distance = Math.sqrt(distanceSquared); var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } // Check if reached castle if (self.lastCastleDistanceSquared > 3600 && distanceSquared <= 3600) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistanceSquared = distanceSquared; }; self.takeDamage = function (damage) { var reducedDamage = damage * (1 - self.armor); self.health -= reducedDamage; LK.effects.flashObject(self, 0xFFFFFF, 200); // Update health bar if (self.healthBar) { self.healthBar.updateHealth(self.health, self.maxHealth); } if (self.health <= 0) { LK.setScore(LK.getScore() + 75); // High score reward scoreText.setText(LK.getScore()); LK.getSound('hit').play(); // Drop multiple scrap metal (guaranteed) for (var c = 0; c < 2; c++) { var scrap = new ScrapMetalDrop(); var angle = Math.random() * Math.PI * 2; var distance = 20 + Math.random() * 40; scrap.x = self.x + Math.cos(angle) * distance; scrap.y = self.y + Math.sin(angle) * distance; scrap.baseY = scrap.y; scrap.value = 12; // High value for elite enemy scrapMetalDrops.push(scrap); game.addChild(scrap); } self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var Enemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 3; self.maxHealth = 3; self.speed = 1; self.damage = 10; self.lastX = 0; self.lastY = 0; self.lastCastleDistanceSquared = Infinity; // Create health bar for this enemy self.healthBar = new EnemyHealthBar(); self.healthBar.y = -50; // Position above enemy enemyHealthBars.push(self.healthBar); self.addChild(self.healthBar); self.update = function () { // Move toward castle var dx = castle.x - self.x; var dy = castle.y - self.y; var distanceSquared = dx * dx + dy * dy; if (distanceSquared > 3600) { // 60^2 = 3600 // Don't overlap with castle var distance = Math.sqrt(distanceSquared); var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } // Check if reached castle if (self.lastCastleDistanceSquared > 3600 && distanceSquared <= 3600) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistanceSquared = distanceSquared; }; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFFFFFF, 200); // Update health bar if (self.healthBar) { self.healthBar.updateHealth(self.health, self.maxHealth); } if (self.health <= 0) { LK.setScore(LK.getScore() + 10); storage.totalEnemiesDefeated++; scoreText.setText(LK.getScore()); LK.getSound('hit').play(); // Drop scrap metal (80% chance - increased for better collection) if (Math.random() < 0.8) { var scrap = new ScrapMetalDrop(); scrap.x = self.x; scrap.y = self.y; scrap.baseY = scrap.y; scrap.value = 5; // Basic enemy drops 5 scrap metal scrapMetalDrops.push(scrap); game.addChild(scrap); } self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var EnemyHealthBar = Container.expand(function () { var self = Container.call(this); // Create background bar (dark red) var backgroundBar = LK.getAsset('bgTile', { anchorX: 0.5, anchorY: 0.5 }); backgroundBar.tint = 0x330000; backgroundBar.scaleX = 0.8; backgroundBar.scaleY = 0.15; self.addChild(backgroundBar); // Create health bar (red) var healthBar = LK.getAsset('bgTile', { anchorX: 0, anchorY: 0.5 }); healthBar.tint = 0xFF0000; healthBar.scaleX = 0.8; healthBar.scaleY = 0.15; healthBar.x = -backgroundBar.width * backgroundBar.scaleX / 2; self.addChild(healthBar); self.backgroundBar = backgroundBar; self.healthBar = healthBar; self.updateHealth = function (currentHealth, maxHealth) { var healthPercentage = Math.max(0, currentHealth / maxHealth); self.healthBar.scaleX = 0.8 * healthPercentage; // Hide if at full health to reduce clutter self.visible = healthPercentage < 1.0; }; return self; }); var FastEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('fastEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 1; self.maxHealth = 1; self.speed = 3; self.damage = 5; self.lastX = 0; self.lastY = 0; self.lastCastleDistance = Infinity; self.update = function () { // Move toward castle with higher speed var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 60) { var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } // Check if reached castle var currentDistance = Math.sqrt(dx * dx + dy * dy); if (self.lastCastleDistance > 60 && currentDistance <= 60) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistance = currentDistance; }; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFFFFFF, 200); if (self.health <= 0) { LK.setScore(LK.getScore() + 15); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); // Drop scrap metal (50% chance, less than basic enemy) if (Math.random() < 0.5) { var scrap = new ScrapMetalDrop(); scrap.x = self.x; scrap.y = self.y; scrap.baseY = scrap.y; scrap.value = 3; // Less value than basic enemy scrapMetalDrops.push(scrap); game.addChild(scrap); } self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var HealerEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('healerEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 4; self.maxHealth = 4; self.speed = 0.8; self.damage = 8; self.healCooldown = 0; self.healInterval = 180; // Heals every 3 seconds self.healRadius = 150; self.lastX = 0; self.lastY = 0; self.lastCastleDistance = Infinity; self.update = function () { var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 60) { var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } // Heal nearby enemies if (self.healCooldown > 0) { self.healCooldown--; } if (self.healCooldown <= 0) { for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy !== self && enemy.health < enemy.maxHealth) { var dx2 = enemy.x - self.x; var dy2 = enemy.y - self.y; var healDistance = Math.sqrt(dx2 * dx2 + dy2 * dy2); if (healDistance <= self.healRadius) { enemy.health = Math.min(enemy.maxHealth, enemy.health + 1); LK.effects.flashObject(enemy, 0x88FF00, 200); } } } self.healCooldown = self.healInterval; } var currentDistance = Math.sqrt(dx * dx + dy * dy); if (self.lastCastleDistance > 60 && currentDistance <= 60) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistance = currentDistance; }; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFFFFFF, 200); if (self.health <= 0) { LK.setScore(LK.getScore() + 35); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var HealthBar = Container.expand(function () { var self = Container.call(this); // Create background bar (red) var backgroundBar = LK.getAsset('bgTile', { anchorX: 0, anchorY: 0.5 }); backgroundBar.tint = 0x660000; backgroundBar.scaleX = 2; backgroundBar.scaleY = 0.3; self.addChild(backgroundBar); // Create health bar (green) var healthBar = LK.getAsset('bgTile', { anchorX: 0, anchorY: 0.5 }); healthBar.tint = 0x00AA00; healthBar.scaleX = 2; healthBar.scaleY = 0.3; self.addChild(healthBar); self.backgroundBar = backgroundBar; self.healthBar = healthBar; self.maxWidth = backgroundBar.width * backgroundBar.scaleX; self.updateHealth = function (currentHealth, maxHealth) { var healthPercentage = Math.max(0, currentHealth / maxHealth); self.healthBar.scaleX = 2 * healthPercentage; // Change color based on health percentage if (healthPercentage > 0.6) { self.healthBar.tint = 0x00AA00; // Green } else if (healthPercentage > 0.3) { self.healthBar.tint = 0xFFAA00; // Orange } else { self.healthBar.tint = 0xFF0000; // Red } }; return self; }); var HealthPack = Container.expand(function () { var self = Container.call(this); var healthGraphics = self.attachAsset('reward', { anchorX: 0.5, anchorY: 0.5 }); // Make it red for health healthGraphics.tint = 0xFF4444; healthGraphics.scaleX = 0.8; healthGraphics.scaleY = 0.8; self.bobOffset = Math.random() * Math.PI * 2; self.baseY = 0; self.lifetime = 420; // 7 seconds at 60fps self.healAmount = 25; // Restore 25 health self.update = function () { // Gentle bobbing animation self.y = self.baseY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 5; // Gentle pulsing effect var pulseScale = 0.8 + Math.sin(LK.ticks * 0.2) * 0.1; healthGraphics.scaleX = pulseScale; healthGraphics.scaleY = pulseScale; // Decrease lifetime self.lifetime--; // Start fading when close to expiring if (self.lifetime <= 120) { var alpha = self.lifetime / 120; healthGraphics.alpha = alpha; } // Remove when lifetime expires if (self.lifetime <= 0) { self.destroy(); for (var i = healthPacks.length - 1; i >= 0; i--) { if (healthPacks[i] === self) { healthPacks.splice(i, 1); break; } } } }; self.down = function (x, y, obj) { // Heal castle var healAmount = Math.min(self.healAmount, castle.maxHealth - castle.health); if (healAmount > 0) { castle.health += healAmount; LK.getSound('award').play(); LK.effects.flashObject(castle, 0x44FF44, 500); // Show heal amount var healText = new Text2('+' + healAmount + ' HEALTH', { size: 40, fill: 0x44FF44 }); healText.anchor.set(0.5, 0.5); healText.x = castle.x; healText.y = castle.y - 60; game.addChild(healText); // Animate heal text tween(healText, { y: healText.y - 50, alpha: 0 }, { duration: 1500, onFinish: function onFinish() { healText.destroy(); } }); } self.destroy(); for (var i = healthPacks.length - 1; i >= 0; i--) { if (healthPacks[i] === self) { healthPacks.splice(i, 1); break; } } }; return self; }); var RegenEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('healerEnemy', { anchorX: 0.5, anchorY: 0.5 }); enemyGraphics.tint = 0x32CD32; // Lime green for regen enemy self.health = 6; self.maxHealth = 6; self.speed = 0.9; self.damage = 14; self.regenRate = 1; self.regenTimer = 0; self.regenInterval = 120; // Regenerate every 2 seconds self.lastX = 0; self.lastY = 0; self.lastCastleDistanceSquared = Infinity; // Create health bar for this enemy self.healthBar = new EnemyHealthBar(); self.healthBar.y = -50; // Position above enemy enemyHealthBars.push(self.healthBar); self.addChild(self.healthBar); self.update = function () { // Regeneration logic self.regenTimer++; if (self.regenTimer >= self.regenInterval && self.health < self.maxHealth) { self.health = Math.min(self.maxHealth, self.health + self.regenRate); self.regenTimer = 0; // Visual feedback for regeneration LK.effects.flashObject(self, 0x32CD32, 300); if (self.healthBar) { self.healthBar.updateHealth(self.health, self.maxHealth); } } // Move toward castle var dx = castle.x - self.x; var dy = castle.y - self.y; var distanceSquared = dx * dx + dy * dy; if (distanceSquared > 3600) { var distance = Math.sqrt(distanceSquared); var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } // Check if reached castle if (self.lastCastleDistanceSquared > 3600 && distanceSquared <= 3600) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistanceSquared = distanceSquared; }; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFFFFFF, 200); // Update health bar if (self.healthBar) { self.healthBar.updateHealth(self.health, self.maxHealth); } if (self.health <= 0) { LK.setScore(LK.getScore() + 30); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); // Drop guaranteed scrap metal var scrap = new ScrapMetalDrop(); scrap.x = self.x; scrap.y = self.y; scrap.baseY = scrap.y; scrap.value = 8; // Higher value for tougher enemy scrapMetalDrops.push(scrap); game.addChild(scrap); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var Reward = Container.expand(function () { var self = Container.call(this); var rewardGraphics = self.attachAsset('reward', { anchorX: 0.5, anchorY: 0.5 }); self.bobOffset = Math.random() * Math.PI * 2; self.baseY = 0; self.lifetime = 420; // 7 seconds at 60fps self.update = function () { // Gentle bobbing animation self.y = self.baseY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 5; // Decrease lifetime self.lifetime--; // Start fading when close to expiring if (self.lifetime <= 120) { var alpha = self.lifetime / 120; rewardGraphics.alpha = alpha; // Add flashing animation when close to expiring var flashInterval = Math.max(5, Math.floor(self.lifetime / 20)); if (LK.ticks % flashInterval === 0) { tween(rewardGraphics, { scaleX: 1.3, scaleY: 1.3 }, { duration: flashInterval * 8, easing: tween.easeOut, onFinish: function onFinish() { tween(rewardGraphics, { scaleX: 1, scaleY: 1 }, { duration: flashInterval * 8, easing: tween.easeIn }); } }); } } // Remove when lifetime expires if (self.lifetime <= 0) { tween(self, { scaleX: 0, scaleY: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); for (var i = rewards.length - 1; i >= 0; i--) { if (rewards[i] === self) { rewards.splice(i, 1); break; } } } }); } }; self.down = function (x, y, obj) { // Define maximum bullet limit to prevent excessive stacking var maxBullets = 8; if (castle.bulletsPerShot < maxBullets) { // Add 1 bullet per reward (consistent increment) castle.bulletsPerShot++; bulletsText.setText('Bullets: ' + castle.bulletsPerShot); LK.getSound('award').play(); LK.effects.flashObject(castle, 0x00FF00, 500); // Visual feedback for bullet upgrade with scaling effect tween(castle, { scaleX: 1.2, scaleY: 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(castle, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn }); } }); } else { // At maximum bullets - give score bonus instead LK.setScore(LK.getScore() + 100); scoreText.setText(LK.getScore()); bulletsText.setText('Bullets: ' + castle.bulletsPerShot + ' (MAX)'); LK.getSound('award').play(); LK.effects.flashObject(castle, 0xFFD700, 500); // Gold flash for max bonus // Brief text notification var maxText = new Text2('+100 SCORE!', { size: 40, fill: 0xFFD700 }); maxText.anchor.set(0.5, 0.5); maxText.x = castle.x; maxText.y = castle.y - 60; game.addChild(maxText); // Animate and remove notification tween(maxText, { y: maxText.y - 50, alpha: 0 }, { duration: 1500, onFinish: function onFinish() { maxText.destroy(); } }); } self.destroy(); for (var i = rewards.length - 1; i >= 0; i--) { if (rewards[i] === self) { rewards.splice(i, 1); break; } } }; return self; }); var ScrapCollectionIndicator = Container.expand(function () { var self = Container.call(this); // Create collection range visualization var rangeCircle = LK.getAsset('bgCircle', { anchorX: 0.5, anchorY: 0.5 }); rangeCircle.tint = 0xC0C0C0; rangeCircle.alpha = 0.1; rangeCircle.scaleX = 0.6; // 180px radius visualization rangeCircle.scaleY = 0.6; self.addChild(rangeCircle); self.rangeCircle = rangeCircle; self.pulseDirection = 1; self.update = function () { // Gentle pulsing effect to show collection range var pulseSpeed = 0.02; self.alpha += self.pulseDirection * pulseSpeed; if (self.alpha >= 0.3) { self.pulseDirection = -1; } else if (self.alpha <= 0.1) { self.pulseDirection = 1; } // Only show when scrap metal is nearby var nearbyScrap = false; for (var i = 0; i < scrapMetalDrops.length; i++) { var scrap = scrapMetalDrops[i]; var dx = scrap.x - castle.x; var dy = scrap.y - castle.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= 250) { // Show range when scrap is within extended range nearbyScrap = true; break; } } self.visible = nearbyScrap; }; return self; }); var ScrapMetalDrop = Container.expand(function () { var self = Container.call(this); var scrapGraphics = self.attachAsset('reward', { anchorX: 0.5, anchorY: 0.5 }); // Make it metallic silver for scrap metal scrapGraphics.tint = 0xC0C0C0; scrapGraphics.scaleX = 0.6; scrapGraphics.scaleY = 0.6; self.value = 5; // Base scrap metal value self.bobOffset = Math.random() * Math.PI * 2; self.baseY = 0; self.lifetime = 600; // 10 seconds at 60fps self.magnetRange = 180; // Increased range for easier collection self.collectDistance = 80; // Distance at which scrap is automatically collected self.update = function () { // Gentle bobbing animation self.y = self.baseY + Math.sin(LK.ticks * 0.15 + self.bobOffset) * 3; // Decrease lifetime self.lifetime--; // Distance calculation var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Auto-collect when very close if (distance <= self.collectDistance) { currentScrapMetal += self.value; scrapMetalText.setText('Scrap Metal: ' + (storage.scrapMetal + currentScrapMetal)); LK.getSound('award').play(); // Visual collection effect LK.effects.flashObject(castle, 0xC0C0C0, 300); // Show collection amount var collectText = new Text2('+' + self.value + ' SCRAP', { size: 35, fill: 0xC0C0C0 }); collectText.anchor.set(0.5, 0.5); collectText.x = self.x; collectText.y = self.y - 30; game.addChild(collectText); // Animate collection text towards castle tween(collectText, { x: castle.x, y: castle.y - 50, alpha: 0 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { collectText.destroy(); } }); self.destroy(); for (var i = scrapMetalDrops.length - 1; i >= 0; i--) { if (scrapMetalDrops[i] === self) { scrapMetalDrops.splice(i, 1); break; } } return; } // Magnet effect when castle is nearby if (distance <= self.magnetRange) { var magnetSpeed = Math.min(5, distance * 0.05); // Speed increases as it gets closer var moveX = dx / distance * magnetSpeed; var moveY = dy / distance * magnetSpeed; self.x += moveX; self.y += moveY; // Add sparkle effect during magnet pull if (LK.ticks % 10 === 0) { LK.effects.flashObject(self, 0xFFFFFF, 100); } } // Start fading when close to expiring if (self.lifetime <= 120) { var alpha = self.lifetime / 120; scrapGraphics.alpha = alpha; } // Remove when lifetime expires if (self.lifetime <= 0) { self.destroy(); for (var i = scrapMetalDrops.length - 1; i >= 0; i--) { if (scrapMetalDrops[i] === self) { scrapMetalDrops.splice(i, 1); break; } } } }; return self; }); var Shield = Container.expand(function () { var self = Container.call(this); var shieldGraphics = self.attachAsset('castle', { anchorX: 0.5, anchorY: 0.5 }); shieldGraphics.tint = 0x0088FF; shieldGraphics.alpha = 0.3; self.scaleX = 1.5; self.scaleY = 1.5; self.duration = 300; // 5 seconds at 60fps self.pulseDirection = 1; self.update = function () { // Pulsing effect var pulseSpeed = 0.1; self.alpha += self.pulseDirection * pulseSpeed; if (self.alpha >= 0.6) { self.pulseDirection = -1; } else if (self.alpha <= 0.2) { self.pulseDirection = 1; } // Decrease duration self.duration--; if (self.duration <= 0) { castle.shield = null; self.destroy(); } }; return self; }); var ShieldEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('shieldEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 6; self.maxHealth = 6; self.speed = 0.7; self.damage = 15; self.shieldRadius = 80; self.lastX = 0; self.lastY = 0; self.lastCastleDistance = Infinity; self.update = function () { var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 60) { var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } var currentDistance = Math.sqrt(dx * dx + dy * dy); if (self.lastCastleDistance > 60 && currentDistance <= 60) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistance = currentDistance; }; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFFFFFF, 200); if (self.health <= 0) { LK.setScore(LK.getScore() + 40); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var Shockwave = Container.expand(function () { var self = Container.call(this); var shockwaveGraphics = self.attachAsset('castle', { anchorX: 0.5, anchorY: 0.5 }); shockwaveGraphics.tint = 0xFF4400; shockwaveGraphics.alpha = 0.8; self.maxRadius = 400; self.currentRadius = 50; self.damage = 5; self.speed = 15; self.hasDealtDamage = []; self.update = function () { // Expand shockwave self.currentRadius += self.speed; var scale = self.currentRadius / 100; self.scaleX = scale; self.scaleY = scale; // Fade out as it expands self.alpha = Math.max(0, 0.8 - self.currentRadius / self.maxRadius * 0.8); // Damage enemies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Check if enemy is within shockwave range and hasn't been damaged yet if (distance <= self.currentRadius && self.hasDealtDamage.indexOf(enemy) === -1) { enemy.takeDamage(self.damage); self.hasDealtDamage.push(enemy); LK.effects.flashObject(enemy, 0xFF4400, 200); } } // Remove when fully expanded if (self.currentRadius >= self.maxRadius) { self.destroy(); for (var j = shockwaves.length - 1; j >= 0; j--) { if (shockwaves[j] === self) { shockwaves.splice(j, 1); break; } } } }; return self; }); var SniperEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('sniperEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 2; self.maxHealth = 2; self.speed = 1.5; self.damage = 15; self.shootCooldown = 0; self.shootInterval = 120; // Shoots every 2 seconds self.lastX = 0; self.lastY = 0; self.lastCastleDistance = Infinity; self.update = function () { var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); // Move toward castle until in range (300 pixels) if (distance > 300) { var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } else { // In range, shoot at castle if (self.shootCooldown > 0) { self.shootCooldown--; } if (self.shootCooldown <= 0) { castle.takeDamage(self.damage); LK.effects.flashObject(self, 0x00FFFF, 300); self.shootCooldown = self.shootInterval; } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistance = distance; }; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFFFFFF, 200); if (self.health <= 0) { LK.setScore(LK.getScore() + 20); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var SplitterEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('splitterEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 5; self.maxHealth = 5; self.speed = 0.9; self.damage = 18; self.splitCount = 2; self.lastX = 0; self.lastY = 0; self.lastCastleDistance = Infinity; self.update = function () { var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 60) { var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } var currentDistance = Math.sqrt(dx * dx + dy * dy); if (self.lastCastleDistance > 60 && currentDistance <= 60) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistance = currentDistance; }; self.split = function () { for (var i = 0; i < self.splitCount; i++) { var smallEnemy = new FastEnemy(); var angle = Math.PI * 2 / self.splitCount * i; smallEnemy.x = self.x + Math.cos(angle) * 30; smallEnemy.y = self.y + Math.sin(angle) * 30; smallEnemy.lastX = smallEnemy.x; smallEnemy.lastY = smallEnemy.y; var dx = castle.x - smallEnemy.x; var dy = castle.y - smallEnemy.y; smallEnemy.lastCastleDistance = Math.sqrt(dx * dx + dy * dy); enemies.push(smallEnemy); game.addChild(smallEnemy); } }; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFFFFFF, 200); if (self.health <= 0) { LK.setScore(LK.getScore() + 50); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); self.split(); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var TankEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('tankEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 8; self.maxHealth = 8; self.speed = 0.5; self.damage = 20; self.armor = 0.5; // Takes 50% less damage self.lastX = 0; self.lastY = 0; self.lastCastleDistance = Infinity; self.update = function () { // Move toward castle slowly var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 60) { var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } // Check if reached castle var currentDistance = Math.sqrt(dx * dx + dy * dy); if (self.lastCastleDistance > 60 && currentDistance <= 60) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistance = currentDistance; }; self.takeDamage = function (damage) { var reducedDamage = damage * (1 - self.armor); self.health -= reducedDamage; LK.effects.flashObject(self, 0xFFFFFF, 200); if (self.health <= 0) { LK.setScore(LK.getScore() + 25); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var TeleporterEnemy = Container.expand(function () { var self = Container.call(this); var enemyGraphics = self.attachAsset('teleporterEnemy', { anchorX: 0.5, anchorY: 0.5 }); self.health = 2; self.maxHealth = 2; self.speed = 1; self.damage = 12; self.teleportCooldown = 0; self.teleportInterval = 240; // Teleports every 4 seconds self.lastX = 0; self.lastY = 0; self.lastCastleDistance = Infinity; self.update = function () { var dx = castle.x - self.x; var dy = castle.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance > 60) { var moveX = dx / distance * self.speed; var moveY = dy / distance * self.speed; self.x += moveX; self.y += moveY; } // Teleport randomly if (self.teleportCooldown > 0) { self.teleportCooldown--; } if (self.teleportCooldown <= 0) { var angle = Math.random() * Math.PI * 2; var teleportDistance = 200 + Math.random() * 200; self.x = castle.x + Math.cos(angle) * teleportDistance; self.y = castle.y + Math.sin(angle) * teleportDistance; // Keep within bounds self.x = Math.max(50, Math.min(1998, self.x)); self.y = Math.max(50, Math.min(2682, self.y)); LK.effects.flashObject(self, 0xFF00FF, 400); self.teleportCooldown = self.teleportInterval; } var currentDistance = Math.sqrt((castle.x - self.x) * (castle.x - self.x) + (castle.y - self.y) * (castle.y - self.y)); if (self.lastCastleDistance > 60 && currentDistance <= 60) { castle.takeDamage(self.damage); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } self.lastX = self.x; self.lastY = self.y; self.lastCastleDistance = currentDistance; }; self.takeDamage = function (damage) { self.health -= damage; LK.effects.flashObject(self, 0xFFFFFF, 200); if (self.health <= 0) { LK.setScore(LK.getScore() + 25); scoreText.setText(LK.getScore()); LK.getSound('hit').play(); self.destroy(); for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i] === self) { enemies.splice(i, 1); break; } } } }; return self; }); var ThreatIndicator = Container.expand(function () { var self = Container.call(this); var indicator = LK.getAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); indicator.tint = 0xFF4400; indicator.scaleX = 0.5; indicator.scaleY = 0.5; indicator.alpha = 0.8; self.addChild(indicator); self.indicator = indicator; self.pulseDirection = 1; self.update = function () { // Pulsing animation var pulseSpeed = 0.05; self.alpha += self.pulseDirection * pulseSpeed; if (self.alpha >= 1) { self.pulseDirection = -1; } else if (self.alpha <= 0.5) { self.pulseDirection = 1; } }; return self; }); var UpgradeMenu = Container.expand(function () { var self = Container.call(this); // Create semi-transparent background var background = LK.getAsset('bgTile', { anchorX: 0, anchorY: 0 }); background.tint = 0x000000; background.alpha = 0.8; background.scaleX = 10.24; // Cover full screen width background.scaleY = 13.66; // Cover full screen height self.addChild(background); // Menu title var titleText = new Text2('CASTLE UPGRADES', { size: 80, fill: 0xFFD700 }); titleText.anchor.set(0.5, 0); titleText.x = 1024; titleText.y = 200; self.addChild(titleText); // Close button var closeButton = new Text2('β CLOSE', { size: 60, fill: 0xFF4444 }); closeButton.anchor.set(1, 0); closeButton.x = 1900; closeButton.y = 250; self.addChild(closeButton); // Upgrade buttons array self.upgradeButtons = []; // Create upgrade buttons for each type var upgradeTypes = [{ type: 'fireRate', name: 'FIRE RATE', color: 0xFF8800, description: '+15% faster shooting' }, { type: 'damage', name: 'DAMAGE', color: 0xFF0000, description: '+20% bullet damage' }, { type: 'health', name: 'HEALTH', color: 0x00FF00, description: '+20 max health' }, { type: 'bulletCount', name: 'BULLETS', color: 0x00AAFF, description: '+1 bullet per shot' }, { type: 'movementSpeed', name: 'MOVEMENT', color: 0xFFFF00, description: '+25% movement speed' }, { type: 'shield', name: 'SHIELD', color: 0x0088FF, description: '10% shorter cooldown' }, { type: 'shockwave', name: 'SHOCKWAVE', color: 0xFF4400, description: '10% shorter cooldown' }]; for (var i = 0; i < upgradeTypes.length; i++) { var upgrade = upgradeTypes[i]; var buttonContainer = new Container(); // Button background var buttonBg = LK.getAsset('bgTile', { anchorX: 0, anchorY: 0 }); buttonBg.tint = upgrade.color; buttonBg.alpha = 0.7; buttonBg.scaleX = 4; buttonBg.scaleY = 0.8; buttonContainer.addChild(buttonBg); // Button text var buttonText = new Text2('', { size: 45, fill: 0xFFFFFF }); buttonText.anchor.set(0, 0.5); buttonText.x = 20; buttonText.y = buttonBg.height * buttonBg.scaleY / 2; buttonContainer.addChild(buttonText); // Position button var col = i % 2; var row = Math.floor(i / 2); buttonContainer.x = 300 + col * 800; buttonContainer.y = 400 + row * 200; // Store references buttonContainer.upgradeType = upgrade.type; buttonContainer.buttonText = buttonText; buttonContainer.buttonBg = buttonBg; buttonContainer.originalColor = upgrade.color; buttonContainer.upgradeName = upgrade.name; buttonContainer.description = upgrade.description; self.upgradeButtons.push(buttonContainer); self.addChild(buttonContainer); } // Current scrap display var scrapDisplay = new Text2('', { size: 55, fill: 0xC0C0C0 }); scrapDisplay.anchor.set(0.5, 0); scrapDisplay.x = 1024; scrapDisplay.y = 320; self.addChild(scrapDisplay); self.scrapDisplay = scrapDisplay; // Update button states and text self.updateButtons = function () { var totalScrap = storage.scrapMetal + currentScrapMetal; self.scrapDisplay.setText('Available Scrap Metal: ' + totalScrap); for (var i = 0; i < self.upgradeButtons.length; i++) { var button = self.upgradeButtons[i]; var currentLevel = storage[button.upgradeType + 'Level'] || 1; var cost = getUpgradeCost(button.upgradeType, currentLevel); var canAfford = canAffordUpgrade(button.upgradeType, currentLevel); var isMaxLevel = currentLevel >= storage.maxUpgradeTier; if (isMaxLevel) { button.buttonText.setText(button.upgradeName + ' - MAX LEVEL'); button.buttonBg.tint = 0x666666; button.buttonBg.alpha = 0.5; } else { button.buttonText.setText(button.upgradeName + ' T' + currentLevel + 'βT' + (currentLevel + 1) + '\nCost: ' + cost + ' | ' + button.description); button.buttonBg.tint = canAfford ? button.originalColor : 0x666666; button.buttonBg.alpha = canAfford ? 0.7 : 0.3; } } }; // Handle button clicks self.down = function (x, y, obj) { // Check close button if (x >= 1700 && x <= 1900 && y >= 250 && y <= 320) { upgradeMenuVisible = false; self.visible = false; return; } // Check upgrade buttons for (var i = 0; i < self.upgradeButtons.length; i++) { var button = self.upgradeButtons[i]; var buttonX = button.x; var buttonY = button.y; var buttonWidth = button.buttonBg.width * button.buttonBg.scaleX; var buttonHeight = button.buttonBg.height * button.buttonBg.scaleY; if (x >= buttonX && x <= buttonX + buttonWidth && y >= buttonY && y <= buttonY + buttonHeight) { var currentLevel = storage[button.upgradeType + 'Level'] || 1; if (currentLevel < storage.maxUpgradeTier && canAffordUpgrade(button.upgradeType, currentLevel)) { if (purchaseUpgrade(button.upgradeType)) { // Apply upgrade immediately self.applyUpgrade(button.upgradeType); // Update button states self.updateButtons(); // Visual feedback LK.effects.flashObject(button, 0x00FF00, 500); LK.getSound('award').play(); } } break; } } }; // Apply upgrades to castle immediately self.applyUpgrade = function (upgradeType) { var newLevel = storage[upgradeType + 'Level']; switch (upgradeType) { case 'fireRate': var fireRateBonus = getUpgradeBonus('fireRate', newLevel); castle.shootInterval = Math.max(20, Math.floor(60 * (1 - fireRateBonus))); break; case 'health': var healthBonus = getUpgradeBonus('health', newLevel); var newMaxHealth = 100 + healthBonus; var healthIncrease = newMaxHealth - castle.maxHealth; castle.maxHealth = newMaxHealth; castle.health += healthIncrease; // Add the health increase castleHealthBar.updateHealth(castle.health, castle.maxHealth); break; case 'damage': var damageBonus = getUpgradeBonus('damage', newLevel); castle.upgradedBulletDamage = 1.01 * (1 + damageBonus); break; case 'bulletCount': var bulletCountBonus = getUpgradeBonus('bulletCount', newLevel); castle.bulletsPerShot = 1 + bulletCountBonus; bulletsText.setText('Bullets: ' + castle.bulletsPerShot); break; case 'movementSpeed': var movementSpeedBonus = getUpgradeBonus('movementSpeed', newLevel); castleMovementSpeed = 400 * (1 + movementSpeedBonus); break; } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x2F4F2F }); /**** * Game Code ****/ // Game variables var castle; var enemies = []; var bullets = []; var rewards = []; var bombRewards = []; var bombs = []; var scrapMetalDrops = []; var shockwaves = []; var healthPacks = []; var waveTimer = 0; var enemySpawnTimer = 0; var rewardSpawnTimer = 0; var waveNumber = 1; var isMiniBossWave = false; var miniBossSpawned = false; var survivalTime = 0; // Track total survival time var lastMilestoneWave = 0; // Track last milestone for rewards // Initialize persistent storage with defaults // Current game scrap metal (resets each game) var currentScrapMetal = 0; // Upgrade tier system functions function getUpgradeCost(upgradeType, currentLevel) { var baseCosts = { fireRate: storage.fireRateBaseCost, movementSpeed: storage.movementSpeedBaseCost, health: storage.healthBaseCost, damage: storage.damageBaseCost, bulletCount: storage.bulletCountBaseCost, shield: storage.shieldBaseCost, shockwave: storage.shockwaveBaseCost }; var baseCost = baseCosts[upgradeType] || 100; // Progressive cost increase: base * (1.5^currentLevel) return Math.floor(baseCost * Math.pow(1.5, currentLevel - 1)); } function canAffordUpgrade(upgradeType, currentLevel) { if (currentLevel >= storage.maxUpgradeTier) return false; var cost = getUpgradeCost(upgradeType, currentLevel); return storage.scrapMetal + currentScrapMetal >= cost; } function purchaseUpgrade(upgradeType) { var currentLevel = storage[upgradeType + 'Level'] || 1; if (!canAffordUpgrade(upgradeType, currentLevel)) return false; var cost = getUpgradeCost(upgradeType, currentLevel); var totalScrap = storage.scrapMetal + currentScrapMetal; // Deduct cost and update storage if (currentScrapMetal >= cost) { currentScrapMetal -= cost; } else { var remainingCost = cost - currentScrapMetal; currentScrapMetal = 0; storage.scrapMetal -= remainingCost; } // Increase upgrade level storage[upgradeType + 'Level'] = currentLevel + 1; // Update scrap metal display scrapMetalText.setText('Scrap Metal: ' + (storage.scrapMetal + currentScrapMetal)); return true; } function getUpgradeBonus(upgradeType, level) { var bonuses = { fireRate: (level - 1) * 0.15, // 15% faster per level movementSpeed: (level - 1) * 0.25, // 25% faster per level health: (level - 1) * 20, // +20 health per level damage: (level - 1) * 0.2, // +20% damage per level bulletCount: level - 1, // +1 bullet per level shield: (level - 1) * 0.1, // 10% cooldown reduction per level shockwave: (level - 1) * 0.1 // 10% cooldown reduction per level }; return bonuses[upgradeType] || 0; } // Create background pattern function createBackground() { var bgContainer = new Container(); // Create simpler grid pattern with fewer random calculations for (var x = 0; x <= 2048; x += 240) { for (var y = 0; y <= 2732; y += 240) { // Main background tiles with minimal randomization var tile = LK.getAsset('bgTile', { anchorX: 0.5, anchorY: 0.5 }); tile.x = x; tile.y = y; bgContainer.addChild(tile); // Reduce accent frequency for better performance if ((x + y) % 960 === 0) { // Every 4th tile instead of random var accent = LK.getAsset('bgAccent', { anchorX: 0.5, anchorY: 0.5 }); accent.x = x; accent.y = y; accent.alpha = 0.5; bgContainer.addChild(accent); } } } // Set background container to lowest z-index game.addChildAt(bgContainer, 0); } // Initialize background createBackground(); // UI elements var scoreText = new Text2('0', { size: 60, fill: 0xFFFFFF }); scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); var bulletsText = new Text2('Bullets: 1', { size: 50, fill: 0x00FF00 }); bulletsText.anchor.set(0, 0); bulletsText.x = 50; bulletsText.y = 50; LK.gui.topLeft.addChild(bulletsText); var healthText = new Text2('Health: 100', { size: 50, fill: 0xFF0000 }); healthText.anchor.set(1, 0); LK.gui.topRight.addChild(healthText); var scrapMetalText = new Text2('Scrap Metal: 0', { size: 45, fill: 0xC0C0C0 }); scrapMetalText.anchor.set(0, 0); scrapMetalText.x = 50; scrapMetalText.y = 120; LK.gui.topLeft.addChild(scrapMetalText); // Wave counter var waveText = new Text2('Wave: 1', { size: 55, fill: 0xFFD700 }); waveText.anchor.set(1, 0); waveText.x = -50; waveText.y = 50; LK.gui.topRight.addChild(waveText); // Survival time counter var survivalTimeText = new Text2('Time: 0:00', { size: 45, fill: 0x88DDFF }); survivalTimeText.anchor.set(1, 0); survivalTimeText.x = -50; survivalTimeText.y = 120; LK.gui.topRight.addChild(survivalTimeText); // High score display var highScoreText = new Text2('Best: ' + storage.highScore + ' (Wave ' + storage.bestWave + ')', { size: 40, fill: 0xFFD700 }); highScoreText.anchor.set(0.5, 0); highScoreText.x = 0; highScoreText.y = 150; LK.gui.top.addChild(highScoreText); // Stats display var statsText = new Text2('Games: ' + storage.totalGamesPlayed + ' | Enemies: ' + storage.totalEnemiesDefeated, { size: 35, fill: 0xCCCCCC }); statsText.anchor.set(0.5, 0); statsText.x = 0; statsText.y = 200; LK.gui.top.addChild(statsText); // Castle health bar var castleHealthBar = new HealthBar(); castleHealthBar.x = 50; castleHealthBar.y = 300; LK.gui.topLeft.addChild(castleHealthBar); // Arrays for UI elements var enemyHealthBars = []; var threatIndicators = []; // Power-up status display var powerUpText = new Text2('', { size: 40, fill: 0x00FF88 }); powerUpText.anchor.set(0.5, 0); powerUpText.x = 0; powerUpText.y = 100; LK.gui.top.addChild(powerUpText); // Track active power-ups var activePowerUps = { shield: 0, extraBullets: 0 }; // Ability UI buttons var shieldButton = new Text2('SHIELD', { size: 45, fill: 0x0088FF }); shieldButton.anchor.set(0, 0); shieldButton.x = 50; shieldButton.y = 180; LK.gui.topLeft.addChild(shieldButton); var shockwaveButton = new Text2('SHOCKWAVE', { size: 45, fill: 0xFF4400 }); shockwaveButton.anchor.set(0, 0); shockwaveButton.x = 50; shockwaveButton.y = 240; LK.gui.topLeft.addChild(shockwaveButton); // Upgrade menu button var upgradeButton = new Text2('β‘ UPGRADES', { size: 50, fill: 0xFFD700 }); upgradeButton.anchor.set(0, 0); upgradeButton.x = 50; upgradeButton.y = 300; LK.gui.topLeft.addChild(upgradeButton); // Initialize castle castle = game.addChild(new Castle()); castle.x = 1024; castle.y = 1366; // Add scrap collection range indicator to castle var scrapIndicator = new ScrapCollectionIndicator(); scrapIndicator.x = castle.x; scrapIndicator.y = castle.y; game.addChild(scrapIndicator); // Apply permanent upgrades from storage var fireRateBonus = getUpgradeBonus('fireRate', storage.fireRateLevel); castle.shootInterval = Math.max(20, Math.floor(castle.shootInterval * (1 - fireRateBonus))); var healthBonus = getUpgradeBonus('health', storage.healthLevel); castle.maxHealth += healthBonus; castle.health = castle.maxHealth; // Start with full health var damageBonus = getUpgradeBonus('damage', storage.damageLevel); var baseBulletDamage = 1.01; // Store upgraded damage for bullets castle.upgradedBulletDamage = baseBulletDamage * (1 + damageBonus); // Apply permanent bullet count bonus (separate from temporary power-ups) var bulletCountBonus = getUpgradeBonus('bulletCount', storage.bulletCountLevel); castle.bulletsPerShot += bulletCountBonus; // Update bullets text with correct value after applying bonus bulletsText.setText('Bullets: ' + castle.bulletsPerShot); // Initialize castle health bar castleHealthBar.updateHealth(castle.health, castle.maxHealth); // Create upgrade menu var upgradeMenu = new UpgradeMenu(); upgradeMenu.visible = false; game.addChild(upgradeMenu); var upgradeMenuVisible = false; // Movement speed will be applied in the movement system var movementSpeedBonus = getUpgradeBonus('movementSpeed', storage.movementSpeedLevel); var baseCastleSpeed = 400; castleMovementSpeed = baseCastleSpeed * (1 + movementSpeedBonus); // Start background music LK.playMusic('bgmusic'); // Update game counter at start storage.totalGamesPlayed++; // Spawn functions function spawnEnemy() { var enemy; // Mini-boss wave logic - every 5th wave if (isMiniBossWave) { if (!miniBossSpawned) { // Always spawn a boss enemy as the first enemy of mini-boss wave enemy = new BossEnemy(); miniBossSpawned = true; } else { // Higher chance for tank/shield enemies in mini-boss waves var enemyType = Math.random(); if (enemyType < 0.4) { enemy = new TankEnemy(); } else if (enemyType < 0.7) { enemy = new ShieldEnemy(); } else if (enemyType < 0.85) { enemy = new BossEnemy(); } else { enemy = new BomberEnemy(); } } } else { // Normal wave enemy distribution with adaptive scaling var enemyType = Math.random(); // Increase chance of advanced enemies in later waves var advancedEnemyChance = Math.min(0.4, waveNumber * 0.02); // Up to 40% chance by wave 20 if (enemyType < 0.25) { enemy = new Enemy(); } else if (enemyType < 0.35) { enemy = new FastEnemy(); } else if (enemyType < 0.45) { enemy = new TankEnemy(); } else if (enemyType < 0.55) { enemy = new SniperEnemy(); } else if (enemyType < 0.63) { enemy = new BomberEnemy(); } else if (enemyType < 0.7) { enemy = new HealerEnemy(); } else if (enemyType < 0.76) { enemy = new ShieldEnemy(); } else if (enemyType < 0.82) { enemy = new TeleporterEnemy(); } else if (enemyType < 0.87) { enemy = new SplitterEnemy(); } else if (enemyType < 0.87 + advancedEnemyChance * 0.3) { // Adaptive enemies become more common in later waves enemy = new AdaptiveEnemy(); } else if (enemyType < 0.87 + advancedEnemyChance * 0.6) { // Regenerating enemies for sustained threat enemy = new RegenEnemy(); } else if (enemyType < 0.87 + advancedEnemyChance * 0.8) { // Elite enemies for high-value targets enemy = new EliteEnemy(); } else if (enemyType < 0.95) { enemy = new BossEnemy(); } else { // Super rare - spawn multiple elite enemies for extreme challenge enemy = new EliteEnemy(); var secondElite = new EliteEnemy(); secondElite.x = -40; // Will be positioned properly below secondElite.y = Math.random() * 2732; secondElite.lastX = secondElite.x; secondElite.lastY = secondElite.y; var dx2 = castle.x - secondElite.x; var dy2 = castle.y - secondElite.y; secondElite.lastCastleDistanceSquared = dx2 * dx2 + dy2 * dy2; // Apply scaling to second elite var healthMultiplier = 1 + (waveNumber - 1) * 0.05; var speedMultiplier = 1 + (waveNumber - 1) * 0.02; var damageMultiplier = 1 + (waveNumber - 1) * 0.05; secondElite.health = Math.ceil(secondElite.health * healthMultiplier); secondElite.maxHealth = secondElite.health; secondElite.speed = secondElite.speed * speedMultiplier; secondElite.damage = Math.ceil(secondElite.damage * damageMultiplier); if (isMiniBossWave) { secondElite.health = Math.ceil(secondElite.health * 1.25); secondElite.maxHealth = secondElite.health; secondElite.damage = Math.ceil(secondElite.damage * 1.15); } enemies.push(secondElite); game.addChild(secondElite); } } // Spawn from random edge var side = Math.floor(Math.random() * 4); switch (side) { case 0: // Top enemy.x = Math.random() * 2048; enemy.y = -20; break; case 1: // Right enemy.x = 2068; enemy.y = Math.random() * 2732; break; case 2: // Bottom enemy.x = Math.random() * 2048; enemy.y = 2752; break; case 3: // Left enemy.x = -20; enemy.y = Math.random() * 2732; break; } // Quantified enemy scaling: +5% health and +2% speed per wave var healthMultiplier = 1 + (waveNumber - 1) * 0.05; var speedMultiplier = 1 + (waveNumber - 1) * 0.02; var damageMultiplier = 1 + (waveNumber - 1) * 0.05; // Apply scaling with proper rounding enemy.health = Math.ceil(enemy.health * healthMultiplier); enemy.maxHealth = enemy.health; enemy.speed = enemy.speed * speedMultiplier; enemy.damage = Math.ceil(enemy.damage * damageMultiplier); // Extra scaling for mini-boss waves if (isMiniBossWave) { enemy.health = Math.ceil(enemy.health * 1.25); // 25% more health enemy.maxHealth = enemy.health; enemy.damage = Math.ceil(enemy.damage * 1.15); // 15% more damage } enemy.lastX = enemy.x; enemy.lastY = enemy.y; var dx = castle.x - enemy.x; var dy = castle.y - enemy.y; enemy.lastCastleDistanceSquared = dx * dx + dy * dy; enemies.push(enemy); game.addChild(enemy); } function spawnReward() { var rewardType = Math.random(); var reward; // Increased chance for health packs when castle health is low var healthPercentage = castle.health / castle.maxHealth; var healthPackChance = healthPercentage < 0.3 ? 0.25 : healthPercentage < 0.6 ? 0.15 : 0.1; // 60% regular reward, 25% bomb reward, 15% health pack (adjusted based on health) if (rewardType < 0.6) { reward = new Reward(); rewards.push(reward); } else if (rewardType < 0.85) { reward = new BombReward(); bombRewards.push(reward); } else { reward = new HealthPack(); healthPacks.push(reward); } // Spawn in accessible areas further away from the castle var angle = Math.random() * Math.PI * 2; var distance = 400 + Math.random() * 500; reward.x = castle.x + Math.cos(angle) * distance; reward.y = castle.y + Math.sin(angle) * distance; // Keep within bounds reward.x = Math.max(100, Math.min(1948, reward.x)); reward.y = Math.max(100, Math.min(2632, reward.y)); reward.baseY = reward.y; game.addChild(reward); // Add spawn animation with tween reward.scaleX = 0; reward.scaleY = 0; tween(reward, { scaleX: 1, scaleY: 1 }, { duration: 300, easing: tween.easeOut }); } // Castle movement system with configurable speed // Note: castleMovementSpeed is set above after applying movement speed bonus var targetX = 1024; var targetY = 1366; var isMoving = false; game.down = function (x, y, obj) { // If upgrade menu is visible, let it handle the input if (upgradeMenuVisible) { upgradeMenu.down(x, y, obj); return; } // Check if clicking on ability buttons var buttonClicked = false; // Check shield button area (approximate) if (x >= 50 && x <= 200 && y >= 180 && y <= 220) { castle.activateShield(); buttonClicked = true; } // Check shockwave button area (approximate) else if (x >= 50 && x <= 250 && y >= 240 && y <= 280) { castle.activateShockwave(); buttonClicked = true; } // Check upgrade button area else if (x >= 50 && x <= 280 && y >= 300 && y <= 350) { upgradeMenuVisible = !upgradeMenuVisible; upgradeMenu.visible = upgradeMenuVisible; if (upgradeMenuVisible) { upgradeMenu.updateButtons(); } buttonClicked = true; } // Only move castle if not clicking on ability buttons if (!buttonClicked) { // Set new target position targetX = x; targetY = y; // Calculate distance to determine movement duration var dx = targetX - castle.x; var dy = targetY - castle.y; var distance = Math.sqrt(dx * dx + dy * dy); // Calculate duration based on configurable movement speed var duration = Math.max(150, distance / castleMovementSpeed * 1000); // minimum 150ms for responsiveness // Stop any existing movement tween tween.stop(castle, { x: true, y: true }); isMoving = true; // Add walking animation with slight bobbing effect var originalY = castle.y; // Start smooth walking animation with subtle bounce tween(castle, { x: targetX, y: targetY }, { duration: duration, easing: tween.easeInOut, onFinish: function onFinish() { isMoving = false; // Subtle landing effect tween(castle, { scaleX: 1.1, scaleY: 0.9 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(castle, { scaleX: 1, scaleY: 1 }, { duration: 100, easing: tween.easeOut }); } }); } }); // Add walking bobbing animation during movement if (duration > 300) { var bobDuration = duration / 3; tween(castle, { scaleY: 1.05 }, { duration: bobDuration, easing: tween.easeInOut, onFinish: function onFinish() { tween(castle, { scaleY: 1 }, { duration: bobDuration, easing: tween.easeInOut }); } }); } } }; game.move = function (x, y, obj) { // Update target while moving (allows for path correction) if (isMoving) { targetX = x; targetY = y; // Calculate new distance and duration var dx = targetX - castle.x; var dy = targetY - castle.y; var distance = Math.sqrt(dx * dx + dy * dy); // Only update if the new target is significantly different if (distance > 40) { var duration = Math.max(150, distance / castleMovementSpeed * 1000); // Stop current tween and start new one tween.stop(castle, { x: true, y: true }); tween(castle, { x: targetX, y: targetY }, { duration: duration, easing: tween.easeInOut, onFinish: function onFinish() { isMoving = false; } }); } } }; game.up = function (x, y, obj) { // Optional: Could add final position adjustment here if needed }; // Game update loop game.update = function () { // Update wave timer waveTimer++; // Spawn enemies enemySpawnTimer++; var spawnRate = Math.max(30, 120 - waveNumber * 5); // Faster spawning each wave // Increase spawn rate for mini-boss waves if (isMiniBossWave) { spawnRate = Math.max(20, spawnRate * 0.7); // 30% faster spawning in mini-boss waves } if (enemySpawnTimer >= spawnRate) { spawnEnemy(); enemySpawnTimer = 0; } // Spawn rewards occasionally - tied to enemy defeats for better balance rewardSpawnTimer++; var enemiesDefeated = Math.floor(LK.getScore() / 10); // Approximate enemies defeated based on score var rewardThreshold = 600 - Math.min(300, enemiesDefeated * 5); // Faster spawning as more enemies defeated if (rewardSpawnTimer >= rewardThreshold && rewards.length + bombRewards.length < 3) { // Spawn rate increases with progress, max 3 rewards total spawnReward(); rewardSpawnTimer = 0; } // Advance wave every 30 seconds if (waveTimer >= 1800) { // 30 seconds at 60fps // Wave completion bonus before advancing var waveBonus = waveNumber * 50; // 50 points per wave number var scrapBonus = Math.floor(waveNumber / 2) + 1; // 1-2 scrap metal per wave LK.setScore(LK.getScore() + waveBonus); currentScrapMetal += scrapBonus; // Show wave completion bonus var bonusText = new Text2('WAVE ' + waveNumber + ' COMPLETE!\n+' + waveBonus + ' SCORE +' + scrapBonus + ' SCRAP', { size: 60, fill: 0x00FF88 }); bonusText.anchor.set(0.5, 0.5); bonusText.x = 1024; bonusText.y = 600; game.addChild(bonusText); // Animate wave completion bonus tween(bonusText, { scaleX: 1.2, scaleY: 1.2 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { tween(bonusText, { alpha: 0, y: bonusText.y - 100 }, { duration: 1500, onFinish: function onFinish() { bonusText.destroy(); } }); } }); waveNumber++; waveTimer = 0; miniBossSpawned = false; // Reset mini-boss spawn flag // Check if this is a mini-boss wave (every 5th wave) isMiniBossWave = waveNumber % 5 === 0; if (isMiniBossWave) { // Special visual effect for mini-boss waves LK.effects.flashScreen(0xFF4400, 1000); // Orange flash for mini-boss // Bigger bonus for mini-boss waves var miniBossBonus = waveNumber * 100; var miniBossScrapBonus = Math.floor(waveNumber / 3) + 3; LK.setScore(LK.getScore() + miniBossBonus); currentScrapMetal += miniBossScrapBonus; // Show wave announcement var waveAnnouncementText = new Text2('MINI-BOSS WAVE ' + waveNumber + '\nBONUS: +' + miniBossBonus + ' SCORE +' + miniBossScrapBonus + ' SCRAP', { size: 70, fill: 0xFF4400 }); waveAnnouncementText.anchor.set(0.5, 0.5); waveAnnouncementText.x = 1024; waveAnnouncementText.y = 800; game.addChild(waveAnnouncementText); // Fade out wave text after 3 seconds for mini-boss tween(waveAnnouncementText, { alpha: 0 }, { duration: 3000, onFinish: function onFinish() { waveAnnouncementText.destroy(); } }); } else if (waveNumber >= 10 && waveNumber % 10 === 0) { // Show special threat warning every 10 waves var threatWarningText = new Text2('THREAT LEVEL INCREASED!\nWAVE ' + waveNumber + ' - ADAPTIVE ENEMIES INCOMING', { size: 65, fill: 0x8A2BE2 }); threatWarningText.anchor.set(0.5, 0.5); threatWarningText.x = 1024; threatWarningText.y = 800; game.addChild(threatWarningText); LK.effects.flashScreen(0x8A2BE2, 800); tween(threatWarningText, { alpha: 0 }, { duration: 2500, onFinish: function onFinish() { threatWarningText.destroy(); } }); } else { // Normal wave flash LK.effects.flashScreen(0x0066FF, 500); } } // Check bullet-enemy collisions for (var b = bullets.length - 1; b >= 0; b--) { var bullet = bullets[b]; var bulletHit = false; // Early exit if bullet is off-screen if (bullet.x < -100 || bullet.x > 2148 || bullet.y < -100 || bullet.y > 2832) { bullet.destroy(); bullets.splice(b, 1); continue; } for (var e = enemies.length - 1; e >= 0; e--) { var enemy = enemies[e]; if (bullet.intersects(enemy)) { enemy.takeDamage(bullet.damage); bullet.destroy(); bullets.splice(b, 1); bulletHit = true; break; } } if (bulletHit) continue; } // Check castle-reward collisions for pickup for (var r = rewards.length - 1; r >= 0; r--) { var reward = rewards[r]; if (castle.intersects(reward)) { // Define maximum bullet limit to prevent excessive stacking var maxBullets = 8; if (castle.bulletsPerShot < maxBullets) { // Add 1 bullet per reward (consistent increment) castle.bulletsPerShot++; bulletsText.setText('Bullets: ' + castle.bulletsPerShot); LK.getSound('award').play(); LK.effects.flashObject(castle, 0x00FF00, 500); // Visual feedback for bullet upgrade with scaling effect tween(castle, { scaleX: 1.2, scaleY: 1.2 }, { duration: 200, easing: tween.easeOut, onFinish: function onFinish() { tween(castle, { scaleX: 1, scaleY: 1 }, { duration: 200, easing: tween.easeIn }); } }); } else { // At maximum bullets - give score bonus instead LK.setScore(LK.getScore() + 100); scoreText.setText(LK.getScore()); bulletsText.setText('Bullets: ' + castle.bulletsPerShot + ' (MAX)'); LK.getSound('award').play(); LK.effects.flashObject(castle, 0xFFD700, 500); // Gold flash for max bonus // Brief text notification var maxText = new Text2('+100 SCORE!', { size: 40, fill: 0xFFD700 }); maxText.anchor.set(0.5, 0.5); maxText.x = castle.x; maxText.y = castle.y - 60; game.addChild(maxText); // Animate and remove notification tween(maxText, { y: maxText.y - 50, alpha: 0 }, { duration: 1500, onFinish: function onFinish() { maxText.destroy(); } }); } reward.destroy(); rewards.splice(r, 1); } } // Check castle-bomb reward collisions for pickup for (var br = bombRewards.length - 1; br >= 0; br--) { var bombReward = bombRewards[br]; if (castle.intersects(bombReward)) { // Schedule bomb to appear at character position after 3 seconds var bombX = castle.x; var bombY = castle.y; LK.setTimeout(function () { var bomb = new Bomb(); bomb.x = bombX; bomb.y = bombY; bombs.push(bomb); game.addChild(bomb); }, 3000); LK.getSound('award').play(); LK.effects.flashObject(castle, 0xFF8800, 500); bombReward.destroy(); bombRewards.splice(br, 1); } } // Scrap metal collection is now handled automatically in ScrapMetalDrop class // Check castle-health pack collisions for pickup for (var hp = healthPacks.length - 1; hp >= 0; hp--) { var healthPack = healthPacks[hp]; if (castle.intersects(healthPack)) { // Heal castle var healAmount = Math.min(healthPack.healAmount, castle.maxHealth - castle.health); if (healAmount > 0) { castle.health += healAmount; LK.getSound('award').play(); LK.effects.flashObject(castle, 0x44FF44, 500); // Update health bar if (castleHealthBar) { castleHealthBar.updateHealth(castle.health, castle.maxHealth); } // Show heal amount var healText = new Text2('+' + healAmount + ' HEALTH', { size: 40, fill: 0x44FF44 }); healText.anchor.set(0.5, 0.5); healText.x = castle.x; healText.y = castle.y - 60; game.addChild(healText); // Animate heal text tween(healText, { y: healText.y - 50, alpha: 0 }, { duration: 1500, onFinish: function onFinish() { healText.destroy(); } }); } healthPack.destroy(); healthPacks.splice(hp, 1); } } // Update ability button text with cooldown status if (castle.shieldCooldown > 0) { var shieldSeconds = Math.ceil(castle.shieldCooldown / 60); shieldButton.setText('SHIELD (' + shieldSeconds + 's)'); shieldButton.fill = 0x666666; // Grayed out } else { shieldButton.setText('SHIELD'); shieldButton.fill = 0x0088FF; // Active color } if (castle.shockwaveCooldown > 0) { var shockwaveSeconds = Math.ceil(castle.shockwaveCooldown / 60); shockwaveButton.setText('SHOCKWAVE (' + shockwaveSeconds + 's)'); shockwaveButton.fill = 0x666666; // Grayed out } else { shockwaveButton.setText('SHOCKWAVE'); shockwaveButton.fill = 0xFF4400; // Active color } // Update survival time survivalTime++; var minutes = Math.floor(survivalTime / 3600); // 60 seconds * 60 fps var seconds = Math.floor(survivalTime % 3600 / 60); var timeString = minutes + ':' + (seconds < 10 ? '0' : '') + seconds; // Update wave counter and survival time waveText.setText('Wave: ' + waveNumber); survivalTimeText.setText('Time: ' + timeString); // Check for survival milestones (every 10 waves) if (waveNumber >= lastMilestoneWave + 10) { lastMilestoneWave = waveNumber; // Milestone rewards var milestoneScoreBonus = waveNumber * 200; var milestoneScrapBonus = Math.floor(waveNumber / 5) + 5; LK.setScore(LK.getScore() + milestoneScoreBonus); currentScrapMetal += milestoneScrapBonus; // Show milestone achievement var milestoneText = new Text2('SURVIVAL MILESTONE!\nWAVE ' + waveNumber + ' REACHED\n+' + milestoneScoreBonus + ' SCORE +' + milestoneScrapBonus + ' SCRAP', { size: 55, fill: 0xFFD700 }); milestoneText.anchor.set(0.5, 0.5); milestoneText.x = 1024; milestoneText.y = 1000; game.addChild(milestoneText); // Special visual effects for milestone LK.effects.flashScreen(0xFFD700, 1200); tween(milestoneText, { scaleX: 1.3, scaleY: 1.3 }, { duration: 800, easing: tween.easeOut, onFinish: function onFinish() { tween(milestoneText, { alpha: 0, y: milestoneText.y - 150 }, { duration: 2000, onFinish: function onFinish() { milestoneText.destroy(); } }); } }); } // Update power-up display var powerUpDisplay = ''; if (castle.shield) { var shieldTime = Math.ceil(castle.shield.duration / 60); powerUpDisplay += 'SHIELD: ' + shieldTime + 's '; } var permanentBullets = getUpgradeBonus('bulletCount', storage.bulletCountLevel) + 1; var tempBullets = castle.bulletsPerShot - permanentBullets; if (tempBullets > 0) { powerUpDisplay += '+' + tempBullets + ' BULLETS '; } powerUpText.setText(powerUpDisplay); // Update threat indicators // Clear old indicators for (var ti = threatIndicators.length - 1; ti >= 0; ti--) { threatIndicators[ti].destroy(); threatIndicators.splice(ti, 1); } // Create threat indicators for enemies near screen edges for (var ei = 0; ei < enemies.length; ei++) { var enemy = enemies[ei]; var indicator = null; // Check if enemy is near edges and create indicators if (enemy.x < 100) { // Left edge indicator = new ThreatIndicator(); indicator.x = 50; indicator.y = Math.max(100, Math.min(2632, enemy.y)); indicator.rotation = Math.PI; // Point right } else if (enemy.x > 1948) { // Right edge indicator = new ThreatIndicator(); indicator.x = 1998; indicator.y = Math.max(100, Math.min(2632, enemy.y)); indicator.rotation = 0; // Point left } else if (enemy.y < 100) { // Top edge indicator = new ThreatIndicator(); indicator.x = Math.max(100, Math.min(1948, enemy.x)); indicator.y = 50; indicator.rotation = Math.PI / 2; // Point down } else if (enemy.y > 2632) { // Bottom edge indicator = new ThreatIndicator(); indicator.x = Math.max(100, Math.min(1948, enemy.x)); indicator.y = 2682; indicator.rotation = -Math.PI / 2; // Point up } if (indicator) { threatIndicators.push(indicator); game.addChild(indicator); } } // Update scrap collection indicator position if (scrapIndicator) { scrapIndicator.x = castle.x; scrapIndicator.y = castle.y; } // Update UI with tier information var totalScrap = storage.scrapMetal + currentScrapMetal; var scrapDisplay = 'Scrap: ' + totalScrap; // Show next upgrade cost for fire rate as example if (storage.fireRateLevel < storage.maxUpgradeTier) { var nextCost = getUpgradeCost('fireRate', storage.fireRateLevel); scrapDisplay += ' | Fire Rate T' + storage.fireRateLevel + 'βT' + (storage.fireRateLevel + 1) + ': ' + nextCost; } else { scrapDisplay += ' | Fire Rate: MAX'; } scrapMetalText.setText(scrapDisplay); healthText.setText('Health: ' + castle.health + '/' + castle.maxHealth); scoreText.setText(LK.getScore()); // Update high score display with current vs best comparison var currentScore = LK.getScore(); if (currentScore > storage.highScore) { highScoreText.setText('BEST: ' + currentScore + ' (Wave ' + waveNumber + ') NEW!'); highScoreText.fill = 0x00FF00; // Green for new record } else { highScoreText.setText('Best: ' + storage.highScore + ' (Wave ' + storage.bestWave + ') | Current: ' + currentScore); highScoreText.fill = 0xFFD700; // Gold for normal display } // Update stats display statsText.setText('Games: ' + storage.totalGamesPlayed + ' | Enemies: ' + storage.totalEnemiesDefeated + ' | Best Time: ' + Math.floor(storage.longestSurvivalTime / 3600) + ':' + (Math.floor(storage.longestSurvivalTime % 3600 / 60) < 10 ? '0' : '') + Math.floor(storage.longestSurvivalTime % 3600 / 60)); // Update upgrade menu if visible if (upgradeMenuVisible && upgradeMenu.visible) { upgradeMenu.updateButtons(); } // Update upgrade button text based on available scrap var upgradeButtonText = 'β‘ UPGRADES'; var totalScrap = storage.scrapMetal + currentScrapMetal; if (totalScrap > 0) { upgradeButtonText += ' (' + totalScrap + ')'; } upgradeButton.setText(upgradeButtonText); };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
scrapMetal: 0,
fireRateLevel: 1,
movementSpeedLevel: 1,
healthLevel: 1,
damageLevel: 1,
bulletCountLevel: 1,
shieldLevel: 1,
shockwaveLevel: 1,
maxUpgradeTier: 10,
fireRateBaseCost: 50,
movementSpeedBaseCost: 75,
healthBaseCost: 100,
damageBaseCost: 80,
bulletCountBaseCost: 120,
shieldBaseCost: 150,
shockwaveBaseCost: 200,
highScore: 0,
bestWave: 1,
longestSurvivalTime: 0,
totalGamesPlayed: 0,
totalEnemiesDefeated: 0,
achievementUnlocked: false
});
/****
* Classes
****/
var AdaptiveEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
enemyGraphics.tint = 0x8A2BE2; // Purple for adaptive enemy
self.health = 4;
self.maxHealth = 4;
self.speed = 1.2;
self.damage = 12;
self.adaptationLevel = 0;
self.adaptationTimer = 0;
self.adaptationInterval = 300; // Adapt every 5 seconds
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistanceSquared = Infinity;
// Create health bar for this enemy
self.healthBar = new EnemyHealthBar();
self.healthBar.y = -50; // Position above enemy
enemyHealthBars.push(self.healthBar);
self.addChild(self.healthBar);
self.update = function () {
// Adaptation logic - enemy becomes stronger over time
self.adaptationTimer++;
if (self.adaptationTimer >= self.adaptationInterval && self.adaptationLevel < 3) {
self.adaptationLevel++;
self.adaptationTimer = 0;
// Increase stats based on adaptation level
self.speed += 0.3;
self.damage += 3;
self.maxHealth += 2;
self.health = Math.min(self.maxHealth, self.health + 2);
// Visual feedback for adaptation
LK.effects.flashObject(self, 0x8A2BE2, 500);
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
// Move toward castle
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distanceSquared = dx * dx + dy * dy;
if (distanceSquared > 3600) {
var distance = Math.sqrt(distanceSquared);
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
// Check if reached castle
if (self.lastCastleDistanceSquared > 3600 && distanceSquared <= 3600) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistanceSquared = distanceSquared;
};
self.takeDamage = function (damage) {
// Adaptive enemy takes less damage as it adapts
var damagereduction = self.adaptationLevel * 0.1;
var actualDamage = damage * (1 - damagereduction);
self.health -= actualDamage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Update health bar
if (self.healthBar) {
self.healthBar.updateHealth(self.health, self.maxHealth);
}
if (self.health <= 0) {
LK.setScore(LK.getScore() + 25 + self.adaptationLevel * 10); // More points for adapted enemies
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
// Drop more scrap metal based on adaptation level
if (Math.random() < 0.7 + self.adaptationLevel * 0.1) {
var scrap = new ScrapMetalDrop();
scrap.x = self.x;
scrap.y = self.y;
scrap.baseY = scrap.y;
scrap.value = 5 + self.adaptationLevel * 2; // More valuable based on adaptation
scrapMetalDrops.push(scrap);
game.addChild(scrap);
}
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var Bomb = Container.expand(function () {
var self = Container.call(this);
var bombGraphics = self.attachAsset('bomb', {
anchorX: 0.5,
anchorY: 0.5
});
bombGraphics.tint = 0xFF0000;
self.explosionRadius = 1000;
self.fuseTime = 120; // 2 seconds at 60fps
self.blinkSpeed = 10;
// Start with small scale
self.scaleX = 0.3;
self.scaleY = 0.3;
self.update = function () {
self.fuseTime--;
// Blinking effect that gets faster as explosion approaches
var blinkInterval = Math.max(5, Math.floor(self.fuseTime / 10));
if (LK.ticks % blinkInterval === 0) {
bombGraphics.alpha = bombGraphics.alpha === 1 ? 0.3 : 1;
}
// Scale pulsing effect
var pulseScale = 1 + Math.sin(LK.ticks * 0.3) * 0.1;
self.scaleX = pulseScale;
self.scaleY = pulseScale;
// Explode when fuse runs out
if (self.fuseTime <= 0) {
self.explode();
}
};
self.explode = function () {
// Visual explosion effect
LK.effects.flashScreen(0xFF4400, 600);
// Scale up rapidly during explosion
tween(self, {
scaleX: 3,
scaleY: 3
}, {
duration: 300,
easing: tween.easeOut
});
// Damage enemies in radius
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.explosionRadius) {
// Instant kill for enemies in blast radius
enemy.takeDamage(999);
}
}
// Remove bomb from game after explosion animation
LK.setTimeout(function () {
self.destroy();
for (var i = bombs.length - 1; i >= 0; i--) {
if (bombs[i] === self) {
bombs.splice(i, 1);
break;
}
}
}, 300);
};
self.activateShield = function () {
if (self.shieldCooldown <= 0 && !self.shield) {
// Create shield
self.shield = new Shield();
self.shield.x = self.x;
self.shield.y = self.y;
game.addChild(self.shield);
// Start cooldown
var cooldownReduction = getUpgradeBonus('shield', storage.shieldLevel);
self.shieldCooldown = Math.floor(self.shieldMaxCooldown * (1 - cooldownReduction));
// Visual feedback
LK.effects.flashScreen(0x0088FF, 300);
}
};
self.activateShockwave = function () {
if (self.shockwaveCooldown <= 0) {
// Create shockwave
var shockwave = new Shockwave();
shockwave.x = self.x;
shockwave.y = self.y;
// Increase damage based on upgrade level
shockwave.damage = 5 + getUpgradeBonus('shockwave', storage.shockwaveLevel) * 20; // +2 damage per level (0.1 * 20 = 2)
shockwaves.push(shockwave);
game.addChild(shockwave);
// Start cooldown
var cooldownReduction = getUpgradeBonus('shockwave', storage.shockwaveLevel);
self.shockwaveCooldown = Math.floor(self.shockwaveMaxCooldown * (1 - cooldownReduction));
// Visual feedback
LK.effects.flashScreen(0xFF4400, 500);
}
};
self.update = function () {
// Update ability cooldowns
if (self.shieldCooldown > 0) {
self.shieldCooldown--;
}
if (self.shockwaveCooldown > 0) {
self.shockwaveCooldown--;
}
// Update shield position if active
if (self.shield) {
self.shield.x = self.x;
self.shield.y = self.y;
}
if (self.shootCooldown > 0) {
self.shootCooldown--;
}
// Auto-fire with priority targeting system
if (self.shootCooldown <= 0 && enemies.length > 0) {
var targetEnemy = null;
var bestPriority = -1;
var bestDistance = Infinity;
// Priority levels: Higher number = higher priority
var priorityMap = {
'SniperEnemy': 5,
// Highest priority - long range damage
'BomberEnemy': 4,
// High priority - explosive threat
'BossEnemy': 4,
// High priority - powerful enemy
'FastEnemy': 3,
// Medium-high priority - quick threat
'HealerEnemy': 3,
// Medium-high priority - supports others
'TeleporterEnemy': 2,
// Medium priority - unpredictable
'SplitterEnemy': 2,
// Medium priority - creates more enemies
'TankEnemy': 1,
// Low priority - slow but tough
'ShieldEnemy': 1,
// Low priority - tough but manageable
'Enemy': 0 // Lowest priority - basic enemy
};
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Determine enemy type by checking constructor name
var enemyType = 'Enemy'; // Default
if (enemy.constructor === SniperEnemy) enemyType = 'SniperEnemy';else if (enemy.constructor === BomberEnemy) enemyType = 'BomberEnemy';else if (enemy.constructor === BossEnemy) enemyType = 'BossEnemy';else if (enemy.constructor === FastEnemy) enemyType = 'FastEnemy';else if (enemy.constructor === HealerEnemy) enemyType = 'HealerEnemy';else if (enemy.constructor === TeleporterEnemy) enemyType = 'TeleporterEnemy';else if (enemy.constructor === SplitterEnemy) enemyType = 'SplitterEnemy';else if (enemy.constructor === TankEnemy) enemyType = 'TankEnemy';else if (enemy.constructor === ShieldEnemy) enemyType = 'ShieldEnemy';
var priority = priorityMap[enemyType] || 0;
// Select target based on priority first, then distance
if (priority > bestPriority || priority === bestPriority && distance < bestDistance) {
bestPriority = priority;
bestDistance = distance;
targetEnemy = enemy;
}
}
if (targetEnemy) {
self.fireAtTarget(targetEnemy);
self.shootCooldown = self.shootInterval;
}
}
};
return self;
});
var BombReward = Container.expand(function () {
var self = Container.call(this);
var rewardGraphics = self.attachAsset('bombReward', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it visually distinct with orange tint
rewardGraphics.tint = 0xFF8800;
self.bobOffset = Math.random() * Math.PI * 2;
self.baseY = 0;
self.lifetime = 420; // 7 seconds at 60fps
self.update = function () {
// Gentle bobbing animation
self.y = self.baseY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 5;
// Decrease lifetime
self.lifetime--;
// Start fading when close to expiring
if (self.lifetime <= 120) {
var alpha = self.lifetime / 120;
rewardGraphics.alpha = alpha;
// Add flashing animation when close to expiring
var flashInterval = Math.max(5, Math.floor(self.lifetime / 20));
if (LK.ticks % flashInterval === 0) {
tween(rewardGraphics, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: flashInterval * 8,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(rewardGraphics, {
scaleX: 1,
scaleY: 1
}, {
duration: flashInterval * 8,
easing: tween.easeIn
});
}
});
}
}
// Remove when lifetime expires
if (self.lifetime <= 0) {
tween(self, {
scaleX: 0,
scaleY: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
for (var i = bombRewards.length - 1; i >= 0; i--) {
if (bombRewards[i] === self) {
bombRewards.splice(i, 1);
break;
}
}
}
});
}
};
self.down = function (x, y, obj) {
// Schedule bomb to appear at character position after 3 seconds
var bombX = castle.x;
var bombY = castle.y;
LK.setTimeout(function () {
var bomb = new Bomb();
bomb.x = bombX;
bomb.y = bombY;
bombs.push(bomb);
game.addChild(bomb);
}, 3000);
LK.getSound('award').play();
LK.effects.flashObject(castle, 0xFF8800, 500);
self.destroy();
for (var i = bombRewards.length - 1; i >= 0; i--) {
if (bombRewards[i] === self) {
bombRewards.splice(i, 1);
break;
}
}
};
return self;
});
var BomberEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('bomberEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 3;
self.maxHealth = 3;
self.speed = 1.2;
self.damage = 12;
self.explosionRadius = 100;
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistance = Infinity;
self.update = function () {
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 60) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
var currentDistance = Math.sqrt(dx * dx + dy * dy);
if (self.lastCastleDistance > 60 && currentDistance <= 60) {
castle.takeDamage(self.damage);
self.explode();
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistance = currentDistance;
};
self.explode = function () {
LK.effects.flashScreen(0xFF8800, 300);
// Damage nearby enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy !== self) {
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.explosionRadius) {
enemy.takeDamage(2);
}
}
}
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
if (self.health <= 0) {
LK.setScore(LK.getScore() + 30);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
self.explode();
}
};
return self;
});
var BossEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('bossEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 15;
self.maxHealth = 15;
self.speed = 0.6;
self.damage = 30;
self.spawnCooldown = 0;
self.spawnInterval = 300; // Spawns minions every 5 seconds
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistance = Infinity;
self.update = function () {
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 60) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
// Spawn minion enemies
if (self.spawnCooldown > 0) {
self.spawnCooldown--;
}
if (self.spawnCooldown <= 0 && enemies.length < 15) {
var minion = new Enemy();
var angle = Math.random() * Math.PI * 2;
minion.x = self.x + Math.cos(angle) * 60;
minion.y = self.y + Math.sin(angle) * 60;
minion.lastX = minion.x;
minion.lastY = minion.y;
var dx2 = castle.x - minion.x;
var dy2 = castle.y - minion.y;
minion.lastCastleDistance = Math.sqrt(dx2 * dx2 + dy2 * dy2);
enemies.push(minion);
game.addChild(minion);
self.spawnCooldown = self.spawnInterval;
}
var currentDistance = Math.sqrt(dx * dx + dy * dy);
if (self.lastCastleDistance > 60 && currentDistance <= 60) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistance = currentDistance;
};
self.takeDamage = function (damage) {
var reducedDamage = damage * 0.7; // Boss takes 30% less damage
self.health -= reducedDamage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
if (self.health <= 0) {
LK.setScore(LK.getScore() + 100);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
LK.effects.flashScreen(0x440044, 1000);
// Drop multiple scrap metal (guaranteed)
for (var c = 0; c < 3; c++) {
var scrap = new ScrapMetalDrop();
var angle = Math.random() * Math.PI * 2;
var distance = 20 + Math.random() * 40;
scrap.x = self.x + Math.cos(angle) * distance;
scrap.y = self.y + Math.sin(angle) * distance;
scrap.baseY = scrap.y;
scrap.value = 15; // High value for boss
scrapMetalDrops.push(scrap);
game.addChild(scrap);
}
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 50;
self.velocityX = 0;
self.velocityY = 0;
self.damage = 1.01;
self.lastX = 0;
self.lastY = 0;
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
self.x += self.velocityX;
self.y += self.velocityY;
// Add spinning animation
self.rotation += 0.2;
// Remove if off screen
if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782) {
self.destroy();
for (var i = bullets.length - 1; i >= 0; i--) {
if (bullets[i] === self) {
bullets.splice(i, 1);
break;
}
}
}
};
return self;
});
var Castle = Container.expand(function () {
var self = Container.call(this);
var castleGraphics = self.attachAsset('castle', {
anchorX: 0.5,
anchorY: 0.5
});
// Set physical diameter to match image dimensions
self.width = castleGraphics.width;
self.height = castleGraphics.height;
self.health = 100;
self.maxHealth = 100;
self.bulletsPerShot = 1;
self.shootCooldown = 0;
self.shootInterval = 60; // Fire every second at 60fps
// Special abilities
self.shield = null;
self.shieldCooldown = 0;
self.shieldMaxCooldown = 900; // 15 seconds at 60fps
self.shockwaveCooldown = 0;
self.shockwaveMaxCooldown = 1200; // 20 seconds at 60fps
self.update = function () {
if (self.shootCooldown > 0) {
self.shootCooldown--;
}
// Auto-fire with priority targeting system
if (self.shootCooldown <= 0 && enemies.length > 0) {
var targetEnemy = null;
var bestPriority = -1;
var bestDistance = Infinity;
// Priority levels: Higher number = higher priority
var priorityMap = {
'EliteEnemy': 6,
// Highest priority - extremely dangerous
'SniperEnemy': 5,
// Highest priority - long range damage
'AdaptiveEnemy': 5,
// Highest priority - becomes stronger over time
'BomberEnemy': 4,
// High priority - explosive threat
'BossEnemy': 4,
// High priority - powerful enemy
'RegenEnemy': 4,
// High priority - heals over time
'FastEnemy': 3,
// Medium-high priority - quick threat
'HealerEnemy': 3,
// Medium-high priority - supports others
'TeleporterEnemy': 2,
// Medium priority - unpredictable
'SplitterEnemy': 2,
// Medium priority - creates more enemies
'TankEnemy': 1,
// Low priority - slow but tough
'ShieldEnemy': 1,
// Low priority - tough but manageable
'Enemy': 0 // Lowest priority - basic enemy
};
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Determine enemy type by checking constructor name
var enemyType = 'Enemy'; // Default
if (enemy.constructor === SniperEnemy) enemyType = 'SniperEnemy';else if (enemy.constructor === BomberEnemy) enemyType = 'BomberEnemy';else if (enemy.constructor === BossEnemy) enemyType = 'BossEnemy';else if (enemy.constructor === FastEnemy) enemyType = 'FastEnemy';else if (enemy.constructor === HealerEnemy) enemyType = 'HealerEnemy';else if (enemy.constructor === TeleporterEnemy) enemyType = 'TeleporterEnemy';else if (enemy.constructor === SplitterEnemy) enemyType = 'SplitterEnemy';else if (enemy.constructor === TankEnemy) enemyType = 'TankEnemy';else if (enemy.constructor === ShieldEnemy) enemyType = 'ShieldEnemy';else if (enemy.constructor === AdaptiveEnemy) enemyType = 'AdaptiveEnemy';else if (enemy.constructor === RegenEnemy) enemyType = 'RegenEnemy';else if (enemy.constructor === EliteEnemy) enemyType = 'EliteEnemy';
var priority = priorityMap[enemyType] || 0;
// Select target based on priority first, then distance
if (priority > bestPriority || priority === bestPriority && distance < bestDistance) {
bestPriority = priority;
bestDistance = distance;
targetEnemy = enemy;
}
}
if (targetEnemy) {
self.fireAtTarget(targetEnemy);
self.shootCooldown = self.shootInterval;
}
}
};
self.fireAtTarget = function (target) {
var baseAngle = Math.atan2(target.y - self.y, target.x - self.x);
var spreadAngle = Math.PI / 6; // 30 degrees spread
for (var i = 0; i < self.bulletsPerShot; i++) {
var bullet = new Bullet();
bullet.x = self.x;
bullet.y = self.y;
// Apply upgraded damage (always use upgraded damage)
bullet.damage = self.upgradedBulletDamage || bullet.damage;
var offset = 0;
if (self.bulletsPerShot > 1) {
offset = (i - (self.bulletsPerShot - 1) / 2) * (spreadAngle / Math.max(1, self.bulletsPerShot - 1));
}
var angle = baseAngle + offset;
bullet.velocityX = Math.cos(angle) * bullet.speed;
bullet.velocityY = Math.sin(angle) * bullet.speed;
// Set bullet rotation to match direction
bullet.rotation = angle;
bullets.push(bullet);
game.addChild(bullet);
}
LK.getSound('shoot').play();
};
self.takeDamage = function (damage) {
// Check if shield is active
if (self.shield) {
// Shield blocks damage and flashes
LK.effects.flashObject(self.shield, 0x0088FF, 200);
return; // No damage taken
}
self.health -= damage;
LK.effects.flashObject(self, 0xFF0000, 300);
// Update health bar
if (castleHealthBar) {
castleHealthBar.updateHealth(self.health, self.maxHealth);
}
if (self.health <= 0) {
// Save collected scrap metal to persistent storage
storage.scrapMetal += currentScrapMetal;
// Check and update high scores
var currentScore = LK.getScore();
var isNewHighScore = false;
var isNewWaveRecord = false;
var isNewTimeRecord = false;
if (currentScore > storage.highScore) {
storage.highScore = currentScore;
isNewHighScore = true;
}
if (waveNumber > storage.bestWave) {
storage.bestWave = waveNumber;
isNewWaveRecord = true;
}
if (survivalTime > storage.longestSurvivalTime) {
storage.longestSurvivalTime = survivalTime;
isNewTimeRecord = true;
}
// Show achievement notifications
if (isNewHighScore || isNewWaveRecord || isNewTimeRecord) {
var achievementText = 'NEW RECORD!\n';
if (isNewHighScore) achievementText += 'HIGH SCORE: ' + currentScore + '\n';
if (isNewWaveRecord) achievementText += 'BEST WAVE: ' + waveNumber + '\n';
if (isNewTimeRecord) {
var minutes = Math.floor(survivalTime / 3600);
var seconds = Math.floor(survivalTime % 3600 / 60);
achievementText += 'LONGEST TIME: ' + minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
}
var recordText = new Text2(achievementText, {
size: 55,
fill: 0xFF00FF
});
recordText.anchor.set(0.5, 0.5);
recordText.x = 1024;
recordText.y = 800;
game.addChild(recordText);
LK.effects.flashScreen(0xFF00FF, 1500);
tween(recordText, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(recordText, {
alpha: 0
}, {
duration: 2000,
onFinish: function onFinish() {
recordText.destroy();
}
});
}
});
}
LK.showGameOver();
}
};
return self;
});
var EliteEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('tankEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
enemyGraphics.tint = 0xFFD700; // Gold for elite enemy
self.health = 12;
self.maxHealth = 12;
self.speed = 1.1;
self.damage = 25;
self.armor = 0.3; // Takes 30% less damage
self.specialAbilityCooldown = 0;
self.specialAbilityInterval = 240; // Special ability every 4 seconds
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistanceSquared = Infinity;
// Create health bar for this enemy
self.healthBar = new EnemyHealthBar();
self.healthBar.y = -50; // Position above enemy
enemyHealthBars.push(self.healthBar);
self.addChild(self.healthBar);
self.update = function () {
// Special ability - damage reduction boost
self.specialAbilityCooldown++;
if (self.specialAbilityCooldown >= self.specialAbilityInterval) {
// Temporary damage reduction boost
self.armor = Math.min(0.6, self.armor + 0.1);
self.specialAbilityCooldown = 0;
LK.effects.flashObject(self, 0xFFD700, 400);
// Create temporary shield effect
tween(self, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
// Move toward castle
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distanceSquared = dx * dx + dy * dy;
if (distanceSquared > 3600) {
var distance = Math.sqrt(distanceSquared);
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
// Check if reached castle
if (self.lastCastleDistanceSquared > 3600 && distanceSquared <= 3600) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistanceSquared = distanceSquared;
};
self.takeDamage = function (damage) {
var reducedDamage = damage * (1 - self.armor);
self.health -= reducedDamage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Update health bar
if (self.healthBar) {
self.healthBar.updateHealth(self.health, self.maxHealth);
}
if (self.health <= 0) {
LK.setScore(LK.getScore() + 75); // High score reward
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
// Drop multiple scrap metal (guaranteed)
for (var c = 0; c < 2; c++) {
var scrap = new ScrapMetalDrop();
var angle = Math.random() * Math.PI * 2;
var distance = 20 + Math.random() * 40;
scrap.x = self.x + Math.cos(angle) * distance;
scrap.y = self.y + Math.sin(angle) * distance;
scrap.baseY = scrap.y;
scrap.value = 12; // High value for elite enemy
scrapMetalDrops.push(scrap);
game.addChild(scrap);
}
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var Enemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 3;
self.maxHealth = 3;
self.speed = 1;
self.damage = 10;
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistanceSquared = Infinity;
// Create health bar for this enemy
self.healthBar = new EnemyHealthBar();
self.healthBar.y = -50; // Position above enemy
enemyHealthBars.push(self.healthBar);
self.addChild(self.healthBar);
self.update = function () {
// Move toward castle
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distanceSquared = dx * dx + dy * dy;
if (distanceSquared > 3600) {
// 60^2 = 3600
// Don't overlap with castle
var distance = Math.sqrt(distanceSquared);
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
// Check if reached castle
if (self.lastCastleDistanceSquared > 3600 && distanceSquared <= 3600) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistanceSquared = distanceSquared;
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Update health bar
if (self.healthBar) {
self.healthBar.updateHealth(self.health, self.maxHealth);
}
if (self.health <= 0) {
LK.setScore(LK.getScore() + 10);
storage.totalEnemiesDefeated++;
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
// Drop scrap metal (80% chance - increased for better collection)
if (Math.random() < 0.8) {
var scrap = new ScrapMetalDrop();
scrap.x = self.x;
scrap.y = self.y;
scrap.baseY = scrap.y;
scrap.value = 5; // Basic enemy drops 5 scrap metal
scrapMetalDrops.push(scrap);
game.addChild(scrap);
}
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var EnemyHealthBar = Container.expand(function () {
var self = Container.call(this);
// Create background bar (dark red)
var backgroundBar = LK.getAsset('bgTile', {
anchorX: 0.5,
anchorY: 0.5
});
backgroundBar.tint = 0x330000;
backgroundBar.scaleX = 0.8;
backgroundBar.scaleY = 0.15;
self.addChild(backgroundBar);
// Create health bar (red)
var healthBar = LK.getAsset('bgTile', {
anchorX: 0,
anchorY: 0.5
});
healthBar.tint = 0xFF0000;
healthBar.scaleX = 0.8;
healthBar.scaleY = 0.15;
healthBar.x = -backgroundBar.width * backgroundBar.scaleX / 2;
self.addChild(healthBar);
self.backgroundBar = backgroundBar;
self.healthBar = healthBar;
self.updateHealth = function (currentHealth, maxHealth) {
var healthPercentage = Math.max(0, currentHealth / maxHealth);
self.healthBar.scaleX = 0.8 * healthPercentage;
// Hide if at full health to reduce clutter
self.visible = healthPercentage < 1.0;
};
return self;
});
var FastEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('fastEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 1;
self.maxHealth = 1;
self.speed = 3;
self.damage = 5;
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistance = Infinity;
self.update = function () {
// Move toward castle with higher speed
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 60) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
// Check if reached castle
var currentDistance = Math.sqrt(dx * dx + dy * dy);
if (self.lastCastleDistance > 60 && currentDistance <= 60) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistance = currentDistance;
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
if (self.health <= 0) {
LK.setScore(LK.getScore() + 15);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
// Drop scrap metal (50% chance, less than basic enemy)
if (Math.random() < 0.5) {
var scrap = new ScrapMetalDrop();
scrap.x = self.x;
scrap.y = self.y;
scrap.baseY = scrap.y;
scrap.value = 3; // Less value than basic enemy
scrapMetalDrops.push(scrap);
game.addChild(scrap);
}
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var HealerEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('healerEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 4;
self.maxHealth = 4;
self.speed = 0.8;
self.damage = 8;
self.healCooldown = 0;
self.healInterval = 180; // Heals every 3 seconds
self.healRadius = 150;
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistance = Infinity;
self.update = function () {
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 60) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
// Heal nearby enemies
if (self.healCooldown > 0) {
self.healCooldown--;
}
if (self.healCooldown <= 0) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy !== self && enemy.health < enemy.maxHealth) {
var dx2 = enemy.x - self.x;
var dy2 = enemy.y - self.y;
var healDistance = Math.sqrt(dx2 * dx2 + dy2 * dy2);
if (healDistance <= self.healRadius) {
enemy.health = Math.min(enemy.maxHealth, enemy.health + 1);
LK.effects.flashObject(enemy, 0x88FF00, 200);
}
}
}
self.healCooldown = self.healInterval;
}
var currentDistance = Math.sqrt(dx * dx + dy * dy);
if (self.lastCastleDistance > 60 && currentDistance <= 60) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistance = currentDistance;
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
if (self.health <= 0) {
LK.setScore(LK.getScore() + 35);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var HealthBar = Container.expand(function () {
var self = Container.call(this);
// Create background bar (red)
var backgroundBar = LK.getAsset('bgTile', {
anchorX: 0,
anchorY: 0.5
});
backgroundBar.tint = 0x660000;
backgroundBar.scaleX = 2;
backgroundBar.scaleY = 0.3;
self.addChild(backgroundBar);
// Create health bar (green)
var healthBar = LK.getAsset('bgTile', {
anchorX: 0,
anchorY: 0.5
});
healthBar.tint = 0x00AA00;
healthBar.scaleX = 2;
healthBar.scaleY = 0.3;
self.addChild(healthBar);
self.backgroundBar = backgroundBar;
self.healthBar = healthBar;
self.maxWidth = backgroundBar.width * backgroundBar.scaleX;
self.updateHealth = function (currentHealth, maxHealth) {
var healthPercentage = Math.max(0, currentHealth / maxHealth);
self.healthBar.scaleX = 2 * healthPercentage;
// Change color based on health percentage
if (healthPercentage > 0.6) {
self.healthBar.tint = 0x00AA00; // Green
} else if (healthPercentage > 0.3) {
self.healthBar.tint = 0xFFAA00; // Orange
} else {
self.healthBar.tint = 0xFF0000; // Red
}
};
return self;
});
var HealthPack = Container.expand(function () {
var self = Container.call(this);
var healthGraphics = self.attachAsset('reward', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it red for health
healthGraphics.tint = 0xFF4444;
healthGraphics.scaleX = 0.8;
healthGraphics.scaleY = 0.8;
self.bobOffset = Math.random() * Math.PI * 2;
self.baseY = 0;
self.lifetime = 420; // 7 seconds at 60fps
self.healAmount = 25; // Restore 25 health
self.update = function () {
// Gentle bobbing animation
self.y = self.baseY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 5;
// Gentle pulsing effect
var pulseScale = 0.8 + Math.sin(LK.ticks * 0.2) * 0.1;
healthGraphics.scaleX = pulseScale;
healthGraphics.scaleY = pulseScale;
// Decrease lifetime
self.lifetime--;
// Start fading when close to expiring
if (self.lifetime <= 120) {
var alpha = self.lifetime / 120;
healthGraphics.alpha = alpha;
}
// Remove when lifetime expires
if (self.lifetime <= 0) {
self.destroy();
for (var i = healthPacks.length - 1; i >= 0; i--) {
if (healthPacks[i] === self) {
healthPacks.splice(i, 1);
break;
}
}
}
};
self.down = function (x, y, obj) {
// Heal castle
var healAmount = Math.min(self.healAmount, castle.maxHealth - castle.health);
if (healAmount > 0) {
castle.health += healAmount;
LK.getSound('award').play();
LK.effects.flashObject(castle, 0x44FF44, 500);
// Show heal amount
var healText = new Text2('+' + healAmount + ' HEALTH', {
size: 40,
fill: 0x44FF44
});
healText.anchor.set(0.5, 0.5);
healText.x = castle.x;
healText.y = castle.y - 60;
game.addChild(healText);
// Animate heal text
tween(healText, {
y: healText.y - 50,
alpha: 0
}, {
duration: 1500,
onFinish: function onFinish() {
healText.destroy();
}
});
}
self.destroy();
for (var i = healthPacks.length - 1; i >= 0; i--) {
if (healthPacks[i] === self) {
healthPacks.splice(i, 1);
break;
}
}
};
return self;
});
var RegenEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('healerEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
enemyGraphics.tint = 0x32CD32; // Lime green for regen enemy
self.health = 6;
self.maxHealth = 6;
self.speed = 0.9;
self.damage = 14;
self.regenRate = 1;
self.regenTimer = 0;
self.regenInterval = 120; // Regenerate every 2 seconds
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistanceSquared = Infinity;
// Create health bar for this enemy
self.healthBar = new EnemyHealthBar();
self.healthBar.y = -50; // Position above enemy
enemyHealthBars.push(self.healthBar);
self.addChild(self.healthBar);
self.update = function () {
// Regeneration logic
self.regenTimer++;
if (self.regenTimer >= self.regenInterval && self.health < self.maxHealth) {
self.health = Math.min(self.maxHealth, self.health + self.regenRate);
self.regenTimer = 0;
// Visual feedback for regeneration
LK.effects.flashObject(self, 0x32CD32, 300);
if (self.healthBar) {
self.healthBar.updateHealth(self.health, self.maxHealth);
}
}
// Move toward castle
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distanceSquared = dx * dx + dy * dy;
if (distanceSquared > 3600) {
var distance = Math.sqrt(distanceSquared);
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
// Check if reached castle
if (self.lastCastleDistanceSquared > 3600 && distanceSquared <= 3600) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistanceSquared = distanceSquared;
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
// Update health bar
if (self.healthBar) {
self.healthBar.updateHealth(self.health, self.maxHealth);
}
if (self.health <= 0) {
LK.setScore(LK.getScore() + 30);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
// Drop guaranteed scrap metal
var scrap = new ScrapMetalDrop();
scrap.x = self.x;
scrap.y = self.y;
scrap.baseY = scrap.y;
scrap.value = 8; // Higher value for tougher enemy
scrapMetalDrops.push(scrap);
game.addChild(scrap);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var Reward = Container.expand(function () {
var self = Container.call(this);
var rewardGraphics = self.attachAsset('reward', {
anchorX: 0.5,
anchorY: 0.5
});
self.bobOffset = Math.random() * Math.PI * 2;
self.baseY = 0;
self.lifetime = 420; // 7 seconds at 60fps
self.update = function () {
// Gentle bobbing animation
self.y = self.baseY + Math.sin(LK.ticks * 0.1 + self.bobOffset) * 5;
// Decrease lifetime
self.lifetime--;
// Start fading when close to expiring
if (self.lifetime <= 120) {
var alpha = self.lifetime / 120;
rewardGraphics.alpha = alpha;
// Add flashing animation when close to expiring
var flashInterval = Math.max(5, Math.floor(self.lifetime / 20));
if (LK.ticks % flashInterval === 0) {
tween(rewardGraphics, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: flashInterval * 8,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(rewardGraphics, {
scaleX: 1,
scaleY: 1
}, {
duration: flashInterval * 8,
easing: tween.easeIn
});
}
});
}
}
// Remove when lifetime expires
if (self.lifetime <= 0) {
tween(self, {
scaleX: 0,
scaleY: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
for (var i = rewards.length - 1; i >= 0; i--) {
if (rewards[i] === self) {
rewards.splice(i, 1);
break;
}
}
}
});
}
};
self.down = function (x, y, obj) {
// Define maximum bullet limit to prevent excessive stacking
var maxBullets = 8;
if (castle.bulletsPerShot < maxBullets) {
// Add 1 bullet per reward (consistent increment)
castle.bulletsPerShot++;
bulletsText.setText('Bullets: ' + castle.bulletsPerShot);
LK.getSound('award').play();
LK.effects.flashObject(castle, 0x00FF00, 500);
// Visual feedback for bullet upgrade with scaling effect
tween(castle, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(castle, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
} else {
// At maximum bullets - give score bonus instead
LK.setScore(LK.getScore() + 100);
scoreText.setText(LK.getScore());
bulletsText.setText('Bullets: ' + castle.bulletsPerShot + ' (MAX)');
LK.getSound('award').play();
LK.effects.flashObject(castle, 0xFFD700, 500); // Gold flash for max bonus
// Brief text notification
var maxText = new Text2('+100 SCORE!', {
size: 40,
fill: 0xFFD700
});
maxText.anchor.set(0.5, 0.5);
maxText.x = castle.x;
maxText.y = castle.y - 60;
game.addChild(maxText);
// Animate and remove notification
tween(maxText, {
y: maxText.y - 50,
alpha: 0
}, {
duration: 1500,
onFinish: function onFinish() {
maxText.destroy();
}
});
}
self.destroy();
for (var i = rewards.length - 1; i >= 0; i--) {
if (rewards[i] === self) {
rewards.splice(i, 1);
break;
}
}
};
return self;
});
var ScrapCollectionIndicator = Container.expand(function () {
var self = Container.call(this);
// Create collection range visualization
var rangeCircle = LK.getAsset('bgCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeCircle.tint = 0xC0C0C0;
rangeCircle.alpha = 0.1;
rangeCircle.scaleX = 0.6; // 180px radius visualization
rangeCircle.scaleY = 0.6;
self.addChild(rangeCircle);
self.rangeCircle = rangeCircle;
self.pulseDirection = 1;
self.update = function () {
// Gentle pulsing effect to show collection range
var pulseSpeed = 0.02;
self.alpha += self.pulseDirection * pulseSpeed;
if (self.alpha >= 0.3) {
self.pulseDirection = -1;
} else if (self.alpha <= 0.1) {
self.pulseDirection = 1;
}
// Only show when scrap metal is nearby
var nearbyScrap = false;
for (var i = 0; i < scrapMetalDrops.length; i++) {
var scrap = scrapMetalDrops[i];
var dx = scrap.x - castle.x;
var dy = scrap.y - castle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 250) {
// Show range when scrap is within extended range
nearbyScrap = true;
break;
}
}
self.visible = nearbyScrap;
};
return self;
});
var ScrapMetalDrop = Container.expand(function () {
var self = Container.call(this);
var scrapGraphics = self.attachAsset('reward', {
anchorX: 0.5,
anchorY: 0.5
});
// Make it metallic silver for scrap metal
scrapGraphics.tint = 0xC0C0C0;
scrapGraphics.scaleX = 0.6;
scrapGraphics.scaleY = 0.6;
self.value = 5; // Base scrap metal value
self.bobOffset = Math.random() * Math.PI * 2;
self.baseY = 0;
self.lifetime = 600; // 10 seconds at 60fps
self.magnetRange = 180; // Increased range for easier collection
self.collectDistance = 80; // Distance at which scrap is automatically collected
self.update = function () {
// Gentle bobbing animation
self.y = self.baseY + Math.sin(LK.ticks * 0.15 + self.bobOffset) * 3;
// Decrease lifetime
self.lifetime--;
// Distance calculation
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Auto-collect when very close
if (distance <= self.collectDistance) {
currentScrapMetal += self.value;
scrapMetalText.setText('Scrap Metal: ' + (storage.scrapMetal + currentScrapMetal));
LK.getSound('award').play();
// Visual collection effect
LK.effects.flashObject(castle, 0xC0C0C0, 300);
// Show collection amount
var collectText = new Text2('+' + self.value + ' SCRAP', {
size: 35,
fill: 0xC0C0C0
});
collectText.anchor.set(0.5, 0.5);
collectText.x = self.x;
collectText.y = self.y - 30;
game.addChild(collectText);
// Animate collection text towards castle
tween(collectText, {
x: castle.x,
y: castle.y - 50,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
collectText.destroy();
}
});
self.destroy();
for (var i = scrapMetalDrops.length - 1; i >= 0; i--) {
if (scrapMetalDrops[i] === self) {
scrapMetalDrops.splice(i, 1);
break;
}
}
return;
}
// Magnet effect when castle is nearby
if (distance <= self.magnetRange) {
var magnetSpeed = Math.min(5, distance * 0.05); // Speed increases as it gets closer
var moveX = dx / distance * magnetSpeed;
var moveY = dy / distance * magnetSpeed;
self.x += moveX;
self.y += moveY;
// Add sparkle effect during magnet pull
if (LK.ticks % 10 === 0) {
LK.effects.flashObject(self, 0xFFFFFF, 100);
}
}
// Start fading when close to expiring
if (self.lifetime <= 120) {
var alpha = self.lifetime / 120;
scrapGraphics.alpha = alpha;
}
// Remove when lifetime expires
if (self.lifetime <= 0) {
self.destroy();
for (var i = scrapMetalDrops.length - 1; i >= 0; i--) {
if (scrapMetalDrops[i] === self) {
scrapMetalDrops.splice(i, 1);
break;
}
}
}
};
return self;
});
var Shield = Container.expand(function () {
var self = Container.call(this);
var shieldGraphics = self.attachAsset('castle', {
anchorX: 0.5,
anchorY: 0.5
});
shieldGraphics.tint = 0x0088FF;
shieldGraphics.alpha = 0.3;
self.scaleX = 1.5;
self.scaleY = 1.5;
self.duration = 300; // 5 seconds at 60fps
self.pulseDirection = 1;
self.update = function () {
// Pulsing effect
var pulseSpeed = 0.1;
self.alpha += self.pulseDirection * pulseSpeed;
if (self.alpha >= 0.6) {
self.pulseDirection = -1;
} else if (self.alpha <= 0.2) {
self.pulseDirection = 1;
}
// Decrease duration
self.duration--;
if (self.duration <= 0) {
castle.shield = null;
self.destroy();
}
};
return self;
});
var ShieldEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('shieldEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 6;
self.maxHealth = 6;
self.speed = 0.7;
self.damage = 15;
self.shieldRadius = 80;
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistance = Infinity;
self.update = function () {
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 60) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
var currentDistance = Math.sqrt(dx * dx + dy * dy);
if (self.lastCastleDistance > 60 && currentDistance <= 60) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistance = currentDistance;
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
if (self.health <= 0) {
LK.setScore(LK.getScore() + 40);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var Shockwave = Container.expand(function () {
var self = Container.call(this);
var shockwaveGraphics = self.attachAsset('castle', {
anchorX: 0.5,
anchorY: 0.5
});
shockwaveGraphics.tint = 0xFF4400;
shockwaveGraphics.alpha = 0.8;
self.maxRadius = 400;
self.currentRadius = 50;
self.damage = 5;
self.speed = 15;
self.hasDealtDamage = [];
self.update = function () {
// Expand shockwave
self.currentRadius += self.speed;
var scale = self.currentRadius / 100;
self.scaleX = scale;
self.scaleY = scale;
// Fade out as it expands
self.alpha = Math.max(0, 0.8 - self.currentRadius / self.maxRadius * 0.8);
// Damage enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is within shockwave range and hasn't been damaged yet
if (distance <= self.currentRadius && self.hasDealtDamage.indexOf(enemy) === -1) {
enemy.takeDamage(self.damage);
self.hasDealtDamage.push(enemy);
LK.effects.flashObject(enemy, 0xFF4400, 200);
}
}
// Remove when fully expanded
if (self.currentRadius >= self.maxRadius) {
self.destroy();
for (var j = shockwaves.length - 1; j >= 0; j--) {
if (shockwaves[j] === self) {
shockwaves.splice(j, 1);
break;
}
}
}
};
return self;
});
var SniperEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('sniperEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 2;
self.maxHealth = 2;
self.speed = 1.5;
self.damage = 15;
self.shootCooldown = 0;
self.shootInterval = 120; // Shoots every 2 seconds
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistance = Infinity;
self.update = function () {
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Move toward castle until in range (300 pixels)
if (distance > 300) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
} else {
// In range, shoot at castle
if (self.shootCooldown > 0) {
self.shootCooldown--;
}
if (self.shootCooldown <= 0) {
castle.takeDamage(self.damage);
LK.effects.flashObject(self, 0x00FFFF, 300);
self.shootCooldown = self.shootInterval;
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistance = distance;
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
if (self.health <= 0) {
LK.setScore(LK.getScore() + 20);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var SplitterEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('splitterEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 5;
self.maxHealth = 5;
self.speed = 0.9;
self.damage = 18;
self.splitCount = 2;
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistance = Infinity;
self.update = function () {
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 60) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
var currentDistance = Math.sqrt(dx * dx + dy * dy);
if (self.lastCastleDistance > 60 && currentDistance <= 60) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistance = currentDistance;
};
self.split = function () {
for (var i = 0; i < self.splitCount; i++) {
var smallEnemy = new FastEnemy();
var angle = Math.PI * 2 / self.splitCount * i;
smallEnemy.x = self.x + Math.cos(angle) * 30;
smallEnemy.y = self.y + Math.sin(angle) * 30;
smallEnemy.lastX = smallEnemy.x;
smallEnemy.lastY = smallEnemy.y;
var dx = castle.x - smallEnemy.x;
var dy = castle.y - smallEnemy.y;
smallEnemy.lastCastleDistance = Math.sqrt(dx * dx + dy * dy);
enemies.push(smallEnemy);
game.addChild(smallEnemy);
}
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
if (self.health <= 0) {
LK.setScore(LK.getScore() + 50);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
self.split();
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var TankEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('tankEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 8;
self.maxHealth = 8;
self.speed = 0.5;
self.damage = 20;
self.armor = 0.5; // Takes 50% less damage
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistance = Infinity;
self.update = function () {
// Move toward castle slowly
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 60) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
// Check if reached castle
var currentDistance = Math.sqrt(dx * dx + dy * dy);
if (self.lastCastleDistance > 60 && currentDistance <= 60) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistance = currentDistance;
};
self.takeDamage = function (damage) {
var reducedDamage = damage * (1 - self.armor);
self.health -= reducedDamage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
if (self.health <= 0) {
LK.setScore(LK.getScore() + 25);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var TeleporterEnemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('teleporterEnemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.health = 2;
self.maxHealth = 2;
self.speed = 1;
self.damage = 12;
self.teleportCooldown = 0;
self.teleportInterval = 240; // Teleports every 4 seconds
self.lastX = 0;
self.lastY = 0;
self.lastCastleDistance = Infinity;
self.update = function () {
var dx = castle.x - self.x;
var dy = castle.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 60) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
}
// Teleport randomly
if (self.teleportCooldown > 0) {
self.teleportCooldown--;
}
if (self.teleportCooldown <= 0) {
var angle = Math.random() * Math.PI * 2;
var teleportDistance = 200 + Math.random() * 200;
self.x = castle.x + Math.cos(angle) * teleportDistance;
self.y = castle.y + Math.sin(angle) * teleportDistance;
// Keep within bounds
self.x = Math.max(50, Math.min(1998, self.x));
self.y = Math.max(50, Math.min(2682, self.y));
LK.effects.flashObject(self, 0xFF00FF, 400);
self.teleportCooldown = self.teleportInterval;
}
var currentDistance = Math.sqrt((castle.x - self.x) * (castle.x - self.x) + (castle.y - self.y) * (castle.y - self.y));
if (self.lastCastleDistance > 60 && currentDistance <= 60) {
castle.takeDamage(self.damage);
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
self.lastX = self.x;
self.lastY = self.y;
self.lastCastleDistance = currentDistance;
};
self.takeDamage = function (damage) {
self.health -= damage;
LK.effects.flashObject(self, 0xFFFFFF, 200);
if (self.health <= 0) {
LK.setScore(LK.getScore() + 25);
scoreText.setText(LK.getScore());
LK.getSound('hit').play();
self.destroy();
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i] === self) {
enemies.splice(i, 1);
break;
}
}
}
};
return self;
});
var ThreatIndicator = Container.expand(function () {
var self = Container.call(this);
var indicator = LK.getAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.tint = 0xFF4400;
indicator.scaleX = 0.5;
indicator.scaleY = 0.5;
indicator.alpha = 0.8;
self.addChild(indicator);
self.indicator = indicator;
self.pulseDirection = 1;
self.update = function () {
// Pulsing animation
var pulseSpeed = 0.05;
self.alpha += self.pulseDirection * pulseSpeed;
if (self.alpha >= 1) {
self.pulseDirection = -1;
} else if (self.alpha <= 0.5) {
self.pulseDirection = 1;
}
};
return self;
});
var UpgradeMenu = Container.expand(function () {
var self = Container.call(this);
// Create semi-transparent background
var background = LK.getAsset('bgTile', {
anchorX: 0,
anchorY: 0
});
background.tint = 0x000000;
background.alpha = 0.8;
background.scaleX = 10.24; // Cover full screen width
background.scaleY = 13.66; // Cover full screen height
self.addChild(background);
// Menu title
var titleText = new Text2('CASTLE UPGRADES', {
size: 80,
fill: 0xFFD700
});
titleText.anchor.set(0.5, 0);
titleText.x = 1024;
titleText.y = 200;
self.addChild(titleText);
// Close button
var closeButton = new Text2('β CLOSE', {
size: 60,
fill: 0xFF4444
});
closeButton.anchor.set(1, 0);
closeButton.x = 1900;
closeButton.y = 250;
self.addChild(closeButton);
// Upgrade buttons array
self.upgradeButtons = [];
// Create upgrade buttons for each type
var upgradeTypes = [{
type: 'fireRate',
name: 'FIRE RATE',
color: 0xFF8800,
description: '+15% faster shooting'
}, {
type: 'damage',
name: 'DAMAGE',
color: 0xFF0000,
description: '+20% bullet damage'
}, {
type: 'health',
name: 'HEALTH',
color: 0x00FF00,
description: '+20 max health'
}, {
type: 'bulletCount',
name: 'BULLETS',
color: 0x00AAFF,
description: '+1 bullet per shot'
}, {
type: 'movementSpeed',
name: 'MOVEMENT',
color: 0xFFFF00,
description: '+25% movement speed'
}, {
type: 'shield',
name: 'SHIELD',
color: 0x0088FF,
description: '10% shorter cooldown'
}, {
type: 'shockwave',
name: 'SHOCKWAVE',
color: 0xFF4400,
description: '10% shorter cooldown'
}];
for (var i = 0; i < upgradeTypes.length; i++) {
var upgrade = upgradeTypes[i];
var buttonContainer = new Container();
// Button background
var buttonBg = LK.getAsset('bgTile', {
anchorX: 0,
anchorY: 0
});
buttonBg.tint = upgrade.color;
buttonBg.alpha = 0.7;
buttonBg.scaleX = 4;
buttonBg.scaleY = 0.8;
buttonContainer.addChild(buttonBg);
// Button text
var buttonText = new Text2('', {
size: 45,
fill: 0xFFFFFF
});
buttonText.anchor.set(0, 0.5);
buttonText.x = 20;
buttonText.y = buttonBg.height * buttonBg.scaleY / 2;
buttonContainer.addChild(buttonText);
// Position button
var col = i % 2;
var row = Math.floor(i / 2);
buttonContainer.x = 300 + col * 800;
buttonContainer.y = 400 + row * 200;
// Store references
buttonContainer.upgradeType = upgrade.type;
buttonContainer.buttonText = buttonText;
buttonContainer.buttonBg = buttonBg;
buttonContainer.originalColor = upgrade.color;
buttonContainer.upgradeName = upgrade.name;
buttonContainer.description = upgrade.description;
self.upgradeButtons.push(buttonContainer);
self.addChild(buttonContainer);
}
// Current scrap display
var scrapDisplay = new Text2('', {
size: 55,
fill: 0xC0C0C0
});
scrapDisplay.anchor.set(0.5, 0);
scrapDisplay.x = 1024;
scrapDisplay.y = 320;
self.addChild(scrapDisplay);
self.scrapDisplay = scrapDisplay;
// Update button states and text
self.updateButtons = function () {
var totalScrap = storage.scrapMetal + currentScrapMetal;
self.scrapDisplay.setText('Available Scrap Metal: ' + totalScrap);
for (var i = 0; i < self.upgradeButtons.length; i++) {
var button = self.upgradeButtons[i];
var currentLevel = storage[button.upgradeType + 'Level'] || 1;
var cost = getUpgradeCost(button.upgradeType, currentLevel);
var canAfford = canAffordUpgrade(button.upgradeType, currentLevel);
var isMaxLevel = currentLevel >= storage.maxUpgradeTier;
if (isMaxLevel) {
button.buttonText.setText(button.upgradeName + ' - MAX LEVEL');
button.buttonBg.tint = 0x666666;
button.buttonBg.alpha = 0.5;
} else {
button.buttonText.setText(button.upgradeName + ' T' + currentLevel + 'βT' + (currentLevel + 1) + '\nCost: ' + cost + ' | ' + button.description);
button.buttonBg.tint = canAfford ? button.originalColor : 0x666666;
button.buttonBg.alpha = canAfford ? 0.7 : 0.3;
}
}
};
// Handle button clicks
self.down = function (x, y, obj) {
// Check close button
if (x >= 1700 && x <= 1900 && y >= 250 && y <= 320) {
upgradeMenuVisible = false;
self.visible = false;
return;
}
// Check upgrade buttons
for (var i = 0; i < self.upgradeButtons.length; i++) {
var button = self.upgradeButtons[i];
var buttonX = button.x;
var buttonY = button.y;
var buttonWidth = button.buttonBg.width * button.buttonBg.scaleX;
var buttonHeight = button.buttonBg.height * button.buttonBg.scaleY;
if (x >= buttonX && x <= buttonX + buttonWidth && y >= buttonY && y <= buttonY + buttonHeight) {
var currentLevel = storage[button.upgradeType + 'Level'] || 1;
if (currentLevel < storage.maxUpgradeTier && canAffordUpgrade(button.upgradeType, currentLevel)) {
if (purchaseUpgrade(button.upgradeType)) {
// Apply upgrade immediately
self.applyUpgrade(button.upgradeType);
// Update button states
self.updateButtons();
// Visual feedback
LK.effects.flashObject(button, 0x00FF00, 500);
LK.getSound('award').play();
}
}
break;
}
}
};
// Apply upgrades to castle immediately
self.applyUpgrade = function (upgradeType) {
var newLevel = storage[upgradeType + 'Level'];
switch (upgradeType) {
case 'fireRate':
var fireRateBonus = getUpgradeBonus('fireRate', newLevel);
castle.shootInterval = Math.max(20, Math.floor(60 * (1 - fireRateBonus)));
break;
case 'health':
var healthBonus = getUpgradeBonus('health', newLevel);
var newMaxHealth = 100 + healthBonus;
var healthIncrease = newMaxHealth - castle.maxHealth;
castle.maxHealth = newMaxHealth;
castle.health += healthIncrease; // Add the health increase
castleHealthBar.updateHealth(castle.health, castle.maxHealth);
break;
case 'damage':
var damageBonus = getUpgradeBonus('damage', newLevel);
castle.upgradedBulletDamage = 1.01 * (1 + damageBonus);
break;
case 'bulletCount':
var bulletCountBonus = getUpgradeBonus('bulletCount', newLevel);
castle.bulletsPerShot = 1 + bulletCountBonus;
bulletsText.setText('Bullets: ' + castle.bulletsPerShot);
break;
case 'movementSpeed':
var movementSpeedBonus = getUpgradeBonus('movementSpeed', newLevel);
castleMovementSpeed = 400 * (1 + movementSpeedBonus);
break;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x2F4F2F
});
/****
* Game Code
****/
// Game variables
var castle;
var enemies = [];
var bullets = [];
var rewards = [];
var bombRewards = [];
var bombs = [];
var scrapMetalDrops = [];
var shockwaves = [];
var healthPacks = [];
var waveTimer = 0;
var enemySpawnTimer = 0;
var rewardSpawnTimer = 0;
var waveNumber = 1;
var isMiniBossWave = false;
var miniBossSpawned = false;
var survivalTime = 0; // Track total survival time
var lastMilestoneWave = 0; // Track last milestone for rewards
// Initialize persistent storage with defaults
// Current game scrap metal (resets each game)
var currentScrapMetal = 0;
// Upgrade tier system functions
function getUpgradeCost(upgradeType, currentLevel) {
var baseCosts = {
fireRate: storage.fireRateBaseCost,
movementSpeed: storage.movementSpeedBaseCost,
health: storage.healthBaseCost,
damage: storage.damageBaseCost,
bulletCount: storage.bulletCountBaseCost,
shield: storage.shieldBaseCost,
shockwave: storage.shockwaveBaseCost
};
var baseCost = baseCosts[upgradeType] || 100;
// Progressive cost increase: base * (1.5^currentLevel)
return Math.floor(baseCost * Math.pow(1.5, currentLevel - 1));
}
function canAffordUpgrade(upgradeType, currentLevel) {
if (currentLevel >= storage.maxUpgradeTier) return false;
var cost = getUpgradeCost(upgradeType, currentLevel);
return storage.scrapMetal + currentScrapMetal >= cost;
}
function purchaseUpgrade(upgradeType) {
var currentLevel = storage[upgradeType + 'Level'] || 1;
if (!canAffordUpgrade(upgradeType, currentLevel)) return false;
var cost = getUpgradeCost(upgradeType, currentLevel);
var totalScrap = storage.scrapMetal + currentScrapMetal;
// Deduct cost and update storage
if (currentScrapMetal >= cost) {
currentScrapMetal -= cost;
} else {
var remainingCost = cost - currentScrapMetal;
currentScrapMetal = 0;
storage.scrapMetal -= remainingCost;
}
// Increase upgrade level
storage[upgradeType + 'Level'] = currentLevel + 1;
// Update scrap metal display
scrapMetalText.setText('Scrap Metal: ' + (storage.scrapMetal + currentScrapMetal));
return true;
}
function getUpgradeBonus(upgradeType, level) {
var bonuses = {
fireRate: (level - 1) * 0.15,
// 15% faster per level
movementSpeed: (level - 1) * 0.25,
// 25% faster per level
health: (level - 1) * 20,
// +20 health per level
damage: (level - 1) * 0.2,
// +20% damage per level
bulletCount: level - 1,
// +1 bullet per level
shield: (level - 1) * 0.1,
// 10% cooldown reduction per level
shockwave: (level - 1) * 0.1 // 10% cooldown reduction per level
};
return bonuses[upgradeType] || 0;
}
// Create background pattern
function createBackground() {
var bgContainer = new Container();
// Create simpler grid pattern with fewer random calculations
for (var x = 0; x <= 2048; x += 240) {
for (var y = 0; y <= 2732; y += 240) {
// Main background tiles with minimal randomization
var tile = LK.getAsset('bgTile', {
anchorX: 0.5,
anchorY: 0.5
});
tile.x = x;
tile.y = y;
bgContainer.addChild(tile);
// Reduce accent frequency for better performance
if ((x + y) % 960 === 0) {
// Every 4th tile instead of random
var accent = LK.getAsset('bgAccent', {
anchorX: 0.5,
anchorY: 0.5
});
accent.x = x;
accent.y = y;
accent.alpha = 0.5;
bgContainer.addChild(accent);
}
}
}
// Set background container to lowest z-index
game.addChildAt(bgContainer, 0);
}
// Initialize background
createBackground();
// UI elements
var scoreText = new Text2('0', {
size: 60,
fill: 0xFFFFFF
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
var bulletsText = new Text2('Bullets: 1', {
size: 50,
fill: 0x00FF00
});
bulletsText.anchor.set(0, 0);
bulletsText.x = 50;
bulletsText.y = 50;
LK.gui.topLeft.addChild(bulletsText);
var healthText = new Text2('Health: 100', {
size: 50,
fill: 0xFF0000
});
healthText.anchor.set(1, 0);
LK.gui.topRight.addChild(healthText);
var scrapMetalText = new Text2('Scrap Metal: 0', {
size: 45,
fill: 0xC0C0C0
});
scrapMetalText.anchor.set(0, 0);
scrapMetalText.x = 50;
scrapMetalText.y = 120;
LK.gui.topLeft.addChild(scrapMetalText);
// Wave counter
var waveText = new Text2('Wave: 1', {
size: 55,
fill: 0xFFD700
});
waveText.anchor.set(1, 0);
waveText.x = -50;
waveText.y = 50;
LK.gui.topRight.addChild(waveText);
// Survival time counter
var survivalTimeText = new Text2('Time: 0:00', {
size: 45,
fill: 0x88DDFF
});
survivalTimeText.anchor.set(1, 0);
survivalTimeText.x = -50;
survivalTimeText.y = 120;
LK.gui.topRight.addChild(survivalTimeText);
// High score display
var highScoreText = new Text2('Best: ' + storage.highScore + ' (Wave ' + storage.bestWave + ')', {
size: 40,
fill: 0xFFD700
});
highScoreText.anchor.set(0.5, 0);
highScoreText.x = 0;
highScoreText.y = 150;
LK.gui.top.addChild(highScoreText);
// Stats display
var statsText = new Text2('Games: ' + storage.totalGamesPlayed + ' | Enemies: ' + storage.totalEnemiesDefeated, {
size: 35,
fill: 0xCCCCCC
});
statsText.anchor.set(0.5, 0);
statsText.x = 0;
statsText.y = 200;
LK.gui.top.addChild(statsText);
// Castle health bar
var castleHealthBar = new HealthBar();
castleHealthBar.x = 50;
castleHealthBar.y = 300;
LK.gui.topLeft.addChild(castleHealthBar);
// Arrays for UI elements
var enemyHealthBars = [];
var threatIndicators = [];
// Power-up status display
var powerUpText = new Text2('', {
size: 40,
fill: 0x00FF88
});
powerUpText.anchor.set(0.5, 0);
powerUpText.x = 0;
powerUpText.y = 100;
LK.gui.top.addChild(powerUpText);
// Track active power-ups
var activePowerUps = {
shield: 0,
extraBullets: 0
};
// Ability UI buttons
var shieldButton = new Text2('SHIELD', {
size: 45,
fill: 0x0088FF
});
shieldButton.anchor.set(0, 0);
shieldButton.x = 50;
shieldButton.y = 180;
LK.gui.topLeft.addChild(shieldButton);
var shockwaveButton = new Text2('SHOCKWAVE', {
size: 45,
fill: 0xFF4400
});
shockwaveButton.anchor.set(0, 0);
shockwaveButton.x = 50;
shockwaveButton.y = 240;
LK.gui.topLeft.addChild(shockwaveButton);
// Upgrade menu button
var upgradeButton = new Text2('β‘ UPGRADES', {
size: 50,
fill: 0xFFD700
});
upgradeButton.anchor.set(0, 0);
upgradeButton.x = 50;
upgradeButton.y = 300;
LK.gui.topLeft.addChild(upgradeButton);
// Initialize castle
castle = game.addChild(new Castle());
castle.x = 1024;
castle.y = 1366;
// Add scrap collection range indicator to castle
var scrapIndicator = new ScrapCollectionIndicator();
scrapIndicator.x = castle.x;
scrapIndicator.y = castle.y;
game.addChild(scrapIndicator);
// Apply permanent upgrades from storage
var fireRateBonus = getUpgradeBonus('fireRate', storage.fireRateLevel);
castle.shootInterval = Math.max(20, Math.floor(castle.shootInterval * (1 - fireRateBonus)));
var healthBonus = getUpgradeBonus('health', storage.healthLevel);
castle.maxHealth += healthBonus;
castle.health = castle.maxHealth; // Start with full health
var damageBonus = getUpgradeBonus('damage', storage.damageLevel);
var baseBulletDamage = 1.01;
// Store upgraded damage for bullets
castle.upgradedBulletDamage = baseBulletDamage * (1 + damageBonus);
// Apply permanent bullet count bonus (separate from temporary power-ups)
var bulletCountBonus = getUpgradeBonus('bulletCount', storage.bulletCountLevel);
castle.bulletsPerShot += bulletCountBonus;
// Update bullets text with correct value after applying bonus
bulletsText.setText('Bullets: ' + castle.bulletsPerShot);
// Initialize castle health bar
castleHealthBar.updateHealth(castle.health, castle.maxHealth);
// Create upgrade menu
var upgradeMenu = new UpgradeMenu();
upgradeMenu.visible = false;
game.addChild(upgradeMenu);
var upgradeMenuVisible = false;
// Movement speed will be applied in the movement system
var movementSpeedBonus = getUpgradeBonus('movementSpeed', storage.movementSpeedLevel);
var baseCastleSpeed = 400;
castleMovementSpeed = baseCastleSpeed * (1 + movementSpeedBonus);
// Start background music
LK.playMusic('bgmusic');
// Update game counter at start
storage.totalGamesPlayed++;
// Spawn functions
function spawnEnemy() {
var enemy;
// Mini-boss wave logic - every 5th wave
if (isMiniBossWave) {
if (!miniBossSpawned) {
// Always spawn a boss enemy as the first enemy of mini-boss wave
enemy = new BossEnemy();
miniBossSpawned = true;
} else {
// Higher chance for tank/shield enemies in mini-boss waves
var enemyType = Math.random();
if (enemyType < 0.4) {
enemy = new TankEnemy();
} else if (enemyType < 0.7) {
enemy = new ShieldEnemy();
} else if (enemyType < 0.85) {
enemy = new BossEnemy();
} else {
enemy = new BomberEnemy();
}
}
} else {
// Normal wave enemy distribution with adaptive scaling
var enemyType = Math.random();
// Increase chance of advanced enemies in later waves
var advancedEnemyChance = Math.min(0.4, waveNumber * 0.02); // Up to 40% chance by wave 20
if (enemyType < 0.25) {
enemy = new Enemy();
} else if (enemyType < 0.35) {
enemy = new FastEnemy();
} else if (enemyType < 0.45) {
enemy = new TankEnemy();
} else if (enemyType < 0.55) {
enemy = new SniperEnemy();
} else if (enemyType < 0.63) {
enemy = new BomberEnemy();
} else if (enemyType < 0.7) {
enemy = new HealerEnemy();
} else if (enemyType < 0.76) {
enemy = new ShieldEnemy();
} else if (enemyType < 0.82) {
enemy = new TeleporterEnemy();
} else if (enemyType < 0.87) {
enemy = new SplitterEnemy();
} else if (enemyType < 0.87 + advancedEnemyChance * 0.3) {
// Adaptive enemies become more common in later waves
enemy = new AdaptiveEnemy();
} else if (enemyType < 0.87 + advancedEnemyChance * 0.6) {
// Regenerating enemies for sustained threat
enemy = new RegenEnemy();
} else if (enemyType < 0.87 + advancedEnemyChance * 0.8) {
// Elite enemies for high-value targets
enemy = new EliteEnemy();
} else if (enemyType < 0.95) {
enemy = new BossEnemy();
} else {
// Super rare - spawn multiple elite enemies for extreme challenge
enemy = new EliteEnemy();
var secondElite = new EliteEnemy();
secondElite.x = -40; // Will be positioned properly below
secondElite.y = Math.random() * 2732;
secondElite.lastX = secondElite.x;
secondElite.lastY = secondElite.y;
var dx2 = castle.x - secondElite.x;
var dy2 = castle.y - secondElite.y;
secondElite.lastCastleDistanceSquared = dx2 * dx2 + dy2 * dy2;
// Apply scaling to second elite
var healthMultiplier = 1 + (waveNumber - 1) * 0.05;
var speedMultiplier = 1 + (waveNumber - 1) * 0.02;
var damageMultiplier = 1 + (waveNumber - 1) * 0.05;
secondElite.health = Math.ceil(secondElite.health * healthMultiplier);
secondElite.maxHealth = secondElite.health;
secondElite.speed = secondElite.speed * speedMultiplier;
secondElite.damage = Math.ceil(secondElite.damage * damageMultiplier);
if (isMiniBossWave) {
secondElite.health = Math.ceil(secondElite.health * 1.25);
secondElite.maxHealth = secondElite.health;
secondElite.damage = Math.ceil(secondElite.damage * 1.15);
}
enemies.push(secondElite);
game.addChild(secondElite);
}
}
// Spawn from random edge
var side = Math.floor(Math.random() * 4);
switch (side) {
case 0:
// Top
enemy.x = Math.random() * 2048;
enemy.y = -20;
break;
case 1:
// Right
enemy.x = 2068;
enemy.y = Math.random() * 2732;
break;
case 2:
// Bottom
enemy.x = Math.random() * 2048;
enemy.y = 2752;
break;
case 3:
// Left
enemy.x = -20;
enemy.y = Math.random() * 2732;
break;
}
// Quantified enemy scaling: +5% health and +2% speed per wave
var healthMultiplier = 1 + (waveNumber - 1) * 0.05;
var speedMultiplier = 1 + (waveNumber - 1) * 0.02;
var damageMultiplier = 1 + (waveNumber - 1) * 0.05;
// Apply scaling with proper rounding
enemy.health = Math.ceil(enemy.health * healthMultiplier);
enemy.maxHealth = enemy.health;
enemy.speed = enemy.speed * speedMultiplier;
enemy.damage = Math.ceil(enemy.damage * damageMultiplier);
// Extra scaling for mini-boss waves
if (isMiniBossWave) {
enemy.health = Math.ceil(enemy.health * 1.25); // 25% more health
enemy.maxHealth = enemy.health;
enemy.damage = Math.ceil(enemy.damage * 1.15); // 15% more damage
}
enemy.lastX = enemy.x;
enemy.lastY = enemy.y;
var dx = castle.x - enemy.x;
var dy = castle.y - enemy.y;
enemy.lastCastleDistanceSquared = dx * dx + dy * dy;
enemies.push(enemy);
game.addChild(enemy);
}
function spawnReward() {
var rewardType = Math.random();
var reward;
// Increased chance for health packs when castle health is low
var healthPercentage = castle.health / castle.maxHealth;
var healthPackChance = healthPercentage < 0.3 ? 0.25 : healthPercentage < 0.6 ? 0.15 : 0.1;
// 60% regular reward, 25% bomb reward, 15% health pack (adjusted based on health)
if (rewardType < 0.6) {
reward = new Reward();
rewards.push(reward);
} else if (rewardType < 0.85) {
reward = new BombReward();
bombRewards.push(reward);
} else {
reward = new HealthPack();
healthPacks.push(reward);
}
// Spawn in accessible areas further away from the castle
var angle = Math.random() * Math.PI * 2;
var distance = 400 + Math.random() * 500;
reward.x = castle.x + Math.cos(angle) * distance;
reward.y = castle.y + Math.sin(angle) * distance;
// Keep within bounds
reward.x = Math.max(100, Math.min(1948, reward.x));
reward.y = Math.max(100, Math.min(2632, reward.y));
reward.baseY = reward.y;
game.addChild(reward);
// Add spawn animation with tween
reward.scaleX = 0;
reward.scaleY = 0;
tween(reward, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
// Castle movement system with configurable speed
// Note: castleMovementSpeed is set above after applying movement speed bonus
var targetX = 1024;
var targetY = 1366;
var isMoving = false;
game.down = function (x, y, obj) {
// If upgrade menu is visible, let it handle the input
if (upgradeMenuVisible) {
upgradeMenu.down(x, y, obj);
return;
}
// Check if clicking on ability buttons
var buttonClicked = false;
// Check shield button area (approximate)
if (x >= 50 && x <= 200 && y >= 180 && y <= 220) {
castle.activateShield();
buttonClicked = true;
}
// Check shockwave button area (approximate)
else if (x >= 50 && x <= 250 && y >= 240 && y <= 280) {
castle.activateShockwave();
buttonClicked = true;
}
// Check upgrade button area
else if (x >= 50 && x <= 280 && y >= 300 && y <= 350) {
upgradeMenuVisible = !upgradeMenuVisible;
upgradeMenu.visible = upgradeMenuVisible;
if (upgradeMenuVisible) {
upgradeMenu.updateButtons();
}
buttonClicked = true;
}
// Only move castle if not clicking on ability buttons
if (!buttonClicked) {
// Set new target position
targetX = x;
targetY = y;
// Calculate distance to determine movement duration
var dx = targetX - castle.x;
var dy = targetY - castle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Calculate duration based on configurable movement speed
var duration = Math.max(150, distance / castleMovementSpeed * 1000); // minimum 150ms for responsiveness
// Stop any existing movement tween
tween.stop(castle, {
x: true,
y: true
});
isMoving = true;
// Add walking animation with slight bobbing effect
var originalY = castle.y;
// Start smooth walking animation with subtle bounce
tween(castle, {
x: targetX,
y: targetY
}, {
duration: duration,
easing: tween.easeInOut,
onFinish: function onFinish() {
isMoving = false;
// Subtle landing effect
tween(castle, {
scaleX: 1.1,
scaleY: 0.9
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(castle, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeOut
});
}
});
}
});
// Add walking bobbing animation during movement
if (duration > 300) {
var bobDuration = duration / 3;
tween(castle, {
scaleY: 1.05
}, {
duration: bobDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(castle, {
scaleY: 1
}, {
duration: bobDuration,
easing: tween.easeInOut
});
}
});
}
}
};
game.move = function (x, y, obj) {
// Update target while moving (allows for path correction)
if (isMoving) {
targetX = x;
targetY = y;
// Calculate new distance and duration
var dx = targetX - castle.x;
var dy = targetY - castle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Only update if the new target is significantly different
if (distance > 40) {
var duration = Math.max(150, distance / castleMovementSpeed * 1000);
// Stop current tween and start new one
tween.stop(castle, {
x: true,
y: true
});
tween(castle, {
x: targetX,
y: targetY
}, {
duration: duration,
easing: tween.easeInOut,
onFinish: function onFinish() {
isMoving = false;
}
});
}
}
};
game.up = function (x, y, obj) {
// Optional: Could add final position adjustment here if needed
};
// Game update loop
game.update = function () {
// Update wave timer
waveTimer++;
// Spawn enemies
enemySpawnTimer++;
var spawnRate = Math.max(30, 120 - waveNumber * 5); // Faster spawning each wave
// Increase spawn rate for mini-boss waves
if (isMiniBossWave) {
spawnRate = Math.max(20, spawnRate * 0.7); // 30% faster spawning in mini-boss waves
}
if (enemySpawnTimer >= spawnRate) {
spawnEnemy();
enemySpawnTimer = 0;
}
// Spawn rewards occasionally - tied to enemy defeats for better balance
rewardSpawnTimer++;
var enemiesDefeated = Math.floor(LK.getScore() / 10); // Approximate enemies defeated based on score
var rewardThreshold = 600 - Math.min(300, enemiesDefeated * 5); // Faster spawning as more enemies defeated
if (rewardSpawnTimer >= rewardThreshold && rewards.length + bombRewards.length < 3) {
// Spawn rate increases with progress, max 3 rewards total
spawnReward();
rewardSpawnTimer = 0;
}
// Advance wave every 30 seconds
if (waveTimer >= 1800) {
// 30 seconds at 60fps
// Wave completion bonus before advancing
var waveBonus = waveNumber * 50; // 50 points per wave number
var scrapBonus = Math.floor(waveNumber / 2) + 1; // 1-2 scrap metal per wave
LK.setScore(LK.getScore() + waveBonus);
currentScrapMetal += scrapBonus;
// Show wave completion bonus
var bonusText = new Text2('WAVE ' + waveNumber + ' COMPLETE!\n+' + waveBonus + ' SCORE +' + scrapBonus + ' SCRAP', {
size: 60,
fill: 0x00FF88
});
bonusText.anchor.set(0.5, 0.5);
bonusText.x = 1024;
bonusText.y = 600;
game.addChild(bonusText);
// Animate wave completion bonus
tween(bonusText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(bonusText, {
alpha: 0,
y: bonusText.y - 100
}, {
duration: 1500,
onFinish: function onFinish() {
bonusText.destroy();
}
});
}
});
waveNumber++;
waveTimer = 0;
miniBossSpawned = false; // Reset mini-boss spawn flag
// Check if this is a mini-boss wave (every 5th wave)
isMiniBossWave = waveNumber % 5 === 0;
if (isMiniBossWave) {
// Special visual effect for mini-boss waves
LK.effects.flashScreen(0xFF4400, 1000); // Orange flash for mini-boss
// Bigger bonus for mini-boss waves
var miniBossBonus = waveNumber * 100;
var miniBossScrapBonus = Math.floor(waveNumber / 3) + 3;
LK.setScore(LK.getScore() + miniBossBonus);
currentScrapMetal += miniBossScrapBonus;
// Show wave announcement
var waveAnnouncementText = new Text2('MINI-BOSS WAVE ' + waveNumber + '\nBONUS: +' + miniBossBonus + ' SCORE +' + miniBossScrapBonus + ' SCRAP', {
size: 70,
fill: 0xFF4400
});
waveAnnouncementText.anchor.set(0.5, 0.5);
waveAnnouncementText.x = 1024;
waveAnnouncementText.y = 800;
game.addChild(waveAnnouncementText);
// Fade out wave text after 3 seconds for mini-boss
tween(waveAnnouncementText, {
alpha: 0
}, {
duration: 3000,
onFinish: function onFinish() {
waveAnnouncementText.destroy();
}
});
} else if (waveNumber >= 10 && waveNumber % 10 === 0) {
// Show special threat warning every 10 waves
var threatWarningText = new Text2('THREAT LEVEL INCREASED!\nWAVE ' + waveNumber + ' - ADAPTIVE ENEMIES INCOMING', {
size: 65,
fill: 0x8A2BE2
});
threatWarningText.anchor.set(0.5, 0.5);
threatWarningText.x = 1024;
threatWarningText.y = 800;
game.addChild(threatWarningText);
LK.effects.flashScreen(0x8A2BE2, 800);
tween(threatWarningText, {
alpha: 0
}, {
duration: 2500,
onFinish: function onFinish() {
threatWarningText.destroy();
}
});
} else {
// Normal wave flash
LK.effects.flashScreen(0x0066FF, 500);
}
}
// Check bullet-enemy collisions
for (var b = bullets.length - 1; b >= 0; b--) {
var bullet = bullets[b];
var bulletHit = false;
// Early exit if bullet is off-screen
if (bullet.x < -100 || bullet.x > 2148 || bullet.y < -100 || bullet.y > 2832) {
bullet.destroy();
bullets.splice(b, 1);
continue;
}
for (var e = enemies.length - 1; e >= 0; e--) {
var enemy = enemies[e];
if (bullet.intersects(enemy)) {
enemy.takeDamage(bullet.damage);
bullet.destroy();
bullets.splice(b, 1);
bulletHit = true;
break;
}
}
if (bulletHit) continue;
}
// Check castle-reward collisions for pickup
for (var r = rewards.length - 1; r >= 0; r--) {
var reward = rewards[r];
if (castle.intersects(reward)) {
// Define maximum bullet limit to prevent excessive stacking
var maxBullets = 8;
if (castle.bulletsPerShot < maxBullets) {
// Add 1 bullet per reward (consistent increment)
castle.bulletsPerShot++;
bulletsText.setText('Bullets: ' + castle.bulletsPerShot);
LK.getSound('award').play();
LK.effects.flashObject(castle, 0x00FF00, 500);
// Visual feedback for bullet upgrade with scaling effect
tween(castle, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(castle, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
} else {
// At maximum bullets - give score bonus instead
LK.setScore(LK.getScore() + 100);
scoreText.setText(LK.getScore());
bulletsText.setText('Bullets: ' + castle.bulletsPerShot + ' (MAX)');
LK.getSound('award').play();
LK.effects.flashObject(castle, 0xFFD700, 500); // Gold flash for max bonus
// Brief text notification
var maxText = new Text2('+100 SCORE!', {
size: 40,
fill: 0xFFD700
});
maxText.anchor.set(0.5, 0.5);
maxText.x = castle.x;
maxText.y = castle.y - 60;
game.addChild(maxText);
// Animate and remove notification
tween(maxText, {
y: maxText.y - 50,
alpha: 0
}, {
duration: 1500,
onFinish: function onFinish() {
maxText.destroy();
}
});
}
reward.destroy();
rewards.splice(r, 1);
}
}
// Check castle-bomb reward collisions for pickup
for (var br = bombRewards.length - 1; br >= 0; br--) {
var bombReward = bombRewards[br];
if (castle.intersects(bombReward)) {
// Schedule bomb to appear at character position after 3 seconds
var bombX = castle.x;
var bombY = castle.y;
LK.setTimeout(function () {
var bomb = new Bomb();
bomb.x = bombX;
bomb.y = bombY;
bombs.push(bomb);
game.addChild(bomb);
}, 3000);
LK.getSound('award').play();
LK.effects.flashObject(castle, 0xFF8800, 500);
bombReward.destroy();
bombRewards.splice(br, 1);
}
}
// Scrap metal collection is now handled automatically in ScrapMetalDrop class
// Check castle-health pack collisions for pickup
for (var hp = healthPacks.length - 1; hp >= 0; hp--) {
var healthPack = healthPacks[hp];
if (castle.intersects(healthPack)) {
// Heal castle
var healAmount = Math.min(healthPack.healAmount, castle.maxHealth - castle.health);
if (healAmount > 0) {
castle.health += healAmount;
LK.getSound('award').play();
LK.effects.flashObject(castle, 0x44FF44, 500);
// Update health bar
if (castleHealthBar) {
castleHealthBar.updateHealth(castle.health, castle.maxHealth);
}
// Show heal amount
var healText = new Text2('+' + healAmount + ' HEALTH', {
size: 40,
fill: 0x44FF44
});
healText.anchor.set(0.5, 0.5);
healText.x = castle.x;
healText.y = castle.y - 60;
game.addChild(healText);
// Animate heal text
tween(healText, {
y: healText.y - 50,
alpha: 0
}, {
duration: 1500,
onFinish: function onFinish() {
healText.destroy();
}
});
}
healthPack.destroy();
healthPacks.splice(hp, 1);
}
}
// Update ability button text with cooldown status
if (castle.shieldCooldown > 0) {
var shieldSeconds = Math.ceil(castle.shieldCooldown / 60);
shieldButton.setText('SHIELD (' + shieldSeconds + 's)');
shieldButton.fill = 0x666666; // Grayed out
} else {
shieldButton.setText('SHIELD');
shieldButton.fill = 0x0088FF; // Active color
}
if (castle.shockwaveCooldown > 0) {
var shockwaveSeconds = Math.ceil(castle.shockwaveCooldown / 60);
shockwaveButton.setText('SHOCKWAVE (' + shockwaveSeconds + 's)');
shockwaveButton.fill = 0x666666; // Grayed out
} else {
shockwaveButton.setText('SHOCKWAVE');
shockwaveButton.fill = 0xFF4400; // Active color
}
// Update survival time
survivalTime++;
var minutes = Math.floor(survivalTime / 3600); // 60 seconds * 60 fps
var seconds = Math.floor(survivalTime % 3600 / 60);
var timeString = minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
// Update wave counter and survival time
waveText.setText('Wave: ' + waveNumber);
survivalTimeText.setText('Time: ' + timeString);
// Check for survival milestones (every 10 waves)
if (waveNumber >= lastMilestoneWave + 10) {
lastMilestoneWave = waveNumber;
// Milestone rewards
var milestoneScoreBonus = waveNumber * 200;
var milestoneScrapBonus = Math.floor(waveNumber / 5) + 5;
LK.setScore(LK.getScore() + milestoneScoreBonus);
currentScrapMetal += milestoneScrapBonus;
// Show milestone achievement
var milestoneText = new Text2('SURVIVAL MILESTONE!\nWAVE ' + waveNumber + ' REACHED\n+' + milestoneScoreBonus + ' SCORE +' + milestoneScrapBonus + ' SCRAP', {
size: 55,
fill: 0xFFD700
});
milestoneText.anchor.set(0.5, 0.5);
milestoneText.x = 1024;
milestoneText.y = 1000;
game.addChild(milestoneText);
// Special visual effects for milestone
LK.effects.flashScreen(0xFFD700, 1200);
tween(milestoneText, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(milestoneText, {
alpha: 0,
y: milestoneText.y - 150
}, {
duration: 2000,
onFinish: function onFinish() {
milestoneText.destroy();
}
});
}
});
}
// Update power-up display
var powerUpDisplay = '';
if (castle.shield) {
var shieldTime = Math.ceil(castle.shield.duration / 60);
powerUpDisplay += 'SHIELD: ' + shieldTime + 's ';
}
var permanentBullets = getUpgradeBonus('bulletCount', storage.bulletCountLevel) + 1;
var tempBullets = castle.bulletsPerShot - permanentBullets;
if (tempBullets > 0) {
powerUpDisplay += '+' + tempBullets + ' BULLETS ';
}
powerUpText.setText(powerUpDisplay);
// Update threat indicators
// Clear old indicators
for (var ti = threatIndicators.length - 1; ti >= 0; ti--) {
threatIndicators[ti].destroy();
threatIndicators.splice(ti, 1);
}
// Create threat indicators for enemies near screen edges
for (var ei = 0; ei < enemies.length; ei++) {
var enemy = enemies[ei];
var indicator = null;
// Check if enemy is near edges and create indicators
if (enemy.x < 100) {
// Left edge
indicator = new ThreatIndicator();
indicator.x = 50;
indicator.y = Math.max(100, Math.min(2632, enemy.y));
indicator.rotation = Math.PI; // Point right
} else if (enemy.x > 1948) {
// Right edge
indicator = new ThreatIndicator();
indicator.x = 1998;
indicator.y = Math.max(100, Math.min(2632, enemy.y));
indicator.rotation = 0; // Point left
} else if (enemy.y < 100) {
// Top edge
indicator = new ThreatIndicator();
indicator.x = Math.max(100, Math.min(1948, enemy.x));
indicator.y = 50;
indicator.rotation = Math.PI / 2; // Point down
} else if (enemy.y > 2632) {
// Bottom edge
indicator = new ThreatIndicator();
indicator.x = Math.max(100, Math.min(1948, enemy.x));
indicator.y = 2682;
indicator.rotation = -Math.PI / 2; // Point up
}
if (indicator) {
threatIndicators.push(indicator);
game.addChild(indicator);
}
}
// Update scrap collection indicator position
if (scrapIndicator) {
scrapIndicator.x = castle.x;
scrapIndicator.y = castle.y;
}
// Update UI with tier information
var totalScrap = storage.scrapMetal + currentScrapMetal;
var scrapDisplay = 'Scrap: ' + totalScrap;
// Show next upgrade cost for fire rate as example
if (storage.fireRateLevel < storage.maxUpgradeTier) {
var nextCost = getUpgradeCost('fireRate', storage.fireRateLevel);
scrapDisplay += ' | Fire Rate T' + storage.fireRateLevel + 'βT' + (storage.fireRateLevel + 1) + ': ' + nextCost;
} else {
scrapDisplay += ' | Fire Rate: MAX';
}
scrapMetalText.setText(scrapDisplay);
healthText.setText('Health: ' + castle.health + '/' + castle.maxHealth);
scoreText.setText(LK.getScore());
// Update high score display with current vs best comparison
var currentScore = LK.getScore();
if (currentScore > storage.highScore) {
highScoreText.setText('BEST: ' + currentScore + ' (Wave ' + waveNumber + ') NEW!');
highScoreText.fill = 0x00FF00; // Green for new record
} else {
highScoreText.setText('Best: ' + storage.highScore + ' (Wave ' + storage.bestWave + ') | Current: ' + currentScore);
highScoreText.fill = 0xFFD700; // Gold for normal display
}
// Update stats display
statsText.setText('Games: ' + storage.totalGamesPlayed + ' | Enemies: ' + storage.totalEnemiesDefeated + ' | Best Time: ' + Math.floor(storage.longestSurvivalTime / 3600) + ':' + (Math.floor(storage.longestSurvivalTime % 3600 / 60) < 10 ? '0' : '') + Math.floor(storage.longestSurvivalTime % 3600 / 60));
// Update upgrade menu if visible
if (upgradeMenuVisible && upgradeMenu.visible) {
upgradeMenu.updateButtons();
}
// Update upgrade button text based on available scrap
var upgradeButtonText = 'β‘ UPGRADES';
var totalScrap = storage.scrapMetal + currentScrapMetal;
if (totalScrap > 0) {
upgradeButtonText += ' (' + totalScrap + ')';
}
upgradeButton.setText(upgradeButtonText);
};
square smiley face thick eyebrows. In-Game asset
pink stone round. In-Game asset
purple square crab. In-Game asset
round mouse face. In-Game asset
bright green fat star. In-Game asset
round red lava stone. In-Game asset
square yellow poisonous stone top view. In-Game asset
yellow flame ze poisonous bead top view. In-Game asset
grass top view. In-Game asset. 2d. High contrast. No shadows
bomb in green star