/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* 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 () {
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
self.targetEnemy.health -= self.damage;
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)
otherEnemy.health -= self.damage * 0.5;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply slow effect
// Make slow percentage scale with tower level (default 50%, up to 80% at max level)
var slowPct = 0.5;
if (self.sourceTowerLevel !== undefined) {
// Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
self.targetEnemy.slowDuration = 180; // Reset duration
}
}
} 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 === 'fire') {
// Create visual fire effect
var fireEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'fire');
game.addChild(fireEffect);
// Apply burning effect - affects all enemies
self.targetEnemy.burning = true;
self.targetEnemy.burnDamage = self.damage * 0.3; // 30% of original damage per tick
self.targetEnemy.burnDuration = 240; // 4 seconds at 60 FPS
// Fire damage to nearby enemies in radius
var fireRadius = CELL_SIZE * 2;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var fireDx = otherEnemy.x - self.targetEnemy.x;
var fireDy = otherEnemy.y - self.targetEnemy.y;
var fireDistance = Math.sqrt(fireDx * fireDx + fireDy * fireDy);
if (fireDistance <= fireRadius) {
// Apply burn damage to nearby enemies
otherEnemy.health -= self.damage * 0.4; // 40% of original damage
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
// Apply burning effect
otherEnemy.burning = true;
otherEnemy.burnDamage = self.damage * 0.2; // 20% burn damage for nearby enemies
otherEnemy.burnDuration = 180; // 3 seconds
}
}
}
} 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);
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 = true;
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;
}
while (debugArrows.length > data.targets.length) {
self.removeChild(debugArrows.pop());
}
for (var a = 0; a < data.targets.length; a++) {
var destination = data.targets[a];
var ox = destination.x - data.x;
var oy = destination.y - data.y;
var angle = Math.atan2(oy, ox);
if (!debugArrows[a]) {
debugArrows[a] = LK.getAsset('arrow', {
anchorX: -.5,
anchorY: 0.5
});
debugArrows[a].alpha = .5;
self.addChildAt(debugArrows[a], 1);
}
debugArrows[a].rotation = angle;
}
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0x008800;
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
};
});
var DiamondIndicator = 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 diamondText = new Text2("+" + value, {
size: 45,
fill: 0x00FFFF,
weight: 800
});
diamondText.anchor.set(0.5, 0.5);
self.addChild(diamondText);
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;
});
// 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;
case 'fire':
effectGraphics.tint = 0xFF4400;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 2;
break;
case 'attack':
effectGraphics.tint = 0xFF0000;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 0.8;
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;
self.attackDamage = 3; // Base attack damage reduced to 1-6 range
self.attackRate = 60; // Frames between attacks (1 second at 60 FPS)
self.lastAttacked = 0; // Track when enemy last attacked
// 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;
self.attackDamage = 2; // Low damage but fast
self.attackRate = 45; // Attacks faster
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
self.attackDamage = 4; // Moderate damage
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
self.attackDamage = 3; // Moderate damage
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
self.attackDamage = 1; // Very low damage but swarm
self.attackRate = 40; // But attack frequently
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave >= 10) {
self.isBoss = true;
// Boss enemies have 25000 health and are larger
self.maxHealth = 25000;
// Bosses deal slightly more damage but still in 1-6 range
self.attackDamage = 6; // Max damage for bosses
// Attack more frequently
self.attackRate = 30;
// 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 !== '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 () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// 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)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed && !self.burning) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle burning effect
if (self.burning) {
// Visual indication of burning status
if (!self.burnEffect) {
self.burnEffect = true;
}
// Apply burn damage every 20 frames (3 times per second)
if (LK.ticks % 20 === 0) {
self.health -= self.burnDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.burnDuration--;
if (self.burnDuration <= 0) {
self.burning = false;
self.burnEffect = false;
// Only reset tint if not slowed or poisoned
if (!self.slowed && !self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.burning && self.poisoned && self.slowed) {
// All three effects: fire + poison + slow (orange-purple-blue mix)
enemyGraphics.tint = 0xFF6600;
} else if (self.burning && self.poisoned) {
// Fire + poison: red-green mix (orange)
enemyGraphics.tint = 0xFF7700;
} else if (self.burning && self.slowed) {
// Fire + slow: red-purple mix
enemyGraphics.tint = 0xFF2266;
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.burning) {
enemyGraphics.tint = 0xFF4400; // Fire effect color
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} 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
});
}
}
}
}
// Check for attack targets (hero and defense towers) - optimize by checking less frequently
var canAttack = LK.ticks - self.lastAttacked >= self.attackRate;
if (canAttack && LK.ticks % 3 === 0) {
// Only check every 3 frames
var attackTarget = null;
var attackDistance = Infinity;
// Check if hero is in attack range
if (hero && !heroIsDead) {
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var heroDistance = dx * dx + dy * dy; // Use squared distance for faster comparison
if (heroDistance < 10000) {
// 100 * 100 = 10000
// Attack range of 100 pixels
attackTarget = hero;
attackDistance = heroDistance;
}
}
// Check for nearby defense towers - limit to first 5 towers
var towersToCheck = Math.min(towers.length, 5);
for (var i = 0; i < towersToCheck; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var towerDistance = dx * dx + dy * dy; // Use squared distance
if (towerDistance < 14400 && towerDistance < attackDistance) {
// 120 * 120 = 14400
// Prefer closer targets
attackTarget = tower;
attackDistance = towerDistance;
}
}
// Attack the target
if (attackTarget) {
self.lastAttacked = LK.ticks;
if (attackTarget === hero) {
// Attack hero
heroHealth = Math.max(0, heroHealth - self.attackDamage);
if (heroHealth <= 0 && !heroIsDead) {
heroIsDead = true;
hero.alpha = 0.3;
var notification = game.addChild(new Notification("Hero destroyed by enemy!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
updateUI();
// Visual effect for hero damage
LK.effects.flashObject(hero, 0xFF0000, 300);
} else {
// Attack tower - towers have no health, so just show visual effect
LK.effects.flashObject(attackTarget, 0xFF0000, 300);
// Create damage indicator
var damageIndicator = new Notification("-" + self.attackDamage);
damageIndicator.x = attackTarget.x;
damageIndicator.y = attackTarget.y - 50;
game.addChild(damageIndicator);
}
// Visual attack effect
var attackEffect = new EffectIndicator(self.x, self.y, 'attack');
game.addChild(attackEffect);
}
}
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
*/
// Define the predefined path coordinates - extended longer path with more turns
var pathCells = [{
x: 11,
y: 4
}, {
x: 11,
y: 5
}, {
x: 11,
y: 6
}, {
x: 11,
y: 7
}, {
x: 11,
y: 8
}, {
x: 10,
y: 8
}, {
x: 9,
y: 8
}, {
x: 8,
y: 8
}, {
x: 7,
y: 8
}, {
x: 6,
y: 8
}, {
x: 5,
y: 8
}, {
x: 4,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 9
}, {
x: 3,
y: 10
}, {
x: 3,
y: 11
}, {
x: 3,
y: 12
}, {
x: 4,
y: 12
}, {
x: 5,
y: 12
}, {
x: 6,
y: 12
}, {
x: 7,
y: 12
}, {
x: 8,
y: 12
}, {
x: 9,
y: 12
}, {
x: 10,
y: 12
}, {
x: 11,
y: 12
}, {
x: 12,
y: 12
}, {
x: 13,
y: 12
}, {
x: 14,
y: 12
}, {
x: 15,
y: 12
}, {
x: 16,
y: 12
}, {
x: 17,
y: 12
}, {
x: 18,
y: 12
}, {
x: 19,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 13
}, {
x: 20,
y: 14
}, {
x: 20,
y: 15
}, {
x: 20,
y: 16
}, {
x: 20,
y: 17
}, {
x: 20,
y: 18
}, {
x: 19,
y: 18
}, {
x: 18,
y: 18
}, {
x: 17,
y: 18
}, {
x: 16,
y: 18
}, {
x: 15,
y: 18
}, {
x: 14,
y: 18
}, {
x: 13,
y: 18
}, {
x: 12,
y: 18
}, {
x: 11,
y: 18
}, {
x: 10,
y: 18
}, {
x: 9,
y: 18
}, {
x: 8,
y: 18
}, {
x: 7,
y: 18
}, {
x: 6,
y: 18
}, {
x: 5,
y: 18
}, {
x: 4,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 19
}, {
x: 3,
y: 20
}, {
x: 3,
y: 21
}, {
x: 3,
y: 22
}, {
x: 3,
y: 23
}, {
x: 4,
y: 23
}, {
x: 5,
y: 23
}, {
x: 6,
y: 23
}, {
x: 7,
y: 23
}, {
x: 8,
y: 23
}, {
x: 9,
y: 23
}, {
x: 10,
y: 23
}, {
x: 11,
y: 23
}, {
x: 12,
y: 23
}, {
x: 13,
y: 23
}, {
x: 14,
y: 23
}, {
x: 15,
y: 23
}, {
x: 16,
y: 23
}, {
x: 17,
y: 23
}, {
x: 18,
y: 23
}, {
x: 19,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 24
}, {
x: 20,
y: 25
}, {
x: 20,
y: 26
}, {
x: 20,
y: 27
}, {
x: 19,
y: 27
}, {
x: 18,
y: 27
}, {
x: 17,
y: 27
}, {
x: 16,
y: 27
}, {
x: 15,
y: 27
}, {
x: 14,
y: 27
}, {
x: 13,
y: 27
}, {
x: 12,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 28
}, {
x: 11,
y: 29
}];
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0;
// Check if this cell is part of the predefined path
var isPathCell = false;
for (var p = 0; p < pathCells.length; p++) {
if (pathCells[p].x === i && pathCells[p].y === j) {
isPathCell = true;
break;
}
}
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = isPathCell ? 0 : 1; // Only allow path cells to be open
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = isPathCell ? 0 : 1; // Only allow path cells to be open
} else {
cellType = isPathCell ? 0 : 1; // Only allow path cells to be open
}
} else {
// For areas outside the spawn/goal columns, only allow path cells
if (j > 4 && j < gridHeight - 4) {
cellType = isPathCell ? 0 : 1;
}
}
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) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
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) {
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;
}
}
}
}
var iterations = 0;
var maxIterations = 5000; // Prevent infinite loops
while (toProcess.length && iterations < maxIterations) {
iterations++;
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.left.type != 1) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
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);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
// Only check first 20 enemies to reduce lag
var enemiesToCheck = Math.min(enemies.length, 20);
for (var a = 0; a < enemiesToCheck; 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 && cell.type == 3) {
// Enemy reached the goal - remove them
return true;
}
// Update shadow position for flying enemies
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;
}
}
// Define the complete path that all enemies must follow
var pathWaypoints = [{
x: 11,
y: 4
}, {
x: 11,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 29
}];
// Initialize waypoint index if not set
if (enemy.waypointIndex === undefined) {
enemy.waypointIndex = 0;
}
// Check if enemy has reached the entry area
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached entry area, move down to first waypoint
if (!hasReachedEntryArea) {
enemy.currentCellY += enemy.speed;
// Ensure enemy moves to the correct X position (path entry)
if (Math.abs(enemy.currentCellX - 11) > 0.1) {
var xDirection = enemy.currentCellX < 11 ? 1 : -1;
enemy.currentCellX += xDirection * enemy.speed * 0.5;
}
// Update position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update cell coordinates when reaching entry area
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// Handle flying enemies - they go straight to goal
if (enemy.isFlying) {
if (!enemy.flyingTarget) {
enemy.flyingTarget = self.goals[0];
}
if (enemy.flyingTarget) {
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) {
return true; // Reached goal
}
var angle = Math.atan2(oy, ox);
// Rotate enemy to face movement direction
if (enemy.children[0]) {
tween(enemy.children[0], {
rotation: angle
}, {
duration: 200,
easing: tween.easeOut
});
}
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
}
return false;
}
// Handle ground enemies following waypoint path
var currentWaypoint = pathWaypoints[enemy.waypointIndex];
if (!currentWaypoint && enemy.waypointIndex >= pathWaypoints.length) {
// All waypoints completed, head to goal
var goalCell = self.goals[0];
if (goalCell) {
currentWaypoint = {
x: goalCell.x,
y: goalCell.y
};
}
}
if (currentWaypoint) {
var ox = currentWaypoint.x - enemy.currentCellX;
var oy = currentWaypoint.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
// Check if reached current waypoint
if (dist < 0.3) {
enemy.waypointIndex++;
// Get next waypoint or goal
if (enemy.waypointIndex < pathWaypoints.length) {
currentWaypoint = pathWaypoints[enemy.waypointIndex];
ox = currentWaypoint.x - enemy.currentCellX;
oy = currentWaypoint.y - enemy.currentCellY;
dist = Math.sqrt(ox * ox + oy * oy);
} else {
// Head to goal
var goalCell = self.goals[0];
if (goalCell) {
ox = goalCell.x - enemy.currentCellX;
oy = goalCell.y - enemy.currentCellY;
dist = Math.sqrt(ox * ox + oy * oy);
}
}
}
// Move toward current target
if (dist > enemy.speed) {
var angle = Math.atan2(oy, ox);
// Smoothly rotate enemy to face movement direction
if (enemy.children[0]) {
tween(enemy.children[0], {
rotation: angle
}, {
duration: 200,
easing: tween.easeOut
});
}
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
} else {
// Close enough to target, snap to it
enemy.currentCellX = currentWaypoint.x;
enemy.currentCellY = currentWaypoint.y;
}
}
// Update final position and cell coordinates
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
return false;
};
});
var Hero = Container.expand(function () {
var self = Container.call(this);
self.speed = 4;
self.fireRate = 20; // Frames between shots
self.lastFired = 0;
self.damage = 15;
self.bulletSpeed = 8;
self.isBossHero = false; // Flag to distinguish boss level hero
self.waypointIndex = 0; // For path following
self.pathSpeed = 0.8; // Speed for following path
self.currentCellX = 0;
self.currentCellY = 0;
self.maxHealth = 2000; // Boss hero health
self.health = self.maxHealth;
var heroGraphics = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.0,
scaleY: 2.0
});
heroGraphics.tint = 0x00FF00; // Green color for hero
// Gun container for rotation
var gunContainer = new Container();
self.addChild(gunContainer);
var gunGraphics = gunContainer.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
gunGraphics.width = 60;
gunGraphics.height = 60;
gunGraphics.tint = 0x444444; // Darker gray for better visibility
gunGraphics.alpha = 1.0; // Ensure gun is fully visible
self.targetX = 0;
self.targetY = 0;
self.isMoving = false;
self.range = 600; // Hero gun range - increased for long radius attack
self.showRange = false; // Toggle for showing range indicator
// Create range indicator
self.rangeIndicator = new Container();
self.addChild(self.rangeIndicator);
var rangeGraphics = self.rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.range * 2;
rangeGraphics.alpha = 0.2;
rangeGraphics.tint = 0x00FF00;
self.rangeIndicator.visible = false;
self.update = function () {
// Handle boss hero path following
if (self.isBossHero) {
// Define the path waypoints that hero must follow - same as enemy path
var pathWaypoints = [{
x: 11,
y: 4
}, {
x: 11,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 29
}];
// Find current waypoint target
if (!self.waypointIndex) {
self.waypointIndex = 0;
}
// Get current waypoint
var currentWaypoint = pathWaypoints[self.waypointIndex];
if (!currentWaypoint) {
// If no more waypoints, stay at last position
return;
}
if (currentWaypoint) {
var ox = currentWaypoint.x - self.currentCellX;
var oy = currentWaypoint.y - self.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
// If close to current waypoint, move to next waypoint
if (dist < 0.5) {
self.waypointIndex++;
// If we've reached the last waypoint, stay there
if (self.waypointIndex >= pathWaypoints.length) {
self.waypointIndex = pathWaypoints.length - 1;
return;
}
}
if (dist > self.pathSpeed) {
var angle = Math.atan2(oy, ox);
self.currentCellX += Math.cos(angle) * self.pathSpeed;
self.currentCellY += Math.sin(angle) * self.pathSpeed;
// Rotate hero graphic to match movement direction
if (heroGraphics.targetRotation === undefined) {
heroGraphics.targetRotation = angle;
heroGraphics.rotation = angle;
} else {
if (Math.abs(angle - heroGraphics.targetRotation) > 0.05) {
tween.stop(heroGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = heroGraphics.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;
}
heroGraphics.targetRotation = angle;
tween(heroGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
self.x = grid.x + self.currentCellX * CELL_SIZE;
self.y = grid.y + self.currentCellY * CELL_SIZE;
} else {
// Move towards target if moving (normal hero behavior)
if (self.isMoving) {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
} else {
self.isMoving = false;
}
}
}
// Show/hide range indicator when hero is selected or moving
self.rangeIndicator.visible = self.showRange || self.isMoving;
// Find target enemy within range using priority system
var targetEnemy = null;
var bestPriority = -1;
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);
// Only consider enemies within range
if (distance <= self.range) {
// Priority system: bosses > closest to goal > closest to hero
var priority = 0;
// Highest priority for bosses
if (enemy.isBoss) {
priority += 1000;
}
// Medium priority based on progress toward goal
if (enemy.isFlying && enemy.flyingTarget) {
var goalDx = enemy.flyingTarget.x - enemy.cellX;
var goalDy = enemy.flyingTarget.y - enemy.cellY;
var distToGoal = Math.sqrt(goalDx * goalDx + goalDy * goalDy);
priority += (100 - distToGoal) * 10; // Closer to goal = higher priority
} else {
// For ground enemies, use path score
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
priority += 1000 - cell.score / 100; // Lower score = closer to goal = higher priority
}
}
// Lowest priority based on distance to hero (closer = slightly higher priority)
priority += (self.range - distance) / 10;
if (priority > bestPriority) {
bestPriority = priority;
targetEnemy = enemy;
}
}
}
if (targetEnemy) {
// Check collision with enemy (hero takes damage)
var dx = targetEnemy.x - self.x;
var dy = targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 80 && !heroIsDead) {
// Hero takes damage when touching enemy
var damage = targetEnemy.isBoss ? 50 : 20;
heroHealth = Math.max(0, heroHealth - damage);
if (heroHealth <= 0) {
heroIsDead = true;
self.alpha = 0.3;
var notification = game.addChild(new Notification("Hero died! Use diamonds to revive!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
updateUI();
// Push enemy away from hero
var pushForce = 50;
var pushAngle = Math.atan2(dy, dx);
targetEnemy.x += Math.cos(pushAngle) * pushForce;
targetEnemy.y += Math.sin(pushAngle) * pushForce;
}
// Aim gun at target enemy
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
// Fire at enemy (only if hero is alive and within range)
if (LK.ticks - self.lastFired >= self.fireRate && !heroIsDead && distance <= self.range) {
self.fire(angle);
self.lastFired = LK.ticks;
}
}
};
self.fire = function (direction) {
var bulletX = self.x + Math.cos(direction) * 30;
var bulletY = self.y + Math.sin(direction) * 30;
var bullet = new HeroBullet(bulletX, bulletY, direction, self.damage, self.bulletSpeed);
game.addChild(bullet);
heroBullets.push(bullet);
// Create muzzle flash effect
var muzzleFlash = new EffectIndicator(bulletX, bulletY, 'attack');
game.addChild(muzzleFlash);
// Gun recoil effect
tween.stop(gunContainer, {
x: true,
y: true
});
var recoilDistance = 8;
var recoilX = -Math.cos(direction) * recoilDistance;
var recoilY = -Math.sin(direction) * recoilDistance;
tween(gunContainer, {
x: recoilX,
y: recoilY
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(gunContainer, {
x: 0,
y: 0
}, {
duration: 120,
easing: tween.easeIn
});
}
});
};
self.moveTo = function (x, y) {
// Don't move if hero is dead
if (heroIsDead) {
return;
}
// Convert screen coordinates to grid coordinates
var gridPosX = (x - grid.x) / CELL_SIZE;
var gridPosY = (y - grid.y) / CELL_SIZE;
var gridX = Math.floor(gridPosX);
var gridY = Math.floor(gridPosY);
// Check if the target position is within valid bounds
var isValidPosition = true;
// Check if position is within grid bounds
if (gridX < 0 || gridX >= 24 || gridY < 4 || gridY >= 29 + 2) {
isValidPosition = false;
}
// Check if position is not a wall (type 1)
if (isValidPosition) {
var cell = grid.getCell(gridX, gridY);
if (cell && cell.type === 1) {
isValidPosition = false;
}
// Check if position is on the enemy path - use the same path as defined in Grid
var pathCells = [{
x: 11,
y: 4
}, {
x: 11,
y: 5
}, {
x: 11,
y: 6
}, {
x: 11,
y: 7
}, {
x: 11,
y: 8
}, {
x: 10,
y: 8
}, {
x: 9,
y: 8
}, {
x: 8,
y: 8
}, {
x: 7,
y: 8
}, {
x: 6,
y: 8
}, {
x: 5,
y: 8
}, {
x: 4,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 9
}, {
x: 3,
y: 10
}, {
x: 3,
y: 11
}, {
x: 3,
y: 12
}, {
x: 4,
y: 12
}, {
x: 5,
y: 12
}, {
x: 6,
y: 12
}, {
x: 7,
y: 12
}, {
x: 8,
y: 12
}, {
x: 9,
y: 12
}, {
x: 10,
y: 12
}, {
x: 11,
y: 12
}, {
x: 12,
y: 12
}, {
x: 13,
y: 12
}, {
x: 14,
y: 12
}, {
x: 15,
y: 12
}, {
x: 16,
y: 12
}, {
x: 17,
y: 12
}, {
x: 18,
y: 12
}, {
x: 19,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 13
}, {
x: 20,
y: 14
}, {
x: 20,
y: 15
}, {
x: 20,
y: 16
}, {
x: 20,
y: 17
}, {
x: 20,
y: 18
}, {
x: 19,
y: 18
}, {
x: 18,
y: 18
}, {
x: 17,
y: 18
}, {
x: 16,
y: 18
}, {
x: 15,
y: 18
}, {
x: 14,
y: 18
}, {
x: 13,
y: 18
}, {
x: 12,
y: 18
}, {
x: 11,
y: 18
}, {
x: 10,
y: 18
}, {
x: 9,
y: 18
}, {
x: 8,
y: 18
}, {
x: 7,
y: 18
}, {
x: 6,
y: 18
}, {
x: 5,
y: 18
}, {
x: 4,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 19
}, {
x: 3,
y: 20
}, {
x: 3,
y: 21
}, {
x: 3,
y: 22
}, {
x: 3,
y: 23
}, {
x: 4,
y: 23
}, {
x: 5,
y: 23
}, {
x: 6,
y: 23
}, {
x: 7,
y: 23
}, {
x: 8,
y: 23
}, {
x: 9,
y: 23
}, {
x: 10,
y: 23
}, {
x: 11,
y: 23
}, {
x: 12,
y: 23
}, {
x: 13,
y: 23
}, {
x: 14,
y: 23
}, {
x: 15,
y: 23
}, {
x: 16,
y: 23
}, {
x: 17,
y: 23
}, {
x: 18,
y: 23
}, {
x: 19,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 24
}, {
x: 20,
y: 25
}, {
x: 20,
y: 26
}, {
x: 20,
y: 27
}, {
x: 19,
y: 27
}, {
x: 18,
y: 27
}, {
x: 17,
y: 27
}, {
x: 16,
y: 27
}, {
x: 15,
y: 27
}, {
x: 14,
y: 27
}, {
x: 13,
y: 27
}, {
x: 12,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 28
}, {
x: 11,
y: 29
}];
for (var p = 0; p < pathCells.length; p++) {
if (pathCells[p].x === gridX && pathCells[p].y === gridY) {
isValidPosition = false;
break;
}
}
}
// Only move if position is valid
if (isValidPosition) {
self.targetX = x;
self.targetY = y;
self.isMoving = true;
}
};
self.down = function (x, y, obj) {
// Toggle range indicator if clicking directly on hero
var heroRadius = 40; // Hero hit radius
var dx = x - self.x;
var dy = y - self.y;
var distanceToHero = Math.sqrt(dx * dx + dy * dy);
if (distanceToHero <= heroRadius) {
// Toggle range display
self.showRange = !self.showRange;
var notification = game.addChild(new Notification(self.showRange ? "Hero range ON" : "Hero range OFF"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
} else {
// Move hero to clicked position
if (obj && obj.parent && obj.position) {
var localPos = self.parent.toLocal(obj.parent.toGlobal(obj.position));
self.moveTo(localPos.x, localPos.y);
} else {
// Fallback to using x, y coordinates directly
self.moveTo(x, y);
}
}
};
return self;
});
var HeroBullet = Container.expand(function (startX, startY, direction, damage, speed) {
var self = Container.call(this);
self.direction = direction;
self.damage = damage || 15;
self.speed = speed || 8;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('hero_bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0xFFD700; // Gold color for hero bullets
bulletGraphics.width = 25; // Make bullets slightly larger
bulletGraphics.height = 25;
self.update = function () {
// Move in the specified direction
self.x += Math.cos(self.direction) * self.speed;
self.y += Math.sin(self.direction) * self.speed;
// Check if bullet is off screen
if (self.x < -50 || self.x > 2048 + 50 || self.y < -50 || self.y > 2732 + 50) {
self.destroy();
return;
}
// Check collision with enemies - limit to first 10 enemies and use distance squared
var enemiesToCheck = Math.min(enemies.length, 10);
for (var i = 0; i < enemiesToCheck; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distanceSquared = dx * dx + dy * dy;
if (distanceSquared < 900) {
// 30 * 30 = 900
// Hit radius
// Apply damage to enemy
enemy.health -= self.damage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
self.destroy();
return;
}
}
};
return self;
});
var LeaderboardButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 250;
buttonBackground.height = 80;
buttonBackground.tint = 0x4444FF;
var buttonText = new Text2("Leaderboard", {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.down = function () {
showLeaderboard();
};
return self;
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
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 () {
// Show button when wave is finished (all enemies defeated) and not in progress
var waveFinished = waveIndicator && waveIndicator.gameStarted && !waveInProgress && enemies.length === 0;
var canStartNextWave = waveFinished && currentWave < totalWaves;
if (canStartNextWave) {
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 () {
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;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) 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 ReviveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 350;
buttonBackground.height = 100;
buttonBackground.tint = 0x00FF00;
var buttonText = new Text2("Revive Hero (5 💎)", {
size: 45,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.visible = false;
self.update = function () {
if (heroIsDead) {
self.visible = true;
self.alpha = diamonds >= 5 ? 1 : 0.5;
buttonBackground.tint = diamonds >= 5 ? 0x00FF00 : 0x888888;
} else {
self.visible = false;
}
};
self.down = function () {
if (heroIsDead) {
reviveHero();
}
};
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(self.towerType === 'fire' ? 'fire_tower' : '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;
case 'fire':
baseGraphics.tint = 0xFF4400;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
var isFireTower = self.towerType === 'fire';
// Add shadow for tower type label
var displayName = self.towerType === 'default' ? 'Standard' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabelShadow = new Text2(displayName, {
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 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 costText = isFireTower ? towerCost + " 💎" : towerCost.toString();
var costLabelShadow = new Text2(costText, {
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 costLabel = new Text2(costText, {
size: 50,
fill: isFireTower ? 0x00FFFF : 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 20 + 12;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = isFireTower ? diamonds >= getTowerCost(self.towerType) : gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Standardized method to get the current range of the tower
self.getRange = function () {
// 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
if (self.level === self.maxLevel) {
return 15 * CELL_SIZE; // Even longer range for max level sniper to reach bosses
}
return (5 + (self.level - 1) * 1.0) * CELL_SIZE;
// Slightly increased range progression
case 'splash':
// Splash: base 2, +0.2 per level (max ~4 blocks at max level)
return (2 + (self.level - 1) * 0.2) * CELL_SIZE;
case 'rapid':
// Rapid: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// Slow: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'fire':
// Fire: base 4, +0.6 per level
return (4 + (self.level - 1) * 0.6) * CELL_SIZE;
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;
self.rangeIndicator = null; // Will hold the range display circle
switch (self.id) {
case 'rapid':
self.fireRate = 30;
self.damage = 5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 75;
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'fire':
self.fireRate = 45;
self.damage = 20;
self.range = 4 * CELL_SIZE;
self.bulletSpeed = 6;
break;
}
var baseGraphics = self.attachAsset(self.id === 'fire' ? 'fire_tower' : '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;
case 'fire':
baseGraphics.tint = 0xFF4400;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
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);
var gunGraphics = gunContainer.attachAsset('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;
case 'fire':
towerLevelIndicator.tint = 0xFF4400;
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++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
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); // 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); // 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); // 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);
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];
// Enhanced upgrade animation with multiple effects
tween(levelDot, {
scaleX: 1.8,
scaleY: 1.8
}, {
duration: 200,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.bounceOut
});
}
});
// Animate the entire tower with upgrade effect
tween(self, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.elasticOut
});
}
});
// Animate range indicator to show new range
if (self.rangeIndicator && self.rangeIndicator.children[0]) {
var rangeGraphics = self.rangeIndicator.children[0];
var oldSize = rangeGraphics.width;
var newSize = self.getRange() * 2;
// Pulse animation to show range increase
tween(rangeGraphics, {
width: newSize * 1.3,
height: newSize * 1.3,
alpha: 0.4
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(rangeGraphics, {
width: newSize,
height: newSize,
alpha: 0.15
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
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()) {
// Boss enemies are always valid targets for all towers
if (enemy.isBoss) {
// Prioritize boss enemies highest
if (0 < bestPriority) {
bestPriority = 10000; // Set highest priority for bosses
bestTarget = enemy;
}
continue;
}
// 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 () {
// Create or update range indicator if it doesn't exist
if (!self.rangeIndicator) {
self.rangeIndicator = new Container();
self.rangeIndicator.isTowerRange = true;
self.rangeIndicator.tower = self;
var rangeGraphics = self.rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.15; // Semi-transparent
rangeGraphics.tint = 0x00FF00; // Green color for range
self.rangeIndicator.x = self.x;
self.rangeIndicator.y = self.y;
game.addChildAt(self.rangeIndicator, 0); // Add behind other objects
}
// Update range indicator position and size
if (self.rangeIndicator) {
self.rangeIndicator.x = self.x;
self.rangeIndicator.y = self.y;
var rangeGraphics = self.rangeIndicator.children[0];
if (rangeGraphics) {
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
}
}
// Find the best enemy target in range
var bestTarget = null;
var bestPriority = -1;
var currentRange = self.getRange();
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 <= currentRange) {
var priority = 0;
// Highest priority for boss enemies - ensure ALL towers always target bosses first
if (enemy.isBoss) {
priority = 100000; // Set absolute highest priority for bosses - increased from 50000
}
// Medium priority based on health (lower health = higher priority to finish them off)
priority += 1000 - enemy.health;
// For flying enemies, prioritize based on distance to goal
if (enemy.isFlying && enemy.flyingTarget) {
var goalDx = enemy.flyingTarget.x - enemy.cellX;
var goalDy = enemy.flyingTarget.y - enemy.cellY;
var distToGoal = Math.sqrt(goalDx * goalDx + goalDy * goalDy);
priority += (100 - distToGoal) * 10; // Closer to goal = higher priority
} else {
// For ground enemies, use path score if available
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
priority += (10000 - cell.score) / 10; // Lower score = closer to goal = higher priority
}
}
// Small bonus for closer enemies to break ties
priority += (currentRange - distance) / 10;
if (priority > bestPriority) {
bestPriority = priority;
bestTarget = enemy;
}
}
}
// Fire at the best target if fire rate allows
if (bestTarget && LK.ticks - self.lastFired >= self.fireRate) {
self.targetEnemy = bestTarget;
var dx = bestTarget.x - self.x;
var dy = bestTarget.y - self.y;
var angle = Math.atan2(dy, dx);
// Aim gun at target
gunContainer.rotation = angle;
// Fire bullet
var bulletX = self.x + Math.cos(angle) * 40;
var bulletY = self.y + Math.sin(angle) * 40;
var bullet = new Bullet(bulletX, bulletY, bestTarget, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// 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 'slow':
bullet.children[0].tint = 0x9900FF;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'fire':
bullet.children[0].tint = 0xFF4400;
bullet.children[0].width = 45;
bullet.children[0].height = 45;
break;
}
game.addChild(bullet);
bullets.push(bullet);
bestTarget.bulletsTargetingThis.push(bullet);
self.lastFired = LK.ticks;
// Create visual muzzle flash effect
var muzzleFlash = new EffectIndicator(self.x + Math.cos(angle) * 40, self.y + Math.sin(angle) * 40, 'attack');
game.addChild(muzzleFlash);
// Play sad fire sound for fire towers
if (self.id === 'fire') {
LK.getSound('sad_fire_attack').play();
}
// Enhanced gun recoil animation
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
var recoilDistance = 8;
var recoilX = -Math.cos(angle) * recoilDistance;
var recoilY = -Math.sin(angle) * recoilDistance;
tween(gunContainer, {
x: recoilX,
y: recoilY,
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(gunContainer, {
x: 0,
y: 0,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 120,
easing: tween.easeIn
});
}
});
} else if (bestTarget) {
// Aim at target even if not firing
self.targetEnemy = bestTarget;
var dx = bestTarget.x - self.x;
var dy = bestTarget.y - self.y;
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
} else {
self.targetEnemy = null;
}
};
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 () {
if (self.targetEnemy) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// 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 'slow':
bullet.children[0].tint = 0x9900FF;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'fire':
bullet.children[0].tint = 0xFF4400;
bullet.children[0].width = 45;
bullet.children[0].height = 45;
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;
// Mark cells as occupied by tower
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 mark as occupied, don't change to wall type
cell.hasTower = true;
}
}
}
self.refreshCellsInRange();
};
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('towerpreview', {
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 isFireTower = self.towerType === 'fire';
self.hasEnoughGold = isFireTower ? diamonds >= getTowerCost(self.towerType) : gold >= getTowerCost(self.towerType);
// 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;
case 'fire':
previewGraphics.tint = 0xFF4400;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
// Check if tower is within valid grid bounds
if (self.gridX < 0 || self.gridX + 1 >= 24 || self.gridY < 5 || self.gridY + 1 >= 29) {
validGridPlacement = false;
} else {
// Check all 4 cells the tower will occupy
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
// Check if cell exists
if (!cell) {
validGridPlacement = false;
break;
}
// Only block if this is part of the enemy path
var pathCells = [{
x: 11,
y: 4
}, {
x: 11,
y: 5
}, {
x: 11,
y: 6
}, {
x: 11,
y: 7
}, {
x: 11,
y: 8
}, {
x: 10,
y: 8
}, {
x: 9,
y: 8
}, {
x: 8,
y: 8
}, {
x: 7,
y: 8
}, {
x: 6,
y: 8
}, {
x: 5,
y: 8
}, {
x: 4,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 9
}, {
x: 3,
y: 10
}, {
x: 3,
y: 11
}, {
x: 3,
y: 12
}, {
x: 4,
y: 12
}, {
x: 5,
y: 12
}, {
x: 6,
y: 12
}, {
x: 7,
y: 12
}, {
x: 8,
y: 12
}, {
x: 9,
y: 12
}, {
x: 10,
y: 12
}, {
x: 11,
y: 12
}, {
x: 12,
y: 12
}, {
x: 13,
y: 12
}, {
x: 14,
y: 12
}, {
x: 15,
y: 12
}, {
x: 16,
y: 12
}, {
x: 17,
y: 12
}, {
x: 18,
y: 12
}, {
x: 19,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 13
}, {
x: 20,
y: 14
}, {
x: 20,
y: 15
}, {
x: 20,
y: 16
}, {
x: 20,
y: 17
}, {
x: 20,
y: 18
}, {
x: 19,
y: 18
}, {
x: 18,
y: 18
}, {
x: 17,
y: 18
}, {
x: 16,
y: 18
}, {
x: 15,
y: 18
}, {
x: 14,
y: 18
}, {
x: 13,
y: 18
}, {
x: 12,
y: 18
}, {
x: 11,
y: 18
}, {
x: 10,
y: 18
}, {
x: 9,
y: 18
}, {
x: 8,
y: 18
}, {
x: 7,
y: 18
}, {
x: 6,
y: 18
}, {
x: 5,
y: 18
}, {
x: 4,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 19
}, {
x: 3,
y: 20
}, {
x: 3,
y: 21
}, {
x: 3,
y: 22
}, {
x: 3,
y: 23
}, {
x: 4,
y: 23
}, {
x: 5,
y: 23
}, {
x: 6,
y: 23
}, {
x: 7,
y: 23
}, {
x: 8,
y: 23
}, {
x: 9,
y: 23
}, {
x: 10,
y: 23
}, {
x: 11,
y: 23
}, {
x: 12,
y: 23
}, {
x: 13,
y: 23
}, {
x: 14,
y: 23
}, {
x: 15,
y: 23
}, {
x: 16,
y: 23
}, {
x: 17,
y: 23
}, {
x: 18,
y: 23
}, {
x: 19,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 24
}, {
x: 20,
y: 25
}, {
x: 20,
y: 26
}, {
x: 20,
y: 27
}, {
x: 19,
y: 27
}, {
x: 18,
y: 27
}, {
x: 17,
y: 27
}, {
x: 16,
y: 27
}, {
x: 15,
y: 27
}, {
x: 14,
y: 27
}, {
x: 13,
y: 27
}, {
x: 12,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 28
}, {
x: 11,
y: 29
}];
var isOnPath = false;
for (var p = 0; p < pathCells.length; p++) {
if (pathCells[p].x === self.gridX + i && pathCells[p].y === self.gridY + j) {
isOnPath = true;
break;
}
}
if (isOnPath) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
// Check if any enemies would block placement
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;
var isFireTower = self.towerType === 'fire';
self.hasEnoughGold = isFireTower ? diamonds >= getTowerCost(self.towerType) : 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 UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' 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('notification', {
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('notification', {
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('notification', {
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;
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) {
cell.hasTower = false;
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();
};
// Override destroy method to clean up range indicator
var originalDestroy = self.destroy;
self.destroy = function () {
if (self.rangeIndicator && self.rangeIndicator.parent) {
game.removeChild(self.rangeIndicator);
}
if (originalDestroy) {
originalDestroy.call(self);
}
};
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('notification', {
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 () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 1; // Start with wave 1
waveTimer = 0;
waveInProgress = true;
waveSpawned = false;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0 && i + 1 >= 10;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0xFFFF00;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Boss Normal";
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to the color of the wave type
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
break;
case 'fast':
block.tint = 0x00AAFF;
break;
case 'immune':
block.tint = 0xAA0000;
break;
case 'flying':
block.tint = 0xFFFF00;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave) {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// 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);
// Add boss prefix for boss waves (every 10th wave starting from wave 10)
if (waveNumber % 10 === 0 && waveNumber >= 10) {
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];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
// Remove automatic wave progression - waves now start manually via NextWaveButton
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a0a0a
});
/****
* Game Code
****/
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 heroBullets = [];
var hero = null;
var bossHeroes = [];
var defenses = [];
var selectedTower = null;
var gold = 100;
var diamonds = 5;
var lives = 20;
var score = 0;
var heroHealth = 500;
var heroMaxHealth = 500;
var heroIsDead = false;
var leaderboardData = storage.leaderboard || [];
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
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 diamondText = new Text2('Diamonds: ' + diamonds, {
size: 60,
fill: 0x00FFFF,
weight: 800
});
diamondText.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 scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var heroHealthText = new Text2('Hero Health: ' + heroHealth + '/' + heroMaxHealth, {
size: 60,
fill: 0xFF6600,
weight: 800
});
heroHealthText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(diamondText);
LK.gui.top.addChild(livesText);
LK.gui.top.addChild(scoreText);
LK.gui.top.addChild(heroHealthText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
diamondText.x = -spacing;
diamondText.y = topMargin + 80;
scoreText.x = spacing;
scoreText.y = topMargin;
heroHealthText.x = spacing;
heroHealthText.y = topMargin + 80;
function updateUI() {
goldText.setText('Gold: ' + gold);
diamondText.setText('Diamonds: ' + diamonds);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
heroHealthText.setText('Hero Health: ' + heroHealth + '/' + heroMaxHealth);
}
function setGold(value) {
gold = value;
updateUI();
}
function setDiamonds(value) {
diamonds = value;
updateUI();
}
// Create horror atmosphere background
var horrorBackgroundLayer = new Container();
var horrorBg = horrorBackgroundLayer.attachAsset('cell', {
anchorX: 0,
anchorY: 0,
scaleX: 30,
scaleY: 40
});
horrorBg.tint = 0x2d0a0a; // Dark red horror tint
horrorBg.alpha = 0.6;
horrorBg.x = 0;
horrorBg.y = 0;
// Add subtle pulsing horror effect
var horrorPulse = horrorBackgroundLayer.attachAsset('cell', {
anchorX: 0,
anchorY: 0,
scaleX: 30,
scaleY: 40
});
horrorPulse.tint = 0x4a0000; // Darker red for pulsing
horrorPulse.alpha = 0.2;
horrorPulse.x = 0;
horrorPulse.y = 0;
// Animate the horror pulse effect
tween(horrorPulse, {
alpha: 0.4
}, {
duration: 2000,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
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);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
grid.pathFind();
grid.renderDebug();
// Apply horror theme to grid
grid.setBackgroundColor = function (color) {
// Override to maintain horror atmosphere
};
debugLayer.addChild(grid);
game.addChild(horrorBackgroundLayer);
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) {
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 = 5;
switch (towerType) {
case 'rapid':
cost = 15;
break;
case 'sniper':
cost = 25;
break;
case 'splash':
cost = 35;
break;
case 'slow':
cost = 45;
break;
case 'poison':
cost = 55;
break;
case 'fire':
cost = 10; // Fire tower costs 10 diamonds
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function saveToLeaderboard(playerScore) {
// Create a simple name input interface
var nameInputContainer = new Container();
nameInputContainer.isNameInput = true;
game.addChild(nameInputContainer);
// Background
var background = nameInputContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
background.width = 1200;
background.height = 600;
background.tint = 0x333333;
background.alpha = 0.95;
nameInputContainer.x = 2048 / 2;
nameInputContainer.y = 2732 / 2;
// Title
var titleText = new Text2("🎉 NEW HIGH SCORE! 🎉", {
size: 70,
fill: 0xFFD700,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 0;
titleText.y = -200;
nameInputContainer.addChild(titleText);
// Score display
var scoreText = new Text2("Score: " + playerScore + " (Wave " + currentWave + ")", {
size: 60,
fill: 0xFFFFFF,
weight: 600
});
scoreText.anchor.set(0.5, 0.5);
scoreText.x = 0;
scoreText.y = -120;
nameInputContainer.addChild(scoreText);
// Instructions
var instructText = new Text2("Click a button to choose your name:", {
size: 45,
fill: 0xCCCCCC,
weight: 400
});
instructText.anchor.set(0.5, 0.5);
instructText.x = 0;
instructText.y = -40;
nameInputContainer.addChild(instructText);
// Name buttons
var nameOptions = ["Player", "Hero", "Champion", "Legend", "Master"];
var buttonWidth = 200;
var buttonSpacing = 220;
var startX = -(nameOptions.length - 1) * buttonSpacing / 2;
for (var i = 0; i < nameOptions.length; i++) {
var nameButton = new Container();
var buttonBg = nameButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = buttonWidth;
buttonBg.height = 80;
buttonBg.tint = 0x0066CC;
var buttonText = new Text2(nameOptions[i], {
size: 40,
fill: 0xFFFFFF,
weight: 600
});
buttonText.anchor.set(0.5, 0.5);
nameButton.addChild(buttonText);
nameButton.x = startX + i * buttonSpacing;
nameButton.y = 50;
nameInputContainer.addChild(nameButton);
// Create closure to capture the name
(function (name) {
nameButton.down = function () {
submitScore(name, playerScore);
game.removeChild(nameInputContainer);
};
})(nameOptions[i]);
}
// Anonymous button
var anonButton = new Container();
var anonBg = anonButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
anonBg.width = 250;
anonBg.height = 80;
anonBg.tint = 0x666666;
var anonText = new Text2("Anonymous", {
size: 40,
fill: 0xFFFFFF,
weight: 600
});
anonText.anchor.set(0.5, 0.5);
anonButton.addChild(anonText);
anonButton.x = 0;
anonButton.y = 160;
nameInputContainer.addChild(anonButton);
anonButton.down = function () {
submitScore("Anonymous", playerScore);
game.removeChild(nameInputContainer);
};
}
function submitScore(playerName, playerScore) {
var entry = {
name: playerName,
score: playerScore,
wave: currentWave,
timestamp: new Date().getTime()
};
leaderboardData.push(entry);
leaderboardData.sort(function (a, b) {
return b.score - a.score;
});
if (leaderboardData.length > 10) {
leaderboardData = leaderboardData.slice(0, 10);
}
storage.leaderboard = leaderboardData;
// Show confirmation
var notification = game.addChild(new Notification("Score saved to leaderboard as " + playerName + "!"));
notification.x = 2048 / 2;
notification.y = 2732 / 2 - 100;
}
function showLeaderboard() {
// Remove any existing leaderboard display
var existingLeaderboard = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isLeaderboard) {
existingLeaderboard = game.children[i];
break;
}
}
if (existingLeaderboard) {
game.removeChild(existingLeaderboard);
return;
}
// Create leaderboard container
var leaderboardContainer = new Container();
leaderboardContainer.isLeaderboard = true;
game.addChild(leaderboardContainer);
// Background
var background = leaderboardContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
background.width = 1800;
background.height = 1400;
background.tint = 0x222222;
background.alpha = 0.95;
leaderboardContainer.x = 2048 / 2;
leaderboardContainer.y = 2732 / 2;
// Title
var titleText = new Text2("🏆 LEADERBOARD 🏆", {
size: 80,
fill: 0xFFD700,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 0;
titleText.y = -600;
leaderboardContainer.addChild(titleText);
// Headers
var headerText = new Text2("RANK NAME SCORE WAVE", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
headerText.anchor.set(0.5, 0.5);
headerText.x = 0;
headerText.y = -450;
leaderboardContainer.addChild(headerText);
// Leaderboard entries
if (leaderboardData.length === 0) {
var noScoresText = new Text2("No high scores yet!\nPlay the game to set the first record!", {
size: 60,
fill: 0xAAAAAA,
weight: 400
});
noScoresText.anchor.set(0.5, 0.5);
noScoresText.x = 0;
noScoresText.y = 0;
leaderboardContainer.addChild(noScoresText);
} else {
for (var i = 0; i < Math.min(leaderboardData.length, 10); i++) {
var entry = leaderboardData[i];
var rank = i + 1;
var name = entry.name || "Anonymous";
var score = entry.score || 0;
var wave = entry.wave || 1;
// Truncate name if too long
if (name.length > 15) {
name = name.substring(0, 12) + "...";
}
// Format the entry text with proper spacing
var entryText = rank + ". " + name;
// Pad name to consistent length
while (entryText.length < 25) {
entryText += " ";
}
entryText += score;
// Pad score to consistent length
var scoreStr = score.toString();
while (scoreStr.length < 8) {
scoreStr = " " + scoreStr;
}
entryText = rank + ". " + name;
while (entryText.length < 30) {
entryText += " ";
}
entryText += scoreStr + " " + wave;
var color = 0xFFFFFF;
if (rank === 1) color = 0xFFD700; // Gold for 1st
else if (rank === 2) color = 0xC0C0C0; // Silver for 2nd
else if (rank === 3) color = 0xCD7F32; // Bronze for 3rd
var entryDisplay = new Text2(entryText, {
size: 45,
fill: color,
weight: rank <= 3 ? 800 : 400
});
entryDisplay.anchor.set(0.5, 0.5);
entryDisplay.x = 0;
entryDisplay.y = -350 + i * 80;
leaderboardContainer.addChild(entryDisplay);
}
}
// Close button
var closeButton = new Container();
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 200;
closeBackground.height = 80;
closeBackground.tint = 0xCC0000;
var closeText = new Text2("CLOSE", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = 0;
closeButton.y = 550;
leaderboardContainer.addChild(closeButton);
closeButton.down = function () {
game.removeChild(leaderboardContainer);
};
// Auto-close after 10 seconds
LK.setTimeout(function () {
if (leaderboardContainer.parent) {
game.removeChild(leaderboardContainer);
}
}, 10000);
}
function reviveHero() {
if (heroIsDead && diamonds >= 5) {
setDiamonds(diamonds - 5);
heroHealth = heroMaxHealth;
heroIsDead = false;
hero.alpha = 1;
hero.visible = true;
updateUI();
var notification = game.addChild(new Notification("Hero revived! -5 diamonds"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
return true;
} else if (heroIsDead && diamonds < 5) {
var notification = game.addChild(new Notification("Need 5 diamonds to revive hero!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
return false;
}
return false;
}
var pathfindingTimer = 0;
var pathfindingInterval = 10; // Update pathfinding every 10 frames instead of every frame
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
var isFireTower = towerType === 'fire';
var canAfford = isFireTower ? diamonds >= towerCost : gold >= towerCost;
if (canAfford) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
if (isFireTower) {
setDiamonds(diamonds - towerCost);
var notification = game.addChild(new Notification("Fire tower purchased for " + towerCost + " diamonds!"));
} else {
setGold(gold - towerCost);
var notification = game.addChild(new Notification("Tower purchased for " + towerCost + " gold!"));
}
notification.x = 2048 / 2;
notification.y = grid.height - 50;
grid.pathFind();
grid.renderDebug();
return true;
} else {
if (isFireTower) {
var notification = game.addChild(new Notification("You don't have enough diamonds!"));
} 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) {
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Check if touching a source tower for dragging
var touchedSourceTower = false;
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) {
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);
touchedSourceTower = true;
break;
}
}
// If not touching a source tower, move hero to touched position
if (!touchedSourceTower && hero) {
hero.moveTo(x, y);
}
};
game.move = function (x, y, obj) {
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) {
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 && towerPreview.hasEnoughGold) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} 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.hasEnoughGold) {
var notification = game.addChild(new Notification("Not enough gold!"));
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;
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);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
nextWaveButton.y = 2732 - 100 + 20;
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
var reviveButton = new ReviveButton();
reviveButton.x = 2048 / 2;
reviveButton.y = 2732 - 200;
game.addChild(reviveButton);
var leaderboardButton = new LeaderboardButton();
leaderboardButton.x = 200;
leaderboardButton.y = 2732 - 100;
game.addChild(leaderboardButton);
var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison', 'fire'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
// Create hero character
hero = new Hero();
hero.x = 2048 / 2; // Center horizontally
hero.y = 2732 - 300; // Near bottom of screen
game.addChild(hero);
// Start sad background music
LK.playMusic('sad_background_music');
game.update = function () {
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
// Get wave type and enemy count from the wave indicator
var waveType = waveIndicator.getWaveType(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
// Check if this is a boss wave - bosses appear every 10 waves starting from wave 10
var isBossWave = currentWave % 10 === 0 && currentWave >= 10;
if (isBossWave) {
// Boss waves have just 1 enemy regardless of what the wave indicator says
enemyCount = 1;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ BOSS WAVE " + currentWave + "! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
// Spawn boss hero that follows the path
var bossHero = new Hero();
bossHero.isBossHero = true;
bossHero.health = 2000;
bossHero.maxHealth = 2000;
bossHero.damage = 25; // Stronger than normal hero
bossHero.fireRate = 15; // Faster fire rate
bossHero.range = 800; // Longer range
bossHero.currentCellX = 11; // Start at path entry
bossHero.currentCellY = 4;
bossHero.waypointIndex = 0;
bossHero.x = grid.x + bossHero.currentCellX * CELL_SIZE;
bossHero.y = grid.y + bossHero.currentCellY * CELL_SIZE;
// Make boss hero visually distinct
var heroGraphics = bossHero.children[0];
heroGraphics.tint = 0xFFD700; // Gold color for boss hero
heroGraphics.scaleX = 2.5; // Larger than normal hero
heroGraphics.scaleY = 2.5;
game.addChild(bossHero);
bossHeroes.push(bossHero);
var heroNotification = game.addChild(new Notification("🛡️ HERO REINFORCEMENT DEPLOYED! 🛡️"));
heroNotification.x = 2048 / 2;
heroNotification.y = grid.height - 250;
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use exponential scaling for health
var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// All enemy types now spawn in the middle 6 tiles at the top spacing
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
// Find a column that isn't occupied by another enemy that's not yet in view
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
// Check if any enemy is already in this column but not yet in view
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);
}
}
// If all columns are occupied, use original random method
var spawnX;
if (availableColumns.length > 0) {
// Choose a random unoccupied column
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
// Fallback to random if all columns are occupied
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);
}
}
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;
}
}
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;
}
// Boss enemies give more gold and score - increased gold rewards
var goldEarned = enemy.isBoss ? Math.floor(200 + (enemy.waveNumber - 1) * 20) : Math.floor(6 + (enemy.waveNumber - 1) * 2);
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Diamond drop chance - rare drop from enemies
var diamondDropChance = enemy.isBoss ? 0.5 : 0.05; // 50% chance for bosses, 5% for normal enemies
if (Math.random() < diamondDropChance) {
var diamondsEarned = enemy.isBoss ? Math.floor(3 + (enemy.waveNumber - 1) * 0.5) : 1;
var diamondIndicator = new DiamondIndicator(diamondsEarned, enemy.x, enemy.y);
game.addChild(diamondIndicator);
setDiamonds(diamonds + diamondsEarned);
}
// 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 - 1);
// Also damage hero when enemy reaches goal
if (!heroIsDead) {
var heroDamage = enemy.isBoss ? 100 : 25;
heroHealth = Math.max(0, heroHealth - heroDamage);
if (heroHealth <= 0) {
heroIsDead = true;
hero.alpha = 0.3;
var notification = game.addChild(new Notification("Hero died! Use diamonds to revive!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
}
updateUI();
if (lives <= 0) {
saveToLeaderboard(score);
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);
}
}
// Clean up hero bullets
for (var i = heroBullets.length - 1; i >= 0; i--) {
if (!heroBullets[i].parent) {
heroBullets.splice(i, 1);
}
}
// Clean up boss heroes that are no longer alive
for (var i = bossHeroes.length - 1; i >= 0; i--) {
var bossHero = bossHeroes[i];
if (bossHero.health <= 0) {
game.removeChild(bossHero);
bossHeroes.splice(i, 1);
var notification = game.addChild(new Notification("Hero reinforcement defeated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
} else if (!bossHero.parent) {
bossHeroes.splice(i, 1);
}
}
// Only update tower preview every 3 frames
if (towerPreview.visible && LK.ticks % 3 === 0) {
towerPreview.checkPlacement();
}
// Update pathfinding less frequently
pathfindingTimer++;
if (pathfindingTimer >= pathfindingInterval) {
pathfindingTimer = 0;
// Only update debug rendering occasionally
if (LK.ticks % 30 === 0) {
grid.renderDebug();
}
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
saveToLeaderboard(score);
LK.showYouWin();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* 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 () {
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
self.targetEnemy.health -= self.damage;
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)
otherEnemy.health -= self.damage * 0.5;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply slow effect
// Make slow percentage scale with tower level (default 50%, up to 80% at max level)
var slowPct = 0.5;
if (self.sourceTowerLevel !== undefined) {
// Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
self.targetEnemy.slowDuration = 180; // Reset duration
}
}
} 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 === 'fire') {
// Create visual fire effect
var fireEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'fire');
game.addChild(fireEffect);
// Apply burning effect - affects all enemies
self.targetEnemy.burning = true;
self.targetEnemy.burnDamage = self.damage * 0.3; // 30% of original damage per tick
self.targetEnemy.burnDuration = 240; // 4 seconds at 60 FPS
// Fire damage to nearby enemies in radius
var fireRadius = CELL_SIZE * 2;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var fireDx = otherEnemy.x - self.targetEnemy.x;
var fireDy = otherEnemy.y - self.targetEnemy.y;
var fireDistance = Math.sqrt(fireDx * fireDx + fireDy * fireDy);
if (fireDistance <= fireRadius) {
// Apply burn damage to nearby enemies
otherEnemy.health -= self.damage * 0.4; // 40% of original damage
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
// Apply burning effect
otherEnemy.burning = true;
otherEnemy.burnDamage = self.damage * 0.2; // 20% burn damage for nearby enemies
otherEnemy.burnDuration = 180; // 3 seconds
}
}
}
} 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);
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 = true;
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;
}
while (debugArrows.length > data.targets.length) {
self.removeChild(debugArrows.pop());
}
for (var a = 0; a < data.targets.length; a++) {
var destination = data.targets[a];
var ox = destination.x - data.x;
var oy = destination.y - data.y;
var angle = Math.atan2(oy, ox);
if (!debugArrows[a]) {
debugArrows[a] = LK.getAsset('arrow', {
anchorX: -.5,
anchorY: 0.5
});
debugArrows[a].alpha = .5;
self.addChildAt(debugArrows[a], 1);
}
debugArrows[a].rotation = angle;
}
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0x008800;
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
};
});
var DiamondIndicator = 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 diamondText = new Text2("+" + value, {
size: 45,
fill: 0x00FFFF,
weight: 800
});
diamondText.anchor.set(0.5, 0.5);
self.addChild(diamondText);
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;
});
// 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;
case 'fire':
effectGraphics.tint = 0xFF4400;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 2;
break;
case 'attack':
effectGraphics.tint = 0xFF0000;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 0.8;
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;
self.attackDamage = 3; // Base attack damage reduced to 1-6 range
self.attackRate = 60; // Frames between attacks (1 second at 60 FPS)
self.lastAttacked = 0; // Track when enemy last attacked
// 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;
self.attackDamage = 2; // Low damage but fast
self.attackRate = 45; // Attacks faster
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
self.attackDamage = 4; // Moderate damage
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
self.attackDamage = 3; // Moderate damage
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
self.attackDamage = 1; // Very low damage but swarm
self.attackRate = 40; // But attack frequently
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave >= 10) {
self.isBoss = true;
// Boss enemies have 25000 health and are larger
self.maxHealth = 25000;
// Bosses deal slightly more damage but still in 1-6 range
self.attackDamage = 6; // Max damage for bosses
// Attack more frequently
self.attackRate = 30;
// 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 !== '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 () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// 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)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed && !self.burning) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle burning effect
if (self.burning) {
// Visual indication of burning status
if (!self.burnEffect) {
self.burnEffect = true;
}
// Apply burn damage every 20 frames (3 times per second)
if (LK.ticks % 20 === 0) {
self.health -= self.burnDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.burnDuration--;
if (self.burnDuration <= 0) {
self.burning = false;
self.burnEffect = false;
// Only reset tint if not slowed or poisoned
if (!self.slowed && !self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.burning && self.poisoned && self.slowed) {
// All three effects: fire + poison + slow (orange-purple-blue mix)
enemyGraphics.tint = 0xFF6600;
} else if (self.burning && self.poisoned) {
// Fire + poison: red-green mix (orange)
enemyGraphics.tint = 0xFF7700;
} else if (self.burning && self.slowed) {
// Fire + slow: red-purple mix
enemyGraphics.tint = 0xFF2266;
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.burning) {
enemyGraphics.tint = 0xFF4400; // Fire effect color
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} 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
});
}
}
}
}
// Check for attack targets (hero and defense towers) - optimize by checking less frequently
var canAttack = LK.ticks - self.lastAttacked >= self.attackRate;
if (canAttack && LK.ticks % 3 === 0) {
// Only check every 3 frames
var attackTarget = null;
var attackDistance = Infinity;
// Check if hero is in attack range
if (hero && !heroIsDead) {
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var heroDistance = dx * dx + dy * dy; // Use squared distance for faster comparison
if (heroDistance < 10000) {
// 100 * 100 = 10000
// Attack range of 100 pixels
attackTarget = hero;
attackDistance = heroDistance;
}
}
// Check for nearby defense towers - limit to first 5 towers
var towersToCheck = Math.min(towers.length, 5);
for (var i = 0; i < towersToCheck; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var towerDistance = dx * dx + dy * dy; // Use squared distance
if (towerDistance < 14400 && towerDistance < attackDistance) {
// 120 * 120 = 14400
// Prefer closer targets
attackTarget = tower;
attackDistance = towerDistance;
}
}
// Attack the target
if (attackTarget) {
self.lastAttacked = LK.ticks;
if (attackTarget === hero) {
// Attack hero
heroHealth = Math.max(0, heroHealth - self.attackDamage);
if (heroHealth <= 0 && !heroIsDead) {
heroIsDead = true;
hero.alpha = 0.3;
var notification = game.addChild(new Notification("Hero destroyed by enemy!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
updateUI();
// Visual effect for hero damage
LK.effects.flashObject(hero, 0xFF0000, 300);
} else {
// Attack tower - towers have no health, so just show visual effect
LK.effects.flashObject(attackTarget, 0xFF0000, 300);
// Create damage indicator
var damageIndicator = new Notification("-" + self.attackDamage);
damageIndicator.x = attackTarget.x;
damageIndicator.y = attackTarget.y - 50;
game.addChild(damageIndicator);
}
// Visual attack effect
var attackEffect = new EffectIndicator(self.x, self.y, 'attack');
game.addChild(attackEffect);
}
}
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
*/
// Define the predefined path coordinates - extended longer path with more turns
var pathCells = [{
x: 11,
y: 4
}, {
x: 11,
y: 5
}, {
x: 11,
y: 6
}, {
x: 11,
y: 7
}, {
x: 11,
y: 8
}, {
x: 10,
y: 8
}, {
x: 9,
y: 8
}, {
x: 8,
y: 8
}, {
x: 7,
y: 8
}, {
x: 6,
y: 8
}, {
x: 5,
y: 8
}, {
x: 4,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 9
}, {
x: 3,
y: 10
}, {
x: 3,
y: 11
}, {
x: 3,
y: 12
}, {
x: 4,
y: 12
}, {
x: 5,
y: 12
}, {
x: 6,
y: 12
}, {
x: 7,
y: 12
}, {
x: 8,
y: 12
}, {
x: 9,
y: 12
}, {
x: 10,
y: 12
}, {
x: 11,
y: 12
}, {
x: 12,
y: 12
}, {
x: 13,
y: 12
}, {
x: 14,
y: 12
}, {
x: 15,
y: 12
}, {
x: 16,
y: 12
}, {
x: 17,
y: 12
}, {
x: 18,
y: 12
}, {
x: 19,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 13
}, {
x: 20,
y: 14
}, {
x: 20,
y: 15
}, {
x: 20,
y: 16
}, {
x: 20,
y: 17
}, {
x: 20,
y: 18
}, {
x: 19,
y: 18
}, {
x: 18,
y: 18
}, {
x: 17,
y: 18
}, {
x: 16,
y: 18
}, {
x: 15,
y: 18
}, {
x: 14,
y: 18
}, {
x: 13,
y: 18
}, {
x: 12,
y: 18
}, {
x: 11,
y: 18
}, {
x: 10,
y: 18
}, {
x: 9,
y: 18
}, {
x: 8,
y: 18
}, {
x: 7,
y: 18
}, {
x: 6,
y: 18
}, {
x: 5,
y: 18
}, {
x: 4,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 19
}, {
x: 3,
y: 20
}, {
x: 3,
y: 21
}, {
x: 3,
y: 22
}, {
x: 3,
y: 23
}, {
x: 4,
y: 23
}, {
x: 5,
y: 23
}, {
x: 6,
y: 23
}, {
x: 7,
y: 23
}, {
x: 8,
y: 23
}, {
x: 9,
y: 23
}, {
x: 10,
y: 23
}, {
x: 11,
y: 23
}, {
x: 12,
y: 23
}, {
x: 13,
y: 23
}, {
x: 14,
y: 23
}, {
x: 15,
y: 23
}, {
x: 16,
y: 23
}, {
x: 17,
y: 23
}, {
x: 18,
y: 23
}, {
x: 19,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 24
}, {
x: 20,
y: 25
}, {
x: 20,
y: 26
}, {
x: 20,
y: 27
}, {
x: 19,
y: 27
}, {
x: 18,
y: 27
}, {
x: 17,
y: 27
}, {
x: 16,
y: 27
}, {
x: 15,
y: 27
}, {
x: 14,
y: 27
}, {
x: 13,
y: 27
}, {
x: 12,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 28
}, {
x: 11,
y: 29
}];
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0;
// Check if this cell is part of the predefined path
var isPathCell = false;
for (var p = 0; p < pathCells.length; p++) {
if (pathCells[p].x === i && pathCells[p].y === j) {
isPathCell = true;
break;
}
}
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = isPathCell ? 0 : 1; // Only allow path cells to be open
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = isPathCell ? 0 : 1; // Only allow path cells to be open
} else {
cellType = isPathCell ? 0 : 1; // Only allow path cells to be open
}
} else {
// For areas outside the spawn/goal columns, only allow path cells
if (j > 4 && j < gridHeight - 4) {
cellType = isPathCell ? 0 : 1;
}
}
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) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
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) {
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;
}
}
}
}
var iterations = 0;
var maxIterations = 5000; // Prevent infinite loops
while (toProcess.length && iterations < maxIterations) {
iterations++;
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.left.type != 1) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
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);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
// Only check first 20 enemies to reduce lag
var enemiesToCheck = Math.min(enemies.length, 20);
for (var a = 0; a < enemiesToCheck; 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 && cell.type == 3) {
// Enemy reached the goal - remove them
return true;
}
// Update shadow position for flying enemies
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;
}
}
// Define the complete path that all enemies must follow
var pathWaypoints = [{
x: 11,
y: 4
}, {
x: 11,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 29
}];
// Initialize waypoint index if not set
if (enemy.waypointIndex === undefined) {
enemy.waypointIndex = 0;
}
// Check if enemy has reached the entry area
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached entry area, move down to first waypoint
if (!hasReachedEntryArea) {
enemy.currentCellY += enemy.speed;
// Ensure enemy moves to the correct X position (path entry)
if (Math.abs(enemy.currentCellX - 11) > 0.1) {
var xDirection = enemy.currentCellX < 11 ? 1 : -1;
enemy.currentCellX += xDirection * enemy.speed * 0.5;
}
// Update position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update cell coordinates when reaching entry area
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// Handle flying enemies - they go straight to goal
if (enemy.isFlying) {
if (!enemy.flyingTarget) {
enemy.flyingTarget = self.goals[0];
}
if (enemy.flyingTarget) {
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) {
return true; // Reached goal
}
var angle = Math.atan2(oy, ox);
// Rotate enemy to face movement direction
if (enemy.children[0]) {
tween(enemy.children[0], {
rotation: angle
}, {
duration: 200,
easing: tween.easeOut
});
}
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
}
return false;
}
// Handle ground enemies following waypoint path
var currentWaypoint = pathWaypoints[enemy.waypointIndex];
if (!currentWaypoint && enemy.waypointIndex >= pathWaypoints.length) {
// All waypoints completed, head to goal
var goalCell = self.goals[0];
if (goalCell) {
currentWaypoint = {
x: goalCell.x,
y: goalCell.y
};
}
}
if (currentWaypoint) {
var ox = currentWaypoint.x - enemy.currentCellX;
var oy = currentWaypoint.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
// Check if reached current waypoint
if (dist < 0.3) {
enemy.waypointIndex++;
// Get next waypoint or goal
if (enemy.waypointIndex < pathWaypoints.length) {
currentWaypoint = pathWaypoints[enemy.waypointIndex];
ox = currentWaypoint.x - enemy.currentCellX;
oy = currentWaypoint.y - enemy.currentCellY;
dist = Math.sqrt(ox * ox + oy * oy);
} else {
// Head to goal
var goalCell = self.goals[0];
if (goalCell) {
ox = goalCell.x - enemy.currentCellX;
oy = goalCell.y - enemy.currentCellY;
dist = Math.sqrt(ox * ox + oy * oy);
}
}
}
// Move toward current target
if (dist > enemy.speed) {
var angle = Math.atan2(oy, ox);
// Smoothly rotate enemy to face movement direction
if (enemy.children[0]) {
tween(enemy.children[0], {
rotation: angle
}, {
duration: 200,
easing: tween.easeOut
});
}
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
} else {
// Close enough to target, snap to it
enemy.currentCellX = currentWaypoint.x;
enemy.currentCellY = currentWaypoint.y;
}
}
// Update final position and cell coordinates
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
return false;
};
});
var Hero = Container.expand(function () {
var self = Container.call(this);
self.speed = 4;
self.fireRate = 20; // Frames between shots
self.lastFired = 0;
self.damage = 15;
self.bulletSpeed = 8;
self.isBossHero = false; // Flag to distinguish boss level hero
self.waypointIndex = 0; // For path following
self.pathSpeed = 0.8; // Speed for following path
self.currentCellX = 0;
self.currentCellY = 0;
self.maxHealth = 2000; // Boss hero health
self.health = self.maxHealth;
var heroGraphics = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.0,
scaleY: 2.0
});
heroGraphics.tint = 0x00FF00; // Green color for hero
// Gun container for rotation
var gunContainer = new Container();
self.addChild(gunContainer);
var gunGraphics = gunContainer.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
gunGraphics.width = 60;
gunGraphics.height = 60;
gunGraphics.tint = 0x444444; // Darker gray for better visibility
gunGraphics.alpha = 1.0; // Ensure gun is fully visible
self.targetX = 0;
self.targetY = 0;
self.isMoving = false;
self.range = 600; // Hero gun range - increased for long radius attack
self.showRange = false; // Toggle for showing range indicator
// Create range indicator
self.rangeIndicator = new Container();
self.addChild(self.rangeIndicator);
var rangeGraphics = self.rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.range * 2;
rangeGraphics.alpha = 0.2;
rangeGraphics.tint = 0x00FF00;
self.rangeIndicator.visible = false;
self.update = function () {
// Handle boss hero path following
if (self.isBossHero) {
// Define the path waypoints that hero must follow - same as enemy path
var pathWaypoints = [{
x: 11,
y: 4
}, {
x: 11,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 29
}];
// Find current waypoint target
if (!self.waypointIndex) {
self.waypointIndex = 0;
}
// Get current waypoint
var currentWaypoint = pathWaypoints[self.waypointIndex];
if (!currentWaypoint) {
// If no more waypoints, stay at last position
return;
}
if (currentWaypoint) {
var ox = currentWaypoint.x - self.currentCellX;
var oy = currentWaypoint.y - self.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
// If close to current waypoint, move to next waypoint
if (dist < 0.5) {
self.waypointIndex++;
// If we've reached the last waypoint, stay there
if (self.waypointIndex >= pathWaypoints.length) {
self.waypointIndex = pathWaypoints.length - 1;
return;
}
}
if (dist > self.pathSpeed) {
var angle = Math.atan2(oy, ox);
self.currentCellX += Math.cos(angle) * self.pathSpeed;
self.currentCellY += Math.sin(angle) * self.pathSpeed;
// Rotate hero graphic to match movement direction
if (heroGraphics.targetRotation === undefined) {
heroGraphics.targetRotation = angle;
heroGraphics.rotation = angle;
} else {
if (Math.abs(angle - heroGraphics.targetRotation) > 0.05) {
tween.stop(heroGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = heroGraphics.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;
}
heroGraphics.targetRotation = angle;
tween(heroGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
self.x = grid.x + self.currentCellX * CELL_SIZE;
self.y = grid.y + self.currentCellY * CELL_SIZE;
} else {
// Move towards target if moving (normal hero behavior)
if (self.isMoving) {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
var moveX = dx / distance * self.speed;
var moveY = dy / distance * self.speed;
self.x += moveX;
self.y += moveY;
} else {
self.isMoving = false;
}
}
}
// Show/hide range indicator when hero is selected or moving
self.rangeIndicator.visible = self.showRange || self.isMoving;
// Find target enemy within range using priority system
var targetEnemy = null;
var bestPriority = -1;
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);
// Only consider enemies within range
if (distance <= self.range) {
// Priority system: bosses > closest to goal > closest to hero
var priority = 0;
// Highest priority for bosses
if (enemy.isBoss) {
priority += 1000;
}
// Medium priority based on progress toward goal
if (enemy.isFlying && enemy.flyingTarget) {
var goalDx = enemy.flyingTarget.x - enemy.cellX;
var goalDy = enemy.flyingTarget.y - enemy.cellY;
var distToGoal = Math.sqrt(goalDx * goalDx + goalDy * goalDy);
priority += (100 - distToGoal) * 10; // Closer to goal = higher priority
} else {
// For ground enemies, use path score
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
priority += 1000 - cell.score / 100; // Lower score = closer to goal = higher priority
}
}
// Lowest priority based on distance to hero (closer = slightly higher priority)
priority += (self.range - distance) / 10;
if (priority > bestPriority) {
bestPriority = priority;
targetEnemy = enemy;
}
}
}
if (targetEnemy) {
// Check collision with enemy (hero takes damage)
var dx = targetEnemy.x - self.x;
var dy = targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 80 && !heroIsDead) {
// Hero takes damage when touching enemy
var damage = targetEnemy.isBoss ? 50 : 20;
heroHealth = Math.max(0, heroHealth - damage);
if (heroHealth <= 0) {
heroIsDead = true;
self.alpha = 0.3;
var notification = game.addChild(new Notification("Hero died! Use diamonds to revive!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
updateUI();
// Push enemy away from hero
var pushForce = 50;
var pushAngle = Math.atan2(dy, dx);
targetEnemy.x += Math.cos(pushAngle) * pushForce;
targetEnemy.y += Math.sin(pushAngle) * pushForce;
}
// Aim gun at target enemy
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
// Fire at enemy (only if hero is alive and within range)
if (LK.ticks - self.lastFired >= self.fireRate && !heroIsDead && distance <= self.range) {
self.fire(angle);
self.lastFired = LK.ticks;
}
}
};
self.fire = function (direction) {
var bulletX = self.x + Math.cos(direction) * 30;
var bulletY = self.y + Math.sin(direction) * 30;
var bullet = new HeroBullet(bulletX, bulletY, direction, self.damage, self.bulletSpeed);
game.addChild(bullet);
heroBullets.push(bullet);
// Create muzzle flash effect
var muzzleFlash = new EffectIndicator(bulletX, bulletY, 'attack');
game.addChild(muzzleFlash);
// Gun recoil effect
tween.stop(gunContainer, {
x: true,
y: true
});
var recoilDistance = 8;
var recoilX = -Math.cos(direction) * recoilDistance;
var recoilY = -Math.sin(direction) * recoilDistance;
tween(gunContainer, {
x: recoilX,
y: recoilY
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(gunContainer, {
x: 0,
y: 0
}, {
duration: 120,
easing: tween.easeIn
});
}
});
};
self.moveTo = function (x, y) {
// Don't move if hero is dead
if (heroIsDead) {
return;
}
// Convert screen coordinates to grid coordinates
var gridPosX = (x - grid.x) / CELL_SIZE;
var gridPosY = (y - grid.y) / CELL_SIZE;
var gridX = Math.floor(gridPosX);
var gridY = Math.floor(gridPosY);
// Check if the target position is within valid bounds
var isValidPosition = true;
// Check if position is within grid bounds
if (gridX < 0 || gridX >= 24 || gridY < 4 || gridY >= 29 + 2) {
isValidPosition = false;
}
// Check if position is not a wall (type 1)
if (isValidPosition) {
var cell = grid.getCell(gridX, gridY);
if (cell && cell.type === 1) {
isValidPosition = false;
}
// Check if position is on the enemy path - use the same path as defined in Grid
var pathCells = [{
x: 11,
y: 4
}, {
x: 11,
y: 5
}, {
x: 11,
y: 6
}, {
x: 11,
y: 7
}, {
x: 11,
y: 8
}, {
x: 10,
y: 8
}, {
x: 9,
y: 8
}, {
x: 8,
y: 8
}, {
x: 7,
y: 8
}, {
x: 6,
y: 8
}, {
x: 5,
y: 8
}, {
x: 4,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 9
}, {
x: 3,
y: 10
}, {
x: 3,
y: 11
}, {
x: 3,
y: 12
}, {
x: 4,
y: 12
}, {
x: 5,
y: 12
}, {
x: 6,
y: 12
}, {
x: 7,
y: 12
}, {
x: 8,
y: 12
}, {
x: 9,
y: 12
}, {
x: 10,
y: 12
}, {
x: 11,
y: 12
}, {
x: 12,
y: 12
}, {
x: 13,
y: 12
}, {
x: 14,
y: 12
}, {
x: 15,
y: 12
}, {
x: 16,
y: 12
}, {
x: 17,
y: 12
}, {
x: 18,
y: 12
}, {
x: 19,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 13
}, {
x: 20,
y: 14
}, {
x: 20,
y: 15
}, {
x: 20,
y: 16
}, {
x: 20,
y: 17
}, {
x: 20,
y: 18
}, {
x: 19,
y: 18
}, {
x: 18,
y: 18
}, {
x: 17,
y: 18
}, {
x: 16,
y: 18
}, {
x: 15,
y: 18
}, {
x: 14,
y: 18
}, {
x: 13,
y: 18
}, {
x: 12,
y: 18
}, {
x: 11,
y: 18
}, {
x: 10,
y: 18
}, {
x: 9,
y: 18
}, {
x: 8,
y: 18
}, {
x: 7,
y: 18
}, {
x: 6,
y: 18
}, {
x: 5,
y: 18
}, {
x: 4,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 19
}, {
x: 3,
y: 20
}, {
x: 3,
y: 21
}, {
x: 3,
y: 22
}, {
x: 3,
y: 23
}, {
x: 4,
y: 23
}, {
x: 5,
y: 23
}, {
x: 6,
y: 23
}, {
x: 7,
y: 23
}, {
x: 8,
y: 23
}, {
x: 9,
y: 23
}, {
x: 10,
y: 23
}, {
x: 11,
y: 23
}, {
x: 12,
y: 23
}, {
x: 13,
y: 23
}, {
x: 14,
y: 23
}, {
x: 15,
y: 23
}, {
x: 16,
y: 23
}, {
x: 17,
y: 23
}, {
x: 18,
y: 23
}, {
x: 19,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 24
}, {
x: 20,
y: 25
}, {
x: 20,
y: 26
}, {
x: 20,
y: 27
}, {
x: 19,
y: 27
}, {
x: 18,
y: 27
}, {
x: 17,
y: 27
}, {
x: 16,
y: 27
}, {
x: 15,
y: 27
}, {
x: 14,
y: 27
}, {
x: 13,
y: 27
}, {
x: 12,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 28
}, {
x: 11,
y: 29
}];
for (var p = 0; p < pathCells.length; p++) {
if (pathCells[p].x === gridX && pathCells[p].y === gridY) {
isValidPosition = false;
break;
}
}
}
// Only move if position is valid
if (isValidPosition) {
self.targetX = x;
self.targetY = y;
self.isMoving = true;
}
};
self.down = function (x, y, obj) {
// Toggle range indicator if clicking directly on hero
var heroRadius = 40; // Hero hit radius
var dx = x - self.x;
var dy = y - self.y;
var distanceToHero = Math.sqrt(dx * dx + dy * dy);
if (distanceToHero <= heroRadius) {
// Toggle range display
self.showRange = !self.showRange;
var notification = game.addChild(new Notification(self.showRange ? "Hero range ON" : "Hero range OFF"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
} else {
// Move hero to clicked position
if (obj && obj.parent && obj.position) {
var localPos = self.parent.toLocal(obj.parent.toGlobal(obj.position));
self.moveTo(localPos.x, localPos.y);
} else {
// Fallback to using x, y coordinates directly
self.moveTo(x, y);
}
}
};
return self;
});
var HeroBullet = Container.expand(function (startX, startY, direction, damage, speed) {
var self = Container.call(this);
self.direction = direction;
self.damage = damage || 15;
self.speed = speed || 8;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('hero_bullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0xFFD700; // Gold color for hero bullets
bulletGraphics.width = 25; // Make bullets slightly larger
bulletGraphics.height = 25;
self.update = function () {
// Move in the specified direction
self.x += Math.cos(self.direction) * self.speed;
self.y += Math.sin(self.direction) * self.speed;
// Check if bullet is off screen
if (self.x < -50 || self.x > 2048 + 50 || self.y < -50 || self.y > 2732 + 50) {
self.destroy();
return;
}
// Check collision with enemies - limit to first 10 enemies and use distance squared
var enemiesToCheck = Math.min(enemies.length, 10);
for (var i = 0; i < enemiesToCheck; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distanceSquared = dx * dx + dy * dy;
if (distanceSquared < 900) {
// 30 * 30 = 900
// Hit radius
// Apply damage to enemy
enemy.health -= self.damage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
self.destroy();
return;
}
}
};
return self;
});
var LeaderboardButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 250;
buttonBackground.height = 80;
buttonBackground.tint = 0x4444FF;
var buttonText = new Text2("Leaderboard", {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.down = function () {
showLeaderboard();
};
return self;
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
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 () {
// Show button when wave is finished (all enemies defeated) and not in progress
var waveFinished = waveIndicator && waveIndicator.gameStarted && !waveInProgress && enemies.length === 0;
var canStartNextWave = waveFinished && currentWave < totalWaves;
if (canStartNextWave) {
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 () {
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;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) 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 ReviveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 350;
buttonBackground.height = 100;
buttonBackground.tint = 0x00FF00;
var buttonText = new Text2("Revive Hero (5 💎)", {
size: 45,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.visible = false;
self.update = function () {
if (heroIsDead) {
self.visible = true;
self.alpha = diamonds >= 5 ? 1 : 0.5;
buttonBackground.tint = diamonds >= 5 ? 0x00FF00 : 0x888888;
} else {
self.visible = false;
}
};
self.down = function () {
if (heroIsDead) {
reviveHero();
}
};
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(self.towerType === 'fire' ? 'fire_tower' : '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;
case 'fire':
baseGraphics.tint = 0xFF4400;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
var isFireTower = self.towerType === 'fire';
// Add shadow for tower type label
var displayName = self.towerType === 'default' ? 'Standard' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabelShadow = new Text2(displayName, {
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 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 costText = isFireTower ? towerCost + " 💎" : towerCost.toString();
var costLabelShadow = new Text2(costText, {
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 costLabel = new Text2(costText, {
size: 50,
fill: isFireTower ? 0x00FFFF : 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 20 + 12;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = isFireTower ? diamonds >= getTowerCost(self.towerType) : gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Standardized method to get the current range of the tower
self.getRange = function () {
// 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
if (self.level === self.maxLevel) {
return 15 * CELL_SIZE; // Even longer range for max level sniper to reach bosses
}
return (5 + (self.level - 1) * 1.0) * CELL_SIZE;
// Slightly increased range progression
case 'splash':
// Splash: base 2, +0.2 per level (max ~4 blocks at max level)
return (2 + (self.level - 1) * 0.2) * CELL_SIZE;
case 'rapid':
// Rapid: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// Slow: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'fire':
// Fire: base 4, +0.6 per level
return (4 + (self.level - 1) * 0.6) * CELL_SIZE;
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;
self.rangeIndicator = null; // Will hold the range display circle
switch (self.id) {
case 'rapid':
self.fireRate = 30;
self.damage = 5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 75;
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'fire':
self.fireRate = 45;
self.damage = 20;
self.range = 4 * CELL_SIZE;
self.bulletSpeed = 6;
break;
}
var baseGraphics = self.attachAsset(self.id === 'fire' ? 'fire_tower' : '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;
case 'fire':
baseGraphics.tint = 0xFF4400;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
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);
var gunGraphics = gunContainer.attachAsset('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;
case 'fire':
towerLevelIndicator.tint = 0xFF4400;
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++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
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); // 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); // 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); // 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);
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];
// Enhanced upgrade animation with multiple effects
tween(levelDot, {
scaleX: 1.8,
scaleY: 1.8
}, {
duration: 200,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.bounceOut
});
}
});
// Animate the entire tower with upgrade effect
tween(self, {
scaleX: 1.15,
scaleY: 1.15
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.elasticOut
});
}
});
// Animate range indicator to show new range
if (self.rangeIndicator && self.rangeIndicator.children[0]) {
var rangeGraphics = self.rangeIndicator.children[0];
var oldSize = rangeGraphics.width;
var newSize = self.getRange() * 2;
// Pulse animation to show range increase
tween(rangeGraphics, {
width: newSize * 1.3,
height: newSize * 1.3,
alpha: 0.4
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(rangeGraphics, {
width: newSize,
height: newSize,
alpha: 0.15
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
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()) {
// Boss enemies are always valid targets for all towers
if (enemy.isBoss) {
// Prioritize boss enemies highest
if (0 < bestPriority) {
bestPriority = 10000; // Set highest priority for bosses
bestTarget = enemy;
}
continue;
}
// 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 () {
// Create or update range indicator if it doesn't exist
if (!self.rangeIndicator) {
self.rangeIndicator = new Container();
self.rangeIndicator.isTowerRange = true;
self.rangeIndicator.tower = self;
var rangeGraphics = self.rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.15; // Semi-transparent
rangeGraphics.tint = 0x00FF00; // Green color for range
self.rangeIndicator.x = self.x;
self.rangeIndicator.y = self.y;
game.addChildAt(self.rangeIndicator, 0); // Add behind other objects
}
// Update range indicator position and size
if (self.rangeIndicator) {
self.rangeIndicator.x = self.x;
self.rangeIndicator.y = self.y;
var rangeGraphics = self.rangeIndicator.children[0];
if (rangeGraphics) {
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
}
}
// Find the best enemy target in range
var bestTarget = null;
var bestPriority = -1;
var currentRange = self.getRange();
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 <= currentRange) {
var priority = 0;
// Highest priority for boss enemies - ensure ALL towers always target bosses first
if (enemy.isBoss) {
priority = 100000; // Set absolute highest priority for bosses - increased from 50000
}
// Medium priority based on health (lower health = higher priority to finish them off)
priority += 1000 - enemy.health;
// For flying enemies, prioritize based on distance to goal
if (enemy.isFlying && enemy.flyingTarget) {
var goalDx = enemy.flyingTarget.x - enemy.cellX;
var goalDy = enemy.flyingTarget.y - enemy.cellY;
var distToGoal = Math.sqrt(goalDx * goalDx + goalDy * goalDy);
priority += (100 - distToGoal) * 10; // Closer to goal = higher priority
} else {
// For ground enemies, use path score if available
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
priority += (10000 - cell.score) / 10; // Lower score = closer to goal = higher priority
}
}
// Small bonus for closer enemies to break ties
priority += (currentRange - distance) / 10;
if (priority > bestPriority) {
bestPriority = priority;
bestTarget = enemy;
}
}
}
// Fire at the best target if fire rate allows
if (bestTarget && LK.ticks - self.lastFired >= self.fireRate) {
self.targetEnemy = bestTarget;
var dx = bestTarget.x - self.x;
var dy = bestTarget.y - self.y;
var angle = Math.atan2(dy, dx);
// Aim gun at target
gunContainer.rotation = angle;
// Fire bullet
var bulletX = self.x + Math.cos(angle) * 40;
var bulletY = self.y + Math.sin(angle) * 40;
var bullet = new Bullet(bulletX, bulletY, bestTarget, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// 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 'slow':
bullet.children[0].tint = 0x9900FF;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'fire':
bullet.children[0].tint = 0xFF4400;
bullet.children[0].width = 45;
bullet.children[0].height = 45;
break;
}
game.addChild(bullet);
bullets.push(bullet);
bestTarget.bulletsTargetingThis.push(bullet);
self.lastFired = LK.ticks;
// Create visual muzzle flash effect
var muzzleFlash = new EffectIndicator(self.x + Math.cos(angle) * 40, self.y + Math.sin(angle) * 40, 'attack');
game.addChild(muzzleFlash);
// Play sad fire sound for fire towers
if (self.id === 'fire') {
LK.getSound('sad_fire_attack').play();
}
// Enhanced gun recoil animation
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
var recoilDistance = 8;
var recoilX = -Math.cos(angle) * recoilDistance;
var recoilY = -Math.sin(angle) * recoilDistance;
tween(gunContainer, {
x: recoilX,
y: recoilY,
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(gunContainer, {
x: 0,
y: 0,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 120,
easing: tween.easeIn
});
}
});
} else if (bestTarget) {
// Aim at target even if not firing
self.targetEnemy = bestTarget;
var dx = bestTarget.x - self.x;
var dy = bestTarget.y - self.y;
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
} else {
self.targetEnemy = null;
}
};
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 () {
if (self.targetEnemy) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// 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 'slow':
bullet.children[0].tint = 0x9900FF;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'fire':
bullet.children[0].tint = 0xFF4400;
bullet.children[0].width = 45;
bullet.children[0].height = 45;
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;
// Mark cells as occupied by tower
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 mark as occupied, don't change to wall type
cell.hasTower = true;
}
}
}
self.refreshCellsInRange();
};
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('towerpreview', {
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 isFireTower = self.towerType === 'fire';
self.hasEnoughGold = isFireTower ? diamonds >= getTowerCost(self.towerType) : gold >= getTowerCost(self.towerType);
// 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;
case 'fire':
previewGraphics.tint = 0xFF4400;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
// Check if tower is within valid grid bounds
if (self.gridX < 0 || self.gridX + 1 >= 24 || self.gridY < 5 || self.gridY + 1 >= 29) {
validGridPlacement = false;
} else {
// Check all 4 cells the tower will occupy
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
// Check if cell exists
if (!cell) {
validGridPlacement = false;
break;
}
// Only block if this is part of the enemy path
var pathCells = [{
x: 11,
y: 4
}, {
x: 11,
y: 5
}, {
x: 11,
y: 6
}, {
x: 11,
y: 7
}, {
x: 11,
y: 8
}, {
x: 10,
y: 8
}, {
x: 9,
y: 8
}, {
x: 8,
y: 8
}, {
x: 7,
y: 8
}, {
x: 6,
y: 8
}, {
x: 5,
y: 8
}, {
x: 4,
y: 8
}, {
x: 3,
y: 8
}, {
x: 3,
y: 9
}, {
x: 3,
y: 10
}, {
x: 3,
y: 11
}, {
x: 3,
y: 12
}, {
x: 4,
y: 12
}, {
x: 5,
y: 12
}, {
x: 6,
y: 12
}, {
x: 7,
y: 12
}, {
x: 8,
y: 12
}, {
x: 9,
y: 12
}, {
x: 10,
y: 12
}, {
x: 11,
y: 12
}, {
x: 12,
y: 12
}, {
x: 13,
y: 12
}, {
x: 14,
y: 12
}, {
x: 15,
y: 12
}, {
x: 16,
y: 12
}, {
x: 17,
y: 12
}, {
x: 18,
y: 12
}, {
x: 19,
y: 12
}, {
x: 20,
y: 12
}, {
x: 20,
y: 13
}, {
x: 20,
y: 14
}, {
x: 20,
y: 15
}, {
x: 20,
y: 16
}, {
x: 20,
y: 17
}, {
x: 20,
y: 18
}, {
x: 19,
y: 18
}, {
x: 18,
y: 18
}, {
x: 17,
y: 18
}, {
x: 16,
y: 18
}, {
x: 15,
y: 18
}, {
x: 14,
y: 18
}, {
x: 13,
y: 18
}, {
x: 12,
y: 18
}, {
x: 11,
y: 18
}, {
x: 10,
y: 18
}, {
x: 9,
y: 18
}, {
x: 8,
y: 18
}, {
x: 7,
y: 18
}, {
x: 6,
y: 18
}, {
x: 5,
y: 18
}, {
x: 4,
y: 18
}, {
x: 3,
y: 18
}, {
x: 3,
y: 19
}, {
x: 3,
y: 20
}, {
x: 3,
y: 21
}, {
x: 3,
y: 22
}, {
x: 3,
y: 23
}, {
x: 4,
y: 23
}, {
x: 5,
y: 23
}, {
x: 6,
y: 23
}, {
x: 7,
y: 23
}, {
x: 8,
y: 23
}, {
x: 9,
y: 23
}, {
x: 10,
y: 23
}, {
x: 11,
y: 23
}, {
x: 12,
y: 23
}, {
x: 13,
y: 23
}, {
x: 14,
y: 23
}, {
x: 15,
y: 23
}, {
x: 16,
y: 23
}, {
x: 17,
y: 23
}, {
x: 18,
y: 23
}, {
x: 19,
y: 23
}, {
x: 20,
y: 23
}, {
x: 20,
y: 24
}, {
x: 20,
y: 25
}, {
x: 20,
y: 26
}, {
x: 20,
y: 27
}, {
x: 19,
y: 27
}, {
x: 18,
y: 27
}, {
x: 17,
y: 27
}, {
x: 16,
y: 27
}, {
x: 15,
y: 27
}, {
x: 14,
y: 27
}, {
x: 13,
y: 27
}, {
x: 12,
y: 27
}, {
x: 11,
y: 27
}, {
x: 11,
y: 28
}, {
x: 11,
y: 29
}];
var isOnPath = false;
for (var p = 0; p < pathCells.length; p++) {
if (pathCells[p].x === self.gridX + i && pathCells[p].y === self.gridY + j) {
isOnPath = true;
break;
}
}
if (isOnPath) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
// Check if any enemies would block placement
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;
var isFireTower = self.towerType === 'fire';
self.hasEnoughGold = isFireTower ? diamonds >= getTowerCost(self.towerType) : 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 UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' 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('notification', {
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('notification', {
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('notification', {
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;
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) {
cell.hasTower = false;
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();
};
// Override destroy method to clean up range indicator
var originalDestroy = self.destroy;
self.destroy = function () {
if (self.rangeIndicator && self.rangeIndicator.parent) {
game.removeChild(self.rangeIndicator);
}
if (originalDestroy) {
originalDestroy.call(self);
}
};
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('notification', {
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 () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 1; // Start with wave 1
waveTimer = 0;
waveInProgress = true;
waveSpawned = false;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0 && i + 1 >= 10;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0xFFFF00;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Boss Normal";
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to the color of the wave type
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
break;
case 'fast':
block.tint = 0x00AAFF;
break;
case 'immune':
block.tint = 0xAA0000;
break;
case 'flying':
block.tint = 0xFFFF00;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave) {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// 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);
// Add boss prefix for boss waves (every 10th wave starting from wave 10)
if (waveNumber % 10 === 0 && waveNumber >= 10) {
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];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
// Remove automatic wave progression - waves now start manually via NextWaveButton
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a0a0a
});
/****
* Game Code
****/
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 heroBullets = [];
var hero = null;
var bossHeroes = [];
var defenses = [];
var selectedTower = null;
var gold = 100;
var diamonds = 5;
var lives = 20;
var score = 0;
var heroHealth = 500;
var heroMaxHealth = 500;
var heroIsDead = false;
var leaderboardData = storage.leaderboard || [];
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
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 diamondText = new Text2('Diamonds: ' + diamonds, {
size: 60,
fill: 0x00FFFF,
weight: 800
});
diamondText.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 scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var heroHealthText = new Text2('Hero Health: ' + heroHealth + '/' + heroMaxHealth, {
size: 60,
fill: 0xFF6600,
weight: 800
});
heroHealthText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(diamondText);
LK.gui.top.addChild(livesText);
LK.gui.top.addChild(scoreText);
LK.gui.top.addChild(heroHealthText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
diamondText.x = -spacing;
diamondText.y = topMargin + 80;
scoreText.x = spacing;
scoreText.y = topMargin;
heroHealthText.x = spacing;
heroHealthText.y = topMargin + 80;
function updateUI() {
goldText.setText('Gold: ' + gold);
diamondText.setText('Diamonds: ' + diamonds);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
heroHealthText.setText('Hero Health: ' + heroHealth + '/' + heroMaxHealth);
}
function setGold(value) {
gold = value;
updateUI();
}
function setDiamonds(value) {
diamonds = value;
updateUI();
}
// Create horror atmosphere background
var horrorBackgroundLayer = new Container();
var horrorBg = horrorBackgroundLayer.attachAsset('cell', {
anchorX: 0,
anchorY: 0,
scaleX: 30,
scaleY: 40
});
horrorBg.tint = 0x2d0a0a; // Dark red horror tint
horrorBg.alpha = 0.6;
horrorBg.x = 0;
horrorBg.y = 0;
// Add subtle pulsing horror effect
var horrorPulse = horrorBackgroundLayer.attachAsset('cell', {
anchorX: 0,
anchorY: 0,
scaleX: 30,
scaleY: 40
});
horrorPulse.tint = 0x4a0000; // Darker red for pulsing
horrorPulse.alpha = 0.2;
horrorPulse.x = 0;
horrorPulse.y = 0;
// Animate the horror pulse effect
tween(horrorPulse, {
alpha: 0.4
}, {
duration: 2000,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
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);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
grid.pathFind();
grid.renderDebug();
// Apply horror theme to grid
grid.setBackgroundColor = function (color) {
// Override to maintain horror atmosphere
};
debugLayer.addChild(grid);
game.addChild(horrorBackgroundLayer);
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) {
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 = 5;
switch (towerType) {
case 'rapid':
cost = 15;
break;
case 'sniper':
cost = 25;
break;
case 'splash':
cost = 35;
break;
case 'slow':
cost = 45;
break;
case 'poison':
cost = 55;
break;
case 'fire':
cost = 10; // Fire tower costs 10 diamonds
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function saveToLeaderboard(playerScore) {
// Create a simple name input interface
var nameInputContainer = new Container();
nameInputContainer.isNameInput = true;
game.addChild(nameInputContainer);
// Background
var background = nameInputContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
background.width = 1200;
background.height = 600;
background.tint = 0x333333;
background.alpha = 0.95;
nameInputContainer.x = 2048 / 2;
nameInputContainer.y = 2732 / 2;
// Title
var titleText = new Text2("🎉 NEW HIGH SCORE! 🎉", {
size: 70,
fill: 0xFFD700,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 0;
titleText.y = -200;
nameInputContainer.addChild(titleText);
// Score display
var scoreText = new Text2("Score: " + playerScore + " (Wave " + currentWave + ")", {
size: 60,
fill: 0xFFFFFF,
weight: 600
});
scoreText.anchor.set(0.5, 0.5);
scoreText.x = 0;
scoreText.y = -120;
nameInputContainer.addChild(scoreText);
// Instructions
var instructText = new Text2("Click a button to choose your name:", {
size: 45,
fill: 0xCCCCCC,
weight: 400
});
instructText.anchor.set(0.5, 0.5);
instructText.x = 0;
instructText.y = -40;
nameInputContainer.addChild(instructText);
// Name buttons
var nameOptions = ["Player", "Hero", "Champion", "Legend", "Master"];
var buttonWidth = 200;
var buttonSpacing = 220;
var startX = -(nameOptions.length - 1) * buttonSpacing / 2;
for (var i = 0; i < nameOptions.length; i++) {
var nameButton = new Container();
var buttonBg = nameButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = buttonWidth;
buttonBg.height = 80;
buttonBg.tint = 0x0066CC;
var buttonText = new Text2(nameOptions[i], {
size: 40,
fill: 0xFFFFFF,
weight: 600
});
buttonText.anchor.set(0.5, 0.5);
nameButton.addChild(buttonText);
nameButton.x = startX + i * buttonSpacing;
nameButton.y = 50;
nameInputContainer.addChild(nameButton);
// Create closure to capture the name
(function (name) {
nameButton.down = function () {
submitScore(name, playerScore);
game.removeChild(nameInputContainer);
};
})(nameOptions[i]);
}
// Anonymous button
var anonButton = new Container();
var anonBg = anonButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
anonBg.width = 250;
anonBg.height = 80;
anonBg.tint = 0x666666;
var anonText = new Text2("Anonymous", {
size: 40,
fill: 0xFFFFFF,
weight: 600
});
anonText.anchor.set(0.5, 0.5);
anonButton.addChild(anonText);
anonButton.x = 0;
anonButton.y = 160;
nameInputContainer.addChild(anonButton);
anonButton.down = function () {
submitScore("Anonymous", playerScore);
game.removeChild(nameInputContainer);
};
}
function submitScore(playerName, playerScore) {
var entry = {
name: playerName,
score: playerScore,
wave: currentWave,
timestamp: new Date().getTime()
};
leaderboardData.push(entry);
leaderboardData.sort(function (a, b) {
return b.score - a.score;
});
if (leaderboardData.length > 10) {
leaderboardData = leaderboardData.slice(0, 10);
}
storage.leaderboard = leaderboardData;
// Show confirmation
var notification = game.addChild(new Notification("Score saved to leaderboard as " + playerName + "!"));
notification.x = 2048 / 2;
notification.y = 2732 / 2 - 100;
}
function showLeaderboard() {
// Remove any existing leaderboard display
var existingLeaderboard = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isLeaderboard) {
existingLeaderboard = game.children[i];
break;
}
}
if (existingLeaderboard) {
game.removeChild(existingLeaderboard);
return;
}
// Create leaderboard container
var leaderboardContainer = new Container();
leaderboardContainer.isLeaderboard = true;
game.addChild(leaderboardContainer);
// Background
var background = leaderboardContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
background.width = 1800;
background.height = 1400;
background.tint = 0x222222;
background.alpha = 0.95;
leaderboardContainer.x = 2048 / 2;
leaderboardContainer.y = 2732 / 2;
// Title
var titleText = new Text2("🏆 LEADERBOARD 🏆", {
size: 80,
fill: 0xFFD700,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 0;
titleText.y = -600;
leaderboardContainer.addChild(titleText);
// Headers
var headerText = new Text2("RANK NAME SCORE WAVE", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
headerText.anchor.set(0.5, 0.5);
headerText.x = 0;
headerText.y = -450;
leaderboardContainer.addChild(headerText);
// Leaderboard entries
if (leaderboardData.length === 0) {
var noScoresText = new Text2("No high scores yet!\nPlay the game to set the first record!", {
size: 60,
fill: 0xAAAAAA,
weight: 400
});
noScoresText.anchor.set(0.5, 0.5);
noScoresText.x = 0;
noScoresText.y = 0;
leaderboardContainer.addChild(noScoresText);
} else {
for (var i = 0; i < Math.min(leaderboardData.length, 10); i++) {
var entry = leaderboardData[i];
var rank = i + 1;
var name = entry.name || "Anonymous";
var score = entry.score || 0;
var wave = entry.wave || 1;
// Truncate name if too long
if (name.length > 15) {
name = name.substring(0, 12) + "...";
}
// Format the entry text with proper spacing
var entryText = rank + ". " + name;
// Pad name to consistent length
while (entryText.length < 25) {
entryText += " ";
}
entryText += score;
// Pad score to consistent length
var scoreStr = score.toString();
while (scoreStr.length < 8) {
scoreStr = " " + scoreStr;
}
entryText = rank + ". " + name;
while (entryText.length < 30) {
entryText += " ";
}
entryText += scoreStr + " " + wave;
var color = 0xFFFFFF;
if (rank === 1) color = 0xFFD700; // Gold for 1st
else if (rank === 2) color = 0xC0C0C0; // Silver for 2nd
else if (rank === 3) color = 0xCD7F32; // Bronze for 3rd
var entryDisplay = new Text2(entryText, {
size: 45,
fill: color,
weight: rank <= 3 ? 800 : 400
});
entryDisplay.anchor.set(0.5, 0.5);
entryDisplay.x = 0;
entryDisplay.y = -350 + i * 80;
leaderboardContainer.addChild(entryDisplay);
}
}
// Close button
var closeButton = new Container();
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 200;
closeBackground.height = 80;
closeBackground.tint = 0xCC0000;
var closeText = new Text2("CLOSE", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = 0;
closeButton.y = 550;
leaderboardContainer.addChild(closeButton);
closeButton.down = function () {
game.removeChild(leaderboardContainer);
};
// Auto-close after 10 seconds
LK.setTimeout(function () {
if (leaderboardContainer.parent) {
game.removeChild(leaderboardContainer);
}
}, 10000);
}
function reviveHero() {
if (heroIsDead && diamonds >= 5) {
setDiamonds(diamonds - 5);
heroHealth = heroMaxHealth;
heroIsDead = false;
hero.alpha = 1;
hero.visible = true;
updateUI();
var notification = game.addChild(new Notification("Hero revived! -5 diamonds"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
return true;
} else if (heroIsDead && diamonds < 5) {
var notification = game.addChild(new Notification("Need 5 diamonds to revive hero!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
return false;
}
return false;
}
var pathfindingTimer = 0;
var pathfindingInterval = 10; // Update pathfinding every 10 frames instead of every frame
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
var isFireTower = towerType === 'fire';
var canAfford = isFireTower ? diamonds >= towerCost : gold >= towerCost;
if (canAfford) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
if (isFireTower) {
setDiamonds(diamonds - towerCost);
var notification = game.addChild(new Notification("Fire tower purchased for " + towerCost + " diamonds!"));
} else {
setGold(gold - towerCost);
var notification = game.addChild(new Notification("Tower purchased for " + towerCost + " gold!"));
}
notification.x = 2048 / 2;
notification.y = grid.height - 50;
grid.pathFind();
grid.renderDebug();
return true;
} else {
if (isFireTower) {
var notification = game.addChild(new Notification("You don't have enough diamonds!"));
} 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) {
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Check if touching a source tower for dragging
var touchedSourceTower = false;
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) {
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);
touchedSourceTower = true;
break;
}
}
// If not touching a source tower, move hero to touched position
if (!touchedSourceTower && hero) {
hero.moveTo(x, y);
}
};
game.move = function (x, y, obj) {
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) {
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 && towerPreview.hasEnoughGold) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} 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.hasEnoughGold) {
var notification = game.addChild(new Notification("Not enough gold!"));
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;
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);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
nextWaveButton.y = 2732 - 100 + 20;
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
var reviveButton = new ReviveButton();
reviveButton.x = 2048 / 2;
reviveButton.y = 2732 - 200;
game.addChild(reviveButton);
var leaderboardButton = new LeaderboardButton();
leaderboardButton.x = 200;
leaderboardButton.y = 2732 - 100;
game.addChild(leaderboardButton);
var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison', 'fire'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
// Create hero character
hero = new Hero();
hero.x = 2048 / 2; // Center horizontally
hero.y = 2732 - 300; // Near bottom of screen
game.addChild(hero);
// Start sad background music
LK.playMusic('sad_background_music');
game.update = function () {
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
// Get wave type and enemy count from the wave indicator
var waveType = waveIndicator.getWaveType(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
// Check if this is a boss wave - bosses appear every 10 waves starting from wave 10
var isBossWave = currentWave % 10 === 0 && currentWave >= 10;
if (isBossWave) {
// Boss waves have just 1 enemy regardless of what the wave indicator says
enemyCount = 1;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ BOSS WAVE " + currentWave + "! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
// Spawn boss hero that follows the path
var bossHero = new Hero();
bossHero.isBossHero = true;
bossHero.health = 2000;
bossHero.maxHealth = 2000;
bossHero.damage = 25; // Stronger than normal hero
bossHero.fireRate = 15; // Faster fire rate
bossHero.range = 800; // Longer range
bossHero.currentCellX = 11; // Start at path entry
bossHero.currentCellY = 4;
bossHero.waypointIndex = 0;
bossHero.x = grid.x + bossHero.currentCellX * CELL_SIZE;
bossHero.y = grid.y + bossHero.currentCellY * CELL_SIZE;
// Make boss hero visually distinct
var heroGraphics = bossHero.children[0];
heroGraphics.tint = 0xFFD700; // Gold color for boss hero
heroGraphics.scaleX = 2.5; // Larger than normal hero
heroGraphics.scaleY = 2.5;
game.addChild(bossHero);
bossHeroes.push(bossHero);
var heroNotification = game.addChild(new Notification("🛡️ HERO REINFORCEMENT DEPLOYED! 🛡️"));
heroNotification.x = 2048 / 2;
heroNotification.y = grid.height - 250;
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use exponential scaling for health
var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// All enemy types now spawn in the middle 6 tiles at the top spacing
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
// Find a column that isn't occupied by another enemy that's not yet in view
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
// Check if any enemy is already in this column but not yet in view
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);
}
}
// If all columns are occupied, use original random method
var spawnX;
if (availableColumns.length > 0) {
// Choose a random unoccupied column
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
// Fallback to random if all columns are occupied
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);
}
}
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;
}
}
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;
}
// Boss enemies give more gold and score - increased gold rewards
var goldEarned = enemy.isBoss ? Math.floor(200 + (enemy.waveNumber - 1) * 20) : Math.floor(6 + (enemy.waveNumber - 1) * 2);
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Diamond drop chance - rare drop from enemies
var diamondDropChance = enemy.isBoss ? 0.5 : 0.05; // 50% chance for bosses, 5% for normal enemies
if (Math.random() < diamondDropChance) {
var diamondsEarned = enemy.isBoss ? Math.floor(3 + (enemy.waveNumber - 1) * 0.5) : 1;
var diamondIndicator = new DiamondIndicator(diamondsEarned, enemy.x, enemy.y);
game.addChild(diamondIndicator);
setDiamonds(diamonds + diamondsEarned);
}
// 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 - 1);
// Also damage hero when enemy reaches goal
if (!heroIsDead) {
var heroDamage = enemy.isBoss ? 100 : 25;
heroHealth = Math.max(0, heroHealth - heroDamage);
if (heroHealth <= 0) {
heroIsDead = true;
hero.alpha = 0.3;
var notification = game.addChild(new Notification("Hero died! Use diamonds to revive!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
}
updateUI();
if (lives <= 0) {
saveToLeaderboard(score);
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);
}
}
// Clean up hero bullets
for (var i = heroBullets.length - 1; i >= 0; i--) {
if (!heroBullets[i].parent) {
heroBullets.splice(i, 1);
}
}
// Clean up boss heroes that are no longer alive
for (var i = bossHeroes.length - 1; i >= 0; i--) {
var bossHero = bossHeroes[i];
if (bossHero.health <= 0) {
game.removeChild(bossHero);
bossHeroes.splice(i, 1);
var notification = game.addChild(new Notification("Hero reinforcement defeated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
} else if (!bossHero.parent) {
bossHeroes.splice(i, 1);
}
}
// Only update tower preview every 3 frames
if (towerPreview.visible && LK.ticks % 3 === 0) {
towerPreview.checkPlacement();
}
// Update pathfinding less frequently
pathfindingTimer++;
if (pathfindingTimer >= pathfindingInterval) {
pathfindingTimer = 0;
// Only update debug rendering occasionally
if (LK.ticks % 30 === 0) {
grid.renderDebug();
}
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
saveToLeaderboard(score);
LK.showYouWin();
}
};
Defense tower. In-Game asset. 2d. High contrast. No shadows
Fast enemy. In-Game asset. 2d. High contrast. No shadows
enemy_flying. In-Game asset. 2d. High contrast. No shadows
enemy. In-Game asset. 2d. High contrast. No shadows
bullet. In-Game asset. 2d. High contrast. No shadows
tower. In-Game asset. 2d. High contrast. No shadows
enemy_immune. In-Game asset. 2d. High contrast. No shadows
Hero. In-Game asset. 2d. High contrast. No shadows