/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
tutorialCompleted: false
});
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Skip bullet update when game is paused (gameSpeed = 0)
if (gameSpeed === 0) {
return;
}
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Apply damage to target enemy
var actualDamage = self.damage;
// Artillery towers deal 40% of enemy's current health as damage
if (self.type === 'splash') {
actualDamage = Math.ceil(self.targetEnemy.health * 0.4);
// AntiAir bullets deal half damage to flying enemies
if (self.targetEnemy.isFlying) {
actualDamage = actualDamage * 0.5;
}
}
// Immune enemies take 10% more damage from all bullets
if (self.targetEnemy.isImmune) {
actualDamage = actualDamage * 1.1;
}
// Immune enemies now take damage from all bullet types
self.targetEnemy.health -= actualDamage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create visual splash effect
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
// Splash damage to nearby enemies
var splashRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var splashDx = otherEnemy.x - self.targetEnemy.x;
var splashDy = otherEnemy.y - self.targetEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage or 20% of enemy health for Artillery)
var splashDamage;
if (self.type === 'splash') {
splashDamage = Math.ceil(otherEnemy.health * 0.2); // 20% of current health for splash
} else {
splashDamage = self.damage * 0.5;
}
// Immune enemies take 10% more splash damage
if (otherEnemy.isImmune) {
splashDamage = splashDamage * 1.1;
}
// Immune enemies now take splash damage from all bullet types
otherEnemy.health -= splashDamage;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
} else if (self.type === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
numberLabel.visible = false; // Hide numbers in battlefield
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
cellGraphics.tint = 0x880000;
return;
}
numberLabel.visible = false; // Hide numbers in battlefield
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
} else {
cellGraphics.tint = 0x88 - tint << 8 | tint;
}
// Remove arrow rendering - no longer display arrows
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0x008800;
numberLabel.visible = false;
break;
}
case 4:
{
self.removeArrows();
cellGraphics.tint = 0x000000; // Black color for castle walls
numberLabel.visible = false;
break;
}
}
// numberLabel.setText(Math.floor(data.score / 1000) / 10); // Removed number display
};
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
}
effectGraphics.alpha = 0.7;
self.alpha = 0;
// Animate the effect
tween(self, {
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
// Base enemy class for common functionality
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'normal';
self.speed = .01;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.speed *= 1.275; // 27.5% faster than normal (reduced by 15% from previous 50%)
self.maxHealth = 40;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'angry':
self.speed *= 1; // Same speed as normal enemies
self.maxHealth = 120; // More health than normal
break;
case 'modern':
self.speed *= 1; // Same speed as angry enemies
self.maxHealth = 600; // 5x health of angry enemies (120 * 5)
break;
case 'bad':
self.speed *= 0.5; // 50% slower than normal
self.maxHealth = 1000; // 10 times the health of a normal enemy
break;
case 'super':
self.speed *= 0.4; // 60% slower than normal
self.maxHealth = 2000; // 20 times the health of a normal enemy (2x bad enemy)
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
// Boss spawning disabled for waves 10, 20, 30
// if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
// self.isBoss = true;
// // Boss enemies have 20x health and are larger
// self.maxHealth *= 20;
// // Slower speed for bosses
// self.speed = self.speed * 0.7;
// }
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type === 'angry') {
assetId = 'Angry_Enemy';
} else if (self.type === 'modern') {
assetId = 'modern_enemy';
} else if (self.type === 'bad') {
assetId = 'enemy_bad';
} else if (self.type === 'super') {
assetId = 'enemy_super';
} else if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Fall back to regular enemy asset if specific type asset not found
// Apply tint to differentiate enemy types
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
break;
case 'immune':
enemyGraphics.tint = 0xAA0000; // Red for immune enemies
break;
case 'flying':
enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
break;
case 'swarm':
enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
shadowGraphics.scaleX = 1.8;
shadowGraphics.scaleY = 1.8;
}
// Position shadow slightly offset
self.shadow.x = 20; // Offset right
self.shadow.y = 20; // Offset down
// Ensure shadow has the same rotation as the enemy
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
// Skip enemy update when game is paused (gameSpeed = 0)
if (gameSpeed === 0) {
return;
}
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Anti-stuck mechanism: track if enemy hasn't moved significantly
if (self.lastPosition === undefined) {
self.lastPosition = {
x: self.currentCellX,
y: self.currentCellY,
time: LK.ticks
};
self.stuckCounter = 0;
}
// Check if enemy has been stuck in same position for too long
var timeSinceLastCheck = LK.ticks - self.lastPosition.time;
if (timeSinceLastCheck > 180) {
// Check every 3 seconds
var distanceMoved = Math.sqrt((self.currentCellX - self.lastPosition.x) * (self.currentCellX - self.lastPosition.x) + (self.currentCellY - self.lastPosition.y) * (self.currentCellY - self.lastPosition.y));
if (distanceMoved < 0.5) {
// If moved less than half a cell
self.stuckCounter++;
if (self.stuckCounter >= 3) {
// If stuck for 3 checks (9 seconds)
// Force reset enemy position and target
self.currentTarget = null;
// Try to find a valid spawn position
for (var sx = 9; sx <= 14; sx++) {
var testCell = grid.getCell(sx, 5);
if (testCell && testCell.pathId === pathId) {
self.cellX = sx;
self.cellY = 5;
self.currentCellX = sx;
self.currentCellY = 5;
self.stuckCounter = 0;
break;
}
}
}
} else {
self.stuckCounter = 0; // Reset stuck counter if moving
}
// Update last position
self.lastPosition = {
x: self.currentCellX,
y: self.currentCellY,
time: LK.ticks
};
}
// Immune enemies attack towers in range
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.poisoned = false;
self.poisonEffect = false;
// Attack towers every 120 frames (2 seconds)
if (LK.ticks % Math.max(1, Math.floor(120 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 2 cell range
if (distance <= CELL_SIZE * 2) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Only attack if not protected by a Healer tower
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
var damage = Math.floor(tower.maxHealth / 10); // 1/10th of tower's max health
// Hardcore mode: enemies deal 2x damage
if (gameSettings.difficulty === 'hardcore') {
damage *= 2;
}
// Hardcore mode: spawn 3 immune enemies every wave
if (gameSettings.difficulty === 'hardcore') {
immuneCount = 3;
}
tower.health -= damage;
if (tower.health <= 0) {
tower.health = 0;
}
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else if (self.type === 'angry') {
// Angry enemies attack towers in range, but are blocked by Healer towers
// Attack towers every 120 frames (2 seconds)
if (LK.ticks % Math.max(1, Math.floor(120 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 2 cell range
if (distance <= CELL_SIZE * 2) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Healer towers completely block damage from Angry enemies
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
var damage = Math.floor(tower.maxHealth * 0.25); // 25% of tower's max health
// Hardcore mode: enemies deal 2x damage
if (gameSettings.difficulty === 'hardcore') {
damage *= 2;
}
tower.health -= damage;
if (tower.health <= 0) {
tower.health = 0;
}
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else if (self.type === 'modern') {
// Modern enemies attack towers in range with 2x damage compared to Angry enemies
// Attack towers every 120 frames (2 seconds)
if (LK.ticks % Math.max(1, Math.floor(120 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 2 cell range
if (distance <= CELL_SIZE * 2) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Healer towers completely block damage from Modern enemies
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
var damage = Math.floor(tower.maxHealth * 0.50); // 50% of tower's max health (2x angry damage)
// Hardcore mode: enemies deal 2x damage
if (gameSettings.difficulty === 'hardcore') {
damage *= 2;
}
tower.health -= damage;
if (tower.health <= 0) {
tower.health = 0;
}
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else if (self.type === 'bad') {
// Bad enemies attack towers more frequently and deal more damage
// Attack towers every 90 frames (1.5 seconds)
if (LK.ticks % Math.max(1, Math.floor(90 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 2.5 cell range (longer range than angry)
if (distance <= CELL_SIZE * 2.5) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Only attack if not protected by a Healer tower
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
// Bad enemies destroy towers in one hit (or deal massive damage in hardcore)
if (gameSettings.difficulty === 'hardcore') {
tower.health = 0; // Still one-shot in hardcore
} else {
tower.health = 0; // One-shot in normal/hard
}
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else if (self.type === 'super') {
// Super enemies attack towers more frequently and deal devastating damage
// Attack towers every 60 frames (1 second)
if (LK.ticks % Math.max(1, Math.floor(60 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 3 cell range (longer range than bad)
if (distance <= CELL_SIZE * 3) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Only attack if not protected by a Healer tower
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
// Super enemies destroy towers instantly regardless of difficulty
tower.health = 0;
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else {
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second), adjusted for game speed
if (LK.ticks % Math.max(1, Math.floor(30 / gameSpeed)) === 0) {
self.health -= self.poisonDamage * gameSpeed;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration -= gameSpeed;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.type === 'bad') {
// Bad enemies use their asset's natural color - no tint applied
} else if (self.type === 'super') {
enemyGraphics.tint = 0x800080; // Purple tint for super enemies
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.spawns = [];
self.goals = [];
for (var i = 0; i < gridWidth; i++) {
self.cells[i] = [];
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j] = {
score: 0,
pathId: 0,
towersInRange: []
};
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
4: Castle wall
*/
for (var i = 0; i < gridWidth; i++) {
var _loop = function _loop() {
cell = self.cells[i][j];
cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; // Create castle structure with random labyrinth
if (j > 4 && j < gridHeight - 4) {
// Simple pseudo-random number generator using seed
var seededRandom = function seededRandom() {
mazeSeed = (mazeSeed * 9301 + 49297) % 233280;
return mazeSeed / 233280;
}; // Main castle outer walls
// Generate maze seed based on reload counter to ensure different maze each reload
// Use reload-based seed as primary factor for maze generation
mazeSeed = gameSessionSeed * 7919 + (currentWave + 1) * 1009 + reloadCounter * 4397;
if ((i === 2 || i === gridWidth - 3) && j >= 8 && j <= gridHeight - 8) {
cellType = 4; // Castle wall
}
// Castle towers at corners
if ((i >= 1 && i <= 4 || i >= gridWidth - 5 && i <= gridWidth - 2) && (j >= 7 && j <= 10 || j >= gridHeight - 11 && j <= gridHeight - 8)) {
cellType = 4;
}
// Generate unique geometric maze patterns based on seed
var mazePattern = (mazeSeed * 17 + (currentWave + 1) * 23) % 100;
if (j >= 12 && j <= gridHeight - 12) {
// Create safe corridors first - ensure at least 3 clear horizontal paths
var safePaths = [16, 20, 24];
var isSafePath = false;
for (var sp = 0; sp < safePaths.length; sp++) {
if (j === safePaths[sp] && i >= 8 && i <= gridWidth - 9) {
isSafePath = true;
break;
}
}
if (!isSafePath) {
// Generate dynamic geometric patterns based on random seed
if (mazePattern < 20) {
// Concentric geometric shapes
var centerX = Math.floor(gridWidth / 2);
var centerY = Math.floor((gridHeight - 12 + 12) / 2);
var distance = Math.sqrt((i - centerX) * (i - centerX) + (j - centerY) * (j - centerY));
var ringSize = 2 + mazeSeed % 3;
if (Math.floor(distance) % ringSize === 1 && distance > 2 && distance < 8 && i >= 6 && i <= gridWidth - 7) {
if (seededRandom() > 0.6) {
cellType = 4;
}
}
} else if (mazePattern < 40) {
// Geometric wave patterns
var waveFreq = 0.2 + mazeSeed % 5 * 0.1;
var waveHeight = 2 + mazeSeed % 3;
var expectedY = Math.floor(Math.sin(i * waveFreq) * waveHeight) + Math.floor((gridHeight - 12 + 12) / 2);
if (Math.abs(j - expectedY) < 2 && i >= 6 && i <= gridWidth - 7) {
if (seededRandom() > 0.5) {
cellType = 4;
}
}
} else if (mazePattern < 60) {
// Diamond and rhombus patterns
var midX = Math.floor(gridWidth / 2);
var midY = Math.floor((gridHeight - 12 + 12) / 2);
var manhattanDist = Math.abs(i - midX) + Math.abs(j - midY);
var diamondSize = 3 + mazeSeed % 4;
if (manhattanDist >= diamondSize && manhattanDist <= diamondSize + 3 && manhattanDist % 2 === 0) {
if (seededRandom() > 0.5) {
cellType = 4;
}
}
} else if (mazePattern < 80) {
// Hexagonal and triangular patterns
var hexSize = 4 + mazeSeed % 3;
var hexX = Math.floor(i / hexSize);
var hexY = Math.floor(j / hexSize);
if ((hexX + hexY) % 3 === 0 && (i % hexSize < 2 || j % hexSize < 2) && i >= 6 && i <= gridWidth - 7) {
if (seededRandom() > 0.4) {
cellType = 4;
}
}
} else {
// Spiral and radial patterns
var centerX = Math.floor(gridWidth / 2);
var centerY = Math.floor((gridHeight - 12 + 12) / 2);
var angle = Math.atan2(j - centerY, i - centerX);
var spiralArms = 4 + mazeSeed % 4;
var armIndex = Math.floor(angle / (Math.PI * 2 / spiralArms));
var distance = Math.sqrt((i - centerX) * (i - centerX) + (j - centerY) * (j - centerY));
if (armIndex % 2 === 0 && distance > 2 && distance < 7 && i >= 6 && i <= gridWidth - 7) {
if (seededRandom() > 0.45) {
cellType = 4;
}
}
}
// Add strategic scattered obstacles with lower density
if (i >= 7 && i <= gridWidth - 8 && j >= 14 && j <= gridHeight - 14) {
if (seededRandom() > 0.8) {
cellType = 4;
}
}
}
}
// Central castle keep - always present
if (i >= 10 && i <= 13 && j >= gridHeight - 16 && j <= gridHeight - 13) {
if (!(i === 11 || i === 12) || !(j === gridHeight - 15 || j === gridHeight - 14)) {
cellType = 4;
}
}
}
// Spawn and goal areas
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
},
cell,
cellType,
mazeSeed,
horizontalWallRows,
hr,
verticalWallCols,
vc,
debugCell;
for (var j = 0; j < gridHeight; j++) {
_loop();
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1 && node.type != 4) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.up.type != 4 && node.left.type != 1 && node.left.type != 4 && node.upLeft && node.upLeft.type != 1 && node.upLeft.type != 4) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.up.type != 4 && node.right.type != 1 && node.right.type != 4 && node.upRight && node.upRight.type != 1 && node.upRight.type != 4) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.down.type != 4 && node.right.type != 1 && node.right.type != 4 && node.downRight && node.downRight.type != 1 && node.downRight.type != 4) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.down.type != 4 && node.left.type != 1 && node.left.type != 4 && node.downLeft && node.downLeft.type != 1 && node.downLeft.type != 4) {
processNode(node.downLeft, targetScore, node);
}
targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
// Check if any spawn point has a valid path to any goal
var hasValidPath = false;
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId === pathId) {
hasValidPath = true;
break;
}
}
// Ensure maze is navigable - return false if paths are blocked
if (!hasValidPath) {
console.log("Maze blocks all paths - this layout will be regenerated");
return true; // Signal that maze needs regeneration
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed * gameSpeed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed * gameSpeed;
enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget || !cell || cell.pathId !== pathId) {
// Reset target if we don't have one or if current cell is invalid
if (cell && cell.pathId === pathId && cell.targets && cell.targets.length > 0) {
enemy.currentTarget = cell.targets[0];
} else {
// If current cell is invalid, try to find nearest valid cell
var nearestValidCell = null;
var minDistance = Infinity;
// Search for nearby valid cells
for (var searchX = Math.max(0, enemy.cellX - 2); searchX <= Math.min(grid.cells.length - 1, enemy.cellX + 2); searchX++) {
for (var searchY = Math.max(0, enemy.cellY - 2); searchY <= Math.min(grid.cells[0].length - 1, enemy.cellY + 2); searchY++) {
var searchCell = grid.getCell(searchX, searchY);
if (searchCell && searchCell.pathId === pathId && searchCell.targets && searchCell.targets.length > 0) {
var dx = searchX - enemy.cellX;
var dy = searchY - enemy.cellY;
var distance = dx * dx + dy * dy;
if (distance < minDistance) {
minDistance = distance;
nearestValidCell = searchCell;
}
}
}
}
if (nearestValidCell) {
enemy.currentTarget = nearestValidCell;
// Snap enemy to the valid cell position
enemy.cellX = nearestValidCell.x;
enemy.cellY = nearestValidCell.y;
enemy.currentCellX = nearestValidCell.x;
enemy.currentCellY = nearestValidCell.y;
}
}
}
if (enemy.currentTarget) {
// Validate that current target is still valid
if (enemy.currentTarget.pathId !== pathId) {
enemy.currentTarget = null;
return; // Will be fixed next frame
}
// If we have a better path from current cell, use it
if (cell && cell.pathId === pathId && cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed * gameSpeed) {
// Reached target, snap to grid and clear target
enemy.cellX = enemy.currentTarget.x;
enemy.cellY = enemy.currentTarget.y;
enemy.currentCellX = enemy.currentTarget.x;
enemy.currentCellY = enemy.currentTarget.y;
enemy.currentTarget = null;
// Don't return here, let position update happen
} else {
// Move toward target
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed * gameSpeed;
enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed;
}
} else {
// No valid target - try to get unstuck by moving toward goal
var goalFound = false;
for (var g = 0; g < self.goals.length; g++) {
var goal = self.goals[g];
var gx = goal.x - enemy.currentCellX;
var gy = goal.y - enemy.currentCellY;
if (Math.abs(gx) > 0.1 || Math.abs(gy) > 0.1) {
var gAngle = Math.atan2(gy, gx);
enemy.currentCellX += Math.cos(gAngle) * enemy.speed * gameSpeed * 0.5; // Move slower when lost
enemy.currentCellY += Math.sin(gAngle) * enemy.speed * gameSpeed * 0.5;
goalFound = true;
break;
}
}
}
// Update visual position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var MainMenu = Container.expand(function () {
var self = Container.call(this);
// Center the menu container
self.x = 2048 / 2;
self.y = 2732 / 2;
// Background overlay
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 2732;
menuBackground.tint = 0x000000;
menuBackground.alpha = 0.8;
// Current state: 'difficulty' or 'empire'
self.currentState = 'difficulty';
self.selectedDifficulty = null;
// Difficulty selection elements
var difficultyTitle = new Text2("Select Difficulty", {
size: 120,
fill: 0xFFFFFF,
weight: 800
});
difficultyTitle.anchor.set(0.5, 0.5);
difficultyTitle.y = -600;
self.addChild(difficultyTitle);
// Difficulty buttons
var difficulties = ['easy', 'normal', 'hard', 'hardcore', 'ultra'];
var difficultyButtons = [];
var difficultyButtonSpacing = 150;
var difficultyStartY = -300;
for (var i = 0; i < difficulties.length; i++) {
var difficulty = difficulties[i];
var button = new Container();
var buttonBG = button.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBG.width = 600;
buttonBG.height = 120;
// Set button color based on difficulty
switch (difficulty) {
case 'easy':
buttonBG.tint = 0x00FF00; // Green
break;
case 'hard':
buttonBG.tint = 0xFF8800; // Orange
break;
case 'hardcore':
buttonBG.tint = 0xFF0000; // Red
break;
case 'ultra':
buttonBG.tint = 0x800080; // Purple for ultra
break;
default:
buttonBG.tint = 0x0088FF; // Blue for normal
break;
}
var buttonText = new Text2(difficulty.charAt(0).toUpperCase() + difficulty.slice(1), {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.y = difficultyStartY + i * difficultyButtonSpacing;
button.difficulty = difficulty;
button.down = function () {
LK.getSound('Click').play();
// Show confirmation popup for Easy, Normal, and Hard, direct to empire selection for hardcore and ultra
if (this.difficulty === 'easy' || this.difficulty === 'normal' || this.difficulty === 'hard') {
self.showConfirmationPopup(this.difficulty);
} else {
self.selectedDifficulty = this.difficulty;
self.showEmpireSelection();
}
};
self.addChild(button);
difficultyButtons.push(button);
}
// Empire selection elements (initially hidden)
var empireTitle = new Text2("Choose Your Empire!", {
size: 100,
fill: 0xFFFFFF,
weight: 800
});
empireTitle.anchor.set(0.5, 0.5);
empireTitle.y = -700;
empireTitle.visible = false;
self.addChild(empireTitle);
// Empire buttons - 9 different empires including Normal
var empires = [{
name: 'Normal',
color: 0xAAAAAA,
description: ''
}, {
name: 'Austria',
color: 0x00FF00,
description: ''
}, {
name: 'Hungary',
color: 0x0088FF,
description: ''
}, {
name: 'Ottoman',
color: 0xFF8800,
description: ''
}, {
name: 'French',
color: 0xFF0000,
description: ''
}, {
name: 'England',
color: 0x800080,
description: ''
}, {
name: 'Scotch',
color: 0x008080,
description: ''
}, {
name: 'Kalmar Union',
color: 0x006400,
description: ''
}, {
name: 'Byzantine',
color: 0xFF69B4,
description: ''
}];
var empireButtons = [];
var empireButtonSpacing = 200;
var empireStartY = -400;
for (var i = 0; i < empires.length; i++) {
var empire = empires[i];
var button = new Container();
var buttonBG = button.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBG.width = 528;
buttonBG.height = 176;
buttonBG.tint = empire.color;
var buttonText = new Text2(empire.name, {
size: 53,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
buttonText.y = 0;
button.addChild(buttonText);
var descText = new Text2(empire.description, {
size: 35,
fill: 0xFFFFFF,
weight: 400
});
descText.anchor.set(0.5, 0.5);
descText.y = 40;
button.addChild(descText);
button.y = empireStartY + i * empireButtonSpacing;
button.visible = false; // Initially hidden
button.down = function (empireData) {
return function () {
LK.getSound('Click').play();
// Show empire characteristics menu instead of starting game directly
self.showEmpireCharacteristics(empireData);
};
}(empire);
self.addChild(button);
empireButtons.push(button);
}
// No exit buttons needed
var exitButtons = [];
// Add confirmation popup for Easy and Hard difficulties
self.confirmationPopup = null;
self.showConfirmationPopup = function (difficulty) {
// Remove any existing confirmation popup
if (self.confirmationPopup) {
self.removeChild(self.confirmationPopup);
self.confirmationPopup.destroy();
}
// Create confirmation popup
self.confirmationPopup = new Container();
// Background for confirmation popup
var confirmBG = self.confirmationPopup.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
confirmBG.width = 1200;
confirmBG.height = 600;
confirmBG.alpha = 0.95;
// Confirmation title
var confirmText = "Are you sure: ";
if (difficulty === "easy") {
confirmText += "Easy?";
} else if (difficulty === "normal") {
confirmText += "Normal?";
} else if (difficulty === "hard") {
confirmText += "Hard?";
} else {
confirmText = "Are You Sure?";
}
var confirmTitle = new Text2(confirmText, {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
confirmTitle.anchor.set(0.5, 0.5);
confirmTitle.y = -150;
self.confirmationPopup.addChild(confirmTitle);
// Yes button
var yesButton = new Container();
var yesBG = yesButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
yesBG.width = 300;
yesBG.height = 120;
yesBG.tint = 0x00AA00;
var yesText = new Text2("Yes", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
yesText.anchor.set(0.5, 0.5);
yesButton.addChild(yesText);
yesButton.x = -200;
yesButton.y = 100;
yesButton.down = function () {
LK.getSound('Click').play();
// Remove confirmation popup and proceed to empire selection
self.removeChild(self.confirmationPopup);
self.confirmationPopup.destroy();
self.confirmationPopup = null;
// Show empire selection
self.selectedDifficulty = difficulty;
self.showEmpireSelection();
};
self.confirmationPopup.addChild(yesButton);
// No button
var noButton = new Container();
var noBG = noButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
noBG.width = 300;
noBG.height = 120;
noBG.tint = 0xAA0000;
var noText = new Text2("No", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
noText.anchor.set(0.5, 0.5);
noButton.addChild(noText);
noButton.x = 200;
noButton.y = 100;
noButton.down = function () {
LK.getSound('Click').play();
// Remove confirmation popup and return to difficulty selection
self.removeChild(self.confirmationPopup);
self.confirmationPopup.destroy();
self.confirmationPopup = null;
self.selectedDifficulty = null;
// Stay on difficulty selection screen - no state change needed
};
self.confirmationPopup.addChild(noButton);
self.addChild(self.confirmationPopup);
};
self.showEmpireSelection = function () {
// Check if hardcore or ultra difficulty was selected
if (self.selectedDifficulty === 'hardcore') {
// Show hardcore warning popup instead of empire selection
self.showHardcoreWarning();
return;
} else if (self.selectedDifficulty === 'ultra') {
// Show ultra warning popup instead of empire selection
self.showUltraWarning();
return;
}
self.currentState = 'empire';
// Hide difficulty elements
difficultyTitle.visible = false;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = false;
}
// Show empire elements
empireTitle.visible = true;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = true;
}
// No exit buttons to show
};
self.showHardcoreWarning = function () {
// Remove any existing warning popup
if (self.hardcoreWarningPopup) {
self.removeChild(self.hardcoreWarningPopup);
self.hardcoreWarningPopup.destroy();
}
// Create hardcore warning popup
self.hardcoreWarningPopup = new Container();
// Background for warning popup
var warningBG = self.hardcoreWarningPopup.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
warningBG.width = 1600;
warningBG.height = 800;
warningBG.alpha = 0.95;
// Warning title
var warningTitle = new Text2("WARNING", {
size: 100,
fill: 0xFF0000,
weight: 800
});
warningTitle.anchor.set(0.5, 0.5);
warningTitle.y = -250;
self.hardcoreWarningPopup.addChild(warningTitle);
// Warning message
var warningText = new Text2("This difficulty is not one of the original game\ndifficulties for Honorbound: Rise on the Fifth Dawn.\nIT IS FOR ADVANCED PLAYERS ONLY!", {
size: 60,
fill: 0xFFFFFF,
weight: 400
});
warningText.anchor.set(0.5, 0.5);
warningText.y = -50;
self.hardcoreWarningPopup.addChild(warningText);
// Okay button
var okayButton = new Container();
var okayBG = okayButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
okayBG.width = 300;
okayBG.height = 120;
okayBG.tint = 0x00AA00;
var okayText = new Text2("Okay", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
okayText.anchor.set(0.5, 0.5);
okayButton.addChild(okayText);
okayButton.x = -200;
okayButton.y = 200;
okayButton.down = function () {
LK.getSound('Click').play();
// Remove warning popup and proceed to empire selection
self.removeChild(self.hardcoreWarningPopup);
self.hardcoreWarningPopup.destroy();
self.hardcoreWarningPopup = null;
// Show empire selection
self.currentState = 'empire';
// Hide difficulty elements
difficultyTitle.visible = false;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = false;
}
// Show empire elements
empireTitle.visible = true;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = true;
}
};
self.hardcoreWarningPopup.addChild(okayButton);
// Back button
var backButton = new Container();
var backBG = backButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
backBG.width = 300;
backBG.height = 120;
backBG.tint = 0xAA0000;
var backText = new Text2("Back", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
backText.anchor.set(0.5, 0.5);
backButton.addChild(backText);
backButton.x = 200;
backButton.y = 200;
backButton.down = function () {
LK.getSound('Click').play();
// Remove warning popup and return to difficulty selection
self.removeChild(self.hardcoreWarningPopup);
self.hardcoreWarningPopup.destroy();
self.hardcoreWarningPopup = null;
self.selectedDifficulty = null;
// Return to difficulty selection
self.currentState = 'difficulty';
// Show difficulty elements
difficultyTitle.visible = true;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = true;
}
// Hide empire elements
empireTitle.visible = false;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = false;
}
};
self.hardcoreWarningPopup.addChild(backButton);
self.addChild(self.hardcoreWarningPopup);
};
self.showUltraWarning = function () {
// Remove any existing warning popup
if (self.ultraWarningPopup) {
self.removeChild(self.ultraWarningPopup);
self.ultraWarningPopup.destroy();
}
// Create ultra warning popup
self.ultraWarningPopup = new Container();
// Background for warning popup
var warningBG = self.ultraWarningPopup.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
warningBG.width = 1600;
warningBG.height = 800;
warningBG.alpha = 0.95;
// Warning title
var warningTitle = new Text2("ULTRA MODE", {
size: 100,
fill: 0x800080,
weight: 800
});
warningTitle.anchor.set(0.5, 0.5);
warningTitle.y = -250;
self.ultraWarningPopup.addChild(warningTitle);
// Warning message
var warningText = new Text2("This is an extreme endurance mode!\n30 waves without any King.\nEverything else is the same as Hardcore.\nAre you ready for the ultimate challenge?", {
size: 60,
fill: 0xFFFFFF,
weight: 400
});
warningText.anchor.set(0.5, 0.5);
warningText.y = -50;
self.ultraWarningPopup.addChild(warningText);
// Okay button
var okayButton = new Container();
var okayBG = okayButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
okayBG.width = 300;
okayBG.height = 120;
okayBG.tint = 0x00AA00;
var okayText = new Text2("Okay", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
okayText.anchor.set(0.5, 0.5);
okayButton.addChild(okayText);
okayButton.x = -200;
okayButton.y = 200;
okayButton.down = function () {
LK.getSound('Click').play();
// Remove warning popup and proceed to empire selection
self.removeChild(self.ultraWarningPopup);
self.ultraWarningPopup.destroy();
self.ultraWarningPopup = null;
// Show empire selection
self.currentState = 'empire';
// Hide difficulty elements
difficultyTitle.visible = false;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = false;
}
// Show empire elements
empireTitle.visible = true;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = true;
}
};
self.ultraWarningPopup.addChild(okayButton);
// Back button
var backButton = new Container();
var backBG = backButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
backBG.width = 300;
backBG.height = 120;
backBG.tint = 0xAA0000;
var backText = new Text2("Back", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
backText.anchor.set(0.5, 0.5);
backButton.addChild(backText);
backButton.x = 200;
backButton.y = 200;
backButton.down = function () {
LK.getSound('Click').play();
// Remove warning popup and return to difficulty selection
self.removeChild(self.ultraWarningPopup);
self.ultraWarningPopup.destroy();
self.ultraWarningPopup = null;
self.selectedDifficulty = null;
// Return to difficulty selection
self.currentState = 'difficulty';
// Show difficulty elements
difficultyTitle.visible = true;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = true;
}
// Hide empire elements
empireTitle.visible = false;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = false;
}
};
self.ultraWarningPopup.addChild(backButton);
self.addChild(self.ultraWarningPopup);
};
self.showEmpireCharacteristics = function (empireData) {
// Remove any existing characteristics popup
if (self.characteristicsPopup) {
self.removeChild(self.characteristicsPopup);
self.characteristicsPopup.destroy();
}
// Create characteristics popup
self.characteristicsPopup = new Container();
// Background for characteristics popup
var charBG = self.characteristicsPopup.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
charBG.width = 1400;
charBG.height = 1000;
charBG.alpha = 0.95;
// Empire name title
var titleText = new Text2(empireData.name + " Empire", {
size: 80,
fill: empireData.color,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -400;
self.characteristicsPopup.addChild(titleText);
// Empire characteristics text
var characteristicsText = "";
switch (empireData.name) {
case 'Normal':
characteristicsText = "Advantages:\nβ’ Standard gameplay\nβ’ No special bonuses\nβ’ Balanced empire\nβ’ Good for beginners";
break;
case 'Austria':
characteristicsText = "Advantages:\nβ’ +20% tower fire rate\nβ’ All towers attack faster\nβ’ Excellent for rapid defense";
break;
case 'Hungary':
characteristicsText = "Advantages:\nβ’ Tower cost -5 gold\n (Normal+ difficulty)\nβ’ Better economy management\nβ’ More affordable towers";
break;
case 'Ottoman':
characteristicsText = "Advantages:\nβ’ Start with 100 gold\n (instead of 80)\nβ’ +20% damage on all attacks\nβ’ Strong early game";
break;
case 'French':
characteristicsText = "Advantages:\nβ’ +20% tower durability\nβ’ Towers have more health\nβ’ Better defensive capability";
break;
case 'England':
characteristicsText = "Advantages:\nβ’ Start with 100 gold\n (instead of 80)\nβ’ 3 fewer enemies per wave\nβ’ Easier defense management";
break;
case 'Scotch':
characteristicsText = "Advantages:\nβ’ Tower costs -3 gold\nβ’ Archer range +50%\nβ’ Better defense capability";
break;
case 'Kalmar Union':
characteristicsText = "Advantages:\nβ’ Healer range +100%\nβ’ Better healing coverage\nβ’ Improved tower protection";
break;
case 'Byzantine':
characteristicsText = "Advantages:\nβ’ Tower durability +100%\nβ’ Towers have double health\nDisadvantages:\nβ’ +3 enemies per wave\nβ’ Higher enemy pressure";
break;
default:
characteristicsText = "Advantages:\nβ’ Standard empire\nβ’ No special bonuses\nβ’ Balanced gameplay";
}
var infoText = new Text2(characteristicsText, {
size: 60,
fill: 0xFFFFFF,
weight: 400
});
infoText.anchor.set(0.5, 0.5);
infoText.y = -150;
self.characteristicsPopup.addChild(infoText);
// Select Empire button
var selectButton = new Container();
var selectBG = selectButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
selectBG.width = 400;
selectBG.height = 120;
selectBG.tint = 0x00AA00;
var selectText = new Text2("Select Empire", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
selectText.anchor.set(0.5, 0.5);
selectButton.addChild(selectText);
selectButton.y = 200;
selectButton.down = function () {
LK.getSound('Click').play();
// Store selected empire and start tutorial for easy/normal, or start game directly for others
gameSettings.selectedEmpire = empireData.name;
self.removeChild(self.characteristicsPopup);
self.characteristicsPopup.destroy();
self.characteristicsPopup = null;
// Show tutorial for easy and normal modes
if (self.selectedDifficulty === 'easy' || self.selectedDifficulty === 'normal') {
self.startTutorial();
} else {
self.startGame();
}
};
self.characteristicsPopup.addChild(selectButton);
// Back button
var backButton = new Container();
var backBG = backButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
backBG.width = 200;
backBG.height = 100;
backBG.tint = 0xAA0000;
var backText = new Text2("Back", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
backText.anchor.set(0.5, 0.5);
backButton.addChild(backText);
backButton.y = 350;
backButton.down = function () {
LK.getSound('Click').play();
self.removeChild(self.characteristicsPopup);
self.characteristicsPopup.destroy();
self.characteristicsPopup = null;
};
self.characteristicsPopup.addChild(backButton);
self.addChild(self.characteristicsPopup);
};
self.startTutorial = function () {
// Check if tutorial has already been completed
if (storage.tutorialCompleted) {
// Skip tutorial and start game directly
self.startGame();
return;
}
// Remove start menu first
self.destroy();
// Apply game settings but don't enable interactions yet
gameSettings.difficulty = self.selectedDifficulty;
gameSettings.selectedDifficulty = self.selectedDifficulty;
updateTotalWaves();
// Apply empire starting gold bonuses
if (gameSettings.selectedEmpire === 'Ottoman' || gameSettings.selectedEmpire === 'England') {
setGold(100);
} else {
setGold(80);
}
// Show UI elements but keep interactions disabled
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update tower cost displays
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].updateCostLabels();
}
// Create and show tutorial
var tutorial = new Tutorial(self.selectedDifficulty);
tutorial.x = 2048 / 2;
tutorial.y = 2732 / 2;
game.addChild(tutorial);
// Override tutorial end to return to main menu
tutorial.endTutorial = function () {
tutorial.isActive = false;
tutorial.destroy();
// Mark tutorial as completed and save to storage
storage.tutorialCompleted = true;
// Return to main menu after tutorial completion
var mainMenu = new MainMenu();
game.addChild(mainMenu);
};
tutorial.startTutorial();
};
self.startGame = function () {
// Apply difficulty settings
gameSettings.difficulty = self.selectedDifficulty;
gameSettings.selectedDifficulty = self.selectedDifficulty;
// Update totalWaves based on selected difficulty
updateTotalWaves();
// Apply empire starting gold bonuses
if (gameSettings.selectedEmpire === 'Ottoman' || gameSettings.selectedEmpire === 'England') {
setGold(100);
} else {
setGold(80);
}
// Enable game interactions
gameStartAllowed = true;
// Restore visibility of hidden elements when game starts
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update tower cost displays for new difficulty
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].updateCostLabels();
}
// Exit menu when empire is selected
self.destroy();
};
return self;
});
var MusicControl = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 200;
buttonBackground.height = 70;
buttonBackground.tint = 0xFF8800;
var musicText = new Text2("Music1", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
musicText.anchor.set(0.5, 0.5);
self.addChild(musicText);
var currentMusicIndex = 1;
self.updateDisplay = function () {
var musicName = currentMusicIndex === 8 ? "Nomusic" : "Music" + currentMusicIndex;
musicText.setText(musicName);
// Update button color based on music selection
switch (currentMusicIndex) {
case 1:
buttonBackground.tint = 0xFF8800; // Orange for Music1
break;
case 2:
buttonBackground.tint = 0x8800FF; // Purple for Music2
break;
case 3:
buttonBackground.tint = 0x00FF88; // Green for Music3
break;
case 4:
buttonBackground.tint = 0xFF0088; // Pink for Music4
break;
case 5:
buttonBackground.tint = 0x0088FF; // Blue for Music5
break;
case 6:
buttonBackground.tint = 0x006400; // Dark green for Music6
break;
case 7:
buttonBackground.tint = 0x800080; // Purple for Music7
break;
case 8:
buttonBackground.tint = 0x444444; // Gray for Nomusic
break;
}
};
self.down = function () {
// Block music control if difficulty not selected
if (!gameStartAllowed) {
return;
}
// Cycle through music options 1-8 (including Nomusic at position 8)
currentMusicIndex = currentMusicIndex >= 8 ? 1 : currentMusicIndex + 1;
self.updateDisplay();
// Play the corresponding music track or stop music for Nomusic
if (currentMusicIndex === 8) {
LK.stopMusic();
} else {
var musicTrack = "warmusic" + (currentMusicIndex === 1 ? "" : currentMusicIndex);
LK.playMusic(musicTrack);
}
};
// Initialize display
self.updateDisplay();
return self;
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
// Block next wave if difficulty not selected
if (!gameStartAllowed) {
return;
}
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
var notification = game.addChild(new Notification("Wave " + currentWave + " activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'default';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset(towerSet === 'futuristic' ? 'future' : towerSet === 'trench' ? 'builds' : towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'Farmland' : 'builds' : 'tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var displayNameShadow = self.towerType === 'default' ? towerSet === 'futuristic' ? 'Robot' : 'Infantry' : self.towerType === 'rapid' ? towerSet === 'futuristic' ? 'Lazerer' : 'Catapult' : self.towerType === 'splash' ? towerSet === 'futuristic' ? 'Fwacha' : 'Hwacha' : self.towerType === 'sniper' ? towerSet === 'futuristic' ? 'Eaglebot' : 'Archer' : self.towerType === 'slow' ? towerSet === 'futuristic' ? 'f-Bullet' : towerSet === 'builds' ? 'Upgrader' : 'Bullet' : self.towerType === 'poison' ? towerSet === 'futuristic' ? 'Hospital' : 'Healer' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabelShadow = new Text2(displayNameShadow, {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -20 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var displayName = self.towerType === 'default' ? towerSet === 'futuristic' ? 'Robot' : towerSet === 'trench' ? 'Blocker' : towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'House' : 'Build1' : 'Infantry' : self.towerType === 'rapid' ? towerSet === 'futuristic' ? 'Lazerer' : towerSet === 'trench' ? 'Trapper' : towerSet === 'builds' ? 'Farm' : 'Catapult' : self.towerType === 'splash' ? towerSet === 'futuristic' ? 'Fwacha' : towerSet === 'builds' ? 'Research' : 'Hwacha' : self.towerType === 'sniper' ? towerSet === 'futuristic' ? 'Eaglebot' : towerSet === 'trench' ? 'Money' : towerSet === 'builds' ? 'Miner' : 'Archer' : self.towerType === 'slow' ? towerSet === 'futuristic' ? 'f-Bullet' : towerSet === 'builds' ? 'Upgrader' : 'Bullet' : self.towerType === 'poison' ? towerSet === 'futuristic' ? 'Hospital' : towerSet === 'builds' ? 'Smith' : 'Healer' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabel = new Text2(displayName, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -20; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var displayCost = towerCost;
var towerOreCost = getTowerOreCost(self.towerType);
if (towerOreCost > 0) {
displayCost = towerCost + "G/" + towerOreCost + "O";
}
var costLabel = new Text2(displayCost, {
size: 50,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 20 + 12;
self.addChild(costLabel);
self.updateCostLabels = function () {
var newCost = getTowerCost(self.towerType);
var towerOreCost = getTowerOreCost(self.towerType);
var displayCost = newCost;
if (towerOreCost > 0) {
displayCost = newCost + "G/" + towerOreCost + "O";
}
costLabel.setText(displayCost);
costLabelShadow.setText(displayCost);
};
self.update = function () {
// Check if player can afford this tower
var towerOreCost = getTowerOreCost(self.towerType);
var canAfford = gold >= getTowerCost(self.towerType) && ore >= towerOreCost;
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var SpeedControl = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 200;
buttonBackground.height = 70;
buttonBackground.tint = 0x0088FF;
var speedText = new Text2("Normal", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
speedText.anchor.set(0.5, 0.5);
self.addChild(speedText);
self.updateDisplay = function () {
var speedName = gameSpeedOptions[currentSpeedIndex];
speedText.setText(speedName.charAt(0).toUpperCase() + speedName.slice(1));
// Update game speed based on selection
switch (speedName) {
case 'slow':
gameSpeed = 0.5;
buttonBackground.tint = 0xFF8800; // Orange for slow
break;
case 'normal':
gameSpeed = 1.0;
buttonBackground.tint = 0x0088FF; // Blue for normal
break;
case 'fast':
gameSpeed = 2.0;
buttonBackground.tint = 0x00FF00; // Green for fast
break;
case 'ultra':
gameSpeed = 2.5; // 25% faster than fast
buttonBackground.tint = 0xFF0000; // Red for ultra
break;
case 'paused':
gameSpeed = 0.0; // Complete pause
buttonBackground.tint = 0x666666; // Gray for paused
break;
}
};
self.down = function () {
// Block speed control if difficulty not selected
if (!gameStartAllowed) {
return;
}
// Cycle through speed options
currentSpeedIndex = (currentSpeedIndex + 1) % gameSpeedOptions.length;
self.updateDisplay();
};
// Initialize display
self.updateDisplay();
return self;
});
var StartMenu = Container.expand(function () {
var self = Container.call(this);
// Center the menu container
self.x = 2048 / 2;
self.y = 2732 / 2;
// Background overlay
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 2732;
menuBackground.tint = 0x000000;
menuBackground.alpha = 0.8;
// Game title
var titleText = new Text2("HONORBOUND", {
size: 150,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -250;
self.addChild(titleText);
// Subtitle
var subtitleText = new Text2("Rise on the Fifth Dawn", {
size: 80,
fill: 0xCCCCCC,
weight: 400
});
subtitleText.anchor.set(0.5, 0.5);
subtitleText.y = -150;
self.addChild(subtitleText);
// Play button
var playButton = new Container();
var playButtonBG = playButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
playButtonBG.width = 400;
playButtonBG.height = 120;
playButtonBG.tint = 0x00AA00; // Green color
var playText = new Text2("PLAY", {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
playText.anchor.set(0.5, 0.5);
playButton.addChild(playText);
playButton.y = 50;
playButton.down = function () {
LK.getSound('Click').play();
// Remove start menu and show main menu (difficulty selection)
self.destroy();
var mainMenu = new MainMenu();
game.addChild(mainMenu);
};
self.addChild(playButton);
// Story Mode button
var storyButton = new Container();
var storyButtonBG = storyButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
storyButtonBG.width = 400;
storyButtonBG.height = 120;
storyButtonBG.tint = 0x1976D2; // Blue color for Story Mode
var storyText = new Text2("Story Mode", {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
storyText.anchor.set(0.5, 0.5);
storyButton.addChild(storyText);
storyButton.y = playButton.y + 160; // Place below Play button
// Tutorial button
var tutorialButton = new Container();
var tutorialButtonBG = tutorialButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
tutorialButtonBG.width = 400;
tutorialButtonBG.height = 120;
tutorialButtonBG.tint = 0x4CAF50; // Green color for Tutorial
var tutorialText = new Text2("Tutorial", {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
tutorialText.anchor.set(0.5, 0.5);
tutorialButton.addChild(tutorialText);
tutorialButton.y = storyButton.y + 160; // Place below Story Mode button
tutorialButton.down = function () {
LK.getSound('Click').play();
// Check if tutorial has already been completed
if (storage.tutorialCompleted) {
// Show notification that tutorial was already completed and start normal game
var notification = game.addChild(new Notification("Tutorial already completed! Starting normal game."));
notification.x = 2048 / 2;
notification.y = 2732 / 2;
// Remove start menu and start normal game
self.destroy();
gameSettings.difficulty = 'normal';
gameSettings.selectedEmpire = 'Normal';
updateTotalWaves();
setGold(80);
gameStartAllowed = true;
// Show UI elements
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update tower cost displays
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].updateCostLabels();
}
return;
}
// Remove start menu and go directly to tutorial with normal difficulty and Normal empire
self.destroy();
// Set up tutorial with default settings
gameSettings.difficulty = 'normal';
gameSettings.selectedEmpire = 'Normal';
updateTotalWaves();
setGold(80);
// Show UI elements but keep interactions disabled initially
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update tower cost displays
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].updateCostLabels();
}
// Create and show tutorial
var tutorial = new Tutorial('normal');
tutorial.x = 2048 / 2;
tutorial.y = 2732 / 2;
game.addChild(tutorial);
// Override tutorial end to return to main menu
tutorial.endTutorial = function () {
tutorial.isActive = false;
tutorial.destroy();
// Mark tutorial as completed and save to storage
storage.tutorialCompleted = true;
// Return to main menu after tutorial completion
var mainMenu = new MainMenu();
game.addChild(mainMenu);
};
tutorial.startTutorial();
};
self.addChild(tutorialButton);
storyButton.down = function () {
LK.getSound('Click').play();
// Remove start menu
self.destroy();
// --- STORY MODE SEQUENCE ---
// Block all input during story mode
gameStartAllowed = false;
// Set a flag for story mode
gameSettings.storyMode = true;
// --- Play music7 for story mode ---
LK.playMusic('warmusic7');
// 1. Black overlay (fade in)
var blackOverlay = new Container();
var blackBG = blackOverlay.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
blackBG.width = 2048;
blackBG.height = 2732;
blackBG.tint = 0x000000;
blackBG.alpha = 1;
blackOverlay.x = 2048 / 2;
blackOverlay.y = 2732 / 2;
blackOverlay.alpha = 0;
game.addChild(blackOverlay);
// Fade in black overlay over 1s
tween(blackOverlay, {
alpha: 1
}, {
duration: 1000,
easing: tween.linear,
onFinish: function onFinish() {
// 2. Show story asset (fade in)
var storyAsset = new Container();
var storyBG = storyAsset.attachAsset('story', {
anchorX: 0.5,
anchorY: 0.5
});
storyBG.width = 2048;
storyBG.height = 2732;
storyBG.alpha = 1;
storyAsset.x = 2048 / 2;
storyAsset.y = 2732 / 2;
storyAsset.alpha = 0;
game.addChild(storyAsset);
tween(storyAsset, {
alpha: 1
}, {
duration: 1000,
easing: tween.linear
});
// 3. After 1s, show first text (fade in)
LK.setTimeout(function () {
var text1 = new Text2("Everyone is dead, you are\nthe only one left.", {
size: 90,
fill: 0xFFFFFF,
weight: 800
});
text1.anchor.set(0.5, 0.5);
text1.x = 2048 / 2;
text1.y = 2732 / 2 - 200;
text1.alpha = 0;
game.addChild(text1);
tween(text1, {
alpha: 1
}, {
duration: 600,
easing: tween.linear
});
// After 3s, fade out text1
LK.setTimeout(function () {
tween(text1, {
alpha: 0
}, {
duration: 1000,
easing: tween.linear,
onFinish: function onFinish() {
text1.destroy();
}
});
}, 3000);
// 4. After 4s, show second text (fade in)
LK.setTimeout(function () {
var text2 = new Text2("The enemies that are coming\nnow are advancing towards\nthe capital.", {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
text2.anchor.set(0.5, 0.5);
text2.x = 2048 / 2;
text2.y = 2732 / 2 - 60;
text2.alpha = 0;
game.addChild(text2);
tween(text2, {
alpha: 1
}, {
duration: 600,
easing: tween.linear
});
// After 5s, fade out text2
LK.setTimeout(function () {
tween(text2, {
alpha: 0
}, {
duration: 1000,
easing: tween.linear,
onFinish: function onFinish() {
text2.destroy();
}
});
}, 5000);
// 5. After 5s (total 9s), show third text (fade in)
LK.setTimeout(function () {
var text3 = new Text2("You are the chosen one,\nI have complete confidence\nthat you will save our\nempire. (From Emperor)", {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
text3.anchor.set(0.5, 0.5);
text3.x = 2048 / 2;
text3.y = 2732 / 2 + 80;
text3.alpha = 0;
game.addChild(text3);
tween(text3, {
alpha: 1
}, {
duration: 600,
easing: tween.linear
});
// After 5s, fade out text3 and story asset, then start game
LK.setTimeout(function () {
tween(text3, {
alpha: 0
}, {
duration: 1000,
easing: tween.linear,
onFinish: function onFinish() {
text3.destroy();
}
});
// Fade out story asset and black overlay
tween(storyAsset, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
storyAsset.destroy();
}
});
tween(blackOverlay, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
blackOverlay.destroy();
// --- Start game in normal mode, Normal empire ---
gameSettings.difficulty = 'normal';
gameSettings.selectedEmpire = 'Normal';
updateTotalWaves();
setGold(80);
// Enable game interactions
gameStartAllowed = true;
// Restore visibility of hidden elements when story mode game starts
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update UI for new settings
updateUI();
// Remove any menus, show main game
// Remove all children except core layers and UI
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
// Remove any menu or overlay containers
if (child instanceof MainMenu || child instanceof StartMenu) {
game.removeChild(child);
child.destroy();
}
}
// --- Switch back to music1 after story mode ends ---
LK.playMusic('warmusic');
}
});
}, 5000);
}, 5000);
}, 4000);
}, 1000);
}
});
};
self.addChild(storyButton);
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.towerSet = towerSet;
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Tower health system
self.maxHealth = 100;
// Apply French empire bonus: 20% more tower durability
if (gameSettings.selectedEmpire === 'French') {
self.maxHealth = Math.floor(self.maxHealth * 1.2); // 20% more health
}
// Apply Byzantine empire bonus: 100% more tower durability
if (gameSettings.selectedEmpire === 'Byzantine') {
self.maxHealth = Math.floor(self.maxHealth * 2.0); // 100% more health
}
self.health = self.maxHealth;
// Special trench tower properties
self.isBlocker = false;
self.isTrapper = false;
self.isMoney = false;
self.goldTimer = 0;
self.goldInterval = 300; // 5 seconds at 60 FPS
// Initialize goldTimer for Farm functionality
if (self.towerSet === 'builds' && self.id === 'rapid') {
self.goldTimer = 0;
}
// Set trench tower properties based on ID
if (self.towerSet === 'trench') {
if (self.id === 'default') {
self.isBlocker = true;
} else if (self.id === 'rapid') {
self.isTrapper = true;
} else if (self.id === 'sniper') {
self.isMoney = true;
}
}
// Standardized method to get the current range of the tower
self.getRange = function () {
// Trench towers: only Trapper has range, Blocker and Money have no range
if (self.towerSet === 'trench') {
if (self.isTrapper) {
// Trapper: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
} else {
// Blocker and Money towers have no range
return 0;
}
}
// Always calculate range based on tower type and level
switch (self.id) {
case 'sniper':
// Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost
var baseRange;
if (self.level === self.maxLevel) {
baseRange = 12 * CELL_SIZE; // Significantly increased range for max level
} else {
baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE;
}
// Apply Scotch empire bonus: 50% more archer range
if (gameSettings.selectedEmpire === 'Scotch') {
baseRange = baseRange * 1.5;
}
return baseRange;
case 'splash':
// Splash: base 4, +0.4 per level (max ~8 blocks at max level)
return (4 + (self.level - 1) * 0.4) * CELL_SIZE;
case 'rapid':
// Rapid: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// f-Bullet: 2x range when futuristic unlocked
if (futuristicUnlocked) {
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE * 2; // 2x normal range
}
// Slow: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Hospital: 2x range when futuristic unlocked
if (futuristicUnlocked) {
var baseRange = (3.2 + (self.level - 1) * 0.5) * CELL_SIZE * 2; // 2x normal range
// Apply Kalmar Union empire bonus: 100% more healer range
if (gameSettings.selectedEmpire === 'Kalmar Union') {
baseRange = baseRange * 2.0;
}
return baseRange;
}
// Poison: base 3.2, +0.5 per level
var baseRange = (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
// Apply Kalmar Union empire bonus: 100% more healer range
if (gameSettings.selectedEmpire === 'Kalmar Union') {
baseRange = baseRange * 2.0;
}
return baseRange;
default:
// Default: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
// Check if futuristic has been unlocked and apply 3x fire rate boost
var fireRateMultiplier = futuristicUnlocked ? 1 / 3 : 1;
switch (self.id) {
case 'rapid':
self.fireRate = 30 * fireRateMultiplier;
self.damage = 5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90 * fireRateMultiplier;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 52 * fireRateMultiplier; // 30% faster than 75 (75 * 0.7 = 52.5, rounded to 52)
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50 * fireRateMultiplier;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70 * fireRateMultiplier;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
var baseGraphics = self.attachAsset(towerSet === 'futuristic' ? 'future' : towerSet === 'trench' ? 'builds' : towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'Farmland' : 'builds' : 'tower', {
anchorX: 0.5,
anchorY: 0.5
});
switch (self.id) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
// Add tower health bar
var towerHealthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var towerHealthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var towerHealthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
towerHealthBarBG.y = towerHealthBarOutline.y = towerHealthBar.y = baseGraphics.height / 2 + 15;
towerHealthBarOutline.x = -towerHealthBarOutline.width / 2;
towerHealthBarBG.x = towerHealthBar.x = -towerHealthBar.width / 2 - .5;
towerHealthBar.tint = 0x00ff00;
towerHealthBarBG.tint = 0xff0000;
self.towerHealthBar = towerHealthBar;
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
// Don't add arrow/gun graphics for trench towers
if (self.towerSet !== 'trench') {
var gunGraphics = gunContainer.attachAsset(towerSet === 'futuristic' ? 'farrow' : 'defense', {
anchorX: 0.5,
anchorY: 0.5
});
}
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// Make last upgrade level extra expensive
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// Increase tower health by 50% on upgrade
self.maxHealth = Math.floor(self.maxHealth * 1.5);
self.health = self.maxHealth; // Restore to full health on upgrade
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
var fireRateMultiplier = futuristicUnlocked ? 1 / 3 : 1;
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 30 - self.level * 9) * fireRateMultiplier; // double the effect
self.damage = 5 + self.level * 10; // double the effect
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(15, 30 - self.level * 3) * fireRateMultiplier; // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24) * fireRateMultiplier; // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8) * fireRateMultiplier;
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = 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);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Artillery towers can target all enemy types
if (self.id === 'splash') {
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
} else {
// Other towers can target both flying and ground enemies
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
}
}
}
if (!closestEnemy) {
self.targetEnemy = null;
}
return closestEnemy;
};
self.update = function () {
// Money tower gold generation
if (self.isMoney) {
// Only generate gold if game has started and is not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
self.goldTimer += gameSpeed;
if (self.goldTimer >= self.goldInterval) {
self.goldTimer = 0;
setGold(gold + 10);
var goldIndicator = new GoldIndicator(10, self.x, self.y);
game.addChild(goldIndicator);
}
}
}
// Miner functionality for sniper tower in builds set (ultra mode)
if (self.towerSet === 'builds' && self.id === 'sniper' && gameSettings.difficulty === 'ultra') {
// Only generate ore if game has started and is not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
self.goldTimer += gameSpeed;
if (self.goldTimer >= 600) {
// 10 seconds at 60 FPS
self.goldTimer = 0;
ore += 1;
var oreIndicator = new GoldIndicator("1 Ore", self.x, self.y);
game.addChild(oreIndicator);
updateUI();
}
}
}
// Farm functionality for rapid tower in builds set
if (self.towerSet === 'builds' && self.id === 'rapid') {
// Only generate gold if game has started and is not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
self.goldTimer += gameSpeed;
if (self.goldTimer >= 600) {
// 10 seconds at 60 FPS
self.goldTimer = 0;
setGold(gold + 2);
var goldIndicator = new GoldIndicator(2, self.x, self.y);
game.addChild(goldIndicator);
}
}
}
// Upgrader functionality for builds set (Build5/Upgrader)
if (self.towerSet === 'builds' && self.id === 'slow') {
// Set range to match Archer (sniper) tower
self.getRange = function () {
// Archer base: 5 + (level-1)*0.8, but use maxLevel for Upgrader
var baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE;
return baseRange;
};
// --- NEW: Instantly increase level by 1 for all towers in range for free ---
if (!self._upgraderBuffedTowers) self._upgraderBuffedTowers = [];
// Only activate if game started and not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
// Find all towers in range that haven't been buffed by this Upgrader
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
if (t !== self && self.isInRange(t)) {
if (!t._upgradedByUpgrader) t._upgradedByUpgrader = [];
if (t._upgradedByUpgrader.indexOf(self) === -1) {
// Only buff if not at max level
if (t.level < t.maxLevel) {
t.level += 1;
t.updateLevelIndicators && t.updateLevelIndicators();
t._upgradedByUpgrader.push(self);
self._upgraderBuffedTowers.push(t);
}
}
}
}
}
}
// Smith (Build6) functionality for builds set
if (self.towerSet === 'builds' && self.id === 'poison') {
// Set range to match Archer (sniper) tower
self.getRange = function () {
var baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE;
return baseRange;
};
// --- NEW: Double maxHealth of towers in range, fully heal every 5s, remove on destroy ---
if (!self._smithBuffedTowers) self._smithBuffedTowers = [];
// Only activate if game started and not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
// Double maxHealth of towers in range (only once per tower)
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
if (t !== self && self.isInRange(t)) {
if (!t._smithBuffedBy) t._smithBuffedBy = [];
if (t._smithBuffedBy.indexOf(self) === -1) {
t.maxHealth = t.maxHealth * 2;
t.health = t.maxHealth;
t._smithBuffedBy.push(self);
self._smithBuffedTowers.push(t);
if (t.towerHealthBar) t.towerHealthBar.width = 70;
}
}
}
// Every 5 seconds, fully heal towers in range
if (typeof self.smithHealTimer === "undefined") self.smithHealTimer = 0;
self.smithHealTimer += gameSpeed;
if (self.smithHealTimer >= 300) {
// 5 seconds at 60 FPS
self.smithHealTimer = 0;
var healed = false;
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
if (t !== self && self.isInRange(t)) {
if (t.health < t.maxHealth) {
t.health = t.maxHealth;
if (t.towerHealthBar) t.towerHealthBar.width = 70;
healed = true;
}
}
}
if (healed) {
var note = game.addChild(new Notification("Smith: Towers in range fully healed!"));
note.x = 2048 / 2;
note.y = grid.height - 60;
}
}
}
}
// Research functionality - unlocks futuristic towers when built
if (self.towerSet === 'builds' && self.id === 'splash') {
// This is Research - it unlocks futuristic towers immediately when built
if (!futuristicUnlocked) {
futuristicUnlocked = true;
var notification = game.addChild(new Notification("Futuristic towers unlocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Update all existing towers to benefit from futuristic unlock
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Apply 3x fire rate boost to all existing towers
tower.fireRate = tower.fireRate / 3;
// Refresh range for slow and poison towers (now cover entire map)
if (tower.id === 'slow' || tower.id === 'poison') {
tower.refreshCellsInRange();
}
}
}
}
// House functionality for builds in ultra mode
if (self.towerSet === 'builds' && self.id === 'default' && gameSettings.difficulty === 'ultra') {
// This is a house - it provides 3 workers when built
// Workers are added when house is placed, not over time
}
// Trapper tower enemy detection and damage
if (self.isTrapper) {
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 <= CELL_SIZE && !enemy.trappedBy) {
enemy.trappedBy = self;
// Damage all enemies in range when triggered
for (var j = 0; j < enemies.length; j++) {
var rangeEnemy = enemies[j];
var rangeDx = rangeEnemy.x - self.x;
var rangeDy = rangeEnemy.y - self.y;
var rangeDistance = Math.sqrt(rangeDx * rangeDx + rangeDy * rangeDy);
if (rangeDistance <= self.getRange()) {
var damage = 0;
if (rangeEnemy.isImmune) {
// Immune enemies take full health damage
damage = rangeEnemy.health;
} else {
// Regular enemies take 25% damage
damage = Math.ceil(rangeEnemy.health * 0.25);
}
rangeEnemy.health -= damage;
if (rangeEnemy.health <= 0) {
rangeEnemy.health = 0;
} else {
rangeEnemy.healthBar.width = rangeEnemy.health / rangeEnemy.maxHealth * 70;
}
}
}
// Destroy trapper after use
self.health = 0;
break;
}
}
}
// Update tower health bar
if (self.health <= 0) {
// Healer towers are immune to destruction
if (self.id === 'poison') {
self.health = self.maxHealth; // Reset to full health
self.towerHealthBar.width = 70; // Full health bar
} else {
self.health = 0;
self.towerHealthBar.width = 0;
// Destroy tower when health reaches zero
// Remove from cells in range
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
// Reset grid cells to floor (if not castle walls)
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (cell && cell.type !== 4) {
cell.type = 0;
}
}
}
// Remove Upgrader/Smith effects from towers in range if this is Upgrader or Smith
if (self.towerSet === 'builds' && (self.id === 'slow' || self.id === 'poison')) {
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
if (t !== self) {
// If Upgrader: revert level if it was increased by this Upgrader
if (self.id === 'slow' && t._upgradedByUpgrader && t._upgradedByUpgrader.indexOf(self) !== -1) {
if (t.level > 1) {
t.level -= 1;
t.updateLevelIndicators && t.updateLevelIndicators();
}
// Remove this Upgrader from the list
var idx = t._upgradedByUpgrader.indexOf(self);
if (idx !== -1) t._upgradedByUpgrader.splice(idx, 1);
}
// If Smith: revert maxHealth and health if it was buffed by this Smith
if (self.id === 'poison' && t._smithBuffedBy && t._smithBuffedBy.indexOf(self) !== -1) {
t.maxHealth = t.maxHealth / 2;
if (t.health > t.maxHealth) t.health = t.maxHealth;
if (t.towerHealthBar) t.towerHealthBar.width = t.health / t.maxHealth * 70;
// Remove this Smith from the buffedBy list
var sidx = t._smithBuffedBy.indexOf(self);
if (sidx !== -1) t._smithBuffedBy.splice(sidx, 1);
}
}
}
// Clean up Upgrader/Smith's buffed towers list
if (self._upgraderBuffedTowers) self._upgraderBuffedTowers = [];
if (self._smithBuffedTowers) self._smithBuffedTowers = [];
}
// Check if this is Research being destroyed - lock futuristic towers and remove existing ones
if (self.towerSet === 'builds' && self.id === 'splash') {
// This is Research being destroyed - lock futuristic towers
futuristicUnlocked = false;
// Remove all existing futuristic towers and refund their cost
for (var i = towers.length - 1; i >= 0; i--) {
var tower = towers[i];
if (tower.towerSet === 'futuristic') {
// Calculate refund value
var totalInvestment = tower.getTotalValue ? tower.getTotalValue() : 0;
var refundValue = getTowerSellValue(totalInvestment);
setGold(gold + refundValue);
// Remove from cells in range
for (var j = 0; j < tower.cellsInRange.length; j++) {
var cell = tower.cellsInRange[j];
var towerIndex = cell.towersInRange.indexOf(tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
// Reset grid cells to floor (if not castle walls)
for (var gx = 0; gx < 2; gx++) {
for (var gy = 0; gy < 2; gy++) {
var cell = grid.getCell(tower.gridX + gx, tower.gridY + gy);
if (cell && cell.type !== 4) {
cell.type = 0;
}
}
}
// Clear selected tower if this was selected
if (selectedTower === tower) {
selectedTower = null;
}
// Remove from towers array
var towerArrayIndex = towers.indexOf(tower);
if (towerArrayIndex !== -1) {
towers.splice(towerArrayIndex, 1);
}
// Remove from game
tower.destroy();
}
}
// Switch back to medieval towers if currently on futuristic
if (towerSet === 'futuristic') {
towerSet = 'medieval';
updateSourceTowers();
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.5;
futuristicBG.tint = 0x222222;
futuristicBG.alpha = 0.5;
}
var notification = game.addChild(new Notification("Research destroyed! Futuristic towers locked and removed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
// Remove tower from towers array
var towerIndex = towers.indexOf(self);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
// Clear selected tower if this was selected
if (selectedTower === self) {
selectedTower = null;
}
// Remove tower from game
self.destroy();
// Update pathfinding
grid.pathFind();
grid.renderDebug();
return;
}
} else {
self.towerHealthBar.width = self.health / self.maxHealth * 70;
}
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
// Only rotate gun container if this is not a Trecher tower or Healer tower
if (self.id !== 'slow' && self.id !== 'poison') {
gunContainer.rotation = angle;
}
// Check if this tower is affected by any Trecher towers
var effectiveFireRate = self.fireRate;
if (self.id !== 'slow') {
// Trechers don't boost themselves
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.id === 'slow' && tower.isInRange(self)) {
effectiveFireRate = self.fireRate * 0.5; // 50% faster firing (half the fire rate delay)
break;
}
}
}
// Apply Austrian empire bonus: 20% faster fire rate (20% less delay)
if (gameSettings.selectedEmpire === 'Austria') {
effectiveFireRate = effectiveFireRate * 0.8; // 20% faster firing
}
if (LK.ticks - self.lastFired >= effectiveFireRate / gameSpeed) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
// Trecher towers don't fire bullets - they only provide support effects
if (self.id === 'slow') {
return;
}
// Healer towers don't fire bullets - they only provide healing/protection effects
if (self.id === 'poison') {
return;
}
// Trench towers don't fire bullets - they only provide support effects
if (self.towerSet === 'trench') {
return;
}
// Builds towers (house, Farm, build 3,4,5,6) don't fire bullets
if (self.towerSet === 'builds') {
return;
}
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bulletDamage = self.damage;
// Apply Ottoman empire bonus: 20% more damage
if (gameSettings.selectedEmpire === 'Ottoman') {
bulletDamage = Math.ceil(bulletDamage * 1.2); // 20% more damage
}
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, bulletDamage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// Customize bullet appearance based on tower type
switch (self.id) {
case 'rapid':
bullet.children[0].tint = 0x00AAFF;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
break;
case 'sniper':
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
break;
case 'splash':
bullet.children[0].tint = 0x33CC00;
bullet.children[0].width = 40;
bullet.children[0].height = 40;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// --- Fire recoil effect for gunContainer ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
// Always use the original resting position for recoil, never accumulate offset
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset to resting position before animating (in case of interrupted tweens)
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Calculate recoil offset (recoil back along the gun's rotation)
var recoilDistance = 8;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
// Animate recoil back from the resting position
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original position/scale
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// All towers block enemy movement except Trapper
if (self.isTrapper) {
// Trapper allows enemies to pass through
cell.type = 0;
} else {
// All other structures block enemy movement
cell.type = 1;
}
}
}
}
self.refreshCellsInRange();
// Completely destroy and recreate tower preview when tower is successfully placed
if (towerPreview && towerPreview.parent) {
game.removeChild(towerPreview);
towerPreview.destroy();
towerPreview = new TowerPreview();
towerPreview.visible = false;
}
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'default';
self.hasEnoughGold = true;
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3;
var previewGraphics = self.attachAsset(towerSet === 'futuristic' ? 'futurep' : towerSet === 'trench' ? 'builds' : towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'Farmland' : 'builds' : 'rare', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
var towerOreCost = getTowerOreCost(self.towerType);
self.hasEnoughGold = gold >= getTowerCost(self.towerType) && ore >= towerOreCost;
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
break;
case 'sniper':
previewGraphics.tint = 0xFF5500;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
break;
case 'slow':
previewGraphics.tint = 0x9900FF;
break;
case 'poison':
previewGraphics.tint = 0x00FFAA;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
// Allow placement only on regular floor (type 0)
if (!cell || cell.type !== 0) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
self.canPlace = validGridPlacement && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var Tutorial = Container.expand(function (difficulty) {
var self = Container.call(this);
self.difficulty = difficulty;
self.currentStep = 0;
self.tutorialSteps = [];
self.isActive = false;
// Tutorial overlay background
var tutorialOverlay = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
tutorialOverlay.width = 2048;
tutorialOverlay.height = 2732;
tutorialOverlay.tint = 0x000000;
tutorialOverlay.alpha = 0.7;
// Tutorial text box
var textBox = new Container();
var textBG = textBox.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
textBG.width = 1600;
textBG.height = 400;
textBG.alpha = 0.95;
var tutorialText = new Text2("", {
size: 60,
fill: 0xFFFFFF,
weight: 600
});
tutorialText.anchor.set(0.5, 0.5);
textBox.addChild(tutorialText);
textBox.x = 0;
textBox.y = -200;
self.addChild(textBox);
// Next button
var nextButton = new Container();
var nextBG = nextButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
nextBG.width = 300;
nextBG.height = 100;
nextBG.tint = 0x00AA00;
var nextText = new Text2("Next", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
nextText.anchor.set(0.5, 0.5);
nextButton.addChild(nextText);
nextButton.x = 0;
nextButton.y = 150;
nextButton.down = function () {
self.nextStep();
};
self.addChild(nextButton);
// Define tutorial steps based on difficulty
if (difficulty === 'easy') {
self.tutorialSteps = [{
text: "Welcome to HONORBOUND!\nThis is Easy Mode - perfect for learning.\nTowers cost 50% less and enemies are easier.",
indicator: {
x: 0,
y: 0,
visible: false
}
}, {
text: "This is your gold (top right).\nYou spend gold to build towers.\nYou earn gold by defeating enemies.",
indicator: {
x: 1500,
y: -1200,
visible: true
}
}, {
text: "These are your lives (top left).\nYou lose lives when enemies reach the bottom.\nDon't let them reach zero!",
indicator: {
x: -1500,
y: -1200,
visible: true
}
}, {
text: "These are tower types you can build.\nDrag them onto the battlefield to place towers.\nEach has different abilities and costs.",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Advanced Technology: Futuristic towers unlock\nat wave 4. They have\n3x faster fire rate and\nspecial abilities. Robot, Lazerer,\nEaglebot, Fwacha, f-Bullet, Hospital.",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Click 'Start Game' to begin the first wave.\nEnemies will spawn from the top and try to reach the bottom.",
indicator: {
x: 0,
y: 800,
visible: true
}
}, {
text: "Good luck, Commander!\nBuild towers to stop the enemies.\nUpgrade towers by clicking on them.",
indicator: {
x: 0,
y: 0,
visible: false
}
}];
} else {
self.tutorialSteps = [{
text: "Welcome to HONORBOUND!\nYou are the last defender of the empire.\nStop the advancing enemies at all costs!",
indicator: {
x: 0,
y: 0,
visible: false
}
}, {
text: "Gold (top right) is your main resource.\nSpend it wisely on towers and upgrades.\nEarn more by defeating enemies.",
indicator: {
x: 1500,
y: -1200,
visible: true
}
}, {
text: "Lives (top left) represent your defenses.\nEach enemy that escapes costs 1 life.\nBoss enemies cost more lives!",
indicator: {
x: -1500,
y: -1200,
visible: true
}
}, {
text: "Tower Selection: Drag towers to build them.\nInfantry: Balanced β’ Catapult: Fast\nArcher: Long range β’ Hwacha: Area damage",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Advanced towers: Bullet (slows enemies)\nHealer (protects nearby towers)\nClick built towers to upgrade them.",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Speed Control (top center): Adjust game speed.\nMusic Control: Change background music.\nUse these to manage your strategy.",
indicator: {
x: 0,
y: -1200,
visible: true
}
}, {
text: "Futuristic Technology: At wave 4,\nunlock advanced towers! Robot, Lazerer,\nEaglebot, Fwacha, f-Bullet, Hospital.\nThey fire 3x faster\nwith special abilities.",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Wave System: Enemies come in waves.\nEach wave gets stronger.\nPrepare defenses between waves!",
indicator: {
x: 0,
y: 800,
visible: true
}
}, {
text: "Strategic Tips:\nβ’ Block enemy paths with towers\nβ’ Upgrade key towers\nβ’ Use terrain to your advantage",
indicator: {
x: 0,
y: 0,
visible: false
}
}, {
text: "The fate of the empire rests with you!\nClick 'Start Game' when ready.\nMay victory be yours, Commander!",
indicator: {
x: 0,
y: 800,
visible: true
}
}];
}
self.startTutorial = function () {
self.isActive = true;
self.currentStep = 0;
self.showCurrentStep();
};
self.showCurrentStep = function () {
if (self.currentStep >= self.tutorialSteps.length) {
self.endTutorial();
return;
}
var step = self.tutorialSteps[self.currentStep];
tutorialText.setText(step.text);
// Remove existing indicators
for (var i = self.children.length - 1; i >= 0; i--) {
if (self.children[i].isTutorialIndicator) {
self.removeChild(self.children[i]);
}
}
// Add indicators based on tutorial step
if (self.difficulty === 'easy') {
if (self.currentStep === 1) {
// Gold instruction
var goldIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
goldIndicator.x = 500;
goldIndicator.y = -1150;
goldIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 2) {
// Lives instruction
var livesIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
livesIndicator.x = -500;
livesIndicator.y = -1150;
livesIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 4) {
// Futuristic towers instruction
var futuristicIndicator = self.attachAsset('isaret2', {
anchorX: 0.5,
anchorY: 0.5
});
futuristicIndicator.x = 0;
futuristicIndicator.y = 1050;
futuristicIndicator.isTutorialIndicator = true;
}
} else {
// Normal difficulty
if (self.currentStep === 1) {
// Gold instruction
var goldIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
goldIndicator.x = 500;
goldIndicator.y = -1150;
goldIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 2) {
// Lives instruction
var livesIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
livesIndicator.x = -500;
livesIndicator.y = -1150;
livesIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 5) {
// Speed Control and Music instruction
var speedIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
speedIndicator.x = -100;
speedIndicator.y = -1150;
speedIndicator.isTutorialIndicator = true;
var musicIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
musicIndicator.x = 100;
musicIndicator.y = -1150;
musicIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 6) {
// Futuristic towers instruction
var futuristicIndicator = self.attachAsset('isaret2', {
anchorX: 0.5,
anchorY: 0.5
});
futuristicIndicator.x = 0;
futuristicIndicator.y = 1050;
futuristicIndicator.isTutorialIndicator = true;
}
}
// Update next button text
if (self.currentStep === self.tutorialSteps.length - 1) {
nextText.setText("Start!");
} else {
nextText.setText("Next");
}
};
self.nextStep = function () {
self.currentStep++;
if (self.currentStep >= self.tutorialSteps.length) {
self.endTutorial();
} else {
self.showCurrentStep();
}
};
self.endTutorial = function () {
self.isActive = false;
// Mark tutorial as completed and save to storage
storage.tutorialCompleted = true;
// Remove any tutorial indicators
for (var i = self.children.length - 1; i >= 0; i--) {
if (self.children[i].isTutorialIndicator) {
self.removeChild(self.children[i]);
}
}
self.destroy();
// Return to main menu after tutorial completion
var mainMenu = new MainMenu();
game.addChild(mainMenu);
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.alpha = 0.9;
var towerDisplayName = self.tower.id === 'default' ? self.tower.towerSet === 'futuristic' ? 'Robot' : self.tower.towerSet === 'trench' ? 'Blocker' : self.tower.towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'House' : 'Build1' : 'Infantry' : self.tower.id === 'rapid' ? self.tower.towerSet === 'futuristic' ? 'Lazerer' : self.tower.towerSet === 'trench' ? 'Trapper' : self.tower.towerSet === 'builds' ? 'Farm' : 'Catapult' : self.tower.id === 'splash' ? self.tower.towerSet === 'futuristic' ? 'Fwacha' : self.tower.towerSet === 'builds' ? 'Research' : 'Hwacha' : self.tower.id === 'sniper' ? self.tower.towerSet === 'futuristic' ? 'Eaglebot' : self.tower.towerSet === 'trench' ? 'Money' : self.tower.towerSet === 'builds' ? 'Miner' : 'Archer' : self.tower.id === 'slow' ? self.tower.towerSet === 'futuristic' ? 'f-Bullet' : self.tower.towerSet === 'builds' ? 'Upgrader' : 'Bullet' : self.tower.id === 'poison' ? self.tower.towerSet === 'futuristic' ? 'Hospital' : self.tower.towerSet === 'builds' ? 'Smith' : 'Healer' : self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1);
var towerTypeText = new Text2(towerDisplayName + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Handle population logic for houses in ultra mode
if (gameSettings.difficulty === 'ultra' && self.tower.towerSet === 'builds' && self.tower.id === 'default') {
// This was a house - reduce population by 1000
population = Math.max(0, population - 1000);
var popNotification = game.addChild(new Notification("-1000 population"));
popNotification.x = 2048 / 2;
popNotification.y = grid.height - 100;
}
updateUI();
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// Only reset to floor if it's not a castle wall
if (cell.type !== 4) {
cell.type = 0;
}
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('hi', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
size: 50,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Game", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
// Block game start if difficulty not selected
if (!gameStartAllowed) {
return;
}
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
// Reset to medieval towers at game start
if (towerSet === 'futuristic') {
towerSet = 'medieval';
updateSourceTowers();
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.5; // Keep transparency
futuristicBG.tint = 0x222222;
futuristicBG.alpha = 0.5; // Keep transparency
}
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Start music when game begins
LK.playMusic('warmusic');
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('hi', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// All waves are normal enemy waves
var waveType = "Dawn";
var enemyType = "normal";
var enemyCount = 10;
block.tint = 0xAAAAAA;
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 4;
waveTypeShadow.y = 4;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "normal";
}
// If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
// then we should return a different boss type
var waveType = self.waveTypes[waveNumber - 1];
return waveType;
};
// Get enemy count for a specific wave number
self.getEnemyCount = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return 10;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Boss spawning disabled - no boss prefix for waves 10, 20, 30
// if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
// typeName = "BOSS";
// }
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
// Only apply red tint after game has started
if (self.gameStarted) {
if (i - 1 < currentWave) {
// Completed waves: red tint with reduced alpha
block.alpha = .5;
tween(block, {
tint: 0xFF0000
}, {
duration: 300,
easing: tween.easeOut
});
}
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
// Skip wave progression when game is paused (gameSpeed = 0)
if (gameSpeed === 0) {
return;
}
if (currentWave < totalWaves) {
waveTimer += gameSpeed;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var notification = game.addChild(new Notification("Wave " + currentWave + " incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
// Initialize gameSettings early to prevent undefined access
var gameSettings = {
difficulty: 'normal',
selectedEmpire: 'Normal'
};
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
// --- Population-based gold income for ultra mode ---
if (gameSettings.difficulty === 'ultra') {
var populationGoldTimer = 0;
game.update = function (origUpdate) {
return function () {
// Call original update
if (typeof origUpdate === "function") origUpdate.apply(this, arguments);
// Only run if game is started and not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
populationGoldTimer += gameSpeed;
if (populationGoldTimer >= 1800) {
// 30 seconds at 60 FPS
populationGoldTimer = 0;
var popK = Math.floor(population / 1000);
if (popK > 0) {
var goldToAdd = popK * 3;
setGold(gold + goldToAdd);
var note = game.addChild(new Notification("+" + goldToAdd + " gold from population!"));
note.x = 2048 / 2;
note.y = grid.height - 180;
}
}
}
};
}(game.update);
}
// Update gameSettings with stored values
gameSettings.difficulty = 'normal';
gameSettings.selectedEmpire = 'Normal';
var goldMines = [];
var gold = gameSettings.selectedEmpire === 'Ottoman' ? 100 : 80;
// Leaderboard system for ultra mode
var playerScore = 0;
var playerName = "";
// Leaderboard functionality removed
var leaderboardData = [];
// Population system for ultra mode
if (gameSettings.difficulty === 'ultra') {
var population = 0; // Start with 0 population
var ultraEnemyKills = 0;
var ore = 0; // Ore resource for ultra mode only - starts at 0
} else {
var population = 0;
var ore = 0; // Initialize ore for all modes to prevent undefined
}
// Generate a new session seed on each reload/restart
var reloadCounter = 0;
reloadCounter++;
var gameSessionSeed = reloadCounter * 12345 + Math.floor(Date.now() / 1000); // Unique seed per reload
// Track if difficulty has been selected and game can start
var gameStartAllowed = false;
var lives = 10;
var currentWave = 0;
var totalWaves = gameSettings.difficulty === 'ultra' ? 30 : gameSettings.difficulty === 'hardcore' ? 10 : 5;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var gameSpeed = 1.0; // 1.0 = normal, 0.5 = slow, 2.0 = fast
var gameSpeedOptions = ['slow', 'normal', 'fast', 'ultra', 'paused'];
var currentSpeedIndex = 1; // Start with normal (index 1)
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var populationText = new Text2('Population: ' + population, {
size: 60,
fill: 0x00FFFF,
weight: 800
});
populationText.anchor.set(0.5, 0.5);
var oreText = new Text2('Ore: ' + ore, {
size: 60,
fill: 0x888888,
weight: 800
});
oreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
// Hide these elements initially in menu
goldText.visible = false;
livesText.visible = false;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(livesText);
// Only show populationText in ultra mode
if (gameSettings.difficulty === 'ultra') {
LK.gui.top.addChild(populationText);
LK.gui.top.addChild(oreText);
populationText.x = spacing;
populationText.y = topMargin + 80; // Position below gold
populationText.visible = true;
oreText.x = spacing;
oreText.y = topMargin + 160; // Position below population text
oreText.visible = true;
} else {
populationText.visible = false;
oreText.visible = false;
}
livesText.x = -spacing;
livesText.y = topMargin;
goldText.x = spacing;
goldText.y = topMargin;
function updateTotalWaves() {
totalWaves = gameSettings.difficulty === 'ultra' ? 30 : gameSettings.difficulty === 'hardcore' ? 10 : 5;
// Recreate wave indicator with correct number of waves
if (waveIndicator && waveIndicator.parent) {
waveIndicator.parent.removeChild(waveIndicator);
waveIndicator.destroy();
}
waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
}
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
if (gameSettings.difficulty === 'ultra') {
// Ensure population and ore UI elements are added to GUI if not already present
if (!populationText.parent) {
LK.gui.top.addChild(populationText);
populationText.x = spacing;
populationText.y = topMargin + 80;
}
if (!oreText.parent) {
LK.gui.top.addChild(oreText);
oreText.x = spacing;
oreText.y = topMargin + 160;
}
populationText.setText('Population: ' + population);
oreText.setText('Ore: ' + ore);
populationText.visible = true;
oreText.visible = true;
} else {
populationText.visible = false;
oreText.visible = false;
}
}
function setGold(value) {
gold = value;
updateUI();
}
function addScore(points) {
if (gameSettings.difficulty === 'ultra') {
playerScore += points;
}
}
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
// Create background tile grid (12x15)
var backgroundContainer = new Container();
var bgTileWidth = 2048 / 12; // Divide screen width by 12
var bgTileHeight = 2732 / 15; // Divide screen height by 15
for (var row = 0; row < 15; row++) {
for (var col = 0; col < 12; col++) {
var bgTile = backgroundContainer.attachAsset('background', {
anchorX: 0,
anchorY: 0,
width: bgTileWidth,
height: bgTileHeight
});
bgTile.x = col * bgTileWidth;
bgTile.y = row * bgTileHeight;
}
}
// Add background container first (behind everything else)
game.addChild(backgroundContainer);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
// Trapper towers don't block paths
if (towerPreview && towerPreview.towerType === 'rapid' && towerSet === 'trench') {
return false;
}
var cells = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cells.push({
cell: cell,
originalType: cell.type
});
cell.type = 1;
}
}
}
var blocked = grid.pathFind();
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
grid.pathFind();
grid.renderDebug();
return blocked;
}
function getTowerCost(towerType) {
var cost = 10;
switch (towerType) {
case 'rapid':
// Different costs for trench vs medieval/futuristic
if (towerSet === 'trench') {
cost = 50; // Trapper cost
} else {
cost = 30; // Original catapult cost
}
break;
case 'sniper':
// Different costs for trench vs medieval/futuristic
if (towerSet === 'trench') {
cost = 120; // Money tower cost
} else {
cost = 50; // Original archer cost
}
break;
case 'splash':
cost = 50;
break;
case 'slow':
cost = gameSettings.difficulty === 'hardcore' ? 100 : 50;
break;
case 'poison':
cost = gameSettings.difficulty === 'hardcore' ? 100 : 50;
break;
}
// Apply Easy mode discount
if (gameSettings.difficulty === 'easy') {
cost = Math.floor(cost / 2);
}
// Apply Hungary empire bonus: 5 gold less on normal and higher difficulties
if (gameSettings.selectedEmpire === 'Hungary' && gameSettings.difficulty !== 'easy') {
cost = Math.max(5, cost - 5); // Minimum cost of 5 gold
}
// Apply Scotch empire bonus: 3 gold less for all towers
if (gameSettings.selectedEmpire === 'Scotch') {
cost = Math.max(5, cost - 3); // Minimum cost of 5 gold
}
// Remove price doubling - research no longer increases costs
return cost;
}
function getTowerOreCost(towerType) {
// Upgrader (Build5) in builds set always costs 10 ore in ultra mode
if (towerSet === 'builds' && gameSettings.difficulty === 'ultra' && towerType === 'slow') {
return 10;
}
// Smith (Build6) in builds set always costs 15 ore in ultra mode
if (towerSet === 'builds' && gameSettings.difficulty === 'ultra' && towerType === 'poison') {
return 15;
}
// Only futuristic towers in ultra mode require ore
if (towerSet === 'futuristic' && gameSettings.difficulty === 'ultra') {
switch (towerType) {
case 'default':
return 5;
case 'rapid':
return 3;
case 'sniper':
return 8;
case 'splash':
return 7;
case 'slow':
return 6;
case 'poison':
return 9;
default:
return 5;
}
}
return 0;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
var towerOreCost = getTowerOreCost(towerType);
if (gold >= towerCost && ore >= towerOreCost) {
// Check if this is a house (Build1 in Ultra mode)
var isHouse = towerSet === 'builds' && towerType === 'default' && gameSettings.difficulty === 'ultra';
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
ore -= towerOreCost;
// Handle population logic only in ultra mode
if (gameSettings.difficulty === 'ultra') {
if (isHouse) {
// Each house gives 1000 population
population += 1000;
var note = game.addChild(new Notification("+1000 population!"));
note.x = 2048 / 2;
note.y = grid.height - 100;
}
}
updateUI();
grid.pathFind();
grid.renderDebug();
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// Block all interactions if difficulty not selected
if (!gameStartAllowed) {
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
// Completely recreate tower preview to ensure clean state
if (towerPreview && towerPreview.parent) {
game.removeChild(towerPreview);
towerPreview.destroy();
}
towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
// Apply the same offset as in move handler to ensure consistency when starting drag
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
break;
}
}
};
game.move = function (x, y, obj) {
// Block all interactions if difficulty not selected
if (!gameStartAllowed) {
return;
}
if (isDragging) {
// Shift the y position upward by 1.5 tiles to show preview above finger
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
}
};
game.up = function (x, y, obj) {
// Block all interactions if difficulty not selected
if (!gameStartAllowed) {
return;
}
var clickedOnTower = false;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var towerLeft = tower.x - tower.width / 2;
var towerRight = tower.x + tower.width / 2;
var towerTop = tower.y - tower.height / 2;
var towerBottom = tower.y + tower.height / 2;
if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
clickedOnTower = true;
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
if (towerPreview.canPlace) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
// Completely destroy and recreate tower preview to ensure it's completely clean
if (towerPreview.parent) {
game.removeChild(towerPreview);
towerPreview.destroy();
towerPreview = new TowerPreview();
towerPreview.visible = false;
}
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
// Create tower set buttons first (they will be behind the menu)
// Tower set buttons are already created above
// Always show start menu at game start with PLAY button
// --- Layered, non-interactive, behind-menu text labels ---
// Create a container for background labels
var menuBackgroundLabels = new Container();
menuBackgroundLabels.x = 2048 / 2;
menuBackgroundLabels.y = 2732 / 2;
menuBackgroundLabels.alpha = 0.13; // Faint, but visible behind menu
// Helper to add a label
function addMenuBGLabel(text, x, y, size, color, rotation) {
var label = new Text2(text, {
size: size,
fill: color,
weight: 800
});
label.anchor.set(0.5, 0.5);
label.x = x;
label.y = y;
if (rotation) label.rotation = rotation;
label.interactive = false;
label.buttonMode = false;
menuBackgroundLabels.addChild(label);
}
// Add the requested labels, spaced and sized for background effect
addMenuBGLabel("Medieval", -600, -700, 220, 0x8B4513, -0.08);
addMenuBGLabel("locked", 600, -700, 180, 0x888888, 0.07);
addMenuBGLabel("Trench", -600, 0, 200, 0x8B4513, 0.05);
addMenuBGLabel("lives", 600, 0, 180, 0x00FF00, -0.06);
addMenuBGLabel("normal", -600, 700, 200, 0x0088FF, 0.08);
addMenuBGLabel("music1", 600, 700, 180, 0xFF8800, -0.07);
addMenuBGLabel("Gold", 0, 0, 300, 0xFFD700, 0);
// Place the label container behind the menu
game.addChildAt(menuBackgroundLabels, 0);
var startMenu = new StartMenu();
game.addChild(startMenu);
var speedControl = new SpeedControl();
speedControl.x = -100; // Position closer to center to be adjacent to music control
speedControl.y = topMargin;
// Hide speed control in menu initially
speedControl.visible = false;
LK.gui.top.addChild(speedControl);
var musicControl = new MusicControl();
musicControl.x = 100; // Position closer to center to be adjacent to speed control
musicControl.y = topMargin;
// Hide music control in menu initially
musicControl.visible = false;
LK.gui.top.addChild(musicControl);
// Leaderboard functionality removed
// --- Tower Set Toggle Buttons ---
var towerSet = 'medieval'; // 'medieval', 'futuristic', or 'trench'
var futuristicUnlocked = false; // Track if futuristic towers have been unlocked
// Define tower types for each set
var medievalTowerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
var futuristicTowerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; // Placeholder, can be changed later
var trenchTowerTypes = ['default', 'rapid', 'sniper']; // Blocker, Trapper, Money
var buildsTowerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; // 6 build options for Ultra mode
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var towerY = 2732 - CELL_SIZE * 3 - 90;
// Button container - add to game so it's in front of towers
var towerSetButtonContainer = new Container();
towerSetButtonContainer.x = 2048 / 2;
// Move the button container up by the button height (100px) so the buttons are higher by their own height
towerSetButtonContainer.y = towerY - 60 - 100; // Move up by button height
game.addChild(towerSetButtonContainer);
// Move to top of display list so it's in front of towers
if (towerSetButtonContainer.parent) {
towerSetButtonContainer.parent.removeChild(towerSetButtonContainer);
}
game.addChild(towerSetButtonContainer);
// Medieval Button
var medievalButton = new Container();
var medievalBG = medievalButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
medievalBG.width = 350;
medievalBG.height = 100;
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.8; // Reduce transparency
var medievalText = new Text2("Medieval", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
medievalText.anchor.set(0.5, 0.5);
medievalText.alpha = 0.9; // Reduce transparency
medievalButton.addChild(medievalText);
// Arrange tower set buttons side by side, not overlapping
var buttonSpacing = 400; // Enough to avoid overlap, adjust as needed for visuals
medievalButton.x = -buttonSpacing;
medievalButton.y = 0;
medievalButton.down = function () {
LK.getSound('Click').play();
if (towerSet !== 'medieval') {
towerSet = 'medieval';
updateSourceTowers();
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.8; // Reduce transparency
futuristicBG.tint = 0x000080; // Dark blue color
futuristicBG.alpha = 0.8; // Reduce transparency
trenchBG.tint = 0x8B4513;
trenchBG.alpha = 0.8; // Reduce transparency
}
};
// Hide medieval button in menu initially
medievalButton.visible = false;
towerSetButtonContainer.addChild(medievalButton);
// Futuristic Button
var futuristicButton = new Container();
var futuristicBG = futuristicButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
futuristicBG.width = 350;
futuristicBG.height = 100;
futuristicBG.tint = 0x000080; // Dark blue color
futuristicBG.alpha = 0.8; // Reduce transparency
var futuristicText = new Text2("Futuristic", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
futuristicText.anchor.set(0.5, 0.5);
futuristicText.alpha = 0.9; // Reduce transparency
futuristicButton.addChild(futuristicText);
futuristicButton.x = 0;
futuristicButton.y = 0;
// Hide futuristic button in menu initially
futuristicButton.visible = false;
// Trench Button
var trenchButton = new Container();
var trenchBG = trenchButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
trenchButton.x = buttonSpacing;
trenchBG.width = 350;
trenchBG.height = 100;
trenchBG.tint = 0x8B4513; // Same as medieval
trenchBG.alpha = 0.8; // Reduce transparency
var trenchText = new Text2("Builds", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
trenchText.anchor.set(0.5, 0.5);
trenchText.alpha = 0.9; // Reduce transparency
trenchButton.addChild(trenchText);
trenchButton.y = 0;
futuristicButton.down = function () {
LK.getSound('Click').play();
// Always block futuristic towers in story mode
if (gameSettings.storyMode) {
var notification = game.addChild(new Notification("Futuristic towers are permanently locked in Story Mode!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
// Block futuristic towers until wave 4 OR Research (Build3) is completed (except in Ultra mode)
if (gameSettings.difficulty === 'ultra' && !futuristicUnlocked) {
// In ultra mode, only allow if Research (Build3) is built and not destroyed
var researchBuilt = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchBuilt = true;
break;
}
}
if (!researchBuilt) {
var notification = game.addChild(new Notification("Futuristic towers are locked until you build Research!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
} else if (gameSettings.difficulty !== 'ultra' && currentWave < 4 && !futuristicUnlocked) {
var notification = game.addChild(new Notification("Futuristic towers unlock at wave 4 or by building Build3!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
// --- NEW: Prevent Futuristic if Research was destroyed in Ultra mode ---
if (gameSettings.difficulty === 'ultra' && !futuristicUnlocked) {
var researchExists = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchExists = true;
break;
}
}
if (!researchExists) {
var notification = game.addChild(new Notification("Futuristic towers are locked until you build Research!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
}
if (towerSet !== 'futuristic') {
towerSet = 'futuristic';
futuristicUnlocked = true; // Mark futuristic as unlocked
updateSourceTowers();
medievalBG.tint = 0x000080; // Dark blue color
medievalBG.alpha = 0.8; // Reduce transparency
futuristicBG.tint = 0x1976D2;
futuristicBG.alpha = 0.8; // Reduce transparency
trenchBG.tint = 0x8B4513;
trenchBG.alpha = 0.8; // Reduce transparency
}
};
futuristicButton.update = function () {
// Always show locked state in story mode
if (gameSettings.storyMode) {
futuristicBG.tint = 0x000080; // Dark blue when locked
futuristicBG.alpha = 0.3; // More transparent when locked
futuristicText.setText("Locked");
return;
}
// Update button appearance based on wave progress OR Research (Build3) in Ultra mode
if (gameSettings.difficulty === 'ultra' && !futuristicUnlocked) {
// In ultra mode, only allow if Research (Build3) is built and not destroyed
var researchBuilt = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchBuilt = true;
break;
}
}
if (!researchBuilt) {
futuristicBG.tint = 0x000080; // Dark blue when locked
futuristicBG.alpha = 0.3; // More transparent when locked
futuristicText.setText("Locked");
return;
}
}
if (gameSettings.difficulty !== 'ultra' && currentWave < 4 && !futuristicUnlocked) {
futuristicBG.tint = 0x000080; // Dark blue when locked
futuristicBG.alpha = 0.3; // More transparent when locked
futuristicText.setText("Locked");
} else {
if (towerSet === 'futuristic') {
futuristicBG.tint = 0x1976D2;
} else {
futuristicBG.tint = 0x000080; // Dark blue when not selected
}
futuristicBG.alpha = 0.8; // Reduce transparency
futuristicText.setText("Futuristic");
}
// --- NEW: Lock Futuristic if Research was destroyed in Ultra mode ---
if (gameSettings.difficulty === 'ultra' && !futuristicUnlocked) {
// If Research was destroyed, lock the button
var researchExists = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchExists = true;
break;
}
}
if (!researchExists) {
futuristicBG.tint = 0x000080;
futuristicBG.alpha = 0.3;
futuristicText.setText("Locked");
}
}
};
towerSetButtonContainer.addChild(futuristicButton);
trenchButton.down = function () {
LK.getSound('Click').play();
var targetSet = gameSettings.difficulty === 'ultra' ? 'builds' : 'trench';
if (towerSet !== targetSet) {
towerSet = targetSet;
updateSourceTowers();
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.8; // Reduce transparency
futuristicBG.tint = 0x000080; // Dark blue color
futuristicBG.alpha = 0.8; // Reduce transparency
trenchBG.tint = 0x8B4513;
trenchBG.alpha = 0.8; // Reduce transparency
}
};
// Hide trench button in menu initially
trenchButton.visible = false;
towerSetButtonContainer.addChild(trenchButton);
// Helper to clear and re-create source towers
// Leaderboard input functionality removed
// Save to leaderboard functionality removed
// Show leaderboard functionality removed
function updateSourceTowers() {
// Remove old source towers
for (var i = 0; i < sourceTowers.length; i++) {
if (sourceTowers[i].parent) {
sourceTowers[i].parent.removeChild(sourceTowers[i]);
}
sourceTowers[i].destroy && sourceTowers[i].destroy();
}
sourceTowers = [];
// Pick correct tower types
var types = towerSet === 'medieval' ? medievalTowerTypes : towerSet === 'futuristic' ? futuristicTowerTypes : towerSet === 'trench' ? trenchTowerTypes : towerSet === 'builds' ? buildsTowerTypes : medievalTowerTypes;
var startX = 2048 / 2 - types.length * towerSpacing / 2 + towerSpacing / 2;
for (var i = 0; i < types.length; i++) {
var tower = new SourceTower(types[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
}
updateSourceTowers();
sourceTower = null;
enemiesToSpawn = 10;
// Always keep tower set button container above towers visually
if (towerSetButtonContainer && towerSetButtonContainer.parent) {
towerSetButtonContainer.parent.removeChild(towerSetButtonContainer);
game.addChild(towerSetButtonContainer);
}
game.update = function () {
// --- NEW: Lock futuristic towers immediately if no Research exists in ultra mode ---
if (gameSettings.difficulty === 'ultra') {
var researchExists = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchExists = true;
break;
}
}
if (!researchExists && futuristicUnlocked) {
futuristicUnlocked = false;
// Switch back to medieval towers if currently on futuristic
if (towerSet === 'futuristic') {
towerSet = 'medieval';
updateSourceTowers();
if (typeof medievalBG !== "undefined") {
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.5;
}
if (typeof futuristicBG !== "undefined") {
futuristicBG.tint = 0x222222;
futuristicBG.alpha = 0.5;
}
}
}
}
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
grid.pathFind();
grid.renderDebug();
// --- ULTRA MODE SPECIAL WAVES ---
if (gameSettings.difficulty === 'ultra' && currentWave > 7) {
// After wave 7, spawn Modern + 10 Angry + 2 Immune
var enemyTypes = [];
// Add 10 Angry enemies
for (var i = 0; i < 10; i++) {
enemyTypes.push('angry');
}
// Add 2 Immune enemies
for (var i = 0; i < 2; i++) {
enemyTypes.push('immune');
}
// --- Modern enemy logic for Ultra mode ---
var modernCount = 0;
if (currentWave % 10 === 0) {
modernCount = 3;
} else {
modernCount = 1;
}
// Add modern enemies to the enemyTypes array
for (var i = 0; i < modernCount; i++) {
enemyTypes.push('modern');
}
// Shuffle enemyTypes for randomness
for (var i = enemyTypes.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = enemyTypes[i];
enemyTypes[i] = enemyTypes[j];
enemyTypes[j] = temp;
}
// Spawn enemies
for (var i = 0; i < enemyTypes.length; i++) {
var waveType = enemyTypes[i];
var enemy = new Enemy(waveType);
enemyLayerBottom.addChild(enemy);
var healthMultiplier = Math.pow(1.12, currentWave);
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2);
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
var spawnX;
if (availableColumns.length > 0) {
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
spawnX = midPoint - 3 + Math.floor(Math.random() * 6);
}
var spawnY = -1 - Math.random() * 5;
enemy.cellX = spawnX;
enemy.cellY = 5;
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
} else if ((gameSettings.difficulty === 'hardcore' || gameSettings.difficulty === 'ultra') && currentWave > 7) {
// --- Modern enemy logic for Hardcore mode (and Ultra fallback for <=10) ---
// Insert after normal spawn logic, so after all other enemies are spawned
// This block will be reached for hardcore and for ultra waves <= 10
// (for ultra > 10, handled above)
// Determine how many modern enemies to spawn
var modernCount = 0;
if (currentWave % 10 === 0) {
modernCount = 3;
} else {
modernCount = 1;
}
for (var m = 0; m < modernCount; m++) {
var modernEnemy = new Enemy('modern');
enemyLayerBottom.addChild(modernEnemy);
var healthMultiplier = Math.pow(1.12, currentWave);
modernEnemy.maxHealth = Math.round(modernEnemy.maxHealth * healthMultiplier);
modernEnemy.health = modernEnemy.maxHealth;
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2);
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
var spawnX;
if (availableColumns.length > 0) {
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
spawnX = midPoint - 3 + Math.floor(Math.random() * 6);
}
var spawnY = -1 - Math.random() * 5;
modernEnemy.cellX = spawnX;
modernEnemy.cellY = 5;
modernEnemy.currentCellX = spawnX;
modernEnemy.currentCellY = spawnY;
modernEnemy.waveNumber = currentWave;
enemies.push(modernEnemy);
}
} else {
// --- ORIGINAL SPAWN LOGIC FOR NON-ULTRA OR <=10 ---
// Every wave spawns 10 enemies on wave 1, then 25% more each wave
var enemyCount = Math.floor(10 * Math.pow(1.25, currentWave - 1));
// England empire: 3 fewer normal enemies per wave
if (gameSettings.selectedEmpire === 'England') {
enemyCount = Math.max(1, enemyCount - 3); // Minimum 1 enemy
}
// Byzantine empire: 3 more enemies per wave
if (gameSettings.selectedEmpire === 'Byzantine') {
enemyCount = enemyCount + 3;
}
// Determine flying enemy count based on wave number
var flyingEnemyCount = 0;
if (currentWave === 1) {
flyingEnemyCount = 0;
} else if (currentWave === 2) {
flyingEnemyCount = 1;
} else if (currentWave === 3 || currentWave === 4) {
flyingEnemyCount = 3;
} else if (currentWave === 5) {
flyingEnemyCount = 5;
enemyCount = 9; // Total enemies for wave 5
} else if (currentWave === 6) {
flyingEnemyCount = 0; // No flying enemies in wave 6
enemyCount = 18; // 5 normal + 3 angry + 10 immune = 18 total enemies
} else if (currentWave === 7) {
flyingEnemyCount = 0; // No flying enemies in wave 7
enemyCount = 15; // 10 angry + 5 immune = 15 total enemies
} else if (currentWave === 8) {
flyingEnemyCount = 0; // No flying enemies in wave 8
enemyCount = 25; // 25 normal enemies
} else if (currentWave === 9) {
flyingEnemyCount = 0; // No flying enemies in wave 9
enemyCount = 25; // 25 normal enemies
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
// Determine enemy type based on specific wave requirements
var waveType = 'normal'; // Default to normal
var immuneCount = 0;
var angryCount = 0;
// Set specific immune enemy counts per wave
if (currentWave === 2 || currentWave === 3 || currentWave === 4 || currentWave === 5) {
immuneCount = 3;
} else if (currentWave === 6) {
immuneCount = 10; // Wave 6: 10 immune enemies
} else if (currentWave === 7) {
immuneCount = 5; // Wave 7: 5 immune enemies
} else if (currentWave === 8 || currentWave === 9) {
immuneCount = 0; // Wave 8 and 9: 0 immune enemies
}
// Hard mode: more immune and angry enemies
if (gameSettings.difficulty === 'hard') {
// Wave 1: 2 immune enemies
if (currentWave === 1) {
immuneCount = 2;
}
// Waves 2,3,4: 3 angry enemies each
if (currentWave === 2 || currentWave === 3 || currentWave === 4) {
angryCount = 3;
immuneCount = 3;
}
// Wave 5: keep existing counts
if (currentWave === 5) {
angryCount = 3;
immuneCount = 5;
}
} else {
// Set angry enemy spawning: 0 in waves 1-3, 2 in waves 4-5, 3 in wave 6
if (currentWave === 4) {
angryCount = 2;
} else if (currentWave === 5) {
angryCount = 2;
} else if (currentWave === 6) {
angryCount = 3; // Wave 6: 3 angry enemies
} else if (currentWave === 7) {
angryCount = 10; // Wave 7: 10 angry enemies
} else if (currentWave === 8 || currentWave === 9) {
angryCount = 0; // Wave 8 and 9: 0 angry enemies
}
}
// Set bad enemy spawning: only in wave 5, but not in Ultra mode
var badCount = 0;
if (currentWave === 5 && gameSettings.difficulty !== 'ultra') {
badCount = 1; // 1 bad enemy - same for all empires including England
immuneCount = 5; // 5 immune enemies
angryCount = 3; // 3 angry enemies
}
// Set super enemy spawning: only in wave 10 for hardcore mode, but not in Ultra mode
var superCount = 0;
if (currentWave === 10 && gameSettings.difficulty === 'hardcore') {
superCount = 0; // No super enemy in wave 10
badCount = 3; // 3 bad enemies in wave 10
angryCount = 0; // No angry enemies in wave 10
immuneCount = 0; // No immune enemies in wave 10
enemyCount = 3; // 3 bad enemies total
}
// Make the first 'angryCount' enemies angry for this wave
if (i < angryCount) {
waveType = 'angry';
}
// Make the next 'badCount' enemies bad for this wave (after angry enemies)
else if (i >= angryCount && i < angryCount + badCount) {
waveType = 'bad';
}
// Make the next 'superCount' enemies super for this wave (after bad enemies)
else if (i >= angryCount + badCount && i < angryCount + badCount + superCount) {
waveType = 'super';
}
// Make the last 'immuneCount' enemies immune for this wave (but not if already angry, bad, or super)
else if (i >= enemyCount - immuneCount) {
waveType = 'immune';
}
var enemy = new Enemy(waveType);
enemyLayerBottom.addChild(enemy);
var healthMultiplier = Math.pow(1.12, currentWave);
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2);
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
var spawnX;
if (availableColumns.length > 0) {
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
}
var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
} // end else (original spawn logic)
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
var notification = game.addChild(new Notification("Wave " + currentWave + " completed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Add 10 points for completing each wave in ultra mode
if (gameSettings.difficulty === 'ultra') {
addScore(10);
// Add all enemy kills for this wave to score (already counted per kill)
// (No extra code needed here, as kills are counted above)
}
// Automatically advance to next wave if all enemies are gone
if (currentWave < totalWaves) {
waveTimer = nextWaveTime; // Trigger next wave immediately
}
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Give gold for every enemy killed - Hard mode gives 5 gold, others give 10
var goldEarned = gameSettings.difficulty === 'hard' ? 5 : 10;
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Add 1 point for each enemy killed in ultra mode and count kills
if (gameSettings.difficulty === 'ultra') {
addScore(1);
if (typeof ultraEnemyKills !== "undefined") {
ultraEnemyKills++;
}
}
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
lives = Math.max(0, lives - (enemy.type === 'bad' ? 10 : 1));
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
tutorialCompleted: false
});
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Skip bullet update when game is paused (gameSpeed = 0)
if (gameSpeed === 0) {
return;
}
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Apply damage to target enemy
var actualDamage = self.damage;
// Artillery towers deal 40% of enemy's current health as damage
if (self.type === 'splash') {
actualDamage = Math.ceil(self.targetEnemy.health * 0.4);
// AntiAir bullets deal half damage to flying enemies
if (self.targetEnemy.isFlying) {
actualDamage = actualDamage * 0.5;
}
}
// Immune enemies take 10% more damage from all bullets
if (self.targetEnemy.isImmune) {
actualDamage = actualDamage * 1.1;
}
// Immune enemies now take damage from all bullet types
self.targetEnemy.health -= actualDamage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create visual splash effect
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
// Splash damage to nearby enemies
var splashRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var splashDx = otherEnemy.x - self.targetEnemy.x;
var splashDy = otherEnemy.y - self.targetEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage or 20% of enemy health for Artillery)
var splashDamage;
if (self.type === 'splash') {
splashDamage = Math.ceil(otherEnemy.health * 0.2); // 20% of current health for splash
} else {
splashDamage = self.damage * 0.5;
}
// Immune enemies take 10% more splash damage
if (otherEnemy.isImmune) {
splashDamage = splashDamage * 1.1;
}
// Immune enemies now take splash damage from all bullet types
otherEnemy.health -= splashDamage;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
} else if (self.type === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
numberLabel.visible = false; // Hide numbers in battlefield
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
cellGraphics.tint = 0x880000;
return;
}
numberLabel.visible = false; // Hide numbers in battlefield
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
} else {
cellGraphics.tint = 0x88 - tint << 8 | tint;
}
// Remove arrow rendering - no longer display arrows
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0x008800;
numberLabel.visible = false;
break;
}
case 4:
{
self.removeArrows();
cellGraphics.tint = 0x000000; // Black color for castle walls
numberLabel.visible = false;
break;
}
}
// numberLabel.setText(Math.floor(data.score / 1000) / 10); // Removed number display
};
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
}
effectGraphics.alpha = 0.7;
self.alpha = 0;
// Animate the effect
tween(self, {
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
// Base enemy class for common functionality
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'normal';
self.speed = .01;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.speed *= 1.275; // 27.5% faster than normal (reduced by 15% from previous 50%)
self.maxHealth = 40;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'angry':
self.speed *= 1; // Same speed as normal enemies
self.maxHealth = 120; // More health than normal
break;
case 'modern':
self.speed *= 1; // Same speed as angry enemies
self.maxHealth = 600; // 5x health of angry enemies (120 * 5)
break;
case 'bad':
self.speed *= 0.5; // 50% slower than normal
self.maxHealth = 1000; // 10 times the health of a normal enemy
break;
case 'super':
self.speed *= 0.4; // 60% slower than normal
self.maxHealth = 2000; // 20 times the health of a normal enemy (2x bad enemy)
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
// Boss spawning disabled for waves 10, 20, 30
// if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
// self.isBoss = true;
// // Boss enemies have 20x health and are larger
// self.maxHealth *= 20;
// // Slower speed for bosses
// self.speed = self.speed * 0.7;
// }
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type === 'angry') {
assetId = 'Angry_Enemy';
} else if (self.type === 'modern') {
assetId = 'modern_enemy';
} else if (self.type === 'bad') {
assetId = 'enemy_bad';
} else if (self.type === 'super') {
assetId = 'enemy_super';
} else if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Fall back to regular enemy asset if specific type asset not found
// Apply tint to differentiate enemy types
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
break;
case 'immune':
enemyGraphics.tint = 0xAA0000; // Red for immune enemies
break;
case 'flying':
enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
break;
case 'swarm':
enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
shadowGraphics.scaleX = 1.8;
shadowGraphics.scaleY = 1.8;
}
// Position shadow slightly offset
self.shadow.x = 20; // Offset right
self.shadow.y = 20; // Offset down
// Ensure shadow has the same rotation as the enemy
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
// Skip enemy update when game is paused (gameSpeed = 0)
if (gameSpeed === 0) {
return;
}
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Anti-stuck mechanism: track if enemy hasn't moved significantly
if (self.lastPosition === undefined) {
self.lastPosition = {
x: self.currentCellX,
y: self.currentCellY,
time: LK.ticks
};
self.stuckCounter = 0;
}
// Check if enemy has been stuck in same position for too long
var timeSinceLastCheck = LK.ticks - self.lastPosition.time;
if (timeSinceLastCheck > 180) {
// Check every 3 seconds
var distanceMoved = Math.sqrt((self.currentCellX - self.lastPosition.x) * (self.currentCellX - self.lastPosition.x) + (self.currentCellY - self.lastPosition.y) * (self.currentCellY - self.lastPosition.y));
if (distanceMoved < 0.5) {
// If moved less than half a cell
self.stuckCounter++;
if (self.stuckCounter >= 3) {
// If stuck for 3 checks (9 seconds)
// Force reset enemy position and target
self.currentTarget = null;
// Try to find a valid spawn position
for (var sx = 9; sx <= 14; sx++) {
var testCell = grid.getCell(sx, 5);
if (testCell && testCell.pathId === pathId) {
self.cellX = sx;
self.cellY = 5;
self.currentCellX = sx;
self.currentCellY = 5;
self.stuckCounter = 0;
break;
}
}
}
} else {
self.stuckCounter = 0; // Reset stuck counter if moving
}
// Update last position
self.lastPosition = {
x: self.currentCellX,
y: self.currentCellY,
time: LK.ticks
};
}
// Immune enemies attack towers in range
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.poisoned = false;
self.poisonEffect = false;
// Attack towers every 120 frames (2 seconds)
if (LK.ticks % Math.max(1, Math.floor(120 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 2 cell range
if (distance <= CELL_SIZE * 2) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Only attack if not protected by a Healer tower
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
var damage = Math.floor(tower.maxHealth / 10); // 1/10th of tower's max health
// Hardcore mode: enemies deal 2x damage
if (gameSettings.difficulty === 'hardcore') {
damage *= 2;
}
// Hardcore mode: spawn 3 immune enemies every wave
if (gameSettings.difficulty === 'hardcore') {
immuneCount = 3;
}
tower.health -= damage;
if (tower.health <= 0) {
tower.health = 0;
}
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else if (self.type === 'angry') {
// Angry enemies attack towers in range, but are blocked by Healer towers
// Attack towers every 120 frames (2 seconds)
if (LK.ticks % Math.max(1, Math.floor(120 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 2 cell range
if (distance <= CELL_SIZE * 2) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Healer towers completely block damage from Angry enemies
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
var damage = Math.floor(tower.maxHealth * 0.25); // 25% of tower's max health
// Hardcore mode: enemies deal 2x damage
if (gameSettings.difficulty === 'hardcore') {
damage *= 2;
}
tower.health -= damage;
if (tower.health <= 0) {
tower.health = 0;
}
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else if (self.type === 'modern') {
// Modern enemies attack towers in range with 2x damage compared to Angry enemies
// Attack towers every 120 frames (2 seconds)
if (LK.ticks % Math.max(1, Math.floor(120 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 2 cell range
if (distance <= CELL_SIZE * 2) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Healer towers completely block damage from Modern enemies
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
var damage = Math.floor(tower.maxHealth * 0.50); // 50% of tower's max health (2x angry damage)
// Hardcore mode: enemies deal 2x damage
if (gameSettings.difficulty === 'hardcore') {
damage *= 2;
}
tower.health -= damage;
if (tower.health <= 0) {
tower.health = 0;
}
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else if (self.type === 'bad') {
// Bad enemies attack towers more frequently and deal more damage
// Attack towers every 90 frames (1.5 seconds)
if (LK.ticks % Math.max(1, Math.floor(90 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 2.5 cell range (longer range than angry)
if (distance <= CELL_SIZE * 2.5) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Only attack if not protected by a Healer tower
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
// Bad enemies destroy towers in one hit (or deal massive damage in hardcore)
if (gameSettings.difficulty === 'hardcore') {
tower.health = 0; // Still one-shot in hardcore
} else {
tower.health = 0; // One-shot in normal/hard
}
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else if (self.type === 'super') {
// Super enemies attack towers more frequently and deal devastating damage
// Attack towers every 60 frames (1 second)
if (LK.ticks % Math.max(1, Math.floor(60 / gameSpeed)) === 0) {
// Find towers in range and attack them
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Attack towers within 3 cell range (longer range than bad)
if (distance <= CELL_SIZE * 3) {
// Check if any Healer towers are protecting this tower
var isProtectedByHealer = false;
for (var h = 0; h < towers.length; h++) {
var healerTower = towers[h];
if (healerTower.id === 'poison' && healerTower.isInRange(tower)) {
isProtectedByHealer = true;
break;
}
}
// Only attack if not protected by a Healer tower
if (!isProtectedByHealer) {
// Easy mode: towers take no damage
if (gameSettings.difficulty !== 'easy') {
// Super enemies destroy towers instantly regardless of difficulty
tower.health = 0;
// Update tower health bar
tower.towerHealthBar.width = tower.health / tower.maxHealth * 70;
}
}
break; // Only attack one tower per update
}
}
}
} else {
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second), adjusted for game speed
if (LK.ticks % Math.max(1, Math.floor(30 / gameSpeed)) === 0) {
self.health -= self.poisonDamage * gameSpeed;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration -= gameSpeed;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.type === 'bad') {
// Bad enemies use their asset's natural color - no tint applied
} else if (self.type === 'super') {
enemyGraphics.tint = 0x800080; // Purple tint for super enemies
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.spawns = [];
self.goals = [];
for (var i = 0; i < gridWidth; i++) {
self.cells[i] = [];
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j] = {
score: 0,
pathId: 0,
towersInRange: []
};
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
4: Castle wall
*/
for (var i = 0; i < gridWidth; i++) {
var _loop = function _loop() {
cell = self.cells[i][j];
cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0; // Create castle structure with random labyrinth
if (j > 4 && j < gridHeight - 4) {
// Simple pseudo-random number generator using seed
var seededRandom = function seededRandom() {
mazeSeed = (mazeSeed * 9301 + 49297) % 233280;
return mazeSeed / 233280;
}; // Main castle outer walls
// Generate maze seed based on reload counter to ensure different maze each reload
// Use reload-based seed as primary factor for maze generation
mazeSeed = gameSessionSeed * 7919 + (currentWave + 1) * 1009 + reloadCounter * 4397;
if ((i === 2 || i === gridWidth - 3) && j >= 8 && j <= gridHeight - 8) {
cellType = 4; // Castle wall
}
// Castle towers at corners
if ((i >= 1 && i <= 4 || i >= gridWidth - 5 && i <= gridWidth - 2) && (j >= 7 && j <= 10 || j >= gridHeight - 11 && j <= gridHeight - 8)) {
cellType = 4;
}
// Generate unique geometric maze patterns based on seed
var mazePattern = (mazeSeed * 17 + (currentWave + 1) * 23) % 100;
if (j >= 12 && j <= gridHeight - 12) {
// Create safe corridors first - ensure at least 3 clear horizontal paths
var safePaths = [16, 20, 24];
var isSafePath = false;
for (var sp = 0; sp < safePaths.length; sp++) {
if (j === safePaths[sp] && i >= 8 && i <= gridWidth - 9) {
isSafePath = true;
break;
}
}
if (!isSafePath) {
// Generate dynamic geometric patterns based on random seed
if (mazePattern < 20) {
// Concentric geometric shapes
var centerX = Math.floor(gridWidth / 2);
var centerY = Math.floor((gridHeight - 12 + 12) / 2);
var distance = Math.sqrt((i - centerX) * (i - centerX) + (j - centerY) * (j - centerY));
var ringSize = 2 + mazeSeed % 3;
if (Math.floor(distance) % ringSize === 1 && distance > 2 && distance < 8 && i >= 6 && i <= gridWidth - 7) {
if (seededRandom() > 0.6) {
cellType = 4;
}
}
} else if (mazePattern < 40) {
// Geometric wave patterns
var waveFreq = 0.2 + mazeSeed % 5 * 0.1;
var waveHeight = 2 + mazeSeed % 3;
var expectedY = Math.floor(Math.sin(i * waveFreq) * waveHeight) + Math.floor((gridHeight - 12 + 12) / 2);
if (Math.abs(j - expectedY) < 2 && i >= 6 && i <= gridWidth - 7) {
if (seededRandom() > 0.5) {
cellType = 4;
}
}
} else if (mazePattern < 60) {
// Diamond and rhombus patterns
var midX = Math.floor(gridWidth / 2);
var midY = Math.floor((gridHeight - 12 + 12) / 2);
var manhattanDist = Math.abs(i - midX) + Math.abs(j - midY);
var diamondSize = 3 + mazeSeed % 4;
if (manhattanDist >= diamondSize && manhattanDist <= diamondSize + 3 && manhattanDist % 2 === 0) {
if (seededRandom() > 0.5) {
cellType = 4;
}
}
} else if (mazePattern < 80) {
// Hexagonal and triangular patterns
var hexSize = 4 + mazeSeed % 3;
var hexX = Math.floor(i / hexSize);
var hexY = Math.floor(j / hexSize);
if ((hexX + hexY) % 3 === 0 && (i % hexSize < 2 || j % hexSize < 2) && i >= 6 && i <= gridWidth - 7) {
if (seededRandom() > 0.4) {
cellType = 4;
}
}
} else {
// Spiral and radial patterns
var centerX = Math.floor(gridWidth / 2);
var centerY = Math.floor((gridHeight - 12 + 12) / 2);
var angle = Math.atan2(j - centerY, i - centerX);
var spiralArms = 4 + mazeSeed % 4;
var armIndex = Math.floor(angle / (Math.PI * 2 / spiralArms));
var distance = Math.sqrt((i - centerX) * (i - centerX) + (j - centerY) * (j - centerY));
if (armIndex % 2 === 0 && distance > 2 && distance < 7 && i >= 6 && i <= gridWidth - 7) {
if (seededRandom() > 0.45) {
cellType = 4;
}
}
}
// Add strategic scattered obstacles with lower density
if (i >= 7 && i <= gridWidth - 8 && j >= 14 && j <= gridHeight - 14) {
if (seededRandom() > 0.8) {
cellType = 4;
}
}
}
}
// Central castle keep - always present
if (i >= 10 && i <= 13 && j >= gridHeight - 16 && j <= gridHeight - 13) {
if (!(i === 11 || i === 12) || !(j === gridHeight - 15 || j === gridHeight - 14)) {
cellType = 4;
}
}
}
// Spawn and goal areas
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
},
cell,
cellType,
mazeSeed,
horizontalWallRows,
hr,
verticalWallCols,
vc,
debugCell;
for (var j = 0; j < gridHeight; j++) {
_loop();
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1 && node.type != 4) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.up.type != 4 && node.left.type != 1 && node.left.type != 4 && node.upLeft && node.upLeft.type != 1 && node.upLeft.type != 4) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.up.type != 4 && node.right.type != 1 && node.right.type != 4 && node.upRight && node.upRight.type != 1 && node.upRight.type != 4) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.down.type != 4 && node.right.type != 1 && node.right.type != 4 && node.downRight && node.downRight.type != 1 && node.downRight.type != 4) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.down.type != 4 && node.left.type != 1 && node.left.type != 4 && node.downLeft && node.downLeft.type != 1 && node.downLeft.type != 4) {
processNode(node.downLeft, targetScore, node);
}
targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
// Check if any spawn point has a valid path to any goal
var hasValidPath = false;
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId === pathId) {
hasValidPath = true;
break;
}
}
// Ensure maze is navigable - return false if paths are blocked
if (!hasValidPath) {
console.log("Maze blocks all paths - this layout will be regenerated");
return true; // Signal that maze needs regeneration
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed * gameSpeed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed * gameSpeed;
enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget || !cell || cell.pathId !== pathId) {
// Reset target if we don't have one or if current cell is invalid
if (cell && cell.pathId === pathId && cell.targets && cell.targets.length > 0) {
enemy.currentTarget = cell.targets[0];
} else {
// If current cell is invalid, try to find nearest valid cell
var nearestValidCell = null;
var minDistance = Infinity;
// Search for nearby valid cells
for (var searchX = Math.max(0, enemy.cellX - 2); searchX <= Math.min(grid.cells.length - 1, enemy.cellX + 2); searchX++) {
for (var searchY = Math.max(0, enemy.cellY - 2); searchY <= Math.min(grid.cells[0].length - 1, enemy.cellY + 2); searchY++) {
var searchCell = grid.getCell(searchX, searchY);
if (searchCell && searchCell.pathId === pathId && searchCell.targets && searchCell.targets.length > 0) {
var dx = searchX - enemy.cellX;
var dy = searchY - enemy.cellY;
var distance = dx * dx + dy * dy;
if (distance < minDistance) {
minDistance = distance;
nearestValidCell = searchCell;
}
}
}
}
if (nearestValidCell) {
enemy.currentTarget = nearestValidCell;
// Snap enemy to the valid cell position
enemy.cellX = nearestValidCell.x;
enemy.cellY = nearestValidCell.y;
enemy.currentCellX = nearestValidCell.x;
enemy.currentCellY = nearestValidCell.y;
}
}
}
if (enemy.currentTarget) {
// Validate that current target is still valid
if (enemy.currentTarget.pathId !== pathId) {
enemy.currentTarget = null;
return; // Will be fixed next frame
}
// If we have a better path from current cell, use it
if (cell && cell.pathId === pathId && cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed * gameSpeed) {
// Reached target, snap to grid and clear target
enemy.cellX = enemy.currentTarget.x;
enemy.cellY = enemy.currentTarget.y;
enemy.currentCellX = enemy.currentTarget.x;
enemy.currentCellY = enemy.currentTarget.y;
enemy.currentTarget = null;
// Don't return here, let position update happen
} else {
// Move toward target
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed * gameSpeed;
enemy.currentCellY += Math.sin(angle) * enemy.speed * gameSpeed;
}
} else {
// No valid target - try to get unstuck by moving toward goal
var goalFound = false;
for (var g = 0; g < self.goals.length; g++) {
var goal = self.goals[g];
var gx = goal.x - enemy.currentCellX;
var gy = goal.y - enemy.currentCellY;
if (Math.abs(gx) > 0.1 || Math.abs(gy) > 0.1) {
var gAngle = Math.atan2(gy, gx);
enemy.currentCellX += Math.cos(gAngle) * enemy.speed * gameSpeed * 0.5; // Move slower when lost
enemy.currentCellY += Math.sin(gAngle) * enemy.speed * gameSpeed * 0.5;
goalFound = true;
break;
}
}
}
// Update visual position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var MainMenu = Container.expand(function () {
var self = Container.call(this);
// Center the menu container
self.x = 2048 / 2;
self.y = 2732 / 2;
// Background overlay
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 2732;
menuBackground.tint = 0x000000;
menuBackground.alpha = 0.8;
// Current state: 'difficulty' or 'empire'
self.currentState = 'difficulty';
self.selectedDifficulty = null;
// Difficulty selection elements
var difficultyTitle = new Text2("Select Difficulty", {
size: 120,
fill: 0xFFFFFF,
weight: 800
});
difficultyTitle.anchor.set(0.5, 0.5);
difficultyTitle.y = -600;
self.addChild(difficultyTitle);
// Difficulty buttons
var difficulties = ['easy', 'normal', 'hard', 'hardcore', 'ultra'];
var difficultyButtons = [];
var difficultyButtonSpacing = 150;
var difficultyStartY = -300;
for (var i = 0; i < difficulties.length; i++) {
var difficulty = difficulties[i];
var button = new Container();
var buttonBG = button.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBG.width = 600;
buttonBG.height = 120;
// Set button color based on difficulty
switch (difficulty) {
case 'easy':
buttonBG.tint = 0x00FF00; // Green
break;
case 'hard':
buttonBG.tint = 0xFF8800; // Orange
break;
case 'hardcore':
buttonBG.tint = 0xFF0000; // Red
break;
case 'ultra':
buttonBG.tint = 0x800080; // Purple for ultra
break;
default:
buttonBG.tint = 0x0088FF; // Blue for normal
break;
}
var buttonText = new Text2(difficulty.charAt(0).toUpperCase() + difficulty.slice(1), {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.y = difficultyStartY + i * difficultyButtonSpacing;
button.difficulty = difficulty;
button.down = function () {
LK.getSound('Click').play();
// Show confirmation popup for Easy, Normal, and Hard, direct to empire selection for hardcore and ultra
if (this.difficulty === 'easy' || this.difficulty === 'normal' || this.difficulty === 'hard') {
self.showConfirmationPopup(this.difficulty);
} else {
self.selectedDifficulty = this.difficulty;
self.showEmpireSelection();
}
};
self.addChild(button);
difficultyButtons.push(button);
}
// Empire selection elements (initially hidden)
var empireTitle = new Text2("Choose Your Empire!", {
size: 100,
fill: 0xFFFFFF,
weight: 800
});
empireTitle.anchor.set(0.5, 0.5);
empireTitle.y = -700;
empireTitle.visible = false;
self.addChild(empireTitle);
// Empire buttons - 9 different empires including Normal
var empires = [{
name: 'Normal',
color: 0xAAAAAA,
description: ''
}, {
name: 'Austria',
color: 0x00FF00,
description: ''
}, {
name: 'Hungary',
color: 0x0088FF,
description: ''
}, {
name: 'Ottoman',
color: 0xFF8800,
description: ''
}, {
name: 'French',
color: 0xFF0000,
description: ''
}, {
name: 'England',
color: 0x800080,
description: ''
}, {
name: 'Scotch',
color: 0x008080,
description: ''
}, {
name: 'Kalmar Union',
color: 0x006400,
description: ''
}, {
name: 'Byzantine',
color: 0xFF69B4,
description: ''
}];
var empireButtons = [];
var empireButtonSpacing = 200;
var empireStartY = -400;
for (var i = 0; i < empires.length; i++) {
var empire = empires[i];
var button = new Container();
var buttonBG = button.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBG.width = 528;
buttonBG.height = 176;
buttonBG.tint = empire.color;
var buttonText = new Text2(empire.name, {
size: 53,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
buttonText.y = 0;
button.addChild(buttonText);
var descText = new Text2(empire.description, {
size: 35,
fill: 0xFFFFFF,
weight: 400
});
descText.anchor.set(0.5, 0.5);
descText.y = 40;
button.addChild(descText);
button.y = empireStartY + i * empireButtonSpacing;
button.visible = false; // Initially hidden
button.down = function (empireData) {
return function () {
LK.getSound('Click').play();
// Show empire characteristics menu instead of starting game directly
self.showEmpireCharacteristics(empireData);
};
}(empire);
self.addChild(button);
empireButtons.push(button);
}
// No exit buttons needed
var exitButtons = [];
// Add confirmation popup for Easy and Hard difficulties
self.confirmationPopup = null;
self.showConfirmationPopup = function (difficulty) {
// Remove any existing confirmation popup
if (self.confirmationPopup) {
self.removeChild(self.confirmationPopup);
self.confirmationPopup.destroy();
}
// Create confirmation popup
self.confirmationPopup = new Container();
// Background for confirmation popup
var confirmBG = self.confirmationPopup.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
confirmBG.width = 1200;
confirmBG.height = 600;
confirmBG.alpha = 0.95;
// Confirmation title
var confirmText = "Are you sure: ";
if (difficulty === "easy") {
confirmText += "Easy?";
} else if (difficulty === "normal") {
confirmText += "Normal?";
} else if (difficulty === "hard") {
confirmText += "Hard?";
} else {
confirmText = "Are You Sure?";
}
var confirmTitle = new Text2(confirmText, {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
confirmTitle.anchor.set(0.5, 0.5);
confirmTitle.y = -150;
self.confirmationPopup.addChild(confirmTitle);
// Yes button
var yesButton = new Container();
var yesBG = yesButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
yesBG.width = 300;
yesBG.height = 120;
yesBG.tint = 0x00AA00;
var yesText = new Text2("Yes", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
yesText.anchor.set(0.5, 0.5);
yesButton.addChild(yesText);
yesButton.x = -200;
yesButton.y = 100;
yesButton.down = function () {
LK.getSound('Click').play();
// Remove confirmation popup and proceed to empire selection
self.removeChild(self.confirmationPopup);
self.confirmationPopup.destroy();
self.confirmationPopup = null;
// Show empire selection
self.selectedDifficulty = difficulty;
self.showEmpireSelection();
};
self.confirmationPopup.addChild(yesButton);
// No button
var noButton = new Container();
var noBG = noButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
noBG.width = 300;
noBG.height = 120;
noBG.tint = 0xAA0000;
var noText = new Text2("No", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
noText.anchor.set(0.5, 0.5);
noButton.addChild(noText);
noButton.x = 200;
noButton.y = 100;
noButton.down = function () {
LK.getSound('Click').play();
// Remove confirmation popup and return to difficulty selection
self.removeChild(self.confirmationPopup);
self.confirmationPopup.destroy();
self.confirmationPopup = null;
self.selectedDifficulty = null;
// Stay on difficulty selection screen - no state change needed
};
self.confirmationPopup.addChild(noButton);
self.addChild(self.confirmationPopup);
};
self.showEmpireSelection = function () {
// Check if hardcore or ultra difficulty was selected
if (self.selectedDifficulty === 'hardcore') {
// Show hardcore warning popup instead of empire selection
self.showHardcoreWarning();
return;
} else if (self.selectedDifficulty === 'ultra') {
// Show ultra warning popup instead of empire selection
self.showUltraWarning();
return;
}
self.currentState = 'empire';
// Hide difficulty elements
difficultyTitle.visible = false;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = false;
}
// Show empire elements
empireTitle.visible = true;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = true;
}
// No exit buttons to show
};
self.showHardcoreWarning = function () {
// Remove any existing warning popup
if (self.hardcoreWarningPopup) {
self.removeChild(self.hardcoreWarningPopup);
self.hardcoreWarningPopup.destroy();
}
// Create hardcore warning popup
self.hardcoreWarningPopup = new Container();
// Background for warning popup
var warningBG = self.hardcoreWarningPopup.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
warningBG.width = 1600;
warningBG.height = 800;
warningBG.alpha = 0.95;
// Warning title
var warningTitle = new Text2("WARNING", {
size: 100,
fill: 0xFF0000,
weight: 800
});
warningTitle.anchor.set(0.5, 0.5);
warningTitle.y = -250;
self.hardcoreWarningPopup.addChild(warningTitle);
// Warning message
var warningText = new Text2("This difficulty is not one of the original game\ndifficulties for Honorbound: Rise on the Fifth Dawn.\nIT IS FOR ADVANCED PLAYERS ONLY!", {
size: 60,
fill: 0xFFFFFF,
weight: 400
});
warningText.anchor.set(0.5, 0.5);
warningText.y = -50;
self.hardcoreWarningPopup.addChild(warningText);
// Okay button
var okayButton = new Container();
var okayBG = okayButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
okayBG.width = 300;
okayBG.height = 120;
okayBG.tint = 0x00AA00;
var okayText = new Text2("Okay", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
okayText.anchor.set(0.5, 0.5);
okayButton.addChild(okayText);
okayButton.x = -200;
okayButton.y = 200;
okayButton.down = function () {
LK.getSound('Click').play();
// Remove warning popup and proceed to empire selection
self.removeChild(self.hardcoreWarningPopup);
self.hardcoreWarningPopup.destroy();
self.hardcoreWarningPopup = null;
// Show empire selection
self.currentState = 'empire';
// Hide difficulty elements
difficultyTitle.visible = false;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = false;
}
// Show empire elements
empireTitle.visible = true;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = true;
}
};
self.hardcoreWarningPopup.addChild(okayButton);
// Back button
var backButton = new Container();
var backBG = backButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
backBG.width = 300;
backBG.height = 120;
backBG.tint = 0xAA0000;
var backText = new Text2("Back", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
backText.anchor.set(0.5, 0.5);
backButton.addChild(backText);
backButton.x = 200;
backButton.y = 200;
backButton.down = function () {
LK.getSound('Click').play();
// Remove warning popup and return to difficulty selection
self.removeChild(self.hardcoreWarningPopup);
self.hardcoreWarningPopup.destroy();
self.hardcoreWarningPopup = null;
self.selectedDifficulty = null;
// Return to difficulty selection
self.currentState = 'difficulty';
// Show difficulty elements
difficultyTitle.visible = true;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = true;
}
// Hide empire elements
empireTitle.visible = false;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = false;
}
};
self.hardcoreWarningPopup.addChild(backButton);
self.addChild(self.hardcoreWarningPopup);
};
self.showUltraWarning = function () {
// Remove any existing warning popup
if (self.ultraWarningPopup) {
self.removeChild(self.ultraWarningPopup);
self.ultraWarningPopup.destroy();
}
// Create ultra warning popup
self.ultraWarningPopup = new Container();
// Background for warning popup
var warningBG = self.ultraWarningPopup.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
warningBG.width = 1600;
warningBG.height = 800;
warningBG.alpha = 0.95;
// Warning title
var warningTitle = new Text2("ULTRA MODE", {
size: 100,
fill: 0x800080,
weight: 800
});
warningTitle.anchor.set(0.5, 0.5);
warningTitle.y = -250;
self.ultraWarningPopup.addChild(warningTitle);
// Warning message
var warningText = new Text2("This is an extreme endurance mode!\n30 waves without any King.\nEverything else is the same as Hardcore.\nAre you ready for the ultimate challenge?", {
size: 60,
fill: 0xFFFFFF,
weight: 400
});
warningText.anchor.set(0.5, 0.5);
warningText.y = -50;
self.ultraWarningPopup.addChild(warningText);
// Okay button
var okayButton = new Container();
var okayBG = okayButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
okayBG.width = 300;
okayBG.height = 120;
okayBG.tint = 0x00AA00;
var okayText = new Text2("Okay", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
okayText.anchor.set(0.5, 0.5);
okayButton.addChild(okayText);
okayButton.x = -200;
okayButton.y = 200;
okayButton.down = function () {
LK.getSound('Click').play();
// Remove warning popup and proceed to empire selection
self.removeChild(self.ultraWarningPopup);
self.ultraWarningPopup.destroy();
self.ultraWarningPopup = null;
// Show empire selection
self.currentState = 'empire';
// Hide difficulty elements
difficultyTitle.visible = false;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = false;
}
// Show empire elements
empireTitle.visible = true;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = true;
}
};
self.ultraWarningPopup.addChild(okayButton);
// Back button
var backButton = new Container();
var backBG = backButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
backBG.width = 300;
backBG.height = 120;
backBG.tint = 0xAA0000;
var backText = new Text2("Back", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
backText.anchor.set(0.5, 0.5);
backButton.addChild(backText);
backButton.x = 200;
backButton.y = 200;
backButton.down = function () {
LK.getSound('Click').play();
// Remove warning popup and return to difficulty selection
self.removeChild(self.ultraWarningPopup);
self.ultraWarningPopup.destroy();
self.ultraWarningPopup = null;
self.selectedDifficulty = null;
// Return to difficulty selection
self.currentState = 'difficulty';
// Show difficulty elements
difficultyTitle.visible = true;
for (var i = 0; i < difficultyButtons.length; i++) {
difficultyButtons[i].visible = true;
}
// Hide empire elements
empireTitle.visible = false;
for (var i = 0; i < empireButtons.length; i++) {
empireButtons[i].visible = false;
}
};
self.ultraWarningPopup.addChild(backButton);
self.addChild(self.ultraWarningPopup);
};
self.showEmpireCharacteristics = function (empireData) {
// Remove any existing characteristics popup
if (self.characteristicsPopup) {
self.removeChild(self.characteristicsPopup);
self.characteristicsPopup.destroy();
}
// Create characteristics popup
self.characteristicsPopup = new Container();
// Background for characteristics popup
var charBG = self.characteristicsPopup.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
charBG.width = 1400;
charBG.height = 1000;
charBG.alpha = 0.95;
// Empire name title
var titleText = new Text2(empireData.name + " Empire", {
size: 80,
fill: empireData.color,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -400;
self.characteristicsPopup.addChild(titleText);
// Empire characteristics text
var characteristicsText = "";
switch (empireData.name) {
case 'Normal':
characteristicsText = "Advantages:\nβ’ Standard gameplay\nβ’ No special bonuses\nβ’ Balanced empire\nβ’ Good for beginners";
break;
case 'Austria':
characteristicsText = "Advantages:\nβ’ +20% tower fire rate\nβ’ All towers attack faster\nβ’ Excellent for rapid defense";
break;
case 'Hungary':
characteristicsText = "Advantages:\nβ’ Tower cost -5 gold\n (Normal+ difficulty)\nβ’ Better economy management\nβ’ More affordable towers";
break;
case 'Ottoman':
characteristicsText = "Advantages:\nβ’ Start with 100 gold\n (instead of 80)\nβ’ +20% damage on all attacks\nβ’ Strong early game";
break;
case 'French':
characteristicsText = "Advantages:\nβ’ +20% tower durability\nβ’ Towers have more health\nβ’ Better defensive capability";
break;
case 'England':
characteristicsText = "Advantages:\nβ’ Start with 100 gold\n (instead of 80)\nβ’ 3 fewer enemies per wave\nβ’ Easier defense management";
break;
case 'Scotch':
characteristicsText = "Advantages:\nβ’ Tower costs -3 gold\nβ’ Archer range +50%\nβ’ Better defense capability";
break;
case 'Kalmar Union':
characteristicsText = "Advantages:\nβ’ Healer range +100%\nβ’ Better healing coverage\nβ’ Improved tower protection";
break;
case 'Byzantine':
characteristicsText = "Advantages:\nβ’ Tower durability +100%\nβ’ Towers have double health\nDisadvantages:\nβ’ +3 enemies per wave\nβ’ Higher enemy pressure";
break;
default:
characteristicsText = "Advantages:\nβ’ Standard empire\nβ’ No special bonuses\nβ’ Balanced gameplay";
}
var infoText = new Text2(characteristicsText, {
size: 60,
fill: 0xFFFFFF,
weight: 400
});
infoText.anchor.set(0.5, 0.5);
infoText.y = -150;
self.characteristicsPopup.addChild(infoText);
// Select Empire button
var selectButton = new Container();
var selectBG = selectButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
selectBG.width = 400;
selectBG.height = 120;
selectBG.tint = 0x00AA00;
var selectText = new Text2("Select Empire", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
selectText.anchor.set(0.5, 0.5);
selectButton.addChild(selectText);
selectButton.y = 200;
selectButton.down = function () {
LK.getSound('Click').play();
// Store selected empire and start tutorial for easy/normal, or start game directly for others
gameSettings.selectedEmpire = empireData.name;
self.removeChild(self.characteristicsPopup);
self.characteristicsPopup.destroy();
self.characteristicsPopup = null;
// Show tutorial for easy and normal modes
if (self.selectedDifficulty === 'easy' || self.selectedDifficulty === 'normal') {
self.startTutorial();
} else {
self.startGame();
}
};
self.characteristicsPopup.addChild(selectButton);
// Back button
var backButton = new Container();
var backBG = backButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
backBG.width = 200;
backBG.height = 100;
backBG.tint = 0xAA0000;
var backText = new Text2("Back", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
backText.anchor.set(0.5, 0.5);
backButton.addChild(backText);
backButton.y = 350;
backButton.down = function () {
LK.getSound('Click').play();
self.removeChild(self.characteristicsPopup);
self.characteristicsPopup.destroy();
self.characteristicsPopup = null;
};
self.characteristicsPopup.addChild(backButton);
self.addChild(self.characteristicsPopup);
};
self.startTutorial = function () {
// Check if tutorial has already been completed
if (storage.tutorialCompleted) {
// Skip tutorial and start game directly
self.startGame();
return;
}
// Remove start menu first
self.destroy();
// Apply game settings but don't enable interactions yet
gameSettings.difficulty = self.selectedDifficulty;
gameSettings.selectedDifficulty = self.selectedDifficulty;
updateTotalWaves();
// Apply empire starting gold bonuses
if (gameSettings.selectedEmpire === 'Ottoman' || gameSettings.selectedEmpire === 'England') {
setGold(100);
} else {
setGold(80);
}
// Show UI elements but keep interactions disabled
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update tower cost displays
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].updateCostLabels();
}
// Create and show tutorial
var tutorial = new Tutorial(self.selectedDifficulty);
tutorial.x = 2048 / 2;
tutorial.y = 2732 / 2;
game.addChild(tutorial);
// Override tutorial end to return to main menu
tutorial.endTutorial = function () {
tutorial.isActive = false;
tutorial.destroy();
// Mark tutorial as completed and save to storage
storage.tutorialCompleted = true;
// Return to main menu after tutorial completion
var mainMenu = new MainMenu();
game.addChild(mainMenu);
};
tutorial.startTutorial();
};
self.startGame = function () {
// Apply difficulty settings
gameSettings.difficulty = self.selectedDifficulty;
gameSettings.selectedDifficulty = self.selectedDifficulty;
// Update totalWaves based on selected difficulty
updateTotalWaves();
// Apply empire starting gold bonuses
if (gameSettings.selectedEmpire === 'Ottoman' || gameSettings.selectedEmpire === 'England') {
setGold(100);
} else {
setGold(80);
}
// Enable game interactions
gameStartAllowed = true;
// Restore visibility of hidden elements when game starts
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update tower cost displays for new difficulty
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].updateCostLabels();
}
// Exit menu when empire is selected
self.destroy();
};
return self;
});
var MusicControl = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 200;
buttonBackground.height = 70;
buttonBackground.tint = 0xFF8800;
var musicText = new Text2("Music1", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
musicText.anchor.set(0.5, 0.5);
self.addChild(musicText);
var currentMusicIndex = 1;
self.updateDisplay = function () {
var musicName = currentMusicIndex === 8 ? "Nomusic" : "Music" + currentMusicIndex;
musicText.setText(musicName);
// Update button color based on music selection
switch (currentMusicIndex) {
case 1:
buttonBackground.tint = 0xFF8800; // Orange for Music1
break;
case 2:
buttonBackground.tint = 0x8800FF; // Purple for Music2
break;
case 3:
buttonBackground.tint = 0x00FF88; // Green for Music3
break;
case 4:
buttonBackground.tint = 0xFF0088; // Pink for Music4
break;
case 5:
buttonBackground.tint = 0x0088FF; // Blue for Music5
break;
case 6:
buttonBackground.tint = 0x006400; // Dark green for Music6
break;
case 7:
buttonBackground.tint = 0x800080; // Purple for Music7
break;
case 8:
buttonBackground.tint = 0x444444; // Gray for Nomusic
break;
}
};
self.down = function () {
// Block music control if difficulty not selected
if (!gameStartAllowed) {
return;
}
// Cycle through music options 1-8 (including Nomusic at position 8)
currentMusicIndex = currentMusicIndex >= 8 ? 1 : currentMusicIndex + 1;
self.updateDisplay();
// Play the corresponding music track or stop music for Nomusic
if (currentMusicIndex === 8) {
LK.stopMusic();
} else {
var musicTrack = "warmusic" + (currentMusicIndex === 1 ? "" : currentMusicIndex);
LK.playMusic(musicTrack);
}
};
// Initialize display
self.updateDisplay();
return self;
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
// Block next wave if difficulty not selected
if (!gameStartAllowed) {
return;
}
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
var notification = game.addChild(new Notification("Wave " + currentWave + " activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'default';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset(towerSet === 'futuristic' ? 'future' : towerSet === 'trench' ? 'builds' : towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'Farmland' : 'builds' : 'tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var displayNameShadow = self.towerType === 'default' ? towerSet === 'futuristic' ? 'Robot' : 'Infantry' : self.towerType === 'rapid' ? towerSet === 'futuristic' ? 'Lazerer' : 'Catapult' : self.towerType === 'splash' ? towerSet === 'futuristic' ? 'Fwacha' : 'Hwacha' : self.towerType === 'sniper' ? towerSet === 'futuristic' ? 'Eaglebot' : 'Archer' : self.towerType === 'slow' ? towerSet === 'futuristic' ? 'f-Bullet' : towerSet === 'builds' ? 'Upgrader' : 'Bullet' : self.towerType === 'poison' ? towerSet === 'futuristic' ? 'Hospital' : 'Healer' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabelShadow = new Text2(displayNameShadow, {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -20 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var displayName = self.towerType === 'default' ? towerSet === 'futuristic' ? 'Robot' : towerSet === 'trench' ? 'Blocker' : towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'House' : 'Build1' : 'Infantry' : self.towerType === 'rapid' ? towerSet === 'futuristic' ? 'Lazerer' : towerSet === 'trench' ? 'Trapper' : towerSet === 'builds' ? 'Farm' : 'Catapult' : self.towerType === 'splash' ? towerSet === 'futuristic' ? 'Fwacha' : towerSet === 'builds' ? 'Research' : 'Hwacha' : self.towerType === 'sniper' ? towerSet === 'futuristic' ? 'Eaglebot' : towerSet === 'trench' ? 'Money' : towerSet === 'builds' ? 'Miner' : 'Archer' : self.towerType === 'slow' ? towerSet === 'futuristic' ? 'f-Bullet' : towerSet === 'builds' ? 'Upgrader' : 'Bullet' : self.towerType === 'poison' ? towerSet === 'futuristic' ? 'Hospital' : towerSet === 'builds' ? 'Smith' : 'Healer' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabel = new Text2(displayName, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -20; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var displayCost = towerCost;
var towerOreCost = getTowerOreCost(self.towerType);
if (towerOreCost > 0) {
displayCost = towerCost + "G/" + towerOreCost + "O";
}
var costLabel = new Text2(displayCost, {
size: 50,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 20 + 12;
self.addChild(costLabel);
self.updateCostLabels = function () {
var newCost = getTowerCost(self.towerType);
var towerOreCost = getTowerOreCost(self.towerType);
var displayCost = newCost;
if (towerOreCost > 0) {
displayCost = newCost + "G/" + towerOreCost + "O";
}
costLabel.setText(displayCost);
costLabelShadow.setText(displayCost);
};
self.update = function () {
// Check if player can afford this tower
var towerOreCost = getTowerOreCost(self.towerType);
var canAfford = gold >= getTowerCost(self.towerType) && ore >= towerOreCost;
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var SpeedControl = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 200;
buttonBackground.height = 70;
buttonBackground.tint = 0x0088FF;
var speedText = new Text2("Normal", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
speedText.anchor.set(0.5, 0.5);
self.addChild(speedText);
self.updateDisplay = function () {
var speedName = gameSpeedOptions[currentSpeedIndex];
speedText.setText(speedName.charAt(0).toUpperCase() + speedName.slice(1));
// Update game speed based on selection
switch (speedName) {
case 'slow':
gameSpeed = 0.5;
buttonBackground.tint = 0xFF8800; // Orange for slow
break;
case 'normal':
gameSpeed = 1.0;
buttonBackground.tint = 0x0088FF; // Blue for normal
break;
case 'fast':
gameSpeed = 2.0;
buttonBackground.tint = 0x00FF00; // Green for fast
break;
case 'ultra':
gameSpeed = 2.5; // 25% faster than fast
buttonBackground.tint = 0xFF0000; // Red for ultra
break;
case 'paused':
gameSpeed = 0.0; // Complete pause
buttonBackground.tint = 0x666666; // Gray for paused
break;
}
};
self.down = function () {
// Block speed control if difficulty not selected
if (!gameStartAllowed) {
return;
}
// Cycle through speed options
currentSpeedIndex = (currentSpeedIndex + 1) % gameSpeedOptions.length;
self.updateDisplay();
};
// Initialize display
self.updateDisplay();
return self;
});
var StartMenu = Container.expand(function () {
var self = Container.call(this);
// Center the menu container
self.x = 2048 / 2;
self.y = 2732 / 2;
// Background overlay
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 2732;
menuBackground.tint = 0x000000;
menuBackground.alpha = 0.8;
// Game title
var titleText = new Text2("HONORBOUND", {
size: 150,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -250;
self.addChild(titleText);
// Subtitle
var subtitleText = new Text2("Rise on the Fifth Dawn", {
size: 80,
fill: 0xCCCCCC,
weight: 400
});
subtitleText.anchor.set(0.5, 0.5);
subtitleText.y = -150;
self.addChild(subtitleText);
// Play button
var playButton = new Container();
var playButtonBG = playButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
playButtonBG.width = 400;
playButtonBG.height = 120;
playButtonBG.tint = 0x00AA00; // Green color
var playText = new Text2("PLAY", {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
playText.anchor.set(0.5, 0.5);
playButton.addChild(playText);
playButton.y = 50;
playButton.down = function () {
LK.getSound('Click').play();
// Remove start menu and show main menu (difficulty selection)
self.destroy();
var mainMenu = new MainMenu();
game.addChild(mainMenu);
};
self.addChild(playButton);
// Story Mode button
var storyButton = new Container();
var storyButtonBG = storyButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
storyButtonBG.width = 400;
storyButtonBG.height = 120;
storyButtonBG.tint = 0x1976D2; // Blue color for Story Mode
var storyText = new Text2("Story Mode", {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
storyText.anchor.set(0.5, 0.5);
storyButton.addChild(storyText);
storyButton.y = playButton.y + 160; // Place below Play button
// Tutorial button
var tutorialButton = new Container();
var tutorialButtonBG = tutorialButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
tutorialButtonBG.width = 400;
tutorialButtonBG.height = 120;
tutorialButtonBG.tint = 0x4CAF50; // Green color for Tutorial
var tutorialText = new Text2("Tutorial", {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
tutorialText.anchor.set(0.5, 0.5);
tutorialButton.addChild(tutorialText);
tutorialButton.y = storyButton.y + 160; // Place below Story Mode button
tutorialButton.down = function () {
LK.getSound('Click').play();
// Check if tutorial has already been completed
if (storage.tutorialCompleted) {
// Show notification that tutorial was already completed and start normal game
var notification = game.addChild(new Notification("Tutorial already completed! Starting normal game."));
notification.x = 2048 / 2;
notification.y = 2732 / 2;
// Remove start menu and start normal game
self.destroy();
gameSettings.difficulty = 'normal';
gameSettings.selectedEmpire = 'Normal';
updateTotalWaves();
setGold(80);
gameStartAllowed = true;
// Show UI elements
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update tower cost displays
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].updateCostLabels();
}
return;
}
// Remove start menu and go directly to tutorial with normal difficulty and Normal empire
self.destroy();
// Set up tutorial with default settings
gameSettings.difficulty = 'normal';
gameSettings.selectedEmpire = 'Normal';
updateTotalWaves();
setGold(80);
// Show UI elements but keep interactions disabled initially
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update tower cost displays
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].updateCostLabels();
}
// Create and show tutorial
var tutorial = new Tutorial('normal');
tutorial.x = 2048 / 2;
tutorial.y = 2732 / 2;
game.addChild(tutorial);
// Override tutorial end to return to main menu
tutorial.endTutorial = function () {
tutorial.isActive = false;
tutorial.destroy();
// Mark tutorial as completed and save to storage
storage.tutorialCompleted = true;
// Return to main menu after tutorial completion
var mainMenu = new MainMenu();
game.addChild(mainMenu);
};
tutorial.startTutorial();
};
self.addChild(tutorialButton);
storyButton.down = function () {
LK.getSound('Click').play();
// Remove start menu
self.destroy();
// --- STORY MODE SEQUENCE ---
// Block all input during story mode
gameStartAllowed = false;
// Set a flag for story mode
gameSettings.storyMode = true;
// --- Play music7 for story mode ---
LK.playMusic('warmusic7');
// 1. Black overlay (fade in)
var blackOverlay = new Container();
var blackBG = blackOverlay.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
blackBG.width = 2048;
blackBG.height = 2732;
blackBG.tint = 0x000000;
blackBG.alpha = 1;
blackOverlay.x = 2048 / 2;
blackOverlay.y = 2732 / 2;
blackOverlay.alpha = 0;
game.addChild(blackOverlay);
// Fade in black overlay over 1s
tween(blackOverlay, {
alpha: 1
}, {
duration: 1000,
easing: tween.linear,
onFinish: function onFinish() {
// 2. Show story asset (fade in)
var storyAsset = new Container();
var storyBG = storyAsset.attachAsset('story', {
anchorX: 0.5,
anchorY: 0.5
});
storyBG.width = 2048;
storyBG.height = 2732;
storyBG.alpha = 1;
storyAsset.x = 2048 / 2;
storyAsset.y = 2732 / 2;
storyAsset.alpha = 0;
game.addChild(storyAsset);
tween(storyAsset, {
alpha: 1
}, {
duration: 1000,
easing: tween.linear
});
// 3. After 1s, show first text (fade in)
LK.setTimeout(function () {
var text1 = new Text2("Everyone is dead, you are\nthe only one left.", {
size: 90,
fill: 0xFFFFFF,
weight: 800
});
text1.anchor.set(0.5, 0.5);
text1.x = 2048 / 2;
text1.y = 2732 / 2 - 200;
text1.alpha = 0;
game.addChild(text1);
tween(text1, {
alpha: 1
}, {
duration: 600,
easing: tween.linear
});
// After 3s, fade out text1
LK.setTimeout(function () {
tween(text1, {
alpha: 0
}, {
duration: 1000,
easing: tween.linear,
onFinish: function onFinish() {
text1.destroy();
}
});
}, 3000);
// 4. After 4s, show second text (fade in)
LK.setTimeout(function () {
var text2 = new Text2("The enemies that are coming\nnow are advancing towards\nthe capital.", {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
text2.anchor.set(0.5, 0.5);
text2.x = 2048 / 2;
text2.y = 2732 / 2 - 60;
text2.alpha = 0;
game.addChild(text2);
tween(text2, {
alpha: 1
}, {
duration: 600,
easing: tween.linear
});
// After 5s, fade out text2
LK.setTimeout(function () {
tween(text2, {
alpha: 0
}, {
duration: 1000,
easing: tween.linear,
onFinish: function onFinish() {
text2.destroy();
}
});
}, 5000);
// 5. After 5s (total 9s), show third text (fade in)
LK.setTimeout(function () {
var text3 = new Text2("You are the chosen one,\nI have complete confidence\nthat you will save our\nempire. (From Emperor)", {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
text3.anchor.set(0.5, 0.5);
text3.x = 2048 / 2;
text3.y = 2732 / 2 + 80;
text3.alpha = 0;
game.addChild(text3);
tween(text3, {
alpha: 1
}, {
duration: 600,
easing: tween.linear
});
// After 5s, fade out text3 and story asset, then start game
LK.setTimeout(function () {
tween(text3, {
alpha: 0
}, {
duration: 1000,
easing: tween.linear,
onFinish: function onFinish() {
text3.destroy();
}
});
// Fade out story asset and black overlay
tween(storyAsset, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
storyAsset.destroy();
}
});
tween(blackOverlay, {
alpha: 0
}, {
duration: 1200,
easing: tween.linear,
onFinish: function onFinish() {
blackOverlay.destroy();
// --- Start game in normal mode, Normal empire ---
gameSettings.difficulty = 'normal';
gameSettings.selectedEmpire = 'Normal';
updateTotalWaves();
setGold(80);
// Enable game interactions
gameStartAllowed = true;
// Restore visibility of hidden elements when story mode game starts
if (typeof medievalButton !== "undefined") medievalButton.visible = true;
if (typeof trenchButton !== "undefined") trenchButton.visible = true;
if (typeof futuristicButton !== "undefined") futuristicButton.visible = true;
if (typeof goldText !== "undefined") goldText.visible = true;
if (typeof livesText !== "undefined") livesText.visible = true;
if (typeof musicControl !== "undefined") musicControl.visible = true;
if (typeof speedControl !== "undefined") speedControl.visible = true;
// Update UI for new settings
updateUI();
// Remove any menus, show main game
// Remove all children except core layers and UI
for (var i = game.children.length - 1; i >= 0; i--) {
var child = game.children[i];
// Remove any menu or overlay containers
if (child instanceof MainMenu || child instanceof StartMenu) {
game.removeChild(child);
child.destroy();
}
}
// --- Switch back to music1 after story mode ends ---
LK.playMusic('warmusic');
}
});
}, 5000);
}, 5000);
}, 4000);
}, 1000);
}
});
};
self.addChild(storyButton);
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.towerSet = towerSet;
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Tower health system
self.maxHealth = 100;
// Apply French empire bonus: 20% more tower durability
if (gameSettings.selectedEmpire === 'French') {
self.maxHealth = Math.floor(self.maxHealth * 1.2); // 20% more health
}
// Apply Byzantine empire bonus: 100% more tower durability
if (gameSettings.selectedEmpire === 'Byzantine') {
self.maxHealth = Math.floor(self.maxHealth * 2.0); // 100% more health
}
self.health = self.maxHealth;
// Special trench tower properties
self.isBlocker = false;
self.isTrapper = false;
self.isMoney = false;
self.goldTimer = 0;
self.goldInterval = 300; // 5 seconds at 60 FPS
// Initialize goldTimer for Farm functionality
if (self.towerSet === 'builds' && self.id === 'rapid') {
self.goldTimer = 0;
}
// Set trench tower properties based on ID
if (self.towerSet === 'trench') {
if (self.id === 'default') {
self.isBlocker = true;
} else if (self.id === 'rapid') {
self.isTrapper = true;
} else if (self.id === 'sniper') {
self.isMoney = true;
}
}
// Standardized method to get the current range of the tower
self.getRange = function () {
// Trench towers: only Trapper has range, Blocker and Money have no range
if (self.towerSet === 'trench') {
if (self.isTrapper) {
// Trapper: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
} else {
// Blocker and Money towers have no range
return 0;
}
}
// Always calculate range based on tower type and level
switch (self.id) {
case 'sniper':
// Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost
var baseRange;
if (self.level === self.maxLevel) {
baseRange = 12 * CELL_SIZE; // Significantly increased range for max level
} else {
baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE;
}
// Apply Scotch empire bonus: 50% more archer range
if (gameSettings.selectedEmpire === 'Scotch') {
baseRange = baseRange * 1.5;
}
return baseRange;
case 'splash':
// Splash: base 4, +0.4 per level (max ~8 blocks at max level)
return (4 + (self.level - 1) * 0.4) * CELL_SIZE;
case 'rapid':
// Rapid: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// f-Bullet: 2x range when futuristic unlocked
if (futuristicUnlocked) {
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE * 2; // 2x normal range
}
// Slow: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Hospital: 2x range when futuristic unlocked
if (futuristicUnlocked) {
var baseRange = (3.2 + (self.level - 1) * 0.5) * CELL_SIZE * 2; // 2x normal range
// Apply Kalmar Union empire bonus: 100% more healer range
if (gameSettings.selectedEmpire === 'Kalmar Union') {
baseRange = baseRange * 2.0;
}
return baseRange;
}
// Poison: base 3.2, +0.5 per level
var baseRange = (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
// Apply Kalmar Union empire bonus: 100% more healer range
if (gameSettings.selectedEmpire === 'Kalmar Union') {
baseRange = baseRange * 2.0;
}
return baseRange;
default:
// Default: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
// Check if futuristic has been unlocked and apply 3x fire rate boost
var fireRateMultiplier = futuristicUnlocked ? 1 / 3 : 1;
switch (self.id) {
case 'rapid':
self.fireRate = 30 * fireRateMultiplier;
self.damage = 5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90 * fireRateMultiplier;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 52 * fireRateMultiplier; // 30% faster than 75 (75 * 0.7 = 52.5, rounded to 52)
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50 * fireRateMultiplier;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70 * fireRateMultiplier;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
var baseGraphics = self.attachAsset(towerSet === 'futuristic' ? 'future' : towerSet === 'trench' ? 'builds' : towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'Farmland' : 'builds' : 'tower', {
anchorX: 0.5,
anchorY: 0.5
});
switch (self.id) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
// Add tower health bar
var towerHealthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var towerHealthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var towerHealthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
towerHealthBarBG.y = towerHealthBarOutline.y = towerHealthBar.y = baseGraphics.height / 2 + 15;
towerHealthBarOutline.x = -towerHealthBarOutline.width / 2;
towerHealthBarBG.x = towerHealthBar.x = -towerHealthBar.width / 2 - .5;
towerHealthBar.tint = 0x00ff00;
towerHealthBarBG.tint = 0xff0000;
self.towerHealthBar = towerHealthBar;
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
// Don't add arrow/gun graphics for trench towers
if (self.towerSet !== 'trench') {
var gunGraphics = gunContainer.attachAsset(towerSet === 'futuristic' ? 'farrow' : 'defense', {
anchorX: 0.5,
anchorY: 0.5
});
}
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// Make last upgrade level extra expensive
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// Increase tower health by 50% on upgrade
self.maxHealth = Math.floor(self.maxHealth * 1.5);
self.health = self.maxHealth; // Restore to full health on upgrade
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
var fireRateMultiplier = futuristicUnlocked ? 1 / 3 : 1;
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 30 - self.level * 9) * fireRateMultiplier; // double the effect
self.damage = 5 + self.level * 10; // double the effect
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(15, 30 - self.level * 3) * fireRateMultiplier; // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24) * fireRateMultiplier; // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8) * fireRateMultiplier;
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = 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);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Artillery towers can target all enemy types
if (self.id === 'splash') {
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
} else {
// Other towers can target both flying and ground enemies
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
}
}
}
if (!closestEnemy) {
self.targetEnemy = null;
}
return closestEnemy;
};
self.update = function () {
// Money tower gold generation
if (self.isMoney) {
// Only generate gold if game has started and is not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
self.goldTimer += gameSpeed;
if (self.goldTimer >= self.goldInterval) {
self.goldTimer = 0;
setGold(gold + 10);
var goldIndicator = new GoldIndicator(10, self.x, self.y);
game.addChild(goldIndicator);
}
}
}
// Miner functionality for sniper tower in builds set (ultra mode)
if (self.towerSet === 'builds' && self.id === 'sniper' && gameSettings.difficulty === 'ultra') {
// Only generate ore if game has started and is not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
self.goldTimer += gameSpeed;
if (self.goldTimer >= 600) {
// 10 seconds at 60 FPS
self.goldTimer = 0;
ore += 1;
var oreIndicator = new GoldIndicator("1 Ore", self.x, self.y);
game.addChild(oreIndicator);
updateUI();
}
}
}
// Farm functionality for rapid tower in builds set
if (self.towerSet === 'builds' && self.id === 'rapid') {
// Only generate gold if game has started and is not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
self.goldTimer += gameSpeed;
if (self.goldTimer >= 600) {
// 10 seconds at 60 FPS
self.goldTimer = 0;
setGold(gold + 2);
var goldIndicator = new GoldIndicator(2, self.x, self.y);
game.addChild(goldIndicator);
}
}
}
// Upgrader functionality for builds set (Build5/Upgrader)
if (self.towerSet === 'builds' && self.id === 'slow') {
// Set range to match Archer (sniper) tower
self.getRange = function () {
// Archer base: 5 + (level-1)*0.8, but use maxLevel for Upgrader
var baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE;
return baseRange;
};
// --- NEW: Instantly increase level by 1 for all towers in range for free ---
if (!self._upgraderBuffedTowers) self._upgraderBuffedTowers = [];
// Only activate if game started and not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
// Find all towers in range that haven't been buffed by this Upgrader
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
if (t !== self && self.isInRange(t)) {
if (!t._upgradedByUpgrader) t._upgradedByUpgrader = [];
if (t._upgradedByUpgrader.indexOf(self) === -1) {
// Only buff if not at max level
if (t.level < t.maxLevel) {
t.level += 1;
t.updateLevelIndicators && t.updateLevelIndicators();
t._upgradedByUpgrader.push(self);
self._upgraderBuffedTowers.push(t);
}
}
}
}
}
}
// Smith (Build6) functionality for builds set
if (self.towerSet === 'builds' && self.id === 'poison') {
// Set range to match Archer (sniper) tower
self.getRange = function () {
var baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE;
return baseRange;
};
// --- NEW: Double maxHealth of towers in range, fully heal every 5s, remove on destroy ---
if (!self._smithBuffedTowers) self._smithBuffedTowers = [];
// Only activate if game started and not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
// Double maxHealth of towers in range (only once per tower)
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
if (t !== self && self.isInRange(t)) {
if (!t._smithBuffedBy) t._smithBuffedBy = [];
if (t._smithBuffedBy.indexOf(self) === -1) {
t.maxHealth = t.maxHealth * 2;
t.health = t.maxHealth;
t._smithBuffedBy.push(self);
self._smithBuffedTowers.push(t);
if (t.towerHealthBar) t.towerHealthBar.width = 70;
}
}
}
// Every 5 seconds, fully heal towers in range
if (typeof self.smithHealTimer === "undefined") self.smithHealTimer = 0;
self.smithHealTimer += gameSpeed;
if (self.smithHealTimer >= 300) {
// 5 seconds at 60 FPS
self.smithHealTimer = 0;
var healed = false;
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
if (t !== self && self.isInRange(t)) {
if (t.health < t.maxHealth) {
t.health = t.maxHealth;
if (t.towerHealthBar) t.towerHealthBar.width = 70;
healed = true;
}
}
}
if (healed) {
var note = game.addChild(new Notification("Smith: Towers in range fully healed!"));
note.x = 2048 / 2;
note.y = grid.height - 60;
}
}
}
}
// Research functionality - unlocks futuristic towers when built
if (self.towerSet === 'builds' && self.id === 'splash') {
// This is Research - it unlocks futuristic towers immediately when built
if (!futuristicUnlocked) {
futuristicUnlocked = true;
var notification = game.addChild(new Notification("Futuristic towers unlocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Update all existing towers to benefit from futuristic unlock
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Apply 3x fire rate boost to all existing towers
tower.fireRate = tower.fireRate / 3;
// Refresh range for slow and poison towers (now cover entire map)
if (tower.id === 'slow' || tower.id === 'poison') {
tower.refreshCellsInRange();
}
}
}
}
// House functionality for builds in ultra mode
if (self.towerSet === 'builds' && self.id === 'default' && gameSettings.difficulty === 'ultra') {
// This is a house - it provides 3 workers when built
// Workers are added when house is placed, not over time
}
// Trapper tower enemy detection and damage
if (self.isTrapper) {
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 <= CELL_SIZE && !enemy.trappedBy) {
enemy.trappedBy = self;
// Damage all enemies in range when triggered
for (var j = 0; j < enemies.length; j++) {
var rangeEnemy = enemies[j];
var rangeDx = rangeEnemy.x - self.x;
var rangeDy = rangeEnemy.y - self.y;
var rangeDistance = Math.sqrt(rangeDx * rangeDx + rangeDy * rangeDy);
if (rangeDistance <= self.getRange()) {
var damage = 0;
if (rangeEnemy.isImmune) {
// Immune enemies take full health damage
damage = rangeEnemy.health;
} else {
// Regular enemies take 25% damage
damage = Math.ceil(rangeEnemy.health * 0.25);
}
rangeEnemy.health -= damage;
if (rangeEnemy.health <= 0) {
rangeEnemy.health = 0;
} else {
rangeEnemy.healthBar.width = rangeEnemy.health / rangeEnemy.maxHealth * 70;
}
}
}
// Destroy trapper after use
self.health = 0;
break;
}
}
}
// Update tower health bar
if (self.health <= 0) {
// Healer towers are immune to destruction
if (self.id === 'poison') {
self.health = self.maxHealth; // Reset to full health
self.towerHealthBar.width = 70; // Full health bar
} else {
self.health = 0;
self.towerHealthBar.width = 0;
// Destroy tower when health reaches zero
// Remove from cells in range
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
// Reset grid cells to floor (if not castle walls)
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (cell && cell.type !== 4) {
cell.type = 0;
}
}
}
// Remove Upgrader/Smith effects from towers in range if this is Upgrader or Smith
if (self.towerSet === 'builds' && (self.id === 'slow' || self.id === 'poison')) {
for (var i = 0; i < towers.length; i++) {
var t = towers[i];
if (t !== self) {
// If Upgrader: revert level if it was increased by this Upgrader
if (self.id === 'slow' && t._upgradedByUpgrader && t._upgradedByUpgrader.indexOf(self) !== -1) {
if (t.level > 1) {
t.level -= 1;
t.updateLevelIndicators && t.updateLevelIndicators();
}
// Remove this Upgrader from the list
var idx = t._upgradedByUpgrader.indexOf(self);
if (idx !== -1) t._upgradedByUpgrader.splice(idx, 1);
}
// If Smith: revert maxHealth and health if it was buffed by this Smith
if (self.id === 'poison' && t._smithBuffedBy && t._smithBuffedBy.indexOf(self) !== -1) {
t.maxHealth = t.maxHealth / 2;
if (t.health > t.maxHealth) t.health = t.maxHealth;
if (t.towerHealthBar) t.towerHealthBar.width = t.health / t.maxHealth * 70;
// Remove this Smith from the buffedBy list
var sidx = t._smithBuffedBy.indexOf(self);
if (sidx !== -1) t._smithBuffedBy.splice(sidx, 1);
}
}
}
// Clean up Upgrader/Smith's buffed towers list
if (self._upgraderBuffedTowers) self._upgraderBuffedTowers = [];
if (self._smithBuffedTowers) self._smithBuffedTowers = [];
}
// Check if this is Research being destroyed - lock futuristic towers and remove existing ones
if (self.towerSet === 'builds' && self.id === 'splash') {
// This is Research being destroyed - lock futuristic towers
futuristicUnlocked = false;
// Remove all existing futuristic towers and refund their cost
for (var i = towers.length - 1; i >= 0; i--) {
var tower = towers[i];
if (tower.towerSet === 'futuristic') {
// Calculate refund value
var totalInvestment = tower.getTotalValue ? tower.getTotalValue() : 0;
var refundValue = getTowerSellValue(totalInvestment);
setGold(gold + refundValue);
// Remove from cells in range
for (var j = 0; j < tower.cellsInRange.length; j++) {
var cell = tower.cellsInRange[j];
var towerIndex = cell.towersInRange.indexOf(tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
// Reset grid cells to floor (if not castle walls)
for (var gx = 0; gx < 2; gx++) {
for (var gy = 0; gy < 2; gy++) {
var cell = grid.getCell(tower.gridX + gx, tower.gridY + gy);
if (cell && cell.type !== 4) {
cell.type = 0;
}
}
}
// Clear selected tower if this was selected
if (selectedTower === tower) {
selectedTower = null;
}
// Remove from towers array
var towerArrayIndex = towers.indexOf(tower);
if (towerArrayIndex !== -1) {
towers.splice(towerArrayIndex, 1);
}
// Remove from game
tower.destroy();
}
}
// Switch back to medieval towers if currently on futuristic
if (towerSet === 'futuristic') {
towerSet = 'medieval';
updateSourceTowers();
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.5;
futuristicBG.tint = 0x222222;
futuristicBG.alpha = 0.5;
}
var notification = game.addChild(new Notification("Research destroyed! Futuristic towers locked and removed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
// Remove tower from towers array
var towerIndex = towers.indexOf(self);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
// Clear selected tower if this was selected
if (selectedTower === self) {
selectedTower = null;
}
// Remove tower from game
self.destroy();
// Update pathfinding
grid.pathFind();
grid.renderDebug();
return;
}
} else {
self.towerHealthBar.width = self.health / self.maxHealth * 70;
}
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
// Only rotate gun container if this is not a Trecher tower or Healer tower
if (self.id !== 'slow' && self.id !== 'poison') {
gunContainer.rotation = angle;
}
// Check if this tower is affected by any Trecher towers
var effectiveFireRate = self.fireRate;
if (self.id !== 'slow') {
// Trechers don't boost themselves
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.id === 'slow' && tower.isInRange(self)) {
effectiveFireRate = self.fireRate * 0.5; // 50% faster firing (half the fire rate delay)
break;
}
}
}
// Apply Austrian empire bonus: 20% faster fire rate (20% less delay)
if (gameSettings.selectedEmpire === 'Austria') {
effectiveFireRate = effectiveFireRate * 0.8; // 20% faster firing
}
if (LK.ticks - self.lastFired >= effectiveFireRate / gameSpeed) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
// Trecher towers don't fire bullets - they only provide support effects
if (self.id === 'slow') {
return;
}
// Healer towers don't fire bullets - they only provide healing/protection effects
if (self.id === 'poison') {
return;
}
// Trench towers don't fire bullets - they only provide support effects
if (self.towerSet === 'trench') {
return;
}
// Builds towers (house, Farm, build 3,4,5,6) don't fire bullets
if (self.towerSet === 'builds') {
return;
}
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bulletDamage = self.damage;
// Apply Ottoman empire bonus: 20% more damage
if (gameSettings.selectedEmpire === 'Ottoman') {
bulletDamage = Math.ceil(bulletDamage * 1.2); // 20% more damage
}
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, bulletDamage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// Customize bullet appearance based on tower type
switch (self.id) {
case 'rapid':
bullet.children[0].tint = 0x00AAFF;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
break;
case 'sniper':
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
break;
case 'splash':
bullet.children[0].tint = 0x33CC00;
bullet.children[0].width = 40;
bullet.children[0].height = 40;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// --- Fire recoil effect for gunContainer ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
// Always use the original resting position for recoil, never accumulate offset
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset to resting position before animating (in case of interrupted tweens)
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Calculate recoil offset (recoil back along the gun's rotation)
var recoilDistance = 8;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
// Animate recoil back from the resting position
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original position/scale
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// All towers block enemy movement except Trapper
if (self.isTrapper) {
// Trapper allows enemies to pass through
cell.type = 0;
} else {
// All other structures block enemy movement
cell.type = 1;
}
}
}
}
self.refreshCellsInRange();
// Completely destroy and recreate tower preview when tower is successfully placed
if (towerPreview && towerPreview.parent) {
game.removeChild(towerPreview);
towerPreview.destroy();
towerPreview = new TowerPreview();
towerPreview.visible = false;
}
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'default';
self.hasEnoughGold = true;
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3;
var previewGraphics = self.attachAsset(towerSet === 'futuristic' ? 'futurep' : towerSet === 'trench' ? 'builds' : towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'Farmland' : 'builds' : 'rare', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
var towerOreCost = getTowerOreCost(self.towerType);
self.hasEnoughGold = gold >= getTowerCost(self.towerType) && ore >= towerOreCost;
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
break;
case 'sniper':
previewGraphics.tint = 0xFF5500;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
break;
case 'slow':
previewGraphics.tint = 0x9900FF;
break;
case 'poison':
previewGraphics.tint = 0x00FFAA;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
// Allow placement only on regular floor (type 0)
if (!cell || cell.type !== 0) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
self.canPlace = validGridPlacement && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var Tutorial = Container.expand(function (difficulty) {
var self = Container.call(this);
self.difficulty = difficulty;
self.currentStep = 0;
self.tutorialSteps = [];
self.isActive = false;
// Tutorial overlay background
var tutorialOverlay = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
tutorialOverlay.width = 2048;
tutorialOverlay.height = 2732;
tutorialOverlay.tint = 0x000000;
tutorialOverlay.alpha = 0.7;
// Tutorial text box
var textBox = new Container();
var textBG = textBox.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
textBG.width = 1600;
textBG.height = 400;
textBG.alpha = 0.95;
var tutorialText = new Text2("", {
size: 60,
fill: 0xFFFFFF,
weight: 600
});
tutorialText.anchor.set(0.5, 0.5);
textBox.addChild(tutorialText);
textBox.x = 0;
textBox.y = -200;
self.addChild(textBox);
// Next button
var nextButton = new Container();
var nextBG = nextButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
nextBG.width = 300;
nextBG.height = 100;
nextBG.tint = 0x00AA00;
var nextText = new Text2("Next", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
nextText.anchor.set(0.5, 0.5);
nextButton.addChild(nextText);
nextButton.x = 0;
nextButton.y = 150;
nextButton.down = function () {
self.nextStep();
};
self.addChild(nextButton);
// Define tutorial steps based on difficulty
if (difficulty === 'easy') {
self.tutorialSteps = [{
text: "Welcome to HONORBOUND!\nThis is Easy Mode - perfect for learning.\nTowers cost 50% less and enemies are easier.",
indicator: {
x: 0,
y: 0,
visible: false
}
}, {
text: "This is your gold (top right).\nYou spend gold to build towers.\nYou earn gold by defeating enemies.",
indicator: {
x: 1500,
y: -1200,
visible: true
}
}, {
text: "These are your lives (top left).\nYou lose lives when enemies reach the bottom.\nDon't let them reach zero!",
indicator: {
x: -1500,
y: -1200,
visible: true
}
}, {
text: "These are tower types you can build.\nDrag them onto the battlefield to place towers.\nEach has different abilities and costs.",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Advanced Technology: Futuristic towers unlock\nat wave 4. They have\n3x faster fire rate and\nspecial abilities. Robot, Lazerer,\nEaglebot, Fwacha, f-Bullet, Hospital.",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Click 'Start Game' to begin the first wave.\nEnemies will spawn from the top and try to reach the bottom.",
indicator: {
x: 0,
y: 800,
visible: true
}
}, {
text: "Good luck, Commander!\nBuild towers to stop the enemies.\nUpgrade towers by clicking on them.",
indicator: {
x: 0,
y: 0,
visible: false
}
}];
} else {
self.tutorialSteps = [{
text: "Welcome to HONORBOUND!\nYou are the last defender of the empire.\nStop the advancing enemies at all costs!",
indicator: {
x: 0,
y: 0,
visible: false
}
}, {
text: "Gold (top right) is your main resource.\nSpend it wisely on towers and upgrades.\nEarn more by defeating enemies.",
indicator: {
x: 1500,
y: -1200,
visible: true
}
}, {
text: "Lives (top left) represent your defenses.\nEach enemy that escapes costs 1 life.\nBoss enemies cost more lives!",
indicator: {
x: -1500,
y: -1200,
visible: true
}
}, {
text: "Tower Selection: Drag towers to build them.\nInfantry: Balanced β’ Catapult: Fast\nArcher: Long range β’ Hwacha: Area damage",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Advanced towers: Bullet (slows enemies)\nHealer (protects nearby towers)\nClick built towers to upgrade them.",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Speed Control (top center): Adjust game speed.\nMusic Control: Change background music.\nUse these to manage your strategy.",
indicator: {
x: 0,
y: -1200,
visible: true
}
}, {
text: "Futuristic Technology: At wave 4,\nunlock advanced towers! Robot, Lazerer,\nEaglebot, Fwacha, f-Bullet, Hospital.\nThey fire 3x faster\nwith special abilities.",
indicator: {
x: 0,
y: 1200,
visible: true
}
}, {
text: "Wave System: Enemies come in waves.\nEach wave gets stronger.\nPrepare defenses between waves!",
indicator: {
x: 0,
y: 800,
visible: true
}
}, {
text: "Strategic Tips:\nβ’ Block enemy paths with towers\nβ’ Upgrade key towers\nβ’ Use terrain to your advantage",
indicator: {
x: 0,
y: 0,
visible: false
}
}, {
text: "The fate of the empire rests with you!\nClick 'Start Game' when ready.\nMay victory be yours, Commander!",
indicator: {
x: 0,
y: 800,
visible: true
}
}];
}
self.startTutorial = function () {
self.isActive = true;
self.currentStep = 0;
self.showCurrentStep();
};
self.showCurrentStep = function () {
if (self.currentStep >= self.tutorialSteps.length) {
self.endTutorial();
return;
}
var step = self.tutorialSteps[self.currentStep];
tutorialText.setText(step.text);
// Remove existing indicators
for (var i = self.children.length - 1; i >= 0; i--) {
if (self.children[i].isTutorialIndicator) {
self.removeChild(self.children[i]);
}
}
// Add indicators based on tutorial step
if (self.difficulty === 'easy') {
if (self.currentStep === 1) {
// Gold instruction
var goldIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
goldIndicator.x = 500;
goldIndicator.y = -1150;
goldIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 2) {
// Lives instruction
var livesIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
livesIndicator.x = -500;
livesIndicator.y = -1150;
livesIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 4) {
// Futuristic towers instruction
var futuristicIndicator = self.attachAsset('isaret2', {
anchorX: 0.5,
anchorY: 0.5
});
futuristicIndicator.x = 0;
futuristicIndicator.y = 1050;
futuristicIndicator.isTutorialIndicator = true;
}
} else {
// Normal difficulty
if (self.currentStep === 1) {
// Gold instruction
var goldIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
goldIndicator.x = 500;
goldIndicator.y = -1150;
goldIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 2) {
// Lives instruction
var livesIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
livesIndicator.x = -500;
livesIndicator.y = -1150;
livesIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 5) {
// Speed Control and Music instruction
var speedIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
speedIndicator.x = -100;
speedIndicator.y = -1150;
speedIndicator.isTutorialIndicator = true;
var musicIndicator = self.attachAsset('isaret', {
anchorX: 0.5,
anchorY: 0.5
});
musicIndicator.x = 100;
musicIndicator.y = -1150;
musicIndicator.isTutorialIndicator = true;
} else if (self.currentStep === 6) {
// Futuristic towers instruction
var futuristicIndicator = self.attachAsset('isaret2', {
anchorX: 0.5,
anchorY: 0.5
});
futuristicIndicator.x = 0;
futuristicIndicator.y = 1050;
futuristicIndicator.isTutorialIndicator = true;
}
}
// Update next button text
if (self.currentStep === self.tutorialSteps.length - 1) {
nextText.setText("Start!");
} else {
nextText.setText("Next");
}
};
self.nextStep = function () {
self.currentStep++;
if (self.currentStep >= self.tutorialSteps.length) {
self.endTutorial();
} else {
self.showCurrentStep();
}
};
self.endTutorial = function () {
self.isActive = false;
// Mark tutorial as completed and save to storage
storage.tutorialCompleted = true;
// Remove any tutorial indicators
for (var i = self.children.length - 1; i >= 0; i--) {
if (self.children[i].isTutorialIndicator) {
self.removeChild(self.children[i]);
}
}
self.destroy();
// Return to main menu after tutorial completion
var mainMenu = new MainMenu();
game.addChild(mainMenu);
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('menu', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.alpha = 0.9;
var towerDisplayName = self.tower.id === 'default' ? self.tower.towerSet === 'futuristic' ? 'Robot' : self.tower.towerSet === 'trench' ? 'Blocker' : self.tower.towerSet === 'builds' ? gameSettings.difficulty === 'ultra' ? 'House' : 'Build1' : 'Infantry' : self.tower.id === 'rapid' ? self.tower.towerSet === 'futuristic' ? 'Lazerer' : self.tower.towerSet === 'trench' ? 'Trapper' : self.tower.towerSet === 'builds' ? 'Farm' : 'Catapult' : self.tower.id === 'splash' ? self.tower.towerSet === 'futuristic' ? 'Fwacha' : self.tower.towerSet === 'builds' ? 'Research' : 'Hwacha' : self.tower.id === 'sniper' ? self.tower.towerSet === 'futuristic' ? 'Eaglebot' : self.tower.towerSet === 'trench' ? 'Money' : self.tower.towerSet === 'builds' ? 'Miner' : 'Archer' : self.tower.id === 'slow' ? self.tower.towerSet === 'futuristic' ? 'f-Bullet' : self.tower.towerSet === 'builds' ? 'Upgrader' : 'Bullet' : self.tower.id === 'poison' ? self.tower.towerSet === 'futuristic' ? 'Hospital' : self.tower.towerSet === 'builds' ? 'Smith' : 'Healer' : self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1);
var towerTypeText = new Text2(towerDisplayName + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Handle population logic for houses in ultra mode
if (gameSettings.difficulty === 'ultra' && self.tower.towerSet === 'builds' && self.tower.id === 'default') {
// This was a house - reduce population by 1000
population = Math.max(0, population - 1000);
var popNotification = game.addChild(new Notification("-1000 population"));
popNotification.x = 2048 / 2;
popNotification.y = grid.height - 100;
}
updateUI();
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// Only reset to floor if it's not a castle wall
if (cell.type !== 4) {
cell.type = 0;
}
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('hi', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
size: 50,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Game", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
// Block game start if difficulty not selected
if (!gameStartAllowed) {
return;
}
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
// Reset to medieval towers at game start
if (towerSet === 'futuristic') {
towerSet = 'medieval';
updateSourceTowers();
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.5; // Keep transparency
futuristicBG.tint = 0x222222;
futuristicBG.alpha = 0.5; // Keep transparency
}
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Start music when game begins
LK.playMusic('warmusic');
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('hi', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// All waves are normal enemy waves
var waveType = "Dawn";
var enemyType = "normal";
var enemyCount = 10;
block.tint = 0xAAAAAA;
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 4;
waveTypeShadow.y = 4;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "normal";
}
// If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
// then we should return a different boss type
var waveType = self.waveTypes[waveNumber - 1];
return waveType;
};
// Get enemy count for a specific wave number
self.getEnemyCount = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return 10;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Boss spawning disabled - no boss prefix for waves 10, 20, 30
// if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
// typeName = "BOSS";
// }
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
// Only apply red tint after game has started
if (self.gameStarted) {
if (i - 1 < currentWave) {
// Completed waves: red tint with reduced alpha
block.alpha = .5;
tween(block, {
tint: 0xFF0000
}, {
duration: 300,
easing: tween.easeOut
});
}
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
// Skip wave progression when game is paused (gameSpeed = 0)
if (gameSpeed === 0) {
return;
}
if (currentWave < totalWaves) {
waveTimer += gameSpeed;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var notification = game.addChild(new Notification("Wave " + currentWave + " incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
// Initialize gameSettings early to prevent undefined access
var gameSettings = {
difficulty: 'normal',
selectedEmpire: 'Normal'
};
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
// --- Population-based gold income for ultra mode ---
if (gameSettings.difficulty === 'ultra') {
var populationGoldTimer = 0;
game.update = function (origUpdate) {
return function () {
// Call original update
if (typeof origUpdate === "function") origUpdate.apply(this, arguments);
// Only run if game is started and not paused
if (waveIndicator && waveIndicator.gameStarted && gameSpeed > 0) {
populationGoldTimer += gameSpeed;
if (populationGoldTimer >= 1800) {
// 30 seconds at 60 FPS
populationGoldTimer = 0;
var popK = Math.floor(population / 1000);
if (popK > 0) {
var goldToAdd = popK * 3;
setGold(gold + goldToAdd);
var note = game.addChild(new Notification("+" + goldToAdd + " gold from population!"));
note.x = 2048 / 2;
note.y = grid.height - 180;
}
}
}
};
}(game.update);
}
// Update gameSettings with stored values
gameSettings.difficulty = 'normal';
gameSettings.selectedEmpire = 'Normal';
var goldMines = [];
var gold = gameSettings.selectedEmpire === 'Ottoman' ? 100 : 80;
// Leaderboard system for ultra mode
var playerScore = 0;
var playerName = "";
// Leaderboard functionality removed
var leaderboardData = [];
// Population system for ultra mode
if (gameSettings.difficulty === 'ultra') {
var population = 0; // Start with 0 population
var ultraEnemyKills = 0;
var ore = 0; // Ore resource for ultra mode only - starts at 0
} else {
var population = 0;
var ore = 0; // Initialize ore for all modes to prevent undefined
}
// Generate a new session seed on each reload/restart
var reloadCounter = 0;
reloadCounter++;
var gameSessionSeed = reloadCounter * 12345 + Math.floor(Date.now() / 1000); // Unique seed per reload
// Track if difficulty has been selected and game can start
var gameStartAllowed = false;
var lives = 10;
var currentWave = 0;
var totalWaves = gameSettings.difficulty === 'ultra' ? 30 : gameSettings.difficulty === 'hardcore' ? 10 : 5;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var gameSpeed = 1.0; // 1.0 = normal, 0.5 = slow, 2.0 = fast
var gameSpeedOptions = ['slow', 'normal', 'fast', 'ultra', 'paused'];
var currentSpeedIndex = 1; // Start with normal (index 1)
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var populationText = new Text2('Population: ' + population, {
size: 60,
fill: 0x00FFFF,
weight: 800
});
populationText.anchor.set(0.5, 0.5);
var oreText = new Text2('Ore: ' + ore, {
size: 60,
fill: 0x888888,
weight: 800
});
oreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
// Hide these elements initially in menu
goldText.visible = false;
livesText.visible = false;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(livesText);
// Only show populationText in ultra mode
if (gameSettings.difficulty === 'ultra') {
LK.gui.top.addChild(populationText);
LK.gui.top.addChild(oreText);
populationText.x = spacing;
populationText.y = topMargin + 80; // Position below gold
populationText.visible = true;
oreText.x = spacing;
oreText.y = topMargin + 160; // Position below population text
oreText.visible = true;
} else {
populationText.visible = false;
oreText.visible = false;
}
livesText.x = -spacing;
livesText.y = topMargin;
goldText.x = spacing;
goldText.y = topMargin;
function updateTotalWaves() {
totalWaves = gameSettings.difficulty === 'ultra' ? 30 : gameSettings.difficulty === 'hardcore' ? 10 : 5;
// Recreate wave indicator with correct number of waves
if (waveIndicator && waveIndicator.parent) {
waveIndicator.parent.removeChild(waveIndicator);
waveIndicator.destroy();
}
waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
}
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
if (gameSettings.difficulty === 'ultra') {
// Ensure population and ore UI elements are added to GUI if not already present
if (!populationText.parent) {
LK.gui.top.addChild(populationText);
populationText.x = spacing;
populationText.y = topMargin + 80;
}
if (!oreText.parent) {
LK.gui.top.addChild(oreText);
oreText.x = spacing;
oreText.y = topMargin + 160;
}
populationText.setText('Population: ' + population);
oreText.setText('Ore: ' + ore);
populationText.visible = true;
oreText.visible = true;
} else {
populationText.visible = false;
oreText.visible = false;
}
}
function setGold(value) {
gold = value;
updateUI();
}
function addScore(points) {
if (gameSettings.difficulty === 'ultra') {
playerScore += points;
}
}
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
// Create background tile grid (12x15)
var backgroundContainer = new Container();
var bgTileWidth = 2048 / 12; // Divide screen width by 12
var bgTileHeight = 2732 / 15; // Divide screen height by 15
for (var row = 0; row < 15; row++) {
for (var col = 0; col < 12; col++) {
var bgTile = backgroundContainer.attachAsset('background', {
anchorX: 0,
anchorY: 0,
width: bgTileWidth,
height: bgTileHeight
});
bgTile.x = col * bgTileWidth;
bgTile.y = row * bgTileHeight;
}
}
// Add background container first (behind everything else)
game.addChild(backgroundContainer);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
// Trapper towers don't block paths
if (towerPreview && towerPreview.towerType === 'rapid' && towerSet === 'trench') {
return false;
}
var cells = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cells.push({
cell: cell,
originalType: cell.type
});
cell.type = 1;
}
}
}
var blocked = grid.pathFind();
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
grid.pathFind();
grid.renderDebug();
return blocked;
}
function getTowerCost(towerType) {
var cost = 10;
switch (towerType) {
case 'rapid':
// Different costs for trench vs medieval/futuristic
if (towerSet === 'trench') {
cost = 50; // Trapper cost
} else {
cost = 30; // Original catapult cost
}
break;
case 'sniper':
// Different costs for trench vs medieval/futuristic
if (towerSet === 'trench') {
cost = 120; // Money tower cost
} else {
cost = 50; // Original archer cost
}
break;
case 'splash':
cost = 50;
break;
case 'slow':
cost = gameSettings.difficulty === 'hardcore' ? 100 : 50;
break;
case 'poison':
cost = gameSettings.difficulty === 'hardcore' ? 100 : 50;
break;
}
// Apply Easy mode discount
if (gameSettings.difficulty === 'easy') {
cost = Math.floor(cost / 2);
}
// Apply Hungary empire bonus: 5 gold less on normal and higher difficulties
if (gameSettings.selectedEmpire === 'Hungary' && gameSettings.difficulty !== 'easy') {
cost = Math.max(5, cost - 5); // Minimum cost of 5 gold
}
// Apply Scotch empire bonus: 3 gold less for all towers
if (gameSettings.selectedEmpire === 'Scotch') {
cost = Math.max(5, cost - 3); // Minimum cost of 5 gold
}
// Remove price doubling - research no longer increases costs
return cost;
}
function getTowerOreCost(towerType) {
// Upgrader (Build5) in builds set always costs 10 ore in ultra mode
if (towerSet === 'builds' && gameSettings.difficulty === 'ultra' && towerType === 'slow') {
return 10;
}
// Smith (Build6) in builds set always costs 15 ore in ultra mode
if (towerSet === 'builds' && gameSettings.difficulty === 'ultra' && towerType === 'poison') {
return 15;
}
// Only futuristic towers in ultra mode require ore
if (towerSet === 'futuristic' && gameSettings.difficulty === 'ultra') {
switch (towerType) {
case 'default':
return 5;
case 'rapid':
return 3;
case 'sniper':
return 8;
case 'splash':
return 7;
case 'slow':
return 6;
case 'poison':
return 9;
default:
return 5;
}
}
return 0;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
var towerOreCost = getTowerOreCost(towerType);
if (gold >= towerCost && ore >= towerOreCost) {
// Check if this is a house (Build1 in Ultra mode)
var isHouse = towerSet === 'builds' && towerType === 'default' && gameSettings.difficulty === 'ultra';
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
ore -= towerOreCost;
// Handle population logic only in ultra mode
if (gameSettings.difficulty === 'ultra') {
if (isHouse) {
// Each house gives 1000 population
population += 1000;
var note = game.addChild(new Notification("+1000 population!"));
note.x = 2048 / 2;
note.y = grid.height - 100;
}
}
updateUI();
grid.pathFind();
grid.renderDebug();
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// Block all interactions if difficulty not selected
if (!gameStartAllowed) {
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
// Completely recreate tower preview to ensure clean state
if (towerPreview && towerPreview.parent) {
game.removeChild(towerPreview);
towerPreview.destroy();
}
towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
// Apply the same offset as in move handler to ensure consistency when starting drag
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
break;
}
}
};
game.move = function (x, y, obj) {
// Block all interactions if difficulty not selected
if (!gameStartAllowed) {
return;
}
if (isDragging) {
// Shift the y position upward by 1.5 tiles to show preview above finger
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
}
};
game.up = function (x, y, obj) {
// Block all interactions if difficulty not selected
if (!gameStartAllowed) {
return;
}
var clickedOnTower = false;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var towerLeft = tower.x - tower.width / 2;
var towerRight = tower.x + tower.width / 2;
var towerTop = tower.y - tower.height / 2;
var towerBottom = tower.y + tower.height / 2;
if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
clickedOnTower = true;
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
if (towerPreview.canPlace) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
// Completely destroy and recreate tower preview to ensure it's completely clean
if (towerPreview.parent) {
game.removeChild(towerPreview);
towerPreview.destroy();
towerPreview = new TowerPreview();
towerPreview.visible = false;
}
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
// Create tower set buttons first (they will be behind the menu)
// Tower set buttons are already created above
// Always show start menu at game start with PLAY button
// --- Layered, non-interactive, behind-menu text labels ---
// Create a container for background labels
var menuBackgroundLabels = new Container();
menuBackgroundLabels.x = 2048 / 2;
menuBackgroundLabels.y = 2732 / 2;
menuBackgroundLabels.alpha = 0.13; // Faint, but visible behind menu
// Helper to add a label
function addMenuBGLabel(text, x, y, size, color, rotation) {
var label = new Text2(text, {
size: size,
fill: color,
weight: 800
});
label.anchor.set(0.5, 0.5);
label.x = x;
label.y = y;
if (rotation) label.rotation = rotation;
label.interactive = false;
label.buttonMode = false;
menuBackgroundLabels.addChild(label);
}
// Add the requested labels, spaced and sized for background effect
addMenuBGLabel("Medieval", -600, -700, 220, 0x8B4513, -0.08);
addMenuBGLabel("locked", 600, -700, 180, 0x888888, 0.07);
addMenuBGLabel("Trench", -600, 0, 200, 0x8B4513, 0.05);
addMenuBGLabel("lives", 600, 0, 180, 0x00FF00, -0.06);
addMenuBGLabel("normal", -600, 700, 200, 0x0088FF, 0.08);
addMenuBGLabel("music1", 600, 700, 180, 0xFF8800, -0.07);
addMenuBGLabel("Gold", 0, 0, 300, 0xFFD700, 0);
// Place the label container behind the menu
game.addChildAt(menuBackgroundLabels, 0);
var startMenu = new StartMenu();
game.addChild(startMenu);
var speedControl = new SpeedControl();
speedControl.x = -100; // Position closer to center to be adjacent to music control
speedControl.y = topMargin;
// Hide speed control in menu initially
speedControl.visible = false;
LK.gui.top.addChild(speedControl);
var musicControl = new MusicControl();
musicControl.x = 100; // Position closer to center to be adjacent to speed control
musicControl.y = topMargin;
// Hide music control in menu initially
musicControl.visible = false;
LK.gui.top.addChild(musicControl);
// Leaderboard functionality removed
// --- Tower Set Toggle Buttons ---
var towerSet = 'medieval'; // 'medieval', 'futuristic', or 'trench'
var futuristicUnlocked = false; // Track if futuristic towers have been unlocked
// Define tower types for each set
var medievalTowerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
var futuristicTowerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; // Placeholder, can be changed later
var trenchTowerTypes = ['default', 'rapid', 'sniper']; // Blocker, Trapper, Money
var buildsTowerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison']; // 6 build options for Ultra mode
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var towerY = 2732 - CELL_SIZE * 3 - 90;
// Button container - add to game so it's in front of towers
var towerSetButtonContainer = new Container();
towerSetButtonContainer.x = 2048 / 2;
// Move the button container up by the button height (100px) so the buttons are higher by their own height
towerSetButtonContainer.y = towerY - 60 - 100; // Move up by button height
game.addChild(towerSetButtonContainer);
// Move to top of display list so it's in front of towers
if (towerSetButtonContainer.parent) {
towerSetButtonContainer.parent.removeChild(towerSetButtonContainer);
}
game.addChild(towerSetButtonContainer);
// Medieval Button
var medievalButton = new Container();
var medievalBG = medievalButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
medievalBG.width = 350;
medievalBG.height = 100;
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.8; // Reduce transparency
var medievalText = new Text2("Medieval", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
medievalText.anchor.set(0.5, 0.5);
medievalText.alpha = 0.9; // Reduce transparency
medievalButton.addChild(medievalText);
// Arrange tower set buttons side by side, not overlapping
var buttonSpacing = 400; // Enough to avoid overlap, adjust as needed for visuals
medievalButton.x = -buttonSpacing;
medievalButton.y = 0;
medievalButton.down = function () {
LK.getSound('Click').play();
if (towerSet !== 'medieval') {
towerSet = 'medieval';
updateSourceTowers();
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.8; // Reduce transparency
futuristicBG.tint = 0x000080; // Dark blue color
futuristicBG.alpha = 0.8; // Reduce transparency
trenchBG.tint = 0x8B4513;
trenchBG.alpha = 0.8; // Reduce transparency
}
};
// Hide medieval button in menu initially
medievalButton.visible = false;
towerSetButtonContainer.addChild(medievalButton);
// Futuristic Button
var futuristicButton = new Container();
var futuristicBG = futuristicButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
futuristicBG.width = 350;
futuristicBG.height = 100;
futuristicBG.tint = 0x000080; // Dark blue color
futuristicBG.alpha = 0.8; // Reduce transparency
var futuristicText = new Text2("Futuristic", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
futuristicText.anchor.set(0.5, 0.5);
futuristicText.alpha = 0.9; // Reduce transparency
futuristicButton.addChild(futuristicText);
futuristicButton.x = 0;
futuristicButton.y = 0;
// Hide futuristic button in menu initially
futuristicButton.visible = false;
// Trench Button
var trenchButton = new Container();
var trenchBG = trenchButton.attachAsset('Buton', {
anchorX: 0.5,
anchorY: 0.5
});
trenchButton.x = buttonSpacing;
trenchBG.width = 350;
trenchBG.height = 100;
trenchBG.tint = 0x8B4513; // Same as medieval
trenchBG.alpha = 0.8; // Reduce transparency
var trenchText = new Text2("Builds", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
trenchText.anchor.set(0.5, 0.5);
trenchText.alpha = 0.9; // Reduce transparency
trenchButton.addChild(trenchText);
trenchButton.y = 0;
futuristicButton.down = function () {
LK.getSound('Click').play();
// Always block futuristic towers in story mode
if (gameSettings.storyMode) {
var notification = game.addChild(new Notification("Futuristic towers are permanently locked in Story Mode!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
// Block futuristic towers until wave 4 OR Research (Build3) is completed (except in Ultra mode)
if (gameSettings.difficulty === 'ultra' && !futuristicUnlocked) {
// In ultra mode, only allow if Research (Build3) is built and not destroyed
var researchBuilt = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchBuilt = true;
break;
}
}
if (!researchBuilt) {
var notification = game.addChild(new Notification("Futuristic towers are locked until you build Research!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
} else if (gameSettings.difficulty !== 'ultra' && currentWave < 4 && !futuristicUnlocked) {
var notification = game.addChild(new Notification("Futuristic towers unlock at wave 4 or by building Build3!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
// --- NEW: Prevent Futuristic if Research was destroyed in Ultra mode ---
if (gameSettings.difficulty === 'ultra' && !futuristicUnlocked) {
var researchExists = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchExists = true;
break;
}
}
if (!researchExists) {
var notification = game.addChild(new Notification("Futuristic towers are locked until you build Research!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
}
if (towerSet !== 'futuristic') {
towerSet = 'futuristic';
futuristicUnlocked = true; // Mark futuristic as unlocked
updateSourceTowers();
medievalBG.tint = 0x000080; // Dark blue color
medievalBG.alpha = 0.8; // Reduce transparency
futuristicBG.tint = 0x1976D2;
futuristicBG.alpha = 0.8; // Reduce transparency
trenchBG.tint = 0x8B4513;
trenchBG.alpha = 0.8; // Reduce transparency
}
};
futuristicButton.update = function () {
// Always show locked state in story mode
if (gameSettings.storyMode) {
futuristicBG.tint = 0x000080; // Dark blue when locked
futuristicBG.alpha = 0.3; // More transparent when locked
futuristicText.setText("Locked");
return;
}
// Update button appearance based on wave progress OR Research (Build3) in Ultra mode
if (gameSettings.difficulty === 'ultra' && !futuristicUnlocked) {
// In ultra mode, only allow if Research (Build3) is built and not destroyed
var researchBuilt = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchBuilt = true;
break;
}
}
if (!researchBuilt) {
futuristicBG.tint = 0x000080; // Dark blue when locked
futuristicBG.alpha = 0.3; // More transparent when locked
futuristicText.setText("Locked");
return;
}
}
if (gameSettings.difficulty !== 'ultra' && currentWave < 4 && !futuristicUnlocked) {
futuristicBG.tint = 0x000080; // Dark blue when locked
futuristicBG.alpha = 0.3; // More transparent when locked
futuristicText.setText("Locked");
} else {
if (towerSet === 'futuristic') {
futuristicBG.tint = 0x1976D2;
} else {
futuristicBG.tint = 0x000080; // Dark blue when not selected
}
futuristicBG.alpha = 0.8; // Reduce transparency
futuristicText.setText("Futuristic");
}
// --- NEW: Lock Futuristic if Research was destroyed in Ultra mode ---
if (gameSettings.difficulty === 'ultra' && !futuristicUnlocked) {
// If Research was destroyed, lock the button
var researchExists = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchExists = true;
break;
}
}
if (!researchExists) {
futuristicBG.tint = 0x000080;
futuristicBG.alpha = 0.3;
futuristicText.setText("Locked");
}
}
};
towerSetButtonContainer.addChild(futuristicButton);
trenchButton.down = function () {
LK.getSound('Click').play();
var targetSet = gameSettings.difficulty === 'ultra' ? 'builds' : 'trench';
if (towerSet !== targetSet) {
towerSet = targetSet;
updateSourceTowers();
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.8; // Reduce transparency
futuristicBG.tint = 0x000080; // Dark blue color
futuristicBG.alpha = 0.8; // Reduce transparency
trenchBG.tint = 0x8B4513;
trenchBG.alpha = 0.8; // Reduce transparency
}
};
// Hide trench button in menu initially
trenchButton.visible = false;
towerSetButtonContainer.addChild(trenchButton);
// Helper to clear and re-create source towers
// Leaderboard input functionality removed
// Save to leaderboard functionality removed
// Show leaderboard functionality removed
function updateSourceTowers() {
// Remove old source towers
for (var i = 0; i < sourceTowers.length; i++) {
if (sourceTowers[i].parent) {
sourceTowers[i].parent.removeChild(sourceTowers[i]);
}
sourceTowers[i].destroy && sourceTowers[i].destroy();
}
sourceTowers = [];
// Pick correct tower types
var types = towerSet === 'medieval' ? medievalTowerTypes : towerSet === 'futuristic' ? futuristicTowerTypes : towerSet === 'trench' ? trenchTowerTypes : towerSet === 'builds' ? buildsTowerTypes : medievalTowerTypes;
var startX = 2048 / 2 - types.length * towerSpacing / 2 + towerSpacing / 2;
for (var i = 0; i < types.length; i++) {
var tower = new SourceTower(types[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
}
updateSourceTowers();
sourceTower = null;
enemiesToSpawn = 10;
// Always keep tower set button container above towers visually
if (towerSetButtonContainer && towerSetButtonContainer.parent) {
towerSetButtonContainer.parent.removeChild(towerSetButtonContainer);
game.addChild(towerSetButtonContainer);
}
game.update = function () {
// --- NEW: Lock futuristic towers immediately if no Research exists in ultra mode ---
if (gameSettings.difficulty === 'ultra') {
var researchExists = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].towerSet === 'builds' && towers[i].id === 'splash') {
researchExists = true;
break;
}
}
if (!researchExists && futuristicUnlocked) {
futuristicUnlocked = false;
// Switch back to medieval towers if currently on futuristic
if (towerSet === 'futuristic') {
towerSet = 'medieval';
updateSourceTowers();
if (typeof medievalBG !== "undefined") {
medievalBG.tint = 0x8B4513;
medievalBG.alpha = 0.5;
}
if (typeof futuristicBG !== "undefined") {
futuristicBG.tint = 0x222222;
futuristicBG.alpha = 0.5;
}
}
}
}
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
grid.pathFind();
grid.renderDebug();
// --- ULTRA MODE SPECIAL WAVES ---
if (gameSettings.difficulty === 'ultra' && currentWave > 7) {
// After wave 7, spawn Modern + 10 Angry + 2 Immune
var enemyTypes = [];
// Add 10 Angry enemies
for (var i = 0; i < 10; i++) {
enemyTypes.push('angry');
}
// Add 2 Immune enemies
for (var i = 0; i < 2; i++) {
enemyTypes.push('immune');
}
// --- Modern enemy logic for Ultra mode ---
var modernCount = 0;
if (currentWave % 10 === 0) {
modernCount = 3;
} else {
modernCount = 1;
}
// Add modern enemies to the enemyTypes array
for (var i = 0; i < modernCount; i++) {
enemyTypes.push('modern');
}
// Shuffle enemyTypes for randomness
for (var i = enemyTypes.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = enemyTypes[i];
enemyTypes[i] = enemyTypes[j];
enemyTypes[j] = temp;
}
// Spawn enemies
for (var i = 0; i < enemyTypes.length; i++) {
var waveType = enemyTypes[i];
var enemy = new Enemy(waveType);
enemyLayerBottom.addChild(enemy);
var healthMultiplier = Math.pow(1.12, currentWave);
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2);
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
var spawnX;
if (availableColumns.length > 0) {
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
spawnX = midPoint - 3 + Math.floor(Math.random() * 6);
}
var spawnY = -1 - Math.random() * 5;
enemy.cellX = spawnX;
enemy.cellY = 5;
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
} else if ((gameSettings.difficulty === 'hardcore' || gameSettings.difficulty === 'ultra') && currentWave > 7) {
// --- Modern enemy logic for Hardcore mode (and Ultra fallback for <=10) ---
// Insert after normal spawn logic, so after all other enemies are spawned
// This block will be reached for hardcore and for ultra waves <= 10
// (for ultra > 10, handled above)
// Determine how many modern enemies to spawn
var modernCount = 0;
if (currentWave % 10 === 0) {
modernCount = 3;
} else {
modernCount = 1;
}
for (var m = 0; m < modernCount; m++) {
var modernEnemy = new Enemy('modern');
enemyLayerBottom.addChild(modernEnemy);
var healthMultiplier = Math.pow(1.12, currentWave);
modernEnemy.maxHealth = Math.round(modernEnemy.maxHealth * healthMultiplier);
modernEnemy.health = modernEnemy.maxHealth;
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2);
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
var spawnX;
if (availableColumns.length > 0) {
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
spawnX = midPoint - 3 + Math.floor(Math.random() * 6);
}
var spawnY = -1 - Math.random() * 5;
modernEnemy.cellX = spawnX;
modernEnemy.cellY = 5;
modernEnemy.currentCellX = spawnX;
modernEnemy.currentCellY = spawnY;
modernEnemy.waveNumber = currentWave;
enemies.push(modernEnemy);
}
} else {
// --- ORIGINAL SPAWN LOGIC FOR NON-ULTRA OR <=10 ---
// Every wave spawns 10 enemies on wave 1, then 25% more each wave
var enemyCount = Math.floor(10 * Math.pow(1.25, currentWave - 1));
// England empire: 3 fewer normal enemies per wave
if (gameSettings.selectedEmpire === 'England') {
enemyCount = Math.max(1, enemyCount - 3); // Minimum 1 enemy
}
// Byzantine empire: 3 more enemies per wave
if (gameSettings.selectedEmpire === 'Byzantine') {
enemyCount = enemyCount + 3;
}
// Determine flying enemy count based on wave number
var flyingEnemyCount = 0;
if (currentWave === 1) {
flyingEnemyCount = 0;
} else if (currentWave === 2) {
flyingEnemyCount = 1;
} else if (currentWave === 3 || currentWave === 4) {
flyingEnemyCount = 3;
} else if (currentWave === 5) {
flyingEnemyCount = 5;
enemyCount = 9; // Total enemies for wave 5
} else if (currentWave === 6) {
flyingEnemyCount = 0; // No flying enemies in wave 6
enemyCount = 18; // 5 normal + 3 angry + 10 immune = 18 total enemies
} else if (currentWave === 7) {
flyingEnemyCount = 0; // No flying enemies in wave 7
enemyCount = 15; // 10 angry + 5 immune = 15 total enemies
} else if (currentWave === 8) {
flyingEnemyCount = 0; // No flying enemies in wave 8
enemyCount = 25; // 25 normal enemies
} else if (currentWave === 9) {
flyingEnemyCount = 0; // No flying enemies in wave 9
enemyCount = 25; // 25 normal enemies
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
// Determine enemy type based on specific wave requirements
var waveType = 'normal'; // Default to normal
var immuneCount = 0;
var angryCount = 0;
// Set specific immune enemy counts per wave
if (currentWave === 2 || currentWave === 3 || currentWave === 4 || currentWave === 5) {
immuneCount = 3;
} else if (currentWave === 6) {
immuneCount = 10; // Wave 6: 10 immune enemies
} else if (currentWave === 7) {
immuneCount = 5; // Wave 7: 5 immune enemies
} else if (currentWave === 8 || currentWave === 9) {
immuneCount = 0; // Wave 8 and 9: 0 immune enemies
}
// Hard mode: more immune and angry enemies
if (gameSettings.difficulty === 'hard') {
// Wave 1: 2 immune enemies
if (currentWave === 1) {
immuneCount = 2;
}
// Waves 2,3,4: 3 angry enemies each
if (currentWave === 2 || currentWave === 3 || currentWave === 4) {
angryCount = 3;
immuneCount = 3;
}
// Wave 5: keep existing counts
if (currentWave === 5) {
angryCount = 3;
immuneCount = 5;
}
} else {
// Set angry enemy spawning: 0 in waves 1-3, 2 in waves 4-5, 3 in wave 6
if (currentWave === 4) {
angryCount = 2;
} else if (currentWave === 5) {
angryCount = 2;
} else if (currentWave === 6) {
angryCount = 3; // Wave 6: 3 angry enemies
} else if (currentWave === 7) {
angryCount = 10; // Wave 7: 10 angry enemies
} else if (currentWave === 8 || currentWave === 9) {
angryCount = 0; // Wave 8 and 9: 0 angry enemies
}
}
// Set bad enemy spawning: only in wave 5, but not in Ultra mode
var badCount = 0;
if (currentWave === 5 && gameSettings.difficulty !== 'ultra') {
badCount = 1; // 1 bad enemy - same for all empires including England
immuneCount = 5; // 5 immune enemies
angryCount = 3; // 3 angry enemies
}
// Set super enemy spawning: only in wave 10 for hardcore mode, but not in Ultra mode
var superCount = 0;
if (currentWave === 10 && gameSettings.difficulty === 'hardcore') {
superCount = 0; // No super enemy in wave 10
badCount = 3; // 3 bad enemies in wave 10
angryCount = 0; // No angry enemies in wave 10
immuneCount = 0; // No immune enemies in wave 10
enemyCount = 3; // 3 bad enemies total
}
// Make the first 'angryCount' enemies angry for this wave
if (i < angryCount) {
waveType = 'angry';
}
// Make the next 'badCount' enemies bad for this wave (after angry enemies)
else if (i >= angryCount && i < angryCount + badCount) {
waveType = 'bad';
}
// Make the next 'superCount' enemies super for this wave (after bad enemies)
else if (i >= angryCount + badCount && i < angryCount + badCount + superCount) {
waveType = 'super';
}
// Make the last 'immuneCount' enemies immune for this wave (but not if already angry, bad, or super)
else if (i >= enemyCount - immuneCount) {
waveType = 'immune';
}
var enemy = new Enemy(waveType);
enemyLayerBottom.addChild(enemy);
var healthMultiplier = Math.pow(1.12, currentWave);
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2);
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
var spawnX;
if (availableColumns.length > 0) {
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
}
var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
} // end else (original spawn logic)
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
var notification = game.addChild(new Notification("Wave " + currentWave + " completed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Add 10 points for completing each wave in ultra mode
if (gameSettings.difficulty === 'ultra') {
addScore(10);
// Add all enemy kills for this wave to score (already counted per kill)
// (No extra code needed here, as kills are counted above)
}
// Automatically advance to next wave if all enemies are gone
if (currentWave < totalWaves) {
waveTimer = nextWaveTime; // Trigger next wave immediately
}
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Give gold for every enemy killed - Hard mode gives 5 gold, others give 10
var goldEarned = gameSettings.difficulty === 'hard' ? 5 : 10;
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Add 1 point for each enemy killed in ultra mode and count kills
if (gameSettings.difficulty === 'ultra') {
addScore(1);
if (typeof ultraEnemyKills !== "undefined") {
ultraEnemyKills++;
}
}
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
lives = Math.max(0, lives - (enemy.type === 'bad' ? 10 : 1));
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
};
White circle with two eyes, seen from above.. In-Game asset. 2d. High contrast. No shadows
tower from top. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
tower from top. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
White simple circular enemy seen from above, black outline. Black eyes, with a single Sword in-font of it. Black and white only. Blue background.
White simple circular enemy seen from above, black outline. Black eyes, on a white horse. Black and white only. Blue background.
crown. In-Game asset. 2d. High contrast. No shadows
information IΜ. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
gece yarΔ±sΔ± eski ve gercΜ§ekcΜ§i bir kale resmi. In-Game asset. 2d. High contrast
fire in a circle. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
money in a circle. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
bu kulenin rasgele noktalarΔ±na parlak yesΜ§il Δ±sΜ§Δ±klar ekle (duvarlarΔ±na sadece!) duvarlarΔ± mavi yap
kale olmasΔ±n. Top down bir orta cΜ§agΜ demirci evi olsun
make it medieval style and not colorful
gold mines. In-Game asset. 2d. High contrast. No shadows
eline parlak yesΜ§il bir kΔ±lΔ±cΜ§ ekle
Butonun yerinde acΜ§Δ±lmΔ±sΜ§ bir orta cΜ§agΜ stilinde bir yΔ±rtΔ±k pΔ±rtΔ±k kagΜΔ±t olsun
2 boyutlu uΜstten goΜruΜlme bir savasΜ§ topu. In-Game asset. 2d. High contrast. No shadows
tekerleri mavi olsun ve demir goΜvdenin uΜzerinde yesΜ§il yesΜ§il Δ±sΜ§Δ±k gibi benekler olsun
buna bir savasΜ§ topu havasΔ± kat
realistic stone wall. In-Game asset. 2d. High contrast. No shadows
karsΜ§Δ±dan goΜzuΜken bir dagΜ. In-Game asset. 2d. High contrast. No shadows
agΜacΜ§. In-Game asset. 2d. High contrast. No shadows
muΜkemmel bir kare sΜ§eklinde olsun
tasΜ§tan degΜil de tahtadan olsun