User prompt
make dragon slayer cannons use dragon slayer cannonballs asset and their respective projectiles use the rocket asset
User prompt
Make it so that the dragon slayer cannons upgrade turns all current AND future cannons be dragon slayer cannons and it becomes slightly more common past 100 score
User prompt
Add asset for dragon slayer cannons and rocket
User prompt
Remove battle horn and all it's code
User prompt
The Battle Horn makes all enemies just swordsmen...make it so that Battle Horn only makes HALF of the newly spawned enemies swordsmen
User prompt
The Battle Horn makes all enemies just swordsmen...make it so that Battle Horn only makes HALF of the newly spawned enemies swordsmen
User prompt
The Battle Horn makes all enemies just swordsmen...make it so that Battle Horn only makes HALF of the newly spawned enemies swordsmen
User prompt
This is UNACCEPTABLE! CHANGE THE NEW UPGRADES' UPGRADEPEDIA EXPLAINATIONS NOW!
User prompt
Add new upgrades: Dragon slayer cannons (turn all current and future cannons into rocket launchers that deal 25 times more damage to dragon type enemies and prioritize them over any enemy), Battle Horn (more enemies spawn, but don't worry! They are all swordsmen: this means more score for more upgrades!)
User prompt
Add new upgrades: Dragon slayer cannons (turn all current and future cannons into rocket launchers that deal 25 times more damage to dragon type enemies and prioritize them over any enemy), Battle Horn (more enemies spawn, but don't worry! They are all swordsmen: this means more score for more upgrades!), Dark SHRDR (All non arrow and cannonball projectiles deal 50% more damage to dark enemies)
User prompt
Make baby dragon have spawn introduction at the same score number as regular war elephant
User prompt
Make a tier list for each enemy for how tough it is (Don't include boss nor swordsman)
User prompt
Rework the ammo relic so that it gives you 1 ammo every 5 seconds and 1 more ammo given per upgrade
User prompt
Rwork the ammo relic so that it gives you 1 ammo every 5 seconds
User prompt
Ammo doesn't change when having the ammo relic
User prompt
Add more relics: Reload relic (you can reload faster while equipping this relic! 0.5 seconds less time per reaload per upgrade), Ammo relic (need more ammo? This relic is perfect for you then! +1 ammo per upgrade), Swordsman relic (start with a permanent swordsman for this battle! +2 damage of the swordsman per upgrade) ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Now the relic shop has the same sorting as Enemypedia and Upgradepedia: You have to go to a relic 1 by 1 using buttons
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'ammo')' in or related to this line: 'var ammoRelicBonus = playerRelics.ammo.enabled ? playerRelics.ammo.level : 0;' Line Number: 1719
User prompt
Add more relics: Reload relic (you can reload faster while equipping this relic! 0.5 seconds faster per upgrade), Ammo relic (need more ammo? This relic is perfect for you then! +1 ammo per upgrade), Swordsman relic (start with a permanent swordsman for this battle! +2 damage of the swordsman per upgrade except the first one cause it unlocks the swordsman itself) ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'storage.playerRelics = {' Line Number: 3222
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'storage.playerRelics = relicsToStore;' Line Number: 3221
User prompt
Please fix the bug: 'Error: Invalid value. Only literals or 1-level deep objects/arrays containing literals are allowed.' in or related to this line: 'storage.playerRelics = playerRelics; // Save relics to storage' Line Number: 3534
User prompt
Add new feature: Relics! After a battle, score gets converted into gold. Every 5 score equals 1 gold. You can use that gold to buy relics that give you permanent buffs for all your games; you can buy them, upgrade them up to 10 times and you can turn them off and on anytime you want. (Relics: Damage relic: +1 damage to all projectiles, swordsmen and poison [+1 damage for each upgrade]; Slowdown relic: All enemies are 10% slower [+2% slower for each upgrade]; Green Relic: All green enemies are 10% slower and take 20% more damage [+2% for each stat per upgrade]; Dark relic: All dark enemies can be hit by arrows and take 10% more damage from them [+5% more damage taken per upgrade]; Dragon relic: all projectiles stun dragons for 0.25 seconds [+0.05 seconds per upgrade]) ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Change enemypedia baby dragon to match the 75 score unlocks
User prompt
Change enemypedia baby dragon to match the 75 score unlocks
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ /** * Represents an Angel of Light ally that stuns enemies and deals high damage to dark enemies. */ var AngelOfLight = Container.expand(function () { var self = Container.call(this); // Create and attach the angel graphic asset (reusing viking_ally for simplicity) // Will need a dedicated asset for the Angel of Light if available. var graphics = self.attachAsset('angel_of_light_asset', { // Placeholder asset anchorX: 0.5, anchorY: 0.5 }); graphics.tint = 0xFFFF00; // Yellow tint for Angel of Light self.attackRange = 600; // Moderate attack range self.attackDamage = 5; // Base damage self.attackInterval = 480; // Attack every 8 seconds (480 ticks) self.attackTimer = 0; // Timer for attacks self.stunDuration = 5 * 60; // Stun duration in ticks (5 seconds) /** * Update method called each game tick by the LK engine. * Handles finding targets and using light ability. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the closest enemy within range var closestEnemy = null; var closestDistance = self.attackRange; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, use light ability if (closestEnemy) { // Apply stun and damage to the target 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) // For now, using a placeholder asset, will need a 'cannon_asset' var graphics = self.attachAsset('cannon_asset', { // Use actual cannon asset anchorX: 0.5, anchorY: 0.5 }); self.attackRange = 800; // Long attack range self.attackDamage = 10; // High damage self.attackInterval = 300; // Attack every 5 seconds (300 ticks) self.attackTimer = 0; // Timer for attacks self.rotation = 0; // For aiming visual if implemented later /** * Update method called each game tick by the LK engine. * Handles attacking the strongest enemy. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the strongest enemy (highest health) var strongestEnemy = null; var maxHealth = -1; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.health > maxHealth) { maxHealth = enemy.health; strongestEnemy = enemy; } } // If a strongest enemy is found within range, attack it if (strongestEnemy) { // Calculate angle towards the strongest enemy var angle = Math.atan2(strongestEnemy.x - self.x, -(strongestEnemy.y - self.y)); // Create a new cannonball instance var newCannonball = new Cannonball(angle); // Position the cannonball at the cannon's location newCannonball.x = self.x; newCannonball.y = self.y; newCannonball.lastY = newCannonball.y; // Initialize lastY for state tracking newCannonball.lastX = newCannonball.x; // Initialize lastX for state tracking // Add the cannonball to the game scene and the tracking array. game.addChild(newCannonball); cannonballs.push(newCannonball); // Add to the new cannonballs array // Optional: Add a visual/sound effect for cannon shot here later } } }; return self; // Return self for potential inheritance }); /** * Represents a Cannonball fired by a Cannon. * @param {number} angle - The angle in radians at which the cannonball is fired. */ var Cannonball = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the cannonball graphic asset var graphics = self.attachAsset('cannonball', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align cannonball graphic with direction self.speed = 20; // Base speed of the cannonball self.damage = 10; // Base damage dealt by this cannonball // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.speed += 5; self.damage += 5; } self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) /** * Update method called each game tick by the LK engine. * Moves the cannonball based on its velocity. */ self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents a Dart projectile fired by a Dart Ally. * @param {number} angle - The angle in radians at which the dart is fired. */ var Dart = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the dart graphic asset (reusing arrow for simplicity) // Will need a dedicated asset for the Dart if available. var graphics = self.attachAsset('dart_asset', { // Placeholder asset anchorX: 0.5, anchorY: 0.5 }); graphics.tint = 0x9933CC; // Purple tint for darts graphics.rotation = angle + Math.PI / 2; // Align dart graphic with direction self.speed = 40; // Fast speed self.damage = 0.5; // Base damage dealt by this dart // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.speed += 5; self.damage += 5; } self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) /** * Update method called each game tick by the LK engine. * Moves the dart based on its velocity. */ self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents a Dart Ally that shoots fast darts. */ var DartAlly = Container.expand(function () { var self = Container.call(this); // Create and attach the dart ally graphic asset (reusing archer asset for simplicity) // Will need a dedicated asset for the Dart Ally if available. var graphics = self.attachAsset('disguised_swordsman', { // Placeholder asset anchorX: 0.5, anchorY: 0.5 }); self.attackRange = Infinity; // Infinite attack range self.attackDamage = 0.5; // Low damage per dart self.attackInterval = 30; // Attack every 0.5 seconds (30 ticks) - very fast self.attackTimer = 0; // Timer for attacks /** * Update method called each game tick by the LK engine. * Handles finding targets and shooting darts. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(10, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.16s if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the closest enemy to shoot at var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate distance to the enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, shoot a dart if (closestEnemy) { // Calculate angle towards the closest enemy var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); // Create a new dart instance var newDart = new Dart(angle); newDart.x = self.x; newDart.y = self.y; newDart.lastY = newDart.y; newDart.lastX = newDart.x; // Add the dart to the game scene and the tracking array. game.addChild(newDart); darts.push(newDart); // Add to the new darts array // Optional: Add throwing sound effect here later } } }; return self; // Return self for potential inheritance }); /** * Represents an Enemy attacker moving towards the bastion. * @param {string} type - The type of enemy ('swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight'). * @param {number} speed - The final calculated speed of the enemy. * @param {number} health - The initial and maximum health of the enemy. * @param {number} dodgeChance - The chance (0 to 1) for the enemy to dodge an attack. */ var Enemy = Container.expand(function (type, speed, health, dodgeChance) { var self = Container.call(this); // Set asset based on type var assetId = 'enemy'; // Default swordsman asset if (type === 'knight') { assetId = 'knight'; } else if (type === 'elite_knight') { assetId = 'elite_knight_asset'; // Use specific asset for elite knight } else if (type === 'thief') { assetId = 'thief'; } else if (type === 'boss') { assetId = 'boss'; } else if (type === 'shield') { assetId = 'shield_enemy'; // Use specific asset for shield } else if (type === 'wizard') { assetId = 'wizard_enemy'; // Use specific asset for wizard } else if (type === 'spearman') { assetId = 'spearman'; // Use specific asset for spearman } else if (type === 'war_elephant') { assetId = 'war_elephant'; // Use specific asset for war elephant } else if (type === 'elite_shield') { assetId = 'elite_shield_asset'; // Use specific asset for elite shield } else if (type === 'shaman') { assetId = 'shaman_enemy'; // Use specific asset for shaman } else if (type === 'hot_air_balloon') { assetId = 'hot_air_balloon_asset'; // Use specific asset for hot air balloon } else if (type === 'dark_bowman') { assetId = 'dark_bowman_asset'; // Use specific asset for dark bowman } else if (type === 'jester') { assetId = 'jester_asset'; // Use specific asset for jester } else if (type === 'dark_war_elephant') { assetId = 'dark_war_elephant_asset'; // Use specific asset for dark war elephant } 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 self.y += self.speed * self.currentSlowAmount; // Apply slowdown to movement } // Decrease slowdown timer and reset slow amount if timer runs out if (self.slowTimer > 0) { self.slowTimer--; if (self.slowTimer <= 0) { self.currentSlowAmount = 1.0; // Reset speed multiplier when slowdown ends // Optional: Remove visual feedback for slowdown here later } } // Apply poison damage if stacks exist if (self.poisonStacks > 0) { self.poisonTimer++; if (self.poisonTimer >= 30) { // Apply poison damage every 0.5 seconds (30 ticks) self.poisonTimer = 0; var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30; // Damage per 0.5s self.health -= poisonDmg; // Optional: Add visual feedback for poison damage here later } // Check if health dropped to zero or below due to poison if (self.health <= 0 && self.parent) { // Ensure enemy is still in the game // Enemy defeated by poison LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy self.destroy(); // The main game update loop needs to handle removing the enemy from the `enemies` array return; // Stop updating this destroyed instance } } // Visual feedback based on health - alpha fade (applies to all types) var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0; graphics.alpha = 0.4 + healthRatio * 0.6; // Fade from 1.0 down to 0.4 // Shaman ability: reduce player ammo periodically if (self.type === 'shaman') { if (self.shamanTimer === undefined) { self.shamanTimer = 0; } self.shamanTimer += 1; var shamanAbilityInterval = 300; // 5 seconds * 60 FPS if (self.shamanTimer >= shamanAbilityInterval) { self.shamanTimer = 0; // Reduce player ammo, but not below 0 arrowsFired = Math.min(maxArrowsBeforeCooldown, arrowsFired + 1); // Make it require one more shot for reload ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Optional: Add a visual/sound effect for shaman ability } } }; //{N} // Adjusted line identifier /** * Method called when the enemy is hit by an arrow. * Handles dodging and health reduction. * @param {number} damage - The amount of damage the arrow deals. * @returns {boolean} - True if the enemy is defeated, false otherwise. */ self.takeDamage = function (damage) { var source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; // Add source parameter, default to null // Check for dodge first (only if dodgeChance is positive) if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) { // Optional: Add a visual effect for dodge here later // console.log(self.type + " dodged!"); return false; // Dodged, so not defeated by this hit } // Check for reflection (only for Jester and non-cannonball projectiles) if (self.type === 'jester' && self.reflectChance > 0 && Math.random() < self.reflectChance && !(source instanceof Cannonball)) { // Optional: Add a visual effect for reflection here later // console.log(self.type + " reflected!"); // Projectile is destroyed, enemy takes no damage if (source && source.destroy) { source.destroy(); // Need to remove from the source array in the main game loop } return false; // Reflected, so not defeated by this hit } // Check if enemy is Black-tagged and source is an Arrow or Cannonball if (self.tag === 'Black' && (source instanceof Arrow || source instanceof Cannonball)) { // Black-tagged enemies are immune to arrow and cannonball projectiles return false; // Not defeated, projectile has no effect } self.health -= damage; // Check if the damage source was a poison arrow and apply poison stacks if (source && source.isPoison) { // Use the 'source' parameter self.poisonStacks++; // Increase poison stacks self.poisonTimer = 0; // Reset timer to apply damage immediately } // Check if the damage source was a Magic Ball and apply slowdown if (source && source instanceof MagicBall) { 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 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 enemyDefeated = closestEnemy.takeDamage(self.attackDamage, self); // Pass the swordsman as source if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score when enemy is defeated by swordsman. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy closestEnemy.destroy(); // Remove from the enemies array in the main game loop } // Optional: Add a visual/sound effect for attack here later } } }; return self; // Return self for potential inheritance }); /** * Represents a Viking ally that throws piercing axes. */ var VikingAlly = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('viking_ally', { anchorX: 0.5, anchorY: 0.5 }); self.attackRange = Infinity; // Infinite attack range self.attackInterval = 150; // Attack every 2.5 seconds (150 ticks) self.attackTimer = 0; // Timer for attacks /** * Update method called each game tick by the LK engine. * Handles finding targets and throwing axes. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the closest enemy within range var closestEnemy = null; var closestDistance = self.attackRange; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, throw an axe if (closestEnemy) { var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); var newAxe = new VikingAxe(angle); newAxe.x = self.x; newAxe.y = self.y; newAxe.lastY = newAxe.y; newAxe.lastX = newAxe.x; game.addChild(newAxe); vikingAxes.push(newAxe); // Add to the vikingAxes array // Optional: Add throwing sound effect here later } } }; return self; }); /** * Represents an Axe thrown by a Viking Ally. * @param {number} angle - The angle in radians at which the axe is thrown. */ var VikingAxe = Container.expand(function (angle) { var self = Container.call(this); var graphics = self.attachAsset('viking_axe', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align axe graphic with direction self.speed = 25; // Speed of the axe // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.speed += 5; } self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) self.damage = 3; // Base damage dealt by this axe // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.damage += 5; } self.pierceLeft = 3; // Can pierce through 3 enemies /** * Update method called each game tick by the LK engine. * Moves the axe based on its velocity and handles rotation. */ self.update = function () { self.x += self.vx; self.y += self.vy; graphics.rotation += 0.2; // Add spinning effect }; return self; }); /** * Represents a Wizard Tower ally that shoots magic balls. */ var WizardTower = Container.expand(function () { var self = Container.call(this); // Create and attach the wizard tower graphic asset var graphics = self.attachAsset('wizard_tower_asset', { anchorX: 0.5, anchorY: 0.5 }); self.attackRange = Infinity; // Infinite attack range self.attackDamage = 0.5; // Low damage, primary effect is slowdown self.attackInterval = 120; // Attack every 2 seconds (120 ticks) self.attackTimer = 0; // Timer for attacks self.slowDuration = 180; // Duration of slowdown effect in ticks (3 seconds) self.slowAmount = 0.5; // Multiplier for enemy speed (0.5 means 50% slower) /** * Update method called each game tick by the LK engine. * Handles attacking enemies. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the closest enemy within range to shoot at var closestEnemy = null; var closestDistance = self.attackRange; // Limit to attack range for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate distance to the enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, fire a magic ball if (closestEnemy) { // Calculate angle towards the closest enemy var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); // Create a new magic ball instance var newMagicBall = new MagicBall(angle); // Position the magic ball at the tower's location newMagicBall.x = self.x; newMagicBall.y = self.y; newMagicBall.lastY = newMagicBall.y; // Initialize lastY for state tracking newMagicBall.lastX = newMagicBall.x; // Initialize lastX for state tracking newMagicBall.slowDuration = self.slowDuration; // Pass slow duration to the magic ball newMagicBall.slowAmount = self.slowAmount; // Pass slow amount to the magic ball // Add the magic ball to the game scene and the tracking array. game.addChild(newMagicBall); magicBalls.push(newMagicBall); // Add to the magicBalls array // Optional: Add a visual/sound effect for magic ball shot here later } } }; return self; // Return self for potential inheritance }); /** * 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 ****/ //Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property. //Only include the plugins you need to create the game. //We have access to the following plugins. (Note that the variable names used are mandetory for each plugin) //Storage library which should be used for persistent game data //var storage = LK.import('@upit/storage.v1'); //Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions //var facekit = LK.import('@upit/facekit.v1'); // Constants defining game dimensions and key vertical positions. // Placeholder ID, adjust size slightly // Placeholder ID // Placeholder ID, adjust size slightly, reuse flipX // Use actual spearman asset // Use actual war elephant asset var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; var BASTION_Y = GAME_HEIGHT - 250; // Y-coordinate representing the defense line. Enemies crossing this trigger game over. var FIRING_POS_Y = GAME_HEIGHT - 150; // Y-coordinate from where arrows are fired. var FIRING_POS_X = GAME_WIDTH / 2; // X-coordinate from where arrows are fired (center). // Arrays to keep track of active arrows and enemies. var arrows = []; var enemies = []; // Variables for game state and difficulty scaling. var scoreTxt; // Text2 object for displaying the score. var ammoTxt; // Text2 object for displaying the current ammo count. var dragStartX = null; // Starting X coordinate of a touch/mouse drag. var dragStartY = null; // Starting Y coordinate of a touch/mouse drag. var enemySpawnInterval = 120; // Initial ticks between enemy spawns (2 seconds at 60 FPS). var arrowsFired = 0; // Counter for arrows fired since the last cooldown. var cooldownTimer = 0; // Timer in ticks for the cooldown period. var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec). Min duration enforced in upgrade. var maxArrowsBeforeCooldown = 15; // Max arrows before reload (Quiver upgrade) var arrowPierceLevel = 1; // How many enemies an arrow can pierce (Piercing Shots upgrade) var arrowDamage = 1; // Base damage for arrows (Heat-tipped arrows upgrade) var enemySpeedMultiplier = 1.0; // Multiplier for enemy speed (Sabotage upgrade) Min multiplier enforced in upgrade. var minEnemySpawnInterval = 30; // Minimum ticks between enemy spawns (0.5 seconds). var enemySpawnRateDecrease = 0.08; // Amount to decrease spawn interval each time an enemy spawns. var currentEnemySpeed = 3; // Initial base speed of enemies. var maxEnemySpeed = 12; // Maximum base speed enemies can reach. var enemySpeedIncrease = 0.015; // Amount to increase enemy base speed each time an enemy spawns. var archerAllies = []; // Array to hold ArcherAlly instances var swordsmen = []; // Array to hold Swordsman instances var cannonballs = []; // Array to hold Cannonball instances var multiShotEnabled = false; // Flag for More Shots upgrade var poisonShotsEnabled = false; // Flag for Poison Shots upgrade var cannons = []; // Array to hold Cannon instances var allyAttackSpeedMultiplier = 1.0; // Multiplier for ally attack speed var wizardTowers = []; // Array to hold WizardTower instances var magicBalls = []; // Array to hold MagicBall instances var aimbotEnabled = false; // Flag for Aimbot upgrade var greenKillerEnabled = false; // Flag for Green Killer upgrade var refinedProjectilesEnabled = false; // Flag for Refined Projectiles upgrade var greenSlowdownEnabled = false; // Flag for Green Slowdown upgrade var vikingAllies = []; // Array to hold VikingAlly instances var vikingAxes = []; // Array to hold VikingAxe instances 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 // --- Upgrade System --- var lastUpgradeScore = -1; // Score at which the last upgrade was offered var isUpgradePopupActive = false; // Flag indicating if the upgrade choice popup is visible var upgradePopup = null; // Container for the upgrade UI elements var currentMusicPlaying = 'Gamemusic'; // Track which music is currently playing var musicFadeInProgress = false; // Flag to track if a music fade transition is in progress // Helper function to apply Sabotage effect to existing enemies function applySabotage() { for (var i = 0; i < enemies.length; i++) { // We assume Enemy class will use enemySpeedMultiplier when calculating effective speed // If Enemy.speed stores base speed, we might need an effectiveSpeed method or update speed directly. // For simplicity, let's assume Enemy.update uses the global multiplier. // If direct modification is needed: enemies[i].speed = baseSpeed * enemySpeedMultiplier; } // Also update the current base speed calculation baseline if necessary, though modifying the multiplier should suffice. } // Helper function placeholder for adding Archer Ally function addArcherAlly() { // Implementation requires ArcherAlly class definition console.log("Archer Ally upgrade chosen!"); var ally = new ArcherAlly(); // Position the ally next to the player's firing position, slightly offset. ally.x = FIRING_POS_X + 150; // Position to the right of the player ally.y = FIRING_POS_Y; game.addChild(ally); archerAllies.push(ally); // Add to the new archerAllies array } // Helper function to add a Cannon ally function addCannon() { console.log("Cannon upgrade chosen!"); var newCannon = new Cannon(); // Position the cannon near the bastion line newCannon.x = GAME_WIDTH / 2 - 300; // Example position to the left of center newCannon.y = BASTION_Y - 100; // Position above the bastion line game.addChild(newCannon); cannons.push(newCannon); // Add to the cannons array } // Helper function to add a Wizard Tower function addWizardTower() { console.log("Wizard Tower upgrade chosen!"); var newTower = new WizardTower(); // Position the wizard tower near the bastion line, offset from center newTower.x = GAME_WIDTH / 2 + 300; // Example position to the right of center newTower.y = BASTION_Y - 100; // Position above the bastion line game.addChild(newTower); wizardTowers.push(newTower); // Add to the wizardTowers array } // Helper function to add a Viking Ally function addVikingAlly() { console.log("Viking Ally upgrade chosen!"); var newViking = new VikingAlly(); // Position the viking near the bastion line, offset from cannons/towers newViking.x = GAME_WIDTH / 2; // Center for now newViking.y = BASTION_Y - 100; // Position above the bastion line game.addChild(newViking); vikingAllies.push(newViking); // Add to the vikingAllies array } // Helper function to add a Dart Ally function addDartAlly() { console.log("Dart Ally upgrade chosen!"); var newDartAlly = new DartAlly(); // Position the dart ally near the bastion line, offset newDartAlly.x = GAME_WIDTH / 2 - 200; // Example position newDartAlly.y = BASTION_Y - 100; // Position above bastion line game.addChild(newDartAlly); darts.push(newDartAlly); // Add to the dartAllies array (need to rename array to dartAllies if creating a separate one) } // Helper function placeholder for adding Swordsman function addSwordsman() { // Create a swordsman tower at the bastion line var tower = new Container(); var towerGraphics = tower.attachAsset('swordsmanTower', { anchorX: 0.5, anchorY: 0.5 }); tower.x = FIRING_POS_X; // Position in the center horizontally tower.y = BASTION_Y - 50; // Position just above the bastion line game.addChild(tower); // Set up interval to spawn swordsmen from the tower var spawnInterval = LK.setInterval(function () { var newSwordsman = new Swordsman(); // Position the swordsman near the tower with a slight random offset newSwordsman.x = tower.x + (Math.random() * 100 - 50); // Random offset of ±50px newSwordsman.y = tower.y; game.addChild(newSwordsman); swordsmen.push(newSwordsman); // Add to the swordsmen array }, 3000); // Spawn a swordsman every 3 seconds // Swordsman spawn does not grant score // Store the interval reference in the tower to potentially clear it later tower.spawnInterval = spawnInterval; } // Helper function to add an Angel of Light function addAngelOfLight() { console.log("Angel of Light upgrade chosen!"); var newAngel = new AngelOfLight(); // Position the angel near the bastion line, offset newAngel.x = GAME_WIDTH / 2 + 200; // Example position newAngel.y = BASTION_Y - 150; // Position slightly higher than other allies game.addChild(newAngel); angelOfLights.push(newAngel); // Add to the angelOfLights array } // 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 }]; // Function to create and show the upgrade selection popup function showUpgradePopup() { isUpgradePopupActive = true; // Simple pause: Game logic checks isUpgradePopupActive flag in game.update // Switch to upgrade theme music with fade effect if (currentMusicPlaying !== 'UpgradeTheme' && !musicFadeInProgress) { musicFadeInProgress = true; // Fade out current music LK.playMusic('Gamemusic', { fade: { start: 1, end: 0, duration: 800 } }); // After fade out completes, start upgrade theme with fade in LK.setTimeout(function () { LK.playMusic('UpgradeTheme', { fade: { start: 0, end: 1, duration: 1000 } }); currentMusicPlaying = 'UpgradeTheme'; musicFadeInProgress = false; }, 850); } // --- Create Popup UI --- upgradePopup = new Container(); upgradePopup.x = GAME_WIDTH / 2; upgradePopup.y = GAME_HEIGHT / 2; game.addChild(upgradePopup); // Add to game layer for positioning relative to center // Add a semi-transparent background overlay var bg = upgradePopup.attachAsset('bastionLine', { // Reusing an asset for shape width: 1200, height: 800, color: 0x000000, // Black background anchorX: 0.5, anchorY: 0.5, alpha: 0.8 }); // Add title text var title = new Text2('Choose an Upgrade!', { size: 80, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.y = -300; // Position relative to popup center upgradePopup.addChild(title); // --- Select 3 Random Unique Upgrades --- var availableToOffer = allUpgrades.slice(); // Copy available upgrades var choices = []; var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices for (var i = 0; i < numChoices; i++) { var randomIndex = Math.floor(Math.random() * availableToOffer.length); choices.push(availableToOffer[randomIndex]); availableToOffer.splice(randomIndex, 1); // Remove chosen upgrade to ensure uniqueness } // --- Create Buttons for Choices --- var buttonYStart = -150; var buttonSpacing = 180; for (var j = 0; j < choices.length; j++) { var upgrade = choices[j]; var button = createUpgradeButton(upgrade, buttonYStart + j * buttonSpacing); upgradePopup.addChild(button); } } // Function to create a single upgrade button function createUpgradeButton(upgradeData, yPos) { var buttonContainer = new Container(); buttonContainer.y = yPos; // Button background var buttonBg = buttonContainer.attachAsset('bastionLine', { // Reusing asset for shape width: 800, height: 150, color: 0x555555, anchorX: 0.5, anchorY: 0.5 }); // Button text (Name + Description) var nameText = new Text2(upgradeData.name, { size: 50, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 0.5); nameText.y = -25; buttonContainer.addChild(nameText); var descText = new Text2(upgradeData.description, { size: 35, fill: 0xCCCCCC }); descText.anchor.set(0.5, 0.5); descText.y = 30; buttonContainer.addChild(descText); // Make button interactive buttonContainer.interactive = true; // Needed for down event // Event handler for button press buttonContainer.down = function (x, y, obj) { upgradeData.apply(); // Apply the selected upgrade's effect hideUpgradePopup(); // Close the popup }; return buttonContainer; } // Function to hide and destroy the upgrade popup function hideUpgradePopup() { if (upgradePopup) { upgradePopup.destroy(); upgradePopup = null; } isUpgradePopupActive = false; // Switch back to game music with fade effect if (currentMusicPlaying !== 'Gamemusic' && !musicFadeInProgress) { musicFadeInProgress = true; // Fade out upgrade theme LK.playMusic('UpgradeTheme', { fade: { start: 1, end: 0, duration: 800 } }); // After fade out completes, start game music with fade in LK.setTimeout(function () { LK.playMusic('Gamemusic', { fade: { start: 0, end: 1, duration: 1000 } }); currentMusicPlaying = 'Gamemusic'; musicFadeInProgress = false; }, 850); } // Resume game logic (handled by checking flag in game.update) } // --- 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 } }; 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.' }; 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); // --- 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 = 20; } // Baby Dragon unlock score 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); // --- Input Event Handlers --- // Handles touch/mouse press down event on the game area. game.down = function (x, y, obj) { // Record the starting position of the drag in game coordinates. var gamePos = game.toLocal({ x: x, y: y }); dragStartX = gamePos.x; dragStartY = gamePos.y; // Potential enhancement: Show an aiming indicator originating from FIRING_POS_X, FIRING_POS_Y towards the drag point. }; // Handles touch/mouse move event while dragging on the game area. game.move = function (x, y, obj) { // Only process if a drag is currently active. if (dragStartX !== null) { var gamePos = game.toLocal({ x: x, y: y }); // Potential enhancement: Update the aiming indicator based on the current drag position (gamePos). } }; // Handles touch/mouse release event on the game area. game.up = function (x, y, obj) { // Only process if a drag was active. if (dragStartX !== null && cooldownTimer <= 0) { // Only allow firing if not on cooldown. var gamePos = game.toLocal({ x: x, y: y }); var dragEndX = gamePos.x; var dragEndY = gamePos.y; // Calculate the difference between the firing position and the release point to determine direction. // A longer drag could potentially mean more power, but we'll keep it simple: direction only. var dx = dragEndX - FIRING_POS_X; var dy = dragEndY - FIRING_POS_Y; // Avoid division by zero or zero vector if start and end points are the same. if (dx === 0 && dy === 0) { // Optionally handle this case, e.g., fire straight up or do nothing. // Let's fire straight up if drag distance is negligible. dy = -1; } // Calculate the angle using atan2. Note the order (dx, dy) and the negation of dy // because the Y-axis is inverted in screen coordinates (positive Y is down). var angle = Math.atan2(dx, -dy); // Create a new arrow instance with the calculated angle. var newArrow = new Arrow(angle); newArrow.x = FIRING_POS_X; newArrow.y = FIRING_POS_Y; // Initialize last position for state tracking (e.g., off-screen detection) newArrow.lastY = newArrow.y; newArrow.lastX = newArrow.x; // Add the arrow to the game scene and the tracking array. game.addChild(newArrow); arrows.push(newArrow); if (multiShotEnabled) { // Fire a second arrow with a slight angle offset var angle2 = angle + Math.PI / 12; // Offset by 15 degrees var newArrow2 = new Arrow(angle2); newArrow2.x = FIRING_POS_X; newArrow2.y = FIRING_POS_Y; newArrow2.lastY = newArrow2.y; newArrow2.lastX = newArrow2.x; game.addChild(newArrow2); arrows.push(newArrow2); // Fire a third arrow with the opposite angle offset var angle3 = angle - Math.PI / 12; // Offset by -15 degrees var newArrow3 = new Arrow(angle3); newArrow3.x = FIRING_POS_X; newArrow3.y = FIRING_POS_Y; newArrow3.lastY = newArrow3.y; newArrow3.lastX = newArrow3.x; game.addChild(newArrow3); arrows.push(newArrow3); } LK.getSound('shoot').play(); // Play shooting sound. arrowsFired++; // Increment arrow count. ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display. // Check if cooldown needs to start based on Quiver upgrade if (arrowsFired >= maxArrowsBeforeCooldown) { cooldownTimer = cooldownDuration; // Start cooldown (duration affected by Faster Reload upgrade) arrowsFired = 0; // Reset arrow count. ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display. LK.getSound('Reload').play(); // Play reload sound. } // Reset drag state. dragStartX = null; dragStartY = null; // Potential enhancement: Hide the aiming indicator. } }; // --- Main Game Update Loop --- // This function is called automatically by the LK engine on every frame (tick). game.update = function () { // --- Upgrade System Check --- var currentScore = LK.getScore(); // Check if score reached a multiple of 10 and is higher than the last upgrade score if (!isUpgradePopupActive && currentScore > 0 && currentScore % 10 === 0 && currentScore !== lastUpgradeScore) { lastUpgradeScore = currentScore; // Mark this score level as having triggered an upgrade offer showUpgradePopup(); // Show the upgrade selection screen } // --- Pause Game Logic if Upgrade Popup is Active --- if (isUpgradePopupActive) { // Potential: Update UI animations if any return; // Skip the rest of the game update loop } // --- Resume Game Logic --- if (isUpgradePopupActive) { return; // Skip enemy movement and spawning if upgrade popup is active } // Decrease cooldown timer if active. if (cooldownTimer > 0) { cooldownTimer--; ammoTxt.setText('Reloading...'); // Show reloading status } else if (ammoTxt.text !== 'Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)) { // Ensure ammo display is correct if not reloading ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); } // 1. Update and check Arrows for (var i = arrows.length - 1; i >= 0; i--) { var arrow = arrows[i]; // Note: LK engine calls arrow.update() automatically as it's added to the game stage. // Initialize last position if it hasn't been set yet (first frame). if (arrow.lastY === undefined) { arrow.lastY = arrow.y; } if (arrow.lastX === undefined) { arrow.lastX = arrow.x; } // Check for collisions between the current arrow and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (arrow.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Calculate damage, applying Green Killer bonus if applicable var damageToDeal = arrow.damage; if (greenKillerEnabled && enemy.tag === 'Green') { damageToDeal *= 1.5; // Apply 50% damage bonus } // Apply damage to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(damageToDeal, arrow); // Pass the arrow object as source // Check if the arrow was reflected (takeDamage returns false and arrow is destroyed) if (!enemyDefeated && !arrow.parent) { // Arrow was reflected, remove it from the array and stop processing arrows.splice(i, 1); hitEnemy = true; // Mark that this arrow is done break; // Stop checking this arrow against other enemies as it's destroyed } if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Handle arrow piercing arrow.pierceLeft--; // Decrease remaining pierces if (arrow.pierceLeft <= 0) { // Arrow has no pierces left, destroy it arrow.destroy(); arrows.splice(i, 1); // Remove arrow from array. hitEnemy = true; // Mark that this arrow is done break; // Stop checking this arrow against other enemies as it's destroyed } else { // Arrow pierced this enemy and can continue // We don't set hitEnemy = true here because the arrow continues // We don't break because it might hit another enemy in the same frame further along its path // Apply Green Slowdown if applicable and arrow is still piercing if (!enemyDefeated && greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } } } } // If the arrow hit an enemy, it was destroyed, so skip to the next arrow. if (hitEnemy) { continue; } // Check if the arrow has gone off-screen (top, left, or right). // Use transition detection: check if it was on screen last frame and is off screen now. var wasOnScreen = arrow.lastY > -arrow.height / 2 && arrow.lastX > -arrow.width / 2 && arrow.lastX < GAME_WIDTH + arrow.width / 2; var isOffScreen = arrow.y < -arrow.height / 2 || // Off top edge arrow.x < -arrow.width / 2 || // Off left edge arrow.x > GAME_WIDTH + arrow.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Arrow is off-screen, destroy it and remove from the array. arrow.destroy(); arrows.splice(i, 1); } else if (!isOffScreen) { // Update last known position only if the arrow is still potentially on screen arrow.lastY = arrow.y; arrow.lastX = arrow.x; } } // 2. Update and check Cannonballs for (var cb = cannonballs.length - 1; cb >= 0; cb--) { var cannonball = cannonballs[cb]; // Note: LK engine calls cannonball.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (cannonball.lastY === undefined) { cannonball.lastY = cannonball.y; } if (cannonball.lastX === undefined) { cannonball.lastX = cannonball.x; } // Check for collisions between the current cannonball and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (cannonball.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply damage to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(cannonball.damage, cannonball); // Pass cannonball as source // Check if the cannonball had no effect (e.g. Dark War Elephant immunity) and is still in game if (!enemyDefeated && cannonball.parent) { // Cannonball had no effect, but wasn't destroyed by a normal hit. // It might have been immune. If so, we still destroy the cannonball. cannonball.destroy(); cannonballs.splice(cb, 1); hitEnemy = true; // Mark that this cannonball is done break; // Stop checking this cannonball against other enemies as it's destroyed } if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Cannonballs are destroyed on hit cannonball.destroy(); // Apply Green Slowdown if applicable before destroying cannonball if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } cannonball.destroy(); cannonballs.splice(cb, 1); // Remove cannonball from array. hitEnemy = true; // Mark that this cannonball is done break; // Stop checking this cannonball against other enemies as it's destroyed } } // If the cannonball hit an enemy, it was destroyed, so skip to the next cannonball. if (hitEnemy) { continue; } // Check if the cannonball has gone off-screen. var wasOnScreen = cannonball.lastY > -cannonball.height / 2 && cannonball.lastX > -cannonball.width / 2 && cannonball.lastX < GAME_WIDTH + cannonball.width / 2; var isOffScreen = cannonball.y < -cannonball.height / 2 || // Off top edge cannonball.x < -cannonball.width / 2 || // Off left edge cannonball.x > GAME_WIDTH + cannonball.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Cannonball is off-screen, destroy it and remove from the array. cannonball.destroy(); cannonballs.splice(cb, 1); } else if (!isOffScreen) { // Update last known position only if the cannonball is still potentially on screen cannonball.lastY = cannonball.y; cannonball.lastX = cannonball.x; } } // 3. Update and check Enemies for (var k = enemies.length - 1; k >= 0; k--) { var enemy = enemies[k]; // Note: LK engine calls enemy.update() automatically. // Initialize last position if not set. if (enemy.lastY === undefined) { enemy.lastY = enemy.y; } // Check if the enemy has reached or passed the bastion line. // Use transition detection: was the enemy's bottom edge above the line last frame, // and is it on or below the line now? var enemyBottomY = enemy.y + enemy.height / 2; var enemyLastBottomY = enemy.lastY + enemy.height / 2; var wasAboveBastion = enemyLastBottomY < BASTION_Y; var isAtOrBelowBastion = enemyBottomY >= BASTION_Y; if (wasAboveBastion && isAtOrBelowBastion) { // Enemy reached the bastion! Game Over. LK.getSound('gameOverSfx').play(); // Play game over sound effect. LK.showGameOver(); // Trigger the engine's game over sequence. // LK.showGameOver handles game state reset, no need to manually clear arrays here. return; // Exit the update loop immediately as the game is over. } else { // Update last known position if game is not over for this enemy. enemy.lastY = enemy.y; } } // 3. Update and check Swordsmen (allies) for (var l = swordsmen.length - 1; l >= 0; l--) { var swordsman = swordsmen[l]; // Swordsman update method handles its own lifetime and attacking // Check if the swordsman has been destroyed by its lifetime timer if (!swordsman.parent) { // If it no longer has a parent, it has been destroyed swordsmen.splice(l, 1); // Remove swordsman from array } } // 4. Update and check Cannons (allies) for (var m = cannons.length - 1; m >= 0; m--) { var cannon = cannons[m]; // Cannons do not have a lifetime timer or destruction logic in this basic version // If they had, we'd check !cannon.parent and splice here as in Swordsmen } // 5. Update and check Wizard Towers (allies) for (var wt = wizardTowers.length - 1; wt >= 0; wt--) { var tower = wizardTowers[wt]; // Wizard towers do not have a lifetime timer or destruction logic in this basic version // If they had, we'd check !tower.parent and splice here } // 5.5 Update and check Viking Allies for (var va = vikingAllies.length - 1; va >= 0; va--) { var viking = vikingAllies[va]; // Vikings do not have a lifetime timer or destruction logic in this version // LK engine calls viking.update() automatically. } // 5.6 Update and check Angel of Lights (allies) for (var angelIdx = angelOfLights.length - 1; angelIdx >= 0; angelIdx--) { var angel = angelOfLights[angelIdx]; // Angels do not have a lifetime timer or destruction logic in this version // LK engine calls angel.update() automatically. } // 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; if (enemy.tag === 'Green') { damageToDealDart *= 2; // Double damage against Green enemies } // Apply damage and check if defeated var enemyDefeated = enemy.takeDamage(damageToDealDart, dart); // Pass dart as source // Check if the dart was reflected (takeDamage returns false and dart is destroyed) if (!enemyDefeated && !dart.parent) { // Dart was reflected, remove it from the array and stop processing darts.splice(dartIdx, 1); hitEnemyDart = true; // Mark that this dart is done break; // Stop checking this dart against other enemies } if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Darts are destroyed on hit dart.destroy(); // Apply Green Slowdown if applicable before destroying dart if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } darts.splice(dartIdx, 1); // Remove dart from array. hitEnemyDart = true; // Mark that this dart is done break; // Stop checking this dart against other enemies } } // If the dart hit and was destroyed, skip to the next dart. if (hitEnemyDart) { continue; } // Check if the dart has gone off-screen. var wasOnScreenDart = dart.lastY > -dart.height / 2 && dart.lastX > -dart.width / 2 && dart.lastX < GAME_WIDTH + dart.width / 2; var isOffScreenDart = dart.y < -dart.height / 2 || dart.y > GAME_HEIGHT + dart.height / 2 || dart.x < -dart.width / 2 || dart.x > GAME_WIDTH + dart.width / 2; if (wasOnScreenDart && isOffScreenDart) { dart.destroy(); darts.splice(dartIdx, 1); } else if (!isOffScreenDart) { dart.lastY = dart.y; dart.lastX = dart.x; } } // 7. Spawn new Enemies periodically // Use LK.ticks and the spawn interval. Ensure interval doesn't go below minimum. if (LK.ticks % Math.max(minEnemySpawnInterval, Math.floor(enemySpawnInterval)) === 0) { var currentScore = LK.getScore(); var baseSpeedForSpawn = Math.min(maxEnemySpeed, currentEnemySpeed); // Base speed adjusted by game difficulty progression // Determine if this spawn *could* be a boss (e.g., every 7th spawn after score 10) var potentialBoss = (enemies.length + 1) % 7 === 0 && currentScore >= 10; var typeToSpawn = 'swordsman'; // Default type var enemyHealth = 1; // Default health var enemyDodgeChance = 0; // Default dodge chance var enemySpeed = baseSpeedForSpawn; // Start with base speed // Determine base type based on score thresholds and randomness var possibleTypes = ['swordsman']; if (currentScore >= 10) { possibleTypes.push('spearman'); } if (currentScore >= 15) { possibleTypes.push('thief'); } if (currentScore >= 23) { possibleTypes.push('knight'); } if (currentScore >= 25) { possibleTypes.push('wizard'); } // Wizards start appearing at score 25 if (currentScore >= 30) { possibleTypes.push('shield'); } // Shields start appearing at score 30 if (currentScore >= 35) { possibleTypes.push('shaman'); // Shaman appears at score 35 console.log("Shaman added to possible enemy types"); } if (currentScore >= 55) { possibleTypes.push('dark_bowman'); // Dark bowman appears at score 55 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'); } 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 } if (currentScore >= 75) { possibleTypes.push('baby_dragon'); // Baby Dragon appears at score 75 } // 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 - 75) / 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 - 75) / 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; } }; // --- Initial Game Setup --- // Set the initial score text based on the starting score (which is 0). scoreTxt.setText(LK.getScore()); // Set the initial ammo display. if (ammoTxt) { // Ensure ammoTxt is initialized ammoTxt.setText('Ammo: ' + maxArrowsBeforeCooldown); } ; // Play the background music. // Check if we need to show a title screen first if (!isGameStarted) {// Assuming a flag 'isGameStarted' for title screen state // Don't play music immediately if showing title screen } else { LK.playMusic('Gamemusic'); ; } // Placeholder for the title screen container var titleScreen = null; // Flag to track game start state var isGameStarted = false; // Function to show the title screen function showTitleScreen() { isGameStarted = false; // Ensure game is not started // Create the title screen container titleScreen = new Container(); titleScreen.x = GAME_WIDTH / 2; titleScreen.y = GAME_HEIGHT / 2; game.addChild(titleScreen); // Add title text var titleText = new Text2('Defend Your Bastion!', { size: 120, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0.5); titleText.y = -200; titleScreen.addChild(titleText); // Add a simple instruction text var instructionText = new Text2('Tap to Start', { size: 60, fill: 0xCCCCCC }); instructionText.anchor.set(0.5, 0.5); instructionText.y = 100; titleScreen.addChild(instructionText); // Make the title screen interactive to start the game titleScreen.interactive = true; titleScreen.down = function () { startGame(); // Start the game when tapped }; // Pause game logic while title screen is active isUpgradePopupActive = true; // Reusing this flag to pause game logic } // Function to start the game function startGame() { isGameStarted = true; // Set game started flag isUpgradePopupActive = false; // Unpause game logic if (titleScreen) { titleScreen.destroy(); titleScreen = null; } // Reset game state if needed (LK might handle this on game start) // Play game music LK.playMusic('Gamemusic'); } // Initially show the title screen showTitleScreen();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
/**
* Represents an 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)
// For now, using a placeholder asset, will need a 'cannon_asset'
var graphics = self.attachAsset('cannon_asset', {
// Use actual cannon asset
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = 800; // Long attack range
self.attackDamage = 10; // High damage
self.attackInterval = 300; // Attack every 5 seconds (300 ticks)
self.attackTimer = 0; // Timer for attacks
self.rotation = 0; // For aiming visual if implemented later
/**
* Update method called each game tick by the LK engine.
* Handles attacking the strongest enemy.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the strongest enemy (highest health)
var strongestEnemy = null;
var maxHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > maxHealth) {
maxHealth = enemy.health;
strongestEnemy = enemy;
}
}
// If a strongest enemy is found within range, attack it
if (strongestEnemy) {
// Calculate angle towards the strongest enemy
var angle = Math.atan2(strongestEnemy.x - self.x, -(strongestEnemy.y - self.y));
// Create a new cannonball instance
var newCannonball = new Cannonball(angle);
// Position the cannonball at the cannon's location
newCannonball.x = self.x;
newCannonball.y = self.y;
newCannonball.lastY = newCannonball.y; // Initialize lastY for state tracking
newCannonball.lastX = newCannonball.x; // Initialize lastX for state tracking
// Add the cannonball to the game scene and the tracking array.
game.addChild(newCannonball);
cannonballs.push(newCannonball); // Add to the new cannonballs array
// Optional: Add a visual/sound effect for cannon shot here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents a Cannonball fired by a Cannon.
* @param {number} angle - The angle in radians at which the cannonball is fired.
*/
var Cannonball = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the cannonball graphic asset
var graphics = self.attachAsset('cannonball', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align cannonball graphic with direction
self.speed = 20; // Base speed of the cannonball
self.damage = 10; // Base damage dealt by this cannonball
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
/**
* Update method called each game tick by the LK engine.
* Moves the cannonball based on its velocity.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Dart projectile fired by a Dart Ally.
* @param {number} angle - The angle in radians at which the dart is fired.
*/
var Dart = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the dart graphic asset (reusing arrow for simplicity)
// Will need a dedicated asset for the Dart if available.
var graphics = self.attachAsset('dart_asset', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5
});
graphics.tint = 0x9933CC; // Purple tint for darts
graphics.rotation = angle + Math.PI / 2; // Align dart graphic with direction
self.speed = 40; // Fast speed
self.damage = 0.5; // Base damage dealt by this dart
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
/**
* Update method called each game tick by the LK engine.
* Moves the dart based on its velocity.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Dart Ally that shoots fast darts.
*/
var DartAlly = Container.expand(function () {
var self = Container.call(this);
// Create and attach the dart ally graphic asset (reusing archer asset for simplicity)
// Will need a dedicated asset for the Dart Ally if available.
var graphics = self.attachAsset('disguised_swordsman', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackDamage = 0.5; // Low damage per dart
self.attackInterval = 30; // Attack every 0.5 seconds (30 ticks) - very fast
self.attackTimer = 0; // Timer for attacks
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and shooting darts.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(10, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.16s
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy to shoot at
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, shoot a dart
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new dart instance
var newDart = new Dart(angle);
newDart.x = self.x;
newDart.y = self.y;
newDart.lastY = newDart.y;
newDart.lastX = newDart.x;
// Add the dart to the game scene and the tracking array.
game.addChild(newDart);
darts.push(newDart); // Add to the new darts array
// Optional: Add throwing sound effect here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents an Enemy attacker moving towards the bastion.
* @param {string} type - The type of enemy ('swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight').
* @param {number} speed - The final calculated speed of the enemy.
* @param {number} health - The initial and maximum health of the enemy.
* @param {number} dodgeChance - The chance (0 to 1) for the enemy to dodge an attack.
*/
var Enemy = Container.expand(function (type, speed, health, dodgeChance) {
var self = Container.call(this);
// Set asset based on type
var assetId = 'enemy'; // Default swordsman asset
if (type === 'knight') {
assetId = 'knight';
} else if (type === 'elite_knight') {
assetId = 'elite_knight_asset'; // Use specific asset for elite knight
} else if (type === 'thief') {
assetId = 'thief';
} else if (type === 'boss') {
assetId = 'boss';
} else if (type === 'shield') {
assetId = 'shield_enemy'; // Use specific asset for shield
} else if (type === 'wizard') {
assetId = 'wizard_enemy'; // Use specific asset for wizard
} else if (type === 'spearman') {
assetId = 'spearman'; // Use specific asset for spearman
} else if (type === 'war_elephant') {
assetId = 'war_elephant'; // Use specific asset for war elephant
} else if (type === 'elite_shield') {
assetId = 'elite_shield_asset'; // Use specific asset for elite shield
} else if (type === 'shaman') {
assetId = 'shaman_enemy'; // Use specific asset for shaman
} else if (type === 'hot_air_balloon') {
assetId = 'hot_air_balloon_asset'; // Use specific asset for hot air balloon
} else if (type === 'dark_bowman') {
assetId = 'dark_bowman_asset'; // Use specific asset for dark bowman
} else if (type === 'jester') {
assetId = 'jester_asset'; // Use specific asset for jester
} else if (type === 'dark_war_elephant') {
assetId = 'dark_war_elephant_asset'; // Use specific asset for dark war elephant
} 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
self.y += self.speed * self.currentSlowAmount; // Apply slowdown to movement
}
// Decrease slowdown timer and reset slow amount if timer runs out
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer <= 0) {
self.currentSlowAmount = 1.0; // Reset speed multiplier when slowdown ends
// Optional: Remove visual feedback for slowdown here later
}
}
// Apply poison damage if stacks exist
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
// Apply poison damage every 0.5 seconds (30 ticks)
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30; // Damage per 0.5s
self.health -= poisonDmg;
// Optional: Add visual feedback for poison damage here later
}
// Check if health dropped to zero or below due to poison
if (self.health <= 0 && self.parent) {
// Ensure enemy is still in the game
// Enemy defeated by poison
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
self.destroy();
// The main game update loop needs to handle removing the enemy from the `enemies` array
return; // Stop updating this destroyed instance
}
}
// Visual feedback based on health - alpha fade (applies to all types)
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6; // Fade from 1.0 down to 0.4
// Shaman ability: reduce player ammo periodically
if (self.type === 'shaman') {
if (self.shamanTimer === undefined) {
self.shamanTimer = 0;
}
self.shamanTimer += 1;
var shamanAbilityInterval = 300; // 5 seconds * 60 FPS
if (self.shamanTimer >= shamanAbilityInterval) {
self.shamanTimer = 0;
// Reduce player ammo, but not below 0
arrowsFired = Math.min(maxArrowsBeforeCooldown, arrowsFired + 1); // Make it require one more shot for reload
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
// Optional: Add a visual/sound effect for shaman ability
}
}
}; //{N} // Adjusted line identifier
/**
* Method called when the enemy is hit by an arrow.
* Handles dodging and health reduction.
* @param {number} damage - The amount of damage the arrow deals.
* @returns {boolean} - True if the enemy is defeated, false otherwise.
*/
self.takeDamage = function (damage) {
var source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
// Add source parameter, default to null
// Check for dodge first (only if dodgeChance is positive)
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
// Optional: Add a visual effect for dodge here later
// console.log(self.type + " dodged!");
return false; // Dodged, so not defeated by this hit
}
// Check for reflection (only for Jester and non-cannonball projectiles)
if (self.type === 'jester' && self.reflectChance > 0 && Math.random() < self.reflectChance && !(source instanceof Cannonball)) {
// Optional: Add a visual effect for reflection here later
// console.log(self.type + " reflected!");
// Projectile is destroyed, enemy takes no damage
if (source && source.destroy) {
source.destroy();
// Need to remove from the source array in the main game loop
}
return false; // Reflected, so not defeated by this hit
}
// Check if enemy is Black-tagged and source is an Arrow or Cannonball
if (self.tag === 'Black' && (source instanceof Arrow || source instanceof Cannonball)) {
// Black-tagged enemies are immune to arrow and cannonball projectiles
return false; // Not defeated, projectile has no effect
}
self.health -= damage;
// Check if the damage source was a poison arrow and apply poison stacks
if (source && source.isPoison) {
// Use the 'source' parameter
self.poisonStacks++; // Increase poison stacks
self.poisonTimer = 0; // Reset timer to apply damage immediately
}
// Check if the damage source was a Magic Ball and apply slowdown
if (source && source instanceof MagicBall) {
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 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 enemyDefeated = closestEnemy.takeDamage(self.attackDamage, self); // Pass the swordsman as source
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score when enemy is defeated by swordsman.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
closestEnemy.destroy();
// Remove from the enemies array in the main game loop
}
// Optional: Add a visual/sound effect for attack here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents a Viking ally that throws piercing axes.
*/
var VikingAlly = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('viking_ally', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackInterval = 150; // Attack every 2.5 seconds (150 ticks)
self.attackTimer = 0; // Timer for attacks
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and throwing axes.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range
var closestEnemy = null;
var closestDistance = self.attackRange;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, throw an axe
if (closestEnemy) {
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
var newAxe = new VikingAxe(angle);
newAxe.x = self.x;
newAxe.y = self.y;
newAxe.lastY = newAxe.y;
newAxe.lastX = newAxe.x;
game.addChild(newAxe);
vikingAxes.push(newAxe); // Add to the vikingAxes array
// Optional: Add throwing sound effect here later
}
}
};
return self;
});
/**
* Represents an Axe thrown by a Viking Ally.
* @param {number} angle - The angle in radians at which the axe is thrown.
*/
var VikingAxe = Container.expand(function (angle) {
var self = Container.call(this);
var graphics = self.attachAsset('viking_axe', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align axe graphic with direction
self.speed = 25; // Speed of the axe
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.damage = 3; // Base damage dealt by this axe
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.damage += 5;
}
self.pierceLeft = 3; // Can pierce through 3 enemies
/**
* Update method called each game tick by the LK engine.
* Moves the axe based on its velocity and handles rotation.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
graphics.rotation += 0.2; // Add spinning effect
};
return self;
});
/**
* Represents a Wizard Tower ally that shoots magic balls.
*/
var WizardTower = Container.expand(function () {
var self = Container.call(this);
// Create and attach the wizard tower graphic asset
var graphics = self.attachAsset('wizard_tower_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackDamage = 0.5; // Low damage, primary effect is slowdown
self.attackInterval = 120; // Attack every 2 seconds (120 ticks)
self.attackTimer = 0; // Timer for attacks
self.slowDuration = 180; // Duration of slowdown effect in ticks (3 seconds)
self.slowAmount = 0.5; // Multiplier for enemy speed (0.5 means 50% slower)
/**
* Update method called each game tick by the LK engine.
* Handles attacking enemies.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range to shoot at
var closestEnemy = null;
var closestDistance = self.attackRange; // Limit to attack range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, fire a magic ball
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new magic ball instance
var newMagicBall = new MagicBall(angle);
// Position the magic ball at the tower's location
newMagicBall.x = self.x;
newMagicBall.y = self.y;
newMagicBall.lastY = newMagicBall.y; // Initialize lastY for state tracking
newMagicBall.lastX = newMagicBall.x; // Initialize lastX for state tracking
newMagicBall.slowDuration = self.slowDuration; // Pass slow duration to the magic ball
newMagicBall.slowAmount = self.slowAmount; // Pass slow amount to the magic ball
// Add the magic ball to the game scene and the tracking array.
game.addChild(newMagicBall);
magicBalls.push(newMagicBall); // Add to the magicBalls array
// Optional: Add a visual/sound effect for magic ball shot here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* 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
****/
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
//Only include the plugins you need to create the game.
//We have access to the following plugins. (Note that the variable names used are mandetory for each plugin)
//Storage library which should be used for persistent game data
//var storage = LK.import('@upit/storage.v1');
//Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions
//var facekit = LK.import('@upit/facekit.v1');
// Constants defining game dimensions and key vertical positions.
// Placeholder ID, adjust size slightly
// Placeholder ID
// Placeholder ID, adjust size slightly, reuse flipX
// Use actual spearman asset
// Use actual war elephant asset
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var BASTION_Y = GAME_HEIGHT - 250; // Y-coordinate representing the defense line. Enemies crossing this trigger game over.
var FIRING_POS_Y = GAME_HEIGHT - 150; // Y-coordinate from where arrows are fired.
var FIRING_POS_X = GAME_WIDTH / 2; // X-coordinate from where arrows are fired (center).
// Arrays to keep track of active arrows and enemies.
var arrows = [];
var enemies = [];
// Variables for game state and difficulty scaling.
var scoreTxt; // Text2 object for displaying the score.
var ammoTxt; // Text2 object for displaying the current ammo count.
var dragStartX = null; // Starting X coordinate of a touch/mouse drag.
var dragStartY = null; // Starting Y coordinate of a touch/mouse drag.
var enemySpawnInterval = 120; // Initial ticks between enemy spawns (2 seconds at 60 FPS).
var arrowsFired = 0; // Counter for arrows fired since the last cooldown.
var cooldownTimer = 0; // Timer in ticks for the cooldown period.
var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec). Min duration enforced in upgrade.
var maxArrowsBeforeCooldown = 15; // Max arrows before reload (Quiver upgrade)
var arrowPierceLevel = 1; // How many enemies an arrow can pierce (Piercing Shots upgrade)
var arrowDamage = 1; // Base damage for arrows (Heat-tipped arrows upgrade)
var enemySpeedMultiplier = 1.0; // Multiplier for enemy speed (Sabotage upgrade) Min multiplier enforced in upgrade.
var minEnemySpawnInterval = 30; // Minimum ticks between enemy spawns (0.5 seconds).
var enemySpawnRateDecrease = 0.08; // Amount to decrease spawn interval each time an enemy spawns.
var currentEnemySpeed = 3; // Initial base speed of enemies.
var maxEnemySpeed = 12; // Maximum base speed enemies can reach.
var enemySpeedIncrease = 0.015; // Amount to increase enemy base speed each time an enemy spawns.
var archerAllies = []; // Array to hold ArcherAlly instances
var swordsmen = []; // Array to hold Swordsman instances
var cannonballs = []; // Array to hold Cannonball instances
var multiShotEnabled = false; // Flag for More Shots upgrade
var poisonShotsEnabled = false; // Flag for Poison Shots upgrade
var cannons = []; // Array to hold Cannon instances
var allyAttackSpeedMultiplier = 1.0; // Multiplier for ally attack speed
var wizardTowers = []; // Array to hold WizardTower instances
var magicBalls = []; // Array to hold MagicBall instances
var aimbotEnabled = false; // Flag for Aimbot upgrade
var greenKillerEnabled = false; // Flag for Green Killer upgrade
var refinedProjectilesEnabled = false; // Flag for Refined Projectiles upgrade
var greenSlowdownEnabled = false; // Flag for Green Slowdown upgrade
var vikingAllies = []; // Array to hold VikingAlly instances
var vikingAxes = []; // Array to hold VikingAxe instances
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
// --- Upgrade System ---
var lastUpgradeScore = -1; // Score at which the last upgrade was offered
var isUpgradePopupActive = false; // Flag indicating if the upgrade choice popup is visible
var upgradePopup = null; // Container for the upgrade UI elements
var currentMusicPlaying = 'Gamemusic'; // Track which music is currently playing
var musicFadeInProgress = false; // Flag to track if a music fade transition is in progress
// Helper function to apply Sabotage effect to existing enemies
function applySabotage() {
for (var i = 0; i < enemies.length; i++) {
// We assume Enemy class will use enemySpeedMultiplier when calculating effective speed
// If Enemy.speed stores base speed, we might need an effectiveSpeed method or update speed directly.
// For simplicity, let's assume Enemy.update uses the global multiplier.
// If direct modification is needed: enemies[i].speed = baseSpeed * enemySpeedMultiplier;
}
// Also update the current base speed calculation baseline if necessary, though modifying the multiplier should suffice.
}
// Helper function placeholder for adding Archer Ally
function addArcherAlly() {
// Implementation requires ArcherAlly class definition
console.log("Archer Ally upgrade chosen!");
var ally = new ArcherAlly();
// Position the ally next to the player's firing position, slightly offset.
ally.x = FIRING_POS_X + 150; // Position to the right of the player
ally.y = FIRING_POS_Y;
game.addChild(ally);
archerAllies.push(ally); // Add to the new archerAllies array
}
// Helper function to add a Cannon ally
function addCannon() {
console.log("Cannon upgrade chosen!");
var newCannon = new Cannon();
// Position the cannon near the bastion line
newCannon.x = GAME_WIDTH / 2 - 300; // Example position to the left of center
newCannon.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newCannon);
cannons.push(newCannon); // Add to the cannons array
}
// Helper function to add a Wizard Tower
function addWizardTower() {
console.log("Wizard Tower upgrade chosen!");
var newTower = new WizardTower();
// Position the wizard tower near the bastion line, offset from center
newTower.x = GAME_WIDTH / 2 + 300; // Example position to the right of center
newTower.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newTower);
wizardTowers.push(newTower); // Add to the wizardTowers array
}
// Helper function to add a Viking Ally
function addVikingAlly() {
console.log("Viking Ally upgrade chosen!");
var newViking = new VikingAlly();
// Position the viking near the bastion line, offset from cannons/towers
newViking.x = GAME_WIDTH / 2; // Center for now
newViking.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newViking);
vikingAllies.push(newViking); // Add to the vikingAllies array
}
// Helper function to add a Dart Ally
function addDartAlly() {
console.log("Dart Ally upgrade chosen!");
var newDartAlly = new DartAlly();
// Position the dart ally near the bastion line, offset
newDartAlly.x = GAME_WIDTH / 2 - 200; // Example position
newDartAlly.y = BASTION_Y - 100; // Position above bastion line
game.addChild(newDartAlly);
darts.push(newDartAlly); // Add to the dartAllies array (need to rename array to dartAllies if creating a separate one)
}
// Helper function placeholder for adding Swordsman
function addSwordsman() {
// Create a swordsman tower at the bastion line
var tower = new Container();
var towerGraphics = tower.attachAsset('swordsmanTower', {
anchorX: 0.5,
anchorY: 0.5
});
tower.x = FIRING_POS_X; // Position in the center horizontally
tower.y = BASTION_Y - 50; // Position just above the bastion line
game.addChild(tower);
// Set up interval to spawn swordsmen from the tower
var spawnInterval = LK.setInterval(function () {
var newSwordsman = new Swordsman();
// Position the swordsman near the tower with a slight random offset
newSwordsman.x = tower.x + (Math.random() * 100 - 50); // Random offset of ±50px
newSwordsman.y = tower.y;
game.addChild(newSwordsman);
swordsmen.push(newSwordsman); // Add to the swordsmen array
}, 3000); // Spawn a swordsman every 3 seconds
// Swordsman spawn does not grant score
// Store the interval reference in the tower to potentially clear it later
tower.spawnInterval = spawnInterval;
}
// Helper function to add an Angel of Light
function addAngelOfLight() {
console.log("Angel of Light upgrade chosen!");
var newAngel = new AngelOfLight();
// Position the angel near the bastion line, offset
newAngel.x = GAME_WIDTH / 2 + 200; // Example position
newAngel.y = BASTION_Y - 150; // Position slightly higher than other allies
game.addChild(newAngel);
angelOfLights.push(newAngel); // Add to the angelOfLights array
}
// 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
}];
// Function to create and show the upgrade selection popup
function showUpgradePopup() {
isUpgradePopupActive = true;
// Simple pause: Game logic checks isUpgradePopupActive flag in game.update
// Switch to upgrade theme music with fade effect
if (currentMusicPlaying !== 'UpgradeTheme' && !musicFadeInProgress) {
musicFadeInProgress = true;
// Fade out current music
LK.playMusic('Gamemusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// After fade out completes, start upgrade theme with fade in
LK.setTimeout(function () {
LK.playMusic('UpgradeTheme', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
currentMusicPlaying = 'UpgradeTheme';
musicFadeInProgress = false;
}, 850);
}
// --- Create Popup UI ---
upgradePopup = new Container();
upgradePopup.x = GAME_WIDTH / 2;
upgradePopup.y = GAME_HEIGHT / 2;
game.addChild(upgradePopup); // Add to game layer for positioning relative to center
// Add a semi-transparent background overlay
var bg = upgradePopup.attachAsset('bastionLine', {
// Reusing an asset for shape
width: 1200,
height: 800,
color: 0x000000,
// Black background
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Add title text
var title = new Text2('Choose an Upgrade!', {
size: 80,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -300; // Position relative to popup center
upgradePopup.addChild(title);
// --- Select 3 Random Unique Upgrades ---
var availableToOffer = allUpgrades.slice(); // Copy available upgrades
var choices = [];
var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices
for (var i = 0; i < numChoices; i++) {
var randomIndex = Math.floor(Math.random() * availableToOffer.length);
choices.push(availableToOffer[randomIndex]);
availableToOffer.splice(randomIndex, 1); // Remove chosen upgrade to ensure uniqueness
}
// --- Create Buttons for Choices ---
var buttonYStart = -150;
var buttonSpacing = 180;
for (var j = 0; j < choices.length; j++) {
var upgrade = choices[j];
var button = createUpgradeButton(upgrade, buttonYStart + j * buttonSpacing);
upgradePopup.addChild(button);
}
}
// Function to create a single upgrade button
function createUpgradeButton(upgradeData, yPos) {
var buttonContainer = new Container();
buttonContainer.y = yPos;
// Button background
var buttonBg = buttonContainer.attachAsset('bastionLine', {
// Reusing asset for shape
width: 800,
height: 150,
color: 0x555555,
anchorX: 0.5,
anchorY: 0.5
});
// Button text (Name + Description)
var nameText = new Text2(upgradeData.name, {
size: 50,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0.5);
nameText.y = -25;
buttonContainer.addChild(nameText);
var descText = new Text2(upgradeData.description, {
size: 35,
fill: 0xCCCCCC
});
descText.anchor.set(0.5, 0.5);
descText.y = 30;
buttonContainer.addChild(descText);
// Make button interactive
buttonContainer.interactive = true; // Needed for down event
// Event handler for button press
buttonContainer.down = function (x, y, obj) {
upgradeData.apply(); // Apply the selected upgrade's effect
hideUpgradePopup(); // Close the popup
};
return buttonContainer;
}
// Function to hide and destroy the upgrade popup
function hideUpgradePopup() {
if (upgradePopup) {
upgradePopup.destroy();
upgradePopup = null;
}
isUpgradePopupActive = false;
// Switch back to game music with fade effect
if (currentMusicPlaying !== 'Gamemusic' && !musicFadeInProgress) {
musicFadeInProgress = true;
// Fade out upgrade theme
LK.playMusic('UpgradeTheme', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// After fade out completes, start game music with fade in
LK.setTimeout(function () {
LK.playMusic('Gamemusic', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
currentMusicPlaying = 'Gamemusic';
musicFadeInProgress = false;
}, 850);
}
// Resume game logic (handled by checking flag in game.update)
}
// --- 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
}
};
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.'
};
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);
// --- 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 = 20;
} // Baby Dragon unlock score
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);
// --- Input Event Handlers ---
// Handles touch/mouse press down event on the game area.
game.down = function (x, y, obj) {
// Record the starting position of the drag in game coordinates.
var gamePos = game.toLocal({
x: x,
y: y
});
dragStartX = gamePos.x;
dragStartY = gamePos.y;
// Potential enhancement: Show an aiming indicator originating from FIRING_POS_X, FIRING_POS_Y towards the drag point.
};
// Handles touch/mouse move event while dragging on the game area.
game.move = function (x, y, obj) {
// Only process if a drag is currently active.
if (dragStartX !== null) {
var gamePos = game.toLocal({
x: x,
y: y
});
// Potential enhancement: Update the aiming indicator based on the current drag position (gamePos).
}
};
// Handles touch/mouse release event on the game area.
game.up = function (x, y, obj) {
// Only process if a drag was active.
if (dragStartX !== null && cooldownTimer <= 0) {
// Only allow firing if not on cooldown.
var gamePos = game.toLocal({
x: x,
y: y
});
var dragEndX = gamePos.x;
var dragEndY = gamePos.y;
// Calculate the difference between the firing position and the release point to determine direction.
// A longer drag could potentially mean more power, but we'll keep it simple: direction only.
var dx = dragEndX - FIRING_POS_X;
var dy = dragEndY - FIRING_POS_Y;
// Avoid division by zero or zero vector if start and end points are the same.
if (dx === 0 && dy === 0) {
// Optionally handle this case, e.g., fire straight up or do nothing.
// Let's fire straight up if drag distance is negligible.
dy = -1;
}
// Calculate the angle using atan2. Note the order (dx, dy) and the negation of dy
// because the Y-axis is inverted in screen coordinates (positive Y is down).
var angle = Math.atan2(dx, -dy);
// Create a new arrow instance with the calculated angle.
var newArrow = new Arrow(angle);
newArrow.x = FIRING_POS_X;
newArrow.y = FIRING_POS_Y;
// Initialize last position for state tracking (e.g., off-screen detection)
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// Add the arrow to the game scene and the tracking array.
game.addChild(newArrow);
arrows.push(newArrow);
if (multiShotEnabled) {
// Fire a second arrow with a slight angle offset
var angle2 = angle + Math.PI / 12; // Offset by 15 degrees
var newArrow2 = new Arrow(angle2);
newArrow2.x = FIRING_POS_X;
newArrow2.y = FIRING_POS_Y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
game.addChild(newArrow2);
arrows.push(newArrow2);
// Fire a third arrow with the opposite angle offset
var angle3 = angle - Math.PI / 12; // Offset by -15 degrees
var newArrow3 = new Arrow(angle3);
newArrow3.x = FIRING_POS_X;
newArrow3.y = FIRING_POS_Y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
LK.getSound('shoot').play(); // Play shooting sound.
arrowsFired++; // Increment arrow count.
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display.
// Check if cooldown needs to start based on Quiver upgrade
if (arrowsFired >= maxArrowsBeforeCooldown) {
cooldownTimer = cooldownDuration; // Start cooldown (duration affected by Faster Reload upgrade)
arrowsFired = 0; // Reset arrow count.
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display.
LK.getSound('Reload').play(); // Play reload sound.
}
// Reset drag state.
dragStartX = null;
dragStartY = null;
// Potential enhancement: Hide the aiming indicator.
}
};
// --- Main Game Update Loop ---
// This function is called automatically by the LK engine on every frame (tick).
game.update = function () {
// --- Upgrade System Check ---
var currentScore = LK.getScore();
// Check if score reached a multiple of 10 and is higher than the last upgrade score
if (!isUpgradePopupActive && currentScore > 0 && currentScore % 10 === 0 && currentScore !== lastUpgradeScore) {
lastUpgradeScore = currentScore; // Mark this score level as having triggered an upgrade offer
showUpgradePopup(); // Show the upgrade selection screen
}
// --- Pause Game Logic if Upgrade Popup is Active ---
if (isUpgradePopupActive) {
// Potential: Update UI animations if any
return; // Skip the rest of the game update loop
}
// --- Resume Game Logic ---
if (isUpgradePopupActive) {
return; // Skip enemy movement and spawning if upgrade popup is active
}
// Decrease cooldown timer if active.
if (cooldownTimer > 0) {
cooldownTimer--;
ammoTxt.setText('Reloading...'); // Show reloading status
} else if (ammoTxt.text !== 'Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)) {
// Ensure ammo display is correct if not reloading
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
}
// 1. Update and check Arrows
for (var i = arrows.length - 1; i >= 0; i--) {
var arrow = arrows[i];
// Note: LK engine calls arrow.update() automatically as it's added to the game stage.
// Initialize last position if it hasn't been set yet (first frame).
if (arrow.lastY === undefined) {
arrow.lastY = arrow.y;
}
if (arrow.lastX === undefined) {
arrow.lastX = arrow.x;
}
// Check for collisions between the current arrow and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (arrow.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Calculate damage, applying Green Killer bonus if applicable
var damageToDeal = arrow.damage;
if (greenKillerEnabled && enemy.tag === 'Green') {
damageToDeal *= 1.5; // Apply 50% damage bonus
}
// Apply damage to the enemy and check if it was defeated
var enemyDefeated = enemy.takeDamage(damageToDeal, arrow); // Pass the arrow object as source
// Check if the arrow was reflected (takeDamage returns false and arrow is destroyed)
if (!enemyDefeated && !arrow.parent) {
// Arrow was reflected, remove it from the array and stop processing
arrows.splice(i, 1);
hitEnemy = true; // Mark that this arrow is done
break; // Stop checking this arrow against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Handle arrow piercing
arrow.pierceLeft--; // Decrease remaining pierces
if (arrow.pierceLeft <= 0) {
// Arrow has no pierces left, destroy it
arrow.destroy();
arrows.splice(i, 1); // Remove arrow from array.
hitEnemy = true; // Mark that this arrow is done
break; // Stop checking this arrow against other enemies as it's destroyed
} else {
// Arrow pierced this enemy and can continue
// We don't set hitEnemy = true here because the arrow continues
// We don't break because it might hit another enemy in the same frame further along its path
// Apply Green Slowdown if applicable and arrow is still piercing
if (!enemyDefeated && greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
}
}
}
// If the arrow hit an enemy, it was destroyed, so skip to the next arrow.
if (hitEnemy) {
continue;
}
// Check if the arrow has gone off-screen (top, left, or right).
// Use transition detection: check if it was on screen last frame and is off screen now.
var wasOnScreen = arrow.lastY > -arrow.height / 2 && arrow.lastX > -arrow.width / 2 && arrow.lastX < GAME_WIDTH + arrow.width / 2;
var isOffScreen = arrow.y < -arrow.height / 2 ||
// Off top edge
arrow.x < -arrow.width / 2 ||
// Off left edge
arrow.x > GAME_WIDTH + arrow.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Arrow is off-screen, destroy it and remove from the array.
arrow.destroy();
arrows.splice(i, 1);
} else if (!isOffScreen) {
// Update last known position only if the arrow is still potentially on screen
arrow.lastY = arrow.y;
arrow.lastX = arrow.x;
}
}
// 2. Update and check Cannonballs
for (var cb = cannonballs.length - 1; cb >= 0; cb--) {
var cannonball = cannonballs[cb];
// Note: LK engine calls cannonball.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (cannonball.lastY === undefined) {
cannonball.lastY = cannonball.y;
}
if (cannonball.lastX === undefined) {
cannonball.lastX = cannonball.x;
}
// Check for collisions between the current cannonball and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (cannonball.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply damage to the enemy and check if it was defeated
var enemyDefeated = enemy.takeDamage(cannonball.damage, cannonball); // Pass cannonball as source
// Check if the cannonball had no effect (e.g. Dark War Elephant immunity) and is still in game
if (!enemyDefeated && cannonball.parent) {
// Cannonball had no effect, but wasn't destroyed by a normal hit.
// It might have been immune. If so, we still destroy the cannonball.
cannonball.destroy();
cannonballs.splice(cb, 1);
hitEnemy = true; // Mark that this cannonball is done
break; // Stop checking this cannonball against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Cannonballs are destroyed on hit
cannonball.destroy();
// Apply Green Slowdown if applicable before destroying cannonball
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
cannonball.destroy();
cannonballs.splice(cb, 1); // Remove cannonball from array.
hitEnemy = true; // Mark that this cannonball is done
break; // Stop checking this cannonball against other enemies as it's destroyed
}
}
// If the cannonball hit an enemy, it was destroyed, so skip to the next cannonball.
if (hitEnemy) {
continue;
}
// Check if the cannonball has gone off-screen.
var wasOnScreen = cannonball.lastY > -cannonball.height / 2 && cannonball.lastX > -cannonball.width / 2 && cannonball.lastX < GAME_WIDTH + cannonball.width / 2;
var isOffScreen = cannonball.y < -cannonball.height / 2 ||
// Off top edge
cannonball.x < -cannonball.width / 2 ||
// Off left edge
cannonball.x > GAME_WIDTH + cannonball.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Cannonball is off-screen, destroy it and remove from the array.
cannonball.destroy();
cannonballs.splice(cb, 1);
} else if (!isOffScreen) {
// Update last known position only if the cannonball is still potentially on screen
cannonball.lastY = cannonball.y;
cannonball.lastX = cannonball.x;
}
}
// 3. Update and check Enemies
for (var k = enemies.length - 1; k >= 0; k--) {
var enemy = enemies[k];
// Note: LK engine calls enemy.update() automatically.
// Initialize last position if not set.
if (enemy.lastY === undefined) {
enemy.lastY = enemy.y;
}
// Check if the enemy has reached or passed the bastion line.
// Use transition detection: was the enemy's bottom edge above the line last frame,
// and is it on or below the line now?
var enemyBottomY = enemy.y + enemy.height / 2;
var enemyLastBottomY = enemy.lastY + enemy.height / 2;
var wasAboveBastion = enemyLastBottomY < BASTION_Y;
var isAtOrBelowBastion = enemyBottomY >= BASTION_Y;
if (wasAboveBastion && isAtOrBelowBastion) {
// Enemy reached the bastion! Game Over.
LK.getSound('gameOverSfx').play(); // Play game over sound effect.
LK.showGameOver(); // Trigger the engine's game over sequence.
// LK.showGameOver handles game state reset, no need to manually clear arrays here.
return; // Exit the update loop immediately as the game is over.
} else {
// Update last known position if game is not over for this enemy.
enemy.lastY = enemy.y;
}
}
// 3. Update and check Swordsmen (allies)
for (var l = swordsmen.length - 1; l >= 0; l--) {
var swordsman = swordsmen[l];
// Swordsman update method handles its own lifetime and attacking
// Check if the swordsman has been destroyed by its lifetime timer
if (!swordsman.parent) {
// If it no longer has a parent, it has been destroyed
swordsmen.splice(l, 1); // Remove swordsman from array
}
}
// 4. Update and check Cannons (allies)
for (var m = cannons.length - 1; m >= 0; m--) {
var cannon = cannons[m];
// Cannons do not have a lifetime timer or destruction logic in this basic version
// If they had, we'd check !cannon.parent and splice here as in Swordsmen
}
// 5. Update and check Wizard Towers (allies)
for (var wt = wizardTowers.length - 1; wt >= 0; wt--) {
var tower = wizardTowers[wt];
// Wizard towers do not have a lifetime timer or destruction logic in this basic version
// If they had, we'd check !tower.parent and splice here
}
// 5.5 Update and check Viking Allies
for (var va = vikingAllies.length - 1; va >= 0; va--) {
var viking = vikingAllies[va];
// Vikings do not have a lifetime timer or destruction logic in this version
// LK engine calls viking.update() automatically.
}
// 5.6 Update and check Angel of Lights (allies)
for (var angelIdx = angelOfLights.length - 1; angelIdx >= 0; angelIdx--) {
var angel = angelOfLights[angelIdx];
// Angels do not have a lifetime timer or destruction logic in this version
// LK engine calls angel.update() automatically.
}
// 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;
if (enemy.tag === 'Green') {
damageToDealDart *= 2; // Double damage against Green enemies
}
// Apply damage and check if defeated
var enemyDefeated = enemy.takeDamage(damageToDealDart, dart); // Pass dart as source
// Check if the dart was reflected (takeDamage returns false and dart is destroyed)
if (!enemyDefeated && !dart.parent) {
// Dart was reflected, remove it from the array and stop processing
darts.splice(dartIdx, 1);
hitEnemyDart = true; // Mark that this dart is done
break; // Stop checking this dart against other enemies
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Darts are destroyed on hit
dart.destroy();
// Apply Green Slowdown if applicable before destroying dart
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
darts.splice(dartIdx, 1); // Remove dart from array.
hitEnemyDart = true; // Mark that this dart is done
break; // Stop checking this dart against other enemies
}
}
// If the dart hit and was destroyed, skip to the next dart.
if (hitEnemyDart) {
continue;
}
// Check if the dart has gone off-screen.
var wasOnScreenDart = dart.lastY > -dart.height / 2 && dart.lastX > -dart.width / 2 && dart.lastX < GAME_WIDTH + dart.width / 2;
var isOffScreenDart = dart.y < -dart.height / 2 || dart.y > GAME_HEIGHT + dart.height / 2 || dart.x < -dart.width / 2 || dart.x > GAME_WIDTH + dart.width / 2;
if (wasOnScreenDart && isOffScreenDart) {
dart.destroy();
darts.splice(dartIdx, 1);
} else if (!isOffScreenDart) {
dart.lastY = dart.y;
dart.lastX = dart.x;
}
}
// 7. Spawn new Enemies periodically
// Use LK.ticks and the spawn interval. Ensure interval doesn't go below minimum.
if (LK.ticks % Math.max(minEnemySpawnInterval, Math.floor(enemySpawnInterval)) === 0) {
var currentScore = LK.getScore();
var baseSpeedForSpawn = Math.min(maxEnemySpeed, currentEnemySpeed); // Base speed adjusted by game difficulty progression
// Determine if this spawn *could* be a boss (e.g., every 7th spawn after score 10)
var potentialBoss = (enemies.length + 1) % 7 === 0 && currentScore >= 10;
var typeToSpawn = 'swordsman'; // Default type
var enemyHealth = 1; // Default health
var enemyDodgeChance = 0; // Default dodge chance
var enemySpeed = baseSpeedForSpawn; // Start with base speed
// Determine base type based on score thresholds and randomness
var possibleTypes = ['swordsman'];
if (currentScore >= 10) {
possibleTypes.push('spearman');
}
if (currentScore >= 15) {
possibleTypes.push('thief');
}
if (currentScore >= 23) {
possibleTypes.push('knight');
}
if (currentScore >= 25) {
possibleTypes.push('wizard');
} // Wizards start appearing at score 25
if (currentScore >= 30) {
possibleTypes.push('shield');
} // Shields start appearing at score 30
if (currentScore >= 35) {
possibleTypes.push('shaman'); // Shaman appears at score 35
console.log("Shaman added to possible enemy types");
}
if (currentScore >= 55) {
possibleTypes.push('dark_bowman'); // Dark bowman appears at score 55
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');
}
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
}
if (currentScore >= 75) {
possibleTypes.push('baby_dragon'); // Baby Dragon appears at score 75
}
// 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 - 75) / 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 - 75) / 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;
}
};
// --- 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