Code edit (1 edits merged)
Please save this source code
User prompt
Make the descriptions of all units in the enemypedia more detailed
User prompt
Dragon-tagged enemies also have the ability to be immune to slowdowns
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55 and gains HP And speed every 10 score), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score and gets HP every 15 score)
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55 and gains HP And speed every 10 score), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score and gets HP every 15 score)
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55 and gains HP And speed every 10 score), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score and gets HP every 15 score)
User prompt
Add new upgrades: Dragon slayers (All projectiles deal 10+ damage to dragon-tagged enemies), Enchained legs (all enemies that can dodge have 10% less chance to dodge), Extra life (you can resist one more enemy that escapes)
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score)
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score)
User prompt
Dark war elephant starts appearing at score 145
User prompt
DartAlly uses "Disguised_swordsman" asset
User prompt
Add assets for Dart shooter, Darts (from dart shooter), Angel of light and Light effect for Angel of light's attack
User prompt
Add new upgrades: Dart shooter (an ally that shoots darts at a fast rate that are really effective against green enemies), Angel of light (this girl knows how to stun! She uses light that can stun enemies for 5 seconds and deals a LOT of damage to dark enemies)
User prompt
Compilation error[L2326]: TypeError: Cannot set properties of undefined (setting 'reflectChance') How can I help?
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'reflectChance')' in or related to this line: 'self.reflectChance = 0.20; // 20% reflect chance, added in Enemy class' Line Number: 2326
User prompt
Enemies aren't spawning anymore; FIX THAT RIGHT NOW!
User prompt
Jester uses Jester_asset and Dark war elephant uses Dark_war_elephant_asset
User prompt
Add asset for jester and dark war elephant
User prompt
Add new enemies: Jester (The Jester ihas a 20% chance to reflect projectiles that aren't cannonballs and a 30% chance of dodging them! Gains speed every 12 score and starts appearing at 69 score), Dark war elephant (This elephant is a black enemy [this means it's immune to arrows] that is similar to it's green brother. However, it has slightly less Hp But spawns 5 dark archers on death! Gains HP every 10 score and starts appearing at 120 score)
User prompt
Hot air balloon now starts spawning at score 100
User prompt
Add in the enemypedia when each enemy appears at what score
User prompt
Add an enemypedia to the game where you see all types of enemies and their stats, looks and properties
User prompt
Make the Enemypedia more organized by adding Buttons to change what enemy you wanna look at instead of all enemies at once
User prompt
Add an enemypedia to the game where you see all types of enemies and their stats, looks and properties
User prompt
Add an enemypedia to the game where you see all types of enemies and their stats and looks
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ /** * Represents an allied archer that shoots arrows independently. */ var ArcherAlly = Container.expand(function () { var self = Container.call(this); // Create and attach the archer graphic asset var graphics = self.attachAsset('Archer', { anchorX: 0.5, anchorY: 0.5 }); // No rotation needed for 'Archer' asset as it's already upright and flipped self.fireTimer = 0; // Timer for shooting self.fireInterval = 180; // Shoot every 3 seconds (3 * 60 ticks) /** * Update method called each game tick by the LK engine. * Handles firing logic. */ self.update = function () { self.fireTimer++; var effectiveFireInterval = Math.max(60, self.fireInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.fireTimer >= effectiveFireInterval) { self.fireTimer = 0; // Reset timer // Find the closest enemy to shoot at var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate distance to the enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, fire an arrow if (closestEnemy) { // Calculate angle towards the closest enemy var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); // Create a new arrow instance var newArrow = new Arrow(angle); // Apply multi-shot to allies if the upgrade is enabled for the player if (multiShotEnabled) { var angle2 = angle + Math.PI / 12; // Offset by 15 degrees var newArrow2 = new Arrow(angle2); newArrow2.x = self.x; newArrow2.y = self.y; newArrow2.lastY = newArrow2.y; newArrow2.lastX = newArrow2.x; game.addChild(newArrow2); arrows.push(newArrow2); var angle3 = angle - Math.PI / 12; // Offset by -15 degrees var newArrow3 = new Arrow(angle3); newArrow3.x = self.x; newArrow3.y = self.y; newArrow3.lastY = newArrow3.y; newArrow3.lastX = newArrow3.x; game.addChild(newArrow3); arrows.push(newArrow3); } newArrow.x = self.x; newArrow.y = self.y; // Ally arrows do not count towards the player's reload counter // The Arrow class handles piercing level based on player upgrade, but the ally doesn't benefit from player reload. // For simplicity, we'll let the ally benefit from player's piercing upgrade. newArrow.lastY = newArrow.y; newArrow.lastX = newArrow.x; // Add the arrow to the game scene and the tracking array. game.addChild(newArrow); arrows.push(newArrow); // Add to the same arrows array for collision detection // Ally doesn't play the 'shoot' sound } } }; return self; // Return self for potential inheritance }); // Sound when an enemy reaches the bastion // No plugins needed for this version of the game. /** * Represents an Arrow fired by the player. * @param {number} angle - The angle in radians at which the arrow is fired. */ var Arrow = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the arrow graphic asset var graphics = self.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align arrow graphic with direction self.speed = 30; // Speed of the arrow in pixels per tick self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) self.pierceLeft = arrowPierceLevel; // How many more enemies this arrow can pierce self.damage = arrowDamage; // Damage dealt by this arrow self.isPoison = poisonShotsEnabled; // Flag if this arrow applies poison self.targetEnemy = null; // Potential target for aimbot self.seekSpeed = 0.1; // How quickly the arrow adjusts its direction to seek if (aimbotEnabled) { // Find the closest enemy to seek if aimbot is enabled var closestEnemy = null; var closestDistance = Infinity; 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); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { self.targetEnemy = closestEnemy; } } /** * Update method called each game tick by the LK engine. * Moves the arrow based on its velocity. */ self.update = function () { if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) { // If aimbot is enabled, a target exists and is still in the game, seek it var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y)); // Smoothly adjust the arrow's angle towards the target angle var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation // Normalize angle difference to be between -PI and PI if (angleDiff > Math.PI) { angleDiff -= 2 * Math.PI; } if (angleDiff < -Math.PI) { angleDiff += 2 * Math.PI; } // Interpolate angle var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed; self.rotation = newAngle + Math.PI / 2; self.vx = Math.sin(newAngle) * self.speed; self.vy = -Math.cos(newAngle) * self.speed; } else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) { // If aimbot is enabled but current target is gone, find a new target self.targetEnemy = null; // Clear old target var closestEnemy = null; var closestDistance = Infinity; 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); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { self.targetEnemy = closestEnemy; } } self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents a Cannon ally that targets the strongest enemy. */ var Cannon = Container.expand(function () { var self = Container.call(this); // Create and attach the cannon graphic asset (need to add a new asset for this) // For now, using a placeholder asset, will need a 'cannon_asset' var graphics = self.attachAsset('cannon_asset', { // Use actual cannon asset anchorX: 0.5, anchorY: 0.5 }); self.attackRange = 800; // Long attack range self.attackDamage = 10; // High damage self.attackInterval = 300; // Attack every 5 seconds (300 ticks) self.attackTimer = 0; // Timer for attacks self.rotation = 0; // For aiming visual if implemented later /** * Update method called each game tick by the LK engine. * Handles attacking the strongest enemy. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the strongest enemy (highest health) var strongestEnemy = null; var maxHealth = -1; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.health > maxHealth) { maxHealth = enemy.health; strongestEnemy = enemy; } } // If a strongest enemy is found within range, attack it if (strongestEnemy) { // Calculate angle towards the strongest enemy var angle = Math.atan2(strongestEnemy.x - self.x, -(strongestEnemy.y - self.y)); // Create a new cannonball instance var newCannonball = new Cannonball(angle); // Position the cannonball at the cannon's location newCannonball.x = self.x; newCannonball.y = self.y; newCannonball.lastY = newCannonball.y; // Initialize lastY for state tracking newCannonball.lastX = newCannonball.x; // Initialize lastX for state tracking // Add the cannonball to the game scene and the tracking array. game.addChild(newCannonball); cannonballs.push(newCannonball); // Add to the new cannonballs array // Optional: Add a visual/sound effect for cannon shot here later } } }; return self; // Return self for potential inheritance }); /** * Represents a Cannonball fired by a Cannon. * @param {number} angle - The angle in radians at which the cannonball is fired. */ var Cannonball = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the cannonball graphic asset var graphics = self.attachAsset('cannonball', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align cannonball graphic with direction self.speed = 20; // Base speed of the cannonball self.damage = 10; // Base damage dealt by this cannonball // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.speed += 5; self.damage += 5; } self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) /** * Update method called each game tick by the LK engine. * Moves the cannonball based on its velocity. */ self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents an Enemy attacker moving towards the bastion. * @param {string} type - The type of enemy ('swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight'). * @param {number} speed - The final calculated speed of the enemy. * @param {number} health - The initial and maximum health of the enemy. * @param {number} dodgeChance - The chance (0 to 1) for the enemy to dodge an attack. */ var Enemy = Container.expand(function (type, speed, health, dodgeChance) { var self = Container.call(this); // Set asset based on type var assetId = 'enemy'; // Default swordsman asset if (type === 'knight') { assetId = 'knight'; } else if (type === 'elite_knight') { assetId = 'elite_knight_asset'; // Use specific asset for elite knight } else if (type === 'thief') { assetId = 'thief'; } else if (type === 'boss') { assetId = 'boss'; } else if (type === 'shield') { assetId = 'shield_enemy'; // Use specific asset for shield } else if (type === 'wizard') { assetId = 'wizard_enemy'; // Use specific asset for wizard } else if (type === 'spearman') { assetId = 'spearman'; // Use specific asset for spearman } else if (type === 'war_elephant') { assetId = 'war_elephant'; // Use specific asset for war elephant } else if (type === 'elite_shield') { assetId = 'elite_shield_asset'; // Use specific asset for elite shield } else if (type === 'shaman') { assetId = 'shaman_enemy'; // Use specific asset for shaman } else if (type === 'hot_air_balloon') { assetId = 'hot_air_balloon_asset'; // Use specific asset for hot air balloon } else if (type === 'dark_bowman') { assetId = 'dark_bowman_asset'; // Use specific asset for dark bowman } var graphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); self.type = type; // 'swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight', 'shaman', etc. self.speed = speed; // Final calculated speed self.health = health; self.maxHealth = health; // Store max health for visual feedback self.dodgeChance = dodgeChance || 0; // Default to 0 if undefined self.poisonStacks = 0; // Number of poison stacks self.poisonTimer = 0; // Timer for poison damage self.poisonDamagePerTick = 0.05; // Damage per tick per stack self.slowTimer = 0; // Timer for slowdown effect self.currentSlowAmount = 1.0; // Multiplier for current slowdown effect (1.0 means no slow) // Initialize shaman-specific properties if (type === 'shaman') { self.shamanTimer = 0; } // Set tag property based on type self.tag = null; // Default tag is null if (self.type === 'wizard' || self.type === 'spearman' || self.type === 'war_elephant' || self.type === 'shaman') { self.tag = 'Green'; } else if (self.type === 'dark_bowman') { self.tag = 'Black'; // Black-tagged enemies are immune to arrows } // Wizard-specific properties if (self.type === 'wizard') { self.teleportTimer = 0; self.teleportInterval = 180; // 3 seconds * 60 FPS } // --- Public Methods (defined before use) --- /** * Update method called each game tick by the LK engine. * Moves the enemy downwards (or teleports for wizard) and updates visual feedback. */ self.update = function () { if (self.type === 'wizard') { self.teleportTimer = (self.teleportTimer || 0) + 1; // Initialize timer if needed if (self.teleportTimer >= self.teleportInterval) { self.teleportTimer = 0; // Teleport logic: Random X, slightly advanced Y var oldY = self.y; var teleportPadding = graphics.width / 2 + 20; // Use actual graphic width var newX = teleportPadding + Math.random() * (GAME_WIDTH - 2 * teleportPadding); // Advance Y slightly, but don't teleport past bastion var newY = Math.min(BASTION_Y - graphics.height, self.y + 100 + Math.random() * 100); // Advance 100-200px // Ensure not teleporting backwards significantly or offscreen top newY = Math.max(graphics.height / 2, newY); self.x = newX; self.y = newY; self.lastY = oldY; // Set lastY to pre-teleport position to avoid false bastion triggers // Add a visual effect for teleport LK.effects.flashObject(self, 0xAA00FF, 300); // Purple flash } else { // Move normally if not teleporting this frame self.y += self.speed; } } else { // Normal movement for other enemy types self.y += self.speed * self.currentSlowAmount; // Apply slowdown to movement } // Decrease slowdown timer and reset slow amount if timer runs out if (self.slowTimer > 0) { self.slowTimer--; if (self.slowTimer <= 0) { self.currentSlowAmount = 1.0; // Reset speed multiplier when slowdown ends // Optional: Remove visual feedback for slowdown here later } } // Apply poison damage if stacks exist if (self.poisonStacks > 0) { self.poisonTimer++; if (self.poisonTimer >= 30) { // Apply poison damage every 0.5 seconds (30 ticks) self.poisonTimer = 0; var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30; // Damage per 0.5s self.health -= poisonDmg; // Optional: Add visual feedback for poison damage here later } // Check if health dropped to zero or below due to poison if (self.health <= 0 && self.parent) { // Ensure enemy is still in the game // Enemy defeated by poison LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy self.destroy(); // The main game update loop needs to handle removing the enemy from the `enemies` array return; // Stop updating this destroyed instance } } // Visual feedback based on health - alpha fade (applies to all types) var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0; graphics.alpha = 0.4 + healthRatio * 0.6; // Fade from 1.0 down to 0.4 // Shaman ability: reduce player ammo periodically if (self.type === 'shaman') { if (self.shamanTimer === undefined) { self.shamanTimer = 0; } self.shamanTimer += 1; var shamanAbilityInterval = 300; // 5 seconds * 60 FPS if (self.shamanTimer >= shamanAbilityInterval) { self.shamanTimer = 0; // Reduce player ammo, but not below 0 arrowsFired = Math.min(maxArrowsBeforeCooldown, arrowsFired + 1); // Make it require one more shot for reload ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Optional: Add a visual/sound effect for shaman ability } } }; //{N} // Adjusted line identifier /** * Method called when the enemy is hit by an arrow. * Handles dodging and health reduction. * @param {number} damage - The amount of damage the arrow deals. * @returns {boolean} - True if the enemy is defeated, false otherwise. */ self.takeDamage = function (damage) { var source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; // Add source parameter, default to null // Check for dodge first (only if dodgeChance is positive) if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) { // Optional: Add a visual effect for dodge here later // console.log(self.type + " dodged!"); return false; // Dodged, so not defeated by this hit } // Check if enemy is Black-tagged and source is an Arrow if (self.tag === 'Black' && source instanceof Arrow) { // Black-tagged enemies are immune to arrow projectiles return false; // Not defeated, arrow has no effect } self.health -= damage; // Check if the damage source was a poison arrow and apply poison stacks if (source && source.isPoison) { // Use the 'source' parameter self.poisonStacks++; // Increase poison stacks self.poisonTimer = 0; // Reset timer to apply damage immediately } // Check if the damage source was a Magic Ball and apply slowdown if (source && source instanceof MagicBall) { self.slowTimer = source.slowDuration; // Apply slowdown duration self.currentSlowAmount = source.slowAmount; // Apply slowdown amount // Optional: Add visual feedback for slowdown here later } // Check if the damage source should apply Green Slowdown if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0) { // Apply Green Slowdown only if the upgrade is active, enemy is Green, and not already slowed self.slowTimer = 10 * 60; // 10 seconds * 60 ticks/sec self.currentSlowAmount = 0.9; // 10% slowdown // Optional: Add visual feedback for green slowdown } // Check if enemy was defeated by this damage var defeated = self.health <= 0; if (defeated && self.type === 'war_elephant') { // If War Elephant is defeated, spawn 5 spearmen for (var i = 0; i < 5; i++) { var spawnPadding = 100; // Padding from edge var spawnX = self.x + (Math.random() * 200 - 100); // Spawn around elephant's x var spawnY = self.y + (Math.random() * 100 - 50); // Spawn around elephant's y, slightly forward // Ensure spawns are within game bounds spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX)); spawnY = Math.max(spawnPadding, Math.min(BASTION_Y - 50, spawnY)); // Don't spawn too close to bastion var newSpearman = new Enemy('spearman', currentEnemySpeed * enemySpeedMultiplier, 10, 0); // Base spearman stats newSpearman.x = spawnX; newSpearman.y = spawnY; newSpearman.lastY = newSpearman.y; game.addChild(newSpearman); enemies.push(newSpearman); } } else if (defeated && self.type === 'hot_air_balloon') { // If Hot Air Balloon is defeated, spawn 5 random non-green enemies var possibleTypes = ['swordsman', 'knight', 'thief', 'shield', 'elite_knight', 'elite_shield', 'dark_bowman']; for (var i = 0; i < 5; i++) { // Choose random enemy type that is not green-tagged var randomType = possibleTypes[Math.floor(Math.random() * possibleTypes.length)]; var randomHP = 1 + Math.floor(Math.random() * 5); // Random HP between 1-5 var randomSpeed = currentEnemySpeed * 0.7 * enemySpeedMultiplier; // Slower than average var newEnemy = new Enemy(randomType, randomSpeed, randomHP, 0); // Spawn in a staggered pattern below the balloon var spawnPadding = 100; var spawnX = self.x + (Math.random() * 300 - 150); // Wider spread than elephant var spawnY = self.y + Math.random() * 200; // Always below the balloon // Ensure spawns are within game bounds spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX)); spawnY = Math.min(BASTION_Y - 50, spawnY); // Don't spawn too close to bastion newEnemy.x = spawnX; newEnemy.y = spawnY; newEnemy.lastY = newEnemy.y; game.addChild(newEnemy); enemies.push(newEnemy); } } // No need to update alpha here, self.update handles it return defeated; // Return true if health is 0 or less }; //{O} // Adjusted line identifier // --- Initialization --- // Apply visual distinctions based on type graphics.tint = 0xFFFFFF; // Reset tint graphics.scale.set(1.0); // Reset scale if (self.type === 'knight') { graphics.tint = 0xCCCCCC; // Grey tint for Knights graphics.scale.set(1.1); } else if (self.type === 'elite_knight') { graphics.tint = 0xFFD700; // Gold tint for Elite Knights graphics.scale.set(1.2); // Slightly larger than normal knight } else if (self.type === 'thief') { graphics.tint = 0xCCFFCC; // Pale Green tint for Thieves graphics.scale.set(0.9); } else if (self.type === 'boss') { graphics.tint = 0xFFCCCC; // Pale Red tint for Bosses graphics.scale.set(1.4); // Make bosses quite large } else if (self.type === 'shield') { graphics.tint = 0xADD8E6; // Light Blue tint for Shield graphics.scale.set(1.2); // Make shield enemies bulky } else if (self.type === 'wizard') { graphics.tint = 0xE0B0FF; // Light Purple tint for Wizard graphics.scale.set(1.0); } else if (self.type === 'elite_shield') { graphics.tint = 0x8A2BE2; // Blue Violet tint for Elite Shield graphics.scale.set(1.3); // Slightly larger than normal shield } //{11} // Modified original line identifier location return self; // Return self for potential inheritance }); /** * Represents a Magic Ball projectile fired by a Wizard Tower. * @param {number} angle - The angle in radians at which the magic ball is fired. */ var MagicBall = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the magic ball graphic asset var graphics = self.attachAsset('magic_ball_asset', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align magic ball graphic with direction self.speed = 15; // Base speed of the magic ball self.damage = 2; // Base damage dealt by this magic ball // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.speed += 5; self.damage += 5; } self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) self.slowDuration = 0; // Duration of slowdown applied on hit self.slowAmount = 0; // Multiplier for enemy speed on hit self.targetEnemy = null; // Track the specific enemy the magic ball is seeking self.seekSpeed = 0.05; // How quickly the magic ball adjusts its direction to seek /** * Update method called each game tick by the LK engine. * Moves the magic ball based on its velocity and seeks target if Aimbot is enabled. */ self.update = function () { if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) { // If aimbot is enabled, a target exists and is still in the game, seek it var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y)); // Smoothly adjust the magic ball's angle towards the target angle var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation // Normalize angle difference to be between -PI and PI if (angleDiff > Math.PI) { angleDiff -= 2 * Math.PI; } if (angleDiff < -Math.PI) { angleDiff += 2 * Math.PI; } // Interpolate angle var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed; self.rotation = newAngle + Math.PI / 2; self.vx = Math.sin(newAngle) * self.speed; self.vy = -Math.cos(newAngle) * self.speed; } else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) { // If aimbot is enabled but current target is gone, find a new target self.targetEnemy = null; // Clear old target var closestEnemy = null; var closestDistance = Infinity; 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); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { self.targetEnemy = closestEnemy; } } self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents a Swordsman ally that attacks enemies in melee range. */ var Swordsman = Container.expand(function () { var self = Container.call(this); // Create and attach the swordsman graphic asset (reusing enemy asset for simplicity) var graphics = self.attachAsset('swordsmanAlly', { anchorX: 0.5, anchorY: 0.5 }); // No tint needed as ally asset has unique colors graphics.scale.set(1.0); // Use the asset's intended scale self.attackRange = 150; // Melee attack range self.attackDamage = 1; // Damage per hit self.attackInterval = 60; // Attack every 1 second (60 ticks) self.attackTimer = 0; // Timer for attacks self.lifetime = 10 * 60; // Lifetime in ticks (10 seconds * 60 ticks/sec) self.lifetimeTimer = 0; // Timer for lifetime /** * Update method called each game tick by the LK engine. * Handles attacking and lifetime. */ self.update = function () { self.attackTimer++; self.lifetimeTimer++; // Check for lifetime var effectiveAttackInterval = Math.max(30, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.5s if (self.lifetimeTimer >= self.lifetime) { self.destroy(); // Remove from the swordsmen array in the main game loop return; // Stop updating this instance } // Find the closest enemy to chase or attack var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate distance to the enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy exists, chase it or attack if within range if (closestEnemy) { // Movement speed toward enemies var moveSpeed = 5; // If not within attack range, move toward the enemy if (closestDistance > self.attackRange) { // Calculate direction vector to enemy var dirX = closestEnemy.x - self.x; var dirY = closestEnemy.y - self.y; // Normalize the direction vector var length = Math.sqrt(dirX * dirX + dirY * dirY); dirX = dirX / length; dirY = dirY / length; // Move toward the enemy self.x += dirX * moveSpeed; self.y += dirY * moveSpeed; } // If within attack range and attack timer is ready, attack else if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Apply damage to the enemy and check if it was defeated var enemyDefeated = closestEnemy.takeDamage(self.attackDamage, self); // Pass the swordsman as source if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score when enemy is defeated by swordsman. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy closestEnemy.destroy(); // Remove from the enemies array in the main game loop } // Optional: Add a visual/sound effect for attack here later } } }; return self; // Return self for potential inheritance }); /** * Represents a Viking ally that throws piercing axes. */ var VikingAlly = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('viking_ally', { anchorX: 0.5, anchorY: 0.5 }); self.attackRange = Infinity; // Infinite attack range self.attackInterval = 150; // Attack every 2.5 seconds (150 ticks) self.attackTimer = 0; // Timer for attacks /** * Update method called each game tick by the LK engine. * Handles finding targets and throwing axes. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the closest enemy within range var closestEnemy = null; var closestDistance = self.attackRange; 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); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, throw an axe if (closestEnemy) { var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); var newAxe = new VikingAxe(angle); newAxe.x = self.x; newAxe.y = self.y; newAxe.lastY = newAxe.y; newAxe.lastX = newAxe.x; game.addChild(newAxe); vikingAxes.push(newAxe); // Add to the vikingAxes array // Optional: Add throwing sound effect here later } } }; return self; }); /** * Represents an Axe thrown by a Viking Ally. * @param {number} angle - The angle in radians at which the axe is thrown. */ var VikingAxe = Container.expand(function (angle) { var self = Container.call(this); var graphics = self.attachAsset('viking_axe', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align axe graphic with direction self.speed = 25; // Speed of the axe // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.speed += 5; } self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) self.damage = 3; // Base damage dealt by this axe // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.damage += 5; } self.pierceLeft = 3; // Can pierce through 3 enemies /** * Update method called each game tick by the LK engine. * Moves the axe based on its velocity and handles rotation. */ self.update = function () { self.x += self.vx; self.y += self.vy; graphics.rotation += 0.2; // Add spinning effect }; return self; }); /** * Represents a Wizard Tower ally that shoots magic balls. */ var WizardTower = Container.expand(function () { var self = Container.call(this); // Create and attach the wizard tower graphic asset var graphics = self.attachAsset('wizard_tower_asset', { anchorX: 0.5, anchorY: 0.5 }); self.attackRange = Infinity; // Infinite attack range self.attackDamage = 0.5; // Low damage, primary effect is slowdown self.attackInterval = 120; // Attack every 2 seconds (120 ticks) self.attackTimer = 0; // Timer for attacks self.slowDuration = 180; // Duration of slowdown effect in ticks (3 seconds) self.slowAmount = 0.5; // Multiplier for enemy speed (0.5 means 50% slower) /** * Update method called each game tick by the LK engine. * Handles attacking enemies. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the closest enemy within range to shoot at var closestEnemy = null; var closestDistance = self.attackRange; // Limit to attack range for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate distance to the enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, fire a magic ball if (closestEnemy) { // Calculate angle towards the closest enemy var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); // Create a new magic ball instance var newMagicBall = new MagicBall(angle); // Position the magic ball at the tower's location newMagicBall.x = self.x; newMagicBall.y = self.y; newMagicBall.lastY = newMagicBall.y; // Initialize lastY for state tracking newMagicBall.lastX = newMagicBall.x; // Initialize lastX for state tracking newMagicBall.slowDuration = self.slowDuration; // Pass slow duration to the magic ball newMagicBall.slowAmount = self.slowAmount; // Pass slow amount to the magic ball // Add the magic ball to the game scene and the tracking array. game.addChild(newMagicBall); magicBalls.push(newMagicBall); // Add to the magicBalls array // Optional: Add a visual/sound effect for magic ball shot here later } } }; return self; // Return self for potential inheritance }); /**** * Initialize Game ****/ // Create the main game instance with a dark background color. var game = new LK.Game({ backgroundColor: 0x101030 // Dark blue/purple background }); /**** * Game Code ****/ //Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property. //Only include the plugins you need to create the game. //We have access to the following plugins. (Note that the variable names used are mandetory for each plugin) //Storage library which should be used for persistent game data //var storage = LK.import('@upit/storage.v1'); //Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions //var facekit = LK.import('@upit/facekit.v1'); // Constants defining game dimensions and key vertical positions. // Placeholder ID, adjust size slightly // Placeholder ID // Placeholder ID, adjust size slightly, reuse flipX // Use actual spearman asset // Use actual war elephant asset var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; var BASTION_Y = GAME_HEIGHT - 250; // Y-coordinate representing the defense line. Enemies crossing this trigger game over. var FIRING_POS_Y = GAME_HEIGHT - 150; // Y-coordinate from where arrows are fired. var FIRING_POS_X = GAME_WIDTH / 2; // X-coordinate from where arrows are fired (center). // Arrays to keep track of active arrows and enemies. var arrows = []; var enemies = []; // Variables for game state and difficulty scaling. var scoreTxt; // Text2 object for displaying the score. var ammoTxt; // Text2 object for displaying the current ammo count. var dragStartX = null; // Starting X coordinate of a touch/mouse drag. var dragStartY = null; // Starting Y coordinate of a touch/mouse drag. var enemySpawnInterval = 120; // Initial ticks between enemy spawns (2 seconds at 60 FPS). var arrowsFired = 0; // Counter for arrows fired since the last cooldown. var cooldownTimer = 0; // Timer in ticks for the cooldown period. var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec). Min duration enforced in upgrade. var maxArrowsBeforeCooldown = 15; // Max arrows before reload (Quiver upgrade) var arrowPierceLevel = 1; // How many enemies an arrow can pierce (Piercing Shots upgrade) var arrowDamage = 1; // Base damage for arrows (Heat-tipped arrows upgrade) var enemySpeedMultiplier = 1.0; // Multiplier for enemy speed (Sabotage upgrade) Min multiplier enforced in upgrade. var minEnemySpawnInterval = 30; // Minimum ticks between enemy spawns (0.5 seconds). var enemySpawnRateDecrease = 0.08; // Amount to decrease spawn interval each time an enemy spawns. var currentEnemySpeed = 3; // Initial base speed of enemies. var maxEnemySpeed = 12; // Maximum base speed enemies can reach. var enemySpeedIncrease = 0.015; // Amount to increase enemy base speed each time an enemy spawns. var archerAllies = []; // Array to hold ArcherAlly instances var swordsmen = []; // Array to hold Swordsman instances var cannonballs = []; // Array to hold Cannonball instances var multiShotEnabled = false; // Flag for More Shots upgrade var poisonShotsEnabled = false; // Flag for Poison Shots upgrade var cannons = []; // Array to hold Cannon instances var allyAttackSpeedMultiplier = 1.0; // Multiplier for ally attack speed var wizardTowers = []; // Array to hold WizardTower instances var magicBalls = []; // Array to hold MagicBall instances var aimbotEnabled = false; // Flag for Aimbot upgrade var greenKillerEnabled = false; // Flag for Green Killer upgrade var refinedProjectilesEnabled = false; // Flag for Refined Projectiles upgrade var greenSlowdownEnabled = false; // Flag for Green Slowdown upgrade var vikingAllies = []; // Array to hold VikingAlly instances var vikingAxes = []; // Array to hold VikingAxe instances // --- Upgrade System --- var lastUpgradeScore = -1; // Score at which the last upgrade was offered var isUpgradePopupActive = false; // Flag indicating if the upgrade choice popup is visible var upgradePopup = null; // Container for the upgrade UI elements var currentMusicPlaying = 'Gamemusic'; // Track which music is currently playing var musicFadeInProgress = false; // Flag to track if a music fade transition is in progress // Helper function to apply Sabotage effect to existing enemies function applySabotage() { for (var i = 0; i < enemies.length; i++) { // We assume Enemy class will use enemySpeedMultiplier when calculating effective speed // If Enemy.speed stores base speed, we might need an effectiveSpeed method or update speed directly. // For simplicity, let's assume Enemy.update uses the global multiplier. // If direct modification is needed: enemies[i].speed = baseSpeed * enemySpeedMultiplier; } // Also update the current base speed calculation baseline if necessary, though modifying the multiplier should suffice. } // Helper function placeholder for adding Archer Ally function addArcherAlly() { // Implementation requires ArcherAlly class definition console.log("Archer Ally upgrade chosen!"); var ally = new ArcherAlly(); // Position the ally next to the player's firing position, slightly offset. ally.x = FIRING_POS_X + 150; // Position to the right of the player ally.y = FIRING_POS_Y; game.addChild(ally); archerAllies.push(ally); // Add to the new archerAllies array } // Helper function to add a Cannon ally function addCannon() { console.log("Cannon upgrade chosen!"); var newCannon = new Cannon(); // Position the cannon near the bastion line newCannon.x = GAME_WIDTH / 2 - 300; // Example position to the left of center newCannon.y = BASTION_Y - 100; // Position above the bastion line game.addChild(newCannon); cannons.push(newCannon); // Add to the cannons array } // Helper function to add a Wizard Tower function addWizardTower() { console.log("Wizard Tower upgrade chosen!"); var newTower = new WizardTower(); // Position the wizard tower near the bastion line, offset from center newTower.x = GAME_WIDTH / 2 + 300; // Example position to the right of center newTower.y = BASTION_Y - 100; // Position above the bastion line game.addChild(newTower); wizardTowers.push(newTower); // Add to the wizardTowers array } // Helper function to add a Viking Ally function addVikingAlly() { console.log("Viking Ally upgrade chosen!"); var newViking = new VikingAlly(); // Position the viking near the bastion line, offset from cannons/towers newViking.x = GAME_WIDTH / 2; // Center for now newViking.y = BASTION_Y - 100; // Position above the bastion line game.addChild(newViking); vikingAllies.push(newViking); // Add to the vikingAllies array } // Helper function placeholder for adding Swordsman function addSwordsman() { // Create a swordsman tower at the bastion line var tower = new Container(); var towerGraphics = tower.attachAsset('swordsmanTower', { anchorX: 0.5, anchorY: 0.5 }); tower.x = FIRING_POS_X; // Position in the center horizontally tower.y = BASTION_Y - 50; // Position just above the bastion line game.addChild(tower); // Set up interval to spawn swordsmen from the tower var spawnInterval = LK.setInterval(function () { var newSwordsman = new Swordsman(); // Position the swordsman near the tower with a slight random offset newSwordsman.x = tower.x + (Math.random() * 100 - 50); // Random offset of ±50px newSwordsman.y = tower.y; game.addChild(newSwordsman); swordsmen.push(newSwordsman); // Add to the swordsmen array }, 3000); // Spawn a swordsman every 3 seconds // Swordsman spawn does not grant score // Store the interval reference in the tower to potentially clear it later tower.spawnInterval = spawnInterval; } // Define all possible upgrades var allUpgrades = [{ id: 'faster_reload', name: 'Faster Reload', description: 'Decrease reload time by 20%', apply: function apply() { cooldownDuration = Math.max(60, Math.floor(cooldownDuration * 0.8)); } }, // Min 1 sec cooldown { id: 'piercing_shots', name: 'Piercing Shots', description: 'Arrows pierce +1 enemy', apply: function apply() { arrowPierceLevel++; } }, { id: 'quiver', name: 'Quiver', description: '+5 Arrows before reload', apply: function apply() { maxArrowsBeforeCooldown += 5; } }, { id: 'heat_tipped', name: 'Heat-tipped Arrows', description: 'Arrows deal +1 damage', apply: function apply() { arrowDamage += 1; console.log("Heat-tipped Arrows upgrade chosen - damage increased to " + arrowDamage); } }, { id: 'archer_ally', name: 'Archer Ally', description: 'Gain an allied archer', apply: addArcherAlly }, { id: 'sabotage', name: 'Sabotage', description: 'Enemies 10% slower', apply: function apply() { enemySpeedMultiplier = Math.max(0.5, enemySpeedMultiplier * 0.9); applySabotage(); } }, // Max 50% slow { id: 'swordsman', name: 'Swordsman', description: 'Gain a melee defender', apply: addSwordsman }, { id: 'more_shots', name: 'More Shots', description: 'Shoot 2 arrows at once', apply: function apply() { multiShotEnabled = true; // Assuming a global flag for this } }, { id: 'poison_shots', name: 'Poison Shots', description: 'Shots deal damage over time', apply: function apply() { poisonShotsEnabled = true; // Assuming a global flag for this } }, { id: 'cannon', name: 'Cannon', description: 'Gain a cannon that targets the strongest enemy', apply: addCannon // Assuming a helper function addCannon }, { id: 'ally_atk_speed', name: 'Ally ATK Speed', description: 'All allies attack faster', apply: function apply() { allyAttackSpeedMultiplier *= 0.8; // Assuming a global multiplier // Apply to existing allies as well in their update logic or a helper function } }, { id: 'wizard_tower', name: 'Wizard Tower', description: 'Gain a wizard tower ally that shoots magic balls that slowdown enemies', apply: addWizardTower // Assuming a helper function addWizardTower }, { id: 'aimbot', name: 'Aimbot', description: 'Arrows seek out enemies', apply: function apply() { aimbotEnabled = true; // Assuming a global flag for this } }, { id: 'green_killer', name: 'Green Killer', description: '+50% Damage vs Green Enemies', apply: function apply() { greenKillerEnabled = true; } }, { id: 'refined_projectiles', name: 'Refined Projectiles', description: 'Non-arrow projectiles +5 damage & faster', apply: function apply() { refinedProjectilesEnabled = true; // Existing projectiles won't update, new ones will have bonus } }, { id: 'viking_ally', name: 'Viking Ally', description: 'Gain a viking that throws piercing axes', apply: addVikingAlly // Assuming helper function addVikingAlly }, { id: 'green_slowdown', name: 'Green Slowdown', description: 'Projectiles slow Green enemies 10% for 10s (no stack)', apply: function apply() { greenSlowdownEnabled = true; } }]; // Function to create and show the upgrade selection popup function showUpgradePopup() { isUpgradePopupActive = true; // Simple pause: Game logic checks isUpgradePopupActive flag in game.update // Switch to upgrade theme music with fade effect if (currentMusicPlaying !== 'UpgradeTheme' && !musicFadeInProgress) { musicFadeInProgress = true; // Fade out current music LK.playMusic('Gamemusic', { fade: { start: 1, end: 0, duration: 800 } }); // After fade out completes, start upgrade theme with fade in LK.setTimeout(function () { LK.playMusic('UpgradeTheme', { fade: { start: 0, end: 1, duration: 1000 } }); currentMusicPlaying = 'UpgradeTheme'; musicFadeInProgress = false; }, 850); } // --- Create Popup UI --- upgradePopup = new Container(); upgradePopup.x = GAME_WIDTH / 2; upgradePopup.y = GAME_HEIGHT / 2; game.addChild(upgradePopup); // Add to game layer for positioning relative to center // Add a semi-transparent background overlay var bg = upgradePopup.attachAsset('bastionLine', { // Reusing an asset for shape width: 1200, height: 800, color: 0x000000, // Black background anchorX: 0.5, anchorY: 0.5, alpha: 0.8 }); // Add title text var title = new Text2('Choose an Upgrade!', { size: 80, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.y = -300; // Position relative to popup center upgradePopup.addChild(title); // --- Select 3 Random Unique Upgrades --- var availableToOffer = allUpgrades.slice(); // Copy available upgrades var choices = []; var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices for (var i = 0; i < numChoices; i++) { var randomIndex = Math.floor(Math.random() * availableToOffer.length); choices.push(availableToOffer[randomIndex]); availableToOffer.splice(randomIndex, 1); // Remove chosen upgrade to ensure uniqueness } // --- Create Buttons for Choices --- var buttonYStart = -150; var buttonSpacing = 180; for (var j = 0; j < choices.length; j++) { var upgrade = choices[j]; var button = createUpgradeButton(upgrade, buttonYStart + j * buttonSpacing); upgradePopup.addChild(button); } } // Function to create a single upgrade button function createUpgradeButton(upgradeData, yPos) { var buttonContainer = new Container(); buttonContainer.y = yPos; // Button background var buttonBg = buttonContainer.attachAsset('bastionLine', { // Reusing asset for shape width: 800, height: 150, color: 0x555555, anchorX: 0.5, anchorY: 0.5 }); // Button text (Name + Description) var nameText = new Text2(upgradeData.name, { size: 50, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 0.5); nameText.y = -25; buttonContainer.addChild(nameText); var descText = new Text2(upgradeData.description, { size: 35, fill: 0xCCCCCC }); descText.anchor.set(0.5, 0.5); descText.y = 30; buttonContainer.addChild(descText); // Make button interactive buttonContainer.interactive = true; // Needed for down event // Event handler for button press buttonContainer.down = function (x, y, obj) { upgradeData.apply(); // Apply the selected upgrade's effect hideUpgradePopup(); // Close the popup }; return buttonContainer; } // Function to hide and destroy the upgrade popup function hideUpgradePopup() { if (upgradePopup) { upgradePopup.destroy(); upgradePopup = null; } isUpgradePopupActive = false; // Switch back to game music with fade effect if (currentMusicPlaying !== 'Gamemusic' && !musicFadeInProgress) { musicFadeInProgress = true; // Fade out upgrade theme LK.playMusic('UpgradeTheme', { fade: { start: 1, end: 0, duration: 800 } }); // After fade out completes, start game music with fade in LK.setTimeout(function () { LK.playMusic('Gamemusic', { fade: { start: 0, end: 1, duration: 1000 } }); currentMusicPlaying = 'Gamemusic'; musicFadeInProgress = false; }, 850); } // Resume game logic (handled by checking flag in game.update) } // --- Visual Setup --- var bastionLine = game.addChild(LK.getAsset('bastionLine', { anchorX: 0.0, // Anchor at the left edge. anchorY: 0.5, // Anchor vertically centered. x: 0, // Position at the left edge of the screen. y: BASTION_Y // Position at the defined bastion Y-coordinate. })); // Create and configure the score display text. scoreTxt = new Text2('0', { size: 150, // Font size. fill: 0xFFFFFF // White color. }); scoreTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge. // Add score text to the GUI layer at the top-center position. LK.gui.top.addChild(scoreTxt); scoreTxt.y = 30; // Add padding below the top edge. Ensure it's clear of the top-left menu icon area. // Create and configure the ammo display text. ammoTxt = new Text2('Ammo: ' + maxArrowsBeforeCooldown, { size: 80, fill: 0xFFFFFF // White color. }); ammoTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge. LK.gui.top.addChild(ammoTxt); ammoTxt.y = 180; // Position below the score text // --- Enemypedia --- var enemypediaButton; var enemypediaPopup = null; var enemyListContainer = null; // Container for enemy list items function showEnemypedia() { isUpgradePopupActive = true; // Pause the game while enemypedia is open enemypediaPopup = new Container(); enemypediaPopup.x = GAME_WIDTH / 2; enemypediaPopup.y = GAME_HEIGHT / 2; game.addChild(enemypediaPopup); // Add a semi-transparent background overlay var bg = enemypediaPopup.attachAsset('bastionLine', { width: 1600, height: 1000, color: 0x000000, anchorX: 0.5, anchorY: 0.5, alpha: 0.8 }); // Add title text var title = new Text2('Enemypedia', { size: 100, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.y = -450; // Position relative to popup center enemypediaPopup.addChild(title); // Add a close button var closeButton = createCloseButton(); closeButton.x = 750; // Position relative to popup center closeButton.y = -450; enemypediaPopup.addChild(closeButton); // Container for the list of enemies (scrollable potentially) enemyListContainer = new Container(); enemyListContainer.y = -350; // Position below title enemypediaPopup.addChild(enemyListContainer); displayEnemyStats(); } function hideEnemypedia() { if (enemypediaPopup) { enemypediaPopup.destroy(); enemypediaPopup = null; } isUpgradePopupActive = false; // Unpause the game } function createCloseButton() { var buttonContainer = new Container(); var buttonBg = buttonContainer.attachAsset('bastionLine', { width: 150, height: 80, color: 0xCC0000, // Red color for close anchorX: 0.5, anchorY: 0.5 }); var buttonText = new Text2('X', { size: 60, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); buttonContainer.addChild(buttonText); buttonContainer.interactive = true; buttonContainer.down = function () { hideEnemypedia(); }; return buttonContainer; } function displayEnemyStats() { // Dummy data representing enemy types and their stats var enemyData = [{ type: 'swordsman', assetId: 'enemy', description: 'Basic melee unit.', baseHealth: 1, baseSpeed: 3, dodgeChance: 0 }, { type: 'knight', assetId: 'knight', description: 'Slow, higher health unit.', baseHealth: 5, baseSpeed: 3 * 0.9, dodgeChance: 0 }, { type: 'thief', assetId: 'thief', description: 'Fast, low health unit with dodge chance.', baseHealth: 1, baseSpeed: 3 * 1.2, dodgeChance: 0.1 }, { type: 'boss', assetId: 'boss', description: 'Very high health boss.', baseHealth: 5, // Base health before scaling baseSpeed: 3 * 0.7, dodgeChance: 0 }, { type: 'shield', assetId: 'shield_enemy', description: 'Very high health, very slow unit.', baseHealth: 20, baseSpeed: 3 * 0.5, dodgeChance: 0 }, { type: 'wizard', assetId: 'wizard_enemy', description: 'Teleports and has moderate speed.', baseHealth: 2, baseSpeed: 3 * 0.7, dodgeChance: 0 }, { type: 'spearman', assetId: 'spearman', description: 'Standard unit, often spawns from Elephants.', baseHealth: 3, baseSpeed: 3 * 1.0, dodgeChance: 0 }, { type: 'war_elephant', assetId: 'war_elephant', description: 'Extremely high health. Spawns spearmen on death.', baseHealth: 500, baseSpeed: 3 * 0.3, dodgeChance: 0 }, { type: 'elite_knight', assetId: 'elite_knight_asset', description: 'High health knight variant.', baseHealth: 45, baseSpeed: 3 * 0.85, dodgeChance: 0 }, { type: 'elite_shield', assetId: 'elite_shield_asset', description: 'Even higher health shield variant.', baseHealth: 200, baseSpeed: 3 * 0.4, dodgeChance: 0 }, { type: 'shaman', assetId: 'shaman_enemy', description: 'Reduces player ammo periodically.', baseHealth: 2, baseSpeed: 3 * 0.8, dodgeChance: 0 }, { type: 'hot_air_balloon', assetId: 'hot_air_balloon_asset', description: 'Very low health. Spawns random enemies on death.', baseHealth: 1, baseSpeed: 3 * 0.2, dodgeChance: 0 }, { type: 'dark_bowman', assetId: 'dark_bowman_asset', description: 'Immune to arrows. Higher health variant.', baseHealth: 5, baseSpeed: 3 * 1.1, dodgeChance: 0 }]; var startY = 0; var spacing = 150; for (var i = 0; i < enemyData.length; i++) { var enemyInfo = enemyData[i]; var enemyItem = new Container(); enemyItem.y = startY + i * spacing; // Add enemy graphic (smaller representation) var enemyGraphic = enemyItem.attachAsset(enemyInfo.assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.5, // Scale down for display scaleY: 0.5, x: -600 // Position to the left }); // Add enemy name var nameText = new Text2(enemyInfo.type.replace(/_/g, ' ').toUpperCase(), { size: 40, fill: 0xFFFFFF }); nameText.anchor.set(0, 0.5); // Align left nameText.x = -500; enemyItem.addChild(nameText); // Add description var descText = new Text2('Description: ' + enemyInfo.description, { size: 30, fill: 0xCCCCCC, wordWrap: true, wordWrapWidth: 800 // Wrap text to fit width }); descText.anchor.set(0, 0); // Align top-left descText.x = -500; descText.y = 30; enemyItem.addChild(descText); // Add stats var statsText = new Text2('Health: ' + enemyInfo.baseHealth + ' | Speed: ' + enemyInfo.baseSpeed.toFixed(1) + ' | Dodge: ' + (enemyInfo.dodgeChance * 100).toFixed(0) + '%', { size: 30, fill: 0xAAAAAA }); statsText.anchor.set(0, 0); // Align top-left statsText.x = -500; statsText.y = 70; enemyItem.addChild(statsText); enemyListContainer.addChild(enemyItem); } } enemypediaButton = new Container(); // Position the button (e.g., bottom right of GUI) enemypediaButton.x = GAME_WIDTH - 200; // Example X enemypediaButton.y = GAME_HEIGHT - 200; // Example Y // Add button background var buttonBg = enemypediaButton.attachAsset('bastionLine', { // Reusing asset for shape width: 300, height: 100, color: 0x444444, // Grey color anchorX: 0.5, anchorY: 0.5 }); // Add button text var buttonText = new Text2('Enemypedia', { size: 40, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); enemypediaButton.addChild(buttonText); // Make button interactive enemypediaButton.interactive = true; enemypediaButton.down = function () { showEnemypedia(); }; // Add the button to the game scene (or GUI if needed) game.addChild(enemypediaButton); // --- Input Event Handlers --- // Handles touch/mouse press down event on the game area. game.down = function (x, y, obj) { // Record the starting position of the drag in game coordinates. var gamePos = game.toLocal({ x: x, y: y }); dragStartX = gamePos.x; dragStartY = gamePos.y; // Potential enhancement: Show an aiming indicator originating from FIRING_POS_X, FIRING_POS_Y towards the drag point. }; // Handles touch/mouse move event while dragging on the game area. game.move = function (x, y, obj) { // Only process if a drag is currently active. if (dragStartX !== null) { var gamePos = game.toLocal({ x: x, y: y }); // Potential enhancement: Update the aiming indicator based on the current drag position (gamePos). } }; // Handles touch/mouse release event on the game area. game.up = function (x, y, obj) { // Only process if a drag was active. if (dragStartX !== null && cooldownTimer <= 0) { // Only allow firing if not on cooldown. var gamePos = game.toLocal({ x: x, y: y }); var dragEndX = gamePos.x; var dragEndY = gamePos.y; // Calculate the difference between the firing position and the release point to determine direction. // A longer drag could potentially mean more power, but we'll keep it simple: direction only. var dx = dragEndX - FIRING_POS_X; var dy = dragEndY - FIRING_POS_Y; // Avoid division by zero or zero vector if start and end points are the same. if (dx === 0 && dy === 0) { // Optionally handle this case, e.g., fire straight up or do nothing. // Let's fire straight up if drag distance is negligible. dy = -1; } // Calculate the angle using atan2. Note the order (dx, dy) and the negation of dy // because the Y-axis is inverted in screen coordinates (positive Y is down). var angle = Math.atan2(dx, -dy); // Create a new arrow instance with the calculated angle. var newArrow = new Arrow(angle); newArrow.x = FIRING_POS_X; newArrow.y = FIRING_POS_Y; // Initialize last position for state tracking (e.g., off-screen detection) newArrow.lastY = newArrow.y; newArrow.lastX = newArrow.x; // Add the arrow to the game scene and the tracking array. game.addChild(newArrow); arrows.push(newArrow); if (multiShotEnabled) { // Fire a second arrow with a slight angle offset var angle2 = angle + Math.PI / 12; // Offset by 15 degrees var newArrow2 = new Arrow(angle2); newArrow2.x = FIRING_POS_X; newArrow2.y = FIRING_POS_Y; newArrow2.lastY = newArrow2.y; newArrow2.lastX = newArrow2.x; game.addChild(newArrow2); arrows.push(newArrow2); // Fire a third arrow with the opposite angle offset var angle3 = angle - Math.PI / 12; // Offset by -15 degrees var newArrow3 = new Arrow(angle3); newArrow3.x = FIRING_POS_X; newArrow3.y = FIRING_POS_Y; newArrow3.lastY = newArrow3.y; newArrow3.lastX = newArrow3.x; game.addChild(newArrow3); arrows.push(newArrow3); } LK.getSound('shoot').play(); // Play shooting sound. arrowsFired++; // Increment arrow count. ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display. // Check if cooldown needs to start based on Quiver upgrade if (arrowsFired >= maxArrowsBeforeCooldown) { cooldownTimer = cooldownDuration; // Start cooldown (duration affected by Faster Reload upgrade) arrowsFired = 0; // Reset arrow count. ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display. LK.getSound('Reload').play(); // Play reload sound. } // Reset drag state. dragStartX = null; dragStartY = null; // Potential enhancement: Hide the aiming indicator. } }; // --- Main Game Update Loop --- // This function is called automatically by the LK engine on every frame (tick). game.update = function () { // --- Upgrade System Check --- var currentScore = LK.getScore(); // Check if score reached a multiple of 10 and is higher than the last upgrade score if (!isUpgradePopupActive && currentScore > 0 && currentScore % 10 === 0 && currentScore !== lastUpgradeScore) { lastUpgradeScore = currentScore; // Mark this score level as having triggered an upgrade offer showUpgradePopup(); // Show the upgrade selection screen } // --- Pause Game Logic if Upgrade Popup is Active --- if (isUpgradePopupActive) { // Potential: Update UI animations if any return; // Skip the rest of the game update loop } // --- Resume Game Logic --- if (isUpgradePopupActive) { return; // Skip enemy movement and spawning if upgrade popup is active } // Decrease cooldown timer if active. if (cooldownTimer > 0) { cooldownTimer--; ammoTxt.setText('Reloading...'); // Show reloading status } else if (ammoTxt.text !== 'Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)) { // Ensure ammo display is correct if not reloading ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); } // 1. Update and check Arrows for (var i = arrows.length - 1; i >= 0; i--) { var arrow = arrows[i]; // Note: LK engine calls arrow.update() automatically as it's added to the game stage. // Initialize last position if it hasn't been set yet (first frame). if (arrow.lastY === undefined) { arrow.lastY = arrow.y; } if (arrow.lastX === undefined) { arrow.lastX = arrow.x; } // Check for collisions between the current arrow and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (arrow.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Calculate damage, applying Green Killer bonus if applicable var damageToDeal = arrow.damage; if (greenKillerEnabled && enemy.tag === 'Green') { damageToDeal *= 1.5; // Apply 50% damage bonus } // Apply damage to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(damageToDeal, arrow); // Pass the arrow object as source if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Handle arrow piercing arrow.pierceLeft--; // Decrease remaining pierces if (arrow.pierceLeft <= 0) { // Arrow has no pierces left, destroy it arrow.destroy(); arrows.splice(i, 1); // Remove arrow from array. hitEnemy = true; // Mark that this arrow is done break; // Stop checking this arrow against other enemies as it's destroyed } else { // Arrow pierced this enemy and can continue // We don't set hitEnemy = true here because the arrow continues // We don't break because it might hit another enemy in the same frame further along its path // Apply Green Slowdown if applicable and arrow is still piercing if (!enemyDefeated && greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } } } } // If the arrow hit an enemy, it was destroyed, so skip to the next arrow. if (hitEnemy) { continue; } // Check if the arrow has gone off-screen (top, left, or right). // Use transition detection: check if it was on screen last frame and is off screen now. var wasOnScreen = arrow.lastY > -arrow.height / 2 && arrow.lastX > -arrow.width / 2 && arrow.lastX < GAME_WIDTH + arrow.width / 2; var isOffScreen = arrow.y < -arrow.height / 2 || // Off top edge arrow.x < -arrow.width / 2 || // Off left edge arrow.x > GAME_WIDTH + arrow.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Arrow is off-screen, destroy it and remove from the array. arrow.destroy(); arrows.splice(i, 1); } else if (!isOffScreen) { // Update last known position only if the arrow is still potentially on screen arrow.lastY = arrow.y; arrow.lastX = arrow.x; } } // 2. Update and check Cannonballs for (var cb = cannonballs.length - 1; cb >= 0; cb--) { var cannonball = cannonballs[cb]; // Note: LK engine calls cannonball.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (cannonball.lastY === undefined) { cannonball.lastY = cannonball.y; } if (cannonball.lastX === undefined) { cannonball.lastX = cannonball.x; } // Check for collisions between the current cannonball and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (cannonball.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply damage to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(cannonball.damage); // Cannonballs don't apply poison if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Cannonballs are destroyed on hit cannonball.destroy(); // Apply Green Slowdown if applicable before destroying cannonball if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } cannonball.destroy(); cannonballs.splice(cb, 1); // Remove cannonball from array. hitEnemy = true; // Mark that this cannonball is done break; // Stop checking this cannonball against other enemies as it's destroyed } } // If the cannonball hit an enemy, it was destroyed, so skip to the next cannonball. if (hitEnemy) { continue; } // Check if the cannonball has gone off-screen. var wasOnScreen = cannonball.lastY > -cannonball.height / 2 && cannonball.lastX > -cannonball.width / 2 && cannonball.lastX < GAME_WIDTH + cannonball.width / 2; var isOffScreen = cannonball.y < -cannonball.height / 2 || // Off top edge cannonball.x < -cannonball.width / 2 || // Off left edge cannonball.x > GAME_WIDTH + cannonball.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Cannonball is off-screen, destroy it and remove from the array. cannonball.destroy(); cannonballs.splice(cb, 1); } else if (!isOffScreen) { // Update last known position only if the cannonball is still potentially on screen cannonball.lastY = cannonball.y; cannonball.lastX = cannonball.x; } } // 3. Update and check Enemies for (var k = enemies.length - 1; k >= 0; k--) { var enemy = enemies[k]; // Note: LK engine calls enemy.update() automatically. // Initialize last position if not set. if (enemy.lastY === undefined) { enemy.lastY = enemy.y; } // Check if the enemy has reached or passed the bastion line. // Use transition detection: was the enemy's bottom edge above the line last frame, // and is it on or below the line now? var enemyBottomY = enemy.y + enemy.height / 2; var enemyLastBottomY = enemy.lastY + enemy.height / 2; var wasAboveBastion = enemyLastBottomY < BASTION_Y; var isAtOrBelowBastion = enemyBottomY >= BASTION_Y; if (wasAboveBastion && isAtOrBelowBastion) { // Enemy reached the bastion! Game Over. LK.getSound('gameOverSfx').play(); // Play game over sound effect. LK.showGameOver(); // Trigger the engine's game over sequence. // LK.showGameOver handles game state reset, no need to manually clear arrays here. return; // Exit the update loop immediately as the game is over. } else { // Update last known position if game is not over for this enemy. enemy.lastY = enemy.y; } } // 3. Update and check Swordsmen (allies) for (var l = swordsmen.length - 1; l >= 0; l--) { var swordsman = swordsmen[l]; // Swordsman update method handles its own lifetime and attacking // Check if the swordsman has been destroyed by its lifetime timer if (!swordsman.parent) { // If it no longer has a parent, it has been destroyed swordsmen.splice(l, 1); // Remove swordsman from array } } // 4. Update and check Cannons (allies) for (var m = cannons.length - 1; m >= 0; m--) { var cannon = cannons[m]; // Cannons do not have a lifetime timer or destruction logic in this basic version // If they had, we'd check !cannon.parent and splice here as in Swordsmen } // 5. Update and check Wizard Towers (allies) for (var wt = wizardTowers.length - 1; wt >= 0; wt--) { var tower = wizardTowers[wt]; // Wizard towers do not have a lifetime timer or destruction logic in this basic version // If they had, we'd check !tower.parent and splice here } // 5.5 Update and check Viking Allies for (var va = vikingAllies.length - 1; va >= 0; va--) { var viking = vikingAllies[va]; // Vikings do not have a lifetime timer or destruction logic in this version // LK engine calls viking.update() automatically. } // 6. Update and check Magic Balls for (var mb = magicBalls.length - 1; mb >= 0; mb--) { var magicBall = magicBalls[mb]; // Note: LK engine calls magicBall.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (magicBall.lastY === undefined) { magicBall.lastY = magicBall.y; } if (magicBall.lastX === undefined) { magicBall.lastX = magicBall.x; } // Check for collisions between the current magic ball and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (magicBall.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply damage (minimal) and slowdown effect to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(magicBall.damage, magicBall); // Pass the magic ball object as source if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Magic balls are destroyed on hit magicBall.destroy(); // Apply Green Slowdown if applicable before destroying magic ball if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { // Note: Enemy.takeDamage already checks for MagicBall source to apply its default slow. // This adds the Green Slowdown effect *on top* if applicable and not already slowed. // However, takeDamage applies slow based on MagicBall properties. Let's apply Green Slowdown here explicitly. enemy.slowTimer = 10 * 60; // 10 seconds enemy.currentSlowAmount = 0.9; // 10% slow } magicBall.destroy(); magicBalls.splice(mb, 1); // Remove magic ball from array. hitEnemy = true; // Mark that this magic ball is done break; // Stop checking this magic ball against other enemies as it's destroyed } } // If the magic ball hit an enemy, it was destroyed, so skip to the next magic ball. if (hitEnemy) { continue; } // Check if the magic ball has gone off-screen. var wasOnScreen = magicBall.lastY > -magicBall.height / 2 && magicBall.lastX > -magicBall.width / 2 && magicBall.lastX < GAME_WIDTH + magicBall.width / 2; var isOffScreen = magicBall.y < -magicBall.height / 2 || // Off top edge magicBall.x < -magicBall.width / 2 || // Off left edge magicBall.x > GAME_WIDTH + magicBall.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Magic ball is off-screen, destroy it and remove from the array. magicBall.destroy(); magicBalls.splice(mb, 1); } else if (!isOffScreen) { // Update last known position only if the magic ball is still potentially on screen magicBall.lastY = magicBall.y; magicBall.lastX = magicBall.x; } } // 6.5 Update and check Viking Axes for (var axeIdx = vikingAxes.length - 1; axeIdx >= 0; axeIdx--) { var axe = vikingAxes[axeIdx]; // Note: LK engine calls axe.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (axe.lastY === undefined) { axe.lastY = axe.y; } if (axe.lastX === undefined) { axe.lastX = axe.x; } // Check for collisions between the current axe and all enemies. var hitEnemyAxe = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; if (axe.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply Green Killer bonus if applicable (Vikings benefit too) var damageToDealAxe = axe.damage; if (greenKillerEnabled && enemy.tag === 'Green') { damageToDealAxe *= 1.5; // Apply 50% damage bonus } // Apply damage and check if defeated var enemyDefeated = enemy.takeDamage(damageToDealAxe, axe); // Pass axe as source for potential effects if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Handle axe piercing axe.pierceLeft--; // Decrease remaining pierces if (axe.pierceLeft <= 0) { // Axe has no pierces left, destroy it axe.destroy(); vikingAxes.splice(axeIdx, 1); // Remove axe from array. hitEnemyAxe = true; // Mark that this axe is done break; // Stop checking this axe against other enemies } else { // Axe pierced this enemy and can continue // Potentially apply Green Slowdown if enabled if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } } } } // If the axe hit and was destroyed, skip to the next axe. if (hitEnemyAxe) { continue; } // Check if the axe has gone off-screen. var wasOnScreenAxe = axe.lastY > -axe.height / 2 && axe.lastX > -axe.width / 2 && axe.lastX < GAME_WIDTH + axe.width / 2; var isOffScreenAxe = axe.y < -axe.height / 2 || axe.y > GAME_HEIGHT + axe.height / 2 || axe.x < -axe.width / 2 || axe.x > GAME_WIDTH + axe.width / 2; if (wasOnScreenAxe && isOffScreenAxe) { axe.destroy(); vikingAxes.splice(axeIdx, 1); } else if (!isOffScreenAxe) { axe.lastY = axe.y; axe.lastX = axe.x; } } // 7. Spawn new Enemies periodically // Use LK.ticks and the spawn interval. Ensure interval doesn't go below minimum. if (LK.ticks % Math.max(minEnemySpawnInterval, Math.floor(enemySpawnInterval)) === 0) { var currentScore = LK.getScore(); var baseSpeedForSpawn = Math.min(maxEnemySpeed, currentEnemySpeed); // Base speed adjusted by game difficulty progression // Determine if this spawn *could* be a boss (e.g., every 7th spawn after score 10) var potentialBoss = (enemies.length + 1) % 7 === 0 && currentScore >= 10; var typeToSpawn = 'swordsman'; // Default type var enemyHealth = 1; // Default health var enemyDodgeChance = 0; // Default dodge chance var enemySpeed = baseSpeedForSpawn; // Start with base speed // Determine base type based on score thresholds and randomness var possibleTypes = ['swordsman']; if (currentScore >= 10) { // Spearman appears at score 10 possibleTypes.push('spearman'); } if (currentScore >= 15) { possibleTypes.push('thief'); } //{3s} // Adjusted original line identifier if (currentScore >= 23) { possibleTypes.push('knight'); } //{3t} // Adjusted original line identifier if (currentScore >= 25) { possibleTypes.push('wizard'); } // Wizards start appearing at score 25 if (currentScore >= 30) { possibleTypes.push('shield'); } // Shields start appearing at score 30 if (currentScore >= 35) { possibleTypes.push('shaman'); // Shaman appears at score 35 console.log("Shaman added to possible enemy types"); } if (currentScore >= 55) { possibleTypes.push('dark_bowman'); // Dark bowman appears at score 55 } if (currentScore >= 100) { possibleTypes.push('elite_knight'); } // Elite Knights start appearing at score 100 if (currentScore >= 112) { possibleTypes.push('elite_shield'); // Elite Shields appear at score 112 } if (currentScore >= 125) { // War Elephant appears at score 125 possibleTypes.push('war_elephant'); } if (currentScore >= 154) { // Hot Air Balloon appears at score 154 possibleTypes.push('hot_air_balloon'); } // Randomly select a type from the available pool for this score level var chosenTypeIndex = Math.floor(Math.random() * possibleTypes.length); typeToSpawn = possibleTypes[chosenTypeIndex]; // Set stats based on chosen type and apply score-based scaling if (typeToSpawn === 'knight') { var baseKnightHP = 5; var hpIncreaseIntervalKnight = 30; // Health increases every 30 score points after appearing var hpIncreasesKnight = Math.floor(Math.max(0, currentScore - 23) / hpIncreaseIntervalKnight); enemyHealth = baseKnightHP + hpIncreasesKnight * 5; enemySpeed *= 0.9; // Knights are slightly slower than base swordsman speed } else if (typeToSpawn === 'thief') { enemyHealth = 1; // Thieves are fragile enemyDodgeChance = 0.10; // 10% dodge chance var speedIncreaseIntervalThief = 25; // Speed increases every 25 score points after appearing var speedIncreasePercentThief = 0.05; // 5% speed increase each time var speedIncreasesThief = Math.floor(Math.max(0, currentScore - 15) / speedIncreaseIntervalThief); var thiefSpeedMultiplier = Math.pow(1 + speedIncreasePercentThief, speedIncreasesThief); enemySpeed *= 1.2 * thiefSpeedMultiplier; // Thieves are faster base + scaling speed } else if (typeToSpawn === 'shield') { var baseShieldHP = 20; var hpIncreaseIntervalShield = 35; // Gains HP every 35 score points after appearing var hpIncreasesShield = Math.floor(Math.max(0, currentScore - 30) / hpIncreaseIntervalShield); enemyHealth = baseShieldHP + hpIncreasesShield * 20; enemySpeed *= 0.5; // Shield enemies are very slow enemyDodgeChance = 0; } else if (typeToSpawn === 'shaman') { var baseShamanHP = 2; var statIncreaseIntervalShaman = 10; // Gains stats every 10 score points after appearing var statIncreasesShaman = Math.floor(Math.max(0, currentScore - 35) / statIncreaseIntervalShaman); enemyHealth = baseShamanHP + statIncreasesShaman * 1; // Gains 1 HP per interval enemySpeed *= 0.8 * Math.pow(1.04, statIncreasesShaman); // Base speed, gains 4% speed per interval enemyDodgeChance = 0; // Shaman cannot dodge } else if (typeToSpawn === 'wizard') { var baseWizardHP = 2; var statIncreaseIntervalWizard = 10; // Gains stats every 10 score points after appearing var statIncreasesWizard = Math.floor(Math.max(0, currentScore - 25) / statIncreaseIntervalWizard); enemyHealth = baseWizardHP + statIncreasesWizard * 1; // Gains 1 HP per interval enemySpeed *= 0.7 * Math.pow(1.05, statIncreasesWizard); // Slow base, gains 5% speed per interval enemyDodgeChance = 0; } else if (typeToSpawn === 'elite_knight') { var baseEliteKnightHP = 45; var hpIncreaseIntervalElite = 30; // Gains HP every 30 score points after appearing var hpIncreasesElite = Math.floor(Math.max(0, currentScore - 100) / hpIncreaseIntervalElite); enemyHealth = baseEliteKnightHP + hpIncreasesElite * 15; enemySpeed *= 0.85; // Slightly slower than base knight speed enemyDodgeChance = 0; } else if (typeToSpawn === 'elite_shield') { var baseEliteShieldHP = 200; // Starts with 200 HP var hpIncreaseIntervalEliteShield = 20; // Gains HP every 20 score points after appearing var hpIncreasesEliteShield = Math.floor(Math.max(0, currentScore - 112) / hpIncreaseIntervalEliteShield); enemyHealth = baseEliteShieldHP + hpIncreasesEliteShield * 50; // Gains 50 HP per interval enemySpeed *= 0.4; // Elite Shield enemies are slower than regular shield enemies enemyDodgeChance = 0; } else if (typeToSpawn === 'spearman') { var baseSpearmanHP = 3; var hpIncreaseIntervalSpearman = 10; // Gains HP every 10 score points after appearing var hpIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / hpIncreaseIntervalSpearman); enemyHealth = baseSpearmanHP + hpIncreasesSpearman * 3; // Gains 3 HP per interval var speedIncreaseIntervalSpearman = 10; // Gains speed every 10 score points after appearing var speedIncreasePercentSpearman = 0.05; // 5% speed increase each time var speedIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / speedIncreaseIntervalSpearman); var spearmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentSpearman, speedIncreasesSpearman); enemySpeed *= 1.0 * spearmanSpeedMultiplier; // Base speed + scaling speed enemyDodgeChance = 0; } else if (typeToSpawn === 'war_elephant') { var baseElephantHP = 500; var hpIncreaseIntervalElephant = 15; // Gains HP every 15 score points after appearing var hpIncreasesElephant = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalElephant); enemyHealth = baseElephantHP + hpIncreasesElephant * 100; // Gains 100 HP per interval enemySpeed *= 0.3; // War Elephants are very slow enemyDodgeChance = 0; // Note: War elephant death spawns spearmen - this will be handled in the Enemy death logic. } else if (typeToSpawn === 'hot_air_balloon') { enemyHealth = 1; // Always has 1 HP enemySpeed *= 0.2; // Very slow movement enemyDodgeChance = 0; // Note: Hot air balloon death spawns 5 random non-green enemies - handled in Enemy death logic } else if (typeToSpawn === 'dark_bowman') { var baseDarkBowmanHP = 5; var statIncreaseIntervalBowman = 10; // Gains stats every 10 score points after appearing var statIncreasesBowman = Math.floor(Math.max(0, currentScore - 55) / statIncreaseIntervalBowman); enemyHealth = baseDarkBowmanHP + statIncreasesBowman * 3; // Gains 3 HP per interval var speedIncreasePercentBowman = 0.05; // 5% speed increase each time var bowmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentBowman, statIncreasesBowman); enemySpeed *= 1.1 * bowmanSpeedMultiplier; // Slightly faster than base speed + scaling enemyDodgeChance = 0; // Note: Dark bowman has Black tag making it immune to arrows } else { // Swordsman (default) enemyHealth = 1; // Speed remains baseSpeedForSpawn initially } // Check if this spawn should be overridden to be a Boss if (potentialBoss) { typeToSpawn = 'boss'; // Set type to boss enemyHealth = 5 + Math.floor(currentScore / 8); // Boss health scales significantly with score enemySpeed = baseSpeedForSpawn * 0.7; // Bosses are slower but much tougher (reset speed based on base) enemyDodgeChance = 0; // Bosses typically don't dodge } // Apply the global Sabotage speed multiplier AFTER type-specific adjustments enemySpeed *= enemySpeedMultiplier; // Create the new enemy instance with the calculated stats var newEnemy = new Enemy(typeToSpawn, enemySpeed, enemyHealth, enemyDodgeChance); // Position the new enemy at the top, random horizontal position with padding // Use the actual width of the created enemy's graphic for padding calculation // Need to access width after creation, use a sensible default or estimate if needed before creation var tempAsset = LK.getAsset(newEnemy.assetId || 'enemy', {}); // Get asset dimensions (might need refinement if assetId isn't on newEnemy yet) var spawnPadding = (tempAsset ? tempAsset.width / 2 : 100) + 20; // Use default if asset not found easily newEnemy.x = spawnPadding + Math.random() * (GAME_WIDTH - 2 * spawnPadding); newEnemy.y = -(tempAsset ? tempAsset.height / 2 : 100); // Start just above the top edge. // Initialize last position for state tracking (used for bastion collision check). newEnemy.lastY = newEnemy.y; // Add the new enemy to the game scene and the tracking array. game.addChild(newEnemy); enemies.push(newEnemy); // Increase difficulty for the next spawn: decrease spawn interval and increase base speed. // These affect the *next* potential spawn's base calculations. enemySpawnInterval -= enemySpawnRateDecrease; currentEnemySpeed += enemySpeedIncrease; } //{2s} // Adjusted original line identifier }; // --- Initial Game Setup --- // Set the initial score text based on the starting score (which is 0). scoreTxt.setText(LK.getScore()); // Set the initial ammo display. if (ammoTxt) { // Ensure ammoTxt is initialized ammoTxt.setText('Ammo: ' + maxArrowsBeforeCooldown); } ; // Play the background music. // Check if we need to show a title screen first if (!isGameStarted) {// Assuming a flag 'isGameStarted' for title screen state // Don't play music immediately if showing title screen } else { LK.playMusic('Gamemusic'); ; } // Placeholder for the title screen container var titleScreen = null; // Flag to track game start state var isGameStarted = false; // Function to show the title screen function showTitleScreen() { isGameStarted = false; // Ensure game is not started // Create the title screen container titleScreen = new Container(); titleScreen.x = GAME_WIDTH / 2; titleScreen.y = GAME_HEIGHT / 2; game.addChild(titleScreen); // Add title text var titleText = new Text2('Defend Your Bastion!', { size: 120, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0.5); titleText.y = -200; titleScreen.addChild(titleText); // Add a simple instruction text var instructionText = new Text2('Tap to Start', { size: 60, fill: 0xCCCCCC }); instructionText.anchor.set(0.5, 0.5); instructionText.y = 100; titleScreen.addChild(instructionText); // Make the title screen interactive to start the game titleScreen.interactive = true; titleScreen.down = function () { startGame(); // Start the game when tapped }; // Pause game logic while title screen is active isUpgradePopupActive = true; // Reusing this flag to pause game logic } // Function to start the game function startGame() { isGameStarted = true; // Set game started flag isUpgradePopupActive = false; // Unpause game logic if (titleScreen) { titleScreen.destroy(); titleScreen = null; } // Reset game state if needed (LK might handle this on game start) // Play game music LK.playMusic('Gamemusic'); } // Initially show the title screen showTitleScreen();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
/**
* Represents an allied archer that shoots arrows independently.
*/
var ArcherAlly = Container.expand(function () {
var self = Container.call(this);
// Create and attach the archer graphic asset
var graphics = self.attachAsset('Archer', {
anchorX: 0.5,
anchorY: 0.5
});
// No rotation needed for 'Archer' asset as it's already upright and flipped
self.fireTimer = 0; // Timer for shooting
self.fireInterval = 180; // Shoot every 3 seconds (3 * 60 ticks)
/**
* Update method called each game tick by the LK engine.
* Handles firing logic.
*/
self.update = function () {
self.fireTimer++;
var effectiveFireInterval = Math.max(60, self.fireInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.fireTimer >= effectiveFireInterval) {
self.fireTimer = 0; // Reset timer
// Find the closest enemy to shoot at
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, fire an arrow
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new arrow instance
var newArrow = new Arrow(angle);
// Apply multi-shot to allies if the upgrade is enabled for the player
if (multiShotEnabled) {
var angle2 = angle + Math.PI / 12; // Offset by 15 degrees
var newArrow2 = new Arrow(angle2);
newArrow2.x = self.x;
newArrow2.y = self.y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
game.addChild(newArrow2);
arrows.push(newArrow2);
var angle3 = angle - Math.PI / 12; // Offset by -15 degrees
var newArrow3 = new Arrow(angle3);
newArrow3.x = self.x;
newArrow3.y = self.y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
newArrow.x = self.x;
newArrow.y = self.y;
// Ally arrows do not count towards the player's reload counter
// The Arrow class handles piercing level based on player upgrade, but the ally doesn't benefit from player reload.
// For simplicity, we'll let the ally benefit from player's piercing upgrade.
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// Add the arrow to the game scene and the tracking array.
game.addChild(newArrow);
arrows.push(newArrow); // Add to the same arrows array for collision detection
// Ally doesn't play the 'shoot' sound
}
}
};
return self; // Return self for potential inheritance
});
// Sound when an enemy reaches the bastion
// No plugins needed for this version of the game.
/**
* Represents an Arrow fired by the player.
* @param {number} angle - The angle in radians at which the arrow is fired.
*/
var Arrow = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the arrow graphic asset
var graphics = self.attachAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align arrow graphic with direction
self.speed = 30; // Speed of the arrow in pixels per tick
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.pierceLeft = arrowPierceLevel; // How many more enemies this arrow can pierce
self.damage = arrowDamage; // Damage dealt by this arrow
self.isPoison = poisonShotsEnabled; // Flag if this arrow applies poison
self.targetEnemy = null; // Potential target for aimbot
self.seekSpeed = 0.1; // How quickly the arrow adjusts its direction to seek
if (aimbotEnabled) {
// Find the closest enemy to seek if aimbot is enabled
var closestEnemy = null;
var closestDistance = Infinity;
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);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.targetEnemy = closestEnemy;
}
}
/**
* Update method called each game tick by the LK engine.
* Moves the arrow based on its velocity.
*/
self.update = function () {
if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) {
// If aimbot is enabled, a target exists and is still in the game, seek it
var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y));
// Smoothly adjust the arrow's angle towards the target angle
var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation
// Normalize angle difference to be between -PI and PI
if (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
if (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Interpolate angle
var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed;
self.rotation = newAngle + Math.PI / 2;
self.vx = Math.sin(newAngle) * self.speed;
self.vy = -Math.cos(newAngle) * self.speed;
} else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) {
// If aimbot is enabled but current target is gone, find a new target
self.targetEnemy = null; // Clear old target
var closestEnemy = null;
var closestDistance = Infinity;
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);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.targetEnemy = closestEnemy;
}
}
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Cannon ally that targets the strongest enemy.
*/
var Cannon = Container.expand(function () {
var self = Container.call(this);
// Create and attach the cannon graphic asset (need to add a new asset for this)
// For now, using a placeholder asset, will need a 'cannon_asset'
var graphics = self.attachAsset('cannon_asset', {
// Use actual cannon asset
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = 800; // Long attack range
self.attackDamage = 10; // High damage
self.attackInterval = 300; // Attack every 5 seconds (300 ticks)
self.attackTimer = 0; // Timer for attacks
self.rotation = 0; // For aiming visual if implemented later
/**
* Update method called each game tick by the LK engine.
* Handles attacking the strongest enemy.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the strongest enemy (highest health)
var strongestEnemy = null;
var maxHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > maxHealth) {
maxHealth = enemy.health;
strongestEnemy = enemy;
}
}
// If a strongest enemy is found within range, attack it
if (strongestEnemy) {
// Calculate angle towards the strongest enemy
var angle = Math.atan2(strongestEnemy.x - self.x, -(strongestEnemy.y - self.y));
// Create a new cannonball instance
var newCannonball = new Cannonball(angle);
// Position the cannonball at the cannon's location
newCannonball.x = self.x;
newCannonball.y = self.y;
newCannonball.lastY = newCannonball.y; // Initialize lastY for state tracking
newCannonball.lastX = newCannonball.x; // Initialize lastX for state tracking
// Add the cannonball to the game scene and the tracking array.
game.addChild(newCannonball);
cannonballs.push(newCannonball); // Add to the new cannonballs array
// Optional: Add a visual/sound effect for cannon shot here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents a Cannonball fired by a Cannon.
* @param {number} angle - The angle in radians at which the cannonball is fired.
*/
var Cannonball = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the cannonball graphic asset
var graphics = self.attachAsset('cannonball', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align cannonball graphic with direction
self.speed = 20; // Base speed of the cannonball
self.damage = 10; // Base damage dealt by this cannonball
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
/**
* Update method called each game tick by the LK engine.
* Moves the cannonball based on its velocity.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents an Enemy attacker moving towards the bastion.
* @param {string} type - The type of enemy ('swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight').
* @param {number} speed - The final calculated speed of the enemy.
* @param {number} health - The initial and maximum health of the enemy.
* @param {number} dodgeChance - The chance (0 to 1) for the enemy to dodge an attack.
*/
var Enemy = Container.expand(function (type, speed, health, dodgeChance) {
var self = Container.call(this);
// Set asset based on type
var assetId = 'enemy'; // Default swordsman asset
if (type === 'knight') {
assetId = 'knight';
} else if (type === 'elite_knight') {
assetId = 'elite_knight_asset'; // Use specific asset for elite knight
} else if (type === 'thief') {
assetId = 'thief';
} else if (type === 'boss') {
assetId = 'boss';
} else if (type === 'shield') {
assetId = 'shield_enemy'; // Use specific asset for shield
} else if (type === 'wizard') {
assetId = 'wizard_enemy'; // Use specific asset for wizard
} else if (type === 'spearman') {
assetId = 'spearman'; // Use specific asset for spearman
} else if (type === 'war_elephant') {
assetId = 'war_elephant'; // Use specific asset for war elephant
} else if (type === 'elite_shield') {
assetId = 'elite_shield_asset'; // Use specific asset for elite shield
} else if (type === 'shaman') {
assetId = 'shaman_enemy'; // Use specific asset for shaman
} else if (type === 'hot_air_balloon') {
assetId = 'hot_air_balloon_asset'; // Use specific asset for hot air balloon
} else if (type === 'dark_bowman') {
assetId = 'dark_bowman_asset'; // Use specific asset for dark bowman
}
var graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.type = type; // 'swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight', 'shaman', etc.
self.speed = speed; // Final calculated speed
self.health = health;
self.maxHealth = health; // Store max health for visual feedback
self.dodgeChance = dodgeChance || 0; // Default to 0 if undefined
self.poisonStacks = 0; // Number of poison stacks
self.poisonTimer = 0; // Timer for poison damage
self.poisonDamagePerTick = 0.05; // Damage per tick per stack
self.slowTimer = 0; // Timer for slowdown effect
self.currentSlowAmount = 1.0; // Multiplier for current slowdown effect (1.0 means no slow)
// Initialize shaman-specific properties
if (type === 'shaman') {
self.shamanTimer = 0;
}
// Set tag property based on type
self.tag = null; // Default tag is null
if (self.type === 'wizard' || self.type === 'spearman' || self.type === 'war_elephant' || self.type === 'shaman') {
self.tag = 'Green';
} else if (self.type === 'dark_bowman') {
self.tag = 'Black'; // Black-tagged enemies are immune to arrows
}
// Wizard-specific properties
if (self.type === 'wizard') {
self.teleportTimer = 0;
self.teleportInterval = 180; // 3 seconds * 60 FPS
}
// --- Public Methods (defined before use) ---
/**
* Update method called each game tick by the LK engine.
* Moves the enemy downwards (or teleports for wizard) and updates visual feedback.
*/
self.update = function () {
if (self.type === 'wizard') {
self.teleportTimer = (self.teleportTimer || 0) + 1; // Initialize timer if needed
if (self.teleportTimer >= self.teleportInterval) {
self.teleportTimer = 0;
// Teleport logic: Random X, slightly advanced Y
var oldY = self.y;
var teleportPadding = graphics.width / 2 + 20; // Use actual graphic width
var newX = teleportPadding + Math.random() * (GAME_WIDTH - 2 * teleportPadding);
// Advance Y slightly, but don't teleport past bastion
var newY = Math.min(BASTION_Y - graphics.height, self.y + 100 + Math.random() * 100); // Advance 100-200px
// Ensure not teleporting backwards significantly or offscreen top
newY = Math.max(graphics.height / 2, newY);
self.x = newX;
self.y = newY;
self.lastY = oldY; // Set lastY to pre-teleport position to avoid false bastion triggers
// Add a visual effect for teleport
LK.effects.flashObject(self, 0xAA00FF, 300); // Purple flash
} else {
// Move normally if not teleporting this frame
self.y += self.speed;
}
} else {
// Normal movement for other enemy types
self.y += self.speed * self.currentSlowAmount; // Apply slowdown to movement
}
// Decrease slowdown timer and reset slow amount if timer runs out
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer <= 0) {
self.currentSlowAmount = 1.0; // Reset speed multiplier when slowdown ends
// Optional: Remove visual feedback for slowdown here later
}
}
// Apply poison damage if stacks exist
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
// Apply poison damage every 0.5 seconds (30 ticks)
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30; // Damage per 0.5s
self.health -= poisonDmg;
// Optional: Add visual feedback for poison damage here later
}
// Check if health dropped to zero or below due to poison
if (self.health <= 0 && self.parent) {
// Ensure enemy is still in the game
// Enemy defeated by poison
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
self.destroy();
// The main game update loop needs to handle removing the enemy from the `enemies` array
return; // Stop updating this destroyed instance
}
}
// Visual feedback based on health - alpha fade (applies to all types)
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6; // Fade from 1.0 down to 0.4
// Shaman ability: reduce player ammo periodically
if (self.type === 'shaman') {
if (self.shamanTimer === undefined) {
self.shamanTimer = 0;
}
self.shamanTimer += 1;
var shamanAbilityInterval = 300; // 5 seconds * 60 FPS
if (self.shamanTimer >= shamanAbilityInterval) {
self.shamanTimer = 0;
// Reduce player ammo, but not below 0
arrowsFired = Math.min(maxArrowsBeforeCooldown, arrowsFired + 1); // Make it require one more shot for reload
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
// Optional: Add a visual/sound effect for shaman ability
}
}
}; //{N} // Adjusted line identifier
/**
* Method called when the enemy is hit by an arrow.
* Handles dodging and health reduction.
* @param {number} damage - The amount of damage the arrow deals.
* @returns {boolean} - True if the enemy is defeated, false otherwise.
*/
self.takeDamage = function (damage) {
var source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
// Add source parameter, default to null
// Check for dodge first (only if dodgeChance is positive)
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
// Optional: Add a visual effect for dodge here later
// console.log(self.type + " dodged!");
return false; // Dodged, so not defeated by this hit
}
// Check if enemy is Black-tagged and source is an Arrow
if (self.tag === 'Black' && source instanceof Arrow) {
// Black-tagged enemies are immune to arrow projectiles
return false; // Not defeated, arrow has no effect
}
self.health -= damage;
// Check if the damage source was a poison arrow and apply poison stacks
if (source && source.isPoison) {
// Use the 'source' parameter
self.poisonStacks++; // Increase poison stacks
self.poisonTimer = 0; // Reset timer to apply damage immediately
}
// Check if the damage source was a Magic Ball and apply slowdown
if (source && source instanceof MagicBall) {
self.slowTimer = source.slowDuration; // Apply slowdown duration
self.currentSlowAmount = source.slowAmount; // Apply slowdown amount
// Optional: Add visual feedback for slowdown here later
}
// Check if the damage source should apply Green Slowdown
if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0) {
// Apply Green Slowdown only if the upgrade is active, enemy is Green, and not already slowed
self.slowTimer = 10 * 60; // 10 seconds * 60 ticks/sec
self.currentSlowAmount = 0.9; // 10% slowdown
// Optional: Add visual feedback for green slowdown
}
// Check if enemy was defeated by this damage
var defeated = self.health <= 0;
if (defeated && self.type === 'war_elephant') {
// If War Elephant is defeated, spawn 5 spearmen
for (var i = 0; i < 5; i++) {
var spawnPadding = 100; // Padding from edge
var spawnX = self.x + (Math.random() * 200 - 100); // Spawn around elephant's x
var spawnY = self.y + (Math.random() * 100 - 50); // Spawn around elephant's y, slightly forward
// Ensure spawns are within game bounds
spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX));
spawnY = Math.max(spawnPadding, Math.min(BASTION_Y - 50, spawnY)); // Don't spawn too close to bastion
var newSpearman = new Enemy('spearman', currentEnemySpeed * enemySpeedMultiplier, 10, 0); // Base spearman stats
newSpearman.x = spawnX;
newSpearman.y = spawnY;
newSpearman.lastY = newSpearman.y;
game.addChild(newSpearman);
enemies.push(newSpearman);
}
} else if (defeated && self.type === 'hot_air_balloon') {
// If Hot Air Balloon is defeated, spawn 5 random non-green enemies
var possibleTypes = ['swordsman', 'knight', 'thief', 'shield', 'elite_knight', 'elite_shield', 'dark_bowman'];
for (var i = 0; i < 5; i++) {
// Choose random enemy type that is not green-tagged
var randomType = possibleTypes[Math.floor(Math.random() * possibleTypes.length)];
var randomHP = 1 + Math.floor(Math.random() * 5); // Random HP between 1-5
var randomSpeed = currentEnemySpeed * 0.7 * enemySpeedMultiplier; // Slower than average
var newEnemy = new Enemy(randomType, randomSpeed, randomHP, 0);
// Spawn in a staggered pattern below the balloon
var spawnPadding = 100;
var spawnX = self.x + (Math.random() * 300 - 150); // Wider spread than elephant
var spawnY = self.y + Math.random() * 200; // Always below the balloon
// Ensure spawns are within game bounds
spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX));
spawnY = Math.min(BASTION_Y - 50, spawnY); // Don't spawn too close to bastion
newEnemy.x = spawnX;
newEnemy.y = spawnY;
newEnemy.lastY = newEnemy.y;
game.addChild(newEnemy);
enemies.push(newEnemy);
}
}
// No need to update alpha here, self.update handles it
return defeated; // Return true if health is 0 or less
}; //{O} // Adjusted line identifier
// --- Initialization ---
// Apply visual distinctions based on type
graphics.tint = 0xFFFFFF; // Reset tint
graphics.scale.set(1.0); // Reset scale
if (self.type === 'knight') {
graphics.tint = 0xCCCCCC; // Grey tint for Knights
graphics.scale.set(1.1);
} else if (self.type === 'elite_knight') {
graphics.tint = 0xFFD700; // Gold tint for Elite Knights
graphics.scale.set(1.2); // Slightly larger than normal knight
} else if (self.type === 'thief') {
graphics.tint = 0xCCFFCC; // Pale Green tint for Thieves
graphics.scale.set(0.9);
} else if (self.type === 'boss') {
graphics.tint = 0xFFCCCC; // Pale Red tint for Bosses
graphics.scale.set(1.4); // Make bosses quite large
} else if (self.type === 'shield') {
graphics.tint = 0xADD8E6; // Light Blue tint for Shield
graphics.scale.set(1.2); // Make shield enemies bulky
} else if (self.type === 'wizard') {
graphics.tint = 0xE0B0FF; // Light Purple tint for Wizard
graphics.scale.set(1.0);
} else if (self.type === 'elite_shield') {
graphics.tint = 0x8A2BE2; // Blue Violet tint for Elite Shield
graphics.scale.set(1.3); // Slightly larger than normal shield
} //{11} // Modified original line identifier location
return self; // Return self for potential inheritance
});
/**
* Represents a Magic Ball projectile fired by a Wizard Tower.
* @param {number} angle - The angle in radians at which the magic ball is fired.
*/
var MagicBall = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the magic ball graphic asset
var graphics = self.attachAsset('magic_ball_asset', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align magic ball graphic with direction
self.speed = 15; // Base speed of the magic ball
self.damage = 2; // Base damage dealt by this magic ball
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.slowDuration = 0; // Duration of slowdown applied on hit
self.slowAmount = 0; // Multiplier for enemy speed on hit
self.targetEnemy = null; // Track the specific enemy the magic ball is seeking
self.seekSpeed = 0.05; // How quickly the magic ball adjusts its direction to seek
/**
* Update method called each game tick by the LK engine.
* Moves the magic ball based on its velocity and seeks target if Aimbot is enabled.
*/
self.update = function () {
if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) {
// If aimbot is enabled, a target exists and is still in the game, seek it
var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y));
// Smoothly adjust the magic ball's angle towards the target angle
var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation
// Normalize angle difference to be between -PI and PI
if (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
if (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Interpolate angle
var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed;
self.rotation = newAngle + Math.PI / 2;
self.vx = Math.sin(newAngle) * self.speed;
self.vy = -Math.cos(newAngle) * self.speed;
} else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) {
// If aimbot is enabled but current target is gone, find a new target
self.targetEnemy = null; // Clear old target
var closestEnemy = null;
var closestDistance = Infinity;
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);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.targetEnemy = closestEnemy;
}
}
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Swordsman ally that attacks enemies in melee range.
*/
var Swordsman = Container.expand(function () {
var self = Container.call(this);
// Create and attach the swordsman graphic asset (reusing enemy asset for simplicity)
var graphics = self.attachAsset('swordsmanAlly', {
anchorX: 0.5,
anchorY: 0.5
});
// No tint needed as ally asset has unique colors
graphics.scale.set(1.0); // Use the asset's intended scale
self.attackRange = 150; // Melee attack range
self.attackDamage = 1; // Damage per hit
self.attackInterval = 60; // Attack every 1 second (60 ticks)
self.attackTimer = 0; // Timer for attacks
self.lifetime = 10 * 60; // Lifetime in ticks (10 seconds * 60 ticks/sec)
self.lifetimeTimer = 0; // Timer for lifetime
/**
* Update method called each game tick by the LK engine.
* Handles attacking and lifetime.
*/
self.update = function () {
self.attackTimer++;
self.lifetimeTimer++;
// Check for lifetime
var effectiveAttackInterval = Math.max(30, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.5s
if (self.lifetimeTimer >= self.lifetime) {
self.destroy();
// Remove from the swordsmen array in the main game loop
return; // Stop updating this instance
}
// Find the closest enemy to chase or attack
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy exists, chase it or attack if within range
if (closestEnemy) {
// Movement speed toward enemies
var moveSpeed = 5;
// If not within attack range, move toward the enemy
if (closestDistance > self.attackRange) {
// Calculate direction vector to enemy
var dirX = closestEnemy.x - self.x;
var dirY = closestEnemy.y - self.y;
// Normalize the direction vector
var length = Math.sqrt(dirX * dirX + dirY * dirY);
dirX = dirX / length;
dirY = dirY / length;
// Move toward the enemy
self.x += dirX * moveSpeed;
self.y += dirY * moveSpeed;
}
// If within attack range and attack timer is ready, attack
else if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Apply damage to the enemy and check if it was defeated
var enemyDefeated = closestEnemy.takeDamage(self.attackDamage, self); // Pass the swordsman as source
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score when enemy is defeated by swordsman.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
closestEnemy.destroy();
// Remove from the enemies array in the main game loop
}
// Optional: Add a visual/sound effect for attack here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents a Viking ally that throws piercing axes.
*/
var VikingAlly = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('viking_ally', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackInterval = 150; // Attack every 2.5 seconds (150 ticks)
self.attackTimer = 0; // Timer for attacks
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and throwing axes.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range
var closestEnemy = null;
var closestDistance = self.attackRange;
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);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, throw an axe
if (closestEnemy) {
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
var newAxe = new VikingAxe(angle);
newAxe.x = self.x;
newAxe.y = self.y;
newAxe.lastY = newAxe.y;
newAxe.lastX = newAxe.x;
game.addChild(newAxe);
vikingAxes.push(newAxe); // Add to the vikingAxes array
// Optional: Add throwing sound effect here later
}
}
};
return self;
});
/**
* Represents an Axe thrown by a Viking Ally.
* @param {number} angle - The angle in radians at which the axe is thrown.
*/
var VikingAxe = Container.expand(function (angle) {
var self = Container.call(this);
var graphics = self.attachAsset('viking_axe', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align axe graphic with direction
self.speed = 25; // Speed of the axe
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.damage = 3; // Base damage dealt by this axe
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.damage += 5;
}
self.pierceLeft = 3; // Can pierce through 3 enemies
/**
* Update method called each game tick by the LK engine.
* Moves the axe based on its velocity and handles rotation.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
graphics.rotation += 0.2; // Add spinning effect
};
return self;
});
/**
* Represents a Wizard Tower ally that shoots magic balls.
*/
var WizardTower = Container.expand(function () {
var self = Container.call(this);
// Create and attach the wizard tower graphic asset
var graphics = self.attachAsset('wizard_tower_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackDamage = 0.5; // Low damage, primary effect is slowdown
self.attackInterval = 120; // Attack every 2 seconds (120 ticks)
self.attackTimer = 0; // Timer for attacks
self.slowDuration = 180; // Duration of slowdown effect in ticks (3 seconds)
self.slowAmount = 0.5; // Multiplier for enemy speed (0.5 means 50% slower)
/**
* Update method called each game tick by the LK engine.
* Handles attacking enemies.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range to shoot at
var closestEnemy = null;
var closestDistance = self.attackRange; // Limit to attack range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, fire a magic ball
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new magic ball instance
var newMagicBall = new MagicBall(angle);
// Position the magic ball at the tower's location
newMagicBall.x = self.x;
newMagicBall.y = self.y;
newMagicBall.lastY = newMagicBall.y; // Initialize lastY for state tracking
newMagicBall.lastX = newMagicBall.x; // Initialize lastX for state tracking
newMagicBall.slowDuration = self.slowDuration; // Pass slow duration to the magic ball
newMagicBall.slowAmount = self.slowAmount; // Pass slow amount to the magic ball
// Add the magic ball to the game scene and the tracking array.
game.addChild(newMagicBall);
magicBalls.push(newMagicBall); // Add to the magicBalls array
// Optional: Add a visual/sound effect for magic ball shot here later
}
}
};
return self; // Return self for potential inheritance
});
/****
* Initialize Game
****/
// Create the main game instance with a dark background color.
var game = new LK.Game({
backgroundColor: 0x101030 // Dark blue/purple background
});
/****
* Game Code
****/
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
//Only include the plugins you need to create the game.
//We have access to the following plugins. (Note that the variable names used are mandetory for each plugin)
//Storage library which should be used for persistent game data
//var storage = LK.import('@upit/storage.v1');
//Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions
//var facekit = LK.import('@upit/facekit.v1');
// Constants defining game dimensions and key vertical positions.
// Placeholder ID, adjust size slightly
// Placeholder ID
// Placeholder ID, adjust size slightly, reuse flipX
// Use actual spearman asset
// Use actual war elephant asset
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var BASTION_Y = GAME_HEIGHT - 250; // Y-coordinate representing the defense line. Enemies crossing this trigger game over.
var FIRING_POS_Y = GAME_HEIGHT - 150; // Y-coordinate from where arrows are fired.
var FIRING_POS_X = GAME_WIDTH / 2; // X-coordinate from where arrows are fired (center).
// Arrays to keep track of active arrows and enemies.
var arrows = [];
var enemies = [];
// Variables for game state and difficulty scaling.
var scoreTxt; // Text2 object for displaying the score.
var ammoTxt; // Text2 object for displaying the current ammo count.
var dragStartX = null; // Starting X coordinate of a touch/mouse drag.
var dragStartY = null; // Starting Y coordinate of a touch/mouse drag.
var enemySpawnInterval = 120; // Initial ticks between enemy spawns (2 seconds at 60 FPS).
var arrowsFired = 0; // Counter for arrows fired since the last cooldown.
var cooldownTimer = 0; // Timer in ticks for the cooldown period.
var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec). Min duration enforced in upgrade.
var maxArrowsBeforeCooldown = 15; // Max arrows before reload (Quiver upgrade)
var arrowPierceLevel = 1; // How many enemies an arrow can pierce (Piercing Shots upgrade)
var arrowDamage = 1; // Base damage for arrows (Heat-tipped arrows upgrade)
var enemySpeedMultiplier = 1.0; // Multiplier for enemy speed (Sabotage upgrade) Min multiplier enforced in upgrade.
var minEnemySpawnInterval = 30; // Minimum ticks between enemy spawns (0.5 seconds).
var enemySpawnRateDecrease = 0.08; // Amount to decrease spawn interval each time an enemy spawns.
var currentEnemySpeed = 3; // Initial base speed of enemies.
var maxEnemySpeed = 12; // Maximum base speed enemies can reach.
var enemySpeedIncrease = 0.015; // Amount to increase enemy base speed each time an enemy spawns.
var archerAllies = []; // Array to hold ArcherAlly instances
var swordsmen = []; // Array to hold Swordsman instances
var cannonballs = []; // Array to hold Cannonball instances
var multiShotEnabled = false; // Flag for More Shots upgrade
var poisonShotsEnabled = false; // Flag for Poison Shots upgrade
var cannons = []; // Array to hold Cannon instances
var allyAttackSpeedMultiplier = 1.0; // Multiplier for ally attack speed
var wizardTowers = []; // Array to hold WizardTower instances
var magicBalls = []; // Array to hold MagicBall instances
var aimbotEnabled = false; // Flag for Aimbot upgrade
var greenKillerEnabled = false; // Flag for Green Killer upgrade
var refinedProjectilesEnabled = false; // Flag for Refined Projectiles upgrade
var greenSlowdownEnabled = false; // Flag for Green Slowdown upgrade
var vikingAllies = []; // Array to hold VikingAlly instances
var vikingAxes = []; // Array to hold VikingAxe instances
// --- Upgrade System ---
var lastUpgradeScore = -1; // Score at which the last upgrade was offered
var isUpgradePopupActive = false; // Flag indicating if the upgrade choice popup is visible
var upgradePopup = null; // Container for the upgrade UI elements
var currentMusicPlaying = 'Gamemusic'; // Track which music is currently playing
var musicFadeInProgress = false; // Flag to track if a music fade transition is in progress
// Helper function to apply Sabotage effect to existing enemies
function applySabotage() {
for (var i = 0; i < enemies.length; i++) {
// We assume Enemy class will use enemySpeedMultiplier when calculating effective speed
// If Enemy.speed stores base speed, we might need an effectiveSpeed method or update speed directly.
// For simplicity, let's assume Enemy.update uses the global multiplier.
// If direct modification is needed: enemies[i].speed = baseSpeed * enemySpeedMultiplier;
}
// Also update the current base speed calculation baseline if necessary, though modifying the multiplier should suffice.
}
// Helper function placeholder for adding Archer Ally
function addArcherAlly() {
// Implementation requires ArcherAlly class definition
console.log("Archer Ally upgrade chosen!");
var ally = new ArcherAlly();
// Position the ally next to the player's firing position, slightly offset.
ally.x = FIRING_POS_X + 150; // Position to the right of the player
ally.y = FIRING_POS_Y;
game.addChild(ally);
archerAllies.push(ally); // Add to the new archerAllies array
}
// Helper function to add a Cannon ally
function addCannon() {
console.log("Cannon upgrade chosen!");
var newCannon = new Cannon();
// Position the cannon near the bastion line
newCannon.x = GAME_WIDTH / 2 - 300; // Example position to the left of center
newCannon.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newCannon);
cannons.push(newCannon); // Add to the cannons array
}
// Helper function to add a Wizard Tower
function addWizardTower() {
console.log("Wizard Tower upgrade chosen!");
var newTower = new WizardTower();
// Position the wizard tower near the bastion line, offset from center
newTower.x = GAME_WIDTH / 2 + 300; // Example position to the right of center
newTower.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newTower);
wizardTowers.push(newTower); // Add to the wizardTowers array
}
// Helper function to add a Viking Ally
function addVikingAlly() {
console.log("Viking Ally upgrade chosen!");
var newViking = new VikingAlly();
// Position the viking near the bastion line, offset from cannons/towers
newViking.x = GAME_WIDTH / 2; // Center for now
newViking.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newViking);
vikingAllies.push(newViking); // Add to the vikingAllies array
}
// Helper function placeholder for adding Swordsman
function addSwordsman() {
// Create a swordsman tower at the bastion line
var tower = new Container();
var towerGraphics = tower.attachAsset('swordsmanTower', {
anchorX: 0.5,
anchorY: 0.5
});
tower.x = FIRING_POS_X; // Position in the center horizontally
tower.y = BASTION_Y - 50; // Position just above the bastion line
game.addChild(tower);
// Set up interval to spawn swordsmen from the tower
var spawnInterval = LK.setInterval(function () {
var newSwordsman = new Swordsman();
// Position the swordsman near the tower with a slight random offset
newSwordsman.x = tower.x + (Math.random() * 100 - 50); // Random offset of ±50px
newSwordsman.y = tower.y;
game.addChild(newSwordsman);
swordsmen.push(newSwordsman); // Add to the swordsmen array
}, 3000); // Spawn a swordsman every 3 seconds
// Swordsman spawn does not grant score
// Store the interval reference in the tower to potentially clear it later
tower.spawnInterval = spawnInterval;
}
// Define all possible upgrades
var allUpgrades = [{
id: 'faster_reload',
name: 'Faster Reload',
description: 'Decrease reload time by 20%',
apply: function apply() {
cooldownDuration = Math.max(60, Math.floor(cooldownDuration * 0.8));
}
},
// Min 1 sec cooldown
{
id: 'piercing_shots',
name: 'Piercing Shots',
description: 'Arrows pierce +1 enemy',
apply: function apply() {
arrowPierceLevel++;
}
}, {
id: 'quiver',
name: 'Quiver',
description: '+5 Arrows before reload',
apply: function apply() {
maxArrowsBeforeCooldown += 5;
}
}, {
id: 'heat_tipped',
name: 'Heat-tipped Arrows',
description: 'Arrows deal +1 damage',
apply: function apply() {
arrowDamage += 1;
console.log("Heat-tipped Arrows upgrade chosen - damage increased to " + arrowDamage);
}
}, {
id: 'archer_ally',
name: 'Archer Ally',
description: 'Gain an allied archer',
apply: addArcherAlly
}, {
id: 'sabotage',
name: 'Sabotage',
description: 'Enemies 10% slower',
apply: function apply() {
enemySpeedMultiplier = Math.max(0.5, enemySpeedMultiplier * 0.9);
applySabotage();
}
},
// Max 50% slow
{
id: 'swordsman',
name: 'Swordsman',
description: 'Gain a melee defender',
apply: addSwordsman
}, {
id: 'more_shots',
name: 'More Shots',
description: 'Shoot 2 arrows at once',
apply: function apply() {
multiShotEnabled = true; // Assuming a global flag for this
}
}, {
id: 'poison_shots',
name: 'Poison Shots',
description: 'Shots deal damage over time',
apply: function apply() {
poisonShotsEnabled = true; // Assuming a global flag for this
}
}, {
id: 'cannon',
name: 'Cannon',
description: 'Gain a cannon that targets the strongest enemy',
apply: addCannon // Assuming a helper function addCannon
}, {
id: 'ally_atk_speed',
name: 'Ally ATK Speed',
description: 'All allies attack faster',
apply: function apply() {
allyAttackSpeedMultiplier *= 0.8; // Assuming a global multiplier
// Apply to existing allies as well in their update logic or a helper function
}
}, {
id: 'wizard_tower',
name: 'Wizard Tower',
description: 'Gain a wizard tower ally that shoots magic balls that slowdown enemies',
apply: addWizardTower // Assuming a helper function addWizardTower
}, {
id: 'aimbot',
name: 'Aimbot',
description: 'Arrows seek out enemies',
apply: function apply() {
aimbotEnabled = true; // Assuming a global flag for this
}
}, {
id: 'green_killer',
name: 'Green Killer',
description: '+50% Damage vs Green Enemies',
apply: function apply() {
greenKillerEnabled = true;
}
}, {
id: 'refined_projectiles',
name: 'Refined Projectiles',
description: 'Non-arrow projectiles +5 damage & faster',
apply: function apply() {
refinedProjectilesEnabled = true;
// Existing projectiles won't update, new ones will have bonus
}
}, {
id: 'viking_ally',
name: 'Viking Ally',
description: 'Gain a viking that throws piercing axes',
apply: addVikingAlly // Assuming helper function addVikingAlly
}, {
id: 'green_slowdown',
name: 'Green Slowdown',
description: 'Projectiles slow Green enemies 10% for 10s (no stack)',
apply: function apply() {
greenSlowdownEnabled = true;
}
}];
// Function to create and show the upgrade selection popup
function showUpgradePopup() {
isUpgradePopupActive = true;
// Simple pause: Game logic checks isUpgradePopupActive flag in game.update
// Switch to upgrade theme music with fade effect
if (currentMusicPlaying !== 'UpgradeTheme' && !musicFadeInProgress) {
musicFadeInProgress = true;
// Fade out current music
LK.playMusic('Gamemusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// After fade out completes, start upgrade theme with fade in
LK.setTimeout(function () {
LK.playMusic('UpgradeTheme', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
currentMusicPlaying = 'UpgradeTheme';
musicFadeInProgress = false;
}, 850);
}
// --- Create Popup UI ---
upgradePopup = new Container();
upgradePopup.x = GAME_WIDTH / 2;
upgradePopup.y = GAME_HEIGHT / 2;
game.addChild(upgradePopup); // Add to game layer for positioning relative to center
// Add a semi-transparent background overlay
var bg = upgradePopup.attachAsset('bastionLine', {
// Reusing an asset for shape
width: 1200,
height: 800,
color: 0x000000,
// Black background
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Add title text
var title = new Text2('Choose an Upgrade!', {
size: 80,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -300; // Position relative to popup center
upgradePopup.addChild(title);
// --- Select 3 Random Unique Upgrades ---
var availableToOffer = allUpgrades.slice(); // Copy available upgrades
var choices = [];
var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices
for (var i = 0; i < numChoices; i++) {
var randomIndex = Math.floor(Math.random() * availableToOffer.length);
choices.push(availableToOffer[randomIndex]);
availableToOffer.splice(randomIndex, 1); // Remove chosen upgrade to ensure uniqueness
}
// --- Create Buttons for Choices ---
var buttonYStart = -150;
var buttonSpacing = 180;
for (var j = 0; j < choices.length; j++) {
var upgrade = choices[j];
var button = createUpgradeButton(upgrade, buttonYStart + j * buttonSpacing);
upgradePopup.addChild(button);
}
}
// Function to create a single upgrade button
function createUpgradeButton(upgradeData, yPos) {
var buttonContainer = new Container();
buttonContainer.y = yPos;
// Button background
var buttonBg = buttonContainer.attachAsset('bastionLine', {
// Reusing asset for shape
width: 800,
height: 150,
color: 0x555555,
anchorX: 0.5,
anchorY: 0.5
});
// Button text (Name + Description)
var nameText = new Text2(upgradeData.name, {
size: 50,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0.5);
nameText.y = -25;
buttonContainer.addChild(nameText);
var descText = new Text2(upgradeData.description, {
size: 35,
fill: 0xCCCCCC
});
descText.anchor.set(0.5, 0.5);
descText.y = 30;
buttonContainer.addChild(descText);
// Make button interactive
buttonContainer.interactive = true; // Needed for down event
// Event handler for button press
buttonContainer.down = function (x, y, obj) {
upgradeData.apply(); // Apply the selected upgrade's effect
hideUpgradePopup(); // Close the popup
};
return buttonContainer;
}
// Function to hide and destroy the upgrade popup
function hideUpgradePopup() {
if (upgradePopup) {
upgradePopup.destroy();
upgradePopup = null;
}
isUpgradePopupActive = false;
// Switch back to game music with fade effect
if (currentMusicPlaying !== 'Gamemusic' && !musicFadeInProgress) {
musicFadeInProgress = true;
// Fade out upgrade theme
LK.playMusic('UpgradeTheme', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// After fade out completes, start game music with fade in
LK.setTimeout(function () {
LK.playMusic('Gamemusic', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
currentMusicPlaying = 'Gamemusic';
musicFadeInProgress = false;
}, 850);
}
// Resume game logic (handled by checking flag in game.update)
}
// --- Visual Setup ---
var bastionLine = game.addChild(LK.getAsset('bastionLine', {
anchorX: 0.0,
// Anchor at the left edge.
anchorY: 0.5,
// Anchor vertically centered.
x: 0,
// Position at the left edge of the screen.
y: BASTION_Y // Position at the defined bastion Y-coordinate.
}));
// Create and configure the score display text.
scoreTxt = new Text2('0', {
size: 150,
// Font size.
fill: 0xFFFFFF // White color.
});
scoreTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge.
// Add score text to the GUI layer at the top-center position.
LK.gui.top.addChild(scoreTxt);
scoreTxt.y = 30; // Add padding below the top edge. Ensure it's clear of the top-left menu icon area.
// Create and configure the ammo display text.
ammoTxt = new Text2('Ammo: ' + maxArrowsBeforeCooldown, {
size: 80,
fill: 0xFFFFFF // White color.
});
ammoTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge.
LK.gui.top.addChild(ammoTxt);
ammoTxt.y = 180; // Position below the score text
// --- Enemypedia ---
var enemypediaButton;
var enemypediaPopup = null;
var enemyListContainer = null; // Container for enemy list items
function showEnemypedia() {
isUpgradePopupActive = true; // Pause the game while enemypedia is open
enemypediaPopup = new Container();
enemypediaPopup.x = GAME_WIDTH / 2;
enemypediaPopup.y = GAME_HEIGHT / 2;
game.addChild(enemypediaPopup);
// Add a semi-transparent background overlay
var bg = enemypediaPopup.attachAsset('bastionLine', {
width: 1600,
height: 1000,
color: 0x000000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Add title text
var title = new Text2('Enemypedia', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -450; // Position relative to popup center
enemypediaPopup.addChild(title);
// Add a close button
var closeButton = createCloseButton();
closeButton.x = 750; // Position relative to popup center
closeButton.y = -450;
enemypediaPopup.addChild(closeButton);
// Container for the list of enemies (scrollable potentially)
enemyListContainer = new Container();
enemyListContainer.y = -350; // Position below title
enemypediaPopup.addChild(enemyListContainer);
displayEnemyStats();
}
function hideEnemypedia() {
if (enemypediaPopup) {
enemypediaPopup.destroy();
enemypediaPopup = null;
}
isUpgradePopupActive = false; // Unpause the game
}
function createCloseButton() {
var buttonContainer = new Container();
var buttonBg = buttonContainer.attachAsset('bastionLine', {
width: 150,
height: 80,
color: 0xCC0000,
// Red color for close
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2('X', {
size: 60,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
buttonContainer.addChild(buttonText);
buttonContainer.interactive = true;
buttonContainer.down = function () {
hideEnemypedia();
};
return buttonContainer;
}
function displayEnemyStats() {
// Dummy data representing enemy types and their stats
var enemyData = [{
type: 'swordsman',
assetId: 'enemy',
description: 'Basic melee unit.',
baseHealth: 1,
baseSpeed: 3,
dodgeChance: 0
}, {
type: 'knight',
assetId: 'knight',
description: 'Slow, higher health unit.',
baseHealth: 5,
baseSpeed: 3 * 0.9,
dodgeChance: 0
}, {
type: 'thief',
assetId: 'thief',
description: 'Fast, low health unit with dodge chance.',
baseHealth: 1,
baseSpeed: 3 * 1.2,
dodgeChance: 0.1
}, {
type: 'boss',
assetId: 'boss',
description: 'Very high health boss.',
baseHealth: 5,
// Base health before scaling
baseSpeed: 3 * 0.7,
dodgeChance: 0
}, {
type: 'shield',
assetId: 'shield_enemy',
description: 'Very high health, very slow unit.',
baseHealth: 20,
baseSpeed: 3 * 0.5,
dodgeChance: 0
}, {
type: 'wizard',
assetId: 'wizard_enemy',
description: 'Teleports and has moderate speed.',
baseHealth: 2,
baseSpeed: 3 * 0.7,
dodgeChance: 0
}, {
type: 'spearman',
assetId: 'spearman',
description: 'Standard unit, often spawns from Elephants.',
baseHealth: 3,
baseSpeed: 3 * 1.0,
dodgeChance: 0
}, {
type: 'war_elephant',
assetId: 'war_elephant',
description: 'Extremely high health. Spawns spearmen on death.',
baseHealth: 500,
baseSpeed: 3 * 0.3,
dodgeChance: 0
}, {
type: 'elite_knight',
assetId: 'elite_knight_asset',
description: 'High health knight variant.',
baseHealth: 45,
baseSpeed: 3 * 0.85,
dodgeChance: 0
}, {
type: 'elite_shield',
assetId: 'elite_shield_asset',
description: 'Even higher health shield variant.',
baseHealth: 200,
baseSpeed: 3 * 0.4,
dodgeChance: 0
}, {
type: 'shaman',
assetId: 'shaman_enemy',
description: 'Reduces player ammo periodically.',
baseHealth: 2,
baseSpeed: 3 * 0.8,
dodgeChance: 0
}, {
type: 'hot_air_balloon',
assetId: 'hot_air_balloon_asset',
description: 'Very low health. Spawns random enemies on death.',
baseHealth: 1,
baseSpeed: 3 * 0.2,
dodgeChance: 0
}, {
type: 'dark_bowman',
assetId: 'dark_bowman_asset',
description: 'Immune to arrows. Higher health variant.',
baseHealth: 5,
baseSpeed: 3 * 1.1,
dodgeChance: 0
}];
var startY = 0;
var spacing = 150;
for (var i = 0; i < enemyData.length; i++) {
var enemyInfo = enemyData[i];
var enemyItem = new Container();
enemyItem.y = startY + i * spacing;
// Add enemy graphic (smaller representation)
var enemyGraphic = enemyItem.attachAsset(enemyInfo.assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
// Scale down for display
scaleY: 0.5,
x: -600 // Position to the left
});
// Add enemy name
var nameText = new Text2(enemyInfo.type.replace(/_/g, ' ').toUpperCase(), {
size: 40,
fill: 0xFFFFFF
});
nameText.anchor.set(0, 0.5); // Align left
nameText.x = -500;
enemyItem.addChild(nameText);
// Add description
var descText = new Text2('Description: ' + enemyInfo.description, {
size: 30,
fill: 0xCCCCCC,
wordWrap: true,
wordWrapWidth: 800 // Wrap text to fit width
});
descText.anchor.set(0, 0); // Align top-left
descText.x = -500;
descText.y = 30;
enemyItem.addChild(descText);
// Add stats
var statsText = new Text2('Health: ' + enemyInfo.baseHealth + ' | Speed: ' + enemyInfo.baseSpeed.toFixed(1) + ' | Dodge: ' + (enemyInfo.dodgeChance * 100).toFixed(0) + '%', {
size: 30,
fill: 0xAAAAAA
});
statsText.anchor.set(0, 0); // Align top-left
statsText.x = -500;
statsText.y = 70;
enemyItem.addChild(statsText);
enemyListContainer.addChild(enemyItem);
}
}
enemypediaButton = new Container();
// Position the button (e.g., bottom right of GUI)
enemypediaButton.x = GAME_WIDTH - 200; // Example X
enemypediaButton.y = GAME_HEIGHT - 200; // Example Y
// Add button background
var buttonBg = enemypediaButton.attachAsset('bastionLine', {
// Reusing asset for shape
width: 300,
height: 100,
color: 0x444444,
// Grey color
anchorX: 0.5,
anchorY: 0.5
});
// Add button text
var buttonText = new Text2('Enemypedia', {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
enemypediaButton.addChild(buttonText);
// Make button interactive
enemypediaButton.interactive = true;
enemypediaButton.down = function () {
showEnemypedia();
};
// Add the button to the game scene (or GUI if needed)
game.addChild(enemypediaButton);
// --- Input Event Handlers ---
// Handles touch/mouse press down event on the game area.
game.down = function (x, y, obj) {
// Record the starting position of the drag in game coordinates.
var gamePos = game.toLocal({
x: x,
y: y
});
dragStartX = gamePos.x;
dragStartY = gamePos.y;
// Potential enhancement: Show an aiming indicator originating from FIRING_POS_X, FIRING_POS_Y towards the drag point.
};
// Handles touch/mouse move event while dragging on the game area.
game.move = function (x, y, obj) {
// Only process if a drag is currently active.
if (dragStartX !== null) {
var gamePos = game.toLocal({
x: x,
y: y
});
// Potential enhancement: Update the aiming indicator based on the current drag position (gamePos).
}
};
// Handles touch/mouse release event on the game area.
game.up = function (x, y, obj) {
// Only process if a drag was active.
if (dragStartX !== null && cooldownTimer <= 0) {
// Only allow firing if not on cooldown.
var gamePos = game.toLocal({
x: x,
y: y
});
var dragEndX = gamePos.x;
var dragEndY = gamePos.y;
// Calculate the difference between the firing position and the release point to determine direction.
// A longer drag could potentially mean more power, but we'll keep it simple: direction only.
var dx = dragEndX - FIRING_POS_X;
var dy = dragEndY - FIRING_POS_Y;
// Avoid division by zero or zero vector if start and end points are the same.
if (dx === 0 && dy === 0) {
// Optionally handle this case, e.g., fire straight up or do nothing.
// Let's fire straight up if drag distance is negligible.
dy = -1;
}
// Calculate the angle using atan2. Note the order (dx, dy) and the negation of dy
// because the Y-axis is inverted in screen coordinates (positive Y is down).
var angle = Math.atan2(dx, -dy);
// Create a new arrow instance with the calculated angle.
var newArrow = new Arrow(angle);
newArrow.x = FIRING_POS_X;
newArrow.y = FIRING_POS_Y;
// Initialize last position for state tracking (e.g., off-screen detection)
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// Add the arrow to the game scene and the tracking array.
game.addChild(newArrow);
arrows.push(newArrow);
if (multiShotEnabled) {
// Fire a second arrow with a slight angle offset
var angle2 = angle + Math.PI / 12; // Offset by 15 degrees
var newArrow2 = new Arrow(angle2);
newArrow2.x = FIRING_POS_X;
newArrow2.y = FIRING_POS_Y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
game.addChild(newArrow2);
arrows.push(newArrow2);
// Fire a third arrow with the opposite angle offset
var angle3 = angle - Math.PI / 12; // Offset by -15 degrees
var newArrow3 = new Arrow(angle3);
newArrow3.x = FIRING_POS_X;
newArrow3.y = FIRING_POS_Y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
LK.getSound('shoot').play(); // Play shooting sound.
arrowsFired++; // Increment arrow count.
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display.
// Check if cooldown needs to start based on Quiver upgrade
if (arrowsFired >= maxArrowsBeforeCooldown) {
cooldownTimer = cooldownDuration; // Start cooldown (duration affected by Faster Reload upgrade)
arrowsFired = 0; // Reset arrow count.
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display.
LK.getSound('Reload').play(); // Play reload sound.
}
// Reset drag state.
dragStartX = null;
dragStartY = null;
// Potential enhancement: Hide the aiming indicator.
}
};
// --- Main Game Update Loop ---
// This function is called automatically by the LK engine on every frame (tick).
game.update = function () {
// --- Upgrade System Check ---
var currentScore = LK.getScore();
// Check if score reached a multiple of 10 and is higher than the last upgrade score
if (!isUpgradePopupActive && currentScore > 0 && currentScore % 10 === 0 && currentScore !== lastUpgradeScore) {
lastUpgradeScore = currentScore; // Mark this score level as having triggered an upgrade offer
showUpgradePopup(); // Show the upgrade selection screen
}
// --- Pause Game Logic if Upgrade Popup is Active ---
if (isUpgradePopupActive) {
// Potential: Update UI animations if any
return; // Skip the rest of the game update loop
}
// --- Resume Game Logic ---
if (isUpgradePopupActive) {
return; // Skip enemy movement and spawning if upgrade popup is active
}
// Decrease cooldown timer if active.
if (cooldownTimer > 0) {
cooldownTimer--;
ammoTxt.setText('Reloading...'); // Show reloading status
} else if (ammoTxt.text !== 'Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)) {
// Ensure ammo display is correct if not reloading
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
}
// 1. Update and check Arrows
for (var i = arrows.length - 1; i >= 0; i--) {
var arrow = arrows[i];
// Note: LK engine calls arrow.update() automatically as it's added to the game stage.
// Initialize last position if it hasn't been set yet (first frame).
if (arrow.lastY === undefined) {
arrow.lastY = arrow.y;
}
if (arrow.lastX === undefined) {
arrow.lastX = arrow.x;
}
// Check for collisions between the current arrow and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (arrow.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Calculate damage, applying Green Killer bonus if applicable
var damageToDeal = arrow.damage;
if (greenKillerEnabled && enemy.tag === 'Green') {
damageToDeal *= 1.5; // Apply 50% damage bonus
}
// Apply damage to the enemy and check if it was defeated
var enemyDefeated = enemy.takeDamage(damageToDeal, arrow); // Pass the arrow object as source
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Handle arrow piercing
arrow.pierceLeft--; // Decrease remaining pierces
if (arrow.pierceLeft <= 0) {
// Arrow has no pierces left, destroy it
arrow.destroy();
arrows.splice(i, 1); // Remove arrow from array.
hitEnemy = true; // Mark that this arrow is done
break; // Stop checking this arrow against other enemies as it's destroyed
} else {
// Arrow pierced this enemy and can continue
// We don't set hitEnemy = true here because the arrow continues
// We don't break because it might hit another enemy in the same frame further along its path
// Apply Green Slowdown if applicable and arrow is still piercing
if (!enemyDefeated && greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
}
}
}
// If the arrow hit an enemy, it was destroyed, so skip to the next arrow.
if (hitEnemy) {
continue;
}
// Check if the arrow has gone off-screen (top, left, or right).
// Use transition detection: check if it was on screen last frame and is off screen now.
var wasOnScreen = arrow.lastY > -arrow.height / 2 && arrow.lastX > -arrow.width / 2 && arrow.lastX < GAME_WIDTH + arrow.width / 2;
var isOffScreen = arrow.y < -arrow.height / 2 ||
// Off top edge
arrow.x < -arrow.width / 2 ||
// Off left edge
arrow.x > GAME_WIDTH + arrow.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Arrow is off-screen, destroy it and remove from the array.
arrow.destroy();
arrows.splice(i, 1);
} else if (!isOffScreen) {
// Update last known position only if the arrow is still potentially on screen
arrow.lastY = arrow.y;
arrow.lastX = arrow.x;
}
}
// 2. Update and check Cannonballs
for (var cb = cannonballs.length - 1; cb >= 0; cb--) {
var cannonball = cannonballs[cb];
// Note: LK engine calls cannonball.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (cannonball.lastY === undefined) {
cannonball.lastY = cannonball.y;
}
if (cannonball.lastX === undefined) {
cannonball.lastX = cannonball.x;
}
// Check for collisions between the current cannonball and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (cannonball.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply damage to the enemy and check if it was defeated
var enemyDefeated = enemy.takeDamage(cannonball.damage); // Cannonballs don't apply poison
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Cannonballs are destroyed on hit
cannonball.destroy();
// Apply Green Slowdown if applicable before destroying cannonball
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
cannonball.destroy();
cannonballs.splice(cb, 1); // Remove cannonball from array.
hitEnemy = true; // Mark that this cannonball is done
break; // Stop checking this cannonball against other enemies as it's destroyed
}
}
// If the cannonball hit an enemy, it was destroyed, so skip to the next cannonball.
if (hitEnemy) {
continue;
}
// Check if the cannonball has gone off-screen.
var wasOnScreen = cannonball.lastY > -cannonball.height / 2 && cannonball.lastX > -cannonball.width / 2 && cannonball.lastX < GAME_WIDTH + cannonball.width / 2;
var isOffScreen = cannonball.y < -cannonball.height / 2 ||
// Off top edge
cannonball.x < -cannonball.width / 2 ||
// Off left edge
cannonball.x > GAME_WIDTH + cannonball.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Cannonball is off-screen, destroy it and remove from the array.
cannonball.destroy();
cannonballs.splice(cb, 1);
} else if (!isOffScreen) {
// Update last known position only if the cannonball is still potentially on screen
cannonball.lastY = cannonball.y;
cannonball.lastX = cannonball.x;
}
}
// 3. Update and check Enemies
for (var k = enemies.length - 1; k >= 0; k--) {
var enemy = enemies[k];
// Note: LK engine calls enemy.update() automatically.
// Initialize last position if not set.
if (enemy.lastY === undefined) {
enemy.lastY = enemy.y;
}
// Check if the enemy has reached or passed the bastion line.
// Use transition detection: was the enemy's bottom edge above the line last frame,
// and is it on or below the line now?
var enemyBottomY = enemy.y + enemy.height / 2;
var enemyLastBottomY = enemy.lastY + enemy.height / 2;
var wasAboveBastion = enemyLastBottomY < BASTION_Y;
var isAtOrBelowBastion = enemyBottomY >= BASTION_Y;
if (wasAboveBastion && isAtOrBelowBastion) {
// Enemy reached the bastion! Game Over.
LK.getSound('gameOverSfx').play(); // Play game over sound effect.
LK.showGameOver(); // Trigger the engine's game over sequence.
// LK.showGameOver handles game state reset, no need to manually clear arrays here.
return; // Exit the update loop immediately as the game is over.
} else {
// Update last known position if game is not over for this enemy.
enemy.lastY = enemy.y;
}
}
// 3. Update and check Swordsmen (allies)
for (var l = swordsmen.length - 1; l >= 0; l--) {
var swordsman = swordsmen[l];
// Swordsman update method handles its own lifetime and attacking
// Check if the swordsman has been destroyed by its lifetime timer
if (!swordsman.parent) {
// If it no longer has a parent, it has been destroyed
swordsmen.splice(l, 1); // Remove swordsman from array
}
}
// 4. Update and check Cannons (allies)
for (var m = cannons.length - 1; m >= 0; m--) {
var cannon = cannons[m];
// Cannons do not have a lifetime timer or destruction logic in this basic version
// If they had, we'd check !cannon.parent and splice here as in Swordsmen
}
// 5. Update and check Wizard Towers (allies)
for (var wt = wizardTowers.length - 1; wt >= 0; wt--) {
var tower = wizardTowers[wt];
// Wizard towers do not have a lifetime timer or destruction logic in this basic version
// If they had, we'd check !tower.parent and splice here
}
// 5.5 Update and check Viking Allies
for (var va = vikingAllies.length - 1; va >= 0; va--) {
var viking = vikingAllies[va];
// Vikings do not have a lifetime timer or destruction logic in this version
// LK engine calls viking.update() automatically.
}
// 6. Update and check Magic Balls
for (var mb = magicBalls.length - 1; mb >= 0; mb--) {
var magicBall = magicBalls[mb];
// Note: LK engine calls magicBall.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (magicBall.lastY === undefined) {
magicBall.lastY = magicBall.y;
}
if (magicBall.lastX === undefined) {
magicBall.lastX = magicBall.x;
}
// Check for collisions between the current magic ball and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (magicBall.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply damage (minimal) and slowdown effect to the enemy and check if it was defeated
var enemyDefeated = enemy.takeDamage(magicBall.damage, magicBall); // Pass the magic ball object as source
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Magic balls are destroyed on hit
magicBall.destroy();
// Apply Green Slowdown if applicable before destroying magic ball
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
// Note: Enemy.takeDamage already checks for MagicBall source to apply its default slow.
// This adds the Green Slowdown effect *on top* if applicable and not already slowed.
// However, takeDamage applies slow based on MagicBall properties. Let's apply Green Slowdown here explicitly.
enemy.slowTimer = 10 * 60; // 10 seconds
enemy.currentSlowAmount = 0.9; // 10% slow
}
magicBall.destroy();
magicBalls.splice(mb, 1); // Remove magic ball from array.
hitEnemy = true; // Mark that this magic ball is done
break; // Stop checking this magic ball against other enemies as it's destroyed
}
}
// If the magic ball hit an enemy, it was destroyed, so skip to the next magic ball.
if (hitEnemy) {
continue;
}
// Check if the magic ball has gone off-screen.
var wasOnScreen = magicBall.lastY > -magicBall.height / 2 && magicBall.lastX > -magicBall.width / 2 && magicBall.lastX < GAME_WIDTH + magicBall.width / 2;
var isOffScreen = magicBall.y < -magicBall.height / 2 ||
// Off top edge
magicBall.x < -magicBall.width / 2 ||
// Off left edge
magicBall.x > GAME_WIDTH + magicBall.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Magic ball is off-screen, destroy it and remove from the array.
magicBall.destroy();
magicBalls.splice(mb, 1);
} else if (!isOffScreen) {
// Update last known position only if the magic ball is still potentially on screen
magicBall.lastY = magicBall.y;
magicBall.lastX = magicBall.x;
}
}
// 6.5 Update and check Viking Axes
for (var axeIdx = vikingAxes.length - 1; axeIdx >= 0; axeIdx--) {
var axe = vikingAxes[axeIdx];
// Note: LK engine calls axe.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (axe.lastY === undefined) {
axe.lastY = axe.y;
}
if (axe.lastX === undefined) {
axe.lastX = axe.x;
}
// Check for collisions between the current axe and all enemies.
var hitEnemyAxe = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (axe.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply Green Killer bonus if applicable (Vikings benefit too)
var damageToDealAxe = axe.damage;
if (greenKillerEnabled && enemy.tag === 'Green') {
damageToDealAxe *= 1.5; // Apply 50% damage bonus
}
// Apply damage and check if defeated
var enemyDefeated = enemy.takeDamage(damageToDealAxe, axe); // Pass axe as source for potential effects
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Handle axe piercing
axe.pierceLeft--; // Decrease remaining pierces
if (axe.pierceLeft <= 0) {
// Axe has no pierces left, destroy it
axe.destroy();
vikingAxes.splice(axeIdx, 1); // Remove axe from array.
hitEnemyAxe = true; // Mark that this axe is done
break; // Stop checking this axe against other enemies
} else {
// Axe pierced this enemy and can continue
// Potentially apply Green Slowdown if enabled
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
}
}
}
// If the axe hit and was destroyed, skip to the next axe.
if (hitEnemyAxe) {
continue;
}
// Check if the axe has gone off-screen.
var wasOnScreenAxe = axe.lastY > -axe.height / 2 && axe.lastX > -axe.width / 2 && axe.lastX < GAME_WIDTH + axe.width / 2;
var isOffScreenAxe = axe.y < -axe.height / 2 || axe.y > GAME_HEIGHT + axe.height / 2 || axe.x < -axe.width / 2 || axe.x > GAME_WIDTH + axe.width / 2;
if (wasOnScreenAxe && isOffScreenAxe) {
axe.destroy();
vikingAxes.splice(axeIdx, 1);
} else if (!isOffScreenAxe) {
axe.lastY = axe.y;
axe.lastX = axe.x;
}
}
// 7. Spawn new Enemies periodically
// Use LK.ticks and the spawn interval. Ensure interval doesn't go below minimum.
if (LK.ticks % Math.max(minEnemySpawnInterval, Math.floor(enemySpawnInterval)) === 0) {
var currentScore = LK.getScore();
var baseSpeedForSpawn = Math.min(maxEnemySpeed, currentEnemySpeed); // Base speed adjusted by game difficulty progression
// Determine if this spawn *could* be a boss (e.g., every 7th spawn after score 10)
var potentialBoss = (enemies.length + 1) % 7 === 0 && currentScore >= 10;
var typeToSpawn = 'swordsman'; // Default type
var enemyHealth = 1; // Default health
var enemyDodgeChance = 0; // Default dodge chance
var enemySpeed = baseSpeedForSpawn; // Start with base speed
// Determine base type based on score thresholds and randomness
var possibleTypes = ['swordsman'];
if (currentScore >= 10) {
// Spearman appears at score 10
possibleTypes.push('spearman');
}
if (currentScore >= 15) {
possibleTypes.push('thief');
} //{3s} // Adjusted original line identifier
if (currentScore >= 23) {
possibleTypes.push('knight');
} //{3t} // Adjusted original line identifier
if (currentScore >= 25) {
possibleTypes.push('wizard');
} // Wizards start appearing at score 25
if (currentScore >= 30) {
possibleTypes.push('shield');
} // Shields start appearing at score 30
if (currentScore >= 35) {
possibleTypes.push('shaman'); // Shaman appears at score 35
console.log("Shaman added to possible enemy types");
}
if (currentScore >= 55) {
possibleTypes.push('dark_bowman'); // Dark bowman appears at score 55
}
if (currentScore >= 100) {
possibleTypes.push('elite_knight');
} // Elite Knights start appearing at score 100
if (currentScore >= 112) {
possibleTypes.push('elite_shield'); // Elite Shields appear at score 112
}
if (currentScore >= 125) {
// War Elephant appears at score 125
possibleTypes.push('war_elephant');
}
if (currentScore >= 154) {
// Hot Air Balloon appears at score 154
possibleTypes.push('hot_air_balloon');
}
// Randomly select a type from the available pool for this score level
var chosenTypeIndex = Math.floor(Math.random() * possibleTypes.length);
typeToSpawn = possibleTypes[chosenTypeIndex];
// Set stats based on chosen type and apply score-based scaling
if (typeToSpawn === 'knight') {
var baseKnightHP = 5;
var hpIncreaseIntervalKnight = 30; // Health increases every 30 score points after appearing
var hpIncreasesKnight = Math.floor(Math.max(0, currentScore - 23) / hpIncreaseIntervalKnight);
enemyHealth = baseKnightHP + hpIncreasesKnight * 5;
enemySpeed *= 0.9; // Knights are slightly slower than base swordsman speed
} else if (typeToSpawn === 'thief') {
enemyHealth = 1; // Thieves are fragile
enemyDodgeChance = 0.10; // 10% dodge chance
var speedIncreaseIntervalThief = 25; // Speed increases every 25 score points after appearing
var speedIncreasePercentThief = 0.05; // 5% speed increase each time
var speedIncreasesThief = Math.floor(Math.max(0, currentScore - 15) / speedIncreaseIntervalThief);
var thiefSpeedMultiplier = Math.pow(1 + speedIncreasePercentThief, speedIncreasesThief);
enemySpeed *= 1.2 * thiefSpeedMultiplier; // Thieves are faster base + scaling speed
} else if (typeToSpawn === 'shield') {
var baseShieldHP = 20;
var hpIncreaseIntervalShield = 35; // Gains HP every 35 score points after appearing
var hpIncreasesShield = Math.floor(Math.max(0, currentScore - 30) / hpIncreaseIntervalShield);
enemyHealth = baseShieldHP + hpIncreasesShield * 20;
enemySpeed *= 0.5; // Shield enemies are very slow
enemyDodgeChance = 0;
} else if (typeToSpawn === 'shaman') {
var baseShamanHP = 2;
var statIncreaseIntervalShaman = 10; // Gains stats every 10 score points after appearing
var statIncreasesShaman = Math.floor(Math.max(0, currentScore - 35) / statIncreaseIntervalShaman);
enemyHealth = baseShamanHP + statIncreasesShaman * 1; // Gains 1 HP per interval
enemySpeed *= 0.8 * Math.pow(1.04, statIncreasesShaman); // Base speed, gains 4% speed per interval
enemyDodgeChance = 0; // Shaman cannot dodge
} else if (typeToSpawn === 'wizard') {
var baseWizardHP = 2;
var statIncreaseIntervalWizard = 10; // Gains stats every 10 score points after appearing
var statIncreasesWizard = Math.floor(Math.max(0, currentScore - 25) / statIncreaseIntervalWizard);
enemyHealth = baseWizardHP + statIncreasesWizard * 1; // Gains 1 HP per interval
enemySpeed *= 0.7 * Math.pow(1.05, statIncreasesWizard); // Slow base, gains 5% speed per interval
enemyDodgeChance = 0;
} else if (typeToSpawn === 'elite_knight') {
var baseEliteKnightHP = 45;
var hpIncreaseIntervalElite = 30; // Gains HP every 30 score points after appearing
var hpIncreasesElite = Math.floor(Math.max(0, currentScore - 100) / hpIncreaseIntervalElite);
enemyHealth = baseEliteKnightHP + hpIncreasesElite * 15;
enemySpeed *= 0.85; // Slightly slower than base knight speed
enemyDodgeChance = 0;
} else if (typeToSpawn === 'elite_shield') {
var baseEliteShieldHP = 200; // Starts with 200 HP
var hpIncreaseIntervalEliteShield = 20; // Gains HP every 20 score points after appearing
var hpIncreasesEliteShield = Math.floor(Math.max(0, currentScore - 112) / hpIncreaseIntervalEliteShield);
enemyHealth = baseEliteShieldHP + hpIncreasesEliteShield * 50; // Gains 50 HP per interval
enemySpeed *= 0.4; // Elite Shield enemies are slower than regular shield enemies
enemyDodgeChance = 0;
} else if (typeToSpawn === 'spearman') {
var baseSpearmanHP = 3;
var hpIncreaseIntervalSpearman = 10; // Gains HP every 10 score points after appearing
var hpIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / hpIncreaseIntervalSpearman);
enemyHealth = baseSpearmanHP + hpIncreasesSpearman * 3; // Gains 3 HP per interval
var speedIncreaseIntervalSpearman = 10; // Gains speed every 10 score points after appearing
var speedIncreasePercentSpearman = 0.05; // 5% speed increase each time
var speedIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / speedIncreaseIntervalSpearman);
var spearmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentSpearman, speedIncreasesSpearman);
enemySpeed *= 1.0 * spearmanSpeedMultiplier; // Base speed + scaling speed
enemyDodgeChance = 0;
} else if (typeToSpawn === 'war_elephant') {
var baseElephantHP = 500;
var hpIncreaseIntervalElephant = 15; // Gains HP every 15 score points after appearing
var hpIncreasesElephant = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalElephant);
enemyHealth = baseElephantHP + hpIncreasesElephant * 100; // Gains 100 HP per interval
enemySpeed *= 0.3; // War Elephants are very slow
enemyDodgeChance = 0;
// Note: War elephant death spawns spearmen - this will be handled in the Enemy death logic.
} else if (typeToSpawn === 'hot_air_balloon') {
enemyHealth = 1; // Always has 1 HP
enemySpeed *= 0.2; // Very slow movement
enemyDodgeChance = 0;
// Note: Hot air balloon death spawns 5 random non-green enemies - handled in Enemy death logic
} else if (typeToSpawn === 'dark_bowman') {
var baseDarkBowmanHP = 5;
var statIncreaseIntervalBowman = 10; // Gains stats every 10 score points after appearing
var statIncreasesBowman = Math.floor(Math.max(0, currentScore - 55) / statIncreaseIntervalBowman);
enemyHealth = baseDarkBowmanHP + statIncreasesBowman * 3; // Gains 3 HP per interval
var speedIncreasePercentBowman = 0.05; // 5% speed increase each time
var bowmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentBowman, statIncreasesBowman);
enemySpeed *= 1.1 * bowmanSpeedMultiplier; // Slightly faster than base speed + scaling
enemyDodgeChance = 0;
// Note: Dark bowman has Black tag making it immune to arrows
} else {
// Swordsman (default)
enemyHealth = 1;
// Speed remains baseSpeedForSpawn initially
}
// Check if this spawn should be overridden to be a Boss
if (potentialBoss) {
typeToSpawn = 'boss'; // Set type to boss
enemyHealth = 5 + Math.floor(currentScore / 8); // Boss health scales significantly with score
enemySpeed = baseSpeedForSpawn * 0.7; // Bosses are slower but much tougher (reset speed based on base)
enemyDodgeChance = 0; // Bosses typically don't dodge
}
// Apply the global Sabotage speed multiplier AFTER type-specific adjustments
enemySpeed *= enemySpeedMultiplier;
// Create the new enemy instance with the calculated stats
var newEnemy = new Enemy(typeToSpawn, enemySpeed, enemyHealth, enemyDodgeChance);
// Position the new enemy at the top, random horizontal position with padding
// Use the actual width of the created enemy's graphic for padding calculation
// Need to access width after creation, use a sensible default or estimate if needed before creation
var tempAsset = LK.getAsset(newEnemy.assetId || 'enemy', {}); // Get asset dimensions (might need refinement if assetId isn't on newEnemy yet)
var spawnPadding = (tempAsset ? tempAsset.width / 2 : 100) + 20; // Use default if asset not found easily
newEnemy.x = spawnPadding + Math.random() * (GAME_WIDTH - 2 * spawnPadding);
newEnemy.y = -(tempAsset ? tempAsset.height / 2 : 100); // Start just above the top edge.
// Initialize last position for state tracking (used for bastion collision check).
newEnemy.lastY = newEnemy.y;
// Add the new enemy to the game scene and the tracking array.
game.addChild(newEnemy);
enemies.push(newEnemy);
// Increase difficulty for the next spawn: decrease spawn interval and increase base speed.
// These affect the *next* potential spawn's base calculations.
enemySpawnInterval -= enemySpawnRateDecrease;
currentEnemySpeed += enemySpeedIncrease;
} //{2s} // Adjusted original line identifier
};
// --- Initial Game Setup ---
// Set the initial score text based on the starting score (which is 0).
scoreTxt.setText(LK.getScore());
// Set the initial ammo display.
if (ammoTxt) {
// Ensure ammoTxt is initialized
ammoTxt.setText('Ammo: ' + maxArrowsBeforeCooldown);
}
;
// Play the background music.
// Check if we need to show a title screen first
if (!isGameStarted) {// Assuming a flag 'isGameStarted' for title screen state
// Don't play music immediately if showing title screen
} else {
LK.playMusic('Gamemusic');
;
}
// Placeholder for the title screen container
var titleScreen = null;
// Flag to track game start state
var isGameStarted = false;
// Function to show the title screen
function showTitleScreen() {
isGameStarted = false; // Ensure game is not started
// Create the title screen container
titleScreen = new Container();
titleScreen.x = GAME_WIDTH / 2;
titleScreen.y = GAME_HEIGHT / 2;
game.addChild(titleScreen);
// Add title text
var titleText = new Text2('Defend Your Bastion!', {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -200;
titleScreen.addChild(titleText);
// Add a simple instruction text
var instructionText = new Text2('Tap to Start', {
size: 60,
fill: 0xCCCCCC
});
instructionText.anchor.set(0.5, 0.5);
instructionText.y = 100;
titleScreen.addChild(instructionText);
// Make the title screen interactive to start the game
titleScreen.interactive = true;
titleScreen.down = function () {
startGame(); // Start the game when tapped
};
// Pause game logic while title screen is active
isUpgradePopupActive = true; // Reusing this flag to pause game logic
}
// Function to start the game
function startGame() {
isGameStarted = true; // Set game started flag
isUpgradePopupActive = false; // Unpause game logic
if (titleScreen) {
titleScreen.destroy();
titleScreen = null;
}
// Reset game state if needed (LK might handle this on game start)
// Play game music
LK.playMusic('Gamemusic');
}
// Initially show the title screen
showTitleScreen();
Arrow. In-Game asset. 2d. High contrast. No shadows. Topdown
Red stickman with a sword. In-Game asset. 2d. High contrast. No shadows. Topdown
Blue stickman with a bow. In-Game asset. 2d. High contrast. No shadows
Red stickman with an iron helmet, iron sword and iron shield. In-Game asset. 2d. High contrast. No shadows. No eyes
Red stickman with a knife and a thief's bandana. In-Game asset. 2d. High contrast. No shadows. No eyes
Purple stickman with a crown. In-Game asset. 2d. High contrast. No shadows
Blue stickman with a sword. In-Game asset. 2d. High contrast. No shadows
Tower. In-Game asset. 2d. High contrast. No shadows
Red stickman with a big wooden shield full of spikes. In-Game asset. 2d. High contrast. No shadows
Green stickman with a blue wizard hat and a staff; no eyes. In-Game asset. 2d. High contrast. No shadows
Red stickman with a golden knight helmet, gold sword and gold shield and gold boots. In-Game asset. 2d. High contrast. No shadows
Cannon. In-Game asset. 2d. High contrast. No shadows
Cannonball. In-Game asset. 2d. High contrast. No shadows
Yellow stickman with an azur wizard hat and staff on a tower. In-Game asset. 2d. High contrast. No shadows
Magic ice ball. In-Game asset. 2d. High contrast. No shadows
Green stickman with a Spartan helmet, Spartan shield and Spartan spear. In-Game asset. 2d. High contrast. No shadows
Green war elephant. In-Game asset. 2d. High contrast. No shadows
Yellow viking stickman holding an axe and is about to throw it. In-Game asset. 2d. High contrast. No shadows
Hatchet. In-Game asset. 2d. High contrast. No shadows