User prompt
Add Viking Ally upgrade definition
User prompt
Add new upgrades: Refined projectiles (All projectiles that aren't arrows deal +5 damage and travel faster); Viking ally (You gain a new ally that throws an axe that pierces through 3 enemies regardless of their health); Green slowdown (All projectiles slow down green enemies by 10% for 10 seconds; this effect doesn't stack)
User prompt
Add new upgrade: Green killer (all projectiles deal +50% more damage to green enemies)
User prompt
Add new upgrade: Green killer (all projectiles deal +50% more damage to green enemies)
User prompt
Wizard, spearman and War elephant are now tagged as "Green" enemies
User prompt
Add score increment if Swordsman kills enemies but remove score increment when swordsman spawns
User prompt
For some reason, you gain score If a swordsman ally spawns or dies; fix that
User prompt
For some reason, you gain score If a swordsman ally spawns or dies; fix that
/**** * Classes ****/ /** * Represents an allied archer that shoots arrows independently. */ var ArcherAlly = Container.expand(function () { var self = Container.call(this); // Create and attach the archer graphic asset var graphics = self.attachAsset('Archer', { anchorX: 0.5, anchorY: 0.5 }); // No rotation needed for 'Archer' asset as it's already upright and flipped self.fireTimer = 0; // Timer for shooting self.fireInterval = 180; // Shoot every 3 seconds (3 * 60 ticks) /** * Update method called each game tick by the LK engine. * Handles firing logic. */ self.update = function () { self.fireTimer++; var effectiveFireInterval = Math.max(60, self.fireInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.fireTimer >= effectiveFireInterval) { self.fireTimer = 0; // Reset timer // Find the closest enemy to shoot at var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate distance to the enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, fire an arrow if (closestEnemy) { // Calculate angle towards the closest enemy var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); // Create a new arrow instance var newArrow = new Arrow(angle); // Apply multi-shot to allies if the upgrade is enabled for the player if (multiShotEnabled) { var angle2 = angle + Math.PI / 12; // Offset by 15 degrees var newArrow2 = new Arrow(angle2); newArrow2.x = self.x; newArrow2.y = self.y; newArrow2.lastY = newArrow2.y; newArrow2.lastX = newArrow2.x; game.addChild(newArrow2); arrows.push(newArrow2); var angle3 = angle - Math.PI / 12; // Offset by -15 degrees var newArrow3 = new Arrow(angle3); newArrow3.x = self.x; newArrow3.y = self.y; newArrow3.lastY = newArrow3.y; newArrow3.lastX = newArrow3.x; game.addChild(newArrow3); arrows.push(newArrow3); } newArrow.x = self.x; newArrow.y = self.y; // Ally arrows do not count towards the player's reload counter // The Arrow class handles piercing level based on player upgrade, but the ally doesn't benefit from player reload. // For simplicity, we'll let the ally benefit from player's piercing upgrade. newArrow.lastY = newArrow.y; newArrow.lastX = newArrow.x; // Add the arrow to the game scene and the tracking array. game.addChild(newArrow); arrows.push(newArrow); // Add to the same arrows array for collision detection // Ally doesn't play the 'shoot' sound } } }; return self; // Return self for potential inheritance }); // Sound when an enemy reaches the bastion // No plugins needed for this version of the game. /** * Represents an Arrow fired by the player. * @param {number} angle - The angle in radians at which the arrow is fired. */ var Arrow = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the arrow graphic asset var graphics = self.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align arrow graphic with direction self.speed = 30; // Speed of the arrow in pixels per tick self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) self.pierceLeft = arrowPierceLevel; // How many more enemies this arrow can pierce self.damage = arrowDamage; // Damage dealt by this arrow self.isPoison = poisonShotsEnabled; // Flag if this arrow applies poison self.targetEnemy = null; // Potential target for aimbot self.seekSpeed = 0.1; // How quickly the arrow adjusts its direction to seek if (aimbotEnabled) { // Find the closest enemy to seek if aimbot is enabled var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { self.targetEnemy = closestEnemy; } } /** * Update method called each game tick by the LK engine. * Moves the arrow based on its velocity. */ self.update = function () { if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) { // If aimbot is enabled, a target exists and is still in the game, seek it var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y)); // Smoothly adjust the arrow's angle towards the target angle var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation // Normalize angle difference to be between -PI and PI if (angleDiff > Math.PI) { angleDiff -= 2 * Math.PI; } if (angleDiff < -Math.PI) { angleDiff += 2 * Math.PI; } // Interpolate angle var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed; self.rotation = newAngle + Math.PI / 2; self.vx = Math.sin(newAngle) * self.speed; self.vy = -Math.cos(newAngle) * self.speed; } else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) { // If aimbot is enabled but current target is gone, find a new target self.targetEnemy = null; // Clear old target var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { self.targetEnemy = closestEnemy; } } self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents a Cannon ally that targets the strongest enemy. */ var Cannon = Container.expand(function () { var self = Container.call(this); // Create and attach the cannon graphic asset (need to add a new asset for this) // For now, using a placeholder asset, will need a 'cannon_asset' var graphics = self.attachAsset('cannon_asset', { // Use actual cannon asset anchorX: 0.5, anchorY: 0.5 }); self.attackRange = 800; // Long attack range self.attackDamage = 10; // High damage self.attackInterval = 300; // Attack every 5 seconds (300 ticks) self.attackTimer = 0; // Timer for attacks self.rotation = 0; // For aiming visual if implemented later /** * Update method called each game tick by the LK engine. * Handles attacking the strongest enemy. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the strongest enemy (highest health) var strongestEnemy = null; var maxHealth = -1; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.health > maxHealth) { maxHealth = enemy.health; strongestEnemy = enemy; } } // If a strongest enemy is found within range, attack it if (strongestEnemy) { // Calculate angle towards the strongest enemy var angle = Math.atan2(strongestEnemy.x - self.x, -(strongestEnemy.y - self.y)); // Create a new cannonball instance var newCannonball = new Cannonball(angle); // Position the cannonball at the cannon's location newCannonball.x = self.x; newCannonball.y = self.y; newCannonball.lastY = newCannonball.y; // Initialize lastY for state tracking newCannonball.lastX = newCannonball.x; // Initialize lastX for state tracking // Add the cannonball to the game scene and the tracking array. game.addChild(newCannonball); cannonballs.push(newCannonball); // Add to the new cannonballs array // Optional: Add a visual/sound effect for cannon shot here later } } }; return self; // Return self for potential inheritance }); /** * Represents a Cannonball fired by a Cannon. * @param {number} angle - The angle in radians at which the cannonball is fired. */ var Cannonball = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the cannonball graphic asset var graphics = self.attachAsset('cannonball', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align cannonball graphic with direction self.speed = 20; // Base speed of the cannonball self.damage = 10; // Base damage dealt by this cannonball // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.speed += 5; self.damage += 5; } self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) /** * Update method called each game tick by the LK engine. * Moves the cannonball based on its velocity. */ self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents an Enemy attacker moving towards the bastion. * @param {string} type - The type of enemy ('swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight'). * @param {number} speed - The final calculated speed of the enemy. * @param {number} health - The initial and maximum health of the enemy. * @param {number} dodgeChance - The chance (0 to 1) for the enemy to dodge an attack. */ var Enemy = Container.expand(function (type, speed, health, dodgeChance) { var self = Container.call(this); // Set asset based on type var assetId = 'enemy'; // Default swordsman asset if (type === 'knight') { assetId = 'knight'; } else if (type === 'elite_knight') { assetId = 'elite_knight_asset'; // Use specific asset for elite knight } else if (type === 'thief') { assetId = 'thief'; } else if (type === 'boss') { assetId = 'boss'; } else if (type === 'shield') { assetId = 'shield_enemy'; // Use specific asset for shield } else if (type === 'wizard') { assetId = 'wizard_enemy'; // Use specific asset for wizard } else if (type === 'spearman') { assetId = 'spearman'; // Use specific asset for spearman } else if (type === 'war_elephant') { assetId = 'war_elephant'; // Use specific asset for war elephant } var graphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); self.type = type; // 'swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight' self.speed = speed; // Final calculated speed self.health = health; self.maxHealth = health; // Store max health for visual feedback self.dodgeChance = dodgeChance || 0; // Default to 0 if undefined self.poisonStacks = 0; // Number of poison stacks self.poisonTimer = 0; // Timer for poison damage self.poisonDamagePerTick = 0.05; // Damage per tick per stack self.slowTimer = 0; // Timer for slowdown effect self.currentSlowAmount = 1.0; // Multiplier for current slowdown effect (1.0 means no slow) // 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.tag = 'Green'; } // 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 }; //{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 } self.health -= damage; // Check if the damage source was a poison arrow and apply poison stacks if (source && source.isPoison) { // Use the 'source' parameter self.poisonStacks++; // Increase poison stacks self.poisonTimer = 0; // Reset timer to apply damage immediately } // Check if the damage source was a Magic Ball and apply slowdown if (source && source instanceof MagicBall) { self.slowTimer = source.slowDuration; // Apply slowdown duration self.currentSlowAmount = source.slowAmount; // Apply slowdown amount // Optional: Add visual feedback for slowdown here later } // Check if the damage source should apply Green Slowdown if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0) { // Apply Green Slowdown only if the upgrade is active, enemy is Green, and not already slowed self.slowTimer = 10 * 60; // 10 seconds * 60 ticks/sec self.currentSlowAmount = 0.9; // 10% slowdown // Optional: Add visual feedback for green slowdown } // Check if enemy was defeated by this damage var defeated = self.health <= 0; if (defeated && self.type === 'war_elephant') { // If War Elephant is defeated, spawn 5 spearmen for (var i = 0; i < 5; i++) { var spawnPadding = 100; // Padding from edge var spawnX = self.x + (Math.random() * 200 - 100); // Spawn around elephant's x var spawnY = self.y + (Math.random() * 100 - 50); // Spawn around elephant's y, slightly forward // Ensure spawns are within game bounds spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX)); spawnY = Math.max(spawnPadding, Math.min(BASTION_Y - 50, spawnY)); // Don't spawn too close to bastion var newSpearman = new Enemy('spearman', currentEnemySpeed * enemySpeedMultiplier, 10, 0); // Base spearman stats newSpearman.x = spawnX; newSpearman.y = spawnY; newSpearman.lastY = newSpearman.y; game.addChild(newSpearman); enemies.push(newSpearman); } } // 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); } //{11} // Modified original line identifier location return self; // Return self for potential inheritance }); /** * Represents a Magic Ball projectile fired by a Wizard Tower. * @param {number} angle - The angle in radians at which the magic ball is fired. */ var MagicBall = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the magic ball graphic asset var graphics = self.attachAsset('magic_ball_asset', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align magic ball graphic with direction self.speed = 15; // Base speed of the magic ball self.damage = 2; // Base damage dealt by this magic ball // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.speed += 5; self.damage += 5; } self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) self.slowDuration = 0; // Duration of slowdown applied on hit self.slowAmount = 0; // Multiplier for enemy speed on hit self.targetEnemy = null; // Track the specific enemy the magic ball is seeking self.seekSpeed = 0.05; // How quickly the magic ball adjusts its direction to seek /** * Update method called each game tick by the LK engine. * Moves the magic ball based on its velocity and seeks target if Aimbot is enabled. */ self.update = function () { if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) { // If aimbot is enabled, a target exists and is still in the game, seek it var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y)); // Smoothly adjust the magic ball's angle towards the target angle var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation // Normalize angle difference to be between -PI and PI if (angleDiff > Math.PI) { angleDiff -= 2 * Math.PI; } if (angleDiff < -Math.PI) { angleDiff += 2 * Math.PI; } // Interpolate angle var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed; self.rotation = newAngle + Math.PI / 2; self.vx = Math.sin(newAngle) * self.speed; self.vy = -Math.cos(newAngle) * self.speed; } else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) { // If aimbot is enabled but current target is gone, find a new target self.targetEnemy = null; // Clear old target var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { self.targetEnemy = closestEnemy; } } self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents a Swordsman ally that attacks enemies in melee range. */ var Swordsman = Container.expand(function () { var self = Container.call(this); // Create and attach the swordsman graphic asset (reusing enemy asset for simplicity) var graphics = self.attachAsset('swordsmanAlly', { anchorX: 0.5, anchorY: 0.5 }); // No tint needed as ally asset has unique colors graphics.scale.set(1.0); // Use the asset's intended scale self.attackRange = 150; // Melee attack range self.attackDamage = 1; // Damage per hit self.attackInterval = 60; // Attack every 1 second (60 ticks) self.attackTimer = 0; // Timer for attacks self.lifetime = 10 * 60; // Lifetime in ticks (10 seconds * 60 ticks/sec) self.lifetimeTimer = 0; // Timer for lifetime /** * Update method called each game tick by the LK engine. * Handles attacking and lifetime. */ self.update = function () { self.attackTimer++; self.lifetimeTimer++; // Check for lifetime var effectiveAttackInterval = Math.max(30, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.5s if (self.lifetimeTimer >= self.lifetime) { self.destroy(); // Remove from the swordsmen array in the main game loop return; // Stop updating this instance } // Find the closest enemy to chase or attack var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate distance to the enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy exists, chase it or attack if within range if (closestEnemy) { // Movement speed toward enemies var moveSpeed = 5; // If not within attack range, move toward the enemy if (closestDistance > self.attackRange) { // Calculate direction vector to enemy var dirX = closestEnemy.x - self.x; var dirY = closestEnemy.y - self.y; // Normalize the direction vector var length = Math.sqrt(dirX * dirX + dirY * dirY); dirX = dirX / length; dirY = dirY / length; // Move toward the enemy self.x += dirX * moveSpeed; self.y += dirY * moveSpeed; } // If within attack range and attack timer is ready, attack else if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Apply damage to the enemy and check if it was defeated var enemyDefeated = closestEnemy.takeDamage(self.attackDamage, self); // Pass the swordsman as source if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score when enemy is defeated by swordsman. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy closestEnemy.destroy(); // Remove from the enemies array in the main game loop } // Optional: Add a visual/sound effect for attack here later } } }; return self; // Return self for potential inheritance }); /** * Represents a Viking ally that throws piercing axes. */ var VikingAlly = Container.expand(function () { var self = Container.call(this); var graphics = self.attachAsset('viking_ally', { anchorX: 0.5, anchorY: 0.5 }); self.attackRange = 1000; // Long 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 = 800; // Long attack range self.attackDamage = 0.5; // Low damage, primary effect is slowdown self.attackInterval = 120; // Attack every 2 seconds (120 ticks) self.attackTimer = 0; // Timer for attacks self.slowDuration = 180; // Duration of slowdown effect in ticks (3 seconds) self.slowAmount = 0.5; // Multiplier for enemy speed (0.5 means 50% slower) /** * Update method called each game tick by the LK engine. * Handles attacking enemies. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the closest enemy within range to shoot at var closestEnemy = null; var closestDistance = self.attackRange; // Limit to attack range for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate distance to the enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, fire a magic ball if (closestEnemy) { // Calculate angle towards the closest enemy var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); // Create a new magic ball instance var newMagicBall = new MagicBall(angle); // Position the magic ball at the tower's location newMagicBall.x = self.x; newMagicBall.y = self.y; newMagicBall.lastY = newMagicBall.y; // Initialize lastY for state tracking newMagicBall.lastX = newMagicBall.x; // Initialize lastX for state tracking newMagicBall.slowDuration = self.slowDuration; // Pass slow duration to the magic ball newMagicBall.slowAmount = self.slowAmount; // Pass slow amount to the magic ball // Add the magic ball to the game scene and the tracking array. game.addChild(newMagicBall); magicBalls.push(newMagicBall); // Add to the magicBalls array // Optional: Add a visual/sound effect for magic ball shot here later } } }; return self; // Return self for potential inheritance }); /**** * Initialize Game ****/ // Create the main game instance with a dark background color. var game = new LK.Game({ backgroundColor: 0x101030 // Dark blue/purple background }); /**** * Game Code ****/ // Placeholder ID, use actual asset, larger size // Placeholder ID, use actual asset // Add cannonball asset // Placeholder ID, need actual asset // Sound for an arrow hitting an enemy // Sound for firing an arrow // Grey line representing the bastion defense line // Red enemy shape // Yellow arrow shape // LK engine will automatically initialize these based on usage. // Define shapes and sounds needed for the game. // 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 dragStartX = null; // Starting X coordinate of a touch/mouse drag. var dragStartY = null; // Starting Y coordinate of a touch/mouse drag. var enemySpawnInterval = 120; // Initial ticks between enemy spawns (2 seconds at 60 FPS). var arrowsFired = 0; // Counter for arrows fired since the last cooldown. var cooldownTimer = 0; // Timer in ticks for the cooldown period. var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec). Min duration enforced in upgrade. var maxArrowsBeforeCooldown = 15; // Max arrows before reload (Quiver upgrade) var arrowPierceLevel = 1; // How many enemies an arrow can pierce (Piercing Shots upgrade) var arrowDamage = 1; // Base damage for arrows (Heat-tipped arrows upgrade) var enemySpeedMultiplier = 1.0; // Multiplier for enemy speed (Sabotage upgrade) Min multiplier enforced in upgrade. var minEnemySpawnInterval = 30; // Minimum ticks between enemy spawns (0.5 seconds). var enemySpawnRateDecrease = 0.08; // Amount to decrease spawn interval each time an enemy spawns. var currentEnemySpeed = 3; // Initial base speed of enemies. var maxEnemySpeed = 12; // Maximum base speed enemies can reach. var enemySpeedIncrease = 0.015; // Amount to increase enemy base speed each time an enemy spawns. var archerAllies = []; // Array to hold ArcherAlly instances var swordsmen = []; // Array to hold Swordsman instances var cannonballs = []; // Array to hold Cannonball instances var multiShotEnabled = false; // Flag for More Shots upgrade var poisonShotsEnabled = false; // Flag for Poison Shots upgrade var cannons = []; // Array to hold Cannon instances var allyAttackSpeedMultiplier = 1.0; // Multiplier for ally attack speed var wizardTowers = []; // Array to hold WizardTower instances var magicBalls = []; // Array to hold MagicBall instances var aimbotEnabled = false; // Flag for Aimbot upgrade var greenKillerEnabled = false; // Flag for Green Killer upgrade var refinedProjectilesEnabled = false; // Flag for Refined Projectiles upgrade var greenSlowdownEnabled = false; // Flag for Green Slowdown upgrade var vikingAllies = []; // Array to hold VikingAlly instances var vikingAxes = []; // Array to hold VikingAxe instances // --- Upgrade System --- var lastUpgradeScore = -1; // Score at which the last upgrade was offered var isUpgradePopupActive = false; // Flag indicating if the upgrade choice popup is visible var upgradePopup = null; // Container for the upgrade UI elements // Helper function to apply Sabotage effect to existing enemies function applySabotage() { for (var i = 0; i < enemies.length; i++) { // We assume Enemy class will use enemySpeedMultiplier when calculating effective speed // If Enemy.speed stores base speed, we might need an effectiveSpeed method or update speed directly. // For simplicity, let's assume Enemy.update uses the global multiplier. // If direct modification is needed: enemies[i].speed = baseSpeed * enemySpeedMultiplier; } // Also update the current base speed calculation baseline if necessary, though modifying the multiplier should suffice. } // Helper function placeholder for adding Archer Ally function addArcherAlly() { // Implementation requires ArcherAlly class definition console.log("Archer Ally upgrade chosen!"); var ally = new ArcherAlly(); // Position the ally next to the player's firing position, slightly offset. ally.x = FIRING_POS_X + 150; // Position to the right of the player ally.y = FIRING_POS_Y; game.addChild(ally); archerAllies.push(ally); // Add to the new archerAllies array } // Helper function to add a Cannon ally function addCannon() { console.log("Cannon upgrade chosen!"); var newCannon = new Cannon(); // Position the cannon near the bastion line newCannon.x = GAME_WIDTH / 2 - 300; // Example position to the left of center newCannon.y = BASTION_Y - 100; // Position above the bastion line game.addChild(newCannon); cannons.push(newCannon); // Add to the cannons array } // Helper function to add a Wizard Tower function addWizardTower() { console.log("Wizard Tower upgrade chosen!"); var newTower = new WizardTower(); // Position the wizard tower near the bastion line, offset from center newTower.x = GAME_WIDTH / 2 + 300; // Example position to the right of center newTower.y = BASTION_Y - 100; // Position above the bastion line game.addChild(newTower); wizardTowers.push(newTower); // Add to the wizardTowers array } // Helper function to add a Viking Ally function addVikingAlly() { console.log("Viking Ally upgrade chosen!"); var newViking = new VikingAlly(); // Position the viking near the bastion line, offset from cannons/towers newViking.x = GAME_WIDTH / 2; // Center for now newViking.y = BASTION_Y - 100; // Position above the bastion line game.addChild(newViking); vikingAllies.push(newViking); // Add to the vikingAllies array } // Helper function placeholder for adding Swordsman function addSwordsman() { // Create a swordsman tower at the bastion line var tower = new Container(); var towerGraphics = tower.attachAsset('swordsmanTower', { anchorX: 0.5, anchorY: 0.5 }); tower.x = FIRING_POS_X; // Position in the center horizontally tower.y = BASTION_Y - 50; // Position just above the bastion line game.addChild(tower); // Set up interval to spawn swordsmen from the tower var spawnInterval = LK.setInterval(function () { var newSwordsman = new Swordsman(); // Position the swordsman near the tower with a slight random offset newSwordsman.x = tower.x + (Math.random() * 100 - 50); // Random offset of ±50px newSwordsman.y = tower.y; game.addChild(newSwordsman); swordsmen.push(newSwordsman); // Add to the swordsmen array }, 3000); // Spawn a swordsman every 3 seconds // Swordsman spawn does not grant score // Store the interval reference in the tower to potentially clear it later tower.spawnInterval = spawnInterval; } // Define all possible upgrades var allUpgrades = [{ id: 'faster_reload', name: 'Faster Reload', description: 'Decrease reload time by 20%', apply: function apply() { cooldownDuration = Math.max(60, Math.floor(cooldownDuration * 0.8)); } }, // Min 1 sec cooldown { id: 'piercing_shots', name: 'Piercing Shots', description: 'Arrows pierce +1 enemy', apply: function apply() { arrowPierceLevel++; } }, { id: 'quiver', name: 'Quiver', description: '+5 Arrows before reload', apply: function apply() { maxArrowsBeforeCooldown += 5; } }, { id: 'heat_tipped', name: 'Heat-tipped Arrows', description: 'Arrows deal +1 damage', apply: function apply() { arrowDamage += 1; console.log("Heat-tipped Arrows upgrade chosen - damage increased to " + arrowDamage); } }, { id: 'archer_ally', name: 'Archer Ally', description: 'Gain an allied archer', apply: addArcherAlly }, { id: 'sabotage', name: 'Sabotage', description: 'Enemies 10% slower', apply: function apply() { enemySpeedMultiplier = Math.max(0.5, enemySpeedMultiplier * 0.9); applySabotage(); } }, // Max 50% slow { id: 'swordsman', name: 'Swordsman', description: 'Gain a melee defender', apply: addSwordsman }, { id: 'more_shots', name: 'More Shots', description: 'Shoot 2 arrows at once', apply: function apply() { multiShotEnabled = true; // Assuming a global flag for this } }, { id: 'poison_shots', name: 'Poison Shots', description: 'Shots deal damage over time', apply: function apply() { poisonShotsEnabled = true; // Assuming a global flag for this } }, { id: 'cannon', name: 'Cannon', description: 'Gain a cannon that targets the strongest enemy', apply: addCannon // Assuming a helper function addCannon }, { id: 'ally_atk_speed', name: 'Ally ATK Speed', description: 'All allies attack faster', apply: function apply() { allyAttackSpeedMultiplier *= 0.8; // Assuming a global multiplier // Apply to existing allies as well in their update logic or a helper function } }, { id: 'wizard_tower', name: 'Wizard Tower', description: 'Gain a wizard tower ally that shoots magic balls that slowdown enemies', apply: addWizardTower // Assuming a helper function addWizardTower }, { id: 'aimbot', name: 'Aimbot', description: 'Arrows seek out enemies', apply: function apply() { aimbotEnabled = true; // Assuming a global flag for this } }, { id: 'green_killer', name: 'Green Killer', description: '+50% Damage vs Green Enemies', apply: function apply() { greenKillerEnabled = true; } }, { id: 'refined_projectiles', name: 'Refined Projectiles', description: 'Non-arrow projectiles +5 damage & faster', apply: function apply() { refinedProjectilesEnabled = true; // Existing projectiles won't update, new ones will have bonus } }, { id: 'viking_ally', name: 'Viking Ally', description: 'Gain a viking that throws piercing axes', apply: addVikingAlly // Assuming helper function addVikingAlly }, { id: 'green_slowdown', name: 'Green Slowdown', description: 'Projectiles slow Green enemies 10% for 10s (no stack)', apply: function apply() { greenSlowdownEnabled = true; } }]; // Function to create and show the upgrade selection popup function showUpgradePopup() { isUpgradePopupActive = true; // Simple pause: Game logic checks isUpgradePopupActive flag in game.update // --- 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; // Resume game logic (handled by checking flag in game.update) } // --- Visual Setup --- var bastionLine = game.addChild(LK.getAsset('bastionLine', { anchorX: 0.0, // Anchor at the left edge. anchorY: 0.5, // Anchor vertically centered. x: 0, // Position at the left edge of the screen. y: BASTION_Y // Position at the defined bastion Y-coordinate. })); // Create and configure the score display text. scoreTxt = new Text2('0', { size: 150, // Font size. fill: 0xFFFFFF // White color. }); scoreTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge. // Add score text to the GUI layer at the top-center position. LK.gui.top.addChild(scoreTxt); scoreTxt.y = 30; // Add padding below the top edge. Ensure it's clear of the top-left menu icon area. // --- 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. // 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. 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 --- // Decrease cooldown timer if active. if (cooldownTimer > 0) { cooldownTimer--; } // 1. Update and check Arrows for (var i = arrows.length - 1; i >= 0; i--) { var arrow = arrows[i]; // Note: LK engine calls arrow.update() automatically as it's added to the game stage. // Initialize last position if it hasn't been set yet (first frame). if (arrow.lastY === undefined) { arrow.lastY = arrow.y; } if (arrow.lastX === undefined) { arrow.lastX = arrow.x; } // Check for collisions between the current arrow and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (arrow.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Calculate damage, applying Green Killer bonus if applicable var damageToDeal = arrow.damage; if (greenKillerEnabled && enemy.tag === 'Green') { damageToDeal *= 1.5; // Apply 50% damage bonus } // Apply damage to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(damageToDeal, arrow); // Pass the arrow object as source if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Handle arrow piercing arrow.pierceLeft--; // Decrease remaining pierces if (arrow.pierceLeft <= 0) { // Arrow has no pierces left, destroy it arrow.destroy(); arrows.splice(i, 1); // Remove arrow from array. hitEnemy = true; // Mark that this arrow is done break; // Stop checking this arrow against other enemies as it's destroyed } else { // Arrow pierced this enemy and can continue // We don't set hitEnemy = true here because the arrow continues // We don't break because it might hit another enemy in the same frame further along its path // Apply Green Slowdown if applicable and arrow is still piercing if (!enemyDefeated && greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } } } } // If the arrow hit an enemy, it was destroyed, so skip to the next arrow. if (hitEnemy) { continue; } // Check if the arrow has gone off-screen (top, left, or right). // Use transition detection: check if it was on screen last frame and is off screen now. var wasOnScreen = arrow.lastY > -arrow.height / 2 && arrow.lastX > -arrow.width / 2 && arrow.lastX < GAME_WIDTH + arrow.width / 2; var isOffScreen = arrow.y < -arrow.height / 2 || // Off top edge arrow.x < -arrow.width / 2 || // Off left edge arrow.x > GAME_WIDTH + arrow.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Arrow is off-screen, destroy it and remove from the array. arrow.destroy(); arrows.splice(i, 1); } else if (!isOffScreen) { // Update last known position only if the arrow is still potentially on screen arrow.lastY = arrow.y; arrow.lastX = arrow.x; } } // 2. Update and check Cannonballs for (var cb = cannonballs.length - 1; cb >= 0; cb--) { var cannonball = cannonballs[cb]; // Note: LK engine calls cannonball.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (cannonball.lastY === undefined) { cannonball.lastY = cannonball.y; } if (cannonball.lastX === undefined) { cannonball.lastX = cannonball.x; } // Check for collisions between the current cannonball and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (cannonball.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply damage to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(cannonball.damage); // Cannonballs don't apply poison if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Cannonballs are destroyed on hit cannonball.destroy(); // Apply Green Slowdown if applicable before destroying cannonball if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } cannonball.destroy(); cannonballs.splice(cb, 1); // Remove cannonball from array. hitEnemy = true; // Mark that this cannonball is done break; // Stop checking this cannonball against other enemies as it's destroyed } } // If the cannonball hit an enemy, it was destroyed, so skip to the next cannonball. if (hitEnemy) { continue; } // Check if the cannonball has gone off-screen. var wasOnScreen = cannonball.lastY > -cannonball.height / 2 && cannonball.lastX > -cannonball.width / 2 && cannonball.lastX < GAME_WIDTH + cannonball.width / 2; var isOffScreen = cannonball.y < -cannonball.height / 2 || // Off top edge cannonball.x < -cannonball.width / 2 || // Off left edge cannonball.x > GAME_WIDTH + cannonball.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Cannonball is off-screen, destroy it and remove from the array. cannonball.destroy(); cannonballs.splice(cb, 1); } else if (!isOffScreen) { // Update last known position only if the cannonball is still potentially on screen cannonball.lastY = cannonball.y; cannonball.lastX = cannonball.x; } } // 3. Update and check Enemies for (var k = enemies.length - 1; k >= 0; k--) { var enemy = enemies[k]; // Note: LK engine calls enemy.update() automatically. // Initialize last position if not set. if (enemy.lastY === undefined) { enemy.lastY = enemy.y; } // Check if the enemy has reached or passed the bastion line. // Use transition detection: was the enemy's bottom edge above the line last frame, // and is it on or below the line now? var enemyBottomY = enemy.y + enemy.height / 2; var enemyLastBottomY = enemy.lastY + enemy.height / 2; var wasAboveBastion = enemyLastBottomY < BASTION_Y; var isAtOrBelowBastion = enemyBottomY >= BASTION_Y; if (wasAboveBastion && isAtOrBelowBastion) { // Enemy reached the bastion! Game Over. LK.getSound('gameOverSfx').play(); // Play game over sound effect. LK.showGameOver(); // Trigger the engine's game over sequence. // LK.showGameOver handles game state reset, no need to manually clear arrays here. return; // Exit the update loop immediately as the game is over. } else { // Update last known position if game is not over for this enemy. enemy.lastY = enemy.y; } } // 3. Update and check Swordsmen (allies) for (var l = swordsmen.length - 1; l >= 0; l--) { var swordsman = swordsmen[l]; // Swordsman update method handles its own lifetime and attacking // Check if the swordsman has been destroyed by its lifetime timer if (!swordsman.parent) { // If it no longer has a parent, it has been destroyed swordsmen.splice(l, 1); // Remove swordsman from array } } // 4. Update and check Cannons (allies) for (var m = cannons.length - 1; m >= 0; m--) { var cannon = cannons[m]; // Cannons do not have a lifetime timer or destruction logic in this basic version // If they had, we'd check !cannon.parent and splice here as in Swordsmen } // 5. Update and check Wizard Towers (allies) for (var wt = wizardTowers.length - 1; wt >= 0; wt--) { var tower = wizardTowers[wt]; // Wizard towers do not have a lifetime timer or destruction logic in this basic version // If they had, we'd check !tower.parent and splice here } // 5.5 Update and check Viking Allies for (var va = vikingAllies.length - 1; va >= 0; va--) { var viking = vikingAllies[va]; // Vikings do not have a lifetime timer or destruction logic in this version // LK engine calls viking.update() automatically. } // 6. Update and check Magic Balls for (var mb = magicBalls.length - 1; mb >= 0; mb--) { var magicBall = magicBalls[mb]; // Note: LK engine calls magicBall.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (magicBall.lastY === undefined) { magicBall.lastY = magicBall.y; } if (magicBall.lastX === undefined) { magicBall.lastX = magicBall.x; } // Check for collisions between the current magic ball and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (magicBall.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply damage (minimal) and slowdown effect to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(magicBall.damage, magicBall); // Pass the magic ball object as source if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Magic balls are destroyed on hit magicBall.destroy(); // Apply Green Slowdown if applicable before destroying magic ball if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { // Note: Enemy.takeDamage already checks for MagicBall source to apply its default slow. // This adds the Green Slowdown effect *on top* if applicable and not already slowed. // However, takeDamage applies slow based on MagicBall properties. Let's apply Green Slowdown here explicitly. enemy.slowTimer = 10 * 60; // 10 seconds enemy.currentSlowAmount = 0.9; // 10% slow } magicBall.destroy(); magicBalls.splice(mb, 1); // Remove magic ball from array. hitEnemy = true; // Mark that this magic ball is done break; // Stop checking this magic ball against other enemies as it's destroyed } } // If the magic ball hit an enemy, it was destroyed, so skip to the next magic ball. if (hitEnemy) { continue; } // Check if the magic ball has gone off-screen. var wasOnScreen = magicBall.lastY > -magicBall.height / 2 && magicBall.lastX > -magicBall.width / 2 && magicBall.lastX < GAME_WIDTH + magicBall.width / 2; var isOffScreen = magicBall.y < -magicBall.height / 2 || // Off top edge magicBall.x < -magicBall.width / 2 || // Off left edge magicBall.x > GAME_WIDTH + magicBall.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Magic ball is off-screen, destroy it and remove from the array. magicBall.destroy(); magicBalls.splice(mb, 1); } else if (!isOffScreen) { // Update last known position only if the magic ball is still potentially on screen magicBall.lastY = magicBall.y; magicBall.lastX = magicBall.x; } } // 6.5 Update and check Viking Axes for (var axeIdx = vikingAxes.length - 1; axeIdx >= 0; axeIdx--) { var axe = vikingAxes[axeIdx]; // Note: LK engine calls axe.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (axe.lastY === undefined) { axe.lastY = axe.y; } if (axe.lastX === undefined) { axe.lastX = axe.x; } // Check for collisions between the current axe and all enemies. var hitEnemyAxe = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; if (axe.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply Green Killer bonus if applicable (Vikings benefit too) var damageToDealAxe = axe.damage; if (greenKillerEnabled && enemy.tag === 'Green') { damageToDealAxe *= 1.5; // Apply 50% damage bonus } // Apply damage and check if defeated var enemyDefeated = enemy.takeDamage(damageToDealAxe, axe); // Pass axe as source for potential effects if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Handle axe piercing axe.pierceLeft--; // Decrease remaining pierces if (axe.pierceLeft <= 0) { // Axe has no pierces left, destroy it axe.destroy(); vikingAxes.splice(axeIdx, 1); // Remove axe from array. hitEnemyAxe = true; // Mark that this axe is done break; // Stop checking this axe against other enemies } else { // Axe pierced this enemy and can continue // Potentially apply Green Slowdown if enabled if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } } } } // If the axe hit and was destroyed, skip to the next axe. if (hitEnemyAxe) { continue; } // Check if the axe has gone off-screen. var wasOnScreenAxe = axe.lastY > -axe.height / 2 && axe.lastX > -axe.width / 2 && axe.lastX < GAME_WIDTH + axe.width / 2; var isOffScreenAxe = axe.y < -axe.height / 2 || axe.y > GAME_HEIGHT + axe.height / 2 || axe.x < -axe.width / 2 || axe.x > GAME_WIDTH + axe.width / 2; if (wasOnScreenAxe && isOffScreenAxe) { axe.destroy(); vikingAxes.splice(axeIdx, 1); } else if (!isOffScreenAxe) { axe.lastY = axe.y; axe.lastX = axe.x; } } // 7. Spawn new Enemies periodically // Use LK.ticks and the spawn interval. Ensure interval doesn't go below minimum. if (LK.ticks % Math.max(minEnemySpawnInterval, Math.floor(enemySpawnInterval)) === 0) { var currentScore = LK.getScore(); var baseSpeedForSpawn = Math.min(maxEnemySpeed, currentEnemySpeed); // Base speed adjusted by game difficulty progression // Determine if this spawn *could* be a boss (e.g., every 7th spawn after score 10) var potentialBoss = (enemies.length + 1) % 7 === 0 && currentScore >= 10; var typeToSpawn = 'swordsman'; // Default type var enemyHealth = 1; // Default health var enemyDodgeChance = 0; // Default dodge chance var enemySpeed = baseSpeedForSpawn; // Start with base speed // Determine base type based on score thresholds and randomness var possibleTypes = ['swordsman']; if (currentScore >= 10) { // Spearman appears at score 10 possibleTypes.push('spearman'); } if (currentScore >= 15) { possibleTypes.push('thief'); } //{3s} // Adjusted original line identifier if (currentScore >= 23) { possibleTypes.push('knight'); } //{3t} // Adjusted original line identifier if (currentScore >= 25) { possibleTypes.push('wizard'); } // Wizards start appearing at score 25 if (currentScore >= 30) { possibleTypes.push('shield'); } // Shields start appearing at score 30 if (currentScore >= 100) { possibleTypes.push('elite_knight'); } // Elite Knights start appearing at score 100 if (currentScore >= 125) { // War Elephant appears at score 125 possibleTypes.push('war_elephant'); } // 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 === '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 === 'spearman') { var baseSpearmanHP = 3; var hpIncreaseIntervalSpearman = 10; // Gains HP every 10 score points after appearing var hpIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / hpIncreaseIntervalSpearman); enemyHealth = baseSpearmanHP + hpIncreasesSpearman * 3; // Gains 3 HP per interval var speedIncreaseIntervalSpearman = 10; // Gains speed every 10 score points after appearing var speedIncreasePercentSpearman = 0.05; // 5% speed increase each time var speedIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / speedIncreaseIntervalSpearman); var spearmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentSpearman, speedIncreasesSpearman); enemySpeed *= 1.0 * spearmanSpeedMultiplier; // Base speed + scaling speed enemyDodgeChance = 0; } else if (typeToSpawn === 'war_elephant') { var baseElephantHP = 500; var hpIncreaseIntervalElephant = 15; // Gains HP every 15 score points after appearing var hpIncreasesElephant = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalElephant); enemyHealth = baseElephantHP + hpIncreasesElephant * 100; // Gains 100 HP per interval enemySpeed *= 0.3; // War Elephants are very slow enemyDodgeChance = 0; // Note: War elephant death spawns spearmen - this will be handled in the Enemy death logic. } else { // Swordsman (default) enemyHealth = 1; // Speed remains baseSpeedForSpawn initially } // Check if this spawn should be overridden to be a Boss if (potentialBoss) { typeToSpawn = 'boss'; // Set type to boss enemyHealth = 5 + Math.floor(currentScore / 8); // Boss health scales significantly with score enemySpeed = baseSpeedForSpawn * 0.7; // Bosses are slower but much tougher (reset speed based on base) enemyDodgeChance = 0; // Bosses typically don't dodge } // Apply the global Sabotage speed multiplier AFTER type-specific adjustments enemySpeed *= enemySpeedMultiplier; // Create the new enemy instance with the calculated stats var newEnemy = new Enemy(typeToSpawn, enemySpeed, enemyHealth, enemyDodgeChance); // Position the new enemy at the top, random horizontal position with padding // Use the actual width of the created enemy's graphic for padding calculation // Need to access width after creation, use a sensible default or estimate if needed before creation var tempAsset = LK.getAsset(newEnemy.assetId || 'enemy', {}); // Get asset dimensions (might need refinement if assetId isn't on newEnemy yet) var spawnPadding = (tempAsset ? tempAsset.width / 2 : 100) + 20; // Use default if asset not found easily newEnemy.x = spawnPadding + Math.random() * (GAME_WIDTH - 2 * spawnPadding); newEnemy.y = -(tempAsset ? tempAsset.height / 2 : 100); // Start just above the top edge. // Initialize last position for state tracking (used for bastion collision check). newEnemy.lastY = newEnemy.y; // Add the new enemy to the game scene and the tracking array. game.addChild(newEnemy); enemies.push(newEnemy); // Increase difficulty for the next spawn: decrease spawn interval and increase base speed. // These affect the *next* potential spawn's base calculations. enemySpawnInterval -= enemySpawnRateDecrease; currentEnemySpeed += enemySpeedIncrease; } //{2s} // Adjusted original line identifier }; // --- Initial Game Setup --- // Set the initial score text based on the starting score (which is 0). scoreTxt.setText(LK.getScore()); ; // Play the background music. LK.playMusic('Gamemusic');
===================================================================
--- original.js
+++ change.js
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