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");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var AchievementsMenu = Container.expand(function () {
var self = Container.call(this);
// Background
var background = self.attachAsset('manualBackground', {
anchorX: 0.5,
anchorY: 0.5
});
background.alpha = 0.95;
// Title
var titleText = new Text2('ACHIEVEMENTS', {
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 achievementSpacing = 120;
// Calculate completion percentage
var completedCount = 0;
var totalCount = 0;
for (var key in achievements) {
totalCount++;
if (achievements[key].completed) {
completedCount++;
}
}
// Progress display
var progressText = new Text2('Progress: ' + completedCount + '/' + totalCount + ' (' + Math.round(completedCount / totalCount * 100) + '%)', {
size: 60,
fill: 0x00FFFF,
weight: 800
});
progressText.anchor.set(0.5, 0);
progressText.x = 0;
progressText.y = yPos;
contentContainer.addChild(progressText);
yPos += 100;
// Achievement list
for (var key in achievements) {
var achievement = achievements[key];
var achievementContainer = new Container();
achievementContainer.y = yPos;
var achievementBg = achievementContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
achievementBg.width = 1400;
achievementBg.height = 100;
achievementBg.tint = achievement.completed ? 0x00AA00 : 0x444444;
var nameText = new Text2(achievement.name, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
nameText.anchor.set(0, 0.5);
nameText.x = -650;
nameText.y = -20;
achievementContainer.addChild(nameText);
var descText = new Text2(achievement.description, {
size: 40,
fill: 0xCCCCCC,
weight: 400
});
descText.anchor.set(0, 0.5);
descText.x = -650;
descText.y = 20;
achievementContainer.addChild(descText);
var progressText = new Text2(achievement.completed ? 'COMPLETED' : achievement.progress + '/' + achievement.target, {
size: 45,
fill: achievement.completed ? 0x00FF00 : 0xFFFFFF,
weight: 800
});
progressText.anchor.set(1, 0.5);
progressText.x = 650;
progressText.y = 0;
achievementContainer.addChild(progressText);
contentContainer.addChild(achievementContainer);
yPos += achievementSpacing;
}
// Scrolling variables
var isDragging = false;
var startY = 0;
var startContentY = 0;
var velocity = 0;
var minY = -1000;
var maxY = Math.min(0, -yPos + 1800);
// Close button
var closeButton = new Container();
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;
self.addChild(closeButton);
closeButton.down = function () {
self.destroy();
};
// Touch event handlers for scrolling
self.down = function (x, y, obj) {
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;
newY = Math.max(maxY, Math.min(minY, newY));
contentContainer.y = newY;
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) {
contentContainer.y += velocity;
contentContainer.y = Math.max(maxY, Math.min(minY, contentContainer.y));
velocity *= 0.92;
if (Math.abs(velocity) < 0.1) {
velocity = 0;
}
}
};
// Initial position
self.x = 2048 / 2;
self.y = 2732 / 2;
return self;
});
var ArtifactMenu = Container.expand(function () {
var self = Container.call(this);
// Background
var background = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
background.width = 1600;
background.height = 1200;
background.tint = 0x222222;
background.alpha = 0.95;
// Title
var titleText = new Text2('ARTIFACTS', {
size: 80,
fill: 0x00FFFF,
weight: 800
});
titleText.anchor.set(0.5, 0);
titleText.x = 0;
titleText.y = -500;
self.addChild(titleText);
// Crystal display
var crystalDisplay = new Text2('Crystals: ' + crystals, {
size: 60,
fill: 0x00FFFF,
weight: 800
});
crystalDisplay.anchor.set(0.5, 0);
crystalDisplay.x = 0;
crystalDisplay.y = -400;
self.addChild(crystalDisplay);
// Artifacts
var artifactData = [{
id: 'thunderbolt',
name: 'Thunderbolt of Zeus',
description: 'All projectiles stun enemies for 0.25s',
owned: artifacts.thunderbolt,
price: artifactPrices.thunderbolt
}, {
id: 'aresHelmet',
name: "Ares' Helmet",
description: 'All projectiles deal double damage',
owned: artifacts.aresHelmet,
price: artifactPrices.aresHelmet
}, {
id: 'ymirsHammer',
name: "Ymir's Hammer",
description: 'Start with 15 extra lives',
owned: artifacts.ymirsHammer,
price: artifactPrices.ymirsHammer
}];
var artifactButtons = [];
var yPos = -250;
for (var i = 0; i < artifactData.length; i++) {
var artifact = artifactData[i];
var artifactContainer = new Container();
artifactContainer.y = yPos;
var artifactBg = artifactContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
artifactBg.width = 1400;
artifactBg.height = 200;
artifactBg.tint = artifact.owned ? 0x00AA00 : 0x444444;
var nameText = new Text2(artifact.name, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
nameText.anchor.set(0, 0.5);
nameText.x = -650;
nameText.y = -40;
artifactContainer.addChild(nameText);
var descText = new Text2(artifact.description, {
size: 40,
fill: 0xCCCCCC,
weight: 400
});
descText.anchor.set(0, 0.5);
descText.x = -650;
descText.y = 20;
artifactContainer.addChild(descText);
var statusText = new Text2(artifact.owned ? 'OWNED' : artifact.price + ' Crystals', {
size: 50,
fill: artifact.owned ? 0x00FF00 : 0x00FFFF,
weight: 800
});
statusText.anchor.set(1, 0.5);
statusText.x = 650;
statusText.y = 0;
artifactContainer.addChild(statusText);
if (!artifact.owned) {
artifactContainer.artifactId = artifact.id;
artifactContainer.price = artifact.price;
artifactContainer.down = function () {
if (crystals >= this.price) {
crystals -= this.price;
artifacts[this.artifactId] = true;
storage.crystals = crystals;
storage.artifacts = artifacts;
// Refresh menu
self.destroy();
var newMenu = new ArtifactMenu();
game.addChild(newMenu);
} else {
var notification = game.addChild(new Notification("Not enough crystals!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
LK.getSound('error').play();
}
};
}
self.addChild(artifactContainer);
artifactButtons.push(artifactContainer);
yPos += 250;
}
// 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();
};
self.x = 2048 / 2;
self.y = 2732 / 2;
return self;
});
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
// 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;
var hitAnyEnemy = false;
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) {
hitAnyEnemy = true;
// 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;
}
}
}
// Achievement tracking: Miss
if (!hitAnyEnemy) {
achievementStats.mortarMisses++;
if (!achievements.miss.completed) {
achievements.miss.completed = true;
achievements.miss.progress = 1;
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.miss.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
saveAchievements();
saveAchievementStats();
}
}
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();
// Apply Thunderbolt of Zeus stun effect
if (artifacts.thunderbolt && !self.targetEnemy.isImmune && !self.targetEnemy.isStunned) {
self.targetEnemy.isStunned = true;
self.targetEnemy.stunDuration = 15; // 0.25 seconds at 60 FPS
self.targetEnemy.originalSpeed = self.targetEnemy.originalSpeed || self.targetEnemy.speed;
self.targetEnemy.speed = 0;
// Visual effect
LK.effects.flashObject(self.targetEnemy, 0xFFFF00, 250);
}
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);
// Achievement tracking: Cowardly (wizard confusion streak)
achievementStats.wizardConfusionStreak++;
if (!achievements.cowardly.completed) {
achievements.cowardly.progress = achievementStats.wizardConfusionStreak;
if (achievements.cowardly.progress >= achievements.cowardly.target) {
achievements.cowardly.completed = true;
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.cowardly.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
saveAchievements();
saveAchievementStats();
}
}
} else {
// Reset streak if confusion didn't trigger
achievementStats.wizardConfusionStreak = 0;
}
}
} 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 'hippo':
self.maxHealth = 150; // Tanky enemy
self.speed *= 0.6; // Slower movement
self.canEatTowers = true;
self.eatingTimer = 0;
break;
case 'dragon':
self.isFlying = true;
self.maxHealth = 90;
self.speed *= 1.2; // Faster than normal flying
self.canStunTowers = true;
self.stunCooldown = 0;
break;
case 'kangaroo':
self.maxHealth = 70;
self.speed *= 1.4; // Fast movement
self.canJumpOverTowers = true;
self.jumpCooldown = 0;
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 King Hippo tower eating
if (self.type === 'hippo' && self.canEatTowers && self.currentCellY >= 4) {
self.eatingTimer++;
if (self.eatingTimer >= 60) {
// Check every second
self.eatingTimer = 0;
// Look for nearby towers to eat
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 1.5) {
// Close enough to eat
// "Eat" the tower (remove it)
var gridX = tower.gridX;
var gridY = tower.gridY;
for (var gi = 0; gi < 2; gi++) {
for (var gj = 0; gj < 2; gj++) {
var cell = grid.getCell(gridX + gi, gridY + gj);
if (cell) {
cell.type = 0;
}
}
}
var towerIndex = towers.indexOf(tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(tower);
// Heal the hippo
self.health = Math.min(self.maxHealth, self.health + 30);
self.healthBar.width = self.health / self.maxHealth * 70;
var notification = game.addChild(new Notification("King Hippo ate a tower!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
grid.pathFind();
updateLaserWalls();
break; // Only eat one tower per check
}
}
}
}
// Handle Dragon tower stunning
if (self.type === 'dragon' && self.canStunTowers && self.currentCellY >= 4) {
self.stunCooldown--;
if (self.stunCooldown <= 0) {
self.stunCooldown = 180; // 3 second cooldown
// Stun nearby towers
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var dx = tower.x - self.x;
var dy = tower.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= CELL_SIZE * 2.5) {
// Stun range
tower.isStunned = true;
tower.stunDuration = 120; // 2 seconds
// Visual effect
LK.effects.flashObject(tower, 0xFFFF00, 300);
}
}
if (towers.length > 0) {
var notification = game.addChild(new Notification("Dragon stunned nearby towers!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
}
// Handle Pink Kangaroo jumping
if (self.type === 'kangaroo' && self.canJumpOverTowers && self.currentCellY >= 4) {
self.jumpCooldown--;
if (self.jumpCooldown <= 0) {
// Check if there's a tower blocking the path ahead
var cellAhead = grid.getCell(self.cellX, self.cellY - 1);
if (cellAhead && cellAhead.type === 1) {
// Tower blocking
self.jumpCooldown = 240; // 4 second cooldown
// Jump over the tower (move 2 cells forward)
self.currentCellY -= 2;
self.y = grid.y + self.currentCellY * CELL_SIZE;
// Visual effect
LK.effects.flashObject(self, 0xFF69B4, 500);
var notification = game.addChild(new Notification("Pink Kangaroo jumped over tower!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
// 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 stun effect
if (self.isStunned && !self.isImmune) {
self.stunDuration--;
if (self.stunDuration <= 0) {
self.isStunned = false;
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
}
}
// 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: 'KING HIPPO',
stats: 'Health: 150 • Speed: 0.6x Normal • Ground unit\nEats towers to heal itself and clear path'
}, {
name: 'DRAGON',
stats: 'Health: 90 • Speed: 1.2x Normal • Flying unit\nStuns nearby towers periodically'
}, {
name: 'PINK KANGAROO',
stats: 'Health: 70 • Speed: 1.4x Normal • Ground unit\nJumps over towers that block its path'
}, {
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;
case 'gatling':
displayName = 'Gatling Gun';
break;
case 'swordsman':
displayName = 'Swordsman';
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 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;
case 'gatling':
// Gatling: base 2.5, +0.3 per level
return (2.5 + (self.level - 1) * 0.3) * CELL_SIZE;
case 'swordsman':
// Swordsman: base 1.5, +0.2 per level (very close range)
return (1.5 + (self.level - 1) * 0.2) * 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;
case 'gatling':
self.fireRate = 8; // Extremely fast fire rate
self.damage = 3;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 12;
break;
case 'swordsman':
self.fireRate = 999999; // Doesn't shoot bullets
self.damage = 8; // Base damage per tick
self.range = 1.5 * CELL_SIZE; // Very close range
self.bulletSpeed = 0;
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;
case 'gatling':
towerLevelIndicator.tint = 0x708090; // Slate gray
break;
case 'swordsman':
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;
}
// Create spinning sword visual if this is a swordsman tower
if (self.id === 'swordsman') {
self.sword = self.attachAsset('defense_swordsman', {
anchorX: 0.5,
anchorY: 0.5
});
self.sword.alpha = 0.8;
self.sword.tint = 0xC0C0C0; // Silver color
self.swordsmanDamageTimer = 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';
case 'gatling':
return 'Bullet Storm';
case 'swordsman':
return 'Whirlwind Strike';
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;
}
// Check if tower is now at max level with special ability (award crystal)
if (self.level === self.maxLevel && self.hasSpecialAbility && !unlimitedMoneyCheat && !oneHitKillCheat) {
crystals += 1;
storage.crystals = crystals;
updateUI();
var crystalNotification = game.addChild(new Notification("+1 Crystal for MAX tower!"));
crystalNotification.x = 2048 / 2;
crystalNotification.y = grid.height - 150;
}
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.id === 'gatling') {
if (self.level === self.maxLevel) {
self.fireRate = Math.max(2, 8 - self.level * 2); // Extremely fast at max level
self.damage = 3 + self.level * 8;
self.bulletSpeed = 12 + self.level * 3;
} else {
self.fireRate = Math.max(5, 8 - self.level * 1);
self.damage = 3 + self.level * 2;
self.bulletSpeed = 12 + self.level * 1;
}
} else if (self.id === 'swordsman') {
// Swordsman damage scales with level (no fire rate since it's area damage)
self.damage = 8 + self.level * 4;
} 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();
// Check if tower reached max level with special ability (award crystal)
if (self.level === self.maxLevel && self.hasSpecialAbility && !unlimitedMoneyCheat && !oneHitKillCheat) {
crystals += 1;
storage.crystals = crystals;
updateUI();
var crystalNotification = game.addChild(new Notification("+1 Crystal for MAX tower!"));
crystalNotification.x = 2048 / 2;
crystalNotification.y = grid.height - 150;
// Achievement tracking: Power Engineer
if (!achievements.power_engineer.completed) {
achievements.power_engineer.completed = true;
achievements.power_engineer.progress = 1;
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.power_engineer.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
// Achievement tracking: All of Em'
var towerTypeName = self.id === 'default' ? 'default' : self.id;
if (achievementStats.maxLevelTowerTypes.indexOf(towerTypeName) === -1) {
achievementStats.maxLevelTowerTypes.push(towerTypeName);
}
if (!achievements.all_of_em.completed) {
achievements.all_of_em.progress = achievementStats.maxLevelTowerTypes.length;
if (achievements.all_of_em.progress >= achievements.all_of_em.target) {
achievements.all_of_em.completed = true;
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.all_of_em.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 250;
}
}
saveAchievements();
saveAchievementStats();
}
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 () {
// Check if tower is stunned by dragon
if (self.isStunned) {
self.stunDuration--;
if (self.stunDuration <= 0) {
self.isStunned = false;
}
return; // Skip normal tower behavior while stunned
}
// 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;
}
// Swordsman tower uses spinning sword area damage
if (self.id === 'swordsman') {
// Continuously spin the sword
self.sword.rotation += 0.3; // Fast spinning
// Deal damage every 15 ticks (0.25 seconds)
self.swordsmanDamageTimer++;
if (self.swordsmanDamageTimer >= 15) {
self.swordsmanDamageTimer = 0;
// Find all enemies in range
var enemiesInRange = [];
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.getRange() && !enemy.isHidden) {
enemiesInRange.push(enemy);
}
}
// Damage all enemies in range
for (var i = 0; i < enemiesInRange.length; i++) {
var enemy = enemiesInRange[i];
var damagePerTick = oneHitKillCheat ? 999999 : self.damage + self.level * 2;
enemy.health -= damagePerTick;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Visual effect
LK.effects.flashObject(enemy, 0xC0C0C0, 100);
}
// Play sound if enemies were hit
if (enemiesInRange.length > 0) {
LK.getSound('tower_shoot').play();
}
}
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 : artifacts.aresHelmet ? self.damage * 2 : 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;
case 'gatling':
previewGraphics.tint = 0x708090; // Slate gray
break;
case 'swordsman':
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;
case 'gatling':
displayName = 'Gatling Gun';
break;
case 'swordsman':
displayName = 'Swordsman';
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 VillageBuilderMenu = Container.expand(function () {
var self = Container.call(this);
// Background
var background = self.attachAsset('manualBackground', {
anchorX: 0.5,
anchorY: 0.5
});
background.alpha = 0.95;
// Title
var titleText = new Text2('VILLAGE BUILDER', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0);
titleText.x = 0;
titleText.y = -1150;
self.addChild(titleText);
// Crystal display
var crystalDisplay = new Text2('Crystals: ' + crystals, {
size: 60,
fill: 0x00FFFF,
weight: 800
});
crystalDisplay.anchor.set(0.5, 0);
crystalDisplay.x = 0;
crystalDisplay.y = -1000;
self.addChild(crystalDisplay);
// Building data
var buildingData = [{
id: 'town_hall',
name: 'Town Hall',
description: 'Creates 5 crystals every 30 minutes',
cost: 0,
owned: villageBuildings.town_hall ? 1 : 0,
maxOwned: 1
}, {
id: 'house',
name: 'House',
description: 'Makes 1 crystal every 30 minutes',
cost: 10,
owned: villageBuildings.house || 0,
maxOwned: 10
}, {
id: 'crystal_mine',
name: 'Crystal Mine',
description: 'Creates 2 crystals every 15 minutes',
cost: 25,
owned: villageBuildings.crystal_mine || 0,
maxOwned: 5
}, {
id: 'defense_tower',
name: 'Defense Tower',
description: 'Defensive structure (cosmetic for now)',
cost: 15,
owned: villageBuildings.defense_tower || 0,
maxOwned: 8
}, {
id: 'barracks',
name: 'Barracks',
description: 'Military building (cosmetic for now)',
cost: 20,
owned: villageBuildings.barracks || 0,
maxOwned: 3
}];
var buildingButtons = [];
var yPos = -850;
for (var i = 0; i < buildingData.length; i++) {
var building = buildingData[i];
var buildingContainer = new Container();
buildingContainer.y = yPos;
var buildingBg = buildingContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buildingBg.width = 1400;
buildingBg.height = 180;
buildingBg.tint = building.owned >= building.maxOwned ? 0x444444 : crystals >= building.cost ? 0x00AA00 : 0x888888;
var nameText = new Text2(building.name + ' (' + building.owned + '/' + building.maxOwned + ')', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
nameText.anchor.set(0, 0.5);
nameText.x = -650;
nameText.y = -30;
buildingContainer.addChild(nameText);
var descText = new Text2(building.description, {
size: 40,
fill: 0xCCCCCC,
weight: 400
});
descText.anchor.set(0, 0.5);
descText.x = -650;
descText.y = 20;
buildingContainer.addChild(descText);
var costText = new Text2(building.owned >= building.maxOwned ? 'MAX BUILT' : building.cost === 0 ? 'FREE' : building.cost + ' Crystals', {
size: 50,
fill: building.owned >= building.maxOwned ? 0x888888 : building.cost === 0 ? 0x00FF00 : 0x00FFFF,
weight: 800
});
costText.anchor.set(1, 0.5);
costText.x = 650;
costText.y = 0;
buildingContainer.addChild(costText);
if (building.owned < building.maxOwned) {
buildingContainer.buildingId = building.id;
buildingContainer.cost = building.cost;
buildingContainer.down = function () {
if (crystals >= this.cost || this.cost === 0) {
crystals -= this.cost;
villageBuildings[this.buildingId] = (villageBuildings[this.buildingId] || 0) + 1;
// Update last collection time for new buildings
var currentTime = Date.now();
if (this.buildingId === 'town_hall') {
villageLastCollection.town_hall = currentTime;
} else if (this.buildingId === 'house') {
var houseKey = 'house_' + (villageBuildings.house - 1);
villageLastCollection[houseKey] = currentTime;
} else if (this.buildingId === 'crystal_mine') {
var mineKey = 'crystal_mine_' + (villageBuildings.crystal_mine - 1);
villageLastCollection[mineKey] = currentTime;
}
storage.crystals = crystals;
storage.villageBuildings = villageBuildings;
// Save collection times using flattened structure
storage.villageLastCollection_town_hall = villageLastCollection.town_hall;
if (this.buildingId === 'house') {
var houseKey = 'house_' + (villageBuildings.house - 1);
storage['villageLastCollection_' + houseKey] = villageLastCollection[houseKey];
} else if (this.buildingId === 'crystal_mine') {
var mineKey = 'crystal_mine_' + (villageBuildings.crystal_mine - 1);
storage['villageLastCollection_' + mineKey] = villageLastCollection[mineKey];
}
// Refresh menu
self.destroy();
var newMenu = new VillageBuilderMenu();
game.addChild(newMenu);
} else {
var notification = game.addChild(new Notification("Not enough crystals!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
LK.getSound('error').play();
}
};
}
self.addChild(buildingContainer);
buildingButtons.push(buildingContainer);
yPos += 200;
}
// Collect all button
var collectButton = new Container();
collectButton.y = yPos + 50;
var collectBg = collectButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
collectBg.width = 600;
collectBg.height = 120;
collectBg.tint = 0x00FF00;
var collectText = new Text2('COLLECT ALL CRYSTALS', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
collectText.anchor.set(0.5, 0.5);
collectButton.addChild(collectText);
collectButton.down = function () {
var totalCollected = collectVillageCrystals();
if (totalCollected > 0) {
crystals += totalCollected;
storage.crystals = crystals;
var notification = game.addChild(new Notification("Collected " + totalCollected + " crystals!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
// Refresh menu
self.destroy();
var newMenu = new VillageBuilderMenu();
game.addChild(newMenu);
} else {
var notification = game.addChild(new Notification("No crystals ready to collect!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
};
self.addChild(collectButton);
// 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();
};
self.x = 2048 / 2;
self.y = 2732 / 2;
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
}, {
type: 'hippo',
name: 'King Hippo',
tint: 0x8B4513,
count: 6
}, {
type: 'dragon',
name: 'Dragon',
tint: 0xFF4500,
count: 8
}, {
type: 'kangaroo',
name: 'Pink Kangaroo',
tint: 0xFF69B4,
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 () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 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;
// Crystal and artifact system
var crystals = storage.crystals || 0;
var artifacts = storage.artifacts || {
thunderbolt: false,
aresHelmet: false,
ymirsHammer: false
};
// Village builder system
var villageBuildings = storage.villageBuildings || {
town_hall: 0,
house: 0,
crystal_mine: 0,
defense_tower: 0,
barracks: 0
};
// Load village collection times from flattened storage
var villageLastCollection = {
town_hall: storage.villageLastCollection_town_hall || 0
};
// Load house collection times
for (var i = 0; i < 10; i++) {
var houseKey = 'house_' + i;
var storageKey = 'villageLastCollection_' + houseKey;
if (storage[storageKey]) {
villageLastCollection[houseKey] = storage[storageKey];
}
}
// Load crystal mine collection times
for (var i = 0; i < 5; i++) {
var mineKey = 'crystal_mine_' + i;
var storageKey = 'villageLastCollection_' + mineKey;
if (storage[storageKey]) {
villageLastCollection[mineKey] = storage[storageKey];
}
}
// Ensure first town hall is free
if (villageBuildings.town_hall === 0) {
villageBuildings.town_hall = 1;
villageLastCollection.town_hall = Date.now();
storage.villageBuildings = villageBuildings;
storage.villageLastCollection_town_hall = villageLastCollection.town_hall;
}
var lives = artifacts.ymirsHammer ? 35 : 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 artifactPrices = {
thunderbolt: 50,
aresHelmet: 100,
ymirsHammer: 75
};
// Achievement system - flattened structure for storage compatibility
var achievements = {
builder: {
completed: storage.achievement_builder_completed || false,
progress: storage.achievement_builder_progress || 0,
target: 10,
name: "Builder",
description: "Build 10 towers"
},
slayer: {
completed: storage.achievement_slayer_completed || false,
progress: storage.achievement_slayer_progress || 0,
target: 200,
name: "Slayer",
description: "Kill 200 enemies"
},
power_engineer: {
completed: storage.achievement_power_engineer_completed || false,
progress: storage.achievement_power_engineer_progress || 0,
target: 1,
name: "Power Engineer",
description: "Make a tower be at max level + have the special ability"
},
all_of_em: {
completed: storage.achievement_all_of_em_completed || false,
progress: storage.achievement_all_of_em_progress || 0,
target: 11,
name: "All of Em'",
description: "Make all types of towers be at max level+ have their special ability"
},
winner: {
completed: storage.achievement_winner_completed || false,
progress: storage.achievement_winner_progress || 0,
target: 1,
name: "Winner",
description: "Defeat all waves"
},
loser: {
completed: storage.achievement_loser_completed || false,
progress: storage.achievement_loser_progress || 0,
target: 3,
name: "Loser",
description: "Get defeated 3 times in a row while having spent at least 1000 gold in defenses"
},
farmer: {
completed: storage.achievement_farmer_completed || false,
progress: storage.achievement_farmer_progress || 0,
target: 1000,
name: "Farmer",
description: "Collect 1000 gold worth of Farm Income"
},
miss: {
completed: storage.achievement_miss_completed || false,
progress: storage.achievement_miss_progress || 0,
target: 1,
name: "Miss",
description: "Make a Mortar Tower miss"
},
cowardly: {
completed: storage.achievement_cowardly_completed || false,
progress: storage.achievement_cowardly_progress || 0,
target: 2,
name: "Cowardly",
description: "Make a magician tower hit 2 enemies in a row with a projectile with confusion"
}
};
// Function to save achievements to flattened storage
function saveAchievements() {
storage.achievement_builder_completed = achievements.builder.completed;
storage.achievement_builder_progress = achievements.builder.progress;
storage.achievement_slayer_completed = achievements.slayer.completed;
storage.achievement_slayer_progress = achievements.slayer.progress;
storage.achievement_power_engineer_completed = achievements.power_engineer.completed;
storage.achievement_power_engineer_progress = achievements.power_engineer.progress;
storage.achievement_all_of_em_completed = achievements.all_of_em.completed;
storage.achievement_all_of_em_progress = achievements.all_of_em.progress;
storage.achievement_winner_completed = achievements.winner.completed;
storage.achievement_winner_progress = achievements.winner.progress;
storage.achievement_loser_completed = achievements.loser.completed;
storage.achievement_loser_progress = achievements.loser.progress;
storage.achievement_farmer_completed = achievements.farmer.completed;
storage.achievement_farmer_progress = achievements.farmer.progress;
storage.achievement_miss_completed = achievements.miss.completed;
storage.achievement_miss_progress = achievements.miss.progress;
storage.achievement_cowardly_completed = achievements.cowardly.completed;
storage.achievement_cowardly_progress = achievements.cowardly.progress;
}
// Function to save achievement stats to flattened storage
function saveAchievementStats() {
storage.achievementStats_totalTowersBuilt = achievementStats.totalTowersBuilt;
storage.achievementStats_totalEnemiesKilled = achievementStats.totalEnemiesKilled;
storage.achievementStats_totalFarmIncome = achievementStats.totalFarmIncome;
storage.achievementStats_consecutiveDefeats = achievementStats.consecutiveDefeats;
storage.achievementStats_lastGameResult = achievementStats.lastGameResult;
storage.achievementStats_totalGoldSpentOnDefenses = achievementStats.totalGoldSpentOnDefenses;
storage.achievementStats_lastGameGoldSpent = achievementStats.lastGameGoldSpent;
storage.achievementStats_mortarMisses = achievementStats.mortarMisses;
storage.achievementStats_wizardConfusionStreak = achievementStats.wizardConfusionStreak;
storage.achievementStats_maxLevelTowerTypes = achievementStats.maxLevelTowerTypes;
}
var achievementStats = {
totalTowersBuilt: storage.achievementStats_totalTowersBuilt || 0,
totalEnemiesKilled: storage.achievementStats_totalEnemiesKilled || 0,
totalFarmIncome: storage.achievementStats_totalFarmIncome || 0,
consecutiveDefeats: storage.achievementStats_consecutiveDefeats || 0,
lastGameResult: storage.achievementStats_lastGameResult || 'none',
totalGoldSpentOnDefenses: storage.achievementStats_totalGoldSpentOnDefenses || 0,
lastGameGoldSpent: storage.achievementStats_lastGameGoldSpent || 0,
mortarMisses: storage.achievementStats_mortarMisses || 0,
wizardConfusionStreak: storage.achievementStats_wizardConfusionStreak || 0,
maxLevelTowerTypes: storage.achievementStats_maxLevelTowerTypes || []
};
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(livesText);
LK.gui.top.addChild(scoreText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
var crystalText = new Text2('Crystals: ' + crystals, {
size: 60,
fill: 0x00FFFF,
weight: 800
});
crystalText.anchor.set(0.5, 0.5);
crystalText.x = spacing;
crystalText.y = topMargin + 70;
LK.gui.top.addChild(crystalText);
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
crystalText.setText('Crystals: ' + crystals);
}
function collectVillageCrystals() {
var currentTime = Date.now();
var totalCrystals = 0;
var minutesToMs = 60 * 1000;
var maxOfflineMs = 4 * 60 * minutesToMs; // 4 hours cap
// Town hall: 5 crystals every 30 minutes
if (villageBuildings.town_hall > 0) {
var townHallInterval = 30 * minutesToMs;
var townHallLastTime = villageLastCollection.town_hall || 0;
var elapsedTime = Math.min(currentTime - townHallLastTime, maxOfflineMs);
var townHallCycles = Math.floor(elapsedTime / townHallInterval);
if (townHallCycles > 0) {
totalCrystals += townHallCycles * 5;
villageLastCollection.town_hall = townHallLastTime + townHallCycles * townHallInterval;
}
}
// Houses: 1 crystal every 30 minutes each
if (villageBuildings.house > 0) {
var houseInterval = 30 * minutesToMs;
for (var i = 0; i < villageBuildings.house; i++) {
var houseKey = 'house_' + i;
var houseLastTime = villageLastCollection[houseKey] || currentTime;
var elapsedTime = Math.min(currentTime - houseLastTime, maxOfflineMs);
var houseCycles = Math.floor(elapsedTime / houseInterval);
if (houseCycles > 0) {
totalCrystals += houseCycles * 1;
villageLastCollection[houseKey] = houseLastTime + houseCycles * houseInterval;
}
}
}
// Crystal mines: 2 crystals every 15 minutes each
if (villageBuildings.crystal_mine > 0) {
var mineInterval = 15 * minutesToMs;
for (var i = 0; i < villageBuildings.crystal_mine; i++) {
var mineKey = 'crystal_mine_' + i;
var mineLastTime = villageLastCollection[mineKey] || currentTime;
var elapsedTime = Math.min(currentTime - mineLastTime, maxOfflineMs);
var mineCycles = Math.floor(elapsedTime / mineInterval);
if (mineCycles > 0) {
totalCrystals += mineCycles * 2;
villageLastCollection[mineKey] = mineLastTime + mineCycles * mineInterval;
}
}
}
// Save updated collection times using flattened structure
storage.villageLastCollection_town_hall = villageLastCollection.town_hall;
// Save house collection times
for (var i = 0; i < villageBuildings.house; i++) {
var houseKey = 'house_' + i;
if (villageLastCollection[houseKey]) {
storage['villageLastCollection_' + houseKey] = villageLastCollection[houseKey];
}
}
// Save crystal mine collection times
for (var i = 0; i < villageBuildings.crystal_mine; i++) {
var mineKey = 'crystal_mine_' + i;
if (villageLastCollection[mineKey]) {
storage['villageLastCollection_' + mineKey] = villageLastCollection[mineKey];
}
}
return totalCrystals;
}
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;
case 'gatling':
cost = 20;
break;
case 'swordsman':
cost = 25;
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();
}
// Achievement tracking: Builder
achievementStats.totalTowersBuilt++;
achievementStats.totalGoldSpentOnDefenses += towerCost;
achievementStats.lastGameGoldSpent += towerCost;
if (!achievements.builder.completed) {
achievements.builder.progress = achievementStats.totalTowersBuilt;
if (achievements.builder.progress >= achievements.builder.target) {
achievements.builder.completed = true;
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.builder.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
}
saveAchievements();
saveAchievementStats();
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) {
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;
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', 'gatling'],
color: 0x4488FF
},
'Area Damage': {
towers: ['splash', 'laser', 'mortar', 'swordsman'],
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();
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]);
// Play theme music
LK.playMusic('game_theme');
// 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.down = function () {
var manual = new Manual();
game.addChild(manual);
};
game.addChild(manualButton);
// Create artifact button
var artifactButton = new Container();
var artifactBg = artifactButton.attachAsset('manualButton', {
anchorX: 0.5,
anchorY: 0.5
});
artifactBg.tint = 0x00FFFF;
var artifactText = new Text2('A', {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
artifactText.anchor.set(0.5, 0.5);
artifactButton.addChild(artifactText);
artifactButton.x = 2048 - 320;
artifactButton.y = 150;
artifactButton.down = function () {
var artifactMenu = new ArtifactMenu();
game.addChild(artifactMenu);
};
game.addChild(artifactButton);
// 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.down = function () {
var cheatMenu = new CheatMenu();
game.addChild(cheatMenu);
};
game.addChild(cheatButton);
// Create village builder button
var villageButton = new Container();
var villageBg = villageButton.attachAsset('manualButton', {
anchorX: 0.5,
anchorY: 0.5
});
villageBg.tint = 0x8B4513;
var villageText = new Text2('V', {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
villageText.anchor.set(0.5, 0.5);
villageButton.addChild(villageText);
villageButton.x = 2048 - 440;
villageButton.y = 150;
villageButton.down = function () {
// Collect crystals automatically when opening village
var collected = collectVillageCrystals();
if (collected > 0) {
crystals += collected;
storage.crystals = crystals;
updateUI();
var notification = game.addChild(new Notification("Auto-collected " + collected + " crystals!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
var villageMenu = new VillageBuilderMenu();
game.addChild(villageMenu);
};
game.addChild(villageButton);
// Create achievements button
var achievementsButton = new Container();
var achievementsBg = achievementsButton.attachAsset('manualButton', {
anchorX: 0.5,
anchorY: 0.5
});
achievementsBg.tint = 0xFFD700;
var achievementsText = new Text2('⚡', {
size: 70,
fill: 0xFFFFFF,
weight: 800
});
achievementsText.anchor.set(0.5, 0.5);
achievementsButton.addChild(achievementsText);
achievementsButton.x = 2048 - 560;
achievementsButton.y = 150;
achievementsButton.down = function () {
var achievementsMenu = new AchievementsMenu();
game.addChild(achievementsMenu);
};
game.addChild(achievementsButton);
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', 'hippo', 'dragon', 'kangaroo'];
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;
// Achievement tracking: Farmer
achievementStats.totalFarmIncome += totalFarmIncome;
if (!achievements.farmer.completed) {
achievements.farmer.progress = achievementStats.totalFarmIncome;
if (achievements.farmer.progress >= achievements.farmer.target) {
achievements.farmer.completed = true;
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.farmer.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 300;
}
}
}
// 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;
}
// Achievement tracking: Slayer
achievementStats.totalEnemiesKilled++;
if (!achievements.slayer.completed) {
achievements.slayer.progress = achievementStats.totalEnemiesKilled;
if (achievements.slayer.progress >= achievements.slayer.target) {
achievements.slayer.completed = true;
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.slayer.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
// 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;
// Award crystals for boss kill (only if no cheats active)
if (!unlimitedMoneyCheat && !oneHitKillCheat) {
crystals += 5;
storage.crystals = crystals;
updateUI();
var crystalNotification = game.addChild(new Notification("+5 Crystals!"));
crystalNotification.x = 2048 / 2;
crystalNotification.y = grid.height - 200;
}
}
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();
// Achievement tracking: Loser
achievementStats.lastGameResult = 'lose';
if (achievementStats.lastGameGoldSpent >= 1000) {
achievementStats.consecutiveDefeats++;
if (!achievements.loser.completed) {
achievements.loser.progress = achievementStats.consecutiveDefeats;
if (achievements.loser.progress >= achievements.loser.target) {
achievements.loser.completed = true;
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.loser.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
} else {
achievementStats.consecutiveDefeats = 0;
}
// Reset game gold spent for next game
achievementStats.lastGameGoldSpent = 0;
saveAchievements();
saveAchievementStats();
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();
// Achievement tracking: Winner
achievementStats.lastGameResult = 'win';
achievementStats.consecutiveDefeats = 0;
if (!achievements.winner.completed) {
achievements.winner.completed = true;
achievements.winner.progress = 1;
var notification = game.addChild(new Notification("Achievement Unlocked: " + achievements.winner.name + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 100;
}
saveAchievements();
saveAchievementStats();
LK.showYouWin();
}
// Auto-collect village crystals every 5 minutes
if (LK.ticks % (60 * 60 * 5) === 0) {
var collected = collectVillageCrystals();
if (collected > 0) {
crystals += collected;
storage.crystals = crystals;
updateUI();
var notification = game.addChild(new Notification("Village produced " + collected + " crystals!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
}; ===================================================================
--- original.js
+++ change.js
@@ -809,8 +809,27 @@
// Mixed will be handled as a special spawner type
self.maxHealth = 1; // Placeholder, should not be attacked
self.isSpawner = true;
break;
+ case 'hippo':
+ self.maxHealth = 150; // Tanky enemy
+ self.speed *= 0.6; // Slower movement
+ self.canEatTowers = true;
+ self.eatingTimer = 0;
+ break;
+ case 'dragon':
+ self.isFlying = true;
+ self.maxHealth = 90;
+ self.speed *= 1.2; // Faster than normal flying
+ self.canStunTowers = true;
+ self.stunCooldown = 0;
+ break;
+ case 'kangaroo':
+ self.maxHealth = 70;
+ self.speed *= 1.4; // Fast movement
+ self.canJumpOverTowers = true;
+ self.jumpCooldown = 0;
+ break;
case 'normal':
default:
// Normal enemy uses default values
break;
@@ -983,8 +1002,97 @@
}
enemies.push(newEnemy);
}
}
+ // Handle King Hippo tower eating
+ if (self.type === 'hippo' && self.canEatTowers && self.currentCellY >= 4) {
+ self.eatingTimer++;
+ if (self.eatingTimer >= 60) {
+ // Check every second
+ self.eatingTimer = 0;
+ // Look for nearby towers to eat
+ for (var i = 0; i < towers.length; i++) {
+ var tower = towers[i];
+ var dx = tower.x - self.x;
+ var dy = tower.y - self.y;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance <= CELL_SIZE * 1.5) {
+ // Close enough to eat
+ // "Eat" the tower (remove it)
+ var gridX = tower.gridX;
+ var gridY = tower.gridY;
+ for (var gi = 0; gi < 2; gi++) {
+ for (var gj = 0; gj < 2; gj++) {
+ var cell = grid.getCell(gridX + gi, gridY + gj);
+ if (cell) {
+ cell.type = 0;
+ }
+ }
+ }
+ var towerIndex = towers.indexOf(tower);
+ if (towerIndex !== -1) {
+ towers.splice(towerIndex, 1);
+ }
+ towerLayer.removeChild(tower);
+ // Heal the hippo
+ self.health = Math.min(self.maxHealth, self.health + 30);
+ self.healthBar.width = self.health / self.maxHealth * 70;
+ var notification = game.addChild(new Notification("King Hippo ate a tower!"));
+ notification.x = 2048 / 2;
+ notification.y = grid.height - 100;
+ grid.pathFind();
+ updateLaserWalls();
+ break; // Only eat one tower per check
+ }
+ }
+ }
+ }
+ // Handle Dragon tower stunning
+ if (self.type === 'dragon' && self.canStunTowers && self.currentCellY >= 4) {
+ self.stunCooldown--;
+ if (self.stunCooldown <= 0) {
+ self.stunCooldown = 180; // 3 second cooldown
+ // Stun nearby towers
+ for (var i = 0; i < towers.length; i++) {
+ var tower = towers[i];
+ var dx = tower.x - self.x;
+ var dy = tower.y - self.y;
+ var distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance <= CELL_SIZE * 2.5) {
+ // Stun range
+ tower.isStunned = true;
+ tower.stunDuration = 120; // 2 seconds
+ // Visual effect
+ LK.effects.flashObject(tower, 0xFFFF00, 300);
+ }
+ }
+ if (towers.length > 0) {
+ var notification = game.addChild(new Notification("Dragon stunned nearby towers!"));
+ notification.x = 2048 / 2;
+ notification.y = grid.height - 50;
+ }
+ }
+ }
+ // Handle Pink Kangaroo jumping
+ if (self.type === 'kangaroo' && self.canJumpOverTowers && self.currentCellY >= 4) {
+ self.jumpCooldown--;
+ if (self.jumpCooldown <= 0) {
+ // Check if there's a tower blocking the path ahead
+ var cellAhead = grid.getCell(self.cellX, self.cellY - 1);
+ if (cellAhead && cellAhead.type === 1) {
+ // Tower blocking
+ self.jumpCooldown = 240; // 4 second cooldown
+ // Jump over the tower (move 2 cells forward)
+ self.currentCellY -= 2;
+ self.y = grid.y + self.currentCellY * CELL_SIZE;
+ // Visual effect
+ LK.effects.flashObject(self, 0xFF69B4, 500);
+ var notification = game.addChild(new Notification("Pink Kangaroo jumped over tower!"));
+ notification.x = 2048 / 2;
+ notification.y = grid.height - 150;
+ }
+ }
+ }
// Handle confusion effect
if (self.isConfused && !self.isImmune) {
self.confusionDuration--;
if (self.confusionDuration <= 0) {
@@ -1990,8 +2098,17 @@
}, {
name: 'MOLE DIGGER',
stats: 'Health: 120 • Speed: 0.8x/2x Normal • Ground unit\nHides underground periodically, untargetable while hidden'
}, {
+ name: 'KING HIPPO',
+ stats: 'Health: 150 • Speed: 0.6x Normal • Ground unit\nEats towers to heal itself and clear path'
+ }, {
+ name: 'DRAGON',
+ stats: 'Health: 90 • Speed: 1.2x Normal • Flying unit\nStuns nearby towers periodically'
+ }, {
+ name: 'PINK KANGAROO',
+ stats: 'Health: 70 • Speed: 1.4x Normal • Ground unit\nJumps over towers that block its path'
+ }, {
name: 'BOSS',
stats: 'Health: 20x Normal • Speed: 0.7x Normal\nAppears every 10th wave, massive health'
}, {
name: 'MIXED',
@@ -2939,8 +3056,16 @@
}
return targetEnemy;
};
self.update = function () {
+ // Check if tower is stunned by dragon
+ if (self.isStunned) {
+ self.stunDuration--;
+ if (self.stunDuration <= 0) {
+ self.isStunned = false;
+ }
+ return; // Skip normal tower behavior while stunned
+ }
// Farm towers don't target or shoot
if (self.id === 'farm') {
return;
}
@@ -4258,8 +4383,23 @@
type: 'mixed',
name: 'Mixed',
tint: 0xFFFFFF,
count: 12
+ }, {
+ type: 'hippo',
+ name: 'King Hippo',
+ tint: 0x8B4513,
+ count: 6
+ }, {
+ type: 'dragon',
+ name: 'Dragon',
+ tint: 0xFF4500,
+ count: 8
+ }, {
+ type: 'kangaroo',
+ name: 'Pink Kangaroo',
+ tint: 0xFF69B4,
+ count: 12
}];
var randomChoice = randomWaveTypes[Math.floor(Math.random() * randomWaveTypes.length)];
enemyType = randomChoice.type;
waveType = randomChoice.name;
@@ -5325,9 +5465,9 @@
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 mixedTypes = ['normal', 'fast', 'immune', 'flying', 'swarm', 'mole', 'hippo', 'dragon', 'kangaroo'];
var randomType = mixedTypes[Math.floor(Math.random() * mixedTypes.length)];
enemy = new Enemy(randomType);
} else {
enemy = new Enemy(waveType);
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