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");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
/**
* Represents an Angel of Light ally that stuns enemies and deals high damage to dark enemies.
*/
var AngelOfLight = Container.expand(function () {
var self = Container.call(this);
// Create and attach the angel graphic asset (reusing viking_ally for simplicity)
// Will need a dedicated asset for the Angel of Light if available.
var graphics = self.attachAsset('angel_of_light_asset', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5
});
graphics.tint = 0xFFFF00; // Yellow tint for Angel of Light
self.attackRange = 600; // Moderate attack range
self.attackDamage = 5; // Base damage
self.attackInterval = 480; // Attack every 8 seconds (480 ticks)
self.attackTimer = 0; // Timer for attacks
self.stunDuration = 5 * 60; // Stun duration in ticks (5 seconds)
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and using light ability.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range
var closestEnemy = null;
var closestDistance = self.attackRange;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, use light ability
if (closestEnemy) {
// Apply stun and damage to the target
if (closestEnemy.tag !== 'Dragon') {
// Dragons are immune to stuns (slowdown effect)
closestEnemy.slowTimer = Math.max(closestEnemy.slowTimer, self.stunDuration); // Apply stun (reusing slowTimer)
closestEnemy.currentSlowAmount = 0.0; // 100% slowdown (stun)
}
var damageToDeal = self.attackDamage;
if (closestEnemy.tag === 'Black') {
damageToDeal *= 3; // Triple damage against Black enemies
}
closestEnemy.takeDamage(damageToDeal, self); // Deal damage
// Optional: Add visual/sound effect for light ability here later
LK.effects.flashObject(closestEnemy, 0xFFFFFF, 300); // Flash white for stun/damage
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents an allied archer that shoots arrows independently.
*/
var ArcherAlly = Container.expand(function () {
var self = Container.call(this);
// Create and attach the archer graphic asset
var graphics = self.attachAsset('Archer', {
anchorX: 0.5,
anchorY: 0.5
});
// No rotation needed for 'Archer' asset as it's already upright and flipped
self.fireTimer = 0; // Timer for shooting
self.fireInterval = 180; // Shoot every 3 seconds (3 * 60 ticks)
/**
* Update method called each game tick by the LK engine.
* Handles firing logic.
*/
self.update = function () {
self.fireTimer++;
var effectiveFireInterval = Math.max(60, self.fireInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.fireTimer >= effectiveFireInterval) {
self.fireTimer = 0; // Reset timer
// Find the closest enemy to shoot at
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, fire an arrow
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new arrow instance
var newArrow = new Arrow(angle);
// Apply multi-shot to allies if the upgrade is enabled for the player
if (multiShotEnabled) {
var angle2 = angle + Math.PI / 12; // Offset by 15 degrees
var newArrow2 = new Arrow(angle2);
newArrow2.x = self.x;
newArrow2.y = self.y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
game.addChild(newArrow2);
arrows.push(newArrow2);
var angle3 = angle - Math.PI / 12; // Offset by -15 degrees
var newArrow3 = new Arrow(angle3);
newArrow3.x = self.x;
newArrow3.y = self.y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
newArrow.x = self.x;
newArrow.y = self.y;
// Ally arrows do not count towards the player's reload counter
// The Arrow class handles piercing level based on player upgrade, but the ally doesn't benefit from player reload.
// For simplicity, we'll let the ally benefit from player's piercing upgrade.
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// Add the arrow to the game scene and the tracking array.
game.addChild(newArrow);
arrows.push(newArrow); // Add to the same arrows array for collision detection
// Ally doesn't play the 'shoot' sound
}
}
};
return self; // Return self for potential inheritance
});
// Sound when an enemy reaches the bastion
// No plugins needed for this version of the game.
/**
* Represents an Arrow fired by the player.
* @param {number} angle - The angle in radians at which the arrow is fired.
*/
var Arrow = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the arrow graphic asset
var graphics = self.attachAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align arrow graphic with direction
self.speed = 30; // Speed of the arrow in pixels per tick
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.pierceLeft = arrowPierceLevel; // How many more enemies this arrow can pierce
self.damage = arrowDamage; // Damage dealt by this arrow
self.isPoison = poisonShotsEnabled; // Flag if this arrow applies poison
self.targetEnemy = null; // Potential target for aimbot
self.seekSpeed = 0.1; // How quickly the arrow adjusts its direction to seek
if (aimbotEnabled) {
// Find the closest enemy to seek if aimbot is enabled
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.targetEnemy = closestEnemy;
}
}
/**
* Update method called each game tick by the LK engine.
* Moves the arrow based on its velocity.
*/
self.update = function () {
if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) {
// If aimbot is enabled, a target exists and is still in the game, seek it
var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y));
// Smoothly adjust the arrow's angle towards the target angle
var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation
// Normalize angle difference to be between -PI and PI
if (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
if (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Interpolate angle
var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed;
self.rotation = newAngle + Math.PI / 2;
self.vx = Math.sin(newAngle) * self.speed;
self.vy = -Math.cos(newAngle) * self.speed;
} else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) {
// If aimbot is enabled but current target is gone, find a new target
self.targetEnemy = null; // Clear old target
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.targetEnemy = closestEnemy;
}
}
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Baby Dragon enemy with rage mode when no other dragons are present.
*/
var BabyDragon = Container.expand(function () {
var self = Container.call(this);
// Create and attach the baby dragon graphic asset
var graphics = self.attachAsset('baby_dragon_asset', {
anchorX: 0.5,
anchorY: 0.5
});
// No base tint - only apply pink tint when raging
self.type = 'baby_dragon';
self.speed = 3; // Base speed (will be set from spawn)
self.health = 50; // Base health (will be set from spawn)
self.maxHealth = 50;
self.dodgeChance = 0.25; // 25% dodge chance (less than adult dragon)
self.tag = 'Dragon'; // Dragon-tagged enemy
self.isRaging = false;
self.rageSpeedMultiplier = 2.0; // Double speed when raging
self.baseSpeed = self.speed;
self.poisonStacks = 0;
self.poisonTimer = 0;
self.poisonDamagePerTick = 0.05;
self.slowTimer = 0;
self.currentSlowAmount = 1.0;
/**
* Update method called each game tick by the LK engine.
* Moves the baby dragon and checks for rage mode.
*/
self.update = function () {
// Check for rage mode (no other dragons on screen)
var otherDragonsExist = false;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy !== self && enemy.tag === 'Dragon') {
otherDragonsExist = true;
break;
}
}
// Apply or remove rage mode
if (!otherDragonsExist && !self.isRaging) {
// Enter rage mode
self.isRaging = true;
self.speed = self.baseSpeed * self.rageSpeedMultiplier;
// Apply pink tint with tween animation
tween(graphics, {
tint: 0xFF69B4
}, {
duration: 300
}); // Pink tint when raging
LK.effects.flashObject(self, 0xFF69B4, 500); // Flash effect with pink
} else if (otherDragonsExist && self.isRaging) {
// Exit rage mode
self.isRaging = false;
self.speed = self.baseSpeed;
// Remove tint with tween animation
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 300
}); // Return to normal color
}
// Normal movement (dragons are immune to slowdown)
self.y += self.speed;
// Apply poison damage
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30;
self.health -= poisonDmg;
}
if (self.health <= 0 && self.parent) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
self.destroy();
return;
}
}
// Visual feedback based on health
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6;
};
/**
* Method called when the baby dragon takes damage.
*/
self.takeDamage = function (damage, source) {
// Check for dodge
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
return false;
}
// Dragons are not immune to arrows/cannonballs
self.health -= damage;
// Apply poison if applicable
if (source && source.isPoison) {
self.poisonStacks++;
self.poisonTimer = 0;
}
// Dragons are immune to slowdown effects
return self.health <= 0;
};
return self;
});
/**
* Represents a Bomb projectile thrown by a Bomber ally.
* @param {number} targetX - The target X coordinate.
* @param {number} targetY - The target Y coordinate.
*/
var Bomb = Container.expand(function (targetX, targetY) {
var self = Container.call(this);
// Create and attach the bomb graphic asset
var graphics = self.attachAsset('bomb_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.targetX = targetX;
self.targetY = targetY;
self.startX = self.x;
self.startY = self.y;
self.damage = 15; // Area damage
self.explosionRadius = 200; // Explosion radius
self.arcHeight = 300; // Height of bomb arc
self.flightTime = 60; // 1 second flight time
self.flightTimer = 0;
self.hasExploded = false;
/**
* Update method called each game tick by the LK engine.
* Handles arc movement and explosion.
*/
self.update = function () {
if (self.hasExploded) {
return;
}
self.flightTimer++;
var progress = self.flightTimer / self.flightTime;
if (progress >= 1) {
// Bomb has reached target, explode
self.explode();
return;
}
// Calculate arc position
var baseX = self.startX + (self.targetX - self.startX) * progress;
var baseY = self.startY + (self.targetY - self.startY) * progress;
// Add arc height (parabola)
var arcOffset = -4 * self.arcHeight * progress * (progress - 1);
self.x = baseX;
self.y = baseY - arcOffset;
// Rotate bomb as it flies
graphics.rotation += 0.2;
};
self.explode = function () {
if (self.hasExploded) {
return;
}
self.hasExploded = true;
// Create explosion visual effect
LK.effects.flashScreen(0xFFAA00, 200); // Orange flash
// Deal area damage to all enemies within radius
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.explosionRadius) {
// Calculate damage falloff (full damage at center, less at edges)
var damageFalloff = 1 - distance / self.explosionRadius * 0.5;
var damageToApply = self.damage * damageFalloff;
var enemyDefeated = enemy.takeDamage(damageToApply, self);
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
enemy.destroy();
enemies.splice(i, 1);
}
// Apply visual effect to hit enemies
LK.effects.flashObject(enemy, 0xFFAA00, 300);
}
}
self.destroy();
};
return self;
});
/**
* Represents a Bomber ally that throws bombs for area damage.
*/
var Bomber = Container.expand(function () {
var self = Container.call(this);
// Create and attach the bomber graphic asset
var graphics = self.attachAsset('bomber_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range like other allies
self.attackInterval = 240; // Attack every 4 seconds
self.attackTimer = 0;
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and throwing bombs.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier);
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0;
// Find a suitable target (not Dragon-tagged)
var bestTarget = null;
var bestScore = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Skip Dragon-tagged enemies
if (enemy.tag === 'Dragon') {
continue;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.attackRange) {
// Prioritize groups of enemies (check nearby enemy count)
var nearbyCount = 0;
for (var j = 0; j < enemies.length; j++) {
if (i !== j) {
var ex = enemies[j].x - enemy.x;
var ey = enemies[j].y - enemy.y;
if (Math.sqrt(ex * ex + ey * ey) <= 200) {
nearbyCount++;
}
}
}
var score = nearbyCount * 10 + (self.attackRange - distance) / 100;
if (score > bestScore) {
bestScore = score;
bestTarget = enemy;
}
}
}
if (bestTarget) {
// Create and throw bomb
var bomb = new Bomb(bestTarget.x, bestTarget.y);
bomb.startX = self.x;
bomb.startY = self.y;
bomb.x = self.x;
bomb.y = self.y;
game.addChild(bomb);
bombs.push(bomb);
}
}
};
return self;
});
/**
* Represents a Bouncy Ball that bounces around the screen dealing damage to enemies.
*/
var BouncyBall = Container.expand(function () {
var self = Container.call(this);
// Create and attach the bouncy ball graphic asset (reusing cannonball for now)
var graphics = self.attachAsset('cannonball', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
graphics.tint = 0xFF00FF; // Magenta tint for bouncy ball
self.speed = 15; // Base speed
self.damage = 20; // Massive damage
self.vx = (Math.random() - 0.5) * self.speed * 2; // Random initial horizontal velocity
self.vy = -Math.abs((Math.random() - 0.5) * self.speed * 2); // Initial upward velocity
self.lifetime = 600; // 10 seconds lifetime (600 ticks)
self.lifetimeTimer = 0;
self.bounceCount = 0; // Track number of bounces
self.maxBounces = 20; // Maximum bounces before disappearing
/**
* Update method called each game tick by the LK engine.
* Handles movement, bouncing, and lifetime.
*/
self.update = function () {
self.lifetimeTimer++;
// Check lifetime
if (self.lifetimeTimer >= self.lifetime || self.bounceCount >= self.maxBounces) {
self.destroy();
return;
}
// Move
self.x += self.vx;
self.y += self.vy;
// Bounce off walls
if (self.x <= graphics.width / 2 || self.x >= GAME_WIDTH - graphics.width / 2) {
self.vx = -self.vx; // Reverse horizontal direction
self.x = Math.max(graphics.width / 2, Math.min(GAME_WIDTH - graphics.width / 2, self.x));
self.bounceCount++;
// Flash on bounce
LK.effects.flashObject(self, 0xFFFFFF, 200);
}
// Bounce off top and bottom
if (self.y <= graphics.height / 2 || self.y >= BASTION_Y - graphics.height / 2) {
self.vy = -self.vy; // Reverse vertical direction
self.y = Math.max(graphics.height / 2, Math.min(BASTION_Y - graphics.height / 2, self.y));
self.bounceCount++;
// Flash on bounce
LK.effects.flashObject(self, 0xFFFFFF, 200);
}
};
return self;
});
/**
* Represents a Cannon ally that targets the strongest enemy.
*/
var Cannon = Container.expand(function () {
var self = Container.call(this);
// Create and attach the cannon graphic asset (need to add a new asset for this)
// Use dragon slayer cannon asset if mode is enabled, otherwise use regular cannon asset
var assetId = self.dragonSlayerMode ? 'dragon_slayer_cannon_asset' : 'cannon_asset';
var graphics = self.attachAsset(assetId, {
// Use actual cannon asset
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = 800; // Long attack range
self.attackDamage = 10; // High damage
self.attackInterval = 300; // Attack every 5 seconds (300 ticks)
self.attackTimer = 0; // Timer for attacks
self.rotation = 0; // For aiming visual if implemented later
self.dragonSlayerMode = false; // Flag to indicate if this cannon is a dragon slayer cannon
/**
* Update method called each game tick by the LK engine.
* Handles attacking the strongest enemy.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
var targetEnemy = null;
if (self.dragonSlayerMode) {
// In dragon slayer mode, prioritize dragons
var closestDragon = null;
var closestDragonDistance = Infinity;
var strongestNonDragon = null;
var maxNonDragonHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.tag === 'Dragon') {
// Check distance for dragons
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDragonDistance) {
closestDragonDistance = distance;
closestDragon = enemy;
}
} else {
// Track strongest non-dragon as fallback
if (enemy.health > maxNonDragonHealth) {
maxNonDragonHealth = enemy.health;
strongestNonDragon = enemy;
}
}
}
// Prioritize dragon if found, otherwise target strongest non-dragon
targetEnemy = closestDragon || strongestNonDragon;
} else {
// Normal mode: find the strongest enemy (highest health)
var maxHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > maxHealth) {
maxHealth = enemy.health;
targetEnemy = enemy;
}
}
}
// If a target enemy is found within range, attack it
if (targetEnemy) {
// Calculate angle towards the target enemy
var angle = Math.atan2(targetEnemy.x - self.x, -(targetEnemy.y - self.y));
// Create a new cannonball instance, passing the dragon slayer mode status
var newCannonball = new Cannonball(angle);
newCannonball.dragonSlayerMode = self.dragonSlayerMode; // Pass the mode status
// Position the cannonball at the cannon's location
newCannonball.x = self.x;
newCannonball.y = self.y;
newCannonball.lastY = newCannonball.y; // Initialize lastY for state tracking
newCannonball.lastX = newCannonball.x; // Initialize lastX for state tracking
// If dragon slayer mode and targeting a dragon, apply damage bonus
if (self.dragonSlayerMode && targetEnemy.tag === 'Dragon') {
newCannonball.dragonDamageMultiplier = 25; // 25x damage to dragons
// The Cannonball class will handle the asset and tint based on the mode.
}
// Add the cannonball to the game scene and the tracking array.
game.addChild(newCannonball);
cannonballs.push(newCannonball); // Add to the new cannonballs array
// Optional: Add a visual/sound effect for cannon shot here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents a Cannonball fired by a Cannon.
* @param {number} angle - The angle in radians at which the cannonball is fired.
*/
var Cannonball = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the graphic asset (rocket if dragon slayer mode, otherwise cannonball)
var assetId = self.dragonSlayerMode ? 'rocket_asset' : 'cannonball';
var graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align graphic with direction
// Apply red tint for rocket if in dragon slayer mode
if (self.dragonSlayerMode) {
graphics.tint = 0xFF0000; // Red tint for dragon slayer rockets
}
self.dragonSlayerMode = false; // Flag to indicate if this is a dragon slayer rocket
self.dragonDamageMultiplier = 1; // Default damage multiplier
self.speed = 20; // Base speed of the cannonball
self.damage = 10; // Base damage dealt by this cannonball
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
/**
* Update method called each game tick by the LK engine.
* Moves the cannonball based on its velocity.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Dart projectile fired by a Dart Ally.
* @param {number} angle - The angle in radians at which the dart is fired.
*/
var Dart = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the dart graphic asset (reusing arrow for simplicity)
// Will need a dedicated asset for the Dart if available.
var graphics = self.attachAsset('dart_asset', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5
});
graphics.tint = 0x9933CC; // Purple tint for darts
graphics.rotation = angle + Math.PI / 2; // Align dart graphic with direction
self.speed = 40; // Fast speed
self.damage = 0.5; // Base damage dealt by this dart
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
/**
* Update method called each game tick by the LK engine.
* Moves the dart based on its velocity.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Dart Ally that shoots fast darts.
*/
var DartAlly = Container.expand(function () {
var self = Container.call(this);
// Create and attach the dart ally graphic asset (reusing archer asset for simplicity)
// Will need a dedicated asset for the Dart Ally if available.
var graphics = self.attachAsset('disguised_swordsman', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackDamage = 0.5; // Low damage per dart
self.attackInterval = 30; // Attack every 0.5 seconds (30 ticks) - very fast
self.attackTimer = 0; // Timer for attacks
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and shooting darts.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(10, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.16s
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy to shoot at
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, shoot a dart
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new dart instance
var newDart = new Dart(angle);
newDart.x = self.x;
newDart.y = self.y;
newDart.lastY = newDart.y;
newDart.lastX = newDart.x;
// Add the dart to the game scene and the tracking array.
game.addChild(newDart);
darts.push(newDart); // Add to the new darts array
// Optional: Add throwing sound effect here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents an Enemy attacker moving towards the bastion.
* @param {string} type - The type of enemy ('swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight').
* @param {number} speed - The final calculated speed of the enemy.
* @param {number} health - The initial and maximum health of the enemy.
* @param {number} dodgeChance - The chance (0 to 1) for the enemy to dodge an attack.
*/
var Enemy = Container.expand(function (type, speed, health, dodgeChance) {
var self = Container.call(this);
// Set asset based on type
var assetId = 'enemy'; // Default swordsman asset
if (type === 'knight') {
assetId = 'knight';
} else if (type === 'elite_knight') {
assetId = 'elite_knight_asset'; // Use specific asset for elite knight
} else if (type === 'thief') {
assetId = 'thief';
} else if (type === 'boss') {
assetId = 'boss';
} else if (type === 'shield') {
assetId = 'shield_enemy'; // Use specific asset for shield
} else if (type === 'wizard') {
assetId = 'wizard_enemy'; // Use specific asset for wizard
} else if (type === 'spearman') {
assetId = 'spearman'; // Use specific asset for spearman
} else if (type === 'war_elephant') {
assetId = 'war_elephant'; // Use specific asset for war elephant
} else if (type === 'elite_shield') {
assetId = 'elite_shield_asset'; // Use specific asset for elite shield
} else if (type === 'shaman') {
assetId = 'shaman_enemy'; // Use specific asset for shaman
} else if (type === 'hot_air_balloon') {
assetId = 'hot_air_balloon_asset'; // Use specific asset for hot air balloon
} else if (type === 'dark_bowman') {
assetId = 'dark_bowman_asset'; // Use specific asset for dark bowman
} else if (type === 'jester') {
assetId = 'jester_asset'; // Use specific asset for jester
} else if (type === 'dark_war_elephant') {
assetId = 'dark_war_elephant_asset'; // Use specific asset for dark war elephant
} else if (type === 'dark_spearman') {
assetId = 'dark_spearman_asset'; // Use specific asset for dark spearman
} else if (type === 'dragon') {
assetId = 'dragon_asset'; // Use specific asset for dragon
} else if (type === 'flag_bearer') {
assetId = 'flag_bearer_asset'; // Use specific asset for flag bearer
} else if (type === 'baby_dragon') {
assetId = 'baby_dragon_asset'; // Use specific baby dragon asset
}
var graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.type = type; // 'swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight', 'shaman', etc.
self.speed = speed; // Final calculated speed
self.health = health;
self.maxHealth = health; // Store max health for visual feedback
self.dodgeChance = dodgeChance || 0; // Default to 0 if undefined
self.reflectChance = type === 'jester' ? 0.20 : 0; // 20% reflect chance, added in Enemy class
self.poisonStacks = 0; // Number of poison stacks
self.poisonTimer = 0; // Timer for poison damage
self.poisonDamagePerTick = 0.05; // Damage per tick per stack
self.slowTimer = 0; // Timer for slowdown effect
self.currentSlowAmount = 1.0; // Multiplier for current slowdown effect (1.0 means no slow)
// Initialize shaman-specific properties
if (type === 'shaman') {
self.shamanTimer = 0;
}
// Set tag property based on type
self.tag = null; // Default tag is null
if (self.type === 'wizard' || self.type === 'spearman' || self.type === 'war_elephant' || self.type === 'shaman') {
self.tag = 'Green';
} else if (self.type === 'dark_bowman' || self.type === 'dark_war_elephant' || self.type === 'dark_spearman') {
self.tag = 'Black'; // Black-tagged enemies are immune to arrows and cannonballs
}
// Wizard-specific properties
if (self.type === 'wizard') {
self.teleportTimer = 0;
self.teleportInterval = 180; // 3 seconds * 60 FPS
}
// --- Public Methods (defined before use) ---
/**
* Update method called each game tick by the LK engine.
* Moves the enemy downwards (or teleports for wizard) and updates visual feedback.
*/
self.update = function () {
if (self.type === 'wizard') {
self.teleportTimer = (self.teleportTimer || 0) + 1; // Initialize timer if needed
if (self.teleportTimer >= self.teleportInterval) {
self.teleportTimer = 0;
// Teleport logic: Random X, slightly advanced Y
var oldY = self.y;
var teleportPadding = graphics.width / 2 + 20; // Use actual graphic width
var newX = teleportPadding + Math.random() * (GAME_WIDTH - 2 * teleportPadding);
// Advance Y slightly, but don't teleport past bastion
var newY = Math.min(BASTION_Y - graphics.height, self.y + 100 + Math.random() * 100); // Advance 100-200px
// Ensure not teleporting backwards significantly or offscreen top
newY = Math.max(graphics.height / 2, newY);
self.x = newX;
self.y = newY;
self.lastY = oldY; // Set lastY to pre-teleport position to avoid false bastion triggers
// Add a visual effect for teleport
LK.effects.flashObject(self, 0xAA00FF, 300); // Purple flash
} else {
// Move normally if not teleporting this frame
self.y += self.speed;
}
} else {
// Normal movement for other enemy types
var effectiveSpeed = self.speed * self.currentSlowAmount;
// Apply global slowdown relic if enabled and not a Dragon
if (playerRelics.slowdown.enabled && self.tag !== 'Dragon') {
effectiveSpeed *= 1 - playerRelics.slowdown.level * 0.02; // Apply global slow
}
// Apply Green Relic slowdown if enabled and enemy is Green-tagged and not a Dragon
if (playerRelics.green.enabled && self.tag === 'Green' && self.tag !== 'Dragon') {
effectiveSpeed *= 1 - playerRelics.green.level * 0.02; // Apply Green Relic slow
}
self.y += effectiveSpeed; // Apply slowdown to movement
}
// Decrease slowdown timer and reset slow amount if timer runs out
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer <= 0) {
self.currentSlowAmount = 1.0; // Reset speed multiplier when slowdown ends
// Optional: Remove visual feedback for slowdown here later
}
}
// Apply poison damage if stacks exist
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
// Apply poison damage every 0.5 seconds (30 ticks)
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30; // Damage per 0.5s
self.health -= poisonDmg;
// Optional: Add visual feedback for poison damage here later
}
// Check if health dropped to zero or below due to poison
if (self.health <= 0 && self.parent) {
// Ensure enemy is still in the game
// Enemy defeated by poison
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
self.destroy();
// The main game update loop needs to handle removing the enemy from the `enemies` array
return; // Stop updating this destroyed instance
}
}
// Visual feedback based on health - alpha fade (applies to all types)
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6; // Fade from 1.0 down to 0.4
// Shaman ability: reduce player ammo periodically
if (self.type === 'shaman') {
if (self.shamanTimer === undefined) {
self.shamanTimer = 0;
}
self.shamanTimer += 1;
var shamanAbilityInterval = 300; // 5 seconds * 60 FPS
if (self.shamanTimer >= shamanAbilityInterval) {
self.shamanTimer = 0;
// Reduce player ammo, but not below 0
arrowsFired = Math.min(maxArrowsBeforeCooldown, arrowsFired + 1); // Make it require one more shot for reload
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
// Optional: Add a visual/sound effect for shaman ability
}
}
}; //{N} // Adjusted line identifier
/**
* Method called when the enemy is hit by an arrow.
* Handles dodging and health reduction.
* @param {number} damage - The amount of damage the arrow deals.
* @returns {boolean} - True if the enemy is defeated, false otherwise.
*/
self.takeDamage = function (damage) {
var source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
// Add source parameter, default to null
// Check for dodge first (only if dodgeChance is positive)
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
// Optional: Add a visual effect for dodge here later
// console.log(self.type + " dodged!");
return false; // Dodged, so not defeated by this hit
}
// Check for reflection (only for Jester and non-cannonball projectiles)
if (self.type === 'jester' && self.reflectChance > 0 && Math.random() < self.reflectChance && !(source instanceof Cannonball)) {
// Optional: Add a visual effect for reflection here later
// console.log(self.type + " reflected!");
// Projectile is destroyed, enemy takes no damage
if (source && source.destroy) {
source.destroy();
// Need to remove from the source array in the main game loop
}
return false; // Reflected, so not defeated by this hit
}
// Check if enemy is Black-tagged and source is an Arrow or Cannonball
if (self.tag === 'Black' && (source instanceof Arrow || source instanceof Cannonball)) {
// Black-tagged enemies are immune to arrow and cannonball projectiles
// Unless Dark Relic is enabled
if (!playerRelics.dark.enabled) {
return false; // Not defeated, projectile has no effect
}
}
var finalDamage = damage;
// Apply damage relic bonus if enabled
if (playerRelics.damage.enabled) {
finalDamage += playerRelics.damage.level;
}
// Apply Green Relic damage bonus if enabled and enemy is Green-tagged
if (playerRelics.green.enabled && self.tag === 'Green') {
finalDamage *= 1 + playerRelics.green.level * 0.2;
}
// Apply Dark Relic damage bonus if enabled and enemy is Black-tagged
if (playerRelics.dark.enabled && self.tag === 'Black' && (source instanceof Arrow || source instanceof Cannonball)) {
finalDamage *= 1 + playerRelics.dark.level * 0.05;
}
self.health -= finalDamage;
// Check if the damage source was a poison arrow and apply poison stacks
if (source && source.isPoison) {
// Use the 'source' parameter
self.poisonStacks++; // Increase poison stacks
self.poisonTimer = 0; // Reset timer to apply damage immediately
}
// Check if the damage source was a Magic Ball and apply slowdown
if (source && source instanceof MagicBall) {
if (self.tag !== 'Dragon') {
// Dragons are immune to slowdowns
self.slowTimer = source.slowDuration; // Apply slowdown duration
self.currentSlowAmount = source.slowAmount; // Apply slowdown amount
// Optional: Add visual feedback for slowdown here later
}
}
// Check if the damage source was an Arrow and apply Dragon relic stun
if (playerRelics.dragon.enabled && source instanceof Arrow && self.tag === 'Dragon') {
// Apply stun (reusing slowTimer for duration, 0.0 for 100% slow)
var stunDuration = 0.25 * 60 + playerRelics.dragon.level * 0.05 * 60; // Base 0.25s + 0.05s per level
self.slowTimer = Math.max(self.slowTimer, stunDuration);
self.currentSlowAmount = 0.0; // 100% slow (stun)
}
// Check if the damage source should apply Green Slowdown
if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0 && self.tag !== 'Dragon') {
// Dragons are immune to slowdowns
// Apply Green Slowdown only if the upgrade is active, enemy is Green, not already slowed, and not a Dragon
self.slowTimer = 10 * 60; // 10 seconds * 60 ticks/sec
self.currentSlowAmount = 0.9; // 10% slowdown
// Optional: Add visual feedback for green slowdown
}
// Check if enemy was defeated by this damage
var defeated = self.health <= 0;
if (defeated && self.type === 'war_elephant') {
// If War Elephant is defeated, spawn 5 spearmen
for (var i = 0; i < 5; i++) {
var spawnPadding = 100; // Padding from edge
var spawnX = self.x + (Math.random() * 200 - 100); // Spawn around elephant's x
var spawnY = self.y + (Math.random() * 100 - 50); // Spawn around elephant's y, slightly forward
// Ensure spawns are within game bounds
spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX));
spawnY = Math.max(spawnPadding, Math.min(BASTION_Y - 50, spawnY)); // Don't spawn too close to bastion
var newSpearman = new Enemy('spearman', currentEnemySpeed * enemySpeedMultiplier, 2, 0); // Base spearman stats
newSpearman.x = spawnX;
newSpearman.y = spawnY;
newSpearman.lastY = newSpearman.y;
game.addChild(newSpearman);
enemies.push(newSpearman);
}
} else if (defeated && self.type === 'dark_war_elephant') {
// If Dark War Elephant is defeated, spawn 5 Dark Bowmen
for (var i = 0; i < 5; i++) {
var spawnPadding = 100; // Padding from edge
var spawnX = self.x + (Math.random() * 200 - 100); // Spawn around elephant's x
var spawnY = self.y + (Math.random() * 100 - 50); // Spawn around elephant's y, slightly forward
// Ensure spawns are within game bounds
spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX));
spawnY = Math.max(spawnPadding, Math.min(BASTION_Y - 50, spawnY)); // Don't spawn too close to bastion
var newDarkBowman = new Enemy('dark_bowman', currentEnemySpeed * enemySpeedMultiplier, 5, 0); // Base dark bowman stats
newDarkBowman.x = spawnX;
newDarkBowman.y = spawnY;
newDarkBowman.lastY = newDarkBowman.y;
game.addChild(newDarkBowman);
enemies.push(newDarkBowman);
}
} else if (defeated && self.type === 'hot_air_balloon') {
// If Hot Air Balloon is defeated, spawn 5 random non-green enemies
var possibleTypes = ['swordsman', 'knight', 'thief', 'shield', 'elite_knight', 'elite_shield', 'dark_bowman'];
for (var i = 0; i < 5; i++) {
// Choose random enemy type that is not green-tagged
var randomType = possibleTypes[Math.floor(Math.random() * possibleTypes.length)];
var randomHP = 1 + Math.floor(Math.random() * 5); // Random HP between 1-5
var randomSpeed = currentEnemySpeed * 0.7 * enemySpeedMultiplier; // Slower than average
var newEnemy = new Enemy(randomType, randomSpeed, randomHP, 0);
// Spawn in a staggered pattern below the balloon
var spawnPadding = 100;
var spawnX = self.x + (Math.random() * 300 - 150); // Wider spread than elephant
var spawnY = self.y + Math.random() * 200; // Always below the balloon
// Ensure spawns are within game bounds
spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX));
spawnY = Math.min(BASTION_Y - 50, spawnY); // Don't spawn too close to bastion
newEnemy.x = spawnX;
newEnemy.y = spawnY;
newEnemy.lastY = newEnemy.y;
game.addChild(newEnemy);
enemies.push(newEnemy);
}
}
// No need to update alpha here, self.update handles it
return defeated; // Return true if health is 0 or less
}; //{O} // Adjusted line identifier
// --- Initialization ---
// Apply visual distinctions based on type
graphics.tint = 0xFFFFFF; // Reset tint
graphics.scale.set(1.0); // Reset scale
if (self.type === 'knight') {
graphics.tint = 0xCCCCCC; // Grey tint for Knights
graphics.scale.set(1.1);
} else if (self.type === 'elite_knight') {
graphics.tint = 0xFFD700; // Gold tint for Elite Knights
graphics.scale.set(1.2); // Slightly larger than normal knight
} else if (self.type === 'thief') {
graphics.tint = 0xCCFFCC; // Pale Green tint for Thieves
graphics.scale.set(0.9);
} else if (self.type === 'boss') {
graphics.tint = 0xFFCCCC; // Pale Red tint for Bosses
graphics.scale.set(1.4); // Make bosses quite large
} else if (self.type === 'shield') {
graphics.tint = 0xADD8E6; // Light Blue tint for Shield
graphics.scale.set(1.2); // Make shield enemies bulky
} else if (self.type === 'wizard') {
graphics.tint = 0xE0B0FF; // Light Purple tint for Wizard
graphics.scale.set(1.0);
} else if (self.type === 'elite_shield') {
graphics.tint = 0x8A2BE2; // Blue Violet tint for Elite Shield
graphics.scale.set(1.3); // Slightly larger than normal shield
} else if (self.type === 'jester') {
graphics.tint = 0xFFB6C1; // Light Pink tint for Jester
graphics.scale.set(1.0);
} else if (self.type === 'dark_war_elephant') {
graphics.tint = 0x333333; // Dark Grey tint for Dark War Elephant
graphics.scale.set(1.4); // Same size as regular elephant
} else if (self.type === 'dark_spearman') {
graphics.scale.set(1.05); // Slightly larger than regular spearman
} else if (self.type === 'dragon') {
graphics.scale.set(1.6); // Dragons are large
} //{11} // Modified original line identifier location
return self; // Return self for potential inheritance
});
/**
* Represents a Flag Bearer enemy that provides a speed boost aura to nearby enemies.
*/
var FlagBearer = Container.expand(function () {
var self = Container.call(this);
// Create and attach the flag bearer graphic asset
var graphics = self.attachAsset('flag_bearer_asset', {
anchorX: 0.5,
anchorY: 0.5
});
// No tint applied to flag bearer
self.type = 'flag_bearer';
self.speed = 3; // Base speed (will be set from spawn)
self.health = 5; // Base health (will be set from spawn)
self.maxHealth = 5;
self.dodgeChance = 0;
self.tag = 'Green'; // Green-tagged enemy
self.auraRadius = 300; // Radius of speed boost aura
self.auraBoost = 1.5; // 50% speed boost to enemies in aura
self.poisonStacks = 0;
self.poisonTimer = 0;
self.poisonDamagePerTick = 0.05;
self.slowTimer = 0;
self.currentSlowAmount = 1.0;
// Create aura visual effect
self.auraCircle = self.attachAsset('light_effect_asset', {
anchorX: 0.5,
anchorY: 0.5,
width: self.auraRadius * 2,
height: self.auraRadius * 2,
tint: 0x00FF00,
alpha: 0.2
});
// Position aura behind the flag bearer
self.setChildIndex(self.auraCircle, 0);
/**
* Update method called each game tick by the LK engine.
* Moves the flag bearer and applies aura effects.
*/
self.update = function () {
// Normal movement
self.y += self.speed * self.currentSlowAmount;
// Apply aura effect to nearby enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy === self) {
continue;
} // Skip self
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.auraRadius) {
// Enemy is within aura range
if (!enemy.hasAuraBoost) {
enemy.hasAuraBoost = true;
enemy.baseSpeed = enemy.speed;
enemy.speed = enemy.baseSpeed * self.auraBoost;
}
} else if (enemy.hasAuraBoost) {
// Enemy left aura range
enemy.hasAuraBoost = false;
enemy.speed = enemy.baseSpeed || enemy.speed;
}
}
// Handle slowdown timer
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer <= 0) {
self.currentSlowAmount = 1.0;
}
}
// Apply poison damage
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30;
self.health -= poisonDmg;
}
if (self.health <= 0 && self.parent) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
self.destroy();
return;
}
}
// Visual feedback based on health
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6;
// Update aura visual effect
if (self.auraCircle) {
self.auraCircle.x = self.x;
self.auraCircle.y = self.y;
// Pulse effect
self.auraPulseTimer = (self.auraPulseTimer || 0) + 0.05;
var pulseScale = 1.0 + Math.sin(self.auraPulseTimer) * 0.1;
self.auraCircle.scale.set(pulseScale);
self.auraCircle.alpha = 0.15 + Math.sin(self.auraPulseTimer * 2) * 0.05;
}
};
/**
* Method called when the flag bearer takes damage.
*/
self.takeDamage = function (damage, source) {
// Check for dodge
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
return false;
}
// Check if source is Arrow or Cannonball (Green enemies are not immune)
self.health -= damage;
// Apply poison if applicable
if (source && source.isPoison) {
self.poisonStacks++;
self.poisonTimer = 0;
}
// Apply slowdown from Magic Ball
if (source && source instanceof MagicBall) {
self.slowTimer = source.slowDuration;
self.currentSlowAmount = source.slowAmount;
}
// Apply Green Slowdown
if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0) {
self.slowTimer = 10 * 60;
self.currentSlowAmount = 0.9;
}
return self.health <= 0;
};
// Clean up aura effects when destroyed
var originalDestroy = self.destroy;
self.destroy = function () {
// Remove aura effects from all enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.hasAuraBoost) {
enemy.hasAuraBoost = false;
enemy.speed = enemy.baseSpeed || enemy.speed;
}
}
// Clean up aura visual
if (self.auraCircle) {
self.auraCircle.destroy();
self.auraCircle = null;
}
originalDestroy.call(self);
};
return self;
});
/**
* Represents a Magic Ball projectile fired by a Wizard Tower.
* @param {number} angle - The angle in radians at which the magic ball is fired.
*/
var MagicBall = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the magic ball graphic asset
var graphics = self.attachAsset('magic_ball_asset', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align magic ball graphic with direction
self.speed = 15; // Base speed of the magic ball
self.damage = 2; // Base damage dealt by this magic ball
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.slowDuration = 0; // Duration of slowdown applied on hit
self.slowAmount = 0; // Multiplier for enemy speed on hit
self.targetEnemy = null; // Track the specific enemy the magic ball is seeking
self.seekSpeed = 0.05; // How quickly the magic ball adjusts its direction to seek
/**
* Update method called each game tick by the LK engine.
* Moves the magic ball based on its velocity and seeks target if Aimbot is enabled.
*/
self.update = function () {
if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) {
// If aimbot is enabled, a target exists and is still in the game, seek it
var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y));
// Smoothly adjust the magic ball's angle towards the target angle
var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation
// Normalize angle difference to be between -PI and PI
if (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
if (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Interpolate angle
var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed;
self.rotation = newAngle + Math.PI / 2;
self.vx = Math.sin(newAngle) * self.speed;
self.vy = -Math.cos(newAngle) * self.speed;
} else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) {
// If aimbot is enabled but current target is gone, find a new target
self.targetEnemy = null; // Clear old target
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.targetEnemy = closestEnemy;
}
}
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Swordsman ally that attacks enemies in melee range.
*/
var Swordsman = Container.expand(function () {
var self = Container.call(this);
// Create and attach the swordsman graphic asset (reusing enemy asset for simplicity)
var graphics = self.attachAsset('swordsmanAlly', {
anchorX: 0.5,
anchorY: 0.5
});
// No tint needed as ally asset has unique colors
graphics.scale.set(1.0); // Use the asset's intended scale
self.attackRange = 150; // Melee attack range
self.attackDamage = 1; // Damage per hit
self.attackInterval = 60; // Attack every 1 second (60 ticks)
self.attackTimer = 0; // Timer for attacks
self.lifetime = 10 * 60; // Lifetime in ticks (10 seconds * 60 ticks/sec)
self.lifetimeTimer = 0; // Timer for lifetime
/**
* Update method called each game tick by the LK engine.
* Handles attacking and lifetime.
*/
self.update = function () {
self.attackTimer++;
self.lifetimeTimer++;
// Check for lifetime
var effectiveAttackInterval = Math.max(30, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.5s
if (self.lifetimeTimer >= self.lifetime) {
self.destroy();
// Remove from the swordsmen array in the main game loop
return; // Stop updating this instance
}
// Find the closest enemy to chase or attack
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy exists, chase it or attack if within range
if (closestEnemy) {
// Movement speed toward enemies
var moveSpeed = 5;
// If not within attack range, move toward the enemy
if (closestDistance > self.attackRange) {
// Calculate direction vector to enemy
var dirX = closestEnemy.x - self.x;
var dirY = closestEnemy.y - self.y;
// Normalize the direction vector
var length = Math.sqrt(dirX * dirX + dirY * dirY);
dirX = dirX / length;
dirY = dirY / length;
// Move toward the enemy
self.x += dirX * moveSpeed;
self.y += dirY * moveSpeed;
}
// If within attack range and attack timer is ready, attack
else if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Apply damage to the enemy and check if it was defeated
var finalSwordsmanDamage = self.attackDamage;
// Apply damage relic bonus if enabled
if (playerRelics.damage.enabled) {
finalSwordsmanDamage += playerRelics.damage.level;
}
var enemyDefeated = closestEnemy.takeDamage(finalSwordsmanDamage, self); // Pass the swordsman as source
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score when enemy is defeated by swordsman.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
closestEnemy.destroy();
// Remove from the enemies array in the main game loop
}
// Optional: Add a visual/sound effect for attack here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents a Viking ally that throws piercing axes.
*/
var VikingAlly = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('viking_ally', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackInterval = 150; // Attack every 2.5 seconds (150 ticks)
self.attackTimer = 0; // Timer for attacks
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and throwing axes.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range
var closestEnemy = null;
var closestDistance = self.attackRange;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, throw an axe
if (closestEnemy) {
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
var newAxe = new VikingAxe(angle);
newAxe.x = self.x;
newAxe.y = self.y;
newAxe.lastY = newAxe.y;
newAxe.lastX = newAxe.x;
game.addChild(newAxe);
vikingAxes.push(newAxe); // Add to the vikingAxes array
// Optional: Add throwing sound effect here later
}
}
};
return self;
});
/**
* Represents an Axe thrown by a Viking Ally.
* @param {number} angle - The angle in radians at which the axe is thrown.
*/
var VikingAxe = Container.expand(function (angle) {
var self = Container.call(this);
var graphics = self.attachAsset('viking_axe', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align axe graphic with direction
self.speed = 25; // Speed of the axe
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.damage = 3; // Base damage dealt by this axe
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.damage += 5;
}
self.pierceLeft = 3; // Can pierce through 3 enemies
/**
* Update method called each game tick by the LK engine.
* Moves the axe based on its velocity and handles rotation.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
graphics.rotation += 0.2; // Add spinning effect
};
return self;
});
/**
* Represents a Wizard Tower ally that shoots magic balls.
*/
var WizardTower = Container.expand(function () {
var self = Container.call(this);
// Create and attach the wizard tower graphic asset
var graphics = self.attachAsset('wizard_tower_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackDamage = 0.5; // Low damage, primary effect is slowdown
self.attackInterval = 120; // Attack every 2 seconds (120 ticks)
self.attackTimer = 0; // Timer for attacks
self.slowDuration = 180; // Duration of slowdown effect in ticks (3 seconds)
self.slowAmount = 0.5; // Multiplier for enemy speed (0.5 means 50% slower)
/**
* Update method called each game tick by the LK engine.
* Handles attacking enemies.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range to shoot at
var closestEnemy = null;
var closestDistance = self.attackRange; // Limit to attack range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, fire a magic ball
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new magic ball instance
var newMagicBall = new MagicBall(angle);
// Position the magic ball at the tower's location
newMagicBall.x = self.x;
newMagicBall.y = self.y;
newMagicBall.lastY = newMagicBall.y; // Initialize lastY for state tracking
newMagicBall.lastX = newMagicBall.x; // Initialize lastX for state tracking
newMagicBall.slowDuration = self.slowDuration; // Pass slow duration to the magic ball
newMagicBall.slowAmount = self.slowAmount; // Pass slow amount to the magic ball
// Add the magic ball to the game scene and the tracking array.
game.addChild(newMagicBall);
magicBalls.push(newMagicBall); // Add to the magicBalls array
// Optional: Add a visual/sound effect for magic ball shot here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents an XBOW (crossbow) ally that rapidly shoots arrows.
*/
var XBOW = Container.expand(function () {
var self = Container.call(this);
// Create and attach the XBOW graphic asset
var graphics = self.attachAsset('xbow_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.fireInterval = 20; // Very rapid fire (every 0.33 seconds)
self.fireTimer = 0;
self.targetingMode = 'closest_to_bastion'; // Default targeting mode
self.smartTargetingEnabled = false; // Flag for smart targeting upgrade
self.tintApplied = false; // Track if tint has been applied
/**
* Update method called each game tick by the LK engine.
* Handles rapid firing logic.
*/
self.update = function () {
self.fireTimer++;
var baseInterval = self.smartTargetingEnabled ? 15.36 : self.fireInterval; // 0.256 seconds when smart targeting enabled
var effectiveFireInterval = Math.max(10, baseInterval * allyAttackSpeedMultiplier);
if (self.fireTimer >= effectiveFireInterval) {
self.fireTimer = 0;
// Find target based on targeting mode
var target = null;
if (self.smartTargetingEnabled) {
// Apply green tint when smart targeting is enabled
if (!self.tintApplied) {
graphics.tint = 0x00FF00; // Green tint
self.tintApplied = true;
}
if (self.targetingMode === 'closest_to_bastion') {
// Find enemy closest to bastion
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var distanceToBastion = BASTION_Y - enemy.y;
if (distanceToBastion > 0 && distanceToBastion < closestDistance) {
closestDistance = distanceToBastion;
target = enemy;
}
}
} else if (self.targetingMode === 'strongest') {
// Find enemy with highest health
var maxHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > maxHealth) {
maxHealth = enemy.health;
target = enemy;
}
}
}
}
// Default behavior: shoot at center of screen
var angle;
if (target) {
// Calculate angle towards target
angle = Math.atan2(target.x - self.x, -(target.y - self.y));
} else {
// No target or smart targeting disabled - shoot straight up towards center
angle = 0; // 0 radians = straight up
}
// Create arrow
var newArrow = new Arrow(angle);
newArrow.x = self.x;
newArrow.y = self.y;
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// XBOW arrows are faster
newArrow.speed *= 1.5;
newArrow.vx = Math.sin(angle) * newArrow.speed;
newArrow.vy = -Math.cos(angle) * newArrow.speed;
// Disable aimbot for XBOW arrows
newArrow.targetEnemy = null;
game.addChild(newArrow);
arrows.push(newArrow);
// Apply multi-shot if enabled
if (multiShotEnabled) {
// Fire a second arrow with a slight angle offset
var angle2 = angle + Math.PI / 12; // Offset by 15 degrees
var newArrow2 = new Arrow(angle2);
newArrow2.x = self.x;
newArrow2.y = self.y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
// XBOW arrows are faster
newArrow2.speed *= 1.5;
newArrow2.vx = Math.sin(angle2) * newArrow2.speed;
newArrow2.vy = -Math.cos(angle2) * newArrow2.speed;
// Disable aimbot for XBOW arrows
newArrow2.targetEnemy = null;
game.addChild(newArrow2);
arrows.push(newArrow2);
// Fire a third arrow with the opposite angle offset
var angle3 = angle - Math.PI / 12; // Offset by -15 degrees
var newArrow3 = new Arrow(angle3);
newArrow3.x = self.x;
newArrow3.y = self.y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
// XBOW arrows are faster
newArrow3.speed *= 1.5;
newArrow3.vx = Math.sin(angle3) * newArrow3.speed;
newArrow3.vy = -Math.cos(angle3) * newArrow3.speed;
// Disable aimbot for XBOW arrows
newArrow3.targetEnemy = null;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
}
};
return self;
});
/****
* Initialize Game
****/
// Create the main game instance with a dark background color.
var game = new LK.Game({
backgroundColor: 0x101030 // Dark blue/purple background
});
/****
* Game Code
****/
// Use actual war elephant asset
// Use actual spearman asset
// Placeholder ID, adjust size slightly, reuse flipX
// Placeholder ID
// Placeholder ID, adjust size slightly
// Constants defining game dimensions and key vertical positions.
//var facekit = LK.import('@upit/facekit.v1');
//Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions
//var storage = LK.import('@upit/storage.v1');
//Storage library which should be used for persistent game data
//We have access to the following plugins. (Note that the variable names used are mandetory for each plugin)
//Only include the plugins you need to create the game.
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var BASTION_Y = GAME_HEIGHT - 250; // Y-coordinate representing the defense line. Enemies crossing this trigger game over.
var FIRING_POS_Y = GAME_HEIGHT - 150; // Y-coordinate from where arrows are fired.
var FIRING_POS_X = GAME_WIDTH / 2; // X-coordinate from where arrows are fired (center).
// Arrays to keep track of active arrows and enemies.
var arrows = [];
var enemies = [];
// Variables for game state and difficulty scaling.
var scoreTxt; // Text2 object for displaying the score.
var ammoTxt; // Text2 object for displaying the current ammo count.
var dragStartX = null; // Starting X coordinate of a touch/mouse drag.
var dragStartY = null; // Starting Y coordinate of a touch/mouse drag.
var enemySpawnInterval = 120; // Initial ticks between enemy spawns (2 seconds at 60 FPS).
var arrowsFired = 0; // Counter for arrows fired since the last cooldown.
var cooldownTimer = 0; // Timer in ticks for the cooldown period.
var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec). Min duration enforced in upgrade.
var baseMaxArrowsBeforeCooldown = 15; // Base max arrows before reload (Quiver upgrade)
var maxArrowsBeforeCooldown = baseMaxArrowsBeforeCooldown; // Max arrows before reload
var ammoRelicTimer = 0; // Timer for ammo relic regeneration
var arrowPierceLevel = 1; // How many enemies an arrow can pierce (Piercing Shots upgrade)
var arrowDamage = 1; // Base damage for arrows (Heat-tipped arrows upgrade)
var enemySpeedMultiplier = 1.0; // Multiplier for enemy speed (Sabotage upgrade) Min multiplier enforced in upgrade.
var minEnemySpawnInterval = 30; // Minimum ticks between enemy spawns (0.5 seconds).
var enemySpawnRateDecrease = 0.08; // Amount to decrease spawn interval each time an enemy spawns.
var currentEnemySpeed = 3; // Initial base speed of enemies.
var maxEnemySpeed = 12; // Maximum base speed enemies can reach.
var enemySpeedIncrease = 0.015; // Amount to increase enemy base speed each time an enemy spawns.
var archerAllies = []; // Array to hold ArcherAlly instances
var swordsmen = []; // Array to hold Swordsman instances
var cannonballs = []; // Array to hold Cannonball instances
var multiShotEnabled = false; // Flag for More Shots upgrade
var poisonShotsEnabled = false; // Flag for Poison Shots upgrade
var cannons = []; // Array to hold Cannon instances
var allyAttackSpeedMultiplier = 1.0; // Multiplier for ally attack speed
var wizardTowers = []; // Array to hold WizardTower instances
var magicBalls = []; // Array to hold MagicBall instances
var aimbotEnabled = false; // Flag for Aimbot upgrade
var greenKillerEnabled = false; // Flag for Green Killer upgrade
var refinedProjectilesEnabled = false; // Flag for Refined Projectiles upgrade
var greenSlowdownEnabled = false; // Flag for Green Slowdown upgrade
var vikingAllies = []; // Array to hold VikingAlly instances
var vikingAxes = []; // Array to hold VikingAxe instances
var darts = []; // Array to hold Dart instances
var angelOfLights = []; // Array to hold AngelOfLight instances
var bouncyBalls = []; // Array to hold BouncyBall instances
var bombs = []; // Array to hold Bomb instances
var bombers = []; // Array to hold Bomber instances
var xbows = []; // Array to hold XBOW instances
var xbowSmartTargetingEnabled = false; // Flag for XBOW Smart Targeting upgrade
var battleHornEnabled = false; // Flag for Battle Horn upgrade
var dragonSlayerCannonsModeEnabled = false; // Flag for Dragon Slayer Cannons upgrade
// --- Relics and Gold System ---
var playerGold = storage.playerGold || 0; // Load gold from storage, default to 0
// Load relics from storage using flat format
var storedRelicsData = storage.playerRelicsData || {};
var playerRelics = {
damage: {
level: storedRelicsData.damage_level || 0,
enabled: storedRelicsData.damage_enabled || false
},
slowdown: {
level: storedRelicsData.slowdown_level || 0,
enabled: storedRelicsData.slowdown_enabled || false
},
green: {
level: storedRelicsData.green_level || 0,
enabled: storedRelicsData.green_enabled || false
},
dark: {
level: storedRelicsData.dark_level || 0,
enabled: storedRelicsData.dark_enabled || false
},
dragon: {
level: storedRelicsData.dragon_level || 0,
enabled: storedRelicsData.dragon_enabled || false
},
reload: {
level: storedRelicsData.reload_level || 0,
enabled: storedRelicsData.reload_enabled || false
},
ammo: {
level: storedRelicsData.ammo_level || 0,
enabled: storedRelicsData.ammo_enabled || false
},
swordsman: {
level: storedRelicsData.swordsman_level || 0,
enabled: storedRelicsData.swordsman_enabled || false
}
};
var RELIC_MAX_LEVEL = 10;
var RELIC_COST_PER_LEVEL = 5; // 5 gold per level
// --- Upgrade System ---
var lastUpgradeScore = -1; // Score at which the last upgrade was offered
var isUpgradePopupActive = false; // Flag indicating if the upgrade choice popup is visible
var upgradePopup = null; // Container for the upgrade UI elements
var currentMusicPlaying = 'Gamemusic'; // Track which music is currently playing
var musicFadeInProgress = false; // Flag to track if a music fade transition is in progress
// Helper function to apply Sabotage effect to existing enemies
function applySabotage() {
for (var i = 0; i < enemies.length; i++) {
// We assume Enemy class will use enemySpeedMultiplier when calculating effective speed
// If Enemy.speed stores base speed, we might need an effectiveSpeed method or update speed directly.
// For simplicity, let's assume Enemy.update uses the global multiplier.
// If direct modification is needed: enemies[i].speed = baseSpeed * enemySpeedMultiplier;
}
// Also update the current base speed calculation baseline if necessary, though modifying the multiplier should suffice.
}
// Helper function placeholder for adding Archer Ally
function addArcherAlly() {
// Implementation requires ArcherAlly class definition
console.log("Archer Ally upgrade chosen!");
var ally = new ArcherAlly();
// Position the ally next to the player's firing position, slightly offset.
ally.x = FIRING_POS_X + 150; // Position to the right of the player
ally.y = FIRING_POS_Y;
game.addChild(ally);
archerAllies.push(ally); // Add to the new archerAllies array
}
// Helper function to add a Cannon ally
function addCannon() {
console.log("Cannon upgrade chosen!");
var newCannon = new Cannon();
// Position the cannon near the bastion line
newCannon.x = GAME_WIDTH / 2 - 300; // Example position to the left of center
newCannon.y = BASTION_Y - 100; // Position above the bastion line
// Apply dragon slayer mode if the global flag is enabled
newCannon.dragonSlayerMode = dragonSlayerCannonsModeEnabled;
game.addChild(newCannon);
cannons.push(newCannon); // Add to the cannons array
}
// Helper function to add a Wizard Tower
function addWizardTower() {
console.log("Wizard Tower upgrade chosen!");
var newTower = new WizardTower();
// Position the wizard tower near the bastion line, offset from center
newTower.x = GAME_WIDTH / 2 + 300; // Example position to the right of center
newTower.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newTower);
wizardTowers.push(newTower); // Add to the wizardTowers array
}
// Helper function to add a Viking Ally
function addVikingAlly() {
console.log("Viking Ally upgrade chosen!");
var newViking = new VikingAlly();
// Position the viking near the bastion line, offset from cannons/towers
newViking.x = GAME_WIDTH / 2; // Center for now
newViking.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newViking);
vikingAllies.push(newViking); // Add to the vikingAllies array
}
// Helper function to add a Dart Ally
function addDartAlly() {
console.log("Dart Ally upgrade chosen!");
var newDartAlly = new DartAlly();
// Position the dart ally near the bastion line, offset
newDartAlly.x = GAME_WIDTH / 2 - 200; // Example position
newDartAlly.y = BASTION_Y - 100; // Position above bastion line
game.addChild(newDartAlly);
darts.push(newDartAlly); // Add to the dartAllies array (need to rename array to dartAllies if creating a separate one)
}
// Helper function placeholder for adding Swordsman
function addSwordsman() {
// Create a swordsman tower at the bastion line
var tower = new Container();
var towerGraphics = tower.attachAsset('swordsmanTower', {
anchorX: 0.5,
anchorY: 0.5
});
tower.x = FIRING_POS_X; // Position in the center horizontally
tower.y = BASTION_Y - 50; // Position just above the bastion line
game.addChild(tower);
// Set up interval to spawn swordsmen from the tower
var spawnInterval = LK.setInterval(function () {
var newSwordsman = new Swordsman();
// Position the swordsman near the tower with a slight random offset
newSwordsman.x = tower.x + (Math.random() * 100 - 50); // Random offset of ±50px
newSwordsman.y = tower.y;
game.addChild(newSwordsman);
swordsmen.push(newSwordsman); // Add to the swordsmen array
}, 3000); // Spawn a swordsman every 3 seconds
// Swordsman spawn does not grant score
// Store the interval reference in the tower to potentially clear it later
tower.spawnInterval = spawnInterval;
}
// Helper function to add an Angel of Light
function addAngelOfLight() {
console.log("Angel of Light upgrade chosen!");
var newAngel = new AngelOfLight();
// Position the angel near the bastion line, offset
newAngel.x = GAME_WIDTH / 2 + 200; // Example position
newAngel.y = BASTION_Y - 150; // Position slightly higher than other allies
game.addChild(newAngel);
angelOfLights.push(newAngel); // Add to the angelOfLights array
}
// Helper function to add a Bouncy Ball
function addBouncyBall() {
console.log("Bouncy Ball upgrade chosen!");
var newBall = new BouncyBall();
// Start from center of screen
newBall.x = GAME_WIDTH / 2;
newBall.y = GAME_HEIGHT / 2;
game.addChild(newBall);
bouncyBalls.push(newBall);
}
// Helper function to add a Bomber
function addBomber() {
console.log("Bomber upgrade chosen!");
var newBomber = new Bomber();
// Position near bastion line
newBomber.x = GAME_WIDTH / 2 - 400;
newBomber.y = BASTION_Y - 100;
game.addChild(newBomber);
bombers.push(newBomber);
}
// Helper function to add an XBOW
function addXBOW() {
console.log("XBOW upgrade chosen!");
var newXBOW = new XBOW();
// Position at the center of the bastion
newXBOW.x = GAME_WIDTH / 2;
newXBOW.y = BASTION_Y - 100;
game.addChild(newXBOW);
xbows.push(newXBOW);
}
// Helper function to enable XBOW Smart Targeting
function enableXBOWSmartTargeting() {
console.log("XBOW Smart Targeting upgrade chosen!");
xbowSmartTargetingEnabled = true;
// Enable smart targeting for all existing XBOWs
for (var i = 0; i < xbows.length; i++) {
xbows[i].smartTargetingEnabled = true;
// Randomly assign targeting mode for variety
xbows[i].targetingMode = Math.random() < 0.5 ? 'closest_to_bastion' : 'strongest';
}
}
// Define all possible upgrades
var allUpgrades = [{
id: 'faster_reload',
name: 'Faster Reload',
description: 'Decrease reload time by 20%',
apply: function apply() {
cooldownDuration = Math.max(60, Math.floor(cooldownDuration * 0.8));
}
},
// Min 1 sec cooldown
{
id: 'piercing_shots',
name: 'Piercing Shots',
description: 'Arrows pierce +1 enemy',
apply: function apply() {
arrowPierceLevel++;
}
}, {
id: 'quiver',
name: 'Quiver',
description: '+5 Arrows before reload',
apply: function apply() {
maxArrowsBeforeCooldown += 5;
}
}, {
id: 'heat_tipped',
name: 'Heat-tipped Arrows',
description: 'Arrows deal +1 damage',
apply: function apply() {
arrowDamage += 1;
console.log("Heat-tipped Arrows upgrade chosen - damage increased to " + arrowDamage);
}
}, {
id: 'archer_ally',
name: 'Archer Ally',
description: 'Gain an allied archer',
apply: addArcherAlly
}, {
id: 'sabotage',
name: 'Sabotage',
description: 'Enemies 10% slower',
apply: function apply() {
enemySpeedMultiplier = Math.max(0.5, enemySpeedMultiplier * 0.9);
applySabotage();
}
},
// Max 50% slow
{
id: 'swordsman',
name: 'Swordsman',
description: 'Gain a melee defender',
apply: addSwordsman
}, {
id: 'more_shots',
name: 'More Shots',
description: 'Shoot 2 arrows at once',
apply: function apply() {
multiShotEnabled = true; // Assuming a global flag for this
}
}, {
id: 'poison_shots',
name: 'Poison Shots',
description: 'Shots deal damage over time',
apply: function apply() {
poisonShotsEnabled = true; // Assuming a global flag for this
}
}, {
id: 'cannon',
name: 'Cannon',
description: 'Gain a cannon that targets the strongest enemy',
apply: addCannon // Assuming a helper function addCannon
}, {
id: 'ally_atk_speed',
name: 'Ally ATK Speed',
description: 'All allies attack faster',
apply: function apply() {
allyAttackSpeedMultiplier *= 0.8; // Assuming a global multiplier
// Apply to existing allies as well in their update logic or a helper function
}
}, {
id: 'wizard_tower',
name: 'Wizard Tower',
description: 'Gain a wizard tower ally that shoots magic balls that slowdown enemies',
apply: addWizardTower // Assuming a helper function addWizardTower
}, {
id: 'aimbot',
name: 'Aimbot',
description: 'Arrows seek out enemies',
apply: function apply() {
aimbotEnabled = true; // Assuming a global flag for this
}
}, {
id: 'green_killer',
name: 'Green Killer',
description: '+50% Damage vs Green Enemies',
apply: function apply() {
greenKillerEnabled = true;
}
}, {
id: 'refined_projectiles',
name: 'Refined Projectiles',
description: 'Non-arrow projectiles +5 damage & faster',
apply: function apply() {
refinedProjectilesEnabled = true;
// Existing projectiles won't update, new ones will have bonus
}
}, {
id: 'viking_ally',
name: 'Viking Ally',
description: 'Gain a viking that throws piercing axes',
apply: addVikingAlly // Assuming helper function addVikingAlly
}, {
id: 'green_slowdown',
name: 'Green Slowdown',
description: 'Projectiles slow Green enemies 10% for 10s (no stack)',
apply: function apply() {
greenSlowdownEnabled = true;
}
}, {
id: 'dart_shooter',
name: 'Dart Shooter',
description: 'Gain an ally that shoots fast darts, effective against green enemies',
apply: addDartAlly // Assuming a helper function addDartAlly
}, {
id: 'angel_of_light',
name: 'Angel of Light',
description: 'Gain an ally that stuns enemies and deals high damage to dark enemies',
apply: addAngelOfLight // Assuming a helper function addAngelOfLight
}, {
id: 'bouncy_ball',
name: 'Bouncy Ball',
description: 'Bounces around dealing massive damage to enemies',
apply: addBouncyBall
}, {
id: 'bomber',
name: 'Bomber',
description: 'Throws bombs that explode for area damage',
apply: addBomber
}, {
id: 'xbow',
name: 'XBOW',
description: 'A big bow that rapidly shoots arrows',
apply: addXBOW
}, {
id: 'xbow_smart_targeting',
name: 'XBOW Smart Targeting',
description: 'XBOWs can target closest to bastion or strongest enemy',
apply: enableXBOWSmartTargeting
}, {
id: 'dragon_slayer_cannons',
name: 'Dragon Slayer Cannons',
description: 'Cannons become rocket launchers that deal 25x damage to dragons and prioritize them',
apply: function apply() {
// Enable dragon slayer mode for all existing cannons
for (var i = 0; i < cannons.length; i++) {
cannons[i].dragonSlayerMode = true;
}
// Set global flag so all future cannons will have dragon slayer mode
dragonSlayerCannonsModeEnabled = true;
}
}, {
id: 'battle_horn',
name: 'Battle Horn',
description: 'More enemies spawn, but they are all swordsmen: more score for more upgrades!',
apply: function apply() {
// Reduce spawn interval for more enemies
enemySpawnInterval = Math.max(15, enemySpawnInterval * 0.5); // 50% faster spawning, min 0.25 seconds
// Set global flag to force swordsman spawns
battleHornEnabled = true;
}
}];
// Function to create and show the upgrade selection popup
function showUpgradePopup() {
isUpgradePopupActive = true;
// Simple pause: Game logic checks isUpgradePopupActive flag in game.update
// Switch to upgrade theme music with fade effect
if (currentMusicPlaying !== 'UpgradeTheme' && !musicFadeInProgress) {
musicFadeInProgress = true;
// Fade out current music
LK.playMusic('Gamemusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// After fade out completes, start upgrade theme with fade in
LK.setTimeout(function () {
LK.playMusic('UpgradeTheme', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
currentMusicPlaying = 'UpgradeTheme';
musicFadeInProgress = false;
}, 850);
}
// --- Create Popup UI ---
upgradePopup = new Container();
upgradePopup.x = GAME_WIDTH / 2;
upgradePopup.y = GAME_HEIGHT / 2;
game.addChild(upgradePopup); // Add to game layer for positioning relative to center
// Add a semi-transparent background overlay
var bg = upgradePopup.attachAsset('bastionLine', {
// Reusing an asset for shape
width: 1200,
height: 800,
color: 0x000000,
// Black background
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Add title text
var title = new Text2('Choose an Upgrade!', {
size: 80,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -300; // Position relative to popup center
upgradePopup.addChild(title);
// --- Select 3 Random Unique Upgrades ---
var availableToOffer = allUpgrades.slice(); // Copy available upgrades
var choices = [];
var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices
// Make dragon slayer cannons more common past score 100
var currentScore = LK.getScore();
if (currentScore > 100 && !dragonSlayerCannonsModeEnabled) {
// Add dragon slayer cannons upgrade multiple times to increase chances
var dragonSlayerUpgrade = null;
for (var ds = 0; ds < availableToOffer.length; ds++) {
if (availableToOffer[ds].id === 'dragon_slayer_cannons') {
dragonSlayerUpgrade = availableToOffer[ds];
break;
}
}
if (dragonSlayerUpgrade) {
// Add it 2 more times to the pool (3x total chance)
availableToOffer.push(dragonSlayerUpgrade);
availableToOffer.push(dragonSlayerUpgrade);
}
}
for (var i = 0; i < numChoices; i++) {
var randomIndex = Math.floor(Math.random() * availableToOffer.length);
choices.push(availableToOffer[randomIndex]);
availableToOffer.splice(randomIndex, 1); // Remove chosen upgrade to ensure uniqueness
}
// --- Create Buttons for Choices ---
var buttonYStart = -150;
var buttonSpacing = 180;
for (var j = 0; j < choices.length; j++) {
var upgrade = choices[j];
var button = createUpgradeButton(upgrade, buttonYStart + j * buttonSpacing);
upgradePopup.addChild(button);
}
}
// Function to create a single upgrade button
function createUpgradeButton(upgradeData, yPos) {
var buttonContainer = new Container();
buttonContainer.y = yPos;
// Button background
var buttonBg = buttonContainer.attachAsset('bastionLine', {
// Reusing asset for shape
width: 800,
height: 150,
color: 0x555555,
anchorX: 0.5,
anchorY: 0.5
});
// Button text (Name + Description)
var nameText = new Text2(upgradeData.name, {
size: 50,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0.5);
nameText.y = -25;
buttonContainer.addChild(nameText);
var descText = new Text2(upgradeData.description, {
size: 35,
fill: 0xCCCCCC
});
descText.anchor.set(0.5, 0.5);
descText.y = 30;
buttonContainer.addChild(descText);
// Make button interactive
buttonContainer.interactive = true; // Needed for down event
// Event handler for button press
buttonContainer.down = function (x, y, obj) {
upgradeData.apply(); // Apply the selected upgrade's effect
hideUpgradePopup(); // Close the popup
};
return buttonContainer;
}
// Function to hide and destroy the upgrade popup
function hideUpgradePopup() {
if (upgradePopup) {
upgradePopup.destroy();
upgradePopup = null;
}
isUpgradePopupActive = false;
// Switch back to game music with fade effect
if (currentMusicPlaying !== 'Gamemusic' && !musicFadeInProgress) {
musicFadeInProgress = true;
// Fade out upgrade theme
LK.playMusic('UpgradeTheme', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// After fade out completes, start game music with fade in
LK.setTimeout(function () {
LK.playMusic('Gamemusic', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
currentMusicPlaying = 'Gamemusic';
musicFadeInProgress = false;
}, 850);
}
// Resume game logic (handled by checking flag in game.update)
}
// --- Upgradepedia ---
var upgradepediaButton;
var upgradepediaPopup = null;
var upgradeListContainer = null;
function showUpgradepedia() {
isUpgradePopupActive = true; // Pause the game while upgradepedia is open
upgradepediaPopup = new Container();
upgradepediaPopup.x = GAME_WIDTH / 2;
upgradepediaPopup.y = GAME_HEIGHT / 2;
game.addChild(upgradepediaPopup);
// Add a semi-transparent background overlay
var bg = upgradepediaPopup.attachAsset('pedia_screen_bg', {
width: 1600,
height: 2000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
});
// Add title text
var title = new Text2('Upgradepedia', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -900;
upgradepediaPopup.addChild(title);
// Add close button
var closeButton = createUpgradepediaCloseButton();
closeButton.x = 750;
closeButton.y = -900;
upgradepediaPopup.addChild(closeButton);
// Container for upgrade details
upgradeListContainer = new Container();
upgradeListContainer.y = -200;
upgradepediaPopup.addChild(upgradeListContainer);
displayUpgradeStats();
}
function hideUpgradepedia() {
if (upgradepediaPopup) {
upgradepediaPopup.destroy();
upgradepediaPopup = null;
}
isUpgradePopupActive = false; // Unpause the game
}
function createUpgradepediaCloseButton() {
var buttonContainer = new Container();
var buttonBg = buttonContainer.attachAsset('pedia_button_bg', {
width: 150,
height: 80,
color: 0xCC0000,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2('X', {
size: 60,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
buttonContainer.addChild(buttonText);
buttonContainer.interactive = true;
buttonContainer.down = function () {
hideUpgradepedia();
};
return buttonContainer;
}
function displayUpgradeStats() {
// Clear any existing content
while (upgradeListContainer.children.length > 0) {
upgradeListContainer.removeChildAt(0);
}
// Current page tracking
var currentUpgrade = 0;
var totalUpgrades = allUpgrades.length;
// Map upgrade IDs to visual assets and additional info
var upgradeAssets = {
'faster_reload': {
asset: 'Reload',
scale: 0.4
},
'piercing_shots': {
asset: 'arrow',
scale: 0.6
},
'quiver': {
asset: 'arrow',
scale: 0.8,
tint: 0xFFD700
},
'heat_tipped': {
asset: 'arrow',
scale: 0.6,
tint: 0xFF4500
},
'archer_ally': {
asset: 'Archer',
scale: 0.8
},
'sabotage': {
asset: 'thief',
scale: 0.8
},
'swordsman': {
asset: 'swordsmanAlly',
scale: 0.8
},
'more_shots': {
asset: 'arrow',
scale: 0.5,
count: 3
},
'poison_shots': {
asset: 'arrow',
scale: 0.6,
tint: 0x00FF00
},
'cannon': {
asset: 'cannon_asset',
scale: 0.8
},
'ally_atk_speed': {
asset: 'viking_axe',
scale: 0.6
},
'wizard_tower': {
asset: 'wizard_tower_asset',
scale: 0.8
},
'aimbot': {
asset: 'arrow',
scale: 0.6,
special: 'seeking'
},
'green_killer': {
asset: 'arrow',
scale: 0.6,
tint: 0xFF0000
},
'refined_projectiles': {
asset: 'cannonball',
scale: 0.6,
tint: 0xFFD700
},
'viking_ally': {
asset: 'viking_ally',
scale: 0.8
},
'green_slowdown': {
asset: 'magic_ball_asset',
scale: 0.6,
tint: 0x00FF00
},
'dart_shooter': {
asset: 'dart_asset',
scale: 0.8
},
'angel_of_light': {
asset: 'angel_of_light_asset',
scale: 0.8
},
'bouncy_ball': {
asset: 'cannonball',
scale: 1.0,
tint: 0xFF00FF
},
'bomber': {
asset: 'bomber_asset',
scale: 0.8
},
'xbow': {
asset: 'xbow_asset',
scale: 0.8
},
'xbow_smart_targeting': {
asset: 'xbow_asset',
scale: 0.8,
tint: 0x00FFFF
},
'dragon_slayer_cannons': {
asset: 'cannon_asset',
scale: 0.8,
tint: 0xFF0000
},
'battle_horn': {
asset: 'flag_bearer_asset',
scale: 0.8,
tint: 0xFFD700
}
};
function displayUpgradeDetails(index) {
// Clear existing content
while (upgradeListContainer.children.length > 0) {
upgradeListContainer.removeChildAt(0);
}
var upgradeInfo = allUpgrades[index];
var assetInfo = upgradeAssets[upgradeInfo.id] || {
asset: 'arrow',
scale: 0.6
};
// Upgrade display container
var upgradeDisplay = new Container();
upgradeDisplay.y = 0;
upgradeListContainer.addChild(upgradeDisplay);
// Add upgrade graphic
if (assetInfo.count) {
// Special case for multiple projectiles
for (var i = 0; i < assetInfo.count; i++) {
var graphic = upgradeDisplay.attachAsset(assetInfo.asset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: assetInfo.scale,
scaleY: assetInfo.scale,
x: -400 + (i - 1) * 100,
y: 50
});
if (assetInfo.tint) {
graphic.tint = assetInfo.tint;
}
}
} else {
var upgradeGraphic = upgradeDisplay.attachAsset(assetInfo.asset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: assetInfo.scale,
scaleY: assetInfo.scale,
x: -400,
y: 50
});
if (assetInfo.tint) {
upgradeGraphic.tint = assetInfo.tint;
}
// Special visual effects
if (assetInfo.special === 'seeking') {
// Add rotation animation hint for seeking arrows
upgradeGraphic.rotation = Math.PI / 6;
}
}
// Add upgrade name
var nameText = new Text2(upgradeInfo.name, {
size: 70,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0);
nameText.x = 0;
nameText.y = -400;
upgradeDisplay.addChild(nameText);
// Add description
var descText = new Text2(upgradeInfo.description, {
size: 45,
fill: 0xCCCCCC,
wordWrap: true,
wordWrapWidth: 1200
});
descText.anchor.set(0.5, 0);
descText.x = 0;
descText.y = -300;
upgradeDisplay.addChild(descText);
// Add detailed ability description
var abilityText = getDetailedAbility(upgradeInfo.id);
var abilityDisplay = new Text2('Ability: ' + abilityText, {
size: 40,
fill: 0x88FF88,
wordWrap: true,
wordWrapWidth: 1200
});
abilityDisplay.anchor.set(0.5, 0);
abilityDisplay.x = 0;
abilityDisplay.y = -150;
upgradeDisplay.addChild(abilityDisplay);
// Status display
var statusText = new Text2('Upgrade ' + (index + 1) + ' of ' + totalUpgrades, {
size: 35,
fill: 0xAAAAAA
});
statusText.anchor.set(0.5, 0);
statusText.x = 0;
statusText.y = 250;
upgradeDisplay.addChild(statusText);
// Navigation buttons
var navButtons = new Container();
navButtons.y = 350;
upgradeDisplay.addChild(navButtons);
// Previous button
var prevButton = createUpgradeNavButton('← Previous', -250, function () {
currentUpgrade = (currentUpgrade - 1 + totalUpgrades) % totalUpgrades;
displayUpgradeDetails(currentUpgrade);
});
navButtons.addChild(prevButton);
// Next button
var nextButton = createUpgradeNavButton('Next →', 250, function () {
currentUpgrade = (currentUpgrade + 1) % totalUpgrades;
displayUpgradeDetails(currentUpgrade);
});
navButtons.addChild(nextButton);
}
function getDetailedAbility(upgradeId) {
var abilities = {
'faster_reload': 'Reduces the cooldown time between reloads by 20%. Stacks multiplicatively with minimum 1 second cooldown.',
'piercing_shots': 'Each arrow can pierce through one additional enemy. Stacks to pierce multiple enemies.',
'quiver': 'Increases maximum ammo capacity by 5 arrows before needing to reload.',
'heat_tipped': 'Increases arrow damage by 1. Affects all arrow-based attacks including allies.',
'archer_ally': 'Spawns an allied archer that independently targets and shoots at enemies every 3 seconds.',
'sabotage': 'Reduces all enemy movement speed by 10%. Stacks multiplicatively with minimum 50% speed.',
'swordsman': 'Creates a tower that spawns temporary swordsmen allies every 3 seconds. Swordsmen chase and attack nearby enemies.',
'more_shots': 'Fire 3 arrows at once in a spread pattern. Works with all arrow upgrades.',
'poison_shots': 'Arrows apply poison stacks that deal damage over time. Each stack deals damage every 0.5 seconds.',
'cannon': 'Deploys a cannon that targets the strongest enemy and fires high-damage cannonballs every 5 seconds.',
'ally_atk_speed': 'All allies attack 20% faster. Stacks multiplicatively.',
'wizard_tower': 'Builds a tower that shoots magic balls causing slowdown effect on hit. Dragons are immune to slowdown.',
'aimbot': 'Arrows and magic balls automatically seek the nearest enemy. Updates target if current target is destroyed.',
'green_killer': 'Deal 50% extra damage to all Green-tagged enemies (Wizards, Spearmen, War Elephants, Shamans).',
'refined_projectiles': 'Non-arrow projectiles (cannonballs, magic balls, axes) gain +5 damage and +5 speed.',
'viking_ally': 'Summons a viking warrior that throws piercing axes. Each axe can pierce through 3 enemies.',
'green_slowdown': 'Projectiles slow Green-tagged enemies by 10% for 10 seconds. Effect does not stack.',
'dart_shooter': 'Deploys a rapid-fire dart shooter that deals double damage to Green enemies.',
'angel_of_light': 'Summons an angel that stuns enemies for 5 seconds and deals triple damage to Black-tagged enemies. Dragons cannot be stunned.',
'bouncy_ball': 'Releases a bouncing projectile that ricochets off walls dealing massive damage. Lasts 10 seconds or 20 bounces.',
'bomber': 'Deploys a bomber that throws area damage bombs. Cannot directly target Dragons but explosion can damage them.',
'xbow': 'Installs a large crossbow that rapidly fires arrows every 0.33 seconds. Arrows travel 50% faster.',
'xbow_smart_targeting': 'XBOWs can now target either the enemy closest to bastion or the strongest enemy. All other allies have this by default.',
'dragon_slayer_cannons': 'Transforms all current and future cannons into rocket launchers. Rockets deal 25x damage to Dragon-type enemies and prioritize targeting them over all other enemy types.',
'battle_horn': 'Doubles enemy spawn rate but forces all spawned enemies to be basic swordsmen. Great for farming score since swordsmen are easy to defeat!'
};
return abilities[upgradeId] || 'Special ability that enhances your defenses.';
}
function createUpgradeNavButton(label, xPos, callback) {
var button = new Container();
button.x = xPos;
var buttonBg = button.attachAsset('pedia_nav_button_bg', {
width: 250,
height: 80,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2(label, {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.interactive = true;
button.down = callback;
return button;
}
// Initial display
displayUpgradeDetails(currentUpgrade);
}
// Create upgradepedia button
upgradepediaButton = new Container();
upgradepediaButton.x = 200; // Position on the left side
upgradepediaButton.y = GAME_HEIGHT - 200;
// Add button background
var upgradeBtnBg = upgradepediaButton.attachAsset('pedia_button_bg', {
width: 300,
height: 100,
anchorX: 0.5,
anchorY: 0.5
});
// Add button text
var upgradeBtnText = new Text2('Upgradepedia', {
size: 40,
fill: 0xFFFFFF
});
upgradeBtnText.anchor.set(0.5, 0.5);
upgradepediaButton.addChild(upgradeBtnText);
// Make button interactive
upgradepediaButton.interactive = true;
upgradepediaButton.down = function () {
showUpgradepedia();
};
// Add the button to the game scene
game.addChild(upgradepediaButton);
// --- Relic Shop Button ---
var relicShopButton = new Container();
relicShopButton.x = GAME_WIDTH - 200; // Position on the right side
relicShopButton.y = GAME_HEIGHT - 320; // Position above Enemypedia button
var relicBtnBg = relicShopButton.attachAsset('pedia_button_bg', {
width: 300,
height: 100,
anchorX: 0.5,
anchorY: 0.5
});
var relicBtnText = new Text2('Relic Shop', {
size: 40,
fill: 0xFFFFFF
});
relicBtnText.anchor.set(0.5, 0.5);
relicShopButton.addChild(relicBtnText);
relicShopButton.interactive = true;
relicShopButton.down = function () {
showRelicShop();
};
game.addChild(relicShopButton);
// --- Visual Setup ---
var bastionLine = game.addChild(LK.getAsset('bastionLine', {
anchorX: 0.0,
// Anchor at the left edge.
anchorY: 0.5,
// Anchor vertically centered.
x: 0,
// Position at the left edge of the screen.
y: BASTION_Y // Position at the defined bastion Y-coordinate.
}));
// Create and configure the score display text.
scoreTxt = new Text2('0', {
size: 150,
// Font size.
fill: 0xFFFFFF // White color.
});
scoreTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge.
// Add score text to the GUI layer at the top-center position.
LK.gui.top.addChild(scoreTxt);
scoreTxt.y = 30; // Add padding below the top edge. Ensure it's clear of the top-left menu icon area.
// Create and configure the ammo display text.
ammoTxt = new Text2('Ammo: ' + maxArrowsBeforeCooldown, {
size: 80,
fill: 0xFFFFFF // White color.
});
ammoTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge.
LK.gui.top.addChild(ammoTxt);
ammoTxt.y = 180; // Position below the score text
// --- Enemypedia ---
var enemypediaButton;
var enemypediaPopup = null;
var enemyListContainer = null; // Container for enemy list items
function showEnemypedia() {
isUpgradePopupActive = true; // Pause the game while enemypedia is open
enemypediaPopup = new Container();
enemypediaPopup.x = GAME_WIDTH / 2;
enemypediaPopup.y = GAME_HEIGHT / 2;
game.addChild(enemypediaPopup);
// Add a semi-transparent background overlay
var bg = enemypediaPopup.attachAsset('bastionLine', {
width: 1600,
height: 1200,
// Increased height for better layout
color: 0x000000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Add title text
var title = new Text2('Enemypedia', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -500; // Position relative to popup center
enemypediaPopup.addChild(title);
// Add a close button
var closeButton = createCloseButton();
closeButton.x = 750; // Position relative to popup center
closeButton.y = -500;
enemypediaPopup.addChild(closeButton);
// Container for the enemy details
enemyListContainer = new Container();
enemyListContainer.y = -100; // Centered position for detailed view
enemypediaPopup.addChild(enemyListContainer);
displayEnemyStats();
}
function hideEnemypedia() {
if (enemypediaPopup) {
enemypediaPopup.destroy();
enemypediaPopup = null;
}
isUpgradePopupActive = false; // Unpause the game
}
function createCloseButton() {
var buttonContainer = new Container();
var buttonBg = buttonContainer.attachAsset('bastionLine', {
width: 150,
height: 80,
color: 0xCC0000,
// Red color for close
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2('X', {
size: 60,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
buttonContainer.addChild(buttonText);
buttonContainer.interactive = true;
buttonContainer.down = function () {
hideEnemypedia();
};
return buttonContainer;
}
function displayEnemyStats() {
// Dummy data representing enemy types and their stats
var enemyData = [{
type: 'swordsman',
assetId: 'enemy',
description: 'Basic melee unit that has very low HP.',
baseHealth: 1,
baseSpeed: 3,
dodgeChance: 0
}, {
type: 'knight',
assetId: 'knight',
description: 'A threat for the first few waves cause of its high health, but is slow.',
baseHealth: 5,
baseSpeed: 3 * 0.9,
dodgeChance: 0
}, {
type: 'thief',
assetId: 'thief',
description: 'Fast, low health unit with a dodge chance; likes gravy.',
baseHealth: 1,
baseSpeed: 3 * 1.2,
dodgeChance: 0.1
}, {
type: 'boss',
assetId: 'boss',
description: 'a boss that spawns randomly when youre in trobule.',
baseHealth: 5,
// Base health before scaling
baseSpeed: 3 * 0.7,
dodgeChance: 0
}, {
type: 'shield',
assetId: 'shield_enemy',
description: 'Slow unit that has high HP',
baseHealth: 20,
baseSpeed: 3 * 0.5,
dodgeChance: 0
}, {
type: 'wizard',
assetId: 'wizard_enemy',
description: 'kinda tricky cause it teleports and has moderate speed.',
baseHealth: 2,
baseSpeed: 3 * 0.7,
dodgeChance: 0
}, {
type: 'spearman',
assetId: 'spearman',
description: 'Standard, annoying green unit that spawns from Elephants most of the time.',
baseHealth: 2,
baseSpeed: 3 * 1.0,
dodgeChance: 0
}, {
type: 'war_elephant',
assetId: 'war_elephant',
description: 'Extremely high health elephant, spawns spearmen on death',
baseHealth: 500,
baseSpeed: 3 * 0.3,
dodgeChance: 0
}, {
type: 'elite_knight',
assetId: 'elite_knight_asset',
description: 'High health knight variant that is loyal to the king',
baseHealth: 45,
baseSpeed: 3 * 0.85,
dodgeChance: 0
}, {
type: 'elite_shield',
assetId: 'elite_shield_asset',
description: 'Even higher health shield variant that is loyal to the king',
baseHealth: 200,
baseSpeed: 3 * 0.4,
dodgeChance: 0
}, {
type: 'shaman',
assetId: 'shaman_enemy',
description: 'Reduces your ammo when he feels like it',
baseHealth: 2,
baseSpeed: 3 * 0.8,
dodgeChance: 0
}, {
type: 'hot_air_balloon',
assetId: 'hot_air_balloon_asset',
description: 'Very low health. Spawns random non-green enemies on death.',
baseHealth: 1,
baseSpeed: 3 * 0.2,
dodgeChance: 0
}, {
type: 'dark_bowman',
assetId: 'dark_bowman_asset',
description: 'Immune to arrows and cannonballs. Worst enemy of blue archers',
baseHealth: 5,
baseSpeed: 3 * 1.1,
dodgeChance: 0
}, {
type: 'jester',
assetId: 'jester_asset',
description: 'High chance to dodge or reflect non-cannonball projectiles...YOU WILL LAUGH AT ALL OF HIS TRICKS',
baseHealth: 2,
baseSpeed: 3 * 1.1,
dodgeChance: 0.3,
// 30% dodge
reflectChance: 0.2 // 20% reflect chance for non-cannonballs
}, {
type: 'dark_war_elephant',
assetId: 'dark_war_elephant_asset',
description: 'Immune to arrows. variant of green elephant, spawns Dark Bowmen on death.',
baseHealth: 450,
// Slightly less than green elephant
baseSpeed: 3 * 0.3,
// Same slow speed
dodgeChance: 0
}, {
type: 'dark_spearman',
assetId: 'dark_spearman_asset',
description: 'A faster and more resistant spearman variant that came straight from rome. Immune to arrows and cannonballs',
baseHealth: 15,
// Base health
baseSpeed: 3 * 1.2,
// Faster than spearman
dodgeChance: 0,
tag: 'Black' // Immune to arrows and cannonballs
}, {
type: 'dragon',
assetId: 'dragon_asset',
description: 'A fearsome dragon with great HP and a high chance to dodge attacks. Cant be slowed down',
baseHealth: 250,
// Great HP
baseSpeed: 3 * 0.9,
// Moderately fast
dodgeChance: 0.40,
// High dodge chance (40%)
tag: 'Dragon' // Special tag if needed for other mechanics, for now dodge is key
}, {
type: 'flag_bearer',
assetId: 'flag_bearer_asset',
description: 'Green-tagged enemy with an aura that makes nearby enemies move 50% faster',
baseHealth: 5,
baseSpeed: 3 * 0.8,
dodgeChance: 0,
tag: 'Green'
}, {
type: 'baby_dragon',
assetId: 'baby_dragon_asset',
description: 'Small dragon with less health but enters rage mode (double speed) when no other dragons are present',
baseHealth: 50,
baseSpeed: 3 * 1.1,
dodgeChance: 0.25,
tag: 'Dragon'
}];
// Clear any existing content in the list container
while (enemyListContainer.children.length > 0) {
enemyListContainer.removeChildAt(0);
}
// Current page tracking
var currentEnemy = 0;
var totalEnemies = enemyData.length;
// Create enemy details display
function displayEnemyDetails(index) {
// Clear any existing content in the list container
while (enemyListContainer.children.length > 0) {
enemyListContainer.removeChildAt(0);
}
var enemyInfo = enemyData[index];
// Enemy display container
var enemyDisplay = new Container();
enemyDisplay.y = 0;
enemyListContainer.addChild(enemyDisplay);
// Add enemy graphic (larger for individual view)
var enemyGraphic = enemyDisplay.attachAsset(enemyInfo.assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8,
x: -400,
y: 50
});
// Add enemy name (larger font)
var nameText = new Text2(enemyInfo.type.replace(/_/g, ' ').toUpperCase(), {
size: 60,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0);
nameText.x = 0;
nameText.y = -300;
enemyDisplay.addChild(nameText);
// Add description
var descText = new Text2('Description: ' + enemyInfo.description, {
size: 40,
fill: 0xCCCCCC,
wordWrap: true,
wordWrapWidth: 1000
});
descText.anchor.set(0, 0);
descText.x = -100;
descText.y = -200;
enemyDisplay.addChild(descText);
// Add stats with improved layout
var healthText = new Text2('Health: ' + enemyInfo.baseHealth, {
size: 40,
fill: 0x88FF88
});
healthText.anchor.set(0, 0);
healthText.x = -100;
healthText.y = -100;
enemyDisplay.addChild(healthText);
var speedText = new Text2('Speed: ' + enemyInfo.baseSpeed.toFixed(1), {
size: 40,
fill: 0x88CCFF
});
speedText.anchor.set(0, 0);
speedText.x = -100;
speedText.y = -40;
enemyDisplay.addChild(speedText);
var dodgeText = new Text2('Dodge Chance: ' + (enemyInfo.dodgeChance * 100).toFixed(0) + '%', {
size: 40,
fill: 0xFFCC88
});
dodgeText.anchor.set(0, 0);
dodgeText.x = -100;
dodgeText.y = 20;
enemyDisplay.addChild(dodgeText);
// Add reflect chance
var reflectText = new Text2('Reflect Chance: ' + (enemyInfo.reflectChance * 100 || 0).toFixed(0) + '%', {
size: 40,
fill: 0xFFCCEE
});
reflectText.anchor.set(0, 0);
reflectText.x = -100;
reflectText.y = 80;
enemyDisplay.addChild(reflectText);
// Add unlock score
var unlockScore = 0; // Default for swordsman
if (enemyInfo.type === 'spearman') {
unlockScore = 10;
} else if (enemyInfo.type === 'thief') {
unlockScore = 15;
} else if (enemyInfo.type === 'knight') {
unlockScore = 23;
} else if (enemyInfo.type === 'wizard') {
unlockScore = 25;
} else if (enemyInfo.type === 'shield') {
unlockScore = 30;
} else if (enemyInfo.type === 'shaman') {
unlockScore = 35;
} else if (enemyInfo.type === 'dark_spearman') {
unlockScore = 55;
} // Dark Spearman unlock score
else if (enemyInfo.type === 'dark_bowman') {
unlockScore = 55;
} // Note: Same as Dark Spearman, might need adjustment if only one is desired at 55. Keeping as per request.
else if (enemyInfo.type === 'jester') {
unlockScore = 69;
} else if (enemyInfo.type === 'dragon') {
unlockScore = 134;
} // Dragon unlock score
else if (enemyInfo.type === 'flag_bearer') {
unlockScore = 27;
} // Flag Bearer unlock score
else if (enemyInfo.type === 'baby_dragon') {
unlockScore = 125;
} // Baby Dragon unlock score (same as war elephant)
else if (enemyInfo.type === 'elite_knight') {
unlockScore = 100;
} else if (enemyInfo.type === 'elite_shield') {
unlockScore = 112;
} else if (enemyInfo.type === 'war_elephant') {
unlockScore = 125;
} else if (enemyInfo.type === 'dark_war_elephant') {
unlockScore = 145;
} // Dark war elephant starts appearing at score 145
else if (enemyInfo.type === 'hot_air_balloon') {
unlockScore = 154;
}
var unlockText = new Text2('Unlocks at Score: ' + unlockScore, {
size: 40,
fill: 0xFF88FF
});
unlockText.anchor.set(0, 0);
unlockText.x = -100;
unlockText.y = 140; // Adjusted Y position
enemyDisplay.addChild(unlockText);
// Status display (current enemy / total)
var statusText = new Text2('Enemy ' + (index + 1) + ' of ' + totalEnemies, {
size: 30,
fill: 0xAAAAAA
});
statusText.anchor.set(0.5, 0);
statusText.x = 0;
statusText.y = 150;
enemyDisplay.addChild(statusText);
// Navigation buttons container
var navButtons = new Container();
navButtons.y = 250;
enemyDisplay.addChild(navButtons);
// Previous button
var prevButton = createNavButton('← Previous', -250, function () {
currentEnemy = (currentEnemy - 1 + totalEnemies) % totalEnemies;
displayEnemyDetails(currentEnemy);
});
navButtons.addChild(prevButton);
// Next button
var nextButton = createNavButton('Next →', 250, function () {
currentEnemy = (currentEnemy + 1) % totalEnemies;
displayEnemyDetails(currentEnemy);
});
navButtons.addChild(nextButton);
}
// Create navigation button
function createNavButton(label, xPos, callback) {
var button = new Container();
button.x = xPos;
var buttonBg = button.attachAsset('bastionLine', {
width: 250,
height: 80,
color: 0x555555,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2(label, {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.interactive = true;
button.down = callback;
return button;
}
// Initial display
displayEnemyDetails(currentEnemy);
}
enemypediaButton = new Container();
// Position the button (e.g., bottom right of GUI)
enemypediaButton.x = GAME_WIDTH - 200; // Example X
enemypediaButton.y = GAME_HEIGHT - 200; // Example Y
// Add button background
var buttonBg = enemypediaButton.attachAsset('bastionLine', {
// Reusing asset for shape
width: 300,
height: 100,
color: 0x444444,
// Grey color
anchorX: 0.5,
anchorY: 0.5
});
// Add button text
var buttonText = new Text2('Enemypedia', {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
enemypediaButton.addChild(buttonText);
// Make button interactive
enemypediaButton.interactive = true;
enemypediaButton.down = function () {
showEnemypedia();
};
// Add the button to the game scene (or GUI if needed)
game.addChild(enemypediaButton);
// --- Relic Shop ---
var relicShopPopup = null;
var relicListContainer = null;
var goldText; // Text display for gold
function showRelicShop() {
isUpgradePopupActive = true; // Pause the game
relicShopPopup = new Container();
relicShopPopup.x = GAME_WIDTH / 2;
relicShopPopup.y = GAME_HEIGHT / 2;
game.addChild(relicShopPopup);
var bg = relicShopPopup.attachAsset('pedia_screen_bg', {
width: 1600,
height: 2000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
});
var title = new Text2('Relic Shop', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -900;
relicShopPopup.addChild(title);
var closeButton = createRelicShopCloseButton();
closeButton.x = 750;
closeButton.y = -900;
relicShopPopup.addChild(closeButton);
// Gold display
goldText = new Text2('Gold: ' + playerGold, {
size: 50,
fill: 0xFFD700
});
goldText.anchor.set(0.5, 0.5);
goldText.y = -800;
relicShopPopup.addChild(goldText);
relicListContainer = new Container();
relicListContainer.y = -200;
relicShopPopup.addChild(relicListContainer);
displayRelicDetails();
}
function hideRelicShop() {
if (relicShopPopup) {
relicShopPopup.destroy();
relicShopPopup = null;
}
isUpgradePopupActive = false; // Unpause the game
}
function createRelicShopCloseButton() {
var buttonContainer = new Container();
var buttonBg = buttonContainer.attachAsset('pedia_button_bg', {
width: 150,
height: 80,
color: 0xCC0000,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2('X', {
size: 60,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
buttonContainer.addChild(buttonText);
buttonContainer.interactive = true;
buttonContainer.down = function () {
hideRelicShop();
};
return buttonContainer;
}
function displayRelicDetails() {
while (relicListContainer.children.length > 0) {
relicListContainer.removeChildAt(0);
}
var relicsData = [{
id: 'damage',
name: 'Damage Relic',
description: 'Increase projectile, swordsman, and poison damage.',
effect: '+1 damage per level'
}, {
id: 'slowdown',
name: 'Slowdown Relic',
description: 'All enemies are slower.',
effect: '+2% slowdown per level'
}, {
id: 'green',
name: 'Green Relic',
description: 'Green enemies are slower and take more damage.',
effect: '+2% slow & +2% damage per level'
}, {
id: 'dark',
name: 'Dark Relic',
description: 'Dark enemies can be hit by arrows/cannonballs and take more damage from them.',
effect: '+5% damage per level'
}, {
id: 'dragon',
name: 'Dragon Relic',
description: 'Projectiles stun dragons.',
effect: '+0.05 seconds stun per level'
}, {
id: 'reload',
name: 'Reload Relic',
description: 'Reload faster! Essential for rapid-fire gameplay.',
effect: '-0.5 seconds reload time per level'
}, {
id: 'ammo',
name: 'Ammo Relic',
description: 'Regenerate ammo over time without reloading.',
effect: '+1 ammo regenerated every 5 seconds per level'
}, {
id: 'swordsman',
name: 'Swordsman Relic',
description: 'Start each battle with a permanent swordsman ally.',
effect: 'Unlocks permanent swordsman, +2 damage per upgrade'
}];
// Current page tracking
var currentRelic = 0;
var totalRelics = relicsData.length;
// Create relic details display
function displayRelicItem(index) {
// Clear any existing content in the list container
while (relicListContainer.children.length > 0) {
relicListContainer.removeChildAt(0);
}
var relic = relicsData[index];
var relicInfo = playerRelics[relic.id];
// Relic display container
var relicDisplay = new Container();
relicDisplay.y = 0;
relicListContainer.addChild(relicDisplay);
// Background
var bg = relicDisplay.attachAsset('pedia_button_bg', {
width: 1400,
height: 400,
color: 0x333355,
anchorX: 0.5,
anchorY: 0.5
});
// Relic Name
var nameText = new Text2(relic.name + ' (Level ' + relicInfo.level + '/' + RELIC_MAX_LEVEL + ')', {
size: 70,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0);
nameText.x = 0;
nameText.y = -300;
relicDisplay.addChild(nameText);
// Relic Description
var descText = new Text2(relic.description, {
size: 45,
fill: 0xCCCCCC,
wordWrap: true,
wordWrapWidth: 1200
});
descText.anchor.set(0.5, 0);
descText.x = 0;
descText.y = -200;
relicDisplay.addChild(descText);
// Relic Effect
var effectText = new Text2('Effect: ' + relic.effect, {
size: 40,
fill: 0x88FF88,
wordWrap: true,
wordWrapWidth: 1200
});
effectText.anchor.set(0.5, 0);
effectText.x = 0;
effectText.y = -100;
relicDisplay.addChild(effectText);
// Buttons container
var buttonsContainer = new Container();
buttonsContainer.y = 0;
relicDisplay.addChild(buttonsContainer);
// Buy/Upgrade Button
var buyButton = new Container();
buyButton.x = -200;
var buyBtnBgColor = relicInfo.level < RELIC_MAX_LEVEL ? 0x00CC00 : 0x666666;
var buyBtnTextColor = relicInfo.level < RELIC_MAX_LEVEL ? 0xFFFFFF : 0xAAAAAA;
var buyBtnBg = buyButton.attachAsset('pedia_nav_button_bg', {
width: 200,
height: 80,
color: buyBtnBgColor,
anchorX: 0.5,
anchorY: 0.5
});
var buyBtnText = new Text2(relicInfo.level < RELIC_MAX_LEVEL ? 'Buy (5)' : 'MAX', {
size: 40,
fill: buyBtnTextColor
});
buyBtnText.anchor.set(0.5, 0.5);
buyButton.addChild(buyBtnText);
buyButton.interactive = relicInfo.level < RELIC_MAX_LEVEL && playerGold >= RELIC_COST_PER_LEVEL;
if (buyButton.interactive) {
buyButton.down = function (relicId) {
return function () {
if (playerGold >= RELIC_COST_PER_LEVEL && playerRelics[relicId].level < RELIC_MAX_LEVEL) {
playerGold -= RELIC_COST_PER_LEVEL;
playerRelics[relicId].level++;
storage.playerGold = playerGold;
// Reset ammo relic timer if ammo relic was upgraded
if (relicId === 'ammo') {
ammoRelicTimer = 0; // Reset timer when upgrading
}
// Save relics to storage using safe serialization
try {
var relicsData = {};
relicsData.damage_level = playerRelics.damage.level;
relicsData.damage_enabled = playerRelics.damage.enabled;
relicsData.slowdown_level = playerRelics.slowdown.level;
relicsData.slowdown_enabled = playerRelics.slowdown.enabled;
relicsData.green_level = playerRelics.green.level;
relicsData.green_enabled = playerRelics.green.enabled;
relicsData.dark_level = playerRelics.dark.level;
relicsData.dark_enabled = playerRelics.dark.enabled;
relicsData.dragon_level = playerRelics.dragon.level;
relicsData.dragon_enabled = playerRelics.dragon.enabled;
relicsData.reload_level = playerRelics.reload.level;
relicsData.reload_enabled = playerRelics.reload.enabled;
relicsData.ammo_level = playerRelics.ammo.level;
relicsData.ammo_enabled = playerRelics.ammo.enabled;
relicsData.swordsman_level = playerRelics.swordsman.level;
relicsData.swordsman_enabled = playerRelics.swordsman.enabled;
storage.playerRelicsData = relicsData;
} catch (e) {
console.log("Error saving relics:", e);
}
goldText.setText('Gold: ' + playerGold);
displayRelicItem(currentRelic); // Refresh the current relic display
}
};
}(relic.id);
}
buttonsContainer.addChild(buyButton);
// On/Off Button
var toggleButton = new Container();
toggleButton.x = 200;
var toggleBtnBgColor = relicInfo.enabled ? 0x008800 : 0x880000;
var toggleBtnBg = toggleButton.attachAsset('pedia_nav_button_bg', {
width: 150,
height: 80,
color: toggleBtnBgColor,
anchorX: 0.5,
anchorY: 0.5
});
var toggleBtnText = new Text2(relicInfo.enabled ? 'ON' : 'OFF', {
size: 40,
fill: 0xFFFFFF
});
toggleBtnText.anchor.set(0.5, 0.5);
toggleButton.addChild(toggleBtnText);
toggleButton.interactive = relicInfo.level > 0; // Only interactive if relic is owned
if (toggleButton.interactive) {
toggleButton.down = function (relicId) {
return function () {
playerRelics[relicId].enabled = !playerRelics[relicId].enabled;
// Reset ammo relic timer if ammo relic was toggled
if (relicId === 'ammo') {
ammoRelicTimer = 0; // Reset timer when toggling
}
// Save relics to storage using safe serialization
try {
var relicsData = {};
relicsData.damage_level = playerRelics.damage.level;
relicsData.damage_enabled = playerRelics.damage.enabled;
relicsData.slowdown_level = playerRelics.slowdown.level;
relicsData.slowdown_enabled = playerRelics.slowdown.enabled;
relicsData.green_level = playerRelics.green.level;
relicsData.green_enabled = playerRelics.green.enabled;
relicsData.dark_level = playerRelics.dark.level;
relicsData.dark_enabled = playerRelics.dark.enabled;
relicsData.dragon_level = playerRelics.dragon.level;
relicsData.dragon_enabled = playerRelics.dragon.enabled;
relicsData.reload_level = playerRelics.reload.level;
relicsData.reload_enabled = playerRelics.reload.enabled;
relicsData.ammo_level = playerRelics.ammo.level;
relicsData.ammo_enabled = playerRelics.ammo.enabled;
relicsData.swordsman_level = playerRelics.swordsman.level;
relicsData.swordsman_enabled = playerRelics.swordsman.enabled;
storage.playerRelicsData = relicsData;
} catch (e) {
console.log("Error saving relics:", e);
}
displayRelicItem(currentRelic); // Refresh the current relic display
};
}(relic.id);
} else {
toggleBtnBg.tint = 0x666666; // Grey out if not owned
}
buttonsContainer.addChild(toggleButton);
// Status display (current relic / total)
var statusText = new Text2('Relic ' + (index + 1) + ' of ' + totalRelics, {
size: 35,
fill: 0xAAAAAA
});
statusText.anchor.set(0.5, 0);
statusText.x = 0;
statusText.y = 150;
relicDisplay.addChild(statusText);
// Navigation buttons container
var navButtons = new Container();
navButtons.y = 250;
relicDisplay.addChild(navButtons);
// Previous button
var prevButton = createRelicNavButton('← Previous', -250, function () {
currentRelic = (currentRelic - 1 + totalRelics) % totalRelics;
displayRelicItem(currentRelic);
});
navButtons.addChild(prevButton);
// Next button
var nextButton = createRelicNavButton('Next →', 250, function () {
currentRelic = (currentRelic + 1) % totalRelics;
displayRelicItem(currentRelic);
});
navButtons.addChild(nextButton);
}
function createRelicNavButton(label, xPos, callback) {
var button = new Container();
button.x = xPos;
var buttonBg = button.attachAsset('pedia_nav_button_bg', {
width: 250,
height: 80,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2(label, {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.interactive = true;
button.down = callback;
return button;
}
// Initial display
displayRelicItem(currentRelic);
}
// --- Input Event Handlers ---
// Handles touch/mouse press down event on the game area.
game.down = function (x, y, obj) {
// Record the starting position of the drag in game coordinates.
var gamePos = game.toLocal({
x: x,
y: y
});
dragStartX = gamePos.x;
dragStartY = gamePos.y;
// Potential enhancement: Show an aiming indicator originating from FIRING_POS_X, FIRING_POS_Y towards the drag point.
};
// Handles touch/mouse move event while dragging on the game area.
game.move = function (x, y, obj) {
// Only process if a drag is currently active.
if (dragStartX !== null) {
var gamePos = game.toLocal({
x: x,
y: y
});
// Potential enhancement: Update the aiming indicator based on the current drag position (gamePos).
}
};
// Handles touch/mouse release event on the game area.
game.up = function (x, y, obj) {
// Only process if a drag was active.
if (dragStartX !== null && cooldownTimer <= 0) {
// Only allow firing if not on cooldown.
var gamePos = game.toLocal({
x: x,
y: y
});
var dragEndX = gamePos.x;
var dragEndY = gamePos.y;
// Calculate the difference between the firing position and the release point to determine direction.
// A longer drag could potentially mean more power, but we'll keep it simple: direction only.
var dx = dragEndX - FIRING_POS_X;
var dy = dragEndY - FIRING_POS_Y;
// Avoid division by zero or zero vector if start and end points are the same.
if (dx === 0 && dy === 0) {
// Optionally handle this case, e.g., fire straight up or do nothing.
// Let's fire straight up if drag distance is negligible.
dy = -1;
}
// Calculate the angle using atan2. Note the order (dx, dy) and the negation of dy
// because the Y-axis is inverted in screen coordinates (positive Y is down).
var angle = Math.atan2(dx, -dy);
// Create a new arrow instance with the calculated angle.
var newArrow = new Arrow(angle);
newArrow.x = FIRING_POS_X;
newArrow.y = FIRING_POS_Y;
// Initialize last position for state tracking (e.g., off-screen detection)
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// Add the arrow to the game scene and the tracking array.
game.addChild(newArrow);
arrows.push(newArrow);
if (multiShotEnabled) {
// Fire a second arrow with a slight angle offset
var angle2 = angle + Math.PI / 12; // Offset by 15 degrees
var newArrow2 = new Arrow(angle2);
newArrow2.x = FIRING_POS_X;
newArrow2.y = FIRING_POS_Y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
game.addChild(newArrow2);
arrows.push(newArrow2);
// Fire a third arrow with the opposite angle offset
var angle3 = angle - Math.PI / 12; // Offset by -15 degrees
var newArrow3 = new Arrow(angle3);
newArrow3.x = FIRING_POS_X;
newArrow3.y = FIRING_POS_Y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
LK.getSound('shoot').play(); // Play shooting sound.
arrowsFired++; // Increment arrow count.
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display.
// Check if cooldown needs to start based on Quiver upgrade
if (arrowsFired >= maxArrowsBeforeCooldown) {
var effectiveCooldownDuration = cooldownDuration;
// Apply reload relic bonus if enabled
if (playerRelics.reload.enabled) {
effectiveCooldownDuration = Math.max(30, cooldownDuration - playerRelics.reload.level * 30); // 0.5 seconds faster per level, min 0.5s
}
cooldownTimer = effectiveCooldownDuration; // Start cooldown (duration affected by Faster Reload upgrade and reload relic)
arrowsFired = 0; // Reset arrow count.
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display.
LK.getSound('Reload').play(); // Play reload sound.
}
// Reset drag state.
dragStartX = null;
dragStartY = null;
// Potential enhancement: Hide the aiming indicator.
}
};
// --- Main Game Update Loop ---
// This function is called automatically by the LK engine on every frame (tick).
game.update = function () {
// --- Upgrade System Check ---
var currentScore = LK.getScore();
// Check if score reached a multiple of 10 and is higher than the last upgrade score
if (!isUpgradePopupActive && currentScore > 0 && currentScore % 10 === 0 && currentScore !== lastUpgradeScore) {
lastUpgradeScore = currentScore; // Mark this score level as having triggered an upgrade offer
showUpgradePopup(); // Show the upgrade selection screen
}
// --- Pause Game Logic if Upgrade Popup is Active ---
if (isUpgradePopupActive) {
// Potential: Update UI animations if any
return; // Skip the rest of the game update loop
}
// --- Resume Game Logic ---
if (isUpgradePopupActive) {
return; // Skip enemy movement and spawning if upgrade popup is active
}
// Handle ammo relic regeneration
if (playerRelics.ammo.enabled && playerRelics.ammo.level > 0) {
ammoRelicTimer++;
// Give ammo every 5 seconds (300 ticks)
if (ammoRelicTimer >= 300) {
ammoRelicTimer = 0;
// Only regenerate if not at max ammo and not reloading
if (arrowsFired > 0 && cooldownTimer <= 0) {
// Give ammo based on relic level (1 per level)
var ammoToRegenerate = playerRelics.ammo.level;
arrowsFired = Math.max(0, arrowsFired - ammoToRegenerate);
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
}
}
}
// Decrease cooldown timer if active.
if (cooldownTimer > 0) {
cooldownTimer--;
ammoTxt.setText('Reloading...'); // Show reloading status
} else if (ammoTxt.text !== 'Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)) {
// Ensure ammo display is correct if not reloading
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
}
// 1. Update and check Arrows
for (var i = arrows.length - 1; i >= 0; i--) {
var arrow = arrows[i];
// Note: LK engine calls arrow.update() automatically as it's added to the game stage.
// Initialize last position if it hasn't been set yet (first frame).
if (arrow.lastY === undefined) {
arrow.lastY = arrow.y;
}
if (arrow.lastX === undefined) {
arrow.lastX = arrow.x;
}
// Check for collisions between the current arrow and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (arrow.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Calculate damage, applying Green Killer bonus if applicable
var damageToDeal = arrow.damage;
if (greenKillerEnabled && enemy.tag === 'Green') {
damageToDeal *= 1.5; // Apply 50% damage bonus
}
// Apply damage to the enemy and check if it was defeated
var enemyDefeated = enemy.takeDamage(damageToDeal, arrow); // Pass the arrow object as source
// Check if the arrow was reflected (takeDamage returns false and arrow is destroyed)
if (!enemyDefeated && !arrow.parent) {
// Arrow was reflected, remove it from the array and stop processing
arrows.splice(i, 1);
hitEnemy = true; // Mark that this arrow is done
break; // Stop checking this arrow against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Handle arrow piercing
arrow.pierceLeft--; // Decrease remaining pierces
if (arrow.pierceLeft <= 0) {
// Arrow has no pierces left, destroy it
arrow.destroy();
arrows.splice(i, 1); // Remove arrow from array.
hitEnemy = true; // Mark that this arrow is done
break; // Stop checking this arrow against other enemies as it's destroyed
} else {
// Arrow pierced this enemy and can continue
// We don't set hitEnemy = true here because the arrow continues
// We don't break because it might hit another enemy in the same frame further along its path
// Apply Green Slowdown if applicable and arrow is still piercing
if (!enemyDefeated && greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
}
}
}
// If the arrow hit an enemy, it was destroyed, so skip to the next arrow.
if (hitEnemy) {
continue;
}
// Check if the arrow has gone off-screen (top, left, or right).
// Use transition detection: check if it was on screen last frame and is off screen now.
var wasOnScreen = arrow.lastY > -arrow.height / 2 && arrow.lastX > -arrow.width / 2 && arrow.lastX < GAME_WIDTH + arrow.width / 2;
var isOffScreen = arrow.y < -arrow.height / 2 ||
// Off top edge
arrow.x < -arrow.width / 2 ||
// Off left edge
arrow.x > GAME_WIDTH + arrow.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Arrow is off-screen, destroy it and remove from the array.
arrow.destroy();
arrows.splice(i, 1);
} else if (!isOffScreen) {
// Update last known position only if the arrow is still potentially on screen
arrow.lastY = arrow.y;
arrow.lastX = arrow.x;
}
}
// 2. Update and check Cannonballs
for (var cb = cannonballs.length - 1; cb >= 0; cb--) {
var cannonball = cannonballs[cb];
// Note: LK engine calls cannonball.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (cannonball.lastY === undefined) {
cannonball.lastY = cannonball.y;
}
if (cannonball.lastX === undefined) {
cannonball.lastX = cannonball.x;
}
// Check for collisions between the current cannonball and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (cannonball.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply damage to the enemy and check if it was defeated
var damageToApply = cannonball.damage;
// Apply dragon slayer damage multiplier if applicable
if (cannonball.dragonDamageMultiplier && enemy.tag === 'Dragon') {
damageToApply *= cannonball.dragonDamageMultiplier;
}
var enemyDefeated = enemy.takeDamage(damageToApply, cannonball); // Pass cannonball as source
// Check if the cannonball had no effect (e.g. Dark War Elephant immunity) and is still in game
if (!enemyDefeated && cannonball.parent) {
// Cannonball had no effect, but wasn't destroyed by a normal hit.
// It might have been immune. If so, we still destroy the cannonball.
cannonball.destroy();
cannonballs.splice(cb, 1);
hitEnemy = true; // Mark that this cannonball is done
break; // Stop checking this cannonball against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Cannonballs are destroyed on hit
cannonball.destroy();
// Apply Green Slowdown if applicable before destroying cannonball
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
cannonball.destroy();
cannonballs.splice(cb, 1); // Remove cannonball from array.
hitEnemy = true; // Mark that this cannonball is done
break; // Stop checking this cannonball against other enemies as it's destroyed
}
}
// If the cannonball hit an enemy, it was destroyed, so skip to the next cannonball.
if (hitEnemy) {
continue;
}
// Check if the cannonball has gone off-screen.
var wasOnScreen = cannonball.lastY > -cannonball.height / 2 && cannonball.lastX > -cannonball.width / 2 && cannonball.lastX < GAME_WIDTH + cannonball.width / 2;
var isOffScreen = cannonball.y < -cannonball.height / 2 ||
// Off top edge
cannonball.x < -cannonball.width / 2 ||
// Off left edge
cannonball.x > GAME_WIDTH + cannonball.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Cannonball is off-screen, destroy it and remove from the array.
cannonball.destroy();
cannonballs.splice(cb, 1);
} else if (!isOffScreen) {
// Update last known position only if the cannonball is still potentially on screen
cannonball.lastY = cannonball.y;
cannonball.lastX = cannonball.x;
}
}
// 3. Update and check Enemies
for (var k = enemies.length - 1; k >= 0; k--) {
var enemy = enemies[k];
// Note: LK engine calls enemy.update() automatically.
// Initialize last position if not set.
if (enemy.lastY === undefined) {
enemy.lastY = enemy.y;
}
// Check if the enemy has reached or passed the bastion line.
// Use transition detection: was the enemy's bottom edge above the line last frame,
// and is it on or below the line now?
var enemyBottomY = enemy.y + enemy.height / 2;
var enemyLastBottomY = enemy.lastY + enemy.height / 2;
var wasAboveBastion = enemyLastBottomY < BASTION_Y;
var isAtOrBelowBastion = enemyBottomY >= BASTION_Y;
if (wasAboveBastion && isAtOrBelowBastion) {
// Enemy reached the bastion! Game Over.
LK.getSound('gameOverSfx').play(); // Play game over sound effect.
LK.showGameOver(); // Trigger the engine's game over sequence.
// Calculate gold earned
var goldEarned = Math.floor(currentScore / 5);
playerGold += goldEarned;
storage.playerGold = playerGold; // Save gold to storage
// Save relics to storage using safe serialization
try {
var relicsData = {};
relicsData.damage_level = playerRelics.damage.level;
relicsData.damage_enabled = playerRelics.damage.enabled;
relicsData.slowdown_level = playerRelics.slowdown.level;
relicsData.slowdown_enabled = playerRelics.slowdown.enabled;
relicsData.green_level = playerRelics.green.level;
relicsData.green_enabled = playerRelics.green.enabled;
relicsData.dark_level = playerRelics.dark.level;
relicsData.dark_enabled = playerRelics.dark.enabled;
relicsData.dragon_level = playerRelics.dragon.level;
relicsData.dragon_enabled = playerRelics.dragon.enabled;
relicsData.reload_level = playerRelics.reload.level;
relicsData.reload_enabled = playerRelics.reload.enabled;
relicsData.ammo_level = playerRelics.ammo.level;
relicsData.ammo_enabled = playerRelics.ammo.enabled;
relicsData.swordsman_level = playerRelics.swordsman.level;
relicsData.swordsman_enabled = playerRelics.swordsman.enabled;
storage.playerRelicsData = relicsData;
} catch (e) {
console.log("Error saving relics:", e);
}
console.log("Game Over! Earned " + goldEarned + " gold. Total gold: " + playerGold);
// LK.showGameOver handles game state reset, no need to manually clear arrays here.
return; // Exit the update loop immediately as the game is over.
} else {
// Update last known position if game is not over for this enemy.
enemy.lastY = enemy.y;
}
}
// 3. Update and check Swordsmen (allies)
for (var l = swordsmen.length - 1; l >= 0; l--) {
var swordsman = swordsmen[l];
// Swordsman update method handles its own lifetime and attacking
// Check if the swordsman has been destroyed by its lifetime timer
if (!swordsman.parent) {
// If it no longer has a parent, it has been destroyed
swordsmen.splice(l, 1); // Remove swordsman from array
}
}
// 4. Update and check Cannons (allies)
for (var m = cannons.length - 1; m >= 0; m--) {
var cannon = cannons[m];
// Cannons do not have a lifetime timer or destruction logic in this basic version
// If they had, we'd check !cannon.parent and splice here as in Swordsmen
}
// 5. Update and check Wizard Towers (allies)
for (var wt = wizardTowers.length - 1; wt >= 0; wt--) {
var tower = wizardTowers[wt];
// Wizard towers do not have a lifetime timer or destruction logic in this basic version
// If they had, we'd check !tower.parent and splice here
}
// 5.5 Update and check Viking Allies
for (var va = vikingAllies.length - 1; va >= 0; va--) {
var viking = vikingAllies[va];
// Vikings do not have a lifetime timer or destruction logic in this version
// LK engine calls viking.update() automatically.
}
// 5.6 Update and check Angel of Lights (allies)
for (var angelIdx = angelOfLights.length - 1; angelIdx >= 0; angelIdx--) {
var angel = angelOfLights[angelIdx];
// Angels do not have a lifetime timer or destruction logic in this version
// LK engine calls angel.update() automatically.
}
// 5.7 Update and check Bouncy Balls
for (var bbIdx = bouncyBalls.length - 1; bbIdx >= 0; bbIdx--) {
var ball = bouncyBalls[bbIdx];
// LK engine calls ball.update() automatically.
// Check for collisions with enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (ball.intersects(enemy)) {
// Apply damage
var enemyDefeated = enemy.takeDamage(ball.damage, ball);
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
enemy.destroy();
enemies.splice(j, 1);
}
// Bouncy ball doesn't get destroyed on hit
LK.effects.flashObject(enemy, 0xFF00FF, 300);
}
}
// Check if ball was destroyed by lifetime
if (!ball.parent) {
bouncyBalls.splice(bbIdx, 1);
}
}
// 5.8 Update and check Bombs
for (var bombIdx = bombs.length - 1; bombIdx >= 0; bombIdx--) {
var bomb = bombs[bombIdx];
// LK engine calls bomb.update() automatically.
// Check if bomb was destroyed after explosion
if (!bomb.parent) {
bombs.splice(bombIdx, 1);
}
}
// 5.9 Update and check Bombers
for (var bomberIdx = bombers.length - 1; bomberIdx >= 0; bomberIdx--) {
var bomber = bombers[bomberIdx];
// LK engine calls bomber.update() automatically.
}
// 5.10 Update and check XBOWs
for (var xbowIdx = xbows.length - 1; xbowIdx >= 0; xbowIdx--) {
var xbow = xbows[xbowIdx];
// LK engine calls xbow.update() automatically.
}
// 6. Update and check Magic Balls
for (var mb = magicBalls.length - 1; mb >= 0; mb--) {
var magicBall = magicBalls[mb];
// Note: LK engine calls magicBall.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (magicBall.lastY === undefined) {
magicBall.lastY = magicBall.y;
}
if (magicBall.lastX === undefined) {
magicBall.lastX = magicBall.x;
}
// Check for collisions between the current magic ball and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (magicBall.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply damage (minimal) and slowdown effect to the enemy and check if it was defeated
var enemyDefeated = enemy.takeDamage(magicBall.damage, magicBall); // Pass the magic ball object as source
// Check if the magic ball was reflected (takeDamage returns false and magicBall is destroyed)
if (!enemyDefeated && !magicBall.parent) {
// Magic ball was reflected, remove it from the array and stop processing
magicBalls.splice(mb, 1);
hitEnemy = true; // Mark that this magic ball is done
break; // Stop checking this magic ball against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Magic balls are destroyed on hit
magicBall.destroy();
// Apply Green Slowdown if applicable before destroying magic ball
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
// Note: Enemy.takeDamage already checks for MagicBall source to apply its default slow.
// This adds the Green Slowdown effect *on top* if applicable and not already slowed.
// However, takeDamage applies slow based on MagicBall properties. Let's apply Green Slowdown here explicitly.
enemy.slowTimer = 10 * 60; // 10 seconds
enemy.currentSlowAmount = 0.9; // 10% slow
}
magicBall.destroy();
magicBalls.splice(mb, 1); // Remove magic ball from array.
hitEnemy = true; // Mark that this magic ball is done
break; // Stop checking this magic ball against other enemies as it's destroyed
}
}
// If the magic ball hit an enemy, it was destroyed, so skip to the next magic ball.
if (hitEnemy) {
continue;
}
// Check if the magic ball has gone off-screen.
var wasOnScreen = magicBall.lastY > -magicBall.height / 2 && magicBall.lastX > -magicBall.width / 2 && magicBall.lastX < GAME_WIDTH + magicBall.width / 2;
var isOffScreen = magicBall.y < -magicBall.height / 2 ||
// Off top edge
magicBall.x < -magicBall.width / 2 ||
// Off left edge
magicBall.x > GAME_WIDTH + magicBall.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Magic ball is off-screen, destroy it and remove from the array.
magicBall.destroy();
magicBalls.splice(mb, 1);
} else if (!isOffScreen) {
// Update last known position only if the magic ball is still potentially on screen
magicBall.lastY = magicBall.y;
magicBall.lastX = magicBall.x;
}
}
// 6.5 Update and check Viking Axes
for (var axeIdx = vikingAxes.length - 1; axeIdx >= 0; axeIdx--) {
var axe = vikingAxes[axeIdx];
// Note: LK engine calls axe.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (axe.lastY === undefined) {
axe.lastY = axe.y;
}
if (axe.lastX === undefined) {
axe.lastX = axe.x;
}
// Check for collisions between the current axe and all enemies.
var hitEnemyAxe = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (axe.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply Green Killer bonus if applicable (Vikings benefit too)
var damageToDealAxe = axe.damage;
if (greenKillerEnabled && enemy.tag === 'Green') {
damageToDealAxe *= 1.5; // Apply 50% damage bonus
}
// Apply damage and check if defeated
var enemyDefeated = enemy.takeDamage(damageToDealAxe, axe); // Pass axe as source for potential effects
// Check if the axe was reflected (takeDamage returns false and axe is destroyed)
if (!enemyDefeated && !axe.parent) {
// Axe was reflected, remove it from the array and stop processing
vikingAxes.splice(axeIdx, 1);
hitEnemyAxe = true; // Mark that this axe is done
break; // Stop checking this axe against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Handle axe piercing
axe.pierceLeft--; // Decrease remaining pierces
if (axe.pierceLeft <= 0) {
// Axe has no pierces left, destroy it
axe.destroy();
vikingAxes.splice(axeIdx, 1); // Remove axe from array.
hitEnemyAxe = true; // Mark that this axe is done
break; // Stop checking this axe against other enemies
} else {
// Axe pierced this enemy and can continue
// Potentially apply Green Slowdown if enabled
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
}
}
}
// If the axe hit and was destroyed, skip to the next axe.
if (hitEnemyAxe) {
continue;
}
// Check if the axe has gone off-screen.
var wasOnScreenAxe = axe.lastY > -axe.height / 2 && axe.lastX > -axe.width / 2 && axe.lastX < GAME_WIDTH + axe.width / 2;
var isOffScreenAxe = axe.y < -axe.height / 2 || axe.y > GAME_HEIGHT + axe.height / 2 || axe.x < -axe.width / 2 || axe.x > GAME_WIDTH + axe.width / 2;
if (wasOnScreenAxe && isOffScreenAxe) {
axe.destroy();
vikingAxes.splice(axeIdx, 1);
} else if (!isOffScreenAxe) {
axe.lastY = axe.y;
axe.lastX = axe.x;
}
}
// 6.6 Update and check Darts
for (var dartIdx = darts.length - 1; dartIdx >= 0; dartIdx--) {
var dart = darts[dartIdx];
// Note: LK engine calls dart.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (dart.lastY === undefined) {
dart.lastY = dart.y;
}
if (dart.lastX === undefined) {
dart.lastX = dart.x;
}
// Check for collisions between the current dart and all enemies.
var hitEnemyDart = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (dart.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Calculate damage, applying bonus against Green enemies
var damageToDealDart = dart.damage;
var greenDamageMultiplier = 2; // Base multiplier for darts vs Green enemies
// Apply Green Relic damage bonus if enabled and enemy is Green-tagged
if (playerRelics.green.enabled && enemy.tag === 'Green') {
greenDamageMultiplier += playerRelics.green.level * 0.02; // Additional damage multiplier from relic
}
if (enemy.tag === 'Green') {
damageToDealDart *= greenDamageMultiplier; // Apply calculated multiplier
}
// Apply damage and check if defeated
var enemyDefeated = enemy.takeDamage(damageToDealDart, dart); // Pass dart as source
// Check if the dart was reflected (takeDamage returns false and dart is destroyed)
if (!enemyDefeated && !dart.parent) {
// Dart was reflected, remove it from the array and stop processing
darts.splice(dartIdx, 1);
hitEnemyDart = true; // Mark that this dart is done
break; // Stop checking this dart against other enemies
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Darts are destroyed on hit
dart.destroy();
// Apply Green Slowdown if applicable before destroying dart
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
darts.splice(dartIdx, 1); // Remove dart from array.
hitEnemyDart = true; // Mark that this dart is done
break; // Stop checking this dart against other enemies
}
}
// If the dart hit and was destroyed, skip to the next dart.
if (hitEnemyDart) {
continue;
}
// Check if the dart has gone off-screen.
var wasOnScreenDart = dart.lastY > -dart.height / 2 && dart.lastX > -dart.width / 2 && dart.lastX < GAME_WIDTH + dart.width / 2;
var isOffScreenDart = dart.y < -dart.height / 2 || dart.y > GAME_HEIGHT + dart.height / 2 || dart.x < -dart.width / 2 || dart.x > GAME_WIDTH + dart.width / 2;
if (wasOnScreenDart && isOffScreenDart) {
dart.destroy();
darts.splice(dartIdx, 1);
} else if (!isOffScreenDart) {
dart.lastY = dart.y;
dart.lastX = dart.x;
}
}
// 7. Spawn new Enemies periodically
// Use LK.ticks and the spawn interval. Ensure interval doesn't go below minimum.
if (LK.ticks % Math.max(minEnemySpawnInterval, Math.floor(enemySpawnInterval)) === 0) {
var currentScore = LK.getScore();
var baseSpeedForSpawn = Math.min(maxEnemySpeed, currentEnemySpeed); // Base speed adjusted by game difficulty progression
// Determine if this spawn *could* be a boss (e.g., every 7th spawn after score 10)
var potentialBoss = (enemies.length + 1) % 7 === 0 && currentScore >= 10;
var typeToSpawn = 'swordsman'; // Default type
var enemyHealth = 1; // Default health
var enemyDodgeChance = 0; // Default dodge chance
var enemySpeed = baseSpeedForSpawn; // Start with base speed
// Determine base type based on score thresholds and randomness
var possibleTypes = ['swordsman'];
if (currentScore >= 10) {
possibleTypes.push('spearman');
}
if (currentScore >= 15) {
possibleTypes.push('thief');
}
if (currentScore >= 23) {
possibleTypes.push('knight');
}
if (currentScore >= 25) {
possibleTypes.push('wizard');
} // Wizards start appearing at score 25
if (currentScore >= 30) {
possibleTypes.push('shield');
} // Shields start appearing at score 30
if (currentScore >= 35) {
possibleTypes.push('shaman'); // Shaman appears at score 35
console.log("Shaman added to possible enemy types");
}
if (currentScore >= 55) {
possibleTypes.push('dark_bowman'); // Dark bowman appears at score 55
possibleTypes.push('dark_spearman'); // Dark Spearman appears at score 55
}
if (currentScore >= 69) {
possibleTypes.push('jester'); // Jester appears at score 69
}
if (currentScore >= 100) {
possibleTypes.push('elite_knight'); // Elite Knights start appearing at score 100
}
if (currentScore >= 134) {
possibleTypes.push('dragon'); // Dragon appears at score 134
}
if (currentScore >= 112) {
possibleTypes.push('elite_shield'); // Elite Shields appear at score 112
}
if (currentScore >= 125) {
// War Elephant appears at score 125
possibleTypes.push('war_elephant');
// Baby Dragon also appears at score 125 (same as war elephant)
possibleTypes.push('baby_dragon');
}
if (currentScore >= 145) {
possibleTypes.push('dark_war_elephant'); // Dark War Elephant appears at score 145
}
if (currentScore >= 154) {
// Hot Air Balloon appears at score 100
possibleTypes.push('hot_air_balloon');
}
if (currentScore >= 27) {
possibleTypes.push('flag_bearer'); // Flag Bearer appears at score 27
}
// Check if Battle Horn is active - if so, make half of spawns swordsmen, rest random
if (battleHornEnabled) {
// 50% chance to force swordsman, 50% chance to pick random
if (Math.random() < 0.5) {
typeToSpawn = 'swordsman';
} else {
var chosenTypeIndex = Math.floor(Math.random() * possibleTypes.length);
typeToSpawn = possibleTypes[chosenTypeIndex];
}
} else {
// Randomly select a type from the available pool for this score level
var chosenTypeIndex = Math.floor(Math.random() * possibleTypes.length);
typeToSpawn = possibleTypes[chosenTypeIndex];
}
// Set stats based on chosen type and apply score-based scaling
if (typeToSpawn === 'knight') {
var baseKnightHP = 5;
var hpIncreaseIntervalKnight = 30; // Health increases every 30 score points after appearing
var hpIncreasesKnight = Math.floor(Math.max(0, currentScore - 23) / hpIncreaseIntervalKnight);
enemyHealth = baseKnightHP + hpIncreasesKnight * 5;
enemySpeed *= 0.9; // Knights are slightly slower than base swordsman speed
} else if (typeToSpawn === 'thief') {
enemyHealth = 1; // Thieves are fragile
enemyDodgeChance = 0.10; // 10% dodge chance
var speedIncreaseIntervalThief = 25; // Speed increases every 25 score points after appearing
var speedIncreasePercentThief = 0.05; // 5% speed increase each time
var speedIncreasesThief = Math.floor(Math.max(0, currentScore - 15) / speedIncreaseIntervalThief);
var thiefSpeedMultiplier = Math.pow(1 + speedIncreasePercentThief, speedIncreasesThief);
enemySpeed *= 1.2 * thiefSpeedMultiplier; // Thieves are faster base + scaling speed
} else if (typeToSpawn === 'shield') {
var baseShieldHP = 20;
var hpIncreaseIntervalShield = 35; // Gains HP every 35 score points after appearing
var hpIncreasesShield = Math.floor(Math.max(0, currentScore - 30) / hpIncreaseIntervalShield);
enemyHealth = baseShieldHP + hpIncreasesShield * 20;
enemySpeed *= 0.5; // Shield enemies are very slow
enemyDodgeChance = 0;
} else if (typeToSpawn === 'shaman') {
var baseShamanHP = 2;
var statIncreaseIntervalShaman = 10; // Gains stats every 10 score points after appearing
var statIncreasesShaman = Math.floor(Math.max(0, currentScore - 35) / statIncreaseIntervalShaman);
enemyHealth = baseShamanHP + statIncreasesShaman * 1; // Gains 1 HP per interval
enemySpeed *= 0.8 * Math.pow(1.04, statIncreasesShaman); // Base speed, gains 4% speed per interval
enemyDodgeChance = 0; // Shaman cannot dodge
} else if (typeToSpawn === 'wizard') {
var baseWizardHP = 2;
var statIncreaseIntervalWizard = 10; // Gains stats every 10 score points after appearing
var statIncreasesWizard = Math.floor(Math.max(0, currentScore - 25) / statIncreaseIntervalWizard);
enemyHealth = baseWizardHP + statIncreasesWizard * 1; // Gains 1 HP per interval
enemySpeed *= 0.7 * Math.pow(1.05, statIncreasesWizard); // Slow base, gains 5% speed per interval
enemyDodgeChance = 0;
} else if (typeToSpawn === 'elite_knight') {
var baseEliteKnightHP = 45;
var hpIncreaseIntervalElite = 30; // Gains HP every 30 score points after appearing
var hpIncreasesElite = Math.floor(Math.max(0, currentScore - 100) / hpIncreaseIntervalElite);
enemyHealth = baseEliteKnightHP + hpIncreasesElite * 15;
enemySpeed *= 0.85; // Slightly slower than base knight speed
enemyDodgeChance = 0;
} else if (typeToSpawn === 'elite_shield') {
var baseEliteShieldHP = 200; // Starts with 200 HP
var hpIncreaseIntervalEliteShield = 20; // Gains HP every 20 score points after appearing
var hpIncreasesEliteShield = Math.floor(Math.max(0, currentScore - 112) / hpIncreaseIntervalEliteShield);
enemyHealth = baseEliteShieldHP + hpIncreasesEliteShield * 50; // Gains 50 HP per interval
enemySpeed *= 0.4; // Elite Shield enemies are slower than regular shield enemies
enemyDodgeChance = 0;
} else if (typeToSpawn === 'spearman') {
var baseSpearmanHP = 2;
var hpIncreaseIntervalSpearman = 10; // Gains HP every 10 score points after appearing
var hpIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / hpIncreaseIntervalSpearman);
enemyHealth = baseSpearmanHP + hpIncreasesSpearman * 3; // Gains 3 HP per interval
var speedIncreaseIntervalSpearman = 10; // Gains speed every 10 score points after appearing
var speedIncreasePercentSpearman = 0.05; // 5% speed increase each time
var speedIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / speedIncreaseIntervalSpearman);
var spearmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentSpearman, speedIncreasesSpearman);
enemySpeed *= 1.0 * spearmanSpeedMultiplier; // Base speed + scaling speed
enemyDodgeChance = 0;
} else if (typeToSpawn === 'war_elephant') {
var baseElephantHP = 500;
var hpIncreaseIntervalElephant = 15; // Gains HP every 15 score points after appearing
var hpIncreasesElephant = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalElephant);
enemyHealth = baseElephantHP + hpIncreasesElephant * 100; // Gains 100 HP per interval
enemySpeed *= 0.3; // War Elephants are very slow
enemyDodgeChance = 0;
// Note: War elephant death spawns spearmen - this will be handled in the Enemy death logic.
} else if (typeToSpawn === 'hot_air_balloon') {
enemyHealth = 1; // Always has 1 HP
enemySpeed *= 0.2; // Very slow movement
enemyDodgeChance = 0;
// Note: Hot air balloon death spawns 5 random non-green enemies - handled in Enemy death logic
} else if (typeToSpawn === 'dark_bowman') {
var baseDarkBowmanHP = 5;
var statIncreaseIntervalBowman = 10; // Gains stats every 10 score points after appearing
var statIncreasesBowman = Math.floor(Math.max(0, currentScore - 55) / statIncreaseIntervalBowman);
enemyHealth = baseDarkBowmanHP + statIncreasesBowman * 3; // Gains 3 HP per interval
var speedIncreasePercentBowman = 0.05; // 5% speed increase each time
var bowmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentBowman, statIncreasesBowman);
enemySpeed *= 1.1 * bowmanSpeedMultiplier; // Slightly faster than base speed + scaling
enemyDodgeChance = 0;
// Note: Dark bowman has Black tag making it immune to arrows
} else if (typeToSpawn === 'jester') {
var baseJesterHP = 2;
enemyHealth = baseJesterHP; // Jester HP doesn't scale with score
var baseJesterSpeed = baseSpeedForSpawn * 1.1; // Jester starts faster
var speedIncreaseIntervalJester = 12; // Gains speed every 12 score points after appearing
var speedIncreasePercentJester = 0.03; // 3% speed increase each time
var speedIncreasesJester = Math.floor(Math.max(0, currentScore - 69) / speedIncreaseIntervalJester);
enemySpeed = baseJesterSpeed * Math.pow(1 + speedIncreasePercentJester, speedIncreasesJester);
enemyDodgeChance = 0.30; // 30% dodge chance
// The reflectChance is set in the Enemy class constructor based on type, no need to set here.
} else if (typeToSpawn === 'dark_war_elephant') {
var baseDarkElephantHP = 450;
var hpIncreaseIntervalDarkElephant = 10; // Gains HP every 10 score points after appearing
var hpIncreasesDarkElephant = Math.floor(Math.max(0, currentScore - 120) / hpIncreaseIntervalDarkElephant);
enemyHealth = baseDarkElephantHP + hpIncreasesDarkElephant * 80; // Gains 80 HP per interval
enemySpeed *= 0.3; // Dark War Elephants are very slow
enemyDodgeChance = 0;
// Note: Dark War elephant death spawns dark bowmen - this will be handled in the Enemy death logic.
} else if (typeToSpawn === 'dark_spearman') {
var baseDarkSpearmanHP = 15;
var baseDarkSpearmanSpeedMultiplier = 1.2;
var statIncreaseIntervalDarkSpearman = 10; // Gains stats every 10 score points after appearing
var statIncreasesDarkSpearman = Math.floor(Math.max(0, currentScore - 55) / statIncreaseIntervalDarkSpearman);
enemyHealth = baseDarkSpearmanHP + statIncreasesDarkSpearman * 5; // Gains 5 HP per interval
var darkSpearmanSpeedBonus = Math.pow(1 + 0.03, statIncreasesDarkSpearman); // Gains 3% speed per interval
enemySpeed *= baseDarkSpearmanSpeedMultiplier * darkSpearmanSpeedBonus;
enemyDodgeChance = 0;
// Tag 'Black' is set in Enemy constructor
} else if (typeToSpawn === 'dragon') {
var baseDragonHP = 250;
var hpIncreaseIntervalDragon = 15; // Gains HP every 15 score points after appearing
var hpIncreasesDragon = Math.floor(Math.max(0, currentScore - 100) / hpIncreaseIntervalDragon);
enemyHealth = baseDragonHP + hpIncreasesDragon * 50; // Gains 50 HP per interval
enemySpeed *= 0.9; // Dragons are moderately fast
enemyDodgeChance = 0.40; // 40% dodge chance
// Tag 'Dragon' could be set in Enemy constructor if needed for other mechanics
} else if (typeToSpawn === 'flag_bearer') {
var baseFlagBearerHP = 5;
var hpIncreaseIntervalFlagBearer = 10; // Gains HP every 10 score points after appearing
var hpIncreasesFlagBearer = Math.floor(Math.max(0, currentScore - 27) / hpIncreaseIntervalFlagBearer);
enemyHealth = baseFlagBearerHP + hpIncreasesFlagBearer * 2; // Gains 2 HP per interval
var speedIncreaseIntervalFlagBearer = 10; // Gains speed every 10 score points
var speedIncreasesFlagBearer = Math.floor(Math.max(0, currentScore - 27) / speedIncreaseIntervalFlagBearer);
var flagBearerSpeedMultiplier = Math.pow(1.03, speedIncreasesFlagBearer); // 3% speed increase per interval
enemySpeed *= 0.8 * flagBearerSpeedMultiplier; // Slower base speed but increases over time
enemyDodgeChance = 0;
} else if (typeToSpawn === 'baby_dragon') {
var baseBabyDragonHP = 50;
var hpIncreaseIntervalBabyDragon = 9; // Gains HP every 9 score points after appearing
var hpIncreasesBabyDragon = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalBabyDragon);
enemyHealth = baseBabyDragonHP + hpIncreasesBabyDragon * 10; // Gains 10 HP per interval
var speedIncreaseIntervalBabyDragon = 9; // Gains speed every 9 score points
var speedIncreasesBabyDragon = Math.floor(Math.max(0, currentScore - 125) / speedIncreaseIntervalBabyDragon);
var babyDragonSpeedMultiplier = Math.pow(1.02, speedIncreasesBabyDragon); // 2% speed increase per interval
enemySpeed *= 1.1 * babyDragonSpeedMultiplier; // Faster than regular dragon
enemyDodgeChance = 0.25; // 25% dodge chance
} else {
// Swordsman (default)
enemyHealth = 1;
// Speed remains baseSpeedForSpawn initially
}
// Check if this spawn should be overridden to be a Boss
if (potentialBoss) {
typeToSpawn = 'boss'; // Set type to boss
enemyHealth = 5 + Math.floor(currentScore / 8); // Boss health scales significantly with score
enemySpeed = baseSpeedForSpawn * 0.7; // Bosses are slower but much tougher (reset speed based on base)
enemyDodgeChance = 0; // Bosses typically don't dodge
}
// Apply the global Sabotage speed multiplier AFTER type-specific adjustments
enemySpeed *= enemySpeedMultiplier;
// Create the new enemy instance with the calculated stats
var newEnemy;
if (typeToSpawn === 'flag_bearer') {
newEnemy = new FlagBearer();
newEnemy.speed = enemySpeed;
newEnemy.baseSpeed = enemySpeed;
newEnemy.health = enemyHealth;
newEnemy.maxHealth = enemyHealth;
newEnemy.dodgeChance = enemyDodgeChance;
} else if (typeToSpawn === 'baby_dragon') {
newEnemy = new BabyDragon();
newEnemy.speed = enemySpeed;
newEnemy.baseSpeed = enemySpeed;
newEnemy.health = enemyHealth;
newEnemy.maxHealth = enemyHealth;
newEnemy.dodgeChance = enemyDodgeChance;
} else {
newEnemy = new Enemy(typeToSpawn, enemySpeed, enemyHealth, enemyDodgeChance);
}
// Position the new enemy at the top, random horizontal position with padding
// Use the actual width of the created enemy's graphic for padding calculation
// Need to access width after creation, use a sensible default or estimate if needed before creation
var tempAsset = LK.getAsset(newEnemy.assetId || 'enemy', {}); // Get asset dimensions (might need refinement if assetId isn't on newEnemy yet)
var spawnPadding = (tempAsset ? tempAsset.width / 2 : 100) + 20; // Use default if asset not found easily
newEnemy.x = spawnPadding + Math.random() * (GAME_WIDTH - 2 * spawnPadding);
newEnemy.y = -(tempAsset ? tempAsset.height / 2 : 100); // Start just above the top edge.
// Initialize last position for state tracking (used for bastion collision check).
newEnemy.lastY = newEnemy.y;
// Add the new enemy to the game scene and the tracking array.
game.addChild(newEnemy);
enemies.push(newEnemy);
// Increase difficulty for the next spawn: decrease spawn interval and increase base speed.
// These affect the *next* potential spawn's base calculations.
enemySpawnInterval -= enemySpawnRateDecrease;
currentEnemySpeed += enemySpeedIncrease;
}
};
// --- Swordsman Relic Initial Setup ---
if (playerRelics.swordsman.enabled && playerRelics.swordsman.level > 0) {
// Spawn permanent swordsman for swordsman relic
var permanentSwordsman = new Swordsman();
permanentSwordsman.x = FIRING_POS_X + 100; // Position near player
permanentSwordsman.y = FIRING_POS_Y - 50;
permanentSwordsman.lifetime = Infinity; // Permanent swordsman
permanentSwordsman.attackDamage = 1 + (playerRelics.swordsman.level - 1) * 2; // Base damage +2 per level after first
game.addChild(permanentSwordsman);
swordsmen.push(permanentSwordsman);
}
// --- Initial Game Setup ---
// Set the initial score text based on the starting score (which is 0).
scoreTxt.setText(LK.getScore());
// Set the initial ammo display.
if (ammoTxt) {
// Ensure ammoTxt is initialized
ammoTxt.setText('Ammo: ' + maxArrowsBeforeCooldown);
}
;
// Play the background music.
// Check if we need to show a title screen first
if (!isGameStarted) {// Assuming a flag 'isGameStarted' for title screen state
// Don't play music immediately if showing title screen
} else {
LK.playMusic('Gamemusic');
;
}
// Placeholder for the title screen container
var titleScreen = null;
// Flag to track game start state
var isGameStarted = false;
// Function to show the title screen
function showTitleScreen() {
isGameStarted = false; // Ensure game is not started
// Create the title screen container
titleScreen = new Container();
titleScreen.x = GAME_WIDTH / 2;
titleScreen.y = GAME_HEIGHT / 2;
game.addChild(titleScreen);
// Add title text
var titleText = new Text2('Defend Your Bastion!', {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -200;
titleScreen.addChild(titleText);
// Add a simple instruction text
var instructionText = new Text2('Tap to Start', {
size: 60,
fill: 0xCCCCCC
});
instructionText.anchor.set(0.5, 0.5);
instructionText.y = 100;
titleScreen.addChild(instructionText);
// Make the title screen interactive to start the game
titleScreen.interactive = true;
titleScreen.down = function () {
startGame(); // Start the game when tapped
};
// Pause game logic while title screen is active
isUpgradePopupActive = true; // Reusing this flag to pause game logic
}
// Function to start the game
function startGame() {
isGameStarted = true; // Set game started flag
isUpgradePopupActive = false; // Unpause game logic
if (titleScreen) {
titleScreen.destroy();
titleScreen = null;
}
// Reset game state if needed (LK might handle this on game start)
// Play game music
LK.playMusic('Gamemusic');
}
// Initially show the title screen
showTitleScreen(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
/**
* Represents an Angel of Light ally that stuns enemies and deals high damage to dark enemies.
*/
var AngelOfLight = Container.expand(function () {
var self = Container.call(this);
// Create and attach the angel graphic asset (reusing viking_ally for simplicity)
// Will need a dedicated asset for the Angel of Light if available.
var graphics = self.attachAsset('angel_of_light_asset', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5
});
graphics.tint = 0xFFFF00; // Yellow tint for Angel of Light
self.attackRange = 600; // Moderate attack range
self.attackDamage = 5; // Base damage
self.attackInterval = 480; // Attack every 8 seconds (480 ticks)
self.attackTimer = 0; // Timer for attacks
self.stunDuration = 5 * 60; // Stun duration in ticks (5 seconds)
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and using light ability.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range
var closestEnemy = null;
var closestDistance = self.attackRange;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, use light ability
if (closestEnemy) {
// Apply stun and damage to the target
if (closestEnemy.tag !== 'Dragon') {
// Dragons are immune to stuns (slowdown effect)
closestEnemy.slowTimer = Math.max(closestEnemy.slowTimer, self.stunDuration); // Apply stun (reusing slowTimer)
closestEnemy.currentSlowAmount = 0.0; // 100% slowdown (stun)
}
var damageToDeal = self.attackDamage;
if (closestEnemy.tag === 'Black') {
damageToDeal *= 3; // Triple damage against Black enemies
}
closestEnemy.takeDamage(damageToDeal, self); // Deal damage
// Optional: Add visual/sound effect for light ability here later
LK.effects.flashObject(closestEnemy, 0xFFFFFF, 300); // Flash white for stun/damage
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents an allied archer that shoots arrows independently.
*/
var ArcherAlly = Container.expand(function () {
var self = Container.call(this);
// Create and attach the archer graphic asset
var graphics = self.attachAsset('Archer', {
anchorX: 0.5,
anchorY: 0.5
});
// No rotation needed for 'Archer' asset as it's already upright and flipped
self.fireTimer = 0; // Timer for shooting
self.fireInterval = 180; // Shoot every 3 seconds (3 * 60 ticks)
/**
* Update method called each game tick by the LK engine.
* Handles firing logic.
*/
self.update = function () {
self.fireTimer++;
var effectiveFireInterval = Math.max(60, self.fireInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.fireTimer >= effectiveFireInterval) {
self.fireTimer = 0; // Reset timer
// Find the closest enemy to shoot at
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, fire an arrow
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new arrow instance
var newArrow = new Arrow(angle);
// Apply multi-shot to allies if the upgrade is enabled for the player
if (multiShotEnabled) {
var angle2 = angle + Math.PI / 12; // Offset by 15 degrees
var newArrow2 = new Arrow(angle2);
newArrow2.x = self.x;
newArrow2.y = self.y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
game.addChild(newArrow2);
arrows.push(newArrow2);
var angle3 = angle - Math.PI / 12; // Offset by -15 degrees
var newArrow3 = new Arrow(angle3);
newArrow3.x = self.x;
newArrow3.y = self.y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
newArrow.x = self.x;
newArrow.y = self.y;
// Ally arrows do not count towards the player's reload counter
// The Arrow class handles piercing level based on player upgrade, but the ally doesn't benefit from player reload.
// For simplicity, we'll let the ally benefit from player's piercing upgrade.
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// Add the arrow to the game scene and the tracking array.
game.addChild(newArrow);
arrows.push(newArrow); // Add to the same arrows array for collision detection
// Ally doesn't play the 'shoot' sound
}
}
};
return self; // Return self for potential inheritance
});
// Sound when an enemy reaches the bastion
// No plugins needed for this version of the game.
/**
* Represents an Arrow fired by the player.
* @param {number} angle - The angle in radians at which the arrow is fired.
*/
var Arrow = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the arrow graphic asset
var graphics = self.attachAsset('arrow', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align arrow graphic with direction
self.speed = 30; // Speed of the arrow in pixels per tick
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.pierceLeft = arrowPierceLevel; // How many more enemies this arrow can pierce
self.damage = arrowDamage; // Damage dealt by this arrow
self.isPoison = poisonShotsEnabled; // Flag if this arrow applies poison
self.targetEnemy = null; // Potential target for aimbot
self.seekSpeed = 0.1; // How quickly the arrow adjusts its direction to seek
if (aimbotEnabled) {
// Find the closest enemy to seek if aimbot is enabled
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.targetEnemy = closestEnemy;
}
}
/**
* Update method called each game tick by the LK engine.
* Moves the arrow based on its velocity.
*/
self.update = function () {
if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) {
// If aimbot is enabled, a target exists and is still in the game, seek it
var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y));
// Smoothly adjust the arrow's angle towards the target angle
var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation
// Normalize angle difference to be between -PI and PI
if (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
if (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Interpolate angle
var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed;
self.rotation = newAngle + Math.PI / 2;
self.vx = Math.sin(newAngle) * self.speed;
self.vy = -Math.cos(newAngle) * self.speed;
} else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) {
// If aimbot is enabled but current target is gone, find a new target
self.targetEnemy = null; // Clear old target
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.targetEnemy = closestEnemy;
}
}
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Baby Dragon enemy with rage mode when no other dragons are present.
*/
var BabyDragon = Container.expand(function () {
var self = Container.call(this);
// Create and attach the baby dragon graphic asset
var graphics = self.attachAsset('baby_dragon_asset', {
anchorX: 0.5,
anchorY: 0.5
});
// No base tint - only apply pink tint when raging
self.type = 'baby_dragon';
self.speed = 3; // Base speed (will be set from spawn)
self.health = 50; // Base health (will be set from spawn)
self.maxHealth = 50;
self.dodgeChance = 0.25; // 25% dodge chance (less than adult dragon)
self.tag = 'Dragon'; // Dragon-tagged enemy
self.isRaging = false;
self.rageSpeedMultiplier = 2.0; // Double speed when raging
self.baseSpeed = self.speed;
self.poisonStacks = 0;
self.poisonTimer = 0;
self.poisonDamagePerTick = 0.05;
self.slowTimer = 0;
self.currentSlowAmount = 1.0;
/**
* Update method called each game tick by the LK engine.
* Moves the baby dragon and checks for rage mode.
*/
self.update = function () {
// Check for rage mode (no other dragons on screen)
var otherDragonsExist = false;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy !== self && enemy.tag === 'Dragon') {
otherDragonsExist = true;
break;
}
}
// Apply or remove rage mode
if (!otherDragonsExist && !self.isRaging) {
// Enter rage mode
self.isRaging = true;
self.speed = self.baseSpeed * self.rageSpeedMultiplier;
// Apply pink tint with tween animation
tween(graphics, {
tint: 0xFF69B4
}, {
duration: 300
}); // Pink tint when raging
LK.effects.flashObject(self, 0xFF69B4, 500); // Flash effect with pink
} else if (otherDragonsExist && self.isRaging) {
// Exit rage mode
self.isRaging = false;
self.speed = self.baseSpeed;
// Remove tint with tween animation
tween(graphics, {
tint: 0xFFFFFF
}, {
duration: 300
}); // Return to normal color
}
// Normal movement (dragons are immune to slowdown)
self.y += self.speed;
// Apply poison damage
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30;
self.health -= poisonDmg;
}
if (self.health <= 0 && self.parent) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
self.destroy();
return;
}
}
// Visual feedback based on health
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6;
};
/**
* Method called when the baby dragon takes damage.
*/
self.takeDamage = function (damage, source) {
// Check for dodge
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
return false;
}
// Dragons are not immune to arrows/cannonballs
self.health -= damage;
// Apply poison if applicable
if (source && source.isPoison) {
self.poisonStacks++;
self.poisonTimer = 0;
}
// Dragons are immune to slowdown effects
return self.health <= 0;
};
return self;
});
/**
* Represents a Bomb projectile thrown by a Bomber ally.
* @param {number} targetX - The target X coordinate.
* @param {number} targetY - The target Y coordinate.
*/
var Bomb = Container.expand(function (targetX, targetY) {
var self = Container.call(this);
// Create and attach the bomb graphic asset
var graphics = self.attachAsset('bomb_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.targetX = targetX;
self.targetY = targetY;
self.startX = self.x;
self.startY = self.y;
self.damage = 15; // Area damage
self.explosionRadius = 200; // Explosion radius
self.arcHeight = 300; // Height of bomb arc
self.flightTime = 60; // 1 second flight time
self.flightTimer = 0;
self.hasExploded = false;
/**
* Update method called each game tick by the LK engine.
* Handles arc movement and explosion.
*/
self.update = function () {
if (self.hasExploded) {
return;
}
self.flightTimer++;
var progress = self.flightTimer / self.flightTime;
if (progress >= 1) {
// Bomb has reached target, explode
self.explode();
return;
}
// Calculate arc position
var baseX = self.startX + (self.targetX - self.startX) * progress;
var baseY = self.startY + (self.targetY - self.startY) * progress;
// Add arc height (parabola)
var arcOffset = -4 * self.arcHeight * progress * (progress - 1);
self.x = baseX;
self.y = baseY - arcOffset;
// Rotate bomb as it flies
graphics.rotation += 0.2;
};
self.explode = function () {
if (self.hasExploded) {
return;
}
self.hasExploded = true;
// Create explosion visual effect
LK.effects.flashScreen(0xFFAA00, 200); // Orange flash
// Deal area damage to all enemies within radius
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.explosionRadius) {
// Calculate damage falloff (full damage at center, less at edges)
var damageFalloff = 1 - distance / self.explosionRadius * 0.5;
var damageToApply = self.damage * damageFalloff;
var enemyDefeated = enemy.takeDamage(damageToApply, self);
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
enemy.destroy();
enemies.splice(i, 1);
}
// Apply visual effect to hit enemies
LK.effects.flashObject(enemy, 0xFFAA00, 300);
}
}
self.destroy();
};
return self;
});
/**
* Represents a Bomber ally that throws bombs for area damage.
*/
var Bomber = Container.expand(function () {
var self = Container.call(this);
// Create and attach the bomber graphic asset
var graphics = self.attachAsset('bomber_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range like other allies
self.attackInterval = 240; // Attack every 4 seconds
self.attackTimer = 0;
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and throwing bombs.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier);
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0;
// Find a suitable target (not Dragon-tagged)
var bestTarget = null;
var bestScore = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Skip Dragon-tagged enemies
if (enemy.tag === 'Dragon') {
continue;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.attackRange) {
// Prioritize groups of enemies (check nearby enemy count)
var nearbyCount = 0;
for (var j = 0; j < enemies.length; j++) {
if (i !== j) {
var ex = enemies[j].x - enemy.x;
var ey = enemies[j].y - enemy.y;
if (Math.sqrt(ex * ex + ey * ey) <= 200) {
nearbyCount++;
}
}
}
var score = nearbyCount * 10 + (self.attackRange - distance) / 100;
if (score > bestScore) {
bestScore = score;
bestTarget = enemy;
}
}
}
if (bestTarget) {
// Create and throw bomb
var bomb = new Bomb(bestTarget.x, bestTarget.y);
bomb.startX = self.x;
bomb.startY = self.y;
bomb.x = self.x;
bomb.y = self.y;
game.addChild(bomb);
bombs.push(bomb);
}
}
};
return self;
});
/**
* Represents a Bouncy Ball that bounces around the screen dealing damage to enemies.
*/
var BouncyBall = Container.expand(function () {
var self = Container.call(this);
// Create and attach the bouncy ball graphic asset (reusing cannonball for now)
var graphics = self.attachAsset('cannonball', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
graphics.tint = 0xFF00FF; // Magenta tint for bouncy ball
self.speed = 15; // Base speed
self.damage = 20; // Massive damage
self.vx = (Math.random() - 0.5) * self.speed * 2; // Random initial horizontal velocity
self.vy = -Math.abs((Math.random() - 0.5) * self.speed * 2); // Initial upward velocity
self.lifetime = 600; // 10 seconds lifetime (600 ticks)
self.lifetimeTimer = 0;
self.bounceCount = 0; // Track number of bounces
self.maxBounces = 20; // Maximum bounces before disappearing
/**
* Update method called each game tick by the LK engine.
* Handles movement, bouncing, and lifetime.
*/
self.update = function () {
self.lifetimeTimer++;
// Check lifetime
if (self.lifetimeTimer >= self.lifetime || self.bounceCount >= self.maxBounces) {
self.destroy();
return;
}
// Move
self.x += self.vx;
self.y += self.vy;
// Bounce off walls
if (self.x <= graphics.width / 2 || self.x >= GAME_WIDTH - graphics.width / 2) {
self.vx = -self.vx; // Reverse horizontal direction
self.x = Math.max(graphics.width / 2, Math.min(GAME_WIDTH - graphics.width / 2, self.x));
self.bounceCount++;
// Flash on bounce
LK.effects.flashObject(self, 0xFFFFFF, 200);
}
// Bounce off top and bottom
if (self.y <= graphics.height / 2 || self.y >= BASTION_Y - graphics.height / 2) {
self.vy = -self.vy; // Reverse vertical direction
self.y = Math.max(graphics.height / 2, Math.min(BASTION_Y - graphics.height / 2, self.y));
self.bounceCount++;
// Flash on bounce
LK.effects.flashObject(self, 0xFFFFFF, 200);
}
};
return self;
});
/**
* Represents a Cannon ally that targets the strongest enemy.
*/
var Cannon = Container.expand(function () {
var self = Container.call(this);
// Create and attach the cannon graphic asset (need to add a new asset for this)
// Use dragon slayer cannon asset if mode is enabled, otherwise use regular cannon asset
var assetId = self.dragonSlayerMode ? 'dragon_slayer_cannon_asset' : 'cannon_asset';
var graphics = self.attachAsset(assetId, {
// Use actual cannon asset
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = 800; // Long attack range
self.attackDamage = 10; // High damage
self.attackInterval = 300; // Attack every 5 seconds (300 ticks)
self.attackTimer = 0; // Timer for attacks
self.rotation = 0; // For aiming visual if implemented later
self.dragonSlayerMode = false; // Flag to indicate if this cannon is a dragon slayer cannon
/**
* Update method called each game tick by the LK engine.
* Handles attacking the strongest enemy.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
var targetEnemy = null;
if (self.dragonSlayerMode) {
// In dragon slayer mode, prioritize dragons
var closestDragon = null;
var closestDragonDistance = Infinity;
var strongestNonDragon = null;
var maxNonDragonHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.tag === 'Dragon') {
// Check distance for dragons
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDragonDistance) {
closestDragonDistance = distance;
closestDragon = enemy;
}
} else {
// Track strongest non-dragon as fallback
if (enemy.health > maxNonDragonHealth) {
maxNonDragonHealth = enemy.health;
strongestNonDragon = enemy;
}
}
}
// Prioritize dragon if found, otherwise target strongest non-dragon
targetEnemy = closestDragon || strongestNonDragon;
} else {
// Normal mode: find the strongest enemy (highest health)
var maxHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > maxHealth) {
maxHealth = enemy.health;
targetEnemy = enemy;
}
}
}
// If a target enemy is found within range, attack it
if (targetEnemy) {
// Calculate angle towards the target enemy
var angle = Math.atan2(targetEnemy.x - self.x, -(targetEnemy.y - self.y));
// Create a new cannonball instance, passing the dragon slayer mode status
var newCannonball = new Cannonball(angle);
newCannonball.dragonSlayerMode = self.dragonSlayerMode; // Pass the mode status
// Position the cannonball at the cannon's location
newCannonball.x = self.x;
newCannonball.y = self.y;
newCannonball.lastY = newCannonball.y; // Initialize lastY for state tracking
newCannonball.lastX = newCannonball.x; // Initialize lastX for state tracking
// If dragon slayer mode and targeting a dragon, apply damage bonus
if (self.dragonSlayerMode && targetEnemy.tag === 'Dragon') {
newCannonball.dragonDamageMultiplier = 25; // 25x damage to dragons
// The Cannonball class will handle the asset and tint based on the mode.
}
// Add the cannonball to the game scene and the tracking array.
game.addChild(newCannonball);
cannonballs.push(newCannonball); // Add to the new cannonballs array
// Optional: Add a visual/sound effect for cannon shot here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents a Cannonball fired by a Cannon.
* @param {number} angle - The angle in radians at which the cannonball is fired.
*/
var Cannonball = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the graphic asset (rocket if dragon slayer mode, otherwise cannonball)
var assetId = self.dragonSlayerMode ? 'rocket_asset' : 'cannonball';
var graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align graphic with direction
// Apply red tint for rocket if in dragon slayer mode
if (self.dragonSlayerMode) {
graphics.tint = 0xFF0000; // Red tint for dragon slayer rockets
}
self.dragonSlayerMode = false; // Flag to indicate if this is a dragon slayer rocket
self.dragonDamageMultiplier = 1; // Default damage multiplier
self.speed = 20; // Base speed of the cannonball
self.damage = 10; // Base damage dealt by this cannonball
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
/**
* Update method called each game tick by the LK engine.
* Moves the cannonball based on its velocity.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Dart projectile fired by a Dart Ally.
* @param {number} angle - The angle in radians at which the dart is fired.
*/
var Dart = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the dart graphic asset (reusing arrow for simplicity)
// Will need a dedicated asset for the Dart if available.
var graphics = self.attachAsset('dart_asset', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5
});
graphics.tint = 0x9933CC; // Purple tint for darts
graphics.rotation = angle + Math.PI / 2; // Align dart graphic with direction
self.speed = 40; // Fast speed
self.damage = 0.5; // Base damage dealt by this dart
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
/**
* Update method called each game tick by the LK engine.
* Moves the dart based on its velocity.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Dart Ally that shoots fast darts.
*/
var DartAlly = Container.expand(function () {
var self = Container.call(this);
// Create and attach the dart ally graphic asset (reusing archer asset for simplicity)
// Will need a dedicated asset for the Dart Ally if available.
var graphics = self.attachAsset('disguised_swordsman', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackDamage = 0.5; // Low damage per dart
self.attackInterval = 30; // Attack every 0.5 seconds (30 ticks) - very fast
self.attackTimer = 0; // Timer for attacks
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and shooting darts.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(10, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.16s
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy to shoot at
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, shoot a dart
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new dart instance
var newDart = new Dart(angle);
newDart.x = self.x;
newDart.y = self.y;
newDart.lastY = newDart.y;
newDart.lastX = newDart.x;
// Add the dart to the game scene and the tracking array.
game.addChild(newDart);
darts.push(newDart); // Add to the new darts array
// Optional: Add throwing sound effect here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents an Enemy attacker moving towards the bastion.
* @param {string} type - The type of enemy ('swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight').
* @param {number} speed - The final calculated speed of the enemy.
* @param {number} health - The initial and maximum health of the enemy.
* @param {number} dodgeChance - The chance (0 to 1) for the enemy to dodge an attack.
*/
var Enemy = Container.expand(function (type, speed, health, dodgeChance) {
var self = Container.call(this);
// Set asset based on type
var assetId = 'enemy'; // Default swordsman asset
if (type === 'knight') {
assetId = 'knight';
} else if (type === 'elite_knight') {
assetId = 'elite_knight_asset'; // Use specific asset for elite knight
} else if (type === 'thief') {
assetId = 'thief';
} else if (type === 'boss') {
assetId = 'boss';
} else if (type === 'shield') {
assetId = 'shield_enemy'; // Use specific asset for shield
} else if (type === 'wizard') {
assetId = 'wizard_enemy'; // Use specific asset for wizard
} else if (type === 'spearman') {
assetId = 'spearman'; // Use specific asset for spearman
} else if (type === 'war_elephant') {
assetId = 'war_elephant'; // Use specific asset for war elephant
} else if (type === 'elite_shield') {
assetId = 'elite_shield_asset'; // Use specific asset for elite shield
} else if (type === 'shaman') {
assetId = 'shaman_enemy'; // Use specific asset for shaman
} else if (type === 'hot_air_balloon') {
assetId = 'hot_air_balloon_asset'; // Use specific asset for hot air balloon
} else if (type === 'dark_bowman') {
assetId = 'dark_bowman_asset'; // Use specific asset for dark bowman
} else if (type === 'jester') {
assetId = 'jester_asset'; // Use specific asset for jester
} else if (type === 'dark_war_elephant') {
assetId = 'dark_war_elephant_asset'; // Use specific asset for dark war elephant
} else if (type === 'dark_spearman') {
assetId = 'dark_spearman_asset'; // Use specific asset for dark spearman
} else if (type === 'dragon') {
assetId = 'dragon_asset'; // Use specific asset for dragon
} else if (type === 'flag_bearer') {
assetId = 'flag_bearer_asset'; // Use specific asset for flag bearer
} else if (type === 'baby_dragon') {
assetId = 'baby_dragon_asset'; // Use specific baby dragon asset
}
var graphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.type = type; // 'swordsman', 'knight', 'thief', 'boss', 'shield', 'wizard', 'elite_knight', 'shaman', etc.
self.speed = speed; // Final calculated speed
self.health = health;
self.maxHealth = health; // Store max health for visual feedback
self.dodgeChance = dodgeChance || 0; // Default to 0 if undefined
self.reflectChance = type === 'jester' ? 0.20 : 0; // 20% reflect chance, added in Enemy class
self.poisonStacks = 0; // Number of poison stacks
self.poisonTimer = 0; // Timer for poison damage
self.poisonDamagePerTick = 0.05; // Damage per tick per stack
self.slowTimer = 0; // Timer for slowdown effect
self.currentSlowAmount = 1.0; // Multiplier for current slowdown effect (1.0 means no slow)
// Initialize shaman-specific properties
if (type === 'shaman') {
self.shamanTimer = 0;
}
// Set tag property based on type
self.tag = null; // Default tag is null
if (self.type === 'wizard' || self.type === 'spearman' || self.type === 'war_elephant' || self.type === 'shaman') {
self.tag = 'Green';
} else if (self.type === 'dark_bowman' || self.type === 'dark_war_elephant' || self.type === 'dark_spearman') {
self.tag = 'Black'; // Black-tagged enemies are immune to arrows and cannonballs
}
// Wizard-specific properties
if (self.type === 'wizard') {
self.teleportTimer = 0;
self.teleportInterval = 180; // 3 seconds * 60 FPS
}
// --- Public Methods (defined before use) ---
/**
* Update method called each game tick by the LK engine.
* Moves the enemy downwards (or teleports for wizard) and updates visual feedback.
*/
self.update = function () {
if (self.type === 'wizard') {
self.teleportTimer = (self.teleportTimer || 0) + 1; // Initialize timer if needed
if (self.teleportTimer >= self.teleportInterval) {
self.teleportTimer = 0;
// Teleport logic: Random X, slightly advanced Y
var oldY = self.y;
var teleportPadding = graphics.width / 2 + 20; // Use actual graphic width
var newX = teleportPadding + Math.random() * (GAME_WIDTH - 2 * teleportPadding);
// Advance Y slightly, but don't teleport past bastion
var newY = Math.min(BASTION_Y - graphics.height, self.y + 100 + Math.random() * 100); // Advance 100-200px
// Ensure not teleporting backwards significantly or offscreen top
newY = Math.max(graphics.height / 2, newY);
self.x = newX;
self.y = newY;
self.lastY = oldY; // Set lastY to pre-teleport position to avoid false bastion triggers
// Add a visual effect for teleport
LK.effects.flashObject(self, 0xAA00FF, 300); // Purple flash
} else {
// Move normally if not teleporting this frame
self.y += self.speed;
}
} else {
// Normal movement for other enemy types
var effectiveSpeed = self.speed * self.currentSlowAmount;
// Apply global slowdown relic if enabled and not a Dragon
if (playerRelics.slowdown.enabled && self.tag !== 'Dragon') {
effectiveSpeed *= 1 - playerRelics.slowdown.level * 0.02; // Apply global slow
}
// Apply Green Relic slowdown if enabled and enemy is Green-tagged and not a Dragon
if (playerRelics.green.enabled && self.tag === 'Green' && self.tag !== 'Dragon') {
effectiveSpeed *= 1 - playerRelics.green.level * 0.02; // Apply Green Relic slow
}
self.y += effectiveSpeed; // Apply slowdown to movement
}
// Decrease slowdown timer and reset slow amount if timer runs out
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer <= 0) {
self.currentSlowAmount = 1.0; // Reset speed multiplier when slowdown ends
// Optional: Remove visual feedback for slowdown here later
}
}
// Apply poison damage if stacks exist
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
// Apply poison damage every 0.5 seconds (30 ticks)
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30; // Damage per 0.5s
self.health -= poisonDmg;
// Optional: Add visual feedback for poison damage here later
}
// Check if health dropped to zero or below due to poison
if (self.health <= 0 && self.parent) {
// Ensure enemy is still in the game
// Enemy defeated by poison
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
self.destroy();
// The main game update loop needs to handle removing the enemy from the `enemies` array
return; // Stop updating this destroyed instance
}
}
// Visual feedback based on health - alpha fade (applies to all types)
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6; // Fade from 1.0 down to 0.4
// Shaman ability: reduce player ammo periodically
if (self.type === 'shaman') {
if (self.shamanTimer === undefined) {
self.shamanTimer = 0;
}
self.shamanTimer += 1;
var shamanAbilityInterval = 300; // 5 seconds * 60 FPS
if (self.shamanTimer >= shamanAbilityInterval) {
self.shamanTimer = 0;
// Reduce player ammo, but not below 0
arrowsFired = Math.min(maxArrowsBeforeCooldown, arrowsFired + 1); // Make it require one more shot for reload
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
// Optional: Add a visual/sound effect for shaman ability
}
}
}; //{N} // Adjusted line identifier
/**
* Method called when the enemy is hit by an arrow.
* Handles dodging and health reduction.
* @param {number} damage - The amount of damage the arrow deals.
* @returns {boolean} - True if the enemy is defeated, false otherwise.
*/
self.takeDamage = function (damage) {
var source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
// Add source parameter, default to null
// Check for dodge first (only if dodgeChance is positive)
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
// Optional: Add a visual effect for dodge here later
// console.log(self.type + " dodged!");
return false; // Dodged, so not defeated by this hit
}
// Check for reflection (only for Jester and non-cannonball projectiles)
if (self.type === 'jester' && self.reflectChance > 0 && Math.random() < self.reflectChance && !(source instanceof Cannonball)) {
// Optional: Add a visual effect for reflection here later
// console.log(self.type + " reflected!");
// Projectile is destroyed, enemy takes no damage
if (source && source.destroy) {
source.destroy();
// Need to remove from the source array in the main game loop
}
return false; // Reflected, so not defeated by this hit
}
// Check if enemy is Black-tagged and source is an Arrow or Cannonball
if (self.tag === 'Black' && (source instanceof Arrow || source instanceof Cannonball)) {
// Black-tagged enemies are immune to arrow and cannonball projectiles
// Unless Dark Relic is enabled
if (!playerRelics.dark.enabled) {
return false; // Not defeated, projectile has no effect
}
}
var finalDamage = damage;
// Apply damage relic bonus if enabled
if (playerRelics.damage.enabled) {
finalDamage += playerRelics.damage.level;
}
// Apply Green Relic damage bonus if enabled and enemy is Green-tagged
if (playerRelics.green.enabled && self.tag === 'Green') {
finalDamage *= 1 + playerRelics.green.level * 0.2;
}
// Apply Dark Relic damage bonus if enabled and enemy is Black-tagged
if (playerRelics.dark.enabled && self.tag === 'Black' && (source instanceof Arrow || source instanceof Cannonball)) {
finalDamage *= 1 + playerRelics.dark.level * 0.05;
}
self.health -= finalDamage;
// Check if the damage source was a poison arrow and apply poison stacks
if (source && source.isPoison) {
// Use the 'source' parameter
self.poisonStacks++; // Increase poison stacks
self.poisonTimer = 0; // Reset timer to apply damage immediately
}
// Check if the damage source was a Magic Ball and apply slowdown
if (source && source instanceof MagicBall) {
if (self.tag !== 'Dragon') {
// Dragons are immune to slowdowns
self.slowTimer = source.slowDuration; // Apply slowdown duration
self.currentSlowAmount = source.slowAmount; // Apply slowdown amount
// Optional: Add visual feedback for slowdown here later
}
}
// Check if the damage source was an Arrow and apply Dragon relic stun
if (playerRelics.dragon.enabled && source instanceof Arrow && self.tag === 'Dragon') {
// Apply stun (reusing slowTimer for duration, 0.0 for 100% slow)
var stunDuration = 0.25 * 60 + playerRelics.dragon.level * 0.05 * 60; // Base 0.25s + 0.05s per level
self.slowTimer = Math.max(self.slowTimer, stunDuration);
self.currentSlowAmount = 0.0; // 100% slow (stun)
}
// Check if the damage source should apply Green Slowdown
if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0 && self.tag !== 'Dragon') {
// Dragons are immune to slowdowns
// Apply Green Slowdown only if the upgrade is active, enemy is Green, not already slowed, and not a Dragon
self.slowTimer = 10 * 60; // 10 seconds * 60 ticks/sec
self.currentSlowAmount = 0.9; // 10% slowdown
// Optional: Add visual feedback for green slowdown
}
// Check if enemy was defeated by this damage
var defeated = self.health <= 0;
if (defeated && self.type === 'war_elephant') {
// If War Elephant is defeated, spawn 5 spearmen
for (var i = 0; i < 5; i++) {
var spawnPadding = 100; // Padding from edge
var spawnX = self.x + (Math.random() * 200 - 100); // Spawn around elephant's x
var spawnY = self.y + (Math.random() * 100 - 50); // Spawn around elephant's y, slightly forward
// Ensure spawns are within game bounds
spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX));
spawnY = Math.max(spawnPadding, Math.min(BASTION_Y - 50, spawnY)); // Don't spawn too close to bastion
var newSpearman = new Enemy('spearman', currentEnemySpeed * enemySpeedMultiplier, 2, 0); // Base spearman stats
newSpearman.x = spawnX;
newSpearman.y = spawnY;
newSpearman.lastY = newSpearman.y;
game.addChild(newSpearman);
enemies.push(newSpearman);
}
} else if (defeated && self.type === 'dark_war_elephant') {
// If Dark War Elephant is defeated, spawn 5 Dark Bowmen
for (var i = 0; i < 5; i++) {
var spawnPadding = 100; // Padding from edge
var spawnX = self.x + (Math.random() * 200 - 100); // Spawn around elephant's x
var spawnY = self.y + (Math.random() * 100 - 50); // Spawn around elephant's y, slightly forward
// Ensure spawns are within game bounds
spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX));
spawnY = Math.max(spawnPadding, Math.min(BASTION_Y - 50, spawnY)); // Don't spawn too close to bastion
var newDarkBowman = new Enemy('dark_bowman', currentEnemySpeed * enemySpeedMultiplier, 5, 0); // Base dark bowman stats
newDarkBowman.x = spawnX;
newDarkBowman.y = spawnY;
newDarkBowman.lastY = newDarkBowman.y;
game.addChild(newDarkBowman);
enemies.push(newDarkBowman);
}
} else if (defeated && self.type === 'hot_air_balloon') {
// If Hot Air Balloon is defeated, spawn 5 random non-green enemies
var possibleTypes = ['swordsman', 'knight', 'thief', 'shield', 'elite_knight', 'elite_shield', 'dark_bowman'];
for (var i = 0; i < 5; i++) {
// Choose random enemy type that is not green-tagged
var randomType = possibleTypes[Math.floor(Math.random() * possibleTypes.length)];
var randomHP = 1 + Math.floor(Math.random() * 5); // Random HP between 1-5
var randomSpeed = currentEnemySpeed * 0.7 * enemySpeedMultiplier; // Slower than average
var newEnemy = new Enemy(randomType, randomSpeed, randomHP, 0);
// Spawn in a staggered pattern below the balloon
var spawnPadding = 100;
var spawnX = self.x + (Math.random() * 300 - 150); // Wider spread than elephant
var spawnY = self.y + Math.random() * 200; // Always below the balloon
// Ensure spawns are within game bounds
spawnX = Math.max(spawnPadding, Math.min(GAME_WIDTH - spawnPadding, spawnX));
spawnY = Math.min(BASTION_Y - 50, spawnY); // Don't spawn too close to bastion
newEnemy.x = spawnX;
newEnemy.y = spawnY;
newEnemy.lastY = newEnemy.y;
game.addChild(newEnemy);
enemies.push(newEnemy);
}
}
// No need to update alpha here, self.update handles it
return defeated; // Return true if health is 0 or less
}; //{O} // Adjusted line identifier
// --- Initialization ---
// Apply visual distinctions based on type
graphics.tint = 0xFFFFFF; // Reset tint
graphics.scale.set(1.0); // Reset scale
if (self.type === 'knight') {
graphics.tint = 0xCCCCCC; // Grey tint for Knights
graphics.scale.set(1.1);
} else if (self.type === 'elite_knight') {
graphics.tint = 0xFFD700; // Gold tint for Elite Knights
graphics.scale.set(1.2); // Slightly larger than normal knight
} else if (self.type === 'thief') {
graphics.tint = 0xCCFFCC; // Pale Green tint for Thieves
graphics.scale.set(0.9);
} else if (self.type === 'boss') {
graphics.tint = 0xFFCCCC; // Pale Red tint for Bosses
graphics.scale.set(1.4); // Make bosses quite large
} else if (self.type === 'shield') {
graphics.tint = 0xADD8E6; // Light Blue tint for Shield
graphics.scale.set(1.2); // Make shield enemies bulky
} else if (self.type === 'wizard') {
graphics.tint = 0xE0B0FF; // Light Purple tint for Wizard
graphics.scale.set(1.0);
} else if (self.type === 'elite_shield') {
graphics.tint = 0x8A2BE2; // Blue Violet tint for Elite Shield
graphics.scale.set(1.3); // Slightly larger than normal shield
} else if (self.type === 'jester') {
graphics.tint = 0xFFB6C1; // Light Pink tint for Jester
graphics.scale.set(1.0);
} else if (self.type === 'dark_war_elephant') {
graphics.tint = 0x333333; // Dark Grey tint for Dark War Elephant
graphics.scale.set(1.4); // Same size as regular elephant
} else if (self.type === 'dark_spearman') {
graphics.scale.set(1.05); // Slightly larger than regular spearman
} else if (self.type === 'dragon') {
graphics.scale.set(1.6); // Dragons are large
} //{11} // Modified original line identifier location
return self; // Return self for potential inheritance
});
/**
* Represents a Flag Bearer enemy that provides a speed boost aura to nearby enemies.
*/
var FlagBearer = Container.expand(function () {
var self = Container.call(this);
// Create and attach the flag bearer graphic asset
var graphics = self.attachAsset('flag_bearer_asset', {
anchorX: 0.5,
anchorY: 0.5
});
// No tint applied to flag bearer
self.type = 'flag_bearer';
self.speed = 3; // Base speed (will be set from spawn)
self.health = 5; // Base health (will be set from spawn)
self.maxHealth = 5;
self.dodgeChance = 0;
self.tag = 'Green'; // Green-tagged enemy
self.auraRadius = 300; // Radius of speed boost aura
self.auraBoost = 1.5; // 50% speed boost to enemies in aura
self.poisonStacks = 0;
self.poisonTimer = 0;
self.poisonDamagePerTick = 0.05;
self.slowTimer = 0;
self.currentSlowAmount = 1.0;
// Create aura visual effect
self.auraCircle = self.attachAsset('light_effect_asset', {
anchorX: 0.5,
anchorY: 0.5,
width: self.auraRadius * 2,
height: self.auraRadius * 2,
tint: 0x00FF00,
alpha: 0.2
});
// Position aura behind the flag bearer
self.setChildIndex(self.auraCircle, 0);
/**
* Update method called each game tick by the LK engine.
* Moves the flag bearer and applies aura effects.
*/
self.update = function () {
// Normal movement
self.y += self.speed * self.currentSlowAmount;
// Apply aura effect to nearby enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy === self) {
continue;
} // Skip self
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.auraRadius) {
// Enemy is within aura range
if (!enemy.hasAuraBoost) {
enemy.hasAuraBoost = true;
enemy.baseSpeed = enemy.speed;
enemy.speed = enemy.baseSpeed * self.auraBoost;
}
} else if (enemy.hasAuraBoost) {
// Enemy left aura range
enemy.hasAuraBoost = false;
enemy.speed = enemy.baseSpeed || enemy.speed;
}
}
// Handle slowdown timer
if (self.slowTimer > 0) {
self.slowTimer--;
if (self.slowTimer <= 0) {
self.currentSlowAmount = 1.0;
}
}
// Apply poison damage
if (self.poisonStacks > 0) {
self.poisonTimer++;
if (self.poisonTimer >= 30) {
self.poisonTimer = 0;
var poisonDmg = self.poisonStacks * self.poisonDamagePerTick * 30;
self.health -= poisonDmg;
}
if (self.health <= 0 && self.parent) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
self.destroy();
return;
}
}
// Visual feedback based on health
var healthRatio = self.maxHealth > 0 ? self.health / self.maxHealth : 0;
graphics.alpha = 0.4 + healthRatio * 0.6;
// Update aura visual effect
if (self.auraCircle) {
self.auraCircle.x = self.x;
self.auraCircle.y = self.y;
// Pulse effect
self.auraPulseTimer = (self.auraPulseTimer || 0) + 0.05;
var pulseScale = 1.0 + Math.sin(self.auraPulseTimer) * 0.1;
self.auraCircle.scale.set(pulseScale);
self.auraCircle.alpha = 0.15 + Math.sin(self.auraPulseTimer * 2) * 0.05;
}
};
/**
* Method called when the flag bearer takes damage.
*/
self.takeDamage = function (damage, source) {
// Check for dodge
if (self.dodgeChance > 0 && Math.random() < self.dodgeChance) {
return false;
}
// Check if source is Arrow or Cannonball (Green enemies are not immune)
self.health -= damage;
// Apply poison if applicable
if (source && source.isPoison) {
self.poisonStacks++;
self.poisonTimer = 0;
}
// Apply slowdown from Magic Ball
if (source && source instanceof MagicBall) {
self.slowTimer = source.slowDuration;
self.currentSlowAmount = source.slowAmount;
}
// Apply Green Slowdown
if (greenSlowdownEnabled && self.tag === 'Green' && self.slowTimer <= 0) {
self.slowTimer = 10 * 60;
self.currentSlowAmount = 0.9;
}
return self.health <= 0;
};
// Clean up aura effects when destroyed
var originalDestroy = self.destroy;
self.destroy = function () {
// Remove aura effects from all enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.hasAuraBoost) {
enemy.hasAuraBoost = false;
enemy.speed = enemy.baseSpeed || enemy.speed;
}
}
// Clean up aura visual
if (self.auraCircle) {
self.auraCircle.destroy();
self.auraCircle = null;
}
originalDestroy.call(self);
};
return self;
});
/**
* Represents a Magic Ball projectile fired by a Wizard Tower.
* @param {number} angle - The angle in radians at which the magic ball is fired.
*/
var MagicBall = Container.expand(function (angle) {
var self = Container.call(this);
// Create and attach the magic ball graphic asset
var graphics = self.attachAsset('magic_ball_asset', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align magic ball graphic with direction
self.speed = 15; // Base speed of the magic ball
self.damage = 2; // Base damage dealt by this magic ball
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
self.damage += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.slowDuration = 0; // Duration of slowdown applied on hit
self.slowAmount = 0; // Multiplier for enemy speed on hit
self.targetEnemy = null; // Track the specific enemy the magic ball is seeking
self.seekSpeed = 0.05; // How quickly the magic ball adjusts its direction to seek
/**
* Update method called each game tick by the LK engine.
* Moves the magic ball based on its velocity and seeks target if Aimbot is enabled.
*/
self.update = function () {
if (aimbotEnabled && self.targetEnemy && self.targetEnemy.parent) {
// If aimbot is enabled, a target exists and is still in the game, seek it
var targetAngle = Math.atan2(self.targetEnemy.x - self.x, -(self.targetEnemy.y - self.y));
// Smoothly adjust the magic ball's angle towards the target angle
var angleDiff = targetAngle - (self.rotation - Math.PI / 2); // Difference considering graphic rotation
// Normalize angle difference to be between -PI and PI
if (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
if (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Interpolate angle
var newAngle = self.rotation - Math.PI / 2 + angleDiff * self.seekSpeed;
self.rotation = newAngle + Math.PI / 2;
self.vx = Math.sin(newAngle) * self.speed;
self.vy = -Math.cos(newAngle) * self.speed;
} else if (aimbotEnabled && (!self.targetEnemy || !self.targetEnemy.parent)) {
// If aimbot is enabled but current target is gone, find a new target
self.targetEnemy = null; // Clear old target
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.targetEnemy = closestEnemy;
}
}
self.x += self.vx;
self.y += self.vy;
};
return self; // Return self for potential inheritance
});
/**
* Represents a Swordsman ally that attacks enemies in melee range.
*/
var Swordsman = Container.expand(function () {
var self = Container.call(this);
// Create and attach the swordsman graphic asset (reusing enemy asset for simplicity)
var graphics = self.attachAsset('swordsmanAlly', {
anchorX: 0.5,
anchorY: 0.5
});
// No tint needed as ally asset has unique colors
graphics.scale.set(1.0); // Use the asset's intended scale
self.attackRange = 150; // Melee attack range
self.attackDamage = 1; // Damage per hit
self.attackInterval = 60; // Attack every 1 second (60 ticks)
self.attackTimer = 0; // Timer for attacks
self.lifetime = 10 * 60; // Lifetime in ticks (10 seconds * 60 ticks/sec)
self.lifetimeTimer = 0; // Timer for lifetime
/**
* Update method called each game tick by the LK engine.
* Handles attacking and lifetime.
*/
self.update = function () {
self.attackTimer++;
self.lifetimeTimer++;
// Check for lifetime
var effectiveAttackInterval = Math.max(30, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade, min 0.5s
if (self.lifetimeTimer >= self.lifetime) {
self.destroy();
// Remove from the swordsmen array in the main game loop
return; // Stop updating this instance
}
// Find the closest enemy to chase or attack
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy exists, chase it or attack if within range
if (closestEnemy) {
// Movement speed toward enemies
var moveSpeed = 5;
// If not within attack range, move toward the enemy
if (closestDistance > self.attackRange) {
// Calculate direction vector to enemy
var dirX = closestEnemy.x - self.x;
var dirY = closestEnemy.y - self.y;
// Normalize the direction vector
var length = Math.sqrt(dirX * dirX + dirY * dirY);
dirX = dirX / length;
dirY = dirY / length;
// Move toward the enemy
self.x += dirX * moveSpeed;
self.y += dirY * moveSpeed;
}
// If within attack range and attack timer is ready, attack
else if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Apply damage to the enemy and check if it was defeated
var finalSwordsmanDamage = self.attackDamage;
// Apply damage relic bonus if enabled
if (playerRelics.damage.enabled) {
finalSwordsmanDamage += playerRelics.damage.level;
}
var enemyDefeated = closestEnemy.takeDamage(finalSwordsmanDamage, self); // Pass the swordsman as source
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score when enemy is defeated by swordsman.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
closestEnemy.destroy();
// Remove from the enemies array in the main game loop
}
// Optional: Add a visual/sound effect for attack here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents a Viking ally that throws piercing axes.
*/
var VikingAlly = Container.expand(function () {
var self = Container.call(this);
var graphics = self.attachAsset('viking_ally', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackInterval = 150; // Attack every 2.5 seconds (150 ticks)
self.attackTimer = 0; // Timer for attacks
/**
* Update method called each game tick by the LK engine.
* Handles finding targets and throwing axes.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range
var closestEnemy = null;
var closestDistance = self.attackRange;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, throw an axe
if (closestEnemy) {
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
var newAxe = new VikingAxe(angle);
newAxe.x = self.x;
newAxe.y = self.y;
newAxe.lastY = newAxe.y;
newAxe.lastX = newAxe.x;
game.addChild(newAxe);
vikingAxes.push(newAxe); // Add to the vikingAxes array
// Optional: Add throwing sound effect here later
}
}
};
return self;
});
/**
* Represents an Axe thrown by a Viking Ally.
* @param {number} angle - The angle in radians at which the axe is thrown.
*/
var VikingAxe = Container.expand(function (angle) {
var self = Container.call(this);
var graphics = self.attachAsset('viking_axe', {
anchorX: 0.5,
anchorY: 0.5
});
graphics.rotation = angle + Math.PI / 2; // Align axe graphic with direction
self.speed = 25; // Speed of the axe
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.speed += 5;
}
self.vx = Math.sin(angle) * self.speed; // Horizontal velocity component
self.vy = -Math.cos(angle) * self.speed; // Vertical velocity component (negative Y is up)
self.damage = 3; // Base damage dealt by this axe
// Apply refined projectiles bonus if enabled
if (refinedProjectilesEnabled) {
self.damage += 5;
}
self.pierceLeft = 3; // Can pierce through 3 enemies
/**
* Update method called each game tick by the LK engine.
* Moves the axe based on its velocity and handles rotation.
*/
self.update = function () {
self.x += self.vx;
self.y += self.vy;
graphics.rotation += 0.2; // Add spinning effect
};
return self;
});
/**
* Represents a Wizard Tower ally that shoots magic balls.
*/
var WizardTower = Container.expand(function () {
var self = Container.call(this);
// Create and attach the wizard tower graphic asset
var graphics = self.attachAsset('wizard_tower_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.attackRange = Infinity; // Infinite attack range
self.attackDamage = 0.5; // Low damage, primary effect is slowdown
self.attackInterval = 120; // Attack every 2 seconds (120 ticks)
self.attackTimer = 0; // Timer for attacks
self.slowDuration = 180; // Duration of slowdown effect in ticks (3 seconds)
self.slowAmount = 0.5; // Multiplier for enemy speed (0.5 means 50% slower)
/**
* Update method called each game tick by the LK engine.
* Handles attacking enemies.
*/
self.update = function () {
self.attackTimer++;
var effectiveAttackInterval = Math.max(60, self.attackInterval * allyAttackSpeedMultiplier); // Apply ally attack speed upgrade
if (self.attackTimer >= effectiveAttackInterval) {
self.attackTimer = 0; // Reset timer
// Find the closest enemy within range to shoot at
var closestEnemy = null;
var closestDistance = self.attackRange; // Limit to attack range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Calculate distance to the enemy
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// If an enemy is found, fire a magic ball
if (closestEnemy) {
// Calculate angle towards the closest enemy
var angle = Math.atan2(closestEnemy.x - self.x, -(closestEnemy.y - self.y));
// Create a new magic ball instance
var newMagicBall = new MagicBall(angle);
// Position the magic ball at the tower's location
newMagicBall.x = self.x;
newMagicBall.y = self.y;
newMagicBall.lastY = newMagicBall.y; // Initialize lastY for state tracking
newMagicBall.lastX = newMagicBall.x; // Initialize lastX for state tracking
newMagicBall.slowDuration = self.slowDuration; // Pass slow duration to the magic ball
newMagicBall.slowAmount = self.slowAmount; // Pass slow amount to the magic ball
// Add the magic ball to the game scene and the tracking array.
game.addChild(newMagicBall);
magicBalls.push(newMagicBall); // Add to the magicBalls array
// Optional: Add a visual/sound effect for magic ball shot here later
}
}
};
return self; // Return self for potential inheritance
});
/**
* Represents an XBOW (crossbow) ally that rapidly shoots arrows.
*/
var XBOW = Container.expand(function () {
var self = Container.call(this);
// Create and attach the XBOW graphic asset
var graphics = self.attachAsset('xbow_asset', {
anchorX: 0.5,
anchorY: 0.5
});
self.fireInterval = 20; // Very rapid fire (every 0.33 seconds)
self.fireTimer = 0;
self.targetingMode = 'closest_to_bastion'; // Default targeting mode
self.smartTargetingEnabled = false; // Flag for smart targeting upgrade
self.tintApplied = false; // Track if tint has been applied
/**
* Update method called each game tick by the LK engine.
* Handles rapid firing logic.
*/
self.update = function () {
self.fireTimer++;
var baseInterval = self.smartTargetingEnabled ? 15.36 : self.fireInterval; // 0.256 seconds when smart targeting enabled
var effectiveFireInterval = Math.max(10, baseInterval * allyAttackSpeedMultiplier);
if (self.fireTimer >= effectiveFireInterval) {
self.fireTimer = 0;
// Find target based on targeting mode
var target = null;
if (self.smartTargetingEnabled) {
// Apply green tint when smart targeting is enabled
if (!self.tintApplied) {
graphics.tint = 0x00FF00; // Green tint
self.tintApplied = true;
}
if (self.targetingMode === 'closest_to_bastion') {
// Find enemy closest to bastion
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var distanceToBastion = BASTION_Y - enemy.y;
if (distanceToBastion > 0 && distanceToBastion < closestDistance) {
closestDistance = distanceToBastion;
target = enemy;
}
}
} else if (self.targetingMode === 'strongest') {
// Find enemy with highest health
var maxHealth = -1;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health > maxHealth) {
maxHealth = enemy.health;
target = enemy;
}
}
}
}
// Default behavior: shoot at center of screen
var angle;
if (target) {
// Calculate angle towards target
angle = Math.atan2(target.x - self.x, -(target.y - self.y));
} else {
// No target or smart targeting disabled - shoot straight up towards center
angle = 0; // 0 radians = straight up
}
// Create arrow
var newArrow = new Arrow(angle);
newArrow.x = self.x;
newArrow.y = self.y;
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// XBOW arrows are faster
newArrow.speed *= 1.5;
newArrow.vx = Math.sin(angle) * newArrow.speed;
newArrow.vy = -Math.cos(angle) * newArrow.speed;
// Disable aimbot for XBOW arrows
newArrow.targetEnemy = null;
game.addChild(newArrow);
arrows.push(newArrow);
// Apply multi-shot if enabled
if (multiShotEnabled) {
// Fire a second arrow with a slight angle offset
var angle2 = angle + Math.PI / 12; // Offset by 15 degrees
var newArrow2 = new Arrow(angle2);
newArrow2.x = self.x;
newArrow2.y = self.y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
// XBOW arrows are faster
newArrow2.speed *= 1.5;
newArrow2.vx = Math.sin(angle2) * newArrow2.speed;
newArrow2.vy = -Math.cos(angle2) * newArrow2.speed;
// Disable aimbot for XBOW arrows
newArrow2.targetEnemy = null;
game.addChild(newArrow2);
arrows.push(newArrow2);
// Fire a third arrow with the opposite angle offset
var angle3 = angle - Math.PI / 12; // Offset by -15 degrees
var newArrow3 = new Arrow(angle3);
newArrow3.x = self.x;
newArrow3.y = self.y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
// XBOW arrows are faster
newArrow3.speed *= 1.5;
newArrow3.vx = Math.sin(angle3) * newArrow3.speed;
newArrow3.vy = -Math.cos(angle3) * newArrow3.speed;
// Disable aimbot for XBOW arrows
newArrow3.targetEnemy = null;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
}
};
return self;
});
/****
* Initialize Game
****/
// Create the main game instance with a dark background color.
var game = new LK.Game({
backgroundColor: 0x101030 // Dark blue/purple background
});
/****
* Game Code
****/
// Use actual war elephant asset
// Use actual spearman asset
// Placeholder ID, adjust size slightly, reuse flipX
// Placeholder ID
// Placeholder ID, adjust size slightly
// Constants defining game dimensions and key vertical positions.
//var facekit = LK.import('@upit/facekit.v1');
//Library for using the camera (the background becomes the user's camera video feed) and the microphone. It can access face coordinates for interactive play, as well detect microphone volume / voice interactions
//var storage = LK.import('@upit/storage.v1');
//Storage library which should be used for persistent game data
//We have access to the following plugins. (Note that the variable names used are mandetory for each plugin)
//Only include the plugins you need to create the game.
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var BASTION_Y = GAME_HEIGHT - 250; // Y-coordinate representing the defense line. Enemies crossing this trigger game over.
var FIRING_POS_Y = GAME_HEIGHT - 150; // Y-coordinate from where arrows are fired.
var FIRING_POS_X = GAME_WIDTH / 2; // X-coordinate from where arrows are fired (center).
// Arrays to keep track of active arrows and enemies.
var arrows = [];
var enemies = [];
// Variables for game state and difficulty scaling.
var scoreTxt; // Text2 object for displaying the score.
var ammoTxt; // Text2 object for displaying the current ammo count.
var dragStartX = null; // Starting X coordinate of a touch/mouse drag.
var dragStartY = null; // Starting Y coordinate of a touch/mouse drag.
var enemySpawnInterval = 120; // Initial ticks between enemy spawns (2 seconds at 60 FPS).
var arrowsFired = 0; // Counter for arrows fired since the last cooldown.
var cooldownTimer = 0; // Timer in ticks for the cooldown period.
var cooldownDuration = 6 * 60; // Cooldown duration in ticks (6 seconds * 60 ticks/sec). Min duration enforced in upgrade.
var baseMaxArrowsBeforeCooldown = 15; // Base max arrows before reload (Quiver upgrade)
var maxArrowsBeforeCooldown = baseMaxArrowsBeforeCooldown; // Max arrows before reload
var ammoRelicTimer = 0; // Timer for ammo relic regeneration
var arrowPierceLevel = 1; // How many enemies an arrow can pierce (Piercing Shots upgrade)
var arrowDamage = 1; // Base damage for arrows (Heat-tipped arrows upgrade)
var enemySpeedMultiplier = 1.0; // Multiplier for enemy speed (Sabotage upgrade) Min multiplier enforced in upgrade.
var minEnemySpawnInterval = 30; // Minimum ticks between enemy spawns (0.5 seconds).
var enemySpawnRateDecrease = 0.08; // Amount to decrease spawn interval each time an enemy spawns.
var currentEnemySpeed = 3; // Initial base speed of enemies.
var maxEnemySpeed = 12; // Maximum base speed enemies can reach.
var enemySpeedIncrease = 0.015; // Amount to increase enemy base speed each time an enemy spawns.
var archerAllies = []; // Array to hold ArcherAlly instances
var swordsmen = []; // Array to hold Swordsman instances
var cannonballs = []; // Array to hold Cannonball instances
var multiShotEnabled = false; // Flag for More Shots upgrade
var poisonShotsEnabled = false; // Flag for Poison Shots upgrade
var cannons = []; // Array to hold Cannon instances
var allyAttackSpeedMultiplier = 1.0; // Multiplier for ally attack speed
var wizardTowers = []; // Array to hold WizardTower instances
var magicBalls = []; // Array to hold MagicBall instances
var aimbotEnabled = false; // Flag for Aimbot upgrade
var greenKillerEnabled = false; // Flag for Green Killer upgrade
var refinedProjectilesEnabled = false; // Flag for Refined Projectiles upgrade
var greenSlowdownEnabled = false; // Flag for Green Slowdown upgrade
var vikingAllies = []; // Array to hold VikingAlly instances
var vikingAxes = []; // Array to hold VikingAxe instances
var darts = []; // Array to hold Dart instances
var angelOfLights = []; // Array to hold AngelOfLight instances
var bouncyBalls = []; // Array to hold BouncyBall instances
var bombs = []; // Array to hold Bomb instances
var bombers = []; // Array to hold Bomber instances
var xbows = []; // Array to hold XBOW instances
var xbowSmartTargetingEnabled = false; // Flag for XBOW Smart Targeting upgrade
var battleHornEnabled = false; // Flag for Battle Horn upgrade
var dragonSlayerCannonsModeEnabled = false; // Flag for Dragon Slayer Cannons upgrade
// --- Relics and Gold System ---
var playerGold = storage.playerGold || 0; // Load gold from storage, default to 0
// Load relics from storage using flat format
var storedRelicsData = storage.playerRelicsData || {};
var playerRelics = {
damage: {
level: storedRelicsData.damage_level || 0,
enabled: storedRelicsData.damage_enabled || false
},
slowdown: {
level: storedRelicsData.slowdown_level || 0,
enabled: storedRelicsData.slowdown_enabled || false
},
green: {
level: storedRelicsData.green_level || 0,
enabled: storedRelicsData.green_enabled || false
},
dark: {
level: storedRelicsData.dark_level || 0,
enabled: storedRelicsData.dark_enabled || false
},
dragon: {
level: storedRelicsData.dragon_level || 0,
enabled: storedRelicsData.dragon_enabled || false
},
reload: {
level: storedRelicsData.reload_level || 0,
enabled: storedRelicsData.reload_enabled || false
},
ammo: {
level: storedRelicsData.ammo_level || 0,
enabled: storedRelicsData.ammo_enabled || false
},
swordsman: {
level: storedRelicsData.swordsman_level || 0,
enabled: storedRelicsData.swordsman_enabled || false
}
};
var RELIC_MAX_LEVEL = 10;
var RELIC_COST_PER_LEVEL = 5; // 5 gold per level
// --- Upgrade System ---
var lastUpgradeScore = -1; // Score at which the last upgrade was offered
var isUpgradePopupActive = false; // Flag indicating if the upgrade choice popup is visible
var upgradePopup = null; // Container for the upgrade UI elements
var currentMusicPlaying = 'Gamemusic'; // Track which music is currently playing
var musicFadeInProgress = false; // Flag to track if a music fade transition is in progress
// Helper function to apply Sabotage effect to existing enemies
function applySabotage() {
for (var i = 0; i < enemies.length; i++) {
// We assume Enemy class will use enemySpeedMultiplier when calculating effective speed
// If Enemy.speed stores base speed, we might need an effectiveSpeed method or update speed directly.
// For simplicity, let's assume Enemy.update uses the global multiplier.
// If direct modification is needed: enemies[i].speed = baseSpeed * enemySpeedMultiplier;
}
// Also update the current base speed calculation baseline if necessary, though modifying the multiplier should suffice.
}
// Helper function placeholder for adding Archer Ally
function addArcherAlly() {
// Implementation requires ArcherAlly class definition
console.log("Archer Ally upgrade chosen!");
var ally = new ArcherAlly();
// Position the ally next to the player's firing position, slightly offset.
ally.x = FIRING_POS_X + 150; // Position to the right of the player
ally.y = FIRING_POS_Y;
game.addChild(ally);
archerAllies.push(ally); // Add to the new archerAllies array
}
// Helper function to add a Cannon ally
function addCannon() {
console.log("Cannon upgrade chosen!");
var newCannon = new Cannon();
// Position the cannon near the bastion line
newCannon.x = GAME_WIDTH / 2 - 300; // Example position to the left of center
newCannon.y = BASTION_Y - 100; // Position above the bastion line
// Apply dragon slayer mode if the global flag is enabled
newCannon.dragonSlayerMode = dragonSlayerCannonsModeEnabled;
game.addChild(newCannon);
cannons.push(newCannon); // Add to the cannons array
}
// Helper function to add a Wizard Tower
function addWizardTower() {
console.log("Wizard Tower upgrade chosen!");
var newTower = new WizardTower();
// Position the wizard tower near the bastion line, offset from center
newTower.x = GAME_WIDTH / 2 + 300; // Example position to the right of center
newTower.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newTower);
wizardTowers.push(newTower); // Add to the wizardTowers array
}
// Helper function to add a Viking Ally
function addVikingAlly() {
console.log("Viking Ally upgrade chosen!");
var newViking = new VikingAlly();
// Position the viking near the bastion line, offset from cannons/towers
newViking.x = GAME_WIDTH / 2; // Center for now
newViking.y = BASTION_Y - 100; // Position above the bastion line
game.addChild(newViking);
vikingAllies.push(newViking); // Add to the vikingAllies array
}
// Helper function to add a Dart Ally
function addDartAlly() {
console.log("Dart Ally upgrade chosen!");
var newDartAlly = new DartAlly();
// Position the dart ally near the bastion line, offset
newDartAlly.x = GAME_WIDTH / 2 - 200; // Example position
newDartAlly.y = BASTION_Y - 100; // Position above bastion line
game.addChild(newDartAlly);
darts.push(newDartAlly); // Add to the dartAllies array (need to rename array to dartAllies if creating a separate one)
}
// Helper function placeholder for adding Swordsman
function addSwordsman() {
// Create a swordsman tower at the bastion line
var tower = new Container();
var towerGraphics = tower.attachAsset('swordsmanTower', {
anchorX: 0.5,
anchorY: 0.5
});
tower.x = FIRING_POS_X; // Position in the center horizontally
tower.y = BASTION_Y - 50; // Position just above the bastion line
game.addChild(tower);
// Set up interval to spawn swordsmen from the tower
var spawnInterval = LK.setInterval(function () {
var newSwordsman = new Swordsman();
// Position the swordsman near the tower with a slight random offset
newSwordsman.x = tower.x + (Math.random() * 100 - 50); // Random offset of ±50px
newSwordsman.y = tower.y;
game.addChild(newSwordsman);
swordsmen.push(newSwordsman); // Add to the swordsmen array
}, 3000); // Spawn a swordsman every 3 seconds
// Swordsman spawn does not grant score
// Store the interval reference in the tower to potentially clear it later
tower.spawnInterval = spawnInterval;
}
// Helper function to add an Angel of Light
function addAngelOfLight() {
console.log("Angel of Light upgrade chosen!");
var newAngel = new AngelOfLight();
// Position the angel near the bastion line, offset
newAngel.x = GAME_WIDTH / 2 + 200; // Example position
newAngel.y = BASTION_Y - 150; // Position slightly higher than other allies
game.addChild(newAngel);
angelOfLights.push(newAngel); // Add to the angelOfLights array
}
// Helper function to add a Bouncy Ball
function addBouncyBall() {
console.log("Bouncy Ball upgrade chosen!");
var newBall = new BouncyBall();
// Start from center of screen
newBall.x = GAME_WIDTH / 2;
newBall.y = GAME_HEIGHT / 2;
game.addChild(newBall);
bouncyBalls.push(newBall);
}
// Helper function to add a Bomber
function addBomber() {
console.log("Bomber upgrade chosen!");
var newBomber = new Bomber();
// Position near bastion line
newBomber.x = GAME_WIDTH / 2 - 400;
newBomber.y = BASTION_Y - 100;
game.addChild(newBomber);
bombers.push(newBomber);
}
// Helper function to add an XBOW
function addXBOW() {
console.log("XBOW upgrade chosen!");
var newXBOW = new XBOW();
// Position at the center of the bastion
newXBOW.x = GAME_WIDTH / 2;
newXBOW.y = BASTION_Y - 100;
game.addChild(newXBOW);
xbows.push(newXBOW);
}
// Helper function to enable XBOW Smart Targeting
function enableXBOWSmartTargeting() {
console.log("XBOW Smart Targeting upgrade chosen!");
xbowSmartTargetingEnabled = true;
// Enable smart targeting for all existing XBOWs
for (var i = 0; i < xbows.length; i++) {
xbows[i].smartTargetingEnabled = true;
// Randomly assign targeting mode for variety
xbows[i].targetingMode = Math.random() < 0.5 ? 'closest_to_bastion' : 'strongest';
}
}
// Define all possible upgrades
var allUpgrades = [{
id: 'faster_reload',
name: 'Faster Reload',
description: 'Decrease reload time by 20%',
apply: function apply() {
cooldownDuration = Math.max(60, Math.floor(cooldownDuration * 0.8));
}
},
// Min 1 sec cooldown
{
id: 'piercing_shots',
name: 'Piercing Shots',
description: 'Arrows pierce +1 enemy',
apply: function apply() {
arrowPierceLevel++;
}
}, {
id: 'quiver',
name: 'Quiver',
description: '+5 Arrows before reload',
apply: function apply() {
maxArrowsBeforeCooldown += 5;
}
}, {
id: 'heat_tipped',
name: 'Heat-tipped Arrows',
description: 'Arrows deal +1 damage',
apply: function apply() {
arrowDamage += 1;
console.log("Heat-tipped Arrows upgrade chosen - damage increased to " + arrowDamage);
}
}, {
id: 'archer_ally',
name: 'Archer Ally',
description: 'Gain an allied archer',
apply: addArcherAlly
}, {
id: 'sabotage',
name: 'Sabotage',
description: 'Enemies 10% slower',
apply: function apply() {
enemySpeedMultiplier = Math.max(0.5, enemySpeedMultiplier * 0.9);
applySabotage();
}
},
// Max 50% slow
{
id: 'swordsman',
name: 'Swordsman',
description: 'Gain a melee defender',
apply: addSwordsman
}, {
id: 'more_shots',
name: 'More Shots',
description: 'Shoot 2 arrows at once',
apply: function apply() {
multiShotEnabled = true; // Assuming a global flag for this
}
}, {
id: 'poison_shots',
name: 'Poison Shots',
description: 'Shots deal damage over time',
apply: function apply() {
poisonShotsEnabled = true; // Assuming a global flag for this
}
}, {
id: 'cannon',
name: 'Cannon',
description: 'Gain a cannon that targets the strongest enemy',
apply: addCannon // Assuming a helper function addCannon
}, {
id: 'ally_atk_speed',
name: 'Ally ATK Speed',
description: 'All allies attack faster',
apply: function apply() {
allyAttackSpeedMultiplier *= 0.8; // Assuming a global multiplier
// Apply to existing allies as well in their update logic or a helper function
}
}, {
id: 'wizard_tower',
name: 'Wizard Tower',
description: 'Gain a wizard tower ally that shoots magic balls that slowdown enemies',
apply: addWizardTower // Assuming a helper function addWizardTower
}, {
id: 'aimbot',
name: 'Aimbot',
description: 'Arrows seek out enemies',
apply: function apply() {
aimbotEnabled = true; // Assuming a global flag for this
}
}, {
id: 'green_killer',
name: 'Green Killer',
description: '+50% Damage vs Green Enemies',
apply: function apply() {
greenKillerEnabled = true;
}
}, {
id: 'refined_projectiles',
name: 'Refined Projectiles',
description: 'Non-arrow projectiles +5 damage & faster',
apply: function apply() {
refinedProjectilesEnabled = true;
// Existing projectiles won't update, new ones will have bonus
}
}, {
id: 'viking_ally',
name: 'Viking Ally',
description: 'Gain a viking that throws piercing axes',
apply: addVikingAlly // Assuming helper function addVikingAlly
}, {
id: 'green_slowdown',
name: 'Green Slowdown',
description: 'Projectiles slow Green enemies 10% for 10s (no stack)',
apply: function apply() {
greenSlowdownEnabled = true;
}
}, {
id: 'dart_shooter',
name: 'Dart Shooter',
description: 'Gain an ally that shoots fast darts, effective against green enemies',
apply: addDartAlly // Assuming a helper function addDartAlly
}, {
id: 'angel_of_light',
name: 'Angel of Light',
description: 'Gain an ally that stuns enemies and deals high damage to dark enemies',
apply: addAngelOfLight // Assuming a helper function addAngelOfLight
}, {
id: 'bouncy_ball',
name: 'Bouncy Ball',
description: 'Bounces around dealing massive damage to enemies',
apply: addBouncyBall
}, {
id: 'bomber',
name: 'Bomber',
description: 'Throws bombs that explode for area damage',
apply: addBomber
}, {
id: 'xbow',
name: 'XBOW',
description: 'A big bow that rapidly shoots arrows',
apply: addXBOW
}, {
id: 'xbow_smart_targeting',
name: 'XBOW Smart Targeting',
description: 'XBOWs can target closest to bastion or strongest enemy',
apply: enableXBOWSmartTargeting
}, {
id: 'dragon_slayer_cannons',
name: 'Dragon Slayer Cannons',
description: 'Cannons become rocket launchers that deal 25x damage to dragons and prioritize them',
apply: function apply() {
// Enable dragon slayer mode for all existing cannons
for (var i = 0; i < cannons.length; i++) {
cannons[i].dragonSlayerMode = true;
}
// Set global flag so all future cannons will have dragon slayer mode
dragonSlayerCannonsModeEnabled = true;
}
}, {
id: 'battle_horn',
name: 'Battle Horn',
description: 'More enemies spawn, but they are all swordsmen: more score for more upgrades!',
apply: function apply() {
// Reduce spawn interval for more enemies
enemySpawnInterval = Math.max(15, enemySpawnInterval * 0.5); // 50% faster spawning, min 0.25 seconds
// Set global flag to force swordsman spawns
battleHornEnabled = true;
}
}];
// Function to create and show the upgrade selection popup
function showUpgradePopup() {
isUpgradePopupActive = true;
// Simple pause: Game logic checks isUpgradePopupActive flag in game.update
// Switch to upgrade theme music with fade effect
if (currentMusicPlaying !== 'UpgradeTheme' && !musicFadeInProgress) {
musicFadeInProgress = true;
// Fade out current music
LK.playMusic('Gamemusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// After fade out completes, start upgrade theme with fade in
LK.setTimeout(function () {
LK.playMusic('UpgradeTheme', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
currentMusicPlaying = 'UpgradeTheme';
musicFadeInProgress = false;
}, 850);
}
// --- Create Popup UI ---
upgradePopup = new Container();
upgradePopup.x = GAME_WIDTH / 2;
upgradePopup.y = GAME_HEIGHT / 2;
game.addChild(upgradePopup); // Add to game layer for positioning relative to center
// Add a semi-transparent background overlay
var bg = upgradePopup.attachAsset('bastionLine', {
// Reusing an asset for shape
width: 1200,
height: 800,
color: 0x000000,
// Black background
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Add title text
var title = new Text2('Choose an Upgrade!', {
size: 80,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -300; // Position relative to popup center
upgradePopup.addChild(title);
// --- Select 3 Random Unique Upgrades ---
var availableToOffer = allUpgrades.slice(); // Copy available upgrades
var choices = [];
var numChoices = Math.min(3, availableToOffer.length); // Offer up to 3 choices
// Make dragon slayer cannons more common past score 100
var currentScore = LK.getScore();
if (currentScore > 100 && !dragonSlayerCannonsModeEnabled) {
// Add dragon slayer cannons upgrade multiple times to increase chances
var dragonSlayerUpgrade = null;
for (var ds = 0; ds < availableToOffer.length; ds++) {
if (availableToOffer[ds].id === 'dragon_slayer_cannons') {
dragonSlayerUpgrade = availableToOffer[ds];
break;
}
}
if (dragonSlayerUpgrade) {
// Add it 2 more times to the pool (3x total chance)
availableToOffer.push(dragonSlayerUpgrade);
availableToOffer.push(dragonSlayerUpgrade);
}
}
for (var i = 0; i < numChoices; i++) {
var randomIndex = Math.floor(Math.random() * availableToOffer.length);
choices.push(availableToOffer[randomIndex]);
availableToOffer.splice(randomIndex, 1); // Remove chosen upgrade to ensure uniqueness
}
// --- Create Buttons for Choices ---
var buttonYStart = -150;
var buttonSpacing = 180;
for (var j = 0; j < choices.length; j++) {
var upgrade = choices[j];
var button = createUpgradeButton(upgrade, buttonYStart + j * buttonSpacing);
upgradePopup.addChild(button);
}
}
// Function to create a single upgrade button
function createUpgradeButton(upgradeData, yPos) {
var buttonContainer = new Container();
buttonContainer.y = yPos;
// Button background
var buttonBg = buttonContainer.attachAsset('bastionLine', {
// Reusing asset for shape
width: 800,
height: 150,
color: 0x555555,
anchorX: 0.5,
anchorY: 0.5
});
// Button text (Name + Description)
var nameText = new Text2(upgradeData.name, {
size: 50,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0.5);
nameText.y = -25;
buttonContainer.addChild(nameText);
var descText = new Text2(upgradeData.description, {
size: 35,
fill: 0xCCCCCC
});
descText.anchor.set(0.5, 0.5);
descText.y = 30;
buttonContainer.addChild(descText);
// Make button interactive
buttonContainer.interactive = true; // Needed for down event
// Event handler for button press
buttonContainer.down = function (x, y, obj) {
upgradeData.apply(); // Apply the selected upgrade's effect
hideUpgradePopup(); // Close the popup
};
return buttonContainer;
}
// Function to hide and destroy the upgrade popup
function hideUpgradePopup() {
if (upgradePopup) {
upgradePopup.destroy();
upgradePopup = null;
}
isUpgradePopupActive = false;
// Switch back to game music with fade effect
if (currentMusicPlaying !== 'Gamemusic' && !musicFadeInProgress) {
musicFadeInProgress = true;
// Fade out upgrade theme
LK.playMusic('UpgradeTheme', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// After fade out completes, start game music with fade in
LK.setTimeout(function () {
LK.playMusic('Gamemusic', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
currentMusicPlaying = 'Gamemusic';
musicFadeInProgress = false;
}, 850);
}
// Resume game logic (handled by checking flag in game.update)
}
// --- Upgradepedia ---
var upgradepediaButton;
var upgradepediaPopup = null;
var upgradeListContainer = null;
function showUpgradepedia() {
isUpgradePopupActive = true; // Pause the game while upgradepedia is open
upgradepediaPopup = new Container();
upgradepediaPopup.x = GAME_WIDTH / 2;
upgradepediaPopup.y = GAME_HEIGHT / 2;
game.addChild(upgradepediaPopup);
// Add a semi-transparent background overlay
var bg = upgradepediaPopup.attachAsset('pedia_screen_bg', {
width: 1600,
height: 2000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
});
// Add title text
var title = new Text2('Upgradepedia', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -900;
upgradepediaPopup.addChild(title);
// Add close button
var closeButton = createUpgradepediaCloseButton();
closeButton.x = 750;
closeButton.y = -900;
upgradepediaPopup.addChild(closeButton);
// Container for upgrade details
upgradeListContainer = new Container();
upgradeListContainer.y = -200;
upgradepediaPopup.addChild(upgradeListContainer);
displayUpgradeStats();
}
function hideUpgradepedia() {
if (upgradepediaPopup) {
upgradepediaPopup.destroy();
upgradepediaPopup = null;
}
isUpgradePopupActive = false; // Unpause the game
}
function createUpgradepediaCloseButton() {
var buttonContainer = new Container();
var buttonBg = buttonContainer.attachAsset('pedia_button_bg', {
width: 150,
height: 80,
color: 0xCC0000,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2('X', {
size: 60,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
buttonContainer.addChild(buttonText);
buttonContainer.interactive = true;
buttonContainer.down = function () {
hideUpgradepedia();
};
return buttonContainer;
}
function displayUpgradeStats() {
// Clear any existing content
while (upgradeListContainer.children.length > 0) {
upgradeListContainer.removeChildAt(0);
}
// Current page tracking
var currentUpgrade = 0;
var totalUpgrades = allUpgrades.length;
// Map upgrade IDs to visual assets and additional info
var upgradeAssets = {
'faster_reload': {
asset: 'Reload',
scale: 0.4
},
'piercing_shots': {
asset: 'arrow',
scale: 0.6
},
'quiver': {
asset: 'arrow',
scale: 0.8,
tint: 0xFFD700
},
'heat_tipped': {
asset: 'arrow',
scale: 0.6,
tint: 0xFF4500
},
'archer_ally': {
asset: 'Archer',
scale: 0.8
},
'sabotage': {
asset: 'thief',
scale: 0.8
},
'swordsman': {
asset: 'swordsmanAlly',
scale: 0.8
},
'more_shots': {
asset: 'arrow',
scale: 0.5,
count: 3
},
'poison_shots': {
asset: 'arrow',
scale: 0.6,
tint: 0x00FF00
},
'cannon': {
asset: 'cannon_asset',
scale: 0.8
},
'ally_atk_speed': {
asset: 'viking_axe',
scale: 0.6
},
'wizard_tower': {
asset: 'wizard_tower_asset',
scale: 0.8
},
'aimbot': {
asset: 'arrow',
scale: 0.6,
special: 'seeking'
},
'green_killer': {
asset: 'arrow',
scale: 0.6,
tint: 0xFF0000
},
'refined_projectiles': {
asset: 'cannonball',
scale: 0.6,
tint: 0xFFD700
},
'viking_ally': {
asset: 'viking_ally',
scale: 0.8
},
'green_slowdown': {
asset: 'magic_ball_asset',
scale: 0.6,
tint: 0x00FF00
},
'dart_shooter': {
asset: 'dart_asset',
scale: 0.8
},
'angel_of_light': {
asset: 'angel_of_light_asset',
scale: 0.8
},
'bouncy_ball': {
asset: 'cannonball',
scale: 1.0,
tint: 0xFF00FF
},
'bomber': {
asset: 'bomber_asset',
scale: 0.8
},
'xbow': {
asset: 'xbow_asset',
scale: 0.8
},
'xbow_smart_targeting': {
asset: 'xbow_asset',
scale: 0.8,
tint: 0x00FFFF
},
'dragon_slayer_cannons': {
asset: 'cannon_asset',
scale: 0.8,
tint: 0xFF0000
},
'battle_horn': {
asset: 'flag_bearer_asset',
scale: 0.8,
tint: 0xFFD700
}
};
function displayUpgradeDetails(index) {
// Clear existing content
while (upgradeListContainer.children.length > 0) {
upgradeListContainer.removeChildAt(0);
}
var upgradeInfo = allUpgrades[index];
var assetInfo = upgradeAssets[upgradeInfo.id] || {
asset: 'arrow',
scale: 0.6
};
// Upgrade display container
var upgradeDisplay = new Container();
upgradeDisplay.y = 0;
upgradeListContainer.addChild(upgradeDisplay);
// Add upgrade graphic
if (assetInfo.count) {
// Special case for multiple projectiles
for (var i = 0; i < assetInfo.count; i++) {
var graphic = upgradeDisplay.attachAsset(assetInfo.asset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: assetInfo.scale,
scaleY: assetInfo.scale,
x: -400 + (i - 1) * 100,
y: 50
});
if (assetInfo.tint) {
graphic.tint = assetInfo.tint;
}
}
} else {
var upgradeGraphic = upgradeDisplay.attachAsset(assetInfo.asset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: assetInfo.scale,
scaleY: assetInfo.scale,
x: -400,
y: 50
});
if (assetInfo.tint) {
upgradeGraphic.tint = assetInfo.tint;
}
// Special visual effects
if (assetInfo.special === 'seeking') {
// Add rotation animation hint for seeking arrows
upgradeGraphic.rotation = Math.PI / 6;
}
}
// Add upgrade name
var nameText = new Text2(upgradeInfo.name, {
size: 70,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0);
nameText.x = 0;
nameText.y = -400;
upgradeDisplay.addChild(nameText);
// Add description
var descText = new Text2(upgradeInfo.description, {
size: 45,
fill: 0xCCCCCC,
wordWrap: true,
wordWrapWidth: 1200
});
descText.anchor.set(0.5, 0);
descText.x = 0;
descText.y = -300;
upgradeDisplay.addChild(descText);
// Add detailed ability description
var abilityText = getDetailedAbility(upgradeInfo.id);
var abilityDisplay = new Text2('Ability: ' + abilityText, {
size: 40,
fill: 0x88FF88,
wordWrap: true,
wordWrapWidth: 1200
});
abilityDisplay.anchor.set(0.5, 0);
abilityDisplay.x = 0;
abilityDisplay.y = -150;
upgradeDisplay.addChild(abilityDisplay);
// Status display
var statusText = new Text2('Upgrade ' + (index + 1) + ' of ' + totalUpgrades, {
size: 35,
fill: 0xAAAAAA
});
statusText.anchor.set(0.5, 0);
statusText.x = 0;
statusText.y = 250;
upgradeDisplay.addChild(statusText);
// Navigation buttons
var navButtons = new Container();
navButtons.y = 350;
upgradeDisplay.addChild(navButtons);
// Previous button
var prevButton = createUpgradeNavButton('← Previous', -250, function () {
currentUpgrade = (currentUpgrade - 1 + totalUpgrades) % totalUpgrades;
displayUpgradeDetails(currentUpgrade);
});
navButtons.addChild(prevButton);
// Next button
var nextButton = createUpgradeNavButton('Next →', 250, function () {
currentUpgrade = (currentUpgrade + 1) % totalUpgrades;
displayUpgradeDetails(currentUpgrade);
});
navButtons.addChild(nextButton);
}
function getDetailedAbility(upgradeId) {
var abilities = {
'faster_reload': 'Reduces the cooldown time between reloads by 20%. Stacks multiplicatively with minimum 1 second cooldown.',
'piercing_shots': 'Each arrow can pierce through one additional enemy. Stacks to pierce multiple enemies.',
'quiver': 'Increases maximum ammo capacity by 5 arrows before needing to reload.',
'heat_tipped': 'Increases arrow damage by 1. Affects all arrow-based attacks including allies.',
'archer_ally': 'Spawns an allied archer that independently targets and shoots at enemies every 3 seconds.',
'sabotage': 'Reduces all enemy movement speed by 10%. Stacks multiplicatively with minimum 50% speed.',
'swordsman': 'Creates a tower that spawns temporary swordsmen allies every 3 seconds. Swordsmen chase and attack nearby enemies.',
'more_shots': 'Fire 3 arrows at once in a spread pattern. Works with all arrow upgrades.',
'poison_shots': 'Arrows apply poison stacks that deal damage over time. Each stack deals damage every 0.5 seconds.',
'cannon': 'Deploys a cannon that targets the strongest enemy and fires high-damage cannonballs every 5 seconds.',
'ally_atk_speed': 'All allies attack 20% faster. Stacks multiplicatively.',
'wizard_tower': 'Builds a tower that shoots magic balls causing slowdown effect on hit. Dragons are immune to slowdown.',
'aimbot': 'Arrows and magic balls automatically seek the nearest enemy. Updates target if current target is destroyed.',
'green_killer': 'Deal 50% extra damage to all Green-tagged enemies (Wizards, Spearmen, War Elephants, Shamans).',
'refined_projectiles': 'Non-arrow projectiles (cannonballs, magic balls, axes) gain +5 damage and +5 speed.',
'viking_ally': 'Summons a viking warrior that throws piercing axes. Each axe can pierce through 3 enemies.',
'green_slowdown': 'Projectiles slow Green-tagged enemies by 10% for 10 seconds. Effect does not stack.',
'dart_shooter': 'Deploys a rapid-fire dart shooter that deals double damage to Green enemies.',
'angel_of_light': 'Summons an angel that stuns enemies for 5 seconds and deals triple damage to Black-tagged enemies. Dragons cannot be stunned.',
'bouncy_ball': 'Releases a bouncing projectile that ricochets off walls dealing massive damage. Lasts 10 seconds or 20 bounces.',
'bomber': 'Deploys a bomber that throws area damage bombs. Cannot directly target Dragons but explosion can damage them.',
'xbow': 'Installs a large crossbow that rapidly fires arrows every 0.33 seconds. Arrows travel 50% faster.',
'xbow_smart_targeting': 'XBOWs can now target either the enemy closest to bastion or the strongest enemy. All other allies have this by default.',
'dragon_slayer_cannons': 'Transforms all current and future cannons into rocket launchers. Rockets deal 25x damage to Dragon-type enemies and prioritize targeting them over all other enemy types.',
'battle_horn': 'Doubles enemy spawn rate but forces all spawned enemies to be basic swordsmen. Great for farming score since swordsmen are easy to defeat!'
};
return abilities[upgradeId] || 'Special ability that enhances your defenses.';
}
function createUpgradeNavButton(label, xPos, callback) {
var button = new Container();
button.x = xPos;
var buttonBg = button.attachAsset('pedia_nav_button_bg', {
width: 250,
height: 80,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2(label, {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.interactive = true;
button.down = callback;
return button;
}
// Initial display
displayUpgradeDetails(currentUpgrade);
}
// Create upgradepedia button
upgradepediaButton = new Container();
upgradepediaButton.x = 200; // Position on the left side
upgradepediaButton.y = GAME_HEIGHT - 200;
// Add button background
var upgradeBtnBg = upgradepediaButton.attachAsset('pedia_button_bg', {
width: 300,
height: 100,
anchorX: 0.5,
anchorY: 0.5
});
// Add button text
var upgradeBtnText = new Text2('Upgradepedia', {
size: 40,
fill: 0xFFFFFF
});
upgradeBtnText.anchor.set(0.5, 0.5);
upgradepediaButton.addChild(upgradeBtnText);
// Make button interactive
upgradepediaButton.interactive = true;
upgradepediaButton.down = function () {
showUpgradepedia();
};
// Add the button to the game scene
game.addChild(upgradepediaButton);
// --- Relic Shop Button ---
var relicShopButton = new Container();
relicShopButton.x = GAME_WIDTH - 200; // Position on the right side
relicShopButton.y = GAME_HEIGHT - 320; // Position above Enemypedia button
var relicBtnBg = relicShopButton.attachAsset('pedia_button_bg', {
width: 300,
height: 100,
anchorX: 0.5,
anchorY: 0.5
});
var relicBtnText = new Text2('Relic Shop', {
size: 40,
fill: 0xFFFFFF
});
relicBtnText.anchor.set(0.5, 0.5);
relicShopButton.addChild(relicBtnText);
relicShopButton.interactive = true;
relicShopButton.down = function () {
showRelicShop();
};
game.addChild(relicShopButton);
// --- Visual Setup ---
var bastionLine = game.addChild(LK.getAsset('bastionLine', {
anchorX: 0.0,
// Anchor at the left edge.
anchorY: 0.5,
// Anchor vertically centered.
x: 0,
// Position at the left edge of the screen.
y: BASTION_Y // Position at the defined bastion Y-coordinate.
}));
// Create and configure the score display text.
scoreTxt = new Text2('0', {
size: 150,
// Font size.
fill: 0xFFFFFF // White color.
});
scoreTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge.
// Add score text to the GUI layer at the top-center position.
LK.gui.top.addChild(scoreTxt);
scoreTxt.y = 30; // Add padding below the top edge. Ensure it's clear of the top-left menu icon area.
// Create and configure the ammo display text.
ammoTxt = new Text2('Ammo: ' + maxArrowsBeforeCooldown, {
size: 80,
fill: 0xFFFFFF // White color.
});
ammoTxt.anchor.set(0.5, 0); // Anchor at the horizontal center, top edge.
LK.gui.top.addChild(ammoTxt);
ammoTxt.y = 180; // Position below the score text
// --- Enemypedia ---
var enemypediaButton;
var enemypediaPopup = null;
var enemyListContainer = null; // Container for enemy list items
function showEnemypedia() {
isUpgradePopupActive = true; // Pause the game while enemypedia is open
enemypediaPopup = new Container();
enemypediaPopup.x = GAME_WIDTH / 2;
enemypediaPopup.y = GAME_HEIGHT / 2;
game.addChild(enemypediaPopup);
// Add a semi-transparent background overlay
var bg = enemypediaPopup.attachAsset('bastionLine', {
width: 1600,
height: 1200,
// Increased height for better layout
color: 0x000000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Add title text
var title = new Text2('Enemypedia', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -500; // Position relative to popup center
enemypediaPopup.addChild(title);
// Add a close button
var closeButton = createCloseButton();
closeButton.x = 750; // Position relative to popup center
closeButton.y = -500;
enemypediaPopup.addChild(closeButton);
// Container for the enemy details
enemyListContainer = new Container();
enemyListContainer.y = -100; // Centered position for detailed view
enemypediaPopup.addChild(enemyListContainer);
displayEnemyStats();
}
function hideEnemypedia() {
if (enemypediaPopup) {
enemypediaPopup.destroy();
enemypediaPopup = null;
}
isUpgradePopupActive = false; // Unpause the game
}
function createCloseButton() {
var buttonContainer = new Container();
var buttonBg = buttonContainer.attachAsset('bastionLine', {
width: 150,
height: 80,
color: 0xCC0000,
// Red color for close
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2('X', {
size: 60,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
buttonContainer.addChild(buttonText);
buttonContainer.interactive = true;
buttonContainer.down = function () {
hideEnemypedia();
};
return buttonContainer;
}
function displayEnemyStats() {
// Dummy data representing enemy types and their stats
var enemyData = [{
type: 'swordsman',
assetId: 'enemy',
description: 'Basic melee unit that has very low HP.',
baseHealth: 1,
baseSpeed: 3,
dodgeChance: 0
}, {
type: 'knight',
assetId: 'knight',
description: 'A threat for the first few waves cause of its high health, but is slow.',
baseHealth: 5,
baseSpeed: 3 * 0.9,
dodgeChance: 0
}, {
type: 'thief',
assetId: 'thief',
description: 'Fast, low health unit with a dodge chance; likes gravy.',
baseHealth: 1,
baseSpeed: 3 * 1.2,
dodgeChance: 0.1
}, {
type: 'boss',
assetId: 'boss',
description: 'a boss that spawns randomly when youre in trobule.',
baseHealth: 5,
// Base health before scaling
baseSpeed: 3 * 0.7,
dodgeChance: 0
}, {
type: 'shield',
assetId: 'shield_enemy',
description: 'Slow unit that has high HP',
baseHealth: 20,
baseSpeed: 3 * 0.5,
dodgeChance: 0
}, {
type: 'wizard',
assetId: 'wizard_enemy',
description: 'kinda tricky cause it teleports and has moderate speed.',
baseHealth: 2,
baseSpeed: 3 * 0.7,
dodgeChance: 0
}, {
type: 'spearman',
assetId: 'spearman',
description: 'Standard, annoying green unit that spawns from Elephants most of the time.',
baseHealth: 2,
baseSpeed: 3 * 1.0,
dodgeChance: 0
}, {
type: 'war_elephant',
assetId: 'war_elephant',
description: 'Extremely high health elephant, spawns spearmen on death',
baseHealth: 500,
baseSpeed: 3 * 0.3,
dodgeChance: 0
}, {
type: 'elite_knight',
assetId: 'elite_knight_asset',
description: 'High health knight variant that is loyal to the king',
baseHealth: 45,
baseSpeed: 3 * 0.85,
dodgeChance: 0
}, {
type: 'elite_shield',
assetId: 'elite_shield_asset',
description: 'Even higher health shield variant that is loyal to the king',
baseHealth: 200,
baseSpeed: 3 * 0.4,
dodgeChance: 0
}, {
type: 'shaman',
assetId: 'shaman_enemy',
description: 'Reduces your ammo when he feels like it',
baseHealth: 2,
baseSpeed: 3 * 0.8,
dodgeChance: 0
}, {
type: 'hot_air_balloon',
assetId: 'hot_air_balloon_asset',
description: 'Very low health. Spawns random non-green enemies on death.',
baseHealth: 1,
baseSpeed: 3 * 0.2,
dodgeChance: 0
}, {
type: 'dark_bowman',
assetId: 'dark_bowman_asset',
description: 'Immune to arrows and cannonballs. Worst enemy of blue archers',
baseHealth: 5,
baseSpeed: 3 * 1.1,
dodgeChance: 0
}, {
type: 'jester',
assetId: 'jester_asset',
description: 'High chance to dodge or reflect non-cannonball projectiles...YOU WILL LAUGH AT ALL OF HIS TRICKS',
baseHealth: 2,
baseSpeed: 3 * 1.1,
dodgeChance: 0.3,
// 30% dodge
reflectChance: 0.2 // 20% reflect chance for non-cannonballs
}, {
type: 'dark_war_elephant',
assetId: 'dark_war_elephant_asset',
description: 'Immune to arrows. variant of green elephant, spawns Dark Bowmen on death.',
baseHealth: 450,
// Slightly less than green elephant
baseSpeed: 3 * 0.3,
// Same slow speed
dodgeChance: 0
}, {
type: 'dark_spearman',
assetId: 'dark_spearman_asset',
description: 'A faster and more resistant spearman variant that came straight from rome. Immune to arrows and cannonballs',
baseHealth: 15,
// Base health
baseSpeed: 3 * 1.2,
// Faster than spearman
dodgeChance: 0,
tag: 'Black' // Immune to arrows and cannonballs
}, {
type: 'dragon',
assetId: 'dragon_asset',
description: 'A fearsome dragon with great HP and a high chance to dodge attacks. Cant be slowed down',
baseHealth: 250,
// Great HP
baseSpeed: 3 * 0.9,
// Moderately fast
dodgeChance: 0.40,
// High dodge chance (40%)
tag: 'Dragon' // Special tag if needed for other mechanics, for now dodge is key
}, {
type: 'flag_bearer',
assetId: 'flag_bearer_asset',
description: 'Green-tagged enemy with an aura that makes nearby enemies move 50% faster',
baseHealth: 5,
baseSpeed: 3 * 0.8,
dodgeChance: 0,
tag: 'Green'
}, {
type: 'baby_dragon',
assetId: 'baby_dragon_asset',
description: 'Small dragon with less health but enters rage mode (double speed) when no other dragons are present',
baseHealth: 50,
baseSpeed: 3 * 1.1,
dodgeChance: 0.25,
tag: 'Dragon'
}];
// Clear any existing content in the list container
while (enemyListContainer.children.length > 0) {
enemyListContainer.removeChildAt(0);
}
// Current page tracking
var currentEnemy = 0;
var totalEnemies = enemyData.length;
// Create enemy details display
function displayEnemyDetails(index) {
// Clear any existing content in the list container
while (enemyListContainer.children.length > 0) {
enemyListContainer.removeChildAt(0);
}
var enemyInfo = enemyData[index];
// Enemy display container
var enemyDisplay = new Container();
enemyDisplay.y = 0;
enemyListContainer.addChild(enemyDisplay);
// Add enemy graphic (larger for individual view)
var enemyGraphic = enemyDisplay.attachAsset(enemyInfo.assetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8,
x: -400,
y: 50
});
// Add enemy name (larger font)
var nameText = new Text2(enemyInfo.type.replace(/_/g, ' ').toUpperCase(), {
size: 60,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0);
nameText.x = 0;
nameText.y = -300;
enemyDisplay.addChild(nameText);
// Add description
var descText = new Text2('Description: ' + enemyInfo.description, {
size: 40,
fill: 0xCCCCCC,
wordWrap: true,
wordWrapWidth: 1000
});
descText.anchor.set(0, 0);
descText.x = -100;
descText.y = -200;
enemyDisplay.addChild(descText);
// Add stats with improved layout
var healthText = new Text2('Health: ' + enemyInfo.baseHealth, {
size: 40,
fill: 0x88FF88
});
healthText.anchor.set(0, 0);
healthText.x = -100;
healthText.y = -100;
enemyDisplay.addChild(healthText);
var speedText = new Text2('Speed: ' + enemyInfo.baseSpeed.toFixed(1), {
size: 40,
fill: 0x88CCFF
});
speedText.anchor.set(0, 0);
speedText.x = -100;
speedText.y = -40;
enemyDisplay.addChild(speedText);
var dodgeText = new Text2('Dodge Chance: ' + (enemyInfo.dodgeChance * 100).toFixed(0) + '%', {
size: 40,
fill: 0xFFCC88
});
dodgeText.anchor.set(0, 0);
dodgeText.x = -100;
dodgeText.y = 20;
enemyDisplay.addChild(dodgeText);
// Add reflect chance
var reflectText = new Text2('Reflect Chance: ' + (enemyInfo.reflectChance * 100 || 0).toFixed(0) + '%', {
size: 40,
fill: 0xFFCCEE
});
reflectText.anchor.set(0, 0);
reflectText.x = -100;
reflectText.y = 80;
enemyDisplay.addChild(reflectText);
// Add unlock score
var unlockScore = 0; // Default for swordsman
if (enemyInfo.type === 'spearman') {
unlockScore = 10;
} else if (enemyInfo.type === 'thief') {
unlockScore = 15;
} else if (enemyInfo.type === 'knight') {
unlockScore = 23;
} else if (enemyInfo.type === 'wizard') {
unlockScore = 25;
} else if (enemyInfo.type === 'shield') {
unlockScore = 30;
} else if (enemyInfo.type === 'shaman') {
unlockScore = 35;
} else if (enemyInfo.type === 'dark_spearman') {
unlockScore = 55;
} // Dark Spearman unlock score
else if (enemyInfo.type === 'dark_bowman') {
unlockScore = 55;
} // Note: Same as Dark Spearman, might need adjustment if only one is desired at 55. Keeping as per request.
else if (enemyInfo.type === 'jester') {
unlockScore = 69;
} else if (enemyInfo.type === 'dragon') {
unlockScore = 134;
} // Dragon unlock score
else if (enemyInfo.type === 'flag_bearer') {
unlockScore = 27;
} // Flag Bearer unlock score
else if (enemyInfo.type === 'baby_dragon') {
unlockScore = 125;
} // Baby Dragon unlock score (same as war elephant)
else if (enemyInfo.type === 'elite_knight') {
unlockScore = 100;
} else if (enemyInfo.type === 'elite_shield') {
unlockScore = 112;
} else if (enemyInfo.type === 'war_elephant') {
unlockScore = 125;
} else if (enemyInfo.type === 'dark_war_elephant') {
unlockScore = 145;
} // Dark war elephant starts appearing at score 145
else if (enemyInfo.type === 'hot_air_balloon') {
unlockScore = 154;
}
var unlockText = new Text2('Unlocks at Score: ' + unlockScore, {
size: 40,
fill: 0xFF88FF
});
unlockText.anchor.set(0, 0);
unlockText.x = -100;
unlockText.y = 140; // Adjusted Y position
enemyDisplay.addChild(unlockText);
// Status display (current enemy / total)
var statusText = new Text2('Enemy ' + (index + 1) + ' of ' + totalEnemies, {
size: 30,
fill: 0xAAAAAA
});
statusText.anchor.set(0.5, 0);
statusText.x = 0;
statusText.y = 150;
enemyDisplay.addChild(statusText);
// Navigation buttons container
var navButtons = new Container();
navButtons.y = 250;
enemyDisplay.addChild(navButtons);
// Previous button
var prevButton = createNavButton('← Previous', -250, function () {
currentEnemy = (currentEnemy - 1 + totalEnemies) % totalEnemies;
displayEnemyDetails(currentEnemy);
});
navButtons.addChild(prevButton);
// Next button
var nextButton = createNavButton('Next →', 250, function () {
currentEnemy = (currentEnemy + 1) % totalEnemies;
displayEnemyDetails(currentEnemy);
});
navButtons.addChild(nextButton);
}
// Create navigation button
function createNavButton(label, xPos, callback) {
var button = new Container();
button.x = xPos;
var buttonBg = button.attachAsset('bastionLine', {
width: 250,
height: 80,
color: 0x555555,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2(label, {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.interactive = true;
button.down = callback;
return button;
}
// Initial display
displayEnemyDetails(currentEnemy);
}
enemypediaButton = new Container();
// Position the button (e.g., bottom right of GUI)
enemypediaButton.x = GAME_WIDTH - 200; // Example X
enemypediaButton.y = GAME_HEIGHT - 200; // Example Y
// Add button background
var buttonBg = enemypediaButton.attachAsset('bastionLine', {
// Reusing asset for shape
width: 300,
height: 100,
color: 0x444444,
// Grey color
anchorX: 0.5,
anchorY: 0.5
});
// Add button text
var buttonText = new Text2('Enemypedia', {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
enemypediaButton.addChild(buttonText);
// Make button interactive
enemypediaButton.interactive = true;
enemypediaButton.down = function () {
showEnemypedia();
};
// Add the button to the game scene (or GUI if needed)
game.addChild(enemypediaButton);
// --- Relic Shop ---
var relicShopPopup = null;
var relicListContainer = null;
var goldText; // Text display for gold
function showRelicShop() {
isUpgradePopupActive = true; // Pause the game
relicShopPopup = new Container();
relicShopPopup.x = GAME_WIDTH / 2;
relicShopPopup.y = GAME_HEIGHT / 2;
game.addChild(relicShopPopup);
var bg = relicShopPopup.attachAsset('pedia_screen_bg', {
width: 1600,
height: 2000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
});
var title = new Text2('Relic Shop', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.y = -900;
relicShopPopup.addChild(title);
var closeButton = createRelicShopCloseButton();
closeButton.x = 750;
closeButton.y = -900;
relicShopPopup.addChild(closeButton);
// Gold display
goldText = new Text2('Gold: ' + playerGold, {
size: 50,
fill: 0xFFD700
});
goldText.anchor.set(0.5, 0.5);
goldText.y = -800;
relicShopPopup.addChild(goldText);
relicListContainer = new Container();
relicListContainer.y = -200;
relicShopPopup.addChild(relicListContainer);
displayRelicDetails();
}
function hideRelicShop() {
if (relicShopPopup) {
relicShopPopup.destroy();
relicShopPopup = null;
}
isUpgradePopupActive = false; // Unpause the game
}
function createRelicShopCloseButton() {
var buttonContainer = new Container();
var buttonBg = buttonContainer.attachAsset('pedia_button_bg', {
width: 150,
height: 80,
color: 0xCC0000,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2('X', {
size: 60,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
buttonContainer.addChild(buttonText);
buttonContainer.interactive = true;
buttonContainer.down = function () {
hideRelicShop();
};
return buttonContainer;
}
function displayRelicDetails() {
while (relicListContainer.children.length > 0) {
relicListContainer.removeChildAt(0);
}
var relicsData = [{
id: 'damage',
name: 'Damage Relic',
description: 'Increase projectile, swordsman, and poison damage.',
effect: '+1 damage per level'
}, {
id: 'slowdown',
name: 'Slowdown Relic',
description: 'All enemies are slower.',
effect: '+2% slowdown per level'
}, {
id: 'green',
name: 'Green Relic',
description: 'Green enemies are slower and take more damage.',
effect: '+2% slow & +2% damage per level'
}, {
id: 'dark',
name: 'Dark Relic',
description: 'Dark enemies can be hit by arrows/cannonballs and take more damage from them.',
effect: '+5% damage per level'
}, {
id: 'dragon',
name: 'Dragon Relic',
description: 'Projectiles stun dragons.',
effect: '+0.05 seconds stun per level'
}, {
id: 'reload',
name: 'Reload Relic',
description: 'Reload faster! Essential for rapid-fire gameplay.',
effect: '-0.5 seconds reload time per level'
}, {
id: 'ammo',
name: 'Ammo Relic',
description: 'Regenerate ammo over time without reloading.',
effect: '+1 ammo regenerated every 5 seconds per level'
}, {
id: 'swordsman',
name: 'Swordsman Relic',
description: 'Start each battle with a permanent swordsman ally.',
effect: 'Unlocks permanent swordsman, +2 damage per upgrade'
}];
// Current page tracking
var currentRelic = 0;
var totalRelics = relicsData.length;
// Create relic details display
function displayRelicItem(index) {
// Clear any existing content in the list container
while (relicListContainer.children.length > 0) {
relicListContainer.removeChildAt(0);
}
var relic = relicsData[index];
var relicInfo = playerRelics[relic.id];
// Relic display container
var relicDisplay = new Container();
relicDisplay.y = 0;
relicListContainer.addChild(relicDisplay);
// Background
var bg = relicDisplay.attachAsset('pedia_button_bg', {
width: 1400,
height: 400,
color: 0x333355,
anchorX: 0.5,
anchorY: 0.5
});
// Relic Name
var nameText = new Text2(relic.name + ' (Level ' + relicInfo.level + '/' + RELIC_MAX_LEVEL + ')', {
size: 70,
fill: 0xFFFFFF
});
nameText.anchor.set(0.5, 0);
nameText.x = 0;
nameText.y = -300;
relicDisplay.addChild(nameText);
// Relic Description
var descText = new Text2(relic.description, {
size: 45,
fill: 0xCCCCCC,
wordWrap: true,
wordWrapWidth: 1200
});
descText.anchor.set(0.5, 0);
descText.x = 0;
descText.y = -200;
relicDisplay.addChild(descText);
// Relic Effect
var effectText = new Text2('Effect: ' + relic.effect, {
size: 40,
fill: 0x88FF88,
wordWrap: true,
wordWrapWidth: 1200
});
effectText.anchor.set(0.5, 0);
effectText.x = 0;
effectText.y = -100;
relicDisplay.addChild(effectText);
// Buttons container
var buttonsContainer = new Container();
buttonsContainer.y = 0;
relicDisplay.addChild(buttonsContainer);
// Buy/Upgrade Button
var buyButton = new Container();
buyButton.x = -200;
var buyBtnBgColor = relicInfo.level < RELIC_MAX_LEVEL ? 0x00CC00 : 0x666666;
var buyBtnTextColor = relicInfo.level < RELIC_MAX_LEVEL ? 0xFFFFFF : 0xAAAAAA;
var buyBtnBg = buyButton.attachAsset('pedia_nav_button_bg', {
width: 200,
height: 80,
color: buyBtnBgColor,
anchorX: 0.5,
anchorY: 0.5
});
var buyBtnText = new Text2(relicInfo.level < RELIC_MAX_LEVEL ? 'Buy (5)' : 'MAX', {
size: 40,
fill: buyBtnTextColor
});
buyBtnText.anchor.set(0.5, 0.5);
buyButton.addChild(buyBtnText);
buyButton.interactive = relicInfo.level < RELIC_MAX_LEVEL && playerGold >= RELIC_COST_PER_LEVEL;
if (buyButton.interactive) {
buyButton.down = function (relicId) {
return function () {
if (playerGold >= RELIC_COST_PER_LEVEL && playerRelics[relicId].level < RELIC_MAX_LEVEL) {
playerGold -= RELIC_COST_PER_LEVEL;
playerRelics[relicId].level++;
storage.playerGold = playerGold;
// Reset ammo relic timer if ammo relic was upgraded
if (relicId === 'ammo') {
ammoRelicTimer = 0; // Reset timer when upgrading
}
// Save relics to storage using safe serialization
try {
var relicsData = {};
relicsData.damage_level = playerRelics.damage.level;
relicsData.damage_enabled = playerRelics.damage.enabled;
relicsData.slowdown_level = playerRelics.slowdown.level;
relicsData.slowdown_enabled = playerRelics.slowdown.enabled;
relicsData.green_level = playerRelics.green.level;
relicsData.green_enabled = playerRelics.green.enabled;
relicsData.dark_level = playerRelics.dark.level;
relicsData.dark_enabled = playerRelics.dark.enabled;
relicsData.dragon_level = playerRelics.dragon.level;
relicsData.dragon_enabled = playerRelics.dragon.enabled;
relicsData.reload_level = playerRelics.reload.level;
relicsData.reload_enabled = playerRelics.reload.enabled;
relicsData.ammo_level = playerRelics.ammo.level;
relicsData.ammo_enabled = playerRelics.ammo.enabled;
relicsData.swordsman_level = playerRelics.swordsman.level;
relicsData.swordsman_enabled = playerRelics.swordsman.enabled;
storage.playerRelicsData = relicsData;
} catch (e) {
console.log("Error saving relics:", e);
}
goldText.setText('Gold: ' + playerGold);
displayRelicItem(currentRelic); // Refresh the current relic display
}
};
}(relic.id);
}
buttonsContainer.addChild(buyButton);
// On/Off Button
var toggleButton = new Container();
toggleButton.x = 200;
var toggleBtnBgColor = relicInfo.enabled ? 0x008800 : 0x880000;
var toggleBtnBg = toggleButton.attachAsset('pedia_nav_button_bg', {
width: 150,
height: 80,
color: toggleBtnBgColor,
anchorX: 0.5,
anchorY: 0.5
});
var toggleBtnText = new Text2(relicInfo.enabled ? 'ON' : 'OFF', {
size: 40,
fill: 0xFFFFFF
});
toggleBtnText.anchor.set(0.5, 0.5);
toggleButton.addChild(toggleBtnText);
toggleButton.interactive = relicInfo.level > 0; // Only interactive if relic is owned
if (toggleButton.interactive) {
toggleButton.down = function (relicId) {
return function () {
playerRelics[relicId].enabled = !playerRelics[relicId].enabled;
// Reset ammo relic timer if ammo relic was toggled
if (relicId === 'ammo') {
ammoRelicTimer = 0; // Reset timer when toggling
}
// Save relics to storage using safe serialization
try {
var relicsData = {};
relicsData.damage_level = playerRelics.damage.level;
relicsData.damage_enabled = playerRelics.damage.enabled;
relicsData.slowdown_level = playerRelics.slowdown.level;
relicsData.slowdown_enabled = playerRelics.slowdown.enabled;
relicsData.green_level = playerRelics.green.level;
relicsData.green_enabled = playerRelics.green.enabled;
relicsData.dark_level = playerRelics.dark.level;
relicsData.dark_enabled = playerRelics.dark.enabled;
relicsData.dragon_level = playerRelics.dragon.level;
relicsData.dragon_enabled = playerRelics.dragon.enabled;
relicsData.reload_level = playerRelics.reload.level;
relicsData.reload_enabled = playerRelics.reload.enabled;
relicsData.ammo_level = playerRelics.ammo.level;
relicsData.ammo_enabled = playerRelics.ammo.enabled;
relicsData.swordsman_level = playerRelics.swordsman.level;
relicsData.swordsman_enabled = playerRelics.swordsman.enabled;
storage.playerRelicsData = relicsData;
} catch (e) {
console.log("Error saving relics:", e);
}
displayRelicItem(currentRelic); // Refresh the current relic display
};
}(relic.id);
} else {
toggleBtnBg.tint = 0x666666; // Grey out if not owned
}
buttonsContainer.addChild(toggleButton);
// Status display (current relic / total)
var statusText = new Text2('Relic ' + (index + 1) + ' of ' + totalRelics, {
size: 35,
fill: 0xAAAAAA
});
statusText.anchor.set(0.5, 0);
statusText.x = 0;
statusText.y = 150;
relicDisplay.addChild(statusText);
// Navigation buttons container
var navButtons = new Container();
navButtons.y = 250;
relicDisplay.addChild(navButtons);
// Previous button
var prevButton = createRelicNavButton('← Previous', -250, function () {
currentRelic = (currentRelic - 1 + totalRelics) % totalRelics;
displayRelicItem(currentRelic);
});
navButtons.addChild(prevButton);
// Next button
var nextButton = createRelicNavButton('Next →', 250, function () {
currentRelic = (currentRelic + 1) % totalRelics;
displayRelicItem(currentRelic);
});
navButtons.addChild(nextButton);
}
function createRelicNavButton(label, xPos, callback) {
var button = new Container();
button.x = xPos;
var buttonBg = button.attachAsset('pedia_nav_button_bg', {
width: 250,
height: 80,
anchorX: 0.5,
anchorY: 0.5
});
var buttonText = new Text2(label, {
size: 40,
fill: 0xFFFFFF
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.interactive = true;
button.down = callback;
return button;
}
// Initial display
displayRelicItem(currentRelic);
}
// --- Input Event Handlers ---
// Handles touch/mouse press down event on the game area.
game.down = function (x, y, obj) {
// Record the starting position of the drag in game coordinates.
var gamePos = game.toLocal({
x: x,
y: y
});
dragStartX = gamePos.x;
dragStartY = gamePos.y;
// Potential enhancement: Show an aiming indicator originating from FIRING_POS_X, FIRING_POS_Y towards the drag point.
};
// Handles touch/mouse move event while dragging on the game area.
game.move = function (x, y, obj) {
// Only process if a drag is currently active.
if (dragStartX !== null) {
var gamePos = game.toLocal({
x: x,
y: y
});
// Potential enhancement: Update the aiming indicator based on the current drag position (gamePos).
}
};
// Handles touch/mouse release event on the game area.
game.up = function (x, y, obj) {
// Only process if a drag was active.
if (dragStartX !== null && cooldownTimer <= 0) {
// Only allow firing if not on cooldown.
var gamePos = game.toLocal({
x: x,
y: y
});
var dragEndX = gamePos.x;
var dragEndY = gamePos.y;
// Calculate the difference between the firing position and the release point to determine direction.
// A longer drag could potentially mean more power, but we'll keep it simple: direction only.
var dx = dragEndX - FIRING_POS_X;
var dy = dragEndY - FIRING_POS_Y;
// Avoid division by zero or zero vector if start and end points are the same.
if (dx === 0 && dy === 0) {
// Optionally handle this case, e.g., fire straight up or do nothing.
// Let's fire straight up if drag distance is negligible.
dy = -1;
}
// Calculate the angle using atan2. Note the order (dx, dy) and the negation of dy
// because the Y-axis is inverted in screen coordinates (positive Y is down).
var angle = Math.atan2(dx, -dy);
// Create a new arrow instance with the calculated angle.
var newArrow = new Arrow(angle);
newArrow.x = FIRING_POS_X;
newArrow.y = FIRING_POS_Y;
// Initialize last position for state tracking (e.g., off-screen detection)
newArrow.lastY = newArrow.y;
newArrow.lastX = newArrow.x;
// Add the arrow to the game scene and the tracking array.
game.addChild(newArrow);
arrows.push(newArrow);
if (multiShotEnabled) {
// Fire a second arrow with a slight angle offset
var angle2 = angle + Math.PI / 12; // Offset by 15 degrees
var newArrow2 = new Arrow(angle2);
newArrow2.x = FIRING_POS_X;
newArrow2.y = FIRING_POS_Y;
newArrow2.lastY = newArrow2.y;
newArrow2.lastX = newArrow2.x;
game.addChild(newArrow2);
arrows.push(newArrow2);
// Fire a third arrow with the opposite angle offset
var angle3 = angle - Math.PI / 12; // Offset by -15 degrees
var newArrow3 = new Arrow(angle3);
newArrow3.x = FIRING_POS_X;
newArrow3.y = FIRING_POS_Y;
newArrow3.lastY = newArrow3.y;
newArrow3.lastX = newArrow3.x;
game.addChild(newArrow3);
arrows.push(newArrow3);
}
LK.getSound('shoot').play(); // Play shooting sound.
arrowsFired++; // Increment arrow count.
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display.
// Check if cooldown needs to start based on Quiver upgrade
if (arrowsFired >= maxArrowsBeforeCooldown) {
var effectiveCooldownDuration = cooldownDuration;
// Apply reload relic bonus if enabled
if (playerRelics.reload.enabled) {
effectiveCooldownDuration = Math.max(30, cooldownDuration - playerRelics.reload.level * 30); // 0.5 seconds faster per level, min 0.5s
}
cooldownTimer = effectiveCooldownDuration; // Start cooldown (duration affected by Faster Reload upgrade and reload relic)
arrowsFired = 0; // Reset arrow count.
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)); // Update ammo display.
LK.getSound('Reload').play(); // Play reload sound.
}
// Reset drag state.
dragStartX = null;
dragStartY = null;
// Potential enhancement: Hide the aiming indicator.
}
};
// --- Main Game Update Loop ---
// This function is called automatically by the LK engine on every frame (tick).
game.update = function () {
// --- Upgrade System Check ---
var currentScore = LK.getScore();
// Check if score reached a multiple of 10 and is higher than the last upgrade score
if (!isUpgradePopupActive && currentScore > 0 && currentScore % 10 === 0 && currentScore !== lastUpgradeScore) {
lastUpgradeScore = currentScore; // Mark this score level as having triggered an upgrade offer
showUpgradePopup(); // Show the upgrade selection screen
}
// --- Pause Game Logic if Upgrade Popup is Active ---
if (isUpgradePopupActive) {
// Potential: Update UI animations if any
return; // Skip the rest of the game update loop
}
// --- Resume Game Logic ---
if (isUpgradePopupActive) {
return; // Skip enemy movement and spawning if upgrade popup is active
}
// Handle ammo relic regeneration
if (playerRelics.ammo.enabled && playerRelics.ammo.level > 0) {
ammoRelicTimer++;
// Give ammo every 5 seconds (300 ticks)
if (ammoRelicTimer >= 300) {
ammoRelicTimer = 0;
// Only regenerate if not at max ammo and not reloading
if (arrowsFired > 0 && cooldownTimer <= 0) {
// Give ammo based on relic level (1 per level)
var ammoToRegenerate = playerRelics.ammo.level;
arrowsFired = Math.max(0, arrowsFired - ammoToRegenerate);
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
}
}
}
// Decrease cooldown timer if active.
if (cooldownTimer > 0) {
cooldownTimer--;
ammoTxt.setText('Reloading...'); // Show reloading status
} else if (ammoTxt.text !== 'Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired)) {
// Ensure ammo display is correct if not reloading
ammoTxt.setText('Ammo: ' + (maxArrowsBeforeCooldown - arrowsFired));
}
// 1. Update and check Arrows
for (var i = arrows.length - 1; i >= 0; i--) {
var arrow = arrows[i];
// Note: LK engine calls arrow.update() automatically as it's added to the game stage.
// Initialize last position if it hasn't been set yet (first frame).
if (arrow.lastY === undefined) {
arrow.lastY = arrow.y;
}
if (arrow.lastX === undefined) {
arrow.lastX = arrow.x;
}
// Check for collisions between the current arrow and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (arrow.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Calculate damage, applying Green Killer bonus if applicable
var damageToDeal = arrow.damage;
if (greenKillerEnabled && enemy.tag === 'Green') {
damageToDeal *= 1.5; // Apply 50% damage bonus
}
// Apply damage to the enemy and check if it was defeated
var enemyDefeated = enemy.takeDamage(damageToDeal, arrow); // Pass the arrow object as source
// Check if the arrow was reflected (takeDamage returns false and arrow is destroyed)
if (!enemyDefeated && !arrow.parent) {
// Arrow was reflected, remove it from the array and stop processing
arrows.splice(i, 1);
hitEnemy = true; // Mark that this arrow is done
break; // Stop checking this arrow against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Handle arrow piercing
arrow.pierceLeft--; // Decrease remaining pierces
if (arrow.pierceLeft <= 0) {
// Arrow has no pierces left, destroy it
arrow.destroy();
arrows.splice(i, 1); // Remove arrow from array.
hitEnemy = true; // Mark that this arrow is done
break; // Stop checking this arrow against other enemies as it's destroyed
} else {
// Arrow pierced this enemy and can continue
// We don't set hitEnemy = true here because the arrow continues
// We don't break because it might hit another enemy in the same frame further along its path
// Apply Green Slowdown if applicable and arrow is still piercing
if (!enemyDefeated && greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
}
}
}
// If the arrow hit an enemy, it was destroyed, so skip to the next arrow.
if (hitEnemy) {
continue;
}
// Check if the arrow has gone off-screen (top, left, or right).
// Use transition detection: check if it was on screen last frame and is off screen now.
var wasOnScreen = arrow.lastY > -arrow.height / 2 && arrow.lastX > -arrow.width / 2 && arrow.lastX < GAME_WIDTH + arrow.width / 2;
var isOffScreen = arrow.y < -arrow.height / 2 ||
// Off top edge
arrow.x < -arrow.width / 2 ||
// Off left edge
arrow.x > GAME_WIDTH + arrow.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Arrow is off-screen, destroy it and remove from the array.
arrow.destroy();
arrows.splice(i, 1);
} else if (!isOffScreen) {
// Update last known position only if the arrow is still potentially on screen
arrow.lastY = arrow.y;
arrow.lastX = arrow.x;
}
}
// 2. Update and check Cannonballs
for (var cb = cannonballs.length - 1; cb >= 0; cb--) {
var cannonball = cannonballs[cb];
// Note: LK engine calls cannonball.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (cannonball.lastY === undefined) {
cannonball.lastY = cannonball.y;
}
if (cannonball.lastX === undefined) {
cannonball.lastX = cannonball.x;
}
// Check for collisions between the current cannonball and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (cannonball.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply damage to the enemy and check if it was defeated
var damageToApply = cannonball.damage;
// Apply dragon slayer damage multiplier if applicable
if (cannonball.dragonDamageMultiplier && enemy.tag === 'Dragon') {
damageToApply *= cannonball.dragonDamageMultiplier;
}
var enemyDefeated = enemy.takeDamage(damageToApply, cannonball); // Pass cannonball as source
// Check if the cannonball had no effect (e.g. Dark War Elephant immunity) and is still in game
if (!enemyDefeated && cannonball.parent) {
// Cannonball had no effect, but wasn't destroyed by a normal hit.
// It might have been immune. If so, we still destroy the cannonball.
cannonball.destroy();
cannonballs.splice(cb, 1);
hitEnemy = true; // Mark that this cannonball is done
break; // Stop checking this cannonball against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Cannonballs are destroyed on hit
cannonball.destroy();
// Apply Green Slowdown if applicable before destroying cannonball
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
cannonball.destroy();
cannonballs.splice(cb, 1); // Remove cannonball from array.
hitEnemy = true; // Mark that this cannonball is done
break; // Stop checking this cannonball against other enemies as it's destroyed
}
}
// If the cannonball hit an enemy, it was destroyed, so skip to the next cannonball.
if (hitEnemy) {
continue;
}
// Check if the cannonball has gone off-screen.
var wasOnScreen = cannonball.lastY > -cannonball.height / 2 && cannonball.lastX > -cannonball.width / 2 && cannonball.lastX < GAME_WIDTH + cannonball.width / 2;
var isOffScreen = cannonball.y < -cannonball.height / 2 ||
// Off top edge
cannonball.x < -cannonball.width / 2 ||
// Off left edge
cannonball.x > GAME_WIDTH + cannonball.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Cannonball is off-screen, destroy it and remove from the array.
cannonball.destroy();
cannonballs.splice(cb, 1);
} else if (!isOffScreen) {
// Update last known position only if the cannonball is still potentially on screen
cannonball.lastY = cannonball.y;
cannonball.lastX = cannonball.x;
}
}
// 3. Update and check Enemies
for (var k = enemies.length - 1; k >= 0; k--) {
var enemy = enemies[k];
// Note: LK engine calls enemy.update() automatically.
// Initialize last position if not set.
if (enemy.lastY === undefined) {
enemy.lastY = enemy.y;
}
// Check if the enemy has reached or passed the bastion line.
// Use transition detection: was the enemy's bottom edge above the line last frame,
// and is it on or below the line now?
var enemyBottomY = enemy.y + enemy.height / 2;
var enemyLastBottomY = enemy.lastY + enemy.height / 2;
var wasAboveBastion = enemyLastBottomY < BASTION_Y;
var isAtOrBelowBastion = enemyBottomY >= BASTION_Y;
if (wasAboveBastion && isAtOrBelowBastion) {
// Enemy reached the bastion! Game Over.
LK.getSound('gameOverSfx').play(); // Play game over sound effect.
LK.showGameOver(); // Trigger the engine's game over sequence.
// Calculate gold earned
var goldEarned = Math.floor(currentScore / 5);
playerGold += goldEarned;
storage.playerGold = playerGold; // Save gold to storage
// Save relics to storage using safe serialization
try {
var relicsData = {};
relicsData.damage_level = playerRelics.damage.level;
relicsData.damage_enabled = playerRelics.damage.enabled;
relicsData.slowdown_level = playerRelics.slowdown.level;
relicsData.slowdown_enabled = playerRelics.slowdown.enabled;
relicsData.green_level = playerRelics.green.level;
relicsData.green_enabled = playerRelics.green.enabled;
relicsData.dark_level = playerRelics.dark.level;
relicsData.dark_enabled = playerRelics.dark.enabled;
relicsData.dragon_level = playerRelics.dragon.level;
relicsData.dragon_enabled = playerRelics.dragon.enabled;
relicsData.reload_level = playerRelics.reload.level;
relicsData.reload_enabled = playerRelics.reload.enabled;
relicsData.ammo_level = playerRelics.ammo.level;
relicsData.ammo_enabled = playerRelics.ammo.enabled;
relicsData.swordsman_level = playerRelics.swordsman.level;
relicsData.swordsman_enabled = playerRelics.swordsman.enabled;
storage.playerRelicsData = relicsData;
} catch (e) {
console.log("Error saving relics:", e);
}
console.log("Game Over! Earned " + goldEarned + " gold. Total gold: " + playerGold);
// LK.showGameOver handles game state reset, no need to manually clear arrays here.
return; // Exit the update loop immediately as the game is over.
} else {
// Update last known position if game is not over for this enemy.
enemy.lastY = enemy.y;
}
}
// 3. Update and check Swordsmen (allies)
for (var l = swordsmen.length - 1; l >= 0; l--) {
var swordsman = swordsmen[l];
// Swordsman update method handles its own lifetime and attacking
// Check if the swordsman has been destroyed by its lifetime timer
if (!swordsman.parent) {
// If it no longer has a parent, it has been destroyed
swordsmen.splice(l, 1); // Remove swordsman from array
}
}
// 4. Update and check Cannons (allies)
for (var m = cannons.length - 1; m >= 0; m--) {
var cannon = cannons[m];
// Cannons do not have a lifetime timer or destruction logic in this basic version
// If they had, we'd check !cannon.parent and splice here as in Swordsmen
}
// 5. Update and check Wizard Towers (allies)
for (var wt = wizardTowers.length - 1; wt >= 0; wt--) {
var tower = wizardTowers[wt];
// Wizard towers do not have a lifetime timer or destruction logic in this basic version
// If they had, we'd check !tower.parent and splice here
}
// 5.5 Update and check Viking Allies
for (var va = vikingAllies.length - 1; va >= 0; va--) {
var viking = vikingAllies[va];
// Vikings do not have a lifetime timer or destruction logic in this version
// LK engine calls viking.update() automatically.
}
// 5.6 Update and check Angel of Lights (allies)
for (var angelIdx = angelOfLights.length - 1; angelIdx >= 0; angelIdx--) {
var angel = angelOfLights[angelIdx];
// Angels do not have a lifetime timer or destruction logic in this version
// LK engine calls angel.update() automatically.
}
// 5.7 Update and check Bouncy Balls
for (var bbIdx = bouncyBalls.length - 1; bbIdx >= 0; bbIdx--) {
var ball = bouncyBalls[bbIdx];
// LK engine calls ball.update() automatically.
// Check for collisions with enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (ball.intersects(enemy)) {
// Apply damage
var enemyDefeated = enemy.takeDamage(ball.damage, ball);
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
enemy.destroy();
enemies.splice(j, 1);
}
// Bouncy ball doesn't get destroyed on hit
LK.effects.flashObject(enemy, 0xFF00FF, 300);
}
}
// Check if ball was destroyed by lifetime
if (!ball.parent) {
bouncyBalls.splice(bbIdx, 1);
}
}
// 5.8 Update and check Bombs
for (var bombIdx = bombs.length - 1; bombIdx >= 0; bombIdx--) {
var bomb = bombs[bombIdx];
// LK engine calls bomb.update() automatically.
// Check if bomb was destroyed after explosion
if (!bomb.parent) {
bombs.splice(bombIdx, 1);
}
}
// 5.9 Update and check Bombers
for (var bomberIdx = bombers.length - 1; bomberIdx >= 0; bomberIdx--) {
var bomber = bombers[bomberIdx];
// LK engine calls bomber.update() automatically.
}
// 5.10 Update and check XBOWs
for (var xbowIdx = xbows.length - 1; xbowIdx >= 0; xbowIdx--) {
var xbow = xbows[xbowIdx];
// LK engine calls xbow.update() automatically.
}
// 6. Update and check Magic Balls
for (var mb = magicBalls.length - 1; mb >= 0; mb--) {
var magicBall = magicBalls[mb];
// Note: LK engine calls magicBall.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (magicBall.lastY === undefined) {
magicBall.lastY = magicBall.y;
}
if (magicBall.lastX === undefined) {
magicBall.lastX = magicBall.x;
}
// Check for collisions between the current magic ball and all enemies.
var hitEnemy = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Use the intersects method for collision detection.
if (magicBall.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply damage (minimal) and slowdown effect to the enemy and check if it was defeated
var enemyDefeated = enemy.takeDamage(magicBall.damage, magicBall); // Pass the magic ball object as source
// Check if the magic ball was reflected (takeDamage returns false and magicBall is destroyed)
if (!enemyDefeated && !magicBall.parent) {
// Magic ball was reflected, remove it from the array and stop processing
magicBalls.splice(mb, 1);
hitEnemy = true; // Mark that this magic ball is done
break; // Stop checking this magic ball against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
// Destroy the enemy
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Magic balls are destroyed on hit
magicBall.destroy();
// Apply Green Slowdown if applicable before destroying magic ball
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
// Note: Enemy.takeDamage already checks for MagicBall source to apply its default slow.
// This adds the Green Slowdown effect *on top* if applicable and not already slowed.
// However, takeDamage applies slow based on MagicBall properties. Let's apply Green Slowdown here explicitly.
enemy.slowTimer = 10 * 60; // 10 seconds
enemy.currentSlowAmount = 0.9; // 10% slow
}
magicBall.destroy();
magicBalls.splice(mb, 1); // Remove magic ball from array.
hitEnemy = true; // Mark that this magic ball is done
break; // Stop checking this magic ball against other enemies as it's destroyed
}
}
// If the magic ball hit an enemy, it was destroyed, so skip to the next magic ball.
if (hitEnemy) {
continue;
}
// Check if the magic ball has gone off-screen.
var wasOnScreen = magicBall.lastY > -magicBall.height / 2 && magicBall.lastX > -magicBall.width / 2 && magicBall.lastX < GAME_WIDTH + magicBall.width / 2;
var isOffScreen = magicBall.y < -magicBall.height / 2 ||
// Off top edge
magicBall.x < -magicBall.width / 2 ||
// Off left edge
magicBall.x > GAME_WIDTH + magicBall.width / 2; // Off right edge
if (wasOnScreen && isOffScreen) {
// Magic ball is off-screen, destroy it and remove from the array.
magicBall.destroy();
magicBalls.splice(mb, 1);
} else if (!isOffScreen) {
// Update last known position only if the magic ball is still potentially on screen
magicBall.lastY = magicBall.y;
magicBall.lastX = magicBall.x;
}
}
// 6.5 Update and check Viking Axes
for (var axeIdx = vikingAxes.length - 1; axeIdx >= 0; axeIdx--) {
var axe = vikingAxes[axeIdx];
// Note: LK engine calls axe.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (axe.lastY === undefined) {
axe.lastY = axe.y;
}
if (axe.lastX === undefined) {
axe.lastX = axe.x;
}
// Check for collisions between the current axe and all enemies.
var hitEnemyAxe = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (axe.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Apply Green Killer bonus if applicable (Vikings benefit too)
var damageToDealAxe = axe.damage;
if (greenKillerEnabled && enemy.tag === 'Green') {
damageToDealAxe *= 1.5; // Apply 50% damage bonus
}
// Apply damage and check if defeated
var enemyDefeated = enemy.takeDamage(damageToDealAxe, axe); // Pass axe as source for potential effects
// Check if the axe was reflected (takeDamage returns false and axe is destroyed)
if (!enemyDefeated && !axe.parent) {
// Axe was reflected, remove it from the array and stop processing
vikingAxes.splice(axeIdx, 1);
hitEnemyAxe = true; // Mark that this axe is done
break; // Stop checking this axe against other enemies as it's destroyed
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Handle axe piercing
axe.pierceLeft--; // Decrease remaining pierces
if (axe.pierceLeft <= 0) {
// Axe has no pierces left, destroy it
axe.destroy();
vikingAxes.splice(axeIdx, 1); // Remove axe from array.
hitEnemyAxe = true; // Mark that this axe is done
break; // Stop checking this axe against other enemies
} else {
// Axe pierced this enemy and can continue
// Potentially apply Green Slowdown if enabled
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
}
}
}
// If the axe hit and was destroyed, skip to the next axe.
if (hitEnemyAxe) {
continue;
}
// Check if the axe has gone off-screen.
var wasOnScreenAxe = axe.lastY > -axe.height / 2 && axe.lastX > -axe.width / 2 && axe.lastX < GAME_WIDTH + axe.width / 2;
var isOffScreenAxe = axe.y < -axe.height / 2 || axe.y > GAME_HEIGHT + axe.height / 2 || axe.x < -axe.width / 2 || axe.x > GAME_WIDTH + axe.width / 2;
if (wasOnScreenAxe && isOffScreenAxe) {
axe.destroy();
vikingAxes.splice(axeIdx, 1);
} else if (!isOffScreenAxe) {
axe.lastY = axe.y;
axe.lastX = axe.x;
}
}
// 6.6 Update and check Darts
for (var dartIdx = darts.length - 1; dartIdx >= 0; dartIdx--) {
var dart = darts[dartIdx];
// Note: LK engine calls dart.update() automatically.
// Initialize last position if it hasn't been set yet (first frame).
if (dart.lastY === undefined) {
dart.lastY = dart.y;
}
if (dart.lastX === undefined) {
dart.lastX = dart.x;
}
// Check for collisions between the current dart and all enemies.
var hitEnemyDart = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (dart.intersects(enemy)) {
// Collision detected!
LK.getSound('hit').play(); // Play hit sound.
// Calculate damage, applying bonus against Green enemies
var damageToDealDart = dart.damage;
var greenDamageMultiplier = 2; // Base multiplier for darts vs Green enemies
// Apply Green Relic damage bonus if enabled and enemy is Green-tagged
if (playerRelics.green.enabled && enemy.tag === 'Green') {
greenDamageMultiplier += playerRelics.green.level * 0.02; // Additional damage multiplier from relic
}
if (enemy.tag === 'Green') {
damageToDealDart *= greenDamageMultiplier; // Apply calculated multiplier
}
// Apply damage and check if defeated
var enemyDefeated = enemy.takeDamage(damageToDealDart, dart); // Pass dart as source
// Check if the dart was reflected (takeDamage returns false and dart is destroyed)
if (!enemyDefeated && !dart.parent) {
// Dart was reflected, remove it from the array and stop processing
darts.splice(dartIdx, 1);
hitEnemyDart = true; // Mark that this dart is done
break; // Stop checking this dart against other enemies
}
if (enemyDefeated) {
LK.setScore(LK.getScore() + 1); // Increment score.
scoreTxt.setText(LK.getScore()); // Update score display.
enemy.destroy();
enemies.splice(j, 1); // Remove enemy from array.
}
// Darts are destroyed on hit
dart.destroy();
// Apply Green Slowdown if applicable before destroying dart
if (greenSlowdownEnabled && enemy.tag === 'Green' && enemy.slowTimer <= 0) {
enemy.slowTimer = 10 * 60;
enemy.currentSlowAmount = 0.9;
}
darts.splice(dartIdx, 1); // Remove dart from array.
hitEnemyDart = true; // Mark that this dart is done
break; // Stop checking this dart against other enemies
}
}
// If the dart hit and was destroyed, skip to the next dart.
if (hitEnemyDart) {
continue;
}
// Check if the dart has gone off-screen.
var wasOnScreenDart = dart.lastY > -dart.height / 2 && dart.lastX > -dart.width / 2 && dart.lastX < GAME_WIDTH + dart.width / 2;
var isOffScreenDart = dart.y < -dart.height / 2 || dart.y > GAME_HEIGHT + dart.height / 2 || dart.x < -dart.width / 2 || dart.x > GAME_WIDTH + dart.width / 2;
if (wasOnScreenDart && isOffScreenDart) {
dart.destroy();
darts.splice(dartIdx, 1);
} else if (!isOffScreenDart) {
dart.lastY = dart.y;
dart.lastX = dart.x;
}
}
// 7. Spawn new Enemies periodically
// Use LK.ticks and the spawn interval. Ensure interval doesn't go below minimum.
if (LK.ticks % Math.max(minEnemySpawnInterval, Math.floor(enemySpawnInterval)) === 0) {
var currentScore = LK.getScore();
var baseSpeedForSpawn = Math.min(maxEnemySpeed, currentEnemySpeed); // Base speed adjusted by game difficulty progression
// Determine if this spawn *could* be a boss (e.g., every 7th spawn after score 10)
var potentialBoss = (enemies.length + 1) % 7 === 0 && currentScore >= 10;
var typeToSpawn = 'swordsman'; // Default type
var enemyHealth = 1; // Default health
var enemyDodgeChance = 0; // Default dodge chance
var enemySpeed = baseSpeedForSpawn; // Start with base speed
// Determine base type based on score thresholds and randomness
var possibleTypes = ['swordsman'];
if (currentScore >= 10) {
possibleTypes.push('spearman');
}
if (currentScore >= 15) {
possibleTypes.push('thief');
}
if (currentScore >= 23) {
possibleTypes.push('knight');
}
if (currentScore >= 25) {
possibleTypes.push('wizard');
} // Wizards start appearing at score 25
if (currentScore >= 30) {
possibleTypes.push('shield');
} // Shields start appearing at score 30
if (currentScore >= 35) {
possibleTypes.push('shaman'); // Shaman appears at score 35
console.log("Shaman added to possible enemy types");
}
if (currentScore >= 55) {
possibleTypes.push('dark_bowman'); // Dark bowman appears at score 55
possibleTypes.push('dark_spearman'); // Dark Spearman appears at score 55
}
if (currentScore >= 69) {
possibleTypes.push('jester'); // Jester appears at score 69
}
if (currentScore >= 100) {
possibleTypes.push('elite_knight'); // Elite Knights start appearing at score 100
}
if (currentScore >= 134) {
possibleTypes.push('dragon'); // Dragon appears at score 134
}
if (currentScore >= 112) {
possibleTypes.push('elite_shield'); // Elite Shields appear at score 112
}
if (currentScore >= 125) {
// War Elephant appears at score 125
possibleTypes.push('war_elephant');
// Baby Dragon also appears at score 125 (same as war elephant)
possibleTypes.push('baby_dragon');
}
if (currentScore >= 145) {
possibleTypes.push('dark_war_elephant'); // Dark War Elephant appears at score 145
}
if (currentScore >= 154) {
// Hot Air Balloon appears at score 100
possibleTypes.push('hot_air_balloon');
}
if (currentScore >= 27) {
possibleTypes.push('flag_bearer'); // Flag Bearer appears at score 27
}
// Check if Battle Horn is active - if so, make half of spawns swordsmen, rest random
if (battleHornEnabled) {
// 50% chance to force swordsman, 50% chance to pick random
if (Math.random() < 0.5) {
typeToSpawn = 'swordsman';
} else {
var chosenTypeIndex = Math.floor(Math.random() * possibleTypes.length);
typeToSpawn = possibleTypes[chosenTypeIndex];
}
} else {
// Randomly select a type from the available pool for this score level
var chosenTypeIndex = Math.floor(Math.random() * possibleTypes.length);
typeToSpawn = possibleTypes[chosenTypeIndex];
}
// Set stats based on chosen type and apply score-based scaling
if (typeToSpawn === 'knight') {
var baseKnightHP = 5;
var hpIncreaseIntervalKnight = 30; // Health increases every 30 score points after appearing
var hpIncreasesKnight = Math.floor(Math.max(0, currentScore - 23) / hpIncreaseIntervalKnight);
enemyHealth = baseKnightHP + hpIncreasesKnight * 5;
enemySpeed *= 0.9; // Knights are slightly slower than base swordsman speed
} else if (typeToSpawn === 'thief') {
enemyHealth = 1; // Thieves are fragile
enemyDodgeChance = 0.10; // 10% dodge chance
var speedIncreaseIntervalThief = 25; // Speed increases every 25 score points after appearing
var speedIncreasePercentThief = 0.05; // 5% speed increase each time
var speedIncreasesThief = Math.floor(Math.max(0, currentScore - 15) / speedIncreaseIntervalThief);
var thiefSpeedMultiplier = Math.pow(1 + speedIncreasePercentThief, speedIncreasesThief);
enemySpeed *= 1.2 * thiefSpeedMultiplier; // Thieves are faster base + scaling speed
} else if (typeToSpawn === 'shield') {
var baseShieldHP = 20;
var hpIncreaseIntervalShield = 35; // Gains HP every 35 score points after appearing
var hpIncreasesShield = Math.floor(Math.max(0, currentScore - 30) / hpIncreaseIntervalShield);
enemyHealth = baseShieldHP + hpIncreasesShield * 20;
enemySpeed *= 0.5; // Shield enemies are very slow
enemyDodgeChance = 0;
} else if (typeToSpawn === 'shaman') {
var baseShamanHP = 2;
var statIncreaseIntervalShaman = 10; // Gains stats every 10 score points after appearing
var statIncreasesShaman = Math.floor(Math.max(0, currentScore - 35) / statIncreaseIntervalShaman);
enemyHealth = baseShamanHP + statIncreasesShaman * 1; // Gains 1 HP per interval
enemySpeed *= 0.8 * Math.pow(1.04, statIncreasesShaman); // Base speed, gains 4% speed per interval
enemyDodgeChance = 0; // Shaman cannot dodge
} else if (typeToSpawn === 'wizard') {
var baseWizardHP = 2;
var statIncreaseIntervalWizard = 10; // Gains stats every 10 score points after appearing
var statIncreasesWizard = Math.floor(Math.max(0, currentScore - 25) / statIncreaseIntervalWizard);
enemyHealth = baseWizardHP + statIncreasesWizard * 1; // Gains 1 HP per interval
enemySpeed *= 0.7 * Math.pow(1.05, statIncreasesWizard); // Slow base, gains 5% speed per interval
enemyDodgeChance = 0;
} else if (typeToSpawn === 'elite_knight') {
var baseEliteKnightHP = 45;
var hpIncreaseIntervalElite = 30; // Gains HP every 30 score points after appearing
var hpIncreasesElite = Math.floor(Math.max(0, currentScore - 100) / hpIncreaseIntervalElite);
enemyHealth = baseEliteKnightHP + hpIncreasesElite * 15;
enemySpeed *= 0.85; // Slightly slower than base knight speed
enemyDodgeChance = 0;
} else if (typeToSpawn === 'elite_shield') {
var baseEliteShieldHP = 200; // Starts with 200 HP
var hpIncreaseIntervalEliteShield = 20; // Gains HP every 20 score points after appearing
var hpIncreasesEliteShield = Math.floor(Math.max(0, currentScore - 112) / hpIncreaseIntervalEliteShield);
enemyHealth = baseEliteShieldHP + hpIncreasesEliteShield * 50; // Gains 50 HP per interval
enemySpeed *= 0.4; // Elite Shield enemies are slower than regular shield enemies
enemyDodgeChance = 0;
} else if (typeToSpawn === 'spearman') {
var baseSpearmanHP = 2;
var hpIncreaseIntervalSpearman = 10; // Gains HP every 10 score points after appearing
var hpIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / hpIncreaseIntervalSpearman);
enemyHealth = baseSpearmanHP + hpIncreasesSpearman * 3; // Gains 3 HP per interval
var speedIncreaseIntervalSpearman = 10; // Gains speed every 10 score points after appearing
var speedIncreasePercentSpearman = 0.05; // 5% speed increase each time
var speedIncreasesSpearman = Math.floor(Math.max(0, currentScore - 10) / speedIncreaseIntervalSpearman);
var spearmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentSpearman, speedIncreasesSpearman);
enemySpeed *= 1.0 * spearmanSpeedMultiplier; // Base speed + scaling speed
enemyDodgeChance = 0;
} else if (typeToSpawn === 'war_elephant') {
var baseElephantHP = 500;
var hpIncreaseIntervalElephant = 15; // Gains HP every 15 score points after appearing
var hpIncreasesElephant = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalElephant);
enemyHealth = baseElephantHP + hpIncreasesElephant * 100; // Gains 100 HP per interval
enemySpeed *= 0.3; // War Elephants are very slow
enemyDodgeChance = 0;
// Note: War elephant death spawns spearmen - this will be handled in the Enemy death logic.
} else if (typeToSpawn === 'hot_air_balloon') {
enemyHealth = 1; // Always has 1 HP
enemySpeed *= 0.2; // Very slow movement
enemyDodgeChance = 0;
// Note: Hot air balloon death spawns 5 random non-green enemies - handled in Enemy death logic
} else if (typeToSpawn === 'dark_bowman') {
var baseDarkBowmanHP = 5;
var statIncreaseIntervalBowman = 10; // Gains stats every 10 score points after appearing
var statIncreasesBowman = Math.floor(Math.max(0, currentScore - 55) / statIncreaseIntervalBowman);
enemyHealth = baseDarkBowmanHP + statIncreasesBowman * 3; // Gains 3 HP per interval
var speedIncreasePercentBowman = 0.05; // 5% speed increase each time
var bowmanSpeedMultiplier = Math.pow(1 + speedIncreasePercentBowman, statIncreasesBowman);
enemySpeed *= 1.1 * bowmanSpeedMultiplier; // Slightly faster than base speed + scaling
enemyDodgeChance = 0;
// Note: Dark bowman has Black tag making it immune to arrows
} else if (typeToSpawn === 'jester') {
var baseJesterHP = 2;
enemyHealth = baseJesterHP; // Jester HP doesn't scale with score
var baseJesterSpeed = baseSpeedForSpawn * 1.1; // Jester starts faster
var speedIncreaseIntervalJester = 12; // Gains speed every 12 score points after appearing
var speedIncreasePercentJester = 0.03; // 3% speed increase each time
var speedIncreasesJester = Math.floor(Math.max(0, currentScore - 69) / speedIncreaseIntervalJester);
enemySpeed = baseJesterSpeed * Math.pow(1 + speedIncreasePercentJester, speedIncreasesJester);
enemyDodgeChance = 0.30; // 30% dodge chance
// The reflectChance is set in the Enemy class constructor based on type, no need to set here.
} else if (typeToSpawn === 'dark_war_elephant') {
var baseDarkElephantHP = 450;
var hpIncreaseIntervalDarkElephant = 10; // Gains HP every 10 score points after appearing
var hpIncreasesDarkElephant = Math.floor(Math.max(0, currentScore - 120) / hpIncreaseIntervalDarkElephant);
enemyHealth = baseDarkElephantHP + hpIncreasesDarkElephant * 80; // Gains 80 HP per interval
enemySpeed *= 0.3; // Dark War Elephants are very slow
enemyDodgeChance = 0;
// Note: Dark War elephant death spawns dark bowmen - this will be handled in the Enemy death logic.
} else if (typeToSpawn === 'dark_spearman') {
var baseDarkSpearmanHP = 15;
var baseDarkSpearmanSpeedMultiplier = 1.2;
var statIncreaseIntervalDarkSpearman = 10; // Gains stats every 10 score points after appearing
var statIncreasesDarkSpearman = Math.floor(Math.max(0, currentScore - 55) / statIncreaseIntervalDarkSpearman);
enemyHealth = baseDarkSpearmanHP + statIncreasesDarkSpearman * 5; // Gains 5 HP per interval
var darkSpearmanSpeedBonus = Math.pow(1 + 0.03, statIncreasesDarkSpearman); // Gains 3% speed per interval
enemySpeed *= baseDarkSpearmanSpeedMultiplier * darkSpearmanSpeedBonus;
enemyDodgeChance = 0;
// Tag 'Black' is set in Enemy constructor
} else if (typeToSpawn === 'dragon') {
var baseDragonHP = 250;
var hpIncreaseIntervalDragon = 15; // Gains HP every 15 score points after appearing
var hpIncreasesDragon = Math.floor(Math.max(0, currentScore - 100) / hpIncreaseIntervalDragon);
enemyHealth = baseDragonHP + hpIncreasesDragon * 50; // Gains 50 HP per interval
enemySpeed *= 0.9; // Dragons are moderately fast
enemyDodgeChance = 0.40; // 40% dodge chance
// Tag 'Dragon' could be set in Enemy constructor if needed for other mechanics
} else if (typeToSpawn === 'flag_bearer') {
var baseFlagBearerHP = 5;
var hpIncreaseIntervalFlagBearer = 10; // Gains HP every 10 score points after appearing
var hpIncreasesFlagBearer = Math.floor(Math.max(0, currentScore - 27) / hpIncreaseIntervalFlagBearer);
enemyHealth = baseFlagBearerHP + hpIncreasesFlagBearer * 2; // Gains 2 HP per interval
var speedIncreaseIntervalFlagBearer = 10; // Gains speed every 10 score points
var speedIncreasesFlagBearer = Math.floor(Math.max(0, currentScore - 27) / speedIncreaseIntervalFlagBearer);
var flagBearerSpeedMultiplier = Math.pow(1.03, speedIncreasesFlagBearer); // 3% speed increase per interval
enemySpeed *= 0.8 * flagBearerSpeedMultiplier; // Slower base speed but increases over time
enemyDodgeChance = 0;
} else if (typeToSpawn === 'baby_dragon') {
var baseBabyDragonHP = 50;
var hpIncreaseIntervalBabyDragon = 9; // Gains HP every 9 score points after appearing
var hpIncreasesBabyDragon = Math.floor(Math.max(0, currentScore - 125) / hpIncreaseIntervalBabyDragon);
enemyHealth = baseBabyDragonHP + hpIncreasesBabyDragon * 10; // Gains 10 HP per interval
var speedIncreaseIntervalBabyDragon = 9; // Gains speed every 9 score points
var speedIncreasesBabyDragon = Math.floor(Math.max(0, currentScore - 125) / speedIncreaseIntervalBabyDragon);
var babyDragonSpeedMultiplier = Math.pow(1.02, speedIncreasesBabyDragon); // 2% speed increase per interval
enemySpeed *= 1.1 * babyDragonSpeedMultiplier; // Faster than regular dragon
enemyDodgeChance = 0.25; // 25% dodge chance
} else {
// Swordsman (default)
enemyHealth = 1;
// Speed remains baseSpeedForSpawn initially
}
// Check if this spawn should be overridden to be a Boss
if (potentialBoss) {
typeToSpawn = 'boss'; // Set type to boss
enemyHealth = 5 + Math.floor(currentScore / 8); // Boss health scales significantly with score
enemySpeed = baseSpeedForSpawn * 0.7; // Bosses are slower but much tougher (reset speed based on base)
enemyDodgeChance = 0; // Bosses typically don't dodge
}
// Apply the global Sabotage speed multiplier AFTER type-specific adjustments
enemySpeed *= enemySpeedMultiplier;
// Create the new enemy instance with the calculated stats
var newEnemy;
if (typeToSpawn === 'flag_bearer') {
newEnemy = new FlagBearer();
newEnemy.speed = enemySpeed;
newEnemy.baseSpeed = enemySpeed;
newEnemy.health = enemyHealth;
newEnemy.maxHealth = enemyHealth;
newEnemy.dodgeChance = enemyDodgeChance;
} else if (typeToSpawn === 'baby_dragon') {
newEnemy = new BabyDragon();
newEnemy.speed = enemySpeed;
newEnemy.baseSpeed = enemySpeed;
newEnemy.health = enemyHealth;
newEnemy.maxHealth = enemyHealth;
newEnemy.dodgeChance = enemyDodgeChance;
} else {
newEnemy = new Enemy(typeToSpawn, enemySpeed, enemyHealth, enemyDodgeChance);
}
// Position the new enemy at the top, random horizontal position with padding
// Use the actual width of the created enemy's graphic for padding calculation
// Need to access width after creation, use a sensible default or estimate if needed before creation
var tempAsset = LK.getAsset(newEnemy.assetId || 'enemy', {}); // Get asset dimensions (might need refinement if assetId isn't on newEnemy yet)
var spawnPadding = (tempAsset ? tempAsset.width / 2 : 100) + 20; // Use default if asset not found easily
newEnemy.x = spawnPadding + Math.random() * (GAME_WIDTH - 2 * spawnPadding);
newEnemy.y = -(tempAsset ? tempAsset.height / 2 : 100); // Start just above the top edge.
// Initialize last position for state tracking (used for bastion collision check).
newEnemy.lastY = newEnemy.y;
// Add the new enemy to the game scene and the tracking array.
game.addChild(newEnemy);
enemies.push(newEnemy);
// Increase difficulty for the next spawn: decrease spawn interval and increase base speed.
// These affect the *next* potential spawn's base calculations.
enemySpawnInterval -= enemySpawnRateDecrease;
currentEnemySpeed += enemySpeedIncrease;
}
};
// --- Swordsman Relic Initial Setup ---
if (playerRelics.swordsman.enabled && playerRelics.swordsman.level > 0) {
// Spawn permanent swordsman for swordsman relic
var permanentSwordsman = new Swordsman();
permanentSwordsman.x = FIRING_POS_X + 100; // Position near player
permanentSwordsman.y = FIRING_POS_Y - 50;
permanentSwordsman.lifetime = Infinity; // Permanent swordsman
permanentSwordsman.attackDamage = 1 + (playerRelics.swordsman.level - 1) * 2; // Base damage +2 per level after first
game.addChild(permanentSwordsman);
swordsmen.push(permanentSwordsman);
}
// --- Initial Game Setup ---
// Set the initial score text based on the starting score (which is 0).
scoreTxt.setText(LK.getScore());
// Set the initial ammo display.
if (ammoTxt) {
// Ensure ammoTxt is initialized
ammoTxt.setText('Ammo: ' + maxArrowsBeforeCooldown);
}
;
// Play the background music.
// Check if we need to show a title screen first
if (!isGameStarted) {// Assuming a flag 'isGameStarted' for title screen state
// Don't play music immediately if showing title screen
} else {
LK.playMusic('Gamemusic');
;
}
// Placeholder for the title screen container
var titleScreen = null;
// Flag to track game start state
var isGameStarted = false;
// Function to show the title screen
function showTitleScreen() {
isGameStarted = false; // Ensure game is not started
// Create the title screen container
titleScreen = new Container();
titleScreen.x = GAME_WIDTH / 2;
titleScreen.y = GAME_HEIGHT / 2;
game.addChild(titleScreen);
// Add title text
var titleText = new Text2('Defend Your Bastion!', {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -200;
titleScreen.addChild(titleText);
// Add a simple instruction text
var instructionText = new Text2('Tap to Start', {
size: 60,
fill: 0xCCCCCC
});
instructionText.anchor.set(0.5, 0.5);
instructionText.y = 100;
titleScreen.addChild(instructionText);
// Make the title screen interactive to start the game
titleScreen.interactive = true;
titleScreen.down = function () {
startGame(); // Start the game when tapped
};
// Pause game logic while title screen is active
isUpgradePopupActive = true; // Reusing this flag to pause game logic
}
// Function to start the game
function startGame() {
isGameStarted = true; // Set game started flag
isUpgradePopupActive = false; // Unpause game logic
if (titleScreen) {
titleScreen.destroy();
titleScreen = null;
}
// Reset game state if needed (LK might handle this on game start)
// Play game music
LK.playMusic('Gamemusic');
}
// Initially show the title screen
showTitleScreen();
Arrow. In-Game asset. 2d. High contrast. No shadows. Topdown
Red stickman with a sword. In-Game asset. 2d. High contrast. No shadows. Topdown
Blue stickman with a bow. In-Game asset. 2d. High contrast. No shadows
Red stickman with an iron helmet, iron sword and iron shield. In-Game asset. 2d. High contrast. No shadows. No eyes
Red stickman with a knife and a thief's bandana. In-Game asset. 2d. High contrast. No shadows. No eyes
Purple stickman with a crown. In-Game asset. 2d. High contrast. No shadows
Blue stickman with a sword. In-Game asset. 2d. High contrast. No shadows
Tower. In-Game asset. 2d. High contrast. No shadows
Red stickman with a big wooden shield full of spikes. In-Game asset. 2d. High contrast. No shadows
Green stickman with a blue wizard hat and a staff; no eyes. In-Game asset. 2d. High contrast. No shadows
Red stickman with a golden knight helmet, gold sword and gold shield and gold boots. In-Game asset. 2d. High contrast. No shadows
Cannon. In-Game asset. 2d. High contrast. No shadows
Cannonball. In-Game asset. 2d. High contrast. No shadows
Yellow stickman with an azur wizard hat and staff on a tower. In-Game asset. 2d. High contrast. No shadows
Magic ice ball. In-Game asset. 2d. High contrast. No shadows
Green stickman with a Spartan helmet, Spartan shield and Spartan spear. In-Game asset. 2d. High contrast. No shadows
Green war elephant. In-Game asset. 2d. High contrast. No shadows
Yellow viking stickman holding an axe and is about to throw it. In-Game asset. 2d. High contrast. No shadows
Hatchet. In-Game asset. 2d. High contrast. No shadows
A red stickman with a big golden shield and golden armor. In-Game asset. 2d. High contrast. No shadows
Gray stickman with it's face covered with a black hood equipped with a bow In-Game asset. 2d. High contrast. No shadows, no eyes
Hot air balloon full of red stickmen. In-Game asset. 2d. High contrast. No shadows
Black war elephant with red eyes. In-Game asset. 2d. High contrast. No shadows
Red stickman that is a jester that is on a blue ball and is juggling. In-Game asset. 2d. High contrast. No shadows
Green stickman with a tribal mask and a stick to shoot darts. In-Game asset. 2d. High contrast. No shadows
White female stickman that is an angel with golden armor and a heavenly sword. In-Game asset. 2d. High contrast. No shadows
Wooden dart In-Game asset. 2d. High contrast. No shadows. Topdown
Orb of light. In-Game asset. 2d. High contrast. No shadows
Purple dragon with a red stickman riding it. In-Game asset. 2d. High contrast. No shadows
Add a Roman shield
Bomb. In-Game asset. 2d. High contrast. No shadows
Blue stickman with safety goggles, yellow safety helmet and a bomb. In-Game asset. 2d. High contrast. No shadows
Giant crossbow. In-Game asset. 2d. High contrast. No shadows. Topdown
Green stickman with a British soldier's hat and with a red flag with 2 swords. In-Game asset. 2d. High contrast. No shadows
Baby dragon from clash of clans. In-Game asset. 2d. High contrast. No shadows
Missile launcher BTD6. In-Game asset. 2d. High contrast. No shadows
Red Missile. In-Game asset. 2d. High contrast. No shadows