User prompt
Change the name in the tutorial to Hangar Panic
User prompt
the basic turret glow isn't visible. make it bigger and pulsing โช๐ก Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add a glow below the basic turret at after the tutorial is hidden. The glow disappears as soon as the player places their first turret โช๐ก Consider importing and using the following plugins: @upit/tween.v1
User prompt
make sure the tutorial lets people know they can drag a turret over top of another to auto-sell
User prompt
Please fix the bug: 'towerTypes is not defined' in or related to this line: 'tween(targetStar, {' Line Number: 3247
User prompt
Once the tutorial window is gone, add wiggle effect to basic turret every 3 seconds until first turret is placed. Also, change the name of the "poison" turret to "hack". Make sure all instances of poison are changed so nothing breaks โช๐ก Consider importing and using the following plugins: @upit/tween.v1
User prompt
Position left gradient fade at the left edge of the screen
User prompt
the left gradient should be at the left edge of the screen
User prompt
add a gradient fade out to the left and right edges of the timeline
User prompt
Upcoming waves in the wave timeline should be darker the furthur they are from being active โช๐ก Consider importing and using the following plugins: @upit/tween.v1
User prompt
add a page that explains boost
User prompt
move the whole tutorial window and all its contents up by 150px
User prompt
add a button click sound when the tutorial buttons are pressed
Code edit (1 edits merged)
Please save this source code
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 6;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Apply special effects based on bullet type
if (self.type === '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 -= 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 === 'hack') {
// Prevent hack effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual hack effect
var hackEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'hack');
game.addChild(hackEffect);
// Apply hack effect
self.targetEnemy.hacked = true;
self.targetEnemy.hackDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.hackDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
} else if (self.type === 'drain') {
// Create visual drain effect
var drainEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'drain');
game.addChild(drainEffect);
// Apply splash damage to nearby enemies (same as splash but half damage)
var drainRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var drainDx = otherEnemy.x - self.targetEnemy.x;
var drainDy = otherEnemy.y - self.targetEnemy.y;
var drainDistance = Math.sqrt(drainDx * drainDx + drainDy * drainDy);
if (drainDistance <= drainRadius) {
// Apply drain damage (50% of original damage, same as splash)
otherEnemy.health -= self.damage * 0.5;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var City = Container.expand(function (spriteVariant) {
var self = Container.call(this);
self.spriteVariant = spriteVariant || 0;
var cityGraphics = self.attachAsset('city', {
anchorX: 0.5,
anchorY: 0.5
});
// Make cities slightly smaller to fit nicely
cityGraphics.scaleX = 0.9;
cityGraphics.scaleY = 0.9;
// Use different tints for different sprite variants
var cityTints = [0x888888, 0x666666, 0x999999, 0x777777, 0xAAAAAA, 0x555555];
cityGraphics.tint = cityTints[self.spriteVariant % cityTints.length];
self.cityGraphics = cityGraphics;
// Add glowing effect to forcefield tiles
self.startGlowEffect = function () {
function glowCycle() {
// Glow brighter (more white tint)
tween(self.cityGraphics, {
tint: 0xCCCCFF
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Return to original color
tween(self.cityGraphics, {
tint: cityTints[self.spriteVariant % cityTints.length]
}, {
duration: 1500,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Continue the glow cycle
glowCycle();
}
});
}
});
}
// Start the glow cycle
glowCycle();
};
// Start glowing immediately
self.startGlowEffect();
self.flashRed = function () {
// Flash red when hit by enemy
tween(self.cityGraphics, {
tint: 0xFF0000
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self.cityGraphics, {
tint: cityTints[self.spriteVariant % cityTints.length]
}, {
duration: 300,
easing: tween.easeIn
});
}
});
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('ground', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
cellGraphics.tint = 0xFFFFFF;
return;
}
numberLabel.visible = false; // Hide numbers for path cells too
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0xFFFFFF;
} else {
cellGraphics.tint = 0xFFFFFF;
}
// Remove all arrows - no longer showing direction indicators
self.removeArrows();
break;
}
case 1:
{
// Check if this is a tower cell (has a tower on it)
var hasTower = false;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (tower.gridX <= data.x && tower.gridX + 1 >= data.x && tower.gridY <= data.y && tower.gridY + 1 >= data.y) {
hasTower = true;
break;
}
}
// Remove current cell graphics
self.removeChild(cellGraphics);
// Use grass under towers, wall graphics for outer walls
if (hasTower) {
cellGraphics = self.attachAsset('ground', {
anchorX: 0.5,
anchorY: 0.5
});
} else {
cellGraphics = self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
}
cellGraphics.tint = 0xFFFFFF;
numberLabel.visible = false;
break;
}
case 3:
{
cellGraphics.tint = 0xFFFFFF;
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
numberLabel.visible = false; // Hide all tile numbers
};
});
// 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;
// Use different assets based on effect type
var assetId = 'rangeCircle';
if (type === 'splash') {
assetId = 'splash_explosion';
}
var effectGraphics = self.attachAsset(assetId, {
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 'hack':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'drain':
effectGraphics.tint = 0x8800FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
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 = .012;
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;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 2; // Twice as fast
self.maxHealth = 100;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Add 5% speed variability to individual mobs (random multiplier between 0.95 and 1.05)
var speedVariability = 0.95 + Math.random() * 0.1;
self.speed = self.speed * speedVariability;
// 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 slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or hacked, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.hacked = false;
self.hackEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle hack effect
if (self.hacked) {
// Visual indication of hacked status
if (!self.hackEffect) {
self.hackEffect = true;
}
// Apply hack damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.hackDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.hackDuration--;
if (self.hackDuration <= 0) {
self.hacked = false;
self.hackEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.hacked && self.slowed) {
// Combine hack (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.hacked) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
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 EnergyBoostIndicator = Container.expand(function (value) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 120,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 4;
shadowText.y = 4;
self.addChild(shadowText);
var energyText = new Text2("+" + value, {
size: 120,
fill: 0xFFD700,
weight: 800
});
energyText.anchor.set(0.5, 0.5);
self.addChild(energyText);
self.x = 2048 / 2;
self.y = 2732 / 2;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.5,
scaleY: 1.5,
y: 2732 / 2 - 100
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 2,
scaleY: 2,
y: 2732 / 2 - 200
}, {
duration: 800,
easing: tween.easeIn,
delay: 500,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
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);
// Check for collision with cities instead of just checking cell type
for (var c = 0; c < cities.length; c++) {
var city = cities[c];
var dx = enemy.x - city.x;
var dy = enemy.y - city.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is close enough to hit the city (collision detection)
if (distance <= CELL_SIZE * 0.8) {
// Flash the city red when hit
city.flashRed();
// Play shield hit sound
LK.getSound('shield_hit').play();
// Shake the shields text - only if not already shaking
if (!livesText.isShaking) {
livesText.isShaking = true;
var originalX = livesText.x;
var shakeAmount = 10;
// Stop any existing shakes and color tweens
tween.stop(livesText, {
x: true,
fill: true
});
// Reset position before starting new shake
livesText.x = originalX;
// Change text color to red and start shaking
tween(livesText, {
fill: 0xFF0000
}, {
duration: 0
});
// First shake to the right
tween(livesText, {
x: originalX + shakeAmount
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
// Then shake to the left
tween(livesText, {
x: originalX - shakeAmount
}, {
duration: 100,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Finally return to center and restore blue color
tween(livesText, {
x: originalX
}, {
duration: 50,
easing: tween.easeIn,
onFinish: function onFinish() {
// Return text color to neon blue
tween(livesText, {
fill: 0x00FFFF
}, {
duration: 0,
onFinish: function onFinish() {
// Mark shake as complete
livesText.isShaking = false;
}
});
}
});
}
});
}
});
}
// Shake the city that was hit
tween.stop(city, {
x: true
});
var cityOriginalX = city.x;
var cityShakeAmount = 8;
tween(city, {
x: cityOriginalX + cityShakeAmount
}, {
duration: 40,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(city, {
x: cityOriginalX - cityShakeAmount
}, {
duration: 80,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(city, {
x: cityOriginalX
}, {
duration: 40,
easing: tween.easeIn
});
}
});
}
});
// Slightly smaller than full cell size for better collision
return true; // Enemy hits city, dies and player loses life
}
}
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.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
if (enemy.currentTarget) {
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var 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("โ BOOST โ", {
size: 50,
fill: 0x000000,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
// Check if all on-screen enemies are dead
var allEnemiesDead = enemies.length === 0;
var waveComplete = !waveInProgress;
// Show boost button when all on-screen enemies are dead and game has started, but hide if shields are 0
if (waveIndicator && waveIndicator.gameStarted && allEnemiesDead && waveComplete && lives > 0) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0xFFD700;
self.alpha = 1;
buttonText.setText("โ BOOST โ");
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
buttonText.setText("โ BOOST โ");
}
};
self.down = function () {
if (!self.enabled) {
return;
}
// Give 10% energy boost, but never less than 2
var energyBoost = Math.max(2, Math.floor(energy * 0.1));
setEnergy(energy + energyBoost);
// Display energy boost indicator in center of screen
var energyBoostIndicator = new EnergyBoostIndicator(energyBoost);
game.addChild(energyBoostIndicator);
// Hide boost button after use
self.visible = false;
// Call the next wave immediately
waveTimer = nextWaveTime;
};
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';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
// Lock overlay will be handled externally to avoid alpha inheritance
self.lockOverlay = null; // Reference will be set externally
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'sniper':
baseGraphics.tint = 0xFF5500;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
case 'slow':
baseGraphics.tint = 0x9900FF;
break;
case 'hack':
baseGraphics.tint = 0x00FFAA;
break;
case 'drain':
baseGraphics.tint = 0xFFD700;
break;
case 'basic':
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var typeLabelShadow = new Text2(self.towerType.toUpperCase(), {
size: 25,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -40 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var typeLabel = new Text2(self.towerType.toUpperCase(), {
size: 25,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -40; // 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 = 4;
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 = 0;
self.addChild(costLabel);
self.update = function () {
// Check if tower type is unlocked based on current wave
var towerTypes = ['basic', 'rapid', 'sniper', 'splash', 'slow', 'hack', 'drain'];
var towerIndex = towerTypes.indexOf(self.towerType);
var unlockWaves = [0, 2, 3, 4, 5, 6, 7]; // Wave requirements for each tower type
var isUnlocked = towerIndex >= 0 && currentWave >= unlockWaves[towerIndex];
// Lock overlay visibility will be handled externally
// Store unlock status for external reference
self.isUnlocked = isUnlocked;
// Check if player can afford this tower (only matters if unlocked)
var canAfford = isUnlocked && energy >= getTowerCost(self.towerType);
// Set opacity based on unlock status and affordability
if (!isUnlocked) {
self.alpha = 0.4; // Darker for locked towers
} else {
self.alpha = canAfford ? 1 : 0.5; // Normal affordability logic for unlocked towers
}
};
self.down = function (x, y, obj) {
// Check if tower type is unlocked
var towerTypes = ['basic', 'rapid', 'sniper', 'splash', 'slow', 'hack', 'drain'];
var towerIndex = towerTypes.indexOf(self.towerType);
var unlockWaves = [0, 2, 3, 4, 5, 6, 7]; // Wave requirements for each tower type
var isUnlocked = towerIndex >= 0 && currentWave >= unlockWaves[towerIndex];
// Prevent interaction with locked towers
if (!isUnlocked) {
var requiredWave = unlockWaves[towerIndex];
var wavesUntilUnlock = requiredWave - currentWave;
var notification = game.addChild(new Notification("Unlocks in " + wavesUntilUnlock + " wave" + (wavesUntilUnlock > 1 ? "s" : "") + "!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
// Only count clicks on drain tower before game starts
if (self.towerType === 'drain' && waveIndicator && !waveIndicator.gameStarted) {
drainTowerClickCount++;
if (drainTowerClickCount >= 6 && !drainTowerCostReduced) {
drainTowerCostReduced = true;
var notification = game.addChild(new Notification("Drain Tower cost reduced to 5!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
switch (self.id) {
case 'sniper':
// Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost
if (self.level === self.maxLevel) {
return 12 * CELL_SIZE; // Significantly increased range for max level
}
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 'hack':
// Hack: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'drain':
// Drain: base 6, +0.6 per level (larger than sniper)
return (6 + (self.level - 1) * 0.6) * CELL_SIZE;
case 'basic':
default:
// Basic: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 6;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
switch (self.id) {
case 'rapid':
self.fireRate = 18; // Adjusted for better DPS balance
self.damage = 3.2; // Increased base damage
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 8.4;
self.rapidFireSide = 0; // Track which side to fire from (0 = left, 1 = right)
break;
case 'sniper':
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 30;
break;
case 'splash':
self.fireRate = 75;
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4.8;
break;
case 'slow':
self.fireRate = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 6;
break;
case 'hack':
self.fireRate = 70;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 6;
break;
case 'drain':
self.fireRate = 150;
self.damage = 1.875; // Quarter of original damage (7.5 / 4)
self.range = 6 * CELL_SIZE; // Larger than sniper base range (5)
self.bulletSpeed = 4.8;
break;
}
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
// Store the base tint color for later use with turret
var baseTintColor;
switch (self.id) {
case 'rapid':
baseTintColor = 0x00AAFF;
baseGraphics.tint = baseTintColor;
break;
case 'sniper':
baseTintColor = 0xFF5500;
baseGraphics.tint = baseTintColor;
break;
case 'splash':
baseTintColor = 0x33CC00;
baseGraphics.tint = baseTintColor;
break;
case 'slow':
baseTintColor = 0x9900FF;
baseGraphics.tint = baseTintColor;
break;
case 'hack':
baseTintColor = 0x00FFAA;
baseGraphics.tint = baseTintColor;
break;
case 'drain':
baseTintColor = 0xFFD700;
baseGraphics.tint = baseTintColor;
break;
case 'basic':
default:
baseTintColor = 0xAAAAAA;
baseGraphics.tint = baseTintColor;
}
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 tower-specific arrow asset based on tower type
var arrowAssetId = 'arrow';
if (self.id !== 'basic') {
arrowAssetId = 'arrow_' + self.id;
}
var gunGraphics = gunContainer.attachAsset(arrowAssetId, {
anchorX: self.id === 'drain' ? 0.5 : 0.35,
anchorY: 0.5
});
// Turret remains default color (no tinting applied)
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 'hack':
towerLevelIndicator.tint = 0x00FFAA;
break;
case 'drain':
towerLevelIndicator.tint = 0xFFD700;
break;
case 'basic':
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// 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 (energy >= upgradeCost) {
setEnergy(energy - upgradeCost);
self.level++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(3, 18 - self.level * 4.2); // Adjusted for better DPS scaling
self.damage = 3.2 + self.level * 4.8; // Increased damage scaling
self.bulletSpeed = 8.4 + self.level * 2.88; // double the effect
} else {
self.fireRate = Math.max(8, 18 - self.level * 1.4); // Slightly slower fire rate reduction
self.damage = 3.2 + self.level * 1.8; // Increased damage scaling
self.bulletSpeed = 8.4 + self.level * 0.84;
}
} 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 = 6 + self.level * 2.88; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 6 + self.level * 0.6;
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
// Play upgrade sound effect
LK.getSound('tower_upgrade').play();
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 energy to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, use the original path-based targeting
// Get the cell for this enemy
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell && cell.pathId === pathId) {
// Use the cell's score (distance to exit) for prioritization
// Lower score means closer to exit
if (cell.score < closestScore) {
closestScore = cell.score;
closestEnemy = enemy;
}
}
}
}
}
if (!closestEnemy) {
self.targetEnemy = null;
}
return closestEnemy;
};
self.update = function () {
if (self.id === 'drain') {
// Drain tower doesn't need to target specific enemies, it affects all in range
var enemiesInRange = false;
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()) {
enemiesInRange = true;
break;
}
}
if (enemiesInRange && LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
} else {
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;
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
}
};
self.down = function (x, y, obj) {
// Check if clicking on the same tower that's already selected
if (selectedTower === self && singleUpgradeButton && singleUpgradeButton.visible) {
// Hide UI and deselect
hideUpgradeButtons();
selectedTower = null;
grid.renderDebug();
return;
}
// Hide any existing UI
hideUpgradeButtons();
// Remove any existing range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
// Create range indicator
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
// Insert range indicator after debugLayer but before towerLayer
var debugLayerIndex = game.getChildIndex(debugLayer);
game.addChildAt(rangeIndicator, debugLayerIndex + 1);
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.15;
// Tint range marker to match tower type color
switch (self.id) {
case 'rapid':
rangeGraphics.tint = 0x00AAFF;
break;
case 'sniper':
rangeGraphics.tint = 0xFF5500;
break;
case 'splash':
rangeGraphics.tint = 0x33CC00;
break;
case 'slow':
rangeGraphics.tint = 0x9900FF;
break;
case 'hack':
rangeGraphics.tint = 0x00FFAA;
break;
case 'drain':
rangeGraphics.tint = 0xFFD700;
break;
case 'basic':
default:
rangeGraphics.tint = 0xAAAAAA;
}
// Create single stats window if it doesn't exist
if (!singleStatsWindow) {
singleStatsWindow = new TowerStatsWindow(self);
game.addChild(singleStatsWindow);
} else {
singleStatsWindow.tower = self;
singleStatsWindow.updateStats();
}
// Check if turret is in bottom left area (bottom 1/3 and left 1/3 of grid)
var gridHeight = grid.cells[0].length;
var gridWidth = grid.cells.length;
var isInBottomThird = self.gridY >= gridHeight * 2 / 3;
var isInLeftThird = self.gridX <= gridWidth / 3;
if (isInBottomThird && isInLeftThird) {
// Position stats window on the right side of the grid, moved 380px left
singleStatsWindow.x = grid.x + gridWidth * CELL_SIZE + 50 - 380;
singleStatsWindow.y = grid.y + grid.cells[0].length * CELL_SIZE - 100 - 300 - 20 + 5;
} else {
// Position stats window at bottom left of play grid (original position)
singleStatsWindow.x = grid.x + 200 + 100 - 15 - 20;
singleStatsWindow.y = grid.y + grid.cells[0].length * CELL_SIZE - 100 - 300 - 20 + 5;
}
singleStatsWindow.visible = true;
// Create single sell button if it doesn't exist
if (!singleSellButton) {
singleSellButton = new Container();
game.addChild(singleSellButton);
var sellButtonBg = singleSellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBg.width = 180;
sellButtonBg.height = 80;
sellButtonBg.tint = 0xCC0000;
// Add sell icon
var sellIcon = singleSellButton.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
sellIcon.width = 30;
sellIcon.height = 30;
sellIcon.x = -35;
sellIcon.y = 0;
sellIcon.tint = 0xFFFFFF;
var sellButtonText = new Text2('', {
size: 32,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButtonText.x = 10;
singleSellButton.addChild(sellButtonText);
singleSellButton.down = function (x, y, obj) {
if (!selectedTower) {
return;
}
var totalInvestment = selectedTower.getTotalValue ? selectedTower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setEnergy(energy + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " energy!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = selectedTower.gridX;
var gridY = selectedTower.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(selectedTower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
var towerIndex = towers.indexOf(selectedTower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(selectedTower);
grid.pathFind();
grid.renderDebug();
hideUpgradeButtons();
selectedTower = null;
};
}
// Create single upgrade button if it doesn't exist
if (!singleUpgradeButton) {
singleUpgradeButton = new Container();
game.addChild(singleUpgradeButton);
var upgradeButtonBg = singleUpgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
upgradeButtonBg.width = 180;
upgradeButtonBg.height = 80;
// Add upgrade icon
var upgradeIcon = singleUpgradeButton.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
upgradeIcon.width = 30;
upgradeIcon.height = 30;
upgradeIcon.x = -35;
upgradeIcon.y = 0;
upgradeIcon.tint = 0xFFFFFF;
var upgradeButtonText = new Text2('', {
size: 32,
fill: 0xFFFFFF,
weight: 800
});
upgradeButtonText.anchor.set(0.5, 0.5);
upgradeButtonText.x = 10;
singleUpgradeButton.addChild(upgradeButtonText);
singleUpgradeButton.down = function (x, y, obj) {
if (!selectedTower) {
return;
}
if (selectedTower.level >= selectedTower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (selectedTower.upgrade()) {
// Update UI after upgrade
updateSingleUI();
// Update stats window if it exists
if (singleStatsWindow && singleStatsWindow.visible) {
singleStatsWindow.updateStats();
}
}
};
}
// Update and position UI elements
updateSingleUI();
function updateSingleUI() {
if (!selectedTower) {
return;
}
// Update sell button
var totalInvestment = selectedTower.getTotalValue ? selectedTower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
singleSellButton.children[2].setText(sellValue.toString());
singleSellButton.x = selectedTower.x - 120;
singleSellButton.y = selectedTower.y + 120;
// Update upgrade button
var isMaxLevel = selectedTower.level >= selectedTower.maxLevel;
var baseUpgradeCost = getTowerCost(selectedTower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (selectedTower.level === selectedTower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, selectedTower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, selectedTower.level - 1));
}
singleUpgradeButton.children[0].tint = isMaxLevel ? 0x888888 : energy >= upgradeCost ? 0x00AA00 : 0x888888;
singleUpgradeButton.children[2].setText(isMaxLevel ? 'MAX' : upgradeCost.toString());
singleUpgradeButton.x = selectedTower.x + 120;
singleUpgradeButton.y = selectedTower.y + 120;
// Update range circle if it exists
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === selectedTower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = selectedTower.getRange() * 2;
rangeGraphics.alpha = 0.15;
// Update tint to match tower type color
switch (selectedTower.id) {
case 'rapid':
rangeGraphics.tint = 0x00AAFF;
break;
case 'sniper':
rangeGraphics.tint = 0xFF5500;
break;
case 'splash':
rangeGraphics.tint = 0x33CC00;
break;
case 'slow':
rangeGraphics.tint = 0x9900FF;
break;
case 'poison':
rangeGraphics.tint = 0x00FFAA;
break;
case 'drain':
rangeGraphics.tint = 0xFFD700;
break;
case 'basic':
default:
rangeGraphics.tint = 0xAAAAAA;
}
}
}
updateSingleUI();
// Show UI elements
singleSellButton.visible = true;
singleUpgradeButton.visible = true;
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
if (self.id === 'drain') {
// Drain tower uses pulse effect instead of bullets
var currentRange = self.getRange();
// Create pulse effect
var pulseEffect = new Container();
pulseEffect.x = self.x;
pulseEffect.y = self.y;
// Add pulse effect after debugLayer (ground) but before towerLayer and enemyLayer
var debugLayerIndex = game.getChildIndex(debugLayer);
game.addChildAt(pulseEffect, debugLayerIndex + 1);
var pulseGraphics = pulseEffect.attachAsset('drain_pulse', {
anchorX: 0.5,
anchorY: 0.5
});
pulseGraphics.width = pulseGraphics.height = currentRange * 2;
pulseGraphics.alpha = 0;
pulseGraphics.scaleX = 0.3;
pulseGraphics.scaleY = 0.3;
// Add random rotation to the pulse
pulseGraphics.rotation = Math.random() * Math.PI * 2;
// Animate pulse expanding slower and fade out before full size
tween(pulseGraphics, {
alpha: 0.3,
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(pulseGraphics, {
alpha: 0.05,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
pulseEffect.destroy();
}
});
}
});
// Apply damage to 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);
if (distance <= currentRange) {
// Apply damage
enemy.health -= self.damage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Create visual drain effect on each enemy hit
var drainEffect = new EffectIndicator(enemy.x, enemy.y, 'drain');
game.addChild(drainEffect);
}
}
// Play drain tower shooting sound
LK.getSound('drain_shoot').play();
// Tower pulse effect - for drain tower, scale the turret instead of base
var elementToScale = self.id === 'drain' ? gunContainer : baseGraphics;
tween.stop(elementToScale, {
scaleX: true,
scaleY: true
});
tween(elementToScale, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(elementToScale, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
} else 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;
// Add alternating offset for rapid fire towers
if (self.id === 'rapid') {
var offsetDistance = 15; // Distance from center
var perpendicularAngle = gunContainer.rotation + Math.PI / 2; // 90 degrees from gun direction
var offsetMultiplier = self.rapidFireSide === 0 ? -1 : 1; // Left or right
bulletX += Math.cos(perpendicularAngle) * offsetDistance * offsetMultiplier;
bulletY += Math.sin(perpendicularAngle) * offsetDistance * offsetMultiplier;
// Toggle side for next shot
self.rapidFireSide = 1 - self.rapidFireSide;
}
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet rotation to match turret rotation
bullet.rotation = gunContainer.rotation;
// Set bullet type based on tower type
bullet.type = self.id;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// Customize bullet appearance based on tower type
switch (self.id) {
case 'rapid':
bullet.children[0].tint = 0x00AAFF;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
break;
case 'sniper':
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
break;
case 'splash':
bullet.children[0].tint = 0x33CC00;
bullet.children[0].width = 40;
bullet.children[0].height = 40;
break;
case 'slow':
bullet.children[0].tint = 0x9900FF;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
case 'hack':
bullet.children[0].tint = 0x00FFAA;
bullet.children[0].width = 35;
bullet.children[0].height = 35;
break;
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// Play turret shooting sound
LK.getSound('turret_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.15;
// Update range marker tint to match tower type color and reduce visibility
switch (self.towerType) {
case 'rapid':
rangeGraphics.tint = 0x00AAFF;
break;
case 'sniper':
rangeGraphics.tint = 0xFF5500;
break;
case 'splash':
rangeGraphics.tint = 0x33CC00;
break;
case 'slow':
rangeGraphics.tint = 0x9900FF;
break;
case 'poison':
rangeGraphics.tint = 0x00FFAA;
break;
case 'drain':
rangeGraphics.tint = 0xFFD700;
break;
case 'basic':
default:
rangeGraphics.tint = 0xAAAAAA;
}
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 = energy >= getTowerCost(self.towerType);
// Check for tower replacement opportunity
self.replacementTower = null;
for (var i = 0; i < towers.length; i++) {
var existingTower = towers[i];
// Check if the preview overlaps exactly with an existing tower
if (existingTower.gridX === self.gridX && existingTower.gridY === self.gridY) {
self.replacementTower = existingTower;
break;
}
}
// 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;
// Update range marker tint to match tower type color
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
rangeGraphics.tint = 0x00AAFF;
break;
case 'sniper':
previewGraphics.tint = 0xFF5500;
rangeGraphics.tint = 0xFF5500;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
rangeGraphics.tint = 0x33CC00;
break;
case 'slow':
previewGraphics.tint = 0x9900FF;
rangeGraphics.tint = 0x9900FF;
break;
case 'hack':
previewGraphics.tint = 0x00FFAA;
rangeGraphics.tint = 0x00FFAA;
break;
case 'drain':
previewGraphics.tint = 0xFFD700;
rangeGraphics.tint = 0xFFD700;
break;
case 'basic':
default:
previewGraphics.tint = 0xAAAAAA;
rangeGraphics.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 {
// Check if we're replacing an existing tower
var isReplacement = self.replacementTower !== null;
if (isReplacement) {
// For replacement, we can place on any tower location
validGridPlacement = true;
} else {
// For new placement, check if cells are free
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 = energy >= 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 TowerStatsWindow = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
var windowBackground = self.attachAsset('info_panel', {
anchorX: 0.5,
anchorY: 0.5
});
var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1), {
size: 40,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0.5, 0);
towerTypeText.x = 0;
towerTypeText.y = -90;
self.addChild(towerTypeText);
// Left column stats (LVL and DMG)
var leftStatsText = new Text2('LVL: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDMG: ' + self.tower.damage, {
size: 32,
fill: 0xFFFFFF,
weight: 400
});
leftStatsText.anchor.set(0.5, 0.5);
leftStatsText.x = -120;
leftStatsText.y = 15;
self.addChild(leftStatsText);
// Right column stats (SPD and RNG)
var rightStatsText = new Text2('SPD: ' + (60 / self.tower.fireRate).toFixed(1) + '/s\nRNG: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1), {
size: 32,
fill: 0xFFFFFF,
weight: 400
});
rightStatsText.anchor.set(0.5, 0.5);
rightStatsText.x = 120;
rightStatsText.y = 15;
self.addChild(rightStatsText);
// Update stats when tower is upgraded
self.updateStats = function () {
leftStatsText.setText('LVL: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDMG: ' + self.tower.damage);
rightStatsText.setText('SPD: ' + (60 / self.tower.fireRate).toFixed(1) + '/s\nRNG: ' + (self.tower.getRange() / CELL_SIZE).toFixed(1));
towerTypeText.setText(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1));
};
return self;
});
var TutorialOverlay = Container.expand(function () {
var self = Container.call(this);
self.currentPanel = 0;
self.totalPanels = 5;
self.isVisible = true;
// Semi-transparent dark background
var backdrop = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
backdrop.width = 4096;
backdrop.height = 5464;
backdrop.tint = 0x000000;
backdrop.alpha = 0.8;
// Main tutorial panel
var panel = self.attachAsset('info_panel', {
anchorX: 0.5,
anchorY: 0.5
});
panel.width = 1600;
panel.height = 1200;
panel.x = 0;
panel.y = 0;
// Title text
var titleText = new Text2('Hangar Panic', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
titleText.anchor.set(0.5, 0);
titleText.x = 0;
titleText.y = -280;
self.addChild(titleText);
// Main content text
var contentText = new Text2('', {
size: 40,
fill: 0xFFFFFF,
weight: 400
});
contentText.anchor.set(0.5, 0.5);
contentText.x = 0;
contentText.y = 40;
self.addChild(contentText);
// Navigation buttons positioned at bottom right, side by side
var prevButton = new Container();
var prevSprite = prevButton.attachAsset('tutorial_button', {
anchorX: 0.5,
anchorY: 0.5
});
prevSprite.width = 260;
prevSprite.height = 120;
prevSprite.tint = 0x0081f9;
var prevText = new Text2('< <', {
size: 32,
fill: 0xFFFFFF,
weight: 800
});
prevText.anchor.set(0.5, 0.5);
prevButton.addChild(prevText);
prevButton.x = 0;
prevButton.y = 480;
self.addChild(prevButton);
// Next button positioned beside prev button
var nextButton = new Container();
var nextSprite = nextButton.attachAsset('tutorial_button', {
anchorX: 0.5,
anchorY: 0.5
});
nextSprite.width = 260;
nextSprite.height = 120;
nextSprite.tint = 0x0081f9;
var nextText = new Text2('> >', {
size: 32,
fill: 0xFFFFFF,
weight: 800
});
nextText.anchor.set(0.5, 0.5);
nextButton.addChild(nextText);
nextButton.x = 270;
nextButton.y = 480;
self.addChild(nextButton);
// Skip button with different sprite at bottom left of panel
var skipButton = new Container();
var skipSprite = skipButton.attachAsset('tutorial_button', {
anchorX: 0.5,
anchorY: 0.5
});
skipSprite.width = 260;
skipSprite.height = 120;
skipSprite.tint = 0xFF4444;
var skipText = new Text2('SKIP', {
size: 32,
fill: 0xFFFFFF,
weight: 800
});
skipText.anchor.set(0.5, 0.5);
skipButton.addChild(skipText);
skipButton.x = -266;
skipButton.y = 480;
self.addChild(skipButton);
// Tutorial content for each panel
var tutorialContent = ["Enemy drones are invading!\n\nDrag turrets from the bottom to build them.\nDifferent turrets unlock as waves progress.\n\nTurrets cost ENERGY to build and upgrade.", "Enemies follow the path from top to bottom.\nThey want to reach your shield wall.\n\nIf enemies reach the shield, you lose SHIELDS.\nGame over when all shields are gone!", "Turret Types:\nโข BASIC - Balanced damage and range\nโข RAPID - Fast firing, short range\nโข SNIPER - Long range, high damage\nโข SPLASH - Area damage\nโข SLOW - Slows enemies\nโข HACK - Damage over time\nโข DRAIN - Energy bonus when enemies die", "ENERGY BOOST\n\nThe BOOST button appears between waves!\n\nClick it to instantly gain 10% energy\n(minimum 2 energy bonus)\n\nUse it to build that crucial turret\nor upgrade before the next wave hits!", "Click START GAME when ready!\n\nClick turrets to upgrade them.\nDrag turrets over existing ones to auto-sell!\nEarn energy by defeating enemies.\nSurvive all 50 waves to win!\n\nGood luck, Commander!"];
self.updatePanel = function () {
contentText.setText(tutorialContent[self.currentPanel]);
// Update button states
prevSprite.tint = self.currentPanel > 0 ? 0x0081f9 : 0x666666;
if (self.currentPanel === self.totalPanels - 1) {
nextText.setText('DONE');
nextSprite.tint = 0x00FF00;
} else {
nextText.setText('> >');
nextSprite.tint = 0x0081f9;
}
};
// Button handlers
prevButton.down = function () {
LK.getSound('turret_shoot').play();
if (self.currentPanel > 0) {
self.currentPanel--;
self.updatePanel();
}
};
nextButton.down = function () {
LK.getSound('turret_shoot').play();
if (self.currentPanel < self.totalPanels - 1) {
self.currentPanel++;
self.updatePanel();
} else {
// Last panel - close tutorial
self.closeTutorial();
}
};
skipButton.down = function () {
LK.getSound('turret_shoot').play();
self.closeTutorial();
};
self.closeTutorial = function () {
if (!self.isVisible) {
return;
}
self.isVisible = false;
tween(self, {
alpha: 0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
// Initialize first panel
self.updatePanel();
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
self.zIndex = 10000; // Ensure upgrade menu appears above everything
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : energy >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' energy', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' energy', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' energy');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' energy');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
// Update stats window if it exists
var statsWindows = game.children.filter(function (child) {
return child instanceof TowerStatsWindow && child.tower === self.tower;
});
for (var i = 0; i < statsWindows.length; i++) {
statsWindows[i].updateStats();
}
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.15;
// Tint range marker to match tower type color
switch (self.tower.id) {
case 'rapid':
rangeGraphics.tint = 0x00AAFF;
break;
case 'sniper':
rangeGraphics.tint = 0xFF5500;
break;
case 'splash':
rangeGraphics.tint = 0x33CC00;
break;
case 'slow':
rangeGraphics.tint = 0x9900FF;
break;
case 'poison':
rangeGraphics.tint = 0x00FFAA;
break;
case 'drain':
rangeGraphics.tint = 0xFFD700;
break;
default:
rangeGraphics.tint = 0xAAAAAA;
}
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setEnergy(energy + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " energy!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = energy >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' energy';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2 * 0.7;
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) {
// Close tutorial when start game is clicked
if (tutorialOverlay && tutorialOverlay.isVisible) {
tutorialOverlay.closeTutorial();
}
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2 * 0.7;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0xFFFF00;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Boss Normal";
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to the color of the wave type
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
break;
case 'fast':
block.tint = 0x00AAFF;
break;
case 'immune':
block.tint = 0xAA0000;
break;
case 'flying':
block.tint = 0xFFFF00;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave && enemyType !== 'swarm') {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 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 + (waveNumber - totalWaves) * 5;
}
return self.enemyCounts[waveNumber - 1] + Math.max(0, (waveNumber - 5) * 2);
};
// 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 * 0.7;
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 * 0.7;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146 * 0.7;
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 * 0.7;
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;
// Play wave start sound
LK.getSound('wave_start').play();
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: 0x0a0a1a,
title: 'Hangar Defender'
});
/****
* Game Code
****/
// Create outer space background elements
var spaceBackground = new Container();
game.addChild(spaceBackground);
// Create multiple layers of stars for depth
for (var starLayer = 0; starLayer < 3; starLayer++) {
var numStars = starLayer === 0 ? 150 : starLayer === 1 ? 100 : 50;
var starSize = starLayer === 0 ? 2 : starLayer === 1 ? 3 : 4;
var starAlpha = starLayer === 0 ? 0.6 : starLayer === 1 ? 0.8 : 1.0;
for (var i = 0; i < numStars; i++) {
var star = spaceBackground.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
star.width = starSize;
star.height = starSize;
star.x = Math.random() * 2048;
star.y = Math.random() * 2732;
star.tint = Math.random() > 0.7 ? 0xffffaa : 0xffffff;
star.alpha = starAlpha;
// Add twinkling effect to some stars
if (Math.random() > 0.8) {
var twinkleDelay = Math.random() * 3000;
LK.setTimeout(function (targetStar) {
return function () {
function twinkle() {
tween(targetStar, {
alpha: targetStar.alpha * 0.3
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(targetStar, {
alpha: starAlpha
}, {
duration: 800 + Math.random() * 400,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Schedule next twinkle
LK.setTimeout(twinkle, 2000 + Math.random() * 4000);
}
});
}
});
}
twinkle();
};
}(star), twinkleDelay);
}
}
}
// Create nebula effects using large, semi-transparent colored circles
for (var i = 0; i < 8; i++) {
var nebula = spaceBackground.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
nebula.width = 400 + Math.random() * 600;
nebula.height = nebula.width;
nebula.x = Math.random() * 2400 - 200; // Allow some off-screen positioning
nebula.y = Math.random() * 3000 - 200;
// Random nebula colors (purples, blues, magentas)
var nebulaColors = [0x4a0e4e, 0x2e1065, 0x1a237e, 0x4a148c, 0x6a1b9a, 0x311b92];
nebula.tint = nebulaColors[Math.floor(Math.random() * nebulaColors.length)];
nebula.alpha = 0.1 + Math.random() * 0.15;
// Slow rotation for nebula
tween(nebula, {
rotation: Math.PI * 2
}, {
duration: 60000 + Math.random() * 120000,
easing: tween.linear,
loop: true
});
}
var towerTypes = ['basic', 'rapid', 'sniper', 'splash', 'slow', 'hack', 'drain'];
var isHidingUpgradeMenu = false;
var drainTowerClickCount = 0;
var drainTowerCostReduced = false;
// Single reusable UI elements
var singleUpgradeButton = null;
var singleSellButton = null;
var singleStatsWindow = null;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
// Also clean up any stats windows for the same tower
var statsWindows = game.children.filter(function (child) {
return child instanceof TowerStatsWindow && child.tower === menu.tower;
});
for (var i = 0; i < statsWindows.length; i++) {
statsWindows[i].destroy();
}
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
// Remove from GUI layer or game layer
if (menu.parent) {
menu.parent.removeChild(menu);
}
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
function hideUpgradeButtons() {
// Hide single UI elements
if (singleUpgradeButton) {
singleUpgradeButton.visible = false;
}
if (singleSellButton) {
singleSellButton.visible = false;
}
if (singleStatsWindow) {
singleStatsWindow.visible = false;
}
// Remove range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var cities = [];
var selectedTower = null;
var energy = 800;
var lives = 20;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2 * 0.8 * 0.8;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var energyText = new Text2('ENERGY: ' + energy, {
size: 45,
fill: 0xFFD700,
weight: 800
});
energyText.anchor.set(0.5, 0.5);
var livesText = new Text2('SHIELDS: ' + lives, {
size: 45,
fill: 0x00FFFF,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('SCORE: ' + score, {
size: 45,
fill: 0x00FF00,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var bottomMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.bottom.addChild(energyText);
LK.gui.bottom.addChild(livesText);
LK.gui.bottom.addChild(scoreText);
livesText.x = 0;
livesText.y = -bottomMargin - 165 - 150;
energyText.x = -spacing;
energyText.y = -bottomMargin - 165 - 150;
scoreText.x = spacing;
scoreText.y = -bottomMargin - 165 - 150;
function updateUI() {
energyText.setText('ENERGY: ' + energy);
livesText.setText('SHIELDS: ' + lives);
scoreText.setText('SCORE: ' + score);
}
function setEnergy(value) {
energy = 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 = -250; // Grid positioned at the very top of the screen
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
// Create city sprites on exit tiles
var cityVariantCounter = 0;
for (var i = 0; i < grid.cells.length; i++) {
for (var j = 0; j < grid.cells[i].length; j++) {
var cell = grid.cells[i][j];
if (cell.type === 3) {
// Exit tiles
var city = new City(cityVariantCounter);
city.x = grid.x + i * CELL_SIZE;
city.y = grid.y + j * CELL_SIZE - 240;
debugLayer.addChild(city);
cities.push(city);
cityVariantCounter++;
}
}
}
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
// Add tutorial overlay
var tutorialOverlay = new TutorialOverlay();
tutorialOverlay.x = 2048 / 2;
tutorialOverlay.y = 2732 / 2 - 150;
game.addChild(tutorialOverlay);
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 = 50;
switch (towerType) {
case 'rapid':
cost = 150;
break;
case 'sniper':
cost = 250;
break;
case 'splash':
cost = 350;
break;
case 'slow':
cost = 450;
break;
case 'hack':
cost = 550;
break;
case 'drain':
cost = drainTowerCostReduced ? 50 : 1000;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (energy >= towerCost) {
var tower = new Tower(towerType || 'basic');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setEnergy(energy - towerCost);
grid.pathFind();
grid.renderDebug();
// Play tower-specific build sound
var soundId = 'build_' + (towerType || 'basic');
if (soundId === 'build_basic') {
soundId = 'build_default';
}
LK.getSound(soundId).play();
return true;
} else {
var notification = game.addChild(new Notification("Not enough energy!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
game.down = function (x, y, obj) {
// Check if any upgrade UI is visible
var upgradeUIVisible = singleUpgradeButton && singleUpgradeButton.visible || singleSellButton && singleSellButton.visible || singleStatsWindow && singleStatsWindow.visible;
if (upgradeUIVisible) {
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) {
// Check if tower type is unlocked before allowing drag
var towerIndex = towerTypes.indexOf(tower.towerType);
var unlockWaves = [0, 2, 3, 4, 5, 6, 7]; // Wave requirements for each tower type
var isUnlocked = towerIndex >= 0 && currentWave >= unlockWaves[towerIndex];
if (!isUnlocked) {
// Don't allow dragging locked towers
break;
}
// Close tutorial when dragging starts
if (tutorialOverlay && tutorialOverlay.isVisible) {
tutorialOverlay.closeTutorial();
}
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;
}
}
// Check if we have any upgrade UI elements visible
var hasUpgradeUI = singleUpgradeButton && singleUpgradeButton.visible || singleSellButton && singleSellButton.visible || singleStatsWindow && singleStatsWindow.visible;
var clickedOnUI = false;
// Check if clicked on single stats window
if (singleStatsWindow && singleStatsWindow.visible) {
var windowWidth = 400;
var windowHeight = 200;
var windowLeft = singleStatsWindow.x - windowWidth / 2;
var windowRight = singleStatsWindow.x + windowWidth / 2;
var windowTop = singleStatsWindow.y - windowHeight / 2;
var windowBottom = singleStatsWindow.y + windowHeight / 2;
if (x >= windowLeft && x <= windowRight && y >= windowTop && y <= windowBottom) {
clickedOnUI = true;
}
}
// Check if clicked on single upgrade button
if (singleUpgradeButton && singleUpgradeButton.visible) {
var buttonWidth = 180;
var buttonHeight = 80;
var buttonLeft = singleUpgradeButton.x - buttonWidth / 2;
var buttonRight = singleUpgradeButton.x + buttonWidth / 2;
var buttonTop = singleUpgradeButton.y - buttonHeight / 2;
var buttonBottom = singleUpgradeButton.y + buttonHeight / 2;
if (x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom) {
clickedOnUI = true;
}
}
// Check if clicked on single sell button
if (singleSellButton && singleSellButton.visible) {
var buttonWidth = 180;
var buttonHeight = 80;
var buttonLeft = singleSellButton.x - buttonWidth / 2;
var buttonRight = singleSellButton.x + buttonWidth / 2;
var buttonTop = singleSellButton.y - buttonHeight / 2;
var buttonBottom = singleSellButton.y + buttonHeight / 2;
if (x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom) {
clickedOnUI = true;
}
}
// Check if clicked on source towers
var clickedOnSourceTower = false;
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) {
clickedOnSourceTower = true;
break;
}
}
// Hide upgrade buttons when clicking on a blank spot (not on towers, UI, source towers, or while dragging)
if (hasUpgradeUI && !isDragging && !clickedOnTower && !clickedOnUI && !clickedOnSourceTower) {
hideUpgradeButtons();
selectedTower = null;
grid.renderDebug();
}
if (isDragging) {
isDragging = false;
if (towerPreview.canPlace) {
// Check if we're replacing an existing tower
if (towerPreview.replacementTower) {
// Calculate sell value for existing tower
var existingTower = towerPreview.replacementTower;
var totalInvestment = existingTower.getTotalValue ? existingTower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var newTowerCost = getTowerCost(towerPreview.towerType);
var netCost = newTowerCost - sellValue;
if (energy >= netCost) {
// Remove existing tower from arrays and layers
var towerIndex = towers.indexOf(existingTower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(existingTower);
// Clear cells occupied by old tower
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(existingTower.gridX + i, existingTower.gridY + j);
if (cell) {
cell.type = 0;
var oldTowerIndex = cell.towersInRange.indexOf(existingTower);
if (oldTowerIndex !== -1) {
cell.towersInRange.splice(oldTowerIndex, 1);
}
}
}
}
// Place new tower
var newTower = new Tower(towerPreview.towerType);
newTower.placeOnGrid(towerPreview.gridX, towerPreview.gridY);
towerLayer.addChild(newTower);
towers.push(newTower);
setEnergy(energy - netCost);
// Show notification about replacement
var notification = game.addChild(new Notification("Tower replaced! Net cost: " + netCost + " energy"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
grid.pathFind();
grid.renderDebug();
} else {
var notification = game.addChild(new Notification("Not enough energy for replacement!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else {
// Normal placement logic
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();
}
}
}
};
// Create background strip for wave display
var waveBackgroundStrip = new Container();
var waveBackground = waveBackgroundStrip.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
waveBackground.width = 2048; // Full screen width
waveBackground.height = 140; // Height to accommodate wave display
waveBackground.tint = 0x222222; // Dark background
waveBackground.alpha = 0.8; // Semi-transparent
waveBackgroundStrip.x = 2048 / 2;
waveBackgroundStrip.y = 2732 - 160;
game.addChild(waveBackgroundStrip);
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 160;
game.addChild(waveIndicator);
// Add gradient fade overlays for timeline edges
var timelineContainer = new Container();
timelineContainer.x = 2048 / 2;
timelineContainer.y = 2732 - 160;
// Left gradient fade
var leftGradient = new Container();
var leftFadeWidth = 200;
for (var i = 0; i < leftFadeWidth; i++) {
var fadeStrip = leftGradient.attachAsset('towerLevelIndicator', {
anchorX: 0,
anchorY: 0.5
});
fadeStrip.width = 1;
fadeStrip.height = 140;
fadeStrip.x = i - 2048 / 2;
fadeStrip.tint = 0x0a0a1a; // Match game background color
fadeStrip.alpha = 1 - i / leftFadeWidth; // Fade from opaque to transparent
}
timelineContainer.addChild(leftGradient);
// Right gradient fade
var rightGradient = new Container();
var rightFadeWidth = 200;
for (var i = 0; i < rightFadeWidth; i++) {
var fadeStrip = rightGradient.attachAsset('towerLevelIndicator', {
anchorX: 0,
anchorY: 0.5
});
fadeStrip.width = 1;
fadeStrip.height = 140;
fadeStrip.x = 2048 / 2 - rightFadeWidth + i;
fadeStrip.tint = 0x0a0a1a; // Match game background color
fadeStrip.alpha = i / rightFadeWidth; // Fade from transparent to opaque
}
timelineContainer.addChild(rightGradient);
game.addChild(timelineContainer);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
// Position boost button over the enemy spawn zone (top-center of grid)
nextWaveButton.x = grid.x + 12 * CELL_SIZE - 40; // Center of spawn columns (9-14), moved left 40px
nextWaveButton.y = grid.y + 2 * CELL_SIZE + 200 - 10 - 15; // Near top of spawn area, moved down 200px, moved up 10px, moved up 15px
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
var restartButton = new Container();
var restartBackground = restartButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
restartBackground.width = 300;
restartBackground.height = 100;
restartBackground.tint = 0xFF4444;
var restartText = new Text2("Restart", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
restartText.anchor.set(0.5, 0.5);
restartButton.addChild(restartText);
restartButton.x = 2048 / 2;
restartButton.y = 2732 / 2;
restartButton.visible = false;
restartButton.down = function () {
// Reset all game state
currentWave = 0;
waveTimer = 0;
waveInProgress = false;
waveSpawned = false;
energy = 800;
lives = 20;
score = 0;
drainTowerClickCount = 0;
drainTowerCostReduced = false;
// Clear all arrays
enemies = [];
towers = [];
bullets = [];
cities = [];
// Clear all layers
enemyLayerBottom.removeChildren();
enemyLayerMiddle.removeChildren();
enemyLayerTop.removeChildren();
towerLayer.removeChildren();
// Clean up existing lock overlays
for (var i = 0; i < sourceTowerLocks.length; i++) {
if (sourceTowerLocks[i].parent) {
game.removeChild(sourceTowerLocks[i]);
}
}
sourceTowerLocks = [];
// Re-add source towers
for (var i = 0; i < sourceTowers.length; i++) {
towerLayer.addChild(sourceTowers[i]);
// Recreate lock overlays for source towers
var tower = sourceTowers[i];
var lockOverlay = new Container();
var lockBackground = lockOverlay.attachAsset('lock', {
anchorX: 0.5,
anchorY: 0.5
});
lockBackground.tint = 0x000000;
lockBackground.alpha = 0.8;
lockBackground.scaleX = 1.1;
lockBackground.scaleY = 1.1;
var lockIcon = lockOverlay.attachAsset('lock_icon', {
anchorX: 0.5,
anchorY: 0.5
});
lockIcon.width = 192;
lockIcon.height = 192;
lockIcon.tint = 0xFFFFFF;
lockIcon.alpha = 1.0;
lockIcon.y = -10;
lockOverlay.x = tower.x;
lockOverlay.y = tower.y;
game.addChild(lockOverlay);
sourceTowerLocks.push(lockOverlay);
tower.lockOverlay = lockOverlay;
}
// Reset grid
for (var i = 0; i < grid.cells.length; i++) {
for (var j = 0; j < grid.cells[i].length; j++) {
var cell = grid.cells[i][j];
// Reset to original cell type
var cellType = i === 0 || i === grid.cells.length - 1 || j <= 4 || j >= grid.cells[i].length - 4 ? 1 : 0;
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
} else if (j <= 4) {
cellType = 0;
} else if (j === grid.cells[i].length - 1) {
cellType = 3;
} else if (j >= grid.cells[i].length - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.towersInRange = [];
}
}
// Reset wave indicator
waveIndicator.gameStarted = false;
waveIndicator.waveMarkers[0].children[0].tint = 0x00AA00;
waveIndicator.waveMarkers[0].children[1].setText("Start Game");
waveIndicator.waveMarkers[0].children[2].setText("Start Game");
// Reset wave marker visibility
for (var i = 1; i < waveIndicator.waveMarkers.length; i++) {
waveIndicator.waveMarkers[i].children[0].alpha = 1;
}
// Clear any upgrade menus from game layer and GUI layer
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
}).concat(LK.gui.children.filter(function (child) {
return child instanceof UpgradeMenu;
}));
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
// Clear any stats windows
var statsWindows = game.children.filter(function (child) {
return child instanceof TowerStatsWindow;
});
for (var i = 0; i < statsWindows.length; i++) {
statsWindows[i].destroy();
}
// Clear range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
pathId = 1;
maxScore = 0;
// Recreate city sprites on exit tiles
var cityVariantCounter = 0;
for (var i = 0; i < grid.cells.length; i++) {
for (var j = 0; j < grid.cells[i].length; j++) {
var cell = grid.cells[i][j];
if (cell.type === 3) {
// Exit tiles
var city = new City(cityVariantCounter);
city.x = grid.x + i * CELL_SIZE;
city.y = grid.y + j * CELL_SIZE - 240;
debugLayer.addChild(city);
cities.push(city);
cityVariantCounter++;
}
}
}
// Recalculate paths and update display
grid.pathFind();
grid.renderDebug();
updateUI();
// Unpause the game
LK.resume();
};
// Show restart button when game is paused
LK.on('pause', function () {
restartButton.visible = true;
});
// Hide restart button when game is resumed
LK.on('resume', function () {
restartButton.visible = false;
});
game.addChild(restartButton);
// Start background music
LK.playMusic('background_music');
var unlockWaves = [0, 2, 3, 4, 5, 6, 7]; // Wave requirements for each tower type
var sourceTowers = [];
var sourceTowerLocks = []; // Container for lock overlays separate from turrets
var towerSpacing = 240; // Spacing between tower types
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 2 - 180 - 180 + 50 + 60 + 40;
var _loop = function _loop() {
tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
// Create separate lock overlay that won't be affected by turret alpha
var lockOverlay = new Container();
var lockBackground = lockOverlay.attachAsset('lock', {
anchorX: 0.5,
anchorY: 0.5
});
lockBackground.tint = 0x000000;
lockBackground.alpha = 0.8;
lockBackground.scaleX = 1.1;
lockBackground.scaleY = 1.1;
var lockIcon = lockOverlay.attachAsset('lock_icon', {
anchorX: 0.5,
anchorY: 0.5
});
lockIcon.width = 192;
lockIcon.height = 192;
lockIcon.tint = 0xFFFFFF;
lockIcon.alpha = 1.0; // Ensure lock icon is fully visible
lockIcon.y = -10; // Position slightly above center for better visibility
// Position lock overlay to match tower position
lockOverlay.x = tower.x;
lockOverlay.y = tower.y;
// Add lock overlay to game (higher level than turret layer)
game.addChild(lockOverlay);
sourceTowerLocks.push(lockOverlay);
// Set reference in tower for easier management
tower.lockOverlay = lockOverlay;
// Add glowing shadow effect for each source tower
shadowGlow = tower.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
shadowGlow.width = shadowGlow.height = CELL_SIZE * 3;
shadowGlow.alpha = 0.2;
shadowGlow.blendMode = 1; // Additive blend for glow effect
// Set glow color based on tower type
switch (tower.towerType) {
case 'rapid':
shadowGlow.tint = 0x00AAFF;
break;
case 'sniper':
shadowGlow.tint = 0xFF5500;
break;
case 'splash':
shadowGlow.tint = 0x33CC00;
break;
case 'slow':
shadowGlow.tint = 0x9900FF;
break;
case 'poison':
shadowGlow.tint = 0x00FFAA;
break;
case 'drain':
shadowGlow.tint = 0xFFD700;
break;
case 'basic':
default:
shadowGlow.tint = 0xAAAAAA;
}
// Insert shadow behind the tower (at index 0)
tower.addChildAt(shadowGlow, 0);
// Add special bright glow for basic turret that shows after tutorial is hidden
if (tower.towerType === 'basic') {
// Create a bright glow below the basic turret
var basicGlow = tower.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
basicGlow.width = basicGlow.height = CELL_SIZE * 6; // Made bigger (was 4)
basicGlow.y = CELL_SIZE * 0.5; // Position below the turret
basicGlow.alpha = 0;
basicGlow.tint = 0xFFFFFF; // Bright white glow
basicGlow.blendMode = 1; // Additive blend for glow effect
// Insert glow behind the tower (at index 0, before shadowGlow)
tower.addChildAt(basicGlow, 0);
// Store reference for later control
tower.basicGlow = basicGlow;
}
// Add pulsing glow animation
function createGlowCycle(glowElement) {
function glowPulse() {
tween(glowElement, {
alpha: 0.4,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(glowElement, {
alpha: 0.2,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 2000,
easing: tween.easeInOut,
onFinish: function onFinish() {
glowPulse();
}
});
}
});
}
glowPulse();
}
// Start the glow cycle with a random delay to stagger the effects
randomDelay = Math.random() * 2000;
LK.setTimeout(function () {
createGlowCycle(shadowGlow);
}, randomDelay);
},
tower,
lockOverlay,
shadowGlow,
randomDelay;
for (var i = 0; i < towerTypes.length; i++) {
_loop();
}
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;
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use exponential scaling for health
var healthMultiplier = Math.pow(1.25, 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() * 2; // Spawn closer to top, within visible area
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;
}
}
var _loop2 = function _loop2() {
enemy = enemies[a];
if (enemy.health <= 0) {
// Play enemy death sound
LK.getSound('enemy_death').play();
for (i = 0; i < enemy.bulletsTargetingThis.length; i++) {
bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Boss enemies give more gold and score
baseEnergyEarned = enemy.isBoss ? Math.floor(500 + (enemy.waveNumber - 1) * 50) : Math.floor(10 + (enemy.waveNumber - 1) * 5); // Check for drain towers in range and apply energy bonus
drainBonusEnergy = 0;
for (t = 0; t < towers.length; t++) {
tower = towers[t];
if (tower.id === 'drain') {
dx = enemy.x - tower.x;
dy = enemy.y - tower.y;
distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= tower.getRange()) {
// 5% energy bonus per level of drain tower
drainBonusEnergy += Math.floor(baseEnergyEarned * 0.05 * tower.level);
}
}
}
totalEnergyEarned = baseEnergyEarned + drainBonusEnergy;
energyIndicator = new GoldIndicator(totalEnergyEarned, enemy.x, enemy.y);
game.addChild(energyIndicator);
setEnergy(energy + totalEnergyEarned);
// Give more score for defeating a boss
scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Add a notification for boss defeat
if (enemy.isBoss) {
notification = game.addChild(new Notification("Boss defeated! +" + totalEnergyEarned + " energy!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
return 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) {
var _explodeNextTurret = function explodeNextTurret() {
if (explosionIndex >= turretsToExplode.length) {
// All turrets exploded, wait 1.5 seconds before showing game over
LK.setTimeout(function () {
LK.showGameOver();
}, 1500);
return;
}
var turret = turretsToExplode[explosionIndex];
explosionIndex++;
// Create explosion effect at turret location
var explosion = new Container();
explosion.x = turret.x;
explosion.y = turret.y;
// Add explosion to a high layer so it's visible above everything
var debugLayerIndex = game.getChildIndex(debugLayer);
game.addChildAt(explosion, debugLayerIndex + 3);
var explosionGraphics = explosion.attachAsset('explosion_sprite', {
anchorX: 0.5,
anchorY: 0.5
});
explosionGraphics.tint = 0xFF4400; // Orange/red explosion
explosionGraphics.width = explosionGraphics.height = 150; // Consistent size for turret explosions
explosionGraphics.alpha = 1; // Start fully visible
explosion.scaleX = 0.8; // Start larger
explosion.scaleY = 0.8; // Start larger
// Hide the turret that just exploded
turret.visible = false;
// Play explosion sound
LK.getSound('explosion_sound').play();
// Animate explosion appearance
tween(explosion, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(explosion, {
alpha: 0,
scaleX: 2.5,
scaleY: 2.5
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
explosion.destroy();
}
});
}
});
// Schedule next explosion with random interval (0.05 - 0.2 seconds)
var nextDelay = 50 + Math.random() * 150; // 50-200ms in milliseconds
LK.setTimeout(_explodeNextTurret, nextDelay);
}; // Start the explosion sequence
// Hide all shield/city sprites
for (c = 0; c < cities.length; c++) {
cities[c].visible = false;
}
// Create a list of all turrets sorted by row (bottom to top)
turretsToExplode = [];
for (t = 0; t < towers.length; t++) {
turretsToExplode.push(towers[t]);
}
// Sort turrets by gridY (row) in descending order (bottom to top)
turretsToExplode.sort(function (a, b) {
if (a.gridY !== b.gridY) {
return b.gridY - a.gridY; // Higher gridY first (bottom rows)
}
return a.gridX - b.gridX; // Then by gridX for consistent ordering within same row
});
// Explode turrets one by one with random intervals
explosionIndex = 0;
_explodeNextTurret();
}
}
},
enemy,
i,
bullet,
baseEnergyEarned,
drainBonusEnergy,
t,
tower,
dx,
dy,
distance,
totalEnergyEarned,
energyIndicator,
scoreValue,
notification,
c,
turretsToExplode,
t,
explosionIndex;
for (var a = enemies.length - 1; a >= 0; a--) {
if (_loop2()) {
continue;
}
}
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();
}
// Process wiggle logic BEFORE visibility changes to prevent conflicts
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
// Use exact same logic as SourceTower.down() method for "unlocks in 1 wave" text
var towerIndex = towerTypes.indexOf(tower.towerType);
var requiredWave = unlockWaves[towerIndex];
var wavesUntilUnlock = requiredWave - currentWave;
var shouldWiggle = wavesUntilUnlock === 1;
// Add wiggle effect for basic turret until first turret is placed
var shouldWiggleBasic = tower.towerType === 'basic' && !tutorialOverlay && waveIndicator && waveIndicator.gameStarted && towers.length === 0;
// Also wiggle basic turret if tutorial window is gone
if (tower.towerType === 'basic' && towers.length === 0) {
var tutorialExists = false;
for (var j = 0; j < game.children.length; j++) {
if (game.children[j] instanceof TutorialOverlay && game.children[j].isVisible) {
tutorialExists = true;
break;
}
}
shouldWiggleBasic = shouldWiggleBasic || !tutorialExists && waveIndicator && waveIndicator.gameStarted;
}
// Add wiggle effect for the next tower to unlock - independent of visibility
if ((shouldWiggle || shouldWiggleBasic) && tower.lockOverlay) {
// Initialize wiggle timer if not exists
if (!tower.lockOverlay.wiggleTimer) {
tower.lockOverlay.wiggleTimer = 0;
}
tower.lockOverlay.wiggleTimer++;
// Wiggle every 3 seconds (180 frames at 60 FPS)
if (tower.lockOverlay.wiggleTimer >= 180) {
tower.lockOverlay.wiggleTimer = 0;
// Use IIFE to capture the specific tower instance for proper closure
(function (currentTower) {
// Stop any existing wiggle animation
tween.stop(currentTower.lockOverlay, {
rotation: true
});
// Create gentle wiggle animation
var wiggleAmount = 0.1; // Small rotation amount in radians
tween(currentTower.lockOverlay, {
rotation: wiggleAmount
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(currentTower.lockOverlay, {
rotation: -wiggleAmount
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(currentTower.lockOverlay, {
rotation: wiggleAmount * 0.5
}, {
duration: 150,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(currentTower.lockOverlay, {
rotation: 0
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
});
}
});
})(tower);
}
} else {
// Reset wiggle timer for towers not about to unlock
if (tower.lockOverlay && tower.lockOverlay.wiggleTimer) {
tower.lockOverlay.wiggleTimer = 0;
}
}
}
// Update lock overlay visibility for source towers AFTER wiggle logic
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (tower.lockOverlay && tower.isUnlocked !== undefined) {
tower.lockOverlay.visible = !tower.isUnlocked;
}
}
// Handle basic turret glow visibility
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (tower.towerType === 'basic' && tower.basicGlow) {
// Check if tutorial is hidden and no towers are placed
var tutorialExists = false;
for (var j = 0; j < game.children.length; j++) {
if (game.children[j] instanceof TutorialOverlay && game.children[j].isVisible) {
tutorialExists = true;
break;
}
}
// Show glow when tutorial is hidden and no towers are placed
var shouldShowGlow = !tutorialExists && towers.length === 0 && waveIndicator && waveIndicator.gameStarted;
if (shouldShowGlow && tower.basicGlow.alpha === 0) {
// Start the bright glow animation with higher visibility
tween(tower.basicGlow, {
alpha: 0.8,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Create pulsing glow effect with stronger contrast
function basicGlowPulse() {
tween(tower.basicGlow, {
alpha: 1.0,
scaleX: 1.6,
scaleY: 1.6
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(tower.basicGlow, {
alpha: 0.4,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (towers.length === 0) {
basicGlowPulse();
}
}
});
}
});
}
basicGlowPulse();
}
});
} else if (!shouldShowGlow && tower.basicGlow.alpha > 0) {
// Hide the glow when tutorial appears or first tower is placed
tween.stop(tower.basicGlow, {
alpha: true,
scaleX: true,
scaleY: true
});
tween(tower.basicGlow, {
alpha: 0,
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeOut
});
}
}
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
}; ===================================================================
--- original.js
+++ change.js
@@ -2410,9 +2410,9 @@
panel.height = 1200;
panel.x = 0;
panel.y = 0;
// Title text
- var titleText = new Text2('HANGAR DEFENDER', {
+ var titleText = new Text2('Hangar Panic', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
soft grass texture, repeating, tiled, top down. In-Game asset. 2d. High contrast. No shadows
steel platform, circular, sci-fi, metal, locking feet on outside, top-down. In-Game asset. 2d. High contrast. No shadows
top down image of a combat drone with a white light on top, pixel art. In-Game asset. 2d. High contrast. No shadows
top down twin barrel sci-fi rapid machine gun cannon. pixel art
pixel art top down insectoid combat drone, blue light on top. In-Game asset. 2d. High contrast. No shadows
Flying pixel art flying drone, top down, insectoid design, yellow light on top. In-Game asset. 2d. High contrast. No shadows
top down sci-fi cannon. pixel art
sci-fi pixel art UI panel. glass with a steel border.. In-Game asset. 2d. High contrast. No shadows
top down sci-fi sniper cannon, red metal, sleek, pixel art
top down sci-fi fat barrel cannon, teal metal, pixel art
top down sci-fi fat barrel cannon with ridges, purple metal, pixel art
top down pixel art of an aggressive war drone with blue wasp stripes with a blue light on top. In-Game asset. 2d. High contrast. No shadows
pixel art sci-fi bullet. In-Game asset. 2d. High contrast. No shadows
top down pixel art view of a rectangular forcefield. In-Game asset. 2d. High contrast. No shadows
acid splash puddle, pixel art, top down, top view In-Game asset. 2d. High contrast. No shadows
top down pixel art, energy crystal, yellow, spiky, charged, held on all sides by sci fi metal clamps In-Game asset. 2d. High contrast. No shadows
crackling circular energy field, pixel art, top down, filled with yellow electricity. In-Game asset. 2d. High contrast. No shadows
sci-fi metallic lock, pixel art. In-Game asset. 2d. High contrast. No shadows
digital pixel art explosion. In-Game asset. 2d. High contrast. No shadows
pixel art range circle, white border, filled with black In-Game asset. 2d. High contrast. No shadows
high tech sci fi pixel art button. greyscale. metallic. no text In-Game asset. 2d. High contrast. No shadows
build_default
Sound effect
build_rapid
Sound effect
build_sniper
Sound effect
build_splash
Sound effect
build_slow
Sound effect
build_poison
Sound effect
build_drain
Sound effect
tower_upgrade
Sound effect
wave_start
Sound effect
turret_shoot
Sound effect
enemy_death
Sound effect
drain_shoot
Sound effect
shield_hit
Sound effect
background_music
Music
explosion_sound
Sound effect
build_hack
Sound effect