/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Asteroid = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.health = 150;
self.maxHealth = 150;
self.isDestroyed = false;
self.gridX = Math.floor((x - grid.x) / CELL_SIZE);
self.gridY = Math.floor((y - grid.y) / CELL_SIZE);
var asteroidGraphics = self.attachAsset('asteroid', {
anchorX: 0.5,
anchorY: 0.5
});
// Add rotation animation
tween(asteroidGraphics, {
rotation: Math.PI * 2
}, {
duration: 8000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed) {
asteroidGraphics.rotation = 0;
}
}
});
// Health bar
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarOutline.y = healthBar.y = -asteroidGraphics.height / 2 - 15;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBar.x = -healthBar.width / 2;
healthBar.tint = 0x8B4513;
self.healthBar = healthBar;
self.takeDamage = function (damage) {
self.health -= damage;
if (self.health <= 0) {
self.health = 0;
self.destroy();
} else {
self.healthBar.width = self.health / self.maxHealth * 70;
}
};
self.down = function () {
// Allow manual destruction by tapping
self.takeDamage(50);
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Create debris effect
for (var i = 0; i < 5; i++) {
var debris = new Container();
debris.x = self.x + (Math.random() - 0.5) * 100;
debris.y = self.y + (Math.random() - 0.5) * 100;
var debrisGraphics = debris.attachAsset('asteroidDebris', {
anchorX: 0.5,
anchorY: 0.5
});
game.addChild(debris);
tween(debris, {
x: debris.x + (Math.random() - 0.5) * 200,
y: debris.y + (Math.random() - 0.5) * 200,
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (debris.parent) {
debris.parent.removeChild(debris);
}
}
});
}
// Clear grid cell
var cell = grid.getCell(self.gridX, self.gridY);
if (cell) {
cell.type = 0;
}
// Remove from asteroids array
var asteroidIndex = asteroids.indexOf(self);
if (asteroidIndex !== -1) {
asteroids.splice(asteroidIndex, 1);
}
// Recalculate paths
grid.pathFind();
grid.renderDebug();
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
// Block the grid cell
var cell = grid.getCell(self.gridX, self.gridY);
if (cell) {
cell.type = 1;
}
return self;
});
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;
// Create sci-fi projectile based on bullet type
var assetName = 'bullet'; // default
var bulletGraphics;
if (self.type === 'rapid') {
bulletGraphics = self.attachAsset('energyBeam', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0x00aaff;
} else if (self.type === 'sniper') {
bulletGraphics = self.attachAsset('laserBurst', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0xff0044;
} else if (self.type === 'splash') {
bulletGraphics = self.attachAsset('plasmaBolt', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0x44ff00;
} else {
bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Add pulsing glow effect to projectiles
tween(bulletGraphics, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(bulletGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
returnBulletToPool(self);
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) {
// Create sci-fi impact flash
if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDestroyed) {
tween(self.targetEnemy, {
tint: 0xffffff
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDestroyed) {
tween(self.targetEnemy, {
tint: 0xffffff
}, {
duration: 200,
easing: tween.easeIn
});
}
}
});
}
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
// Play enemy destroy sound
LK.getSound('enemyDestroy').play();
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
// Play enemy hit sound
LK.getSound('enemyHit').play();
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create visual splash effect with limiting
if (canCreateEffect()) {
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
registerEffect(splashEffect);
}
// Determine splash radius and damage based on specialization
var splashRadius = CELL_SIZE * 1.5;
var splashDamageMultiplier = 0.5;
if (self.specialization === 'nuclear') {
splashRadius = CELL_SIZE * 3;
splashDamageMultiplier = 0.8;
// Create nuclear explosion effect
if (canCreateEffect()) {
var nuclearEffect = new Container();
nuclearEffect.x = self.targetEnemy.x;
nuclearEffect.y = self.targetEnemy.y;
var nuclearGraphics = nuclearEffect.attachAsset('nuclearBlast', {
anchorX: 0.5,
anchorY: 0.5
});
nuclearGraphics.tint = 0xFF0000;
nuclearGraphics.alpha = 0.8;
game.addChild(nuclearEffect);
tween(nuclearEffect, {
scaleX: 3,
scaleY: 3,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (nuclearEffect.parent) {
nuclearEffect.parent.removeChild(nuclearEffect);
}
}
});
}
}
// Splash damage to nearby enemies
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) {
var splashDamage = self.damage * splashDamageMultiplier;
otherEnemy.health -= splashDamage;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
// Chain reaction effect
if (self.specialization === 'chainreaction' && Math.random() < 0.3) {
// 30% chance to chain to another enemy
for (var j = 0; j < enemies.length; j++) {
var chainEnemy = enemies[j];
if (chainEnemy !== otherEnemy && chainEnemy !== self.targetEnemy) {
var chainDx = chainEnemy.x - otherEnemy.x;
var chainDy = chainEnemy.y - otherEnemy.y;
var chainDistance = Math.sqrt(chainDx * chainDx + chainDy * chainDy);
if (chainDistance <= CELL_SIZE * 2) {
chainEnemy.health -= splashDamage * 0.7;
if (chainEnemy.health <= 0) {
chainEnemy.health = 0;
} else {
chainEnemy.healthBar.width = chainEnemy.health / chainEnemy.maxHealth * 70;
}
break;
}
}
}
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect with limiting
if (canCreateEffect()) {
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
registerEffect(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 with limiting
if (canCreateEffect()) {
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
registerEffect(poisonEffect);
}
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper with limiting
if (canCreateEffect()) {
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
registerEffect(sniperEffect);
}
}
returnBulletToPool(self);
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this bullet and its graphics
tween.stop(self);
if (self.children && self.children[0]) {
tween.stop(self.children[0]);
}
// Remove from target enemy's bullets array
if (self.targetEnemy && self.targetEnemy.bulletsTargetingThis) {
var bulletIndex = self.targetEnemy.bulletsTargetingThis.indexOf(self);
if (bulletIndex !== -1) {
self.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
// Remove from bullets array
var bulletArrayIndex = bullets.indexOf(self);
if (bulletArrayIndex !== -1) {
bullets.splice(bulletArrayIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Nullify all object references
self.targetEnemy = null;
// Call parent destroy
Container.prototype.destroy.call(self);
};
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 = 0x220044; // Deep space color
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; // Energy field blue
} else {
// Create starfield effect with bluish tint
var starfieldTint = 0x001122 + (tint << 8) + (tint >> 1);
cellGraphics.tint = starfieldTint;
}
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);
};
});
// 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;
// Create energy particles around main effect
var particles = [];
for (var i = 0; i < 6; i++) {
var particle = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = 8;
particle.height = 8;
var angle = i / 6 * Math.PI * 2;
particle.x = Math.cos(angle) * 30;
particle.y = Math.sin(angle) * 30;
particles.push(particle);
}
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0x88FF44;
}
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0xCC44FF;
}
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0x44FFCC;
}
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0xFF8844;
}
break;
}
effectGraphics.alpha = 0.7;
// Animate particles spiraling outward
for (var i = 0; i < particles.length; i++) {
var particle = particles[i];
var delay = i * 50;
tween(particle, {
x: particle.x * 2,
y: particle.y * 2,
scaleX: 0.5,
scaleY: 0.5,
alpha: 0
}, {
duration: 400,
delay: delay,
easing: tween.easeOut
});
}
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();
}
});
}
});
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this effect and its children
tween.stop(self);
for (var i = 0; i < self.children.length; i++) {
if (self.children[i]) {
tween.stop(self.children[i]);
}
}
// Remove from activeEffects array
var effectIndex = activeEffects.indexOf(self);
if (effectIndex !== -1) {
activeEffects.splice(effectIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Call parent destroy
Container.prototype.destroy.call(self);
};
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.isDestroyed = false;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type !== '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;
// Add visual type indicator for better enemy identification
if (self.type !== 'normal') {
var typeIndicator = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
typeIndicator.width = 16;
typeIndicator.height = 16;
typeIndicator.y = -enemyGraphics.height / 2 - 35; // Above health bar
// Color code the indicator based on enemy type
switch (self.type) {
case 'fast':
typeIndicator.tint = 0x00AAFF;
break;
case 'immune':
typeIndicator.tint = 0xAA0000;
break;
case 'flying':
typeIndicator.tint = 0xFFFF00;
break;
case 'swarm':
typeIndicator.tint = 0xFF00FF;
break;
}
// Make boss indicators larger and more prominent
if (self.isBoss) {
typeIndicator.width = 24;
typeIndicator.height = 24;
typeIndicator.tint = 0xFFD700; // Gold for bosses
// Add pulsing effect for boss indicator
tween(typeIndicator, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(typeIndicator, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
}
}
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) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Improved visual clarity for enemy types with better differentiation
var baseTint = 0xFFFFFF;
// First set base tint based on enemy type for better visual clarity
switch (self.type) {
case 'fast':
baseTint = 0x4488FF; // Bright blue for fast enemies
break;
case 'immune':
baseTint = 0xFF4444; // Bright red for immune enemies
break;
case 'flying':
baseTint = 0xFFDD44; // Bright yellow for flying enemies
break;
case 'swarm':
baseTint = 0xFF44DD; // Bright magenta for swarm enemies
break;
case 'normal':
default:
baseTint = 0xCCCCCC; // Light gray for normal enemies
break;
}
// Apply boss scaling to tint if this is a boss
if (self.isBoss) {
// Make boss enemies more vibrant and add a red outline effect
var r = baseTint >> 16 & 0xFF;
var g = baseTint >> 8 & 0xFF;
var b = baseTint & 0xFF;
// Boost color intensity for bosses
r = Math.min(255, Math.floor(r * 1.3));
g = Math.min(255, Math.floor(g * 1.3));
b = Math.min(255, Math.floor(b * 1.3));
baseTint = r << 16 | g << 8 | b;
}
// Then apply status effect overlays
if (self.isImmune) {
// Keep immune tint but add pulsing effect for better visibility
enemyGraphics.tint = baseTint;
// Add shield shimmer effect for immune enemies
if (!self.shieldEffect && !self.isDestroyed) {
self.shieldEffect = true;
tween(enemyGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(enemyGraphics, {
alpha: 1.0
}, {
duration: 500,
easing: tween.easeInOut
});
}
}
});
}
} else if (self.poisoned && self.slowed) {
// Blend poison green with slow purple over the base tint
enemyGraphics.tint = 0x4C7FD4; // Keep existing blend color
} else if (self.poisoned) {
// Poison overlay - green tint
var r = Math.floor((baseTint >> 16 & 0xFF) * 0.5);
var g = Math.min(255, Math.floor((baseTint >> 8 & 0xFF) * 0.5 + 170));
var b = Math.floor((baseTint & 0xFF) * 0.5 + 85);
enemyGraphics.tint = r << 16 | g << 8 | b;
// Add crackling energy effect for poisoned
if (!self.poisonFlicker && !self.isDestroyed) {
self.poisonFlicker = true;
tween(enemyGraphics, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(enemyGraphics, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.poisonFlicker = false;
}
});
}
}
});
}
} else if (self.slowed) {
// Slow overlay - purple tint
var r = Math.floor((baseTint >> 16 & 0xFF) * 0.5 + 76);
var g = Math.floor((baseTint >> 8 & 0xFF) * 0.5);
var b = Math.min(255, Math.floor((baseTint & 0xFF) * 0.5 + 127));
enemyGraphics.tint = r << 16 | g << 8 | b;
} else {
enemyGraphics.tint = baseTint;
self.shieldEffect = false;
}
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 && !self.isDestroyed) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this enemy and its graphics
tween.stop(self);
if (self.children && self.children[0]) {
tween.stop(self.children[0]);
}
// Clear all bullets targeting this enemy
for (var i = 0; i < self.bulletsTargetingThis.length; i++) {
var bullet = self.bulletsTargetingThis[i];
if (bullet) {
bullet.targetEnemy = null;
}
}
self.bulletsTargetingThis = [];
// Remove from enemies array
var enemyIndex = enemies.indexOf(self);
if (enemyIndex !== -1) {
enemies.splice(enemyIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Clean up shadow if it's a flying enemy
if (self.isFlying && self.shadow) {
if (self.shadow.parent) {
self.shadow.parent.removeChild(self.shadow);
}
self.shadow = null;
}
// Nullify all object references
self.targetEnemy = null;
self.currentTarget = null;
self.flyingTarget = null;
self.healthBar = null;
self.energyCore = null;
// Call parent destroy
Container.prototype.destroy.call(self);
};
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
*/
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;
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
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;
}
// Mark junction cells for branching
for (var i = 0; i < pathJunctions.length; i++) {
var junction = pathJunctions[i];
var junctionCell = self.getCell(junction.gridX, junction.gridY);
if (junctionCell) {
junctionCell.isJunction = true;
junctionCell.junction = junction;
}
}
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;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.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;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05 && !enemy.isDestroyed) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05 && !enemy.isDestroyed) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget) {
// Check if enemy is at a junction
if (cell && cell.isJunction && !enemy.chosenBranch) {
var junction = cell.junction;
var chosenBranch = junction.choosePath(enemy);
if (chosenBranch) {
enemy.chosenBranch = chosenBranch;
enemy.currentTarget = grid.getCell(chosenBranch.targetX, chosenBranch.targetY);
// Show visual path indicator
if (routeVisualizer && Math.random() < 0.3) {
// Show for 30% of enemies
routeVisualizer.showPaths(junction, enemy);
}
// Add notification for significant route changes
if (Math.random() < 0.1) {
// 10% chance
var routeType = chosenBranch.isShortPath ? "direct" : "alternate";
var notification = game.addChild(new Notification("Enemy taking " + routeType + " route!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
} else {
enemy.currentTarget = cell.targets[0];
}
} else {
enemy.currentTarget = cell.targets[0];
}
}
if (enemy.currentTarget) {
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var Hero = Container.expand(function (heroType) {
var self = Container.call(this);
self.heroType = heroType || 'tank';
self.level = storage['hero_' + heroType + '_level'] || 1;
self.experience = storage['hero_' + heroType + '_xp'] || 0;
self.maxHealth = 200;
self.health = self.maxHealth;
self.isDestroyed = false;
self.movementSpeed = 1.5;
self.abilities = [];
self.currentTarget = null;
self.lastAbilityUse = 0;
self.abilityCooldown = 300; // 5 seconds at 60fps
// Set hero-specific stats
switch (self.heroType) {
case 'tank':
self.maxHealth = 300 + (self.level - 1) * 50;
self.movementSpeed = 1.0;
self.abilityCooldown = 600; // 10 seconds
break;
case 'support':
self.maxHealth = 150 + (self.level - 1) * 30;
self.movementSpeed = 2.0;
self.abilityCooldown = 480; // 8 seconds
break;
case 'dps':
self.maxHealth = 180 + (self.level - 1) * 35;
self.movementSpeed = 1.8;
self.abilityCooldown = 360; // 6 seconds
break;
}
self.health = self.maxHealth;
// Get appropriate asset
var assetName = 'hero_' + self.heroType;
var heroGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Create health bar
var healthBarOutline = self.attachAsset('heroHealthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('heroHealthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarOutline.y = healthBar.y = -heroGraphics.height / 2 - 20;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBar.x = -healthBar.width / 2;
self.healthBar = healthBar;
// Create XP bar
var xpBarOutline = self.attachAsset('heroXpBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var xpBar = self.attachAsset('heroXpBar', {
anchorX: 0,
anchorY: 0.5
});
xpBarOutline.y = xpBar.y = -heroGraphics.height / 2 - 35;
xpBarOutline.x = -xpBarOutline.width / 2;
xpBar.x = -xpBar.width / 2;
self.xpBar = xpBar;
// Create ability icon
var abilityIcon = self.attachAsset('heroAbilityIcon', {
anchorX: 0.5,
anchorY: 0.5
});
abilityIcon.y = heroGraphics.height / 2 + 20;
self.abilityIcon = abilityIcon;
// Level indicator
var levelText = new Text2(self.level.toString(), {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
levelText.anchor.set(0.5, 0.5);
levelText.y = heroGraphics.height / 2 + 20;
levelText.x = 40;
self.addChild(levelText);
self.levelText = levelText;
self.getExperienceForNextLevel = function () {
return self.level * 100; // 100 XP per level
};
self.gainExperience = function (amount) {
self.experience += amount;
var xpNeeded = self.getExperienceForNextLevel();
if (self.experience >= xpNeeded) {
self.experience -= xpNeeded;
self.level++;
self.levelText.setText(self.level.toString());
// Save progression
storage['hero_' + self.heroType + '_level'] = self.level;
// Level up effects
self.onLevelUp();
// Visual level up effect
tween(heroGraphics, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFFD700
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(heroGraphics, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
// Save XP
storage['hero_' + self.heroType + '_xp'] = self.experience;
self.updateXpBar();
};
self.onLevelUp = function () {
// Increase health
var healthIncrease = self.heroType === 'tank' ? 50 : self.heroType === 'support' ? 30 : 35;
self.maxHealth += healthIncrease;
self.health = self.maxHealth; // Full heal on level up
self.updateHealthBar();
};
self.updateHealthBar = function () {
self.healthBar.width = self.health / self.maxHealth * 80;
};
self.updateXpBar = function () {
var xpProgress = self.experience / self.getExperienceForNextLevel();
self.xpBar.width = xpProgress * 80;
};
self.useAbility = function () {
if (LK.ticks - self.lastAbilityUse < self.abilityCooldown) {
return false; // Ability on cooldown
}
self.lastAbilityUse = LK.ticks;
// Play hero ability sound
LK.getSound('heroAbility').play();
// Ability cooldown visual effect
tween(self.abilityIcon, {
tint: 0x888888,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: self.abilityCooldown,
easing: tween.linear,
onFinish: function onFinish() {
tween(self.abilityIcon, {
tint: 0xFFD700,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeOut
});
}
});
switch (self.heroType) {
case 'tank':
return self.tankAbility();
case 'support':
return self.supportAbility();
case 'dps':
return self.dpsAbility();
}
return false;
};
self.tankAbility = function () {
// Taunt - Draw all enemies within range to attack hero
var tauntRadius = CELL_SIZE * 4;
var taunted = 0;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= tauntRadius) {
enemy.heroTarget = self;
enemy.heroTargetDuration = 300; // 5 seconds
taunted++;
}
}
// Visual effect
var effectRadius = tauntRadius;
var tauntEffect = new Container();
tauntEffect.x = self.x;
tauntEffect.y = self.y;
var effectGraphics = tauntEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.width = effectGraphics.height = effectRadius * 2;
effectGraphics.tint = 0x4CAF50;
effectGraphics.alpha = 0.6;
game.addChild(tauntEffect);
tween(tauntEffect, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (tauntEffect.parent) {
tauntEffect.parent.removeChild(tauntEffect);
}
}
});
return taunted > 0;
};
self.supportAbility = function () {
// Buff nearby towers
var buffRadius = CELL_SIZE * 3;
var buffed = 0;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= buffRadius) {
tower.heroBuff = {
damage: 1.5,
fireRate: 0.7,
duration: 600 // 10 seconds
};
buffed++;
// Visual effect on tower
tween(tower.children[0], {
tint: 0x2196F3
}, {
duration: 600,
easing: tween.linear,
onFinish: function onFinish() {
tower.children[0].tint = 0x445566;
}
});
}
}
return buffed > 0;
};
self.dpsAbility = function () {
// Area damage attack
var damageRadius = CELL_SIZE * 2.5;
var damaged = 0;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= damageRadius) {
var damage = 100 + (self.level - 1) * 20;
enemy.health -= damage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
damaged++;
// Visual damage effect
tween(enemy, {
tint: 0xFF5722
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(enemy, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
// Visual explosion effect
var explosionEffect = new Container();
explosionEffect.x = self.x;
explosionEffect.y = self.y;
var effectGraphics = explosionEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.width = effectGraphics.height = damageRadius * 2;
effectGraphics.tint = 0xFF5722;
effectGraphics.alpha = 0.8;
game.addChild(explosionEffect);
tween(explosionEffect, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (explosionEffect.parent) {
explosionEffect.parent.removeChild(explosionEffect);
}
}
});
return damaged > 0;
};
self.update = function () {
if (self.health <= 0) {
return;
}
// Update bars
self.updateHealthBar();
self.updateXpBar();
// Move towards enemies or patrol
if (!self.currentTarget) {
self.findTarget();
}
if (self.currentTarget && self.currentTarget.parent && self.currentTarget.health > 0) {
var dx = self.currentTarget.x - self.x;
var dy = self.currentTarget.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > CELL_SIZE * 0.8) {
// Move towards target
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.movementSpeed;
self.y += Math.sin(angle) * self.movementSpeed;
} else {
// Attack target
if (self.heroType === 'tank') {
self.currentTarget.health -= 15;
} else if (self.heroType === 'dps') {
self.currentTarget.health -= 25;
} else {
self.currentTarget.health -= 10;
}
if (self.currentTarget.health <= 0) {
self.currentTarget.health = 0;
self.gainExperience(20);
self.currentTarget = null;
} else {
self.currentTarget.healthBar.width = self.currentTarget.health / self.currentTarget.maxHealth * 70;
}
}
} else {
self.currentTarget = null;
}
};
self.findTarget = function () {
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health <= 0) continue;
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance && distance < CELL_SIZE * 5) {
closestDistance = distance;
closestEnemy = enemy;
}
}
self.currentTarget = closestEnemy;
};
self.down = function (x, y, obj) {
// Use ability when tapped
if (self.useAbility()) {
var notification = game.addChild(new Notification("Hero ability activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
tween.stop(self);
for (var i = 0; i < self.children.length; i++) {
if (self.children[i]) {
tween.stop(self.children[i]);
}
}
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var NebulaCloud = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.radius = 100;
self.isDestroyed = false;
var nebulaGraphics = self.attachAsset('nebulaCloud', {
anchorX: 0.5,
anchorY: 0.5
});
nebulaGraphics.alpha = 0.6;
// Pulsing animation
tween(nebulaGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(nebulaGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.6
}, {
duration: 2000,
easing: tween.easeInOut
});
}
}
});
self.update = function () {
// Apply effects to units within the nebula
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
// Slow effect
if (!enemy.nebulaSlowed) {
enemy.originalSpeed = enemy.originalSpeed || enemy.speed;
enemy.speed *= 0.7; // 30% speed reduction
enemy.nebulaSlowed = true;
}
// Stealth effect - harder for towers to target
enemy.nebulaStealthed = true;
} else {
// Remove effects when outside nebula
if (enemy.nebulaSlowed) {
enemy.speed = enemy.originalSpeed || enemy.speed;
enemy.nebulaSlowed = false;
}
enemy.nebulaStealthed = false;
}
}
// Apply slow effect to heroes
for (var i = 0; i < heroes.length; i++) {
var hero = heroes[i];
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
if (!hero.nebulaSlowed) {
hero.originalMovementSpeed = hero.originalMovementSpeed || hero.movementSpeed;
hero.movementSpeed *= 0.7;
hero.nebulaSlowed = true;
}
} else {
if (hero.nebulaSlowed) {
hero.movementSpeed = hero.originalMovementSpeed || hero.movementSpeed;
hero.nebulaSlowed = false;
}
}
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove effects from all units
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.nebulaSlowed) {
enemy.speed = enemy.originalSpeed || enemy.speed;
enemy.nebulaSlowed = false;
}
enemy.nebulaStealthed = false;
}
for (var i = 0; i < heroes.length; i++) {
var hero = heroes[i];
if (hero.nebulaSlowed) {
hero.movementSpeed = hero.originalMovementSpeed || hero.movementSpeed;
hero.nebulaSlowed = false;
}
}
var nebulaIndex = nebulaClouds.indexOf(self);
if (nebulaIndex !== -1) {
nebulaClouds.splice(nebulaIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
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 () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
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: 0xFFFFFF,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 60;
notificationGraphics.height = 80;
notificationGraphics.tint = 0x1A1A1A;
notificationGraphics.alpha = 0.95;
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 PathJunction = Container.expand(function (x, y, branches) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.branches = branches || []; // Array of possible path directions
self.gridX = Math.floor((x - grid.x) / CELL_SIZE);
self.gridY = Math.floor((y - grid.y) / CELL_SIZE);
self.isDestroyed = false;
// Visual junction indicator
var junctionGraphics = self.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
junctionGraphics.width = CELL_SIZE * 0.8;
junctionGraphics.height = CELL_SIZE * 0.8;
junctionGraphics.tint = 0x9c27b0;
junctionGraphics.alpha = 0.6;
// Pulsing animation to indicate junction
tween(junctionGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(junctionGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.6
}, {
duration: 1500,
easing: tween.easeInOut
});
}
}
});
// Choose path for enemy based on various factors
self.choosePath = function (enemy) {
if (self.branches.length === 0) return null;
var pathScores = [];
// Evaluate each branch
for (var i = 0; i < self.branches.length; i++) {
var branch = self.branches[i];
var score = 0;
// Base score - some randomness for variety
score += Math.random() * 100;
// Factor in enemy type preferences
switch (enemy.type) {
case 'fast':
// Fast enemies prefer shorter paths
score += branch.isShortPath ? 150 : 50;
break;
case 'immune':
// Immune enemies prefer heavily defended paths (they can tank it)
score += branch.towerDensity * 80;
break;
case 'flying':
// Flying enemies prefer paths with fewer anti-air towers
score += branch.hasAntiAir ? 20 : 120;
break;
case 'swarm':
// Swarm enemies prefer paths other swarm enemies haven't taken recently
score += branch.recentSwarmTraffic > 3 ? 30 : 100;
break;
default:
// Normal enemies prefer balanced paths
score += branch.difficulty < 50 ? 80 : 60;
break;
}
// Factor in current congestion (avoid clustering)
var congestionPenalty = branch.currentEnemyCount * 25;
score -= congestionPenalty;
// Factor in tower coverage (enemies avoid heavy defenses unless immune)
if (!enemy.isImmune) {
score -= branch.towerDensity * 40;
}
// Factor in path length (generally prefer shorter paths)
score -= branch.pathLength * 2;
pathScores.push({
branch: branch,
score: score
});
}
// Sort by score and add some weighted randomness
pathScores.sort(function (a, b) {
return b.score - a.score;
});
// Weighted selection - higher scores more likely but not guaranteed
var totalWeight = 0;
for (var i = 0; i < pathScores.length; i++) {
var weight = Math.max(1, pathScores[i].score);
pathScores[i].weight = weight;
totalWeight += weight;
}
var randomValue = Math.random() * totalWeight;
var currentWeight = 0;
for (var i = 0; i < pathScores.length; i++) {
currentWeight += pathScores[i].weight;
if (randomValue <= currentWeight) {
return pathScores[i].branch;
}
}
// Fallback to first option
return pathScores[0].branch;
};
// Update junction statistics
self.update = function () {
// Update branch statistics
for (var i = 0; i < self.branches.length; i++) {
var branch = self.branches[i];
// Count enemies currently on this branch
branch.currentEnemyCount = 0;
branch.recentSwarmTraffic = Math.max(0, branch.recentSwarmTraffic - 0.1);
for (var j = 0; j < enemies.length; j++) {
var enemy = enemies[j];
if (enemy.chosenBranch === branch) {
branch.currentEnemyCount++;
if (enemy.type === 'swarm') {
branch.recentSwarmTraffic += 0.1;
}
}
}
// Calculate tower density in branch area
branch.towerDensity = 0;
branch.hasAntiAir = false;
for (var t = 0; t < towers.length; t++) {
var tower = towers[t];
var dx = tower.x - (grid.x + branch.targetX * CELL_SIZE);
var dy = tower.y - (grid.y + branch.targetY * CELL_SIZE);
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 4) {
branch.towerDensity++;
if (tower.id === 'sniper' || tower.id === 'rapid') {
branch.hasAntiAir = true;
}
}
}
// Update path difficulty
branch.difficulty = branch.towerDensity * 10 + branch.pathLength;
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
var junctionIndex = pathJunctions.indexOf(self);
if (junctionIndex !== -1) {
pathJunctions.splice(junctionIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var RouteVisualizer = Container.expand(function () {
var self = Container.call(this);
self.pathLines = [];
self.isDestroyed = false;
self.showPaths = function (junction, enemy) {
// Clear existing path lines
self.clearPaths();
// Create visual lines for each possible branch
for (var i = 0; i < junction.branches.length; i++) {
var branch = junction.branches[i];
var pathLine = new Container();
// Create line segments to show the path
var segments = [];
var currentX = junction.gridX;
var currentY = junction.gridY;
// Draw path to target
var targetX = branch.targetX;
var targetY = branch.targetY;
var steps = Math.max(Math.abs(targetX - currentX), Math.abs(targetY - currentY));
for (var step = 0; step <= steps; step++) {
var progress = step / steps;
var segmentX = Math.round(currentX + (targetX - currentX) * progress);
var segmentY = Math.round(currentY + (targetY - currentY) * progress);
var segment = pathLine.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
segment.width = 12;
segment.height = 12;
segment.x = grid.x + segmentX * CELL_SIZE;
segment.y = grid.y + segmentY * CELL_SIZE;
// Color code based on path difficulty
var difficulty = branch.difficulty || 0;
if (difficulty < 30) {
segment.tint = 0x00FF00; // Green for easy
} else if (difficulty < 60) {
segment.tint = 0xFFFF00; // Yellow for medium
} else {
segment.tint = 0xFF0000; // Red for hard
}
segment.alpha = 0.7;
}
game.addChild(pathLine);
self.pathLines.push(pathLine);
// Animate the path appearance
tween(pathLine, {
alpha: 1.0
}, {
duration: 500,
easing: tween.easeOut
});
}
// Auto-hide after a few seconds
LK.setTimeout(function () {
self.clearPaths();
}, 3000);
};
self.clearPaths = function () {
for (var i = 0; i < self.pathLines.length; i++) {
if (self.pathLines[i].parent) {
self.pathLines[i].parent.removeChild(self.pathLines[i]);
}
}
self.pathLines = [];
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
self.clearPaths();
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var SolarFlare = Container.expand(function () {
var self = Container.call(this);
self.duration = 600; // 10 seconds at 60fps
self.remainingTime = self.duration;
self.isActive = true;
self.isDestroyed = false;
// Position in center of battlefield
self.x = grid.x + 12 * CELL_SIZE;
self.y = grid.y + 15 * CELL_SIZE;
var flareGraphics = self.attachAsset('solarFlare', {
anchorX: 0.5,
anchorY: 0.5
});
flareGraphics.alpha = 0.8;
flareGraphics.blendMode = 1; // Additive blending
// Intense pulsing animation
tween(flareGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(flareGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 300,
easing: tween.easeInOut
});
}
}
});
// Show notification
var notification = game.addChild(new Notification("⚡ Solar Flare! Energy towers boosted! ⚡"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
self.update = function () {
self.remainingTime--;
if (self.remainingTime <= 0) {
self.destroy();
return;
}
// Boost energy-based towers (rapid, sniper, default)
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.id === 'rapid' || tower.id === 'sniper' || tower.id === 'default') {
if (!tower.solarFlareBoost) {
tower.originalFireRate = tower.fireRate;
tower.originalDamage = tower.damage;
tower.fireRate = Math.floor(tower.fireRate * 0.6); // 40% faster
tower.damage = Math.floor(tower.damage * 1.3); // 30% more damage
tower.solarFlareBoost = true;
// Visual effect on boosted towers
tween(tower.energyCore, {
tint: 0xFFD700,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut
});
}
}
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove boosts from all towers
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.solarFlareBoost) {
tower.fireRate = tower.originalFireRate;
tower.damage = tower.originalDamage;
tower.solarFlareBoost = false;
// Reset visual effects
var originalTint = 0x88AACC;
switch (tower.id) {
case 'rapid':
originalTint = 0x00AAFF;
break;
case 'sniper':
originalTint = 0xFF5500;
break;
case 'default':
originalTint = 0x88AACC;
break;
}
tween(tower.energyCore, {
tint: originalTint,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
}
var flareIndex = solarFlares.indexOf(self);
if (flareIndex !== -1) {
solarFlares.splice(flareIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
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('tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
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(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -20; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost, {
size: 50,
fill: 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 = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var SpecializationMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 300;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 1800;
menuBackground.height = 600;
menuBackground.tint = 0x1A1A1A;
menuBackground.alpha = 0.98;
var titleText = new Text2('Choose Specialization Path', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -200;
self.addChild(titleText);
var subtitleText = new Text2('Your tower has reached level 3 and can specialize!', {
size: 50,
fill: 0xCCCCCC,
weight: 400
});
subtitleText.anchor.set(0.5, 0.5);
subtitleText.y = -140;
self.addChild(subtitleText);
// Create specialization options based on tower type
var options = [];
if (self.tower.id === 'sniper') {
options = [{
type: 'antiarmor',
name: 'Anti-Armor',
desc: 'Pierce shields\n+50% damage vs immune',
color: 0xFF6600
}, {
type: 'longrange',
name: 'Long-Range',
desc: 'Double firing range\nBetter enemy targeting',
color: 0x00AAFF
}];
} else if (self.tower.id === 'splash') {
options = [{
type: 'nuclear',
name: 'Nuclear',
desc: 'Huge damage\nLarger splash radius',
color: 0xFF0000
}, {
type: 'chainreaction',
name: 'Chain Reaction',
desc: 'Damage spreads\nbetween enemies',
color: 0xFFFF00
}];
}
// Create option buttons
for (var i = 0; i < options.length; i++) {
var option = options[i];
var button = new Container();
var buttonBg = button.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 700;
buttonBg.height = 200;
buttonBg.tint = option.color;
buttonBg.alpha = 0.8;
var nameText = new Text2(option.name, {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
nameText.anchor.set(0.5, 0.5);
nameText.y = -40;
button.addChild(nameText);
var descText = new Text2(option.desc, {
size: 45,
fill: 0xFFFFFF,
weight: 400
});
descText.anchor.set(0.5, 0.5);
descText.y = 40;
button.addChild(descText);
button.x = (i - 0.5) * 800;
button.y = 50;
button.optionType = option.type;
button.down = function () {
self.selectSpecialization(this.optionType);
};
self.addChild(button);
}
// Close button
var closeButton = new Container();
var closeBg = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBg.width = 100;
closeBg.height = 100;
closeBg.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = 850;
closeButton.y = -250;
closeButton.down = function () {
self.destroy();
};
self.addChild(closeButton);
self.selectSpecialization = function (branchType) {
// Apply specialization to tower
var specialization = new TowerSpecialization(self.tower, branchType);
self.tower.specialization = specialization;
self.tower.addChild(specialization);
specialization.applySpecialization();
var notification = game.addChild(new Notification("Tower specialized: " + branchType));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.destroy();
};
return self;
});
var StationCustomizer = Container.expand(function () {
var self = Container.call(this);
self.modules = [];
self.moduleSlots = [];
self.selectedSlot = null;
self.customizationMode = false;
// Create module slots around the station
var slotPositions = [{
x: -2,
y: -2
}, {
x: 0,
y: -2
}, {
x: 2,
y: -2
}, {
x: -2,
y: 0
}, {
x: 2,
y: 0
}, {
x: -2,
y: 2
}, {
x: 0,
y: 2
}, {
x: 2,
y: 2
}];
for (var i = 0; i < slotPositions.length; i++) {
var slot = new Container();
var slotGraphics = slot.attachAsset('moduleSocket', {
anchorX: 0.5,
anchorY: 0.5
});
slotGraphics.alpha = 0.5;
slotGraphics.tint = 0x556677;
slot.gridX = slotPositions[i].x;
slot.gridY = slotPositions[i].y;
slot.x = grid.x + (12 + slot.gridX) * CELL_SIZE;
slot.y = grid.y + (15 + slot.gridY) * CELL_SIZE;
slot.module = null;
slot.slotIndex = i;
slot.down = function () {
if (self.customizationMode) {
self.selectedSlot = this;
self.showModuleMenu();
}
};
self.addChild(slot);
self.moduleSlots.push(slot);
}
self.showModuleMenu = function () {
if (!self.selectedSlot) return;
var menu = new Container();
var menuBg = menu.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBg.width = 600;
menuBg.height = 300;
menuBg.tint = 0x333333;
var moduleTypes = ['research', 'shield', 'teleporter'];
for (var i = 0; i < moduleTypes.length; i++) {
var button = new Container();
var buttonBg = button.attachAsset('techTreeNode', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 120;
buttonBg.height = 80;
var cost = getModuleCost(moduleTypes[i], 1);
var buttonText = new Text2(moduleTypes[i] + '\n' + cost + 'g', {
size: 30,
fill: 0xFFFFFF,
weight: 600
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.moduleType = moduleTypes[i];
button.x = -180 + i * 180;
button.y = 50;
button.down = function () {
self.buildModule(this.moduleType);
menu.destroy();
};
menu.addChild(button);
}
menu.x = 2048 / 2;
menu.y = 1400;
game.addChild(menu);
LK.setTimeout(function () {
if (menu.parent) {
menu.parent.removeChild(menu);
}
}, 5000);
};
self.buildModule = function (moduleType) {
if (!self.selectedSlot || self.selectedSlot.module) return;
var cost = getModuleCost(moduleType, 1);
if (gold < cost) {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
var module;
switch (moduleType) {
case 'research':
module = new ResearchLab();
break;
case 'shield':
module = new ShieldGenerator();
break;
case 'teleporter':
module = new TeleporterNode();
break;
default:
module = new StationModule(moduleType);
}
module.gridX = self.selectedSlot.gridX;
module.gridY = self.selectedSlot.gridY;
module.x = self.selectedSlot.x;
module.y = self.selectedSlot.y;
self.selectedSlot.module = module;
self.selectedSlot.children[0].alpha = 0; // Hide slot graphic
self.addChild(module);
self.modules.push(module);
setGold(gold - cost);
var notification = game.addChild(new Notification(moduleType + " module built!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.selectedSlot = null;
};
self.toggleCustomizationMode = function () {
self.customizationMode = !self.customizationMode;
for (var i = 0; i < self.moduleSlots.length; i++) {
var slot = self.moduleSlots[i];
if (!slot.module) {
slot.children[0].alpha = self.customizationMode ? 0.8 : 0.3;
}
}
var modeText = self.customizationMode ? "ON" : "OFF";
var notification = game.addChild(new Notification("Customization mode: " + modeText));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
};
self.update = function () {
for (var i = 0; i < self.modules.length; i++) {
if (self.modules[i].update) {
self.modules[i].update();
}
}
};
return self;
});
var StationModule = Container.expand(function (moduleType) {
var self = Container.call(this);
self.moduleType = moduleType || 'basic';
self.level = 1;
self.maxLevel = 3;
self.isActive = true;
self.gridX = 0;
self.gridY = 0;
self.isDestroyed = false;
// Get appropriate asset
var assetName = 'stationModule';
switch (moduleType) {
case 'research':
assetName = 'researchLab';
break;
case 'shield':
assetName = 'shieldGenerator';
break;
case 'teleporter':
assetName = 'teleporter';
break;
}
var moduleGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add energy core
var energyCore = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
energyCore.width = 30;
energyCore.height = 30;
energyCore.tint = 0x00aaff;
// Level indicators
var levelIndicators = [];
for (var i = 0; i < self.maxLevel; i++) {
var indicator = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = 12;
indicator.height = 12;
indicator.x = -30 + i * 30;
indicator.y = 60;
indicator.tint = i < self.level ? 0xffffff : 0x444444;
levelIndicators.push(indicator);
}
self.updateLevelIndicators = function () {
for (var i = 0; i < levelIndicators.length; i++) {
levelIndicators[i].tint = i < self.level ? 0xffffff : 0x444444;
}
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
var upgradeCost = getModuleCost(self.moduleType, self.level + 1);
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
self.updateLevelIndicators();
self.onUpgrade();
return true;
}
}
return false;
};
self.onUpgrade = function () {
// Override in specific module types
};
self.update = function () {
if (!self.isActive) return;
// Pulsing energy core
if (LK.ticks % 120 === 0) {
tween(energyCore, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(energyCore, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var TeleporterNode = StationModule.expand(function () {
var self = StationModule.call(this, 'teleporter');
self.teleportEnergy = 100;
self.maxTeleportEnergy = 100;
self.linkedNodes = [];
self.onUpgrade = function () {
self.maxTeleportEnergy = 100 + (self.level - 1) * 25;
self.teleportEnergy = self.maxTeleportEnergy;
};
self.teleportHero = function (hero, targetNode) {
if (self.teleportEnergy >= 30 && targetNode && targetNode.teleportEnergy >= 30) {
// Create teleport effect at source
var sourceEffect = new Container();
sourceEffect.x = self.x;
sourceEffect.y = self.y;
var sourceGraphics = sourceEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
sourceGraphics.tint = 0x9c27b0;
sourceGraphics.alpha = 0.8;
game.addChild(sourceEffect);
// Create teleport effect at destination
var destEffect = new Container();
destEffect.x = targetNode.x;
destEffect.y = targetNode.y;
var destGraphics = destEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
destGraphics.tint = 0x9c27b0;
destGraphics.alpha = 0.8;
game.addChild(destEffect);
// Animate effects
tween(sourceEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (sourceEffect.parent) {
sourceEffect.parent.removeChild(sourceEffect);
}
}
});
tween(destEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (destEffect.parent) {
destEffect.parent.removeChild(destEffect);
}
}
});
// Teleport hero
hero.x = targetNode.x;
hero.y = targetNode.y;
// Consume energy
self.teleportEnergy = Math.max(0, self.teleportEnergy - 30);
targetNode.teleportEnergy = Math.max(0, targetNode.teleportEnergy - 30);
return true;
}
return false;
};
self.redirectEnemyPath = function () {
if (self.teleportEnergy >= 50 && self.linkedNodes.length > 0) {
var targetNode = self.linkedNodes[Math.floor(Math.random() * self.linkedNodes.length)];
// Find nearby enemies and redirect one
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy.isFlying) {
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 2) {
// Teleport enemy
enemy.x = targetNode.x;
enemy.y = targetNode.y;
enemy.currentCellX = targetNode.gridX;
enemy.currentCellY = targetNode.gridY;
enemy.currentTarget = null;
self.teleportEnergy = Math.max(0, self.teleportEnergy - 50);
var notification = game.addChild(new Notification("Enemy redirected through teleporter!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
break;
}
}
}
}
};
self.update = function () {
StationModule.prototype.update.call(self);
// Regenerate teleport energy
if (LK.ticks % 120 === 0 && self.teleportEnergy < self.maxTeleportEnergy) {
// Every 2 seconds
self.teleportEnergy = Math.min(self.maxTeleportEnergy, self.teleportEnergy + self.level * 3);
}
};
self.down = function () {
self.redirectEnemyPath();
};
return self;
});
var ShieldGenerator = StationModule.expand(function () {
var self = StationModule.call(this, 'shield');
self.shieldEnergy = 100;
self.maxShieldEnergy = 100;
self.shieldActive = false;
self.shieldEffect = null;
self.onUpgrade = function () {
self.maxShieldEnergy = 100 + (self.level - 1) * 50;
self.shieldEnergy = self.maxShieldEnergy;
};
self.activateShield = function () {
if (self.shieldEnergy >= 50 && !self.shieldActive) {
self.shieldActive = true;
self.shieldEnergy = Math.max(0, self.shieldEnergy - 50);
// Create visual shield effect
self.shieldEffect = new Container();
self.shieldEffect.x = self.x;
self.shieldEffect.y = self.y;
var shieldGraphics = self.shieldEffect.attachAsset('shieldEffect', {
anchorX: 0.5,
anchorY: 0.5
});
shieldGraphics.alpha = 0.3;
shieldGraphics.tint = 0x00ffff;
game.addChild(self.shieldEffect);
// Shield lasts for 10 seconds
LK.setTimeout(function () {
self.deactivateShield();
}, 10000);
var notification = game.addChild(new Notification("Station shields activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
self.deactivateShield = function () {
self.shieldActive = false;
if (self.shieldEffect && self.shieldEffect.parent) {
self.shieldEffect.parent.removeChild(self.shieldEffect);
self.shieldEffect = null;
}
};
self.update = function () {
StationModule.prototype.update.call(self);
// Regenerate shield energy
if (LK.ticks % 180 === 0 && self.shieldEnergy < self.maxShieldEnergy) {
// Every 3 seconds
self.shieldEnergy = Math.min(self.maxShieldEnergy, self.shieldEnergy + self.level * 5);
}
// Update shield effect position
if (self.shieldEffect) {
self.shieldEffect.x = self.x;
self.shieldEffect.y = self.y;
}
};
self.down = function () {
self.activateShield();
};
return self;
});
var ResearchLab = StationModule.expand(function () {
var self = StationModule.call(this, 'research');
self.researchPoints = 0;
self.activeResearch = null;
self.completedTechs = [];
self.onUpgrade = function () {
// Higher level labs generate research points faster
self.researchPointsPerTick = self.level;
};
self.update = function () {
StationModule.prototype.update.call(self);
// Generate research points
if (LK.ticks % 60 === 0) {
// Every second
self.researchPoints += self.level;
storage.researchPoints = self.researchPoints;
}
// Complete active research
if (self.activeResearch && self.researchPoints >= self.activeResearch.cost) {
self.completeResearch();
}
};
self.startResearch = function (techId) {
var tech = getTechnology(techId);
if (tech && !self.activeResearch) {
self.activeResearch = tech;
var notification = game.addChild(new Notification("Research started: " + tech.name));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
self.completeResearch = function () {
if (self.activeResearch) {
self.researchPoints -= self.activeResearch.cost;
self.completedTechs.push(self.activeResearch.id);
storage.completedTechs = self.completedTechs;
// Apply research benefits
self.activeResearch.onComplete();
var notification = game.addChild(new Notification("Research complete: " + self.activeResearch.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.activeResearch = null;
}
};
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
var baseRange;
switch (self.id) {
case 'sniper':
// Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost
if (self.level === self.maxLevel) {
baseRange = 12 * CELL_SIZE; // Significantly increased range for max level
} else {
baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE;
}
break;
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;
default:
// Default: base 3, +0.5 per level
baseRange = (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
// Apply specialization range multiplier
if (self.specialization && self.specialization.branchType === 'longrange') {
baseRange *= 2.0;
}
return baseRange;
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
switch (self.id) {
case 'rapid':
self.fireRate = 35; // Slightly slower to reduce overwhelming power
self.damage = 8; // Increased from 5 for better effectiveness
self.range = 2.8 * CELL_SIZE; // Slightly better range
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 100; // Slower but more impactful
self.damage = 40; // Increased significantly for true sniper feel
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 80; // Slightly slower
self.damage = 18; // Increased base damage
self.range = 2.2 * CELL_SIZE; // Slightly better range
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 45; // Faster to apply slow effects more consistently
self.damage = 12; // Increased from 8 for better utility
self.range = 3.8 * CELL_SIZE; // Better range for support role
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 60; // Faster to apply poison more effectively
self.damage = 15; // Increased from 12
self.range = 3.5 * CELL_SIZE; // Better range
self.bulletSpeed = 5;
break;
}
// Create tower base structure
var baseAssetName = 'tower_base_' + (self.id === 'default' ? 'default' : self.id);
var baseGraphics = self.attachAsset(baseAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Create support structures for higher levels
self.supportStructures = [];
// Create energy core based on tower type
var energyCore = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
energyCore.width = 40;
energyCore.height = 40;
switch (self.id) {
case 'rapid':
energyCore.tint = 0x00AAFF;
break;
case 'sniper':
energyCore.tint = 0xFF5500;
break;
case 'splash':
energyCore.tint = 0x33CC00;
break;
case 'slow':
energyCore.tint = 0x9900FF;
break;
case 'poison':
energyCore.tint = 0x00FFAA;
break;
default:
energyCore.tint = 0x88AACC;
}
// Add pulsing energy core animation
self.energyCore = energyCore;
tween(energyCore, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(energyCore, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 1000,
easing: tween.easeInOut
});
}
});
// Method to update tower visual appearance based on level
self.updateTowerVisuals = function () {
// Ensure gunContainer exists before trying to use it
if (!gunContainer) {
console.warn("gunContainer not found, skipping visual update");
return;
}
// Clear existing gun graphics
if (self.gunGraphics && self.gunGraphics.parent) {
gunContainer.removeChild(self.gunGraphics);
}
// Clear existing support structures
for (var i = 0; i < self.supportStructures.length; i++) {
if (self.supportStructures[i].parent) {
self.removeChild(self.supportStructures[i]);
}
}
self.supportStructures = [];
// Create new gun graphics based on current level
var gunAssetName = 'tower_gun_' + (self.id === 'default' ? 'default' : self.id) + '_' + self.level;
self.gunGraphics = gunContainer.attachAsset(gunAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add support structures for higher levels
if (self.level >= 2) {
// Add support beams
for (var i = 0; i < 2; i++) {
var beam = self.attachAsset('tower_support_beam', {
anchorX: 0.5,
anchorY: 1.0
});
beam.x = i === 0 ? -30 : 30;
beam.y = baseGraphics.height / 2 - 10;
beam.rotation = i === 0 ? -0.3 : 0.3;
self.supportStructures.push(beam);
}
}
if (self.level >= 3) {
// Add radar dish for better targeting
var radar = self.attachAsset('tower_radar_dish', {
anchorX: 0.5,
anchorY: 0.5
});
radar.x = 0;
radar.y = -baseGraphics.height / 2 - 20;
radar.alpha = 0.8;
self.supportStructures.push(radar);
// Rotate radar dish slowly
tween(radar, {
rotation: Math.PI * 2
}, {
duration: 4000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed && radar.parent) {
radar.rotation = 0;
}
}
});
}
if (self.level >= 4) {
// Add antenna for enhanced range
var antenna = self.attachAsset('tower_antenna', {
anchorX: 0.5,
anchorY: 1.0
});
antenna.x = 0;
antenna.y = -baseGraphics.height / 2 - 60;
self.supportStructures.push(antenna);
// Add armor plates
for (var i = 0; i < 4; i++) {
var armor = self.attachAsset('tower_armor_plate', {
anchorX: 0.5,
anchorY: 0.5
});
var angle = i / 4 * Math.PI * 2;
armor.x = Math.cos(angle) * 40;
armor.y = Math.sin(angle) * 40;
armor.rotation = angle;
armor.alpha = 0.7;
self.supportStructures.push(armor);
}
}
if (self.level >= 5) {
// Add energy conduits
for (var i = 0; i < 3; i++) {
var conduit = self.attachAsset('tower_energy_conduit', {
anchorX: 0.5,
anchorY: 0.5
});
var angle = i / 3 * Math.PI * 2;
conduit.x = Math.cos(angle) * 50;
conduit.y = Math.sin(angle) * 50;
conduit.rotation = angle + Math.PI / 2;
// Pulsing energy effect
tween(conduit, {
alpha: 0.4
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed && conduit.parent) {
tween(conduit, {
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
}
});
self.supportStructures.push(conduit);
}
}
if (self.level >= 6) {
// Max level - add ultimate enhancement visual
var enhancement = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
enhancement.width = 80;
enhancement.height = 80;
enhancement.tint = 0xFFD700;
enhancement.alpha = 0.3;
enhancement.y = 0;
// Rotating golden aura for max level
tween(enhancement, {
rotation: Math.PI * 2,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed && enhancement.parent) {
enhancement.rotation = 0;
enhancement.scaleX = 1.0;
enhancement.scaleY = 1.0;
}
}
});
self.supportStructures.push(enhancement);
}
// Update energy core size based on level
var coreSize = 40 + (self.level - 1) * 8;
self.energyCore.width = coreSize;
self.energyCore.height = coreSize;
// Add specialization visuals if present
if (self.specialization) {
self.updateSpecializationVisuals();
}
};
// Method to add specialization visual indicators
self.updateSpecializationVisuals = function () {
if (!self.specialization) return;
var specAssetName;
switch (self.specialization.branchType) {
case 'antiarmor':
specAssetName = 'tower_spec_armor_pierce';
break;
case 'longrange':
specAssetName = 'tower_spec_long_range';
break;
case 'nuclear':
specAssetName = 'tower_spec_nuclear';
break;
case 'chainreaction':
specAssetName = 'tower_spec_chain_reaction';
break;
default:
return;
}
var specVisual = self.attachAsset(specAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
specVisual.y = -baseGraphics.height / 2 - 40;
specVisual.alpha = 0.9;
// Pulsing effect for specialization indicator
tween(specVisual, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.6
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed && specVisual.parent) {
tween(specVisual, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.9
}, {
duration: 1200,
easing: tween.easeInOut
});
}
}
});
self.supportStructures.push(specVisual);
};
// Initialize tower visuals
self.updateTowerVisuals();
var gunContainer = new Container();
self.addChild(gunContainer);
var gunGraphics = gunContainer.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
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);
}
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// More balanced upgrade cost scaling
if (self.level === self.maxLevel - 1) {
// Final upgrade is expensive but not prohibitive
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(1.8, self.level - 1) * 2.5);
} else {
// Use 1.8x multiplier instead of 2x for more manageable costs
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(1.8, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// Play upgrade sound
LK.getSound('towerUpgrade').play();
// Update tower visuals for new level
self.updateTowerVisuals();
// Check for specialization unlock at level 3
if (self.level === 3 && !self.specialization && (self.id === 'sniper' || self.id === 'splash')) {
var specMenu = new SpecializationMenu(self);
game.addChild(specMenu);
specMenu.x = 2048 / 2;
tween(specMenu, {
y: 2732 - 300
}, {
duration: 300,
easing: tween.backOut
});
specializationMenus.push(specMenu);
return true;
}
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type with balanced scaling
switch (self.id) {
case 'rapid':
if (self.level === self.maxLevel) {
// Max level gets significant boost
self.fireRate = Math.max(8, 35 - self.level * 4.2);
self.damage = 8 + self.level * 6; // More modest scaling
self.bulletSpeed = 7 + self.level * 1.5;
} else {
self.fireRate = Math.max(18, 35 - self.level * 2.8);
self.damage = 8 + self.level * 3.5;
self.bulletSpeed = 7 + self.level * 0.8;
}
break;
case 'sniper':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(15, 100 - self.level * 12);
self.damage = 40 + self.level * 18; // High damage scaling
self.bulletSpeed = 25 + self.level * 2;
} else {
self.fireRate = Math.max(30, 100 - self.level * 8);
self.damage = 40 + self.level * 12;
self.bulletSpeed = 25 + self.level * 1;
}
break;
case 'splash':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(12, 80 - self.level * 10);
self.damage = 18 + self.level * 12; // Strong splash scaling
self.bulletSpeed = 4 + self.level * 1.5;
} else {
self.fireRate = Math.max(25, 80 - self.level * 6);
self.damage = 18 + self.level * 8;
self.bulletSpeed = 4 + self.level * 0.8;
}
break;
case 'slow':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(8, 45 - self.level * 5.5);
self.damage = 12 + self.level * 8; // Better damage for utility tower
self.bulletSpeed = 5 + self.level * 1.2;
} else {
self.fireRate = Math.max(20, 45 - self.level * 3.5);
self.damage = 12 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.7;
}
break;
case 'poison':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(10, 60 - self.level * 7.5);
self.damage = 15 + self.level * 10; // Strong poison scaling
self.bulletSpeed = 5 + self.level * 1.3;
} else {
self.fireRate = Math.max(22, 60 - self.level * 5);
self.damage = 15 + self.level * 6;
self.bulletSpeed = 5 + self.level * 0.8;
}
break;
default:
if (self.level === self.maxLevel) {
self.fireRate = Math.max(10, 60 - self.level * 8);
self.damage = 10 + self.level * 12;
self.bulletSpeed = 5 + self.level * 1.5;
} else {
self.fireRate = Math.max(25, 60 - self.level * 5);
self.damage = 10 + self.level * 7;
self.bulletSpeed = 5 + self.level * 0.8;
}
}
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
// Use spatial partitioning for better performance
var nearbyEnemies = spatialGrid.getNearbyEnemies(self.x, self.y, self.getRange());
for (var i = 0; i < nearbyEnemies.length; i++) {
var enemy = nearbyEnemies[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()) {
// Apply nebula stealth effect - reduce targeting chance
var targetingChance = enemy.nebulaStealthed ? 0.3 : 1.0;
if (Math.random() > targetingChance) {
continue; // Skip this enemy due to stealth
}
// 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 () {
// Update ultimate ability
if (self.ultimate) {
self.ultimate.update();
}
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
var currentFireRate = self.fireRate;
if (self.heroBuff && self.heroBuff.duration > 0) {
currentFireRate = Math.floor(self.fireRate * self.heroBuff.fireRate);
}
if (LK.ticks - self.lastFired >= currentFireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
tutorialSystem.checkActionCompleted('selectTower');
}
// Check for ultimate ability activation (double tap or long press simulation)
if (self.ultimate && self.ultimate.canActivate()) {
// Simple activation - tap when ultimate is ready
if (self.ultimate.activate()) {
return; // Ultimate was activated, don't show menu
}
}
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 potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
// Apply hero buff if present
var finalDamage = self.damage;
var finalFireRate = self.fireRate;
if (self.heroBuff && self.heroBuff.duration > 0) {
finalDamage = Math.floor(self.damage * self.heroBuff.damage);
finalFireRate = Math.floor(self.fireRate * self.heroBuff.fireRate);
self.heroBuff.duration--;
if (self.heroBuff.duration <= 0) {
self.heroBuff = null;
}
}
// Apply support bonus from linked towers
if (self.supportBonus) {
finalDamage = Math.floor(finalDamage * self.supportBonus);
finalFireRate = Math.floor(finalFireRate / self.supportBonus);
}
// Apply specialization effects
if (self.specialization) {
if (self.specialization.branchType === 'antiarmor' && self.targetEnemy.isImmune) {
finalDamage = Math.floor(finalDamage * 1.5);
}
}
var bullet = getBulletFromPool(bulletX, bulletY, self.targetEnemy, finalDamage, 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;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// Play shooting sound
LK.getSound('towerShoot').play();
// --- Sci-fi charge and firing effect ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
// Add energy charge effect to core
if (self.energyCore) {
tween.stop(self.energyCore, {
scaleX: true,
scaleY: true,
alpha: true
});
tween(self.energyCore, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.5
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.energyCore, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
// Always use the original resting position for recoil, never accumulate offset
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset to resting position before animating (in case of interrupted tweens)
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Calculate recoil offset (recoil back along the gun's rotation)
var recoilDistance = 8;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
// Animate recoil back from the resting position
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original position/scale
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 1;
}
}
}
self.refreshCellsInRange();
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this tower and its components
tween.stop(self);
tween.stop(gunContainer);
if (self.energyCore) {
tween.stop(self.energyCore);
}
// Clean up all support structures and their tweens
if (self.supportStructures) {
for (var i = 0; i < self.supportStructures.length; i++) {
if (self.supportStructures[i]) {
tween.stop(self.supportStructures[i]);
}
}
self.supportStructures = [];
}
// Clean up gun graphics
if (self.gunGraphics) {
tween.stop(self.gunGraphics);
self.gunGraphics = null;
}
// Clear cells in range references
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 = [];
// Clear grid cells occupied by this tower
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (cell) {
cell.type = 0;
}
}
}
// Remove from towers array
var towerIndex = towers.indexOf(self);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Remove any associated range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
game.removeChild(game.children[i]);
}
}
// Close any upgrade menus for this tower
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu && child.tower === self;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
// Clear selected tower if it's this tower
if (selectedTower === self) {
selectedTower = null;
}
// Clean up ultimate ability
if (self.ultimate) {
self.ultimate.destroy();
self.ultimate = null;
}
// Nullify all object references
self.targetEnemy = null;
self.energyCore = null;
// Call parent destroy
Container.prototype.destroy.call(self);
};
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;
self.hasEnoughGold = 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;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (!cell || cell.type !== 0) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
self.canPlace = validGridPlacement && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var TowerSpecialization = Container.expand(function (tower, branchType) {
var self = Container.call(this);
self.tower = tower;
self.branchType = branchType || 'none';
self.linkedTowers = [];
self.isDestroyed = false;
// Add specialization visual indicator
var specIcon = self.attachAsset('specializationIcon', {
anchorX: 0.5,
anchorY: 0.5
});
specIcon.width = 30;
specIcon.height = 30;
specIcon.y = -self.tower.children[0].height / 2 - 20;
// Set specialization color based on branch
switch (self.branchType) {
case 'antiarmor':
specIcon.tint = 0xFF6600;
break;
case 'longrange':
specIcon.tint = 0x00AAFF;
break;
case 'nuclear':
specIcon.tint = 0xFF0000;
break;
case 'chainreaction':
specIcon.tint = 0xFFFF00;
break;
default:
specIcon.tint = 0xFFD700;
}
self.applySpecialization = function () {
switch (self.branchType) {
case 'antiarmor':
// Anti-armor: pierce shields, extra damage to immune enemies
self.tower.armorPiercing = true;
self.tower.damage = Math.floor(self.tower.damage * 1.5);
break;
case 'longrange':
// Long-range: significantly extended range
self.tower.rangeMultiplier = 2.0;
self.tower.refreshCellsInRange();
break;
case 'nuclear':
// Nuclear: huge splash damage, slower fire rate
self.tower.damage = Math.floor(self.tower.damage * 2.5);
self.tower.fireRate = Math.floor(self.tower.fireRate * 1.8);
self.tower.splashRadius = CELL_SIZE * 3;
break;
case 'chainreaction':
// Chain reaction: damage spreads between enemies
self.tower.chainReaction = true;
self.tower.chainRange = CELL_SIZE * 2;
self.tower.chainDamage = 0.7;
break;
}
// Update tower visuals to show specialization
if (self.tower.updateSpecializationVisuals) {
self.tower.updateSpecializationVisuals();
}
};
self.findNearbyTowers = function () {
var nearby = [];
var searchRadius = CELL_SIZE * 4;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower !== self.tower && tower.specialization) {
var dx = tower.x - self.tower.x;
var dy = tower.y - self.tower.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= searchRadius) {
nearby.push(tower);
}
}
}
return nearby;
};
self.createSupportLinks = function () {
var nearbyTowers = self.findNearbyTowers();
for (var i = 0; i < nearbyTowers.length; i++) {
var tower = nearbyTowers[i];
if (self.linkedTowers.indexOf(tower) === -1) {
self.linkedTowers.push(tower);
// Create visual link beam
var linkBeam = new Container();
linkBeam.x = self.tower.x;
linkBeam.y = self.tower.y;
var beamGraphics = linkBeam.attachAsset('linkBeam', {
anchorX: 0,
anchorY: 0.5
});
var dx = tower.x - self.tower.x;
var dy = tower.y - self.tower.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
beamGraphics.width = distance;
beamGraphics.rotation = angle;
beamGraphics.alpha = 0.5;
beamGraphics.tint = 0x00FF88;
game.addChild(linkBeam);
self.tower.linkBeam = linkBeam;
}
}
};
self.applySupportBonus = function () {
// Linked towers get damage and fire rate bonus
var bonusMultiplier = 1 + self.linkedTowers.length * 0.15;
self.tower.supportBonus = bonusMultiplier;
};
self.update = function () {
if (self.branchType !== 'none') {
self.createSupportLinks();
self.applySupportBonus();
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove link beams
if (self.tower.linkBeam && self.tower.linkBeam.parent) {
self.tower.linkBeam.parent.removeChild(self.tower.linkBeam);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var TowerUltimate = Container.expand(function (tower, ultimateType) {
var self = Container.call(this);
self.tower = tower;
self.ultimateType = ultimateType || 'none';
self.cooldownTime = 1800; // 30 seconds at 60fps
self.currentCooldown = 0;
self.isDestroyed = false;
// Add ultimate indicator
var ultimateIcon = self.attachAsset('ultimateIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
ultimateIcon.y = -self.tower.children[0].height / 2 - 35;
ultimateIcon.alpha = 0.3; // Dim when on cooldown
// Set ultimate-specific properties
switch (self.ultimateType) {
case 'orbital':
self.cooldownTime = 2400; // 40 seconds
ultimateIcon.tint = 0xFF0000;
break;
case 'nanite':
self.cooldownTime = 2100; // 35 seconds
ultimateIcon.tint = 0x00FF88;
break;
case 'temporal':
self.cooldownTime = 1800; // 30 seconds
ultimateIcon.tint = 0x9966FF;
break;
default:
ultimateIcon.tint = 0xFFD700;
}
self.canActivate = function () {
return self.currentCooldown <= 0 && !self.isDestroyed;
};
self.activate = function () {
if (!self.canActivate()) return false;
self.currentCooldown = self.cooldownTime;
// Visual cooldown effect
ultimateIcon.alpha = 0.3;
tween(ultimateIcon, {
alpha: 1.0
}, {
duration: self.cooldownTime,
easing: tween.linear
});
switch (self.ultimateType) {
case 'orbital':
return self.orbitalStrike();
case 'nanite':
return self.naniteSwarm();
case 'temporal':
return self.timeDilation();
}
return false;
};
self.orbitalStrike = function () {
// Find target area with most enemies
var bestTarget = null;
var maxEnemies = 0;
var strikeRadius = CELL_SIZE * 3;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var nearbyCount = 0;
for (var j = 0; j < enemies.length; j++) {
var otherEnemy = enemies[j];
var dx = otherEnemy.x - enemy.x;
var dy = otherEnemy.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= strikeRadius) {
nearbyCount++;
}
}
if (nearbyCount > maxEnemies) {
maxEnemies = nearbyCount;
bestTarget = enemy;
}
}
if (bestTarget) {
// Create orbital strike effect
var strikeEffect = new Container();
strikeEffect.x = bestTarget.x;
strikeEffect.y = bestTarget.y;
var strikeGraphics = strikeEffect.attachAsset('orbitalStrike', {
anchorX: 0.5,
anchorY: 0.5
});
strikeGraphics.alpha = 0.0;
strikeGraphics.scaleX = 0.1;
strikeGraphics.scaleY = 0.1;
strikeGraphics.blendMode = 1; // Additive
game.addChild(strikeEffect);
// Warning phase
tween(strikeGraphics, {
alpha: 0.7,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Strike phase - massive damage
var strikeDamage = self.tower.damage * 15;
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
var dx = enemy.x - bestTarget.x;
var dy = enemy.y - bestTarget.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= strikeRadius) {
enemy.health -= strikeDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Visual damage effect
tween(enemy, {
tint: 0xFFFFFF,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(enemy, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
// Final explosion effect
tween(strikeGraphics, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (strikeEffect.parent) {
strikeEffect.parent.removeChild(strikeEffect);
}
}
});
}
});
var notification = game.addChild(new Notification("🛰️ ORBITAL STRIKE INCOMING! 🛰️"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return true;
}
return false;
};
self.naniteSwarm = function () {
// Create nanite swarm that spreads poison and self-replicates
var swarmCenter = self.tower;
var swarmRadius = CELL_SIZE * 4;
var swarmDamage = self.tower.damage * 3;
// Create visual swarm effect
var swarmEffect = new Container();
swarmEffect.x = swarmCenter.x;
swarmEffect.y = swarmCenter.y;
var swarmGraphics = swarmEffect.attachAsset('naniteSwarm', {
anchorX: 0.5,
anchorY: 0.5
});
swarmGraphics.alpha = 0.8;
swarmGraphics.tint = 0x00FF88;
game.addChild(swarmEffect);
// Create multiple nanite particles
var particles = [];
for (var i = 0; i < 12; i++) {
var particle = swarmEffect.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = 15;
particle.height = 15;
particle.tint = 0x44FFAA;
var angle = i / 12 * Math.PI * 2;
particle.x = Math.cos(angle) * 50;
particle.y = Math.sin(angle) * 50;
particles.push(particle);
}
// Animate swarm expansion
tween(swarmEffect, {
scaleX: 3.0,
scaleY: 3.0
}, {
duration: 2000,
easing: tween.easeOut
});
// Animate particles spiraling outward
for (var i = 0; i < particles.length; i++) {
var particle = particles[i];
tween(particle, {
x: particle.x * 3,
y: particle.y * 3,
rotation: Math.PI * 4
}, {
duration: 2000,
delay: i * 100,
easing: tween.easeOut
});
}
// Apply nanite effects over time
var swarmDuration = 180; // 3 seconds
var _swarmTick = function swarmTick() {
swarmDuration--;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - swarmCenter.x;
var dy = enemy.y - swarmCenter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= swarmRadius) {
// Apply nanite damage
enemy.health -= swarmDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Apply enhanced poison effect
enemy.poisoned = true;
enemy.poisonDamage = swarmDamage * 0.5;
enemy.poisonDuration = 300; // Extended duration
// Self-replication chance
if (Math.random() < 0.3 && swarmDuration > 60) {
// 30% chance to spread to nearby enemies
for (var j = 0; j < enemies.length; j++) {
var nearbyEnemy = enemies[j];
if (nearbyEnemy !== enemy) {
var nearbyDx = nearbyEnemy.x - enemy.x;
var nearbyDy = nearbyEnemy.y - enemy.y;
var nearbyDistance = Math.sqrt(nearbyDx * nearbyDx + nearbyDy * nearbyDy);
if (nearbyDistance <= CELL_SIZE * 2) {
nearbyEnemy.poisoned = true;
nearbyEnemy.poisonDamage = swarmDamage * 0.3;
nearbyEnemy.poisonDuration = 240;
break;
}
}
}
}
}
}
if (swarmDuration > 0) {
LK.setTimeout(_swarmTick, 100); // Continue every 100ms
} else {
// Clean up swarm effect
tween(swarmEffect, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
if (swarmEffect.parent) {
swarmEffect.parent.removeChild(swarmEffect);
}
}
});
}
};
LK.setTimeout(_swarmTick, 100); // Start swarm effects
var notification = game.addChild(new Notification("🤖 NANITE SWARM DEPLOYED! 🤖"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return true;
};
self.timeDilation = function () {
// Create temporal distortion field that slows time for enemies
var dilationCenter = self.tower;
var dilationRadius = CELL_SIZE * 5;
var dilationDuration = 600; // 10 seconds
// Create visual time dilation effect
var dilationEffect = new Container();
dilationEffect.x = dilationCenter.x;
dilationEffect.y = dilationCenter.y;
var dilationGraphics = dilationEffect.attachAsset('timeDilation', {
anchorX: 0.5,
anchorY: 0.5
});
dilationGraphics.alpha = 0.4;
dilationGraphics.tint = 0x9966FF;
dilationGraphics.blendMode = 1; // Additive
game.addChild(dilationEffect);
// Pulsing time distortion effect
tween(dilationGraphics, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(dilationGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.4
}, {
duration: 1000,
easing: tween.easeInOut
});
}
});
// Apply time dilation effects
var affectedEnemies = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - dilationCenter.x;
var dy = enemy.y - dilationCenter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= dilationRadius) {
// Store original speed
if (!enemy.originalDilationSpeed) {
enemy.originalDilationSpeed = enemy.speed;
}
// Extreme slow effect (90% speed reduction)
enemy.speed = enemy.originalDilationSpeed * 0.1;
enemy.temporallyDilated = true;
enemy.dilationDuration = dilationDuration;
affectedEnemies.push(enemy);
// Visual distortion effect on enemy
tween(enemy, {
tint: 0x9966FF,
alpha: 0.8
}, {
duration: 300,
easing: tween.easeOut
});
}
}
// During dilation, towers in the field get boosted fire rate
var boostedTowers = [];
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - dilationCenter.x;
var dy = tower.y - dilationCenter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= dilationRadius && tower !== self.tower) {
tower.originalDilationFireRate = tower.fireRate;
tower.fireRate = Math.floor(tower.fireRate * 0.3); // 70% faster firing
tower.temporallyBoosted = true;
boostedTowers.push(tower);
}
}
// Clean up after duration
LK.setTimeout(function () {
// Restore enemy speeds
for (var i = 0; i < affectedEnemies.length; i++) {
var enemy = affectedEnemies[i];
if (enemy.parent && !enemy.isDestroyed) {
enemy.speed = enemy.originalDilationSpeed;
enemy.temporallyDilated = false;
// Restore enemy appearance
tween(enemy, {
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
}
// Restore tower fire rates
for (var i = 0; i < boostedTowers.length; i++) {
var tower = boostedTowers[i];
if (tower.parent && !tower.isDestroyed) {
tower.fireRate = tower.originalDilationFireRate;
tower.temporallyBoosted = false;
}
}
// Remove visual effect
tween(dilationEffect, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: function onFinish() {
if (dilationEffect.parent) {
dilationEffect.parent.removeChild(dilationEffect);
}
}
});
}, dilationDuration * 16.67); // Convert frames to milliseconds
var notification = game.addChild(new Notification("⏰ TIME DILATION ACTIVATED! ⏰"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return true;
};
self.update = function () {
if (self.currentCooldown > 0) {
self.currentCooldown--;
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var TutorialSystem = Container.expand(function () {
var self = Container.call(this);
self.currentStep = 0;
self.isActive = false;
self.isWaitingForAction = false;
self.tutorialCompleted = storage.tutorialCompleted || false;
self.currentPhase = 'introduction'; // introduction, basics, combat, advanced, endgame
self.highlightOverlay = null;
self.actionArrow = null;
self.tutorialSteps = [
// INTRODUCTION PHASE
{
phase: 'introduction',
title: "Welcome to Space Tower Defense!",
text: "Commander, our space station is under attack!\nYou must defend against waves of alien invaders.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'introduction',
title: "Your Mission",
text: "Build defensive towers to stop enemies from reaching\nthe bottom of the screen and destroying our base.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'introduction',
title: "Resources Overview",
text: "Gold: Used to build and upgrade towers\nLives: Lost when enemies reach the bottom\nScore: Points earned for defeating enemies",
highlight: "ui",
action: null,
waitForAction: false
},
// BASICS PHASE
{
phase: 'basics',
title: "Building Your First Tower",
text: "Let's start by building a basic tower.\nDrag the 'Default' tower from the bottom panel.",
highlight: "sourceTowers",
action: "buildTower",
waitForAction: true
}, {
phase: 'basics',
title: "Tower Placement",
text: "Good! Towers need 2x2 space and cannot block enemy paths.\nThe green preview shows valid placement areas.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'basics',
title: "Tower Information",
text: "Tap on your tower to see its stats and upgrade options.\nTry tapping the tower you just built.",
highlight: "tower",
action: "selectTower",
waitForAction: true
}, {
phase: 'basics',
title: "Upgrading Towers",
text: "Excellent! Upgrading increases damage and fire rate.\nUpgrades get more expensive at higher levels.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'basics',
title: "Starting Combat",
text: "Now let's face some enemies!\nTap the 'Start Game' button to begin wave 1.",
highlight: "startButton",
action: "startGame",
waitForAction: true
},
// COMBAT PHASE
{
phase: 'combat',
title: "Enemy Types - Normal",
text: "The first enemies are normal type - balanced health and speed.\nYour towers will automatically target and fire at them.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Earning Gold",
text: "Great! You earn gold for each enemy defeated.\nUse this gold to build more towers and upgrades.",
highlight: "gold",
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Tower Specialization",
text: "Different tower types are effective against different enemies.\nLet's build a Rapid tower for faster firing.",
highlight: "sourceTowers",
action: "buildRapidTower",
waitForAction: true
}, {
phase: 'combat',
title: "Enemy Types - Fast",
text: "Wave 2 brings fast enemies (blue).\nRapid towers are excellent against quick targets.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Tower Range",
text: "Each tower has a different range.\nSniper towers have long range but fire slowly.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Build a Sniper Tower",
text: "Try building a Sniper tower (orange) for long-range support.\nPlace it where it can cover a large area.",
highlight: "sourceTowers",
action: "buildSniperTower",
waitForAction: true
},
// ADVANCED PHASE
{
phase: 'advanced',
title: "Special Enemy Types",
text: "As waves progress, you'll face special enemies:\n• Flying (yellow) - immune to some towers\n• Immune (red) - resistant to effects",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Splash Damage",
text: "Splash towers (green) deal area damage.\nThey're effective against groups of enemies.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Support Towers",
text: "Slow towers (purple) reduce enemy speed.\nPoison towers (cyan) deal damage over time.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Hero Units",
text: "Deploy hero units for additional firepower!\nHeroes can move around and have special abilities.",
highlight: "heroPanel",
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Deploy a Tank Hero",
text: "Try deploying a Tank hero (green).\nTap the tank button, then tap on the battlefield.",
highlight: "heroPanel",
action: "deployHero",
waitForAction: true
}, {
phase: 'advanced',
title: "Hero Abilities",
text: "Heroes gain experience and can use special abilities.\nTap on your hero to activate its ability when ready.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Station Modules",
text: "Customize your space station with modules!\nTap the 'Customize' button to manage station upgrades.",
highlight: "stationPanel",
action: null,
waitForAction: false
},
// ENDGAME PHASE
{
phase: 'endgame',
title: "Boss Waves",
text: "Every 10th wave is a Boss wave with powerful enemies.\nBoss enemies have much more health but give more gold.",
highlight: "waveIndicator",
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Environmental Hazards",
text: "Watch for space hazards like asteroids, nebulas, and solar flares.\nThese can help or hinder your defense strategy.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Advanced Strategies",
text: "• Upgrade key towers to maximum level\n• Use hero abilities strategically\n• Adapt to enemy types each wave",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Tower Specializations",
text: "At level 3, some towers can specialize:\n• Sniper: Anti-Armor or Long-Range\n• Splash: Nuclear or Chain Reaction",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Path Branching",
text: "Enemies may take different routes through your defenses.\nSome paths are shorter but more defended.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Tutorial Complete!",
text: "You're ready to defend the station, Commander!\nSurvive all 50 waves to achieve victory.",
highlight: null,
action: null,
waitForAction: false
}];
var tutorialContainer = new Container();
self.addChild(tutorialContainer);
var overlay = tutorialContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
overlay.width = 1900;
overlay.height = 450;
overlay.tint = 0x0A0A0A;
overlay.alpha = 0.95;
// Phase indicator
var phaseText = new Text2("", {
size: 40,
fill: 0xFFD700,
weight: 600
});
phaseText.anchor.set(0.5, 0.5);
phaseText.y = -160;
tutorialContainer.addChild(phaseText);
var titleText = new Text2("", {
size: 65,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -100;
tutorialContainer.addChild(titleText);
var bodyText = new Text2("", {
size: 45,
fill: 0xE0E0E0,
weight: 400
});
bodyText.anchor.set(0.5, 0.5);
bodyText.y = 0;
tutorialContainer.addChild(bodyText);
// Progress indicator
var progressText = new Text2("", {
size: 35,
fill: 0xCCCCCC,
weight: 400
});
progressText.anchor.set(0.5, 0.5);
progressText.y = 80;
tutorialContainer.addChild(progressText);
var buttonContainer = new Container();
buttonContainer.y = 140;
tutorialContainer.addChild(buttonContainer);
var nextButton = new Container();
var nextBg = nextButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
nextBg.width = 200;
nextBg.height = 80;
nextBg.tint = 0x00AA00;
var nextText = new Text2("Next", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
nextText.anchor.set(0.5, 0.5);
nextButton.addChild(nextText);
nextButton.x = -250;
buttonContainer.addChild(nextButton);
var skipButton = new Container();
var skipBg = skipButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
skipBg.width = 200;
skipBg.height = 80;
skipBg.tint = 0xAA0000;
var skipText = new Text2("Skip", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
skipText.anchor.set(0.5, 0.5);
skipButton.addChild(skipText);
skipButton.x = 0;
buttonContainer.addChild(skipButton);
var prevButton = new Container();
var prevBg = prevButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
prevBg.width = 200;
prevBg.height = 80;
prevBg.tint = 0x0066CC;
var prevText = new Text2("Back", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
prevText.anchor.set(0.5, 0.5);
prevButton.addChild(prevText);
prevButton.x = 250;
buttonContainer.addChild(prevButton);
self.createHighlight = function (target) {
if (self.highlightOverlay) {
self.highlightOverlay.parent.removeChild(self.highlightOverlay);
}
self.highlightOverlay = new Container();
var highlight = self.highlightOverlay.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
highlight.width = 300;
highlight.height = 300;
highlight.tint = 0xFFD700;
highlight.alpha = 0.3;
// Pulsing animation
tween(highlight, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.6
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.highlightOverlay.isDestroyed) {
tween(highlight, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut
});
}
}
});
game.addChild(self.highlightOverlay);
return self.highlightOverlay;
};
self.removeHighlight = function () {
if (self.highlightOverlay && self.highlightOverlay.parent) {
tween.stop(self.highlightOverlay);
self.highlightOverlay.parent.removeChild(self.highlightOverlay);
self.highlightOverlay = null;
}
};
self.showStep = function (stepIndex) {
if (stepIndex >= self.tutorialSteps.length) {
self.endTutorial();
return;
}
var step = self.tutorialSteps[stepIndex];
var phaseDisplay = step.phase.charAt(0).toUpperCase() + step.phase.slice(1) + " Phase";
phaseText.setText(phaseDisplay);
titleText.setText(step.title);
bodyText.setText(step.text);
progressText.setText("Step " + (stepIndex + 1) + " of " + self.tutorialSteps.length);
// Update button states
nextButton.visible = !step.waitForAction;
skipButton.visible = true;
prevButton.visible = stepIndex > 0;
if (stepIndex === self.tutorialSteps.length - 1) {
nextText.setText("Finish!");
skipText.setText("Start!");
}
// Handle highlights
self.removeHighlight();
if (step.highlight) {
switch (step.highlight) {
case "ui":
// Highlight UI area
if (self.highlightOverlay) {
self.highlightOverlay.x = 2048 / 2;
self.highlightOverlay.y = 100;
}
break;
case "sourceTowers":
// Highlight tower selection area
if (sourceTowers.length > 0) {
var highlight = self.createHighlight();
highlight.x = sourceTowers[0].x;
highlight.y = sourceTowers[0].y;
}
break;
case "gold":
// Highlight gold display
var highlight = self.createHighlight();
highlight.x = 2048 / 2 - 400;
highlight.y = 100;
break;
case "heroPanel":
// Highlight hero panel
var highlight = self.createHighlight();
highlight.x = 2048 / 2;
highlight.y = 200;
break;
case "stationPanel":
// Highlight station panel
var highlight = self.createHighlight();
highlight.x = 2048 - 200;
highlight.y = 250;
break;
case "waveIndicator":
// Highlight wave indicator
var highlight = self.createHighlight();
highlight.x = 2048 / 2;
highlight.y = 2732 - 80;
break;
case "startButton":
// Highlight start game button
if (waveIndicator && waveIndicator.waveMarkers.length > 0) {
var highlight = self.createHighlight();
highlight.x = waveIndicator.waveMarkers[0].x + waveIndicator.x;
highlight.y = waveIndicator.waveMarkers[0].y + waveIndicator.y;
}
break;
}
}
// Set up action waiting
if (step.waitForAction) {
self.isWaitingForAction = true;
self.expectedAction = step.action;
}
};
self.startTutorial = function () {
self.isActive = true;
self.visible = true;
self.currentStep = 0;
self.showStep(0);
// Position tutorial in center of screen
tutorialContainer.x = 2048 / 2;
tutorialContainer.y = 1400;
};
self.nextStep = function () {
if (self.isWaitingForAction) return;
self.currentStep++;
self.showStep(self.currentStep);
};
self.prevStep = function () {
if (self.currentStep > 0) {
self.currentStep--;
self.showStep(self.currentStep);
}
};
self.endTutorial = function () {
self.isActive = false;
self.visible = false;
self.removeHighlight();
storage.tutorialCompleted = true;
self.tutorialCompleted = true;
// Auto-start the game after tutorial
if (waveIndicator && !waveIndicator.gameStarted) {
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
}
var notification = game.addChild(new Notification("Tutorial completed! Good luck, Commander!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
};
self.checkActionCompleted = function (action) {
if (!self.isWaitingForAction || self.expectedAction !== action) return;
self.isWaitingForAction = false;
self.expectedAction = null;
// Show next button and auto-advance after delay
nextButton.visible = true;
LK.setTimeout(function () {
if (self.isActive && !self.isWaitingForAction) {
self.nextStep();
}
}, 1500);
};
nextButton.down = function () {
self.nextStep();
};
skipButton.down = function () {
self.endTutorial();
};
prevButton.down = function () {
self.prevStep();
};
self.visible = false;
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: 0xE0E0E0,
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.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('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 = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
tutorialSystem.checkActionCompleted('startGame');
}
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Play wave start sound
LK.getSound('waveStart').play();
}
};
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;
// 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 && enemyType !== 'swarm') {
// 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 = 3;
waveTypeShadow.y = 3;
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)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
var Wormhole = Container.expand(function (x, y, targetX, targetY) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.targetX = targetX;
self.targetY = targetY;
self.radius = 90;
self.isDestroyed = false;
self.cooldown = 0;
var wormholeGraphics = self.attachAsset('wormhole', {
anchorX: 0.5,
anchorY: 0.5
});
wormholeGraphics.alpha = 0.7;
wormholeGraphics.blendMode = 1; // Additive blending
// Spinning animation
tween(wormholeGraphics, {
rotation: Math.PI * 2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed) {
wormholeGraphics.rotation = 0;
}
}
});
// Create target wormhole visual
self.targetWormhole = new Container();
self.targetWormhole.x = targetX;
self.targetWormhole.y = targetY;
var targetGraphics = self.targetWormhole.attachAsset('wormhole', {
anchorX: 0.5,
anchorY: 0.5
});
targetGraphics.alpha = 0.5;
targetGraphics.tint = 0xFF1493; // Different color for exit
targetGraphics.blendMode = 1;
game.addChild(self.targetWormhole);
// Sync rotation with main wormhole
tween(targetGraphics, {
rotation: -Math.PI * 2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed) {
targetGraphics.rotation = 0;
}
}
});
self.update = function () {
if (self.cooldown > 0) {
self.cooldown--;
return;
}
// Check for enemies within range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.isFlying) continue; // Flying enemies ignore wormholes
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
// Teleport enemy
self.teleportEnemy(enemy);
self.cooldown = 180; // 3 second cooldown
break;
}
}
};
self.teleportEnemy = function (enemy) {
// Create teleport effect at source
var sourceEffect = new Container();
sourceEffect.x = self.x;
sourceEffect.y = self.y;
var sourceGraphics = sourceEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
sourceGraphics.tint = 0x9400D3;
sourceGraphics.alpha = 0.8;
game.addChild(sourceEffect);
// Create teleport effect at destination
var destEffect = new Container();
destEffect.x = self.targetX;
destEffect.y = self.targetY;
var destGraphics = destEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
destGraphics.tint = 0xFF1493;
destGraphics.alpha = 0.8;
game.addChild(destEffect);
// Animate effects
tween(sourceEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (sourceEffect.parent) {
sourceEffect.parent.removeChild(sourceEffect);
}
}
});
tween(destEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (destEffect.parent) {
destEffect.parent.removeChild(destEffect);
}
}
});
// Teleport enemy
enemy.x = self.targetX;
enemy.y = self.targetY;
enemy.currentCellX = Math.floor((self.targetX - grid.x) / CELL_SIZE);
enemy.currentCellY = Math.floor((self.targetY - grid.y) / CELL_SIZE);
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = null; // Reset pathfinding
// Flash the wormhole
tween(wormholeGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(wormholeGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeIn
});
}
});
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove target wormhole
if (self.targetWormhole && self.targetWormhole.parent) {
self.targetWormhole.parent.removeChild(self.targetWormhole);
}
var wormholeIndex = wormholes.indexOf(self);
if (wormholeIndex !== -1) {
wormholes.splice(wormholeIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000011
});
/****
* Game Code
****/
// Add background image
// Base tower structures for each type
// Tower gun/weapon parts - Level 1
// Tower gun/weapon parts - Level 2
// Tower gun/weapon parts - Level 3
// Tower gun/weapon parts - Level 4
// Tower gun/weapon parts - Level 5
// Tower gun/weapon parts - Level 6 (Max Level)
// Support structures and details
// Visual indicators
// Specialization visual elements
var backgroundImage = game.attachAsset('spaceBackground', {
anchorX: 0.5,
anchorY: 0.5
});
backgroundImage.x = 2048 / 2;
backgroundImage.y = 2732 / 2;
backgroundImage.alpha = 0.8;
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 = [];
// Object pooling for bullets
var bulletPool = [];
var maxBulletPoolSize = 100;
function getBulletFromPool(startX, startY, targetEnemy, damage, speed) {
var bullet;
if (bulletPool.length > 0) {
bullet = bulletPool.pop();
// Reset bullet properties
bullet.targetEnemy = targetEnemy;
bullet.damage = damage || 10;
bullet.speed = speed || 5;
bullet.x = startX;
bullet.y = startY;
bullet.isDestroyed = false;
bullet.visible = true;
bullet.alpha = 1;
if (bullet.children[0]) {
bullet.children[0].alpha = 1;
bullet.children[0].scaleX = 1;
bullet.children[0].scaleY = 1;
}
} else {
bullet = new Bullet(startX, startY, targetEnemy, damage, speed);
}
return bullet;
}
function returnBulletToPool(bullet) {
if (bulletPool.length < maxBulletPoolSize && bullet.parent) {
// Clean up target enemy reference properly
if (bullet.targetEnemy && bullet.targetEnemy.bulletsTargetingThis) {
var bulletIndex = bullet.targetEnemy.bulletsTargetingThis.indexOf(bullet);
if (bulletIndex !== -1) {
bullet.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
// Stop all tweens before pooling
tween.stop(bullet);
if (bullet.children && bullet.children[0]) {
tween.stop(bullet.children[0]);
}
bullet.parent.removeChild(bullet);
bullet.targetEnemy = null;
bullet.visible = false;
bulletPool.push(bullet);
} else {
bullet.destroy();
}
}
var defenses = [];
var selectedTower = null;
var gold = 60; // Reduced starting gold to make early choices more meaningful
var lives = 20;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
// Hero system variables
var heroes = [];
var maxHeroes = 3;
var heroDeploymentMode = false;
var heroPreview = null;
var selectedHeroType = 'tank';
// Station customization variables
var stationCustomizer = null;
var researchLabs = [];
var shieldGenerators = [];
var teleporterNodes = [];
// Environmental hazards variables
var asteroids = [];
var nebulaClouds = [];
var solarFlares = [];
var wormholes = [];
var hazardTimer = 0;
var nextHazardTime = 1800; // 30 seconds at 60fps
// Branching routes system variables
var pathJunctions = [];
var alternativePaths = [];
var routeChoiceInfluences = [];
var pathDifficulties = {};
// Tower specialization variables
var specializationMenus = [];
var towerLinks = [];
// Ultimate ability variables
var activeUltimates = [];
var ultimateEffects = [];
// Load station data from storage
var stationData = storage.stationData || {
modules: [],
researchPoints: 0,
completedTechs: []
};
// Initialize route visualizer
var routeVisualizer = new RouteVisualizer();
game.addChild(routeVisualizer);
// Initialize branching routes system
function initializeBranchingRoutes() {
// Create main junction points in the middle area
var mainJunction = new PathJunction(grid.x + 12 * CELL_SIZE, grid.y + 15 * CELL_SIZE, [{
targetX: 10,
targetY: 22,
// Left branch - shorter but more defended
isShortPath: true,
pathLength: 8,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 20
}, {
targetX: 14,
targetY: 22,
// Right branch - longer but less defended
isShortPath: false,
pathLength: 12,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 15
}]);
// Secondary junction for more complex routing
var secondaryJunction = new PathJunction(grid.x + 8 * CELL_SIZE, grid.y + 12 * CELL_SIZE, [{
targetX: 6,
targetY: 18,
// Far left route
isShortPath: false,
pathLength: 15,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 25
}, {
targetX: 12,
targetY: 15,
// Center route to main junction
isShortPath: true,
pathLength: 6,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 30
}]);
game.addChild(mainJunction);
game.addChild(secondaryJunction);
pathJunctions.push(mainJunction);
pathJunctions.push(secondaryJunction);
var notification = game.addChild(new Notification("🔀 Multi-path routing system activated! 🔀"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
// Initialize the branching system after a short delay
LK.setTimeout(function () {
initializeBranchingRoutes();
}, 2000);
function getModuleCost(moduleType, level) {
var baseCosts = {
research: 50,
shield: 75,
teleporter: 100
};
var baseCost = baseCosts[moduleType] || 50;
return Math.floor(baseCost * Math.pow(1.5, level - 1));
}
function getTechnology(techId) {
var technologies = {
'tower_damage': {
id: 'tower_damage',
name: 'Enhanced Weapons',
cost: 100,
onComplete: function onComplete() {
// Increase all tower damage by 20%
for (var i = 0; i < towers.length; i++) {
towers[i].damage = Math.floor(towers[i].damage * 1.2);
}
}
},
'tower_range': {
id: 'tower_range',
name: 'Extended Range',
cost: 150,
onComplete: function onComplete() {
// Increase all tower ranges
for (var i = 0; i < towers.length; i++) {
towers[i].refreshCellsInRange();
}
}
},
'shield_efficiency': {
id: 'shield_efficiency',
name: 'Shield Efficiency',
cost: 200,
onComplete: function onComplete() {
// Reduce shield energy costs
for (var i = 0; i < shieldGenerators.length; i++) {
shieldGenerators[i].maxShieldEnergy *= 1.5;
shieldGenerators[i].shieldEnergy = shieldGenerators[i].maxShieldEnergy;
}
}
}
};
return technologies[techId];
}
// Spatial partitioning for collision optimization
var spatialGrid = {
cellSize: CELL_SIZE * 2,
cells: {},
getKey: function getKey(x, y) {
var gridX = Math.floor(x / this.cellSize);
var gridY = Math.floor(y / this.cellSize);
return gridX + ',' + gridY;
},
addEnemy: function addEnemy(enemy) {
var key = this.getKey(enemy.x, enemy.y);
if (!this.cells[key]) this.cells[key] = [];
this.cells[key].push(enemy);
},
removeEnemy: function removeEnemy(enemy) {
var key = this.getKey(enemy.x, enemy.y);
if (this.cells[key]) {
var index = this.cells[key].indexOf(enemy);
if (index !== -1) {
this.cells[key].splice(index, 1);
}
}
},
getNearbyEnemies: function getNearbyEnemies(x, y, range) {
var nearby = [];
var checkRadius = Math.ceil(range / this.cellSize);
var centerX = Math.floor(x / this.cellSize);
var centerY = Math.floor(y / this.cellSize);
for (var dx = -checkRadius; dx <= checkRadius; dx++) {
for (var dy = -checkRadius; dy <= checkRadius; dy++) {
var key = centerX + dx + ',' + (centerY + dy);
if (this.cells[key]) {
nearby = nearby.concat(this.cells[key]);
}
}
}
return nearby;
},
clear: function clear() {
this.cells = {};
}
};
var enemiesToSpawn = 10; // Default number of enemies per wave
// Visual effects limiting
var activeEffects = [];
var maxActiveEffects = 20;
var effectSkipCounter = 0;
function canCreateEffect() {
// Count active effects and remove destroyed ones
activeEffects = activeEffects.filter(function (effect) {
return effect.parent && !effect.isDestroyed;
});
// Limit based on performance
if (activeEffects.length >= maxActiveEffects) {
effectSkipCounter++;
// Skip every other effect when at limit
return effectSkipCounter % 2 === 0;
}
return true;
}
function registerEffect(effect) {
if (activeEffects.length < maxActiveEffects) {
activeEffects.push(effect);
}
}
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.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(livesText);
LK.gui.top.addChild(scoreText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
}
function setGold(value) {
gold = value;
updateUI();
}
// Performance monitoring and adaptive logic
var performanceMonitor = {
frameTime: 16.67,
// Target 60 FPS
lastTime: 0,
avgFrameTime: 16.67,
frameCount: 0,
performanceLevel: 1.0,
// 1.0 = full performance, 0.5 = half performance
update: function update() {
var currentTime = Date.now();
if (this.lastTime > 0) {
var deltaTime = currentTime - this.lastTime;
this.avgFrameTime = this.avgFrameTime * 0.9 + deltaTime * 0.1;
// Adjust performance level based on frame time
if (this.avgFrameTime > 25) {
// Below 40 FPS
this.performanceLevel = Math.max(0.3, this.performanceLevel - 0.1);
} else if (this.avgFrameTime < 18) {
// Above 55 FPS
this.performanceLevel = Math.min(1.0, this.performanceLevel + 0.05);
}
}
this.lastTime = currentTime;
this.frameCount++;
},
shouldSkipUpdate: function shouldSkipUpdate() {
return Math.random() > this.performanceLevel;
}
};
// Memory monitoring system
var memoryMonitor = {
lastEnemyCount: 0,
lastBulletCount: 0,
lastTowerCount: 0,
lastEffectCount: 0,
update: function update() {
// Track object counts for memory leak detection
this.lastEnemyCount = enemies.length;
this.lastBulletCount = bullets.length;
this.lastTowerCount = towers.length;
this.lastEffectCount = activeEffects.length;
// Log warnings if counts are unexpectedly high
if (this.lastEnemyCount > 100) {
console.warn("High enemy count detected:", this.lastEnemyCount);
}
if (this.lastBulletCount > 200) {
console.warn("High bullet count detected:", this.lastBulletCount);
}
if (this.lastEffectCount > 50) {
console.warn("High effect count detected:", this.lastEffectCount);
}
},
forceCleanup: function forceCleanup() {
// Emergency cleanup when memory usage is too high
console.log("Performing emergency cleanup...");
// Clean up destroyed effects
activeEffects = activeEffects.filter(function (effect) {
return effect.parent && !effect.isDestroyed;
});
// Clean up orphaned bullets
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent || bullets[i].isDestroyed) {
bullets.splice(i, 1);
}
}
// Clean up bullet pool
bulletPool = bulletPool.filter(function (bullet) {
return !bullet.isDestroyed;
});
console.log("Cleanup complete. Objects remaining:", {
enemies: enemies.length,
bullets: bullets.length,
towers: towers.length,
effects: activeEffects.length,
pooledBullets: bulletPool.length
});
}
};
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();
debugLayer.addChild(grid);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
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 = 8; // Increased base cost from 5
switch (towerType) {
case 'rapid':
cost = 12; // Reduced from 15 - cheaper early game tower
break;
case 'sniper':
cost = 20; // Reduced from 25 for better accessibility
break;
case 'splash':
cost = 28; // Reduced from 35
break;
case 'slow':
cost = 32; // Reduced from 45 - utility tower should be accessible
break;
case 'poison':
cost = 38; // Reduced from 55 - was too expensive
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
grid.pathFind();
grid.renderDebug();
// Play tower placement sound
LK.getSound('towerPlace').play();
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
if (towerType === 'default') {
tutorialSystem.checkActionCompleted('buildTower');
} else if (towerType === 'rapid') {
tutorialSystem.checkActionCompleted('buildRapidTower');
} else if (towerType === 'sniper') {
tutorialSystem.checkActionCompleted('buildSniperTower');
}
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// Handle hero deployment
if (heroDeploymentMode) {
// Check if tap is in valid deployment area (not on towers or paths)
var deploymentValid = true;
var gridX = Math.floor((x - grid.x) / CELL_SIZE);
var gridY = Math.floor((y - grid.y) / CELL_SIZE);
// Check if position is within grid bounds and not on blocked areas
if (gridX < 2 || gridX >= 22 || gridY < 6 || gridY >= 25) {
deploymentValid = false;
} else {
var cell = grid.getCell(gridX, gridY);
if (!cell || cell.type !== 0) {
deploymentValid = false;
}
}
if (deploymentValid && heroes.length < maxHeroes) {
var hero = new Hero(selectedHeroType);
hero.x = x;
hero.y = y;
enemyLayerTop.addChild(hero); // Add heroes to top layer
heroes.push(hero);
updateHeroPanel();
heroDeploymentMode = false;
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
tutorialSystem.checkActionCompleted('deployHero');
}
var notification = game.addChild(new Notification(selectedHeroType.charAt(0).toUpperCase() + selectedHeroType.slice(1) + " hero deployed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
} else {
var notification = game.addChild(new Notification("Cannot deploy hero here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
heroDeploymentMode = false;
return;
}
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Improved touch detection with larger hit areas for mobile
var touchPadding = 40; // Extra padding for easier touch
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
var hitArea = {
left: tower.x - tower.width / 2 - touchPadding,
right: tower.x + tower.width / 2 + touchPadding,
top: tower.y - tower.height / 2 - touchPadding,
bottom: tower.y + tower.height / 2 + touchPadding
};
if (x >= hitArea.left && x <= hitArea.right && y >= hitArea.top && y <= hitArea.bottom) {
// Check if player can afford this tower
if (gold >= getTowerCost(tower.towerType)) {
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
// Improved drag offset for better mobile experience
var dragOffsetY = CELL_SIZE * 2; // Larger offset to keep preview visible above finger
towerPreview.snapToGrid(x, y - dragOffsetY);
// Add immediate visual feedback with scaling animation
tween(tower, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(tower, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
}
});
} else {
// Visual feedback for unaffordable towers
tween(tower, {
scaleX: 0.95,
scaleY: 0.95,
alpha: 0.7
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(tower, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 150,
easing: tween.easeIn
});
}
});
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
break;
}
}
};
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) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
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 towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
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;
// Initialize station customizer
stationCustomizer = new StationCustomizer();
stationCustomizer.x = 0;
stationCustomizer.y = 0;
game.addChild(stationCustomizer);
// Create station customization panel
var stationPanel = new Container();
var stationPanelBg = stationPanel.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
stationPanelBg.width = 400;
stationPanelBg.height = 100;
stationPanelBg.tint = 0x2E7D32;
stationPanelBg.alpha = 0.9;
var stationTitleText = new Text2("Station", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
stationTitleText.anchor.set(0.5, 0.5);
stationTitleText.y = -15;
stationPanel.addChild(stationTitleText);
var customizeButton = new Container();
var customizeBg = customizeButton.attachAsset('heroAbilityIcon', {
anchorX: 0.5,
anchorY: 0.5
});
customizeBg.width = 80;
customizeBg.height = 40;
customizeBg.tint = 0x4CAF50;
customizeButton.y = 20;
var customizeText = new Text2("Customize", {
size: 30,
fill: 0xFFFFFF,
weight: 600
});
customizeText.anchor.set(0.5, 0.5);
customizeButton.addChild(customizeText);
customizeButton.down = function () {
stationCustomizer.toggleCustomizationMode();
};
stationPanel.addChild(customizeButton);
stationPanel.x = 2048 - 200; // Position on right side
stationPanel.y = 200; // Below the score display which is at y=50
game.addChild(stationPanel);
// Create hero deployment panel
var heroPanel = new Container();
var heroPanelBg = heroPanel.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
heroPanelBg.width = 800;
heroPanelBg.height = 120;
heroPanelBg.tint = 0x1A237E;
heroPanelBg.alpha = 0.9;
var heroTitleText = new Text2("Heroes (" + heroes.length + "/" + maxHeroes + ")", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
heroTitleText.anchor.set(0.5, 0.5);
heroTitleText.y = -30;
heroPanel.addChild(heroTitleText);
// Hero type buttons
var heroTypes = ['tank', 'support', 'dps'];
var heroTypeButtons = [];
for (var i = 0; i < heroTypes.length; i++) {
var heroButton = new Container();
var buttonBg = heroButton.attachAsset('heroAbilityIcon', {
anchorX: 0.5,
anchorY: 0.5
});
switch (heroTypes[i]) {
case 'tank':
buttonBg.tint = 0x4CAF50;
break;
case 'support':
buttonBg.tint = 0x2196F3;
break;
case 'dps':
buttonBg.tint = 0xFF5722;
break;
}
buttonBg.width = 80;
buttonBg.height = 80;
heroButton.heroType = heroTypes[i];
heroButton.x = -240 + i * 120;
heroButton.y = 10;
heroButton.down = function () {
if (heroes.length >= maxHeroes) {
var notification = game.addChild(new Notification("Maximum heroes deployed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
selectedHeroType = this.heroType;
heroDeploymentMode = true;
var notification = game.addChild(new Notification("Tap anywhere to deploy " + this.heroType + " hero"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
};
heroPanel.addChild(heroButton);
heroTypeButtons.push(heroButton);
}
heroPanel.x = 2048 / 2;
heroPanel.y = 120;
game.addChild(heroPanel);
function updateHeroPanel() {
heroTitleText.setText("Heroes (" + heroes.length + "/" + maxHeroes + ")");
}
enemiesToSpawn = 10;
// Initialize tutorial system for new players
var tutorialSystem = new TutorialSystem();
game.addChild(tutorialSystem);
// Start background music
LK.playMusic('bgMusic');
// Show tutorial automatically for new players
var hasSeenTutorial = storage.tutorialCompleted || false;
if (!hasSeenTutorial) {
tutorialSystem.startTutorial();
}
// Add tutorial restart button for experienced players
var tutorialRestartButton = new Container();
var restartBg = tutorialRestartButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
restartBg.width = 200;
restartBg.height = 60;
restartBg.tint = 0x4A90E2;
restartBg.alpha = 0.9;
var restartText = new Text2("Tutorial", {
size: 35,
fill: 0xFFFFFF,
weight: 600
});
restartText.anchor.set(0.5, 0.5);
tutorialRestartButton.addChild(restartText);
tutorialRestartButton.x = 150;
tutorialRestartButton.y = 2732 - 50;
tutorialRestartButton.down = function () {
if (!tutorialSystem.isActive) {
tutorialSystem.startTutorial();
}
};
game.addChild(tutorialRestartButton);
game.update = function () {
performanceMonitor.update();
// Clear and rebuild spatial grid each frame for enemies
spatialGrid.clear();
for (var i = 0; i < enemies.length; i++) {
spatialGrid.addEnemy(enemies[i]);
}
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
var isBossWave = currentWave % 10 === 0 && currentWave > 0;
if (isBossWave && waveType !== 'swarm') {
// 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! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
// Play boss spawn sound
LK.getSound('bossSpawn').play();
}
// 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 more balanced scaling that starts linear and becomes logarithmic
var healthMultiplier;
if (currentWave <= 10) {
// Linear scaling for early waves
healthMultiplier = 1 + (currentWave - 1) * 0.15; // 15% per wave for first 10 waves
} else if (currentWave <= 25) {
// Moderate scaling for mid waves
healthMultiplier = 2.35 + (currentWave - 10) * 0.08; // Start at 2.35x, add 8% per wave
} else {
// Logarithmic scaling for late waves to prevent exponential growth
healthMultiplier = 3.55 + Math.log(currentWave - 24) * 0.5; // Logarithmic growth
}
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;
}
}
// Update heroes
for (var i = heroes.length - 1; i >= 0; i--) {
var hero = heroes[i];
if (hero.health <= 0) {
// Hero defeated - remove from play but don't reset progression
var notification = game.addChild(new Notification(hero.heroType.charAt(0).toUpperCase() + hero.heroType.slice(1) + " hero defeated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
hero.destroy();
heroes.splice(i, 1);
updateHeroPanel();
continue;
}
hero.update();
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
// Skip some enemy updates when performance is poor
if (performanceMonitor.shouldSkipUpdate() && enemy.health > 0) {
continue;
}
// Handle hero targeting for enemies
if (enemy.heroTarget && enemy.heroTarget.parent && enemy.heroTarget.health > 0) {
if (enemy.heroTargetDuration > 0) {
enemy.heroTargetDuration--;
// Move towards hero instead of following path
var dx = enemy.heroTarget.x - enemy.x;
var dy = enemy.heroTarget.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > CELL_SIZE * 0.5) {
var angle = Math.atan2(dy, dx);
enemy.x += Math.cos(angle) * enemy.speed * 30;
enemy.y += Math.sin(angle) * enemy.speed * 30;
} else {
// Attack hero
enemy.heroTarget.health -= 10;
if (enemy.heroTarget.health <= 0) {
enemy.heroTarget.health = 0;
}
}
continue; // Skip normal pathfinding
} else {
enemy.heroTarget = null;
enemy.heroTargetDuration = 0;
}
} else {
enemy.heroTarget = null;
enemy.heroTargetDuration = 0;
}
if (enemy.health <= 0) {
// Improved gold rewards that scale better with difficulty
var goldEarned;
if (enemy.isBoss) {
// Boss gold scales more reasonably: 25 base + 3 per wave
goldEarned = Math.floor(25 + (enemy.waveNumber - 1) * 3);
} else {
// Regular enemy gold: better scaling for later waves
if (enemy.waveNumber <= 10) {
goldEarned = Math.floor(2 + (enemy.waveNumber - 1) * 0.3); // 2-4.7 gold for waves 1-10
} else if (enemy.waveNumber <= 25) {
goldEarned = Math.floor(4.7 + (enemy.waveNumber - 10) * 0.4); // 4.7-10.7 gold for waves 11-25
} else {
goldEarned = Math.floor(10.7 + (enemy.waveNumber - 25) * 0.2); // Slower growth for late waves
}
}
// Bonus gold for enemies who took difficult routes
if (enemy.chosenBranch && enemy.chosenBranch.difficulty > 50) {
var routeBonus = Math.floor(goldEarned * 0.3);
goldEarned += routeBonus;
if (routeBonus > 0) {
var notification = game.addChild(new Notification("Route bonus: +" + routeBonus + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
}
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;
// 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();
// Use proper destroy method for comprehensive cleanup
enemy.destroy();
continue;
}
if (grid.updateEnemy(enemy)) {
// Check if shields are active
var shieldsActive = false;
for (var s = 0; s < stationCustomizer.modules.length; s++) {
var module = stationCustomizer.modules[s];
if (module.moduleType === 'shield' && module.shieldActive) {
shieldsActive = true;
break;
}
}
if (shieldsActive) {
// Shields absorb damage
var notification = game.addChild(new Notification("Enemy blocked by shields!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
lives = Math.max(0, lives - 1);
updateUI();
if (lives <= 0) {
// Play game over sound
LK.getSound('gameOver').play();
LK.showGameOver();
}
}
// Use proper destroy method for comprehensive cleanup
enemy.destroy();
}
}
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);
}
}
// Limit bullet count when performance drops
if (performanceMonitor.performanceLevel < 0.7 && bullets.length > 50) {
// Remove oldest bullets when performance is poor
var bulletsToRemove = Math.min(10, bullets.length - 30);
for (var i = 0; i < bulletsToRemove; i++) {
var oldBullet = bullets[i];
if (oldBullet.targetEnemy) {
var bulletIndex = oldBullet.targetEnemy.bulletsTargetingThis.indexOf(oldBullet);
if (bulletIndex !== -1) {
oldBullet.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
returnBulletToPool(oldBullet);
bullets.splice(i, 1);
i--; // Adjust index after removal
bulletsToRemove--;
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Update station customizer
if (stationCustomizer) {
stationCustomizer.update();
// Update module arrays for quick access
researchLabs = stationCustomizer.modules.filter(function (m) {
return m.moduleType === 'research';
});
shieldGenerators = stationCustomizer.modules.filter(function (m) {
return m.moduleType === 'shield';
});
teleporterNodes = stationCustomizer.modules.filter(function (m) {
return m.moduleType === 'teleporter';
});
// Link teleporter nodes
for (var i = 0; i < teleporterNodes.length; i++) {
teleporterNodes[i].linkedNodes = teleporterNodes.filter(function (node, index) {
return index !== i;
});
}
}
// Update tower specializations and support links
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.specialization && tower.specialization.update) {
tower.specialization.update();
}
}
// Clean up destroyed specialization menus
specializationMenus = specializationMenus.filter(function (menu) {
return menu.parent && !menu.isDestroyed;
});
// Environmental hazards management
if (waveIndicator && waveIndicator.gameStarted) {
hazardTimer++;
if (hazardTimer >= nextHazardTime) {
hazardTimer = 0;
nextHazardTime = 1200 + Math.random() * 1200; // 20-40 seconds
// Spawn random environmental hazard
var hazardType = Math.floor(Math.random() * 4);
switch (hazardType) {
case 0:
// Asteroid field
if (asteroids.length < 3) {
var validPositions = [];
for (var x = 2; x < 22; x++) {
for (var y = 6; y < 25; y++) {
var cell = grid.getCell(x, y);
if (cell && cell.type === 0) {
validPositions.push({
x: x,
y: y
});
}
}
}
if (validPositions.length > 0) {
var pos = validPositions[Math.floor(Math.random() * validPositions.length)];
var asteroid = new Asteroid(grid.x + pos.x * CELL_SIZE + CELL_SIZE / 2, grid.y + pos.y * CELL_SIZE + CELL_SIZE / 2);
game.addChild(asteroid);
asteroids.push(asteroid);
var notification = game.addChild(new Notification("⚠️ Asteroid field detected! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
break;
case 1:
// Nebula cloud
if (nebulaClouds.length < 2) {
var cloudX = grid.x + (5 + Math.random() * 14) * CELL_SIZE;
var cloudY = grid.y + (8 + Math.random() * 12) * CELL_SIZE;
var nebula = new NebulaCloud(cloudX, cloudY);
game.addChild(nebula);
nebulaClouds.push(nebula);
var notification = game.addChild(new Notification("🌌 Nebula cloud formation! 🌌"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Remove after 15 seconds
LK.setTimeout(function () {
if (nebula.parent) {
nebula.destroy();
}
}, 15000);
}
break;
case 2:
// Solar flare
if (solarFlares.length === 0) {
var flare = new SolarFlare();
game.addChild(flare);
solarFlares.push(flare);
}
break;
case 3:
// Wormhole
if (wormholes.length < 2) {
// Create entry and exit points
var entryX = grid.x + (3 + Math.random() * 6) * CELL_SIZE;
var entryY = grid.y + (8 + Math.random() * 8) * CELL_SIZE;
var exitX = grid.x + (15 + Math.random() * 6) * CELL_SIZE;
var exitY = grid.y + (16 + Math.random() * 6) * CELL_SIZE;
var wormhole = new Wormhole(entryX, entryY, exitX, exitY);
game.addChild(wormhole);
wormholes.push(wormhole);
var notification = game.addChild(new Notification("🌀 Wormhole opened! 🌀"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Remove after 20 seconds
LK.setTimeout(function () {
if (wormhole.parent) {
wormhole.destroy();
}
}, 20000);
}
break;
}
}
}
// Update path junctions and route analysis
for (var i = 0; i < pathJunctions.length; i++) {
if (pathJunctions[i].parent) {
pathJunctions[i].update();
}
}
// Update environmental hazards
for (var i = asteroids.length - 1; i >= 0; i--) {
if (asteroids[i].parent) {
// Asteroids can be damaged by bullets
for (var j = 0; j < bullets.length; j++) {
var bullet = bullets[j];
var dx = bullet.x - asteroids[i].x;
var dy = bullet.y - asteroids[i].y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 60) {
// Hit asteroid
asteroids[i].takeDamage(bullet.damage);
returnBulletToPool(bullet);
break;
}
}
}
}
for (var i = 0; i < nebulaClouds.length; i++) {
if (nebulaClouds[i].parent) {
nebulaClouds[i].update();
}
}
for (var i = 0; i < solarFlares.length; i++) {
if (solarFlares[i].parent) {
solarFlares[i].update();
}
}
for (var i = 0; i < wormholes.length; i++) {
if (wormholes[i].parent) {
wormholes[i].update();
}
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
// Play victory sound
LK.getSound('victory').play();
LK.showYouWin();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Asteroid = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.health = 150;
self.maxHealth = 150;
self.isDestroyed = false;
self.gridX = Math.floor((x - grid.x) / CELL_SIZE);
self.gridY = Math.floor((y - grid.y) / CELL_SIZE);
var asteroidGraphics = self.attachAsset('asteroid', {
anchorX: 0.5,
anchorY: 0.5
});
// Add rotation animation
tween(asteroidGraphics, {
rotation: Math.PI * 2
}, {
duration: 8000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed) {
asteroidGraphics.rotation = 0;
}
}
});
// Health bar
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarOutline.y = healthBar.y = -asteroidGraphics.height / 2 - 15;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBar.x = -healthBar.width / 2;
healthBar.tint = 0x8B4513;
self.healthBar = healthBar;
self.takeDamage = function (damage) {
self.health -= damage;
if (self.health <= 0) {
self.health = 0;
self.destroy();
} else {
self.healthBar.width = self.health / self.maxHealth * 70;
}
};
self.down = function () {
// Allow manual destruction by tapping
self.takeDamage(50);
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Create debris effect
for (var i = 0; i < 5; i++) {
var debris = new Container();
debris.x = self.x + (Math.random() - 0.5) * 100;
debris.y = self.y + (Math.random() - 0.5) * 100;
var debrisGraphics = debris.attachAsset('asteroidDebris', {
anchorX: 0.5,
anchorY: 0.5
});
game.addChild(debris);
tween(debris, {
x: debris.x + (Math.random() - 0.5) * 200,
y: debris.y + (Math.random() - 0.5) * 200,
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (debris.parent) {
debris.parent.removeChild(debris);
}
}
});
}
// Clear grid cell
var cell = grid.getCell(self.gridX, self.gridY);
if (cell) {
cell.type = 0;
}
// Remove from asteroids array
var asteroidIndex = asteroids.indexOf(self);
if (asteroidIndex !== -1) {
asteroids.splice(asteroidIndex, 1);
}
// Recalculate paths
grid.pathFind();
grid.renderDebug();
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
// Block the grid cell
var cell = grid.getCell(self.gridX, self.gridY);
if (cell) {
cell.type = 1;
}
return self;
});
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;
// Create sci-fi projectile based on bullet type
var assetName = 'bullet'; // default
var bulletGraphics;
if (self.type === 'rapid') {
bulletGraphics = self.attachAsset('energyBeam', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0x00aaff;
} else if (self.type === 'sniper') {
bulletGraphics = self.attachAsset('laserBurst', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0xff0044;
} else if (self.type === 'splash') {
bulletGraphics = self.attachAsset('plasmaBolt', {
anchorX: 0.5,
anchorY: 0.5
});
bulletGraphics.tint = 0x44ff00;
} else {
bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Add pulsing glow effect to projectiles
tween(bulletGraphics, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(bulletGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
returnBulletToPool(self);
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) {
// Create sci-fi impact flash
if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDestroyed) {
tween(self.targetEnemy, {
tint: 0xffffff
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
if (self.targetEnemy && self.targetEnemy.parent && !self.targetEnemy.isDestroyed) {
tween(self.targetEnemy, {
tint: 0xffffff
}, {
duration: 200,
easing: tween.easeIn
});
}
}
});
}
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
// Play enemy destroy sound
LK.getSound('enemyDestroy').play();
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
// Play enemy hit sound
LK.getSound('enemyHit').play();
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create visual splash effect with limiting
if (canCreateEffect()) {
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
registerEffect(splashEffect);
}
// Determine splash radius and damage based on specialization
var splashRadius = CELL_SIZE * 1.5;
var splashDamageMultiplier = 0.5;
if (self.specialization === 'nuclear') {
splashRadius = CELL_SIZE * 3;
splashDamageMultiplier = 0.8;
// Create nuclear explosion effect
if (canCreateEffect()) {
var nuclearEffect = new Container();
nuclearEffect.x = self.targetEnemy.x;
nuclearEffect.y = self.targetEnemy.y;
var nuclearGraphics = nuclearEffect.attachAsset('nuclearBlast', {
anchorX: 0.5,
anchorY: 0.5
});
nuclearGraphics.tint = 0xFF0000;
nuclearGraphics.alpha = 0.8;
game.addChild(nuclearEffect);
tween(nuclearEffect, {
scaleX: 3,
scaleY: 3,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (nuclearEffect.parent) {
nuclearEffect.parent.removeChild(nuclearEffect);
}
}
});
}
}
// Splash damage to nearby enemies
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) {
var splashDamage = self.damage * splashDamageMultiplier;
otherEnemy.health -= splashDamage;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
// Chain reaction effect
if (self.specialization === 'chainreaction' && Math.random() < 0.3) {
// 30% chance to chain to another enemy
for (var j = 0; j < enemies.length; j++) {
var chainEnemy = enemies[j];
if (chainEnemy !== otherEnemy && chainEnemy !== self.targetEnemy) {
var chainDx = chainEnemy.x - otherEnemy.x;
var chainDy = chainEnemy.y - otherEnemy.y;
var chainDistance = Math.sqrt(chainDx * chainDx + chainDy * chainDy);
if (chainDistance <= CELL_SIZE * 2) {
chainEnemy.health -= splashDamage * 0.7;
if (chainEnemy.health <= 0) {
chainEnemy.health = 0;
} else {
chainEnemy.healthBar.width = chainEnemy.health / chainEnemy.maxHealth * 70;
}
break;
}
}
}
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect with limiting
if (canCreateEffect()) {
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
registerEffect(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 with limiting
if (canCreateEffect()) {
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
registerEffect(poisonEffect);
}
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper with limiting
if (canCreateEffect()) {
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
registerEffect(sniperEffect);
}
}
returnBulletToPool(self);
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this bullet and its graphics
tween.stop(self);
if (self.children && self.children[0]) {
tween.stop(self.children[0]);
}
// Remove from target enemy's bullets array
if (self.targetEnemy && self.targetEnemy.bulletsTargetingThis) {
var bulletIndex = self.targetEnemy.bulletsTargetingThis.indexOf(self);
if (bulletIndex !== -1) {
self.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
// Remove from bullets array
var bulletArrayIndex = bullets.indexOf(self);
if (bulletArrayIndex !== -1) {
bullets.splice(bulletArrayIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Nullify all object references
self.targetEnemy = null;
// Call parent destroy
Container.prototype.destroy.call(self);
};
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 = 0x220044; // Deep space color
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; // Energy field blue
} else {
// Create starfield effect with bluish tint
var starfieldTint = 0x001122 + (tint << 8) + (tint >> 1);
cellGraphics.tint = starfieldTint;
}
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);
};
});
// 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;
// Create energy particles around main effect
var particles = [];
for (var i = 0; i < 6; i++) {
var particle = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = 8;
particle.height = 8;
var angle = i / 6 * Math.PI * 2;
particle.x = Math.cos(angle) * 30;
particle.y = Math.sin(angle) * 30;
particles.push(particle);
}
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0x88FF44;
}
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0xCC44FF;
}
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0x44FFCC;
}
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
for (var i = 0; i < particles.length; i++) {
particles[i].tint = 0xFF8844;
}
break;
}
effectGraphics.alpha = 0.7;
// Animate particles spiraling outward
for (var i = 0; i < particles.length; i++) {
var particle = particles[i];
var delay = i * 50;
tween(particle, {
x: particle.x * 2,
y: particle.y * 2,
scaleX: 0.5,
scaleY: 0.5,
alpha: 0
}, {
duration: 400,
delay: delay,
easing: tween.easeOut
});
}
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();
}
});
}
});
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this effect and its children
tween.stop(self);
for (var i = 0; i < self.children.length; i++) {
if (self.children[i]) {
tween.stop(self.children[i]);
}
}
// Remove from activeEffects array
var effectIndex = activeEffects.indexOf(self);
if (effectIndex !== -1) {
activeEffects.splice(effectIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Call parent destroy
Container.prototype.destroy.call(self);
};
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.isDestroyed = false;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type !== '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;
// Add visual type indicator for better enemy identification
if (self.type !== 'normal') {
var typeIndicator = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
typeIndicator.width = 16;
typeIndicator.height = 16;
typeIndicator.y = -enemyGraphics.height / 2 - 35; // Above health bar
// Color code the indicator based on enemy type
switch (self.type) {
case 'fast':
typeIndicator.tint = 0x00AAFF;
break;
case 'immune':
typeIndicator.tint = 0xAA0000;
break;
case 'flying':
typeIndicator.tint = 0xFFFF00;
break;
case 'swarm':
typeIndicator.tint = 0xFF00FF;
break;
}
// Make boss indicators larger and more prominent
if (self.isBoss) {
typeIndicator.width = 24;
typeIndicator.height = 24;
typeIndicator.tint = 0xFFD700; // Gold for bosses
// Add pulsing effect for boss indicator
tween(typeIndicator, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(typeIndicator, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
});
}
}
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) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Improved visual clarity for enemy types with better differentiation
var baseTint = 0xFFFFFF;
// First set base tint based on enemy type for better visual clarity
switch (self.type) {
case 'fast':
baseTint = 0x4488FF; // Bright blue for fast enemies
break;
case 'immune':
baseTint = 0xFF4444; // Bright red for immune enemies
break;
case 'flying':
baseTint = 0xFFDD44; // Bright yellow for flying enemies
break;
case 'swarm':
baseTint = 0xFF44DD; // Bright magenta for swarm enemies
break;
case 'normal':
default:
baseTint = 0xCCCCCC; // Light gray for normal enemies
break;
}
// Apply boss scaling to tint if this is a boss
if (self.isBoss) {
// Make boss enemies more vibrant and add a red outline effect
var r = baseTint >> 16 & 0xFF;
var g = baseTint >> 8 & 0xFF;
var b = baseTint & 0xFF;
// Boost color intensity for bosses
r = Math.min(255, Math.floor(r * 1.3));
g = Math.min(255, Math.floor(g * 1.3));
b = Math.min(255, Math.floor(b * 1.3));
baseTint = r << 16 | g << 8 | b;
}
// Then apply status effect overlays
if (self.isImmune) {
// Keep immune tint but add pulsing effect for better visibility
enemyGraphics.tint = baseTint;
// Add shield shimmer effect for immune enemies
if (!self.shieldEffect && !self.isDestroyed) {
self.shieldEffect = true;
tween(enemyGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(enemyGraphics, {
alpha: 1.0
}, {
duration: 500,
easing: tween.easeInOut
});
}
}
});
}
} else if (self.poisoned && self.slowed) {
// Blend poison green with slow purple over the base tint
enemyGraphics.tint = 0x4C7FD4; // Keep existing blend color
} else if (self.poisoned) {
// Poison overlay - green tint
var r = Math.floor((baseTint >> 16 & 0xFF) * 0.5);
var g = Math.min(255, Math.floor((baseTint >> 8 & 0xFF) * 0.5 + 170));
var b = Math.floor((baseTint & 0xFF) * 0.5 + 85);
enemyGraphics.tint = r << 16 | g << 8 | b;
// Add crackling energy effect for poisoned
if (!self.poisonFlicker && !self.isDestroyed) {
self.poisonFlicker = true;
tween(enemyGraphics, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(enemyGraphics, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.poisonFlicker = false;
}
});
}
}
});
}
} else if (self.slowed) {
// Slow overlay - purple tint
var r = Math.floor((baseTint >> 16 & 0xFF) * 0.5 + 76);
var g = Math.floor((baseTint >> 8 & 0xFF) * 0.5);
var b = Math.min(255, Math.floor((baseTint & 0xFF) * 0.5 + 127));
enemyGraphics.tint = r << 16 | g << 8 | b;
} else {
enemyGraphics.tint = baseTint;
self.shieldEffect = false;
}
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 && !self.isDestroyed) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this enemy and its graphics
tween.stop(self);
if (self.children && self.children[0]) {
tween.stop(self.children[0]);
}
// Clear all bullets targeting this enemy
for (var i = 0; i < self.bulletsTargetingThis.length; i++) {
var bullet = self.bulletsTargetingThis[i];
if (bullet) {
bullet.targetEnemy = null;
}
}
self.bulletsTargetingThis = [];
// Remove from enemies array
var enemyIndex = enemies.indexOf(self);
if (enemyIndex !== -1) {
enemies.splice(enemyIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Clean up shadow if it's a flying enemy
if (self.isFlying && self.shadow) {
if (self.shadow.parent) {
self.shadow.parent.removeChild(self.shadow);
}
self.shadow = null;
}
// Nullify all object references
self.targetEnemy = null;
self.currentTarget = null;
self.flyingTarget = null;
self.healthBar = null;
self.energyCore = null;
// Call parent destroy
Container.prototype.destroy.call(self);
};
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
*/
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;
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
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;
}
// Mark junction cells for branching
for (var i = 0; i < pathJunctions.length; i++) {
var junction = pathJunctions[i];
var junctionCell = self.getCell(junction.gridX, junction.gridY);
if (junctionCell) {
junctionCell.isJunction = true;
junctionCell.junction = junction;
}
}
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;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.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;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05 && !enemy.isDestroyed) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05 && !enemy.isDestroyed) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget) {
// Check if enemy is at a junction
if (cell && cell.isJunction && !enemy.chosenBranch) {
var junction = cell.junction;
var chosenBranch = junction.choosePath(enemy);
if (chosenBranch) {
enemy.chosenBranch = chosenBranch;
enemy.currentTarget = grid.getCell(chosenBranch.targetX, chosenBranch.targetY);
// Show visual path indicator
if (routeVisualizer && Math.random() < 0.3) {
// Show for 30% of enemies
routeVisualizer.showPaths(junction, enemy);
}
// Add notification for significant route changes
if (Math.random() < 0.1) {
// 10% chance
var routeType = chosenBranch.isShortPath ? "direct" : "alternate";
var notification = game.addChild(new Notification("Enemy taking " + routeType + " route!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
} else {
enemy.currentTarget = cell.targets[0];
}
} else {
enemy.currentTarget = cell.targets[0];
}
}
if (enemy.currentTarget) {
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var Hero = Container.expand(function (heroType) {
var self = Container.call(this);
self.heroType = heroType || 'tank';
self.level = storage['hero_' + heroType + '_level'] || 1;
self.experience = storage['hero_' + heroType + '_xp'] || 0;
self.maxHealth = 200;
self.health = self.maxHealth;
self.isDestroyed = false;
self.movementSpeed = 1.5;
self.abilities = [];
self.currentTarget = null;
self.lastAbilityUse = 0;
self.abilityCooldown = 300; // 5 seconds at 60fps
// Set hero-specific stats
switch (self.heroType) {
case 'tank':
self.maxHealth = 300 + (self.level - 1) * 50;
self.movementSpeed = 1.0;
self.abilityCooldown = 600; // 10 seconds
break;
case 'support':
self.maxHealth = 150 + (self.level - 1) * 30;
self.movementSpeed = 2.0;
self.abilityCooldown = 480; // 8 seconds
break;
case 'dps':
self.maxHealth = 180 + (self.level - 1) * 35;
self.movementSpeed = 1.8;
self.abilityCooldown = 360; // 6 seconds
break;
}
self.health = self.maxHealth;
// Get appropriate asset
var assetName = 'hero_' + self.heroType;
var heroGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Create health bar
var healthBarOutline = self.attachAsset('heroHealthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('heroHealthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarOutline.y = healthBar.y = -heroGraphics.height / 2 - 20;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBar.x = -healthBar.width / 2;
self.healthBar = healthBar;
// Create XP bar
var xpBarOutline = self.attachAsset('heroXpBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var xpBar = self.attachAsset('heroXpBar', {
anchorX: 0,
anchorY: 0.5
});
xpBarOutline.y = xpBar.y = -heroGraphics.height / 2 - 35;
xpBarOutline.x = -xpBarOutline.width / 2;
xpBar.x = -xpBar.width / 2;
self.xpBar = xpBar;
// Create ability icon
var abilityIcon = self.attachAsset('heroAbilityIcon', {
anchorX: 0.5,
anchorY: 0.5
});
abilityIcon.y = heroGraphics.height / 2 + 20;
self.abilityIcon = abilityIcon;
// Level indicator
var levelText = new Text2(self.level.toString(), {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
levelText.anchor.set(0.5, 0.5);
levelText.y = heroGraphics.height / 2 + 20;
levelText.x = 40;
self.addChild(levelText);
self.levelText = levelText;
self.getExperienceForNextLevel = function () {
return self.level * 100; // 100 XP per level
};
self.gainExperience = function (amount) {
self.experience += amount;
var xpNeeded = self.getExperienceForNextLevel();
if (self.experience >= xpNeeded) {
self.experience -= xpNeeded;
self.level++;
self.levelText.setText(self.level.toString());
// Save progression
storage['hero_' + self.heroType + '_level'] = self.level;
// Level up effects
self.onLevelUp();
// Visual level up effect
tween(heroGraphics, {
scaleX: 1.3,
scaleY: 1.3,
tint: 0xFFD700
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(heroGraphics, {
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
// Save XP
storage['hero_' + self.heroType + '_xp'] = self.experience;
self.updateXpBar();
};
self.onLevelUp = function () {
// Increase health
var healthIncrease = self.heroType === 'tank' ? 50 : self.heroType === 'support' ? 30 : 35;
self.maxHealth += healthIncrease;
self.health = self.maxHealth; // Full heal on level up
self.updateHealthBar();
};
self.updateHealthBar = function () {
self.healthBar.width = self.health / self.maxHealth * 80;
};
self.updateXpBar = function () {
var xpProgress = self.experience / self.getExperienceForNextLevel();
self.xpBar.width = xpProgress * 80;
};
self.useAbility = function () {
if (LK.ticks - self.lastAbilityUse < self.abilityCooldown) {
return false; // Ability on cooldown
}
self.lastAbilityUse = LK.ticks;
// Play hero ability sound
LK.getSound('heroAbility').play();
// Ability cooldown visual effect
tween(self.abilityIcon, {
tint: 0x888888,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: self.abilityCooldown,
easing: tween.linear,
onFinish: function onFinish() {
tween(self.abilityIcon, {
tint: 0xFFD700,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeOut
});
}
});
switch (self.heroType) {
case 'tank':
return self.tankAbility();
case 'support':
return self.supportAbility();
case 'dps':
return self.dpsAbility();
}
return false;
};
self.tankAbility = function () {
// Taunt - Draw all enemies within range to attack hero
var tauntRadius = CELL_SIZE * 4;
var taunted = 0;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= tauntRadius) {
enemy.heroTarget = self;
enemy.heroTargetDuration = 300; // 5 seconds
taunted++;
}
}
// Visual effect
var effectRadius = tauntRadius;
var tauntEffect = new Container();
tauntEffect.x = self.x;
tauntEffect.y = self.y;
var effectGraphics = tauntEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.width = effectGraphics.height = effectRadius * 2;
effectGraphics.tint = 0x4CAF50;
effectGraphics.alpha = 0.6;
game.addChild(tauntEffect);
tween(tauntEffect, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (tauntEffect.parent) {
tauntEffect.parent.removeChild(tauntEffect);
}
}
});
return taunted > 0;
};
self.supportAbility = function () {
// Buff nearby towers
var buffRadius = CELL_SIZE * 3;
var buffed = 0;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= buffRadius) {
tower.heroBuff = {
damage: 1.5,
fireRate: 0.7,
duration: 600 // 10 seconds
};
buffed++;
// Visual effect on tower
tween(tower.children[0], {
tint: 0x2196F3
}, {
duration: 600,
easing: tween.linear,
onFinish: function onFinish() {
tower.children[0].tint = 0x445566;
}
});
}
}
return buffed > 0;
};
self.dpsAbility = function () {
// Area damage attack
var damageRadius = CELL_SIZE * 2.5;
var damaged = 0;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= damageRadius) {
var damage = 100 + (self.level - 1) * 20;
enemy.health -= damage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
damaged++;
// Visual damage effect
tween(enemy, {
tint: 0xFF5722
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(enemy, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
// Visual explosion effect
var explosionEffect = new Container();
explosionEffect.x = self.x;
explosionEffect.y = self.y;
var effectGraphics = explosionEffect.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.width = effectGraphics.height = damageRadius * 2;
effectGraphics.tint = 0xFF5722;
effectGraphics.alpha = 0.8;
game.addChild(explosionEffect);
tween(explosionEffect, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (explosionEffect.parent) {
explosionEffect.parent.removeChild(explosionEffect);
}
}
});
return damaged > 0;
};
self.update = function () {
if (self.health <= 0) {
return;
}
// Update bars
self.updateHealthBar();
self.updateXpBar();
// Move towards enemies or patrol
if (!self.currentTarget) {
self.findTarget();
}
if (self.currentTarget && self.currentTarget.parent && self.currentTarget.health > 0) {
var dx = self.currentTarget.x - self.x;
var dy = self.currentTarget.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > CELL_SIZE * 0.8) {
// Move towards target
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.movementSpeed;
self.y += Math.sin(angle) * self.movementSpeed;
} else {
// Attack target
if (self.heroType === 'tank') {
self.currentTarget.health -= 15;
} else if (self.heroType === 'dps') {
self.currentTarget.health -= 25;
} else {
self.currentTarget.health -= 10;
}
if (self.currentTarget.health <= 0) {
self.currentTarget.health = 0;
self.gainExperience(20);
self.currentTarget = null;
} else {
self.currentTarget.healthBar.width = self.currentTarget.health / self.currentTarget.maxHealth * 70;
}
}
} else {
self.currentTarget = null;
}
};
self.findTarget = function () {
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.health <= 0) continue;
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance && distance < CELL_SIZE * 5) {
closestDistance = distance;
closestEnemy = enemy;
}
}
self.currentTarget = closestEnemy;
};
self.down = function (x, y, obj) {
// Use ability when tapped
if (self.useAbility()) {
var notification = game.addChild(new Notification("Hero ability activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
tween.stop(self);
for (var i = 0; i < self.children.length; i++) {
if (self.children[i]) {
tween.stop(self.children[i]);
}
}
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var NebulaCloud = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.radius = 100;
self.isDestroyed = false;
var nebulaGraphics = self.attachAsset('nebulaCloud', {
anchorX: 0.5,
anchorY: 0.5
});
nebulaGraphics.alpha = 0.6;
// Pulsing animation
tween(nebulaGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(nebulaGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.6
}, {
duration: 2000,
easing: tween.easeInOut
});
}
}
});
self.update = function () {
// Apply effects to units within the nebula
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
// Slow effect
if (!enemy.nebulaSlowed) {
enemy.originalSpeed = enemy.originalSpeed || enemy.speed;
enemy.speed *= 0.7; // 30% speed reduction
enemy.nebulaSlowed = true;
}
// Stealth effect - harder for towers to target
enemy.nebulaStealthed = true;
} else {
// Remove effects when outside nebula
if (enemy.nebulaSlowed) {
enemy.speed = enemy.originalSpeed || enemy.speed;
enemy.nebulaSlowed = false;
}
enemy.nebulaStealthed = false;
}
}
// Apply slow effect to heroes
for (var i = 0; i < heroes.length; i++) {
var hero = heroes[i];
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
if (!hero.nebulaSlowed) {
hero.originalMovementSpeed = hero.originalMovementSpeed || hero.movementSpeed;
hero.movementSpeed *= 0.7;
hero.nebulaSlowed = true;
}
} else {
if (hero.nebulaSlowed) {
hero.movementSpeed = hero.originalMovementSpeed || hero.movementSpeed;
hero.nebulaSlowed = false;
}
}
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove effects from all units
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.nebulaSlowed) {
enemy.speed = enemy.originalSpeed || enemy.speed;
enemy.nebulaSlowed = false;
}
enemy.nebulaStealthed = false;
}
for (var i = 0; i < heroes.length; i++) {
var hero = heroes[i];
if (hero.nebulaSlowed) {
hero.movementSpeed = hero.originalMovementSpeed || hero.movementSpeed;
hero.nebulaSlowed = false;
}
}
var nebulaIndex = nebulaClouds.indexOf(self);
if (nebulaIndex !== -1) {
nebulaClouds.splice(nebulaIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
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 () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
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: 0xFFFFFF,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 60;
notificationGraphics.height = 80;
notificationGraphics.tint = 0x1A1A1A;
notificationGraphics.alpha = 0.95;
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 PathJunction = Container.expand(function (x, y, branches) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.branches = branches || []; // Array of possible path directions
self.gridX = Math.floor((x - grid.x) / CELL_SIZE);
self.gridY = Math.floor((y - grid.y) / CELL_SIZE);
self.isDestroyed = false;
// Visual junction indicator
var junctionGraphics = self.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
junctionGraphics.width = CELL_SIZE * 0.8;
junctionGraphics.height = CELL_SIZE * 0.8;
junctionGraphics.tint = 0x9c27b0;
junctionGraphics.alpha = 0.6;
// Pulsing animation to indicate junction
tween(junctionGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(junctionGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.6
}, {
duration: 1500,
easing: tween.easeInOut
});
}
}
});
// Choose path for enemy based on various factors
self.choosePath = function (enemy) {
if (self.branches.length === 0) return null;
var pathScores = [];
// Evaluate each branch
for (var i = 0; i < self.branches.length; i++) {
var branch = self.branches[i];
var score = 0;
// Base score - some randomness for variety
score += Math.random() * 100;
// Factor in enemy type preferences
switch (enemy.type) {
case 'fast':
// Fast enemies prefer shorter paths
score += branch.isShortPath ? 150 : 50;
break;
case 'immune':
// Immune enemies prefer heavily defended paths (they can tank it)
score += branch.towerDensity * 80;
break;
case 'flying':
// Flying enemies prefer paths with fewer anti-air towers
score += branch.hasAntiAir ? 20 : 120;
break;
case 'swarm':
// Swarm enemies prefer paths other swarm enemies haven't taken recently
score += branch.recentSwarmTraffic > 3 ? 30 : 100;
break;
default:
// Normal enemies prefer balanced paths
score += branch.difficulty < 50 ? 80 : 60;
break;
}
// Factor in current congestion (avoid clustering)
var congestionPenalty = branch.currentEnemyCount * 25;
score -= congestionPenalty;
// Factor in tower coverage (enemies avoid heavy defenses unless immune)
if (!enemy.isImmune) {
score -= branch.towerDensity * 40;
}
// Factor in path length (generally prefer shorter paths)
score -= branch.pathLength * 2;
pathScores.push({
branch: branch,
score: score
});
}
// Sort by score and add some weighted randomness
pathScores.sort(function (a, b) {
return b.score - a.score;
});
// Weighted selection - higher scores more likely but not guaranteed
var totalWeight = 0;
for (var i = 0; i < pathScores.length; i++) {
var weight = Math.max(1, pathScores[i].score);
pathScores[i].weight = weight;
totalWeight += weight;
}
var randomValue = Math.random() * totalWeight;
var currentWeight = 0;
for (var i = 0; i < pathScores.length; i++) {
currentWeight += pathScores[i].weight;
if (randomValue <= currentWeight) {
return pathScores[i].branch;
}
}
// Fallback to first option
return pathScores[0].branch;
};
// Update junction statistics
self.update = function () {
// Update branch statistics
for (var i = 0; i < self.branches.length; i++) {
var branch = self.branches[i];
// Count enemies currently on this branch
branch.currentEnemyCount = 0;
branch.recentSwarmTraffic = Math.max(0, branch.recentSwarmTraffic - 0.1);
for (var j = 0; j < enemies.length; j++) {
var enemy = enemies[j];
if (enemy.chosenBranch === branch) {
branch.currentEnemyCount++;
if (enemy.type === 'swarm') {
branch.recentSwarmTraffic += 0.1;
}
}
}
// Calculate tower density in branch area
branch.towerDensity = 0;
branch.hasAntiAir = false;
for (var t = 0; t < towers.length; t++) {
var tower = towers[t];
var dx = tower.x - (grid.x + branch.targetX * CELL_SIZE);
var dy = tower.y - (grid.y + branch.targetY * CELL_SIZE);
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 4) {
branch.towerDensity++;
if (tower.id === 'sniper' || tower.id === 'rapid') {
branch.hasAntiAir = true;
}
}
}
// Update path difficulty
branch.difficulty = branch.towerDensity * 10 + branch.pathLength;
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
var junctionIndex = pathJunctions.indexOf(self);
if (junctionIndex !== -1) {
pathJunctions.splice(junctionIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var RouteVisualizer = Container.expand(function () {
var self = Container.call(this);
self.pathLines = [];
self.isDestroyed = false;
self.showPaths = function (junction, enemy) {
// Clear existing path lines
self.clearPaths();
// Create visual lines for each possible branch
for (var i = 0; i < junction.branches.length; i++) {
var branch = junction.branches[i];
var pathLine = new Container();
// Create line segments to show the path
var segments = [];
var currentX = junction.gridX;
var currentY = junction.gridY;
// Draw path to target
var targetX = branch.targetX;
var targetY = branch.targetY;
var steps = Math.max(Math.abs(targetX - currentX), Math.abs(targetY - currentY));
for (var step = 0; step <= steps; step++) {
var progress = step / steps;
var segmentX = Math.round(currentX + (targetX - currentX) * progress);
var segmentY = Math.round(currentY + (targetY - currentY) * progress);
var segment = pathLine.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
segment.width = 12;
segment.height = 12;
segment.x = grid.x + segmentX * CELL_SIZE;
segment.y = grid.y + segmentY * CELL_SIZE;
// Color code based on path difficulty
var difficulty = branch.difficulty || 0;
if (difficulty < 30) {
segment.tint = 0x00FF00; // Green for easy
} else if (difficulty < 60) {
segment.tint = 0xFFFF00; // Yellow for medium
} else {
segment.tint = 0xFF0000; // Red for hard
}
segment.alpha = 0.7;
}
game.addChild(pathLine);
self.pathLines.push(pathLine);
// Animate the path appearance
tween(pathLine, {
alpha: 1.0
}, {
duration: 500,
easing: tween.easeOut
});
}
// Auto-hide after a few seconds
LK.setTimeout(function () {
self.clearPaths();
}, 3000);
};
self.clearPaths = function () {
for (var i = 0; i < self.pathLines.length; i++) {
if (self.pathLines[i].parent) {
self.pathLines[i].parent.removeChild(self.pathLines[i]);
}
}
self.pathLines = [];
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
self.clearPaths();
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var SolarFlare = Container.expand(function () {
var self = Container.call(this);
self.duration = 600; // 10 seconds at 60fps
self.remainingTime = self.duration;
self.isActive = true;
self.isDestroyed = false;
// Position in center of battlefield
self.x = grid.x + 12 * CELL_SIZE;
self.y = grid.y + 15 * CELL_SIZE;
var flareGraphics = self.attachAsset('solarFlare', {
anchorX: 0.5,
anchorY: 0.5
});
flareGraphics.alpha = 0.8;
flareGraphics.blendMode = 1; // Additive blending
// Intense pulsing animation
tween(flareGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed) {
tween(flareGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 300,
easing: tween.easeInOut
});
}
}
});
// Show notification
var notification = game.addChild(new Notification("⚡ Solar Flare! Energy towers boosted! ⚡"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
self.update = function () {
self.remainingTime--;
if (self.remainingTime <= 0) {
self.destroy();
return;
}
// Boost energy-based towers (rapid, sniper, default)
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.id === 'rapid' || tower.id === 'sniper' || tower.id === 'default') {
if (!tower.solarFlareBoost) {
tower.originalFireRate = tower.fireRate;
tower.originalDamage = tower.damage;
tower.fireRate = Math.floor(tower.fireRate * 0.6); // 40% faster
tower.damage = Math.floor(tower.damage * 1.3); // 30% more damage
tower.solarFlareBoost = true;
// Visual effect on boosted towers
tween(tower.energyCore, {
tint: 0xFFD700,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut
});
}
}
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove boosts from all towers
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.solarFlareBoost) {
tower.fireRate = tower.originalFireRate;
tower.damage = tower.originalDamage;
tower.solarFlareBoost = false;
// Reset visual effects
var originalTint = 0x88AACC;
switch (tower.id) {
case 'rapid':
originalTint = 0x00AAFF;
break;
case 'sniper':
originalTint = 0xFF5500;
break;
case 'default':
originalTint = 0x88AACC;
break;
}
tween(tower.energyCore, {
tint: originalTint,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
}
var flareIndex = solarFlares.indexOf(self);
if (flareIndex !== -1) {
solarFlares.splice(flareIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
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('tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
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(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -20; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost, {
size: 50,
fill: 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 = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var SpecializationMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 300;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 1800;
menuBackground.height = 600;
menuBackground.tint = 0x1A1A1A;
menuBackground.alpha = 0.98;
var titleText = new Text2('Choose Specialization Path', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -200;
self.addChild(titleText);
var subtitleText = new Text2('Your tower has reached level 3 and can specialize!', {
size: 50,
fill: 0xCCCCCC,
weight: 400
});
subtitleText.anchor.set(0.5, 0.5);
subtitleText.y = -140;
self.addChild(subtitleText);
// Create specialization options based on tower type
var options = [];
if (self.tower.id === 'sniper') {
options = [{
type: 'antiarmor',
name: 'Anti-Armor',
desc: 'Pierce shields\n+50% damage vs immune',
color: 0xFF6600
}, {
type: 'longrange',
name: 'Long-Range',
desc: 'Double firing range\nBetter enemy targeting',
color: 0x00AAFF
}];
} else if (self.tower.id === 'splash') {
options = [{
type: 'nuclear',
name: 'Nuclear',
desc: 'Huge damage\nLarger splash radius',
color: 0xFF0000
}, {
type: 'chainreaction',
name: 'Chain Reaction',
desc: 'Damage spreads\nbetween enemies',
color: 0xFFFF00
}];
}
// Create option buttons
for (var i = 0; i < options.length; i++) {
var option = options[i];
var button = new Container();
var buttonBg = button.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 700;
buttonBg.height = 200;
buttonBg.tint = option.color;
buttonBg.alpha = 0.8;
var nameText = new Text2(option.name, {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
nameText.anchor.set(0.5, 0.5);
nameText.y = -40;
button.addChild(nameText);
var descText = new Text2(option.desc, {
size: 45,
fill: 0xFFFFFF,
weight: 400
});
descText.anchor.set(0.5, 0.5);
descText.y = 40;
button.addChild(descText);
button.x = (i - 0.5) * 800;
button.y = 50;
button.optionType = option.type;
button.down = function () {
self.selectSpecialization(this.optionType);
};
self.addChild(button);
}
// Close button
var closeButton = new Container();
var closeBg = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBg.width = 100;
closeBg.height = 100;
closeBg.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = 850;
closeButton.y = -250;
closeButton.down = function () {
self.destroy();
};
self.addChild(closeButton);
self.selectSpecialization = function (branchType) {
// Apply specialization to tower
var specialization = new TowerSpecialization(self.tower, branchType);
self.tower.specialization = specialization;
self.tower.addChild(specialization);
specialization.applySpecialization();
var notification = game.addChild(new Notification("Tower specialized: " + branchType));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.destroy();
};
return self;
});
var StationCustomizer = Container.expand(function () {
var self = Container.call(this);
self.modules = [];
self.moduleSlots = [];
self.selectedSlot = null;
self.customizationMode = false;
// Create module slots around the station
var slotPositions = [{
x: -2,
y: -2
}, {
x: 0,
y: -2
}, {
x: 2,
y: -2
}, {
x: -2,
y: 0
}, {
x: 2,
y: 0
}, {
x: -2,
y: 2
}, {
x: 0,
y: 2
}, {
x: 2,
y: 2
}];
for (var i = 0; i < slotPositions.length; i++) {
var slot = new Container();
var slotGraphics = slot.attachAsset('moduleSocket', {
anchorX: 0.5,
anchorY: 0.5
});
slotGraphics.alpha = 0.5;
slotGraphics.tint = 0x556677;
slot.gridX = slotPositions[i].x;
slot.gridY = slotPositions[i].y;
slot.x = grid.x + (12 + slot.gridX) * CELL_SIZE;
slot.y = grid.y + (15 + slot.gridY) * CELL_SIZE;
slot.module = null;
slot.slotIndex = i;
slot.down = function () {
if (self.customizationMode) {
self.selectedSlot = this;
self.showModuleMenu();
}
};
self.addChild(slot);
self.moduleSlots.push(slot);
}
self.showModuleMenu = function () {
if (!self.selectedSlot) return;
var menu = new Container();
var menuBg = menu.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBg.width = 600;
menuBg.height = 300;
menuBg.tint = 0x333333;
var moduleTypes = ['research', 'shield', 'teleporter'];
for (var i = 0; i < moduleTypes.length; i++) {
var button = new Container();
var buttonBg = button.attachAsset('techTreeNode', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 120;
buttonBg.height = 80;
var cost = getModuleCost(moduleTypes[i], 1);
var buttonText = new Text2(moduleTypes[i] + '\n' + cost + 'g', {
size: 30,
fill: 0xFFFFFF,
weight: 600
});
buttonText.anchor.set(0.5, 0.5);
button.addChild(buttonText);
button.moduleType = moduleTypes[i];
button.x = -180 + i * 180;
button.y = 50;
button.down = function () {
self.buildModule(this.moduleType);
menu.destroy();
};
menu.addChild(button);
}
menu.x = 2048 / 2;
menu.y = 1400;
game.addChild(menu);
LK.setTimeout(function () {
if (menu.parent) {
menu.parent.removeChild(menu);
}
}, 5000);
};
self.buildModule = function (moduleType) {
if (!self.selectedSlot || self.selectedSlot.module) return;
var cost = getModuleCost(moduleType, 1);
if (gold < cost) {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
var module;
switch (moduleType) {
case 'research':
module = new ResearchLab();
break;
case 'shield':
module = new ShieldGenerator();
break;
case 'teleporter':
module = new TeleporterNode();
break;
default:
module = new StationModule(moduleType);
}
module.gridX = self.selectedSlot.gridX;
module.gridY = self.selectedSlot.gridY;
module.x = self.selectedSlot.x;
module.y = self.selectedSlot.y;
self.selectedSlot.module = module;
self.selectedSlot.children[0].alpha = 0; // Hide slot graphic
self.addChild(module);
self.modules.push(module);
setGold(gold - cost);
var notification = game.addChild(new Notification(moduleType + " module built!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.selectedSlot = null;
};
self.toggleCustomizationMode = function () {
self.customizationMode = !self.customizationMode;
for (var i = 0; i < self.moduleSlots.length; i++) {
var slot = self.moduleSlots[i];
if (!slot.module) {
slot.children[0].alpha = self.customizationMode ? 0.8 : 0.3;
}
}
var modeText = self.customizationMode ? "ON" : "OFF";
var notification = game.addChild(new Notification("Customization mode: " + modeText));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
};
self.update = function () {
for (var i = 0; i < self.modules.length; i++) {
if (self.modules[i].update) {
self.modules[i].update();
}
}
};
return self;
});
var StationModule = Container.expand(function (moduleType) {
var self = Container.call(this);
self.moduleType = moduleType || 'basic';
self.level = 1;
self.maxLevel = 3;
self.isActive = true;
self.gridX = 0;
self.gridY = 0;
self.isDestroyed = false;
// Get appropriate asset
var assetName = 'stationModule';
switch (moduleType) {
case 'research':
assetName = 'researchLab';
break;
case 'shield':
assetName = 'shieldGenerator';
break;
case 'teleporter':
assetName = 'teleporter';
break;
}
var moduleGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add energy core
var energyCore = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
energyCore.width = 30;
energyCore.height = 30;
energyCore.tint = 0x00aaff;
// Level indicators
var levelIndicators = [];
for (var i = 0; i < self.maxLevel; i++) {
var indicator = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = 12;
indicator.height = 12;
indicator.x = -30 + i * 30;
indicator.y = 60;
indicator.tint = i < self.level ? 0xffffff : 0x444444;
levelIndicators.push(indicator);
}
self.updateLevelIndicators = function () {
for (var i = 0; i < levelIndicators.length; i++) {
levelIndicators[i].tint = i < self.level ? 0xffffff : 0x444444;
}
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
var upgradeCost = getModuleCost(self.moduleType, self.level + 1);
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
self.updateLevelIndicators();
self.onUpgrade();
return true;
}
}
return false;
};
self.onUpgrade = function () {
// Override in specific module types
};
self.update = function () {
if (!self.isActive) return;
// Pulsing energy core
if (LK.ticks % 120 === 0) {
tween(energyCore, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(energyCore, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeInOut
});
}
});
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var TeleporterNode = StationModule.expand(function () {
var self = StationModule.call(this, 'teleporter');
self.teleportEnergy = 100;
self.maxTeleportEnergy = 100;
self.linkedNodes = [];
self.onUpgrade = function () {
self.maxTeleportEnergy = 100 + (self.level - 1) * 25;
self.teleportEnergy = self.maxTeleportEnergy;
};
self.teleportHero = function (hero, targetNode) {
if (self.teleportEnergy >= 30 && targetNode && targetNode.teleportEnergy >= 30) {
// Create teleport effect at source
var sourceEffect = new Container();
sourceEffect.x = self.x;
sourceEffect.y = self.y;
var sourceGraphics = sourceEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
sourceGraphics.tint = 0x9c27b0;
sourceGraphics.alpha = 0.8;
game.addChild(sourceEffect);
// Create teleport effect at destination
var destEffect = new Container();
destEffect.x = targetNode.x;
destEffect.y = targetNode.y;
var destGraphics = destEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
destGraphics.tint = 0x9c27b0;
destGraphics.alpha = 0.8;
game.addChild(destEffect);
// Animate effects
tween(sourceEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (sourceEffect.parent) {
sourceEffect.parent.removeChild(sourceEffect);
}
}
});
tween(destEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (destEffect.parent) {
destEffect.parent.removeChild(destEffect);
}
}
});
// Teleport hero
hero.x = targetNode.x;
hero.y = targetNode.y;
// Consume energy
self.teleportEnergy = Math.max(0, self.teleportEnergy - 30);
targetNode.teleportEnergy = Math.max(0, targetNode.teleportEnergy - 30);
return true;
}
return false;
};
self.redirectEnemyPath = function () {
if (self.teleportEnergy >= 50 && self.linkedNodes.length > 0) {
var targetNode = self.linkedNodes[Math.floor(Math.random() * self.linkedNodes.length)];
// Find nearby enemies and redirect one
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy.isFlying) {
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 2) {
// Teleport enemy
enemy.x = targetNode.x;
enemy.y = targetNode.y;
enemy.currentCellX = targetNode.gridX;
enemy.currentCellY = targetNode.gridY;
enemy.currentTarget = null;
self.teleportEnergy = Math.max(0, self.teleportEnergy - 50);
var notification = game.addChild(new Notification("Enemy redirected through teleporter!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
break;
}
}
}
}
};
self.update = function () {
StationModule.prototype.update.call(self);
// Regenerate teleport energy
if (LK.ticks % 120 === 0 && self.teleportEnergy < self.maxTeleportEnergy) {
// Every 2 seconds
self.teleportEnergy = Math.min(self.maxTeleportEnergy, self.teleportEnergy + self.level * 3);
}
};
self.down = function () {
self.redirectEnemyPath();
};
return self;
});
var ShieldGenerator = StationModule.expand(function () {
var self = StationModule.call(this, 'shield');
self.shieldEnergy = 100;
self.maxShieldEnergy = 100;
self.shieldActive = false;
self.shieldEffect = null;
self.onUpgrade = function () {
self.maxShieldEnergy = 100 + (self.level - 1) * 50;
self.shieldEnergy = self.maxShieldEnergy;
};
self.activateShield = function () {
if (self.shieldEnergy >= 50 && !self.shieldActive) {
self.shieldActive = true;
self.shieldEnergy = Math.max(0, self.shieldEnergy - 50);
// Create visual shield effect
self.shieldEffect = new Container();
self.shieldEffect.x = self.x;
self.shieldEffect.y = self.y;
var shieldGraphics = self.shieldEffect.attachAsset('shieldEffect', {
anchorX: 0.5,
anchorY: 0.5
});
shieldGraphics.alpha = 0.3;
shieldGraphics.tint = 0x00ffff;
game.addChild(self.shieldEffect);
// Shield lasts for 10 seconds
LK.setTimeout(function () {
self.deactivateShield();
}, 10000);
var notification = game.addChild(new Notification("Station shields activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
self.deactivateShield = function () {
self.shieldActive = false;
if (self.shieldEffect && self.shieldEffect.parent) {
self.shieldEffect.parent.removeChild(self.shieldEffect);
self.shieldEffect = null;
}
};
self.update = function () {
StationModule.prototype.update.call(self);
// Regenerate shield energy
if (LK.ticks % 180 === 0 && self.shieldEnergy < self.maxShieldEnergy) {
// Every 3 seconds
self.shieldEnergy = Math.min(self.maxShieldEnergy, self.shieldEnergy + self.level * 5);
}
// Update shield effect position
if (self.shieldEffect) {
self.shieldEffect.x = self.x;
self.shieldEffect.y = self.y;
}
};
self.down = function () {
self.activateShield();
};
return self;
});
var ResearchLab = StationModule.expand(function () {
var self = StationModule.call(this, 'research');
self.researchPoints = 0;
self.activeResearch = null;
self.completedTechs = [];
self.onUpgrade = function () {
// Higher level labs generate research points faster
self.researchPointsPerTick = self.level;
};
self.update = function () {
StationModule.prototype.update.call(self);
// Generate research points
if (LK.ticks % 60 === 0) {
// Every second
self.researchPoints += self.level;
storage.researchPoints = self.researchPoints;
}
// Complete active research
if (self.activeResearch && self.researchPoints >= self.activeResearch.cost) {
self.completeResearch();
}
};
self.startResearch = function (techId) {
var tech = getTechnology(techId);
if (tech && !self.activeResearch) {
self.activeResearch = tech;
var notification = game.addChild(new Notification("Research started: " + tech.name));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
self.completeResearch = function () {
if (self.activeResearch) {
self.researchPoints -= self.activeResearch.cost;
self.completedTechs.push(self.activeResearch.id);
storage.completedTechs = self.completedTechs;
// Apply research benefits
self.activeResearch.onComplete();
var notification = game.addChild(new Notification("Research complete: " + self.activeResearch.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
self.activeResearch = null;
}
};
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
var baseRange;
switch (self.id) {
case 'sniper':
// Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost
if (self.level === self.maxLevel) {
baseRange = 12 * CELL_SIZE; // Significantly increased range for max level
} else {
baseRange = (5 + (self.level - 1) * 0.8) * CELL_SIZE;
}
break;
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;
default:
// Default: base 3, +0.5 per level
baseRange = (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
// Apply specialization range multiplier
if (self.specialization && self.specialization.branchType === 'longrange') {
baseRange *= 2.0;
}
return baseRange;
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
switch (self.id) {
case 'rapid':
self.fireRate = 35; // Slightly slower to reduce overwhelming power
self.damage = 8; // Increased from 5 for better effectiveness
self.range = 2.8 * CELL_SIZE; // Slightly better range
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 100; // Slower but more impactful
self.damage = 40; // Increased significantly for true sniper feel
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 80; // Slightly slower
self.damage = 18; // Increased base damage
self.range = 2.2 * CELL_SIZE; // Slightly better range
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 45; // Faster to apply slow effects more consistently
self.damage = 12; // Increased from 8 for better utility
self.range = 3.8 * CELL_SIZE; // Better range for support role
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 60; // Faster to apply poison more effectively
self.damage = 15; // Increased from 12
self.range = 3.5 * CELL_SIZE; // Better range
self.bulletSpeed = 5;
break;
}
// Create tower base structure
var baseAssetName = 'tower_base_' + (self.id === 'default' ? 'default' : self.id);
var baseGraphics = self.attachAsset(baseAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Create support structures for higher levels
self.supportStructures = [];
// Create energy core based on tower type
var energyCore = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
energyCore.width = 40;
energyCore.height = 40;
switch (self.id) {
case 'rapid':
energyCore.tint = 0x00AAFF;
break;
case 'sniper':
energyCore.tint = 0xFF5500;
break;
case 'splash':
energyCore.tint = 0x33CC00;
break;
case 'slow':
energyCore.tint = 0x9900FF;
break;
case 'poison':
energyCore.tint = 0x00FFAA;
break;
default:
energyCore.tint = 0x88AACC;
}
// Add pulsing energy core animation
self.energyCore = energyCore;
tween(energyCore, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(energyCore, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 1000,
easing: tween.easeInOut
});
}
});
// Method to update tower visual appearance based on level
self.updateTowerVisuals = function () {
// Ensure gunContainer exists before trying to use it
if (!gunContainer) {
console.warn("gunContainer not found, skipping visual update");
return;
}
// Clear existing gun graphics
if (self.gunGraphics && self.gunGraphics.parent) {
gunContainer.removeChild(self.gunGraphics);
}
// Clear existing support structures
for (var i = 0; i < self.supportStructures.length; i++) {
if (self.supportStructures[i].parent) {
self.removeChild(self.supportStructures[i]);
}
}
self.supportStructures = [];
// Create new gun graphics based on current level
var gunAssetName = 'tower_gun_' + (self.id === 'default' ? 'default' : self.id) + '_' + self.level;
self.gunGraphics = gunContainer.attachAsset(gunAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add support structures for higher levels
if (self.level >= 2) {
// Add support beams
for (var i = 0; i < 2; i++) {
var beam = self.attachAsset('tower_support_beam', {
anchorX: 0.5,
anchorY: 1.0
});
beam.x = i === 0 ? -30 : 30;
beam.y = baseGraphics.height / 2 - 10;
beam.rotation = i === 0 ? -0.3 : 0.3;
self.supportStructures.push(beam);
}
}
if (self.level >= 3) {
// Add radar dish for better targeting
var radar = self.attachAsset('tower_radar_dish', {
anchorX: 0.5,
anchorY: 0.5
});
radar.x = 0;
radar.y = -baseGraphics.height / 2 - 20;
radar.alpha = 0.8;
self.supportStructures.push(radar);
// Rotate radar dish slowly
tween(radar, {
rotation: Math.PI * 2
}, {
duration: 4000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed && radar.parent) {
radar.rotation = 0;
}
}
});
}
if (self.level >= 4) {
// Add antenna for enhanced range
var antenna = self.attachAsset('tower_antenna', {
anchorX: 0.5,
anchorY: 1.0
});
antenna.x = 0;
antenna.y = -baseGraphics.height / 2 - 60;
self.supportStructures.push(antenna);
// Add armor plates
for (var i = 0; i < 4; i++) {
var armor = self.attachAsset('tower_armor_plate', {
anchorX: 0.5,
anchorY: 0.5
});
var angle = i / 4 * Math.PI * 2;
armor.x = Math.cos(angle) * 40;
armor.y = Math.sin(angle) * 40;
armor.rotation = angle;
armor.alpha = 0.7;
self.supportStructures.push(armor);
}
}
if (self.level >= 5) {
// Add energy conduits
for (var i = 0; i < 3; i++) {
var conduit = self.attachAsset('tower_energy_conduit', {
anchorX: 0.5,
anchorY: 0.5
});
var angle = i / 3 * Math.PI * 2;
conduit.x = Math.cos(angle) * 50;
conduit.y = Math.sin(angle) * 50;
conduit.rotation = angle + Math.PI / 2;
// Pulsing energy effect
tween(conduit, {
alpha: 0.4
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed && conduit.parent) {
tween(conduit, {
alpha: 1.0
}, {
duration: 800,
easing: tween.easeInOut
});
}
}
});
self.supportStructures.push(conduit);
}
}
if (self.level >= 6) {
// Max level - add ultimate enhancement visual
var enhancement = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
enhancement.width = 80;
enhancement.height = 80;
enhancement.tint = 0xFFD700;
enhancement.alpha = 0.3;
enhancement.y = 0;
// Rotating golden aura for max level
tween(enhancement, {
rotation: Math.PI * 2,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed && enhancement.parent) {
enhancement.rotation = 0;
enhancement.scaleX = 1.0;
enhancement.scaleY = 1.0;
}
}
});
self.supportStructures.push(enhancement);
}
// Update energy core size based on level
var coreSize = 40 + (self.level - 1) * 8;
self.energyCore.width = coreSize;
self.energyCore.height = coreSize;
// Add specialization visuals if present
if (self.specialization) {
self.updateSpecializationVisuals();
}
};
// Method to add specialization visual indicators
self.updateSpecializationVisuals = function () {
if (!self.specialization) return;
var specAssetName;
switch (self.specialization.branchType) {
case 'antiarmor':
specAssetName = 'tower_spec_armor_pierce';
break;
case 'longrange':
specAssetName = 'tower_spec_long_range';
break;
case 'nuclear':
specAssetName = 'tower_spec_nuclear';
break;
case 'chainreaction':
specAssetName = 'tower_spec_chain_reaction';
break;
default:
return;
}
var specVisual = self.attachAsset(specAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
specVisual.y = -baseGraphics.height / 2 - 40;
specVisual.alpha = 0.9;
// Pulsing effect for specialization indicator
tween(specVisual, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.6
}, {
duration: 1200,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.isDestroyed && specVisual.parent) {
tween(specVisual, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.9
}, {
duration: 1200,
easing: tween.easeInOut
});
}
}
});
self.supportStructures.push(specVisual);
};
// Initialize tower visuals
self.updateTowerVisuals();
var gunContainer = new Container();
self.addChild(gunContainer);
var gunGraphics = gunContainer.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
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);
}
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// More balanced upgrade cost scaling
if (self.level === self.maxLevel - 1) {
// Final upgrade is expensive but not prohibitive
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(1.8, self.level - 1) * 2.5);
} else {
// Use 1.8x multiplier instead of 2x for more manageable costs
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(1.8, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// Play upgrade sound
LK.getSound('towerUpgrade').play();
// Update tower visuals for new level
self.updateTowerVisuals();
// Check for specialization unlock at level 3
if (self.level === 3 && !self.specialization && (self.id === 'sniper' || self.id === 'splash')) {
var specMenu = new SpecializationMenu(self);
game.addChild(specMenu);
specMenu.x = 2048 / 2;
tween(specMenu, {
y: 2732 - 300
}, {
duration: 300,
easing: tween.backOut
});
specializationMenus.push(specMenu);
return true;
}
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type with balanced scaling
switch (self.id) {
case 'rapid':
if (self.level === self.maxLevel) {
// Max level gets significant boost
self.fireRate = Math.max(8, 35 - self.level * 4.2);
self.damage = 8 + self.level * 6; // More modest scaling
self.bulletSpeed = 7 + self.level * 1.5;
} else {
self.fireRate = Math.max(18, 35 - self.level * 2.8);
self.damage = 8 + self.level * 3.5;
self.bulletSpeed = 7 + self.level * 0.8;
}
break;
case 'sniper':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(15, 100 - self.level * 12);
self.damage = 40 + self.level * 18; // High damage scaling
self.bulletSpeed = 25 + self.level * 2;
} else {
self.fireRate = Math.max(30, 100 - self.level * 8);
self.damage = 40 + self.level * 12;
self.bulletSpeed = 25 + self.level * 1;
}
break;
case 'splash':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(12, 80 - self.level * 10);
self.damage = 18 + self.level * 12; // Strong splash scaling
self.bulletSpeed = 4 + self.level * 1.5;
} else {
self.fireRate = Math.max(25, 80 - self.level * 6);
self.damage = 18 + self.level * 8;
self.bulletSpeed = 4 + self.level * 0.8;
}
break;
case 'slow':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(8, 45 - self.level * 5.5);
self.damage = 12 + self.level * 8; // Better damage for utility tower
self.bulletSpeed = 5 + self.level * 1.2;
} else {
self.fireRate = Math.max(20, 45 - self.level * 3.5);
self.damage = 12 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.7;
}
break;
case 'poison':
if (self.level === self.maxLevel) {
self.fireRate = Math.max(10, 60 - self.level * 7.5);
self.damage = 15 + self.level * 10; // Strong poison scaling
self.bulletSpeed = 5 + self.level * 1.3;
} else {
self.fireRate = Math.max(22, 60 - self.level * 5);
self.damage = 15 + self.level * 6;
self.bulletSpeed = 5 + self.level * 0.8;
}
break;
default:
if (self.level === self.maxLevel) {
self.fireRate = Math.max(10, 60 - self.level * 8);
self.damage = 10 + self.level * 12;
self.bulletSpeed = 5 + self.level * 1.5;
} else {
self.fireRate = Math.max(25, 60 - self.level * 5);
self.damage = 10 + self.level * 7;
self.bulletSpeed = 5 + self.level * 0.8;
}
}
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
// Use spatial partitioning for better performance
var nearbyEnemies = spatialGrid.getNearbyEnemies(self.x, self.y, self.getRange());
for (var i = 0; i < nearbyEnemies.length; i++) {
var enemy = nearbyEnemies[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()) {
// Apply nebula stealth effect - reduce targeting chance
var targetingChance = enemy.nebulaStealthed ? 0.3 : 1.0;
if (Math.random() > targetingChance) {
continue; // Skip this enemy due to stealth
}
// 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 () {
// Update ultimate ability
if (self.ultimate) {
self.ultimate.update();
}
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
var currentFireRate = self.fireRate;
if (self.heroBuff && self.heroBuff.duration > 0) {
currentFireRate = Math.floor(self.fireRate * self.heroBuff.fireRate);
}
if (LK.ticks - self.lastFired >= currentFireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
tutorialSystem.checkActionCompleted('selectTower');
}
// Check for ultimate ability activation (double tap or long press simulation)
if (self.ultimate && self.ultimate.canActivate()) {
// Simple activation - tap when ultimate is ready
if (self.ultimate.activate()) {
return; // Ultimate was activated, don't show menu
}
}
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 potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
// Apply hero buff if present
var finalDamage = self.damage;
var finalFireRate = self.fireRate;
if (self.heroBuff && self.heroBuff.duration > 0) {
finalDamage = Math.floor(self.damage * self.heroBuff.damage);
finalFireRate = Math.floor(self.fireRate * self.heroBuff.fireRate);
self.heroBuff.duration--;
if (self.heroBuff.duration <= 0) {
self.heroBuff = null;
}
}
// Apply support bonus from linked towers
if (self.supportBonus) {
finalDamage = Math.floor(finalDamage * self.supportBonus);
finalFireRate = Math.floor(finalFireRate / self.supportBonus);
}
// Apply specialization effects
if (self.specialization) {
if (self.specialization.branchType === 'antiarmor' && self.targetEnemy.isImmune) {
finalDamage = Math.floor(finalDamage * 1.5);
}
}
var bullet = getBulletFromPool(bulletX, bulletY, self.targetEnemy, finalDamage, 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;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// Play shooting sound
LK.getSound('towerShoot').play();
// --- Sci-fi charge and firing effect ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
// Add energy charge effect to core
if (self.energyCore) {
tween.stop(self.energyCore, {
scaleX: true,
scaleY: true,
alpha: true
});
tween(self.energyCore, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.5
}, {
duration: 120,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.energyCore, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
});
}
// Always use the original resting position for recoil, never accumulate offset
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset to resting position before animating (in case of interrupted tweens)
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Calculate recoil offset (recoil back along the gun's rotation)
var recoilDistance = 8;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
// Animate recoil back from the resting position
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original position/scale
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 1;
}
}
}
self.refreshCellsInRange();
};
self.destroy = function () {
if (self.isDestroyed) {
return;
}
self.isDestroyed = true;
// Stop all active tweens on this tower and its components
tween.stop(self);
tween.stop(gunContainer);
if (self.energyCore) {
tween.stop(self.energyCore);
}
// Clean up all support structures and their tweens
if (self.supportStructures) {
for (var i = 0; i < self.supportStructures.length; i++) {
if (self.supportStructures[i]) {
tween.stop(self.supportStructures[i]);
}
}
self.supportStructures = [];
}
// Clean up gun graphics
if (self.gunGraphics) {
tween.stop(self.gunGraphics);
self.gunGraphics = null;
}
// Clear cells in range references
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 = [];
// Clear grid cells occupied by this tower
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (cell) {
cell.type = 0;
}
}
}
// Remove from towers array
var towerIndex = towers.indexOf(self);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
// Remove from parent container
if (self.parent) {
self.parent.removeChild(self);
}
// Remove any associated range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
game.removeChild(game.children[i]);
}
}
// Close any upgrade menus for this tower
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu && child.tower === self;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
// Clear selected tower if it's this tower
if (selectedTower === self) {
selectedTower = null;
}
// Clean up ultimate ability
if (self.ultimate) {
self.ultimate.destroy();
self.ultimate = null;
}
// Nullify all object references
self.targetEnemy = null;
self.energyCore = null;
// Call parent destroy
Container.prototype.destroy.call(self);
};
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;
self.hasEnoughGold = 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;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (!cell || cell.type !== 0) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
self.canPlace = validGridPlacement && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var TowerSpecialization = Container.expand(function (tower, branchType) {
var self = Container.call(this);
self.tower = tower;
self.branchType = branchType || 'none';
self.linkedTowers = [];
self.isDestroyed = false;
// Add specialization visual indicator
var specIcon = self.attachAsset('specializationIcon', {
anchorX: 0.5,
anchorY: 0.5
});
specIcon.width = 30;
specIcon.height = 30;
specIcon.y = -self.tower.children[0].height / 2 - 20;
// Set specialization color based on branch
switch (self.branchType) {
case 'antiarmor':
specIcon.tint = 0xFF6600;
break;
case 'longrange':
specIcon.tint = 0x00AAFF;
break;
case 'nuclear':
specIcon.tint = 0xFF0000;
break;
case 'chainreaction':
specIcon.tint = 0xFFFF00;
break;
default:
specIcon.tint = 0xFFD700;
}
self.applySpecialization = function () {
switch (self.branchType) {
case 'antiarmor':
// Anti-armor: pierce shields, extra damage to immune enemies
self.tower.armorPiercing = true;
self.tower.damage = Math.floor(self.tower.damage * 1.5);
break;
case 'longrange':
// Long-range: significantly extended range
self.tower.rangeMultiplier = 2.0;
self.tower.refreshCellsInRange();
break;
case 'nuclear':
// Nuclear: huge splash damage, slower fire rate
self.tower.damage = Math.floor(self.tower.damage * 2.5);
self.tower.fireRate = Math.floor(self.tower.fireRate * 1.8);
self.tower.splashRadius = CELL_SIZE * 3;
break;
case 'chainreaction':
// Chain reaction: damage spreads between enemies
self.tower.chainReaction = true;
self.tower.chainRange = CELL_SIZE * 2;
self.tower.chainDamage = 0.7;
break;
}
// Update tower visuals to show specialization
if (self.tower.updateSpecializationVisuals) {
self.tower.updateSpecializationVisuals();
}
};
self.findNearbyTowers = function () {
var nearby = [];
var searchRadius = CELL_SIZE * 4;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower !== self.tower && tower.specialization) {
var dx = tower.x - self.tower.x;
var dy = tower.y - self.tower.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= searchRadius) {
nearby.push(tower);
}
}
}
return nearby;
};
self.createSupportLinks = function () {
var nearbyTowers = self.findNearbyTowers();
for (var i = 0; i < nearbyTowers.length; i++) {
var tower = nearbyTowers[i];
if (self.linkedTowers.indexOf(tower) === -1) {
self.linkedTowers.push(tower);
// Create visual link beam
var linkBeam = new Container();
linkBeam.x = self.tower.x;
linkBeam.y = self.tower.y;
var beamGraphics = linkBeam.attachAsset('linkBeam', {
anchorX: 0,
anchorY: 0.5
});
var dx = tower.x - self.tower.x;
var dy = tower.y - self.tower.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
beamGraphics.width = distance;
beamGraphics.rotation = angle;
beamGraphics.alpha = 0.5;
beamGraphics.tint = 0x00FF88;
game.addChild(linkBeam);
self.tower.linkBeam = linkBeam;
}
}
};
self.applySupportBonus = function () {
// Linked towers get damage and fire rate bonus
var bonusMultiplier = 1 + self.linkedTowers.length * 0.15;
self.tower.supportBonus = bonusMultiplier;
};
self.update = function () {
if (self.branchType !== 'none') {
self.createSupportLinks();
self.applySupportBonus();
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove link beams
if (self.tower.linkBeam && self.tower.linkBeam.parent) {
self.tower.linkBeam.parent.removeChild(self.tower.linkBeam);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var TowerUltimate = Container.expand(function (tower, ultimateType) {
var self = Container.call(this);
self.tower = tower;
self.ultimateType = ultimateType || 'none';
self.cooldownTime = 1800; // 30 seconds at 60fps
self.currentCooldown = 0;
self.isDestroyed = false;
// Add ultimate indicator
var ultimateIcon = self.attachAsset('ultimateIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
ultimateIcon.y = -self.tower.children[0].height / 2 - 35;
ultimateIcon.alpha = 0.3; // Dim when on cooldown
// Set ultimate-specific properties
switch (self.ultimateType) {
case 'orbital':
self.cooldownTime = 2400; // 40 seconds
ultimateIcon.tint = 0xFF0000;
break;
case 'nanite':
self.cooldownTime = 2100; // 35 seconds
ultimateIcon.tint = 0x00FF88;
break;
case 'temporal':
self.cooldownTime = 1800; // 30 seconds
ultimateIcon.tint = 0x9966FF;
break;
default:
ultimateIcon.tint = 0xFFD700;
}
self.canActivate = function () {
return self.currentCooldown <= 0 && !self.isDestroyed;
};
self.activate = function () {
if (!self.canActivate()) return false;
self.currentCooldown = self.cooldownTime;
// Visual cooldown effect
ultimateIcon.alpha = 0.3;
tween(ultimateIcon, {
alpha: 1.0
}, {
duration: self.cooldownTime,
easing: tween.linear
});
switch (self.ultimateType) {
case 'orbital':
return self.orbitalStrike();
case 'nanite':
return self.naniteSwarm();
case 'temporal':
return self.timeDilation();
}
return false;
};
self.orbitalStrike = function () {
// Find target area with most enemies
var bestTarget = null;
var maxEnemies = 0;
var strikeRadius = CELL_SIZE * 3;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var nearbyCount = 0;
for (var j = 0; j < enemies.length; j++) {
var otherEnemy = enemies[j];
var dx = otherEnemy.x - enemy.x;
var dy = otherEnemy.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= strikeRadius) {
nearbyCount++;
}
}
if (nearbyCount > maxEnemies) {
maxEnemies = nearbyCount;
bestTarget = enemy;
}
}
if (bestTarget) {
// Create orbital strike effect
var strikeEffect = new Container();
strikeEffect.x = bestTarget.x;
strikeEffect.y = bestTarget.y;
var strikeGraphics = strikeEffect.attachAsset('orbitalStrike', {
anchorX: 0.5,
anchorY: 0.5
});
strikeGraphics.alpha = 0.0;
strikeGraphics.scaleX = 0.1;
strikeGraphics.scaleY = 0.1;
strikeGraphics.blendMode = 1; // Additive
game.addChild(strikeEffect);
// Warning phase
tween(strikeGraphics, {
alpha: 0.7,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Strike phase - massive damage
var strikeDamage = self.tower.damage * 15;
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
var dx = enemy.x - bestTarget.x;
var dy = enemy.y - bestTarget.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= strikeRadius) {
enemy.health -= strikeDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Visual damage effect
tween(enemy, {
tint: 0xFFFFFF,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(enemy, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
}
// Final explosion effect
tween(strikeGraphics, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
if (strikeEffect.parent) {
strikeEffect.parent.removeChild(strikeEffect);
}
}
});
}
});
var notification = game.addChild(new Notification("🛰️ ORBITAL STRIKE INCOMING! 🛰️"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return true;
}
return false;
};
self.naniteSwarm = function () {
// Create nanite swarm that spreads poison and self-replicates
var swarmCenter = self.tower;
var swarmRadius = CELL_SIZE * 4;
var swarmDamage = self.tower.damage * 3;
// Create visual swarm effect
var swarmEffect = new Container();
swarmEffect.x = swarmCenter.x;
swarmEffect.y = swarmCenter.y;
var swarmGraphics = swarmEffect.attachAsset('naniteSwarm', {
anchorX: 0.5,
anchorY: 0.5
});
swarmGraphics.alpha = 0.8;
swarmGraphics.tint = 0x00FF88;
game.addChild(swarmEffect);
// Create multiple nanite particles
var particles = [];
for (var i = 0; i < 12; i++) {
var particle = swarmEffect.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
particle.width = 15;
particle.height = 15;
particle.tint = 0x44FFAA;
var angle = i / 12 * Math.PI * 2;
particle.x = Math.cos(angle) * 50;
particle.y = Math.sin(angle) * 50;
particles.push(particle);
}
// Animate swarm expansion
tween(swarmEffect, {
scaleX: 3.0,
scaleY: 3.0
}, {
duration: 2000,
easing: tween.easeOut
});
// Animate particles spiraling outward
for (var i = 0; i < particles.length; i++) {
var particle = particles[i];
tween(particle, {
x: particle.x * 3,
y: particle.y * 3,
rotation: Math.PI * 4
}, {
duration: 2000,
delay: i * 100,
easing: tween.easeOut
});
}
// Apply nanite effects over time
var swarmDuration = 180; // 3 seconds
var _swarmTick = function swarmTick() {
swarmDuration--;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - swarmCenter.x;
var dy = enemy.y - swarmCenter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= swarmRadius) {
// Apply nanite damage
enemy.health -= swarmDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Apply enhanced poison effect
enemy.poisoned = true;
enemy.poisonDamage = swarmDamage * 0.5;
enemy.poisonDuration = 300; // Extended duration
// Self-replication chance
if (Math.random() < 0.3 && swarmDuration > 60) {
// 30% chance to spread to nearby enemies
for (var j = 0; j < enemies.length; j++) {
var nearbyEnemy = enemies[j];
if (nearbyEnemy !== enemy) {
var nearbyDx = nearbyEnemy.x - enemy.x;
var nearbyDy = nearbyEnemy.y - enemy.y;
var nearbyDistance = Math.sqrt(nearbyDx * nearbyDx + nearbyDy * nearbyDy);
if (nearbyDistance <= CELL_SIZE * 2) {
nearbyEnemy.poisoned = true;
nearbyEnemy.poisonDamage = swarmDamage * 0.3;
nearbyEnemy.poisonDuration = 240;
break;
}
}
}
}
}
}
if (swarmDuration > 0) {
LK.setTimeout(_swarmTick, 100); // Continue every 100ms
} else {
// Clean up swarm effect
tween(swarmEffect, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
if (swarmEffect.parent) {
swarmEffect.parent.removeChild(swarmEffect);
}
}
});
}
};
LK.setTimeout(_swarmTick, 100); // Start swarm effects
var notification = game.addChild(new Notification("🤖 NANITE SWARM DEPLOYED! 🤖"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return true;
};
self.timeDilation = function () {
// Create temporal distortion field that slows time for enemies
var dilationCenter = self.tower;
var dilationRadius = CELL_SIZE * 5;
var dilationDuration = 600; // 10 seconds
// Create visual time dilation effect
var dilationEffect = new Container();
dilationEffect.x = dilationCenter.x;
dilationEffect.y = dilationCenter.y;
var dilationGraphics = dilationEffect.attachAsset('timeDilation', {
anchorX: 0.5,
anchorY: 0.5
});
dilationGraphics.alpha = 0.4;
dilationGraphics.tint = 0x9966FF;
dilationGraphics.blendMode = 1; // Additive
game.addChild(dilationEffect);
// Pulsing time distortion effect
tween(dilationGraphics, {
scaleX: 1.3,
scaleY: 1.3,
alpha: 0.7
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(dilationGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.4
}, {
duration: 1000,
easing: tween.easeInOut
});
}
});
// Apply time dilation effects
var affectedEnemies = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - dilationCenter.x;
var dy = enemy.y - dilationCenter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= dilationRadius) {
// Store original speed
if (!enemy.originalDilationSpeed) {
enemy.originalDilationSpeed = enemy.speed;
}
// Extreme slow effect (90% speed reduction)
enemy.speed = enemy.originalDilationSpeed * 0.1;
enemy.temporallyDilated = true;
enemy.dilationDuration = dilationDuration;
affectedEnemies.push(enemy);
// Visual distortion effect on enemy
tween(enemy, {
tint: 0x9966FF,
alpha: 0.8
}, {
duration: 300,
easing: tween.easeOut
});
}
}
// During dilation, towers in the field get boosted fire rate
var boostedTowers = [];
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - dilationCenter.x;
var dy = tower.y - dilationCenter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= dilationRadius && tower !== self.tower) {
tower.originalDilationFireRate = tower.fireRate;
tower.fireRate = Math.floor(tower.fireRate * 0.3); // 70% faster firing
tower.temporallyBoosted = true;
boostedTowers.push(tower);
}
}
// Clean up after duration
LK.setTimeout(function () {
// Restore enemy speeds
for (var i = 0; i < affectedEnemies.length; i++) {
var enemy = affectedEnemies[i];
if (enemy.parent && !enemy.isDestroyed) {
enemy.speed = enemy.originalDilationSpeed;
enemy.temporallyDilated = false;
// Restore enemy appearance
tween(enemy, {
alpha: 1.0
}, {
duration: 300,
easing: tween.easeIn
});
}
}
// Restore tower fire rates
for (var i = 0; i < boostedTowers.length; i++) {
var tower = boostedTowers[i];
if (tower.parent && !tower.isDestroyed) {
tower.fireRate = tower.originalDilationFireRate;
tower.temporallyBoosted = false;
}
}
// Remove visual effect
tween(dilationEffect, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 1000,
easing: tween.easeIn,
onFinish: function onFinish() {
if (dilationEffect.parent) {
dilationEffect.parent.removeChild(dilationEffect);
}
}
});
}, dilationDuration * 16.67); // Convert frames to milliseconds
var notification = game.addChild(new Notification("⏰ TIME DILATION ACTIVATED! ⏰"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return true;
};
self.update = function () {
if (self.currentCooldown > 0) {
self.currentCooldown--;
}
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
var TutorialSystem = Container.expand(function () {
var self = Container.call(this);
self.currentStep = 0;
self.isActive = false;
self.isWaitingForAction = false;
self.tutorialCompleted = storage.tutorialCompleted || false;
self.currentPhase = 'introduction'; // introduction, basics, combat, advanced, endgame
self.highlightOverlay = null;
self.actionArrow = null;
self.tutorialSteps = [
// INTRODUCTION PHASE
{
phase: 'introduction',
title: "Welcome to Space Tower Defense!",
text: "Commander, our space station is under attack!\nYou must defend against waves of alien invaders.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'introduction',
title: "Your Mission",
text: "Build defensive towers to stop enemies from reaching\nthe bottom of the screen and destroying our base.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'introduction',
title: "Resources Overview",
text: "Gold: Used to build and upgrade towers\nLives: Lost when enemies reach the bottom\nScore: Points earned for defeating enemies",
highlight: "ui",
action: null,
waitForAction: false
},
// BASICS PHASE
{
phase: 'basics',
title: "Building Your First Tower",
text: "Let's start by building a basic tower.\nDrag the 'Default' tower from the bottom panel.",
highlight: "sourceTowers",
action: "buildTower",
waitForAction: true
}, {
phase: 'basics',
title: "Tower Placement",
text: "Good! Towers need 2x2 space and cannot block enemy paths.\nThe green preview shows valid placement areas.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'basics',
title: "Tower Information",
text: "Tap on your tower to see its stats and upgrade options.\nTry tapping the tower you just built.",
highlight: "tower",
action: "selectTower",
waitForAction: true
}, {
phase: 'basics',
title: "Upgrading Towers",
text: "Excellent! Upgrading increases damage and fire rate.\nUpgrades get more expensive at higher levels.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'basics',
title: "Starting Combat",
text: "Now let's face some enemies!\nTap the 'Start Game' button to begin wave 1.",
highlight: "startButton",
action: "startGame",
waitForAction: true
},
// COMBAT PHASE
{
phase: 'combat',
title: "Enemy Types - Normal",
text: "The first enemies are normal type - balanced health and speed.\nYour towers will automatically target and fire at them.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Earning Gold",
text: "Great! You earn gold for each enemy defeated.\nUse this gold to build more towers and upgrades.",
highlight: "gold",
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Tower Specialization",
text: "Different tower types are effective against different enemies.\nLet's build a Rapid tower for faster firing.",
highlight: "sourceTowers",
action: "buildRapidTower",
waitForAction: true
}, {
phase: 'combat',
title: "Enemy Types - Fast",
text: "Wave 2 brings fast enemies (blue).\nRapid towers are excellent against quick targets.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Tower Range",
text: "Each tower has a different range.\nSniper towers have long range but fire slowly.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'combat',
title: "Build a Sniper Tower",
text: "Try building a Sniper tower (orange) for long-range support.\nPlace it where it can cover a large area.",
highlight: "sourceTowers",
action: "buildSniperTower",
waitForAction: true
},
// ADVANCED PHASE
{
phase: 'advanced',
title: "Special Enemy Types",
text: "As waves progress, you'll face special enemies:\n• Flying (yellow) - immune to some towers\n• Immune (red) - resistant to effects",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Splash Damage",
text: "Splash towers (green) deal area damage.\nThey're effective against groups of enemies.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Support Towers",
text: "Slow towers (purple) reduce enemy speed.\nPoison towers (cyan) deal damage over time.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Hero Units",
text: "Deploy hero units for additional firepower!\nHeroes can move around and have special abilities.",
highlight: "heroPanel",
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Deploy a Tank Hero",
text: "Try deploying a Tank hero (green).\nTap the tank button, then tap on the battlefield.",
highlight: "heroPanel",
action: "deployHero",
waitForAction: true
}, {
phase: 'advanced',
title: "Hero Abilities",
text: "Heroes gain experience and can use special abilities.\nTap on your hero to activate its ability when ready.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'advanced',
title: "Station Modules",
text: "Customize your space station with modules!\nTap the 'Customize' button to manage station upgrades.",
highlight: "stationPanel",
action: null,
waitForAction: false
},
// ENDGAME PHASE
{
phase: 'endgame',
title: "Boss Waves",
text: "Every 10th wave is a Boss wave with powerful enemies.\nBoss enemies have much more health but give more gold.",
highlight: "waveIndicator",
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Environmental Hazards",
text: "Watch for space hazards like asteroids, nebulas, and solar flares.\nThese can help or hinder your defense strategy.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Advanced Strategies",
text: "• Upgrade key towers to maximum level\n• Use hero abilities strategically\n• Adapt to enemy types each wave",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Tower Specializations",
text: "At level 3, some towers can specialize:\n• Sniper: Anti-Armor or Long-Range\n• Splash: Nuclear or Chain Reaction",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Path Branching",
text: "Enemies may take different routes through your defenses.\nSome paths are shorter but more defended.",
highlight: null,
action: null,
waitForAction: false
}, {
phase: 'endgame',
title: "Tutorial Complete!",
text: "You're ready to defend the station, Commander!\nSurvive all 50 waves to achieve victory.",
highlight: null,
action: null,
waitForAction: false
}];
var tutorialContainer = new Container();
self.addChild(tutorialContainer);
var overlay = tutorialContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
overlay.width = 1900;
overlay.height = 450;
overlay.tint = 0x0A0A0A;
overlay.alpha = 0.95;
// Phase indicator
var phaseText = new Text2("", {
size: 40,
fill: 0xFFD700,
weight: 600
});
phaseText.anchor.set(0.5, 0.5);
phaseText.y = -160;
tutorialContainer.addChild(phaseText);
var titleText = new Text2("", {
size: 65,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -100;
tutorialContainer.addChild(titleText);
var bodyText = new Text2("", {
size: 45,
fill: 0xE0E0E0,
weight: 400
});
bodyText.anchor.set(0.5, 0.5);
bodyText.y = 0;
tutorialContainer.addChild(bodyText);
// Progress indicator
var progressText = new Text2("", {
size: 35,
fill: 0xCCCCCC,
weight: 400
});
progressText.anchor.set(0.5, 0.5);
progressText.y = 80;
tutorialContainer.addChild(progressText);
var buttonContainer = new Container();
buttonContainer.y = 140;
tutorialContainer.addChild(buttonContainer);
var nextButton = new Container();
var nextBg = nextButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
nextBg.width = 200;
nextBg.height = 80;
nextBg.tint = 0x00AA00;
var nextText = new Text2("Next", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
nextText.anchor.set(0.5, 0.5);
nextButton.addChild(nextText);
nextButton.x = -250;
buttonContainer.addChild(nextButton);
var skipButton = new Container();
var skipBg = skipButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
skipBg.width = 200;
skipBg.height = 80;
skipBg.tint = 0xAA0000;
var skipText = new Text2("Skip", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
skipText.anchor.set(0.5, 0.5);
skipButton.addChild(skipText);
skipButton.x = 0;
buttonContainer.addChild(skipButton);
var prevButton = new Container();
var prevBg = prevButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
prevBg.width = 200;
prevBg.height = 80;
prevBg.tint = 0x0066CC;
var prevText = new Text2("Back", {
size: 55,
fill: 0xFFFFFF,
weight: 800
});
prevText.anchor.set(0.5, 0.5);
prevButton.addChild(prevText);
prevButton.x = 250;
buttonContainer.addChild(prevButton);
self.createHighlight = function (target) {
if (self.highlightOverlay) {
self.highlightOverlay.parent.removeChild(self.highlightOverlay);
}
self.highlightOverlay = new Container();
var highlight = self.highlightOverlay.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
highlight.width = 300;
highlight.height = 300;
highlight.tint = 0xFFD700;
highlight.alpha = 0.3;
// Pulsing animation
tween(highlight, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.6
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.highlightOverlay.isDestroyed) {
tween(highlight, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.3
}, {
duration: 1000,
easing: tween.easeInOut
});
}
}
});
game.addChild(self.highlightOverlay);
return self.highlightOverlay;
};
self.removeHighlight = function () {
if (self.highlightOverlay && self.highlightOverlay.parent) {
tween.stop(self.highlightOverlay);
self.highlightOverlay.parent.removeChild(self.highlightOverlay);
self.highlightOverlay = null;
}
};
self.showStep = function (stepIndex) {
if (stepIndex >= self.tutorialSteps.length) {
self.endTutorial();
return;
}
var step = self.tutorialSteps[stepIndex];
var phaseDisplay = step.phase.charAt(0).toUpperCase() + step.phase.slice(1) + " Phase";
phaseText.setText(phaseDisplay);
titleText.setText(step.title);
bodyText.setText(step.text);
progressText.setText("Step " + (stepIndex + 1) + " of " + self.tutorialSteps.length);
// Update button states
nextButton.visible = !step.waitForAction;
skipButton.visible = true;
prevButton.visible = stepIndex > 0;
if (stepIndex === self.tutorialSteps.length - 1) {
nextText.setText("Finish!");
skipText.setText("Start!");
}
// Handle highlights
self.removeHighlight();
if (step.highlight) {
switch (step.highlight) {
case "ui":
// Highlight UI area
if (self.highlightOverlay) {
self.highlightOverlay.x = 2048 / 2;
self.highlightOverlay.y = 100;
}
break;
case "sourceTowers":
// Highlight tower selection area
if (sourceTowers.length > 0) {
var highlight = self.createHighlight();
highlight.x = sourceTowers[0].x;
highlight.y = sourceTowers[0].y;
}
break;
case "gold":
// Highlight gold display
var highlight = self.createHighlight();
highlight.x = 2048 / 2 - 400;
highlight.y = 100;
break;
case "heroPanel":
// Highlight hero panel
var highlight = self.createHighlight();
highlight.x = 2048 / 2;
highlight.y = 200;
break;
case "stationPanel":
// Highlight station panel
var highlight = self.createHighlight();
highlight.x = 2048 - 200;
highlight.y = 250;
break;
case "waveIndicator":
// Highlight wave indicator
var highlight = self.createHighlight();
highlight.x = 2048 / 2;
highlight.y = 2732 - 80;
break;
case "startButton":
// Highlight start game button
if (waveIndicator && waveIndicator.waveMarkers.length > 0) {
var highlight = self.createHighlight();
highlight.x = waveIndicator.waveMarkers[0].x + waveIndicator.x;
highlight.y = waveIndicator.waveMarkers[0].y + waveIndicator.y;
}
break;
}
}
// Set up action waiting
if (step.waitForAction) {
self.isWaitingForAction = true;
self.expectedAction = step.action;
}
};
self.startTutorial = function () {
self.isActive = true;
self.visible = true;
self.currentStep = 0;
self.showStep(0);
// Position tutorial in center of screen
tutorialContainer.x = 2048 / 2;
tutorialContainer.y = 1400;
};
self.nextStep = function () {
if (self.isWaitingForAction) return;
self.currentStep++;
self.showStep(self.currentStep);
};
self.prevStep = function () {
if (self.currentStep > 0) {
self.currentStep--;
self.showStep(self.currentStep);
}
};
self.endTutorial = function () {
self.isActive = false;
self.visible = false;
self.removeHighlight();
storage.tutorialCompleted = true;
self.tutorialCompleted = true;
// Auto-start the game after tutorial
if (waveIndicator && !waveIndicator.gameStarted) {
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
}
var notification = game.addChild(new Notification("Tutorial completed! Good luck, Commander!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
};
self.checkActionCompleted = function (action) {
if (!self.isWaitingForAction || self.expectedAction !== action) return;
self.isWaitingForAction = false;
self.expectedAction = null;
// Show next button and auto-advance after delay
nextButton.visible = true;
LK.setTimeout(function () {
if (self.isActive && !self.isWaitingForAction) {
self.nextStep();
}
}, 1500);
};
nextButton.down = function () {
self.nextStep();
};
skipButton.down = function () {
self.endTutorial();
};
prevButton.down = function () {
self.prevStep();
};
self.visible = false;
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: 0xE0E0E0,
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.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('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 = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
tutorialSystem.checkActionCompleted('startGame');
}
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Play wave start sound
LK.getSound('waveStart').play();
}
};
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;
// 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 && enemyType !== 'swarm') {
// 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 = 3;
waveTypeShadow.y = 3;
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)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
var Wormhole = Container.expand(function (x, y, targetX, targetY) {
var self = Container.call(this);
self.x = x;
self.y = y;
self.targetX = targetX;
self.targetY = targetY;
self.radius = 90;
self.isDestroyed = false;
self.cooldown = 0;
var wormholeGraphics = self.attachAsset('wormhole', {
anchorX: 0.5,
anchorY: 0.5
});
wormholeGraphics.alpha = 0.7;
wormholeGraphics.blendMode = 1; // Additive blending
// Spinning animation
tween(wormholeGraphics, {
rotation: Math.PI * 2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed) {
wormholeGraphics.rotation = 0;
}
}
});
// Create target wormhole visual
self.targetWormhole = new Container();
self.targetWormhole.x = targetX;
self.targetWormhole.y = targetY;
var targetGraphics = self.targetWormhole.attachAsset('wormhole', {
anchorX: 0.5,
anchorY: 0.5
});
targetGraphics.alpha = 0.5;
targetGraphics.tint = 0xFF1493; // Different color for exit
targetGraphics.blendMode = 1;
game.addChild(self.targetWormhole);
// Sync rotation with main wormhole
tween(targetGraphics, {
rotation: -Math.PI * 2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (!self.isDestroyed) {
targetGraphics.rotation = 0;
}
}
});
self.update = function () {
if (self.cooldown > 0) {
self.cooldown--;
return;
}
// Check for enemies within range
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.isFlying) continue; // Flying enemies ignore wormholes
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.radius) {
// Teleport enemy
self.teleportEnemy(enemy);
self.cooldown = 180; // 3 second cooldown
break;
}
}
};
self.teleportEnemy = function (enemy) {
// Create teleport effect at source
var sourceEffect = new Container();
sourceEffect.x = self.x;
sourceEffect.y = self.y;
var sourceGraphics = sourceEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
sourceGraphics.tint = 0x9400D3;
sourceGraphics.alpha = 0.8;
game.addChild(sourceEffect);
// Create teleport effect at destination
var destEffect = new Container();
destEffect.x = self.targetX;
destEffect.y = self.targetY;
var destGraphics = destEffect.attachAsset('teleportEffect', {
anchorX: 0.5,
anchorY: 0.5
});
destGraphics.tint = 0xFF1493;
destGraphics.alpha = 0.8;
game.addChild(destEffect);
// Animate effects
tween(sourceEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (sourceEffect.parent) {
sourceEffect.parent.removeChild(sourceEffect);
}
}
});
tween(destEffect, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
if (destEffect.parent) {
destEffect.parent.removeChild(destEffect);
}
}
});
// Teleport enemy
enemy.x = self.targetX;
enemy.y = self.targetY;
enemy.currentCellX = Math.floor((self.targetX - grid.x) / CELL_SIZE);
enemy.currentCellY = Math.floor((self.targetY - grid.y) / CELL_SIZE);
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = null; // Reset pathfinding
// Flash the wormhole
tween(wormholeGraphics, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 1.0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(wormholeGraphics, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 0.7
}, {
duration: 300,
easing: tween.easeIn
});
}
});
};
self.destroy = function () {
if (self.isDestroyed) return;
self.isDestroyed = true;
// Remove target wormhole
if (self.targetWormhole && self.targetWormhole.parent) {
self.targetWormhole.parent.removeChild(self.targetWormhole);
}
var wormholeIndex = wormholes.indexOf(self);
if (wormholeIndex !== -1) {
wormholes.splice(wormholeIndex, 1);
}
tween.stop(self);
if (self.parent) {
self.parent.removeChild(self);
}
Container.prototype.destroy.call(self);
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000011
});
/****
* Game Code
****/
// Add background image
// Base tower structures for each type
// Tower gun/weapon parts - Level 1
// Tower gun/weapon parts - Level 2
// Tower gun/weapon parts - Level 3
// Tower gun/weapon parts - Level 4
// Tower gun/weapon parts - Level 5
// Tower gun/weapon parts - Level 6 (Max Level)
// Support structures and details
// Visual indicators
// Specialization visual elements
var backgroundImage = game.attachAsset('spaceBackground', {
anchorX: 0.5,
anchorY: 0.5
});
backgroundImage.x = 2048 / 2;
backgroundImage.y = 2732 / 2;
backgroundImage.alpha = 0.8;
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 = [];
// Object pooling for bullets
var bulletPool = [];
var maxBulletPoolSize = 100;
function getBulletFromPool(startX, startY, targetEnemy, damage, speed) {
var bullet;
if (bulletPool.length > 0) {
bullet = bulletPool.pop();
// Reset bullet properties
bullet.targetEnemy = targetEnemy;
bullet.damage = damage || 10;
bullet.speed = speed || 5;
bullet.x = startX;
bullet.y = startY;
bullet.isDestroyed = false;
bullet.visible = true;
bullet.alpha = 1;
if (bullet.children[0]) {
bullet.children[0].alpha = 1;
bullet.children[0].scaleX = 1;
bullet.children[0].scaleY = 1;
}
} else {
bullet = new Bullet(startX, startY, targetEnemy, damage, speed);
}
return bullet;
}
function returnBulletToPool(bullet) {
if (bulletPool.length < maxBulletPoolSize && bullet.parent) {
// Clean up target enemy reference properly
if (bullet.targetEnemy && bullet.targetEnemy.bulletsTargetingThis) {
var bulletIndex = bullet.targetEnemy.bulletsTargetingThis.indexOf(bullet);
if (bulletIndex !== -1) {
bullet.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
// Stop all tweens before pooling
tween.stop(bullet);
if (bullet.children && bullet.children[0]) {
tween.stop(bullet.children[0]);
}
bullet.parent.removeChild(bullet);
bullet.targetEnemy = null;
bullet.visible = false;
bulletPool.push(bullet);
} else {
bullet.destroy();
}
}
var defenses = [];
var selectedTower = null;
var gold = 60; // Reduced starting gold to make early choices more meaningful
var lives = 20;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
// Hero system variables
var heroes = [];
var maxHeroes = 3;
var heroDeploymentMode = false;
var heroPreview = null;
var selectedHeroType = 'tank';
// Station customization variables
var stationCustomizer = null;
var researchLabs = [];
var shieldGenerators = [];
var teleporterNodes = [];
// Environmental hazards variables
var asteroids = [];
var nebulaClouds = [];
var solarFlares = [];
var wormholes = [];
var hazardTimer = 0;
var nextHazardTime = 1800; // 30 seconds at 60fps
// Branching routes system variables
var pathJunctions = [];
var alternativePaths = [];
var routeChoiceInfluences = [];
var pathDifficulties = {};
// Tower specialization variables
var specializationMenus = [];
var towerLinks = [];
// Ultimate ability variables
var activeUltimates = [];
var ultimateEffects = [];
// Load station data from storage
var stationData = storage.stationData || {
modules: [],
researchPoints: 0,
completedTechs: []
};
// Initialize route visualizer
var routeVisualizer = new RouteVisualizer();
game.addChild(routeVisualizer);
// Initialize branching routes system
function initializeBranchingRoutes() {
// Create main junction points in the middle area
var mainJunction = new PathJunction(grid.x + 12 * CELL_SIZE, grid.y + 15 * CELL_SIZE, [{
targetX: 10,
targetY: 22,
// Left branch - shorter but more defended
isShortPath: true,
pathLength: 8,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 20
}, {
targetX: 14,
targetY: 22,
// Right branch - longer but less defended
isShortPath: false,
pathLength: 12,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 15
}]);
// Secondary junction for more complex routing
var secondaryJunction = new PathJunction(grid.x + 8 * CELL_SIZE, grid.y + 12 * CELL_SIZE, [{
targetX: 6,
targetY: 18,
// Far left route
isShortPath: false,
pathLength: 15,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 25
}, {
targetX: 12,
targetY: 15,
// Center route to main junction
isShortPath: true,
pathLength: 6,
towerDensity: 0,
currentEnemyCount: 0,
recentSwarmTraffic: 0,
hasAntiAir: false,
difficulty: 30
}]);
game.addChild(mainJunction);
game.addChild(secondaryJunction);
pathJunctions.push(mainJunction);
pathJunctions.push(secondaryJunction);
var notification = game.addChild(new Notification("🔀 Multi-path routing system activated! 🔀"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
// Initialize the branching system after a short delay
LK.setTimeout(function () {
initializeBranchingRoutes();
}, 2000);
function getModuleCost(moduleType, level) {
var baseCosts = {
research: 50,
shield: 75,
teleporter: 100
};
var baseCost = baseCosts[moduleType] || 50;
return Math.floor(baseCost * Math.pow(1.5, level - 1));
}
function getTechnology(techId) {
var technologies = {
'tower_damage': {
id: 'tower_damage',
name: 'Enhanced Weapons',
cost: 100,
onComplete: function onComplete() {
// Increase all tower damage by 20%
for (var i = 0; i < towers.length; i++) {
towers[i].damage = Math.floor(towers[i].damage * 1.2);
}
}
},
'tower_range': {
id: 'tower_range',
name: 'Extended Range',
cost: 150,
onComplete: function onComplete() {
// Increase all tower ranges
for (var i = 0; i < towers.length; i++) {
towers[i].refreshCellsInRange();
}
}
},
'shield_efficiency': {
id: 'shield_efficiency',
name: 'Shield Efficiency',
cost: 200,
onComplete: function onComplete() {
// Reduce shield energy costs
for (var i = 0; i < shieldGenerators.length; i++) {
shieldGenerators[i].maxShieldEnergy *= 1.5;
shieldGenerators[i].shieldEnergy = shieldGenerators[i].maxShieldEnergy;
}
}
}
};
return technologies[techId];
}
// Spatial partitioning for collision optimization
var spatialGrid = {
cellSize: CELL_SIZE * 2,
cells: {},
getKey: function getKey(x, y) {
var gridX = Math.floor(x / this.cellSize);
var gridY = Math.floor(y / this.cellSize);
return gridX + ',' + gridY;
},
addEnemy: function addEnemy(enemy) {
var key = this.getKey(enemy.x, enemy.y);
if (!this.cells[key]) this.cells[key] = [];
this.cells[key].push(enemy);
},
removeEnemy: function removeEnemy(enemy) {
var key = this.getKey(enemy.x, enemy.y);
if (this.cells[key]) {
var index = this.cells[key].indexOf(enemy);
if (index !== -1) {
this.cells[key].splice(index, 1);
}
}
},
getNearbyEnemies: function getNearbyEnemies(x, y, range) {
var nearby = [];
var checkRadius = Math.ceil(range / this.cellSize);
var centerX = Math.floor(x / this.cellSize);
var centerY = Math.floor(y / this.cellSize);
for (var dx = -checkRadius; dx <= checkRadius; dx++) {
for (var dy = -checkRadius; dy <= checkRadius; dy++) {
var key = centerX + dx + ',' + (centerY + dy);
if (this.cells[key]) {
nearby = nearby.concat(this.cells[key]);
}
}
}
return nearby;
},
clear: function clear() {
this.cells = {};
}
};
var enemiesToSpawn = 10; // Default number of enemies per wave
// Visual effects limiting
var activeEffects = [];
var maxActiveEffects = 20;
var effectSkipCounter = 0;
function canCreateEffect() {
// Count active effects and remove destroyed ones
activeEffects = activeEffects.filter(function (effect) {
return effect.parent && !effect.isDestroyed;
});
// Limit based on performance
if (activeEffects.length >= maxActiveEffects) {
effectSkipCounter++;
// Skip every other effect when at limit
return effectSkipCounter % 2 === 0;
}
return true;
}
function registerEffect(effect) {
if (activeEffects.length < maxActiveEffects) {
activeEffects.push(effect);
}
}
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.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(livesText);
LK.gui.top.addChild(scoreText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
}
function setGold(value) {
gold = value;
updateUI();
}
// Performance monitoring and adaptive logic
var performanceMonitor = {
frameTime: 16.67,
// Target 60 FPS
lastTime: 0,
avgFrameTime: 16.67,
frameCount: 0,
performanceLevel: 1.0,
// 1.0 = full performance, 0.5 = half performance
update: function update() {
var currentTime = Date.now();
if (this.lastTime > 0) {
var deltaTime = currentTime - this.lastTime;
this.avgFrameTime = this.avgFrameTime * 0.9 + deltaTime * 0.1;
// Adjust performance level based on frame time
if (this.avgFrameTime > 25) {
// Below 40 FPS
this.performanceLevel = Math.max(0.3, this.performanceLevel - 0.1);
} else if (this.avgFrameTime < 18) {
// Above 55 FPS
this.performanceLevel = Math.min(1.0, this.performanceLevel + 0.05);
}
}
this.lastTime = currentTime;
this.frameCount++;
},
shouldSkipUpdate: function shouldSkipUpdate() {
return Math.random() > this.performanceLevel;
}
};
// Memory monitoring system
var memoryMonitor = {
lastEnemyCount: 0,
lastBulletCount: 0,
lastTowerCount: 0,
lastEffectCount: 0,
update: function update() {
// Track object counts for memory leak detection
this.lastEnemyCount = enemies.length;
this.lastBulletCount = bullets.length;
this.lastTowerCount = towers.length;
this.lastEffectCount = activeEffects.length;
// Log warnings if counts are unexpectedly high
if (this.lastEnemyCount > 100) {
console.warn("High enemy count detected:", this.lastEnemyCount);
}
if (this.lastBulletCount > 200) {
console.warn("High bullet count detected:", this.lastBulletCount);
}
if (this.lastEffectCount > 50) {
console.warn("High effect count detected:", this.lastEffectCount);
}
},
forceCleanup: function forceCleanup() {
// Emergency cleanup when memory usage is too high
console.log("Performing emergency cleanup...");
// Clean up destroyed effects
activeEffects = activeEffects.filter(function (effect) {
return effect.parent && !effect.isDestroyed;
});
// Clean up orphaned bullets
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent || bullets[i].isDestroyed) {
bullets.splice(i, 1);
}
}
// Clean up bullet pool
bulletPool = bulletPool.filter(function (bullet) {
return !bullet.isDestroyed;
});
console.log("Cleanup complete. Objects remaining:", {
enemies: enemies.length,
bullets: bullets.length,
towers: towers.length,
effects: activeEffects.length,
pooledBullets: bulletPool.length
});
}
};
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();
debugLayer.addChild(grid);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
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 = 8; // Increased base cost from 5
switch (towerType) {
case 'rapid':
cost = 12; // Reduced from 15 - cheaper early game tower
break;
case 'sniper':
cost = 20; // Reduced from 25 for better accessibility
break;
case 'splash':
cost = 28; // Reduced from 35
break;
case 'slow':
cost = 32; // Reduced from 45 - utility tower should be accessible
break;
case 'poison':
cost = 38; // Reduced from 55 - was too expensive
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
grid.pathFind();
grid.renderDebug();
// Play tower placement sound
LK.getSound('towerPlace').play();
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
if (towerType === 'default') {
tutorialSystem.checkActionCompleted('buildTower');
} else if (towerType === 'rapid') {
tutorialSystem.checkActionCompleted('buildRapidTower');
} else if (towerType === 'sniper') {
tutorialSystem.checkActionCompleted('buildSniperTower');
}
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// Handle hero deployment
if (heroDeploymentMode) {
// Check if tap is in valid deployment area (not on towers or paths)
var deploymentValid = true;
var gridX = Math.floor((x - grid.x) / CELL_SIZE);
var gridY = Math.floor((y - grid.y) / CELL_SIZE);
// Check if position is within grid bounds and not on blocked areas
if (gridX < 2 || gridX >= 22 || gridY < 6 || gridY >= 25) {
deploymentValid = false;
} else {
var cell = grid.getCell(gridX, gridY);
if (!cell || cell.type !== 0) {
deploymentValid = false;
}
}
if (deploymentValid && heroes.length < maxHeroes) {
var hero = new Hero(selectedHeroType);
hero.x = x;
hero.y = y;
enemyLayerTop.addChild(hero); // Add heroes to top layer
heroes.push(hero);
updateHeroPanel();
heroDeploymentMode = false;
// Tutorial action tracking
if (tutorialSystem && tutorialSystem.isActive) {
tutorialSystem.checkActionCompleted('deployHero');
}
var notification = game.addChild(new Notification(selectedHeroType.charAt(0).toUpperCase() + selectedHeroType.slice(1) + " hero deployed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
} else {
var notification = game.addChild(new Notification("Cannot deploy hero here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
heroDeploymentMode = false;
return;
}
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Improved touch detection with larger hit areas for mobile
var touchPadding = 40; // Extra padding for easier touch
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
var hitArea = {
left: tower.x - tower.width / 2 - touchPadding,
right: tower.x + tower.width / 2 + touchPadding,
top: tower.y - tower.height / 2 - touchPadding,
bottom: tower.y + tower.height / 2 + touchPadding
};
if (x >= hitArea.left && x <= hitArea.right && y >= hitArea.top && y <= hitArea.bottom) {
// Check if player can afford this tower
if (gold >= getTowerCost(tower.towerType)) {
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
// Improved drag offset for better mobile experience
var dragOffsetY = CELL_SIZE * 2; // Larger offset to keep preview visible above finger
towerPreview.snapToGrid(x, y - dragOffsetY);
// Add immediate visual feedback with scaling animation
tween(tower, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(tower, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
}
});
} else {
// Visual feedback for unaffordable towers
tween(tower, {
scaleX: 0.95,
scaleY: 0.95,
alpha: 0.7
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(tower, {
scaleX: 1.0,
scaleY: 1.0,
alpha: 1.0
}, {
duration: 150,
easing: tween.easeIn
});
}
});
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
break;
}
}
};
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) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
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 towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
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;
// Initialize station customizer
stationCustomizer = new StationCustomizer();
stationCustomizer.x = 0;
stationCustomizer.y = 0;
game.addChild(stationCustomizer);
// Create station customization panel
var stationPanel = new Container();
var stationPanelBg = stationPanel.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
stationPanelBg.width = 400;
stationPanelBg.height = 100;
stationPanelBg.tint = 0x2E7D32;
stationPanelBg.alpha = 0.9;
var stationTitleText = new Text2("Station", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
stationTitleText.anchor.set(0.5, 0.5);
stationTitleText.y = -15;
stationPanel.addChild(stationTitleText);
var customizeButton = new Container();
var customizeBg = customizeButton.attachAsset('heroAbilityIcon', {
anchorX: 0.5,
anchorY: 0.5
});
customizeBg.width = 80;
customizeBg.height = 40;
customizeBg.tint = 0x4CAF50;
customizeButton.y = 20;
var customizeText = new Text2("Customize", {
size: 30,
fill: 0xFFFFFF,
weight: 600
});
customizeText.anchor.set(0.5, 0.5);
customizeButton.addChild(customizeText);
customizeButton.down = function () {
stationCustomizer.toggleCustomizationMode();
};
stationPanel.addChild(customizeButton);
stationPanel.x = 2048 - 200; // Position on right side
stationPanel.y = 200; // Below the score display which is at y=50
game.addChild(stationPanel);
// Create hero deployment panel
var heroPanel = new Container();
var heroPanelBg = heroPanel.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
heroPanelBg.width = 800;
heroPanelBg.height = 120;
heroPanelBg.tint = 0x1A237E;
heroPanelBg.alpha = 0.9;
var heroTitleText = new Text2("Heroes (" + heroes.length + "/" + maxHeroes + ")", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
heroTitleText.anchor.set(0.5, 0.5);
heroTitleText.y = -30;
heroPanel.addChild(heroTitleText);
// Hero type buttons
var heroTypes = ['tank', 'support', 'dps'];
var heroTypeButtons = [];
for (var i = 0; i < heroTypes.length; i++) {
var heroButton = new Container();
var buttonBg = heroButton.attachAsset('heroAbilityIcon', {
anchorX: 0.5,
anchorY: 0.5
});
switch (heroTypes[i]) {
case 'tank':
buttonBg.tint = 0x4CAF50;
break;
case 'support':
buttonBg.tint = 0x2196F3;
break;
case 'dps':
buttonBg.tint = 0xFF5722;
break;
}
buttonBg.width = 80;
buttonBg.height = 80;
heroButton.heroType = heroTypes[i];
heroButton.x = -240 + i * 120;
heroButton.y = 10;
heroButton.down = function () {
if (heroes.length >= maxHeroes) {
var notification = game.addChild(new Notification("Maximum heroes deployed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
selectedHeroType = this.heroType;
heroDeploymentMode = true;
var notification = game.addChild(new Notification("Tap anywhere to deploy " + this.heroType + " hero"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
};
heroPanel.addChild(heroButton);
heroTypeButtons.push(heroButton);
}
heroPanel.x = 2048 / 2;
heroPanel.y = 120;
game.addChild(heroPanel);
function updateHeroPanel() {
heroTitleText.setText("Heroes (" + heroes.length + "/" + maxHeroes + ")");
}
enemiesToSpawn = 10;
// Initialize tutorial system for new players
var tutorialSystem = new TutorialSystem();
game.addChild(tutorialSystem);
// Start background music
LK.playMusic('bgMusic');
// Show tutorial automatically for new players
var hasSeenTutorial = storage.tutorialCompleted || false;
if (!hasSeenTutorial) {
tutorialSystem.startTutorial();
}
// Add tutorial restart button for experienced players
var tutorialRestartButton = new Container();
var restartBg = tutorialRestartButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
restartBg.width = 200;
restartBg.height = 60;
restartBg.tint = 0x4A90E2;
restartBg.alpha = 0.9;
var restartText = new Text2("Tutorial", {
size: 35,
fill: 0xFFFFFF,
weight: 600
});
restartText.anchor.set(0.5, 0.5);
tutorialRestartButton.addChild(restartText);
tutorialRestartButton.x = 150;
tutorialRestartButton.y = 2732 - 50;
tutorialRestartButton.down = function () {
if (!tutorialSystem.isActive) {
tutorialSystem.startTutorial();
}
};
game.addChild(tutorialRestartButton);
game.update = function () {
performanceMonitor.update();
// Clear and rebuild spatial grid each frame for enemies
spatialGrid.clear();
for (var i = 0; i < enemies.length; i++) {
spatialGrid.addEnemy(enemies[i]);
}
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
var isBossWave = currentWave % 10 === 0 && currentWave > 0;
if (isBossWave && waveType !== 'swarm') {
// 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! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
// Play boss spawn sound
LK.getSound('bossSpawn').play();
}
// 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 more balanced scaling that starts linear and becomes logarithmic
var healthMultiplier;
if (currentWave <= 10) {
// Linear scaling for early waves
healthMultiplier = 1 + (currentWave - 1) * 0.15; // 15% per wave for first 10 waves
} else if (currentWave <= 25) {
// Moderate scaling for mid waves
healthMultiplier = 2.35 + (currentWave - 10) * 0.08; // Start at 2.35x, add 8% per wave
} else {
// Logarithmic scaling for late waves to prevent exponential growth
healthMultiplier = 3.55 + Math.log(currentWave - 24) * 0.5; // Logarithmic growth
}
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;
}
}
// Update heroes
for (var i = heroes.length - 1; i >= 0; i--) {
var hero = heroes[i];
if (hero.health <= 0) {
// Hero defeated - remove from play but don't reset progression
var notification = game.addChild(new Notification(hero.heroType.charAt(0).toUpperCase() + hero.heroType.slice(1) + " hero defeated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
hero.destroy();
heroes.splice(i, 1);
updateHeroPanel();
continue;
}
hero.update();
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
// Skip some enemy updates when performance is poor
if (performanceMonitor.shouldSkipUpdate() && enemy.health > 0) {
continue;
}
// Handle hero targeting for enemies
if (enemy.heroTarget && enemy.heroTarget.parent && enemy.heroTarget.health > 0) {
if (enemy.heroTargetDuration > 0) {
enemy.heroTargetDuration--;
// Move towards hero instead of following path
var dx = enemy.heroTarget.x - enemy.x;
var dy = enemy.heroTarget.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > CELL_SIZE * 0.5) {
var angle = Math.atan2(dy, dx);
enemy.x += Math.cos(angle) * enemy.speed * 30;
enemy.y += Math.sin(angle) * enemy.speed * 30;
} else {
// Attack hero
enemy.heroTarget.health -= 10;
if (enemy.heroTarget.health <= 0) {
enemy.heroTarget.health = 0;
}
}
continue; // Skip normal pathfinding
} else {
enemy.heroTarget = null;
enemy.heroTargetDuration = 0;
}
} else {
enemy.heroTarget = null;
enemy.heroTargetDuration = 0;
}
if (enemy.health <= 0) {
// Improved gold rewards that scale better with difficulty
var goldEarned;
if (enemy.isBoss) {
// Boss gold scales more reasonably: 25 base + 3 per wave
goldEarned = Math.floor(25 + (enemy.waveNumber - 1) * 3);
} else {
// Regular enemy gold: better scaling for later waves
if (enemy.waveNumber <= 10) {
goldEarned = Math.floor(2 + (enemy.waveNumber - 1) * 0.3); // 2-4.7 gold for waves 1-10
} else if (enemy.waveNumber <= 25) {
goldEarned = Math.floor(4.7 + (enemy.waveNumber - 10) * 0.4); // 4.7-10.7 gold for waves 11-25
} else {
goldEarned = Math.floor(10.7 + (enemy.waveNumber - 25) * 0.2); // Slower growth for late waves
}
}
// Bonus gold for enemies who took difficult routes
if (enemy.chosenBranch && enemy.chosenBranch.difficulty > 50) {
var routeBonus = Math.floor(goldEarned * 0.3);
goldEarned += routeBonus;
if (routeBonus > 0) {
var notification = game.addChild(new Notification("Route bonus: +" + routeBonus + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
}
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;
// 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();
// Use proper destroy method for comprehensive cleanup
enemy.destroy();
continue;
}
if (grid.updateEnemy(enemy)) {
// Check if shields are active
var shieldsActive = false;
for (var s = 0; s < stationCustomizer.modules.length; s++) {
var module = stationCustomizer.modules[s];
if (module.moduleType === 'shield' && module.shieldActive) {
shieldsActive = true;
break;
}
}
if (shieldsActive) {
// Shields absorb damage
var notification = game.addChild(new Notification("Enemy blocked by shields!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
lives = Math.max(0, lives - 1);
updateUI();
if (lives <= 0) {
// Play game over sound
LK.getSound('gameOver').play();
LK.showGameOver();
}
}
// Use proper destroy method for comprehensive cleanup
enemy.destroy();
}
}
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);
}
}
// Limit bullet count when performance drops
if (performanceMonitor.performanceLevel < 0.7 && bullets.length > 50) {
// Remove oldest bullets when performance is poor
var bulletsToRemove = Math.min(10, bullets.length - 30);
for (var i = 0; i < bulletsToRemove; i++) {
var oldBullet = bullets[i];
if (oldBullet.targetEnemy) {
var bulletIndex = oldBullet.targetEnemy.bulletsTargetingThis.indexOf(oldBullet);
if (bulletIndex !== -1) {
oldBullet.targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
returnBulletToPool(oldBullet);
bullets.splice(i, 1);
i--; // Adjust index after removal
bulletsToRemove--;
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Update station customizer
if (stationCustomizer) {
stationCustomizer.update();
// Update module arrays for quick access
researchLabs = stationCustomizer.modules.filter(function (m) {
return m.moduleType === 'research';
});
shieldGenerators = stationCustomizer.modules.filter(function (m) {
return m.moduleType === 'shield';
});
teleporterNodes = stationCustomizer.modules.filter(function (m) {
return m.moduleType === 'teleporter';
});
// Link teleporter nodes
for (var i = 0; i < teleporterNodes.length; i++) {
teleporterNodes[i].linkedNodes = teleporterNodes.filter(function (node, index) {
return index !== i;
});
}
}
// Update tower specializations and support links
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.specialization && tower.specialization.update) {
tower.specialization.update();
}
}
// Clean up destroyed specialization menus
specializationMenus = specializationMenus.filter(function (menu) {
return menu.parent && !menu.isDestroyed;
});
// Environmental hazards management
if (waveIndicator && waveIndicator.gameStarted) {
hazardTimer++;
if (hazardTimer >= nextHazardTime) {
hazardTimer = 0;
nextHazardTime = 1200 + Math.random() * 1200; // 20-40 seconds
// Spawn random environmental hazard
var hazardType = Math.floor(Math.random() * 4);
switch (hazardType) {
case 0:
// Asteroid field
if (asteroids.length < 3) {
var validPositions = [];
for (var x = 2; x < 22; x++) {
for (var y = 6; y < 25; y++) {
var cell = grid.getCell(x, y);
if (cell && cell.type === 0) {
validPositions.push({
x: x,
y: y
});
}
}
}
if (validPositions.length > 0) {
var pos = validPositions[Math.floor(Math.random() * validPositions.length)];
var asteroid = new Asteroid(grid.x + pos.x * CELL_SIZE + CELL_SIZE / 2, grid.y + pos.y * CELL_SIZE + CELL_SIZE / 2);
game.addChild(asteroid);
asteroids.push(asteroid);
var notification = game.addChild(new Notification("⚠️ Asteroid field detected! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
break;
case 1:
// Nebula cloud
if (nebulaClouds.length < 2) {
var cloudX = grid.x + (5 + Math.random() * 14) * CELL_SIZE;
var cloudY = grid.y + (8 + Math.random() * 12) * CELL_SIZE;
var nebula = new NebulaCloud(cloudX, cloudY);
game.addChild(nebula);
nebulaClouds.push(nebula);
var notification = game.addChild(new Notification("🌌 Nebula cloud formation! 🌌"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Remove after 15 seconds
LK.setTimeout(function () {
if (nebula.parent) {
nebula.destroy();
}
}, 15000);
}
break;
case 2:
// Solar flare
if (solarFlares.length === 0) {
var flare = new SolarFlare();
game.addChild(flare);
solarFlares.push(flare);
}
break;
case 3:
// Wormhole
if (wormholes.length < 2) {
// Create entry and exit points
var entryX = grid.x + (3 + Math.random() * 6) * CELL_SIZE;
var entryY = grid.y + (8 + Math.random() * 8) * CELL_SIZE;
var exitX = grid.x + (15 + Math.random() * 6) * CELL_SIZE;
var exitY = grid.y + (16 + Math.random() * 6) * CELL_SIZE;
var wormhole = new Wormhole(entryX, entryY, exitX, exitY);
game.addChild(wormhole);
wormholes.push(wormhole);
var notification = game.addChild(new Notification("🌀 Wormhole opened! 🌀"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Remove after 20 seconds
LK.setTimeout(function () {
if (wormhole.parent) {
wormhole.destroy();
}
}, 20000);
}
break;
}
}
}
// Update path junctions and route analysis
for (var i = 0; i < pathJunctions.length; i++) {
if (pathJunctions[i].parent) {
pathJunctions[i].update();
}
}
// Update environmental hazards
for (var i = asteroids.length - 1; i >= 0; i--) {
if (asteroids[i].parent) {
// Asteroids can be damaged by bullets
for (var j = 0; j < bullets.length; j++) {
var bullet = bullets[j];
var dx = bullet.x - asteroids[i].x;
var dy = bullet.y - asteroids[i].y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 60) {
// Hit asteroid
asteroids[i].takeDamage(bullet.damage);
returnBulletToPool(bullet);
break;
}
}
}
}
for (var i = 0; i < nebulaClouds.length; i++) {
if (nebulaClouds[i].parent) {
nebulaClouds[i].update();
}
}
for (var i = 0; i < solarFlares.length; i++) {
if (solarFlares[i].parent) {
solarFlares[i].update();
}
}
for (var i = 0; i < wormholes.length; i++) {
if (wormholes[i].parent) {
wormholes[i].update();
}
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
// Play victory sound
LK.getSound('victory').play();
LK.showYouWin();
}
};
White circle with two eyes, seen from above.. In-Game asset. 2d. High contrast. No shadows
White simple circular enemy seen from above, black outline. Black eyes, with a single shield in-font of it. Black and white only. Blue background.
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
wormwhole for TD game towdown. In-Game asset. 2d. High contrast. No shadows
tank hero character space galaxy theme tower defense game. In-Game asset. 2d. High contrast. No shadows
support hero character space galaxy theme tower defense game. In-Game asset. 2d. High contrast. No shadows
poison tower space galaxy theme tower defense game, advanced base level tower. In-Game asset. 2d. High contrast. No shadows