Code edit (1 edits merged)
Please save this source code
User prompt
Make the descriptions of all units in the enemypedia more detailed
User prompt
Dragon-tagged enemies also have the ability to be immune to slowdowns
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55 and gains HP And speed every 10 score), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score and gets HP every 15 score)
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55 and gains HP And speed every 10 score), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score and gets HP every 15 score)
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55 and gains HP And speed every 10 score), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score and gets HP every 15 score)
User prompt
Add new upgrades: Dragon slayers (All projectiles deal 10+ damage to dragon-tagged enemies), Enchained legs (all enemies that can dodge have 10% less chance to dodge), Extra life (you can resist one more enemy that escapes)
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score)
User prompt
Add new enemies: Dark spearman (a faster and more resistant variant of spearman that is tagged black, so it's immune to arrows; starts appearing at score 55), Dragon (a Dragon-tagged enemy [dragon tagged enemies have a high chance of dodging] that is moderately fast and has GREAT HP, starts appearing at 100 score)
User prompt
Dark war elephant starts appearing at score 145
User prompt
DartAlly uses "Disguised_swordsman" asset
User prompt
Add assets for Dart shooter, Darts (from dart shooter), Angel of light and Light effect for Angel of light's attack
User prompt
Add new upgrades: Dart shooter (an ally that shoots darts at a fast rate that are really effective against green enemies), Angel of light (this girl knows how to stun! She uses light that can stun enemies for 5 seconds and deals a LOT of damage to dark enemies)
User prompt
Compilation error[L2326]: TypeError: Cannot set properties of undefined (setting 'reflectChance') How can I help?
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'reflectChance')' in or related to this line: 'self.reflectChance = 0.20; // 20% reflect chance, added in Enemy class' Line Number: 2326
User prompt
Enemies aren't spawning anymore; FIX THAT RIGHT NOW!
User prompt
Jester uses Jester_asset and Dark war elephant uses Dark_war_elephant_asset
User prompt
Add asset for jester and dark war elephant
User prompt
Add new enemies: Jester (The Jester ihas a 20% chance to reflect projectiles that aren't cannonballs and a 30% chance of dodging them! Gains speed every 12 score and starts appearing at 69 score), Dark war elephant (This elephant is a black enemy [this means it's immune to arrows] that is similar to it's green brother. However, it has slightly less Hp But spawns 5 dark archers on death! Gains HP every 10 score and starts appearing at 120 score)
User prompt
Hot air balloon now starts spawning at score 100
User prompt
Add in the enemypedia when each enemy appears at what score
User prompt
Add an enemypedia to the game where you see all types of enemies and their stats, looks and properties
User prompt
Make the Enemypedia more organized by adding Buttons to change what enemy you wanna look at instead of all enemies at once
User prompt
Add an enemypedia to the game where you see all types of enemies and their stats, looks and properties
User prompt
Add an enemypedia to the game where you see all types of enemies and their stats and looks
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ /** * Represents an allied archer that shoots arrows independently. */ var ArcherAlly = Container.expand(function () { var self = Container.call(this); // Create and attach the archer graphic asset var graphics = self.attachAsset('Archer', { anchorX: 0.5, anchorY: 0.5 }); // No rotation needed for 'Archer' asset as it's already upright and flipped self.fireTimer = 0; // Timer for shooting self.fireInterval = 180; // Shoot every 3 seconds (3 * 60 ticks) /** * Update method called each game tick by the LK engine. * Handles firing logic. */ self.update = function () { self.fireTimer++; var effectiveFireInterval = Math.max(60, self.fireInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.fireTimer >= effectiveFireInterval) { self.fireTimer = 0; // Reset timer // Find the closest enemy to shoot at var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; // Calculate distance to the enemy var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } // If an enemy is found, fire an arrow if (closestEnemy) { // Calculate angle towards the closest enemy var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y)); // Create a new arrow instance var newArrow = new Arrow(angle); // Apply multi-shot to allies if the upgrade is enabled for the player if (multiShotEnabled) { var angle2 = angle + Math.PI / 12; // Offset by 15 degrees var newArrow2 = new Arrow(angle2); newArrow2.x = self.x; newArrow2.y = self.y; newArrow2.lastY = newArrow2.y; newArrow2.lastX = newArrow2.x; game.addChild(newArrow2); arrows.push(newArrow2); var angle3 = angle - Math.PI / 12; // Offset by -15 degrees var newArrow3 = new Arrow(angle3); newArrow3.x = self.x; newArrow3.y = self.y; newArrow3.lastY = newArrow3.y; newArrow3.lastX = newArrow3.x; game.addChild(newArrow3); arrows.push(newArrow3); } newArrow.x = self.x; newArrow.y = self.y; // Ally arrows do not count towards the player's reload counter // The Arrow class handles piercing level based on player upgrade, but the ally doesn't benefit from player reload. // For simplicity, we'll let the ally benefit from player's piercing upgrade. newArrow.lastY = newArrow.y; newArrow.lastX = newArrow.x; // Add the arrow to the game scene and the tracking array. game.addChild(newArrow); arrows.push(newArrow); // Add to the same arrows array for collision detection // Ally doesn't play the 'shoot' sound } } }; return self; // Return self for potential inheritance }); // Sound when an enemy reaches the bastion // No plugins needed for this version of the game. /** * Represents an Arrow fired by the player. * @param {number} angle - The angle in radians at which the arrow is fired. */ var Arrow = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the arrow graphic asset var graphics = self.attachAsset('arrow', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align arrow graphic with direction self.speed = 30; // Speed of the arrow in pixels per tick self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) self.pierceLeft = arrowPierceLevel; // How many more enemies this arrow can pierce self.damage = arrowDamage; // Damage dealt by this arrow self.isPoison = poisonShotsEnabled; // Flag if this arrow applies poison self.targetEnemy = null; // Potential target for aimbot self.seekSpeed = 0.1; // How quickly the arrow adjusts its direction to seek if (aimbotEnabled) { // Find the closest enemy to seek if aimbot is enabled var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { self.targetEnemy = closestEnemy; } } /** * Update method called each game tick by the LK engine. * Moves the arrow based on its velocity. */ self.update = function () { if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) { // If aimbot is enabled, a target exists and is still in the game, seek it var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y)); // Smoothly adjust the arrow's angle towards the target angle var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation // Normalize angle difference to be between -PI and PI if (angleDiff > Math.PI) { angleDiff -= 2 * Math.PI; } if (angleDiff < -Math.PI) { angleDiff += 2 * Math.PI; } // Interpolate angle var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed; self.rotation = newAngle + Math.PI / 2; self.vx = Math.sin(newAngle) * self.speed; self.vy = -Math.cos(newAngle) * self.speed; } else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) { // If aimbot is enabled but current target is gone, find a new target self.targetEnemy = null; // Clear old target var closestEnemy = null; var closestDistance = Infinity; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; var dx = enemy.x - self.x; var dy = enemy.y - self.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestEnemy = enemy; } } if (closestEnemy) { self.targetEnemy = closestEnemy; } } self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents a Cannon ally that targets the strongest enemy. */ var Cannon = Container.expand(function () { var self = Container.call(this); // Create and attach the cannon graphic asset (need to add a new asset for this) // For now, using a placeholder asset, will need a 'cannon_asset' var graphics = self.attachAsset('cannon_asset', { // Use actual cannon asset anchorX: 0.5, anchorY: 0.5 }); self.attackRange = 800; // Long attack range self.attackDamage = 10; // High damage self.attackInterval = 300; // Attack every 5 seconds (300 ticks) self.attackTimer = 0; // Timer for attacks self.rotation = 0; // For aiming visual if implemented later /** * Update method called each game tick by the LK engine. * Handles attacking the strongest enemy. */ self.update = function () { self.attackTimer++; var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade if (self.attackTimer >= effectiveAttackInterval) { self.attackTimer = 0; // Reset timer // Find the strongest enemy (highest health) var strongestEnemy = null; var maxHealth = -1; for (var i = 0; i < enemies.length; i++) { var enemy = enemies[i]; if (enemy.health > maxHealth) { maxHealth = enemy.health; strongestEnemy = enemy; } } // If a strongest enemy is found within range, attack it if (strongestEnemy) { // Calculate angle towards the strongest enemy var angle = Math.atan2(strongestEnemy.x - self.x, -(strongestEnemy.y - self.y)); // Create a new cannonball instance var newCannonball = new Cannonball(angle); // Position the cannonball at the cannon's location newCannonball.x = self.x; newCannonball.y = self.y; newCannonball.lastY = newCannonball.y; // Initialize lastY for state tracking newCannonball.lastX = newCannonball.x; // Initialize lastX for state tracking // Add the cannonball to the game scene and the tracking array. game.addChild(newCannonball); cannonballs.push(newCannonball); // Add to the new cannonballs array // Optional: Add a visual/sound effect for cannon shot here later } } }; return self; // Return self for potential inheritance }); /** * Represents a Cannonball fired by a Cannon. * @param {number} angle - The angle in radians at which the cannonball is fired. */ var Cannonball = Container.expand(function (angle) { var self = Container.call(this); // Create and attach the cannonball graphic asset var graphics = self.attachAsset('cannonball', { anchorX: 0.5, anchorY: 0.5 }); graphics.rotation = angle + Math.PI / 2; // Align cannonball graphic with direction self.speed = 20; // Base speed of the cannonball self.damage = 10; // Base damage dealt by this cannonball // Apply refined projectiles bonus if enabled if (refinedProjectilesEnabled) { self.speed += 5; self.damage += 5; } self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up) /** * Update method called each game tick by the LK engine. * Moves the cannonball based on its velocity. */ self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; // Return self for potential inheritance }); /** * Represents an Enemy attacker moving towards the bastion. * @param {string} type - The type of enemy ('swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight'). * @param {number} speed - The final calculated speed of the enemy. * @param {number} health - The initial and maximum health of the enemy. * @param {number} dodgeChance - The chance (0 to 1) for the enemy to dodge an attack. */ var Enemy = Container.expand(function (type, speed, health, dodgeChance) { var self = Container.call(this); // Set asset based on type var assetId = 'enemy'; // Default swordsman asset if (type === 'knight') { assetId = 'knight'; } else if (type === 'elite_knight') { assetId = 'elite_knight_asset'; // Use specific asset for elite knight } else if (type === 'thief') { assetId = 'thief'; } else if (type === 'boss') { assetId = 'boss'; } else if (type === 'shield') { assetId = 'shield_enemy'; // Use specific asset for shield } else if (type === 'wizard') { assetId = 'wizard_enemy'; // Use specific asset for wizard } else if (type === 'spearman') { assetId = 'spearman'; // Use specific asset for spearman } else if (type === 'war_elephant') { assetId = 'war_elephant'; // Use specific asset for war elephant } else if (type === 'elite_shield') { assetId = 'elite_shield_asset'; // Use specific asset for elite shield } else if (type === 'shaman') { assetId = 'shaman_enemy'; // Use specific asset for shaman } else if (type === 'hot_air_balloon') { assetId = 'hot_air_balloon_asset'; // Use specific asset for hot air balloon } else if (type === 'dark_bowman') { assetId = 'dark_bowman_asset'; // Use specific asset for dark bowman } var graphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); self.type = type; // 'swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight', 'shaman', etc. self.speed = speed; // Final calculated speed self.health = health; self.maxHealth = health; // Store max health for visual feedback self.dodgeChance = dodgeChance || 0; // Default to 0 if undefined self.poisonStacks = 0; // Number of poison stacks self.poisonTimer = 0; // Timer for poison damage self.poisonDamagePerTick = 0.05; // Damage per tick per stack self.slowTimer = 0; // Timer for slowdown effect self.currentSlowAmount = 1.0; // Multiplier for current slowdown effect (1.0 means no slow) // Initialize shaman-specific properties if (type === 'shaman') { self.shamanTimer = 0; } // Set tag property based on type self.tag = null; // Default tag is null if (self.type === 'wizard' || self.type === 'spearman' || self.type === 'war_elephant' || self.type === 'shaman') { self.tag = 'Green'; } else if (self.type === 'dark_bowman') { self.tag = 'Black'; // Black-tagged enemies are immune to arrows } // Wizard-specific properties if (self.type === 'wizard') { self.teleportTimer = 0; self.teleportInterval = 180; // 3 seconds * 60 FPS } // --- Public Methods (defined before use) --- /** * Update method called each game tick by the LK engine. * Moves the enemy downwards (or teleports for wizard) and updates visual feedback. */ self.update = function () { if (self.type === 'wizard') { self.teleportTimer = (self.teleportTimer || 0) + 1; // Initialize timer if needed if (self.teleportTimer >= self.teleportInterval) { self.teleportTimer = 0; // Teleport logic: Random X, slightly advanced Y var oldY = self.y; var teleportPadding = graphics.width / 2 + 20; // Use actual graphic width var newX = teleportPadding + Math.random() * (GAME_WIDTH - 2 * teleportPadding); // Advance Y slightly, but don't teleport past bastion var newY = Math.min(BASTION_Y - graphics.height, self.y + 100 + Math.random() * 100); // Advance 100-200px // Ensure not teleporting backwards significantly or offscreen top newY = Math.max(graphics.height / 2, newY); self.x = newX; self.y = newY; self.lastY = oldY; // Set lastY to pre-teleport position to avoid false bastion triggers // Add a visual effect for teleport LK.effects.flashObject(self, 0xAA00FF, 300); // Purple flash } else { // Move normally if not teleporting this frame self.y += self.speed; } } else { // Normal movement for other enemy types self.y += self.speed * self.currentSlowAmount; // Apply slowdown to movement } // Decrease slowdown timer and reset slow amount if timer runs out if (self.slowTimer > 0) { self.slowTimer--; if (self.slowTimer <= 0) { self.currentSlowAmount = 1.0; // Reset speed multiplier when slowdown ends // Optional: Remove visual feedback for slowdown here later } } // Apply poison damage if stacks exist if (self.poisonStacks > 0) { self.poisonTimer++; if (self.poisonTimer >= 30) { // Apply poison damage every 0.5 seconds (30 ticks) self.poisonTimer = 0; var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30; // Damage per 0.5s self.health -= poisonDmg; // Optional: Add visual feedback for poison damage here later } // Check if health dropped to zero or below due to poison if (self.health <= 0 && self.parent) { // Ensure enemy is still in the game // Enemy defeated by poison LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy self.destroy(); // The main game update loop needs to handle removing the enemy from the `enemies` array return; // Stop updating this destroyed instance } } // Visual feedback based on health - alpha fade (applies to all types) var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0; graphics.alpha = 0.4 + healthRatio * 0.6; // Fade from 1.0 down to 0.4 // Shaman ability: reduce player ammo periodically if (self.type === 'shaman') { if (self.shamanTimer === undefined) { self.shamanTimer = 0; } self.shamanTimer += 1; var shamanAbilityInterval = 300; // 5 seconds * 60 FPS if (self.shamanTimer >= shamanAbilityInterval) { self.shamanTimer = 0; // Reduce player ammo, but not below 0 arrowsFired = Math.min(maxArrowsBeforeCooldown, arrowsFired + 1); // Make it require one more shot for reload ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Optional: Add a visual/sound effect for shaman ability } } }; //{N} // Adjusted line identifier /** * Method called when the enemy is hit by an arrow. * Handles dodging and health reduction. * @param {number} damage - The amount of damage the arrow deals. * @returns {boolean} - True if the enemy is defeated, false otherwise. */ self.takeDamage = function (damage) { var source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; // Add source parameter, default to null // Check for dodge first (only if dodgeChance is positive) if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) { // Optional: Add a visual effect for dodge here later // console.log(self.type + " dodged!"); return false; // Dodged, so not defeated by this hit } // Check if enemy is Black-tagged and source is an Arrow if (self.tag === 'Black' && source instanceof Arrow) { // Black-tagged enemies are immune to arrow projectiles return false; // Not defeated, arrow has no effect } self.health -= damage; // Check if the damage source was a poison arrow and apply poison stacks if (source && source.isPoison) { // Use the 'source' parameter self.poisonStacks++; // Increase poison stacks self.poisonTimer = 0; // Reset timer to apply damage immediately } // Check if the damage source was a Magic Ball and apply slowdown if (source && source instanceof MagicBall) { self.slowTimer = source.slowDuration; // Apply slowdown duration self.currentSlowAmount = source.slowAmount; // Apply slowdown amount // Optional: Add visual feedback for slowdown here later } // Check if the damage source should apply Green Slowdown if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0) { // Apply Green Slowdown only if the upgrade is active, enemy is Green, and not already slowed self.slowTimer = 10 * 60; // 10 seconds * 60 ticks/sec self.currentSlowAmount = 0.9; // 10% slowdown // Optional: Add visual feedback for green slowdown } // Check if enemy was defeated by this damage var defeated = self.health <= 0; if (defeated && self.type === 'war_elephant') { // If War Elephant is defeated, spawn 5 spearmen for (var i = 0; i < 5; i++) { var spawnPadding = 100; // Padding from edge var spawnX = self.x + (Math.random() * 200 - 100); // Spawn around elephant's x var spawnY = self.y + (Math.random() * 100 - 50); // Spawn around elephant's y, slightly forward // Ensure spawns are within game bounds spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX)); spawnY = Math.max(spawnPadding, Math.min(BASTION_Y - 50, spawnY)); // Don't spawn too close to bastion var newSpearman = new Enemy('spearman', currentEnemySpeed * enemySpeedMultiplier, 10, 0); // Base spearman stats newSpearman.x = spawnX; newSpearman.y = spawnY; newSpearman.lastY = newSpearman.y; game.addChild(newSpearman); enemies.push(newSpearman); } } else if (defeated && self.type === 'hot_air_balloon') { // If Hot Air Balloon is defeated, spawn 5 random non-green enemies var possibleTypes = ['swordsman', 'knight', 'thief', 'shield', 'elite_knight', 'elite_shield', 'dark_bowman']; for (var i = 0; i < 5; i++) { // Choose random enemy type that is not green-tagged var randomType = possibleTypes[Math.floor(Math.random() * possibleTypes.length)]; var randomHP = 1 + Math.floor(Math.random() * 5); // Random HP between 1-5 var randomSpeed = currentEnemySpeed * 0.7 * enemySpeedMultiplier; // Slower than average var newEnemy = new Enemy(randomType, randomSpeed, randomHP, 0); // Spawn in a staggered pattern below the balloon var spawnPadding = 100; var spawnX = self.x + (Math.random() * 300 - 150); // Wider spread than elephant var spawnY = self.y + Math.random() * 200; // Always below the balloon // Ensure spawns are within game bounds spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX)); spawnY = Math.min(BASTION_Y - 50, spawnY); // Don't spawn too close to bastion newEnemy.x = spawnX; newEnemy.y = spawnY; newEnemy.lastY = newEnemy.y; game.addChild(newEnemy); enemies.push(newEnemy); } } // No need to update alpha here, self.update handles it return defeated; // Return true if health is 0 or less }; //{O} // Adjusted line identifier // --- Initialization --- // Apply visual distinctions based on type graphics.tint = 0xFFFFFF; // Reset tint graphics.scale.set(1.0); // Reset scale if (self.type === 'knight') { graphics.tint = 0xCCCCCC; // Grey tint for Knights graphics.scale.set(1.1); } else if (self.type === 'elite_knight') { graphics.tint = 0xFFD700; // Gold tint for Elite Knights graphics.scale.set(1.2); // Slightly larger than normal knight } else if (self.type === 'thief') { graphics.tint = 0xCCFFCC; // Pale Green tint for Thieves graphics.scale.set(0.9); } else if (self.type === 'boss') { graphics.tint = 0xFFCCCC; // Pale Red tint for Bosses graphics.scale.set(1.4); // Make bosses quite large } else if (self.type === 'shield') { graphics.tint = 0xADD8E6; // Light Blue tint for Shield graphics.scale.set(1.2); // Make shield enemies bulky } else if (self.type === 'wizard') { graphics.tint = 0xE0B0FF; // Light Purple tint for Wizard graphics.scale.set(1.0); } else if (self.type === 'elite_shield') { graphics.tint = 0x8A2BE2; // Blue Violet tint for Elite Shield graphics.scale.set(1.3); // Slightly larger than normal shield } //{11} // Modified original line identifier location return self; // Return self for potential inheritance }); var EnemypediaScreen = Container.expand(function () { var self = Container.call(this); self.currentPage = 0; self.totalPages = enemyPediaData.length; // Background var bgAssetHeight = LK.getAsset('pedia_screen_bg', {}).height; // Get actual asset height for calculations var bgAssetWidth = LK.getAsset('pedia_screen_bg', {}).width; // Get actual asset width var bg = self.attachAsset('pedia_screen_bg', { anchorX: 0.5, anchorY: 0.5 // width and height are from asset default }); self.x = GAME_WIDTH / 2; self.y = GAME_HEIGHT / 2; // Title var titleText = new Text2('Enemypedia', { size: 90, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0.5); titleText.x = 0; titleText.y = -(bgAssetHeight / 2) + 70; // Positioned from top of BG self.addChild(titleText); // Content container for enemy details self.contentContainer = new Container(); // Position content container centrally between title and nav buttons self.contentContainer.x = 0; // Centered horizontally self.contentContainer.y = titleText.y + titleText.height / 2 + 20; // Below title self.addChild(self.contentContainer); // Navigation and Close Buttons var navButtonYPos = bgAssetHeight / 2 - 60; // Positioned from bottom of BG var closeButton = new Container(); var closeButtonBg = closeButton.attachAsset('pedia_nav_button_bg', { anchorX: 0.5, anchorY: 0.5, color: 0xAA4444 }); var closeButtonText = new Text2('Close', { size: 40, fill: 0xFFFFFF }); closeButtonText.anchor.set(0.5, 0.5); closeButton.addChild(closeButtonText); closeButton.x = 0; closeButton.y = navButtonYPos; closeButton.interactive = true; closeButton.down = function () { hideEnemypediaScreen(); }; self.addChild(closeButton); var prevButton = new Container(); var prevButtonBg = prevButton.attachAsset('pedia_nav_button_bg', { anchorX: 0.5, anchorY: 0.5 }); var prevButtonText = new Text2('Previous', { size: 40, fill: 0xFFFFFF }); prevButtonText.anchor.set(0.5, 0.5); prevButton.addChild(prevButtonText); prevButton.x = -(closeButtonBg.width / 2 + prevButtonBg.width / 2 + 20); // Place left of close prevButton.y = navButtonYPos; prevButton.interactive = true; prevButton.down = function () { self.changePage(-1); }; self.addChild(prevButton); var nextButton = new Container(); var nextButtonBg = nextButton.attachAsset('pedia_nav_button_bg', { anchorX: 0.5, anchorY: 0.5 }); var nextButtonText = new Text2('Next', { size: 40, fill: 0xFFFFFF }); nextButtonText.anchor.set(0.5, 0.5); nextButton.addChild(nextButtonText); nextButton.x = closeButtonBg.width / 2 + nextButtonBg.width / 2 + 20; // Place right of close nextButton.y = navButtonYPos; nextButton.interactive = true; nextButton.down = function () { self.changePage(1); }; self.addChild(nextButton); self.pageNumberText = new Text2('', { size: 30, fill: 0xCCCCCC }); self.pageNumberText.anchor.set(0.5, 0.5); self.pageNumberText.x = 0; self.pageNumberText.y = navButtonYPos - prevButtonBg.height / 2 - 25; // Above nav buttons self.addChild(self.pageNumberText); self.changePage = function (direction) { if (self.totalPages === 0) return; self.currentPage += direction; if (self.currentPage < 0) self.currentPage = self.totalPages - 1; if (self.currentPage >= self.totalPages) self.currentPage = 0; self.renderPage(); }; self.renderPage = function () { self.contentContainer.removeChildren(); if (self.totalPages === 0) { var noDataText = new Text2('No enemy data available.', { size: 50, fill: 0xFFFFFF }); noDataText.anchor.set(0.5, 0.5); self.contentContainer.addChild(noDataText); self.pageNumberText.setText(''); prevButton.visible = false; nextButton.visible = false; return; } prevButton.visible = true; nextButton.visible = true; var enemy = enemyPediaData[self.currentPage]; if (!enemy) return; self.pageNumberText.setText('Page ' + (self.currentPage + 1) + ' of ' + self.totalPages); var contentCurrentY = -(bgAssetHeight / 2) * 0.45; // Start Y for content, relative to contentContainer's center var enemyImage = LK.getAsset(enemy.assetId, { anchorX: 0.5, anchorY: 0.5 }); var maxImgSize = 280; var imgScale = 1; if (enemyImage.width > maxImgSize || enemyImage.height > maxImgSize) { imgScale = Math.min(maxImgSize / enemyImage.width, maxImgSize / enemyImage.height); } enemyImage.scale.set(imgScale); enemyImage.x = 0; // Centered horizontally in contentContainer for now enemyImage.y = contentCurrentY + enemyImage.height * imgScale / 2; self.contentContainer.addChild(enemyImage); contentCurrentY = enemyImage.y + enemyImage.height * imgScale / 2 + 30; var nameText = new Text2(enemy.name, { size: 60, fill: 0xFFD700, align: 'center', wordWrap: true, wordWrapWidth: bgAssetWidth * 0.8 }); nameText.anchor.set(0.5, 0); nameText.x = 0; nameText.y = contentCurrentY; self.contentContainer.addChild(nameText); contentCurrentY += nameText.height + 20; var descText = new Text2(enemy.description, { size: 38, fill: 0xCCCCCC, align: 'center', style: { wordWrap: true, wordWrapWidth: bgAssetWidth * 0.75, leading: 5 } }); descText.anchor.set(0.5, 0); descText.x = 0; descText.y = contentCurrentY; self.contentContainer.addChild(descText); contentCurrentY += descText.height + 30; var statsTitle = new Text2("Stats & Abilities:", { size: 40, fill: 0xDDDDFF, align: 'center' }); statsTitle.anchor.set(0.5, 0); statsTitle.x = 0; statsTitle.y = contentCurrentY; self.contentContainer.addChild(statsTitle); contentCurrentY += statsTitle.height + 15; for (var i = 0; i < enemy.stats.length; i++) { var statText = new Text2('- ' + enemy.stats[i], { size: 32, fill: 0xBBBBBB, style: { wordWrap: true, wordWrapWidth: bgAssetWidth * 0.7, leading: 5 } }); statText.anchor.set(0.5, 0); // Center align stats text statText.x = 0; statText.y = contentCurrentY; self.contentContainer.addChild(statText); contentCurrentY += statText.height + 5; // Reduced spacing for stat lines } if (enemy.tags && enemy.tags.length > 0) { contentCurrentY += 15; // Extra space before tags var tagsText = new Text2('Tags: ' + enemy.tags.join(', '), { size: 32, fill: 0xAAAAFF, align: 'center', style: { wordWrap: true, wordWrapWidth: bgAssetWidth * 0.7 } }); tagsText.anchor.set(0.5, 0); tagsText.x = 0; tagsText.y = contentCurrentY; self.contentContainer.addChild(tagsText); } }; self.renderPage(); 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 }); /**** * 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 // --- Enemypedia System --- var enemyPediaData = [{ name: 'Swordsman', assetId: 'enemy', description: 'A basic melee unit. The backbone of early waves.', stats: ['Health: Low (scales with progression)', 'Speed: Moderate (scales)', 'Dodge: 0%'], tags: [] }, { name: 'Knight', assetId: 'knight', description: 'A tougher melee unit, clad in armor. Appears from score 23.', stats: ['Health: Medium (scales significantly)', 'Speed: Slightly Slower (scales)', 'Dodge: 0%', 'Special: Increased durability'], tags: [] }, { name: 'Thief', assetId: 'thief', description: 'A fast and evasive unit. Appears from score 15.', stats: ['Health: Very Low (1 base)', 'Speed: Very Fast (scales significantly)', 'Dodge: 10%', 'Special: High speed, chance to dodge attacks'], tags: [] }, { name: 'Boss', assetId: 'boss', description: 'A formidable leader enemy appearing periodically (every 7th spawn after score 10). Very tough.', stats: ['Health: Very High (scales massively with score)', 'Speed: Slow (scales)', 'Dodge: 0%', 'Special: Extremely durable, a major threat'], tags: [] }, { name: 'Shield Enemy', assetId: 'shield_enemy', description: 'Carries a large shield, making it very resilient. Appears from score 30.', stats: ['Health: High (scales significantly)', 'Speed: Very Slow (scales)', 'Dodge: 0%', 'Special: Absorbs a lot of damage'], tags: [] }, { name: 'Wizard', assetId: 'wizard_enemy', description: 'A magical foe that can teleport to advance. Appears from score 25.', stats: ['Health: Low (scales)', 'Speed: Slow, but teleports (movement speed scales)', 'Dodge: 0%', 'Special: Teleports forward periodically'], tags: ['Green'] }, { name: 'Spearman', assetId: 'spearman', description: 'A common enemy unit. Appears from score 10.', stats: ['Health: Low (scales)', 'Speed: Moderate (scales)', 'Dodge: 0%'], tags: ['Green'] }, { name: 'War Elephant', assetId: 'war_elephant', description: 'A massive, slow behemoth that spawns Spearmen upon defeat. Appears from score 125.', stats: ['Health: Extremely High (scales massively)', 'Speed: Extremely Slow (scales)', 'Dodge: 0%', 'Special: Spawns 5 Spearmen on death'], tags: ['Green'] }, { name: 'Elite Knight', assetId: 'elite_knight_asset', description: 'A heavily armored knight, tougher than its standard counterpart. Appears from score 100.', stats: ['Health: High (scales significantly)', 'Speed: Slower (scales)', 'Dodge: 0%', 'Special: Superior durability'], tags: [] }, { name: 'Elite Shield', assetId: 'elite_shield_asset', description: 'The ultimate defensive unit with an incredibly tough shield. Appears from score 112.', stats: ['Health: Very High (scales massively)', 'Speed: Extremely Slow (scales)', 'Dodge: 0%', 'Special: Unmatched resilience'], tags: [] }, { name: 'Shaman', assetId: 'shaman_enemy', description: 'A mystical enemy that interferes with your reload. Appears from score 35.', stats: ['Health: Low (scales)', 'Speed: Moderate (scales)', 'Dodge: 0%', 'Special: Periodically increases player arrow reload count'], tags: ['Green'] }, { name: 'Hot Air Balloon', assetId: 'hot_air_balloon_asset', description: 'A slow aerial transport that deploys enemies upon destruction. Appears from score 154.', stats: ['Health: Very Low (1 base)', 'Speed: Very Slow (scales)', 'Dodge: 0%', 'Special: Spawns 5 random non-green enemies on death'], tags: [] }, { name: 'Dark Bowman', assetId: 'dark_bowman_asset', description: 'A sinister archer immune to arrows. Appears from score 55.', stats: ['Health: Low (scales)', 'Speed: Fast (scales)', 'Dodge: 0%', 'Special: Immune to arrow projectiles'], tags: ['Black'] }]; var enemypediaScreenInstance = null; // Instance of the EnemypediaScreen class var isEnemypediaActive = false; // Flag if the enemypedia is currently shown var enemypediaButtonContainer = null; // UI Button to open the pedia // --- 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 } // --- Enemypedia Functions --- function showEnemypediaScreen() { if (isEnemypediaActive || isUpgradePopupActive) return; // Don't show if already active or upgrade popup is on isEnemypediaActive = true; if (enemypediaScreenInstance) { enemypediaScreenInstance.destroy(); } enemypediaScreenInstance = new EnemypediaScreen(); game.addChild(enemypediaScreenInstance); // Music handling can be added here if desired, similar to upgrade popup } function hideEnemypediaScreen() { if (!isEnemypediaActive) return; isEnemypediaActive = false; if (enemypediaScreenInstance) { enemypediaScreenInstance.destroy(); enemypediaScreenInstance = null; } // Music handling can be added here if desired } // Helper function placeholder for adding Swordsman function addSwordsman() { // Create a swordsman tower at the bastion line var tower = new Container(); var towerGraphics = tower.attachAsset('swordsmanTower', { anchorX: 0.5, anchorY: 0.5 }); tower.x = FIRING_POS_X; // Position in the center horizontally tower.y = BASTION_Y - 50; // Position just above the bastion line game.addChild(tower); // Set up interval to spawn swordsmen from the tower var spawnInterval = LK.setInterval(function () { var newSwordsman = new Swordsman(); // Position the swordsman near the tower with a slight random offset newSwordsman.x = tower.x + (Math.random() * 100 - 50); // Random offset of ±50px newSwordsman.y = tower.y; game.addChild(newSwordsman); swordsmen.push(newSwordsman); // Add to the swordsmen array }, 3000); // Spawn a swordsman every 3 seconds // Swordsman spawn does not grant score // Store the interval reference in the tower to potentially clear it later tower.spawnInterval = spawnInterval; } // Define all possible upgrades var allUpgrades = [{ id: 'faster_reload', name: 'Faster Reload', description: 'Decrease reload time by 20%', apply: function apply() { cooldownDuration = Math.max(60, Math.floor(cooldownDuration * 0.8)); } }, // Min 1 sec cooldown { id: 'piercing_shots', name: 'Piercing Shots', description: 'Arrows pierce +1 enemy', apply: function apply() { arrowPierceLevel++; } }, { id: 'quiver', name: 'Quiver', description: '+5 Arrows before reload', apply: function apply() { maxArrowsBeforeCooldown += 5; } }, { id: 'heat_tipped', name: 'Heat-tipped Arrows', description: 'Arrows deal +1 damage', apply: function apply() { arrowDamage += 1; console.log("Heat-tipped Arrows upgrade chosen - damage increased to " + arrowDamage); } }, { id: 'archer_ally', name: 'Archer Ally', description: 'Gain an allied archer', apply: addArcherAlly }, { id: 'sabotage', name: 'Sabotage', description: 'Enemies 10% slower', apply: function apply() { enemySpeedMultiplier = Math.max(0.5, enemySpeedMultiplier * 0.9); applySabotage(); } }, // Max 50% slow { id: 'swordsman', name: 'Swordsman', description: 'Gain a melee defender', apply: addSwordsman }, { id: 'more_shots', name: 'More Shots', description: 'Shoot 2 arrows at once', apply: function apply() { multiShotEnabled = true; // Assuming a global flag for this } }, { id: 'poison_shots', name: 'Poison Shots', description: 'Shots deal damage over time', apply: function apply() { poisonShotsEnabled = true; // Assuming a global flag for this } }, { id: 'cannon', name: 'Cannon', description: 'Gain a cannon that targets the strongest enemy', apply: addCannon // Assuming a helper function addCannon }, { id: 'ally_atk_speed', name: 'Ally ATK Speed', description: 'All allies attack faster', apply: function apply() { allyAttackSpeedMultiplier *= 0.8; // Assuming a global multiplier // Apply to existing allies as well in their update logic or a helper function } }, { id: 'wizard_tower', name: 'Wizard Tower', description: 'Gain a wizard tower ally that shoots magic balls that slowdown enemies', apply: addWizardTower // Assuming a helper function addWizardTower }, { id: 'aimbot', name: 'Aimbot', description: 'Arrows seek out enemies', apply: function apply() { aimbotEnabled = true; // Assuming a global flag for this } }, { id: 'green_killer', name: 'Green Killer', description: '+50% Damage vs Green Enemies', apply: function apply() { greenKillerEnabled = true; } }, { id: 'refined_projectiles', name: 'Refined Projectiles', description: 'Non-arrow projectiles +5 damage & faster', apply: function apply() { refinedProjectilesEnabled = true; // Existing projectiles won't update, new ones will have bonus } }, { id: 'viking_ally', name: 'Viking Ally', description: 'Gain a viking that throws piercing axes', apply: addVikingAlly // Assuming helper function addVikingAlly }, { id: 'green_slowdown', name: 'Green Slowdown', description: 'Projectiles slow Green enemies 10% for 10s (no stack)', apply: function apply() { greenSlowdownEnabled = true; } }]; // Function to create and show the upgrade selection popup function showUpgradePopup() { isUpgradePopupActive = true; // Simple pause: Game logic checks isUpgradePopupActive flag in game.update // Switch to upgrade theme music with fade effect if (currentMusicPlaying !== 'UpgradeTheme' && !musicFadeInProgress) { musicFadeInProgress = true; // Fade out current music LK.playMusic('Gamemusic', { fade: { start: 1, end: 0, duration: 800 } }); // After fade out completes, start upgrade theme with fade in LK.setTimeout(function () { LK.playMusic('UpgradeTheme', { fade: { start: 0, end: 1, duration: 1000 } }); currentMusicPlaying = 'UpgradeTheme'; musicFadeInProgress = false; }, 850); } // --- Create Popup UI --- upgradePopup = new Container(); upgradePopup.x = GAME_WIDTH / 2; upgradePopup.y = GAME_HEIGHT / 2; game.addChild(upgradePopup); // Add to game layer for positioning relative to center // Add a semi-transparent background overlay var bg = upgradePopup.attachAsset('bastionLine', { // Reusing an asset for shape width: 1200, height: 800, color: 0x000000, // Black background anchorX: 0.5, anchorY: 0.5, alpha: 0.8 }); // Add title text var title = new Text2('Choose an Upgrade!', { size: 80, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.y = -300; // Position relative to popup center upgradePopup.addChild(title); // --- Select 3 Random Unique Upgrades --- var availableToOffer = allUpgrades.slice(); // Copy available upgrades var choices = []; var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices for (var i = 0; i < numChoices; i++) { var randomIndex = Math.floor(Math.random() * availableToOffer.length); choices.push(availableToOffer[randomIndex]); availableToOffer.splice(randomIndex, 1); // Remove chosen upgrade to ensure uniqueness } // --- Create Buttons for Choices --- var buttonYStart = -150; var buttonSpacing = 180; for (var j = 0; j < choices.length; j++) { var upgrade = choices[j]; var button = createUpgradeButton(upgrade, buttonYStart + j * buttonSpacing); upgradePopup.addChild(button); } } // Function to create a single upgrade button function createUpgradeButton(upgradeData, yPos) { var buttonContainer = new Container(); buttonContainer.y = yPos; // Button background var buttonBg = buttonContainer.attachAsset('bastionLine', { // Reusing asset for shape width: 800, height: 150, color: 0x555555, anchorX: 0.5, anchorY: 0.5 }); // Button text (Name + Description) var nameText = new Text2(upgradeData.name, { size: 50, fill: 0xFFFFFF }); nameText.anchor.set(0.5, 0.5); nameText.y = -25; buttonContainer.addChild(nameText); var descText = new Text2(upgradeData.description, { size: 35, fill: 0xCCCCCC }); descText.anchor.set(0.5, 0.5); descText.y = 30; buttonContainer.addChild(descText); // Make button interactive buttonContainer.interactive = true; // Needed for down event // Event handler for button press buttonContainer.down = function (x, y, obj) { upgradeData.apply(); // Apply the selected upgrade's effect hideUpgradePopup(); // Close the popup }; return buttonContainer; } // Function to hide and destroy the upgrade popup function hideUpgradePopup() { if (upgradePopup) { upgradePopup.destroy(); upgradePopup = null; } isUpgradePopupActive = false; // Switch back to game music with fade effect if (currentMusicPlaying !== 'Gamemusic' && !musicFadeInProgress) { musicFadeInProgress = true; // Fade out upgrade theme LK.playMusic('UpgradeTheme', { fade: { start: 1, end: 0, duration: 800 } }); // After fade out completes, start game music with fade in LK.setTimeout(function () { LK.playMusic('Gamemusic', { fade: { start: 0, end: 1, duration: 1000 } }); currentMusicPlaying = 'Gamemusic'; musicFadeInProgress = false; }, 850); } // Resume game logic (handled by checking flag in game.update) } // --- Visual Setup --- var bastionLine = game.addChild(LK.getAsset('bastionLine', { anchorX: 0.0, // Anchor at the left edge. anchorY: 0.5, // Anchor vertically centered. x: 0, // Position at the left edge of the screen. y: BASTION_Y // Position at the defined bastion Y-coordinate. })); // Create and configure the score display text. scoreTxt = new Text2('0', { size: 150, // Font size. fill: 0xFFFFFF // White color. }); scoreTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge. // Add score text to the GUI layer at the top-center position. LK.gui.top.addChild(scoreTxt); scoreTxt.y = 30; // Add padding below the top edge. Ensure it's clear of the top-left menu icon area. // Create and configure the ammo display text. ammoTxt = new Text2('Ammo: ' + maxArrowsBeforeCooldown, { size: 80, fill: 0xFFFFFF // White color. }); ammoTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge. LK.gui.top.addChild(ammoTxt); ammoTxt.y = 180; // Position below the score text // --- 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 // Ensure Enemypedia is not active before showing upgrade popup if (!isUpgradePopupActive && !isEnemypediaActive && 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 or Enemypedia is Active --- if (isUpgradePopupActive || isEnemypediaActive) { // Added isEnemypediaActive check // Potential: Update UI animations if any for popups return; // Skip the rest of the game update loop } // --- Resume Game Logic --- // This redundant check can be removed due to the combined check above. // if (isUpgradePopupActive) { // return; // Skip enemy movement and spawning if upgrade popup is active // } // Decrease cooldown timer if active. if (cooldownTimer > 0) { cooldownTimer--; ammoTxt.setText('Reloading...'); // Show reloading status } else if (ammoTxt.text !== 'Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)) { // Ensure ammo display is correct if not reloading ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); } // 1. Update and check Arrows for (var i = arrows.length - 1; i >= 0; i--) { var arrow = arrows[i]; // Note: LK engine calls arrow.update() automatically as it's added to the game stage. // Initialize last position if it hasn't been set yet (first frame). if (arrow.lastY === undefined) { arrow.lastY = arrow.y; } if (arrow.lastX === undefined) { arrow.lastX = arrow.x; } // Check for collisions between the current arrow and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (arrow.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Calculate damage, applying Green Killer bonus if applicable var damageToDeal = arrow.damage; if (greenKillerEnabled && enemy.tag === 'Green') { damageToDeal *= 1.5; // Apply 50% damage bonus } // Apply damage to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(damageToDeal, arrow); // Pass the arrow object as source if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Handle arrow piercing arrow.pierceLeft--; // Decrease remaining pierces if (arrow.pierceLeft <= 0) { // Arrow has no pierces left, destroy it arrow.destroy(); arrows.splice(i, 1); // Remove arrow from array. hitEnemy = true; // Mark that this arrow is done break; // Stop checking this arrow against other enemies as it's destroyed } else { // Arrow pierced this enemy and can continue // We don't set hitEnemy = true here because the arrow continues // We don't break because it might hit another enemy in the same frame further along its path // Apply Green Slowdown if applicable and arrow is still piercing if (!enemyDefeated && greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } } } } // If the arrow hit an enemy, it was destroyed, so skip to the next arrow. if (hitEnemy) { continue; } // Check if the arrow has gone off-screen (top, left, or right). // Use transition detection: check if it was on screen last frame and is off screen now. var wasOnScreen = arrow.lastY > -arrow.height / 2 && arrow.lastX > -arrow.width / 2 && arrow.lastX < GAME_WIDTH + arrow.width / 2; var isOffScreen = arrow.y < -arrow.height / 2 || // Off top edge arrow.x < -arrow.width / 2 || // Off left edge arrow.x > GAME_WIDTH + arrow.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Arrow is off-screen, destroy it and remove from the array. arrow.destroy(); arrows.splice(i, 1); } else if (!isOffScreen) { // Update last known position only if the arrow is still potentially on screen arrow.lastY = arrow.y; arrow.lastX = arrow.x; } } // 2. Update and check Cannonballs for (var cb = cannonballs.length - 1; cb >= 0; cb--) { var cannonball = cannonballs[cb]; // Note: LK engine calls cannonball.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (cannonball.lastY === undefined) { cannonball.lastY = cannonball.y; } if (cannonball.lastX === undefined) { cannonball.lastX = cannonball.x; } // Check for collisions between the current cannonball and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (cannonball.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply damage to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(cannonball.damage); // Cannonballs don't apply poison if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Cannonballs are destroyed on hit cannonball.destroy(); // Apply Green Slowdown if applicable before destroying cannonball if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } cannonball.destroy(); cannonballs.splice(cb, 1); // Remove cannonball from array. hitEnemy = true; // Mark that this cannonball is done break; // Stop checking this cannonball against other enemies as it's destroyed } } // If the cannonball hit an enemy, it was destroyed, so skip to the next cannonball. if (hitEnemy) { continue; } // Check if the cannonball has gone off-screen. var wasOnScreen = cannonball.lastY > -cannonball.height / 2 && cannonball.lastX > -cannonball.width / 2 && cannonball.lastX < GAME_WIDTH + cannonball.width / 2; var isOffScreen = cannonball.y < -cannonball.height / 2 || // Off top edge cannonball.x < -cannonball.width / 2 || // Off left edge cannonball.x > GAME_WIDTH + cannonball.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Cannonball is off-screen, destroy it and remove from the array. cannonball.destroy(); cannonballs.splice(cb, 1); } else if (!isOffScreen) { // Update last known position only if the cannonball is still potentially on screen cannonball.lastY = cannonball.y; cannonball.lastX = cannonball.x; } } // 3. Update and check Enemies for (var k = enemies.length - 1; k >= 0; k--) { var enemy = enemies[k]; // Note: LK engine calls enemy.update() automatically. // Initialize last position if not set. if (enemy.lastY === undefined) { enemy.lastY = enemy.y; } // Check if the enemy has reached or passed the bastion line. // Use transition detection: was the enemy's bottom edge above the line last frame, // and is it on or below the line now? var enemyBottomY = enemy.y + enemy.height / 2; var enemyLastBottomY = enemy.lastY + enemy.height / 2; var wasAboveBastion = enemyLastBottomY < BASTION_Y; var isAtOrBelowBastion = enemyBottomY >= BASTION_Y; if (wasAboveBastion && isAtOrBelowBastion) { // Enemy reached the bastion! Game Over. LK.getSound('gameOverSfx').play(); // Play game over sound effect. LK.showGameOver(); // Trigger the engine's game over sequence. // LK.showGameOver handles game state reset, no need to manually clear arrays here. return; // Exit the update loop immediately as the game is over. } else { // Update last known position if game is not over for this enemy. enemy.lastY = enemy.y; } } // 3. Update and check Swordsmen (allies) for (var l = swordsmen.length - 1; l >= 0; l--) { var swordsman = swordsmen[l]; // Swordsman update method handles its own lifetime and attacking // Check if the swordsman has been destroyed by its lifetime timer if (!swordsman.parent) { // If it no longer has a parent, it has been destroyed swordsmen.splice(l, 1); // Remove swordsman from array } } // 4. Update and check Cannons (allies) for (var m = cannons.length - 1; m >= 0; m--) { var cannon = cannons[m]; // Cannons do not have a lifetime timer or destruction logic in this basic version // If they had, we'd check !cannon.parent and splice here as in Swordsmen } // 5. Update and check Wizard Towers (allies) for (var wt = wizardTowers.length - 1; wt >= 0; wt--) { var tower = wizardTowers[wt]; // Wizard towers do not have a lifetime timer or destruction logic in this basic version // If they had, we'd check !tower.parent and splice here } // 5.5 Update and check Viking Allies for (var va = vikingAllies.length - 1; va >= 0; va--) { var viking = vikingAllies[va]; // Vikings do not have a lifetime timer or destruction logic in this version // LK engine calls viking.update() automatically. } // 6. Update and check Magic Balls for (var mb = magicBalls.length - 1; mb >= 0; mb--) { var magicBall = magicBalls[mb]; // Note: LK engine calls magicBall.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (magicBall.lastY === undefined) { magicBall.lastY = magicBall.y; } if (magicBall.lastX === undefined) { magicBall.lastX = magicBall.x; } // Check for collisions between the current magic ball and all enemies. var hitEnemy = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (magicBall.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply damage (minimal) and slowdown effect to the enemy and check if it was defeated var enemyDefeated = enemy.takeDamage(magicBall.damage, magicBall); // Pass the magic ball object as source if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Destroy the enemy enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Magic balls are destroyed on hit magicBall.destroy(); // Apply Green Slowdown if applicable before destroying magic ball if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { // Note: Enemy.takeDamage already checks for MagicBall source to apply its default slow. // This adds the Green Slowdown effect *on top* if applicable and not already slowed. // However, takeDamage applies slow based on MagicBall properties. Let's apply Green Slowdown here explicitly. enemy.slowTimer = 10 * 60; // 10 seconds enemy.currentSlowAmount = 0.9; // 10% slow } magicBall.destroy(); magicBalls.splice(mb, 1); // Remove magic ball from array. hitEnemy = true; // Mark that this magic ball is done break; // Stop checking this magic ball against other enemies as it's destroyed } } // If the magic ball hit an enemy, it was destroyed, so skip to the next magic ball. if (hitEnemy) { continue; } // Check if the magic ball has gone off-screen. var wasOnScreen = magicBall.lastY > -magicBall.height / 2 && magicBall.lastX > -magicBall.width / 2 && magicBall.lastX < GAME_WIDTH + magicBall.width / 2; var isOffScreen = magicBall.y < -magicBall.height / 2 || // Off top edge magicBall.x < -magicBall.width / 2 || // Off left edge magicBall.x > GAME_WIDTH + magicBall.width / 2; // Off right edge if (wasOnScreen && isOffScreen) { // Magic ball is off-screen, destroy it and remove from the array. magicBall.destroy(); magicBalls.splice(mb, 1); } else if (!isOffScreen) { // Update last known position only if the magic ball is still potentially on screen magicBall.lastY = magicBall.y; magicBall.lastX = magicBall.x; } } // 6.5 Update and check Viking Axes for (var axeIdx = vikingAxes.length - 1; axeIdx >= 0; axeIdx--) { var axe = vikingAxes[axeIdx]; // Note: LK engine calls axe.update() automatically. // Initialize last position if it hasn't been set yet (first frame). if (axe.lastY === undefined) { axe.lastY = axe.y; } if (axe.lastX === undefined) { axe.lastX = axe.x; } // Check for collisions between the current axe and all enemies. var hitEnemyAxe = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; if (axe.intersects(enemy)) { // Collision detected! LK.getSound('hit').play(); // Play hit sound. // Apply Green Killer bonus if applicable (Vikings benefit too) var damageToDealAxe = axe.damage; if (greenKillerEnabled && enemy.tag === 'Green') { damageToDealAxe *= 1.5; // Apply 50% damage bonus } // Apply damage and check if defeated var enemyDefeated = enemy.takeDamage(damageToDealAxe, axe); // Pass axe as source for potential effects if (enemyDefeated) { LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } // Handle axe piercing axe.pierceLeft--; // Decrease remaining pierces if (axe.pierceLeft <= 0) { // Axe has no pierces left, destroy it axe.destroy(); vikingAxes.splice(axeIdx, 1); // Remove axe from array. hitEnemyAxe = true; // Mark that this axe is done break; // Stop checking this axe against other enemies } else { // Axe pierced this enemy and can continue // Potentially apply Green Slowdown if enabled if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) { enemy.slowTimer = 10 * 60; enemy.currentSlowAmount = 0.9; } } } } // If the axe hit and was destroyed, skip to the next axe. if (hitEnemyAxe) { continue; } // Check if the axe has gone off-screen. var wasOnScreenAxe = axe.lastY > -axe.height / 2 && axe.lastX > -axe.width / 2 && axe.lastX < GAME_WIDTH + axe.width / 2; var isOffScreenAxe = axe.y < -axe.height / 2 || axe.y > GAME_HEIGHT + axe.height / 2 || axe.x < -axe.width / 2 || axe.x > GAME_WIDTH + axe.width / 2; if (wasOnScreenAxe && isOffScreenAxe) { axe.destroy(); vikingAxes.splice(axeIdx, 1); } else if (!isOffScreenAxe) { axe.lastY = axe.y; axe.lastX = axe.x; } } // 7. Spawn new Enemies periodically // Use LK.ticks and the spawn interval. Ensure interval doesn't go below minimum. if (LK.ticks % Math.max(minEnemySpawnInterval, Math.floor(enemySpawnInterval)) === 0) { var currentScore = LK.getScore(); var baseSpeedForSpawn = Math.min(maxEnemySpeed, currentEnemySpeed); // Base speed adjusted by game difficulty progression // Determine if this spawn *could* be a boss (e.g., every 7th spawn after score 10) var potentialBoss = (enemies.length + 1) % 7 === 0 && currentScore >= 10; var typeToSpawn = 'swordsman'; // Default type var enemyHealth = 1; // Default health var enemyDodgeChance = 0; // Default dodge chance var enemySpeed = baseSpeedForSpawn; // Start with base speed // Determine base type based on score thresholds and randomness var possibleTypes = ['swordsman']; if (currentScore >= 10) { // Spearman appears at score 10 possibleTypes.push('spearman'); } if (currentScore >= 15) { possibleTypes.push('thief'); } //{3s} // Adjusted original line identifier if (currentScore >= 23) { possibleTypes.push('knight'); } //{3t} // Adjusted original line identifier if (currentScore >= 25) { possibleTypes.push('wizard'); } // Wizards start appearing at score 25 if (currentScore >= 30) { possibleTypes.push('shield'); } // Shields start appearing at score 30 if (currentScore >= 35) { possibleTypes.push('shaman'); // Shaman appears at score 35 console.log("Shaman added to possible enemy types"); } if (currentScore >= 55) { possibleTypes.push('dark_bowman'); // Dark bowman appears at score 55 } if (currentScore >= 100) { possibleTypes.push('elite_knight'); } // Elite Knights start appearing at score 100 if (currentScore >= 112) { possibleTypes.push('elite_shield'); // Elite Shields appear at score 112 } if (currentScore >= 125) { // War Elephant appears at score 125 possibleTypes.push('war_elephant'); } if (currentScore >= 154) { // Hot Air Balloon appears at score 154 possibleTypes.push('hot_air_balloon'); } // Randomly select a type from the available pool for this score level var chosenTypeIndex = Math.floor(Math.random() * possibleTypes.length); typeToSpawn = possibleTypes[chosenTypeIndex]; // Set stats based on chosen type and apply score-based scaling if (typeToSpawn === 'knight') { var baseKnightHP = 5; var hpIncreaseIntervalKnight = 30; // Health increases every 30 score points after appearing var hpIncreasesKnight = Math.floor(Math.max(0, currentScore - 23) / hpIncreaseIntervalKnight); enemyHealth = baseKnightHP + hpIncreasesKnight * 5; enemySpeed *= 0.9; // Knights are slightly slower than base swordsman speed } else if (typeToSpawn === 'thief') { enemyHealth = 1; // Thieves are fragile enemyDodgeChance = 0.10; // 10% dodge chance var speedIncreaseIntervalThief = 25; // Speed increases every 25 score points after appearing var speedIncreasePercentThief = 0.05; // 5% speed increase each time var speedIncreasesThief = Math.floor(Math.max(0, currentScore - 15) / speedIncreaseIntervalThief); var thiefSpeedMultiplier = Math.pow(1 + speedIncreasePercentThief, speedIncreasesThief); enemySpeed *= 1.2 * thiefSpeedMultiplier; // Thieves are faster base + scaling speed } else if (typeToSpawn === 'shield') { var baseShieldHP = 20; var hpIncreaseIntervalShield = 35; // Gains HP every 35 score points after appearing var hpIncreasesShield = Math.floor(Math.max(0, currentScore - 30) / hpIncreaseIntervalShield); enemyHealth = baseShieldHP + hpIncreasesShield * 20; enemySpeed *= 0.5; // Shield enemies are very slow enemyDodgeChance = 0; } else if (typeToSpawn === 'shaman') { var baseShamanHP = 2; var statIncreaseIntervalShaman = 10; // Gains stats every 10 score points after appearing var statIncreasesShaman = Math.floor(Math.max(0, currentScore - 35) / statIncreaseIntervalShaman); enemyHealth = baseShamanHP + statIncreasesShaman * 1; // Gains 1 HP per interval enemySpeed *= 0.8 * Math.pow(1.04, statIncreasesShaman); // Base speed, gains 4% speed per interval enemyDodgeChance = 0; // Shaman cannot dodge } else if (typeToSpawn === 'wizard') { var baseWizardHP = 2; var statIncreaseIntervalWizard = 10; // Gains stats every 10 score points after appearing var statIncreasesWizard = Math.floor(Math.max(0, currentScore - 25) / statIncreaseIntervalWizard); enemyHealth = baseWizardHP + statIncreasesWizard * 1; // Gains 1 HP per interval enemySpeed *= 0.7 * Math.pow(1.05, statIncreasesWizard); // Slow base, gains 5% speed per interval enemyDodgeChance = 0; } else if (typeToSpawn === 'elite_knight') { var baseEliteKnightHP = 45; var hpIncreaseIntervalElite = 30; // Gains HP every 30 score points after appearing var hpIncreasesElite = Math.floor(Math.max(0, currentScore - 100) / hpIncreaseIntervalElite); enemyHealth = baseEliteKnightHP + hpIncreasesElite * 15; enemySpeed *= 0.85; // Slightly slower than base knight speed enemyDodgeChance = 0; } else if (typeToSpawn === 'elite_shield') { var baseEliteShieldHP = 200; // Starts with 200 HP var hpIncreaseIntervalEliteShield = 20; // Gains HP every 20 score points after appearing var hpIncreasesEliteShield = Math.floor(Math.max(0, currentScore - 112) / hpIncreaseIntervalEliteShield); enemyHealth = baseEliteShieldHP + hpIncreasesEliteShield * 50; // Gains 50 HP per interval enemySpeed *= 0.4; // Elite Shield enemies are slower than regular shield enemies enemyDodgeChance = 0; } else if (typeToSpawn === 'spearman') { var baseSpearmanHP = 3; var hpIncreaseIntervalSpearman = 10; // Gains HP every 10 score points after appearing var hpIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / hpIncreaseIntervalSpearman); enemyHealth = baseSpearmanHP + hpIncreasesSpearman * 3; // Gains 3 HP per interval var speedIncreaseIntervalSpearman = 10; // Gains speed every 10 score points after appearing var speedIncreasePercentSpearman = 0.05; // 5% speed increase each time var speedIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / speedIncreaseIntervalSpearman); var spearmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentSpearman, speedIncreasesSpearman); enemySpeed *= 1.0 * spearmanSpeedMultiplier; // Base speed + scaling speed enemyDodgeChance = 0; } else if (typeToSpawn === 'war_elephant') { var baseElephantHP = 500; var hpIncreaseIntervalElephant = 15; // Gains HP every 15 score points after appearing var hpIncreasesElephant = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalElephant); enemyHealth = baseElephantHP + hpIncreasesElephant * 100; // Gains 100 HP per interval enemySpeed *= 0.3; // War Elephants are very slow enemyDodgeChance = 0; // Note: War elephant death spawns spearmen - this will be handled in the Enemy death logic. } else if (typeToSpawn === 'hot_air_balloon') { enemyHealth = 1; // Always has 1 HP enemySpeed *= 0.2; // Very slow movement enemyDodgeChance = 0; // Note: Hot air balloon death spawns 5 random non-green enemies - handled in Enemy death logic } else if (typeToSpawn === 'dark_bowman') { var baseDarkBowmanHP = 5; var statIncreaseIntervalBowman = 10; // Gains stats every 10 score points after appearing var statIncreasesBowman = Math.floor(Math.max(0, currentScore - 55) / statIncreaseIntervalBowman); enemyHealth = baseDarkBowmanHP + statIncreasesBowman * 3; // Gains 3 HP per interval var speedIncreasePercentBowman = 0.05; // 5% speed increase each time var bowmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentBowman, statIncreasesBowman); enemySpeed *= 1.1 * bowmanSpeedMultiplier; // Slightly faster than base speed + scaling enemyDodgeChance = 0; // Note: Dark bowman has Black tag making it immune to arrows } else { // Swordsman (default) enemyHealth = 1; // Speed remains baseSpeedForSpawn initially } // Check if this spawn should be overridden to be a Boss if (potentialBoss) { typeToSpawn = 'boss'; // Set type to boss enemyHealth = 5 + Math.floor(currentScore / 8); // Boss health scales significantly with score enemySpeed = baseSpeedForSpawn * 0.7; // Bosses are slower but much tougher (reset speed based on base) enemyDodgeChance = 0; // Bosses typically don't dodge } // Apply the global Sabotage speed multiplier AFTER type-specific adjustments enemySpeed *= enemySpeedMultiplier; // Create the new enemy instance with the calculated stats var newEnemy = new Enemy(typeToSpawn, enemySpeed, enemyHealth, enemyDodgeChance); // Position the new enemy at the top, random horizontal position with padding // Use the actual width of the created enemy's graphic for padding calculation // Need to access width after creation, use a sensible default or estimate if needed before creation var tempAsset = LK.getAsset(newEnemy.assetId || 'enemy', {}); // Get asset dimensions (might need refinement if assetId isn't on newEnemy yet) var spawnPadding = (tempAsset ? tempAsset.width / 2 : 100) + 20; // Use default if asset not found easily newEnemy.x = spawnPadding + Math.random() * (GAME_WIDTH - 2 * spawnPadding); newEnemy.y = -(tempAsset ? tempAsset.height / 2 : 100); // Start just above the top edge. // Initialize last position for state tracking (used for bastion collision check). newEnemy.lastY = newEnemy.y; // Add the new enemy to the game scene and the tracking array. game.addChild(newEnemy); enemies.push(newEnemy); // Increase difficulty for the next spawn: decrease spawn interval and increase base speed. // These affect the *next* potential spawn's base calculations. enemySpawnInterval -= enemySpawnRateDecrease; currentEnemySpeed += enemySpeedIncrease; } //{2s} // Adjusted original line identifier }; // --- Initial Game Setup --- // Set the initial score text based on the starting score (which is 0). scoreTxt.setText(LK.getScore()); // Set the initial ammo display. if (ammoTxt) { // Ensure ammoTxt is initialized ammoTxt.setText('Ammo: ' + maxArrowsBeforeCooldown); } ; // Play the background music. // Check if we need to show a title screen first if (!isGameStarted) {// Assuming a flag 'isGameStarted' for title screen state // Don't play music immediately if showing title screen } else { LK.playMusic('Gamemusic'); ; } // Placeholder for the title screen container var titleScreen = null; // Flag to track game start state var isGameStarted = false; // Function to show the title screen function showTitleScreen() { isGameStarted = false; // Ensure game is not started // Create the title screen container titleScreen = new Container(); titleScreen.x = GAME_WIDTH / 2; titleScreen.y = GAME_HEIGHT / 2; game.addChild(titleScreen); // Add title text var titleText = new Text2('Defend Your Bastion!', { size: 120, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0.5); titleText.y = -200; titleScreen.addChild(titleText); // Add a simple instruction text var instructionText = new Text2('Tap to Start', { size: 60, fill: 0xCCCCCC }); instructionText.anchor.set(0.5, 0.5); instructionText.y = 100; titleScreen.addChild(instructionText); // Make the title screen interactive to start the game titleScreen.interactive = true; titleScreen.down = function () { startGame(); // Start the game when tapped }; if (enemypediaButtonContainer) { enemypediaButtonContainer.visible = false; // Hide pedia button during title screen } // 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; } // Create and add Enemypedia button if (!enemypediaButtonContainer) { enemypediaButtonContainer = new Container(); var pediaButtonBgAsset = LK.getAsset('pedia_button_bg', {}); // Get asset for size var pediaButtonBg = enemypediaButtonContainer.attachAsset('pedia_button_bg', { anchorX: 0.5, anchorY: 0.5 }); var pediaButtonText = new Text2('Enemies', { size: 35, fill: 0xFFFFFF }); pediaButtonText.anchor.set(0.5, 0.5); enemypediaButtonContainer.addChild(pediaButtonText); enemypediaButtonContainer.interactive = true; enemypediaButtonContainer.down = function () { if (!isUpgradePopupActive) { // Only open if upgrade popup isn't active showEnemypediaScreen(); } }; LK.gui.topRight.addChild(enemypediaButtonContainer); // Position it with padding from top-right corner enemypediaButtonContainer.x = -(pediaButtonBgAsset.width / 2 + 30); enemypediaButtonContainer.y = pediaButtonBgAsset.height / 2 + 30; } enemypediaButtonContainer.visible = true; // Reset game state if needed (LK might handle this on game start) // Play game music LK.playMusic('Gamemusic'); } // Initially show the title screen showTitleScreen();
===================================================================
--- original.js
+++ change.js
@@ -513,8 +513,216 @@
graphics.scale.set(1.3); // Slightly larger than normal shield
} //{11} // Modified original line identifier location
return self; // Return self for potential inheritance
});
+var EnemypediaScreen = Container.expand(function () {
+ var self = Container.call(this);
+ self.currentPage = 0;
+ self.totalPages = enemyPediaData.length;
+ // Background
+ var bgAssetHeight = LK.getAsset('pedia_screen_bg', {}).height; // Get actual asset height for calculations
+ var bgAssetWidth = LK.getAsset('pedia_screen_bg', {}).width; // Get actual asset width
+ var bg = self.attachAsset('pedia_screen_bg', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ // width and height are from asset default
+ });
+ self.x = GAME_WIDTH / 2;
+ self.y = GAME_HEIGHT / 2;
+ // Title
+ var titleText = new Text2('Enemypedia', {
+ size: 90,
+ fill: 0xFFFFFF
+ });
+ titleText.anchor.set(0.5, 0.5);
+ titleText.x = 0;
+ titleText.y = -(bgAssetHeight / 2) + 70; // Positioned from top of BG
+ self.addChild(titleText);
+ // Content container for enemy details
+ self.contentContainer = new Container();
+ // Position content container centrally between title and nav buttons
+ self.contentContainer.x = 0; // Centered horizontally
+ self.contentContainer.y = titleText.y + titleText.height / 2 + 20; // Below title
+ self.addChild(self.contentContainer);
+ // Navigation and Close Buttons
+ var navButtonYPos = bgAssetHeight / 2 - 60; // Positioned from bottom of BG
+ var closeButton = new Container();
+ var closeButtonBg = closeButton.attachAsset('pedia_nav_button_bg', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ color: 0xAA4444
+ });
+ var closeButtonText = new Text2('Close', {
+ size: 40,
+ fill: 0xFFFFFF
+ });
+ closeButtonText.anchor.set(0.5, 0.5);
+ closeButton.addChild(closeButtonText);
+ closeButton.x = 0;
+ closeButton.y = navButtonYPos;
+ closeButton.interactive = true;
+ closeButton.down = function () {
+ hideEnemypediaScreen();
+ };
+ self.addChild(closeButton);
+ var prevButton = new Container();
+ var prevButtonBg = prevButton.attachAsset('pedia_nav_button_bg', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ var prevButtonText = new Text2('Previous', {
+ size: 40,
+ fill: 0xFFFFFF
+ });
+ prevButtonText.anchor.set(0.5, 0.5);
+ prevButton.addChild(prevButtonText);
+ prevButton.x = -(closeButtonBg.width / 2 + prevButtonBg.width / 2 + 20); // Place left of close
+ prevButton.y = navButtonYPos;
+ prevButton.interactive = true;
+ prevButton.down = function () {
+ self.changePage(-1);
+ };
+ self.addChild(prevButton);
+ var nextButton = new Container();
+ var nextButtonBg = nextButton.attachAsset('pedia_nav_button_bg', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ var nextButtonText = new Text2('Next', {
+ size: 40,
+ fill: 0xFFFFFF
+ });
+ nextButtonText.anchor.set(0.5, 0.5);
+ nextButton.addChild(nextButtonText);
+ nextButton.x = closeButtonBg.width / 2 + nextButtonBg.width / 2 + 20; // Place right of close
+ nextButton.y = navButtonYPos;
+ nextButton.interactive = true;
+ nextButton.down = function () {
+ self.changePage(1);
+ };
+ self.addChild(nextButton);
+ self.pageNumberText = new Text2('', {
+ size: 30,
+ fill: 0xCCCCCC
+ });
+ self.pageNumberText.anchor.set(0.5, 0.5);
+ self.pageNumberText.x = 0;
+ self.pageNumberText.y = navButtonYPos - prevButtonBg.height / 2 - 25; // Above nav buttons
+ self.addChild(self.pageNumberText);
+ self.changePage = function (direction) {
+ if (self.totalPages === 0) return;
+ self.currentPage += direction;
+ if (self.currentPage < 0) self.currentPage = self.totalPages - 1;
+ if (self.currentPage >= self.totalPages) self.currentPage = 0;
+ self.renderPage();
+ };
+ self.renderPage = function () {
+ self.contentContainer.removeChildren();
+ if (self.totalPages === 0) {
+ var noDataText = new Text2('No enemy data available.', {
+ size: 50,
+ fill: 0xFFFFFF
+ });
+ noDataText.anchor.set(0.5, 0.5);
+ self.contentContainer.addChild(noDataText);
+ self.pageNumberText.setText('');
+ prevButton.visible = false;
+ nextButton.visible = false;
+ return;
+ }
+ prevButton.visible = true;
+ nextButton.visible = true;
+ var enemy = enemyPediaData[self.currentPage];
+ if (!enemy) return;
+ self.pageNumberText.setText('Page ' + (self.currentPage + 1) + ' of ' + self.totalPages);
+ var contentCurrentY = -(bgAssetHeight / 2) * 0.45; // Start Y for content, relative to contentContainer's center
+ var enemyImage = LK.getAsset(enemy.assetId, {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ var maxImgSize = 280;
+ var imgScale = 1;
+ if (enemyImage.width > maxImgSize || enemyImage.height > maxImgSize) {
+ imgScale = Math.min(maxImgSize / enemyImage.width, maxImgSize / enemyImage.height);
+ }
+ enemyImage.scale.set(imgScale);
+ enemyImage.x = 0; // Centered horizontally in contentContainer for now
+ enemyImage.y = contentCurrentY + enemyImage.height * imgScale / 2;
+ self.contentContainer.addChild(enemyImage);
+ contentCurrentY = enemyImage.y + enemyImage.height * imgScale / 2 + 30;
+ var nameText = new Text2(enemy.name, {
+ size: 60,
+ fill: 0xFFD700,
+ align: 'center',
+ wordWrap: true,
+ wordWrapWidth: bgAssetWidth * 0.8
+ });
+ nameText.anchor.set(0.5, 0);
+ nameText.x = 0;
+ nameText.y = contentCurrentY;
+ self.contentContainer.addChild(nameText);
+ contentCurrentY += nameText.height + 20;
+ var descText = new Text2(enemy.description, {
+ size: 38,
+ fill: 0xCCCCCC,
+ align: 'center',
+ style: {
+ wordWrap: true,
+ wordWrapWidth: bgAssetWidth * 0.75,
+ leading: 5
+ }
+ });
+ descText.anchor.set(0.5, 0);
+ descText.x = 0;
+ descText.y = contentCurrentY;
+ self.contentContainer.addChild(descText);
+ contentCurrentY += descText.height + 30;
+ var statsTitle = new Text2("Stats & Abilities:", {
+ size: 40,
+ fill: 0xDDDDFF,
+ align: 'center'
+ });
+ statsTitle.anchor.set(0.5, 0);
+ statsTitle.x = 0;
+ statsTitle.y = contentCurrentY;
+ self.contentContainer.addChild(statsTitle);
+ contentCurrentY += statsTitle.height + 15;
+ for (var i = 0; i < enemy.stats.length; i++) {
+ var statText = new Text2('- ' + enemy.stats[i], {
+ size: 32,
+ fill: 0xBBBBBB,
+ style: {
+ wordWrap: true,
+ wordWrapWidth: bgAssetWidth * 0.7,
+ leading: 5
+ }
+ });
+ statText.anchor.set(0.5, 0); // Center align stats text
+ statText.x = 0;
+ statText.y = contentCurrentY;
+ self.contentContainer.addChild(statText);
+ contentCurrentY += statText.height + 5; // Reduced spacing for stat lines
+ }
+ if (enemy.tags && enemy.tags.length > 0) {
+ contentCurrentY += 15; // Extra space before tags
+ var tagsText = new Text2('Tags: ' + enemy.tags.join(', '), {
+ size: 32,
+ fill: 0xAAAAFF,
+ align: 'center',
+ style: {
+ wordWrap: true,
+ wordWrapWidth: bgAssetWidth * 0.7
+ }
+ });
+ tagsText.anchor.set(0.5, 0);
+ tagsText.x = 0;
+ tagsText.y = contentCurrentY;
+ self.contentContainer.addChild(tagsText);
+ }
+ };
+ self.renderPage();
+ 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.
*/
@@ -877,8 +1085,91 @@
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
+// --- Enemypedia System ---
+var enemyPediaData = [{
+ name: 'Swordsman',
+ assetId: 'enemy',
+ description: 'A basic melee unit. The backbone of early waves.',
+ stats: ['Health: Low (scales with progression)', 'Speed: Moderate (scales)', 'Dodge: 0%'],
+ tags: []
+}, {
+ name: 'Knight',
+ assetId: 'knight',
+ description: 'A tougher melee unit, clad in armor. Appears from score 23.',
+ stats: ['Health: Medium (scales significantly)', 'Speed: Slightly Slower (scales)', 'Dodge: 0%', 'Special: Increased durability'],
+ tags: []
+}, {
+ name: 'Thief',
+ assetId: 'thief',
+ description: 'A fast and evasive unit. Appears from score 15.',
+ stats: ['Health: Very Low (1 base)', 'Speed: Very Fast (scales significantly)', 'Dodge: 10%', 'Special: High speed, chance to dodge attacks'],
+ tags: []
+}, {
+ name: 'Boss',
+ assetId: 'boss',
+ description: 'A formidable leader enemy appearing periodically (every 7th spawn after score 10). Very tough.',
+ stats: ['Health: Very High (scales massively with score)', 'Speed: Slow (scales)', 'Dodge: 0%', 'Special: Extremely durable, a major threat'],
+ tags: []
+}, {
+ name: 'Shield Enemy',
+ assetId: 'shield_enemy',
+ description: 'Carries a large shield, making it very resilient. Appears from score 30.',
+ stats: ['Health: High (scales significantly)', 'Speed: Very Slow (scales)', 'Dodge: 0%', 'Special: Absorbs a lot of damage'],
+ tags: []
+}, {
+ name: 'Wizard',
+ assetId: 'wizard_enemy',
+ description: 'A magical foe that can teleport to advance. Appears from score 25.',
+ stats: ['Health: Low (scales)', 'Speed: Slow, but teleports (movement speed scales)', 'Dodge: 0%', 'Special: Teleports forward periodically'],
+ tags: ['Green']
+}, {
+ name: 'Spearman',
+ assetId: 'spearman',
+ description: 'A common enemy unit. Appears from score 10.',
+ stats: ['Health: Low (scales)', 'Speed: Moderate (scales)', 'Dodge: 0%'],
+ tags: ['Green']
+}, {
+ name: 'War Elephant',
+ assetId: 'war_elephant',
+ description: 'A massive, slow behemoth that spawns Spearmen upon defeat. Appears from score 125.',
+ stats: ['Health: Extremely High (scales massively)', 'Speed: Extremely Slow (scales)', 'Dodge: 0%', 'Special: Spawns 5 Spearmen on death'],
+ tags: ['Green']
+}, {
+ name: 'Elite Knight',
+ assetId: 'elite_knight_asset',
+ description: 'A heavily armored knight, tougher than its standard counterpart. Appears from score 100.',
+ stats: ['Health: High (scales significantly)', 'Speed: Slower (scales)', 'Dodge: 0%', 'Special: Superior durability'],
+ tags: []
+}, {
+ name: 'Elite Shield',
+ assetId: 'elite_shield_asset',
+ description: 'The ultimate defensive unit with an incredibly tough shield. Appears from score 112.',
+ stats: ['Health: Very High (scales massively)', 'Speed: Extremely Slow (scales)', 'Dodge: 0%', 'Special: Unmatched resilience'],
+ tags: []
+}, {
+ name: 'Shaman',
+ assetId: 'shaman_enemy',
+ description: 'A mystical enemy that interferes with your reload. Appears from score 35.',
+ stats: ['Health: Low (scales)', 'Speed: Moderate (scales)', 'Dodge: 0%', 'Special: Periodically increases player arrow reload count'],
+ tags: ['Green']
+}, {
+ name: 'Hot Air Balloon',
+ assetId: 'hot_air_balloon_asset',
+ description: 'A slow aerial transport that deploys enemies upon destruction. Appears from score 154.',
+ stats: ['Health: Very Low (1 base)', 'Speed: Very Slow (scales)', 'Dodge: 0%', 'Special: Spawns 5 random non-green enemies on death'],
+ tags: []
+}, {
+ name: 'Dark Bowman',
+ assetId: 'dark_bowman_asset',
+ description: 'A sinister archer immune to arrows. Appears from score 55.',
+ stats: ['Health: Low (scales)', 'Speed: Fast (scales)', 'Dodge: 0%', 'Special: Immune to arrow projectiles'],
+ tags: ['Black']
+}];
+var enemypediaScreenInstance = null; // Instance of the EnemypediaScreen class
+var isEnemypediaActive = false; // Flag if the enemypedia is currently shown
+var enemypediaButtonContainer = null; // UI Button to open the pedia
// --- 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
@@ -934,8 +1225,28 @@
newViking.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newViking);
vikingAllies.push(newViking); // Add to the vikingAllies array
}
+// --- Enemypedia Functions ---
+function showEnemypediaScreen() {
+ if (isEnemypediaActive || isUpgradePopupActive) return; // Don't show if already active or upgrade popup is on
+ isEnemypediaActive = true;
+ if (enemypediaScreenInstance) {
+ enemypediaScreenInstance.destroy();
+ }
+ enemypediaScreenInstance = new EnemypediaScreen();
+ game.addChild(enemypediaScreenInstance);
+ // Music handling can be added here if desired, similar to upgrade popup
+}
+function hideEnemypediaScreen() {
+ if (!isEnemypediaActive) return;
+ isEnemypediaActive = false;
+ if (enemypediaScreenInstance) {
+ enemypediaScreenInstance.destroy();
+ enemypediaScreenInstance = null;
+ }
+ // Music handling can be added here if desired
+}
// Helper function placeholder for adding Swordsman
function addSwordsman() {
// Create a swordsman tower at the bastion line
var tower = new Container();
@@ -1245,244 +1556,8 @@
});
ammoTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge.
LK.gui.top.addChild(ammoTxt);
ammoTxt.y = 180; // Position below the score text
-// --- Enemypedia ---
-var enemypediaButton;
-var enemypediaPopup = null;
-var enemyListContainer = null; // Container for enemy list items
-function showEnemypedia() {
- isUpgradePopupActive = true; // Pause the game while enemypedia is open
- enemypediaPopup = new Container();
- enemypediaPopup.x = GAME_WIDTH / 2;
- enemypediaPopup.y = GAME_HEIGHT / 2;
- game.addChild(enemypediaPopup);
- // Add a semi-transparent background overlay
- var bg = enemypediaPopup.attachAsset('bastionLine', {
- width: 1600,
- height: 1000,
- color: 0x000000,
- anchorX: 0.5,
- anchorY: 0.5,
- alpha: 0.8
- });
- // Add title text
- var title = new Text2('Enemypedia', {
- size: 100,
- fill: 0xFFFFFF
- });
- title.anchor.set(0.5, 0.5);
- title.y = -450; // Position relative to popup center
- enemypediaPopup.addChild(title);
- // Add a close button
- var closeButton = createCloseButton();
- closeButton.x = 750; // Position relative to popup center
- closeButton.y = -450;
- enemypediaPopup.addChild(closeButton);
- // Container for the list of enemies (scrollable potentially)
- enemyListContainer = new Container();
- enemyListContainer.y = -350; // Position below title
- enemypediaPopup.addChild(enemyListContainer);
- displayEnemyStats();
-}
-function hideEnemypedia() {
- if (enemypediaPopup) {
- enemypediaPopup.destroy();
- enemypediaPopup = null;
- }
- isUpgradePopupActive = false; // Unpause the game
-}
-function createCloseButton() {
- var buttonContainer = new Container();
- var buttonBg = buttonContainer.attachAsset('bastionLine', {
- width: 150,
- height: 80,
- color: 0xCC0000,
- // Red color for close
- anchorX: 0.5,
- anchorY: 0.5
- });
- var buttonText = new Text2('X', {
- size: 60,
- fill: 0xFFFFFF
- });
- buttonText.anchor.set(0.5, 0.5);
- buttonContainer.addChild(buttonText);
- buttonContainer.interactive = true;
- buttonContainer.down = function () {
- hideEnemypedia();
- };
- return buttonContainer;
-}
-function displayEnemyStats() {
- // Dummy data representing enemy types and their stats
- var enemyData = [{
- type: 'swordsman',
- assetId: 'enemy',
- description: 'Basic melee unit.',
- baseHealth: 1,
- baseSpeed: 3,
- dodgeChance: 0
- }, {
- type: 'knight',
- assetId: 'knight',
- description: 'Slow, higher health unit.',
- baseHealth: 5,
- baseSpeed: 3 * 0.9,
- dodgeChance: 0
- }, {
- type: 'thief',
- assetId: 'thief',
- description: 'Fast, low health unit with dodge chance.',
- baseHealth: 1,
- baseSpeed: 3 * 1.2,
- dodgeChance: 0.1
- }, {
- type: 'boss',
- assetId: 'boss',
- description: 'Very high health boss.',
- baseHealth: 5,
- // Base health before scaling
- baseSpeed: 3 * 0.7,
- dodgeChance: 0
- }, {
- type: 'shield',
- assetId: 'shield_enemy',
- description: 'Very high health, very slow unit.',
- baseHealth: 20,
- baseSpeed: 3 * 0.5,
- dodgeChance: 0
- }, {
- type: 'wizard',
- assetId: 'wizard_enemy',
- description: 'Teleports and has moderate speed.',
- baseHealth: 2,
- baseSpeed: 3 * 0.7,
- dodgeChance: 0
- }, {
- type: 'spearman',
- assetId: 'spearman',
- description: 'Standard unit, often spawns from Elephants.',
- baseHealth: 3,
- baseSpeed: 3 * 1.0,
- dodgeChance: 0
- }, {
- type: 'war_elephant',
- assetId: 'war_elephant',
- description: 'Extremely high health. Spawns spearmen on death.',
- baseHealth: 500,
- baseSpeed: 3 * 0.3,
- dodgeChance: 0
- }, {
- type: 'elite_knight',
- assetId: 'elite_knight_asset',
- description: 'High health knight variant.',
- baseHealth: 45,
- baseSpeed: 3 * 0.85,
- dodgeChance: 0
- }, {
- type: 'elite_shield',
- assetId: 'elite_shield_asset',
- description: 'Even higher health shield variant.',
- baseHealth: 200,
- baseSpeed: 3 * 0.4,
- dodgeChance: 0
- }, {
- type: 'shaman',
- assetId: 'shaman_enemy',
- description: 'Reduces player ammo periodically.',
- baseHealth: 2,
- baseSpeed: 3 * 0.8,
- dodgeChance: 0
- }, {
- type: 'hot_air_balloon',
- assetId: 'hot_air_balloon_asset',
- description: 'Very low health. Spawns random enemies on death.',
- baseHealth: 1,
- baseSpeed: 3 * 0.2,
- dodgeChance: 0
- }, {
- type: 'dark_bowman',
- assetId: 'dark_bowman_asset',
- description: 'Immune to arrows. Higher health variant.',
- baseHealth: 5,
- baseSpeed: 3 * 1.1,
- dodgeChance: 0
- }];
- var startY = 0;
- var spacing = 150;
- for (var i = 0; i < enemyData.length; i++) {
- var enemyInfo = enemyData[i];
- var enemyItem = new Container();
- enemyItem.y = startY + i * spacing;
- // Add enemy graphic (smaller representation)
- var enemyGraphic = enemyItem.attachAsset(enemyInfo.assetId, {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 0.5,
- // Scale down for display
- scaleY: 0.5,
- x: -600 // Position to the left
- });
- // Add enemy name
- var nameText = new Text2(enemyInfo.type.replace(/_/g, ' ').toUpperCase(), {
- size: 40,
- fill: 0xFFFFFF
- });
- nameText.anchor.set(0, 0.5); // Align left
- nameText.x = -500;
- enemyItem.addChild(nameText);
- // Add description
- var descText = new Text2('Description: ' + enemyInfo.description, {
- size: 30,
- fill: 0xCCCCCC,
- wordWrap: true,
- wordWrapWidth: 800 // Wrap text to fit width
- });
- descText.anchor.set(0, 0); // Align top-left
- descText.x = -500;
- descText.y = 30;
- enemyItem.addChild(descText);
- // Add stats
- var statsText = new Text2('Health: ' + enemyInfo.baseHealth + ' | Speed: ' + enemyInfo.baseSpeed.toFixed(1) + ' | Dodge: ' + (enemyInfo.dodgeChance * 100).toFixed(0) + '%', {
- size: 30,
- fill: 0xAAAAAA
- });
- statsText.anchor.set(0, 0); // Align top-left
- statsText.x = -500;
- statsText.y = 70;
- enemyItem.addChild(statsText);
- enemyListContainer.addChild(enemyItem);
- }
-}
-enemypediaButton = new Container();
-// Position the button (e.g., bottom right of GUI)
-enemypediaButton.x = GAME_WIDTH - 200; // Example X
-enemypediaButton.y = GAME_HEIGHT - 200; // Example Y
-// Add button background
-var buttonBg = enemypediaButton.attachAsset('bastionLine', {
- // Reusing asset for shape
- width: 300,
- height: 100,
- color: 0x444444,
- // Grey color
- anchorX: 0.5,
- anchorY: 0.5
-});
-// Add button text
-var buttonText = new Text2('Enemypedia', {
- size: 40,
- fill: 0xFFFFFF
-});
-buttonText.anchor.set(0.5, 0.5);
-enemypediaButton.addChild(buttonText);
-// Make button interactive
-enemypediaButton.interactive = true;
-enemypediaButton.down = function () {
- showEnemypedia();
-};
-// Add the button to the game scene (or GUI if needed)
-game.addChild(enemypediaButton);
// --- Input Event Handlers ---
// Handles touch/mouse press down event on the game area.
game.down = function (x, y, obj) {
// Record the starting position of the drag in game coordinates.
@@ -1580,21 +1655,24 @@
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) {
+ // Ensure Enemypedia is not active before showing upgrade popup
+ if (!isUpgradePopupActive && !isEnemypediaActive && 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
+ // --- Pause Game Logic if Upgrade Popup or Enemypedia is Active ---
+ if (isUpgradePopupActive || isEnemypediaActive) {
+ // Added isEnemypediaActive check
+ // Potential: Update UI animations if any for popups
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
- }
+ // This redundant check can be removed due to the combined check above.
+ // 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
@@ -2142,8 +2220,11 @@
titleScreen.interactive = true;
titleScreen.down = function () {
startGame(); // Start the game when tapped
};
+ if (enemypediaButtonContainer) {
+ enemypediaButtonContainer.visible = false; // Hide pedia button during title screen
+ }
// Pause game logic while title screen is active
isUpgradePopupActive = true; // Reusing this flag to pause game logic
}
// Function to start the game
@@ -2153,8 +2234,35 @@
if (titleScreen) {
titleScreen.destroy();
titleScreen = null;
}
+ // Create and add Enemypedia button
+ if (!enemypediaButtonContainer) {
+ enemypediaButtonContainer = new Container();
+ var pediaButtonBgAsset = LK.getAsset('pedia_button_bg', {}); // Get asset for size
+ var pediaButtonBg = enemypediaButtonContainer.attachAsset('pedia_button_bg', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ var pediaButtonText = new Text2('Enemies', {
+ size: 35,
+ fill: 0xFFFFFF
+ });
+ pediaButtonText.anchor.set(0.5, 0.5);
+ enemypediaButtonContainer.addChild(pediaButtonText);
+ enemypediaButtonContainer.interactive = true;
+ enemypediaButtonContainer.down = function () {
+ if (!isUpgradePopupActive) {
+ // Only open if upgrade popup isn't active
+ showEnemypediaScreen();
+ }
+ };
+ LK.gui.topRight.addChild(enemypediaButtonContainer);
+ // Position it with padding from top-right corner
+ enemypediaButtonContainer.x = -(pediaButtonBgAsset.width / 2 + 30);
+ enemypediaButtonContainer.y = pediaButtonBgAsset.height / 2 + 30;
+ }
+ enemypediaButtonContainer.visible = true;
// Reset game state if needed (LK might handle this on game start)
// Play game music
LK.playMusic('Gamemusic');
}
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