User prompt
For some reason, Heat-tipped arrows doesn't appear when choosing upgrades, fix that
User prompt
Archer ally uses "Archer" asset
User prompt
Archer ally shoots at enemies every 4 seconds; add an asset for him!
User prompt
Now, every 10 score, you can unlock one of these 3 of these 7 upgrades: Faster reaload (decreases the time to reaload), Piercing shots (Increases how many enemies an arrow can pierce through), Quiver (Increases how many arrows you can shoot before realoading), Heat-tipped arrows (Increases damage), Archer ally (You gain an archer ally that shoots arrows at enemies independently; his arrows don't affect the reload counter), Sabotage (enemies are slower), Swordsman (you gain a swordsman that attacks enemies on melee) ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Archer ally doesn't really do anything, Fix that by making him shoot every 3 seconds and by adding an asset for him ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Archer ally shoots at enemies every 4 seconds; add an asset for him!
User prompt
Now, every 10 score, you can unlock one upgrade of your choice of these 3 of these 7 upgrades: Faster reaload (decreases the time to reaload), Piercing shots (Increases how many enemies an arrow can pierce through), Quiver (Increases how many arrows you can shoot before realoading), Heat-tipped arrows (Increases damage), Archer ally (You gain an archer ally that shoots arrows at enemies independently; his arrows don't affect the reload counter), Sabotage (enemies are slower), Swordsman (you gain a swordsman that attacks enemies on melee)
User prompt
Now, every 10 score, you can unlock one of these 3 of these 7 upgrades: Faster reaload (decreases the time to reaload), Piercing shots (Increases how many enemies an arrow can pierce through), Quiver (Increases how many arrows you can shoot before realoading), Heat-tipped arrows (Increases damage), Archer ally (You gain an archer ally that shoots arrows at enemies independently; his arrows don't affect the reload counter), Sabotage (enemies are slower), Swordsman (you gain a swordsman that attacks enemies on melee)
User prompt
Now, every 10 score, you can unlock one of these 3 of these 7 upgrades: Faster reaload (decreases the time to reaload), Piercing shots (Increases how many enemies an arrow can pierce through), Quiver (Increases how many arrows you can shoot before realoading), Heat-tipped arrows (Increases damage), Archer ally (You gain an archer ally that shoots arrows at enemies independently; his arrows don't affect the reload counter), Sabotage (enemies are slower), Swordsman (you gain a swordsman that attacks enemies on melee)
User prompt
When playing, the song that plays now is "Game music"
User prompt
Now, when realoading, we do the "Reload" sound effect
User prompt
Add a 6 second cooldown after firing 15 arrows
Code edit (1 edits merged)
Please save this source code
User prompt
Archery Bastions
Initial prompt
Archery bastions
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ /** * Represents an archer ally that automatically fires arrows at enemies. */ var ArcherAlly = Container.expand(function () { var self = Container.call(this); // Create and attach the archer graphic var graphics = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, tint: 0x00FF00 // Green tint to distinguish from enemies }); self.fireRate = 90; // Ticks between shots (1.5 seconds) self.lastFired = 0; self.update = function () { self.lastFired++; if (self.lastFired >= self.fireRate && enemies.length > 0) { self.lastFired = 0; self.fireArrow(); } }; self.fireArrow = function () { // Find the closest enemy to target var target = null; var minDist = Number.MAX_VALUE; 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 dist = Math.sqrt(dx * dx + dy * dy); if (dist < minDist) { minDist = dist; target = enemy; } } if (target) { // Calculate angle to target var dx = target.x - self.x; var dy = target.y - self.y; var angle = Math.atan2(dx, -dy); // Create and fire arrow var arrow = new Arrow(angle); arrow.x = self.x; arrow.y = self.y; arrow.lastX = arrow.x; arrow.lastY = arrow.y; arrow.allyArrow = true; // Mark as ally arrow game.addChild(arrow); arrows.push(arrow); LK.getSound('shoot').play(); } }; return self; }); // 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.piercingCount = 0; // Number of enemies this arrow can pierce through self.damage = 1; // Base damage of arrow /** * Update method called each game tick by the LK engine. * Moves the arrow 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 {number} speed - The vertical speed of the enemy in pixels per tick. */ var Enemy = Container.expand(function (speed) { var self = Container.call(this); // Create and attach the enemy graphic asset var graphics = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5 }); self.speed = speed; // Vertical speed (pixels per tick) self.maxHealth = 1; self.health = 1; /** * Update method called each game tick by the LK engine. * Moves the enemy downwards. */ self.update = function () { self.y += self.speed; }; // Method to apply damage to the enemy self.takeDamage = function (amount) { self.health -= amount; // Flash red to indicate damage tween(graphics, { alpha: 0.5 }, { duration: 100, onFinish: function onFinish() { tween(graphics, { alpha: 1 }, { duration: 100 }); } }); return self.health <= 0; // Return true if enemy died }; 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 var graphics = self.attachAsset('enemy', { anchorX: 0.5, anchorY: 0.5, tint: 0x0000FF // Blue tint to distinguish from enemies }); self.attackRange = 200; // Range at which swordsman can attack enemies self.attackRate = 60; // Ticks between attacks (1 second) self.lastAttacked = 0; self.damage = 2; // Damage per attack self.update = function () { self.lastAttacked++; if (self.lastAttacked >= self.attackRate) { // Find any enemy in range to attack 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 dist = Math.sqrt(dx * dx + dy * dy); if (dist <= self.attackRange) { self.lastAttacked = 0; // Attack animation (slight scale up and down) tween(self, { scaleX: 1.2, scaleY: 1.2 }, { duration: 150, onFinish: function onFinish() { tween(self, { scaleX: 1, scaleY: 1 }, { duration: 150 }); } }); // Deal damage to enemy if (enemy.takeDamage(self.damage)) { LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); LK.getSound('hit').play(); enemy.destroy(); enemies.splice(i, 1); } break; } } } }; return self; }); // Upgrade class to handle available upgrades var Upgrade = Container.expand(function (type, description, x, y) { var self = Container.call(this); self.type = type; // Background for the upgrade button var background = self.attachAsset('bastionLine', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.3, scaleY: 5, tint: 0x444444 }); // Text showing the upgrade type var titleText = new Text2(type, { size: 40, fill: 0xFFFFFF }); titleText.anchor.set(0.5, 0); titleText.y = -90; self.addChild(titleText); // Text showing the upgrade description var descText = new Text2(description, { size: 30, fill: 0xFFFFFF }); descText.anchor.set(0.5, 0); descText.y = -40; self.addChild(descText); // Position the upgrade button self.x = x; self.y = y; // Handle touch events self.down = function (x, y, obj) { background.tint = 0x888888; }; self.up = function (x, y, obj) { background.tint = 0x444444; applyUpgrade(self.type); hideUpgradeMenu(); }; return self; }); /**** * Initialize Game ****/ // --- Visual Setup --- // Create a visual line representing the bastion defense zone. // Create the main game instance with a dark background color. var game = new LK.Game({ backgroundColor: 0x101030 // Dark blue/purple background }); /**** * Game Code ****/ // Constants defining game dimensions and key vertical positions. // Sound for an arrow hitting an enemy // Sound for firing an arrow // Grey line representing the bastion defense line // Red enemy shape // Yellow arrow shape // LK engine will automatically initialize these based on usage. // Define shapes and sounds needed for the game. // Import tween plugin for animations 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 = []; var allies = []; // Array for archer and swordsman allies // Variables for game state and difficulty scaling. var scoreTxt; // Text2 object for displaying the score. var dragStartX = null; // Starting X coordinate of a touch/mouse drag. var dragStartY = null; // Starting Y coordinate of a touch/mouse drag. var enemySpawnInterval = 120; // Initial ticks between enemy spawns (2 seconds at 60 FPS). var arrowsFired = 0; // Counter for arrows fired since the last cooldown. var cooldownTimer = 0; // Timer in ticks for the cooldown period. var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec). 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 speed of enemies. var maxEnemySpeed = 12; // Maximum speed enemies can reach. var enemySpeedIncrease = 0.015; // Amount to increase enemy speed each time an enemy spawns. // Upgrade system variables var lastUpgradeScore = 0; var scorePerUpgrade = 10; var maxUpgradesPerCategory = 3; var upgradablePiercing = 0; var upgradableReload = 0; var upgradableQuiver = 0; var upgradableDamage = 0; var upgradableArchers = 0; var upgradableSabotage = 0; var upgradableSwordsmen = 0; var upgradeMenuVisible = false; var upgradeOptions = []; // --- Visual Setup --- // Create a visual line representing the bastion defense zone. 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 overlay container for the upgrade menu var upgradeOverlay = new Container(); upgradeOverlay.visible = false; game.addChild(upgradeOverlay); // Create semi-transparent background for upgrade menu var menuBackground = LK.getAsset('bastionLine', { anchorX: 0.5, anchorY: 0.5, scaleX: 20, scaleY: 27, tint: 0x000000, alpha: 0.7 }); menuBackground.x = GAME_WIDTH / 2; menuBackground.y = GAME_HEIGHT / 2; upgradeOverlay.addChild(menuBackground); // Title for upgrade menu var upgradeTitle = new Text2("Choose an Upgrade", { size: 80, fill: 0xFFFFFF }); upgradeTitle.anchor.set(0.5, 0); upgradeTitle.x = GAME_WIDTH / 2; upgradeTitle.y = 200; upgradeOverlay.addChild(upgradeTitle); // Function to show upgrade menu function showUpgradeMenu() { if (upgradeMenuVisible) { return; } upgradeMenuVisible = true; upgradeOverlay.visible = true; // Pause game functionality by hiding cooldown var oldCooldown = cooldownTimer; cooldownTimer = cooldownDuration; // Prevent firing while upgrade menu is open // Clear previous upgrade options for (var i = upgradeOptions.length - 1; i >= 0; i--) { upgradeOptions[i].destroy(); } upgradeOptions = []; // Available upgrade types var availableUpgrades = []; // Add upgrades that haven't reached max level if (upgradableReload < maxUpgradesPerCategory) { availableUpgrades.push({ type: "Faster Reload", description: "Decrease reload time by 15%" }); } if (upgradablePiercing < maxUpgradesPerCategory) { availableUpgrades.push({ type: "Piercing Shots", description: "Arrows pierce through " + (upgradablePiercing + 1) + " enemies" }); } if (upgradableQuiver < maxUpgradesPerCategory) { availableUpgrades.push({ type: "Larger Quiver", description: "Shoot " + (5 + upgradableQuiver * 5) + " more arrows before reloading" }); } if (upgradableDamage < maxUpgradesPerCategory) { availableUpgrades.push({ type: "Heat-tipped Arrows", description: "Arrows deal " + (upgradableDamage + 1) + " extra damage" }); } if (upgradableArchers < maxUpgradesPerCategory) { availableUpgrades.push({ type: "Archer Ally", description: "Add an archer to help you" }); } if (upgradableSabotage < maxUpgradesPerCategory) { availableUpgrades.push({ type: "Sabotage", description: "Enemies move 15% slower" }); } if (upgradableSwordsmen < maxUpgradesPerCategory) { availableUpgrades.push({ type: "Swordsman", description: "Add a swordsman to protect your bastion" }); } // Shuffle and take 3 random upgrades for (var i = availableUpgrades.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var temp = availableUpgrades[i]; availableUpgrades[i] = availableUpgrades[j]; availableUpgrades[j] = temp; } var upgradesToShow = Math.min(3, availableUpgrades.length); // No more upgrades available if (upgradesToShow === 0) { var noUpgradesText = new Text2("All upgrades maxed out!", { size: 60, fill: 0xFFFFFF }); noUpgradesText.anchor.set(0.5, 0.5); noUpgradesText.x = GAME_WIDTH / 2; noUpgradesText.y = GAME_HEIGHT / 2; upgradeOverlay.addChild(noUpgradesText); // Auto-hide after 2 seconds LK.setTimeout(function () { hideUpgradeMenu(); cooldownTimer = oldCooldown; // Restore cooldown }, 2000); return; } // Create upgrade options for (var i = 0; i < upgradesToShow; i++) { var xPos = GAME_WIDTH / 4 + GAME_WIDTH / 2 * (i / (upgradesToShow - 1)); if (upgradesToShow === 1) { xPos = GAME_WIDTH / 2; } var upgrade = new Upgrade(availableUpgrades[i].type, availableUpgrades[i].description, xPos, GAME_HEIGHT / 2); upgradeOverlay.addChild(upgrade); upgradeOptions.push(upgrade); } } // Function to hide upgrade menu function hideUpgradeMenu() { upgradeMenuVisible = false; upgradeOverlay.visible = false; } // Function to apply selected upgrade function applyUpgrade(type) { switch (type) { case "Faster Reload": cooldownDuration = Math.floor(cooldownDuration * 0.85); // 15% reduction upgradableReload++; break; case "Piercing Shots": upgradablePiercing++; break; case "Larger Quiver": // 5, 10, 15 more arrows before reload upgradableQuiver++; break; case "Heat-tipped Arrows": upgradableDamage++; break; case "Archer Ally": var archer = new ArcherAlly(); // Position the archer along the bastion line archer.x = 400 + Math.random() * (GAME_WIDTH - 800); archer.y = BASTION_Y - 50; game.addChild(archer); allies.push(archer); upgradableArchers++; break; case "Sabotage": // Reduce enemy speed by 15% currentEnemySpeed *= 0.85; for (var i = 0; i < enemies.length; i++) { enemies[i].speed *= 0.85; } upgradableSabotage++; break; case "Swordsman": var swordsman = new Swordsman(); // Position the swordsman in front of the bastion swordsman.x = 600 + Math.random() * (GAME_WIDTH - 1200); swordsman.y = BASTION_Y - 100; game.addChild(swordsman); allies.push(swordsman); upgradableSwordsmen++; break; } } // --- 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 and not in upgrade menu if (dragStartX !== null && cooldownTimer <= 0 && !upgradeMenuVisible) { // 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; // Apply piercing upgrade newArrow.piercingCount = upgradablePiercing; // Apply damage upgrade newArrow.damage = 1 + upgradableDamage; // Add the arrow to the game scene and the tracking array. game.addChild(newArrow); arrows.push(newArrow); LK.getSound('shoot').play(); // Play shooting sound. arrowsFired++; // Increment arrow count. // Larger quiver upgrade (15 + additional from upgrade) var maxArrows = 15 + upgradableQuiver * 5; if (arrowsFired >= maxArrows) { cooldownTimer = cooldownDuration; // Start cooldown. arrowsFired = 0; // Reset arrow count. LK.getSound('Reload').play(); // Play reload sound. } // Reset drag state. dragStartX = null; dragStartY = null; // Potential enhancement: Hide the aiming indicator. } }; // --- Main Game Update Loop --- // This function is called automatically by the LK engine on every frame (tick). game.update = function () { // Don't update game logic if upgrade menu is visible if (upgradeMenuVisible) { return; } // Decrease cooldown timer if active. if (cooldownTimer > 0) { cooldownTimer--; } // Update allies for (var i = 0; i < allies.length; i++) { allies[i].update(); } // 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; var pierceCount = 0; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Use the intersects method for collision detection. if (arrow.intersects(enemy)) { // Apply damage and check if enemy died if (enemy.takeDamage(arrow.damage)) { // Enemy died LK.setScore(LK.getScore() + 1); // Increment score. scoreTxt.setText(LK.getScore()); // Update score display. // Check if we should show upgrade menu (every 10 points) var currentScore = LK.getScore(); if (Math.floor(currentScore / scorePerUpgrade) > Math.floor(lastUpgradeScore / scorePerUpgrade)) { lastUpgradeScore = currentScore; showUpgradeMenu(); } LK.getSound('hit').play(); // Play hit sound. enemy.destroy(); enemies.splice(j, 1); // Remove enemy from array. } hitEnemy = true; // Mark that a hit occurred. pierceCount++; // If the arrow is not a piercing arrow or has reached its piercing limit, destroy it if (!arrow.allyArrow && (arrow.piercingCount <= 0 || pierceCount > arrow.piercingCount)) { arrow.destroy(); arrows.splice(i, 1); // Remove arrow from array. break; // Stop checking more enemies for this arrow } } } // If the arrow hit an enemy, it was destroyed, so skip to the next arrow. if (hitEnemy && !arrow.allyArrow && (arrow.piercingCount <= 0 || pierceCount > arrow.piercingCount)) { 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 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. 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) { // Determine the speed for the new enemy, capping at max speed. var newEnemySpeed = Math.min(maxEnemySpeed, currentEnemySpeed); var newEnemy = new Enemy(newEnemySpeed); // Scale enemy health based on score/progress var scoreBasedHealth = 1 + Math.floor(LK.getScore() / 30); newEnemy.maxHealth = scoreBasedHealth; newEnemy.health = scoreBasedHealth; // Position the new enemy at the top of the screen at a random horizontal position. // Add padding to avoid spawning exactly at the screen edges. var spawnPadding = newEnemy.width / 2 + 20; // Add a little extra padding newEnemy.x = spawnPadding + Math.random() * (GAME_WIDTH - 2 * spawnPadding); newEnemy.y = -newEnemy.height / 2; // Start just above the top edge. // Initialize last position for state tracking. 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 speed. enemySpawnInterval -= enemySpawnRateDecrease; currentEnemySpeed += enemySpeedIncrease; } }; // --- Initial Game Setup --- // Set the initial score text based on the starting score (which is 0). scoreTxt.setText(LK.getScore()); ; // Play the background music. LK.playMusic('Gamemusic');
===================================================================
--- original.js
+++ change.js
@@ -1,64 +1,64 @@
/****
+* Plugins
+****/
+var tween = LK.import("@upit/tween.v1");
+
+/****
* Classes
****/
/**
-* Represents an allied archer that shoots arrows independently.
+* Represents an archer ally that automatically fires arrows at enemies.
*/
var ArcherAlly = Container.expand(function () {
var self = Container.call(this);
- // Create and attach the archer graphic asset (reusing arrow asset for simplicity)
- var graphics = self.attachAsset('arrow', {
- // Reusing arrow asset for simplicity
+ // Create and attach the archer graphic
+ var graphics = self.attachAsset('enemy', {
anchorX: 0.5,
- anchorY: 0.5
+ anchorY: 0.5,
+ tint: 0x00FF00 // Green tint to distinguish from enemies
});
- graphics.rotation = Math.PI / 2; // Rotate to stand upright
- 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.fireRate = 90; // Ticks between shots (1.5 seconds)
+ self.lastFired = 0;
self.update = function () {
- self.fireTimer++;
- if (self.fireTimer >= self.fireInterval) {
- 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;
- }
+ self.lastFired++;
+ if (self.lastFired >= self.fireRate && enemies.length > 0) {
+ self.lastFired = 0;
+ self.fireArrow();
+ }
+ };
+ self.fireArrow = function () {
+ // Find the closest enemy to target
+ var target = null;
+ var minDist = Number.MAX_VALUE;
+ 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 dist = Math.sqrt(dx * dx + dy * dy);
+ if (dist < minDist) {
+ minDist = dist;
+ target = 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);
- 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
- }
}
+ if (target) {
+ // Calculate angle to target
+ var dx = target.x - self.x;
+ var dy = target.y - self.y;
+ var angle = Math.atan2(dx, -dy);
+ // Create and fire arrow
+ var arrow = new Arrow(angle);
+ arrow.x = self.x;
+ arrow.y = self.y;
+ arrow.lastX = arrow.x;
+ arrow.lastY = arrow.y;
+ arrow.allyArrow = true; // Mark as ally arrow
+ game.addChild(arrow);
+ arrows.push(arrow);
+ LK.getSound('shoot').play();
+ }
};
- return self; // Return self for potential inheritance
+ return self;
});
// Sound when an enemy reaches the bastion
// No plugins needed for this version of the game.
/**
@@ -75,9 +75,10 @@
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.piercingCount = 0; // Number of enemies this arrow can pierce through
+ self.damage = 1; // Base damage of arrow
/**
* Update method called each game tick by the LK engine.
* Moves the arrow based on its velocity.
*/
@@ -98,23 +99,141 @@
anchorX: 0.5,
anchorY: 0.5
});
self.speed = speed; // Vertical speed (pixels per tick)
+ self.maxHealth = 1;
+ self.health = 1;
/**
* Update method called each game tick by the LK engine.
* Moves the enemy downwards.
*/
self.update = function () {
- // Apply the global speed multiplier for Sabotage upgrade
- var effectiveSpeed = self.speed * enemySpeedMultiplier;
- self.y += effectiveSpeed;
+ self.y += self.speed;
};
+ // Method to apply damage to the enemy
+ self.takeDamage = function (amount) {
+ self.health -= amount;
+ // Flash red to indicate damage
+ tween(graphics, {
+ alpha: 0.5
+ }, {
+ duration: 100,
+ onFinish: function onFinish() {
+ tween(graphics, {
+ alpha: 1
+ }, {
+ duration: 100
+ });
+ }
+ });
+ return self.health <= 0; // Return true if enemy died
+ };
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
+ var graphics = self.attachAsset('enemy', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ tint: 0x0000FF // Blue tint to distinguish from enemies
+ });
+ self.attackRange = 200; // Range at which swordsman can attack enemies
+ self.attackRate = 60; // Ticks between attacks (1 second)
+ self.lastAttacked = 0;
+ self.damage = 2; // Damage per attack
+ self.update = function () {
+ self.lastAttacked++;
+ if (self.lastAttacked >= self.attackRate) {
+ // Find any enemy in range to attack
+ 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 dist = Math.sqrt(dx * dx + dy * dy);
+ if (dist <= self.attackRange) {
+ self.lastAttacked = 0;
+ // Attack animation (slight scale up and down)
+ tween(self, {
+ scaleX: 1.2,
+ scaleY: 1.2
+ }, {
+ duration: 150,
+ onFinish: function onFinish() {
+ tween(self, {
+ scaleX: 1,
+ scaleY: 1
+ }, {
+ duration: 150
+ });
+ }
+ });
+ // Deal damage to enemy
+ if (enemy.takeDamage(self.damage)) {
+ LK.setScore(LK.getScore() + 1);
+ scoreTxt.setText(LK.getScore());
+ LK.getSound('hit').play();
+ enemy.destroy();
+ enemies.splice(i, 1);
+ }
+ break;
+ }
+ }
+ }
+ };
+ return self;
+});
+// Upgrade class to handle available upgrades
+var Upgrade = Container.expand(function (type, description, x, y) {
+ var self = Container.call(this);
+ self.type = type;
+ // Background for the upgrade button
+ var background = self.attachAsset('bastionLine', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ scaleX: 0.3,
+ scaleY: 5,
+ tint: 0x444444
+ });
+ // Text showing the upgrade type
+ var titleText = new Text2(type, {
+ size: 40,
+ fill: 0xFFFFFF
+ });
+ titleText.anchor.set(0.5, 0);
+ titleText.y = -90;
+ self.addChild(titleText);
+ // Text showing the upgrade description
+ var descText = new Text2(description, {
+ size: 30,
+ fill: 0xFFFFFF
+ });
+ descText.anchor.set(0.5, 0);
+ descText.y = -40;
+ self.addChild(descText);
+ // Position the upgrade button
+ self.x = x;
+ self.y = y;
+ // Handle touch events
+ self.down = function (x, y, obj) {
+ background.tint = 0x888888;
+ };
+ self.up = function (x, y, obj) {
+ background.tint = 0x444444;
+ applyUpgrade(self.type);
+ hideUpgradeMenu();
+ };
+ return self;
+});
/****
* Initialize Game
****/
+// --- Visual Setup ---
+// Create a visual line representing the bastion defense zone.
// Create the main game instance with a dark background color.
var game = new LK.Game({
backgroundColor: 0x101030 // Dark blue/purple background
});
@@ -122,212 +241,53 @@
/****
* Game Code
****/
// Constants defining game dimensions and key vertical positions.
-// Define shapes and sounds needed for the game.
-// LK engine will automatically initialize these based on usage.
-// Yellow arrow shape
-// Red enemy shape
-// Grey line representing the bastion defense line
-// Sound for firing an arrow
// Sound for an arrow hitting an enemy
+// Sound for firing an arrow
+// Grey line representing the bastion defense line
+// Red enemy shape
+// Yellow arrow shape
+// LK engine will automatically initialize these based on usage.
+// Define shapes and sounds needed for the game.
+// Import tween plugin for animations
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 = [];
+var allies = []; // Array for archer and swordsman allies
// Variables for game state and difficulty scaling.
var scoreTxt; // Text2 object for displaying the score.
var dragStartX = null; // Starting X coordinate of a touch/mouse drag.
var dragStartY = null; // Starting Y coordinate of a touch/mouse drag.
var enemySpawnInterval = 120; // Initial ticks between enemy spawns (2 seconds at 60 FPS).
var arrowsFired = 0; // Counter for arrows fired since the last cooldown.
var cooldownTimer = 0; // Timer in ticks for the cooldown period.
-var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec). Min duration enforced in upgrade.
-var maxArrowsBeforeCooldown = 15; // Max arrows before reload (Quiver upgrade)
-var arrowPierceLevel = 1; // How many enemies an arrow can pierce (Piercing Shots upgrade)
-var enemySpeedMultiplier = 1.0; // Multiplier for enemy speed (Sabotage upgrade) Min multiplier enforced in upgrade.
+var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec).
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
-// --- Upgrade System ---
-var lastUpgradeScore = -1; // Score at which the last upgrade was offered
-var isUpgradePopupActive = false; // Flag indicating if the upgrade choice popup is visible
-var upgradePopup = null; // Container for the upgrade UI elements
-// Helper function to apply Sabotage effect to existing enemies
-function applySabotage() {
- for (var i = 0; i < enemies.length; i++) {
- // We assume Enemy class will use enemySpeedMultiplier when calculating effective speed
- // If Enemy.speed stores base speed, we might need an effectiveSpeed method or update speed directly.
- // For simplicity, let's assume Enemy.update uses the global multiplier.
- // If direct modification is needed: enemies[i].speed = baseSpeed * enemySpeedMultiplier;
- }
- // Also update the current base speed calculation baseline if necessary, though modifying the multiplier should suffice.
-}
-// Helper function placeholder for adding Archer Ally
-function addArcherAlly() {
- // Implementation requires ArcherAlly class definition
- console.log("Archer Ally upgrade chosen!");
- var ally = new ArcherAlly();
- // Position the ally next to the player's firing position, slightly offset.
- ally.x = FIRING_POS_X + 150; // Position to the right of the player
- ally.y = FIRING_POS_Y;
- game.addChild(ally);
- archerAllies.push(ally); // Add to the new archerAllies array
-}
-// Helper function placeholder for adding Swordsman
-function addSwordsman() {
- // Implementation requires Swordsman class definition
- console.log("Swordsman upgrade chosen - placeholder");
- // Example: var swordsman = new Swordsman(); game.addChild(swordsman); swordsmen.push(swordsman);
-}
-// 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: 'Increases damage' }, // Skipping for now due to lack of enemy health
-{
- 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
-}];
-// Function to create and show the upgrade selection popup
-function showUpgradePopup() {
- isUpgradePopupActive = true;
- // Simple pause: Game logic checks isUpgradePopupActive flag in game.update
- // --- Create Popup UI ---
- upgradePopup = new Container();
- upgradePopup.x = GAME_WIDTH / 2;
- upgradePopup.y = GAME_HEIGHT / 2;
- game.addChild(upgradePopup); // Add to game layer for positioning relative to center
- // Add a semi-transparent background overlay
- var bg = upgradePopup.attachAsset('bastionLine', {
- // Reusing an asset for shape
- width: 1200,
- height: 800,
- color: 0x000000,
- // Black background
- anchorX: 0.5,
- anchorY: 0.5,
- alpha: 0.8
- });
- // Add title text
- var title = new Text2('Choose an Upgrade!', {
- size: 80,
- fill: 0xFFFFFF
- });
- title.anchor.set(0.5, 0.5);
- title.y = -300; // Position relative to popup center
- upgradePopup.addChild(title);
- // --- Select 3 Random Unique Upgrades ---
- var availableToOffer = allUpgrades.slice(); // Copy available upgrades
- var choices = [];
- var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices
- for (var i = 0; i < numChoices; i++) {
- var randomIndex = Math.floor(Math.random() * availableToOffer.length);
- choices.push(availableToOffer[randomIndex]);
- availableToOffer.splice(randomIndex, 1); // Remove chosen upgrade to ensure uniqueness
- }
- // --- Create Buttons for Choices ---
- var buttonYStart = -150;
- var buttonSpacing = 180;
- for (var j = 0; j < choices.length; j++) {
- var upgrade = choices[j];
- var button = createUpgradeButton(upgrade, buttonYStart + j * buttonSpacing);
- upgradePopup.addChild(button);
- }
-}
-// Function to create a single upgrade button
-function createUpgradeButton(upgradeData, yPos) {
- var buttonContainer = new Container();
- buttonContainer.y = yPos;
- // Button background
- var buttonBg = buttonContainer.attachAsset('bastionLine', {
- // Reusing asset for shape
- width: 800,
- height: 150,
- color: 0x555555,
- anchorX: 0.5,
- anchorY: 0.5
- });
- // Button text (Name + Description)
- var nameText = new Text2(upgradeData.name, {
- size: 50,
- fill: 0xFFFFFF
- });
- nameText.anchor.set(0.5, 0.5);
- nameText.y = -25;
- buttonContainer.addChild(nameText);
- var descText = new Text2(upgradeData.description, {
- size: 35,
- fill: 0xCCCCCC
- });
- descText.anchor.set(0.5, 0.5);
- descText.y = 30;
- buttonContainer.addChild(descText);
- // Make button interactive
- buttonContainer.interactive = true; // Needed for down event
- // Event handler for button press
- buttonContainer.down = function (x, y, obj) {
- upgradeData.apply(); // Apply the selected upgrade's effect
- hideUpgradePopup(); // Close the popup
- };
- return buttonContainer;
-}
-// Function to hide and destroy the upgrade popup
-function hideUpgradePopup() {
- if (upgradePopup) {
- upgradePopup.destroy();
- upgradePopup = null;
- }
- isUpgradePopupActive = false;
- // Resume game logic (handled by checking flag in game.update)
-}
+var currentEnemySpeed = 3; // Initial speed of enemies.
+var maxEnemySpeed = 12; // Maximum speed enemies can reach.
+var enemySpeedIncrease = 0.015; // Amount to increase enemy speed each time an enemy spawns.
+// Upgrade system variables
+var lastUpgradeScore = 0;
+var scorePerUpgrade = 10;
+var maxUpgradesPerCategory = 3;
+var upgradablePiercing = 0;
+var upgradableReload = 0;
+var upgradableQuiver = 0;
+var upgradableDamage = 0;
+var upgradableArchers = 0;
+var upgradableSabotage = 0;
+var upgradableSwordsmen = 0;
+var upgradeMenuVisible = false;
+var upgradeOptions = [];
// --- Visual Setup ---
+// Create a visual line representing the bastion defense zone.
var bastionLine = game.addChild(LK.getAsset('bastionLine', {
anchorX: 0.0,
// Anchor at the left edge.
anchorY: 0.5,
@@ -345,8 +305,179 @@
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 overlay container for the upgrade menu
+var upgradeOverlay = new Container();
+upgradeOverlay.visible = false;
+game.addChild(upgradeOverlay);
+// Create semi-transparent background for upgrade menu
+var menuBackground = LK.getAsset('bastionLine', {
+ anchorX: 0.5,
+ anchorY: 0.5,
+ scaleX: 20,
+ scaleY: 27,
+ tint: 0x000000,
+ alpha: 0.7
+});
+menuBackground.x = GAME_WIDTH / 2;
+menuBackground.y = GAME_HEIGHT / 2;
+upgradeOverlay.addChild(menuBackground);
+// Title for upgrade menu
+var upgradeTitle = new Text2("Choose an Upgrade", {
+ size: 80,
+ fill: 0xFFFFFF
+});
+upgradeTitle.anchor.set(0.5, 0);
+upgradeTitle.x = GAME_WIDTH / 2;
+upgradeTitle.y = 200;
+upgradeOverlay.addChild(upgradeTitle);
+// Function to show upgrade menu
+function showUpgradeMenu() {
+ if (upgradeMenuVisible) {
+ return;
+ }
+ upgradeMenuVisible = true;
+ upgradeOverlay.visible = true;
+ // Pause game functionality by hiding cooldown
+ var oldCooldown = cooldownTimer;
+ cooldownTimer = cooldownDuration; // Prevent firing while upgrade menu is open
+ // Clear previous upgrade options
+ for (var i = upgradeOptions.length - 1; i >= 0; i--) {
+ upgradeOptions[i].destroy();
+ }
+ upgradeOptions = [];
+ // Available upgrade types
+ var availableUpgrades = [];
+ // Add upgrades that haven't reached max level
+ if (upgradableReload < maxUpgradesPerCategory) {
+ availableUpgrades.push({
+ type: "Faster Reload",
+ description: "Decrease reload time by 15%"
+ });
+ }
+ if (upgradablePiercing < maxUpgradesPerCategory) {
+ availableUpgrades.push({
+ type: "Piercing Shots",
+ description: "Arrows pierce through " + (upgradablePiercing + 1) + " enemies"
+ });
+ }
+ if (upgradableQuiver < maxUpgradesPerCategory) {
+ availableUpgrades.push({
+ type: "Larger Quiver",
+ description: "Shoot " + (5 + upgradableQuiver * 5) + " more arrows before reloading"
+ });
+ }
+ if (upgradableDamage < maxUpgradesPerCategory) {
+ availableUpgrades.push({
+ type: "Heat-tipped Arrows",
+ description: "Arrows deal " + (upgradableDamage + 1) + " extra damage"
+ });
+ }
+ if (upgradableArchers < maxUpgradesPerCategory) {
+ availableUpgrades.push({
+ type: "Archer Ally",
+ description: "Add an archer to help you"
+ });
+ }
+ if (upgradableSabotage < maxUpgradesPerCategory) {
+ availableUpgrades.push({
+ type: "Sabotage",
+ description: "Enemies move 15% slower"
+ });
+ }
+ if (upgradableSwordsmen < maxUpgradesPerCategory) {
+ availableUpgrades.push({
+ type: "Swordsman",
+ description: "Add a swordsman to protect your bastion"
+ });
+ }
+ // Shuffle and take 3 random upgrades
+ for (var i = availableUpgrades.length - 1; i > 0; i--) {
+ var j = Math.floor(Math.random() * (i + 1));
+ var temp = availableUpgrades[i];
+ availableUpgrades[i] = availableUpgrades[j];
+ availableUpgrades[j] = temp;
+ }
+ var upgradesToShow = Math.min(3, availableUpgrades.length);
+ // No more upgrades available
+ if (upgradesToShow === 0) {
+ var noUpgradesText = new Text2("All upgrades maxed out!", {
+ size: 60,
+ fill: 0xFFFFFF
+ });
+ noUpgradesText.anchor.set(0.5, 0.5);
+ noUpgradesText.x = GAME_WIDTH / 2;
+ noUpgradesText.y = GAME_HEIGHT / 2;
+ upgradeOverlay.addChild(noUpgradesText);
+ // Auto-hide after 2 seconds
+ LK.setTimeout(function () {
+ hideUpgradeMenu();
+ cooldownTimer = oldCooldown; // Restore cooldown
+ }, 2000);
+ return;
+ }
+ // Create upgrade options
+ for (var i = 0; i < upgradesToShow; i++) {
+ var xPos = GAME_WIDTH / 4 + GAME_WIDTH / 2 * (i / (upgradesToShow - 1));
+ if (upgradesToShow === 1) {
+ xPos = GAME_WIDTH / 2;
+ }
+ var upgrade = new Upgrade(availableUpgrades[i].type, availableUpgrades[i].description, xPos, GAME_HEIGHT / 2);
+ upgradeOverlay.addChild(upgrade);
+ upgradeOptions.push(upgrade);
+ }
+}
+// Function to hide upgrade menu
+function hideUpgradeMenu() {
+ upgradeMenuVisible = false;
+ upgradeOverlay.visible = false;
+}
+// Function to apply selected upgrade
+function applyUpgrade(type) {
+ switch (type) {
+ case "Faster Reload":
+ cooldownDuration = Math.floor(cooldownDuration * 0.85); // 15% reduction
+ upgradableReload++;
+ break;
+ case "Piercing Shots":
+ upgradablePiercing++;
+ break;
+ case "Larger Quiver":
+ // 5, 10, 15 more arrows before reload
+ upgradableQuiver++;
+ break;
+ case "Heat-tipped Arrows":
+ upgradableDamage++;
+ break;
+ case "Archer Ally":
+ var archer = new ArcherAlly();
+ // Position the archer along the bastion line
+ archer.x = 400 + Math.random() * (GAME_WIDTH - 800);
+ archer.y = BASTION_Y - 50;
+ game.addChild(archer);
+ allies.push(archer);
+ upgradableArchers++;
+ break;
+ case "Sabotage":
+ // Reduce enemy speed by 15%
+ currentEnemySpeed *= 0.85;
+ for (var i = 0; i < enemies.length; i++) {
+ enemies[i].speed *= 0.85;
+ }
+ upgradableSabotage++;
+ break;
+ case "Swordsman":
+ var swordsman = new Swordsman();
+ // Position the swordsman in front of the bastion
+ swordsman.x = 600 + Math.random() * (GAME_WIDTH - 1200);
+ swordsman.y = BASTION_Y - 100;
+ game.addChild(swordsman);
+ allies.push(swordsman);
+ upgradableSwordsmen++;
+ break;
+ }
+}
// --- 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.
@@ -370,10 +501,10 @@
}
};
// 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 process if a drag was active and not in upgrade menu
+ if (dragStartX !== null && cooldownTimer <= 0 && !upgradeMenuVisible) {
// Only allow firing if not on cooldown.
var gamePos = game.toLocal({
x: x,
y: y
@@ -399,16 +530,21 @@
newArrow.y = FIRING_POS_Y;
// Initialize last position for state tracking (e.g., off-screen detection)
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
+ // Apply piercing upgrade
+ newArrow.piercingCount = upgradablePiercing;
+ // Apply damage upgrade
+ newArrow.damage = 1 + upgradableDamage;
// Add the arrow to the game scene and the tracking array.
game.addChild(newArrow);
arrows.push(newArrow);
LK.getSound('shoot').play(); // Play shooting sound.
arrowsFired++; // Increment arrow count.
- // Check if cooldown needs to start based on Quiver upgrade
- if (arrowsFired >= maxArrowsBeforeCooldown) {
- cooldownTimer = cooldownDuration; // Start cooldown (duration affected by Faster Reload upgrade)
+ // Larger quiver upgrade (15 + additional from upgrade)
+ var maxArrows = 15 + upgradableQuiver * 5;
+ if (arrowsFired >= maxArrows) {
+ cooldownTimer = cooldownDuration; // Start cooldown.
arrowsFired = 0; // Reset arrow count.
LK.getSound('Reload').play(); // Play reload sound.
}
// Reset drag state.
@@ -419,25 +555,20 @@
};
// --- Main Game Update Loop ---
// This function is called automatically by the LK engine on every frame (tick).
game.update = function () {
- // --- Upgrade System Check ---
- var currentScore = LK.getScore();
- // Check if score reached a multiple of 10 and is higher than the last upgrade score
- if (!isUpgradePopupActive && currentScore > 0 && currentScore % 10 === 0 && currentScore !== lastUpgradeScore) {
- lastUpgradeScore = currentScore; // Mark this score level as having triggered an upgrade offer
- showUpgradePopup(); // Show the upgrade selection screen
+ // Don't update game logic if upgrade menu is visible
+ if (upgradeMenuVisible) {
+ return;
}
- // --- Pause Game Logic if Upgrade Popup is Active ---
- if (isUpgradePopupActive) {
- // Potential: Update UI animations if any
- return; // Skip the rest of the game update loop
- }
- // --- Resume Game Logic ---
// Decrease cooldown timer if active.
if (cooldownTimer > 0) {
cooldownTimer--;
}
+ // Update allies
+ for (var i = 0; i < allies.length; i++) {
+ allies[i].update();
+ }
// 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.
@@ -449,40 +580,40 @@
arrow.lastX = arrow.x;
}
// Check for collisions between the current arrow and all enemies.
var hitEnemy = false;
+ var pierceCount = 0;
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.setScore(LK.getScore() + 1); // Increment score.
- scoreTxt.setText(LK.getScore()); // Update score display.
- LK.getSound('hit').play(); // Play hit sound.
- // Collision detected!
- LK.setScore(LK.getScore() + 1); // Increment score.
- scoreTxt.setText(LK.getScore()); // Update score display.
- LK.getSound('hit').play(); // Play hit sound.
- // 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
+ // Apply damage and check if enemy died
+ if (enemy.takeDamage(arrow.damage)) {
+ // Enemy died
+ LK.setScore(LK.getScore() + 1); // Increment score.
+ scoreTxt.setText(LK.getScore()); // Update score display.
+ // Check if we should show upgrade menu (every 10 points)
+ var currentScore = LK.getScore();
+ if (Math.floor(currentScore / scorePerUpgrade) > Math.floor(lastUpgradeScore / scorePerUpgrade)) {
+ lastUpgradeScore = currentScore;
+ showUpgradeMenu();
+ }
+ LK.getSound('hit').play(); // Play hit sound.
+ enemy.destroy();
+ enemies.splice(j, 1); // Remove enemy from array.
+ }
+ hitEnemy = true; // Mark that a hit occurred.
+ pierceCount++;
+ // If the arrow is not a piercing arrow or has reached its piercing limit, destroy it
+ if (!arrow.allyArrow && (arrow.piercingCount <= 0 || pierceCount > arrow.piercingCount)) {
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
+ break; // Stop checking more enemies for this arrow
}
}
}
// If the arrow hit an enemy, it was destroyed, so skip to the next arrow.
- if (hitEnemy) {
+ if (hitEnemy && !arrow.allyArrow && (arrow.piercingCount <= 0 || pierceCount > arrow.piercingCount)) {
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.
@@ -530,12 +661,15 @@
}
// 3. 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) {
- // Determine the base speed for the new enemy, capping at max base speed.
- var baseSpeed = Math.min(maxEnemySpeed, currentEnemySpeed);
- // The Enemy class constructor expects the base speed. The multiplier is applied in Enemy.update.
- var newEnemy = new Enemy(baseSpeed);
+ // Determine the speed for the new enemy, capping at max speed.
+ var newEnemySpeed = Math.min(maxEnemySpeed, currentEnemySpeed);
+ var newEnemy = new Enemy(newEnemySpeed);
+ // Scale enemy health based on score/progress
+ var scoreBasedHealth = 1 + Math.floor(LK.getScore() / 30);
+ newEnemy.maxHealth = scoreBasedHealth;
+ newEnemy.health = scoreBasedHealth;
// Position the new enemy at the top of the screen at a random horizontal position.
// Add padding to avoid spawning exactly at the screen edges.
var spawnPadding = newEnemy.width / 2 + 20; // Add a little extra padding
newEnemy.x = spawnPadding + Math.random() * (GAME_WIDTH - 2 * spawnPadding);
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