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 Angel of Light ally that stuns enemies and deals high damage to dark enemies. */ var AngelOfLight = Container.expand(function () { var self = Container.call(this); // Create and attach the angel graphic asset (reusing viking_ally for simplicity) // Will need a dedicated asset for the Angel of Light if available. var graphics = self.attachAsset('angel_of_light_asset', { // Placeholder asset anchorX: 0.5, anchorY: 0.5 }); graphics.tint = 0xFFFF00; // Yellow tint for Angel of Light self.attackRange = 600; // Moderate attack range self.attackDamage = 5; // Base damage self.attackInterval = 480; // Attack every 8 seconds (480 ticks) self.attackTimer = 0; // Timer for attacks self.stunDuration = 5 * 60; // Stun duration in ticks (5 seconds) /** * Update method called each game tick by the LK engine. * Handles finding targets and using light ability. */ 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, use light ability if (closestEnemy) { // Apply stun and damage to the target closestEnemy.slowTimer = Math.max(closestEnemy.slowTimer, self.stunDuration); // Apply stun (reusing slowTimer) closestEnemy.currentSlowAmount = 0.0; // 100% slowdown (stun) var damageToDeal = self.attackDamage; if (closestEnemy.tag === 'Black') { damageToDeal *= 3; // Triple damage against Black enemies } closestEnemy.takeDamage(damageToDeal, self); // Deal damage // Optional: Add visual/sound effect for light ability here later LK.effects.flashObject(closestEnemy, 0xFFFFFF, 300); // Flash white for stun/damage } } }; return self; // Return self for potential inheritance }); /** * 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 a Dart projectile fired by a Dart Ally. * @param {number} angle - The angle in radians at which the dart is fired. */ var Dart = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the dart graphic asset (reusing arrow for simplicity) // Will need a dedicated asset for the Dart if available. var graphics = self.attachAsset('dart_asset', { // Placeholder asset anchorX: 0.5, anchorY: 0.5 }); graphics.tint = 0x9933CC; // Purple tint for darts graphics.rotation = angle + Math.PI / 2; // Align dart graphic with direction self.speed = 40; // Fast speed self.damage = 0.5; // Base damage dealt by this dart // 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 dart based on its velocity. */ self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents a Dart Ally that shoots fast darts. */ var DartAlly = Container.expand(function () { var self = Container.call(this); // Create and attach the dart ally graphic asset (reusing archer asset for simplicity) // Will need a dedicated asset for the Dart Ally if available. var graphics = self.attachAsset('disguised_swordsman', { // Placeholder asset anchorX: 0.5, anchorY: 0.5 }); self.attackRange = Infinity; // Infinite attack range self.attackDamage = 0.5; // Low damage per dart self.attackInterval = 30; // Attack every 0.5 seconds (30 ticks) - very fast self.attackTimer = 0; // Timer for attacks /** * Update method called each game tick by the LK engine. * Handles finding targets and shooting darts. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(10, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.16s if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 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, shoot a dart if (closestEnemy) { // Calculate angle towards the closest enemy var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); // Create a new dart instance var newDart = new Dart(angle); newDart.x = self.x; newDart.y = self.y; newDart.lastY = newDart.y; newDart.lastX = newDart.x; // Add the dart to the game scene and the tracking array. game.addChild(newDart); darts.push(newDart); // Add to the new darts array // Optional: Add throwing sound effect here later } } }; 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 } else if (type === 'jester') { assetId = 'jester_asset'; // Use specific asset for jester } else if (type === 'dark_war_elephant') { assetId = 'dark_war_elephant_asset'; // Use specific asset for dark war elephant } 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.reflectChance = type === 'jester' ? 0.20 : 0; // 20% reflect chance, added in Enemy class 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.type === 'dark_war_elephant') { self.tag = 'Black'; // Black-tagged enemies are immune to arrows and cannonballs } // 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 for reflection (only for Jester and non-cannonball projectiles) if (self.type === 'jester' && self.reflectChance > 0 && Math.random() < self.reflectChance && !(source instanceof Cannonball)) { // Optional: Add a visual effect for reflection here later // console.log(self.type + " reflected!"); // Projectile is destroyed, enemy takes no damage if (source && source.destroy) { source.destroy(); // Need to remove from the source array in the main game loop } return false; // Reflected, so not defeated by this hit } // Check if enemy is Black-tagged and source is an Arrow or Cannonball if (self.tag === 'Black' && (source instanceof Arrow || source instanceof Cannonball)) { // Black-tagged enemies are immune to arrow and cannonball projectiles return false; // Not defeated, projectile 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 === 'dark_war_elephant') { // If Dark War Elephant is defeated, spawn 5 Dark Bowmen 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 newDarkBowman = new Enemy('dark_bowman', currentEnemySpeed * enemySpeedMultiplier, 5, 0); // Base dark bowman stats newDarkBowman.x = spawnX; newDarkBowman.y = spawnY; newDarkBowman.lastY = newDarkBowman.y; game.addChild(newDarkBowman); enemies.push(newDarkBowman); } } 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 } else if (self.type === 'jester') { graphics.tint = 0xFFB6C1; // Light Pink tint for Jester graphics.scale.set(1.0); } else if (self.type === 'dark_war_elephant') { graphics.tint = 0x333333; // Dark Grey tint for Dark War Elephant graphics.scale.set(1.4); // Same size as regular elephant } //{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 ****/ // Use actual war elephant asset // Use actual spearman asset // Placeholder ID, adjust size slightly, reuse flipX // Placeholder ID // Placeholder ID, adjust size slightly // Constants defining game dimensions and key vertical positions. //var facekit = LK.import('@upit/facekit.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 storage = LK.import('@upit/storage.v1'); //Storage library which should be used for persistent game data //We have access to the following plugins. (Note that the variable names used are mandetory for each plugin) //Only include the plugins you need to create the game. //Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property. 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 var darts = []; // Array to hold Dart instances var angelOfLights = []; // Array to hold AngelOfLight 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 to add a Dart Ally function addDartAlly() { console.log("Dart Ally upgrade chosen!"); var newDartAlly = new DartAlly(); // Position the dart ally near the bastion line, offset newDartAlly.x = GAME_WIDTH / 2 - 200; // Example position newDartAlly.y = BASTION_Y - 100; // Position above bastion line game.addChild(newDartAlly); darts.push(newDartAlly); // Add to the dartAllies array (need to rename array to dartAllies if creating a separate one) } // 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; } // Helper function to add an Angel of Light function addAngelOfLight() { console.log("Angel of Light upgrade chosen!"); var newAngel = new AngelOfLight(); // Position the angel near the bastion line, offset newAngel.x = GAME_WIDTH / 2 + 200; // Example position newAngel.y = BASTION_Y - 150; // Position slightly higher than other allies game.addChild(newAngel); angelOfLights.push(newAngel); // Add to the angelOfLights array } // 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; } }, { id: 'dart_shooter', name: 'Dart Shooter', description: 'Gain an ally that shoots fast darts, effective against green enemies', apply: addDartAlly // Assuming a helper function addDartAlly }, { id: 'angel_of_light', name: 'Angel of Light', description: 'Gain an ally that stuns enemies and deals high damage to dark enemies', apply: addAngelOfLight // Assuming a helper function addAngelOfLight }]; // 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: 1200, // Increased height for better layout 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 = -500; // 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 = -500; enemypediaPopup.addChild(closeButton); // Container for the enemy details enemyListContainer = new Container(); enemyListContainer.y = -100; // Centered position for detailed view 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 }, { type: 'jester', assetId: 'jester_asset', description: 'High chance to dodge or reflect non-cannonball projectiles. Gains speed with score.', baseHealth: 2, baseSpeed: 3 * 1.1, dodgeChance: 0.3, // 30% dodge reflectChance: 0.2 // 20% reflect chance for non-cannonballs }, { type: 'dark_war_elephant', assetId: 'dark_war_elephant_asset', description: 'Immune to arrows. Slightly less HP than green elephant, spawns Dark Bowmen on death.', baseHealth: 450, // Slightly less than green elephant baseSpeed: 3 * 0.3, // Same slow speed dodgeChance: 0 }, { type: 'dark_spearman', assetId: 'spearman', // Reusing spearman asset description: 'Faster, more resistant spearman variant. Immune to arrows/cannonballs.', baseHealth: 10, // Higher health than standard spearman baseSpeed: 3 * 1.3, // Faster than standard spearman dodgeChance: 0 // Cannot dodge }, { type: 'dragon', assetId: 'boss', // Reusing boss asset for now, needs dedicated Dragon asset description: 'Moderately fast, very high HP unit with high dodge chance.', baseHealth: 300, // High HP baseSpeed: 3 * 1.1, // Moderately fast dodgeChance: 0.3 // High dodge chance }]; // Clear any existing content in the list container while (enemyListContainer.children.length > 0) { enemyListContainer.removeChildAt(0); } // Current page tracking var currentEnemy = 0; var totalEnemies = enemyData.length; // Create enemy details display function displayEnemyDetails(index) { // Clear any existing content in the list container while (enemyListContainer.children.length > 0) { enemyListContainer.removeChildAt(0); } var enemyInfo = enemyData[index]; // Enemy display container var enemyDisplay = new Container(); enemyDisplay.y = 0; enemyListContainer.addChild(enemyDisplay); // Add enemy graphic (larger for individual view) var enemyGraphic = enemyDisplay.attachAsset(enemyInfo.assetId, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.8, scaleY: 0.8, x: -400, y: 50 }); // Add enemy name (larger font) var nameText = new Text2(enemyInfo.type.replace(/_/g, ' ').toUpperCase(), { size: 60, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 0); nameText.x = 0; nameText.y = -300; enemyDisplay.addChild(nameText); // Add description var descText = new Text2('Description: ' + enemyInfo.description, { size: 40, fill: 0xCCCCCC, wordWrap: true, wordWrapWidth: 1000 }); descText.anchor.set(0, 0); descText.x = -100; descText.y = -200; enemyDisplay.addChild(descText); // Add stats with improved layout var healthText = new Text2('Health: ' + enemyInfo.baseHealth, { size: 40, fill: 0x88FF88 }); healthText.anchor.set(0, 0); healthText.x = -100; healthText.y = -100; enemyDisplay.addChild(healthText); var speedText = new Text2('Speed: ' + enemyInfo.baseSpeed.toFixed(1), { size: 40, fill: 0x88CCFF }); speedText.anchor.set(0, 0); speedText.x = -100; speedText.y = -40; enemyDisplay.addChild(speedText); var dodgeText = new Text2('Dodge Chance: ' + (enemyInfo.dodgeChance * 100).toFixed(0) + '%', { size: 40, fill: 0xFFCC88 }); dodgeText.anchor.set(0, 0); dodgeText.x = -100; dodgeText.y = 20; enemyDisplay.addChild(dodgeText); // Add reflect chance var reflectText = new Text2('Reflect Chance: ' + (enemyInfo.reflectChance * 100 || 0).toFixed(0) + '%', { size: 40, fill: 0xFFCCEE }); reflectText.anchor.set(0, 0); reflectText.x = -100; reflectText.y = 80; enemyDisplay.addChild(reflectText); // Add unlock score var unlockScore = 0; // Default for swordsman if (enemyInfo.type === 'spearman') unlockScore = 10;else if (enemyInfo.type === 'thief') unlockScore = 15;else if (enemyInfo.type === 'knight') unlockScore = 23;else if (enemyInfo.type === 'wizard') unlockScore = 25;else if (enemyInfo.type === 'shield') unlockScore = 30;else if (enemyInfo.type === 'shaman') unlockScore = 35;else if (enemyInfo.type === 'dark_bowman') unlockScore = 55;else if (enemyInfo.type === 'elite_knight') unlockScore = 100;else if (enemyInfo.type === 'elite_shield') unlockScore = 112;else if (enemyInfo.type === 'war_elephant') unlockScore = 125;else if (enemyInfo.type === 'hot_air_balloon') unlockScore = 154;else if (enemyInfo.type === 'jester') unlockScore = 69;else if (enemyInfo.type === 'dark_war_elephant') unlockScore = 145; // Dark war elephant starts appearing at score 145 var unlockText = new Text2('Unlocks at Score: ' + unlockScore, { size: 40, fill: 0xFF88FF }); unlockText.anchor.set(0, 0); unlockText.x = -100; unlockText.y = 140; // Adjusted Y position enemyDisplay.addChild(unlockText); // Status display (current enemy / total) var statusText = new Text2('Enemy ' + (index + 1) + ' of ' + totalEnemies, { size: 30, fill: 0xAAAAAA }); statusText.anchor.set(0.5, 0); statusText.x = 0; statusText.y = 150; enemyDisplay.addChild(statusText); // Navigation buttons container var navButtons = new Container(); navButtons.y = 250; enemyDisplay.addChild(navButtons); // Previous button var prevButton = createNavButton('← Previous', -250, function () { currentEnemy = (currentEnemy - 1 + totalEnemies) % totalEnemies; displayEnemyDetails(currentEnemy); }); navButtons.addChild(prevButton); // Next button var nextButton = createNavButton('Next →', 250, function () { currentEnemy = (currentEnemy + 1) % totalEnemies; displayEnemyDetails(currentEnemy); }); navButtons.addChild(nextButton); } // Create navigation button function createNavButton(label, xPos, callback) { var button = new Container(); button.x = xPos; var buttonBg = button.attachAsset('bastionLine', { width: 250, height: 80, color: 0x555555, anchorX: 0.5, anchorY: 0.5 }); var buttonText = new Text2(label, { size: 40, fill: 0xFFFFFF }); buttonText.anchor.set(0.5, 0.5); button.addChild(buttonText); button.interactive = true; button.down = callback; return button; } // Initial display displayEnemyDetails(currentEnemy); } 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 // Check if the arrow was reflected (takeDamage returns false and arrow is destroyed) if (!enemyDefeated && !arrow.parent) { // Arrow was reflected, remove it from the array and stop processing arrows.splice(i, 1); hitEnemy = true; // Mark that this arrow is done break; // Stop checking this arrow against other enemies as it's destroyed } 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, cannonball); // Pass cannonball as source // Check if the cannonball had no effect (e.g. Dark War Elephant immunity) and is still in game if (!enemyDefeated && cannonball.parent) { // Cannonball had no effect, but wasn't destroyed by a normal hit. // It might have been immune. If so, we still destroy the cannonball. cannonball.destroy(); cannonballs.splice(cb, 1); hitEnemy = true; // Mark that this cannonball is done break; // Stop checking this cannonball against other enemies as it's destroyed } 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. } // 5.6 Update and check Angel of Lights (allies) for (var angelIdx = angelOfLights.length - 1; angelIdx >= 0; angelIdx--) { var angel = angelOfLights[angelIdx]; // Angels do not have a lifetime timer or destruction logic in this version // LK engine calls angel.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 // Check if the magic ball was reflected (takeDamage returns false and magicBall is destroyed) if (!enemyDefeated && !magicBall.parent) { // Magic ball was reflected, remove it from the array and stop processing magicBalls.splice(mb, 1); hitEnemy = true; // Mark that this magic ball is done break; // Stop checking this magic ball against other enemies as it's destroyed } 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 // Check if the axe was reflected (takeDamage returns false and axe is destroyed) if (!enemyDefeated && !axe.parent) { // Axe was reflected, remove it from the array and stop processing vikingAxes.splice(axeIdx, 1); hitEnemyAxe = true; // Mark that this axe is done break; // Stop checking this axe against other enemies as it's destroyed } 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; } } // 6.6 Update and check Darts for (var dartIdx = darts.length - 1; dartIdx >= 0; dartIdx--) { var dart = darts[dartIdx]; // Note: LK engine calls dart.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (dart.lastY === undefined) { dart.lastY = dart.y; } if (dart.lastX === undefined) { dart.lastX = dart.x; } // Check for collisions between the current dart and all enemies. var hitEnemyDart = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; if (dart.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Calculate damage, applying bonus against Green enemies var damageToDealDart = dart.damage; if (enemy.tag === 'Green') { damageToDealDart *= 2; // Double damage against Green enemies } // Apply damage and check if defeated var enemyDefeated = enemy.takeDamage(damageToDealDart, dart); // Pass dart as source // Check if the dart was reflected (takeDamage returns false and dart is destroyed) if (!enemyDefeated && !dart.parent) { // Dart was reflected, remove it from the array and stop processing darts.splice(dartIdx, 1); hitEnemyDart = true; // Mark that this dart is done break; // Stop checking this dart against other enemies } 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. } // Darts are destroyed on hit dart.destroy(); // Apply Green Slowdown if applicable before destroying dart if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } darts.splice(dartIdx, 1); // Remove dart from array. hitEnemyDart = true; // Mark that this dart is done break; // Stop checking this dart against other enemies } } // If the dart hit and was destroyed, skip to the next dart. if (hitEnemyDart) { continue; } // Check if the dart has gone off-screen. var wasOnScreenDart = dart.lastY > -dart.height / 2 && dart.lastX > -dart.width / 2 && dart.lastX < GAME_WIDTH + dart.width / 2; var isOffScreenDart = dart.y < -dart.height / 2 || dart.y > GAME_HEIGHT + dart.height / 2 || dart.x < -dart.width / 2 || dart.x > GAME_WIDTH + dart.width / 2; if (wasOnScreenDart && isOffScreenDart) { dart.destroy(); darts.splice(dartIdx, 1); } else if (!isOffScreenDart) { dart.lastY = dart.y; dart.lastX = dart.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) { possibleTypes.push('spearman'); } if (currentScore >= 15) { possibleTypes.push('thief'); } if (currentScore >= 23) { possibleTypes.push('knight'); } 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 100 possibleTypes.push('hot_air_balloon'); } if (currentScore >= 69) { possibleTypes.push('jester'); // Jester appears at score 69 } if (currentScore >= 145) { possibleTypes.push('dark_war_elephant'); // Dark War Elephant appears at score 145 } // 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 if (typeToSpawn === 'jester') { var baseJesterHP = 2; enemyHealth = baseJesterHP; // Jester HP doesn't scale with score var baseJesterSpeed = baseSpeedForSpawn * 1.1; // Jester starts faster var speedIncreaseIntervalJester = 12; // Gains speed every 12 score points after appearing var speedIncreasePercentJester = 0.03; // 3% speed increase each time var speedIncreasesJester = Math.floor(Math.max(0, currentScore - 69) / speedIncreaseIntervalJester); enemySpeed = baseJesterSpeed * Math.pow(1 + speedIncreasePercentJester, speedIncreasesJester); enemyDodgeChance = 0.30; // 30% dodge chance // The reflectChance is set in the Enemy class constructor based on type, no need to set here. } else if (typeToSpawn === 'dark_war_elephant') { var baseDarkElephantHP = 450; var hpIncreaseIntervalDarkElephant = 10; // Gains HP every 10 score points after appearing var hpIncreasesDarkElephant = Math.floor(Math.max(0, currentScore - 120) / hpIncreaseIntervalDarkElephant); enemyHealth = baseDarkElephantHP + hpIncreasesDarkElephant * 80; // Gains 80 HP per interval enemySpeed *= 0.3; // Dark War Elephants are very slow enemyDodgeChance = 0; // Note: Dark War elephant death spawns dark bowmen - this will be handled in the Enemy death logic. } 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; } }; // --- 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();
===================================================================
--- original.js
+++ change.js
@@ -1644,8 +1644,28 @@
// Slightly less than green elephant
baseSpeed: 3 * 0.3,
// Same slow speed
dodgeChance: 0
+ }, {
+ type: 'dark_spearman',
+ assetId: 'spearman',
+ // Reusing spearman asset
+ description: 'Faster, more resistant spearman variant. Immune to arrows/cannonballs.',
+ baseHealth: 10,
+ // Higher health than standard spearman
+ baseSpeed: 3 * 1.3,
+ // Faster than standard spearman
+ dodgeChance: 0 // Cannot dodge
+ }, {
+ type: 'dragon',
+ assetId: 'boss',
+ // Reusing boss asset for now, needs dedicated Dragon asset
+ description: 'Moderately fast, very high HP unit with high dodge chance.',
+ baseHealth: 300,
+ // High HP
+ baseSpeed: 3 * 1.1,
+ // Moderately fast
+ dodgeChance: 0.3 // High dodge chance
}];
// Clear any existing content in the list container
while (enemyListContainer.children.length > 0) {
enemyListContainer.removeChildAt(0);
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