User prompt
right sided tank enemy should stand at the upper quarter of the screen to support convoy movement
User prompt
Enemy tank should stand at the upper quarter of the screen to support convoy movement
User prompt
Restrict tank enemies to upper quarter of map for convoy support
User prompt
right tank enemy should be stand in the upper half of the map to support enemy convoy movement
User prompt
Reduce immune convoy enemy number to 1 for wave 11
User prompt
Reduce immune convoy enemy number to 2 for wave 5
User prompt
reduce immune convoy enemy number to 3.
User prompt
you loaded the second fast enemy line too late. fix it.
User prompt
change the fast enemies loading in horizontal lines!
User prompt
you idiiot. i didn't ask you to change the fast enemies loading way from lines to this, but count them from licounter by one by one.
User prompt
you counted fast enemies just 2 lines but Each individual element counts as one and not counted together in rows or columns.
User prompt
Each intruder that reaches the bottom of the screen takes away 10 percent of the player's life.
User prompt
increase Lives counter from 18 too 100 %
User prompt
Increase immune enemy speed much more after completing first turn using tween animation ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
increase immune enemy speed after first turn ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Change tank enemy spawn lane right side of road to next to the right edge of the road.
User prompt
Reduce tank enemy number to 1 in wave 4 and load it from the right side of the road insted of left side.
User prompt
you left too much time - distance for delay between immune 2 enemies
User prompt
you left too much time for delay between Fast 1 enemies and Immune 2 enemies loading
User prompt
ensure enemy tank can movig on the road and also on the ground too
User prompt
ensure enemy tank movig in the middle of the road to avoid crashing
User prompt
You should repair Immune and Tank movement because they are crash into each other. They should moving well in convoy.
User prompt
Repair it and avoid overlaps with other enemies.
User prompt
You add too less coins for destroyed enemies
User prompt
LOAD ENEMY TANKS FROM MIDDLE-LEFT AND MIDDLE RIGHT SIDE OF THE TRACK TO AVOID COLLISIONS WITH OTHER ENEMIES
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Handle enemy bullets targeting towers
if (self.isEnemyBullet) {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Damage the tower instead of an enemy
var tower = self.targetEnemy;
if (tower.takeDamage) {
tower.takeDamage(self.damage);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
return;
}
// Original enemy targeting logic
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Special handling for missiles hitting immune, flying, or tank enemies - destroy instantly
if (self.type === 'slow' && (self.targetEnemy.isImmune || self.targetEnemy.isFlying || self.targetEnemy.type === 'tank')) {
// Instantly destroy immune, flying, or tank enemy regardless of health
self.targetEnemy.health = 0;
} else if (self.type === 'tank' && (self.targetEnemy.isImmune || self.targetEnemy.isFlying || self.targetEnemy.type === 'tank' || self.targetEnemy.isBoss)) {
// Tank weapons instantly destroy immune, flying, tank, and boss enemies
self.targetEnemy.health = 0;
} else {
// Apply damage to target enemy normally
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
}
// Create explosion effect for missile hits
if (self.type === 'slow') {
// Store explosion position for area damage calculation
var explosionX = self.targetEnemy.x;
var explosionY = self.targetEnemy.y;
// Increased explosion radius to destroy nearby enemies (especially fast enemies)
var explosionRadius = CELL_SIZE * 2.5; // Increased from smaller radius to 2.5 cells
// Apply area damage to all enemies within explosion radius
for (var i = 0; i < enemies.length; i++) {
var nearbyEnemy = enemies[i];
if (nearbyEnemy !== self.targetEnemy) {
var dx = nearbyEnemy.x - explosionX;
var dy = nearbyEnemy.y - explosionY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= explosionRadius) {
// Instantly destroy fast enemies within explosion radius
if (nearbyEnemy.type === 'fast') {
nearbyEnemy.health = 0;
} else {
// Apply significant damage to other enemy types
nearbyEnemy.health -= self.damage * 1.5; // 1.5x damage for area effect
if (nearbyEnemy.health <= 0) {
nearbyEnemy.health = 0;
} else {
nearbyEnemy.healthBar.width = nearbyEnemy.health / nearbyEnemy.maxHealth * 70;
}
}
// Create individual explosion effect for each affected enemy
var nearbyExplosion = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'splash');
nearbyExplosion.children[0].tint = 0xFF6600; // Orange-red for area damage
nearbyExplosion.children[0].width = nearbyExplosion.children[0].height = CELL_SIZE * 1.5;
nearbyExplosion.alpha = 0.8;
nearbyExplosion.scaleX = 0.2;
nearbyExplosion.scaleY = 0.2;
game.addChild(nearbyExplosion);
// Animate nearby explosion
tween(nearbyExplosion, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.6
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(nearbyExplosion, {
scaleX: 1.8,
scaleY: 1.8,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
nearbyExplosion.destroy();
}
});
}
});
}
}
}
// Create MASSIVE tank weapon explosion with devastating area damage
if (self.type === 'tank') {
// Store explosion position for area damage calculation
var explosionX = self.targetEnemy.x;
var explosionY = self.targetEnemy.y;
// MASSIVE explosion radius - much larger than missiles for devastating effect
var explosionRadius = CELL_SIZE * 4.0; // 4 cell radius for tank weapons (60% larger than missiles)
// Apply devastating area damage to all enemies within explosion radius
for (var i = 0; i < enemies.length; i++) {
var nearbyEnemy = enemies[i];
if (nearbyEnemy !== self.targetEnemy) {
var dx = nearbyEnemy.x - explosionX;
var dy = nearbyEnemy.y - explosionY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= explosionRadius) {
// Tank weapons cause MASSIVE damage - instantly destroy most enemies
if (nearbyEnemy.type === 'fast' || nearbyEnemy.type === 'flying' || nearbyEnemy.isFlying) {
// Instantly destroy fast and flying enemies
nearbyEnemy.health = 0;
} else if (nearbyEnemy.type === 'immune' || nearbyEnemy.isImmune) {
// Tank weapons instantly destroy immune enemies
nearbyEnemy.health = 0;
} else if (nearbyEnemy.type === 'tank') {
// Tank weapons instantly destroy tank enemies
nearbyEnemy.health = 0;
} else if (nearbyEnemy.isBoss) {
// Tank weapons instantly destroy boss enemies
nearbyEnemy.health = 0;
} else {
// Massive damage to other enemy types
nearbyEnemy.health -= self.damage * 3.0; // 3x damage for devastating effect
if (nearbyEnemy.health <= 0) {
nearbyEnemy.health = 0;
} else {
nearbyEnemy.healthBar.width = nearbyEnemy.health / nearbyEnemy.maxHealth * 70;
}
}
// Create individual massive explosion effect for each affected enemy
var nearbyExplosion = new EffectIndicator(nearbyEnemy.x, nearbyEnemy.y, 'splash');
nearbyExplosion.children[0].tint = 0x8B4513; // Dark brown for tank explosion
nearbyExplosion.children[0].width = nearbyExplosion.children[0].height = CELL_SIZE * 2.0; // Larger than missile explosions
nearbyExplosion.alpha = 1.0;
nearbyExplosion.scaleX = 0.3;
nearbyExplosion.scaleY = 0.3;
game.addChild(nearbyExplosion);
// Animate massive nearby explosion
tween(nearbyExplosion, {
scaleX: 1.8,
scaleY: 1.8,
alpha: 0.8
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(nearbyExplosion, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
nearbyExplosion.destroy();
}
});
}
});
}
}
}
// Create ULTRA-MASSIVE tank weapon explosion with enhanced visual effects
// Stage 1: Initial massive impact flash - GIGANTIC white flash
var impactFlash = new EffectIndicator(explosionX, explosionY, 'splash');
impactFlash.children[0].tint = 0xFFFFFF; // Bright white impact flash
impactFlash.children[0].width = impactFlash.children[0].height = CELL_SIZE * 6.0; // ENORMOUS initial flash
impactFlash.alpha = 1;
impactFlash.scaleX = 0.1;
impactFlash.scaleY = 0.1;
game.addChild(impactFlash);
// Ultra-fast bright flash expansion
tween(impactFlash, {
scaleX: 8.0,
// Much bigger than missile explosions
scaleY: 8.0,
alpha: 0
}, {
duration: 100,
easing: tween.easeOut
});
// Stage 2: MASSIVE main tank explosion fireball - dark brown/orange core
var mainExplosion = new EffectIndicator(explosionX, explosionY, 'splash');
mainExplosion.children[0].tint = 0x8B4513; // Dark brown for tank explosion
mainExplosion.children[0].width = mainExplosion.children[0].height = CELL_SIZE * 10.0; // ENORMOUS main explosion
mainExplosion.alpha = 1;
mainExplosion.scaleX = 0.2;
mainExplosion.scaleY = 0.2;
game.addChild(mainExplosion);
// Dramatic MASSIVE fireball expansion
tween(mainExplosion, {
scaleX: 6.5,
// Bigger than missile explosions
scaleY: 6.5,
alpha: 0.95
}, {
duration: 300,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(mainExplosion, {
scaleX: 10.0,
// ENORMOUS final scale
scaleY: 10.0,
alpha: 0
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
mainExplosion.destroy();
}
});
}
});
// Stage 3: Create QUADRUPLE the explosion particles for MASSIVE debris effect
for (var particleIndex = 0; particleIndex < 32; particleIndex++) {
// More particles than missiles
var explosionParticle = new EffectIndicator(explosionX, explosionY, 'splash');
// Tank explosion colors - browns and oranges
var particleColors = [0x8B4513, 0xA0522D, 0xD2691E, 0xFF8C00, 0xCD853F, 0xDEB887, 0x654321, 0x8B7355];
explosionParticle.children[0].tint = particleColors[particleIndex % particleColors.length];
explosionParticle.children[0].width = explosionParticle.children[0].height = CELL_SIZE * (2.2 + Math.random() * 1.8); // Bigger particles
explosionParticle.alpha = 0.95;
explosionParticle.scaleX = 0.6 + Math.random() * 0.5;
explosionParticle.scaleY = 0.6 + Math.random() * 0.5;
// Random direction for particle scatter with ULTRA-LONG distance
var particleAngle = particleIndex / 32 * Math.PI * 2 + (Math.random() - 0.5) * 0.8;
var particleDistance = CELL_SIZE * (4.5 + Math.random() * 3.5); // Ultra-far particle scatter
var targetX = explosionX + Math.cos(particleAngle) * particleDistance;
var targetY = explosionY + Math.sin(particleAngle) * particleDistance;
game.addChild(explosionParticle);
// Animate particles flying outward
tween(explosionParticle, {
x: targetX,
y: targetY,
scaleX: 4.0 + Math.random() * 1.5,
// Much bigger final scale
scaleY: 4.0 + Math.random() * 1.5,
alpha: 0.8
}, {
duration: 400 + Math.random() * 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(explosionParticle, {
scaleX: 0.3,
scaleY: 0.3,
alpha: 0
}, {
duration: 500 + Math.random() * 250,
easing: tween.easeIn,
onFinish: function onFinish() {
explosionParticle.destroy();
}
});
}
});
}
// Stage 4: QUINTUPLE shockwave rings for tank weapons
var primaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
primaryShockwave.children[0].tint = 0xDEB887; // Light brown shockwave
primaryShockwave.children[0].alpha = 0.8;
primaryShockwave.children[0].width = primaryShockwave.children[0].height = CELL_SIZE * 2.0;
primaryShockwave.alpha = 1.0;
game.addChild(primaryShockwave);
// MASSIVE expanding primary shockwave
tween(primaryShockwave, {
scaleX: 15.0,
// Bigger than missile shockwaves
scaleY: 15.0,
alpha: 0
}, {
duration: 900,
easing: tween.easeOut,
onFinish: function onFinish() {
primaryShockwave.destroy();
}
});
// Multiple additional shockwaves for devastating effect
for (var shockIndex = 1; shockIndex <= 4; shockIndex++) {
var additionalShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
additionalShockwave.children[0].tint = 0x8B7355; // Darker brown shockwave
additionalShockwave.children[0].alpha = 0.6 - shockIndex * 0.1;
additionalShockwave.children[0].width = additionalShockwave.children[0].height = CELL_SIZE * (2.0 - shockIndex * 0.2);
additionalShockwave.alpha = 0.8 - shockIndex * 0.1;
game.addChild(additionalShockwave);
// Delayed massive shockwaves
tween(additionalShockwave, {
scaleX: 16.0 + shockIndex * 1.0,
scaleY: 16.0 + shockIndex * 1.0,
alpha: 0
}, {
duration: 1000 + shockIndex * 100,
delay: shockIndex * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
additionalShockwave.destroy();
}
});
}
// Stage 5: MASSIVE lingering smoke cloud
var smokeCloud = new EffectIndicator(explosionX, explosionY, 'splash');
smokeCloud.children[0].tint = 0x696969; // Dark gray smoke
smokeCloud.children[0].alpha = 0.7;
smokeCloud.children[0].width = smokeCloud.children[0].height = CELL_SIZE * 8.0; // Bigger smoke cloud
smokeCloud.alpha = 0;
smokeCloud.scaleX = 1.2;
smokeCloud.scaleY = 1.2;
game.addChild(smokeCloud);
// Delayed massive smoke appearance
tween(smokeCloud, {
alpha: 1.0,
scaleX: 8.0,
scaleY: 8.0
}, {
duration: 500,
delay: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
// Smoke slowly dissipates over very long time
tween(smokeCloud, {
alpha: 0,
scaleX: 14.0,
// ENORMOUS final scale
scaleY: 14.0
}, {
duration: 2000,
// Very long dissipation time
easing: tween.easeIn,
onFinish: function onFinish() {
smokeCloud.destroy();
}
});
}
});
}
// Create ULTRA-MASSIVE spectacular multi-stage missile explosion with ENHANCED particle effects
// Stage 1: Initial impact flash - ULTRA bright white flash (GIGANTIC)
var impactFlash = new EffectIndicator(explosionX, explosionY, 'splash');
impactFlash.children[0].tint = 0xFFFFFF; // Bright white impact flash
impactFlash.children[0].width = impactFlash.children[0].height = CELL_SIZE * 5.0; // GIGANTIC initial flash (increased from 3.5)
impactFlash.alpha = 1;
impactFlash.scaleX = 0.1;
impactFlash.scaleY = 0.1;
game.addChild(impactFlash);
// Ultra-fast bright flash expansion (GIGANTIC SCALE)
tween(impactFlash, {
scaleX: 7.0,
// MUCH bigger scale (increased from 4.5)
scaleY: 7.0,
alpha: 0
}, {
duration: 120,
easing: tween.easeOut
});
// Stage 2: ULTRA-MASSIVE main explosion fireball - orange/red core (ENORMOUS)
var mainExplosion = new EffectIndicator(explosionX, explosionY, 'splash');
mainExplosion.children[0].tint = 0xFF4500; // Orange-red fireball
mainExplosion.children[0].width = mainExplosion.children[0].height = CELL_SIZE * 8.0; // ENORMOUS main explosion (increased from 6.0)
mainExplosion.alpha = 1;
mainExplosion.scaleX = 0.2;
mainExplosion.scaleY = 0.2;
game.addChild(mainExplosion);
// Dramatic ULTRA-MASSIVE fireball expansion with bounce effect
tween(mainExplosion, {
scaleX: 5.5,
// Much bigger scale (increased from 3.8)
scaleY: 5.5,
alpha: 0.95
}, {
duration: 250,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(mainExplosion, {
scaleX: 8.5,
// ENORMOUS final scale (increased from 6.0)
scaleY: 8.5,
alpha: 0
}, {
duration: 700,
easing: tween.easeIn,
onFinish: function onFinish() {
mainExplosion.destroy();
}
});
}
});
// Stage 3: Create TRIPLE the explosion particles for ULTRA-MASSIVE debris effect (TRIPLED)
for (var particleIndex = 0; particleIndex < 24; particleIndex++) {
// Increased from 16 to 24
var explosionParticle = new EffectIndicator(explosionX, explosionY, 'splash');
// Vary particle colors for realistic explosion with MORE variety
var particleColors = [0xFF6600, 0xFF4500, 0xFF8800, 0xFFAA00, 0xFF2200, 0xFFCC00, 0xFF3300, 0xFFDD00];
explosionParticle.children[0].tint = particleColors[particleIndex % particleColors.length];
explosionParticle.children[0].width = explosionParticle.children[0].height = CELL_SIZE * (1.8 + Math.random() * 1.5); // MUCH bigger particles (increased from 1.2 + 1.0)
explosionParticle.alpha = 0.9;
explosionParticle.scaleX = 0.5 + Math.random() * 0.4;
explosionParticle.scaleY = 0.5 + Math.random() * 0.4;
// Random direction for particle scatter with ULTRA-LONG distance
var particleAngle = particleIndex / 24 * Math.PI * 2 + (Math.random() - 0.5) * 0.8;
var particleDistance = CELL_SIZE * (3.5 + Math.random() * 3.0); // ULTRA-FAR particle scatter (increased from 2.5 + 2.0)
var targetX = explosionX + Math.cos(particleAngle) * particleDistance;
var targetY = explosionY + Math.sin(particleAngle) * particleDistance;
game.addChild(explosionParticle);
// Animate particles flying outward with ULTRA-enhanced physics
tween(explosionParticle, {
x: targetX,
y: targetY,
scaleX: 3.5 + Math.random() * 1.2,
// Much bigger final scale (increased from 2.5 + 0.8)
scaleY: 3.5 + Math.random() * 1.2,
alpha: 0.7
}, {
duration: 350 + Math.random() * 250,
// Longer duration
easing: tween.easeOut,
onFinish: function onFinish() {
tween(explosionParticle, {
scaleX: 0.2,
scaleY: 0.2,
alpha: 0
}, {
duration: 450 + Math.random() * 200,
// Longer fade duration
easing: tween.easeIn,
onFinish: function onFinish() {
explosionParticle.destroy();
}
});
}
});
}
// Stage 4: ULTRA-MASSIVE enhanced shockwave with QUADRUPLE rings
var primaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
primaryShockwave.children[0].tint = 0xFFDDDD; // Light pink shockwave
primaryShockwave.children[0].alpha = 0.7;
primaryShockwave.children[0].width = primaryShockwave.children[0].height = CELL_SIZE * 1.5; // Bigger base
primaryShockwave.alpha = 0.9;
game.addChild(primaryShockwave);
// ULTRA-HUGE expanding primary shockwave
tween(primaryShockwave, {
scaleX: 12.0,
// ENORMOUS scale (increased from 8.5)
scaleY: 12.0,
alpha: 0
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
primaryShockwave.destroy();
}
});
// Secondary ULTRA-MASSIVE shockwave
var secondaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
secondaryShockwave.children[0].tint = 0xFFFFFF; // White secondary wave
secondaryShockwave.children[0].alpha = 0.5;
secondaryShockwave.children[0].width = secondaryShockwave.children[0].height = CELL_SIZE * 1.0;
secondaryShockwave.alpha = 0.8;
game.addChild(secondaryShockwave);
// Delayed ULTRA-MASSIVE secondary shockwave
tween(secondaryShockwave, {
scaleX: 13.0,
// ENORMOUS scale (increased from 9.0)
scaleY: 13.0,
alpha: 0
}, {
duration: 900,
delay: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
secondaryShockwave.destroy();
}
});
// Third shockwave for QUADRUPLE-ring ULTRA-MASSIVE effect
var tertiaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
tertiaryShockwave.children[0].tint = 0xFFAAAA; // Light red tertiary wave
tertiaryShockwave.children[0].alpha = 0.3;
tertiaryShockwave.children[0].width = tertiaryShockwave.children[0].height = CELL_SIZE * 0.8;
tertiaryShockwave.alpha = 0.7;
game.addChild(tertiaryShockwave);
// Third delayed ULTRA-MASSIVE shockwave
tween(tertiaryShockwave, {
scaleX: 14.0,
// ENORMOUS scale (increased from 10.0)
scaleY: 14.0,
alpha: 0
}, {
duration: 1000,
delay: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tertiaryShockwave.destroy();
}
});
// Fourth shockwave for QUADRUPLE-ring effect (NEW!)
var quaternaryShockwave = new EffectIndicator(explosionX, explosionY, 'splash');
quaternaryShockwave.children[0].tint = 0xFFCCCC; // Very light pink quaternary wave
quaternaryShockwave.children[0].alpha = 0.2;
quaternaryShockwave.children[0].width = quaternaryShockwave.children[0].height = CELL_SIZE * 0.6;
quaternaryShockwave.alpha = 0.6;
game.addChild(quaternaryShockwave);
// Fourth delayed ULTRA-MASSIVE shockwave
tween(quaternaryShockwave, {
scaleX: 15.0,
// ULTRA-ENORMOUS scale
scaleY: 15.0,
alpha: 0
}, {
duration: 1100,
delay: 450,
easing: tween.easeOut,
onFinish: function onFinish() {
quaternaryShockwave.destroy();
}
});
// Stage 5: ULTRA-MASSIVE smoke cloud effect - ENORMOUS and lingers much longer
var smokeCloud = new EffectIndicator(explosionX, explosionY, 'splash');
smokeCloud.children[0].tint = 0x666666; // Dark gray smoke
smokeCloud.children[0].alpha = 0.6;
smokeCloud.children[0].width = smokeCloud.children[0].height = CELL_SIZE * 6.0; // ENORMOUS smoke cloud (increased from 4.0)
smokeCloud.alpha = 0;
smokeCloud.scaleX = 1.0;
smokeCloud.scaleY = 1.0;
game.addChild(smokeCloud);
// Delayed ULTRA-MASSIVE smoke appearance that grows slowly and lingers
tween(smokeCloud, {
alpha: 0.9,
scaleX: 6.0,
// Bigger initial expansion (increased from 4.0)
scaleY: 6.0
}, {
duration: 450,
delay: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
// Smoke slowly dissipates over ULTRA-LONG time
tween(smokeCloud, {
alpha: 0,
scaleX: 10.0,
// ENORMOUS final scale (increased from 7.0)
scaleY: 10.0
}, {
duration: 1500,
// ULTRA-LONG dissipation time (increased from 1200)
easing: tween.easeIn,
onFinish: function onFinish() {
smokeCloud.destroy();
}
});
}
});
}
// Apply special effects based on bullet type
if (self.type === 'flamethrower') {
// Create visual flame effect with enhanced animation
var flameEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'flamethrower');
game.addChild(flameEffect);
// Apply burning effect to target enemy
if (!self.targetEnemy.isImmune) {
self.targetEnemy.burning = true;
self.targetEnemy.burnDamage = self.damage * 0.3; // 30% of original damage per tick
self.targetEnemy.burnDuration = 180; // 3 seconds at 60 FPS
}
// Flame damage to enemies in a cone behind the target
var coneRange = CELL_SIZE * 1.2;
var targetAngle = Math.atan2(self.targetEnemy.y - self.y, self.targetEnemy.x - self.x);
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var flameDx = otherEnemy.x - self.targetEnemy.x;
var flameDy = otherEnemy.y - self.targetEnemy.y;
var flameDistance = Math.sqrt(flameDx * flameDx + flameDy * flameDy);
var enemyAngle = Math.atan2(flameDy, flameDx);
var angleDiff = Math.abs(enemyAngle - targetAngle);
// Normalize angle difference
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
angleDiff = Math.abs(angleDiff);
// Check if enemy is within cone (45 degrees = PI/4 radians)
if (flameDistance <= coneRange && angleDiff <= Math.PI / 4) {
// Apply flame damage (40% of original damage)
otherEnemy.health -= self.damage * 0.4;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
// Apply burning effect
if (!otherEnemy.isImmune) {
otherEnemy.burning = true;
otherEnemy.burnDamage = self.damage * 0.2;
otherEnemy.burnDuration = 120; // 2 seconds for cone targets
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply slow effect
// Make slow percentage scale with tower level (default 50%, up to 80% at max level)
var slowPct = 0.5;
if (self.sourceTowerLevel !== undefined) {
// Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
self.targetEnemy.slowDuration = 180; // Reset duration
}
}
} else if (self.type === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
// Apply spread angle for machine gun bullets
if (self.type === 'trap' && self.spreadAngle !== undefined) {
angle += self.spreadAngle;
}
// Rotate missile asset to face target direction
if (self.type === 'slow' && self.children.length > 0) {
var missileGraphics = self.children[0];
if (missileGraphics.targetRotation === undefined) {
missileGraphics.targetRotation = angle;
missileGraphics.rotation = angle;
} else {
// Only rotate if angle difference is significant
if (Math.abs(angle - missileGraphics.targetRotation) > 0.1) {
tween.stop(missileGraphics, {
rotation: true
});
// Calculate shortest rotation path
var currentRotation = missileGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
missileGraphics.targetRotation = angle;
tween(missileGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 100,
easing: tween.easeOut
});
}
}
}
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
// DebugCell class removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'flamethrower':
effectGraphics.tint = 0xFF4500;
effectGraphics.width = CELL_SIZE * 1.5;
effectGraphics.height = CELL_SIZE * 0.8; // Elongated flame effect
break;
}
effectGraphics.alpha = 0.7;
self.alpha = 0;
// Animate the effect
tween(self, {
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
// Base enemy class for common functionality
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'normal';
self.speed = .02;
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.collisionRadius = 25; // Base collision detection radius
self.lastCollisionCheck = 0; // Performance optimization
// Adjust collision radius based on enemy type
switch (self.type) {
case 'tank':
self.collisionRadius = 40; // Larger radius for tank enemies
break;
case 'fast':
self.collisionRadius = 20; // Smaller radius for fast enemies
break;
case 'flying':
self.collisionRadius = 30; // Medium radius for flying enemies
break;
default:
self.collisionRadius = 25; // Default radius
break;
}
// Boss enemies have larger collision radius
if (self.isBoss) {
self.collisionRadius *= 1.5;
}
// 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 *= 1; // Normal speed (half of previous 2x)
self.maxHealth = 1;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 200;
break;
case 'flying':
self.isFlying = true;
self.speed *= 2; // 2x speed for flying enemies
self.maxHealth = 80;
break;
case 'tank':
self.speed *= 0.5; // Half speed for tank enemies
self.maxHealth = 300; // Very high health for tank enemies
break;
default:
// Default enemy uses fast values
self.speed *= 1; // Normal speed (half of previous 2x)
self.maxHealth = 1;
break;
}
if (currentWave === totalWaves && 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.isBoss) {
assetId = 'Boss';
} else if (self.type === 'tank') {
assetId = 'Tank';
} else if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Fall back to regular enemy asset if specific type asset not found
// Apply tint to differentiate enemy types
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
break;
case 'immune':
enemyGraphics.tint = 0xAA0000; // Red for immune enemies
break;
case 'flying':
enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
break;
case 'swarm':
enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
shadowGraphics.scaleX = 1.8;
shadowGraphics.scaleY = 1.8;
}
// Position shadow slightly offset
self.shadow.x = 20; // Offset right
self.shadow.y = 20; // Offset down
// Ensure shadow has the same rotation as the enemy
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed, poisoned, or burned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
self.burning = false;
self.burnEffect = 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
}
}
}
// Handle burning effect from flamethrower
if (self.burning) {
// Visual indication of burning status
if (!self.burnEffect) {
self.burnEffect = true;
}
// Apply burn damage every 20 frames (3 times per second)
if (LK.ticks % 20 === 0) {
self.health -= self.burnDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.burnDuration--;
if (self.burnDuration <= 0) {
self.burning = false;
self.burnEffect = false;
// Only reset tint if not slowed or poisoned
if (!self.slowed && !self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Enemy overlap avoidance - check every frame for immune and tank enemies for better convoy formation
if (self.type === 'immune' || self.type === 'tank' || LK.ticks % 3 === 0) {
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self && otherEnemy.parent) {
var dx = otherEnemy.x - self.x;
var dy = otherEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var minDistance = (self.collisionRadius + otherEnemy.collisionRadius) * 1.4; // Increased buffer for better separation
if (distance < minDistance && distance > 0) {
// Much stronger separation force for immune and tank enemies
var overlap = minDistance - distance;
var separationForce;
if (self.type === 'immune' || self.type === 'tank' || otherEnemy.type === 'immune' || otherEnemy.type === 'tank') {
// Extra strong separation for immune/tank enemies to maintain convoy formation
separationForce = Math.pow(overlap / minDistance, 1.5) * 4.0; // Stronger force with smoother scaling
} else {
separationForce = Math.pow(overlap / minDistance, 2) * 2.0; // Original force for other enemies
}
var separationX = dx / distance * separationForce;
var separationY = dy / distance * separationForce;
// Apply separation with stronger horizontal bias to maintain lanes
self.currentCellX -= separationX;
self.currentCellY -= separationY * 0.7; // Reduced Y separation to maintain forward movement
// Apply weaker counter-force to maintain formation
otherEnemy.currentCellX += separationX * 0.3;
otherEnemy.currentCellY += separationY * 0.3;
// Keep enemies within reasonable bounds
self.currentCellX = Math.max(2, Math.min(22, self.currentCellX));
self.currentCellY = Math.max(-20, Math.min(33, self.currentCellY));
otherEnemy.currentCellX = Math.max(2, Math.min(22, otherEnemy.currentCellX));
otherEnemy.currentCellY = Math.max(-20, Math.min(33, otherEnemy.currentCellY));
}
}
}
}
// Fast enemy shooting capability
if (self.type === 'fast') {
// Initialize shooting properties if not already set
if (self.shootingCooldown === undefined) {
self.shootingCooldown = 0;
self.shootingRange = 12 * CELL_SIZE; // 12 cell shooting range (3x increase)
self.shootingDamage = 5;
self.bulletSpeed = 3;
}
// Decrease shooting cooldown
if (self.shootingCooldown > 0) {
self.shootingCooldown--;
}
// Try to find and shoot at towers within range
if (self.shootingCooldown <= 0 && self.currentCellY >= 4) {
var targetTower = null;
var closestDistance = self.shootingRange;
// Find closest tower within shooting range
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Skip landmine towers - enemies cannot see them
if (tower.id === 'landmine') {
continue;
}
// Allow targeting of all other tower types including trap/shooter towers
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.shootingRange && distance < closestDistance) {
closestDistance = distance;
targetTower = tower;
}
}
// Shoot at the target tower if found
if (targetTower) {
var bulletX = self.x;
var bulletY = self.y;
var enemyBullet = new Bullet(bulletX, bulletY, targetTower, self.shootingDamage, self.bulletSpeed);
// Customize enemy bullet appearance
if (enemyBullet.children[0]) {
enemyBullet.children[0].tint = 0xFF0000; // Red color for enemy bullets
enemyBullet.children[0].width = 20;
enemyBullet.children[0].height = 20;
}
// Mark as enemy bullet to handle differently
enemyBullet.isEnemyBullet = true;
game.addChild(enemyBullet);
bullets.push(enemyBullet);
// Set cooldown (shoot every 1.5 seconds at 60 FPS)
self.shootingCooldown = 90;
}
}
}
// Immune enemy shooting capability
if (self.type === 'immune') {
// Initialize shooting properties if not already set
if (self.shootingCooldown === undefined) {
self.shootingCooldown = 0;
self.shootingRange = 8 * CELL_SIZE; // 8 cell shooting range for immune enemies
self.shootingDamage = 8;
self.bulletSpeed = 4;
}
// Decrease shooting cooldown
if (self.shootingCooldown > 0) {
self.shootingCooldown--;
}
// Try to find and shoot at towers within range
if (self.shootingCooldown <= 0 && self.currentCellY >= 4) {
var targetTower = null;
var closestDistance = self.shootingRange;
// Find closest tower within shooting range
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Skip landmine towers - enemies cannot see them
if (tower.id === 'landmine') {
continue;
}
// Allow targeting of all other tower types including trap/shooter towers
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.shootingRange && distance < closestDistance) {
closestDistance = distance;
targetTower = tower;
}
}
// Shoot at the target tower if found
if (targetTower) {
var bulletX = self.x;
var bulletY = self.y;
var enemyBullet = new Bullet(bulletX, bulletY, targetTower, self.shootingDamage, self.bulletSpeed);
// Customize immune enemy bullet appearance
if (enemyBullet.children[0]) {
enemyBullet.children[0].tint = 0xAA0000; // Dark red color for immune enemy bullets
enemyBullet.children[0].width = 25;
enemyBullet.children[0].height = 25;
}
// Mark as enemy bullet to handle differently
enemyBullet.isEnemyBullet = true;
game.addChild(enemyBullet);
bullets.push(enemyBullet);
// Set cooldown (shoot every 1 second at 60 FPS)
self.shootingCooldown = 60;
}
}
}
// Immune enemy trap collision detection
if (self.type === 'immune') {
// Initialize trap contact tracking if not set
if (self.trapContactMap === undefined) {
self.trapContactMap = {};
}
// Check collision with trap towers every 10 frames for performance
if (LK.ticks % 10 === 0) {
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.id === 'trap') {
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if immune enemy is touching the trap (within 80 pixels)
if (distance <= 80) {
// Only reduce health if this trap hasn't been touched before
var trapKey = tower.x + ',' + tower.y; // Unique identifier for this trap
if (!self.trapContactMap[trapKey]) {
self.trapContactMap[trapKey] = true;
// Reduce immune enemy's health by half
self.health = Math.floor(self.health / 2);
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
// Create visual effect for trap contact
var trapEffect = new EffectIndicator(self.x, self.y, 'splash');
trapEffect.children[0].tint = 0xAAAAAA; // Gray color for trap effect
trapEffect.children[0].width = trapEffect.children[0].height = CELL_SIZE * 1.2;
game.addChild(trapEffect);
}
// Break after first trap collision to avoid multiple hits per frame
break;
}
}
}
}
}
// Tank enemy shooting capability
if (self.type === 'tank') {
// Initialize shooting properties if not already set
if (self.shootingCooldown === undefined) {
self.shootingCooldown = 0;
self.shootingRange = 6 * CELL_SIZE; // 6 cell shooting range for tank enemies
self.shootingDamage = 12;
self.bulletSpeed = 2;
}
// Decrease shooting cooldown
if (self.shootingCooldown > 0) {
self.shootingCooldown--;
}
// Try to find and shoot at towers within range
if (self.shootingCooldown <= 0 && self.currentCellY >= 4) {
var targetTower = null;
var closestDistance = self.shootingRange;
// Find closest tower within shooting range
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
// Skip landmine towers - enemies cannot see them
if (tower.id === 'landmine') {
continue;
}
// Allow targeting of all other tower types including trap/shooter towers
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.shootingRange && distance < closestDistance) {
closestDistance = distance;
targetTower = tower;
}
}
// Shoot at the target tower if found
if (targetTower) {
var bulletX = self.x;
var bulletY = self.y;
var enemyBullet = new Bullet(bulletX, bulletY, targetTower, self.shootingDamage, self.bulletSpeed);
// Customize tank enemy bullet appearance
if (enemyBullet.children[0]) {
enemyBullet.children[0].tint = 0x888888; // Gray color for tank enemy bullets
enemyBullet.children[0].width = 30;
enemyBullet.children[0].height = 30;
}
// Mark as enemy bullet to handle differently
enemyBullet.isEnemyBullet = true;
game.addChild(enemyBullet);
bullets.push(enemyBullet);
// Set cooldown (shoot every 2 seconds at 60 FPS)
self.shootingCooldown = 120;
}
}
}
// Convoy formation for immune and tank enemies - maintain proper spacing and formation
if ((self.type === 'immune' || self.type === 'tank') && self.currentCellY >= 4) {
// Find nearby same-type enemies to form convoy with
var nearbyConvoyEnemies = [];
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self && otherEnemy.parent && otherEnemy.type === self.type) {
var dx = otherEnemy.x - self.x;
var dy = otherEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 4) {
// Within 4 cells for convoy formation
nearbyConvoyEnemies.push({
enemy: otherEnemy,
distance: distance,
dx: dx,
dy: dy
});
}
}
}
// Apply convoy formation forces
if (nearbyConvoyEnemies.length > 0) {
for (var j = 0; j < nearbyConvoyEnemies.length; j++) {
var convoyData = nearbyConvoyEnemies[j];
var convoyEnemy = convoyData.enemy;
var distance = convoyData.distance;
var dx = convoyData.dx;
var dy = convoyData.dy;
var idealDistance = CELL_SIZE * 2.8; // Ideal convoy spacing
if (distance < idealDistance) {
// Too close - apply separation
var separationForce = (idealDistance - distance) / idealDistance * 0.8;
var separationX = dx / distance * separationForce;
var separationY = dy / distance * separationForce * 0.4; // Less Y separation
self.currentCellX -= separationX;
self.currentCellY -= separationY;
} else if (distance > idealDistance * 1.5) {
// Too far - apply attraction to maintain convoy
var attractionForce = (distance - idealDistance) / distance * 0.3;
var attractionX = dx / distance * attractionForce;
var attractionY = dy / distance * attractionForce * 0.2;
self.currentCellX += attractionX;
self.currentCellY += attractionY;
}
}
}
// Keep enemies within valid bounds
self.currentCellX = Math.max(3, Math.min(21, self.currentCellX));
self.currentCellY = Math.max(-20, Math.min(33, self.currentCellY));
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed && self.burning) {
// Combine all three effects
enemyGraphics.tint = 0x8B4513; // Brown-ish mix
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned && self.burning) {
// Combine poison and burn
enemyGraphics.tint = 0x8B7355; // Brownish-green mix
} else if (self.slowed && self.burning) {
// Combine slow and burn
enemyGraphics.tint = 0xCC4500; // Orange-red mix
} else if (self.burning) {
enemyGraphics.tint = 0xFF4500; // Orange for burning
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox) + Math.PI / 2;
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.25) {
// Further increased threshold to reduce tween frequency
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: 150,
// Reduced duration for faster rotation
easing: tween.easeOut
});
}
}
}
}
// Keep health bars positioned below level indicators
var healthBarY = CELL_SIZE * 0.7 + 25;
if (self.towerHealthBarOutline) {
self.towerHealthBarOutline.y = healthBarY;
self.towerHealthBarBG.y = healthBarY;
self.towerHealthBar.y = healthBarY;
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.spawns = [];
self.goals = [];
for (var i = 0; i < gridWidth; i++) {
self.cells[i] = [];
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j] = {
score: 0,
pathId: 0,
towersInRange: []
};
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
*/
// Create simplified straight road path waypoints with 3-cell width
var pathWaypoints = [{
x: 12,
y: 0
},
// Start point (spawn)
{
x: 12,
y: 8
},
// First horizontal road
{
x: 2,
y: 8
},
// Turn down
{
x: 2,
y: 15
},
// Second horizontal road
{
x: 22,
y: 15
},
// Turn down
{
x: 22,
y: 22
},
// Third horizontal road
{
x: 12,
y: 22
},
// Final segment to goal
{
x: 12,
y: gridHeight - 1
} // End point (goal)
];
// Create path cells array to mark which cells are part of the road
var pathCells = [];
// Generate path between waypoints
for (var w = 0; w < pathWaypoints.length - 1; w++) {
var start = pathWaypoints[w];
var end = pathWaypoints[w + 1];
// Create straight line between waypoints
var steps = Math.max(Math.abs(end.x - start.x), Math.abs(end.y - start.y));
for (var s = 0; s <= steps; s++) {
var t = steps === 0 ? 0 : s / steps;
var pathX = Math.round(start.x + (end.x - start.x) * t);
var pathY = Math.round(start.y + (end.y - start.y) * t);
// Add path width - make road 7 cells wide horizontally, 3 cells wide vertically
for (var offsetX = -3; offsetX <= 3; offsetX++) {
for (var offsetY = -1; offsetY <= 1; offsetY++) {
var cellX = pathX + offsetX;
var cellY = pathY + offsetY;
if (cellX >= 0 && cellX < gridWidth && cellY >= 0 && cellY < gridHeight) {
var key = cellX + ',' + cellY;
if (pathCells.indexOf(key) === -1) {
pathCells.push(key);
}
}
}
}
}
}
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellKey = i + ',' + j;
var isOnPath = pathCells.indexOf(cellKey) !== -1;
// Default to wall, then carve out path and other areas
var cellType = 1; // Wall by default
// Set path cells
if (isOnPath) {
cellType = 0; // Path
}
// Set spawn points at the start of the path
if (i >= 11 && i <= 13 && j === 0) {
cellType = 2; // Spawn
self.spawns.push(cell);
}
// Set goal points at the end of the path
if (i >= 11 && i <= 13 && j === gridHeight - 1) {
cellType = 3; // Goal
self.goals.push(cell);
}
// Keep borders as walls
if (i === 0 || i === gridWidth - 1 || j === 0 || j === gridHeight - 1) {
if (cellType !== 2 && cellType !== 3) {
// Don't override spawn/goal
cellType = 1; // Wall
}
}
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 = [];
// Render road asset for path cells
if (isOnPath) {
var roadVisual = self.attachAsset('road', {
anchorX: 0.5,
anchorY: 0.6
});
roadVisual.x = i * CELL_SIZE + CELL_SIZE / 2;
roadVisual.y = j * CELL_SIZE + CELL_SIZE / 2;
}
// Debug cells removed for performance
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
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 () {
// Debug cells removed for performance
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && 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) {
// Consistent movement speed for all enemy types during initial phase
var initialSpeed = enemy.speed;
if (enemy.type === 'tank') {
initialSpeed = 0.03; // 3x faster than normal tank speed for smoother entry
}
// Move directly downward
enemy.currentCellY += initialSpeed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees) + 90 degrees right
var angle = Math.PI / 2 + Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position (reduce calculation frequency)
if (LK.ticks % 2 === 0) {
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) {
// Assign flying enemies to one of 3 vertical lanes if not already assigned
if (enemy.flyingLane === undefined) {
// Determine lane based on spawn position: left (9-10), center (11-12), right (13-14)
if (enemy.cellX <= 10) {
enemy.flyingLane = 0; // Left lane - x position 6
enemy.targetX = 6;
} else if (enemy.cellX <= 12) {
enemy.flyingLane = 1; // Center lane - x position 12
enemy.targetX = 12;
} else {
enemy.flyingLane = 2; // Right lane - x position 18
enemy.targetX = 18;
}
}
// Move straight down in the assigned lane
enemy.currentCellY += enemy.speed;
// Gradually move to the target X position (lane center)
var xDiff = enemy.targetX - enemy.currentCellX;
if (Math.abs(xDiff) > 0.1) {
enemy.currentCellX += xDiff * 0.05; // Gradual movement to lane center
}
// Rotate enemy graphic to face downward
var angle = Math.PI; // Face downward (180 degrees)
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Update visual position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Check if enemy reached the bottom (goal)
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.type == 3) {
return true;
}
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (enemy.type === 'fast') {
// Fast enemies move straight down without pathfinding or turning
enemy.currentCellY += enemy.speed;
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Set rotation to face straight down without animation
if (enemy.children[0]) {
enemy.children[0].rotation = Math.PI; // Face downward (180 degrees)
enemy.children[0].targetRotation = Math.PI;
}
} else if (enemy.type === 'tank') {
// Tank enemies can move on both road and ground - use direct path to goal
// Move directly toward the goal area (bottom center of map)
var goalX = 12; // Center of grid
var goalY = 33; // Bottom of grid (goal area)
// Calculate direction to goal
var dx = goalX - enemy.currentCellX;
var dy = goalY - enemy.currentCellY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction and apply speed
var moveX = dx / distance * enemy.speed;
var moveY = dy / distance * enemy.speed;
// Add some path variation to avoid straight lines
var pathVariation = Math.sin(LK.ticks * 0.02 + enemy.currentCellX * 0.5) * 0.3;
moveX += pathVariation * enemy.speed * 0.2;
enemy.currentCellX += moveX;
enemy.currentCellY += moveY;
// Keep tanks within reasonable bounds
enemy.currentCellX = Math.max(1, Math.min(23, enemy.currentCellX));
// Set rotation to face movement direction
if (enemy.children[0]) {
var angle = Math.atan2(moveY, moveX) + Math.PI / 2;
if (enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.1) {
tween.stop(enemy.children[0], {
rotation: true
});
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 200,
easing: tween.easeOut
});
}
}
}
}
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
} else {
// Handle normal pathfinding enemies
if (!enemy.currentTarget && cell && cell.targets && cell.targets.length > 0) {
enemy.currentTarget = cell.targets[0];
}
if (enemy.currentTarget) {
// Check if current cell has a better target (closer to goal)
if (cell && cell.targets && cell.targets.length > 0 && cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell.targets[0];
}
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) {
// Check for immune enemy first turn completion (reaching waypoint after first horizontal segment)
if (self.type === 'immune' && !self.speedBoosted && enemy.currentCellY >= 8 && enemy.currentCellX <= 3) {
// Mark as speed boosted to prevent multiple applications
self.speedBoosted = true;
// Store original speed if not already stored
if (self.originalSpeedBeforeBoost === undefined) {
self.originalSpeedBeforeBoost = self.speed;
}
// Increase speed by 150% after first turn with smooth tween animation (2.5x original speed)
var targetSpeed = self.originalSpeedBeforeBoost * 2.5;
tween(self, {
speed: targetSpeed
}, {
duration: 300,
easing: tween.easeOut
});
}
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox) + Math.PI / 2;
enemy.currentCellX += Math.cos(angle - Math.PI / 2) * enemy.speed;
enemy.currentCellY += Math.sin(angle - Math.PI / 2) * enemy.speed;
} else if (cell && cell.targets && cell.targets.length > 0) {
// If no current target, pick the first available target from current cell
enemy.currentTarget = cell.targets[0];
}
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
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: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'trap';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset('Box', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
switch (self.towerType) {
case 'landmine':
baseGraphics.tint = 0x9B8B6D; // Lighter tan/brown camouflage
break;
case 'sniper':
baseGraphics.tint = 0x7A9B5F; // Lighter olive green camouflage
break;
case 'flamethrower':
baseGraphics.tint = 0xBBA385; //{aO} // Lighter sandy brown camouflage
break;
case 'slow':
baseGraphics.tint = 0x7F7F5F; //{aR} // Lighter olive drab camouflage
break;
case 'poison':
baseGraphics.tint = 0x969696; //{aU} // Lighter gray camouflage
break;
case 'tank':
baseGraphics.tint = 0x8B4513; // Dark brown for tank
break;
default:
baseGraphics.tint = 0x8C8C77;
//{aX} // Lighter neutral camouflage green-gray
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var displayTextShadow = self.towerType === 'flamethrower' ? 'Flame' : self.towerType === 'trap' ? 'Shooter' : self.towerType === 'slow' ? 'Missiles' : self.towerType === 'tank' ? 'Tank' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabelShadow = new Text2(displayTextShadow, {
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 displayText = self.towerType === 'flamethrower' ? 'Flame' : self.towerType === 'trap' ? 'Shooter' : self.towerType === 'slow' ? 'Missiles' : self.towerType === 'tank' ? 'Tank' : self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
var typeLabel = new Text2(displayText, {
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 Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'trap';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
switch (self.id) {
case 'sniper':
// Sniper: massive range to cover entire map from any position
if (self.level === self.maxLevel) {
return 75 * CELL_SIZE; // Extreme range for max level to cover entire map (1.5x increase)
}
return (22.5 + (self.level - 1) * 7.5) * CELL_SIZE;
// Increased base and scaling (1.5x increase)
case 'flamethrower':
// Flamethrower: base 4.5, +0.5 per level for cone range (increased aiming distance)
return (4.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'landmine':
// Landmine: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// Slow: base 8.75, +1.25 per level (2.5x increase)
return (8.75 + (self.level - 1) * 1.25) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'tank':
// Tank: base 14.0, +2.8 per level (3.5x increase)
return (14.0 + (self.level - 1) * 2.8) * CELL_SIZE;
default:
// Trap: base 6, +1.0 per level (double range)
return (6 + (self.level - 1) * 1.0) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
// Continuous shooting variables for flamethrower
self.isContinuousFiring = false;
self.continuousFireTimer = 0;
self.continuousFireDuration = 300; // 5 seconds at 60 FPS
self.continuousReloadTimer = 0;
self.continuousReloadDuration = 180; // 3 seconds at 60 FPS
switch (self.id) {
case 'landmine':
self.fireRate = 30;
self.damage = 5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'flamethrower':
self.fireRate = 2.5; // Very fast fire rate during continuous shooting (quarter of original delay)
self.damage = 8; // Lower per-hit damage but continuous
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 6;
break;
case 'slow':
self.fireRate = 30;
self.damage = 15; // Increased from 8 to 15 for stronger missiles
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'tank':
self.fireRate = 90;
self.damage = 50; // Increased base damage for tank
self.range = 4.0 * CELL_SIZE;
self.bulletSpeed = 4;
break;
}
var assetToUse = self.id === 'landmine' ? 'landmine' : self.id === 'sniper' ? 'Sniper' : self.id === 'flamethrower' ? 'Flamethrower' : self.id === 'slow' ? 'Rocket' : self.id === 'trap' ? 'Shooter' : self.id === 'tank' ? 'Tank' : 'tower';
var baseGraphics = self.attachAsset(assetToUse, {
anchorX: 0.5,
anchorY: 0.5
});
// Flame effects removed for performance
switch (self.id) {
case 'landmine':
// Remove tint to show natural landmine asset
break;
case 'sniper':
// Remove tint to show natural sniper asset
break;
case 'flamethrower':
// Remove tint to show natural flamethrower asset
break;
case 'slow':
// Remove tint to show natural rocket asset
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
case 'tank':
// Remove tint to show natural tank asset
break;
case 'trap':
baseGraphics.tint = 0x555555; // Dark gray for machine gun
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
// Add health bar for towers that need them (excluding landmine)
self.towerHealthBarOutline = null;
self.towerHealthBarBG = null;
self.towerHealthBar = null;
if (self.id === 'flamethrower' || self.id === 'trap' || self.id === 'sniper' || self.id === 'slow' || self.id === 'tank') {
// Create health bar outline
self.towerHealthBarOutline = self.attachAsset('towerHealthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
// Create health bar background (red)
self.towerHealthBarBG = self.attachAsset('towerHealthBar', {
anchorX: 0,
anchorY: 0.5
});
self.towerHealthBarBG.tint = 0xff0000;
// Create health bar foreground (green)
self.towerHealthBar = self.attachAsset('towerHealthBar', {
anchorX: 0,
anchorY: 0.5
});
self.towerHealthBar.tint = 0x00ff00;
// Position health bars below the level indicators to avoid overlap
var healthBarY = CELL_SIZE * 0.7 + 25; // Position below level indicators with some spacing
self.towerHealthBarOutline.y = healthBarY;
self.towerHealthBarBG.y = healthBarY;
self.towerHealthBar.y = healthBarY;
// Center health bars horizontally
self.towerHealthBarOutline.x = -self.towerHealthBarOutline.width / 2;
self.towerHealthBarBG.x = -self.towerHealthBarBG.width / 2;
self.towerHealthBar.x = -self.towerHealthBar.width / 2;
}
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
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 'landmine':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'flamethrower':
towerLevelIndicator.tint = 0xFF4500;
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);
}
}
}
}
// Only render debug when tower is selected to improve performance
if (selectedTower === self) {
grid.renderDebug();
}
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// Make last upgrade level extra expensive
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
if (self.id === 'landmine') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect
self.damage = 5 + self.level * 10; // double the effect
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
// Special scaling for missiles towers
if (self.id === 'slow') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for missiles (triple the effect)
self.fireRate = Math.max(3, 30 - self.level * 15); // Much faster fire rate
self.damage = 15 + self.level * 30; // Much higher damage scaling
self.bulletSpeed = 5 + self.level * 3; // Faster missiles
} else {
self.fireRate = Math.max(10, 30 - self.level * 5); // Faster than base scaling
self.damage = 15 + self.level * 10; // Higher damage per level
self.bulletSpeed = 5 + self.level * 1;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in range
if (distance <= self.getRange()) {
// 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, landmine towers target any ground enemy in range
if (self.id === 'landmine') {
// Landmine targets any ground enemy by distance
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
} else {
// For other towers, 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 () {
// Only find targets every 3 frames to reduce performance impact
if (LK.ticks % 3 === 0) {
self.targetEnemy = self.findTarget();
}
// Flame animation removed for flamethrower towers
// Special handling for landmine towers - explode on contact
if (self.id === 'landmine' && self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is close enough to trigger explosion (within 50 pixels) and is ground-based
if (distance <= 50 && !self.targetEnemy.isFlying) {
// Create landmine explosion effect with enhanced animation
var landmineExplosion = new EffectIndicator(self.x, self.y, 'splash');
landmineExplosion.children[0].tint = 0xFF4500; // Orange explosion color
landmineExplosion.children[0].width = landmineExplosion.children[0].height = CELL_SIZE * 2.5;
landmineExplosion.scaleX = 0.3;
landmineExplosion.scaleY = 0.3;
landmineExplosion.alpha = 1;
game.addChild(landmineExplosion);
// Multi-stage landmine explosion animation
tween(landmineExplosion, {
scaleX: 1.8,
scaleY: 1.8,
alpha: 0.9
}, {
duration: 150,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(landmineExplosion, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
landmineExplosion.destroy();
}
});
}
});
// Create enemy explosion effect with enhanced animation
var enemyExplosion = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
enemyExplosion.children[0].tint = 0xFF0000; // Red explosion color
enemyExplosion.children[0].width = enemyExplosion.children[0].height = CELL_SIZE * 2;
enemyExplosion.scaleX = 0.2;
enemyExplosion.scaleY = 0.2;
enemyExplosion.alpha = 1;
game.addChild(enemyExplosion);
// Multi-stage enemy explosion animation
tween(enemyExplosion, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0.8
}, {
duration: 120,
easing: tween.bounceOut,
onFinish: function onFinish() {
tween(enemyExplosion, {
scaleX: 2.2,
scaleY: 2.2,
alpha: 0
}, {
duration: 250,
easing: tween.easeOut,
onFinish: function onFinish() {
enemyExplosion.destroy();
}
});
}
});
// Create additional shockwave effect for landmine
var shockwave = new EffectIndicator(self.x, self.y, 'splash');
shockwave.children[0].tint = 0xFFFFFF;
shockwave.children[0].alpha = 0.3;
shockwave.children[0].width = shockwave.children[0].height = CELL_SIZE * 0.5;
shockwave.alpha = 0.8;
game.addChild(shockwave);
// Animate shockwave expanding rapidly
tween(shockwave, {
scaleX: 4,
scaleY: 4,
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
shockwave.destroy();
}
});
// Mark enemy for destruction
self.targetEnemy.health = 0;
// Mark landmine tower for destruction
self.shouldDestroy = true;
return;
}
}
if (self.targetEnemy && self.id !== 'landmine') {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
// Special rotation handling for sniper towers
if (self.id === 'sniper') {
// Use the main tower graphics (baseGraphics) for sniper rotation
var sniperGraphics = self.children[0]; // The sniper asset
if (sniperGraphics.targetRotation === undefined) {
sniperGraphics.targetRotation = angle;
sniperGraphics.rotation = angle;
} else {
if (Math.abs(angle - sniperGraphics.targetRotation) > 0.1) {
// Stop any ongoing rotation tweens
tween.stop(sniperGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = sniperGraphics.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;
}
sniperGraphics.targetRotation = angle;
tween(sniperGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 200,
easing: tween.easeOut
});
}
}
} else {
gunContainer.rotation = angle;
}
// Special continuous firing logic for flamethrower
if (self.id === 'flamethrower') {
// Handle continuous reload timer
if (self.continuousReloadTimer > 0) {
self.continuousReloadTimer--;
} else if (!self.isContinuousFiring && self.targetEnemy) {
// Start continuous firing
self.isContinuousFiring = true;
self.continuousFireTimer = self.continuousFireDuration;
} else if (self.isContinuousFiring) {
// Continue firing during the continuous period
if (self.continuousFireTimer > 0) {
self.continuousFireTimer--;
// Fire at the fast rate during continuous shooting
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
} else {
// End continuous firing, start reload
self.isContinuousFiring = false;
self.continuousReloadTimer = self.continuousReloadDuration;
}
}
} else {
// Normal firing logic for other tower types
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
// Initialize tower health based on tower type
if (self.id === 'flamethrower' || self.id === 'trap' || self.id === 'sniper' || self.id === 'slow' || self.id === 'tank') {
// Set health values based on tower type
if (self.id === 'slow') {
self.towerHealth = 300; // 3x base health for missiles
self.maxTowerHealth = 300;
} else if (self.id === 'tank') {
self.towerHealth = 500; // 5x base health for tank towers - very strong
self.maxTowerHealth = 500;
} else if (self.id === 'sniper') {
self.towerHealth = 150; // Medium health for sniper
self.maxTowerHealth = 150;
} else if (self.id === 'flamethrower') {
self.towerHealth = 120; // Medium health for flamethrower
self.maxTowerHealth = 120;
} else {
self.towerHealth = 100; // Base tower health for trap/shooter
self.maxTowerHealth = 100;
}
}
// Method to handle damage from enemy bullets
self.takeDamage = function (damage) {
// Special handling for sniper towers - track hits instead of health
if (self.id === 'sniper') {
// Initialize hit counter if not set
if (self.hitCount === undefined) {
self.hitCount = 0;
}
self.hitCount++;
// Create hit indicator effect
var hitEffect = new EffectIndicator(self.x, self.y, 'sniper');
hitEffect.children[0].tint = 0xFF0000; // Red hit indicator
hitEffect.children[0].width = hitEffect.children[0].height = CELL_SIZE;
game.addChild(hitEffect);
// Update health bar
if (self.towerHealthBar) {
var healthPercentage = Math.max(0, (5 - self.hitCount) / 5);
self.towerHealthBar.width = 60 * healthPercentage;
}
// Destroy sniper after 5 hits
if (self.hitCount >= 5) {
// Mark sniper for destruction
self.shouldDestroy = true;
// Create destruction effect
var destructionEffect = new EffectIndicator(self.x, self.y, 'splash');
destructionEffect.children[0].tint = 0xFF4500; // Orange destruction color
destructionEffect.children[0].width = destructionEffect.children[0].height = CELL_SIZE * 2;
game.addChild(destructionEffect);
}
} else {
// Initialize tower health if not set for non-sniper towers
if (self.towerHealth === undefined) {
// Missiles towers have much higher health to make them stronger
if (self.id === 'slow') {
self.towerHealth = 300; // 3x base health for missiles
self.maxTowerHealth = 300;
} else if (self.id === 'tank') {
self.towerHealth = 500; // 5x base health for tank towers - very strong
self.maxTowerHealth = 500;
} else {
self.towerHealth = 100; // Base tower health
self.maxTowerHealth = 100;
}
}
self.towerHealth -= damage;
// Update health bar
if (self.towerHealthBar && self.maxTowerHealth > 0) {
var healthPercentage = Math.max(0, self.towerHealth / self.maxTowerHealth);
self.towerHealthBar.width = 60 * healthPercentage;
}
if (self.towerHealth <= 0) {
self.towerHealth = 0;
// Mark tower for destruction
self.shouldDestroy = true;
// Create destruction effect
var destructionEffect = new EffectIndicator(self.x, self.y, 'splash');
destructionEffect.children[0].tint = 0xFF4500; // Orange destruction color
destructionEffect.children[0].width = destructionEffect.children[0].height = CELL_SIZE * 2;
game.addChild(destructionEffect);
}
}
};
self.fire = function () {
// Landmine towers cannot shoot normally
if (self.id === 'landmine') {
return;
}
// Machine gun shooting function
if (self.id === 'trap') {
if (self.targetEnemy) {
// Fire multiple bullets in rapid succession for higher ammunition density
var bulletsToFire = 3; // Fire 3 bullets per shot for higher density
for (var bulletIndex = 0; bulletIndex < bulletsToFire; bulletIndex++) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type for machine gun
bullet.type = 'trap';
// Machine gun bullets are smaller and faster
bullet.children[0].tint = 0x555555; // Dark gray for machine gun bullets
bullet.children[0].width = 25;
bullet.children[0].height = 25;
// Add slight delay between bullets for visual effect
if (bulletIndex > 0) {
bullet.x += (Math.random() - 0.5) * 10; // Small random offset
bullet.y += (Math.random() - 0.5) * 10;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
}
// Machine gun recoil effect
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
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;
}
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
var recoilDistance = 6;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 40,
easing: tween.cubicOut,
onFinish: function onFinish() {
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 60,
easing: tween.cubicIn
});
}
});
}
return;
}
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
// Special handling for flamethrower - create continuous flame spray
if (self.id === 'flamethrower') {
// Create multiple flame particles for continuous spray effect
var flamesToCreate = 3; // Create multiple flames for denser spray
for (var flameIndex = 0; flameIndex < flamesToCreate; flameIndex++) {
var flameParticle = new Container();
var flameGraphics = flameParticle.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
flameGraphics.width = 25 + Math.random() * 15;
flameGraphics.height = 35 + Math.random() * 20;
flameGraphics.tint = Math.random() > 0.5 ? 0xFF4500 : 0xFF6600;
flameGraphics.alpha = 0.7 + Math.random() * 0.3;
// Position flame particle at tower with slight spread
var spreadAngle = (Math.random() - 0.5) * 0.4; // 20 degree spread
var startAngle = gunContainer.rotation + spreadAngle;
var startDistance = 30 + Math.random() * 10;
flameParticle.x = self.x + Math.cos(startAngle) * startDistance;
flameParticle.y = self.y + Math.sin(startAngle) * startDistance;
// Calculate target position with spread
var targetDistance = self.getRange() * (0.6 + Math.random() * 0.4);
var targetX = self.x + Math.cos(startAngle) * targetDistance;
var targetY = self.y + Math.sin(startAngle) * targetDistance;
// Store damage info for area effect
flameParticle.damage = self.damage;
flameParticle.sourceTowerLevel = self.level;
game.addChild(flameParticle);
// Animate flame particle moving toward target area
tween(flameParticle, {
x: targetX,
y: targetY,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 400 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Apply area damage when flame reaches target area
var coneRange = CELL_SIZE * 1.2;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - flameParticle.x;
var dy = enemy.y - flameParticle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= coneRange) {
// Apply flame damage
enemy.health -= flameParticle.damage * 0.8;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Apply burning effect
if (!enemy.isImmune) {
enemy.burning = true;
enemy.burnDamage = flameParticle.damage * 0.3;
enemy.burnDuration = 180;
}
// Create visual flame effect
var flameEffect = new EffectIndicator(enemy.x, enemy.y, 'flamethrower');
game.addChild(flameEffect);
}
}
// Fade out flame particle
tween(flameParticle, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
flameParticle.destroy();
}
});
}
});
}
} else {
// Regular bullet creation for other tower types
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// Customize bullet appearance based on tower type
switch (self.id) {
case 'sniper':
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
break;
case 'slow':
// Replace bullet graphics with missile asset
var missileGraphics = bullet.attachAsset('missiles', {
anchorX: 0.5,
anchorY: 0.5
});
// Remove the original bullet graphics
bullet.removeChild(bullet.children[0]);
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'tank':
bullet.children[0].tint = 0x8B4513;
bullet.children[0].width = 40;
bullet.children[0].height = 40;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
}
// --- Fire recoil effect for gunContainer ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
// Always use the original resting position for recoil, never accumulate offset
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset to resting position before animating (in case of interrupted tweens)
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Calculate recoil offset (recoil back along the gun's rotation)
var recoilDistance = 8;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
// Animate recoil back from the resting position
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original position/scale
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// Store original type before placing tower
cell.originalType = cell.type;
// Allow placement on any cell type - don't force to wall type
}
}
}
self.refreshCellsInRange();
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'trap';
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('Box', {
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;
var updateCounter = 0;
self.update = function () {
// Only check every 10 frames to reduce performance impact
updateCounter++;
if (updateCounter % 10 === 0) {
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 'landmine':
previewGraphics.tint = 0x9B8B6D; // Lighter tan/brown camouflage
break;
case 'sniper':
previewGraphics.tint = 0x7A9B5F; // Lighter olive green camouflage
break;
case 'flamethrower':
previewGraphics.tint = 0xBBA385; // Lighter sandy brown camouflage
break;
case 'slow':
previewGraphics.tint = 0x7F7F5F; // Lighter olive drab camouflage
break;
case 'poison':
previewGraphics.tint = 0x969696; // Lighter gray camouflage
break;
case 'tank':
previewGraphics.tint = 0x8B4513; // Dark brown for tank
break;
default:
previewGraphics.tint = 0x8C8C77;
// Lighter neutral camouflage green-gray
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
// Prevent placement in upper sixth of screen (y < 455)
var screenY = grid.y + self.gridY * CELL_SIZE;
if (screenY < 455) {
validGridPlacement = false;
} else {
// For tank weapons, only allow placement in lower half of screen
if (self.towerType === 'tank') {
var screenCenterY = grid.y + (29 + 6) * CELL_SIZE / 2; // Grid height / 2
if (screenY < screenCenterY) {
validGridPlacement = false;
}
}
// Allow placement anywhere else on the grid
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) {
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 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 towerTypeName = self.tower.id === 'slow' ? 'Missiles' : self.tower.id === 'tank' ? 'Tank' : self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1);
var towerTypeText = new Text2(towerTypeName + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// Restore original cell type or default to ground (type 1) if no original type stored
cell.type = cell.originalType !== undefined ? cell.originalType : 1;
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;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = i + 1 === totalWaves;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 3;
} else if (i === 3) {
block.tint = 0x888888;
waveType = "Tank";
enemyType = "tank";
enemyCount = 1;
} else if (i === 4) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 8) {
// Wave 9 - Fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 9) {
// Wave 10 - Flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 3;
} else if (i === 10) {
// Wave 11 - Immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['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 '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 '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 = 3;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave) {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 4;
waveTypeShadow.y = 4;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "fast";
}
// 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 (final wave)
if (waveNumber === totalWaves) {
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;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xD2B48C
});
/****
* Game Code
****/
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
var gold = 100;
var lives = 100;
var score = 0;
var currentWave = 0;
var totalWaves = 12;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 600; // Reduced from 1500 to 600 frames (10 seconds instead of 25)
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var 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;
var lastGold = -1;
var lastLives = -1;
var lastScore = -1;
function updateUI() {
if (gold !== lastGold) {
goldText.setText('Gold: ' + gold);
lastGold = gold;
}
if (lives !== lastLives) {
livesText.setText('Lives: ' + lives);
lastLives = lives;
}
if (score !== lastScore) {
scoreText.setText('Score: ' + score);
lastScore = score;
}
}
function setGold(value) {
gold = value;
updateUI();
}
var debugLayer = new Container();
var towerLayer = new Container();
// Fast enemy can shoot - already implemented
// This functionality is already working in the Enemy class update method
// Fast enemies have shooting range, damage, cooldown, and create red bullets targeting towers
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
});
// Only set to blocking type if it's currently a path (type 0)
// Ground cells (type 1) can be built on without blocking paths
if (cell.type === 0) {
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 = 30;
switch (towerType) {
case 'landmine':
cost = 10;
break;
case 'sniper':
cost = 40;
break;
case 'flamethrower':
cost = 20;
break;
case 'slow':
cost = 50;
break;
case 'poison':
cost = 55;
break;
case 'tank':
cost = 60;
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 || 'trap');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
// Only recalculate pathfinding if needed
if (!grid.pathFind()) {
grid.renderDebug();
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
// Apply the same offset as in move handler to ensure consistency when starting drag
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
break;
}
}
};
game.move = function (x, y, obj) {
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) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.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 = ['landmine', 'flamethrower', 'trap', 'sniper', 'slow', 'tank'];
var sourceTowers = [];
var towerSpacing = 350; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
game.update = function () {
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
// Get wave type and enemy count from the wave indicator
var waveType = waveIndicator.getWaveType(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
// Check if this is a boss wave
var isBossWave = currentWave === totalWaves;
if (isBossWave) {
// Boss waves have just 1 enemy regardless of what the wave indicator says
enemyCount = 1;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use exponential scaling for health
var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// Assign specific lanes for different enemy types to prevent overlapping
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
var spawnX;
// Define strict non-overlapping lanes with buffer zones for different enemy types
if (waveType === 'fast') {
// Fast enemies spawn in horizontal lines
var enemiesPerLine = 5; // Number of enemies per horizontal line
var lineNumber = Math.floor(i / enemiesPerLine); // Which horizontal line this enemy belongs to
var positionInLine = i % enemiesPerLine; // Position within the line (0-4)
var lineColumns = [7, 10, 13, 16, 19]; // Available columns for horizontal lines
spawnX = lineColumns[positionInLine]; // Position enemy in the line
} else if (waveType === 'immune') {
// Immune enemies use center-left lanes (columns 11-12) with buffer from fast and tank
var immuneColumns = [11, 12];
var availableImmuneColumns = [];
for (var col = 0; col < immuneColumns.length; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === immuneColumns[col] && enemies[e].currentCellY < 8) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableImmuneColumns.push(immuneColumns[col]);
}
}
// If no columns available, queue the enemy by using the first column
spawnX = availableImmuneColumns.length > 0 ? availableImmuneColumns[Math.floor(Math.random() * availableImmuneColumns.length)] : immuneColumns[0];
} else if (waveType === 'tank') {
// Tank enemies spawn from next to the right edge of road: columns 20, 21
var tankColumns = [20, 21]; // Next to right edge lanes for tank enemies
spawnX = tankColumns[i % tankColumns.length]; // Cycle through right edge lanes
} else {
// Flying and other enemy types use middle lanes (columns 12-14) avoiding other types
var flyingColumns = [12, 13, 14];
var availableColumns = [];
for (var col = 0; col < flyingColumns.length; 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 === flyingColumns[col] && enemies[e].currentCellY < 8) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(flyingColumns[col]);
}
}
// If no columns available, use the center column
spawnX = availableColumns.length > 0 ? availableColumns[Math.floor(Math.random() * availableColumns.length)] : flyingColumns[1];
}
// Calculate spawn position with much larger spacing to ensure no visual overlap
// Convert units to cell units (pixels / 76 pixels per cell)
var enemySpacing;
if (waveType === 'fast') {
// Calculate spacing between horizontal lines (not individual enemies)
var lineSpacing = 600 / CELL_SIZE; // Spacing between horizontal lines (~7.9 cells)
var lineNumber = Math.floor(i / 5); // Which horizontal line this enemy belongs to
enemySpacing = lineSpacing;
// Position fast enemies in their assigned column without horizontal offset
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
enemy.currentCellY = -1 - lineNumber * enemySpacing; // Each line spawns with line spacing, not individual spacing
} else {
if (waveType === 'tank') {
enemySpacing = 800 / CELL_SIZE; // Reduced spacing for tank enemies with better lane distribution (~10.5 cells)
} else if (waveType === 'immune') {
enemySpacing = 400 / CELL_SIZE; // Reduced spacing for immune enemies (~5.3 cells)
} else {
enemySpacing = 900 / CELL_SIZE; // Large spacing for other enemies (~11.8 cells)
}
var spawnY = -1 - i * enemySpacing; // Each enemy spawns based on enemy type spacing
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
}
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Remove all flame particles from the map when enemy is destroyed
for (var flameIndex = game.children.length - 1; flameIndex >= 0; flameIndex--) {
var child = game.children[flameIndex];
// Remove EffectIndicator flame effects
if (child instanceof EffectIndicator) {
var dx = child.x - enemy.x;
var dy = child.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 3) {
child.destroy();
}
}
// Remove flame particles (Container objects with flame graphics)
else if (child.children && child.children.length > 0 && child.children[0].tint !== undefined) {
var flameGraphics = child.children[0];
// Check for flame particles by tint colors and properties
if ((flameGraphics.tint === 0xFF4500 || flameGraphics.tint === 0xFF6600) && flameGraphics.width && flameGraphics.height) {
// Stop any ongoing tweens and destroy all flame particles
tween.stop(child);
child.destroy();
}
}
// Remove flame particles that have damage property (flamethrower particles)
else if (child.damage !== undefined && child.sourceTowerLevel !== undefined) {
// Stop any ongoing tweens and destroy flamethrower particles
tween.stop(child);
child.destroy();
}
}
// Boss enemies give more gold and score - significantly increased rewards
var goldEarned = enemy.isBoss ? Math.floor(200 + (enemy.waveNumber - 1) * 25) : Math.floor(15 + (enemy.waveNumber - 1) * 3);
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();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
// Only update enemy position every few frames to reduce performance impact
if (LK.ticks % 2 === 0) {
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
// Each individual enemy reduces lives by 10 (10% of 100 total lives)
lives = Math.max(0, lives - 10);
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
}
// Only clean bullets every 5 frames to reduce processing
if (LK.ticks % 5 === 0) {
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
if (!bullet.parent) {
if (bullet.targetEnemy) {
var targetEnemy = bullet.targetEnemy;
if (targetEnemy && targetEnemy.bulletsTargetingThis) {
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullet);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
}
// Clear bullet references
bullet.targetEnemy = null;
bullets.splice(i, 1);
}
}
}
// Check for landmine towers that need to be destroyed after explosion
for (var i = towers.length - 1; i >= 0; i--) {
var tower = towers[i];
if (tower.shouldDestroy) {
// Remove from grid cells
var gridX = tower.gridX;
var gridY = tower.gridY;
for (var j = 0; j < 2; j++) {
for (var k = 0; k < 2; k++) {
var cell = grid.getCell(gridX + j, gridY + k);
if (cell) {
// Restore original cell type or default to ground (type 1) if no original type stored
cell.type = cell.originalType !== undefined ? cell.originalType : 1;
var towerIndex = cell.towersInRange.indexOf(tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
// Remove from towers array and tower layer
towers.splice(i, 1);
towerLayer.removeChild(tower);
// Clean up any upgrade menus or range indicators
if (selectedTower === tower) {
selectedTower = null;
}
// Remove any upgrade menus for this tower
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu && child.tower === tower;
});
for (var menuIndex = 0; menuIndex < upgradeMenus.length; menuIndex++) {
upgradeMenus[menuIndex].destroy();
}
// Remove any range indicators for this tower
for (var rangeIndex = game.children.length - 1; rangeIndex >= 0; rangeIndex--) {
if (game.children[rangeIndex].isTowerRange && game.children[rangeIndex].tower === tower) {
game.removeChild(game.children[rangeIndex]);
}
}
// Recalculate pathfinding
grid.pathFind();
grid.renderDebug();
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
}; ===================================================================
--- original.js
+++ change.js
@@ -4132,11 +4132,14 @@
var midPoint = Math.floor(gridWidth / 2); // 12
var spawnX;
// Define strict non-overlapping lanes with buffer zones for different enemy types
if (waveType === 'fast') {
- // Fast enemies spawn horizontally with increased distance between columns for harder gameplay
- var fastColumns = [7, 10, 13, 16, 19]; // Use columns 7, 10, 13, 16, 19 for fast enemies (5 total lines with larger gaps)
- spawnX = fastColumns[i % fastColumns.length]; // Cycle through available columns
+ // Fast enemies spawn in horizontal lines
+ var enemiesPerLine = 5; // Number of enemies per horizontal line
+ var lineNumber = Math.floor(i / enemiesPerLine); // Which horizontal line this enemy belongs to
+ var positionInLine = i % enemiesPerLine; // Position within the line (0-4)
+ var lineColumns = [7, 10, 13, 16, 19]; // Available columns for horizontal lines
+ spawnX = lineColumns[positionInLine]; // Position enemy in the line
} else if (waveType === 'immune') {
// Immune enemies use center-left lanes (columns 11-12) with buffer from fast and tank
var immuneColumns = [11, 12];
var availableImmuneColumns = [];
@@ -4181,14 +4184,17 @@
// Calculate spawn position with much larger spacing to ensure no visual overlap
// Convert units to cell units (pixels / 76 pixels per cell)
var enemySpacing;
if (waveType === 'fast') {
- enemySpacing = 200 / CELL_SIZE; // Reduced spacing for fast enemies (~2.6 cells)
+ // Calculate spacing between horizontal lines (not individual enemies)
+ var lineSpacing = 600 / CELL_SIZE; // Spacing between horizontal lines (~7.9 cells)
+ var lineNumber = Math.floor(i / 5); // Which horizontal line this enemy belongs to
+ enemySpacing = lineSpacing;
// Position fast enemies in their assigned column without horizontal offset
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
- enemy.currentCellY = -1 - i * enemySpacing; // Each individual enemy spawns with its own spacing
+ enemy.currentCellY = -1 - lineNumber * enemySpacing; // Each line spawns with line spacing, not individual spacing
} else {
if (waveType === 'tank') {
enemySpacing = 800 / CELL_SIZE; // Reduced spacing for tank enemies with better lane distribution (~10.5 cells)
} else if (waveType === 'immune') {
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
Light grey asphalt texture. Top view
Armed off-road vehicle with camouflage livery. Top view.
Military tank, top view
Army Soldier with gun. Topview.
Bomber Drone with spinning rotors, from top view.
Army box,but no text needed on image. top view
landmine from top view
Sniper man from top view
Rocket launcher cannon, from top view.
Army soldier laying with machine gun, top view from back
Missile, top view