User prompt
Add new enemies: King Hippo (will eat your towers to pass!), Dragon (flies the skies and can stun your towers), Pink Kangaroo (jumps over your towers)
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'storage.achievementStats = achievementStats;' Line Number: 5032 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'storage.achievements = achievements;' Line Number: 5010 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Add new towers: Gatling Gun (fires immensly fast! Faster even than the crossbow. Type: single damage), Swordsman (has a sword spinning around him, it will deal decent damage. Type: area damage)
User prompt
Add new thingy: Achievements! Complete Achievements to get progress to fully complete the game (achievements: Builder: build 10 towers; Slayer: kill 200 enemies; power engineer: Make a tower be at max level + have the special ability; all of Em': make all types of towers be at max level+ have their special ability; Winner: defeat all waves; Loser: get defeated 3 times in a row while having spent at least 1000 gold in defenses; Farmer: collect 1000 gold worth of Farm Income; Miss: make a Mortar Tower miss; Cowardly: make a magician tower hit 2 enemies in a row with a projectile with confusion) ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Make it so that there's an offline production cap of 4 hours ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'Error: Invalid value. Only literals or 1-level deep objects/arrays containing literals are allowed.' in or related to this line: 'storage.villageLastCollection = villageLastCollection;' Line Number: 4419 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'Error: Invalid value. Only literals or 1-level deep objects/arrays containing literals are allowed.' in or related to this line: 'storage.villageLastCollection = villageLastCollection;' Line Number: 4419 ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Add new game mode: Village builder! Build your own village! Making a village can give you crystals, but you need to invest some... Fortunately, your first building [that will always be a town hall] is free! (Buildings: Town hall you can only have one town hall. Creates 5 crystals every 30 minutes, INCLUDING when you're offline; House: makes 1 crystal every 30 minutes including when you're offline; Crystal Mine creates 2 crystals every 15 minutes including when you're offline; Defense tower [can be any tower from the standard game that's isn't a farm or a church, for now, it does nothing], Barracks (also does nothing for now)) ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'ymirsHammer')' in or related to this line: 'var lives = artifacts.ymirsHammer ? 35 : 20;' Line Number: 4068
User prompt
Add new thingy: Artifacts! Gain artifacts to get permanent buffs! (Artifacts: Thunderbolt of Zeus: all projectiles stun enemies for 0.25 seconds; Ares' helmet: all projectiles deal double damage; Ymir's hammer: start with 15 extra lives). They cost Crystals that are a resouce that isnt reset after games and is gained by defeating bosses (Without using any cheats) or by upgrading a tower to MAX level including buying the special ability ↪💡 Consider importing and using the following plugins: @upit/storage.v1
User prompt
Add a title screen to the game
User prompt
Make it so that there is a path system for towers. For now it's exclusive to the archer tower (Upgrade path 1 upgrades ordered: Faster shooting, Even Faster shooting, Rapid fire (AKA special ability), Super Rapid Fire [Firing multiple bullets concecutively increases]. Upgrade path 2 upgrades ordered: Sharp arrows, Sharpest arrows [both make the arrows deal extra damage], Giant arrow [Activated ability: Make the bow tower shoot a big arrow that deals high damage and pierced thru all enemies], Even bigger arrow [Activated ability damage increased]). You can upgrade both paths but you can only have one Third upgrade (AKA only one upgrade that is ordered third or higher in a path can be bought)
User prompt
Make the targeting button under all upgrades when choosing path
User prompt
Make the targeting button under all upgrades when choosing path
User prompt
Move the targeting button to be under the selling button
User prompt
Make the buttons more spread out when choosing path
User prompt
Please fix the bug: 'TypeError: Cannot read properties of undefined (reading 'text')' in or related to this line: 'if (buttonText.text !== newText) {' Line Number: 3757
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'tint')' in or related to this line: 'buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;' Line Number: 3753
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'tint')' in or related to this line: 'buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;' Line Number: 3751
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'tint')' in or related to this line: 'buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;' Line Number: 3751
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'tint')' in or related to this line: 'buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;' Line Number: 3749
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot set properties of undefined (setting 'down')' in or related to this line: 'upgradeButton.down = function (x, y, obj) {' Line Number: 3552
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot set properties of undefined (setting 'down')' in or related to this line: 'upgradeButton.down = function (x, y, obj) {' Line Number: 3552
User prompt
Make it so that there is a path system for towers. For now it's exclusive to the archer tower (Upgrade path 1 upgrades ordered: Faster shooting, Even Faster shooting, Rapid fire (AKA special ability), Super Rapid Fire [Firing multiple bullets concecutively increases]. Upgrade path 2 upgrades ordered: Sharp arrows, Sharpest arrows [both make the arrows deal extra damage], Giant arrow [Activated ability: Make the bow tower shoot a big arrow that deals high damage and pierced thru all enemies], Even bigger arrow [Activated ability damage increased])
/****
* 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;
// Use default bullet asset initially, will be updated based on type
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Mortar projectile logic
if (self.type === 'mortar') {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Create explosion effect
var mortarEffect = new EffectIndicator(self.targetX, self.targetY, 'mortar');
game.addChild(mortarEffect);
// Area damage
var explosionRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var enemyDx = enemy.x - self.targetX;
var enemyDy = enemy.y - self.targetY;
var enemyDist = Math.sqrt(enemyDx * enemyDx + enemyDy * enemyDy);
if (enemyDist <= explosionRadius) {
// Damage falls off with distance
var damageFactor = 1 - enemyDist / explosionRadius * 0.5;
enemy.health -= oneHitKillCheat ? 999999 : self.damage * damageFactor;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
}
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
return;
}
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
LK.getSound('enemy_hit').play();
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create visual splash effect
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
// Splash damage to nearby enemies
var splashRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var splashDx = otherEnemy.x - self.targetEnemy.x;
var splashDy = otherEnemy.y - self.targetEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage)
otherEnemy.health -= oneHitKillCheat ? 999999 : self.damage * 0.5;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply slow effect
// Make slow percentage scale with tower level (default 50%, up to 80% at max level)
var slowPct = 0.5;
if (self.sourceTowerLevel !== undefined) {
// Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
self.targetEnemy.slowDuration = 180; // Reset duration
}
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
} else if (self.type === 'wizard') {
// Check if tower has charm ability
if (self.hasCharmAbility) {
// Charm effect instead of confusion
if (!self.targetEnemy.isCharmed && !self.targetEnemy.isImmune) {
self.targetEnemy.isCharmed = true;
self.targetEnemy.charmDuration = 300; // 5 seconds at 60 FPS
self.targetEnemy.originalTeam = 'enemy';
// Visual effect for charm
var charmEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'charm');
game.addChild(charmEffect);
}
} else {
// Regular confusion effect
if (Math.random() < 0.5 && !self.targetEnemy.isConfused && !self.targetEnemy.isImmune) {
self.targetEnemy.isConfused = true;
self.targetEnemy.confusionDuration = 180; // 3 seconds at 60 FPS
// Visual effect for confusion
var confuseEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'confuse');
game.addChild(confuseEffect);
}
}
} else if (self.type === 'rapid' && self.hasBleedAbility) {
// 10% chance to apply bleed
if (Math.random() < 0.1 && !self.targetEnemy.isBleeding && !self.targetEnemy.isImmune) {
self.targetEnemy.isBleeding = true;
self.targetEnemy.bleedDuration = 300; // 5 seconds at 60 FPS
self.targetEnemy.bleedDamage = 5; // 5 damage per second
// Visual effect for bleed
var bleedEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'bleed');
game.addChild(bleedEffect);
}
} else if (self.type === 'slow' && self.hasPermafrostAbility) {
// Permanent slow effect
if (!self.targetEnemy.isImmune) {
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply permanent slow
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 0.2; // 80% slow
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 999999; // Permanent
self.targetEnemy.permanentSlow = true;
}
}
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var CheatMenu = Container.expand(function () {
var self = Container.call(this);
// Background
var background = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
background.width = 800;
background.height = 600;
background.tint = 0x222222;
background.alpha = 0.95;
// Title
var titleText = new Text2('CHEATS', {
size: 80,
fill: 0xFF0000,
weight: 800
});
titleText.anchor.set(0.5, 0);
titleText.x = 0;
titleText.y = -250;
self.addChild(titleText);
// Unlimited Money Toggle
var moneyToggle = new Container();
var moneyBg = moneyToggle.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
moneyBg.width = 700;
moneyBg.height = 120;
moneyBg.tint = unlimitedMoneyCheat ? 0x00FF00 : 0x888888;
var moneyText = new Text2('Unlimited Money: ' + (unlimitedMoneyCheat ? 'ON' : 'OFF'), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
moneyText.anchor.set(0.5, 0.5);
moneyToggle.addChild(moneyText);
moneyToggle.y = -80;
self.addChild(moneyToggle);
moneyToggle.down = function () {
unlimitedMoneyCheat = !unlimitedMoneyCheat;
moneyBg.tint = unlimitedMoneyCheat ? 0x00FF00 : 0x888888;
moneyText.setText('Unlimited Money: ' + (unlimitedMoneyCheat ? 'ON' : 'OFF'));
if (unlimitedMoneyCheat) {
setGold(999999);
}
};
// One Hit Kill Toggle
var oneHitToggle = new Container();
var oneHitBg = oneHitToggle.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
oneHitBg.width = 700;
oneHitBg.height = 120;
oneHitBg.tint = oneHitKillCheat ? 0x00FF00 : 0x888888;
var oneHitText = new Text2('One Hit Kills: ' + (oneHitKillCheat ? 'ON' : 'OFF'), {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
oneHitText.anchor.set(0.5, 0.5);
oneHitToggle.addChild(oneHitText);
oneHitToggle.y = 80;
self.addChild(oneHitToggle);
oneHitToggle.down = function () {
oneHitKillCheat = !oneHitKillCheat;
oneHitBg.tint = oneHitKillCheat ? 0x00FF00 : 0x888888;
oneHitText.setText('One Hit Kills: ' + (oneHitKillCheat ? 'ON' : 'OFF'));
};
// Close button
var closeButton = new Container();
var closeBg = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBg.width = 90;
closeBg.height = 90;
closeBg.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 = background.width / 2 - 57;
closeButton.y = -background.height / 2 + 57;
self.addChild(closeButton);
closeButton.down = function () {
self.destroy();
};
// Initial position (centered)
self.x = 2048 / 2;
self.y = 2732 / 2;
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
numberLabel.visible = false;
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.visible = false;
cellGraphics.tint = 0x880000;
return;
}
numberLabel.visible = false;
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
} else {
cellGraphics.tint = 0x88 - tint << 8 | tint;
}
self.removeArrows();
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.tint = 0x008800;
numberLabel.visible = false;
break;
}
}
};
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'confuse':
effectGraphics.tint = 0xFF1493;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.2;
break;
case 'mortar':
effectGraphics.tint = 0x8B4513;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 3;
break;
case 'charm':
effectGraphics.tint = 0xFFB6C1;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'bleed':
effectGraphics.tint = 0x8B0000;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 0.8;
break;
}
effectGraphics.alpha = 0.7;
self.alpha = 0;
// Animate the effect
tween(self, {
alpha: 0.8,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 300,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
// Base enemy class for common functionality
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
self.type = type || 'normal';
self.speed = .01;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
self.isHidden = false;
self.hideDuration = 0;
self.hideTimer = 0;
self.hasSpawned = false;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'flying_horde':
self.isFlying = true;
self.maxHealth = 25; // Very weak
self.speed *= 1.5; // Fast
break;
case 'mole':
self.maxHealth = 120; // Slightly tankier
self.speed *= 0.8; // Slower when visible
self.hideTimer = 300; // 5 seconds until first hide
break;
case 'mixed':
// Mixed will be handled as a special spawner type
self.maxHealth = 1; // Placeholder, should not be attacked
self.isSpawner = true;
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Fall back to regular enemy asset if specific type asset not found
// Apply tint to differentiate enemy types
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
break;
case 'immune':
enemyGraphics.tint = 0xAA0000; // Red for immune enemies
break;
case 'flying':
enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
break;
case 'swarm':
enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
shadowGraphics.scaleX = 1.8;
shadowGraphics.scaleY = 1.8;
}
// Position shadow slightly offset
self.shadow.x = 20; // Offset right
self.shadow.y = 20; // Offset down
// Ensure shadow has the same rotation as the enemy
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle mole digging mechanics
if (self.type === 'mole') {
self.hideTimer--;
if (self.hideTimer <= 0) {
if (!self.isHidden) {
// Start hiding (go underground)
self.isHidden = true;
self.hideDuration = 90; // 1.5 seconds at 60 FPS
self.hideTimer = 300; // 5 seconds until next hide
// Double speed while hidden
if (self.originalSpeed === undefined) {
self.originalSpeed = self.speed;
}
self.speed = self.originalSpeed * 2;
// Visual effect - make semi-transparent and darker
enemyGraphics.alpha = 0.3;
enemyGraphics.tint = 0x444444;
// Hide health bar while underground
healthBarOutline.visible = false;
healthBarBG.visible = false;
healthBar.visible = false;
} else {
// Come back up (surface)
self.isHidden = false;
self.hideTimer = 300; // 5 seconds until next hide
// Reset speed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
// Restore visibility
enemyGraphics.alpha = 1;
enemyGraphics.tint = 0xFFFFFF;
// Show health bar again
healthBarOutline.visible = true;
healthBarBG.visible = true;
healthBar.visible = true;
}
} else if (self.isHidden) {
self.hideDuration--;
if (self.hideDuration <= 0) {
// Force surface if duration is over
self.isHidden = false;
self.hideTimer = 300; // Reset timer
// Reset speed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
// Restore visibility
enemyGraphics.alpha = 1;
enemyGraphics.tint = 0xFFFFFF;
// Show health bar again
healthBarOutline.visible = true;
healthBarBG.visible = true;
healthBar.visible = true;
}
}
}
// Handle flying horde spawning
if (self.type === 'flying_horde' && !self.hasSpawned && self.currentCellY >= 2) {
self.hasSpawned = true;
// Spawn 4-6 additional weak flying enemies
var spawnCount = 4 + Math.floor(Math.random() * 3);
for (var i = 0; i < spawnCount; i++) {
var newEnemy = new Enemy('flying');
newEnemy.maxHealth = 15; // Very weak
newEnemy.health = newEnemy.maxHealth;
newEnemy.speed = self.speed * (0.8 + Math.random() * 0.4); // Varied speed
// Position around the spawner
var angle = Math.PI * 2 / spawnCount * i + Math.random() * 0.5;
var distance = 50 + Math.random() * 30;
newEnemy.cellX = self.cellX;
newEnemy.cellY = self.cellY;
newEnemy.currentCellX = self.currentCellX + Math.cos(angle) * distance / CELL_SIZE;
newEnemy.currentCellY = self.currentCellY + Math.sin(angle) * distance / CELL_SIZE;
newEnemy.x = grid.x + newEnemy.currentCellX * CELL_SIZE;
newEnemy.y = grid.y + newEnemy.currentCellY * CELL_SIZE;
newEnemy.waveNumber = self.waveNumber;
// Add to flying layer
enemyLayerTop.addChild(newEnemy);
if (newEnemy.shadow) {
enemyLayerMiddle.addChild(newEnemy.shadow);
}
enemies.push(newEnemy);
}
}
// Handle confusion effect
if (self.isConfused && !self.isImmune) {
self.confusionDuration--;
if (self.confusionDuration <= 0) {
self.isConfused = false;
// Reset pathfinding when confusion ends
self.currentTarget = undefined;
}
}
// Handle charm effect
if (self.isCharmed && !self.isImmune) {
self.charmDuration--;
if (self.charmDuration <= 0) {
self.isCharmed = false;
self.originalTeam = 'enemy';
// Reset pathfinding when charm ends
self.currentTarget = undefined;
}
}
// Handle bleed effect
if (self.isBleeding && !self.isImmune) {
self.bleedDuration--;
// Take 5 damage every second (60 ticks)
if (self.bleedDuration % 60 === 0) {
self.health -= oneHitKillCheat ? 999999 : self.bleedDamage;
if (self.health <= 0) {
self.health = 0;
} else {
self.healthBar.width = self.health / self.maxHealth * 70;
}
// Visual feedback
LK.effects.flashObject(self, 0x8B0000, 100);
}
if (self.bleedDuration <= 0) {
self.isBleeding = false;
}
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed, clear any such effects
self.slowed = false;
self.slowEffect = 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
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.isCharmed) {
enemyGraphics.tint = 0xFFB6C1; // Light pink for charmed
} else if (self.isConfused) {
enemyGraphics.tint = 0xFF1493; // Pink for confused
} else if (self.isBleeding) {
enemyGraphics.tint = 0x8B0000; // Dark red for bleeding
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
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
*/
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0;
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.left.type != 1) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
processNode(node.downLeft, targetScore, node);
}
targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (enemy.isCharmed) {
// Charmed enemies attack other enemies
var nearestEnemy = null;
var nearestDist = Infinity;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== enemy && !otherEnemy.isCharmed) {
var dx = otherEnemy.cellX - enemy.cellX;
var dy = otherEnemy.cellY - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < nearestDist) {
nearestDist = dist;
nearestEnemy = otherEnemy;
}
}
}
if (nearestEnemy) {
// Move towards the nearest enemy
enemy.currentTarget = {
x: nearestEnemy.cellX,
y: nearestEnemy.cellY
};
// Deal damage if close enough
if (nearestDist < 2) {
if (LK.ticks % 60 === 0) {
// Attack once per second
nearestEnemy.health -= oneHitKillCheat ? 999999 : 10;
if (nearestEnemy.health <= 0) {
nearestEnemy.health = 0;
} else {
nearestEnemy.healthBar.width = nearestEnemy.health / nearestEnemy.maxHealth * 70;
}
LK.effects.flashObject(nearestEnemy, 0xFFB6C1, 100);
}
}
}
} else if (enemy.isConfused) {
// Confused enemies move towards the nearest tower
var nearestTower = null;
var nearestDist = Infinity;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.gridX - enemy.cellX;
var dy = tower.gridY - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < nearestDist) {
nearestDist = dist;
nearestTower = tower;
}
}
if (nearestTower) {
// Set target to move towards the tower
enemy.currentTarget = {
x: nearestTower.gridX,
y: nearestTower.gridY
};
}
} else if (!enemy.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
if (enemy.currentTarget) {
if (!enemy.isConfused && cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var LaserWall = Container.expand(function (tower1, tower2) {
var self = Container.call(this);
self.tower1 = tower1;
self.tower2 = tower2;
self.damage = 18; // Base damage per tick (increased by 2)
self.level = 1;
self.maxLevel = 6;
// Create laser visual
var laserGraphics = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
laserGraphics.tint = 0xFF0088;
laserGraphics.alpha = 0.8;
// Position between the two towers
self.updatePosition = function () {
self.x = Math.min(self.tower1.x, self.tower2.x);
self.y = self.tower1.y;
var distance = Math.abs(self.tower2.x - self.tower1.x);
laserGraphics.width = distance;
laserGraphics.height = CELL_SIZE * 0.5;
};
self.updatePosition();
// Level indicators
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSize = CELL_SIZE / 8;
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 + 2;
outlineCircle.height = dotSize + 2;
outlineCircle.tint = 0x000000;
var levelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
levelIndicator.width = dotSize;
levelIndicator.height = dotSize;
levelIndicator.tint = 0xCCCCCC;
dot.y = -CELL_SIZE * 0.4;
self.addChild(dot);
levelIndicators.push(dot);
}
self.updateLevelIndicators = function () {
var centerX = laserGraphics.width / 2;
var spacing = laserGraphics.width / (maxDots + 1);
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
dot.x = spacing * (i + 1);
var levelIndicator = dot.children[1];
if (i < self.level) {
levelIndicator.tint = 0xFFFFFF;
} else {
levelIndicator.tint = 0xFF0088;
}
}
};
self.updateLevelIndicators();
self.upgrade = function () {
if (self.level < self.maxLevel) {
var upgradeCost = Math.floor(40 * Math.pow(2, self.level - 1));
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(upgradeCost * 3.5 / 2);
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
self.damage = 18 + self.level * 24; // Increased base by 2 and maintain scaling
// Unlock special ability at level 3
if (self.level === 3) {
self.hasSpecialAbility = true;
var notification = game.addChild(new Notification("Laser Wall: Sky Piercer unlocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
self.updateLevelIndicators();
// Visual feedback
tween(laserGraphics, {
scaleY: 1.5,
alpha: 1
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(laserGraphics, {
scaleY: 1,
alpha: 0.8
}, {
duration: 200,
easing: tween.easeIn
});
}
});
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade laser wall!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.getTotalValue = function () {
var totalInvestment = 0;
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(40 * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.update = function () {
// Check for enemies passing through the laser
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Skip flying enemies unless laser wall has special ability
if (enemy.isFlying && !self.hasSpecialAbility) {
continue;
}
// Skip hidden moles unless laser wall has special ability
if (enemy.isHidden && !self.hasSpecialAbility) {
continue;
}
// Check if enemy is in the same row as the laser
if (Math.abs(enemy.y - self.y) < CELL_SIZE / 2) {
// Check if enemy is within the laser's horizontal range
if (enemy.x >= self.x && enemy.x <= self.x + laserGraphics.width) {
// Deal damage every 30 ticks (0.5 seconds)
if (LK.ticks % 30 === 0) {
enemy.health -= oneHitKillCheat ? 999999 : self.damage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Visual effect
LK.effects.flashObject(enemy, 0xFF0088, 100);
// Play laser sound occasionally
if (LK.ticks % 120 === 0) {
LK.getSound('laser_beam').play();
}
}
}
}
}
// Pulse effect
laserGraphics.alpha = 0.8 + Math.sin(LK.ticks * 0.1) * 0.2;
};
self.down = function (x, y, obj) {
// Show upgrade menu for laser wall
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
selectedTower = self;
var upgradeMenu = new LaserWallUpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 275
}, {
duration: 200,
easing: tween.backOut
});
};
return self;
});
var LaserWallUpgradeMenu = Container.expand(function (laserWall) {
var self = Container.call(this);
self.laserWall = laserWall;
self.y = 2732 + 275;
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 titleText = new Text2('Laser Wall', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0, 0);
titleText.x = -840;
titleText.y = -160;
self.addChild(titleText);
var statsText = new Text2('Level: ' + self.laserWall.level + '/' + self.laserWall.maxLevel + '\nDamage: ' + self.laserWall.damage + '/0.5s', {
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);
// Upgrade button
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.laserWall.level >= self.laserWall.maxLevel;
var upgradeCost = 0;
if (!isMaxLevel) {
upgradeCost = Math.floor(40 * Math.pow(2, self.laserWall.level - 1));
if (self.laserWall.level === self.laserWall.maxLevel - 1) {
upgradeCost = Math.floor(upgradeCost * 3.5 / 2);
}
}
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);
// Sell button
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 sellValue = getTowerSellValue(self.laserWall.getTotalValue());
var sellButtonText = new Text2('Remove Wall: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
// Close button
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 () {
if (self.laserWall.level >= self.laserWall.maxLevel) {
var notification = game.addChild(new Notification("Laser wall is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.laserWall.upgrade()) {
statsText.setText('Level: ' + self.laserWall.level + '/' + self.laserWall.maxLevel + '\nDamage: ' + self.laserWall.damage + '/0.5s');
if (self.laserWall.level >= self.laserWall.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
} else {
var newCost = Math.floor(40 * Math.pow(2, self.laserWall.level - 1));
if (self.laserWall.level === self.laserWall.maxLevel - 1) {
newCost = Math.floor(newCost * 3.5 / 2);
}
buttonText.setText('Upgrade: ' + newCost + ' gold');
}
var newSellValue = getTowerSellValue(self.laserWall.getTotalValue());
sellButtonText.setText('Remove Wall: +' + newSellValue + ' gold');
}
};
sellButton.down = function () {
var sellValue = getTowerSellValue(self.laserWall.getTotalValue());
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Laser wall removed for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Remove laser wall from game
var wallIndex = laserWalls.indexOf(self.laserWall);
if (wallIndex !== -1) {
laserWalls.splice(wallIndex, 1);
}
game.removeChild(self.laserWall);
self.destroy();
selectedTower = null;
};
closeButton.down = function () {
hideUpgradeMenu(self);
selectedTower = null;
};
self.update = function () {
if (self.laserWall.level >= self.laserWall.maxLevel) {
return;
}
var currentUpgradeCost = Math.floor(40 * Math.pow(2, self.laserWall.level - 1));
if (self.laserWall.level === self.laserWall.maxLevel - 1) {
currentUpgradeCost = Math.floor(currentUpgradeCost * 3.5 / 2);
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
};
return self;
});
var Manual = Container.expand(function () {
var self = Container.call(this);
// Manual background
var background = self.attachAsset('manualBackground', {
anchorX: 0.5,
anchorY: 0.5
});
background.alpha = 0.95;
// Title
var titleText = new Text2('TOWER DEFENSE MANUAL', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0);
titleText.x = 0;
titleText.y = -1150;
self.addChild(titleText);
// Create scrollable content container
var contentContainer = new Container();
self.addChild(contentContainer);
var yPos = -1000;
var sectionSpacing = 350;
// Scrolling variables
var isDragging = false;
var startY = 0;
var startContentY = 0;
var velocity = 0;
var minY = -1000; // Top limit
var maxY = 0; // Will be calculated based on content height
var contentHeight = 0; // Will be calculated after adding all content
// Tower Classes Section
self.createSection = function (title, items, startY) {
var sectionY = startY;
// Section title
var sectionTitle = new Text2(title, {
size: 60,
fill: 0xFFD700,
weight: 800
});
sectionTitle.anchor.set(0.5, 0);
sectionTitle.x = 0;
sectionTitle.y = sectionY;
contentContainer.addChild(sectionTitle);
sectionY += 80;
// Items
for (var i = 0; i < items.length; i++) {
var item = items[i];
// Item background
var itemBg = contentContainer.attachAsset('manualSection', {
anchorX: 0.5,
anchorY: 0
});
itemBg.x = 0;
itemBg.y = sectionY;
itemBg.height = 200;
itemBg.alpha = 0.3;
// Item name
var itemName = new Text2(item.name, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
itemName.anchor.set(0, 0);
itemName.x = -800;
itemName.y = sectionY + 20;
contentContainer.addChild(itemName);
// Item stats
var itemStats = new Text2(item.stats, {
size: 40,
fill: 0xCCCCCC,
weight: 400
});
itemStats.anchor.set(0, 0);
itemStats.x = -800;
itemStats.y = sectionY + 80;
contentContainer.addChild(itemStats);
sectionY += 220;
}
return sectionY + 50;
};
// Tower data
var towerData = [{
name: 'BOW (Default)',
stats: 'Cost: 5 gold • Damage: 10 • Range: 3 tiles • Rate: 1/s\nBalanced tower good for early game'
}, {
name: 'CROSSBOW (Rapid)',
stats: 'Cost: 15 gold • Damage: 5 • Range: 2.5 tiles • Rate: 2/s\nFast firing tower, good against swarms'
}, {
name: 'SNIPER',
stats: 'Cost: 25 gold • Damage: 25 • Range: 5 tiles • Rate: 0.67/s\nLong range, high damage, slow firing'
}, {
name: 'CANNON (Splash)',
stats: 'Cost: 35 gold • Damage: 15 • Range: 2 tiles • Rate: 0.8/s\nArea damage affects nearby enemies'
}, {
name: 'ICE WIZARD (Slow)',
stats: 'Cost: 45 gold • Damage: 8 • Range: 3.5 tiles • Rate: 1.2/s\nSlows enemies, ineffective vs immune'
}, {
name: 'FARM',
stats: 'Cost: 30 gold • Income: 10 gold/wave (+15 per level)\nGenerates passive income, no combat'
}, {
name: 'LASER BASE ALPHA',
stats: 'Cost: 40 gold • Requires 2+ in same row\nCreates laser wall, blocks ground enemies'
}, {
name: 'INFERNO (Single)',
stats: 'Cost: 50 gold • Damage: 5 DPS • Range: 4 tiles\nContinuous beam that drains health'
}, {
name: 'TRICKY WIZARD (Weakening)',
stats: 'Cost: 40 gold • Damage: 10 • Range: 3 tiles\n50% chance to confuse enemies'
}, {
name: 'CHURCH (Income)',
stats: 'Cost: 60 gold • Gives 1 life per wave\nNo combat ability, provides lives'
}, {
name: 'MORTAR (Area)',
stats: 'Cost: 55 gold • Damage: 30 • Range: 7 tiles\nHigh range area damage, min range 2 tiles'
}];
// Enemy data
var enemyData = [{
name: 'NORMAL',
stats: 'Health: 100 • Speed: Normal • Ground unit\nBasic enemy type'
}, {
name: 'FAST',
stats: 'Health: 100 • Speed: 2x Normal • Ground unit\nQuick moving enemy'
}, {
name: 'IMMUNE',
stats: 'Health: 80 • Speed: Normal • Ground unit\nImmune to slow effects'
}, {
name: 'FLYING',
stats: 'Health: 80 • Speed: Normal • Flying unit\nFlies over obstacles, immune to laser walls'
}, {
name: 'SWARM',
stats: 'Health: 50 • Speed: Normal • Ground unit\nWeaker but comes in large numbers'
}, {
name: 'FLYING HORDE',
stats: 'Health: 25 • Speed: 1.5x Normal • Flying unit\nSpawner creates multiple weak flying enemies'
}, {
name: 'MOLE DIGGER',
stats: 'Health: 120 • Speed: 0.8x/2x Normal • Ground unit\nHides underground periodically, untargetable while hidden'
}, {
name: 'BOSS',
stats: 'Health: 20x Normal • Speed: 0.7x Normal\nAppears every 10th wave, massive health'
}, {
name: 'MIXED',
stats: 'Spawns various random enemy types\nUnpredictable wave composition'
}];
// Create sections
yPos = self.createSection('TOWER TYPES', towerData, yPos);
yPos = self.createSection('ENEMY TYPES', enemyData, yPos);
// Game mechanics section
var mechanicsData = [{
name: 'TOWER TARGETING',
stats: 'First: Closest to exit • Last: Farthest from exit\nStrong: Highest HP • Crowd: Most nearby enemies'
}, {
name: 'WAVE PROGRESSION',
stats: 'Waves 1-10: Fixed types to introduce mechanics\nAfter wave 10: Random types except boss waves'
}, {
name: 'DIFFICULTY SCALING',
stats: 'Enemy health increases 12% per wave\nBoss waves every 10th wave'
}, {
name: 'TOWER UPGRADES',
stats: 'Level 1-5: Exponential cost increase\nLevel 6: Extra powerful, costs 3.5x previous'
}, {
name: 'SELLING TOWERS',
stats: 'Returns 60% of total investment\nStrategic for repositioning'
}];
yPos = self.createSection('GAME MECHANICS', mechanicsData, yPos);
// Calculate content height and set scroll limits
contentHeight = yPos + 1000; // yPos is negative, so we add back the initial offset
maxY = Math.min(0, -contentHeight + 1800); // 1800 is approximate visible height
// Close button
var closeButton = new Container();
self.addChild(closeButton);
var closeBg = closeButton.attachAsset('manualButton', {
anchorX: 0.5,
anchorY: 0.5
});
closeBg.width = 100;
closeBg.height = 100;
closeBg.tint = 0xFF4444;
var closeText = new Text2('X', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = 850;
closeButton.y = -1150;
closeButton.down = function () {
self.destroy();
};
// Touch event handlers for scrolling
self.down = function (x, y, obj) {
// Don't start scrolling if clicking on the close button
// Check if click is on close button using passed x,y coordinates
var dx = x - closeButton.x;
var dy = y - closeButton.y;
if (Math.abs(dx) < 50 && Math.abs(dy) < 50) {
return;
}
isDragging = true;
startY = y;
startContentY = contentContainer.y;
velocity = 0;
};
self.move = function (x, y, obj) {
if (isDragging) {
var deltaY = y - startY;
var newY = startContentY + deltaY;
// Apply bounds
newY = Math.max(maxY, Math.min(minY, newY));
contentContainer.y = newY;
// Calculate velocity for momentum
velocity = deltaY * 0.5;
}
};
self.up = function (x, y, obj) {
isDragging = false;
};
// Update function for momentum scrolling
self.update = function () {
if (!isDragging && Math.abs(velocity) > 0.1) {
// Apply momentum
contentContainer.y += velocity;
// Apply bounds
contentContainer.y = Math.max(maxY, Math.min(minY, contentContainer.y));
// Apply friction
velocity *= 0.92;
// Stop if velocity is too small
if (Math.abs(velocity) < 0.1) {
velocity = 0;
}
}
};
// Initial position (hidden)
self.x = 2048 / 2; // Center horizontally
self.y = 2732 + 1200;
// Animate in
tween(self, {
y: 2732 / 2
}, {
duration: 300,
easing: tween.backOut
});
return self;
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'default';
// Use specific tower asset based on type
var towerAssetId = 'tower_' + self.towerType;
if (self.towerType === 'default') {
towerAssetId = 'tower_default';
}
// Increase size of base for easier touch
var baseGraphics = self.attachAsset(towerAssetId, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
// No need to tint anymore as we have specific assets
var towerCost = getTowerCost(self.towerType);
// Get display name for tower type
var displayName = self.towerType;
switch (self.towerType) {
case 'default':
displayName = 'Bow';
break;
case 'rapid':
displayName = 'Crossbow';
break;
case 'splash':
displayName = 'Cannon';
break;
case 'slow':
displayName = 'Ice Wizard';
break;
case 'farm':
displayName = 'Farm';
break;
case 'laser':
displayName = 'Laser Base';
break;
case 'inferno':
displayName = 'Inferno';
break;
case 'wizard':
displayName = 'Tricky Wizard';
break;
case 'church':
displayName = 'Church';
break;
case 'mortar':
displayName = 'Mortar';
break;
}
// Add shadow for tower type label
var typeLabelShadow = new Text2(displayName, {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -20 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var typeLabel = new Text2(displayName, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -20; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var 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 TitleScreen = Container.expand(function () {
var self = Container.call(this);
// Full screen background
var background = self.attachAsset('manualBackground', {
anchorX: 0.5,
anchorY: 0.5
});
background.width = 2048;
background.height = 2732;
background.tint = 0x1a1a1a;
// Title text with shadow
var titleShadow = new Text2('TOWER DEFENSE', {
size: 120,
fill: 0x000000,
weight: 800
});
titleShadow.anchor.set(0.5, 0.5);
titleShadow.x = 6;
titleShadow.y = -400 + 6;
self.addChild(titleShadow);
var titleText = new Text2('TOWER DEFENSE', {
size: 120,
fill: 0xFFD700,
weight: 800
});
titleText.anchor.set(0.5, 0.5);
titleText.y = -400;
self.addChild(titleText);
// Subtitle
var subtitleText = new Text2('Defend your base from waves of enemies!', {
size: 50,
fill: 0xCCCCCC,
weight: 400
});
subtitleText.anchor.set(0.5, 0.5);
subtitleText.y = -250;
self.addChild(subtitleText);
// Start button
var startButton = new Container();
var startBg = startButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBg.width = 600;
startBg.height = 200;
startBg.tint = 0x00AA00;
var startTextShadow = new Text2('START GAME', {
size: 80,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startButton.addChild(startTextShadow);
var startText = new Text2('START GAME', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startButton.addChild(startText);
startButton.y = 100;
self.addChild(startButton);
// Animate button pulse
self.pulseTimer = 0;
// Button click handler
startButton.down = function () {
LK.getSound('button_click').play();
// Fade out the title screen
tween(self, {
alpha: 0
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
self.destroy();
}
});
};
// Instructions text
var instructionsText = new Text2('Tap to place towers • Upgrade to increase power', {
size: 40,
fill: 0x888888,
weight: 400
});
instructionsText.anchor.set(0.5, 0.5);
instructionsText.y = 400;
self.addChild(instructionsText);
// Version text
var versionText = new Text2('v1.0', {
size: 30,
fill: 0x666666,
weight: 400
});
versionText.anchor.set(1, 1);
versionText.x = 900;
versionText.y = 1300;
self.addChild(versionText);
self.update = function () {
// Pulse animation for start button
self.pulseTimer += 0.05;
var scale = 1 + Math.sin(self.pulseTimer) * 0.05;
startButton.scaleX = scale;
startButton.scaleY = scale;
};
// Center on screen
self.x = 2048 / 2;
self.y = 2732 / 2;
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
self.targetingMode = 'first'; // Options: 'first', 'last', 'strong', 'crowd'
self.hasSpecialAbility = false; // Unlocked at max level
self.specialAbilityActive = false;
self.specialAbilityTimer = 0;
self.rapidBurstCount = 0; // For archer rapid fire
self.infernoTargets = []; // For inferno multi-target
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
switch (self.id) {
case 'sniper':
// Sniper: base 5, +0.8 per level, infinite range with special ability
if (self.hasSpecialAbility) {
return 999 * CELL_SIZE; // Effectively infinite range
}
return (5 + (self.level - 1) * 0.8) * CELL_SIZE;
case 'splash':
// Splash: base 2, +0.2 per level (max ~4 blocks at max level)
return (2 + (self.level - 1) * 0.2) * CELL_SIZE;
case 'rapid':
// Rapid: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// Slow: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'farm':
// Farm has no range
return 0;
case 'laser':
// Laser has no shooting range
return 0;
case 'inferno':
// Inferno: base 4, +0.3 per level
return (4 + (self.level - 1) * 0.3) * CELL_SIZE;
case 'wizard':
// Wizard: base 3, +0.4 per level
return (3 + (self.level - 1) * 0.4) * CELL_SIZE;
case 'church':
// Church has no range
return 0;
case 'mortar':
// Mortar: base 7, +0.5 per level (high range)
return (7 + (self.level - 1) * 0.5) * CELL_SIZE;
default:
// Default: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
switch (self.id) {
case 'rapid':
self.fireRate = 30;
self.damage = 5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 75;
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'farm':
self.fireRate = 999999; // Farm doesn't shoot
self.damage = 0;
self.range = 0;
self.bulletSpeed = 0;
break;
case 'laser':
self.fireRate = 999999; // Laser doesn't shoot normally
self.damage = 0;
self.range = 0;
self.bulletSpeed = 0;
break;
case 'inferno':
self.fireRate = 999999; // Inferno uses continuous beam
self.damage = 5; // Base DPS
self.range = 4 * CELL_SIZE;
self.bulletSpeed = 0;
break;
case 'wizard':
self.fireRate = 120; // Slower fire rate
self.damage = 10;
self.range = 3 * CELL_SIZE;
self.bulletSpeed = 6;
break;
case 'church':
self.fireRate = 999999; // Church doesn't shoot
self.damage = 0;
self.range = 0;
self.bulletSpeed = 0;
break;
case 'mortar':
self.fireRate = 180; // Very slow fire rate
self.damage = 30;
self.range = 7 * CELL_SIZE;
self.minRange = 2 * CELL_SIZE; // Can't hit close enemies
self.bulletSpeed = 3;
break;
}
// Use specific tower asset based on type
var towerAssetId = 'tower_' + self.id;
if (self.id === 'default') {
towerAssetId = 'tower_default';
}
var baseGraphics = self.attachAsset(towerAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
// No need to tint anymore as we have specific assets
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);
// Use specific defense asset based on type
var defenseAssetId = 'defense_' + self.id;
if (self.id === 'default') {
defenseAssetId = 'defense_default';
}
var gunGraphics = gunContainer.attachAsset(defenseAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'farm':
towerLevelIndicator.tint = 0xFFD700; // Gold color
break;
case 'laser':
towerLevelIndicator.tint = 0xFF0088; // Pink color
break;
case 'inferno':
towerLevelIndicator.tint = 0xFF4500; // Orange-red
break;
case 'wizard':
towerLevelIndicator.tint = 0xFF1493; // Deep pink
break;
case 'church':
towerLevelIndicator.tint = 0xFFD700; // Gold
break;
case 'mortar':
towerLevelIndicator.tint = 0x8B4513; // Saddle brown
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
// Create inferno beam visual if this is an inferno tower
if (self.id === 'inferno') {
self.infernoBeam = self.attachAsset('infernoBeam', {
anchorX: 0,
anchorY: 0.5
});
self.infernoBeam.visible = false;
self.infernoBeam.alpha = 0.8;
self.infernoDamageTimer = 0;
}
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.getSpecialAbilityName = function () {
switch (self.id) {
case 'default':
return 'Rapid Fire';
case 'rapid':
return 'Bleeding Shots';
case 'sniper':
return 'Infinite Range';
case 'inferno':
return 'Triple Beam';
case 'splash':
return 'Mega Blast';
case 'laser':
return 'Sky Piercer';
case 'mortar':
return 'Quad Shot';
case 'slow':
return 'Permafrost';
case 'wizard':
return 'Charm';
case 'farm':
return 'Gold Boost';
case 'church':
return 'Divine Intervention';
default:
return null;
}
};
self.getSpecialAbilityCost = function () {
// Special ability costs 3x the base tower cost
return getTowerCost(self.id) * 3;
};
self.unlockSpecialAbility = function () {
if (self.level >= 3 && !self.hasSpecialAbility) {
var cost = self.getSpecialAbilityCost();
if (gold >= cost) {
setGold(gold - cost);
self.hasSpecialAbility = true;
// Show special ability unlocked notification
var abilityName = self.getSpecialAbilityName();
if (abilityName) {
var notification = game.addChild(new Notification("Special Ability Unlocked: " + abilityName));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold for special ability!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
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++;
LK.getSound('tower_upgrade').play();
// Special ability is unlocked separately at level 3 (no longer automatic)
// Special ability unlocking is now handled by separate purchase button
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect
self.damage = 5 + self.level * 10; // double the effect
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else if (self.id === 'inferno') {
// Inferno tower damage scales differently (DPS)
self.damage = 5 + self.level * 3;
} else if (self.id === 'wizard') {
if (self.level === self.maxLevel) {
self.fireRate = Math.max(30, 120 - self.level * 30);
self.damage = 10 + self.level * 15;
self.bulletSpeed = 6 + self.level * 1.5;
} else {
self.fireRate = Math.max(60, 120 - self.level * 10);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 6 + self.level * 0.5;
}
} else if (self.id === 'mortar') {
if (self.level === self.maxLevel) {
self.fireRate = Math.max(60, 180 - self.level * 40);
self.damage = 30 + self.level * 25;
self.bulletSpeed = 3 + self.level * 1.0;
} else {
self.fireRate = Math.max(120, 180 - self.level * 10);
self.damage = 30 + self.level * 8;
self.bulletSpeed = 3 + self.level * 0.3;
}
} 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 enemiesInRange = [];
// First, collect all enemies in range
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 and not hidden underground
if (distance <= self.getRange() && !enemy.isHidden) {
// Mortar can't hit enemies that are too close
if (self.id === 'mortar' && distance < self.minRange) {
continue;
}
enemiesInRange.push({
enemy: enemy,
distance: distance,
dx: dx,
dy: dy
});
}
}
if (enemiesInRange.length === 0) {
self.targetEnemy = null;
return null;
}
var targetEnemy = null;
switch (self.targetingMode) {
case 'first':
// Target enemy closest to the base (lowest path score or closest to goal for flying)
var lowestScore = Infinity;
for (var i = 0; i < enemiesInRange.length; i++) {
var enemyData = enemiesInRange[i];
var enemy = enemyData.enemy;
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 < lowestScore) {
lowestScore = distToGoal;
targetEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (enemyData.distance < lowestScore) {
lowestScore = enemyData.distance;
targetEnemy = enemy;
}
}
} else {
// For ground enemies, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < lowestScore) {
lowestScore = cell.score;
targetEnemy = enemy;
}
}
}
}
break;
case 'last':
// Target enemy farthest from the base (highest path score)
var highestScore = -Infinity;
for (var i = 0; i < enemiesInRange.length; i++) {
var enemyData = enemiesInRange[i];
var enemy = enemyData.enemy;
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal (inverse)
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 inverse distance to goal as score
if (distToGoal > highestScore) {
highestScore = distToGoal;
targetEnemy = enemy;
}
}
} else {
// For ground enemies
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Higher score means farther from exit
if (cell.score > highestScore) {
highestScore = cell.score;
targetEnemy = enemy;
}
}
}
}
break;
case 'strong':
// Target enemy with the most HP
var mostHP = -1;
for (var i = 0; i < enemiesInRange.length; i++) {
var enemy = enemiesInRange[i].enemy;
if (enemy.health > mostHP) {
mostHP = enemy.health;
targetEnemy = enemy;
}
}
break;
case 'crowd':
// Target enemy with the most other enemies nearby
var maxCrowdScore = -1;
for (var i = 0; i < enemiesInRange.length; i++) {
var enemyData = enemiesInRange[i];
var enemy = enemyData.enemy;
var crowdScore = 0;
// Count enemies within 2 cells of this enemy
for (var j = 0; j < enemies.length; j++) {
if (enemies[j] !== enemy) {
var edx = enemies[j].x - enemy.x;
var edy = enemies[j].y - enemy.y;
var edist = Math.sqrt(edx * edx + edy * edy);
if (edist <= CELL_SIZE * 2) {
crowdScore++;
}
}
}
if (crowdScore > maxCrowdScore) {
maxCrowdScore = crowdScore;
targetEnemy = enemy;
}
}
break;
}
// For inferno tower with special ability, find up to 3 targets
if (self.id === 'inferno' && self.hasSpecialAbility && enemiesInRange.length > 0) {
self.infernoTargets = [];
// Sort enemies by distance
enemiesInRange.sort(function (a, b) {
return a.distance - b.distance;
});
// Get up to 3 targets
for (var i = 0; i < Math.min(3, enemiesInRange.length); i++) {
self.infernoTargets.push(enemiesInRange[i].enemy);
}
return self.infernoTargets[0]; // Return primary target
}
return targetEnemy;
};
self.update = function () {
// Farm towers don't target or shoot
if (self.id === 'farm') {
return;
}
// Laser towers are handled separately
if (self.id === 'laser') {
return;
}
// Church towers don't shoot
if (self.id === 'church') {
return;
}
// Inferno tower uses continuous beam
if (self.id === 'inferno') {
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
// Show and position beam
self.infernoBeam.visible = true;
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
var distance = Math.sqrt(dx * dx + dy * dy);
self.infernoBeam.rotation = angle;
self.infernoBeam.width = distance;
gunContainer.rotation = angle;
// Deal damage every 10 ticks
self.infernoDamageTimer++;
if (self.infernoDamageTimer >= 10) {
self.infernoDamageTimer = 0;
// Calculate damage based on level
var damagePerTick = oneHitKillCheat ? 999999 : self.damage + self.level * 3;
self.targetEnemy.health -= damagePerTick;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Visual effect
LK.effects.flashObject(self.targetEnemy, 0xFF4500, 100);
// Handle multi-target ability
if (self.hasSpecialAbility && self.infernoTargets.length > 1) {
// Damage additional targets (50% damage)
for (var i = 1; i < self.infernoTargets.length; i++) {
var additionalTarget = self.infernoTargets[i];
if (additionalTarget && additionalTarget.parent && !additionalTarget.isHidden) {
additionalTarget.health -= oneHitKillCheat ? 999999 : damagePerTick * 0.5;
if (additionalTarget.health <= 0) {
additionalTarget.health = 0;
} else {
additionalTarget.healthBar.width = additionalTarget.health / additionalTarget.maxHealth * 70;
}
LK.effects.flashObject(additionalTarget, 0xFF4500, 100);
}
}
}
}
} else {
self.infernoBeam.visible = false;
self.infernoDamageTimer = 0;
self.infernoTargets = [];
}
return;
}
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
// Handle rapid burst fire for archer
if (self.rapidBurstCount > 0) {
if (LK.ticks - self.lastFired >= 6) {
// Very fast fire rate during burst
self.fire();
self.lastFired = LK.ticks;
self.rapidBurstCount--;
if (self.rapidBurstCount === 0) {
self.specialAbilityActive = false;
}
}
} else 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 - 325
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
// Handle archer rapid fire ability
if (self.id === 'default' && self.hasSpecialAbility && Math.random() < 0.25) {
self.rapidBurstCount = 5;
self.specialAbilityActive = true;
}
// Handle mortar quad shot ability
if (self.id === 'mortar' && self.hasSpecialAbility) {
// Fire 4 projectiles at once
if (self.targetEnemy) {
for (var i = 0; i < 4; i++) {
var offsetAngle = (i - 1.5) * 0.3; // Spread the shots
var targetX = self.targetEnemy.x + Math.cos(offsetAngle) * 50;
var targetY = self.targetEnemy.y + Math.sin(offsetAngle) * 50;
var bulletX = self.x + Math.cos(gunContainer.rotation + offsetAngle) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation + offsetAngle) * 40;
var bullet = new Bullet(bulletX, bulletY, null, self.damage, self.bulletSpeed);
bullet.type = 'mortar';
bullet.targetX = targetX;
bullet.targetY = targetY;
// Replace bullet graphics
bullet.removeChild(bullet.children[0]);
var mortarGraphics = bullet.attachAsset('bullet_mortar', {
anchorX: 0.5,
anchorY: 0.5
});
game.addChild(bullet);
bullets.push(bullet);
}
}
return;
}
// Handle cannon mega blast ability
if (self.id === 'splash' && self.hasSpecialAbility && self.specialAbilityTimer <= 0) {
self.fireRate = 25; // Triple fire rate
self.specialAbilityTimer = 180; // 3 second duration
}
if (self.id === 'splash' && self.specialAbilityTimer > 0) {
self.specialAbilityTimer--;
if (self.specialAbilityTimer === 0) {
self.fireRate = 75; // Reset to normal
}
}
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bulletDamage = oneHitKillCheat ? 999999 : self.damage;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, bulletDamage, 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;
bullet.hasPermafrostAbility = self.hasSpecialAbility;
} else if (self.id === 'rapid') {
bullet.hasBleedAbility = self.hasSpecialAbility;
} else if (self.id === 'wizard') {
bullet.hasCharmAbility = self.hasSpecialAbility;
}
// For mortar, store target position instead of enemy
if (self.id === 'mortar') {
bullet.targetX = self.targetEnemy.x;
bullet.targetY = self.targetEnemy.y;
bullet.targetEnemy = null; // Mortar targets location, not enemy
}
// Replace bullet graphics with tower-specific asset
var bulletAssetId = 'bullet';
if (self.id !== 'default') {
bulletAssetId = 'bullet_' + self.id;
}
// Remove the old generic bullet graphic
bullet.removeChild(bullet.children[0]);
// Attach the specific bullet asset
var specificBulletGraphics = bullet.attachAsset(bulletAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// Play shooting sound based on tower type
if (self.id === 'sniper') {
LK.getSound('tower_shoot').play();
} else if (self.id === 'splash' || self.id === 'mortar') {
LK.getSound('explosion').play();
} else if (self.id === 'slow') {
LK.getSound('freeze').play();
} else if (self.id === 'wizard') {
LK.getSound('charm').play();
} else {
LK.getSound('tower_shoot').play();
}
// --- 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) {
cell.type = 1;
}
}
}
self.refreshCellsInRange();
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'default';
self.hasEnoughGold = true;
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3;
var previewGraphics = self.attachAsset('towerpreview', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
break;
case 'sniper':
previewGraphics.tint = 0xFF5500;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
break;
case 'slow':
previewGraphics.tint = 0x9900FF;
break;
case 'farm':
previewGraphics.tint = 0xFFD700; // Gold color
break;
case 'laser':
previewGraphics.tint = 0xFF0088; // Pink color
break;
case 'inferno':
previewGraphics.tint = 0xFF4500; // Orange-red
break;
case 'wizard':
previewGraphics.tint = 0xFF1493; // Deep pink
break;
case 'church':
previewGraphics.tint = 0xFFD700; // Gold
break;
case 'mortar':
previewGraphics.tint = 0x8B4513; // Saddle brown
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (!cell || cell.type !== 0) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
self.canPlace = validGridPlacement && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 325;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 700;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
// Get display name for tower type
var displayName = self.tower.id;
switch (self.tower.id) {
case 'default':
displayName = 'Bow';
break;
case 'rapid':
displayName = 'Crossbow';
break;
case 'splash':
displayName = 'Cannon';
break;
case 'slow':
displayName = 'Ice Wizard';
break;
case 'farm':
displayName = 'Farm';
break;
case 'laser':
displayName = 'Laser Base Alpha';
break;
case 'inferno':
displayName = 'Inferno';
break;
case 'wizard':
displayName = 'Tricky Wizard';
break;
case 'church':
displayName = 'Church';
break;
case 'mortar':
displayName = 'Mortar';
break;
}
var towerTypeText = new Text2(displayName + ' 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(self.tower.id === 'inferno' ? 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + (self.tower.damage + self.tower.level * 3) + ' DPS\nRange: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1) + ' tiles' : self.tower.id === 'church' ? 'Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nLives per wave: ' + self.tower.level + '\nNo combat ability' : '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);
// Special ability button
var specialButton = new Container();
buttonsContainer.addChild(specialButton);
var specialBackground = specialButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
specialBackground.width = 500;
specialBackground.height = 150;
var hasAbility = self.tower.hasSpecialAbility;
var abilityName = self.tower.getSpecialAbilityName();
var canUnlock = self.tower.level >= 3 && !hasAbility;
var abilityCost = canUnlock ? self.tower.getSpecialAbilityCost() : 0;
var lockText = self.tower.level < 3 ? 'Locked (Lv 3)' : canUnlock ? 'Buy: ' + abilityCost + ' gold' : 'Locked';
specialBackground.tint = hasAbility ? 0x9400D3 : canUnlock ? gold >= abilityCost ? 0x00AA00 : 0x888888 : 0x444444;
var specialText = new Text2(hasAbility ? abilityName : lockText, {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
specialText.anchor.set(0.5, 0.5);
specialButton.addChild(specialText);
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);
// Targeting mode button
var targetingButton = new Container();
buttonsContainer.addChild(targetingButton);
var targetingBackground = targetingButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
targetingBackground.width = 500;
targetingBackground.height = 150;
targetingBackground.tint = 0x0088FF;
var targetingModeNames = {
'first': 'First',
'last': 'Last',
'strong': 'Strong',
'crowd': 'Crowd'
};
var targetingText = new Text2('Target: ' + targetingModeNames[self.tower.targetingMode], {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
targetingText.anchor.set(0.5, 0.5);
targetingButton.addChild(targetingText);
upgradeButton.y = -210;
specialButton.y = -70;
targetingButton.y = 70;
sellButton.y = 210;
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) {
LK.getSound('button_click').play();
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));
}
if (self.tower.id === 'inferno') {
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + (self.tower.damage + self.tower.level * 3) + ' DPS\nRange: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1) + ' tiles');
} else if (self.tower.id === 'church') {
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nLives per wave: ' + self.tower.level + '\nNo combat ability');
} else {
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
}
// Update special ability button when upgrading
if (self.tower.hasSpecialAbility && !hasAbility) {
specialBackground.tint = 0x9400D3;
specialText.setText(self.tower.getSpecialAbilityName());
}
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
});
}
});
}
};
specialButton.down = function () {
if (self.tower.hasSpecialAbility) {
var notification = game.addChild(new Notification("Special Ability: " + self.tower.getSpecialAbilityName()));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (self.tower.level >= 3) {
// Try to purchase special ability
if (self.tower.unlockSpecialAbility()) {
// Update button appearance after successful purchase
specialBackground.tint = 0x9400D3;
specialText.setText(self.tower.getSpecialAbilityName());
}
} else {
var notification = game.addChild(new Notification("Reach level 3 to unlock special ability!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
targetingButton.down = function () {
// Cycle through targeting modes
var modes = ['first', 'last', 'strong', 'crowd'];
var currentIndex = modes.indexOf(self.tower.targetingMode);
var nextIndex = (currentIndex + 1) % modes.length;
self.tower.targetingMode = modes[nextIndex];
var targetingModeNames = {
'first': 'First',
'last': 'Last',
'strong': 'Strong',
'crowd': 'Crowd'
};
targetingText.setText('Target: ' + targetingModeNames[self.tower.targetingMode]);
// Visual feedback
tween(targetingButton, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(targetingButton, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
// Update laser walls if we removed a laser tower
if (self.tower.id === 'laser') {
updateLaserWalls();
}
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;
}
} else {
// 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);
}
}
// Update special ability button
if (!self.tower.hasSpecialAbility && self.tower.level >= 3) {
var abilityCost = self.tower.getSpecialAbilityCost();
var canAffordAbility = gold >= abilityCost;
specialBackground.tint = canAffordAbility ? 0x00AA00 : 0x888888;
var newSpecialText = 'Buy: ' + abilityCost + ' gold';
if (specialText.text !== newSpecialText) {
specialText.setText(newSpecialText);
}
}
};
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;
LK.getSound('wave_start').play();
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (i === 5) {
block.tint = 0x00FFFF;
waveType = "Flying Horde";
enemyType = "flying_horde";
enemyCount = 3; // Few spawners that create many
} else if (i === 6) {
block.tint = 0x8B4513;
waveType = "Mole Digger";
enemyType = "mole";
enemyCount = 8;
} else if (i === 7) {
block.tint = 0xFFFFFF;
waveType = "Mixed";
enemyType = "mixed";
enemyCount = 12;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0xFFFF00;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Boss Normal";
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to the color of the wave type
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
break;
case 'fast':
block.tint = 0x00AAFF;
break;
case 'immune':
block.tint = 0xAA0000;
break;
case 'flying':
block.tint = 0xFFFF00;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if (i + 1 > 10) {
// After wave 10, all non-boss waves are completely random
var randomWaveTypes = [{
type: 'normal',
name: 'Normal',
tint: 0xAAAAAA,
count: 10
}, {
type: 'fast',
name: 'Fast',
tint: 0x00AAFF,
count: 10
}, {
type: 'immune',
name: 'Immune',
tint: 0xAA0000,
count: 10
}, {
type: 'flying',
name: 'Flying',
tint: 0xFFFF00,
count: 10
}, {
type: 'swarm',
name: 'Swarm',
tint: 0xFF00FF,
count: 30
}, {
type: 'flying_horde',
name: 'Flying Horde',
tint: 0x00FFFF,
count: 3
}, {
type: 'mole',
name: 'Mole Digger',
tint: 0x8B4513,
count: 8
}, {
type: 'mixed',
name: 'Mixed',
tint: 0xFFFFFF,
count: 12
}];
var randomChoice = randomWaveTypes[Math.floor(Math.random() * randomWaveTypes.length)];
enemyType = randomChoice.type;
waveType = randomChoice.name;
block.tint = randomChoice.tint;
enemyCount = randomChoice.count;
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if ((i + 1) % 8 === 0) {
// Every 8th non-boss wave is flying horde
block.tint = 0x00FFFF;
waveType = "Flying Horde";
enemyType = "flying_horde";
enemyCount = 3;
} else if ((i + 1) % 9 === 0) {
// Every 9th non-boss wave is mole digger
block.tint = 0x8B4513;
waveType = "Mole Digger";
enemyType = "mole";
enemyCount = 8;
} else if ((i + 1) % 6 === 0) {
// Every 6th non-boss wave is mixed
block.tint = 0xFFFFFF;
waveType = "Mixed";
enemyType = "mixed";
enemyCount = 12;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave && enemyType !== 'swarm') {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 4;
waveTypeShadow.y = 4;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "normal";
}
// If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
// then we should return a different boss type
var waveType = self.waveTypes[waveNumber - 1];
return waveType;
};
// Get enemy count for a specific wave number
self.getEnemyCount = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return 10;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
// Check if title screen is still active in the game
var titleScreenActive = false;
if (game && game.children) {
titleScreenActive = game.children.some(function (child) {
return child instanceof TitleScreen;
});
}
if (titleScreenActive || !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: 0x333333
});
/****
* Game Code
****/
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 325
}, {
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 laserWalls = [];
var selectedTower = null;
var gold = 100;
var lives = 20;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
// Cheat variables
var unlimitedMoneyCheat = false;
var oneHitKillCheat = false;
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
goldText.visible = false;
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
livesText.visible = false;
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
scoreText.visible = false;
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(livesText);
LK.gui.top.addChild(scoreText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
}
function setGold(value) {
if (unlimitedMoneyCheat) {
gold = 999999;
} else {
gold = value;
}
updateUI();
}
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
var cells = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cells.push({
cell: cell,
originalType: cell.type
});
cell.type = 1;
}
}
}
var blocked = grid.pathFind();
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
grid.pathFind();
grid.renderDebug();
return blocked;
}
function getTowerCost(towerType) {
var cost = 5;
switch (towerType) {
case 'rapid':
cost = 15;
break;
case 'sniper':
cost = 25;
break;
case 'splash':
cost = 35;
break;
case 'slow':
cost = 45;
break;
case 'farm':
cost = 30;
break;
case 'laser':
cost = 40;
break;
case 'inferno':
cost = 50;
break;
case 'wizard':
cost = 40;
break;
case 'church':
cost = 60;
break;
case 'mortar':
cost = 55;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function updateLaserWalls() {
// Remove all existing laser walls
for (var i = laserWalls.length - 1; i >= 0; i--) {
game.removeChild(laserWalls[i]);
}
laserWalls = [];
// Group laser towers by row
var laserTowersByRow = {};
for (var i = 0; i < towers.length; i++) {
if (towers[i].id === 'laser') {
var row = towers[i].gridY;
if (!laserTowersByRow[row]) {
laserTowersByRow[row] = [];
}
laserTowersByRow[row].push(towers[i]);
}
}
// Create laser walls for rows with 2 or more laser towers
for (var row in laserTowersByRow) {
var rowTowers = laserTowersByRow[row];
if (rowTowers.length >= 2) {
// Sort towers by x position
rowTowers.sort(function (a, b) {
return a.x - b.x;
});
// Create walls between adjacent towers
for (var i = 0; i < rowTowers.length - 1; i++) {
var wall = new LaserWall(rowTowers[i], rowTowers[i + 1]);
game.addChild(wall);
laserWalls.push(wall);
}
}
}
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
LK.getSound('place_tower').play();
grid.pathFind();
grid.renderDebug();
// Update laser walls if we placed a laser tower
if (towerType === 'laser') {
updateLaserWalls();
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
LK.getSound('error').play();
return false;
}
}
game.down = function (x, y, obj) {
// Check if title screen is active
var titleScreenActive = game.children.some(function (child) {
return child instanceof TitleScreen;
});
if (titleScreenActive) {
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Check if clicking on a class button
for (var i = 0; i < classButtons.length; i++) {
var button = classButtons[i];
if (x >= button.x - button.width / 2 && x <= button.x + button.width / 2 && y >= button.y - button.height / 2 && y <= button.y + button.height / 2) {
// Class button was clicked, let it handle the event
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) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
waveIndicator.visible = false;
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);
// Tower classes configuration
var towerClasses = {
'Single Damage': {
towers: ['default', 'rapid', 'sniper', 'inferno'],
color: 0x4488FF
},
'Area Damage': {
towers: ['splash', 'laser', 'mortar'],
color: 0xFF8844
},
'Weakening': {
towers: ['slow', 'wizard'],
color: 0x8844FF
},
'Income/Lives': {
towers: ['farm', 'church'],
color: 0x44FF88
}
};
var sourceTowers = [];
var classButtons = [];
var currentClass = null;
var towerSelectionContainer = new Container();
towerSelectionContainer.visible = false;
game.addChild(towerSelectionContainer);
// Create class buttons
var classButtonWidth = 400;
var classButtonHeight = 80;
var classButtonSpacing = 20;
var classNames = Object.keys(towerClasses);
var totalClassWidth = classNames.length * classButtonWidth + (classNames.length - 1) * classButtonSpacing;
var classStartX = 2048 / 2 - totalClassWidth / 2 + classButtonWidth / 2;
var classButtonY = 2732 - CELL_SIZE * 3 - 180;
for (var i = 0; i < classNames.length; i++) {
var className = classNames[i];
var classData = towerClasses[className];
var classButton = new Container();
classButton.className = className;
classButton.classData = classData;
var buttonBg = classButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = classButtonWidth;
buttonBg.height = classButtonHeight;
buttonBg.tint = classData.color;
buttonBg.alpha = 0.7;
var buttonText = new Text2(className, {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
classButton.addChild(buttonText);
classButton.x = classStartX + i * (classButtonWidth + classButtonSpacing);
classButton.y = classButtonY;
classButton.down = function () {
selectTowerClass(this.className);
};
towerSelectionContainer.addChild(classButton);
classButtons.push(classButton);
}
// Function to select a tower class
function selectTowerClass(className) {
// Update class button appearances
for (var i = 0; i < classButtons.length; i++) {
var button = classButtons[i];
var bg = button.children[0];
if (button.className === className) {
bg.alpha = 1.0;
bg.tint = button.classData.color;
} else {
bg.alpha = 0.3;
bg.tint = 0x666666;
}
}
// Clear existing source towers
for (var i = 0; i < sourceTowers.length; i++) {
towerLayer.removeChild(sourceTowers[i]);
}
sourceTowers = [];
// Create towers for selected class
currentClass = className;
var classData = towerClasses[className];
var towerTypes = classData.towers;
var towerSpacing = 300;
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);
}
}
// Select first class by default
selectTowerClass(classNames[0]);
// Create title screen
var titleScreen = new TitleScreen();
game.addChild(titleScreen);
// Play theme music after title screen is dismissed
titleScreen.onDestroy = function () {
LK.playMusic('game_theme');
// Show UI elements
goldText.visible = true;
livesText.visible = true;
scoreText.visible = true;
waveIndicator.visible = true;
towerSelectionContainer.visible = true;
manualButton.visible = true;
cheatButton.visible = true;
};
// Create manual button
var manualButton = new Container();
var manualBg = manualButton.attachAsset('manualButton', {
anchorX: 0.5,
anchorY: 0.5
});
manualBg.tint = 0x4CAF50;
var manualText = new Text2('?', {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
manualText.anchor.set(0.5, 0.5);
manualButton.addChild(manualText);
manualButton.x = 2048 - 80;
manualButton.y = 150;
manualButton.visible = false;
manualButton.down = function () {
var manual = new Manual();
game.addChild(manual);
};
game.addChild(manualButton);
// Create cheat button
var cheatButton = new Container();
var cheatBg = cheatButton.attachAsset('manualButton', {
anchorX: 0.5,
anchorY: 0.5
});
cheatBg.tint = 0xFF0000;
var cheatText = new Text2('C', {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
cheatText.anchor.set(0.5, 0.5);
cheatButton.addChild(cheatText);
cheatButton.x = 2048 - 200;
cheatButton.y = 150;
cheatButton.visible = false;
cheatButton.down = function () {
var cheatMenu = new CheatMenu();
game.addChild(cheatMenu);
};
game.addChild(cheatButton);
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 % 10 === 0 && currentWave > 0;
if (isBossWave && waveType !== 'swarm') {
// Boss waves have just 1 enemy regardless of what the wave indicator says
enemyCount = 1;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
LK.getSound('boss_spawn').play();
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy;
if (waveType === 'mixed') {
// Mixed wave spawns random enemy types
var mixedTypes = ['normal', 'fast', 'immune', 'flying', 'swarm', 'mole'];
var randomType = mixedTypes[Math.floor(Math.random() * mixedTypes.length)];
enemy = new Enemy(randomType);
} else {
enemy = new Enemy(waveType);
}
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use exponential scaling for health
var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// All enemy types now spawn in the middle 6 tiles at the top spacing
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
// Find a column that isn't occupied by another enemy that's not yet in view
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
// Check if any enemy is already in this column but not yet in view
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
// If all columns are occupied, use original random method
var spawnX;
if (availableColumns.length > 0) {
// Choose a random unoccupied column
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
// Fallback to random if all columns are occupied
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
}
var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
// Give income from all farm towers
var totalFarmIncome = 0;
for (var i = 0; i < towers.length; i++) {
if (towers[i].id === 'farm') {
// Base income 10, +15 per level
var farmIncome = 10 + (towers[i].level - 1) * 15;
totalFarmIncome += farmIncome;
}
}
if (totalFarmIncome > 0) {
setGold(gold + totalFarmIncome);
var notification = game.addChild(new Notification("Farm income: +" + totalFarmIncome + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 250;
}
// Give lives from all church towers
var totalChurchLives = 0;
var hasDivineIntervention = false;
for (var i = 0; i < towers.length; i++) {
if (towers[i].id === 'church') {
// Church gives 1 life per wave at level 1, +1 per level upgrade
totalChurchLives += towers[i].level; // Level 1 = 1 life, Level 2 = 2 lives, etc.
// Check for divine intervention ability
if (towers[i].hasSpecialAbility && Math.random() < 0.1) {
// 10% chance
hasDivineIntervention = true;
}
}
}
if (totalChurchLives > 0) {
lives += totalChurchLives;
updateUI();
var notification = game.addChild(new Notification("Church blessing: +" + totalChurchLives + " lives!"));
notification.x = 2048 / 2;
notification.y = grid.height - 300;
}
// Handle divine intervention
if (hasDivineIntervention && currentWave < totalWaves) {
// Skip the next wave and give 350 gold
currentWave++;
waveTimer = 0;
setGold(gold + 350);
var notification = game.addChild(new Notification("Divine Intervention! Wave skipped +350 gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 350;
LK.getSound('divine_intervention').play();
}
// Check for laser tower pairs and create/update laser walls
updateLaserWalls();
}
}
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);
// Check if killed by a tower in range of a farm with special ability
var farmBonus = 0;
for (var i = 0; i < towers.length; i++) {
if (towers[i].id === 'farm' && towers[i].hasSpecialAbility) {
// Check if any tower that could have killed this enemy is in range of the farm
var farmX = towers[i].x;
var farmY = towers[i].y;
var farmRange = 3 * CELL_SIZE; // Farm boost range
for (var j = 0; j < towers.length; j++) {
if (towers[j].id !== 'farm') {
var dx = towers[j].x - farmX;
var dy = towers[j].y - farmY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= farmRange) {
// Check if this tower could have killed the enemy
var towerToEnemyDx = enemy.x - towers[j].x;
var towerToEnemyDy = enemy.y - towers[j].y;
var towerToEnemyDist = Math.sqrt(towerToEnemyDx * towerToEnemyDx + towerToEnemyDy * towerToEnemyDy);
if (towerToEnemyDist <= towers[j].getRange()) {
farmBonus = Math.ceil(goldEarned * 0.25); // 25% bonus
break;
}
}
}
}
if (farmBonus > 0) break;
}
}
var totalGold = goldEarned + farmBonus;
var goldIndicator = new GoldIndicator(totalGold, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + totalGold);
LK.getSound('enemy_death').play();
LK.getSound('gold_collect').play();
// 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;
}
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.getSound('game_over').play();
LK.stopMusic();
LK.showGameOver();
}
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Update all laser walls
for (var i = 0; i < laserWalls.length; i++) {
laserWalls[i].update();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.getSound('victory').play();
LK.stopMusic();
LK.showYouWin();
}
}; ===================================================================
--- original.js
+++ change.js
@@ -5,221 +5,8 @@
/****
* Classes
****/
-var ArcherUpgradeMenu = Container.expand(function (tower) {
- var self = Container.call(this);
- self.tower = tower;
- self.y = 2732 + 450;
- var menuBackground = self.attachAsset('notification', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- menuBackground.width = 2048;
- menuBackground.height = 900;
- menuBackground.tint = 0x444444;
- menuBackground.alpha = 0.9;
- // Title
- var titleText = new Text2('Bow Tower - Upgrade Paths', {
- size: 80,
- fill: 0xFFFFFF,
- weight: 800
- });
- titleText.anchor.set(0.5, 0);
- titleText.x = 0;
- titleText.y = -400;
- self.addChild(titleText);
- // Path 1 (Speed)
- var path1Container = new Container();
- path1Container.x = -400;
- path1Container.y = -200;
- self.addChild(path1Container);
- var path1Title = new Text2('Speed Path', {
- size: 60,
- fill: 0x00AAFF,
- weight: 800
- });
- path1Title.anchor.set(0.5, 0);
- path1Container.addChild(path1Title);
- // Path 1 upgrades
- for (var i = 0; i < 4; i++) {
- var upgradeButton = new Container();
- var upgrade = tower.upgradePaths.path1.upgrades[i];
- var buttonBg = upgradeButton.attachAsset('notification', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- buttonBg.width = 350;
- buttonBg.height = 80;
- var cost = getTowerCost(tower.id) * Math.pow(2, upgrade.level);
- var canAfford = gold >= cost && !upgrade.purchased;
- var canPurchase = canAfford && (!self.tower.hasThirdUpgrade || upgrade.level < 3);
- buttonBg.tint = upgrade.purchased ? 0x00FF00 : canPurchase ? 0x0088FF : 0x444444;
- var buttonText = new Text2(upgrade.purchased ? upgrade.name + ' ✓' : upgrade.name + ': ' + cost + 'g', {
- size: 35,
- fill: 0xFFFFFF,
- weight: 600
- });
- buttonText.anchor.set(0.5, 0.5);
- upgradeButton.addChild(buttonText);
- upgradeButton.y = 60 + i * 90;
- upgradeButton.upgradeIndex = i;
- upgradeButton.pathName = 'path1';
- upgradeButton.down = function () {
- if (self.tower.purchasePathUpgrade(this.pathName, this.upgradeIndex)) {
- self.refreshButtons();
- }
- };
- path1Container.addChild(upgradeButton);
- }
- // Path 2 (Damage)
- var path2Container = new Container();
- path2Container.x = 400;
- path2Container.y = -200;
- self.addChild(path2Container);
- var path2Title = new Text2('Damage Path', {
- size: 60,
- fill: 0xFF4444,
- weight: 800
- });
- path2Title.anchor.set(0.5, 0);
- path2Container.addChild(path2Title);
- // Path 2 upgrades
- for (var i = 0; i < 4; i++) {
- var upgradeButton = new Container();
- var upgrade = tower.upgradePaths.path2.upgrades[i];
- var buttonBg = upgradeButton.attachAsset('notification', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- buttonBg.width = 350;
- buttonBg.height = 80;
- var cost = getTowerCost(tower.id) * Math.pow(2, upgrade.level);
- var canAfford = gold >= cost && !upgrade.purchased;
- var canPurchase = canAfford && (!self.tower.hasThirdUpgrade || upgrade.level < 3);
- buttonBg.tint = upgrade.purchased ? 0x00FF00 : canPurchase ? 0x0088FF : 0x444444;
- var buttonText = new Text2(upgrade.purchased ? upgrade.name + ' ✓' : upgrade.name + ': ' + cost + 'g', {
- size: 35,
- fill: 0xFFFFFF,
- weight: 600
- });
- buttonText.anchor.set(0.5, 0.5);
- upgradeButton.addChild(buttonText);
- upgradeButton.y = 60 + i * 90;
- upgradeButton.upgradeIndex = i;
- upgradeButton.pathName = 'path2';
- upgradeButton.down = function () {
- if (self.tower.purchasePathUpgrade(this.pathName, this.upgradeIndex)) {
- self.refreshButtons();
- }
- };
- path2Container.addChild(upgradeButton);
- }
- // Regular buttons container
- var buttonsContainer = new Container();
- buttonsContainer.y = 200;
- self.addChild(buttonsContainer);
- // Sell button
- var sellButton = new Container();
- buttonsContainer.addChild(sellButton);
- var sellButtonBackground = sellButton.attachAsset('notification', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- sellButtonBackground.width = 500;
- sellButtonBackground.height = 120;
- 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);
- sellButton.down = function () {
- 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;
- // Clean up tower
- var gridX = self.tower.gridX;
- var gridY = self.tower.gridY;
- for (var i = 0; i < 2; i++) {
- for (var j = 0; j < 2; j++) {
- var cell = grid.getCell(gridX + i, gridY + j);
- if (cell) {
- cell.type = 0;
- var towerIndex = cell.towersInRange.indexOf(self.tower);
- if (towerIndex !== -1) {
- cell.towersInRange.splice(towerIndex, 1);
- }
- }
- }
- }
- if (selectedTower === self.tower) {
- selectedTower = null;
- }
- var towerIndex = towers.indexOf(self.tower);
- if (towerIndex !== -1) {
- towers.splice(towerIndex, 1);
- }
- towerLayer.removeChild(self.tower);
- grid.pathFind();
- grid.renderDebug();
- self.destroy();
- for (var i = 0; i < game.children.length; i++) {
- if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
- game.removeChild(game.children[i]);
- break;
- }
- }
- };
- // Close button
- 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;
- closeButton.down = function () {
- hideUpgradeMenu(self);
- selectedTower = null;
- grid.renderDebug();
- };
- self.refreshButtons = function () {
- // Recreate the menu to refresh button states
- self.destroy();
- var newMenu = new ArcherUpgradeMenu(tower);
- game.addChild(newMenu);
- newMenu.x = 2048 / 2;
- tween(newMenu, {
- y: 2732 - 450
- }, {
- duration: 200,
- easing: tween.backOut
- });
- };
- self.update = function () {
- // Update button colors based on current gold
- };
- return self;
-});
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
@@ -2136,8 +1923,119 @@
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
+var TitleScreen = Container.expand(function () {
+ var self = Container.call(this);
+ // Full screen background
+ var background = self.attachAsset('manualBackground', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ background.width = 2048;
+ background.height = 2732;
+ background.tint = 0x1a1a1a;
+ // Title text with shadow
+ var titleShadow = new Text2('TOWER DEFENSE', {
+ size: 120,
+ fill: 0x000000,
+ weight: 800
+ });
+ titleShadow.anchor.set(0.5, 0.5);
+ titleShadow.x = 6;
+ titleShadow.y = -400 + 6;
+ self.addChild(titleShadow);
+ var titleText = new Text2('TOWER DEFENSE', {
+ size: 120,
+ fill: 0xFFD700,
+ weight: 800
+ });
+ titleText.anchor.set(0.5, 0.5);
+ titleText.y = -400;
+ self.addChild(titleText);
+ // Subtitle
+ var subtitleText = new Text2('Defend your base from waves of enemies!', {
+ size: 50,
+ fill: 0xCCCCCC,
+ weight: 400
+ });
+ subtitleText.anchor.set(0.5, 0.5);
+ subtitleText.y = -250;
+ self.addChild(subtitleText);
+ // Start button
+ var startButton = new Container();
+ var startBg = startButton.attachAsset('notification', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ startBg.width = 600;
+ startBg.height = 200;
+ startBg.tint = 0x00AA00;
+ var startTextShadow = new Text2('START GAME', {
+ size: 80,
+ fill: 0x000000,
+ weight: 800
+ });
+ startTextShadow.anchor.set(0.5, 0.5);
+ startTextShadow.x = 4;
+ startTextShadow.y = 4;
+ startButton.addChild(startTextShadow);
+ var startText = new Text2('START GAME', {
+ size: 80,
+ fill: 0xFFFFFF,
+ weight: 800
+ });
+ startText.anchor.set(0.5, 0.5);
+ startButton.addChild(startText);
+ startButton.y = 100;
+ self.addChild(startButton);
+ // Animate button pulse
+ self.pulseTimer = 0;
+ // Button click handler
+ startButton.down = function () {
+ LK.getSound('button_click').play();
+ // Fade out the title screen
+ tween(self, {
+ alpha: 0
+ }, {
+ duration: 500,
+ easing: tween.easeIn,
+ onFinish: function onFinish() {
+ self.destroy();
+ }
+ });
+ };
+ // Instructions text
+ var instructionsText = new Text2('Tap to place towers • Upgrade to increase power', {
+ size: 40,
+ fill: 0x888888,
+ weight: 400
+ });
+ instructionsText.anchor.set(0.5, 0.5);
+ instructionsText.y = 400;
+ self.addChild(instructionsText);
+ // Version text
+ var versionText = new Text2('v1.0', {
+ size: 30,
+ fill: 0x666666,
+ weight: 400
+ });
+ versionText.anchor.set(1, 1);
+ versionText.x = 900;
+ versionText.y = 1300;
+ self.addChild(versionText);
+ self.update = function () {
+ // Pulse animation for start button
+ self.pulseTimer += 0.05;
+ var scale = 1 + Math.sin(self.pulseTimer) * 0.05;
+ startButton.scaleX = scale;
+ startButton.scaleY = scale;
+ };
+ // Center on screen
+ self.x = 2048 / 2;
+ self.y = 2732 / 2;
+ return self;
+});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
@@ -2150,55 +2048,8 @@
self.specialAbilityActive = false;
self.specialAbilityTimer = 0;
self.rapidBurstCount = 0; // For archer rapid fire
self.infernoTargets = []; // For inferno multi-target
- // Upgrade path system (only for archer)
- self.upgradePaths = {
- path1: {
- name: 'Speed Path',
- upgrades: [{
- name: 'Faster Shooting',
- level: 1,
- purchased: false
- }, {
- name: 'Even Faster Shooting',
- level: 2,
- purchased: false
- }, {
- name: 'Rapid Fire',
- level: 3,
- purchased: false,
- isSpecial: true
- }, {
- name: 'Super Rapid Fire',
- level: 4,
- purchased: false
- }]
- },
- path2: {
- name: 'Damage Path',
- upgrades: [{
- name: 'Sharp Arrows',
- level: 1,
- purchased: false
- }, {
- name: 'Sharpest Arrows',
- level: 2,
- purchased: false
- }, {
- name: 'Giant Arrow',
- level: 3,
- purchased: false,
- isSpecial: true
- }, {
- name: 'Even Bigger Arrow',
- level: 4,
- purchased: false
- }]
- }
- };
- self.hasThirdUpgrade = false; // Track if any third+ upgrade is purchased
- self.giantArrowActive = false; // For giant arrow ability
// 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) {
@@ -2450,31 +2301,14 @@
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));
}
- // Add path upgrade costs for archer
- if (self.id === 'default') {
- for (var path in self.upgradePaths) {
- var upgrades = self.upgradePaths[path].upgrades;
- for (var j = 0; j < upgrades.length; j++) {
- if (upgrades[j].purchased) {
- totalInvestment += baseTowerCost * Math.pow(2, upgrades[j].level);
- }
- }
- }
- }
return totalInvestment;
};
self.getSpecialAbilityName = function () {
switch (self.id) {
case 'default':
- // For archer, check which path has the special ability
- if (self.upgradePaths.path1.upgrades[2].purchased) {
- return 'Rapid Fire';
- } else if (self.upgradePaths.path2.upgrades[2].purchased) {
- return 'Giant Arrow';
- }
- return 'Special Ability';
+ return 'Rapid Fire';
case 'rapid':
return 'Bleeding Shots';
case 'sniper':
return 'Infinite Range';
@@ -2501,79 +2335,8 @@
self.getSpecialAbilityCost = function () {
// Special ability costs 3x the base tower cost
return getTowerCost(self.id) * 3;
};
- self.purchasePathUpgrade = function (pathName, upgradeIndex) {
- if (self.id !== 'default') return false; // Only archer has paths
- var path = self.upgradePaths[pathName];
- if (!path || !path.upgrades[upgradeIndex]) return false;
- var upgrade = path.upgrades[upgradeIndex];
- if (upgrade.purchased) return false;
- // Check if this is a third+ upgrade and if we already have one
- if (upgrade.level >= 3 && self.hasThirdUpgrade) return false;
- // Calculate cost based on upgrade level
- var baseCost = getTowerCost(self.id);
- var cost = baseCost * Math.pow(2, upgrade.level);
- if (gold >= cost) {
- setGold(gold - cost);
- upgrade.purchased = true;
- // Mark if this is a third+ upgrade
- if (upgrade.level >= 3) {
- self.hasThirdUpgrade = true;
- if (upgrade.isSpecial) {
- self.hasSpecialAbility = true;
- }
- }
- // Apply upgrade effects
- self.applyPathUpgrade(pathName, upgradeIndex);
- LK.getSound('tower_upgrade').play();
- return true;
- }
- return false;
- };
- self.applyPathUpgrade = function (pathName, upgradeIndex) {
- if (pathName === 'path1') {
- // Speed path upgrades
- switch (upgradeIndex) {
- case 0:
- // Faster Shooting
- self.fireRate = Math.max(self.fireRate - 15, 20);
- break;
- case 1:
- // Even Faster Shooting
- self.fireRate = Math.max(self.fireRate - 10, 15);
- break;
- case 2:
- // Rapid Fire (special ability)
- // Already handled by hasSpecialAbility flag
- break;
- case 3:
- // Super Rapid Fire
- self.fireRate = Math.max(self.fireRate - 5, 10);
- break;
- }
- } else if (pathName === 'path2') {
- // Damage path upgrades
- switch (upgradeIndex) {
- case 0:
- // Sharp Arrows
- self.damage += 5;
- break;
- case 1:
- // Sharpest Arrows
- self.damage += 8;
- break;
- case 2:
- // Giant Arrow (special ability)
- // Already handled by hasSpecialAbility flag
- break;
- case 3:
- // Even Bigger Arrow
- self.damage += 15;
- break;
- }
- }
- };
self.unlockSpecialAbility = function () {
if (self.level >= 3 && !self.hasSpecialAbility) {
var cost = self.getSpecialAbilityCost();
if (gold >= cost) {
@@ -2979,19 +2742,13 @@
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
- var upgradeMenu;
- if (self.id === 'default') {
- upgradeMenu = new ArcherUpgradeMenu(self);
- } else {
- upgradeMenu = new UpgradeMenu(self);
- }
+ var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
- var menuY = self.id === 'default' ? 2732 - 450 : 2732 - 325;
tween(upgradeMenu, {
- y: menuY
+ y: 2732 - 325
}, {
duration: 200,
easing: tween.backOut
});
@@ -3006,18 +2763,12 @@
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
- // Handle archer abilities based on which path is chosen
- if (self.id === 'default' && self.hasSpecialAbility) {
- if (self.upgradePaths.path1.upgrades[2].purchased && Math.random() < 0.25) {
- // Rapid Fire ability
- self.rapidBurstCount = 5;
- self.specialAbilityActive = true;
- } else if (self.upgradePaths.path2.upgrades[2].purchased && Math.random() < 0.15) {
- // Giant Arrow ability
- self.giantArrowActive = true;
- }
+ // Handle archer rapid fire ability
+ if (self.id === 'default' && self.hasSpecialAbility && Math.random() < 0.25) {
+ self.rapidBurstCount = 5;
+ self.specialAbilityActive = true;
}
// Handle mortar quad shot ability
if (self.id === 'mortar' && self.hasSpecialAbility) {
// Fire 4 projectiles at once
@@ -3063,13 +2814,8 @@
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bulletDamage = oneHitKillCheat ? 999999 : self.damage;
- // Apply giant arrow damage multiplier
- if (self.id === 'default' && self.giantArrowActive) {
- bulletDamage *= self.upgradePaths.path2.upgrades[3].purchased ? 5 : 3; // 5x for even bigger, 3x for giant
- self.giantArrowActive = false; // Reset after firing
- }
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, bulletDamage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
@@ -3098,13 +2844,8 @@
var specificBulletGraphics = bullet.attachAsset(bulletAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
- // Scale up giant arrow bullets
- if (self.id === 'default' && bulletDamage > self.damage * 2) {
- specificBulletGraphics.scaleX = 2;
- specificBulletGraphics.scaleY = 2;
- }
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// Play shooting sound based on tower type
@@ -4114,9 +3855,16 @@
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
- if (!self.gameStarted) {
+ // Check if title screen is still active in the game
+ var titleScreenActive = false;
+ if (game && game.children) {
+ titleScreenActive = game.children.some(function (child) {
+ return child instanceof TitleScreen;
+ });
+ }
+ if (titleScreenActive || !self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
@@ -4155,11 +3903,10 @@
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
- var hideY = menu instanceof ArcherUpgradeMenu ? 2732 + 450 : 2732 + 325;
tween(menu, {
- y: hideY
+ y: 2732 + 325
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
@@ -4196,20 +3943,23 @@
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
+goldText.visible = false;
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
+livesText.visible = false;
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
+scoreText.visible = false;
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.top.addChild(goldText);
@@ -4378,8 +4128,15 @@
return false;
}
}
game.down = function (x, y, obj) {
+ // Check if title screen is active
+ var titleScreenActive = game.children.some(function (child) {
+ return child instanceof TitleScreen;
+ });
+ if (titleScreenActive) {
+ return;
+ }
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
@@ -4425,9 +4182,9 @@
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
- return child instanceof UpgradeMenu || child instanceof ArcherUpgradeMenu;
+ return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
@@ -4478,9 +4235,9 @@
}
towerPreview.visible = false;
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
- return child instanceof UpgradeMenu || child instanceof ArcherUpgradeMenu;
+ return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
@@ -4489,8 +4246,9 @@
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
+waveIndicator.visible = false;
game.addChild(waveIndicator);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
@@ -4519,8 +4277,9 @@
var sourceTowers = [];
var classButtons = [];
var currentClass = null;
var towerSelectionContainer = new Container();
+towerSelectionContainer.visible = false;
game.addChild(towerSelectionContainer);
// Create class buttons
var classButtonWidth = 400;
var classButtonHeight = 80;
@@ -4593,10 +4352,23 @@
}
}
// Select first class by default
selectTowerClass(classNames[0]);
-// Play theme music
-LK.playMusic('game_theme');
+// Create title screen
+var titleScreen = new TitleScreen();
+game.addChild(titleScreen);
+// Play theme music after title screen is dismissed
+titleScreen.onDestroy = function () {
+ LK.playMusic('game_theme');
+ // Show UI elements
+ goldText.visible = true;
+ livesText.visible = true;
+ scoreText.visible = true;
+ waveIndicator.visible = true;
+ towerSelectionContainer.visible = true;
+ manualButton.visible = true;
+ cheatButton.visible = true;
+};
// Create manual button
var manualButton = new Container();
var manualBg = manualButton.attachAsset('manualButton', {
anchorX: 0.5,
@@ -4611,8 +4383,9 @@
manualText.anchor.set(0.5, 0.5);
manualButton.addChild(manualText);
manualButton.x = 2048 - 80;
manualButton.y = 150;
+manualButton.visible = false;
manualButton.down = function () {
var manual = new Manual();
game.addChild(manual);
};
@@ -4632,8 +4405,9 @@
cheatText.anchor.set(0.5, 0.5);
cheatButton.addChild(cheatText);
cheatButton.x = 2048 - 200;
cheatButton.y = 150;
+cheatButton.visible = false;
cheatButton.down = function () {
var cheatMenu = new CheatMenu();
game.addChild(cheatMenu);
};
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
Wooden Guard Tower. In-Game asset. 2d. High contrast. No shadows
Bow: make it face 90 degrees In-Game asset. 2d. High contrast. No shadows
Wooden Tower base like the ones the Cannons in Clash of Clans have. In-Game asset. 2d. High contrast. No shadows. Topdown
Cannon without wheels or base, just the cannon. In-Game asset. 2d. High contrast. No shadows. Topdown
Crossbow rotated 90 degrees. In-Game asset. 2d. High contrast. No shadows
Sniper rifle rotated 90 degrees. In-Game asset. 2d. High contrast. No shadows
Ice tower. In-Game asset. 2d. High contrast. No shadows
Ice staff. In-Game asset. 2d. High contrast. No shadows
Windmill. In-Game asset. 2d. High contrast. No shadows
Laser pointer without pointing a laser. In-Game asset. 2d. High contrast. No shadows. Topdown
Laser projectile In-Game asset. 2d. High contrast. No shadows
Orc holding a small axe. In-Game asset. 2d. High contrast. No shadows
Orc with a big wooden shield full of spikes. In-Game asset. 2d. High contrast. No shadows
Orc in a wooden helicopter. In-Game asset. 2d. High contrast. No shadows
Spider. In-Game asset. 2d. High contrast. No shadows
One minion from Clash Royale. In-Game asset. 2d. High contrast. No shadows
Mole with a minerer's hat and a pickaxe. In-Game asset. 2d. High contrast. No shadows
Wolf. In-Game asset. 2d. High contrast. No shadows
Dark magma tower. In-Game asset. 2d. High contrast. No shadows. Topdown
Add outlines
Magician's staff. In-Game asset. 2d. High contrast. No shadows
Mortar from Clash Royale without base, just the Mortar. In-Game asset. 2d. High contrast. No shadows. Topdown
Tophat house. In-Game asset. 2d. High contrast. No shadows
Church. In-Game asset. 2d. High contrast. No shadows
tower_upgrade
Sound effect
enemy_hit
Sound effect
enemy_death
Sound effect
wave_start
Sound effect
place_tower
Sound effect
boss_spawn
Sound effect
game_over
Sound effect
victory
Sound effect
laser_beam
Sound effect
game_theme
Music
explosion
Sound effect
freeze
Sound effect
divine_intervention
Sound effect
gold_collect
Sound effect