/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.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 if (closestEnemy.tag !== 'Dragon') { // Dragons are immune to stuns (slowdown effect) 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 Baby Dragon enemy with rage mode when no other dragons are present. */ var BabyDragon = Container.expand(function () { var self = Container.call(this); // Create and attach the baby dragon graphic asset var graphics = self.attachAsset('baby_dragon_asset', { anchorX: 0.5, anchorY: 0.5 }); // No base tint - only apply pink tint when raging self.type = 'baby_dragon'; self.speed = 3; // Base speed (will be set from spawn) self.health = 50; // Base health (will be set from spawn) self.maxHealth = 50; self.dodgeChance = 0.25; // 25% dodge chance (less than adult dragon) self.tag = 'Dragon'; // Dragon-tagged enemy self.isRaging = false; self.rageSpeedMultiplier = 2.0; // Double speed when raging self.baseSpeed = self.speed; self.poisonStacks = 0; self.poisonTimer = 0; self.poisonDamagePerTick = 0.05; self.slowTimer = 0; self.currentSlowAmount = 1.0; /** * Update method called each game tick by the LK engine. * Moves the baby dragon and checks for rage mode. */ self.update = function () { // Check for rage mode (no other dragons on screen) var otherDragonsExist = false; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy !== self && enemy.tag === 'Dragon') { otherDragonsExist = true; break; } } // Apply or remove rage mode if (!otherDragonsExist && !self.isRaging) { // Enter rage mode self.isRaging = true; self.speed = self.baseSpeed * self.rageSpeedMultiplier; // Apply pink tint with tween animation tween(graphics, { tint: 0xFF69B4 }, { duration: 300 }); // Pink tint when raging LK.effects.flashObject(self, 0xFF69B4, 500); // Flash effect with pink } else if (otherDragonsExist && self.isRaging) { // Exit rage mode self.isRaging = false; self.speed = self.baseSpeed; // Remove tint with tween animation tween(graphics, { tint: 0xFFFFFF }, { duration: 300 }); // Return to normal color } // Normal movement (dragons are immune to slowdown) self.y += self.speed; // Apply poison damage if (self.poisonStacks > 0) { self.poisonTimer++; if (self.poisonTimer >= 30) { self.poisonTimer = 0; var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30; self.health -= poisonDmg; } if (self.health <= 0 && self.parent) { LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); self.destroy(); return; } } // Visual feedback based on health var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0; graphics.alpha = 0.4 + healthRatio * 0.6; }; /** * Method called when the baby dragon takes damage. */ self.takeDamage = function (damage, source) { // Check for dodge if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) { return false; } // Dragons are not immune to arrows/cannonballs self.health -= damage; // Apply poison if applicable if (source && source.isPoison) { self.poisonStacks++; self.poisonTimer = 0; } // Dragons are immune to slowdown effects return self.health <= 0; }; return self; }); /** * Represents a Bomb projectile thrown by a Bomber ally. * @param {number} targetX - The target X coordinate. * @param {number} targetY - The target Y coordinate. */ var Bomb = Container.expand(function (targetX, targetY) { var self = Container.call(this); // Create and attach the bomb graphic asset var graphics = self.attachAsset('bomb_asset', { anchorX: 0.5, anchorY: 0.5 }); self.targetX = targetX; self.targetY = targetY; self.startX = self.x; self.startY = self.y; self.damage = 15; // Area damage self.explosionRadius = 200; // Explosion radius self.arcHeight = 300; // Height of bomb arc self.flightTime = 60; // 1 second flight time self.flightTimer = 0; self.hasExploded = false; /** * Update method called each game tick by the LK engine. * Handles arc movement and explosion. */ self.update = function () { if (self.hasExploded) { return; } self.flightTimer++; var progress = self.flightTimer / self.flightTime; if (progress >= 1) { // Bomb has reached target, explode self.explode(); return; } // Calculate arc position var baseX = self.startX + (self.targetX - self.startX) * progress; var baseY = self.startY + (self.targetY - self.startY) * progress; // Add arc height (parabola) var arcOffset = -4 * self.arcHeight * progress * (progress - 1); self.x = baseX; self.y = baseY - arcOffset; // Rotate bomb as it flies graphics.rotation += 0.2; }; self.explode = function () { if (self.hasExploded) { return; } self.hasExploded = true; // Create explosion visual effect LK.effects.flashScreen(0xFFAA00, 200); // Orange flash // Deal area damage to all enemies within radius for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.explosionRadius) { // Calculate damage falloff (full damage at center, less at edges) var damageFalloff = 1 - distance / self.explosionRadius * 0.5; var damageToApply = self.damage * damageFalloff; var enemyDefeated = enemy.takeDamage(damageToApply, self); if (enemyDefeated) { LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); enemy.destroy(); enemies.splice(i, 1); } // Apply visual effect to hit enemies LK.effects.flashObject(enemy, 0xFFAA00, 300); } } self.destroy(); }; return self; }); /** * Represents a Bomber ally that throws bombs for area damage. */ var Bomber = Container.expand(function () { var self = Container.call(this); // Create and attach the bomber graphic asset var graphics = self.attachAsset('bomber_asset', { anchorX: 0.5, anchorY: 0.5 }); self.attackRange = Infinity; // Infinite attack range like other allies self.attackInterval = 240; // Attack every 4 seconds self.attackTimer = 0; /** * Update method called each game tick by the LK engine. * Handles finding targets and throwing bombs. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Find a suitable target (not Dragon-tagged) var bestTarget = null; var bestScore = -1; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Skip Dragon-tagged enemies if (enemy.tag === 'Dragon') { continue; } var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.attackRange) { // Prioritize groups of enemies (check nearby enemy count) var nearbyCount = 0; for (var j = 0; j < enemies.length; j++) { if (i !== j) { var ex = enemies[j].x - enemy.x; var ey = enemies[j].y - enemy.y; if (Math.sqrt(ex * ex + ey * ey) <= 200) { nearbyCount++; } } } var score = nearbyCount * 10 + (self.attackRange - distance) / 100; if (score > bestScore) { bestScore = score; bestTarget = enemy; } } } if (bestTarget) { // Create and throw bomb var bomb = new Bomb(bestTarget.x, bestTarget.y); bomb.startX = self.x; bomb.startY = self.y; bomb.x = self.x; bomb.y = self.y; game.addChild(bomb); bombs.push(bomb); } } }; return self; }); /** * Represents a Bouncy Ball that bounces around the screen dealing damage to enemies. */ var BouncyBall = Container.expand(function () { var self = Container.call(this); // Create and attach the bouncy ball graphic asset (reusing cannonball for now) var graphics = self.attachAsset('cannonball', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); graphics.tint = 0xFF00FF; // Magenta tint for bouncy ball self.speed = 15; // Base speed self.damage = 20; // Massive damage self.vx = (Math.random() - 0.5) * self.speed * 2; // Random initial horizontal velocity self.vy = -Math.abs((Math.random() - 0.5) * self.speed * 2); // Initial upward velocity self.lifetime = 600; // 10 seconds lifetime (600 ticks) self.lifetimeTimer = 0; self.bounceCount = 0; // Track number of bounces self.maxBounces = 20; // Maximum bounces before disappearing /** * Update method called each game tick by the LK engine. * Handles movement, bouncing, and lifetime. */ self.update = function () { self.lifetimeTimer++; // Check lifetime if (self.lifetimeTimer >= self.lifetime || self.bounceCount >= self.maxBounces) { self.destroy(); return; } // Move self.x += self.vx; self.y += self.vy; // Bounce off walls if (self.x <= graphics.width / 2 || self.x >= GAME_WIDTH - graphics.width / 2) { self.vx = -self.vx; // Reverse horizontal direction self.x = Math.max(graphics.width / 2, Math.min(GAME_WIDTH - graphics.width / 2, self.x)); self.bounceCount++; // Flash on bounce LK.effects.flashObject(self, 0xFFFFFF, 200); } // Bounce off top and bottom if (self.y <= graphics.height / 2 || self.y >= BASTION_Y - graphics.height / 2) { self.vy = -self.vy; // Reverse vertical direction self.y = Math.max(graphics.height / 2, Math.min(BASTION_Y - graphics.height / 2, self.y)); self.bounceCount++; // Flash on bounce LK.effects.flashObject(self, 0xFFFFFF, 200); } }; return self; }); /** * 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) // Use dragon slayer cannon asset if mode is enabled, otherwise use regular cannon asset var assetId = self.dragonSlayerMode ? 'dragon_slayer_cannon_asset' : 'cannon_asset'; var graphics = self.attachAsset(assetId, { // 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 self.dragonSlayerMode = false; // Flag to indicate if this cannon is a dragon slayer cannon /** * 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 var targetEnemy = null; if (self.dragonSlayerMode) { // In dragon slayer mode, prioritize dragons var closestDragon = null; var closestDragonDistance = Infinity; var strongestNonDragon = null; var maxNonDragonHealth = -1; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.tag === 'Dragon') { // Check distance for dragons var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDragonDistance) { closestDragonDistance = distance; closestDragon = enemy; } } else { // Track strongest non-dragon as fallback if (enemy.health > maxNonDragonHealth) { maxNonDragonHealth = enemy.health; strongestNonDragon = enemy; } } } // Prioritize dragon if found, otherwise target strongest non-dragon targetEnemy = closestDragon || strongestNonDragon; } else { // Normal mode: find the strongest enemy (highest health) var maxHealth = -1; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.health > maxHealth) { maxHealth = enemy.health; targetEnemy = enemy; } } } // If a target enemy is found within range, attack it if (targetEnemy) { // Calculate angle towards the target enemy var angle = Math.atan2(targetEnemy.x - self.x, -(targetEnemy.y - self.y)); // Create a new cannonball instance, passing the dragon slayer mode status var newCannonball = new Cannonball(angle); newCannonball.dragonSlayerMode = self.dragonSlayerMode; // Pass the mode status // 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 // If dragon slayer mode and targeting a dragon, apply damage bonus if (self.dragonSlayerMode && targetEnemy.tag === 'Dragon') { newCannonball.dragonDamageMultiplier = 25; // 25x damage to dragons // The Cannonball class will handle the asset and tint based on the mode. } // 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 graphic asset (rocket if dragon slayer mode, otherwise cannonball) var assetId = self.dragonSlayerMode ? 'rocket_asset' : 'cannonball'; var graphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align graphic with direction // Apply red tint for rocket if in dragon slayer mode if (self.dragonSlayerMode) { graphics.tint = 0xFF0000; // Red tint for dragon slayer rockets } self.dragonSlayerMode = false; // Flag to indicate if this is a dragon slayer rocket self.dragonDamageMultiplier = 1; // Default damage multiplier 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 } else if (type === 'dark_spearman') { assetId = 'dark_spearman_asset'; // Use specific asset for dark spearman } else if (type === 'dragon') { assetId = 'dragon_asset'; // Use specific asset for dragon } else if (type === 'flag_bearer') { assetId = 'flag_bearer_asset'; // Use specific asset for flag bearer } else if (type === 'baby_dragon') { assetId = 'baby_dragon_asset'; // Use specific baby dragon asset } 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.type === 'dark_spearman') { 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 var effectiveSpeed = self.speed * self.currentSlowAmount; // Apply global slowdown relic if enabled and not a Dragon if (playerRelics.slowdown.enabled && self.tag !== 'Dragon') { effectiveSpeed *= 1 - playerRelics.slowdown.level * 0.02; // Apply global slow } // Apply Green Relic slowdown if enabled and enemy is Green-tagged and not a Dragon if (playerRelics.green.enabled && self.tag === 'Green' && self.tag !== 'Dragon') { effectiveSpeed *= 1 - playerRelics.green.level * 0.02; // Apply Green Relic slow } self.y += effectiveSpeed; // 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 // Unless Dark Relic is enabled if (!playerRelics.dark.enabled) { return false; // Not defeated, projectile has no effect } } var finalDamage = damage; // Apply damage relic bonus if enabled if (playerRelics.damage.enabled) { finalDamage += playerRelics.damage.level; } // Apply Green Relic damage bonus if enabled and enemy is Green-tagged if (playerRelics.green.enabled && self.tag === 'Green') { finalDamage *= 1 + playerRelics.green.level * 0.2; } // Apply Dark Relic damage bonus if enabled and enemy is Black-tagged if (playerRelics.dark.enabled && self.tag === 'Black' && (source instanceof Arrow || source instanceof Cannonball)) { finalDamage *= 1 + playerRelics.dark.level * 0.05; } self.health -= finalDamage; // 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) { if (self.tag !== 'Dragon') { // Dragons are immune to slowdowns 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 was an Arrow and apply Dragon relic stun if (playerRelics.dragon.enabled && source instanceof Arrow && self.tag === 'Dragon') { // Apply stun (reusing slowTimer for duration, 0.0 for 100% slow) var stunDuration = 0.25 * 60 + playerRelics.dragon.level * 0.05 * 60; // Base 0.25s + 0.05s per level self.slowTimer = Math.max(self.slowTimer, stunDuration); self.currentSlowAmount = 0.0; // 100% slow (stun) } // Check if the damage source should apply Green Slowdown if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0 && self.tag !== 'Dragon') { // Dragons are immune to slowdowns // Apply Green Slowdown only if the upgrade is active, enemy is Green, not already slowed, and not a Dragon 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, 2, 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 } else if (self.type === 'dark_spearman') { graphics.scale.set(1.05); // Slightly larger than regular spearman } else if (self.type === 'dragon') { graphics.scale.set(1.6); // Dragons are large } //{11} // Modified original line identifier location return self; // Return self for potential inheritance }); /** * Represents a Flag Bearer enemy that provides a speed boost aura to nearby enemies. */ var FlagBearer = Container.expand(function () { var self = Container.call(this); // Create and attach the flag bearer graphic asset var graphics = self.attachAsset('flag_bearer_asset', { anchorX: 0.5, anchorY: 0.5 }); // No tint applied to flag bearer self.type = 'flag_bearer'; self.speed = 3; // Base speed (will be set from spawn) self.health = 5; // Base health (will be set from spawn) self.maxHealth = 5; self.dodgeChance = 0; self.tag = 'Green'; // Green-tagged enemy self.auraRadius = 300; // Radius of speed boost aura self.auraBoost = 1.5; // 50% speed boost to enemies in aura self.poisonStacks = 0; self.poisonTimer = 0; self.poisonDamagePerTick = 0.05; self.slowTimer = 0; self.currentSlowAmount = 1.0; // Create aura visual effect self.auraCircle = self.attachAsset('light_effect_asset', { anchorX: 0.5, anchorY: 0.5, width: self.auraRadius * 2, height: self.auraRadius * 2, tint: 0x00FF00, alpha: 0.2 }); // Position aura behind the flag bearer self.setChildIndex(self.auraCircle, 0); /** * Update method called each game tick by the LK engine. * Moves the flag bearer and applies aura effects. */ self.update = function () { // Normal movement self.y += self.speed * self.currentSlowAmount; // Apply aura effect to nearby enemies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy === self) { continue; } // Skip self var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance <= self.auraRadius) { // Enemy is within aura range if (!enemy.hasAuraBoost) { enemy.hasAuraBoost = true; enemy.baseSpeed = enemy.speed; enemy.speed = enemy.baseSpeed * self.auraBoost; } } else if (enemy.hasAuraBoost) { // Enemy left aura range enemy.hasAuraBoost = false; enemy.speed = enemy.baseSpeed || enemy.speed; } } // Handle slowdown timer if (self.slowTimer > 0) { self.slowTimer--; if (self.slowTimer <= 0) { self.currentSlowAmount = 1.0; } } // Apply poison damage if (self.poisonStacks > 0) { self.poisonTimer++; if (self.poisonTimer >= 30) { self.poisonTimer = 0; var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30; self.health -= poisonDmg; } if (self.health <= 0 && self.parent) { LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); self.destroy(); return; } } // Visual feedback based on health var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0; graphics.alpha = 0.4 + healthRatio * 0.6; // Update aura visual effect if (self.auraCircle) { self.auraCircle.x = self.x; self.auraCircle.y = self.y; // Pulse effect self.auraPulseTimer = (self.auraPulseTimer || 0) + 0.05; var pulseScale = 1.0 + Math.sin(self.auraPulseTimer) * 0.1; self.auraCircle.scale.set(pulseScale); self.auraCircle.alpha = 0.15 + Math.sin(self.auraPulseTimer * 2) * 0.05; } }; /** * Method called when the flag bearer takes damage. */ self.takeDamage = function (damage, source) { // Check for dodge if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) { return false; } // Check if source is Arrow or Cannonball (Green enemies are not immune) self.health -= damage; // Apply poison if applicable if (source && source.isPoison) { self.poisonStacks++; self.poisonTimer = 0; } // Apply slowdown from Magic Ball if (source && source instanceof MagicBall) { self.slowTimer = source.slowDuration; self.currentSlowAmount = source.slowAmount; } // Apply Green Slowdown if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0) { self.slowTimer = 10 * 60; self.currentSlowAmount = 0.9; } return self.health <= 0; }; // Clean up aura effects when destroyed var originalDestroy = self.destroy; self.destroy = function () { // Remove aura effects from all enemies for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.hasAuraBoost) { enemy.hasAuraBoost = false; enemy.speed = enemy.baseSpeed || enemy.speed; } } // Clean up aura visual if (self.auraCircle) { self.auraCircle.destroy(); self.auraCircle = null; } originalDestroy.call(self); }; return self; }); /** * 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 finalSwordsmanDamage = self.attackDamage; // Apply damage relic bonus if enabled if (playerRelics.damage.enabled) { finalSwordsmanDamage += playerRelics.damage.level; } var enemyDefeated = closestEnemy.takeDamage(finalSwordsmanDamage, 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 }); /** * Represents an XBOW (crossbow) ally that rapidly shoots arrows. */ var XBOW = Container.expand(function () { var self = Container.call(this); // Create and attach the XBOW graphic asset var graphics = self.attachAsset('xbow_asset', { anchorX: 0.5, anchorY: 0.5 }); self.fireInterval = 20; // Very rapid fire (every 0.33 seconds) self.fireTimer = 0; self.targetingMode = 'closest_to_bastion'; // Default targeting mode self.smartTargetingEnabled = false; // Flag for smart targeting upgrade self.tintApplied = false; // Track if tint has been applied /** * Update method called each game tick by the LK engine. * Handles rapid firing logic. */ self.update = function () { self.fireTimer++; var baseInterval = self.smartTargetingEnabled ? 15.36 : self.fireInterval; // 0.256 seconds when smart targeting enabled var effectiveFireInterval = Math.max(10, baseInterval * allyAttackSpeedMultiplier); if (self.fireTimer >= effectiveFireInterval) { self.fireTimer = 0; // Find target based on targeting mode var target = null; if (self.smartTargetingEnabled) { // Apply green tint when smart targeting is enabled if (!self.tintApplied) { graphics.tint = 0x00FF00; // Green tint self.tintApplied = true; } if (self.targetingMode === 'closest_to_bastion') { // Find enemy closest to bastion var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var distanceToBastion = BASTION_Y - enemy.y; if (distanceToBastion > 0 && distanceToBastion < closestDistance) { closestDistance = distanceToBastion; target = enemy; } } } else if (self.targetingMode === 'strongest') { // Find enemy with highest health var maxHealth = -1; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.health > maxHealth) { maxHealth = enemy.health; target = enemy; } } } } // Default behavior: shoot at center of screen var angle; if (target) { // Calculate angle towards target angle = Math.atan2(target.x - self.x, -(target.y - self.y)); } else { // No target or smart targeting disabled - shoot straight up towards center angle = 0; // 0 radians = straight up } // Create arrow var newArrow = new Arrow(angle); newArrow.x = self.x; newArrow.y = self.y; newArrow.lastY = newArrow.y; newArrow.lastX = newArrow.x; // XBOW arrows are faster newArrow.speed *= 1.5; newArrow.vx = Math.sin(angle) * newArrow.speed; newArrow.vy = -Math.cos(angle) * newArrow.speed; // Disable aimbot for XBOW arrows newArrow.targetEnemy = null; game.addChild(newArrow); arrows.push(newArrow); // Apply multi-shot if enabled 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 = self.x; newArrow2.y = self.y; newArrow2.lastY = newArrow2.y; newArrow2.lastX = newArrow2.x; // XBOW arrows are faster newArrow2.speed *= 1.5; newArrow2.vx = Math.sin(angle2) * newArrow2.speed; newArrow2.vy = -Math.cos(angle2) * newArrow2.speed; // Disable aimbot for XBOW arrows newArrow2.targetEnemy = null; 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 = self.x; newArrow3.y = self.y; newArrow3.lastY = newArrow3.y; newArrow3.lastX = newArrow3.x; // XBOW arrows are faster newArrow3.speed *= 1.5; newArrow3.vx = Math.sin(angle3) * newArrow3.speed; newArrow3.vy = -Math.cos(angle3) * newArrow3.speed; // Disable aimbot for XBOW arrows newArrow3.targetEnemy = null; game.addChild(newArrow3); arrows.push(newArrow3); } } }; return self; }); /**** * 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 baseMaxArrowsBeforeCooldown = 15; // Base max arrows before reload (Quiver upgrade) var maxArrowsBeforeCooldown = baseMaxArrowsBeforeCooldown; // Max arrows before reload var ammoRelicTimer = 0; // Timer for ammo relic regeneration 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 var bouncyBalls = []; // Array to hold BouncyBall instances var bombs = []; // Array to hold Bomb instances var bombers = []; // Array to hold Bomber instances var xbows = []; // Array to hold XBOW instances var xbowSmartTargetingEnabled = false; // Flag for XBOW Smart Targeting upgrade var battleHornEnabled = false; // Flag for Battle Horn upgrade var dragonSlayerCannonsModeEnabled = false; // Flag for Dragon Slayer Cannons upgrade // --- Relics and Gold System --- var playerGold = storage.playerGold || 0; // Load gold from storage, default to 0 // Load relics from storage using flat format var storedRelicsData = storage.playerRelicsData || {}; var playerRelics = { damage: { level: storedRelicsData.damage_level || 0, enabled: storedRelicsData.damage_enabled || false }, slowdown: { level: storedRelicsData.slowdown_level || 0, enabled: storedRelicsData.slowdown_enabled || false }, green: { level: storedRelicsData.green_level || 0, enabled: storedRelicsData.green_enabled || false }, dark: { level: storedRelicsData.dark_level || 0, enabled: storedRelicsData.dark_enabled || false }, dragon: { level: storedRelicsData.dragon_level || 0, enabled: storedRelicsData.dragon_enabled || false }, reload: { level: storedRelicsData.reload_level || 0, enabled: storedRelicsData.reload_enabled || false }, ammo: { level: storedRelicsData.ammo_level || 0, enabled: storedRelicsData.ammo_enabled || false }, swordsman: { level: storedRelicsData.swordsman_level || 0, enabled: storedRelicsData.swordsman_enabled || false } }; var RELIC_MAX_LEVEL = 10; var RELIC_COST_PER_LEVEL = 5; // 5 gold per level // --- 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 // Apply dragon slayer mode if the global flag is enabled newCannon.dragonSlayerMode = dragonSlayerCannonsModeEnabled; 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 } // Helper function to add a Bouncy Ball function addBouncyBall() { console.log("Bouncy Ball upgrade chosen!"); var newBall = new BouncyBall(); // Start from center of screen newBall.x = GAME_WIDTH / 2; newBall.y = GAME_HEIGHT / 2; game.addChild(newBall); bouncyBalls.push(newBall); } // Helper function to add a Bomber function addBomber() { console.log("Bomber upgrade chosen!"); var newBomber = new Bomber(); // Position near bastion line newBomber.x = GAME_WIDTH / 2 - 400; newBomber.y = BASTION_Y - 100; game.addChild(newBomber); bombers.push(newBomber); } // Helper function to add an XBOW function addXBOW() { console.log("XBOW upgrade chosen!"); var newXBOW = new XBOW(); // Position at the center of the bastion newXBOW.x = GAME_WIDTH / 2; newXBOW.y = BASTION_Y - 100; game.addChild(newXBOW); xbows.push(newXBOW); } // Helper function to enable XBOW Smart Targeting function enableXBOWSmartTargeting() { console.log("XBOW Smart Targeting upgrade chosen!"); xbowSmartTargetingEnabled = true; // Enable smart targeting for all existing XBOWs for (var i = 0; i < xbows.length; i++) { xbows[i].smartTargetingEnabled = true; // Randomly assign targeting mode for variety xbows[i].targetingMode = Math.random() < 0.5 ? 'closest_to_bastion' : 'strongest'; } } // 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 }, { id: 'bouncy_ball', name: 'Bouncy Ball', description: 'Bounces around dealing massive damage to enemies', apply: addBouncyBall }, { id: 'bomber', name: 'Bomber', description: 'Throws bombs that explode for area damage', apply: addBomber }, { id: 'xbow', name: 'XBOW', description: 'A big bow that rapidly shoots arrows', apply: addXBOW }, { id: 'xbow_smart_targeting', name: 'XBOW Smart Targeting', description: 'XBOWs can target closest to bastion or strongest enemy', apply: enableXBOWSmartTargeting }, { id: 'dragon_slayer_cannons', name: 'Dragon Slayer Cannons', description: 'Cannons become rocket launchers that deal 25x damage to dragons and prioritize them', apply: function apply() { // Enable dragon slayer mode for all existing cannons for (var i = 0; i < cannons.length; i++) { cannons[i].dragonSlayerMode = true; } // Set global flag so all future cannons will have dragon slayer mode dragonSlayerCannonsModeEnabled = true; } }, { id: 'battle_horn', name: 'Battle Horn', description: 'More enemies spawn, but they are all swordsmen: more score for more upgrades!', apply: function apply() { // Reduce spawn interval for more enemies enemySpawnInterval = Math.max(15, enemySpawnInterval * 0.5); // 50% faster spawning, min 0.25 seconds // Set global flag to force swordsman spawns battleHornEnabled = true; } }]; // Function to create and show the upgrade selection popup function showUpgradePopup() { isUpgradePopupActive = true; // Simple pause: Game logic checks isUpgradePopupActive flag in game.update // Switch to upgrade theme music with fade effect if (currentMusicPlaying !== 'UpgradeTheme' && !musicFadeInProgress) { musicFadeInProgress = true; // Fade out current music LK.playMusic('Gamemusic', { fade: { start: 1, end: 0, duration: 800 } }); // After fade out completes, start upgrade theme with fade in LK.setTimeout(function () { LK.playMusic('UpgradeTheme', { fade: { start: 0, end: 1, duration: 1000 } }); currentMusicPlaying = 'UpgradeTheme'; musicFadeInProgress = false; }, 850); } // --- Create Popup UI --- upgradePopup = new Container(); upgradePopup.x = GAME_WIDTH / 2; upgradePopup.y = GAME_HEIGHT / 2; game.addChild(upgradePopup); // Add to game layer for positioning relative to center // Add a semi-transparent background overlay var bg = upgradePopup.attachAsset('bastionLine', { // Reusing an asset for shape width: 1200, height: 800, color: 0x000000, // Black background anchorX: 0.5, anchorY: 0.5, alpha: 0.8 }); // Add title text var title = new Text2('Choose an Upgrade!', { size: 80, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.y = -300; // Position relative to popup center upgradePopup.addChild(title); // --- Select 3 Random Unique Upgrades --- var availableToOffer = allUpgrades.slice(); // Copy available upgrades var choices = []; var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices // Make dragon slayer cannons more common past score 100 var currentScore = LK.getScore(); if (currentScore > 100 && !dragonSlayerCannonsModeEnabled) { // Add dragon slayer cannons upgrade multiple times to increase chances var dragonSlayerUpgrade = null; for (var ds = 0; ds < availableToOffer.length; ds++) { if (availableToOffer[ds].id === 'dragon_slayer_cannons') { dragonSlayerUpgrade = availableToOffer[ds]; break; } } if (dragonSlayerUpgrade) { // Add it 2 more times to the pool (3x total chance) availableToOffer.push(dragonSlayerUpgrade); availableToOffer.push(dragonSlayerUpgrade); } } 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) } // --- Upgradepedia --- var upgradepediaButton; var upgradepediaPopup = null; var upgradeListContainer = null; function showUpgradepedia() { isUpgradePopupActive = true; // Pause the game while upgradepedia is open upgradepediaPopup = new Container(); upgradepediaPopup.x = GAME_WIDTH / 2; upgradepediaPopup.y = GAME_HEIGHT / 2; game.addChild(upgradepediaPopup); // Add a semi-transparent background overlay var bg = upgradepediaPopup.attachAsset('pedia_screen_bg', { width: 1600, height: 2000, anchorX: 0.5, anchorY: 0.5, alpha: 0.9 }); // Add title text var title = new Text2('Upgradepedia', { size: 100, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.y = -900; upgradepediaPopup.addChild(title); // Add close button var closeButton = createUpgradepediaCloseButton(); closeButton.x = 750; closeButton.y = -900; upgradepediaPopup.addChild(closeButton); // Container for upgrade details upgradeListContainer = new Container(); upgradeListContainer.y = -200; upgradepediaPopup.addChild(upgradeListContainer); displayUpgradeStats(); } function hideUpgradepedia() { if (upgradepediaPopup) { upgradepediaPopup.destroy(); upgradepediaPopup = null; } isUpgradePopupActive = false; // Unpause the game } function createUpgradepediaCloseButton() { var buttonContainer = new Container(); var buttonBg = buttonContainer.attachAsset('pedia_button_bg', { width: 150, height: 80, color: 0xCC0000, 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 () { hideUpgradepedia(); }; return buttonContainer; } function displayUpgradeStats() { // Clear any existing content while (upgradeListContainer.children.length > 0) { upgradeListContainer.removeChildAt(0); } // Current page tracking var currentUpgrade = 0; var totalUpgrades = allUpgrades.length; // Map upgrade IDs to visual assets and additional info var upgradeAssets = { 'faster_reload': { asset: 'Reload', scale: 0.4 }, 'piercing_shots': { asset: 'arrow', scale: 0.6 }, 'quiver': { asset: 'arrow', scale: 0.8, tint: 0xFFD700 }, 'heat_tipped': { asset: 'arrow', scale: 0.6, tint: 0xFF4500 }, 'archer_ally': { asset: 'Archer', scale: 0.8 }, 'sabotage': { asset: 'thief', scale: 0.8 }, 'swordsman': { asset: 'swordsmanAlly', scale: 0.8 }, 'more_shots': { asset: 'arrow', scale: 0.5, count: 3 }, 'poison_shots': { asset: 'arrow', scale: 0.6, tint: 0x00FF00 }, 'cannon': { asset: 'cannon_asset', scale: 0.8 }, 'ally_atk_speed': { asset: 'viking_axe', scale: 0.6 }, 'wizard_tower': { asset: 'wizard_tower_asset', scale: 0.8 }, 'aimbot': { asset: 'arrow', scale: 0.6, special: 'seeking' }, 'green_killer': { asset: 'arrow', scale: 0.6, tint: 0xFF0000 }, 'refined_projectiles': { asset: 'cannonball', scale: 0.6, tint: 0xFFD700 }, 'viking_ally': { asset: 'viking_ally', scale: 0.8 }, 'green_slowdown': { asset: 'magic_ball_asset', scale: 0.6, tint: 0x00FF00 }, 'dart_shooter': { asset: 'dart_asset', scale: 0.8 }, 'angel_of_light': { asset: 'angel_of_light_asset', scale: 0.8 }, 'bouncy_ball': { asset: 'cannonball', scale: 1.0, tint: 0xFF00FF }, 'bomber': { asset: 'bomber_asset', scale: 0.8 }, 'xbow': { asset: 'xbow_asset', scale: 0.8 }, 'xbow_smart_targeting': { asset: 'xbow_asset', scale: 0.8, tint: 0x00FFFF }, 'dragon_slayer_cannons': { asset: 'cannon_asset', scale: 0.8, tint: 0xFF0000 }, 'battle_horn': { asset: 'flag_bearer_asset', scale: 0.8, tint: 0xFFD700 } }; function displayUpgradeDetails(index) { // Clear existing content while (upgradeListContainer.children.length > 0) { upgradeListContainer.removeChildAt(0); } var upgradeInfo = allUpgrades[index]; var assetInfo = upgradeAssets[upgradeInfo.id] || { asset: 'arrow', scale: 0.6 }; // Upgrade display container var upgradeDisplay = new Container(); upgradeDisplay.y = 0; upgradeListContainer.addChild(upgradeDisplay); // Add upgrade graphic if (assetInfo.count) { // Special case for multiple projectiles for (var i = 0; i < assetInfo.count; i++) { var graphic = upgradeDisplay.attachAsset(assetInfo.asset, { anchorX: 0.5, anchorY: 0.5, scaleX: assetInfo.scale, scaleY: assetInfo.scale, x: -400 + (i - 1) * 100, y: 50 }); if (assetInfo.tint) { graphic.tint = assetInfo.tint; } } } else { var upgradeGraphic = upgradeDisplay.attachAsset(assetInfo.asset, { anchorX: 0.5, anchorY: 0.5, scaleX: assetInfo.scale, scaleY: assetInfo.scale, x: -400, y: 50 }); if (assetInfo.tint) { upgradeGraphic.tint = assetInfo.tint; } // Special visual effects if (assetInfo.special === 'seeking') { // Add rotation animation hint for seeking arrows upgradeGraphic.rotation = Math.PI / 6; } } // Add upgrade name var nameText = new Text2(upgradeInfo.name, { size: 70, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 0); nameText.x = 0; nameText.y = -400; upgradeDisplay.addChild(nameText); // Add description var descText = new Text2(upgradeInfo.description, { size: 45, fill: 0xCCCCCC, wordWrap: true, wordWrapWidth: 1200 }); descText.anchor.set(0.5, 0); descText.x = 0; descText.y = -300; upgradeDisplay.addChild(descText); // Add detailed ability description var abilityText = getDetailedAbility(upgradeInfo.id); var abilityDisplay = new Text2('Ability: ' + abilityText, { size: 40, fill: 0x88FF88, wordWrap: true, wordWrapWidth: 1200 }); abilityDisplay.anchor.set(0.5, 0); abilityDisplay.x = 0; abilityDisplay.y = -150; upgradeDisplay.addChild(abilityDisplay); // Status display var statusText = new Text2('Upgrade ' + (index + 1) + ' of ' + totalUpgrades, { size: 35, fill: 0xAAAAAA }); statusText.anchor.set(0.5, 0); statusText.x = 0; statusText.y = 250; upgradeDisplay.addChild(statusText); // Navigation buttons var navButtons = new Container(); navButtons.y = 350; upgradeDisplay.addChild(navButtons); // Previous button var prevButton = createUpgradeNavButton('โ Previous', -250, function () { currentUpgrade = (currentUpgrade - 1 + totalUpgrades) % totalUpgrades; displayUpgradeDetails(currentUpgrade); }); navButtons.addChild(prevButton); // Next button var nextButton = createUpgradeNavButton('Next โ', 250, function () { currentUpgrade = (currentUpgrade + 1) % totalUpgrades; displayUpgradeDetails(currentUpgrade); }); navButtons.addChild(nextButton); } function getDetailedAbility(upgradeId) { var abilities = { 'faster_reload': 'Reduces the cooldown time between reloads by 20%. Stacks multiplicatively with minimum 1 second cooldown.', 'piercing_shots': 'Each arrow can pierce through one additional enemy. Stacks to pierce multiple enemies.', 'quiver': 'Increases maximum ammo capacity by 5 arrows before needing to reload.', 'heat_tipped': 'Increases arrow damage by 1. Affects all arrow-based attacks including allies.', 'archer_ally': 'Spawns an allied archer that independently targets and shoots at enemies every 3 seconds.', 'sabotage': 'Reduces all enemy movement speed by 10%. Stacks multiplicatively with minimum 50% speed.', 'swordsman': 'Creates a tower that spawns temporary swordsmen allies every 3 seconds. Swordsmen chase and attack nearby enemies.', 'more_shots': 'Fire 3 arrows at once in a spread pattern. Works with all arrow upgrades.', 'poison_shots': 'Arrows apply poison stacks that deal damage over time. Each stack deals damage every 0.5 seconds.', 'cannon': 'Deploys a cannon that targets the strongest enemy and fires high-damage cannonballs every 5 seconds.', 'ally_atk_speed': 'All allies attack 20% faster. Stacks multiplicatively.', 'wizard_tower': 'Builds a tower that shoots magic balls causing slowdown effect on hit. Dragons are immune to slowdown.', 'aimbot': 'Arrows and magic balls automatically seek the nearest enemy. Updates target if current target is destroyed.', 'green_killer': 'Deal 50% extra damage to all Green-tagged enemies (Wizards, Spearmen, War Elephants, Shamans).', 'refined_projectiles': 'Non-arrow projectiles (cannonballs, magic balls, axes) gain +5 damage and +5 speed.', 'viking_ally': 'Summons a viking warrior that throws piercing axes. Each axe can pierce through 3 enemies.', 'green_slowdown': 'Projectiles slow Green-tagged enemies by 10% for 10 seconds. Effect does not stack.', 'dart_shooter': 'Deploys a rapid-fire dart shooter that deals double damage to Green enemies.', 'angel_of_light': 'Summons an angel that stuns enemies for 5 seconds and deals triple damage to Black-tagged enemies. Dragons cannot be stunned.', 'bouncy_ball': 'Releases a bouncing projectile that ricochets off walls dealing massive damage. Lasts 10 seconds or 20 bounces.', 'bomber': 'Deploys a bomber that throws area damage bombs. Cannot directly target Dragons but explosion can damage them.', 'xbow': 'Installs a large crossbow that rapidly fires arrows every 0.33 seconds. Arrows travel 50% faster.', 'xbow_smart_targeting': 'XBOWs can now target either the enemy closest to bastion or the strongest enemy. All other allies have this by default.', 'dragon_slayer_cannons': 'Transforms all current and future cannons into rocket launchers. Rockets deal 25x damage to Dragon-type enemies and prioritize targeting them over all other enemy types.', 'battle_horn': 'Doubles enemy spawn rate but forces all spawned enemies to be basic swordsmen. Great for farming score since swordsmen are easy to defeat!' }; return abilities[upgradeId] || 'Special ability that enhances your defenses.'; } function createUpgradeNavButton(label, xPos, callback) { var button = new Container(); button.x = xPos; var buttonBg = button.attachAsset('pedia_nav_button_bg', { width: 250, height: 80, 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 displayUpgradeDetails(currentUpgrade); } // Create upgradepedia button upgradepediaButton = new Container(); upgradepediaButton.x = 200; // Position on the left side upgradepediaButton.y = GAME_HEIGHT - 200; // Add button background var upgradeBtnBg = upgradepediaButton.attachAsset('pedia_button_bg', { width: 300, height: 100, anchorX: 0.5, anchorY: 0.5 }); // Add button text var upgradeBtnText = new Text2('Upgradepedia', { size: 40, fill: 0xFFFFFF }); upgradeBtnText.anchor.set(0.5, 0.5); upgradepediaButton.addChild(upgradeBtnText); // Make button interactive upgradepediaButton.interactive = true; upgradepediaButton.down = function () { showUpgradepedia(); }; // Add the button to the game scene game.addChild(upgradepediaButton); // --- Relic Shop Button --- var relicShopButton = new Container(); relicShopButton.x = GAME_WIDTH - 200; // Position on the right side relicShopButton.y = GAME_HEIGHT - 320; // Position above Enemypedia button var relicBtnBg = relicShopButton.attachAsset('pedia_button_bg', { width: 300, height: 100, anchorX: 0.5, anchorY: 0.5 }); var relicBtnText = new Text2('Relic Shop', { size: 40, fill: 0xFFFFFF }); relicBtnText.anchor.set(0.5, 0.5); relicShopButton.addChild(relicBtnText); relicShopButton.interactive = true; relicShopButton.down = function () { showRelicShop(); }; game.addChild(relicShopButton); // --- 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 that has very low HP.', baseHealth: 1, baseSpeed: 3, dodgeChance: 0 }, { type: 'knight', assetId: 'knight', description: 'A threat for the first few waves cause of its high health, but is slow.', baseHealth: 5, baseSpeed: 3 * 0.9, dodgeChance: 0 }, { type: 'thief', assetId: 'thief', description: 'Fast, low health unit with a dodge chance; likes gravy.', baseHealth: 1, baseSpeed: 3 * 1.2, dodgeChance: 0.1 }, { type: 'boss', assetId: 'boss', description: 'a boss that spawns randomly when youre in trobule.', baseHealth: 5, // Base health before scaling baseSpeed: 3 * 0.7, dodgeChance: 0 }, { type: 'shield', assetId: 'shield_enemy', description: 'Slow unit that has high HP', baseHealth: 20, baseSpeed: 3 * 0.5, dodgeChance: 0 }, { type: 'wizard', assetId: 'wizard_enemy', description: 'kinda tricky cause it teleports and has moderate speed.', baseHealth: 2, baseSpeed: 3 * 0.7, dodgeChance: 0 }, { type: 'spearman', assetId: 'spearman', description: 'Standard, annoying green unit that spawns from Elephants most of the time.', baseHealth: 2, baseSpeed: 3 * 1.0, dodgeChance: 0 }, { type: 'war_elephant', assetId: 'war_elephant', description: 'Extremely high health elephant, spawns spearmen on death', baseHealth: 500, baseSpeed: 3 * 0.3, dodgeChance: 0 }, { type: 'elite_knight', assetId: 'elite_knight_asset', description: 'High health knight variant that is loyal to the king', baseHealth: 45, baseSpeed: 3 * 0.85, dodgeChance: 0 }, { type: 'elite_shield', assetId: 'elite_shield_asset', description: 'Even higher health shield variant that is loyal to the king', baseHealth: 200, baseSpeed: 3 * 0.4, dodgeChance: 0 }, { type: 'shaman', assetId: 'shaman_enemy', description: 'Reduces your ammo when he feels like it', baseHealth: 2, baseSpeed: 3 * 0.8, dodgeChance: 0 }, { type: 'hot_air_balloon', assetId: 'hot_air_balloon_asset', description: 'Very low health. Spawns random non-green enemies on death.', baseHealth: 1, baseSpeed: 3 * 0.2, dodgeChance: 0 }, { type: 'dark_bowman', assetId: 'dark_bowman_asset', description: 'Immune to arrows and cannonballs. Worst enemy of blue archers', baseHealth: 5, baseSpeed: 3 * 1.1, dodgeChance: 0 }, { type: 'jester', assetId: 'jester_asset', description: 'High chance to dodge or reflect non-cannonball projectiles...YOU WILL LAUGH AT ALL OF HIS TRICKS', 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. variant of 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: 'dark_spearman_asset', description: 'A faster and more resistant spearman variant that came straight from rome. Immune to arrows and cannonballs', baseHealth: 15, // Base health baseSpeed: 3 * 1.2, // Faster than spearman dodgeChance: 0, tag: 'Black' // Immune to arrows and cannonballs }, { type: 'dragon', assetId: 'dragon_asset', description: 'A fearsome dragon with great HP and a high chance to dodge attacks. Cant be slowed down', baseHealth: 250, // Great HP baseSpeed: 3 * 0.9, // Moderately fast dodgeChance: 0.40, // High dodge chance (40%) tag: 'Dragon' // Special tag if needed for other mechanics, for now dodge is key }, { type: 'flag_bearer', assetId: 'flag_bearer_asset', description: 'Green-tagged enemy with an aura that makes nearby enemies move 50% faster', baseHealth: 5, baseSpeed: 3 * 0.8, dodgeChance: 0, tag: 'Green' }, { type: 'baby_dragon', assetId: 'baby_dragon_asset', description: 'Small dragon with less health but enters rage mode (double speed) when no other dragons are present', baseHealth: 50, baseSpeed: 3 * 1.1, dodgeChance: 0.25, tag: 'Dragon' }]; // 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_spearman') { unlockScore = 55; } // Dark Spearman unlock score else if (enemyInfo.type === 'dark_bowman') { unlockScore = 55; } // Note: Same as Dark Spearman, might need adjustment if only one is desired at 55. Keeping as per request. else if (enemyInfo.type === 'jester') { unlockScore = 69; } else if (enemyInfo.type === 'dragon') { unlockScore = 134; } // Dragon unlock score else if (enemyInfo.type === 'flag_bearer') { unlockScore = 27; } // Flag Bearer unlock score else if (enemyInfo.type === 'baby_dragon') { unlockScore = 125; } // Baby Dragon unlock score (same as war elephant) 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 === 'dark_war_elephant') { unlockScore = 145; } // Dark war elephant starts appearing at score 145 else if (enemyInfo.type === 'hot_air_balloon') { unlockScore = 154; } 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); // --- Relic Shop --- var relicShopPopup = null; var relicListContainer = null; var goldText; // Text display for gold function showRelicShop() { isUpgradePopupActive = true; // Pause the game relicShopPopup = new Container(); relicShopPopup.x = GAME_WIDTH / 2; relicShopPopup.y = GAME_HEIGHT / 2; game.addChild(relicShopPopup); var bg = relicShopPopup.attachAsset('pedia_screen_bg', { width: 1600, height: 2000, anchorX: 0.5, anchorY: 0.5, alpha: 0.9 }); var title = new Text2('Relic Shop', { size: 100, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.y = -900; relicShopPopup.addChild(title); var closeButton = createRelicShopCloseButton(); closeButton.x = 750; closeButton.y = -900; relicShopPopup.addChild(closeButton); // Gold display goldText = new Text2('Gold: ' + playerGold, { size: 50, fill: 0xFFD700 }); goldText.anchor.set(0.5, 0.5); goldText.y = -800; relicShopPopup.addChild(goldText); relicListContainer = new Container(); relicListContainer.y = -200; relicShopPopup.addChild(relicListContainer); displayRelicDetails(); } function hideRelicShop() { if (relicShopPopup) { relicShopPopup.destroy(); relicShopPopup = null; } isUpgradePopupActive = false; // Unpause the game } function createRelicShopCloseButton() { var buttonContainer = new Container(); var buttonBg = buttonContainer.attachAsset('pedia_button_bg', { width: 150, height: 80, color: 0xCC0000, 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 () { hideRelicShop(); }; return buttonContainer; } function displayRelicDetails() { while (relicListContainer.children.length > 0) { relicListContainer.removeChildAt(0); } var relicsData = [{ id: 'damage', name: 'Damage Relic', description: 'Increase projectile, swordsman, and poison damage.', effect: '+1 damage per level' }, { id: 'slowdown', name: 'Slowdown Relic', description: 'All enemies are slower.', effect: '+2% slowdown per level' }, { id: 'green', name: 'Green Relic', description: 'Green enemies are slower and take more damage.', effect: '+2% slow & +2% damage per level' }, { id: 'dark', name: 'Dark Relic', description: 'Dark enemies can be hit by arrows/cannonballs and take more damage from them.', effect: '+5% damage per level' }, { id: 'dragon', name: 'Dragon Relic', description: 'Projectiles stun dragons.', effect: '+0.05 seconds stun per level' }, { id: 'reload', name: 'Reload Relic', description: 'Reload faster! Essential for rapid-fire gameplay.', effect: '-0.5 seconds reload time per level' }, { id: 'ammo', name: 'Ammo Relic', description: 'Regenerate ammo over time without reloading.', effect: '+1 ammo regenerated every 5 seconds per level' }, { id: 'swordsman', name: 'Swordsman Relic', description: 'Start each battle with a permanent swordsman ally.', effect: 'Unlocks permanent swordsman, +2 damage per upgrade' }]; // Current page tracking var currentRelic = 0; var totalRelics = relicsData.length; // Create relic details display function displayRelicItem(index) { // Clear any existing content in the list container while (relicListContainer.children.length > 0) { relicListContainer.removeChildAt(0); } var relic = relicsData[index]; var relicInfo = playerRelics[relic.id]; // Relic display container var relicDisplay = new Container(); relicDisplay.y = 0; relicListContainer.addChild(relicDisplay); // Background var bg = relicDisplay.attachAsset('pedia_button_bg', { width: 1400, height: 400, color: 0x333355, anchorX: 0.5, anchorY: 0.5 }); // Relic Name var nameText = new Text2(relic.name + ' (Level ' + relicInfo.level + '/' + RELIC_MAX_LEVEL + ')', { size: 70, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 0); nameText.x = 0; nameText.y = -300; relicDisplay.addChild(nameText); // Relic Description var descText = new Text2(relic.description, { size: 45, fill: 0xCCCCCC, wordWrap: true, wordWrapWidth: 1200 }); descText.anchor.set(0.5, 0); descText.x = 0; descText.y = -200; relicDisplay.addChild(descText); // Relic Effect var effectText = new Text2('Effect: ' + relic.effect, { size: 40, fill: 0x88FF88, wordWrap: true, wordWrapWidth: 1200 }); effectText.anchor.set(0.5, 0); effectText.x = 0; effectText.y = -100; relicDisplay.addChild(effectText); // Buttons container var buttonsContainer = new Container(); buttonsContainer.y = 0; relicDisplay.addChild(buttonsContainer); // Buy/Upgrade Button var buyButton = new Container(); buyButton.x = -200; var buyBtnBgColor = relicInfo.level < RELIC_MAX_LEVEL ? 0x00CC00 : 0x666666; var buyBtnTextColor = relicInfo.level < RELIC_MAX_LEVEL ? 0xFFFFFF : 0xAAAAAA; var buyBtnBg = buyButton.attachAsset('pedia_nav_button_bg', { width: 200, height: 80, color: buyBtnBgColor, anchorX: 0.5, anchorY: 0.5 }); var buyBtnText = new Text2(relicInfo.level < RELIC_MAX_LEVEL ? 'Buy (5)' : 'MAX', { size: 40, fill: buyBtnTextColor }); buyBtnText.anchor.set(0.5, 0.5); buyButton.addChild(buyBtnText); buyButton.interactive = relicInfo.level < RELIC_MAX_LEVEL && playerGold >= RELIC_COST_PER_LEVEL; if (buyButton.interactive) { buyButton.down = function (relicId) { return function () { if (playerGold >= RELIC_COST_PER_LEVEL && playerRelics[relicId].level < RELIC_MAX_LEVEL) { playerGold -= RELIC_COST_PER_LEVEL; playerRelics[relicId].level++; storage.playerGold = playerGold; // Reset ammo relic timer if ammo relic was upgraded if (relicId === 'ammo') { ammoRelicTimer = 0; // Reset timer when upgrading } // Save relics to storage using safe serialization try { var relicsData = {}; relicsData.damage_level = playerRelics.damage.level; relicsData.damage_enabled = playerRelics.damage.enabled; relicsData.slowdown_level = playerRelics.slowdown.level; relicsData.slowdown_enabled = playerRelics.slowdown.enabled; relicsData.green_level = playerRelics.green.level; relicsData.green_enabled = playerRelics.green.enabled; relicsData.dark_level = playerRelics.dark.level; relicsData.dark_enabled = playerRelics.dark.enabled; relicsData.dragon_level = playerRelics.dragon.level; relicsData.dragon_enabled = playerRelics.dragon.enabled; relicsData.reload_level = playerRelics.reload.level; relicsData.reload_enabled = playerRelics.reload.enabled; relicsData.ammo_level = playerRelics.ammo.level; relicsData.ammo_enabled = playerRelics.ammo.enabled; relicsData.swordsman_level = playerRelics.swordsman.level; relicsData.swordsman_enabled = playerRelics.swordsman.enabled; storage.playerRelicsData = relicsData; } catch (e) { console.log("Error saving relics:", e); } goldText.setText('Gold: ' + playerGold); displayRelicItem(currentRelic); // Refresh the current relic display } }; }(relic.id); } buttonsContainer.addChild(buyButton); // On/Off Button var toggleButton = new Container(); toggleButton.x = 200; var toggleBtnBgColor = relicInfo.enabled ? 0x008800 : 0x880000; var toggleBtnBg = toggleButton.attachAsset('pedia_nav_button_bg', { width: 150, height: 80, color: toggleBtnBgColor, anchorX: 0.5, anchorY: 0.5 }); var toggleBtnText = new Text2(relicInfo.enabled ? 'ON' : 'OFF', { size: 40, fill: 0xFFFFFF }); toggleBtnText.anchor.set(0.5, 0.5); toggleButton.addChild(toggleBtnText); toggleButton.interactive = relicInfo.level > 0; // Only interactive if relic is owned if (toggleButton.interactive) { toggleButton.down = function (relicId) { return function () { playerRelics[relicId].enabled = !playerRelics[relicId].enabled; // Reset ammo relic timer if ammo relic was toggled if (relicId === 'ammo') { ammoRelicTimer = 0; // Reset timer when toggling } // Save relics to storage using safe serialization try { var relicsData = {}; relicsData.damage_level = playerRelics.damage.level; relicsData.damage_enabled = playerRelics.damage.enabled; relicsData.slowdown_level = playerRelics.slowdown.level; relicsData.slowdown_enabled = playerRelics.slowdown.enabled; relicsData.green_level = playerRelics.green.level; relicsData.green_enabled = playerRelics.green.enabled; relicsData.dark_level = playerRelics.dark.level; relicsData.dark_enabled = playerRelics.dark.enabled; relicsData.dragon_level = playerRelics.dragon.level; relicsData.dragon_enabled = playerRelics.dragon.enabled; relicsData.reload_level = playerRelics.reload.level; relicsData.reload_enabled = playerRelics.reload.enabled; relicsData.ammo_level = playerRelics.ammo.level; relicsData.ammo_enabled = playerRelics.ammo.enabled; relicsData.swordsman_level = playerRelics.swordsman.level; relicsData.swordsman_enabled = playerRelics.swordsman.enabled; storage.playerRelicsData = relicsData; } catch (e) { console.log("Error saving relics:", e); } displayRelicItem(currentRelic); // Refresh the current relic display }; }(relic.id); } else { toggleBtnBg.tint = 0x666666; // Grey out if not owned } buttonsContainer.addChild(toggleButton); // Status display (current relic / total) var statusText = new Text2('Relic ' + (index + 1) + ' of ' + totalRelics, { size: 35, fill: 0xAAAAAA }); statusText.anchor.set(0.5, 0); statusText.x = 0; statusText.y = 150; relicDisplay.addChild(statusText); // Navigation buttons container var navButtons = new Container(); navButtons.y = 250; relicDisplay.addChild(navButtons); // Previous button var prevButton = createRelicNavButton('โ Previous', -250, function () { currentRelic = (currentRelic - 1 + totalRelics) % totalRelics; displayRelicItem(currentRelic); }); navButtons.addChild(prevButton); // Next button var nextButton = createRelicNavButton('Next โ', 250, function () { currentRelic = (currentRelic + 1) % totalRelics; displayRelicItem(currentRelic); }); navButtons.addChild(nextButton); } function createRelicNavButton(label, xPos, callback) { var button = new Container(); button.x = xPos; var buttonBg = button.attachAsset('pedia_nav_button_bg', { width: 250, height: 80, 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 displayRelicItem(currentRelic); } // --- 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) { var effectiveCooldownDuration = cooldownDuration; // Apply reload relic bonus if enabled if (playerRelics.reload.enabled) { effectiveCooldownDuration = Math.max(30, cooldownDuration - playerRelics.reload.level * 30); // 0.5 seconds faster per level, min 0.5s } cooldownTimer = effectiveCooldownDuration; // Start cooldown (duration affected by Faster Reload upgrade and reload relic) 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 } // Handle ammo relic regeneration if (playerRelics.ammo.enabled && playerRelics.ammo.level > 0) { ammoRelicTimer++; // Give ammo every 5 seconds (300 ticks) if (ammoRelicTimer >= 300) { ammoRelicTimer = 0; // Only regenerate if not at max ammo and not reloading if (arrowsFired > 0 && cooldownTimer <= 0) { // Give ammo based on relic level (1 per level) var ammoToRegenerate = playerRelics.ammo.level; arrowsFired = Math.max(0, arrowsFired - ammoToRegenerate); ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); } } } // 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 damageToApply = cannonball.damage; // Apply dragon slayer damage multiplier if applicable if (cannonball.dragonDamageMultiplier && enemy.tag === 'Dragon') { damageToApply *= cannonball.dragonDamageMultiplier; } var enemyDefeated = enemy.takeDamage(damageToApply, 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. // Calculate gold earned var goldEarned = Math.floor(currentScore / 5); playerGold += goldEarned; storage.playerGold = playerGold; // Save gold to storage // Save relics to storage using safe serialization try { var relicsData = {}; relicsData.damage_level = playerRelics.damage.level; relicsData.damage_enabled = playerRelics.damage.enabled; relicsData.slowdown_level = playerRelics.slowdown.level; relicsData.slowdown_enabled = playerRelics.slowdown.enabled; relicsData.green_level = playerRelics.green.level; relicsData.green_enabled = playerRelics.green.enabled; relicsData.dark_level = playerRelics.dark.level; relicsData.dark_enabled = playerRelics.dark.enabled; relicsData.dragon_level = playerRelics.dragon.level; relicsData.dragon_enabled = playerRelics.dragon.enabled; relicsData.reload_level = playerRelics.reload.level; relicsData.reload_enabled = playerRelics.reload.enabled; relicsData.ammo_level = playerRelics.ammo.level; relicsData.ammo_enabled = playerRelics.ammo.enabled; relicsData.swordsman_level = playerRelics.swordsman.level; relicsData.swordsman_enabled = playerRelics.swordsman.enabled; storage.playerRelicsData = relicsData; } catch (e) { console.log("Error saving relics:", e); } console.log("Game Over! Earned " + goldEarned + " gold. Total gold: " + playerGold); // 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. } // 5.7 Update and check Bouncy Balls for (var bbIdx = bouncyBalls.length - 1; bbIdx >= 0; bbIdx--) { var ball = bouncyBalls[bbIdx]; // LK engine calls ball.update() automatically. // Check for collisions with enemies for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; if (ball.intersects(enemy)) { // Apply damage var enemyDefeated = enemy.takeDamage(ball.damage, ball); if (enemyDefeated) { LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); enemy.destroy(); enemies.splice(j, 1); } // Bouncy ball doesn't get destroyed on hit LK.effects.flashObject(enemy, 0xFF00FF, 300); } } // Check if ball was destroyed by lifetime if (!ball.parent) { bouncyBalls.splice(bbIdx, 1); } } // 5.8 Update and check Bombs for (var bombIdx = bombs.length - 1; bombIdx >= 0; bombIdx--) { var bomb = bombs[bombIdx]; // LK engine calls bomb.update() automatically. // Check if bomb was destroyed after explosion if (!bomb.parent) { bombs.splice(bombIdx, 1); } } // 5.9 Update and check Bombers for (var bomberIdx = bombers.length - 1; bomberIdx >= 0; bomberIdx--) { var bomber = bombers[bomberIdx]; // LK engine calls bomber.update() automatically. } // 5.10 Update and check XBOWs for (var xbowIdx = xbows.length - 1; xbowIdx >= 0; xbowIdx--) { var xbow = xbows[xbowIdx]; // LK engine calls xbow.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; var greenDamageMultiplier = 2; // Base multiplier for darts vs Green enemies // Apply Green Relic damage bonus if enabled and enemy is Green-tagged if (playerRelics.green.enabled && enemy.tag === 'Green') { greenDamageMultiplier += playerRelics.green.level * 0.02; // Additional damage multiplier from relic } if (enemy.tag === 'Green') { damageToDealDart *= greenDamageMultiplier; // Apply calculated multiplier } // 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 possibleTypes.push('dark_spearman'); // Dark Spearman appears at score 55 } if (currentScore >= 69) { possibleTypes.push('jester'); // Jester appears at score 69 } if (currentScore >= 100) { possibleTypes.push('elite_knight'); // Elite Knights start appearing at score 100 } if (currentScore >= 134) { possibleTypes.push('dragon'); // Dragon appears at score 134 } 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'); // Baby Dragon also appears at score 125 (same as war elephant) possibleTypes.push('baby_dragon'); } if (currentScore >= 145) { possibleTypes.push('dark_war_elephant'); // Dark War Elephant appears at score 145 } if (currentScore >= 154) { // Hot Air Balloon appears at score 100 possibleTypes.push('hot_air_balloon'); } if (currentScore >= 27) { possibleTypes.push('flag_bearer'); // Flag Bearer appears at score 27 } // Check if Battle Horn is active - if so, make half of spawns swordsmen, rest random if (battleHornEnabled) { // 50% chance to force swordsman, 50% chance to pick random if (Math.random() < 0.5) { typeToSpawn = 'swordsman'; } else { var chosenTypeIndex = Math.floor(Math.random() * possibleTypes.length); typeToSpawn = possibleTypes[chosenTypeIndex]; } } else { // 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 = 2; 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 if (typeToSpawn === 'dark_spearman') { var baseDarkSpearmanHP = 15; var baseDarkSpearmanSpeedMultiplier = 1.2; var statIncreaseIntervalDarkSpearman = 10; // Gains stats every 10 score points after appearing var statIncreasesDarkSpearman = Math.floor(Math.max(0, currentScore - 55) / statIncreaseIntervalDarkSpearman); enemyHealth = baseDarkSpearmanHP + statIncreasesDarkSpearman * 5; // Gains 5 HP per interval var darkSpearmanSpeedBonus = Math.pow(1 + 0.03, statIncreasesDarkSpearman); // Gains 3% speed per interval enemySpeed *= baseDarkSpearmanSpeedMultiplier * darkSpearmanSpeedBonus; enemyDodgeChance = 0; // Tag 'Black' is set in Enemy constructor } else if (typeToSpawn === 'dragon') { var baseDragonHP = 250; var hpIncreaseIntervalDragon = 15; // Gains HP every 15 score points after appearing var hpIncreasesDragon = Math.floor(Math.max(0, currentScore - 100) / hpIncreaseIntervalDragon); enemyHealth = baseDragonHP + hpIncreasesDragon * 50; // Gains 50 HP per interval enemySpeed *= 0.9; // Dragons are moderately fast enemyDodgeChance = 0.40; // 40% dodge chance // Tag 'Dragon' could be set in Enemy constructor if needed for other mechanics } else if (typeToSpawn === 'flag_bearer') { var baseFlagBearerHP = 5; var hpIncreaseIntervalFlagBearer = 10; // Gains HP every 10 score points after appearing var hpIncreasesFlagBearer = Math.floor(Math.max(0, currentScore - 27) / hpIncreaseIntervalFlagBearer); enemyHealth = baseFlagBearerHP + hpIncreasesFlagBearer * 2; // Gains 2 HP per interval var speedIncreaseIntervalFlagBearer = 10; // Gains speed every 10 score points var speedIncreasesFlagBearer = Math.floor(Math.max(0, currentScore - 27) / speedIncreaseIntervalFlagBearer); var flagBearerSpeedMultiplier = Math.pow(1.03, speedIncreasesFlagBearer); // 3% speed increase per interval enemySpeed *= 0.8 * flagBearerSpeedMultiplier; // Slower base speed but increases over time enemyDodgeChance = 0; } else if (typeToSpawn === 'baby_dragon') { var baseBabyDragonHP = 50; var hpIncreaseIntervalBabyDragon = 9; // Gains HP every 9 score points after appearing var hpIncreasesBabyDragon = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalBabyDragon); enemyHealth = baseBabyDragonHP + hpIncreasesBabyDragon * 10; // Gains 10 HP per interval var speedIncreaseIntervalBabyDragon = 9; // Gains speed every 9 score points var speedIncreasesBabyDragon = Math.floor(Math.max(0, currentScore - 125) / speedIncreaseIntervalBabyDragon); var babyDragonSpeedMultiplier = Math.pow(1.02, speedIncreasesBabyDragon); // 2% speed increase per interval enemySpeed *= 1.1 * babyDragonSpeedMultiplier; // Faster than regular dragon enemyDodgeChance = 0.25; // 25% dodge chance } 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; if (typeToSpawn === 'flag_bearer') { newEnemy = new FlagBearer(); newEnemy.speed = enemySpeed; newEnemy.baseSpeed = enemySpeed; newEnemy.health = enemyHealth; newEnemy.maxHealth = enemyHealth; newEnemy.dodgeChance = enemyDodgeChance; } else if (typeToSpawn === 'baby_dragon') { newEnemy = new BabyDragon(); newEnemy.speed = enemySpeed; newEnemy.baseSpeed = enemySpeed; newEnemy.health = enemyHealth; newEnemy.maxHealth = enemyHealth; newEnemy.dodgeChance = enemyDodgeChance; } else { 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; } }; // --- Swordsman Relic Initial Setup --- if (playerRelics.swordsman.enabled && playerRelics.swordsman.level > 0) { // Spawn permanent swordsman for swordsman relic var permanentSwordsman = new Swordsman(); permanentSwordsman.x = FIRING_POS_X + 100; // Position near player permanentSwordsman.y = FIRING_POS_Y - 50; permanentSwordsman.lifetime = Infinity; // Permanent swordsman permanentSwordsman.attackDamage = 1 + (playerRelics.swordsman.level - 1) * 2; // Base damage +2 per level after first game.addChild(permanentSwordsman); swordsmen.push(permanentSwordsman); } // --- Initial Game Setup --- // Set the initial score text based on the starting score (which is 0). scoreTxt.setText(LK.getScore()); // Set the initial ammo display. if (ammoTxt) { // Ensure ammoTxt is initialized ammoTxt.setText('Ammo: ' + maxArrowsBeforeCooldown); } ; // Play the background music. // Check if we need to show a title screen first if (!isGameStarted) {// Assuming a flag 'isGameStarted' for title screen state // Don't play music immediately if showing title screen } else { LK.playMusic('Gamemusic'); ; } // Placeholder for the title screen container var titleScreen = null; // Flag to track game start state var isGameStarted = false; // Function to show the title screen function showTitleScreen() { isGameStarted = false; // Ensure game is not started // Create the title screen container titleScreen = new Container(); titleScreen.x = GAME_WIDTH / 2; titleScreen.y = GAME_HEIGHT / 2; game.addChild(titleScreen); // Add title text var titleText = new Text2('Defend Your Bastion!', { size: 120, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0.5); titleText.y = -200; titleScreen.addChild(titleText); // Add a simple instruction text var instructionText = new Text2('Tap to Start', { size: 60, fill: 0xCCCCCC }); instructionText.anchor.set(0.5, 0.5); instructionText.y = 100; titleScreen.addChild(instructionText); // Make the title screen interactive to start the game titleScreen.interactive = true; titleScreen.down = function () { startGame(); // Start the game when tapped }; // Pause game logic while title screen is active isUpgradePopupActive = true; // Reusing this flag to pause game logic } // Function to start the game function startGame() { isGameStarted = true; // Set game started flag isUpgradePopupActive = false; // Unpause game logic if (titleScreen) { titleScreen.destroy(); titleScreen = null; } // Reset game state if needed (LK might handle this on game start) // Play game music LK.playMusic('Gamemusic'); } // Initially show the title screen showTitleScreen();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.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
if (closestEnemy.tag !== 'Dragon') {
// Dragons are immune to stuns (slowdown effect)
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 Baby Dragon enemy with rage mode when no other dragons are present.
*/
var BabyDragon = Container.expand(function () {
var self = Container.call(this);
// Create and attach the baby dragon graphic asset
var graphics = self.attachAsset('baby_dragon_asset', {
anchorX: 0.5,
anchorY: 0.5
});
// No base tint - only apply pink tint when raging
self.type = 'baby_dragon';
self.speed = 3; // Base speed (will be set from spawn)
self.health = 50; // Base health (will be set from spawn)
self.maxHealth = 50;
self.dodgeChance = 0.25; // 25% dodge chance (less than adult dragon)
self.tag = 'Dragon'; // Dragon-tagged enemy
self.isRaging = false;
self.rageSpeedMultiplier = 2.0; // Double speed when raging
self.baseSpeed = self.speed;
self.poisonStacks = 0;
self.poisonTimer = 0;
self.poisonDamagePerTick = 0.05;
self.slowTimer = 0;
self.currentSlowAmount = 1.0;
/**
* Update method called each game tick by the LK engine.
* Moves the baby dragon and checks for rage mode.
*/
self.update = function () {
// Check for rage mode (no other dragons on screen)
var otherDragonsExist = false;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy !== self && enemy.tag === 'Dragon') {
otherDragonsExist = true;
break;
}
}
// Apply or remove rage mode
if (!otherDragonsExist && !self.isRaging) {
// Enter rage mode
self.isRaging = true;
self.speed = self.baseSpeed * self.rageSpeedMultiplier;
// Apply pink tint with tween animation
tween(graphics, {
tint: 0xFF69B4
}, {
duration: 300
}); // Pink tint when raging
LK.effects.flashObject(self, 0xFF69B4, 500); // Flash effect with pink
} else if (otherDragonsExist && self.isRaging) {
// Exit rage mode
self.isRaging = false;
self.speed = self.baseSpeed;
// Remove tint with tween animation
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 300
}); // Return to normal color
}
// Normal movement (dragons are immune to slowdown)
self.y += self.speed;
// Apply poison damage
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30;
self.health -= poisonDmg;
}
if (self.health <= 0 && self.parent) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
self.destroy();
return;
}
}
// Visual feedback based on health
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6;
};
/**
* Method called when the baby dragon takes damage.
*/
self.takeDamage = function (damage, source) {
// Check for dodge
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
return false;
}
// Dragons are not immune to arrows/cannonballs
self.health -= damage;
// Apply poison if applicable
if (source && source.isPoison) {
self.poisonStacks++;
self.poisonTimer = 0;
}
// Dragons are immune to slowdown effects
return self.health <= 0;
};
return self;
});
/**
* Represents a Bomb projectile thrown by a Bomber ally.
* @param {number} targetX - The target X coordinate.
* @param {number} targetY - The target Y coordinate.
*/
var Bomb = Container.expand(function (targetX, targetY) {
var self = Container.call(this);
// Create and attach the bomb graphic asset
var graphics = self.attachAsset('bomb_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.targetX = targetX;
self.targetY = targetY;
self.startX = self.x;
self.startY = self.y;
self.damage = 15; // Area damage
self.explosionRadius = 200; // Explosion radius
self.arcHeight = 300; // Height of bomb arc
self.flightTime = 60; // 1 second flight time
self.flightTimer = 0;
self.hasExploded = false;
/**
* Update method called each game tick by the LK engine.
* Handles arc movement and explosion.
*/
self.update = function () {
if (self.hasExploded) {
return;
}
self.flightTimer++;
var progress = self.flightTimer / self.flightTime;
if (progress >= 1) {
// Bomb has reached target, explode
self.explode();
return;
}
// Calculate arc position
var baseX = self.startX + (self.targetX - self.startX) * progress;
var baseY = self.startY + (self.targetY - self.startY) * progress;
// Add arc height (parabola)
var arcOffset = -4 * self.arcHeight * progress * (progress - 1);
self.x = baseX;
self.y = baseY - arcOffset;
// Rotate bomb as it flies
graphics.rotation += 0.2;
};
self.explode = function () {
if (self.hasExploded) {
return;
}
self.hasExploded = true;
// Create explosion visual effect
LK.effects.flashScreen(0xFFAA00, 200); // Orange flash
// Deal area damage to all enemies within radius
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.explosionRadius) {
// Calculate damage falloff (full damage at center, less at edges)
var damageFalloff = 1 - distance / self.explosionRadius * 0.5;
var damageToApply = self.damage * damageFalloff;
var enemyDefeated = enemy.takeDamage(damageToApply, self);
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
enemy.destroy();
enemies.splice(i, 1);
}
// Apply visual effect to hit enemies
LK.effects.flashObject(enemy, 0xFFAA00, 300);
}
}
self.destroy();
};
return self;
});
/**
* Represents a Bomber ally that throws bombs for area damage.
*/
var Bomber = Container.expand(function () {
var self = Container.call(this);
// Create and attach the bomber graphic asset
var graphics = self.attachAsset('bomber_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range like other allies
self.attackInterval = 240; // Attack every 4 seconds
self.attackTimer = 0;
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and throwing bombs.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier);
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0;
// Find a suitable target (not Dragon-tagged)
var bestTarget = null;
var bestScore = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Skip Dragon-tagged enemies
if (enemy.tag === 'Dragon') {
continue;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.attackRange) {
// Prioritize groups of enemies (check nearby enemy count)
var nearbyCount = 0;
for (var j = 0; j < enemies.length; j++) {
if (i !== j) {
var ex = enemies[j].x - enemy.x;
var ey = enemies[j].y - enemy.y;
if (Math.sqrt(ex * ex + ey * ey) <= 200) {
nearbyCount++;
}
}
}
var score = nearbyCount * 10 + (self.attackRange - distance) / 100;
if (score > bestScore) {
bestScore = score;
bestTarget = enemy;
}
}
}
if (bestTarget) {
// Create and throw bomb
var bomb = new Bomb(bestTarget.x, bestTarget.y);
bomb.startX = self.x;
bomb.startY = self.y;
bomb.x = self.x;
bomb.y = self.y;
game.addChild(bomb);
bombs.push(bomb);
}
}
};
return self;
});
/**
* Represents a Bouncy Ball that bounces around the screen dealing damage to enemies.
*/
var BouncyBall = Container.expand(function () {
var self = Container.call(this);
// Create and attach the bouncy ball graphic asset (reusing cannonball for now)
var graphics = self.attachAsset('cannonball', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
graphics.tint = 0xFF00FF; // Magenta tint for bouncy ball
self.speed = 15; // Base speed
self.damage = 20; // Massive damage
self.vx = (Math.random() - 0.5) * self.speed * 2; // Random initial horizontal velocity
self.vy = -Math.abs((Math.random() - 0.5) * self.speed * 2); // Initial upward velocity
self.lifetime = 600; // 10 seconds lifetime (600 ticks)
self.lifetimeTimer = 0;
self.bounceCount = 0; // Track number of bounces
self.maxBounces = 20; // Maximum bounces before disappearing
/**
* Update method called each game tick by the LK engine.
* Handles movement, bouncing, and lifetime.
*/
self.update = function () {
self.lifetimeTimer++;
// Check lifetime
if (self.lifetimeTimer >= self.lifetime || self.bounceCount >= self.maxBounces) {
self.destroy();
return;
}
// Move
self.x += self.vx;
self.y += self.vy;
// Bounce off walls
if (self.x <= graphics.width / 2 || self.x >= GAME_WIDTH - graphics.width / 2) {
self.vx = -self.vx; // Reverse horizontal direction
self.x = Math.max(graphics.width / 2, Math.min(GAME_WIDTH - graphics.width / 2, self.x));
self.bounceCount++;
// Flash on bounce
LK.effects.flashObject(self, 0xFFFFFF, 200);
}
// Bounce off top and bottom
if (self.y <= graphics.height / 2 || self.y >= BASTION_Y - graphics.height / 2) {
self.vy = -self.vy; // Reverse vertical direction
self.y = Math.max(graphics.height / 2, Math.min(BASTION_Y - graphics.height / 2, self.y));
self.bounceCount++;
// Flash on bounce
LK.effects.flashObject(self, 0xFFFFFF, 200);
}
};
return self;
});
/**
* 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)
// Use dragon slayer cannon asset if mode is enabled, otherwise use regular cannon asset
var assetId = self.dragonSlayerMode ? 'dragon_slayer_cannon_asset' : 'cannon_asset';
var graphics = self.attachAsset(assetId, {
// 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
self.dragonSlayerMode = false; // Flag to indicate if this cannon is a dragon slayer cannon
/**
* 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
var targetEnemy = null;
if (self.dragonSlayerMode) {
// In dragon slayer mode, prioritize dragons
var closestDragon = null;
var closestDragonDistance = Infinity;
var strongestNonDragon = null;
var maxNonDragonHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.tag === 'Dragon') {
// Check distance for dragons
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDragonDistance) {
closestDragonDistance = distance;
closestDragon = enemy;
}
} else {
// Track strongest non-dragon as fallback
if (enemy.health > maxNonDragonHealth) {
maxNonDragonHealth = enemy.health;
strongestNonDragon = enemy;
}
}
}
// Prioritize dragon if found, otherwise target strongest non-dragon
targetEnemy = closestDragon || strongestNonDragon;
} else {
// Normal mode: find the strongest enemy (highest health)
var maxHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > maxHealth) {
maxHealth = enemy.health;
targetEnemy = enemy;
}
}
}
// If a target enemy is found within range, attack it
if (targetEnemy) {
// Calculate angle towards the target enemy
var angle = Math.atan2(targetEnemy.x - self.x, -(targetEnemy.y - self.y));
// Create a new cannonball instance, passing the dragon slayer mode status
var newCannonball = new Cannonball(angle);
newCannonball.dragonSlayerMode = self.dragonSlayerMode; // Pass the mode status
// 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
// If dragon slayer mode and targeting a dragon, apply damage bonus
if (self.dragonSlayerMode && targetEnemy.tag === 'Dragon') {
newCannonball.dragonDamageMultiplier = 25; // 25x damage to dragons
// The Cannonball class will handle the asset and tint based on the mode.
}
// 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 graphic asset (rocket if dragon slayer mode, otherwise cannonball)
var assetId = self.dragonSlayerMode ? 'rocket_asset' : 'cannonball';
var graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align graphic with direction
// Apply red tint for rocket if in dragon slayer mode
if (self.dragonSlayerMode) {
graphics.tint = 0xFF0000; // Red tint for dragon slayer rockets
}
self.dragonSlayerMode = false; // Flag to indicate if this is a dragon slayer rocket
self.dragonDamageMultiplier = 1; // Default damage multiplier
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
} else if (type === 'dark_spearman') {
assetId = 'dark_spearman_asset'; // Use specific asset for dark spearman
} else if (type === 'dragon') {
assetId = 'dragon_asset'; // Use specific asset for dragon
} else if (type === 'flag_bearer') {
assetId = 'flag_bearer_asset'; // Use specific asset for flag bearer
} else if (type === 'baby_dragon') {
assetId = 'baby_dragon_asset'; // Use specific baby dragon asset
}
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.type === 'dark_spearman') {
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
var effectiveSpeed = self.speed * self.currentSlowAmount;
// Apply global slowdown relic if enabled and not a Dragon
if (playerRelics.slowdown.enabled && self.tag !== 'Dragon') {
effectiveSpeed *= 1 - playerRelics.slowdown.level * 0.02; // Apply global slow
}
// Apply Green Relic slowdown if enabled and enemy is Green-tagged and not a Dragon
if (playerRelics.green.enabled && self.tag === 'Green' && self.tag !== 'Dragon') {
effectiveSpeed *= 1 - playerRelics.green.level * 0.02; // Apply Green Relic slow
}
self.y += effectiveSpeed; // 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
// Unless Dark Relic is enabled
if (!playerRelics.dark.enabled) {
return false; // Not defeated, projectile has no effect
}
}
var finalDamage = damage;
// Apply damage relic bonus if enabled
if (playerRelics.damage.enabled) {
finalDamage += playerRelics.damage.level;
}
// Apply Green Relic damage bonus if enabled and enemy is Green-tagged
if (playerRelics.green.enabled && self.tag === 'Green') {
finalDamage *= 1 + playerRelics.green.level * 0.2;
}
// Apply Dark Relic damage bonus if enabled and enemy is Black-tagged
if (playerRelics.dark.enabled && self.tag === 'Black' && (source instanceof Arrow || source instanceof Cannonball)) {
finalDamage *= 1 + playerRelics.dark.level * 0.05;
}
self.health -= finalDamage;
// 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) {
if (self.tag !== 'Dragon') {
// Dragons are immune to slowdowns
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 was an Arrow and apply Dragon relic stun
if (playerRelics.dragon.enabled && source instanceof Arrow && self.tag === 'Dragon') {
// Apply stun (reusing slowTimer for duration, 0.0 for 100% slow)
var stunDuration = 0.25 * 60 + playerRelics.dragon.level * 0.05 * 60; // Base 0.25s + 0.05s per level
self.slowTimer = Math.max(self.slowTimer, stunDuration);
self.currentSlowAmount = 0.0; // 100% slow (stun)
}
// Check if the damage source should apply Green Slowdown
if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0 && self.tag !== 'Dragon') {
// Dragons are immune to slowdowns
// Apply Green Slowdown only if the upgrade is active, enemy is Green, not already slowed, and not a Dragon
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, 2, 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
} else if (self.type === 'dark_spearman') {
graphics.scale.set(1.05); // Slightly larger than regular spearman
} else if (self.type === 'dragon') {
graphics.scale.set(1.6); // Dragons are large
} //{11} // Modified original line identifier location
return self; // Return self for potential inheritance
});
/**
* Represents a Flag Bearer enemy that provides a speed boost aura to nearby enemies.
*/
var FlagBearer = Container.expand(function () {
var self = Container.call(this);
// Create and attach the flag bearer graphic asset
var graphics = self.attachAsset('flag_bearer_asset', {
anchorX: 0.5,
anchorY: 0.5
});
// No tint applied to flag bearer
self.type = 'flag_bearer';
self.speed = 3; // Base speed (will be set from spawn)
self.health = 5; // Base health (will be set from spawn)
self.maxHealth = 5;
self.dodgeChance = 0;
self.tag = 'Green'; // Green-tagged enemy
self.auraRadius = 300; // Radius of speed boost aura
self.auraBoost = 1.5; // 50% speed boost to enemies in aura
self.poisonStacks = 0;
self.poisonTimer = 0;
self.poisonDamagePerTick = 0.05;
self.slowTimer = 0;
self.currentSlowAmount = 1.0;
// Create aura visual effect
self.auraCircle = self.attachAsset('light_effect_asset', {
anchorX: 0.5,
anchorY: 0.5,
width: self.auraRadius * 2,
height: self.auraRadius * 2,
tint: 0x00FF00,
alpha: 0.2
});
// Position aura behind the flag bearer
self.setChildIndex(self.auraCircle, 0);
/**
* Update method called each game tick by the LK engine.
* Moves the flag bearer and applies aura effects.
*/
self.update = function () {
// Normal movement
self.y += self.speed * self.currentSlowAmount;
// Apply aura effect to nearby enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy === self) {
continue;
} // Skip self
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.auraRadius) {
// Enemy is within aura range
if (!enemy.hasAuraBoost) {
enemy.hasAuraBoost = true;
enemy.baseSpeed = enemy.speed;
enemy.speed = enemy.baseSpeed * self.auraBoost;
}
} else if (enemy.hasAuraBoost) {
// Enemy left aura range
enemy.hasAuraBoost = false;
enemy.speed = enemy.baseSpeed || enemy.speed;
}
}
// Handle slowdown timer
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer <= 0) {
self.currentSlowAmount = 1.0;
}
}
// Apply poison damage
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30;
self.health -= poisonDmg;
}
if (self.health <= 0 && self.parent) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
self.destroy();
return;
}
}
// Visual feedback based on health
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6;
// Update aura visual effect
if (self.auraCircle) {
self.auraCircle.x = self.x;
self.auraCircle.y = self.y;
// Pulse effect
self.auraPulseTimer = (self.auraPulseTimer || 0) + 0.05;
var pulseScale = 1.0 + Math.sin(self.auraPulseTimer) * 0.1;
self.auraCircle.scale.set(pulseScale);
self.auraCircle.alpha = 0.15 + Math.sin(self.auraPulseTimer * 2) * 0.05;
}
};
/**
* Method called when the flag bearer takes damage.
*/
self.takeDamage = function (damage, source) {
// Check for dodge
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
return false;
}
// Check if source is Arrow or Cannonball (Green enemies are not immune)
self.health -= damage;
// Apply poison if applicable
if (source && source.isPoison) {
self.poisonStacks++;
self.poisonTimer = 0;
}
// Apply slowdown from Magic Ball
if (source && source instanceof MagicBall) {
self.slowTimer = source.slowDuration;
self.currentSlowAmount = source.slowAmount;
}
// Apply Green Slowdown
if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0) {
self.slowTimer = 10 * 60;
self.currentSlowAmount = 0.9;
}
return self.health <= 0;
};
// Clean up aura effects when destroyed
var originalDestroy = self.destroy;
self.destroy = function () {
// Remove aura effects from all enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.hasAuraBoost) {
enemy.hasAuraBoost = false;
enemy.speed = enemy.baseSpeed || enemy.speed;
}
}
// Clean up aura visual
if (self.auraCircle) {
self.auraCircle.destroy();
self.auraCircle = null;
}
originalDestroy.call(self);
};
return self;
});
/**
* 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 finalSwordsmanDamage = self.attackDamage;
// Apply damage relic bonus if enabled
if (playerRelics.damage.enabled) {
finalSwordsmanDamage += playerRelics.damage.level;
}
var enemyDefeated = closestEnemy.takeDamage(finalSwordsmanDamage, 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
});
/**
* Represents an XBOW (crossbow) ally that rapidly shoots arrows.
*/
var XBOW = Container.expand(function () {
var self = Container.call(this);
// Create and attach the XBOW graphic asset
var graphics = self.attachAsset('xbow_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.fireInterval = 20; // Very rapid fire (every 0.33 seconds)
self.fireTimer = 0;
self.targetingMode = 'closest_to_bastion'; // Default targeting mode
self.smartTargetingEnabled = false; // Flag for smart targeting upgrade
self.tintApplied = false; // Track if tint has been applied
/**
* Update method called each game tick by the LK engine.
* Handles rapid firing logic.
*/
self.update = function () {
self.fireTimer++;
var baseInterval = self.smartTargetingEnabled ? 15.36 : self.fireInterval; // 0.256 seconds when smart targeting enabled
var effectiveFireInterval = Math.max(10, baseInterval * allyAttackSpeedMultiplier);
if (self.fireTimer >= effectiveFireInterval) {
self.fireTimer = 0;
// Find target based on targeting mode
var target = null;
if (self.smartTargetingEnabled) {
// Apply green tint when smart targeting is enabled
if (!self.tintApplied) {
graphics.tint = 0x00FF00; // Green tint
self.tintApplied = true;
}
if (self.targetingMode === 'closest_to_bastion') {
// Find enemy closest to bastion
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var distanceToBastion = BASTION_Y - enemy.y;
if (distanceToBastion > 0 && distanceToBastion < closestDistance) {
closestDistance = distanceToBastion;
target = enemy;
}
}
} else if (self.targetingMode === 'strongest') {
// Find enemy with highest health
var maxHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > maxHealth) {
maxHealth = enemy.health;
target = enemy;
}
}
}
}
// Default behavior: shoot at center of screen
var angle;
if (target) {
// Calculate angle towards target
angle = Math.atan2(target.x - self.x, -(target.y - self.y));
} else {
// No target or smart targeting disabled - shoot straight up towards center
angle = 0; // 0 radians = straight up
}
// Create arrow
var newArrow = new Arrow(angle);
newArrow.x = self.x;
newArrow.y = self.y;
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// XBOW arrows are faster
newArrow.speed *= 1.5;
newArrow.vx = Math.sin(angle) * newArrow.speed;
newArrow.vy = -Math.cos(angle) * newArrow.speed;
// Disable aimbot for XBOW arrows
newArrow.targetEnemy = null;
game.addChild(newArrow);
arrows.push(newArrow);
// Apply multi-shot if enabled
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 = self.x;
newArrow2.y = self.y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
// XBOW arrows are faster
newArrow2.speed *= 1.5;
newArrow2.vx = Math.sin(angle2) * newArrow2.speed;
newArrow2.vy = -Math.cos(angle2) * newArrow2.speed;
// Disable aimbot for XBOW arrows
newArrow2.targetEnemy = null;
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 = self.x;
newArrow3.y = self.y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
// XBOW arrows are faster
newArrow3.speed *= 1.5;
newArrow3.vx = Math.sin(angle3) * newArrow3.speed;
newArrow3.vy = -Math.cos(angle3) * newArrow3.speed;
// Disable aimbot for XBOW arrows
newArrow3.targetEnemy = null;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
}
};
return self;
});
/****
* 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 baseMaxArrowsBeforeCooldown = 15; // Base max arrows before reload (Quiver upgrade)
var maxArrowsBeforeCooldown = baseMaxArrowsBeforeCooldown; // Max arrows before reload
var ammoRelicTimer = 0; // Timer for ammo relic regeneration
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
var bouncyBalls = []; // Array to hold BouncyBall instances
var bombs = []; // Array to hold Bomb instances
var bombers = []; // Array to hold Bomber instances
var xbows = []; // Array to hold XBOW instances
var xbowSmartTargetingEnabled = false; // Flag for XBOW Smart Targeting upgrade
var battleHornEnabled = false; // Flag for Battle Horn upgrade
var dragonSlayerCannonsModeEnabled = false; // Flag for Dragon Slayer Cannons upgrade
// --- Relics and Gold System ---
var playerGold = storage.playerGold || 0; // Load gold from storage, default to 0
// Load relics from storage using flat format
var storedRelicsData = storage.playerRelicsData || {};
var playerRelics = {
damage: {
level: storedRelicsData.damage_level || 0,
enabled: storedRelicsData.damage_enabled || false
},
slowdown: {
level: storedRelicsData.slowdown_level || 0,
enabled: storedRelicsData.slowdown_enabled || false
},
green: {
level: storedRelicsData.green_level || 0,
enabled: storedRelicsData.green_enabled || false
},
dark: {
level: storedRelicsData.dark_level || 0,
enabled: storedRelicsData.dark_enabled || false
},
dragon: {
level: storedRelicsData.dragon_level || 0,
enabled: storedRelicsData.dragon_enabled || false
},
reload: {
level: storedRelicsData.reload_level || 0,
enabled: storedRelicsData.reload_enabled || false
},
ammo: {
level: storedRelicsData.ammo_level || 0,
enabled: storedRelicsData.ammo_enabled || false
},
swordsman: {
level: storedRelicsData.swordsman_level || 0,
enabled: storedRelicsData.swordsman_enabled || false
}
};
var RELIC_MAX_LEVEL = 10;
var RELIC_COST_PER_LEVEL = 5; // 5 gold per level
// --- 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
// Apply dragon slayer mode if the global flag is enabled
newCannon.dragonSlayerMode = dragonSlayerCannonsModeEnabled;
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
}
// Helper function to add a Bouncy Ball
function addBouncyBall() {
console.log("Bouncy Ball upgrade chosen!");
var newBall = new BouncyBall();
// Start from center of screen
newBall.x = GAME_WIDTH / 2;
newBall.y = GAME_HEIGHT / 2;
game.addChild(newBall);
bouncyBalls.push(newBall);
}
// Helper function to add a Bomber
function addBomber() {
console.log("Bomber upgrade chosen!");
var newBomber = new Bomber();
// Position near bastion line
newBomber.x = GAME_WIDTH / 2 - 400;
newBomber.y = BASTION_Y - 100;
game.addChild(newBomber);
bombers.push(newBomber);
}
// Helper function to add an XBOW
function addXBOW() {
console.log("XBOW upgrade chosen!");
var newXBOW = new XBOW();
// Position at the center of the bastion
newXBOW.x = GAME_WIDTH / 2;
newXBOW.y = BASTION_Y - 100;
game.addChild(newXBOW);
xbows.push(newXBOW);
}
// Helper function to enable XBOW Smart Targeting
function enableXBOWSmartTargeting() {
console.log("XBOW Smart Targeting upgrade chosen!");
xbowSmartTargetingEnabled = true;
// Enable smart targeting for all existing XBOWs
for (var i = 0; i < xbows.length; i++) {
xbows[i].smartTargetingEnabled = true;
// Randomly assign targeting mode for variety
xbows[i].targetingMode = Math.random() < 0.5 ? 'closest_to_bastion' : 'strongest';
}
}
// 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
}, {
id: 'bouncy_ball',
name: 'Bouncy Ball',
description: 'Bounces around dealing massive damage to enemies',
apply: addBouncyBall
}, {
id: 'bomber',
name: 'Bomber',
description: 'Throws bombs that explode for area damage',
apply: addBomber
}, {
id: 'xbow',
name: 'XBOW',
description: 'A big bow that rapidly shoots arrows',
apply: addXBOW
}, {
id: 'xbow_smart_targeting',
name: 'XBOW Smart Targeting',
description: 'XBOWs can target closest to bastion or strongest enemy',
apply: enableXBOWSmartTargeting
}, {
id: 'dragon_slayer_cannons',
name: 'Dragon Slayer Cannons',
description: 'Cannons become rocket launchers that deal 25x damage to dragons and prioritize them',
apply: function apply() {
// Enable dragon slayer mode for all existing cannons
for (var i = 0; i < cannons.length; i++) {
cannons[i].dragonSlayerMode = true;
}
// Set global flag so all future cannons will have dragon slayer mode
dragonSlayerCannonsModeEnabled = true;
}
}, {
id: 'battle_horn',
name: 'Battle Horn',
description: 'More enemies spawn, but they are all swordsmen: more score for more upgrades!',
apply: function apply() {
// Reduce spawn interval for more enemies
enemySpawnInterval = Math.max(15, enemySpawnInterval * 0.5); // 50% faster spawning, min 0.25 seconds
// Set global flag to force swordsman spawns
battleHornEnabled = true;
}
}];
// Function to create and show the upgrade selection popup
function showUpgradePopup() {
isUpgradePopupActive = true;
// Simple pause: Game logic checks isUpgradePopupActive flag in game.update
// Switch to upgrade theme music with fade effect
if (currentMusicPlaying !== 'UpgradeTheme' && !musicFadeInProgress) {
musicFadeInProgress = true;
// Fade out current music
LK.playMusic('Gamemusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// After fade out completes, start upgrade theme with fade in
LK.setTimeout(function () {
LK.playMusic('UpgradeTheme', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
currentMusicPlaying = 'UpgradeTheme';
musicFadeInProgress = false;
}, 850);
}
// --- Create Popup UI ---
upgradePopup = new Container();
upgradePopup.x = GAME_WIDTH / 2;
upgradePopup.y = GAME_HEIGHT / 2;
game.addChild(upgradePopup); // Add to game layer for positioning relative to center
// Add a semi-transparent background overlay
var bg = upgradePopup.attachAsset('bastionLine', {
// Reusing an asset for shape
width: 1200,
height: 800,
color: 0x000000,
// Black background
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Add title text
var title = new Text2('Choose an Upgrade!', {
size: 80,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -300; // Position relative to popup center
upgradePopup.addChild(title);
// --- Select 3 Random Unique Upgrades ---
var availableToOffer = allUpgrades.slice(); // Copy available upgrades
var choices = [];
var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices
// Make dragon slayer cannons more common past score 100
var currentScore = LK.getScore();
if (currentScore > 100 && !dragonSlayerCannonsModeEnabled) {
// Add dragon slayer cannons upgrade multiple times to increase chances
var dragonSlayerUpgrade = null;
for (var ds = 0; ds < availableToOffer.length; ds++) {
if (availableToOffer[ds].id === 'dragon_slayer_cannons') {
dragonSlayerUpgrade = availableToOffer[ds];
break;
}
}
if (dragonSlayerUpgrade) {
// Add it 2 more times to the pool (3x total chance)
availableToOffer.push(dragonSlayerUpgrade);
availableToOffer.push(dragonSlayerUpgrade);
}
}
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)
}
// --- Upgradepedia ---
var upgradepediaButton;
var upgradepediaPopup = null;
var upgradeListContainer = null;
function showUpgradepedia() {
isUpgradePopupActive = true; // Pause the game while upgradepedia is open
upgradepediaPopup = new Container();
upgradepediaPopup.x = GAME_WIDTH / 2;
upgradepediaPopup.y = GAME_HEIGHT / 2;
game.addChild(upgradepediaPopup);
// Add a semi-transparent background overlay
var bg = upgradepediaPopup.attachAsset('pedia_screen_bg', {
width: 1600,
height: 2000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
});
// Add title text
var title = new Text2('Upgradepedia', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -900;
upgradepediaPopup.addChild(title);
// Add close button
var closeButton = createUpgradepediaCloseButton();
closeButton.x = 750;
closeButton.y = -900;
upgradepediaPopup.addChild(closeButton);
// Container for upgrade details
upgradeListContainer = new Container();
upgradeListContainer.y = -200;
upgradepediaPopup.addChild(upgradeListContainer);
displayUpgradeStats();
}
function hideUpgradepedia() {
if (upgradepediaPopup) {
upgradepediaPopup.destroy();
upgradepediaPopup = null;
}
isUpgradePopupActive = false; // Unpause the game
}
function createUpgradepediaCloseButton() {
var buttonContainer = new Container();
var buttonBg = buttonContainer.attachAsset('pedia_button_bg', {
width: 150,
height: 80,
color: 0xCC0000,
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 () {
hideUpgradepedia();
};
return buttonContainer;
}
function displayUpgradeStats() {
// Clear any existing content
while (upgradeListContainer.children.length > 0) {
upgradeListContainer.removeChildAt(0);
}
// Current page tracking
var currentUpgrade = 0;
var totalUpgrades = allUpgrades.length;
// Map upgrade IDs to visual assets and additional info
var upgradeAssets = {
'faster_reload': {
asset: 'Reload',
scale: 0.4
},
'piercing_shots': {
asset: 'arrow',
scale: 0.6
},
'quiver': {
asset: 'arrow',
scale: 0.8,
tint: 0xFFD700
},
'heat_tipped': {
asset: 'arrow',
scale: 0.6,
tint: 0xFF4500
},
'archer_ally': {
asset: 'Archer',
scale: 0.8
},
'sabotage': {
asset: 'thief',
scale: 0.8
},
'swordsman': {
asset: 'swordsmanAlly',
scale: 0.8
},
'more_shots': {
asset: 'arrow',
scale: 0.5,
count: 3
},
'poison_shots': {
asset: 'arrow',
scale: 0.6,
tint: 0x00FF00
},
'cannon': {
asset: 'cannon_asset',
scale: 0.8
},
'ally_atk_speed': {
asset: 'viking_axe',
scale: 0.6
},
'wizard_tower': {
asset: 'wizard_tower_asset',
scale: 0.8
},
'aimbot': {
asset: 'arrow',
scale: 0.6,
special: 'seeking'
},
'green_killer': {
asset: 'arrow',
scale: 0.6,
tint: 0xFF0000
},
'refined_projectiles': {
asset: 'cannonball',
scale: 0.6,
tint: 0xFFD700
},
'viking_ally': {
asset: 'viking_ally',
scale: 0.8
},
'green_slowdown': {
asset: 'magic_ball_asset',
scale: 0.6,
tint: 0x00FF00
},
'dart_shooter': {
asset: 'dart_asset',
scale: 0.8
},
'angel_of_light': {
asset: 'angel_of_light_asset',
scale: 0.8
},
'bouncy_ball': {
asset: 'cannonball',
scale: 1.0,
tint: 0xFF00FF
},
'bomber': {
asset: 'bomber_asset',
scale: 0.8
},
'xbow': {
asset: 'xbow_asset',
scale: 0.8
},
'xbow_smart_targeting': {
asset: 'xbow_asset',
scale: 0.8,
tint: 0x00FFFF
},
'dragon_slayer_cannons': {
asset: 'cannon_asset',
scale: 0.8,
tint: 0xFF0000
},
'battle_horn': {
asset: 'flag_bearer_asset',
scale: 0.8,
tint: 0xFFD700
}
};
function displayUpgradeDetails(index) {
// Clear existing content
while (upgradeListContainer.children.length > 0) {
upgradeListContainer.removeChildAt(0);
}
var upgradeInfo = allUpgrades[index];
var assetInfo = upgradeAssets[upgradeInfo.id] || {
asset: 'arrow',
scale: 0.6
};
// Upgrade display container
var upgradeDisplay = new Container();
upgradeDisplay.y = 0;
upgradeListContainer.addChild(upgradeDisplay);
// Add upgrade graphic
if (assetInfo.count) {
// Special case for multiple projectiles
for (var i = 0; i < assetInfo.count; i++) {
var graphic = upgradeDisplay.attachAsset(assetInfo.asset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: assetInfo.scale,
scaleY: assetInfo.scale,
x: -400 + (i - 1) * 100,
y: 50
});
if (assetInfo.tint) {
graphic.tint = assetInfo.tint;
}
}
} else {
var upgradeGraphic = upgradeDisplay.attachAsset(assetInfo.asset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: assetInfo.scale,
scaleY: assetInfo.scale,
x: -400,
y: 50
});
if (assetInfo.tint) {
upgradeGraphic.tint = assetInfo.tint;
}
// Special visual effects
if (assetInfo.special === 'seeking') {
// Add rotation animation hint for seeking arrows
upgradeGraphic.rotation = Math.PI / 6;
}
}
// Add upgrade name
var nameText = new Text2(upgradeInfo.name, {
size: 70,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0);
nameText.x = 0;
nameText.y = -400;
upgradeDisplay.addChild(nameText);
// Add description
var descText = new Text2(upgradeInfo.description, {
size: 45,
fill: 0xCCCCCC,
wordWrap: true,
wordWrapWidth: 1200
});
descText.anchor.set(0.5, 0);
descText.x = 0;
descText.y = -300;
upgradeDisplay.addChild(descText);
// Add detailed ability description
var abilityText = getDetailedAbility(upgradeInfo.id);
var abilityDisplay = new Text2('Ability: ' + abilityText, {
size: 40,
fill: 0x88FF88,
wordWrap: true,
wordWrapWidth: 1200
});
abilityDisplay.anchor.set(0.5, 0);
abilityDisplay.x = 0;
abilityDisplay.y = -150;
upgradeDisplay.addChild(abilityDisplay);
// Status display
var statusText = new Text2('Upgrade ' + (index + 1) + ' of ' + totalUpgrades, {
size: 35,
fill: 0xAAAAAA
});
statusText.anchor.set(0.5, 0);
statusText.x = 0;
statusText.y = 250;
upgradeDisplay.addChild(statusText);
// Navigation buttons
var navButtons = new Container();
navButtons.y = 350;
upgradeDisplay.addChild(navButtons);
// Previous button
var prevButton = createUpgradeNavButton('โ Previous', -250, function () {
currentUpgrade = (currentUpgrade - 1 + totalUpgrades) % totalUpgrades;
displayUpgradeDetails(currentUpgrade);
});
navButtons.addChild(prevButton);
// Next button
var nextButton = createUpgradeNavButton('Next โ', 250, function () {
currentUpgrade = (currentUpgrade + 1) % totalUpgrades;
displayUpgradeDetails(currentUpgrade);
});
navButtons.addChild(nextButton);
}
function getDetailedAbility(upgradeId) {
var abilities = {
'faster_reload': 'Reduces the cooldown time between reloads by 20%. Stacks multiplicatively with minimum 1 second cooldown.',
'piercing_shots': 'Each arrow can pierce through one additional enemy. Stacks to pierce multiple enemies.',
'quiver': 'Increases maximum ammo capacity by 5 arrows before needing to reload.',
'heat_tipped': 'Increases arrow damage by 1. Affects all arrow-based attacks including allies.',
'archer_ally': 'Spawns an allied archer that independently targets and shoots at enemies every 3 seconds.',
'sabotage': 'Reduces all enemy movement speed by 10%. Stacks multiplicatively with minimum 50% speed.',
'swordsman': 'Creates a tower that spawns temporary swordsmen allies every 3 seconds. Swordsmen chase and attack nearby enemies.',
'more_shots': 'Fire 3 arrows at once in a spread pattern. Works with all arrow upgrades.',
'poison_shots': 'Arrows apply poison stacks that deal damage over time. Each stack deals damage every 0.5 seconds.',
'cannon': 'Deploys a cannon that targets the strongest enemy and fires high-damage cannonballs every 5 seconds.',
'ally_atk_speed': 'All allies attack 20% faster. Stacks multiplicatively.',
'wizard_tower': 'Builds a tower that shoots magic balls causing slowdown effect on hit. Dragons are immune to slowdown.',
'aimbot': 'Arrows and magic balls automatically seek the nearest enemy. Updates target if current target is destroyed.',
'green_killer': 'Deal 50% extra damage to all Green-tagged enemies (Wizards, Spearmen, War Elephants, Shamans).',
'refined_projectiles': 'Non-arrow projectiles (cannonballs, magic balls, axes) gain +5 damage and +5 speed.',
'viking_ally': 'Summons a viking warrior that throws piercing axes. Each axe can pierce through 3 enemies.',
'green_slowdown': 'Projectiles slow Green-tagged enemies by 10% for 10 seconds. Effect does not stack.',
'dart_shooter': 'Deploys a rapid-fire dart shooter that deals double damage to Green enemies.',
'angel_of_light': 'Summons an angel that stuns enemies for 5 seconds and deals triple damage to Black-tagged enemies. Dragons cannot be stunned.',
'bouncy_ball': 'Releases a bouncing projectile that ricochets off walls dealing massive damage. Lasts 10 seconds or 20 bounces.',
'bomber': 'Deploys a bomber that throws area damage bombs. Cannot directly target Dragons but explosion can damage them.',
'xbow': 'Installs a large crossbow that rapidly fires arrows every 0.33 seconds. Arrows travel 50% faster.',
'xbow_smart_targeting': 'XBOWs can now target either the enemy closest to bastion or the strongest enemy. All other allies have this by default.',
'dragon_slayer_cannons': 'Transforms all current and future cannons into rocket launchers. Rockets deal 25x damage to Dragon-type enemies and prioritize targeting them over all other enemy types.',
'battle_horn': 'Doubles enemy spawn rate but forces all spawned enemies to be basic swordsmen. Great for farming score since swordsmen are easy to defeat!'
};
return abilities[upgradeId] || 'Special ability that enhances your defenses.';
}
function createUpgradeNavButton(label, xPos, callback) {
var button = new Container();
button.x = xPos;
var buttonBg = button.attachAsset('pedia_nav_button_bg', {
width: 250,
height: 80,
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
displayUpgradeDetails(currentUpgrade);
}
// Create upgradepedia button
upgradepediaButton = new Container();
upgradepediaButton.x = 200; // Position on the left side
upgradepediaButton.y = GAME_HEIGHT - 200;
// Add button background
var upgradeBtnBg = upgradepediaButton.attachAsset('pedia_button_bg', {
width: 300,
height: 100,
anchorX: 0.5,
anchorY: 0.5
});
// Add button text
var upgradeBtnText = new Text2('Upgradepedia', {
size: 40,
fill: 0xFFFFFF
});
upgradeBtnText.anchor.set(0.5, 0.5);
upgradepediaButton.addChild(upgradeBtnText);
// Make button interactive
upgradepediaButton.interactive = true;
upgradepediaButton.down = function () {
showUpgradepedia();
};
// Add the button to the game scene
game.addChild(upgradepediaButton);
// --- Relic Shop Button ---
var relicShopButton = new Container();
relicShopButton.x = GAME_WIDTH - 200; // Position on the right side
relicShopButton.y = GAME_HEIGHT - 320; // Position above Enemypedia button
var relicBtnBg = relicShopButton.attachAsset('pedia_button_bg', {
width: 300,
height: 100,
anchorX: 0.5,
anchorY: 0.5
});
var relicBtnText = new Text2('Relic Shop', {
size: 40,
fill: 0xFFFFFF
});
relicBtnText.anchor.set(0.5, 0.5);
relicShopButton.addChild(relicBtnText);
relicShopButton.interactive = true;
relicShopButton.down = function () {
showRelicShop();
};
game.addChild(relicShopButton);
// --- 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 that has very low HP.',
baseHealth: 1,
baseSpeed: 3,
dodgeChance: 0
}, {
type: 'knight',
assetId: 'knight',
description: 'A threat for the first few waves cause of its high health, but is slow.',
baseHealth: 5,
baseSpeed: 3 * 0.9,
dodgeChance: 0
}, {
type: 'thief',
assetId: 'thief',
description: 'Fast, low health unit with a dodge chance; likes gravy.',
baseHealth: 1,
baseSpeed: 3 * 1.2,
dodgeChance: 0.1
}, {
type: 'boss',
assetId: 'boss',
description: 'a boss that spawns randomly when youre in trobule.',
baseHealth: 5,
// Base health before scaling
baseSpeed: 3 * 0.7,
dodgeChance: 0
}, {
type: 'shield',
assetId: 'shield_enemy',
description: 'Slow unit that has high HP',
baseHealth: 20,
baseSpeed: 3 * 0.5,
dodgeChance: 0
}, {
type: 'wizard',
assetId: 'wizard_enemy',
description: 'kinda tricky cause it teleports and has moderate speed.',
baseHealth: 2,
baseSpeed: 3 * 0.7,
dodgeChance: 0
}, {
type: 'spearman',
assetId: 'spearman',
description: 'Standard, annoying green unit that spawns from Elephants most of the time.',
baseHealth: 2,
baseSpeed: 3 * 1.0,
dodgeChance: 0
}, {
type: 'war_elephant',
assetId: 'war_elephant',
description: 'Extremely high health elephant, spawns spearmen on death',
baseHealth: 500,
baseSpeed: 3 * 0.3,
dodgeChance: 0
}, {
type: 'elite_knight',
assetId: 'elite_knight_asset',
description: 'High health knight variant that is loyal to the king',
baseHealth: 45,
baseSpeed: 3 * 0.85,
dodgeChance: 0
}, {
type: 'elite_shield',
assetId: 'elite_shield_asset',
description: 'Even higher health shield variant that is loyal to the king',
baseHealth: 200,
baseSpeed: 3 * 0.4,
dodgeChance: 0
}, {
type: 'shaman',
assetId: 'shaman_enemy',
description: 'Reduces your ammo when he feels like it',
baseHealth: 2,
baseSpeed: 3 * 0.8,
dodgeChance: 0
}, {
type: 'hot_air_balloon',
assetId: 'hot_air_balloon_asset',
description: 'Very low health. Spawns random non-green enemies on death.',
baseHealth: 1,
baseSpeed: 3 * 0.2,
dodgeChance: 0
}, {
type: 'dark_bowman',
assetId: 'dark_bowman_asset',
description: 'Immune to arrows and cannonballs. Worst enemy of blue archers',
baseHealth: 5,
baseSpeed: 3 * 1.1,
dodgeChance: 0
}, {
type: 'jester',
assetId: 'jester_asset',
description: 'High chance to dodge or reflect non-cannonball projectiles...YOU WILL LAUGH AT ALL OF HIS TRICKS',
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. variant of 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: 'dark_spearman_asset',
description: 'A faster and more resistant spearman variant that came straight from rome. Immune to arrows and cannonballs',
baseHealth: 15,
// Base health
baseSpeed: 3 * 1.2,
// Faster than spearman
dodgeChance: 0,
tag: 'Black' // Immune to arrows and cannonballs
}, {
type: 'dragon',
assetId: 'dragon_asset',
description: 'A fearsome dragon with great HP and a high chance to dodge attacks. Cant be slowed down',
baseHealth: 250,
// Great HP
baseSpeed: 3 * 0.9,
// Moderately fast
dodgeChance: 0.40,
// High dodge chance (40%)
tag: 'Dragon' // Special tag if needed for other mechanics, for now dodge is key
}, {
type: 'flag_bearer',
assetId: 'flag_bearer_asset',
description: 'Green-tagged enemy with an aura that makes nearby enemies move 50% faster',
baseHealth: 5,
baseSpeed: 3 * 0.8,
dodgeChance: 0,
tag: 'Green'
}, {
type: 'baby_dragon',
assetId: 'baby_dragon_asset',
description: 'Small dragon with less health but enters rage mode (double speed) when no other dragons are present',
baseHealth: 50,
baseSpeed: 3 * 1.1,
dodgeChance: 0.25,
tag: 'Dragon'
}];
// 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_spearman') {
unlockScore = 55;
} // Dark Spearman unlock score
else if (enemyInfo.type === 'dark_bowman') {
unlockScore = 55;
} // Note: Same as Dark Spearman, might need adjustment if only one is desired at 55. Keeping as per request.
else if (enemyInfo.type === 'jester') {
unlockScore = 69;
} else if (enemyInfo.type === 'dragon') {
unlockScore = 134;
} // Dragon unlock score
else if (enemyInfo.type === 'flag_bearer') {
unlockScore = 27;
} // Flag Bearer unlock score
else if (enemyInfo.type === 'baby_dragon') {
unlockScore = 125;
} // Baby Dragon unlock score (same as war elephant)
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 === 'dark_war_elephant') {
unlockScore = 145;
} // Dark war elephant starts appearing at score 145
else if (enemyInfo.type === 'hot_air_balloon') {
unlockScore = 154;
}
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);
// --- Relic Shop ---
var relicShopPopup = null;
var relicListContainer = null;
var goldText; // Text display for gold
function showRelicShop() {
isUpgradePopupActive = true; // Pause the game
relicShopPopup = new Container();
relicShopPopup.x = GAME_WIDTH / 2;
relicShopPopup.y = GAME_HEIGHT / 2;
game.addChild(relicShopPopup);
var bg = relicShopPopup.attachAsset('pedia_screen_bg', {
width: 1600,
height: 2000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
});
var title = new Text2('Relic Shop', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -900;
relicShopPopup.addChild(title);
var closeButton = createRelicShopCloseButton();
closeButton.x = 750;
closeButton.y = -900;
relicShopPopup.addChild(closeButton);
// Gold display
goldText = new Text2('Gold: ' + playerGold, {
size: 50,
fill: 0xFFD700
});
goldText.anchor.set(0.5, 0.5);
goldText.y = -800;
relicShopPopup.addChild(goldText);
relicListContainer = new Container();
relicListContainer.y = -200;
relicShopPopup.addChild(relicListContainer);
displayRelicDetails();
}
function hideRelicShop() {
if (relicShopPopup) {
relicShopPopup.destroy();
relicShopPopup = null;
}
isUpgradePopupActive = false; // Unpause the game
}
function createRelicShopCloseButton() {
var buttonContainer = new Container();
var buttonBg = buttonContainer.attachAsset('pedia_button_bg', {
width: 150,
height: 80,
color: 0xCC0000,
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 () {
hideRelicShop();
};
return buttonContainer;
}
function displayRelicDetails() {
while (relicListContainer.children.length > 0) {
relicListContainer.removeChildAt(0);
}
var relicsData = [{
id: 'damage',
name: 'Damage Relic',
description: 'Increase projectile, swordsman, and poison damage.',
effect: '+1 damage per level'
}, {
id: 'slowdown',
name: 'Slowdown Relic',
description: 'All enemies are slower.',
effect: '+2% slowdown per level'
}, {
id: 'green',
name: 'Green Relic',
description: 'Green enemies are slower and take more damage.',
effect: '+2% slow & +2% damage per level'
}, {
id: 'dark',
name: 'Dark Relic',
description: 'Dark enemies can be hit by arrows/cannonballs and take more damage from them.',
effect: '+5% damage per level'
}, {
id: 'dragon',
name: 'Dragon Relic',
description: 'Projectiles stun dragons.',
effect: '+0.05 seconds stun per level'
}, {
id: 'reload',
name: 'Reload Relic',
description: 'Reload faster! Essential for rapid-fire gameplay.',
effect: '-0.5 seconds reload time per level'
}, {
id: 'ammo',
name: 'Ammo Relic',
description: 'Regenerate ammo over time without reloading.',
effect: '+1 ammo regenerated every 5 seconds per level'
}, {
id: 'swordsman',
name: 'Swordsman Relic',
description: 'Start each battle with a permanent swordsman ally.',
effect: 'Unlocks permanent swordsman, +2 damage per upgrade'
}];
// Current page tracking
var currentRelic = 0;
var totalRelics = relicsData.length;
// Create relic details display
function displayRelicItem(index) {
// Clear any existing content in the list container
while (relicListContainer.children.length > 0) {
relicListContainer.removeChildAt(0);
}
var relic = relicsData[index];
var relicInfo = playerRelics[relic.id];
// Relic display container
var relicDisplay = new Container();
relicDisplay.y = 0;
relicListContainer.addChild(relicDisplay);
// Background
var bg = relicDisplay.attachAsset('pedia_button_bg', {
width: 1400,
height: 400,
color: 0x333355,
anchorX: 0.5,
anchorY: 0.5
});
// Relic Name
var nameText = new Text2(relic.name + ' (Level ' + relicInfo.level + '/' + RELIC_MAX_LEVEL + ')', {
size: 70,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0);
nameText.x = 0;
nameText.y = -300;
relicDisplay.addChild(nameText);
// Relic Description
var descText = new Text2(relic.description, {
size: 45,
fill: 0xCCCCCC,
wordWrap: true,
wordWrapWidth: 1200
});
descText.anchor.set(0.5, 0);
descText.x = 0;
descText.y = -200;
relicDisplay.addChild(descText);
// Relic Effect
var effectText = new Text2('Effect: ' + relic.effect, {
size: 40,
fill: 0x88FF88,
wordWrap: true,
wordWrapWidth: 1200
});
effectText.anchor.set(0.5, 0);
effectText.x = 0;
effectText.y = -100;
relicDisplay.addChild(effectText);
// Buttons container
var buttonsContainer = new Container();
buttonsContainer.y = 0;
relicDisplay.addChild(buttonsContainer);
// Buy/Upgrade Button
var buyButton = new Container();
buyButton.x = -200;
var buyBtnBgColor = relicInfo.level < RELIC_MAX_LEVEL ? 0x00CC00 : 0x666666;
var buyBtnTextColor = relicInfo.level < RELIC_MAX_LEVEL ? 0xFFFFFF : 0xAAAAAA;
var buyBtnBg = buyButton.attachAsset('pedia_nav_button_bg', {
width: 200,
height: 80,
color: buyBtnBgColor,
anchorX: 0.5,
anchorY: 0.5
});
var buyBtnText = new Text2(relicInfo.level < RELIC_MAX_LEVEL ? 'Buy (5)' : 'MAX', {
size: 40,
fill: buyBtnTextColor
});
buyBtnText.anchor.set(0.5, 0.5);
buyButton.addChild(buyBtnText);
buyButton.interactive = relicInfo.level < RELIC_MAX_LEVEL && playerGold >= RELIC_COST_PER_LEVEL;
if (buyButton.interactive) {
buyButton.down = function (relicId) {
return function () {
if (playerGold >= RELIC_COST_PER_LEVEL && playerRelics[relicId].level < RELIC_MAX_LEVEL) {
playerGold -= RELIC_COST_PER_LEVEL;
playerRelics[relicId].level++;
storage.playerGold = playerGold;
// Reset ammo relic timer if ammo relic was upgraded
if (relicId === 'ammo') {
ammoRelicTimer = 0; // Reset timer when upgrading
}
// Save relics to storage using safe serialization
try {
var relicsData = {};
relicsData.damage_level = playerRelics.damage.level;
relicsData.damage_enabled = playerRelics.damage.enabled;
relicsData.slowdown_level = playerRelics.slowdown.level;
relicsData.slowdown_enabled = playerRelics.slowdown.enabled;
relicsData.green_level = playerRelics.green.level;
relicsData.green_enabled = playerRelics.green.enabled;
relicsData.dark_level = playerRelics.dark.level;
relicsData.dark_enabled = playerRelics.dark.enabled;
relicsData.dragon_level = playerRelics.dragon.level;
relicsData.dragon_enabled = playerRelics.dragon.enabled;
relicsData.reload_level = playerRelics.reload.level;
relicsData.reload_enabled = playerRelics.reload.enabled;
relicsData.ammo_level = playerRelics.ammo.level;
relicsData.ammo_enabled = playerRelics.ammo.enabled;
relicsData.swordsman_level = playerRelics.swordsman.level;
relicsData.swordsman_enabled = playerRelics.swordsman.enabled;
storage.playerRelicsData = relicsData;
} catch (e) {
console.log("Error saving relics:", e);
}
goldText.setText('Gold: ' + playerGold);
displayRelicItem(currentRelic); // Refresh the current relic display
}
};
}(relic.id);
}
buttonsContainer.addChild(buyButton);
// On/Off Button
var toggleButton = new Container();
toggleButton.x = 200;
var toggleBtnBgColor = relicInfo.enabled ? 0x008800 : 0x880000;
var toggleBtnBg = toggleButton.attachAsset('pedia_nav_button_bg', {
width: 150,
height: 80,
color: toggleBtnBgColor,
anchorX: 0.5,
anchorY: 0.5
});
var toggleBtnText = new Text2(relicInfo.enabled ? 'ON' : 'OFF', {
size: 40,
fill: 0xFFFFFF
});
toggleBtnText.anchor.set(0.5, 0.5);
toggleButton.addChild(toggleBtnText);
toggleButton.interactive = relicInfo.level > 0; // Only interactive if relic is owned
if (toggleButton.interactive) {
toggleButton.down = function (relicId) {
return function () {
playerRelics[relicId].enabled = !playerRelics[relicId].enabled;
// Reset ammo relic timer if ammo relic was toggled
if (relicId === 'ammo') {
ammoRelicTimer = 0; // Reset timer when toggling
}
// Save relics to storage using safe serialization
try {
var relicsData = {};
relicsData.damage_level = playerRelics.damage.level;
relicsData.damage_enabled = playerRelics.damage.enabled;
relicsData.slowdown_level = playerRelics.slowdown.level;
relicsData.slowdown_enabled = playerRelics.slowdown.enabled;
relicsData.green_level = playerRelics.green.level;
relicsData.green_enabled = playerRelics.green.enabled;
relicsData.dark_level = playerRelics.dark.level;
relicsData.dark_enabled = playerRelics.dark.enabled;
relicsData.dragon_level = playerRelics.dragon.level;
relicsData.dragon_enabled = playerRelics.dragon.enabled;
relicsData.reload_level = playerRelics.reload.level;
relicsData.reload_enabled = playerRelics.reload.enabled;
relicsData.ammo_level = playerRelics.ammo.level;
relicsData.ammo_enabled = playerRelics.ammo.enabled;
relicsData.swordsman_level = playerRelics.swordsman.level;
relicsData.swordsman_enabled = playerRelics.swordsman.enabled;
storage.playerRelicsData = relicsData;
} catch (e) {
console.log("Error saving relics:", e);
}
displayRelicItem(currentRelic); // Refresh the current relic display
};
}(relic.id);
} else {
toggleBtnBg.tint = 0x666666; // Grey out if not owned
}
buttonsContainer.addChild(toggleButton);
// Status display (current relic / total)
var statusText = new Text2('Relic ' + (index + 1) + ' of ' + totalRelics, {
size: 35,
fill: 0xAAAAAA
});
statusText.anchor.set(0.5, 0);
statusText.x = 0;
statusText.y = 150;
relicDisplay.addChild(statusText);
// Navigation buttons container
var navButtons = new Container();
navButtons.y = 250;
relicDisplay.addChild(navButtons);
// Previous button
var prevButton = createRelicNavButton('โ Previous', -250, function () {
currentRelic = (currentRelic - 1 + totalRelics) % totalRelics;
displayRelicItem(currentRelic);
});
navButtons.addChild(prevButton);
// Next button
var nextButton = createRelicNavButton('Next โ', 250, function () {
currentRelic = (currentRelic + 1) % totalRelics;
displayRelicItem(currentRelic);
});
navButtons.addChild(nextButton);
}
function createRelicNavButton(label, xPos, callback) {
var button = new Container();
button.x = xPos;
var buttonBg = button.attachAsset('pedia_nav_button_bg', {
width: 250,
height: 80,
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
displayRelicItem(currentRelic);
}
// --- 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) {
var effectiveCooldownDuration = cooldownDuration;
// Apply reload relic bonus if enabled
if (playerRelics.reload.enabled) {
effectiveCooldownDuration = Math.max(30, cooldownDuration - playerRelics.reload.level * 30); // 0.5 seconds faster per level, min 0.5s
}
cooldownTimer = effectiveCooldownDuration; // Start cooldown (duration affected by Faster Reload upgrade and reload relic)
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
}
// Handle ammo relic regeneration
if (playerRelics.ammo.enabled && playerRelics.ammo.level > 0) {
ammoRelicTimer++;
// Give ammo every 5 seconds (300 ticks)
if (ammoRelicTimer >= 300) {
ammoRelicTimer = 0;
// Only regenerate if not at max ammo and not reloading
if (arrowsFired > 0 && cooldownTimer <= 0) {
// Give ammo based on relic level (1 per level)
var ammoToRegenerate = playerRelics.ammo.level;
arrowsFired = Math.max(0, arrowsFired - ammoToRegenerate);
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
}
}
}
// 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 damageToApply = cannonball.damage;
// Apply dragon slayer damage multiplier if applicable
if (cannonball.dragonDamageMultiplier && enemy.tag === 'Dragon') {
damageToApply *= cannonball.dragonDamageMultiplier;
}
var enemyDefeated = enemy.takeDamage(damageToApply, 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.
// Calculate gold earned
var goldEarned = Math.floor(currentScore / 5);
playerGold += goldEarned;
storage.playerGold = playerGold; // Save gold to storage
// Save relics to storage using safe serialization
try {
var relicsData = {};
relicsData.damage_level = playerRelics.damage.level;
relicsData.damage_enabled = playerRelics.damage.enabled;
relicsData.slowdown_level = playerRelics.slowdown.level;
relicsData.slowdown_enabled = playerRelics.slowdown.enabled;
relicsData.green_level = playerRelics.green.level;
relicsData.green_enabled = playerRelics.green.enabled;
relicsData.dark_level = playerRelics.dark.level;
relicsData.dark_enabled = playerRelics.dark.enabled;
relicsData.dragon_level = playerRelics.dragon.level;
relicsData.dragon_enabled = playerRelics.dragon.enabled;
relicsData.reload_level = playerRelics.reload.level;
relicsData.reload_enabled = playerRelics.reload.enabled;
relicsData.ammo_level = playerRelics.ammo.level;
relicsData.ammo_enabled = playerRelics.ammo.enabled;
relicsData.swordsman_level = playerRelics.swordsman.level;
relicsData.swordsman_enabled = playerRelics.swordsman.enabled;
storage.playerRelicsData = relicsData;
} catch (e) {
console.log("Error saving relics:", e);
}
console.log("Game Over! Earned " + goldEarned + " gold. Total gold: " + playerGold);
// 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.
}
// 5.7 Update and check Bouncy Balls
for (var bbIdx = bouncyBalls.length - 1; bbIdx >= 0; bbIdx--) {
var ball = bouncyBalls[bbIdx];
// LK engine calls ball.update() automatically.
// Check for collisions with enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (ball.intersects(enemy)) {
// Apply damage
var enemyDefeated = enemy.takeDamage(ball.damage, ball);
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
enemy.destroy();
enemies.splice(j, 1);
}
// Bouncy ball doesn't get destroyed on hit
LK.effects.flashObject(enemy, 0xFF00FF, 300);
}
}
// Check if ball was destroyed by lifetime
if (!ball.parent) {
bouncyBalls.splice(bbIdx, 1);
}
}
// 5.8 Update and check Bombs
for (var bombIdx = bombs.length - 1; bombIdx >= 0; bombIdx--) {
var bomb = bombs[bombIdx];
// LK engine calls bomb.update() automatically.
// Check if bomb was destroyed after explosion
if (!bomb.parent) {
bombs.splice(bombIdx, 1);
}
}
// 5.9 Update and check Bombers
for (var bomberIdx = bombers.length - 1; bomberIdx >= 0; bomberIdx--) {
var bomber = bombers[bomberIdx];
// LK engine calls bomber.update() automatically.
}
// 5.10 Update and check XBOWs
for (var xbowIdx = xbows.length - 1; xbowIdx >= 0; xbowIdx--) {
var xbow = xbows[xbowIdx];
// LK engine calls xbow.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;
var greenDamageMultiplier = 2; // Base multiplier for darts vs Green enemies
// Apply Green Relic damage bonus if enabled and enemy is Green-tagged
if (playerRelics.green.enabled && enemy.tag === 'Green') {
greenDamageMultiplier += playerRelics.green.level * 0.02; // Additional damage multiplier from relic
}
if (enemy.tag === 'Green') {
damageToDealDart *= greenDamageMultiplier; // Apply calculated multiplier
}
// 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
possibleTypes.push('dark_spearman'); // Dark Spearman appears at score 55
}
if (currentScore >= 69) {
possibleTypes.push('jester'); // Jester appears at score 69
}
if (currentScore >= 100) {
possibleTypes.push('elite_knight'); // Elite Knights start appearing at score 100
}
if (currentScore >= 134) {
possibleTypes.push('dragon'); // Dragon appears at score 134
}
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');
// Baby Dragon also appears at score 125 (same as war elephant)
possibleTypes.push('baby_dragon');
}
if (currentScore >= 145) {
possibleTypes.push('dark_war_elephant'); // Dark War Elephant appears at score 145
}
if (currentScore >= 154) {
// Hot Air Balloon appears at score 100
possibleTypes.push('hot_air_balloon');
}
if (currentScore >= 27) {
possibleTypes.push('flag_bearer'); // Flag Bearer appears at score 27
}
// Check if Battle Horn is active - if so, make half of spawns swordsmen, rest random
if (battleHornEnabled) {
// 50% chance to force swordsman, 50% chance to pick random
if (Math.random() < 0.5) {
typeToSpawn = 'swordsman';
} else {
var chosenTypeIndex = Math.floor(Math.random() * possibleTypes.length);
typeToSpawn = possibleTypes[chosenTypeIndex];
}
} else {
// 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 = 2;
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 if (typeToSpawn === 'dark_spearman') {
var baseDarkSpearmanHP = 15;
var baseDarkSpearmanSpeedMultiplier = 1.2;
var statIncreaseIntervalDarkSpearman = 10; // Gains stats every 10 score points after appearing
var statIncreasesDarkSpearman = Math.floor(Math.max(0, currentScore - 55) / statIncreaseIntervalDarkSpearman);
enemyHealth = baseDarkSpearmanHP + statIncreasesDarkSpearman * 5; // Gains 5 HP per interval
var darkSpearmanSpeedBonus = Math.pow(1 + 0.03, statIncreasesDarkSpearman); // Gains 3% speed per interval
enemySpeed *= baseDarkSpearmanSpeedMultiplier * darkSpearmanSpeedBonus;
enemyDodgeChance = 0;
// Tag 'Black' is set in Enemy constructor
} else if (typeToSpawn === 'dragon') {
var baseDragonHP = 250;
var hpIncreaseIntervalDragon = 15; // Gains HP every 15 score points after appearing
var hpIncreasesDragon = Math.floor(Math.max(0, currentScore - 100) / hpIncreaseIntervalDragon);
enemyHealth = baseDragonHP + hpIncreasesDragon * 50; // Gains 50 HP per interval
enemySpeed *= 0.9; // Dragons are moderately fast
enemyDodgeChance = 0.40; // 40% dodge chance
// Tag 'Dragon' could be set in Enemy constructor if needed for other mechanics
} else if (typeToSpawn === 'flag_bearer') {
var baseFlagBearerHP = 5;
var hpIncreaseIntervalFlagBearer = 10; // Gains HP every 10 score points after appearing
var hpIncreasesFlagBearer = Math.floor(Math.max(0, currentScore - 27) / hpIncreaseIntervalFlagBearer);
enemyHealth = baseFlagBearerHP + hpIncreasesFlagBearer * 2; // Gains 2 HP per interval
var speedIncreaseIntervalFlagBearer = 10; // Gains speed every 10 score points
var speedIncreasesFlagBearer = Math.floor(Math.max(0, currentScore - 27) / speedIncreaseIntervalFlagBearer);
var flagBearerSpeedMultiplier = Math.pow(1.03, speedIncreasesFlagBearer); // 3% speed increase per interval
enemySpeed *= 0.8 * flagBearerSpeedMultiplier; // Slower base speed but increases over time
enemyDodgeChance = 0;
} else if (typeToSpawn === 'baby_dragon') {
var baseBabyDragonHP = 50;
var hpIncreaseIntervalBabyDragon = 9; // Gains HP every 9 score points after appearing
var hpIncreasesBabyDragon = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalBabyDragon);
enemyHealth = baseBabyDragonHP + hpIncreasesBabyDragon * 10; // Gains 10 HP per interval
var speedIncreaseIntervalBabyDragon = 9; // Gains speed every 9 score points
var speedIncreasesBabyDragon = Math.floor(Math.max(0, currentScore - 125) / speedIncreaseIntervalBabyDragon);
var babyDragonSpeedMultiplier = Math.pow(1.02, speedIncreasesBabyDragon); // 2% speed increase per interval
enemySpeed *= 1.1 * babyDragonSpeedMultiplier; // Faster than regular dragon
enemyDodgeChance = 0.25; // 25% dodge chance
} 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;
if (typeToSpawn === 'flag_bearer') {
newEnemy = new FlagBearer();
newEnemy.speed = enemySpeed;
newEnemy.baseSpeed = enemySpeed;
newEnemy.health = enemyHealth;
newEnemy.maxHealth = enemyHealth;
newEnemy.dodgeChance = enemyDodgeChance;
} else if (typeToSpawn === 'baby_dragon') {
newEnemy = new BabyDragon();
newEnemy.speed = enemySpeed;
newEnemy.baseSpeed = enemySpeed;
newEnemy.health = enemyHealth;
newEnemy.maxHealth = enemyHealth;
newEnemy.dodgeChance = enemyDodgeChance;
} else {
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;
}
};
// --- Swordsman Relic Initial Setup ---
if (playerRelics.swordsman.enabled && playerRelics.swordsman.level > 0) {
// Spawn permanent swordsman for swordsman relic
var permanentSwordsman = new Swordsman();
permanentSwordsman.x = FIRING_POS_X + 100; // Position near player
permanentSwordsman.y = FIRING_POS_Y - 50;
permanentSwordsman.lifetime = Infinity; // Permanent swordsman
permanentSwordsman.attackDamage = 1 + (playerRelics.swordsman.level - 1) * 2; // Base damage +2 per level after first
game.addChild(permanentSwordsman);
swordsmen.push(permanentSwordsman);
}
// --- Initial Game Setup ---
// Set the initial score text based on the starting score (which is 0).
scoreTxt.setText(LK.getScore());
// Set the initial ammo display.
if (ammoTxt) {
// Ensure ammoTxt is initialized
ammoTxt.setText('Ammo: ' + maxArrowsBeforeCooldown);
}
;
// Play the background music.
// Check if we need to show a title screen first
if (!isGameStarted) {// Assuming a flag 'isGameStarted' for title screen state
// Don't play music immediately if showing title screen
} else {
LK.playMusic('Gamemusic');
;
}
// Placeholder for the title screen container
var titleScreen = null;
// Flag to track game start state
var isGameStarted = false;
// Function to show the title screen
function showTitleScreen() {
isGameStarted = false; // Ensure game is not started
// Create the title screen container
titleScreen = new Container();
titleScreen.x = GAME_WIDTH / 2;
titleScreen.y = GAME_HEIGHT / 2;
game.addChild(titleScreen);
// Add title text
var titleText = new Text2('Defend Your Bastion!', {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -200;
titleScreen.addChild(titleText);
// Add a simple instruction text
var instructionText = new Text2('Tap to Start', {
size: 60,
fill: 0xCCCCCC
});
instructionText.anchor.set(0.5, 0.5);
instructionText.y = 100;
titleScreen.addChild(instructionText);
// Make the title screen interactive to start the game
titleScreen.interactive = true;
titleScreen.down = function () {
startGame(); // Start the game when tapped
};
// Pause game logic while title screen is active
isUpgradePopupActive = true; // Reusing this flag to pause game logic
}
// Function to start the game
function startGame() {
isGameStarted = true; // Set game started flag
isUpgradePopupActive = false; // Unpause game logic
if (titleScreen) {
titleScreen.destroy();
titleScreen = null;
}
// Reset game state if needed (LK might handle this on game start)
// Play game music
LK.playMusic('Gamemusic');
}
// Initially show the title screen
showTitleScreen();
Arrow. In-Game asset. 2d. High contrast. No shadows. Topdown
Red stickman with a sword. In-Game asset. 2d. High contrast. No shadows. Topdown
Blue stickman with a bow. In-Game asset. 2d. High contrast. No shadows
Red stickman with an iron helmet, iron sword and iron shield. In-Game asset. 2d. High contrast. No shadows. No eyes
Red stickman with a knife and a thief's bandana. In-Game asset. 2d. High contrast. No shadows. No eyes
Purple stickman with a crown. In-Game asset. 2d. High contrast. No shadows
Blue stickman with a sword. In-Game asset. 2d. High contrast. No shadows
Tower. In-Game asset. 2d. High contrast. No shadows
Red stickman with a big wooden shield full of spikes. In-Game asset. 2d. High contrast. No shadows
Green stickman with a blue wizard hat and a staff; no eyes. In-Game asset. 2d. High contrast. No shadows
Red stickman with a golden knight helmet, gold sword and gold shield and gold boots. In-Game asset. 2d. High contrast. No shadows
Cannon. In-Game asset. 2d. High contrast. No shadows
Cannonball. In-Game asset. 2d. High contrast. No shadows
Yellow stickman with an azur wizard hat and staff on a tower. In-Game asset. 2d. High contrast. No shadows
Magic ice ball. In-Game asset. 2d. High contrast. No shadows
Green stickman with a Spartan helmet, Spartan shield and Spartan spear. In-Game asset. 2d. High contrast. No shadows
Green war elephant. In-Game asset. 2d. High contrast. No shadows
Yellow viking stickman holding an axe and is about to throw it. In-Game asset. 2d. High contrast. No shadows
Hatchet. In-Game asset. 2d. High contrast. No shadows
A red stickman with a big golden shield and golden armor. In-Game asset. 2d. High contrast. No shadows
Gray stickman with it's face covered with a black hood equipped with a bow In-Game asset. 2d. High contrast. No shadows, no eyes
Hot air balloon full of red stickmen. In-Game asset. 2d. High contrast. No shadows
Black war elephant with red eyes. In-Game asset. 2d. High contrast. No shadows
Red stickman that is a jester that is on a blue ball and is juggling. In-Game asset. 2d. High contrast. No shadows
Green stickman with a tribal mask and a stick to shoot darts. In-Game asset. 2d. High contrast. No shadows
White female stickman that is an angel with golden armor and a heavenly sword. In-Game asset. 2d. High contrast. No shadows
Wooden dart In-Game asset. 2d. High contrast. No shadows. Topdown
Orb of light. In-Game asset. 2d. High contrast. No shadows
Purple dragon with a red stickman riding it. In-Game asset. 2d. High contrast. No shadows
Add a Roman shield
Bomb. In-Game asset. 2d. High contrast. No shadows
Blue stickman with safety goggles, yellow safety helmet and a bomb. In-Game asset. 2d. High contrast. No shadows
Giant crossbow. In-Game asset. 2d. High contrast. No shadows. Topdown
Green stickman with a British soldier's hat and with a red flag with 2 swords. In-Game asset. 2d. High contrast. No shadows
Baby dragon from clash of clans. In-Game asset. 2d. High contrast. No shadows
Missile launcher BTD6. In-Game asset. 2d. High contrast. No shadows
Red Missile. In-Game asset. 2d. High contrast. No shadows