User prompt
change missiles weapon ammunition from purple dots to missile asset
User prompt
make missiles shooting speed faster
User prompt
increase Missiles shooting speed
User prompt
change missiles box value from 45 to 50
User prompt
Rename slow weapon to Missiles
User prompt
Increase slow tower range to 2.5x current values
User prompt
Increase slow tower range to 2x current values
User prompt
Change placed slow weapon to rocket asset
User prompt
Change placed slow weapon to rocket asset
User prompt
Add 2 more fast enemy to the left side
User prompt
Increase fast 1 enemy with 2 lines in left side
User prompt
Increase horizontal distance between fast 1 enemies to make game harder.
User prompt
Increase distance between fast 1 enemies to make game harder.
User prompt
Add more fast enemy lines to the right side
User prompt
Add more fast enemy lines next to this 2
User prompt
Immune Enemy dies too fast
User prompt
Increase immune enemy life power
User prompt
Change placed shooter weapon to shooter asset
User prompt
Remove all flame particles from the map when enemy is destroyed
User prompt
A few monent later remove all left flames from the map ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Remove flame particles from the map when enemy is destroyed
User prompt
Remove flame effects from map when enemy is destroyed
User prompt
Remove flames from the map after destroyed an enemy ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Ensure flamethrower can spray countinous flame ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Remove flame animation from placed flamethrower weapon.
/****
* 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) {
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Apply special effects based on bullet type
if (self.type === '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;
}
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 = 80;
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 5 frames for performance
if (LK.ticks % 5 === 0 && LK.ticks !== self.lastCollisionCheck) {
self.lastCollisionCheck = LK.ticks;
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;
if (distance < minDistance && distance > 0) {
// Calculate separation force
var separationForce = (minDistance - distance) / minDistance;
var separationX = dx / distance * separationForce * 0.5;
var separationY = dy / distance * separationForce * 0.5;
// Apply separation - move away from other enemy
self.currentCellX -= separationX;
self.currentCellY -= separationY;
// Keep enemies within reasonable bounds
self.currentCellX = Math.max(1, Math.min(23, self.currentCellX));
self.currentCellY = Math.max(-10, Math.min(35, self.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;
}
// Skip trap towers - they are just obstacles, not hostile weapons
if (tower.id === 'trap') {
continue;
}
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;
}
// Skip trap towers - they are just obstacles, not hostile weapons
if (tower.id === 'trap') {
continue;
}
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;
}
// Skip trap towers - they are just obstacles, not hostile weapons
if (tower.id === 'trap') {
continue;
}
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;
}
}
}
// 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
});
}
}
}
}
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) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// 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 {
// Normal pathfinding for non-fast 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) {
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;
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.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.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 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
default:
// 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 = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
var assetToUse = self.id === 'landmine' ? 'landmine' : self.id === 'sniper' ? 'Sniper' : self.id === 'flamethrower' ? 'Flamethrower' : self.id === 'trap' ? 'machine' : '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':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
case 'trap':
baseGraphics.tint = 0x555555; // Dark gray for machine gun
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
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 {
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();
};
// 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);
// 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) {
self.towerHealth = 100; // Base tower health
self.maxTowerHealth = 100;
}
self.towerHealth -= damage;
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 flame projectiles
if (self.id === 'flamethrower') {
// Create a flame projectile that travels to the enemy
var flameProjectile = new Container();
var flameGraphics = flameProjectile.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
flameGraphics.width = 40;
flameGraphics.height = 60;
flameGraphics.tint = 0xFF4500;
flameGraphics.alpha = 0.8;
// Position flame projectile at tower
flameProjectile.x = self.x + Math.cos(gunContainer.rotation) * 30;
flameProjectile.y = self.y + Math.sin(gunContainer.rotation) * 30;
// Add flame trail effect
flameProjectile.trailParticles = [];
// Store target and damage info
flameProjectile.targetEnemy = self.targetEnemy;
flameProjectile.damage = self.damage;
flameProjectile.speed = self.bulletSpeed;
flameProjectile.type = 'flamethrower';
flameProjectile.sourceTowerLevel = self.level;
// Add flame projectile update method
flameProjectile.update = function () {
if (!this.targetEnemy || !this.targetEnemy.parent) {
this.destroy();
return;
}
var dx = this.targetEnemy.x - this.x;
var dy = this.targetEnemy.y - this.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.speed) {
// Apply damage to target enemy
this.targetEnemy.health -= this.damage;
if (this.targetEnemy.health <= 0) {
this.targetEnemy.health = 0;
} else {
this.targetEnemy.healthBar.width = this.targetEnemy.health / this.targetEnemy.maxHealth * 70;
}
// Create visual flame effect with enhanced animation
var flameEffect = new EffectIndicator(this.targetEnemy.x, this.targetEnemy.y, 'flamethrower');
game.addChild(flameEffect);
// Apply burning effect to target enemy
if (!this.targetEnemy.isImmune) {
this.targetEnemy.burning = true;
this.targetEnemy.burnDamage = this.damage * 0.3;
this.targetEnemy.burnDuration = 180;
}
// Flame damage to enemies in a cone behind the target
var coneRange = CELL_SIZE * 1.2;
var targetAngle = Math.atan2(this.targetEnemy.y - this.y, this.targetEnemy.x - this.x);
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== this.targetEnemy) {
var flameDx = otherEnemy.x - this.targetEnemy.x;
var flameDy = otherEnemy.y - this.targetEnemy.y;
var flameDistance = Math.sqrt(flameDx * flameDx + flameDy * flameDy);
var enemyAngle = Math.atan2(flameDy, flameDx);
var angleDiff = Math.abs(enemyAngle - targetAngle);
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
angleDiff = Math.abs(angleDiff);
if (flameDistance <= coneRange && angleDiff <= Math.PI / 4) {
otherEnemy.health -= this.damage * 0.4;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
if (!otherEnemy.isImmune) {
otherEnemy.burning = true;
otherEnemy.burnDamage = this.damage * 0.2;
otherEnemy.burnDuration = 120;
}
}
}
}
this.destroy();
} else {
var angle = Math.atan2(dy, dx);
this.x += Math.cos(angle) * this.speed;
this.y += Math.sin(angle) * this.speed;
// Create trailing flame particles
if (LK.ticks % 3 === 0) {
var trailFlame = new Container();
var trailGraphics = trailFlame.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
trailGraphics.width = 20 + Math.random() * 10;
trailGraphics.height = 25 + Math.random() * 15;
trailGraphics.tint = Math.random() > 0.5 ? 0xFF6600 : 0xFF8800;
trailGraphics.alpha = 0.6 + Math.random() * 0.3;
trailFlame.x = this.x + (Math.random() - 0.5) * 15;
trailFlame.y = this.y + (Math.random() - 0.5) * 15;
game.addChild(trailFlame);
// Animate trail flame fade out
tween(trailFlame, {
alpha: 0,
scaleX: 0.3,
scaleY: 0.3
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
trailFlame.destroy();
}
});
}
}
};
game.addChild(flameProjectile);
bullets.push(flameProjectile);
self.targetEnemy.bulletsTargetingThis.push(flameProjectile);
} 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':
bullet.children[0].tint = 0x9900FF;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
}
// --- 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;
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 {
// 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 towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// 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 = 5;
} 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 = 20;
var score = 0;
var currentWave = 0;
var totalWaves = 12;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 1500;
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 = 45;
break;
case 'poison':
cost = 55;
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', 'poison'];
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 horizontally next to each other in a single column
spawnX = 9; // Use column 9 for all fast enemies
} 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 use rightmost lanes (columns 15-16) with clear separation from immune
var tankColumns = [15, 16];
var availableTankColumns = [];
for (var col = 0; col < tankColumns.length; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === tankColumns[col] && enemies[e].currentCellY < 8) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableTankColumns.push(tankColumns[col]);
}
}
// If no columns available, queue the enemy by using the first column
spawnX = availableTankColumns.length > 0 ? availableTankColumns[Math.floor(Math.random() * availableTankColumns.length)] : tankColumns[0];
} 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') {
enemySpacing = 250 / CELL_SIZE; // Further reduced spacing for fast enemies (~3.3 cells)
// Position fast enemies horizontally next to each other
var horizontalOffset = i % 2 * 1.5; // Alternate between 0 and 1.5 cell offset
enemy.cellX = spawnX + horizontalOffset;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX + horizontalOffset;
enemy.currentCellY = -1 - Math.floor(i / 2) * enemySpacing; // Pair enemies vertically
} else {
if (waveType === 'tank') {
enemySpacing = 1200 / CELL_SIZE; // Maximum spacing for tank enemies (~15.8 cells)
} else if (waveType === 'immune') {
enemySpacing = 1000 / CELL_SIZE; // Large spacing for immune enemies (~13.2 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;
}
// Boss enemies give more gold and score
var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5);
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);
lives = Math.max(0, lives - 1);
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();
}
}; /****
* 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) {
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Apply special effects based on bullet type
if (self.type === '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;
}
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 = 80;
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 5 frames for performance
if (LK.ticks % 5 === 0 && LK.ticks !== self.lastCollisionCheck) {
self.lastCollisionCheck = LK.ticks;
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;
if (distance < minDistance && distance > 0) {
// Calculate separation force
var separationForce = (minDistance - distance) / minDistance;
var separationX = dx / distance * separationForce * 0.5;
var separationY = dy / distance * separationForce * 0.5;
// Apply separation - move away from other enemy
self.currentCellX -= separationX;
self.currentCellY -= separationY;
// Keep enemies within reasonable bounds
self.currentCellX = Math.max(1, Math.min(23, self.currentCellX));
self.currentCellY = Math.max(-10, Math.min(35, self.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;
}
// Skip trap towers - they are just obstacles, not hostile weapons
if (tower.id === 'trap') {
continue;
}
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;
}
// Skip trap towers - they are just obstacles, not hostile weapons
if (tower.id === 'trap') {
continue;
}
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;
}
// Skip trap towers - they are just obstacles, not hostile weapons
if (tower.id === 'trap') {
continue;
}
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;
}
}
}
// 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
});
}
}
}
}
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) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// 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 {
// Normal pathfinding for non-fast 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) {
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;
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.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.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 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
default:
// 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 = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
var assetToUse = self.id === 'landmine' ? 'landmine' : self.id === 'sniper' ? 'Sniper' : self.id === 'flamethrower' ? 'Flamethrower' : self.id === 'trap' ? 'machine' : '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':
baseGraphics.tint = 0x9900FF;
break;
case 'poison':
baseGraphics.tint = 0x00FFAA;
break;
case 'trap':
baseGraphics.tint = 0x555555; // Dark gray for machine gun
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
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 {
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();
};
// 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);
// 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) {
self.towerHealth = 100; // Base tower health
self.maxTowerHealth = 100;
}
self.towerHealth -= damage;
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 flame projectiles
if (self.id === 'flamethrower') {
// Create a flame projectile that travels to the enemy
var flameProjectile = new Container();
var flameGraphics = flameProjectile.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
flameGraphics.width = 40;
flameGraphics.height = 60;
flameGraphics.tint = 0xFF4500;
flameGraphics.alpha = 0.8;
// Position flame projectile at tower
flameProjectile.x = self.x + Math.cos(gunContainer.rotation) * 30;
flameProjectile.y = self.y + Math.sin(gunContainer.rotation) * 30;
// Add flame trail effect
flameProjectile.trailParticles = [];
// Store target and damage info
flameProjectile.targetEnemy = self.targetEnemy;
flameProjectile.damage = self.damage;
flameProjectile.speed = self.bulletSpeed;
flameProjectile.type = 'flamethrower';
flameProjectile.sourceTowerLevel = self.level;
// Add flame projectile update method
flameProjectile.update = function () {
if (!this.targetEnemy || !this.targetEnemy.parent) {
this.destroy();
return;
}
var dx = this.targetEnemy.x - this.x;
var dy = this.targetEnemy.y - this.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.speed) {
// Apply damage to target enemy
this.targetEnemy.health -= this.damage;
if (this.targetEnemy.health <= 0) {
this.targetEnemy.health = 0;
} else {
this.targetEnemy.healthBar.width = this.targetEnemy.health / this.targetEnemy.maxHealth * 70;
}
// Create visual flame effect with enhanced animation
var flameEffect = new EffectIndicator(this.targetEnemy.x, this.targetEnemy.y, 'flamethrower');
game.addChild(flameEffect);
// Apply burning effect to target enemy
if (!this.targetEnemy.isImmune) {
this.targetEnemy.burning = true;
this.targetEnemy.burnDamage = this.damage * 0.3;
this.targetEnemy.burnDuration = 180;
}
// Flame damage to enemies in a cone behind the target
var coneRange = CELL_SIZE * 1.2;
var targetAngle = Math.atan2(this.targetEnemy.y - this.y, this.targetEnemy.x - this.x);
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== this.targetEnemy) {
var flameDx = otherEnemy.x - this.targetEnemy.x;
var flameDy = otherEnemy.y - this.targetEnemy.y;
var flameDistance = Math.sqrt(flameDx * flameDx + flameDy * flameDy);
var enemyAngle = Math.atan2(flameDy, flameDx);
var angleDiff = Math.abs(enemyAngle - targetAngle);
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
angleDiff = Math.abs(angleDiff);
if (flameDistance <= coneRange && angleDiff <= Math.PI / 4) {
otherEnemy.health -= this.damage * 0.4;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
if (!otherEnemy.isImmune) {
otherEnemy.burning = true;
otherEnemy.burnDamage = this.damage * 0.2;
otherEnemy.burnDuration = 120;
}
}
}
}
this.destroy();
} else {
var angle = Math.atan2(dy, dx);
this.x += Math.cos(angle) * this.speed;
this.y += Math.sin(angle) * this.speed;
// Create trailing flame particles
if (LK.ticks % 3 === 0) {
var trailFlame = new Container();
var trailGraphics = trailFlame.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
trailGraphics.width = 20 + Math.random() * 10;
trailGraphics.height = 25 + Math.random() * 15;
trailGraphics.tint = Math.random() > 0.5 ? 0xFF6600 : 0xFF8800;
trailGraphics.alpha = 0.6 + Math.random() * 0.3;
trailFlame.x = this.x + (Math.random() - 0.5) * 15;
trailFlame.y = this.y + (Math.random() - 0.5) * 15;
game.addChild(trailFlame);
// Animate trail flame fade out
tween(trailFlame, {
alpha: 0,
scaleX: 0.3,
scaleY: 0.3
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
trailFlame.destroy();
}
});
}
}
};
game.addChild(flameProjectile);
bullets.push(flameProjectile);
self.targetEnemy.bulletsTargetingThis.push(flameProjectile);
} 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':
bullet.children[0].tint = 0x9900FF;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'poison':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
}
// --- 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;
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 {
// 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 towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
// 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 = 5;
} 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 = 20;
var score = 0;
var currentWave = 0;
var totalWaves = 12;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 1500;
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 = 45;
break;
case 'poison':
cost = 55;
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', 'poison'];
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 horizontally next to each other in a single column
spawnX = 9; // Use column 9 for all fast enemies
} 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 use rightmost lanes (columns 15-16) with clear separation from immune
var tankColumns = [15, 16];
var availableTankColumns = [];
for (var col = 0; col < tankColumns.length; col++) {
var columnOccupied = false;
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === tankColumns[col] && enemies[e].currentCellY < 8) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableTankColumns.push(tankColumns[col]);
}
}
// If no columns available, queue the enemy by using the first column
spawnX = availableTankColumns.length > 0 ? availableTankColumns[Math.floor(Math.random() * availableTankColumns.length)] : tankColumns[0];
} 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') {
enemySpacing = 250 / CELL_SIZE; // Further reduced spacing for fast enemies (~3.3 cells)
// Position fast enemies horizontally next to each other
var horizontalOffset = i % 2 * 1.5; // Alternate between 0 and 1.5 cell offset
enemy.cellX = spawnX + horizontalOffset;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX + horizontalOffset;
enemy.currentCellY = -1 - Math.floor(i / 2) * enemySpacing; // Pair enemies vertically
} else {
if (waveType === 'tank') {
enemySpacing = 1200 / CELL_SIZE; // Maximum spacing for tank enemies (~15.8 cells)
} else if (waveType === 'immune') {
enemySpacing = 1000 / CELL_SIZE; // Large spacing for immune enemies (~13.2 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;
}
// Boss enemies give more gold and score
var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5);
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);
lives = Math.max(0, lives - 1);
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();
}
};
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