/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var BaseCastle = Container.expand(function () {
var self = Container.call(this);
// Castle stats
self.maxHealth = 100;
self.health = self.maxHealth;
// Attack properties
self.canAttack = false;
self.attackDamage = 20;
self.attackRange = CELL_SIZE * 4;
self.fireRate = 120; // Attack every 2 seconds
self.lastFired = 0;
self.targetEnemy = null;
// Create castle graphics
var castleGraphics = self.attachAsset('base', {
anchorX: 0.5,
anchorY: 0.5
});
// Create health bar background
var healthBarBg = self.attachAsset('castleHealthBarBg', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarBg.y = -castleGraphics.height / 2 - 40;
// Create health bar
var healthBar = self.attachAsset('castleHealthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBar.x = -healthBarBg.width / 2;
healthBar.y = -castleGraphics.height / 2 - 40;
self.takeDamage = function (damage) {
self.health -= damage;
if (self.health <= 0) {
self.health = 0;
// Play destruction sound
LK.getSound('castle_destroy').play();
// Game over - castle destroyed
LK.showGameOver();
} else {
// Play damage sound
LK.getSound('castle_damage').play();
// Flash castle red when taking damage
LK.effects.flashObject(self, 0xff0000, 500);
}
// Update health bar visual
var healthPercent = self.health / self.maxHealth;
healthBar.width = healthBarBg.width * healthPercent;
// Change health bar color based on health level
if (healthPercent > 0.6) {
healthBar.tint = 0x00ff00; // Green
} else if (healthPercent > 0.3) {
healthBar.tint = 0xffff00; // Yellow
} else {
healthBar.tint = 0xff0000; // Red
}
};
self.findTarget = function () {
if (!self.canAttack) return null;
var closestEnemy = null;
var closestDistance = 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);
if (distance <= self.attackRange && distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
return closestEnemy;
};
self.update = function () {
if (!self.canAttack) return;
self.targetEnemy = self.findTarget();
if (self.targetEnemy && LK.ticks - self.lastFired >= self.fireRate) {
// Create bullet to attack enemy
var bullet = new Bullet(self.x, self.y, self.targetEnemy, self.attackDamage, 8);
bullet.type = 'castle';
// Make castle bullets more visible
bullet.children[0].tint = 0xFF0000;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
self.lastFired = LK.ticks;
}
};
return self;
});
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
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) {
// Check if target enemy still exists before applying damage
if (self.targetEnemy && self.targetEnemy.health !== undefined) {
// 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') {
// Check if target enemy still exists before creating splash effect
if (self.targetEnemy && self.targetEnemy.x !== undefined && self.targetEnemy.y !== undefined) {
// 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 === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 28,
fill: 0xFFFFFF,
weight: 700
});
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 = 0x880000;
return;
}
numberLabel.visible = false;
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.alpha = 0.3;
cellGraphics.tint = 0x8B4513; // Brown road color
} else {
cellGraphics.alpha = 0.3;
cellGraphics.tint = 0x8B4513; // Brown road color
}
while (debugArrows.length > data.targets.length) {
self.removeChild(debugArrows.pop());
}
// Remove arrow rendering - arrows no longer displayed
break;
}
case 1:
{
self.removeArrows();
cellGraphics.alpha = 0;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.alpha = 0;
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
};
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'slash':
effectGraphics.tint = 0xFFFFFF;
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 = .01;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
// 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;
}
// Apply difficulty scaling based on selected difficulty
if (game.selectedDifficulty) {
var difficultyMultiplier = 1.0;
switch (game.selectedDifficulty) {
case 'EASY':
difficultyMultiplier = 0.8; // Easy - 20% less health (reduced difficulty)
break;
case 'MEDIUM':
difficultyMultiplier = 1.1; // Medium - 10% more health (reduced difficulty)
break;
case 'HARD':
difficultyMultiplier = 1.6; // Hard - 60% more health (reduced difficulty)
break;
case 'EXTREME':
difficultyMultiplier = 2.3; // Extreme - 130% more health (reduced difficulty)
break;
}
self.maxHealth = Math.round(self.maxHealth * difficultyMultiplier);
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy-aberrant_titans';
if (self.type !== 'normal') {
switch (self.type) {
case 'fast':
assetId = 'enemyfast-armoredtitan';
break;
case 'flying':
assetId = 'enemyflying-annie';
break;
case 'immune':
assetId = 'enemyimmune-female_titan';
break;
case 'swarm':
assetId = 'enemyswarm-ymir_titan';
break;
default:
assetId = 'enemy-aberrant_titans';
}
}
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-aberrant_titans', {
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 poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
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 GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value.toString(), {
size: 44,
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.toString(), {
size: 44,
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;
}
}
// Set castle area as goal (2x2 area around castle position)
var castleGridX = gridWidth - 3; // 2 cells from right edge
var castleGridY = gridHeight - 8; // 7 cells up from bottom
for (var ci = 0; ci < 2; ci++) {
for (var cj = 0; cj < 2; cj++) {
var castleCell = self.cells[castleGridX + ci] && self.cells[castleGridX + ci][castleGridY + cj];
if (castleCell) {
castleCell.type = 3; // Set as goal
self.goals.push(castleCell);
}
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.left.type != 1) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
processNode(node.downLeft, targetScore, node);
}
targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
// Enemy reached castle - damage it
if (baseCastle) {
var damage = enemy.isBoss ? 20 : 5; // Boss enemies deal more damage
baseCastle.takeDamage(damage);
// Show damage indicator
var damageIndicator = new Notification("-" + damage + " Castle HP");
damageIndicator.x = baseCastle.x;
damageIndicator.y = baseCastle.y - 100;
game.addChild(damageIndicator);
}
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.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("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message.toString(), {
size: 48,
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 Player = Container.expand(function () {
var self = Container.call(this);
// Player stats
self.maxStamina = 80;
self.stamina = self.maxStamina;
self.staminaRegenRate = 0.2; // Points per frame when not attacking
self.attackCost = 25; // Stamina cost per attack
self.attackDamage = 15;
self.attackRange = CELL_SIZE * 1.0; // Attack range in pixels
self.attackCooldown = 0;
self.attackCooldownMax = 30; // Frames between attacks
self.moveSpeed = 3;
// Movement bounds (keep player within map area)
self.minX = grid.x + CELL_SIZE;
self.maxX = grid.x + grid.cells.length * CELL_SIZE - CELL_SIZE;
self.minY = grid.y + CELL_SIZE * 4;
self.maxY = grid.y + (grid.cells[0].length - 4) * CELL_SIZE - CELL_SIZE;
// Create player graphics
var playerGraphics = self.attachAsset('PlayerCh', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
// Create stamina bar background
var staminaBarBg = self.attachAsset('staminaBarBg', {
anchorX: 0.5,
anchorY: 0.5
});
staminaBarBg.y = -playerGraphics.height / 2 - 30 + 57;
// Create stamina bar
var staminaBar = self.attachAsset('staminaBar', {
anchorX: 0,
anchorY: 0.5
});
staminaBar.x = -staminaBarBg.width / 2;
staminaBar.y = -playerGraphics.height / 2 - 30 + 57;
// Movement state
self.targetX = 0;
self.targetY = 0;
self.isMoving = false;
self.update = function () {
// Handle movement
if (self.isMoving) {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > self.moveSpeed) {
// Move towards target
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.moveSpeed;
self.y += Math.sin(angle) * self.moveSpeed;
// Rotate player to face movement direction
playerGraphics.rotation = angle;
} else {
// Reached target
self.x = self.targetX;
self.y = self.targetY;
self.isMoving = false;
}
}
// Handle attack cooldown
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
// Regenerate stamina when not attacking
if (self.attackCooldown <= 0 && self.stamina < self.maxStamina) {
self.stamina = Math.min(self.maxStamina, self.stamina + self.staminaRegenRate);
}
// Update stamina bar visual
var staminaPercent = self.stamina / self.maxStamina;
staminaBar.width = staminaBarBg.width * staminaPercent;
// Change stamina bar color based on stamina level
if (staminaPercent > 0.6) {
staminaBar.tint = 0x00ff00; // Green
} else if (staminaPercent > 0.3) {
staminaBar.tint = 0xffff00; // Yellow
} else {
staminaBar.tint = 0xff0000; // Red
}
};
self.moveTo = function (x, y) {
// Clamp movement within bounds
self.targetX = Math.max(self.minX, Math.min(self.maxX, x));
self.targetY = Math.max(self.minY, Math.min(self.maxY, y));
self.isMoving = true;
};
self.attack = function () {
// Check if can attack (has stamina and not on cooldown)
if (self.stamina >= self.attackCost && self.attackCooldown <= 0) {
// Consume stamina
self.stamina -= self.attackCost;
self.attackCooldown = self.attackCooldownMax;
// Find enemies in range
var enemiesHit = [];
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.attackRange) {
enemiesHit.push(enemy);
}
}
// Switch to attack animation asset
var attackGraphics = self.attachAsset('Attackanim', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
// Hide the original player graphics
playerGraphics.visible = false;
// Attack all enemies in range
for (var i = 0; i < enemiesHit.length; i++) {
var enemy = enemiesHit[i];
enemy.health -= self.attackDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Create attack effect
var attackEffect = new EffectIndicator(enemy.x, enemy.y, 'slash');
game.addChild(attackEffect);
}
// Create slash animation around player
var slashEffect = new EffectIndicator(self.x, self.y, 'slash');
game.addChild(slashEffect);
// After attack animation duration, switch back to normal graphics
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original player graphics again
playerGraphics.visible = true;
}, 500); // 500ms attack animation duration
return true; // Attack successful
}
return false; // Attack failed (no stamina or on cooldown)
};
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;
switch (self.towerType) {
case 'rapid':
baseGraphics = self.attachAsset('eren', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
case 'sniper':
baseGraphics = self.attachAsset('sasha', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
case 'splash':
baseGraphics = self.attachAsset('mikasa', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
case 'slow':
baseGraphics = self.attachAsset('Erwin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
case 'poison':
baseGraphics = self.attachAsset('levi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
default:
baseGraphics = self.attachAsset('jean', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
}
var towerCost = getTowerCost(self.towerType);
// Get character name based on tower type
var characterName;
switch (self.towerType) {
case 'rapid':
characterName = 'Kael';
break;
case 'sniper':
characterName = 'Lyra';
break;
case 'splash':
characterName = 'Eira';
break;
case 'slow':
characterName = 'Darius';
break;
case 'poison':
characterName = 'Vale';
break;
default:
characterName = 'Raen';
}
// Create decorative background for name
var nameBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
nameBackground.width = 220;
nameBackground.height = 80;
nameBackground.y = -180; // Move higher for Mikasa and Levi
// Set background color based on tower type with more aesthetic colors
switch (self.towerType) {
case 'rapid':
nameBackground.tint = 0x1B4F72; // Darker Rich Blue
nameBackground.y = -185; // Adjust for Eren
break;
case 'sniper':
nameBackground.tint = 0x1E8449; // Darker Forest Green
nameBackground.y = -190; // Move higher for Sasha
break;
case 'splash':
nameBackground.tint = 0x922B21; // Deeper Red for Mikasa
break;
case 'slow':
nameBackground.tint = 0x6C3483; // Darker Royal Purple
break;
case 'poison':
nameBackground.tint = 0x0E6655; // Deeper Teal for Levi
nameBackground.y = -190; // Move higher for Levi
break;
default:
nameBackground.tint = 0x424949; // Darker Steel Gray
nameBackground.y = -185;
// Adjust for Jean
}
nameBackground.alpha = 0.95;
// Add shadow for tower type label
var typeLabelShadow = new Text2(characterName, {
size: 48,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 2;
typeLabelShadow.y = -182;
self.addChild(typeLabelShadow);
// Add tower type label
var typeLabel = new Text2(characterName, {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -180;
self.addChild(typeLabel);
// Create decorative background for cost
var costBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
costBackground.width = 140;
costBackground.height = 60;
costBackground.y = 160; // Move higher
costBackground.tint = 0x1C2833; // Darker, more elegant background
costBackground.alpha = 0.95;
// Add decorative gold coin squares around cost
var leftCoin = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftCoin.width = 18;
leftCoin.height = 18;
leftCoin.tint = 0xF1C40F; // Brighter gold
leftCoin.x = -55;
leftCoin.y = 160;
var rightCoin = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightCoin.width = 18;
rightCoin.height = 18;
rightCoin.tint = 0xF1C40F; // Brighter gold
rightCoin.x = 55;
rightCoin.y = 160;
// Add cost shadow
var costLabelShadow = new Text2(towerCost.toString(), {
size: 42,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 2;
costLabelShadow.y = 162;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost.toString(), {
size: 42,
fill: 0xF1C40F,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 160;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// 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 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
default:
// Default: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
// Initialize channeling properties for Erwin tower
self.isChanneling = false;
self.channelingTimer = 0;
self.hangeActive = false;
self.hangeSummoned = false;
self.hangeInstance = null;
// Initialize transformation properties for Eren tower
self.isTransforming = false;
self.transformationTimer = 0;
self.isTitan = false;
self.titanDuration = 0;
// Declare baseGraphics early to avoid undefined errors
var baseGraphics;
switch (self.id) {
case 'rapid':
baseGraphics = self.attachAsset('erentower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
case 'sniper':
baseGraphics = self.attachAsset('sashatower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
case 'splash':
baseGraphics = self.attachAsset('mikasatower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
case 'slow':
baseGraphics = self.attachAsset('erwintower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
case 'poison':
baseGraphics = self.attachAsset('levitower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
default:
baseGraphics = self.attachAsset('jeantower', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Create channel bar for Erwin tower (initially hidden)
self.channelBarBg = null;
self.channelBar = null;
if (self.id === 'slow') {
// Create channel bar background
self.channelBarBg = self.attachAsset('staminaBarBg', {
anchorX: 0.5,
anchorY: 0.5
});
self.channelBarBg.y = -baseGraphics.height / 2 - 40;
self.channelBarBg.width = 150;
self.channelBarBg.height = 16;
self.channelBarBg.visible = false;
// Create channel bar
self.channelBar = self.attachAsset('staminaBar', {
anchorX: 0,
anchorY: 0.5
});
self.channelBar.x = -self.channelBarBg.width / 2;
self.channelBar.y = -baseGraphics.height / 2 - 40;
self.channelBar.width = 0;
self.channelBar.height = 16;
self.channelBar.tint = 0x00ff00; // Green color
self.channelBar.visible = false;
}
// Create transformation meter for Eren tower (initially hidden)
self.transformBarBg = null;
self.transformBar = null;
if (self.id === 'rapid') {
// Create transformation bar background
self.transformBarBg = self.attachAsset('staminaBarBg', {
anchorX: 0.5,
anchorY: 0.5
});
self.transformBarBg.y = -baseGraphics.height / 2 - 40;
self.transformBarBg.width = 150;
self.transformBarBg.height = 16;
self.transformBarBg.visible = false;
// Create transformation bar
self.transformBar = self.attachAsset('staminaBar', {
anchorX: 0,
anchorY: 0.5
});
self.transformBar.x = -self.transformBarBg.width / 2;
self.transformBar.y = -baseGraphics.height / 2 - 40;
self.transformBar.width = 0;
self.transformBar.height = 16;
self.transformBar.tint = 0xff0000; // Red color for transformation
self.transformBar.visible = false;
}
switch (self.id) {
case 'rapid':
self.fireRate = 60;
self.damage = 18;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 75;
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 13;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
// Remove level indicators - all towers now use only character images without dots
var levelIndicators = []; // Keep empty array for existing code compatibility
// Remove gunContainer - all towers now use only character images without arrows
var gunContainer = new Container(); // Keep reference for existing code but don't add graphics
self.updateLevelIndicators = function () {
// No level indicators for any towers - all use only character images
return;
};
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 (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect
self.damage = 5 + self.level * 10; // double the effect
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
// No upgrade animation needed - all towers use only character images without level indicators
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, 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 () {
// Handle Eren tower transformation
if (self.id === 'rapid') {
if (!self.isTransforming && !self.isTitan) {
// Normal Eren tower - start transformation timer after spawning
self.transformationTimer++;
// Show transformation meter
if (self.transformBarBg && self.transformBar) {
self.transformBarBg.visible = true;
self.transformBar.visible = true;
// Calculate progress (transformation takes 600 frames = 10 seconds)
var progress = self.transformationTimer / 600;
self.transformBar.width = self.transformBarBg.width * progress;
}
// Transform to titan after 10 seconds
if (self.transformationTimer >= 600) {
self.isTitan = true;
self.titanDuration = 600; // Stay as titan for 10 seconds
self.transformationTimer = 0;
// Switch to titan asset
baseGraphics.visible = false;
var titanGraphics = self.attachAsset('erentitan', {
anchorX: 0.5,
anchorY: 0.5
});
self.titanGraphics = titanGraphics;
// Hide transformation meter when transformed
if (self.transformBarBg && self.transformBar) {
self.transformBarBg.visible = false;
self.transformBar.visible = false;
}
}
} else if (self.isTitan) {
// In titan form - countdown to transform back
self.titanDuration--;
if (self.titanDuration <= 0) {
// Transform back to normal Eren tower
self.isTitan = false;
self.transformationTimer = 0;
// Remove titan graphics and show normal graphics
if (self.titanGraphics && self.titanGraphics.parent) {
self.removeChild(self.titanGraphics);
}
baseGraphics.visible = true;
}
}
}
// Handle Erwin tower channeling and Hange summoning
if (self.id === 'slow' && self.isChanneling) {
self.channelingTimer--;
// Update channel bar progress
if (self.channelBarBg && self.channelBar) {
self.channelBarBg.visible = true;
self.channelBar.visible = true;
// Calculate progress (channeling starts at 600 and counts down to 0)
var progress = (600 - self.channelingTimer) / 600;
self.channelBar.width = self.channelBarBg.width * progress;
}
// After 5 seconds (300 frames), summon Hange
if (!self.hangeSummoned && self.channelingTimer <= 300) {
self.hangeSummoned = true;
self.hangeActive = true;
// Switch Erwin tower to attack asset at the exact moment of spawning
var attackGraphics = self.attachAsset('erwinattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Switch back to normal graphics after 1 second (60 frames)
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 1000); // 1000ms attack animation duration
// Create Hange near the tower
self.hangeInstance = new Container();
var hangeGraphics = self.hangeInstance.attachAsset('hange', {
anchorX: 0.5,
anchorY: 0.5
});
// Position Hange next to the tower
self.hangeInstance.x = self.x + 50;
self.hangeInstance.y = self.y;
game.addChild(self.hangeInstance);
// Initialize Hange properties
self.hangeInstance.attackTimer = 0;
self.hangeInstance.attackRate = 48; // Attack every 0.8 seconds
self.hangeInstance.damage = self.damage * 2; // Hange deals double damage
self.hangeInstance.range = self.getRange() * 1.5; // Hange has 1.5x range
self.hangeInstance.lifetime = 300; // Dies after 5 seconds
}
// Handle Hange's attacks if summoned
if (self.hangeSummoned && self.hangeInstance && self.hangeInstance.parent) {
self.hangeInstance.attackTimer++;
self.hangeInstance.lifetime--;
// Hange attacks enemies
if (self.hangeInstance.attackTimer >= self.hangeInstance.attackRate) {
self.hangeInstance.attackTimer = 0;
// Find closest enemy in range
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.hangeInstance.x;
var dy = enemy.y - self.hangeInstance.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.hangeInstance.range && distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Attack the closest enemy
if (closestEnemy) {
// Switch to attack asset
var attackGraphics = self.hangeInstance.attachAsset('hangeattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original hange graphics
var hangeGraphics = self.hangeInstance.children[0];
hangeGraphics.visible = false;
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent && self.hangeInstance && self.hangeInstance.parent) {
self.hangeInstance.removeChild(attackGraphics);
}
// Show original hange graphics again
if (hangeGraphics && self.hangeInstance && self.hangeInstance.parent) {
hangeGraphics.visible = true;
}
}, 500); // 500ms attack animation duration
// Send slashanim asset to enemy instead of bullet
var slashAnim = LK.getAsset('slashanim', {
anchorX: 0.5,
anchorY: 0.5
});
slashAnim.x = self.hangeInstance.x;
slashAnim.y = self.hangeInstance.y;
game.addChild(slashAnim);
// Animate slashanim to enemy using tween
tween(slashAnim, {
x: closestEnemy.x,
y: closestEnemy.y
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Apply damage when animation reaches enemy
if (self.hangeInstance && self.hangeInstance.parent) {
closestEnemy.health -= self.hangeInstance.damage;
} else {
// Use default damage if hangeInstance is no longer available
closestEnemy.health -= self.damage * 2; // Hange deals double damage
}
if (closestEnemy.health <= 0) {
closestEnemy.health = 0;
} else {
closestEnemy.healthBar.width = closestEnemy.health / closestEnemy.maxHealth * 70;
}
// Apply splash damage effects since Hange uses splash attacks
// Create visual splash effect
var splashEffect = new EffectIndicator(closestEnemy.x, closestEnemy.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 !== closestEnemy) {
var splashDx = otherEnemy.x - closestEnemy.x;
var splashDy = otherEnemy.y - closestEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage)
if (self.hangeInstance && self.hangeInstance.parent) {
otherEnemy.health -= self.hangeInstance.damage * 0.5;
} else {
// Use default damage if hangeInstance is no longer available
otherEnemy.health -= self.damage * 2 * 0.5; // Hange deals double damage
}
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
// Remove the slashanim asset after applying effects
slashAnim.destroy();
}
});
}
}
// Remove Hange after lifetime expires
if (self.hangeInstance.lifetime <= 0) {
game.removeChild(self.hangeInstance);
self.hangeInstance = null;
self.hangeActive = false;
}
}
// End channeling after 10 seconds
if (self.channelingTimer <= 0) {
self.isChanneling = false;
// Hide channel bar when channeling ends
if (self.channelBarBg && self.channelBar) {
self.channelBarBg.visible = false;
self.channelBar.visible = false;
}
}
return; // Skip normal tower behavior while channeling
}
// Hide channel bar when not channeling (for Erwin towers)
if (self.id === 'slow' && !self.isChanneling && self.channelBarBg && self.channelBar) {
self.channelBarBg.visible = false;
self.channelBar.visible = false;
}
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);
// All towers now use static character images without rotation
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
// Show upgrade menu without creating range indicators that block dragging
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
// Special handling for Eren tower - can only attack in titan form
if (self.id === 'rapid') {
if (!self.isTitan) {
return; // Cannot attack in normal form
}
}
// Special handling for Erwin tower
if (self.id === 'slow') {
// Check if already channeling or if Hange is active
if (self.isChanneling || self.hangeActive) {
return; // Cannot attack while channeling or Hange is active
}
// Start channeling process
self.isChanneling = true;
self.channelingTimer = 600; // 10 seconds at 60 FPS
self.hangeActive = false;
self.hangeSummoned = false;
// Show channel bar when starting to channel
if (self.channelBarBg && self.channelBar) {
self.channelBarBg.visible = true;
self.channelBar.visible = true;
self.channelBar.width = 0; // Start with empty bar
}
return;
}
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
// Only create bullets for Sasha tower (sniper)
if (self.id === 'sniper') {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// Customize bullet appearance for sniper
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
} else {
// For all other towers, send slashanim asset to enemy
var slashAnim = LK.getAsset('slashanim', {
anchorX: 0.5,
anchorY: 0.5
});
slashAnim.x = self.x;
slashAnim.y = self.y;
game.addChild(slashAnim);
// Animate slashanim to enemy using tween
tween(slashAnim, {
x: self.targetEnemy.x,
y: self.targetEnemy.y
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Apply damage when animation reaches enemy
// Check if target enemy still exists before accessing health property
if (self.targetEnemy && self.targetEnemy.health !== undefined) {
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 tower type
if (self.id === '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.id === '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
var slowPct = 0.5;
if (self.level !== undefined) {
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.level - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct;
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180;
} else {
self.targetEnemy.slowDuration = 180;
}
}
} else if (self.id === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2;
self.targetEnemy.poisonDuration = 300;
}
}
// Remove the slashanim asset after applying effects
slashAnim.destroy();
}
});
}
// Handle tower attack animations and add sword wave effects for all towers except Sasha
if (self.id === 'rapid' && self.isTitan) {
// Switch to attack asset
var attackGraphics = self.attachAsset('erentitanattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the titan graphics
if (self.titanGraphics) {
self.titanGraphics.visible = false;
}
// Create sword wave animation
var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash');
game.addChild(slashWave);
// Switch back to titan graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show titan graphics again
if (self.titanGraphics) {
self.titanGraphics.visible = true;
}
}, 500); // 500ms attack animation duration
}
// Handle Levi tower attack animation
if (self.id === 'poison') {
// Switch to attack asset
var attackGraphics = self.attachAsset('leviattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 500); // 500ms attack animation duration
}
// Handle Sasha tower attack animation (no sword wave for sniper)
if (self.id === 'sniper') {
// Switch to attack asset
var attackGraphics = self.attachAsset('sashaattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 500); // 500ms attack animation duration
}
// Handle Mikasa tower attack animation
if (self.id === 'splash') {
// Switch to attack asset
var attackGraphics = self.attachAsset('mikasaattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Create sword wave animation
var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash');
game.addChild(slashWave);
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 500); // 500ms attack animation duration
}
// Handle Armin tower attack animation (default type)
if (self.id === 'default') {
// Switch to attack asset
var attackGraphics = self.attachAsset('arminattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Create sword wave animation
var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash');
game.addChild(slashWave);
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 500); // 500ms attack animation duration
}
// Handle other tower types (slow, poison) - add sword wave animations
if (self.id === 'slow' || self.id === 'poison') {
// Create sword wave animation for these tower types
var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash');
game.addChild(slashWave);
}
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 1;
}
}
}
self.refreshCellsInRange();
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'default';
self.hasEnoughGold = true;
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3;
var previewGraphics = self.attachAsset('towerpreview', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
// Reset tint first
previewGraphics.tint = 0xFFFFFF;
// Apply red tint only if cannot place OR not enough gold
// Apply green tint if can place AND has enough gold
if (self.canPlace && self.hasEnoughGold) {
previewGraphics.tint = 0x00FF00; // Green for valid placement
} else {
previewGraphics.tint = 0xFF0000; // Red for invalid placement or insufficient gold
}
};
self.updatePlacementStatus = function () {
// Check if tower fits in grid bounds and doesn't block path or overlap roads
self.canPlace = self.gridX >= 0 && self.gridY >= 0 && self.gridX + 1 < grid.cells.length && self.gridY + 1 < grid.cells[0].length && !wouldBlockPath(self.gridX, self.gridY);
self.blockedByEnemy = false;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
// Get character name based on tower type
var characterName;
switch (self.tower.id) {
case 'rapid':
characterName = 'Kael';
break;
case 'sniper':
characterName = 'Lyra';
break;
case 'splash':
characterName = 'Eira';
break;
case 'slow':
characterName = 'Darius';
break;
case 'poison':
characterName = 'Vale';
break;
default:
characterName = 'Raen';
}
// Add background for tower type text for better readability
var towerTypeBackground = self.attachAsset('notification', {
anchorX: 0,
anchorY: 0.5
});
towerTypeBackground.width = 600;
towerTypeBackground.height = 100;
towerTypeBackground.x = -870;
towerTypeBackground.y = -160;
towerTypeBackground.tint = 0x2C3E50;
towerTypeBackground.alpha = 0.9;
var towerTypeText = new Text2(characterName + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0.5);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level.toString() + '/' + self.tower.maxLevel.toString() + '\nDamage: ' + self.tower.damage.toString() + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 68,
fill: 0xFFFFFF,
weight: 600
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
if (statsText && statsText.setText) {
statsText.setText('Level: ' + self.tower.level.toString() + '/' + self.tower.maxLevel.toString() + '\nDamage: ' + self.tower.damage.toString() + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
}
if (buttonText && buttonText.setText) {
buttonText.setText('Upgrade: ' + upgradeCost.toString() + ' gold');
}
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
if (sellButtonText && sellButtonText.setText) {
sellButtonText.setText('Sell: +' + sellValue.toString() + ' gold');
}
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
if (buttonText && buttonText.setText) {
buttonText.setText('Max Level');
}
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
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 && buttonText.setText) {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost.toString() + ' gold';
if (buttonText && buttonText.setText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
size: 48,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 2;
startTextShadow.y = 2;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Game", {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 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;
}
var baseCount = self.enemyCounts[waveNumber - 1];
// Apply difficulty scaling to displayed enemy count
if (game.selectedDifficulty) {
var enemyCountMultiplier = 1.0;
switch (game.selectedDifficulty) {
case 'EASY':
enemyCountMultiplier = 0.7; // Easy - 30% fewer enemies (reduced difficulty)
break;
case 'MEDIUM':
enemyCountMultiplier = 1.1; // Medium - 10% more enemies (reduced difficulty)
break;
case 'HARD':
enemyCountMultiplier = 1.4; // Hard - 40% more enemies (reduced difficulty)
break;
case 'EXTREME':
enemyCountMultiplier = 1.8; // Extreme - 80% more enemies (reduced difficulty)
break;
}
baseCount = Math.max(1, Math.round(baseCount * enemyCountMultiplier));
}
return baseCount;
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName;
// Map enemy types to their proper names based on the assets
switch (type) {
case 'normal':
typeName = "Giants";
break;
case 'fast':
typeName = "Fast Giant";
break;
case 'flying':
typeName = "Flying Giant";
break;
case 'immune':
typeName = "Immune Giant";
break;
case 'swarm':
typeName = "Swarm Giant";
break;
default:
typeName = "Giants";
}
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS " + typeName;
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
// Add background asset to the game
var backgroundAsset = LK.getAsset('background', {
anchorX: 0,
anchorY: 0
});
// Scale the background to fill the entire screen
backgroundAsset.width = 2048;
backgroundAsset.height = 2732;
backgroundAsset.x = 0;
backgroundAsset.y = 0;
backgroundAsset.alpha = 0.55; // Set 55% transparency (45% opacity)
game.addChildAt(backgroundAsset, 0); // Add as the first child so it appears behind everything
// Using defense icon as heart placeholder
var isHidingUpgradeMenu = false;
var gameStarted = false;
var mainMenuContainer = null;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
function createMainMenu() {
mainMenuContainer = new Container();
game.addChild(mainMenuContainer);
// Add black background behind main menu
var blackBackground = mainMenuContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
blackBackground.width = 2048;
blackBackground.height = 2732;
blackBackground.tint = 0x000000;
blackBackground.x = 2048 / 2;
blackBackground.y = 2732 / 2;
// Add main menu background
var menuBackground = mainMenuContainer.attachAsset('Mainmenu', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.x = 2048 / 2;
menuBackground.y = 2732 / 2;
// Create start button
var startButton = new Container();
mainMenuContainer.addChild(startButton);
// Add button background for better visual feedback
var startButtonBg = startButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startButtonBg.width = 350;
startButtonBg.height = 120;
startButtonBg.tint = 0x2C3E50;
startButtonBg.alpha = 0.8;
var startText = new Text2("START", {
size: 60,
fill: 0xFFFFFF,
weight: 800,
font: "'Copperplate Gothic'"
});
startText.anchor.set(0.5, 0.5);
startButton.addChild(startText);
// Add hover effect asset
var hoverEffect = startButton.attachAsset('menuhover', {
anchorX: 0.5,
anchorY: 0.5
});
hoverEffect.visible = false; // Initially hidden
hoverEffect.width = 370;
hoverEffect.height = 140;
startButton.x = 2048 - 150 - 189 + 76; // Move 2cm right (2cm * 37.8 pixels/cm)
startButton.y = 2732 - 150 - 567 - 189 - 189 + 76 - 76; // Move 2cm higher (2cm * 37.8 pixels/cm)
// Remove the button's move handler - we'll handle everything in the global handler
// Move handling is now done in the main game.move handler
startButton.down = function () {
startGame();
};
// Create options button
var optionsButton = new Container();
mainMenuContainer.addChild(optionsButton);
// Add button background for options
var optionsButtonBg = optionsButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
optionsButtonBg.width = 350;
optionsButtonBg.height = 120;
optionsButtonBg.tint = 0x2C3E50;
optionsButtonBg.alpha = 0.8;
var optionsText = new Text2("OPTIONS", {
size: 60,
fill: 0xFFFFFF,
weight: 800,
font: "'Copperplate Gothic'"
});
optionsText.anchor.set(0.5, 0.5);
optionsButton.addChild(optionsText);
// Add hover effect asset for options button
var optionsHoverEffect = optionsButton.attachAsset('menuhover', {
anchorX: 0.5,
anchorY: 0.5
});
optionsHoverEffect.visible = false; // Initially hidden
optionsHoverEffect.width = 370;
optionsHoverEffect.height = 140;
optionsButton.x = 2048 - 150 - 189 + 76; // Same X position as start button
optionsButton.y = 2732 - 150 - 567 - 189 - 189 + 76 - 38 + 189; // 5cm (189 pixels) lower than start button
optionsButton.down = function () {
// Options functionality can be added here later
var notification = game.addChild(new Notification("Options menu coming soon!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
};
// Create story button
var storyButton = new Container();
mainMenuContainer.addChild(storyButton);
// Add button background for story
var storyButtonBg = storyButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
storyButtonBg.width = 350;
storyButtonBg.height = 120;
storyButtonBg.tint = 0x2C3E50;
storyButtonBg.alpha = 0.8;
var storyText = new Text2("STORY", {
size: 60,
fill: 0xFFFFFF,
weight: 800,
font: "'Copperplate Gothic'"
});
storyText.anchor.set(0.5, 0.5);
storyButton.addChild(storyText);
// Add hover effect asset for story button
var storyHoverEffect = storyButton.attachAsset('menuhover', {
anchorX: 0.5,
anchorY: 0.5
});
storyHoverEffect.visible = false; // Initially hidden
storyHoverEffect.width = 370;
storyHoverEffect.height = 140;
storyButton.x = 2048 - 150 - 189 + 76; // Same X position as start and options buttons
storyButton.y = 2732 - 150 - 567 - 189 - 189 + 76 - 38 + 189 + 189 + 38; // 1cm (38 pixels) lower than previous position
storyButton.down = function () {
showStoryScreen();
};
}
function startGame() {
if (gameStarted) return;
// Show difficulty page instead of starting game directly
showDifficultyPage();
}
function showStoryScreen() {
// Hide main menu
if (mainMenuContainer) {
mainMenuContainer.visible = false;
}
// Create story screen container
var storyContainer = new Container();
game.addChild(storyContainer);
// Add black background
var blackBackground = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
blackBackground.width = 2048;
blackBackground.height = 2732;
blackBackground.tint = 0x000000;
blackBackground.x = 2048 / 2;
blackBackground.y = 2732 / 2;
// Add elegant gradient-style background layers
var outerFrame = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
outerFrame.width = 1900;
outerFrame.height = 2400;
outerFrame.tint = 0x1A252F; // Deep blue-gray frame
outerFrame.alpha = 0.95;
outerFrame.x = 2048 / 2;
outerFrame.y = 2732 / 2;
// Add story content background with refined design
var storyBackground = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
storyBackground.width = 1800;
storyBackground.height = 2300;
storyBackground.tint = 0x2C3E50; // Rich slate background
storyBackground.alpha = 0.98;
storyBackground.x = 2048 / 2;
storyBackground.y = 2732 / 2;
// Add decorative title frame
var titleFrame = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
titleFrame.width = 1600;
titleFrame.height = 140;
titleFrame.tint = 0x8B4513; // Warm bronze frame
titleFrame.alpha = 0.8;
titleFrame.x = 2048 / 2;
titleFrame.y = 380;
// Add enhanced story title with shadow
var storyTitleShadow = new Text2("⚔ THE LAST STAND ⚔", {
size: 100,
fill: 0x000000,
weight: 900
});
storyTitleShadow.anchor.set(0.5, 0.5);
storyTitleShadow.x = 2048 / 2 + 4;
storyTitleShadow.y = 384;
storyContainer.addChild(storyTitleShadow);
var storyTitle = new Text2("⚔ THE LAST STAND ⚔", {
size: 100,
fill: 0xFFD700,
weight: 900
});
storyTitle.anchor.set(0.5, 0.5);
storyTitle.x = 2048 / 2;
storyTitle.y = 380;
storyContainer.addChild(storyTitle);
// Improved story text with better formatting and readability
var storyText = "The Colossi have broken through. Humanity's final hour approaches.\n\n" + "After centuries of uneasy peace, the monstrous giants known as Colossi\n" + "have shattered the last barrier protecting mankind. The fortified city of\n" + "Aetherhold—humanity's final bastion—now faces total annihilation.\n\n" + "With the main army scattered and the city walls crumbling, only one\n" + "defense remains: YOU, the last Strategist of the Vanguard Order.\n\n" + "Command the six legendary heroes who stand between hope and despair:\n\n" + "• KAEL - Swift blade master with lightning-fast strikes\n" + "• LYRA - Precision marksman who never misses her mark\n" + "• DARIUS - Fearless commander who rallies nearby allies\n" + "• VALE - Silent assassin striking from the shadows\n" + "• EIRA - Elite guardian with unmatched combat reflexes\n" + "• RAEN - Volatile warrior wielding the giants' own power\n\n" + "Deploy your heroes strategically. Upgrade their abilities.\n" + "Adapt to each wave's unique threats and devastating power.\n\n" + "This is humanity's final gambit—not mere survival, but defiance.\n\n" + "Defend the last wall. Command the final heroes.\n" + "Turn the tide against the Colossi apocalypse.";
// Create content area with better spacing
var contentArea = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
contentArea.width = 1650;
contentArea.height = 1600;
contentArea.tint = 0x34495E; // Darker content area
contentArea.alpha = 0.7;
contentArea.x = 2048 / 2;
contentArea.y = 1400;
// Add story content with shadow for depth
var storyContentShadow = new Text2(storyText, {
size: 44,
fill: 0x000000,
weight: 600
});
storyContentShadow.anchor.set(0.5, 0.5);
storyContentShadow.x = 2048 / 2 + 3;
storyContentShadow.y = 1403;
storyContainer.addChild(storyContentShadow);
var storyContentText = new Text2(storyText, {
size: 44,
fill: 0xECF0F1,
//{t1} // Softer white for better readability
weight: 600
});
storyContentText.anchor.set(0.5, 0.5);
storyContentText.x = 2048 / 2;
storyContentText.y = 1400;
storyContainer.addChild(storyContentText);
// Enhanced back button with better styling
var backButton = new Container();
storyContainer.addChild(backButton);
// Add glow effect for back button
var backButtonGlow = backButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
backButtonGlow.width = 460;
backButtonGlow.height = 140;
backButtonGlow.tint = 0x8B0000;
backButtonGlow.alpha = 0.3;
// Add button background
var backButtonBg = backButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
backButtonBg.width = 420;
backButtonBg.height = 120;
backButtonBg.tint = 0x8B0000;
backButtonBg.alpha = 0.95;
// Add decorative corners
var leftCorner = backButton.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftCorner.width = 20;
leftCorner.height = 20;
leftCorner.tint = 0xFFD700;
leftCorner.x = -180;
leftCorner.y = 0;
var rightCorner = backButton.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightCorner.width = 20;
rightCorner.height = 20;
rightCorner.tint = 0xFFD700;
rightCorner.x = 180;
rightCorner.y = 0;
// Enhanced back text with shadow
var backTextShadow = new Text2("◄ RETURN", {
size: 58,
fill: 0x000000,
weight: 800
});
backTextShadow.anchor.set(0.5, 0.5);
backTextShadow.x = 2;
backTextShadow.y = 2;
backButton.addChild(backTextShadow);
var backText = new Text2("◄ RETURN", {
size: 58,
fill: 0xFFFFFF,
weight: 800
});
backText.anchor.set(0.5, 0.5);
backButton.addChild(backText);
backButton.x = 2048 / 2;
backButton.y = 2500;
// Add hover effect for back button
var backHoverEffect = backButton.attachAsset('menuhover', {
anchorX: 0.5,
anchorY: 0.5
});
backHoverEffect.visible = false;
backHoverEffect.width = 440;
backHoverEffect.height = 140;
// Add entrance animation for the entire story screen
storyContainer.alpha = 0;
storyContainer.scaleX = 0.9;
storyContainer.scaleY = 0.9;
tween(storyContainer, {
alpha: 1,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 400,
easing: tween.easeOut
});
// Back button functionality
backButton.down = function () {
// Add exit animation
tween(storyContainer, {
alpha: 0,
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 250,
easing: tween.easeIn,
onFinish: function onFinish() {
// Remove story screen
storyContainer.destroy();
// Show main menu again
if (mainMenuContainer) {
mainMenuContainer.visible = true;
}
}
});
};
// Enhanced hover effects for back button
backButton.move = function (x, y, obj) {
var buttonLeft = this.x - 210;
var buttonRight = this.x + 210;
var buttonTop = this.y - 60;
var buttonBottom = this.y + 60;
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
backHoverEffect.visible = mouseInButton;
if (mouseInButton) {
// Enhanced hover animations
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 150,
easing: tween.easeOut
});
tween.stop(backText, {
tint: true
});
tween(backText, {
tint: 0xFF4444 //{tq} // Brighter red on hover
}, {
duration: 200,
easing: tween.easeOut
});
backButtonGlow.alpha = 0.5;
} else {
// Reset animations
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 150,
easing: tween.easeOut
});
tween.stop(backText, {
tint: true
});
tween(backText, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
backButtonGlow.alpha = 0.3;
}
};
}
function showDifficultyPage() {
// Hide main menu
if (mainMenuContainer) {
mainMenuContainer.visible = false;
}
// Create difficulty page container
var difficultyContainer = new Container();
game.addChild(difficultyContainer);
// Add black background
var blackBackground = difficultyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
blackBackground.width = 2048;
blackBackground.height = 2732;
blackBackground.tint = 0x000000;
blackBackground.x = 2048 / 2;
blackBackground.y = 2732 / 2;
// Add difficulty page background
var difficultyBackground = difficultyContainer.attachAsset('Difficulty', {
anchorX: 0.5,
anchorY: 0.5
});
difficultyBackground.x = 2048 / 2;
difficultyBackground.y = 2732 / 2;
// Create difficulty buttons
var difficulties = [{
name: "EASY",
y: 862 // Move 40 pixels lower
}, {
name: "MEDIUM",
y: 1287 // 3cm higher (113 pixels)
}, {
name: "HARD",
y: 1789 // Move 2cm lower (76 pixels)
}, {
name: "EXTREME",
y: 2253 // Move 3cm lower (113 pixels)
}];
for (var i = 0; i < difficulties.length; i++) {
var difficulty = difficulties[i];
var difficultyButton = new Container();
difficultyContainer.addChild(difficultyButton);
// Add button background
var buttonBg = difficultyButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 600;
buttonBg.height = 120;
// Apply specific colors for difficulty buttons
if (difficulty.name === "EASY") {
buttonBg.tint = 0xD2B48C; // Tan color
} else if (difficulty.name === "MEDIUM") {
buttonBg.tint = 0x93C47D; // Pistachio green color
} else if (difficulty.name === "HARD") {
buttonBg.tint = 0x8B0000; // Dark red color
} else if (difficulty.name === "EXTREME") {
buttonBg.tint = 0x4682B4; // Darker steel blue color
} else {
buttonBg.tint = 0x2C3E50;
}
buttonBg.alpha = 0.8;
var buttonText = new Text2(difficulty.name, {
size: 52,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
difficultyButton.addChild(buttonText);
difficultyButton.x = 2048 / 2;
difficultyButton.y = difficulty.y;
// Add tween animation for all difficulty buttons
if (difficulty.name === "EASY") {
// Start slightly higher and animate to final position
difficultyButton.y = difficulty.y - 50;
tween(difficultyButton, {
y: difficulty.y
}, {
duration: 800,
easing: tween.bounceOut
});
} else if (difficulty.name === "MEDIUM") {
// Start slightly higher and animate to final position
difficultyButton.y = difficulty.y - 50;
tween(difficultyButton, {
y: difficulty.y
}, {
duration: 800,
easing: tween.bounceOut
});
} else if (difficulty.name === "HARD") {
// Start slightly higher and animate to final position
difficultyButton.y = difficulty.y - 50;
tween(difficultyButton, {
y: difficulty.y
}, {
duration: 800,
easing: tween.bounceOut
});
} else if (difficulty.name === "EXTREME") {
// Start slightly higher and animate to final position
difficultyButton.y = difficulty.y - 50;
tween(difficultyButton, {
y: difficulty.y
}, {
duration: 800,
easing: tween.bounceOut
});
}
// Store difficulty name for button handler
difficultyButton.difficultyName = difficulty.name;
// Add hover animation for each difficulty button
difficultyButton.move = function (x, y, obj) {
// Check if mouse is within button bounds
var buttonLeft = this.x - 300; // half of button width (600/2)
var buttonRight = this.x + 300;
var buttonTop = this.y - 60; // half of button height (120/2)
var buttonBottom = this.y + 60;
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
if (mouseInButton) {
// Stop any existing scale tweens
tween.stop(this, {
scaleX: true,
scaleY: true
});
// Scale up on hover
tween(this, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 200,
easing: tween.easeOut
});
} else {
// Stop any existing scale tweens
tween.stop(this, {
scaleX: true,
scaleY: true
});
// Scale back to normal
tween(this, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeOut
});
}
};
difficultyButton.down = function () {
// Start game with selected difficulty
startGameWithDifficulty(this.difficultyName);
// Remove difficulty page
difficultyContainer.destroy();
};
}
}
function startGameWithDifficulty(selectedDifficulty) {
if (gameStarted) return;
gameStarted = true;
// Stop main menu music immediately
LK.stopMusic();
// Store selected difficulty (can be used for game balancing)
game.selectedDifficulty = selectedDifficulty;
// Reset upgrades to zero and clear storage to prevent stacking
upgrades = {
damage: 0,
range: 0,
attackSpeed: 0,
stamina: 0,
castleHealth: 0,
castleAttack: 0
};
// Clear all stored upgrade values
storage.damageUpgrades = 0;
storage.rangeUpgrades = 0;
storage.attackSpeedUpgrades = 0;
storage.staminaUpgrades = 0;
storage.castleHealthUpgrades = 0;
storage.castleAttackUpgrades = 0;
// Initialize game elements that were previously created at startup
initializeGameElements();
// Start the first wave
if (waveIndicator) {
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
}
// Show difficulty selection notification
var notification = game.addChild(new Notification("Difficulty: " + selectedDifficulty + " selected!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
function showShopInterface() {
// Create shop interface container
var shopInterface = new Container();
game.addChild(shopInterface);
// Add animated black semi-transparent background with gradient effect
var shopBackground = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopBackground.width = 2048;
shopBackground.height = 2732;
shopBackground.tint = 0x000000;
shopBackground.alpha = 0.85; // Slightly more opaque for better focus
shopBackground.x = 2048 / 2;
shopBackground.y = 2732 / 2;
// Add premium shop window with enhanced design
var shopWindow = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopWindow.width = 1700; // Slightly wider for better proportions
shopWindow.height = 1300; // Slightly taller for better spacing
shopWindow.tint = 0x1A252F; // Premium dark blue-gray
shopWindow.alpha = 0.98; // Near opaque for professional look
shopWindow.x = 2048 / 2;
shopWindow.y = 2732 / 2;
// Add decorative border for the shop window
var shopBorder = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopBorder.width = 1720;
shopBorder.height = 1320;
shopBorder.tint = 0x52796F; // Elegant sage green border
shopBorder.alpha = 0.9;
shopBorder.x = 2048 / 2;
shopBorder.y = 2732 / 2;
// Add premium title background with glow effect
var titleBackground = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
titleBackground.width = 1600;
titleBackground.height = 100;
titleBackground.tint = 0x2D3436; // Professional dark gray
titleBackground.alpha = 0.95;
titleBackground.x = 2048 / 2;
titleBackground.y = 2732 / 2 - 550;
// Add enhanced shop title with shadow effect
var shopTitleShadow = new Text2("⚔ UPGRADE ARSENAL ⚔", {
size: 90,
fill: 0x000000,
weight: 900
});
shopTitleShadow.anchor.set(0.5, 0.5);
shopTitleShadow.x = 2048 / 2 + 3;
shopTitleShadow.y = 2732 / 2 - 547;
shopInterface.addChild(shopTitleShadow);
var shopTitle = new Text2("⚔ UPGRADE ARSENAL ⚔", {
size: 90,
fill: 0xFDCB6E,
// Premium gold color
weight: 900
});
shopTitle.anchor.set(0.5, 0.5);
shopTitle.x = 2048 / 2;
shopTitle.y = 2732 / 2 - 550; // Moved up slightly for better positioning
shopInterface.addChild(shopTitle);
// Add current gold display with enhanced styling
var goldDisplayBg = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
goldDisplayBg.width = 400;
goldDisplayBg.height = 70;
goldDisplayBg.tint = 0x8B6914; // Rich gold background
goldDisplayBg.alpha = 0.9;
goldDisplayBg.x = 2048 / 2;
goldDisplayBg.y = 2732 / 2 - 460;
// Add coin decorations around gold display
var leftCoin = shopInterface.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftCoin.width = 25;
leftCoin.height = 25;
leftCoin.tint = 0xFFD700; // Bright gold
leftCoin.x = 2048 / 2 - 120;
leftCoin.y = 2732 / 2 - 460;
var rightCoin = shopInterface.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightCoin.width = 25;
rightCoin.height = 25;
rightCoin.tint = 0xFFD700; // Bright gold
rightCoin.x = 2048 / 2 + 120;
rightCoin.y = 2732 / 2 - 460;
var goldDisplayShadow = new Text2("Gold: " + gold, {
size: 48,
fill: 0x000000,
weight: 800
});
goldDisplayShadow.anchor.set(0.5, 0.5);
goldDisplayShadow.x = 2048 / 2 + 2;
goldDisplayShadow.y = 2732 / 2 - 458;
shopInterface.addChild(goldDisplayShadow);
var goldDisplay = new Text2("Gold: " + gold, {
size: 48,
fill: 0xFFD700,
// Bright gold text
weight: 800
});
goldDisplay.anchor.set(0.5, 0.5);
goldDisplay.x = 2048 / 2;
goldDisplay.y = 2732 / 2 - 460;
shopInterface.addChild(goldDisplay);
// Enhanced close button with premium styling
var closeButton = new Container();
shopInterface.addChild(closeButton);
// Add glow effect for close button
var closeGlow = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeGlow.width = 140;
closeGlow.height = 140;
closeGlow.tint = 0xFF4444;
closeGlow.alpha = 0.3;
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 120;
closeBackground.height = 120;
closeBackground.tint = 0x8B0000; // Darker red for premium look
closeBackground.alpha = 0.95;
// Add close button shadow
var closeTextShadow = new Text2('✕', {
size: 70,
fill: 0x000000,
weight: 900
});
closeTextShadow.anchor.set(0.5, 0.5);
closeTextShadow.x = 2;
closeTextShadow.y = 2;
closeButton.addChild(closeTextShadow);
var closeText = new Text2('✕', {
size: 70,
fill: 0xFFFFFF,
weight: 900
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = 2048 / 2 + 790; // Adjusted for new window size
closeButton.y = 2732 / 2 - 590; // Adjusted for new window size
// Add hover effect for close button
closeButton.move = function (x, y, obj) {
var buttonLeft = this.x - 60;
var buttonRight = this.x + 60;
var buttonTop = this.y - 60;
var buttonBottom = this.y + 60;
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
if (mouseInButton) {
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 150,
easing: tween.easeOut
});
closeGlow.alpha = 0.5;
} else {
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 150,
easing: tween.easeOut
});
closeGlow.alpha = 0.3;
}
};
closeButton.down = function () {
// Add satisfying close animation
tween(shopInterface, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
shopInterface.destroy();
}
});
};
// Add entrance animation for the shop
shopInterface.alpha = 0;
shopInterface.scaleX = 0.8;
shopInterface.scaleY = 0.8;
tween(shopInterface, {
alpha: 1,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.backOut
});
// Create upgrade buttons container with better positioning
var upgradesContainer = new Container();
upgradesContainer.x = 2048 / 2;
upgradesContainer.y = 2732 / 2 - 100; // Better centering
shopInterface.addChild(upgradesContainer);
// Helper function to create premium upgrade button
function createUpgradeButton(upgradeType, yPos, iconAsset, title, description, currentLevel, maxLevel, cost, color) {
var upgradeContainer = new Container();
upgradesContainer.addChild(upgradeContainer);
upgradeContainer.y = yPos;
// Add subtle glow background
var glowBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
glowBg.width = 1520;
glowBg.height = 100;
glowBg.tint = color;
glowBg.alpha = 0.15;
// Main button background with gradient effect
var mainBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
mainBg.width = 1500;
mainBg.height = 90;
var isMaxLevel = currentLevel >= maxLevel;
var canAfford = gold >= cost;
mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? color : 0x2C1810;
mainBg.alpha = 0.95;
// Left section for icon and title
var iconBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
iconBg.width = 80;
iconBg.height = 80;
iconBg.tint = color;
iconBg.alpha = 0.8;
iconBg.x = -680;
// Add upgrade icon
var upgradeIcon = upgradeContainer.attachAsset(iconAsset, {
anchorX: 0.5,
anchorY: 0.5
});
upgradeIcon.width = 60;
upgradeIcon.height = 60;
upgradeIcon.x = -680;
upgradeIcon.tint = 0xFFFFFF;
upgradeIcon.alpha = 1.0;
// Title section
var titleShadow = new Text2(title, {
size: 52,
fill: 0x000000,
weight: 900
});
titleShadow.anchor.set(0, 0.5);
titleShadow.x = -620;
titleShadow.y = -12;
upgradeContainer.addChild(titleShadow);
var titleText = new Text2(title, {
size: 52,
fill: 0xFFFFFF,
weight: 900
});
titleText.anchor.set(0, 0.5);
titleText.x = -618;
titleText.y = -15;
upgradeContainer.addChild(titleText);
// Description section
var descShadow = new Text2(description, {
size: 36,
fill: 0x000000,
weight: 700
});
descShadow.anchor.set(0, 0.5);
descShadow.x = -620;
descShadow.y = 17;
upgradeContainer.addChild(descShadow);
var descText = new Text2(description, {
size: 36,
fill: 0xCCCCCC,
weight: 700
});
descText.anchor.set(0, 0.5);
descText.x = -618;
descText.y = 15;
upgradeContainer.addChild(descText);
// Level indicator section
var levelBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
levelBg.width = 200;
levelBg.height = 50;
levelBg.tint = 0x2D3436;
levelBg.alpha = 0.9;
levelBg.x = 150;
var levelShadow = new Text2("LV " + currentLevel + "/" + maxLevel, {
size: 40,
fill: 0x000000,
weight: 800
});
levelShadow.anchor.set(0.5, 0.5);
levelShadow.x = 152;
levelShadow.y = 2;
upgradeContainer.addChild(levelShadow);
var levelText = new Text2("LV " + currentLevel + "/" + maxLevel, {
size: 40,
fill: isMaxLevel ? 0xFFD700 : 0x74B9FF,
weight: 800
});
levelText.anchor.set(0.5, 0.5);
levelText.x = 150;
levelText.y = 0;
upgradeContainer.addChild(levelText);
// Cost section with coin decorations
var costBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
costBg.width = 250;
costBg.height = 60;
costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
costBg.alpha = 0.9;
costBg.x = 450;
// Add coin decorations
var leftCoinCost = upgradeContainer.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftCoinCost.width = 20;
leftCoinCost.height = 20;
leftCoinCost.tint = 0xFFD700;
leftCoinCost.x = 375;
var rightCoinCost = upgradeContainer.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightCoinCost.width = 20;
rightCoinCost.height = 20;
rightCoinCost.tint = 0xFFD700;
rightCoinCost.x = 525;
var costShadow = new Text2(isMaxLevel ? "MAX" : cost.toString(), {
size: 42,
fill: 0x000000,
weight: 800
});
costShadow.anchor.set(0.5, 0.5);
costShadow.x = 452;
costShadow.y = 2;
upgradeContainer.addChild(costShadow);
var costText = new Text2(isMaxLevel ? "MAX" : cost.toString(), {
size: 42,
fill: isMaxLevel ? 0xCCCCCC : 0xFFD700,
weight: 800
});
costText.anchor.set(0.5, 0.5);
costText.x = 450;
costText.y = 0;
upgradeContainer.addChild(costText);
// Store references for updates
upgradeContainer.elements = {
mainBg: mainBg,
titleText: titleText,
descText: descText,
levelText: levelText,
levelShadow: levelShadow,
costText: costText,
costShadow: costShadow,
costBg: costBg,
leftCoinCost: leftCoinCost,
rightCoinCost: rightCoinCost
};
// Add hover effects
upgradeContainer.move = function (x, y, obj) {
if (isMaxLevel) return;
var buttonLeft = this.x - 750;
var buttonRight = this.x + 750;
var buttonTop = this.y - 45;
var buttonBottom = this.y + 45;
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
if (mouseInButton && canAfford) {
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.02,
scaleY: 1.02
}, {
duration: 150,
easing: tween.easeOut
});
glowBg.alpha = 0.25;
} else {
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 150,
easing: tween.easeOut
});
glowBg.alpha = 0.15;
}
};
return upgradeContainer;
}
// Create damage upgrade with enhanced styling
var damageUpgradeContainer = createUpgradeButton('damage', -250, 'attackst', 'DAMAGE BOOST', 'Increase hero attack power', upgrades.damage, upgradeMaxLevels.damage, upgradeCosts.damage, 0xE74C3C);
// Create all remaining upgrade buttons
var rangeUpgradeContainer = createUpgradeButton('range', -150, 'rangest', 'RANGE EXTENSION', 'Extend hero attack reach', upgrades.range, upgradeMaxLevels.range, upgradeCosts.range, 0x3498DB);
var attackSpeedUpgradeContainer = createUpgradeButton('attackSpeed', -50, 'speedst', 'COMBAT SPEED', 'Increase attack frequency', upgrades.attackSpeed, upgradeMaxLevels.attackSpeed, upgradeCosts.attackSpeed, 0x2ECC71);
var staminaUpgradeContainer = createUpgradeButton('stamina', 50, 'staminast', 'ENDURANCE BOOST', 'Increase maximum stamina', upgrades.stamina, upgradeMaxLevels.stamina, upgradeCosts.stamina, 0xF39C12);
var castleHealthUpgradeContainer = createUpgradeButton('castleHealth', 150, 'castlearmor', 'FORTRESS ARMOR', 'Strengthen castle defenses', upgrades.castleHealth, upgradeMaxLevels.castleHealth, upgradeCosts.castleHealth, 0x9B59B6);
var castleAttackUpgradeContainer = createUpgradeButton('castleAttack', 250, 'siegegun', 'SIEGE WEAPONS', 'Activate castle cannons', upgrades.castleAttack, upgradeMaxLevels.castleAttack, upgradeCosts.castleAttack, 0x34495E);
// Update function for all upgrade buttons
function updateAllUpgradeButtons() {
// Update gold display
goldDisplay.setText("Gold: " + gold);
goldDisplayShadow.setText("Gold: " + gold);
// Update damage upgrade
var isMaxLevel = upgrades.damage >= upgradeMaxLevels.damage;
var canAfford = gold >= upgradeCosts.damage;
damageUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0xE74C3C : 0x2C1810;
damageUpgradeContainer.elements.levelText.setText("LV " + upgrades.damage + "/" + upgradeMaxLevels.damage);
damageUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.damage + "/" + upgradeMaxLevels.damage);
damageUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
damageUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.damage.toString());
damageUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.damage.toString());
damageUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update range upgrade
isMaxLevel = upgrades.range >= upgradeMaxLevels.range;
canAfford = gold >= upgradeCosts.range;
rangeUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0x3498DB : 0x2C1810;
rangeUpgradeContainer.elements.levelText.setText("LV " + upgrades.range + "/" + upgradeMaxLevels.range);
rangeUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.range + "/" + upgradeMaxLevels.range);
rangeUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
rangeUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.range.toString());
rangeUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.range.toString());
rangeUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update attack speed upgrade
isMaxLevel = upgrades.attackSpeed >= upgradeMaxLevels.attackSpeed;
canAfford = gold >= upgradeCosts.attackSpeed;
attackSpeedUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0x2ECC71 : 0x2C1810;
attackSpeedUpgradeContainer.elements.levelText.setText("LV " + upgrades.attackSpeed + "/" + upgradeMaxLevels.attackSpeed);
attackSpeedUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.attackSpeed + "/" + upgradeMaxLevels.attackSpeed);
attackSpeedUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
attackSpeedUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.attackSpeed.toString());
attackSpeedUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.attackSpeed.toString());
attackSpeedUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update stamina upgrade
isMaxLevel = upgrades.stamina >= upgradeMaxLevels.stamina;
canAfford = gold >= upgradeCosts.stamina;
staminaUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0xF39C12 : 0x2C1810;
staminaUpgradeContainer.elements.levelText.setText("LV " + upgrades.stamina + "/" + upgradeMaxLevels.stamina);
staminaUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.stamina + "/" + upgradeMaxLevels.stamina);
staminaUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
staminaUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.stamina.toString());
staminaUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.stamina.toString());
staminaUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update castle health upgrade
isMaxLevel = upgrades.castleHealth >= upgradeMaxLevels.castleHealth;
canAfford = gold >= upgradeCosts.castleHealth;
castleHealthUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0x9B59B6 : 0x2C1810;
castleHealthUpgradeContainer.elements.levelText.setText("LV " + upgrades.castleHealth + "/" + upgradeMaxLevels.castleHealth);
castleHealthUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.castleHealth + "/" + upgradeMaxLevels.castleHealth);
castleHealthUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
castleHealthUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.castleHealth.toString());
castleHealthUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.castleHealth.toString());
castleHealthUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update castle attack upgrade
isMaxLevel = upgrades.castleAttack >= upgradeMaxLevels.castleAttack;
canAfford = gold >= upgradeCosts.castleAttack;
castleAttackUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0x34495E : 0x2C1810;
castleAttackUpgradeContainer.elements.levelText.setText("LV " + upgrades.castleAttack + "/" + upgradeMaxLevels.castleAttack);
castleAttackUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.castleAttack + "/" + upgradeMaxLevels.castleAttack);
castleAttackUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
castleAttackUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.castleAttack.toString());
castleAttackUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.castleAttack.toString());
castleAttackUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
}
// Call update function initially
updateAllUpgradeButtons();
// Unified upgrade handler function
function handleUpgrade(upgradeType, upgradeContainer) {
var currentLevel = upgrades[upgradeType];
var maxLevel = upgradeMaxLevels[upgradeType];
var cost = upgradeCosts[upgradeType];
if (currentLevel >= maxLevel) {
var notification = game.addChild(new Notification(upgradeType.charAt(0).toUpperCase() + upgradeType.slice(1) + " upgrade maxed out!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (gold >= cost) {
// Add satisfying purchase animation
tween(upgradeContainer, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(upgradeContainer, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
}
});
setGold(gold - cost);
upgrades[upgradeType]++;
storage[upgradeType + 'Upgrades'] = upgrades[upgradeType];
// Apply upgrades based on type
switch (upgradeType) {
case 'damage':
if (player) player.attackDamage += 5;
var notification = game.addChild(new Notification("⚔ DAMAGE BOOST! +" + upgrades.damage * 5 + " total damage"));
break;
case 'range':
if (player) player.attackRange += 0.3 * CELL_SIZE;
var notification = game.addChild(new Notification("🎯 RANGE EXTENDED! +" + (upgrades.range * 0.3).toFixed(1) + " range"));
break;
case 'attackSpeed':
if (player) player.attackCooldownMax = Math.max(5, player.attackCooldownMax - 9);
var notification = game.addChild(new Notification("⚡ SPEED BOOST! Faster attacks"));
break;
case 'stamina':
if (player) {
player.maxStamina += 20;
player.stamina = Math.min(player.stamina + 20, player.maxStamina);
}
var notification = game.addChild(new Notification("💪 ENDURANCE UP! +" + upgrades.stamina * 20 + " max stamina"));
break;
case 'castleHealth':
if (baseCastle) {
baseCastle.maxHealth += 30;
baseCastle.health = Math.min(baseCastle.health + 30, baseCastle.maxHealth);
}
var notification = game.addChild(new Notification("🏰 FORTRESS STRENGTHENED! +" + upgrades.castleHealth * 30 + " max health"));
break;
case 'castleAttack':
if (baseCastle) {
baseCastle.canAttack = true;
baseCastle.attackDamage = 20;
baseCastle.attackRange = CELL_SIZE * 4;
baseCastle.fireRate = 120;
}
var notification = game.addChild(new Notification("⚔ SIEGE WEAPONS ACTIVATED!"));
break;
}
notification.x = 2048 / 2;
notification.y = grid.height - 50;
updateAllUpgradeButtons();
} else {
var notification = game.addChild(new Notification("💰 Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
// Assign handlers to all upgrade containers
damageUpgradeContainer.down = function () {
handleUpgrade('damage', this);
};
rangeUpgradeContainer.down = function () {
handleUpgrade('range', this);
};
attackSpeedUpgradeContainer.down = function () {
handleUpgrade('attackSpeed', this);
};
staminaUpgradeContainer.down = function () {
handleUpgrade('stamina', this);
};
castleHealthUpgradeContainer.down = function () {
handleUpgrade('castleHealth', this);
};
castleAttackUpgradeContainer.down = function () {
handleUpgrade('castleAttack', this);
};
// Range upgrade - using proper elements structure
// (This duplicate range upgrade container is removed as it's already created by createUpgradeButton above)
// Old individual upgrade containers removed - now using unified premium upgrade system
// Close shop when clicking on background
shopBackground.down = function () {
shopInterface.destroy();
};
}
// Create kill count UI display
var killCountContainer = new Container();
LK.gui.topRight.addChild(killCountContainer);
// Position container below hero stats area
killCountContainer.x = -40; // Same X position as hero stats
killCountContainer.y = 320; // Position below hero stats area
// Create main background for kill count UI
var killCountBackground = killCountContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
killCountBackground.width = 300; // Compact width for kill count
killCountBackground.height = 120; // Compact height
killCountBackground.tint = 0x8B0000; // Dark red background for combat theme
killCountBackground.alpha = 0.95;
// Create decorative border
var killCountBorder = killCountContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
killCountBorder.width = 310;
killCountBorder.height = 130;
killCountBorder.tint = 0x2C3E50; // Dark border
killCountBorder.alpha = 0.9;
killCountBorder.x = 5;
killCountBorder.y = -5;
// Create title background
var killCountTitleBg = killCountContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
killCountTitleBg.width = 280;
killCountTitleBg.height = 40;
killCountTitleBg.tint = 0x2D3436; // Dark gray for title
killCountTitleBg.alpha = 0.98;
killCountTitleBg.x = -10;
killCountTitleBg.y = 10;
// Add title with shadow effect
var killCountTitleShadow = new Text2('💀 KILLS 💀', {
size: 28,
fill: 0x000000,
weight: 900
});
killCountTitleShadow.anchor.set(1.0, 0.5);
killCountTitleShadow.x = -25;
killCountTitleShadow.y = 32;
killCountContainer.addChild(killCountTitleShadow);
var killCountTitle = new Text2('💀 KILLS 💀', {
size: 28,
fill: 0xFF6B6B,
// Red color for kills title
weight: 900
});
killCountTitle.anchor.set(1.0, 0.5);
killCountTitle.x = -22;
killCountTitle.y = 30;
killCountContainer.addChild(killCountTitle);
// Add kill count value with shadow
var killCountValueShadow = new Text2('0', {
size: 48,
fill: 0x000000,
weight: 900
});
killCountValueShadow.anchor.set(1.0, 0.5);
killCountValueShadow.x = -22;
killCountValueShadow.y = 82;
killCountContainer.addChild(killCountValueShadow);
var killCountValue = new Text2('0', {
size: 48,
fill: 0xFFD700,
// Gold color for the count
weight: 900
});
killCountValue.anchor.set(1.0, 0.5);
killCountValue.x = -20;
killCountValue.y = 80;
killCountContainer.addChild(killCountValue);
// Hide kill count UI initially
killCountContainer.visible = false;
// Create hero stats display with enhanced aesthetic design and better positioning
var heroStatsContainer = new Container();
LK.gui.topRight.addChild(heroStatsContainer);
// Position container away from road and with proper spacing
heroStatsContainer.x = -40; // Further from right edge to avoid road overlap
heroStatsContainer.y = -6; // Moved 0.7cm lower (-33 + 27 = -6) for better positioning
// Create main background with better design to avoid road overlap
var statsBackground = heroStatsContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
statsBackground.width = 380; // Slightly narrower to avoid road
statsBackground.height = 300; // Optimal height
statsBackground.tint = 0x1A252F; // Better contrasting dark background
statsBackground.alpha = 0.95;
// Create decorative border with better visibility
var borderBackground = heroStatsContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
borderBackground.width = 390;
borderBackground.height = 310;
borderBackground.tint = 0x52796F; // Darker sage green border
borderBackground.alpha = 0.9;
borderBackground.x = 5;
borderBackground.y = -5;
// Create title background with better contrast
var titleBackground = heroStatsContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
titleBackground.width = 360; // Better proportions
titleBackground.height = 55; // Optimal title bar height
titleBackground.tint = 0x2D3436; // Better contrast dark gray
titleBackground.alpha = 0.98;
titleBackground.x = -10;
titleBackground.y = 10;
// Add title with enhanced shadow effect
var titleShadow = new Text2('⚔ HERO STATUS ⚔', {
// Enhanced title with symbols
size: 34,
fill: 0x000000,
weight: 900
});
titleShadow.anchor.set(1.0, 0.5);
titleShadow.x = -25;
titleShadow.y = 39; // Centered in title bar
heroStatsContainer.addChild(titleShadow);
var titleText = new Text2('⚔ HERO STATUS ⚔', {
size: 34,
fill: 0xFDCB6E,
// Better golden color for title
weight: 900
});
titleText.anchor.set(1.0, 0.5);
titleText.x = -22;
titleText.y = 37; // Centered in title bar
heroStatsContainer.addChild(titleText);
// Create enhanced stat rows with better spacing and design
var statRows = [];
var statIcons = ['attackst', 'rangest', 'speedst', 'staminast'];
var statYPositions = [85, 135, 185, 235]; // Better compact spacing
var statLabels = ['DAMAGE', 'RANGE', 'SPEED', 'STAMINA']; // Full descriptive names
for (var i = 0; i < 4; i++) {
var statRow = new Container();
heroStatsContainer.addChild(statRow);
statRow.y = statYPositions[i];
statRows.push(statRow);
// Add subtle glow background for each stat row
var statGlow = statRow.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0.5
});
statGlow.width = 355;
statGlow.height = 42;
statGlow.tint = 0x636E72; // Better neutral glow
statGlow.alpha = 0.25;
statGlow.x = -12;
statGlow.y = 0;
// Add main stat background with better design
var statBg = statRow.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0.5
});
statBg.width = 350;
statBg.height = 40;
statBg.tint = 0x2D3436; // Consistent with title
statBg.alpha = 0.9;
statBg.x = -15;
statBg.y = 0;
// Add icon background circle for better contrast
var iconBg = statRow.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
iconBg.width = 44;
iconBg.height = 44;
iconBg.tint = 0x74B9FF; // Vibrant blue background for icons
iconBg.alpha = 0.85;
iconBg.x = -45; // Better positioning for icon background
iconBg.y = 0;
// Add stat icon with enhanced positioning and proper coloring
var statIcon = statRow.attachAsset(statIcons[i], {
anchorX: 0.5,
anchorY: 0.5
});
statIcon.width = 36; // Proper icon size
statIcon.height = 36;
statIcon.x = -45; // Centered on icon background
statIcon.y = 0;
statIcon.tint = 0xFFFFFF; // Make icons white/visible instead of transparent
statIcon.alpha = 1.0; // Full opacity
// Add stat label with enhanced shadow
var statLabelShadow = new Text2(statLabels[i], {
size: 28,
fill: 0x000000,
weight: 800
});
statLabelShadow.anchor.set(1.0, 0.5);
statLabelShadow.x = -250;
statLabelShadow.y = 2;
statRow.addChild(statLabelShadow);
// Add stat label with better styling and colors
var statLabel = new Text2(statLabels[i], {
size: 28,
fill: 0xFFFFFF,
// White text for better visibility
weight: 800
});
statLabel.anchor.set(1.0, 0.5);
statLabel.x = -248;
statLabel.y = 0;
statRow.addChild(statLabel);
// Add stat value shadow with offset
var statValueShadow = new Text2('', {
size: 32,
fill: 0x000000,
weight: 900
});
statValueShadow.anchor.set(1.0, 0.5);
statValueShadow.x = -82;
statValueShadow.y = 2;
statRow.addChild(statValueShadow);
// Add stat value with color coding
var statValue = new Text2('', {
size: 32,
fill: 0xCCCCCC,
// Light gray for stat values
weight: 900
});
statValue.anchor.set(1.0, 0.5);
statValue.x = -80;
statValue.y = 0;
statRow.addChild(statValue);
}
// Update hero stats display with enhanced formatting and effects
function updateHeroStats() {
if (player && statRows.length >= 4) {
// Calculate stats with proper bounds checking and safe conversion
var damage = Math.max(0, Math.round(player.attackDamage || 0));
var range = Math.max(0, (player.attackRange || 0) / CELL_SIZE).toFixed(1);
var attacksPerSecond = Math.max(0, 60 / Math.max(1, player.attackCooldownMax || 1)).toFixed(1);
var stamina = Math.max(0, Math.round(player.stamina || 0));
var maxStamina = Math.max(1, Math.round(player.maxStamina || 1));
var staminaPercent = stamina / maxStamina;
// Enhanced stat values with consistent formatting and safe string conversion
var statValues = [damage.toString(), range.toString() + ' tiles', attacksPerSecond.toString() + '/sec', stamina.toString() + '/' + maxStamina.toString()];
// Use bright, consistent colors for all stats with better visibility
var statColors = [0xFFD700,
// Damage - bright gold
0x00BFFF,
// Range - bright blue
0x32CD32,
// Speed - bright green
0xFF6347 // Stamina - bright orange-red
];
var statLabels = ['DAMAGE', 'RANGE', 'SPEED', 'STAMINA'];
for (var i = 0; i < 4; i++) {
if (statRows[i] && statRows[i].children && statRows[i].children.length >= 8) {
// Ensure all text objects exist before updating
var labelShadow = statRows[i].children[4];
var label = statRows[i].children[5];
var valueShadow = statRows[i].children[6];
var value = statRows[i].children[7];
// Update label shadow (child index 4 in new layout) with consistent formatting
if (labelShadow && labelShadow.setText && statLabels[i]) {
labelShadow.setText(statLabels[i]);
labelShadow.tint = 0x000000;
labelShadow.alpha = 1.0;
}
// Update label (child index 5) with bright white text
if (label && label.setText && statLabels[i]) {
label.setText(statLabels[i]);
label.tint = 0xFFFFFF;
label.alpha = 1.0;
}
// Update value shadow (child index 6) with consistent black shadow
if (valueShadow && valueShadow.setText && statValues[i]) {
valueShadow.setText(statValues[i]);
valueShadow.tint = 0x000000;
valueShadow.alpha = 1.0;
}
// Update value with bright color coding (child index 7)
if (value && value.setText && statValues[i]) {
value.setText(statValues[i]);
value.tint = statColors[i];
value.alpha = 1.0;
}
// Add special visual effects for low stamina
if (i === 3 && staminaPercent < 0.3) {
// Flash stamina when critically low
if (LK.ticks % 60 < 30) {
// Flash every second
if (value) value.alpha = 0.6;
if (valueShadow) valueShadow.alpha = 0.6;
} else {
if (value) value.alpha = 1.0;
if (valueShadow) valueShadow.alpha = 1.0;
}
} else {
if (value) value.alpha = 1.0;
if (valueShadow) valueShadow.alpha = 1.0;
}
}
}
}
}
// Hide stats initially
heroStatsContainer.visible = false;
// Create base health UI display with heart icon
var baseHealthContainer = new Container();
LK.gui.topLeft.addChild(baseHealthContainer);
// Position container away from the top-left platform menu icon (avoid 100x100 px area)
baseHealthContainer.x = 120; // 120px from left edge to avoid platform menu
baseHealthContainer.y = 20; // 20px from top edge
// Create main background for base health UI
var baseHealthBackground = baseHealthContainer.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
baseHealthBackground.width = 280; // Compact width
baseHealthBackground.height = 80; // Compact height
baseHealthBackground.tint = 0x2C3E50; // Dark blue-gray background
baseHealthBackground.alpha = 0.9;
// Create decorative border
var baseHealthBorder = baseHealthContainer.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
baseHealthBorder.width = 290;
baseHealthBorder.height = 90;
baseHealthBorder.tint = 0x8B0000; // Dark red border for castle theme
baseHealthBorder.alpha = 0.8;
baseHealthBorder.x = -5;
baseHealthBorder.y = -5;
// Add health UI icon
var heartIcon = baseHealthContainer.attachAsset('healthui', {
anchorX: 0.5,
anchorY: 0.5
});
heartIcon.width = 50; // Appropriate size for compact UI
heartIcon.height = 50;
heartIcon.tint = 0xFF0000; // Red heart color
heartIcon.x = 35; // Position near left side
heartIcon.y = 40; // Center vertically
// Add castle health text with shadow
var baseHealthTextShadow = new Text2('', {
size: 32,
fill: 0x000000,
weight: 800
});
baseHealthTextShadow.anchor.set(0, 0.5);
baseHealthTextShadow.x = 72; // Position after heart icon + margin
baseHealthTextShadow.y = 42; // Slightly offset for shadow effect
baseHealthContainer.addChild(baseHealthTextShadow);
// Add main castle health text
var baseHealthText = new Text2('', {
size: 32,
fill: 0xFFFFFF,
weight: 800
});
baseHealthText.anchor.set(0, 0.5);
baseHealthText.x = 70; // Position after heart icon + margin
baseHealthText.y = 40; // Center vertically
baseHealthContainer.addChild(baseHealthText);
// Function to update kill count display
function updateKillCount() {
if (killCountValue && killCountValueShadow) {
var killText = killCount.toString();
killCountValue.setText(killText);
killCountValueShadow.setText(killText);
// Change color based on kill count milestones
if (killCount >= 100) {
killCountValue.tint = 0xFF1744; // Bright red for high kills
} else if (killCount >= 50) {
killCountValue.tint = 0xFF5722; // Orange-red for medium kills
} else if (killCount >= 25) {
killCountValue.tint = 0xFFD700; // Gold for decent kills
} else {
killCountValue.tint = 0xFFFFFF; // White for low kills
}
}
}
// Function to update base health display
function updateBaseHealth() {
if (baseCastle && baseHealthText && baseHealthTextShadow) {
var healthText = baseCastle.health + '/' + baseCastle.maxHealth;
baseHealthText.setText(healthText);
baseHealthTextShadow.setText(healthText);
// Change heart color based on health percentage
var healthPercent = baseCastle.health / baseCastle.maxHealth;
if (healthPercent > 0.6) {
heartIcon.tint = 0xFF0000; // Red for healthy
} else if (healthPercent > 0.3) {
heartIcon.tint = 0xFF6600; // Orange for damaged
} else {
heartIcon.tint = 0x990000; // Dark red for critical
}
// Change text color based on health
if (healthPercent > 0.6) {
baseHealthText.tint = 0x00FF00; // Green for healthy
} else if (healthPercent > 0.3) {
baseHealthText.tint = 0xFFFF00; // Yellow for damaged
} else {
baseHealthText.tint = 0xFF0000; // Red for critical
}
}
}
// Hide base health UI initially
baseHealthContainer.visible = false;
function initializeGameElements() {
// Show previously hidden UI elements
// waveIndicator.visible = true; // Removed - keep wave indicator hidden
nextWaveButton.visible = true;
shopButton.visible = true; // Show shop button when game starts
towerLayer.visible = true;
enemyLayer.visible = true;
goldImage.visible = true;
goldText.visible = true;
heroStatsContainer.visible = true; // Show hero stats when game starts
baseHealthContainer.visible = true; // Show base health when game starts
killCountContainer.visible = true; // Show kill count when game starts
// Reset kill count for new game
killCount = 0;
updateKillCount();
// Update gold text to show current gold amount
updateUI();
// Show source towers
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].visible = true;
}
// Show player character
if (player) {
player.visible = true;
// Reset player stats to base values only (no upgrades applied)
player.attackDamage = 15; // Base damage
player.attackRange = CELL_SIZE * 1.0; // Base range
player.attackCooldownMax = 30; // Base cooldown
// No upgrades applied since they are all reset to 0
}
// Show spawn point
if (spawnPoint) {
spawnPoint.visible = true;
}
// Show base castle
if (baseCastle) {
baseCastle.visible = true;
}
// Initialize hero stats display
updateHeroStats();
// Any other game initialization that should happen when starting
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
// Initialize upgrade system with storage - reset everything to 0
var upgrades = {
damage: 0,
range: 0,
attackSpeed: 0,
stamina: 0,
castleHealth: 0,
castleAttack: 0
};
// Clear all stored upgrade values
storage.damageUpgrades = 0;
storage.rangeUpgrades = 0;
storage.attackSpeedUpgrades = 0;
storage.staminaUpgrades = 0;
storage.castleHealthUpgrades = 0;
storage.castleAttackUpgrades = 0;
// Function to reset game state for new games
function resetGameState() {
// Reset upgrades to zero and clear storage
upgrades = {
damage: 0,
range: 0,
attackSpeed: 0,
stamina: 0,
castleHealth: 0,
castleAttack: 0
};
// Clear all stored upgrade values
storage.damageUpgrades = 0;
storage.rangeUpgrades = 0;
storage.attackSpeedUpgrades = 0;
storage.staminaUpgrades = 0;
storage.castleHealthUpgrades = 0;
storage.castleAttackUpgrades = 0;
// Reset other game variables
gameStarted = false;
currentWave = 0;
killCount = 0; // Reset kill count
// Hide base health UI when resetting
if (baseHealthContainer) {
baseHealthContainer.visible = false;
}
// Hide kill count UI when resetting
if (killCountContainer) {
killCountContainer.visible = false;
}
waveTimer = 0;
waveInProgress = false;
waveSpawned = false;
enemies = [];
towers = [];
bullets = [];
gold = 60;
selectedTower = null;
}
// Upgrade costs and limits
var upgradeCosts = {
damage: 40,
range: 50,
attackSpeed: 40,
stamina: 50,
castleHealth: 35,
castleAttack: 70
};
var upgradeMaxLevels = {
damage: 3,
range: 3,
attackSpeed: 3,
stamina: 3,
castleHealth: 3,
castleAttack: 1
};
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
var gold = 60;
var lives = 20;
var score = 0;
var killCount = 0; // Track total kills
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 3000;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var goldImage = LK.getAsset('gold', {
anchorX: 0.5,
anchorY: 0.5
});
goldImage.visible = false; // Hide gold image initially
LK.gui.left.addChild(goldImage);
goldImage.x = 38; // Move 1 cm right (1cm * 37.8 pixels/cm)
goldImage.y = -378; // Move gold UI 10cm upper (0 - 378 pixels)
var goldText = new Text2('0', {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0, 0.5);
goldText.visible = false; // Hide gold text initially
LK.gui.left.addChild(goldText);
goldText.x = goldImage.x + goldImage.width / 2 + 10; // Position next to gold image with small gap
goldText.y = -378; // Match gold image y position (10cm upper)
function updateUI() {
goldText.setText(gold.toString());
}
function setGold(value) {
gold = value;
updateUI();
// Initialize base health display
updateBaseHealth();
}
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
// Create road path from spawn to base
function createRoadPath() {
// Clear existing path cells (set them to grass/floor)
for (var i = 0; i < grid.cells.length; i++) {
for (var j = 0; j < grid.cells[0].length; j++) {
var cell = grid.cells[i][j];
if (cell.type === 0) {
// Convert floor cells to walls first
cell.type = 1;
}
}
}
// Define the maze-like road path coordinates with longer turns
var roadPath = [
// Start from spawn point (column 11) - extended downward
{
x: 11,
y: 0
}, {
x: 11,
y: 1
}, {
x: 11,
y: 2
}, {
x: 11,
y: 3
}, {
x: 11,
y: 4
}, {
x: 11,
y: 5
}, {
x: 11,
y: 6
}, {
x: 11,
y: 7
}, {
x: 11,
y: 8
},
// First turn right - extended rightward
{
x: 12,
y: 8
}, {
x: 13,
y: 8
}, {
x: 14,
y: 8
}, {
x: 15,
y: 8
}, {
x: 16,
y: 8
}, {
x: 17,
y: 8
},
// Turn down - extended downward
{
x: 17,
y: 9
}, {
x: 17,
y: 10
}, {
x: 17,
y: 11
}, {
x: 17,
y: 12
}, {
x: 17,
y: 13
}, {
x: 17,
y: 14
},
// Turn left - extended leftward
{
x: 16,
y: 14
}, {
x: 15,
y: 14
}, {
x: 14,
y: 14
}, {
x: 13,
y: 14
}, {
x: 12,
y: 14
}, {
x: 11,
y: 14
}, {
x: 10,
y: 14
}, {
x: 9,
y: 14
}, {
x: 8,
y: 14
}, {
x: 7,
y: 14
},
// Turn down - extended downward
{
x: 7,
y: 15
}, {
x: 7,
y: 16
}, {
x: 7,
y: 17
}, {
x: 7,
y: 18
}, {
x: 7,
y: 19
},
// Turn right - extended rightward
{
x: 8,
y: 19
}, {
x: 9,
y: 19
}, {
x: 10,
y: 19
}, {
x: 11,
y: 19
}, {
x: 12,
y: 19
}, {
x: 13,
y: 19
}, {
x: 14,
y: 19
}, {
x: 15,
y: 19
}, {
x: 16,
y: 19
}, {
x: 17,
y: 19
}, {
x: 18,
y: 19
},
// Turn down - extended downward
{
x: 18,
y: 20
}, {
x: 18,
y: 21
}, {
x: 18,
y: 22
}, {
x: 18,
y: 23
}, {
x: 18,
y: 24
},
// Turn left - extended leftward
{
x: 17,
y: 24
}, {
x: 16,
y: 24
}, {
x: 15,
y: 24
}, {
x: 14,
y: 24
}, {
x: 13,
y: 24
}, {
x: 12,
y: 24
},
// Turn down - extended downward
{
x: 12,
y: 25
}, {
x: 12,
y: 26
}, {
x: 12,
y: 27
},
// Turn right towards base - extended rightward
{
x: 13,
y: 27
}, {
x: 14,
y: 27
}, {
x: 15,
y: 27
}, {
x: 16,
y: 27
}, {
x: 17,
y: 27
}, {
x: 18,
y: 27
}, {
x: 19,
y: 27
}, {
x: 20,
y: 27
}, {
x: 21,
y: 27
}];
// Set road tiles along the path
for (var i = 0; i < roadPath.length; i++) {
var pathPoint = roadPath[i];
var cell = grid.getCell(pathPoint.x, pathPoint.y);
if (cell) {
cell.type = 0; // Set as passable road
}
}
// Ensure spawn and goal areas remain correct
for (var i = 11 - 3; i <= 11 + 3; i++) {
var spawnCell = grid.getCell(i, 0);
if (spawnCell) {
spawnCell.type = 2; // Spawn
}
}
// Set castle area as goal (2x2 area around castle position)
var castleGridX = grid.cells.length - 3; // 2 cells from right edge
var castleGridY = grid.cells[0].length - 8; // 7 cells up from bottom
for (var ci = 0; ci < 2; ci++) {
for (var cj = 0; cj < 2; cj++) {
var castleCell = grid.getCell(castleGridX + ci, castleGridY + cj);
if (castleCell) {
castleCell.type = 3; // Set as goal
}
}
}
}
// Create the road path
createRoadPath();
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
debugLayer.visible = true; // Show the debug layer so road tiles are visible
game.addChild(debugLayer);
towerLayer.visible = false; // Hide tower layer initially
enemyLayer.visible = false; // Hide enemy layer initially
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
// First check if any cell in the 2x2 tower footprint is a road tile (type 0)
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) {
// Cannot build on road tiles
return true;
}
}
}
// Check if any existing tower overlaps with the proposed tower position
for (var t = 0; t < towers.length; t++) {
var existingTower = towers[t];
var existingGridX = existingTower.gridX;
var existingGridY = existingTower.gridY;
// Check if the 2x2 footprints overlap
var overlapX = !(gridX + 2 <= existingGridX || gridX >= existingGridX + 2);
var overlapY = !(gridY + 2 <= existingGridY || gridY >= existingGridY + 2);
if (overlapX && overlapY) {
// Towers would overlap
return true;
}
}
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 = 10;
switch (towerType) {
case 'rapid':
cost = 20;
break;
case 'sniper':
cost = 30;
break;
case 'splash':
cost = 40;
break;
case 'slow':
cost = 50;
break;
case 'poison':
cost = 60;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
grid.pathFind();
grid.renderDebug();
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
var draggedTower = null;
var dragStartTime = 0;
game.down = function (x, y, obj) {
if (!gameStarted) {
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Record drag start time for distinguishing between drag and click
dragStartTime = LK.ticks;
// Handle contextual player actions based on distance
if (player && player.visible) {
// Check if clicking in a valid game area (not on UI elements)
var inGameArea = x >= grid.x && x <= grid.x + grid.cells.length * CELL_SIZE && y >= grid.y + CELL_SIZE * 4 && y <= grid.y + (grid.cells[0].length - 4) * CELL_SIZE;
if (inGameArea) {
// Calculate distance from player to tap location
var dx = x - player.x;
var dy = y - player.y;
var distanceToTap = Math.sqrt(dx * dx + dy * dy);
// If tapping close to hero (within attack range): attack
if (distanceToTap <= player.attackRange) {
// Trigger attack
if (player.attack()) {
// Attack successful - show feedback
var notification = game.addChild(new Notification("Slash attack!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
// Attack failed - not enough stamina
var notification = game.addChild(new Notification("Not enough stamina!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else {
// If tapping far from hero: move to that location
player.moveTo(x, y);
}
}
}
// Check if clicking on existing towers (no dragging allowed - towers are permanent)
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) {
// Store the clicked tower for upgrade menu only (no dragging)
draggedTower = tower;
return; // Exit early to prevent other interactions
}
}
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 player can afford this tower type first
if (gold >= getTowerCost(tower.towerType)) {
// Initialize dragging state
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.visible = true;
// Position the preview at the initial drag position and update its status
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
towerPreview.updatePlacementStatus();
} else {
// Show notification if can't afford
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
break;
}
}
};
game.move = function (x, y, obj) {
if (!gameStarted) {
// Handle main menu hover effects when game hasn't started
if (mainMenuContainer && mainMenuContainer.visible) {
// Get button references from the main menu container
var startButton = mainMenuContainer.children[2]; // third child after backgrounds
var optionsButton = mainMenuContainer.children[3]; // fourth child
var storyButton = mainMenuContainer.children[4]; // fifth child
var startText = startButton.children[1]; // text is second child after background
var optionsText = optionsButton.children[1]; // text is second child after background
var storyText = storyButton.children[1]; // text is second child after background
var hoverEffect = startButton.children[2]; // hover effect is third child
var optionsHoverEffect = optionsButton.children[2]; // hover effect is third child
var storyHoverEffect = storyButton.children[2]; // hover effect is third child
// Calculate start button bounds more precisely - use button background size
var buttonLeft = startButton.x - 175; // half of button background width (350/2)
var buttonRight = startButton.x + 175;
var buttonTop = startButton.y - 60; // half of button background height (120/2)
var buttonBottom = startButton.y + 60;
// Check if mouse is within start button bounds
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
// Only show hover effect when mouse is precisely over the button
hoverEffect.visible = mouseInButton;
// Animate text color change with tween
if (mouseInButton) {
// Stop any existing tween on the text
tween.stop(startText, {
tint: true
});
// Tween to red color
tween(startText, {
tint: 0xFF0000
}, {
duration: 200,
easing: tween.easeOut
});
} else {
// Stop any existing tween on the text
tween.stop(startText, {
tint: true
});
// Tween back to white color
tween(startText, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
// Calculate options button bounds more precisely
var optionsButtonLeft = optionsButton.x - 175;
var optionsButtonRight = optionsButton.x + 175;
var optionsButtonTop = optionsButton.y - 60;
var optionsButtonBottom = optionsButton.y + 60;
// Check if mouse is within options button bounds
var mouseInOptionsButton = x >= optionsButtonLeft && x <= optionsButtonRight && y >= optionsButtonTop && y <= optionsButtonBottom;
// Only show hover effect when mouse is precisely over the options button
optionsHoverEffect.visible = mouseInOptionsButton;
// Calculate story button bounds more precisely
var storyButtonLeft = storyButton.x - 175;
var storyButtonRight = storyButton.x + 175;
var storyButtonTop = storyButton.y - 60;
var storyButtonBottom = storyButton.y + 60;
// Check if mouse is within story button bounds
var mouseInStoryButton = x >= storyButtonLeft && x <= storyButtonRight && y >= storyButtonTop && y <= storyButtonBottom;
// Only show hover effect when mouse is precisely over the story button
storyHoverEffect.visible = mouseInStoryButton;
// Animate options text color change with tween
if (mouseInOptionsButton) {
// Stop any existing tween on the options text
tween.stop(optionsText, {
tint: true
});
// Tween to green color
tween(optionsText, {
tint: 0x00FF00
}, {
duration: 200,
easing: tween.easeOut
});
} else {
// Stop any existing tween on the options text
tween.stop(optionsText, {
tint: true
});
// Tween back to white color
tween(optionsText, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
// Animate story text color change with tween
if (mouseInStoryButton) {
// Stop any existing tween on the story text
tween.stop(storyText, {
tint: true
});
// Tween to blue color
tween(storyText, {
tint: 0x4444FF
}, {
duration: 200,
easing: tween.easeOut
});
} else {
// Stop any existing tween on the story text
tween.stop(storyText, {
tint: true
});
// Tween back to white color
tween(storyText, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
} else {
// Hide hover effect when not in main menu - but check if elements exist first
if (mainMenuContainer && mainMenuContainer.children.length > 2) {
var startButton = mainMenuContainer.children[2];
var optionsButton = mainMenuContainer.children[3];
var storyButton = mainMenuContainer.children[4];
if (startButton && startButton.children.length > 2) {
startButton.children[2].visible = false; // hoverEffect
}
if (optionsButton && optionsButton.children.length > 2) {
optionsButton.children[2].visible = false; // optionsHoverEffect
}
if (storyButton && storyButton.children.length > 2) {
storyButton.children[2].visible = false; // storyHoverEffect
}
}
}
return;
}
// Handle tower dragging when game is started
if (isDragging && towerPreview.visible) {
// Shift the y position upward by 1.5 tiles to show preview above finger
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
// Force update the preview appearance after position change
towerPreview.updatePlacementStatus();
}
};
game.up = function (x, y, obj) {
if (!gameStarted) {
return;
}
var dragDuration = LK.ticks - dragStartTime;
var wasQuickClick = dragDuration < 10; // Less than ~1/6 second is considered a click
var clickedOnTower = false;
var clickedTower = null;
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;
clickedTower = tower;
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
// Only allow placing new towers from source towers (no tower repositioning)
if (!draggedTower && towerPreview.canPlace && towerPreview.hasEnoughGold) {
// This is placing a new tower from source towers
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else if (!towerPreview.hasEnoughGold) {
var notification = game.addChild(new Notification("Not enough gold!"));
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;
// For existing towers, show upgrade menu on quick click
if (draggedTower && wasQuickClick) {
clickedTower = draggedTower;
clickedOnTower = true;
}
draggedTower = null;
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
// Handle tower click for upgrade menu (only if it was a quick click)
if (clickedOnTower && clickedTower && wasQuickClick) {
// Trigger the tower's down method to show upgrade menu
clickedTower.down(x, y, obj);
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.visible = false; // Hide the wave indicator UI - permanently hidden
game.addChild(waveIndicator);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.visible = false; // Hide the next wave button
nextWaveButton.x = 189; // Move 5cm right (5cm * 37.8 pixels/cm)
nextWaveButton.y = 378; // Move 10cm down (10cm * 37.8 pixels/cm)
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
// Create shop button 3cm below next wave button
var shopContainer = new Container();
var shopButton = new Container();
var shopGraphics = shopButton.attachAsset('shop', {
anchorX: 0.5,
anchorY: 0.5
});
shopButton.x = 189; // Same X position as next wave button
shopButton.y = 378 + 113 + 113; // 6cm total below next wave button (6cm * 37.8 pixels/cm = 226 pixels)
shopButton.visible = false; // Hide initially until game starts
shopButton.down = function () {
if (!gameStarted) {
return;
}
// Show shop interface
showShopInterface();
};
shopContainer.addChild(shopButton);
game.addChild(shopContainer);
// Start playing main menu music immediately
LK.playMusic('MainMenuSt');
// Create main menu when game loads
createMainMenu();
var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
tower.visible = false; // Hide source towers initially
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
// Create spawn point image at the enemy spawn location
var spawnPoint = LK.getAsset('spawnpoint', {
anchorX: 0.5,
anchorY: 0.5
});
spawnPoint.x = grid.x + 11 * CELL_SIZE + CELL_SIZE / 2 - 38; // Center of spawn area (middle of 6 tiles), moved 1cm left (38 pixels)
spawnPoint.y = grid.y + 264 - 76; // Top of the grid where enemies spawn, moved 7cm (264 pixels) lower, then 2cm up (76 pixels)
spawnPoint.visible = false; // Hide initially until game starts
game.addChild(spawnPoint);
// Create base castle - positioned 7cm (264 pixels) up from bottom right
var baseCastle = new BaseCastle();
baseCastle.x = grid.x + (grid.cells.length - 2) * CELL_SIZE; // 2 cells from right edge
baseCastle.y = grid.y + (grid.cells[0].length - 7) * CELL_SIZE; // 7 cells up from bottom
baseCastle.visible = false; // Hide initially until game starts
game.addChild(baseCastle);
// Create player character
var player = new Player();
player.x = grid.x + grid.cells.length * CELL_SIZE / 2; // Center X of the map
player.y = grid.y + grid.cells[0].length * CELL_SIZE / 2; // Center Y of the map
player.visible = false; // Hide initially until game starts
game.addChild(player);
game.update = function () {
if (!gameStarted) {
return;
}
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);
// Apply difficulty scaling to enemy count
if (game.selectedDifficulty) {
var enemyCountMultiplier = 1.0;
switch (game.selectedDifficulty) {
case 'EASY':
enemyCountMultiplier = 0.7; // Easy - 30% fewer enemies (reduced difficulty)
break;
case 'MEDIUM':
enemyCountMultiplier = 1.1; // Medium - 10% more enemies (reduced difficulty)
break;
case 'HARD':
enemyCountMultiplier = 1.4; // Hard - 40% more enemies (reduced difficulty)
break;
case 'EXTREME':
enemyCountMultiplier = 1.8; // Extreme - 80% more enemies (reduced difficulty)
break;
}
enemyCount = Math.max(1, Math.round(enemyCount * enemyCountMultiplier));
}
// 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;
}
// Initialize enemy spawn tracking if not already set
if (!game.enemySpawnQueue) {
game.enemySpawnQueue = [];
game.enemySpawnTimer = 0;
game.enemySpawnDelay = 120; // 2 second delay (120 frames = 2 seconds at 60 FPS)
}
// Create spawn queue for this wave
for (var i = 0; i < enemyCount; i++) {
game.enemySpawnQueue.push({
waveType: waveType,
waveNumber: currentWave
});
}
}
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;
}
}
// Handle enemy spawning from queue one by one
if (game.enemySpawnQueue && game.enemySpawnQueue.length > 0) {
game.enemySpawnTimer++;
if (game.enemySpawnTimer >= game.enemySpawnDelay) {
game.enemySpawnTimer = 0;
// Spawn one enemy from the queue
var spawnData = game.enemySpawnQueue.shift();
var enemy = new Enemy(spawnData.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
var healthMultiplier = Math.pow(1.12, spawnData.waveNumber); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Position enemy exactly on top of spawnpoint image
var spawnX = 11; // Center column where spawnpoint is located
var spawnY = 0; // Exactly at the spawnpoint position
enemy.cellX = spawnX;
enemy.cellY = spawnY;
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
// Set enemy position to match spawnpoint exactly, then move 3cm lower and 1cm right
enemy.x = spawnPoint.x + 38; // Move 1cm right (1cm * 37.8 pixels/cm)
enemy.y = spawnPoint.y + 113; // Move 3cm lower (3cm * 37.8 pixels/cm)
enemy.waveNumber = spawnData.waveNumber;
enemies.push(enemy);
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Increment kill count
killCount++;
updateKillCount();
// Boss enemies give more gold and score
var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5);
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Update hero stats display
if (gameStarted && LK.ticks % 30 === 0) {
// Update every 0.5 seconds
updateHeroStats();
// Update base health display
updateBaseHealth();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var BaseCastle = Container.expand(function () {
var self = Container.call(this);
// Castle stats
self.maxHealth = 100;
self.health = self.maxHealth;
// Attack properties
self.canAttack = false;
self.attackDamage = 20;
self.attackRange = CELL_SIZE * 4;
self.fireRate = 120; // Attack every 2 seconds
self.lastFired = 0;
self.targetEnemy = null;
// Create castle graphics
var castleGraphics = self.attachAsset('base', {
anchorX: 0.5,
anchorY: 0.5
});
// Create health bar background
var healthBarBg = self.attachAsset('castleHealthBarBg', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarBg.y = -castleGraphics.height / 2 - 40;
// Create health bar
var healthBar = self.attachAsset('castleHealthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBar.x = -healthBarBg.width / 2;
healthBar.y = -castleGraphics.height / 2 - 40;
self.takeDamage = function (damage) {
self.health -= damage;
if (self.health <= 0) {
self.health = 0;
// Play destruction sound
LK.getSound('castle_destroy').play();
// Game over - castle destroyed
LK.showGameOver();
} else {
// Play damage sound
LK.getSound('castle_damage').play();
// Flash castle red when taking damage
LK.effects.flashObject(self, 0xff0000, 500);
}
// Update health bar visual
var healthPercent = self.health / self.maxHealth;
healthBar.width = healthBarBg.width * healthPercent;
// Change health bar color based on health level
if (healthPercent > 0.6) {
healthBar.tint = 0x00ff00; // Green
} else if (healthPercent > 0.3) {
healthBar.tint = 0xffff00; // Yellow
} else {
healthBar.tint = 0xff0000; // Red
}
};
self.findTarget = function () {
if (!self.canAttack) return null;
var closestEnemy = null;
var closestDistance = 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);
if (distance <= self.attackRange && distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
return closestEnemy;
};
self.update = function () {
if (!self.canAttack) return;
self.targetEnemy = self.findTarget();
if (self.targetEnemy && LK.ticks - self.lastFired >= self.fireRate) {
// Create bullet to attack enemy
var bullet = new Bullet(self.x, self.y, self.targetEnemy, self.attackDamage, 8);
bullet.type = 'castle';
// Make castle bullets more visible
bullet.children[0].tint = 0xFF0000;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
self.lastFired = LK.ticks;
}
};
return self;
});
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
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) {
// Check if target enemy still exists before applying damage
if (self.targetEnemy && self.targetEnemy.health !== undefined) {
// 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') {
// Check if target enemy still exists before creating splash effect
if (self.targetEnemy && self.targetEnemy.x !== undefined && self.targetEnemy.y !== undefined) {
// 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 === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.tint = Math.random() * 0xffffff;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 28,
fill: 0xFFFFFF,
weight: 700
});
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 = 0x880000;
return;
}
numberLabel.visible = false;
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.alpha = 0.3;
cellGraphics.tint = 0x8B4513; // Brown road color
} else {
cellGraphics.alpha = 0.3;
cellGraphics.tint = 0x8B4513; // Brown road color
}
while (debugArrows.length > data.targets.length) {
self.removeChild(debugArrows.pop());
}
// Remove arrow rendering - arrows no longer displayed
break;
}
case 1:
{
self.removeArrows();
cellGraphics.alpha = 0;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
cellGraphics.alpha = 0;
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 1000) / 10);
};
});
// This update method was incorrectly placed here and should be removed
var EffectIndicator = Container.expand(function (x, y, type) {
var self = Container.call(this);
self.x = x;
self.y = y;
var effectGraphics = self.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
effectGraphics.blendMode = 1;
switch (type) {
case 'splash':
effectGraphics.tint = 0x33CC00;
effectGraphics.width = effectGraphics.height = CELL_SIZE * 1.5;
break;
case 'slow':
effectGraphics.tint = 0x9900FF;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'poison':
effectGraphics.tint = 0x00FFAA;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'sniper':
effectGraphics.tint = 0xFF5500;
effectGraphics.width = effectGraphics.height = CELL_SIZE;
break;
case 'slash':
effectGraphics.tint = 0xFFFFFF;
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 = .01;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
// 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;
}
// Apply difficulty scaling based on selected difficulty
if (game.selectedDifficulty) {
var difficultyMultiplier = 1.0;
switch (game.selectedDifficulty) {
case 'EASY':
difficultyMultiplier = 0.8; // Easy - 20% less health (reduced difficulty)
break;
case 'MEDIUM':
difficultyMultiplier = 1.1; // Medium - 10% more health (reduced difficulty)
break;
case 'HARD':
difficultyMultiplier = 1.6; // Hard - 60% more health (reduced difficulty)
break;
case 'EXTREME':
difficultyMultiplier = 2.3; // Extreme - 130% more health (reduced difficulty)
break;
}
self.maxHealth = Math.round(self.maxHealth * difficultyMultiplier);
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy-aberrant_titans';
if (self.type !== 'normal') {
switch (self.type) {
case 'fast':
assetId = 'enemyfast-armoredtitan';
break;
case 'flying':
assetId = 'enemyflying-annie';
break;
case 'immune':
assetId = 'enemyimmune-female_titan';
break;
case 'swarm':
assetId = 'enemyswarm-ymir_titan';
break;
default:
assetId = 'enemy-aberrant_titans';
}
}
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-aberrant_titans', {
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 poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
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 GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value.toString(), {
size: 44,
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.toString(), {
size: 44,
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;
}
}
// Set castle area as goal (2x2 area around castle position)
var castleGridX = gridWidth - 3; // 2 cells from right edge
var castleGridY = gridHeight - 8; // 7 cells up from bottom
for (var ci = 0; ci < 2; ci++) {
for (var cj = 0; cj < 2; cj++) {
var castleCell = self.cells[castleGridX + ci] && self.cells[castleGridX + ci][castleGridY + cj];
if (castleCell) {
castleCell.type = 3; // Set as goal
self.goals.push(castleCell);
}
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.left.type != 1) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
processNode(node.downLeft, targetScore, node);
}
targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
// Enemy reached castle - damage it
if (baseCastle) {
var damage = enemy.isBoss ? 20 : 5; // Boss enemies deal more damage
baseCastle.takeDamage(damage);
// Show damage indicator
var damageIndicator = new Notification("-" + damage + " Castle HP");
damageIndicator.x = baseCastle.x;
damageIndicator.y = baseCastle.y - 100;
game.addChild(damageIndicator);
}
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.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("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message.toString(), {
size: 48,
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 Player = Container.expand(function () {
var self = Container.call(this);
// Player stats
self.maxStamina = 80;
self.stamina = self.maxStamina;
self.staminaRegenRate = 0.2; // Points per frame when not attacking
self.attackCost = 25; // Stamina cost per attack
self.attackDamage = 15;
self.attackRange = CELL_SIZE * 1.0; // Attack range in pixels
self.attackCooldown = 0;
self.attackCooldownMax = 30; // Frames between attacks
self.moveSpeed = 3;
// Movement bounds (keep player within map area)
self.minX = grid.x + CELL_SIZE;
self.maxX = grid.x + grid.cells.length * CELL_SIZE - CELL_SIZE;
self.minY = grid.y + CELL_SIZE * 4;
self.maxY = grid.y + (grid.cells[0].length - 4) * CELL_SIZE - CELL_SIZE;
// Create player graphics
var playerGraphics = self.attachAsset('PlayerCh', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
// Create stamina bar background
var staminaBarBg = self.attachAsset('staminaBarBg', {
anchorX: 0.5,
anchorY: 0.5
});
staminaBarBg.y = -playerGraphics.height / 2 - 30 + 57;
// Create stamina bar
var staminaBar = self.attachAsset('staminaBar', {
anchorX: 0,
anchorY: 0.5
});
staminaBar.x = -staminaBarBg.width / 2;
staminaBar.y = -playerGraphics.height / 2 - 30 + 57;
// Movement state
self.targetX = 0;
self.targetY = 0;
self.isMoving = false;
self.update = function () {
// Handle movement
if (self.isMoving) {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > self.moveSpeed) {
// Move towards target
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.moveSpeed;
self.y += Math.sin(angle) * self.moveSpeed;
// Rotate player to face movement direction
playerGraphics.rotation = angle;
} else {
// Reached target
self.x = self.targetX;
self.y = self.targetY;
self.isMoving = false;
}
}
// Handle attack cooldown
if (self.attackCooldown > 0) {
self.attackCooldown--;
}
// Regenerate stamina when not attacking
if (self.attackCooldown <= 0 && self.stamina < self.maxStamina) {
self.stamina = Math.min(self.maxStamina, self.stamina + self.staminaRegenRate);
}
// Update stamina bar visual
var staminaPercent = self.stamina / self.maxStamina;
staminaBar.width = staminaBarBg.width * staminaPercent;
// Change stamina bar color based on stamina level
if (staminaPercent > 0.6) {
staminaBar.tint = 0x00ff00; // Green
} else if (staminaPercent > 0.3) {
staminaBar.tint = 0xffff00; // Yellow
} else {
staminaBar.tint = 0xff0000; // Red
}
};
self.moveTo = function (x, y) {
// Clamp movement within bounds
self.targetX = Math.max(self.minX, Math.min(self.maxX, x));
self.targetY = Math.max(self.minY, Math.min(self.maxY, y));
self.isMoving = true;
};
self.attack = function () {
// Check if can attack (has stamina and not on cooldown)
if (self.stamina >= self.attackCost && self.attackCooldown <= 0) {
// Consume stamina
self.stamina -= self.attackCost;
self.attackCooldown = self.attackCooldownMax;
// Find enemies in range
var enemiesHit = [];
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.attackRange) {
enemiesHit.push(enemy);
}
}
// Switch to attack animation asset
var attackGraphics = self.attachAsset('Attackanim', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
// Hide the original player graphics
playerGraphics.visible = false;
// Attack all enemies in range
for (var i = 0; i < enemiesHit.length; i++) {
var enemy = enemiesHit[i];
enemy.health -= self.attackDamage;
if (enemy.health <= 0) {
enemy.health = 0;
} else {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
// Create attack effect
var attackEffect = new EffectIndicator(enemy.x, enemy.y, 'slash');
game.addChild(attackEffect);
}
// Create slash animation around player
var slashEffect = new EffectIndicator(self.x, self.y, 'slash');
game.addChild(slashEffect);
// After attack animation duration, switch back to normal graphics
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original player graphics again
playerGraphics.visible = true;
}, 500); // 500ms attack animation duration
return true; // Attack successful
}
return false; // Attack failed (no stamina or on cooldown)
};
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;
switch (self.towerType) {
case 'rapid':
baseGraphics = self.attachAsset('eren', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
case 'sniper':
baseGraphics = self.attachAsset('sasha', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
case 'splash':
baseGraphics = self.attachAsset('mikasa', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
case 'slow':
baseGraphics = self.attachAsset('Erwin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
case 'poison':
baseGraphics = self.attachAsset('levi', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
break;
default:
baseGraphics = self.attachAsset('jean', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
}
var towerCost = getTowerCost(self.towerType);
// Get character name based on tower type
var characterName;
switch (self.towerType) {
case 'rapid':
characterName = 'Kael';
break;
case 'sniper':
characterName = 'Lyra';
break;
case 'splash':
characterName = 'Eira';
break;
case 'slow':
characterName = 'Darius';
break;
case 'poison':
characterName = 'Vale';
break;
default:
characterName = 'Raen';
}
// Create decorative background for name
var nameBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
nameBackground.width = 220;
nameBackground.height = 80;
nameBackground.y = -180; // Move higher for Mikasa and Levi
// Set background color based on tower type with more aesthetic colors
switch (self.towerType) {
case 'rapid':
nameBackground.tint = 0x1B4F72; // Darker Rich Blue
nameBackground.y = -185; // Adjust for Eren
break;
case 'sniper':
nameBackground.tint = 0x1E8449; // Darker Forest Green
nameBackground.y = -190; // Move higher for Sasha
break;
case 'splash':
nameBackground.tint = 0x922B21; // Deeper Red for Mikasa
break;
case 'slow':
nameBackground.tint = 0x6C3483; // Darker Royal Purple
break;
case 'poison':
nameBackground.tint = 0x0E6655; // Deeper Teal for Levi
nameBackground.y = -190; // Move higher for Levi
break;
default:
nameBackground.tint = 0x424949; // Darker Steel Gray
nameBackground.y = -185;
// Adjust for Jean
}
nameBackground.alpha = 0.95;
// Add shadow for tower type label
var typeLabelShadow = new Text2(characterName, {
size: 48,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 2;
typeLabelShadow.y = -182;
self.addChild(typeLabelShadow);
// Add tower type label
var typeLabel = new Text2(characterName, {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -180;
self.addChild(typeLabel);
// Create decorative background for cost
var costBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
costBackground.width = 140;
costBackground.height = 60;
costBackground.y = 160; // Move higher
costBackground.tint = 0x1C2833; // Darker, more elegant background
costBackground.alpha = 0.95;
// Add decorative gold coin squares around cost
var leftCoin = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftCoin.width = 18;
leftCoin.height = 18;
leftCoin.tint = 0xF1C40F; // Brighter gold
leftCoin.x = -55;
leftCoin.y = 160;
var rightCoin = self.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightCoin.width = 18;
rightCoin.height = 18;
rightCoin.tint = 0xF1C40F; // Brighter gold
rightCoin.x = 55;
rightCoin.y = 160;
// Add cost shadow
var costLabelShadow = new Text2(towerCost.toString(), {
size: 42,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 2;
costLabelShadow.y = 162;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost.toString(), {
size: 42,
fill: 0xF1C40F,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 160;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
// 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 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
default:
// Default: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
// Initialize channeling properties for Erwin tower
self.isChanneling = false;
self.channelingTimer = 0;
self.hangeActive = false;
self.hangeSummoned = false;
self.hangeInstance = null;
// Initialize transformation properties for Eren tower
self.isTransforming = false;
self.transformationTimer = 0;
self.isTitan = false;
self.titanDuration = 0;
// Declare baseGraphics early to avoid undefined errors
var baseGraphics;
switch (self.id) {
case 'rapid':
baseGraphics = self.attachAsset('erentower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
case 'sniper':
baseGraphics = self.attachAsset('sashatower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
case 'splash':
baseGraphics = self.attachAsset('mikasatower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
case 'slow':
baseGraphics = self.attachAsset('erwintower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
case 'poison':
baseGraphics = self.attachAsset('levitower', {
anchorX: 0.5,
anchorY: 0.5
});
break;
default:
baseGraphics = self.attachAsset('jeantower', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Create channel bar for Erwin tower (initially hidden)
self.channelBarBg = null;
self.channelBar = null;
if (self.id === 'slow') {
// Create channel bar background
self.channelBarBg = self.attachAsset('staminaBarBg', {
anchorX: 0.5,
anchorY: 0.5
});
self.channelBarBg.y = -baseGraphics.height / 2 - 40;
self.channelBarBg.width = 150;
self.channelBarBg.height = 16;
self.channelBarBg.visible = false;
// Create channel bar
self.channelBar = self.attachAsset('staminaBar', {
anchorX: 0,
anchorY: 0.5
});
self.channelBar.x = -self.channelBarBg.width / 2;
self.channelBar.y = -baseGraphics.height / 2 - 40;
self.channelBar.width = 0;
self.channelBar.height = 16;
self.channelBar.tint = 0x00ff00; // Green color
self.channelBar.visible = false;
}
// Create transformation meter for Eren tower (initially hidden)
self.transformBarBg = null;
self.transformBar = null;
if (self.id === 'rapid') {
// Create transformation bar background
self.transformBarBg = self.attachAsset('staminaBarBg', {
anchorX: 0.5,
anchorY: 0.5
});
self.transformBarBg.y = -baseGraphics.height / 2 - 40;
self.transformBarBg.width = 150;
self.transformBarBg.height = 16;
self.transformBarBg.visible = false;
// Create transformation bar
self.transformBar = self.attachAsset('staminaBar', {
anchorX: 0,
anchorY: 0.5
});
self.transformBar.x = -self.transformBarBg.width / 2;
self.transformBar.y = -baseGraphics.height / 2 - 40;
self.transformBar.width = 0;
self.transformBar.height = 16;
self.transformBar.tint = 0xff0000; // Red color for transformation
self.transformBar.visible = false;
}
switch (self.id) {
case 'rapid':
self.fireRate = 60;
self.damage = 18;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 75;
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 13;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
// Remove level indicators - all towers now use only character images without dots
var levelIndicators = []; // Keep empty array for existing code compatibility
// Remove gunContainer - all towers now use only character images without arrows
var gunContainer = new Container(); // Keep reference for existing code but don't add graphics
self.updateLevelIndicators = function () {
// No level indicators for any towers - all use only character images
return;
};
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 (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect
self.damage = 5 + self.level * 10; // double the effect
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
// No upgrade animation needed - all towers use only character images without level indicators
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, 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 () {
// Handle Eren tower transformation
if (self.id === 'rapid') {
if (!self.isTransforming && !self.isTitan) {
// Normal Eren tower - start transformation timer after spawning
self.transformationTimer++;
// Show transformation meter
if (self.transformBarBg && self.transformBar) {
self.transformBarBg.visible = true;
self.transformBar.visible = true;
// Calculate progress (transformation takes 600 frames = 10 seconds)
var progress = self.transformationTimer / 600;
self.transformBar.width = self.transformBarBg.width * progress;
}
// Transform to titan after 10 seconds
if (self.transformationTimer >= 600) {
self.isTitan = true;
self.titanDuration = 600; // Stay as titan for 10 seconds
self.transformationTimer = 0;
// Switch to titan asset
baseGraphics.visible = false;
var titanGraphics = self.attachAsset('erentitan', {
anchorX: 0.5,
anchorY: 0.5
});
self.titanGraphics = titanGraphics;
// Hide transformation meter when transformed
if (self.transformBarBg && self.transformBar) {
self.transformBarBg.visible = false;
self.transformBar.visible = false;
}
}
} else if (self.isTitan) {
// In titan form - countdown to transform back
self.titanDuration--;
if (self.titanDuration <= 0) {
// Transform back to normal Eren tower
self.isTitan = false;
self.transformationTimer = 0;
// Remove titan graphics and show normal graphics
if (self.titanGraphics && self.titanGraphics.parent) {
self.removeChild(self.titanGraphics);
}
baseGraphics.visible = true;
}
}
}
// Handle Erwin tower channeling and Hange summoning
if (self.id === 'slow' && self.isChanneling) {
self.channelingTimer--;
// Update channel bar progress
if (self.channelBarBg && self.channelBar) {
self.channelBarBg.visible = true;
self.channelBar.visible = true;
// Calculate progress (channeling starts at 600 and counts down to 0)
var progress = (600 - self.channelingTimer) / 600;
self.channelBar.width = self.channelBarBg.width * progress;
}
// After 5 seconds (300 frames), summon Hange
if (!self.hangeSummoned && self.channelingTimer <= 300) {
self.hangeSummoned = true;
self.hangeActive = true;
// Switch Erwin tower to attack asset at the exact moment of spawning
var attackGraphics = self.attachAsset('erwinattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Switch back to normal graphics after 1 second (60 frames)
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 1000); // 1000ms attack animation duration
// Create Hange near the tower
self.hangeInstance = new Container();
var hangeGraphics = self.hangeInstance.attachAsset('hange', {
anchorX: 0.5,
anchorY: 0.5
});
// Position Hange next to the tower
self.hangeInstance.x = self.x + 50;
self.hangeInstance.y = self.y;
game.addChild(self.hangeInstance);
// Initialize Hange properties
self.hangeInstance.attackTimer = 0;
self.hangeInstance.attackRate = 48; // Attack every 0.8 seconds
self.hangeInstance.damage = self.damage * 2; // Hange deals double damage
self.hangeInstance.range = self.getRange() * 1.5; // Hange has 1.5x range
self.hangeInstance.lifetime = 300; // Dies after 5 seconds
}
// Handle Hange's attacks if summoned
if (self.hangeSummoned && self.hangeInstance && self.hangeInstance.parent) {
self.hangeInstance.attackTimer++;
self.hangeInstance.lifetime--;
// Hange attacks enemies
if (self.hangeInstance.attackTimer >= self.hangeInstance.attackRate) {
self.hangeInstance.attackTimer = 0;
// Find closest enemy in range
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.hangeInstance.x;
var dy = enemy.y - self.hangeInstance.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.hangeInstance.range && distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
// Attack the closest enemy
if (closestEnemy) {
// Switch to attack asset
var attackGraphics = self.hangeInstance.attachAsset('hangeattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original hange graphics
var hangeGraphics = self.hangeInstance.children[0];
hangeGraphics.visible = false;
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent && self.hangeInstance && self.hangeInstance.parent) {
self.hangeInstance.removeChild(attackGraphics);
}
// Show original hange graphics again
if (hangeGraphics && self.hangeInstance && self.hangeInstance.parent) {
hangeGraphics.visible = true;
}
}, 500); // 500ms attack animation duration
// Send slashanim asset to enemy instead of bullet
var slashAnim = LK.getAsset('slashanim', {
anchorX: 0.5,
anchorY: 0.5
});
slashAnim.x = self.hangeInstance.x;
slashAnim.y = self.hangeInstance.y;
game.addChild(slashAnim);
// Animate slashanim to enemy using tween
tween(slashAnim, {
x: closestEnemy.x,
y: closestEnemy.y
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Apply damage when animation reaches enemy
if (self.hangeInstance && self.hangeInstance.parent) {
closestEnemy.health -= self.hangeInstance.damage;
} else {
// Use default damage if hangeInstance is no longer available
closestEnemy.health -= self.damage * 2; // Hange deals double damage
}
if (closestEnemy.health <= 0) {
closestEnemy.health = 0;
} else {
closestEnemy.healthBar.width = closestEnemy.health / closestEnemy.maxHealth * 70;
}
// Apply splash damage effects since Hange uses splash attacks
// Create visual splash effect
var splashEffect = new EffectIndicator(closestEnemy.x, closestEnemy.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 !== closestEnemy) {
var splashDx = otherEnemy.x - closestEnemy.x;
var splashDy = otherEnemy.y - closestEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage)
if (self.hangeInstance && self.hangeInstance.parent) {
otherEnemy.health -= self.hangeInstance.damage * 0.5;
} else {
// Use default damage if hangeInstance is no longer available
otherEnemy.health -= self.damage * 2 * 0.5; // Hange deals double damage
}
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
// Remove the slashanim asset after applying effects
slashAnim.destroy();
}
});
}
}
// Remove Hange after lifetime expires
if (self.hangeInstance.lifetime <= 0) {
game.removeChild(self.hangeInstance);
self.hangeInstance = null;
self.hangeActive = false;
}
}
// End channeling after 10 seconds
if (self.channelingTimer <= 0) {
self.isChanneling = false;
// Hide channel bar when channeling ends
if (self.channelBarBg && self.channelBar) {
self.channelBarBg.visible = false;
self.channelBar.visible = false;
}
}
return; // Skip normal tower behavior while channeling
}
// Hide channel bar when not channeling (for Erwin towers)
if (self.id === 'slow' && !self.isChanneling && self.channelBarBg && self.channelBar) {
self.channelBarBg.visible = false;
self.channelBar.visible = false;
}
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);
// All towers now use static character images without rotation
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
// Show upgrade menu without creating range indicators that block dragging
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
// Special handling for Eren tower - can only attack in titan form
if (self.id === 'rapid') {
if (!self.isTitan) {
return; // Cannot attack in normal form
}
}
// Special handling for Erwin tower
if (self.id === 'slow') {
// Check if already channeling or if Hange is active
if (self.isChanneling || self.hangeActive) {
return; // Cannot attack while channeling or Hange is active
}
// Start channeling process
self.isChanneling = true;
self.channelingTimer = 600; // 10 seconds at 60 FPS
self.hangeActive = false;
self.hangeSummoned = false;
// Show channel bar when starting to channel
if (self.channelBarBg && self.channelBar) {
self.channelBarBg.visible = true;
self.channelBar.visible = true;
self.channelBar.width = 0; // Start with empty bar
}
return;
}
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
// Only create bullets for Sasha tower (sniper)
if (self.id === 'sniper') {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// Customize bullet appearance for sniper
bullet.children[0].tint = 0xFF5500;
bullet.children[0].width = 15;
bullet.children[0].height = 15;
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
} else {
// For all other towers, send slashanim asset to enemy
var slashAnim = LK.getAsset('slashanim', {
anchorX: 0.5,
anchorY: 0.5
});
slashAnim.x = self.x;
slashAnim.y = self.y;
game.addChild(slashAnim);
// Animate slashanim to enemy using tween
tween(slashAnim, {
x: self.targetEnemy.x,
y: self.targetEnemy.y
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
// Apply damage when animation reaches enemy
// Check if target enemy still exists before accessing health property
if (self.targetEnemy && self.targetEnemy.health !== undefined) {
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 tower type
if (self.id === '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.id === '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
var slowPct = 0.5;
if (self.level !== undefined) {
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.level - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct;
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180;
} else {
self.targetEnemy.slowDuration = 180;
}
}
} else if (self.id === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2;
self.targetEnemy.poisonDuration = 300;
}
}
// Remove the slashanim asset after applying effects
slashAnim.destroy();
}
});
}
// Handle tower attack animations and add sword wave effects for all towers except Sasha
if (self.id === 'rapid' && self.isTitan) {
// Switch to attack asset
var attackGraphics = self.attachAsset('erentitanattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the titan graphics
if (self.titanGraphics) {
self.titanGraphics.visible = false;
}
// Create sword wave animation
var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash');
game.addChild(slashWave);
// Switch back to titan graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show titan graphics again
if (self.titanGraphics) {
self.titanGraphics.visible = true;
}
}, 500); // 500ms attack animation duration
}
// Handle Levi tower attack animation
if (self.id === 'poison') {
// Switch to attack asset
var attackGraphics = self.attachAsset('leviattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 500); // 500ms attack animation duration
}
// Handle Sasha tower attack animation (no sword wave for sniper)
if (self.id === 'sniper') {
// Switch to attack asset
var attackGraphics = self.attachAsset('sashaattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 500); // 500ms attack animation duration
}
// Handle Mikasa tower attack animation
if (self.id === 'splash') {
// Switch to attack asset
var attackGraphics = self.attachAsset('mikasaattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Create sword wave animation
var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash');
game.addChild(slashWave);
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 500); // 500ms attack animation duration
}
// Handle Armin tower attack animation (default type)
if (self.id === 'default') {
// Switch to attack asset
var attackGraphics = self.attachAsset('arminattack', {
anchorX: 0.5,
anchorY: 0.5
});
// Hide the original tower graphics
baseGraphics.visible = false;
// Create sword wave animation
var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash');
game.addChild(slashWave);
// Switch back to normal graphics after attack duration
LK.setTimeout(function () {
// Remove attack graphics
if (attackGraphics && attackGraphics.parent) {
self.removeChild(attackGraphics);
}
// Show original tower graphics again
baseGraphics.visible = true;
}, 500); // 500ms attack animation duration
}
// Handle other tower types (slow, poison) - add sword wave animations
if (self.id === 'slow' || self.id === 'poison') {
// Create sword wave animation for these tower types
var slashWave = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slash');
game.addChild(slashWave);
}
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 1;
}
}
}
self.refreshCellsInRange();
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'default';
self.hasEnoughGold = true;
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3;
var previewGraphics = self.attachAsset('towerpreview', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
// Reset tint first
previewGraphics.tint = 0xFFFFFF;
// Apply red tint only if cannot place OR not enough gold
// Apply green tint if can place AND has enough gold
if (self.canPlace && self.hasEnoughGold) {
previewGraphics.tint = 0x00FF00; // Green for valid placement
} else {
previewGraphics.tint = 0xFF0000; // Red for invalid placement or insufficient gold
}
};
self.updatePlacementStatus = function () {
// Check if tower fits in grid bounds and doesn't block path or overlap roads
self.canPlace = self.gridX >= 0 && self.gridY >= 0 && self.gridX + 1 < grid.cells.length && self.gridY + 1 < grid.cells[0].length && !wouldBlockPath(self.gridX, self.gridY);
self.blockedByEnemy = false;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
// Get character name based on tower type
var characterName;
switch (self.tower.id) {
case 'rapid':
characterName = 'Kael';
break;
case 'sniper':
characterName = 'Lyra';
break;
case 'splash':
characterName = 'Eira';
break;
case 'slow':
characterName = 'Darius';
break;
case 'poison':
characterName = 'Vale';
break;
default:
characterName = 'Raen';
}
// Add background for tower type text for better readability
var towerTypeBackground = self.attachAsset('notification', {
anchorX: 0,
anchorY: 0.5
});
towerTypeBackground.width = 600;
towerTypeBackground.height = 100;
towerTypeBackground.x = -870;
towerTypeBackground.y = -160;
towerTypeBackground.tint = 0x2C3E50;
towerTypeBackground.alpha = 0.9;
var towerTypeText = new Text2(characterName + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0.5);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level.toString() + '/' + self.tower.maxLevel.toString() + '\nDamage: ' + self.tower.damage.toString() + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 68,
fill: 0xFFFFFF,
weight: 600
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
if (statsText && statsText.setText) {
statsText.setText('Level: ' + self.tower.level.toString() + '/' + self.tower.maxLevel.toString() + '\nDamage: ' + self.tower.damage.toString() + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
}
if (buttonText && buttonText.setText) {
buttonText.setText('Upgrade: ' + upgradeCost.toString() + ' gold');
}
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
if (sellButtonText && sellButtonText.setText) {
sellButtonText.setText('Sell: +' + sellValue.toString() + ' gold');
}
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
if (buttonText && buttonText.setText) {
buttonText.setText('Max Level');
}
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
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 && buttonText.setText) {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost.toString() + ' gold';
if (buttonText && buttonText.setText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
size: 48,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 2;
startTextShadow.y = 2;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Game", {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 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;
}
var baseCount = self.enemyCounts[waveNumber - 1];
// Apply difficulty scaling to displayed enemy count
if (game.selectedDifficulty) {
var enemyCountMultiplier = 1.0;
switch (game.selectedDifficulty) {
case 'EASY':
enemyCountMultiplier = 0.7; // Easy - 30% fewer enemies (reduced difficulty)
break;
case 'MEDIUM':
enemyCountMultiplier = 1.1; // Medium - 10% more enemies (reduced difficulty)
break;
case 'HARD':
enemyCountMultiplier = 1.4; // Hard - 40% more enemies (reduced difficulty)
break;
case 'EXTREME':
enemyCountMultiplier = 1.8; // Extreme - 80% more enemies (reduced difficulty)
break;
}
baseCount = Math.max(1, Math.round(baseCount * enemyCountMultiplier));
}
return baseCount;
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName;
// Map enemy types to their proper names based on the assets
switch (type) {
case 'normal':
typeName = "Giants";
break;
case 'fast':
typeName = "Fast Giant";
break;
case 'flying':
typeName = "Flying Giant";
break;
case 'immune':
typeName = "Immune Giant";
break;
case 'swarm':
typeName = "Swarm Giant";
break;
default:
typeName = "Giants";
}
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS " + typeName;
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
// Add background asset to the game
var backgroundAsset = LK.getAsset('background', {
anchorX: 0,
anchorY: 0
});
// Scale the background to fill the entire screen
backgroundAsset.width = 2048;
backgroundAsset.height = 2732;
backgroundAsset.x = 0;
backgroundAsset.y = 0;
backgroundAsset.alpha = 0.55; // Set 55% transparency (45% opacity)
game.addChildAt(backgroundAsset, 0); // Add as the first child so it appears behind everything
// Using defense icon as heart placeholder
var isHidingUpgradeMenu = false;
var gameStarted = false;
var mainMenuContainer = null;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
function createMainMenu() {
mainMenuContainer = new Container();
game.addChild(mainMenuContainer);
// Add black background behind main menu
var blackBackground = mainMenuContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
blackBackground.width = 2048;
blackBackground.height = 2732;
blackBackground.tint = 0x000000;
blackBackground.x = 2048 / 2;
blackBackground.y = 2732 / 2;
// Add main menu background
var menuBackground = mainMenuContainer.attachAsset('Mainmenu', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.x = 2048 / 2;
menuBackground.y = 2732 / 2;
// Create start button
var startButton = new Container();
mainMenuContainer.addChild(startButton);
// Add button background for better visual feedback
var startButtonBg = startButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startButtonBg.width = 350;
startButtonBg.height = 120;
startButtonBg.tint = 0x2C3E50;
startButtonBg.alpha = 0.8;
var startText = new Text2("START", {
size: 60,
fill: 0xFFFFFF,
weight: 800,
font: "'Copperplate Gothic'"
});
startText.anchor.set(0.5, 0.5);
startButton.addChild(startText);
// Add hover effect asset
var hoverEffect = startButton.attachAsset('menuhover', {
anchorX: 0.5,
anchorY: 0.5
});
hoverEffect.visible = false; // Initially hidden
hoverEffect.width = 370;
hoverEffect.height = 140;
startButton.x = 2048 - 150 - 189 + 76; // Move 2cm right (2cm * 37.8 pixels/cm)
startButton.y = 2732 - 150 - 567 - 189 - 189 + 76 - 76; // Move 2cm higher (2cm * 37.8 pixels/cm)
// Remove the button's move handler - we'll handle everything in the global handler
// Move handling is now done in the main game.move handler
startButton.down = function () {
startGame();
};
// Create options button
var optionsButton = new Container();
mainMenuContainer.addChild(optionsButton);
// Add button background for options
var optionsButtonBg = optionsButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
optionsButtonBg.width = 350;
optionsButtonBg.height = 120;
optionsButtonBg.tint = 0x2C3E50;
optionsButtonBg.alpha = 0.8;
var optionsText = new Text2("OPTIONS", {
size: 60,
fill: 0xFFFFFF,
weight: 800,
font: "'Copperplate Gothic'"
});
optionsText.anchor.set(0.5, 0.5);
optionsButton.addChild(optionsText);
// Add hover effect asset for options button
var optionsHoverEffect = optionsButton.attachAsset('menuhover', {
anchorX: 0.5,
anchorY: 0.5
});
optionsHoverEffect.visible = false; // Initially hidden
optionsHoverEffect.width = 370;
optionsHoverEffect.height = 140;
optionsButton.x = 2048 - 150 - 189 + 76; // Same X position as start button
optionsButton.y = 2732 - 150 - 567 - 189 - 189 + 76 - 38 + 189; // 5cm (189 pixels) lower than start button
optionsButton.down = function () {
// Options functionality can be added here later
var notification = game.addChild(new Notification("Options menu coming soon!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
};
// Create story button
var storyButton = new Container();
mainMenuContainer.addChild(storyButton);
// Add button background for story
var storyButtonBg = storyButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
storyButtonBg.width = 350;
storyButtonBg.height = 120;
storyButtonBg.tint = 0x2C3E50;
storyButtonBg.alpha = 0.8;
var storyText = new Text2("STORY", {
size: 60,
fill: 0xFFFFFF,
weight: 800,
font: "'Copperplate Gothic'"
});
storyText.anchor.set(0.5, 0.5);
storyButton.addChild(storyText);
// Add hover effect asset for story button
var storyHoverEffect = storyButton.attachAsset('menuhover', {
anchorX: 0.5,
anchorY: 0.5
});
storyHoverEffect.visible = false; // Initially hidden
storyHoverEffect.width = 370;
storyHoverEffect.height = 140;
storyButton.x = 2048 - 150 - 189 + 76; // Same X position as start and options buttons
storyButton.y = 2732 - 150 - 567 - 189 - 189 + 76 - 38 + 189 + 189 + 38; // 1cm (38 pixels) lower than previous position
storyButton.down = function () {
showStoryScreen();
};
}
function startGame() {
if (gameStarted) return;
// Show difficulty page instead of starting game directly
showDifficultyPage();
}
function showStoryScreen() {
// Hide main menu
if (mainMenuContainer) {
mainMenuContainer.visible = false;
}
// Create story screen container
var storyContainer = new Container();
game.addChild(storyContainer);
// Add black background
var blackBackground = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
blackBackground.width = 2048;
blackBackground.height = 2732;
blackBackground.tint = 0x000000;
blackBackground.x = 2048 / 2;
blackBackground.y = 2732 / 2;
// Add elegant gradient-style background layers
var outerFrame = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
outerFrame.width = 1900;
outerFrame.height = 2400;
outerFrame.tint = 0x1A252F; // Deep blue-gray frame
outerFrame.alpha = 0.95;
outerFrame.x = 2048 / 2;
outerFrame.y = 2732 / 2;
// Add story content background with refined design
var storyBackground = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
storyBackground.width = 1800;
storyBackground.height = 2300;
storyBackground.tint = 0x2C3E50; // Rich slate background
storyBackground.alpha = 0.98;
storyBackground.x = 2048 / 2;
storyBackground.y = 2732 / 2;
// Add decorative title frame
var titleFrame = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
titleFrame.width = 1600;
titleFrame.height = 140;
titleFrame.tint = 0x8B4513; // Warm bronze frame
titleFrame.alpha = 0.8;
titleFrame.x = 2048 / 2;
titleFrame.y = 380;
// Add enhanced story title with shadow
var storyTitleShadow = new Text2("⚔ THE LAST STAND ⚔", {
size: 100,
fill: 0x000000,
weight: 900
});
storyTitleShadow.anchor.set(0.5, 0.5);
storyTitleShadow.x = 2048 / 2 + 4;
storyTitleShadow.y = 384;
storyContainer.addChild(storyTitleShadow);
var storyTitle = new Text2("⚔ THE LAST STAND ⚔", {
size: 100,
fill: 0xFFD700,
weight: 900
});
storyTitle.anchor.set(0.5, 0.5);
storyTitle.x = 2048 / 2;
storyTitle.y = 380;
storyContainer.addChild(storyTitle);
// Improved story text with better formatting and readability
var storyText = "The Colossi have broken through. Humanity's final hour approaches.\n\n" + "After centuries of uneasy peace, the monstrous giants known as Colossi\n" + "have shattered the last barrier protecting mankind. The fortified city of\n" + "Aetherhold—humanity's final bastion—now faces total annihilation.\n\n" + "With the main army scattered and the city walls crumbling, only one\n" + "defense remains: YOU, the last Strategist of the Vanguard Order.\n\n" + "Command the six legendary heroes who stand between hope and despair:\n\n" + "• KAEL - Swift blade master with lightning-fast strikes\n" + "• LYRA - Precision marksman who never misses her mark\n" + "• DARIUS - Fearless commander who rallies nearby allies\n" + "• VALE - Silent assassin striking from the shadows\n" + "• EIRA - Elite guardian with unmatched combat reflexes\n" + "• RAEN - Volatile warrior wielding the giants' own power\n\n" + "Deploy your heroes strategically. Upgrade their abilities.\n" + "Adapt to each wave's unique threats and devastating power.\n\n" + "This is humanity's final gambit—not mere survival, but defiance.\n\n" + "Defend the last wall. Command the final heroes.\n" + "Turn the tide against the Colossi apocalypse.";
// Create content area with better spacing
var contentArea = storyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
contentArea.width = 1650;
contentArea.height = 1600;
contentArea.tint = 0x34495E; // Darker content area
contentArea.alpha = 0.7;
contentArea.x = 2048 / 2;
contentArea.y = 1400;
// Add story content with shadow for depth
var storyContentShadow = new Text2(storyText, {
size: 44,
fill: 0x000000,
weight: 600
});
storyContentShadow.anchor.set(0.5, 0.5);
storyContentShadow.x = 2048 / 2 + 3;
storyContentShadow.y = 1403;
storyContainer.addChild(storyContentShadow);
var storyContentText = new Text2(storyText, {
size: 44,
fill: 0xECF0F1,
//{t1} // Softer white for better readability
weight: 600
});
storyContentText.anchor.set(0.5, 0.5);
storyContentText.x = 2048 / 2;
storyContentText.y = 1400;
storyContainer.addChild(storyContentText);
// Enhanced back button with better styling
var backButton = new Container();
storyContainer.addChild(backButton);
// Add glow effect for back button
var backButtonGlow = backButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
backButtonGlow.width = 460;
backButtonGlow.height = 140;
backButtonGlow.tint = 0x8B0000;
backButtonGlow.alpha = 0.3;
// Add button background
var backButtonBg = backButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
backButtonBg.width = 420;
backButtonBg.height = 120;
backButtonBg.tint = 0x8B0000;
backButtonBg.alpha = 0.95;
// Add decorative corners
var leftCorner = backButton.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftCorner.width = 20;
leftCorner.height = 20;
leftCorner.tint = 0xFFD700;
leftCorner.x = -180;
leftCorner.y = 0;
var rightCorner = backButton.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightCorner.width = 20;
rightCorner.height = 20;
rightCorner.tint = 0xFFD700;
rightCorner.x = 180;
rightCorner.y = 0;
// Enhanced back text with shadow
var backTextShadow = new Text2("◄ RETURN", {
size: 58,
fill: 0x000000,
weight: 800
});
backTextShadow.anchor.set(0.5, 0.5);
backTextShadow.x = 2;
backTextShadow.y = 2;
backButton.addChild(backTextShadow);
var backText = new Text2("◄ RETURN", {
size: 58,
fill: 0xFFFFFF,
weight: 800
});
backText.anchor.set(0.5, 0.5);
backButton.addChild(backText);
backButton.x = 2048 / 2;
backButton.y = 2500;
// Add hover effect for back button
var backHoverEffect = backButton.attachAsset('menuhover', {
anchorX: 0.5,
anchorY: 0.5
});
backHoverEffect.visible = false;
backHoverEffect.width = 440;
backHoverEffect.height = 140;
// Add entrance animation for the entire story screen
storyContainer.alpha = 0;
storyContainer.scaleX = 0.9;
storyContainer.scaleY = 0.9;
tween(storyContainer, {
alpha: 1,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 400,
easing: tween.easeOut
});
// Back button functionality
backButton.down = function () {
// Add exit animation
tween(storyContainer, {
alpha: 0,
scaleX: 0.9,
scaleY: 0.9
}, {
duration: 250,
easing: tween.easeIn,
onFinish: function onFinish() {
// Remove story screen
storyContainer.destroy();
// Show main menu again
if (mainMenuContainer) {
mainMenuContainer.visible = true;
}
}
});
};
// Enhanced hover effects for back button
backButton.move = function (x, y, obj) {
var buttonLeft = this.x - 210;
var buttonRight = this.x + 210;
var buttonTop = this.y - 60;
var buttonBottom = this.y + 60;
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
backHoverEffect.visible = mouseInButton;
if (mouseInButton) {
// Enhanced hover animations
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 150,
easing: tween.easeOut
});
tween.stop(backText, {
tint: true
});
tween(backText, {
tint: 0xFF4444 //{tq} // Brighter red on hover
}, {
duration: 200,
easing: tween.easeOut
});
backButtonGlow.alpha = 0.5;
} else {
// Reset animations
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 150,
easing: tween.easeOut
});
tween.stop(backText, {
tint: true
});
tween(backText, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
backButtonGlow.alpha = 0.3;
}
};
}
function showDifficultyPage() {
// Hide main menu
if (mainMenuContainer) {
mainMenuContainer.visible = false;
}
// Create difficulty page container
var difficultyContainer = new Container();
game.addChild(difficultyContainer);
// Add black background
var blackBackground = difficultyContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
blackBackground.width = 2048;
blackBackground.height = 2732;
blackBackground.tint = 0x000000;
blackBackground.x = 2048 / 2;
blackBackground.y = 2732 / 2;
// Add difficulty page background
var difficultyBackground = difficultyContainer.attachAsset('Difficulty', {
anchorX: 0.5,
anchorY: 0.5
});
difficultyBackground.x = 2048 / 2;
difficultyBackground.y = 2732 / 2;
// Create difficulty buttons
var difficulties = [{
name: "EASY",
y: 862 // Move 40 pixels lower
}, {
name: "MEDIUM",
y: 1287 // 3cm higher (113 pixels)
}, {
name: "HARD",
y: 1789 // Move 2cm lower (76 pixels)
}, {
name: "EXTREME",
y: 2253 // Move 3cm lower (113 pixels)
}];
for (var i = 0; i < difficulties.length; i++) {
var difficulty = difficulties[i];
var difficultyButton = new Container();
difficultyContainer.addChild(difficultyButton);
// Add button background
var buttonBg = difficultyButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBg.width = 600;
buttonBg.height = 120;
// Apply specific colors for difficulty buttons
if (difficulty.name === "EASY") {
buttonBg.tint = 0xD2B48C; // Tan color
} else if (difficulty.name === "MEDIUM") {
buttonBg.tint = 0x93C47D; // Pistachio green color
} else if (difficulty.name === "HARD") {
buttonBg.tint = 0x8B0000; // Dark red color
} else if (difficulty.name === "EXTREME") {
buttonBg.tint = 0x4682B4; // Darker steel blue color
} else {
buttonBg.tint = 0x2C3E50;
}
buttonBg.alpha = 0.8;
var buttonText = new Text2(difficulty.name, {
size: 52,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
difficultyButton.addChild(buttonText);
difficultyButton.x = 2048 / 2;
difficultyButton.y = difficulty.y;
// Add tween animation for all difficulty buttons
if (difficulty.name === "EASY") {
// Start slightly higher and animate to final position
difficultyButton.y = difficulty.y - 50;
tween(difficultyButton, {
y: difficulty.y
}, {
duration: 800,
easing: tween.bounceOut
});
} else if (difficulty.name === "MEDIUM") {
// Start slightly higher and animate to final position
difficultyButton.y = difficulty.y - 50;
tween(difficultyButton, {
y: difficulty.y
}, {
duration: 800,
easing: tween.bounceOut
});
} else if (difficulty.name === "HARD") {
// Start slightly higher and animate to final position
difficultyButton.y = difficulty.y - 50;
tween(difficultyButton, {
y: difficulty.y
}, {
duration: 800,
easing: tween.bounceOut
});
} else if (difficulty.name === "EXTREME") {
// Start slightly higher and animate to final position
difficultyButton.y = difficulty.y - 50;
tween(difficultyButton, {
y: difficulty.y
}, {
duration: 800,
easing: tween.bounceOut
});
}
// Store difficulty name for button handler
difficultyButton.difficultyName = difficulty.name;
// Add hover animation for each difficulty button
difficultyButton.move = function (x, y, obj) {
// Check if mouse is within button bounds
var buttonLeft = this.x - 300; // half of button width (600/2)
var buttonRight = this.x + 300;
var buttonTop = this.y - 60; // half of button height (120/2)
var buttonBottom = this.y + 60;
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
if (mouseInButton) {
// Stop any existing scale tweens
tween.stop(this, {
scaleX: true,
scaleY: true
});
// Scale up on hover
tween(this, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 200,
easing: tween.easeOut
});
} else {
// Stop any existing scale tweens
tween.stop(this, {
scaleX: true,
scaleY: true
});
// Scale back to normal
tween(this, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeOut
});
}
};
difficultyButton.down = function () {
// Start game with selected difficulty
startGameWithDifficulty(this.difficultyName);
// Remove difficulty page
difficultyContainer.destroy();
};
}
}
function startGameWithDifficulty(selectedDifficulty) {
if (gameStarted) return;
gameStarted = true;
// Stop main menu music immediately
LK.stopMusic();
// Store selected difficulty (can be used for game balancing)
game.selectedDifficulty = selectedDifficulty;
// Reset upgrades to zero and clear storage to prevent stacking
upgrades = {
damage: 0,
range: 0,
attackSpeed: 0,
stamina: 0,
castleHealth: 0,
castleAttack: 0
};
// Clear all stored upgrade values
storage.damageUpgrades = 0;
storage.rangeUpgrades = 0;
storage.attackSpeedUpgrades = 0;
storage.staminaUpgrades = 0;
storage.castleHealthUpgrades = 0;
storage.castleAttackUpgrades = 0;
// Initialize game elements that were previously created at startup
initializeGameElements();
// Start the first wave
if (waveIndicator) {
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
}
// Show difficulty selection notification
var notification = game.addChild(new Notification("Difficulty: " + selectedDifficulty + " selected!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
function showShopInterface() {
// Create shop interface container
var shopInterface = new Container();
game.addChild(shopInterface);
// Add animated black semi-transparent background with gradient effect
var shopBackground = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopBackground.width = 2048;
shopBackground.height = 2732;
shopBackground.tint = 0x000000;
shopBackground.alpha = 0.85; // Slightly more opaque for better focus
shopBackground.x = 2048 / 2;
shopBackground.y = 2732 / 2;
// Add premium shop window with enhanced design
var shopWindow = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopWindow.width = 1700; // Slightly wider for better proportions
shopWindow.height = 1300; // Slightly taller for better spacing
shopWindow.tint = 0x1A252F; // Premium dark blue-gray
shopWindow.alpha = 0.98; // Near opaque for professional look
shopWindow.x = 2048 / 2;
shopWindow.y = 2732 / 2;
// Add decorative border for the shop window
var shopBorder = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
shopBorder.width = 1720;
shopBorder.height = 1320;
shopBorder.tint = 0x52796F; // Elegant sage green border
shopBorder.alpha = 0.9;
shopBorder.x = 2048 / 2;
shopBorder.y = 2732 / 2;
// Add premium title background with glow effect
var titleBackground = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
titleBackground.width = 1600;
titleBackground.height = 100;
titleBackground.tint = 0x2D3436; // Professional dark gray
titleBackground.alpha = 0.95;
titleBackground.x = 2048 / 2;
titleBackground.y = 2732 / 2 - 550;
// Add enhanced shop title with shadow effect
var shopTitleShadow = new Text2("⚔ UPGRADE ARSENAL ⚔", {
size: 90,
fill: 0x000000,
weight: 900
});
shopTitleShadow.anchor.set(0.5, 0.5);
shopTitleShadow.x = 2048 / 2 + 3;
shopTitleShadow.y = 2732 / 2 - 547;
shopInterface.addChild(shopTitleShadow);
var shopTitle = new Text2("⚔ UPGRADE ARSENAL ⚔", {
size: 90,
fill: 0xFDCB6E,
// Premium gold color
weight: 900
});
shopTitle.anchor.set(0.5, 0.5);
shopTitle.x = 2048 / 2;
shopTitle.y = 2732 / 2 - 550; // Moved up slightly for better positioning
shopInterface.addChild(shopTitle);
// Add current gold display with enhanced styling
var goldDisplayBg = shopInterface.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
goldDisplayBg.width = 400;
goldDisplayBg.height = 70;
goldDisplayBg.tint = 0x8B6914; // Rich gold background
goldDisplayBg.alpha = 0.9;
goldDisplayBg.x = 2048 / 2;
goldDisplayBg.y = 2732 / 2 - 460;
// Add coin decorations around gold display
var leftCoin = shopInterface.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftCoin.width = 25;
leftCoin.height = 25;
leftCoin.tint = 0xFFD700; // Bright gold
leftCoin.x = 2048 / 2 - 120;
leftCoin.y = 2732 / 2 - 460;
var rightCoin = shopInterface.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightCoin.width = 25;
rightCoin.height = 25;
rightCoin.tint = 0xFFD700; // Bright gold
rightCoin.x = 2048 / 2 + 120;
rightCoin.y = 2732 / 2 - 460;
var goldDisplayShadow = new Text2("Gold: " + gold, {
size: 48,
fill: 0x000000,
weight: 800
});
goldDisplayShadow.anchor.set(0.5, 0.5);
goldDisplayShadow.x = 2048 / 2 + 2;
goldDisplayShadow.y = 2732 / 2 - 458;
shopInterface.addChild(goldDisplayShadow);
var goldDisplay = new Text2("Gold: " + gold, {
size: 48,
fill: 0xFFD700,
// Bright gold text
weight: 800
});
goldDisplay.anchor.set(0.5, 0.5);
goldDisplay.x = 2048 / 2;
goldDisplay.y = 2732 / 2 - 460;
shopInterface.addChild(goldDisplay);
// Enhanced close button with premium styling
var closeButton = new Container();
shopInterface.addChild(closeButton);
// Add glow effect for close button
var closeGlow = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeGlow.width = 140;
closeGlow.height = 140;
closeGlow.tint = 0xFF4444;
closeGlow.alpha = 0.3;
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 120;
closeBackground.height = 120;
closeBackground.tint = 0x8B0000; // Darker red for premium look
closeBackground.alpha = 0.95;
// Add close button shadow
var closeTextShadow = new Text2('✕', {
size: 70,
fill: 0x000000,
weight: 900
});
closeTextShadow.anchor.set(0.5, 0.5);
closeTextShadow.x = 2;
closeTextShadow.y = 2;
closeButton.addChild(closeTextShadow);
var closeText = new Text2('✕', {
size: 70,
fill: 0xFFFFFF,
weight: 900
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = 2048 / 2 + 790; // Adjusted for new window size
closeButton.y = 2732 / 2 - 590; // Adjusted for new window size
// Add hover effect for close button
closeButton.move = function (x, y, obj) {
var buttonLeft = this.x - 60;
var buttonRight = this.x + 60;
var buttonTop = this.y - 60;
var buttonBottom = this.y + 60;
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
if (mouseInButton) {
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 150,
easing: tween.easeOut
});
closeGlow.alpha = 0.5;
} else {
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 150,
easing: tween.easeOut
});
closeGlow.alpha = 0.3;
}
};
closeButton.down = function () {
// Add satisfying close animation
tween(shopInterface, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
shopInterface.destroy();
}
});
};
// Add entrance animation for the shop
shopInterface.alpha = 0;
shopInterface.scaleX = 0.8;
shopInterface.scaleY = 0.8;
tween(shopInterface, {
alpha: 1,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 300,
easing: tween.backOut
});
// Create upgrade buttons container with better positioning
var upgradesContainer = new Container();
upgradesContainer.x = 2048 / 2;
upgradesContainer.y = 2732 / 2 - 100; // Better centering
shopInterface.addChild(upgradesContainer);
// Helper function to create premium upgrade button
function createUpgradeButton(upgradeType, yPos, iconAsset, title, description, currentLevel, maxLevel, cost, color) {
var upgradeContainer = new Container();
upgradesContainer.addChild(upgradeContainer);
upgradeContainer.y = yPos;
// Add subtle glow background
var glowBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
glowBg.width = 1520;
glowBg.height = 100;
glowBg.tint = color;
glowBg.alpha = 0.15;
// Main button background with gradient effect
var mainBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
mainBg.width = 1500;
mainBg.height = 90;
var isMaxLevel = currentLevel >= maxLevel;
var canAfford = gold >= cost;
mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? color : 0x2C1810;
mainBg.alpha = 0.95;
// Left section for icon and title
var iconBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
iconBg.width = 80;
iconBg.height = 80;
iconBg.tint = color;
iconBg.alpha = 0.8;
iconBg.x = -680;
// Add upgrade icon
var upgradeIcon = upgradeContainer.attachAsset(iconAsset, {
anchorX: 0.5,
anchorY: 0.5
});
upgradeIcon.width = 60;
upgradeIcon.height = 60;
upgradeIcon.x = -680;
upgradeIcon.tint = 0xFFFFFF;
upgradeIcon.alpha = 1.0;
// Title section
var titleShadow = new Text2(title, {
size: 52,
fill: 0x000000,
weight: 900
});
titleShadow.anchor.set(0, 0.5);
titleShadow.x = -620;
titleShadow.y = -12;
upgradeContainer.addChild(titleShadow);
var titleText = new Text2(title, {
size: 52,
fill: 0xFFFFFF,
weight: 900
});
titleText.anchor.set(0, 0.5);
titleText.x = -618;
titleText.y = -15;
upgradeContainer.addChild(titleText);
// Description section
var descShadow = new Text2(description, {
size: 36,
fill: 0x000000,
weight: 700
});
descShadow.anchor.set(0, 0.5);
descShadow.x = -620;
descShadow.y = 17;
upgradeContainer.addChild(descShadow);
var descText = new Text2(description, {
size: 36,
fill: 0xCCCCCC,
weight: 700
});
descText.anchor.set(0, 0.5);
descText.x = -618;
descText.y = 15;
upgradeContainer.addChild(descText);
// Level indicator section
var levelBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
levelBg.width = 200;
levelBg.height = 50;
levelBg.tint = 0x2D3436;
levelBg.alpha = 0.9;
levelBg.x = 150;
var levelShadow = new Text2("LV " + currentLevel + "/" + maxLevel, {
size: 40,
fill: 0x000000,
weight: 800
});
levelShadow.anchor.set(0.5, 0.5);
levelShadow.x = 152;
levelShadow.y = 2;
upgradeContainer.addChild(levelShadow);
var levelText = new Text2("LV " + currentLevel + "/" + maxLevel, {
size: 40,
fill: isMaxLevel ? 0xFFD700 : 0x74B9FF,
weight: 800
});
levelText.anchor.set(0.5, 0.5);
levelText.x = 150;
levelText.y = 0;
upgradeContainer.addChild(levelText);
// Cost section with coin decorations
var costBg = upgradeContainer.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
costBg.width = 250;
costBg.height = 60;
costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
costBg.alpha = 0.9;
costBg.x = 450;
// Add coin decorations
var leftCoinCost = upgradeContainer.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftCoinCost.width = 20;
leftCoinCost.height = 20;
leftCoinCost.tint = 0xFFD700;
leftCoinCost.x = 375;
var rightCoinCost = upgradeContainer.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightCoinCost.width = 20;
rightCoinCost.height = 20;
rightCoinCost.tint = 0xFFD700;
rightCoinCost.x = 525;
var costShadow = new Text2(isMaxLevel ? "MAX" : cost.toString(), {
size: 42,
fill: 0x000000,
weight: 800
});
costShadow.anchor.set(0.5, 0.5);
costShadow.x = 452;
costShadow.y = 2;
upgradeContainer.addChild(costShadow);
var costText = new Text2(isMaxLevel ? "MAX" : cost.toString(), {
size: 42,
fill: isMaxLevel ? 0xCCCCCC : 0xFFD700,
weight: 800
});
costText.anchor.set(0.5, 0.5);
costText.x = 450;
costText.y = 0;
upgradeContainer.addChild(costText);
// Store references for updates
upgradeContainer.elements = {
mainBg: mainBg,
titleText: titleText,
descText: descText,
levelText: levelText,
levelShadow: levelShadow,
costText: costText,
costShadow: costShadow,
costBg: costBg,
leftCoinCost: leftCoinCost,
rightCoinCost: rightCoinCost
};
// Add hover effects
upgradeContainer.move = function (x, y, obj) {
if (isMaxLevel) return;
var buttonLeft = this.x - 750;
var buttonRight = this.x + 750;
var buttonTop = this.y - 45;
var buttonBottom = this.y + 45;
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
if (mouseInButton && canAfford) {
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.02,
scaleY: 1.02
}, {
duration: 150,
easing: tween.easeOut
});
glowBg.alpha = 0.25;
} else {
tween.stop(this, {
scaleX: true,
scaleY: true
});
tween(this, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 150,
easing: tween.easeOut
});
glowBg.alpha = 0.15;
}
};
return upgradeContainer;
}
// Create damage upgrade with enhanced styling
var damageUpgradeContainer = createUpgradeButton('damage', -250, 'attackst', 'DAMAGE BOOST', 'Increase hero attack power', upgrades.damage, upgradeMaxLevels.damage, upgradeCosts.damage, 0xE74C3C);
// Create all remaining upgrade buttons
var rangeUpgradeContainer = createUpgradeButton('range', -150, 'rangest', 'RANGE EXTENSION', 'Extend hero attack reach', upgrades.range, upgradeMaxLevels.range, upgradeCosts.range, 0x3498DB);
var attackSpeedUpgradeContainer = createUpgradeButton('attackSpeed', -50, 'speedst', 'COMBAT SPEED', 'Increase attack frequency', upgrades.attackSpeed, upgradeMaxLevels.attackSpeed, upgradeCosts.attackSpeed, 0x2ECC71);
var staminaUpgradeContainer = createUpgradeButton('stamina', 50, 'staminast', 'ENDURANCE BOOST', 'Increase maximum stamina', upgrades.stamina, upgradeMaxLevels.stamina, upgradeCosts.stamina, 0xF39C12);
var castleHealthUpgradeContainer = createUpgradeButton('castleHealth', 150, 'castlearmor', 'FORTRESS ARMOR', 'Strengthen castle defenses', upgrades.castleHealth, upgradeMaxLevels.castleHealth, upgradeCosts.castleHealth, 0x9B59B6);
var castleAttackUpgradeContainer = createUpgradeButton('castleAttack', 250, 'siegegun', 'SIEGE WEAPONS', 'Activate castle cannons', upgrades.castleAttack, upgradeMaxLevels.castleAttack, upgradeCosts.castleAttack, 0x34495E);
// Update function for all upgrade buttons
function updateAllUpgradeButtons() {
// Update gold display
goldDisplay.setText("Gold: " + gold);
goldDisplayShadow.setText("Gold: " + gold);
// Update damage upgrade
var isMaxLevel = upgrades.damage >= upgradeMaxLevels.damage;
var canAfford = gold >= upgradeCosts.damage;
damageUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0xE74C3C : 0x2C1810;
damageUpgradeContainer.elements.levelText.setText("LV " + upgrades.damage + "/" + upgradeMaxLevels.damage);
damageUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.damage + "/" + upgradeMaxLevels.damage);
damageUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
damageUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.damage.toString());
damageUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.damage.toString());
damageUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update range upgrade
isMaxLevel = upgrades.range >= upgradeMaxLevels.range;
canAfford = gold >= upgradeCosts.range;
rangeUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0x3498DB : 0x2C1810;
rangeUpgradeContainer.elements.levelText.setText("LV " + upgrades.range + "/" + upgradeMaxLevels.range);
rangeUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.range + "/" + upgradeMaxLevels.range);
rangeUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
rangeUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.range.toString());
rangeUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.range.toString());
rangeUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update attack speed upgrade
isMaxLevel = upgrades.attackSpeed >= upgradeMaxLevels.attackSpeed;
canAfford = gold >= upgradeCosts.attackSpeed;
attackSpeedUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0x2ECC71 : 0x2C1810;
attackSpeedUpgradeContainer.elements.levelText.setText("LV " + upgrades.attackSpeed + "/" + upgradeMaxLevels.attackSpeed);
attackSpeedUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.attackSpeed + "/" + upgradeMaxLevels.attackSpeed);
attackSpeedUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
attackSpeedUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.attackSpeed.toString());
attackSpeedUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.attackSpeed.toString());
attackSpeedUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update stamina upgrade
isMaxLevel = upgrades.stamina >= upgradeMaxLevels.stamina;
canAfford = gold >= upgradeCosts.stamina;
staminaUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0xF39C12 : 0x2C1810;
staminaUpgradeContainer.elements.levelText.setText("LV " + upgrades.stamina + "/" + upgradeMaxLevels.stamina);
staminaUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.stamina + "/" + upgradeMaxLevels.stamina);
staminaUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
staminaUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.stamina.toString());
staminaUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.stamina.toString());
staminaUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update castle health upgrade
isMaxLevel = upgrades.castleHealth >= upgradeMaxLevels.castleHealth;
canAfford = gold >= upgradeCosts.castleHealth;
castleHealthUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0x9B59B6 : 0x2C1810;
castleHealthUpgradeContainer.elements.levelText.setText("LV " + upgrades.castleHealth + "/" + upgradeMaxLevels.castleHealth);
castleHealthUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.castleHealth + "/" + upgradeMaxLevels.castleHealth);
castleHealthUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
castleHealthUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.castleHealth.toString());
castleHealthUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.castleHealth.toString());
castleHealthUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
// Update castle attack upgrade
isMaxLevel = upgrades.castleAttack >= upgradeMaxLevels.castleAttack;
canAfford = gold >= upgradeCosts.castleAttack;
castleAttackUpgradeContainer.elements.mainBg.tint = isMaxLevel ? 0x4A4A4A : canAfford ? 0x34495E : 0x2C1810;
castleAttackUpgradeContainer.elements.levelText.setText("LV " + upgrades.castleAttack + "/" + upgradeMaxLevels.castleAttack);
castleAttackUpgradeContainer.elements.levelShadow.setText("LV " + upgrades.castleAttack + "/" + upgradeMaxLevels.castleAttack);
castleAttackUpgradeContainer.elements.levelText.tint = isMaxLevel ? 0xFFD700 : 0x74B9FF;
castleAttackUpgradeContainer.elements.costText.setText(isMaxLevel ? "MAX" : upgradeCosts.castleAttack.toString());
castleAttackUpgradeContainer.elements.costShadow.setText(isMaxLevel ? "MAX" : upgradeCosts.castleAttack.toString());
castleAttackUpgradeContainer.elements.costBg.tint = isMaxLevel ? 0x555555 : canAfford ? 0x8B6914 : 0x8B0000;
}
// Call update function initially
updateAllUpgradeButtons();
// Unified upgrade handler function
function handleUpgrade(upgradeType, upgradeContainer) {
var currentLevel = upgrades[upgradeType];
var maxLevel = upgradeMaxLevels[upgradeType];
var cost = upgradeCosts[upgradeType];
if (currentLevel >= maxLevel) {
var notification = game.addChild(new Notification(upgradeType.charAt(0).toUpperCase() + upgradeType.slice(1) + " upgrade maxed out!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (gold >= cost) {
// Add satisfying purchase animation
tween(upgradeContainer, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(upgradeContainer, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 100,
easing: tween.easeIn
});
}
});
setGold(gold - cost);
upgrades[upgradeType]++;
storage[upgradeType + 'Upgrades'] = upgrades[upgradeType];
// Apply upgrades based on type
switch (upgradeType) {
case 'damage':
if (player) player.attackDamage += 5;
var notification = game.addChild(new Notification("⚔ DAMAGE BOOST! +" + upgrades.damage * 5 + " total damage"));
break;
case 'range':
if (player) player.attackRange += 0.3 * CELL_SIZE;
var notification = game.addChild(new Notification("🎯 RANGE EXTENDED! +" + (upgrades.range * 0.3).toFixed(1) + " range"));
break;
case 'attackSpeed':
if (player) player.attackCooldownMax = Math.max(5, player.attackCooldownMax - 9);
var notification = game.addChild(new Notification("⚡ SPEED BOOST! Faster attacks"));
break;
case 'stamina':
if (player) {
player.maxStamina += 20;
player.stamina = Math.min(player.stamina + 20, player.maxStamina);
}
var notification = game.addChild(new Notification("💪 ENDURANCE UP! +" + upgrades.stamina * 20 + " max stamina"));
break;
case 'castleHealth':
if (baseCastle) {
baseCastle.maxHealth += 30;
baseCastle.health = Math.min(baseCastle.health + 30, baseCastle.maxHealth);
}
var notification = game.addChild(new Notification("🏰 FORTRESS STRENGTHENED! +" + upgrades.castleHealth * 30 + " max health"));
break;
case 'castleAttack':
if (baseCastle) {
baseCastle.canAttack = true;
baseCastle.attackDamage = 20;
baseCastle.attackRange = CELL_SIZE * 4;
baseCastle.fireRate = 120;
}
var notification = game.addChild(new Notification("⚔ SIEGE WEAPONS ACTIVATED!"));
break;
}
notification.x = 2048 / 2;
notification.y = grid.height - 50;
updateAllUpgradeButtons();
} else {
var notification = game.addChild(new Notification("💰 Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
// Assign handlers to all upgrade containers
damageUpgradeContainer.down = function () {
handleUpgrade('damage', this);
};
rangeUpgradeContainer.down = function () {
handleUpgrade('range', this);
};
attackSpeedUpgradeContainer.down = function () {
handleUpgrade('attackSpeed', this);
};
staminaUpgradeContainer.down = function () {
handleUpgrade('stamina', this);
};
castleHealthUpgradeContainer.down = function () {
handleUpgrade('castleHealth', this);
};
castleAttackUpgradeContainer.down = function () {
handleUpgrade('castleAttack', this);
};
// Range upgrade - using proper elements structure
// (This duplicate range upgrade container is removed as it's already created by createUpgradeButton above)
// Old individual upgrade containers removed - now using unified premium upgrade system
// Close shop when clicking on background
shopBackground.down = function () {
shopInterface.destroy();
};
}
// Create kill count UI display
var killCountContainer = new Container();
LK.gui.topRight.addChild(killCountContainer);
// Position container below hero stats area
killCountContainer.x = -40; // Same X position as hero stats
killCountContainer.y = 320; // Position below hero stats area
// Create main background for kill count UI
var killCountBackground = killCountContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
killCountBackground.width = 300; // Compact width for kill count
killCountBackground.height = 120; // Compact height
killCountBackground.tint = 0x8B0000; // Dark red background for combat theme
killCountBackground.alpha = 0.95;
// Create decorative border
var killCountBorder = killCountContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
killCountBorder.width = 310;
killCountBorder.height = 130;
killCountBorder.tint = 0x2C3E50; // Dark border
killCountBorder.alpha = 0.9;
killCountBorder.x = 5;
killCountBorder.y = -5;
// Create title background
var killCountTitleBg = killCountContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
killCountTitleBg.width = 280;
killCountTitleBg.height = 40;
killCountTitleBg.tint = 0x2D3436; // Dark gray for title
killCountTitleBg.alpha = 0.98;
killCountTitleBg.x = -10;
killCountTitleBg.y = 10;
// Add title with shadow effect
var killCountTitleShadow = new Text2('💀 KILLS 💀', {
size: 28,
fill: 0x000000,
weight: 900
});
killCountTitleShadow.anchor.set(1.0, 0.5);
killCountTitleShadow.x = -25;
killCountTitleShadow.y = 32;
killCountContainer.addChild(killCountTitleShadow);
var killCountTitle = new Text2('💀 KILLS 💀', {
size: 28,
fill: 0xFF6B6B,
// Red color for kills title
weight: 900
});
killCountTitle.anchor.set(1.0, 0.5);
killCountTitle.x = -22;
killCountTitle.y = 30;
killCountContainer.addChild(killCountTitle);
// Add kill count value with shadow
var killCountValueShadow = new Text2('0', {
size: 48,
fill: 0x000000,
weight: 900
});
killCountValueShadow.anchor.set(1.0, 0.5);
killCountValueShadow.x = -22;
killCountValueShadow.y = 82;
killCountContainer.addChild(killCountValueShadow);
var killCountValue = new Text2('0', {
size: 48,
fill: 0xFFD700,
// Gold color for the count
weight: 900
});
killCountValue.anchor.set(1.0, 0.5);
killCountValue.x = -20;
killCountValue.y = 80;
killCountContainer.addChild(killCountValue);
// Hide kill count UI initially
killCountContainer.visible = false;
// Create hero stats display with enhanced aesthetic design and better positioning
var heroStatsContainer = new Container();
LK.gui.topRight.addChild(heroStatsContainer);
// Position container away from road and with proper spacing
heroStatsContainer.x = -40; // Further from right edge to avoid road overlap
heroStatsContainer.y = -6; // Moved 0.7cm lower (-33 + 27 = -6) for better positioning
// Create main background with better design to avoid road overlap
var statsBackground = heroStatsContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
statsBackground.width = 380; // Slightly narrower to avoid road
statsBackground.height = 300; // Optimal height
statsBackground.tint = 0x1A252F; // Better contrasting dark background
statsBackground.alpha = 0.95;
// Create decorative border with better visibility
var borderBackground = heroStatsContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
borderBackground.width = 390;
borderBackground.height = 310;
borderBackground.tint = 0x52796F; // Darker sage green border
borderBackground.alpha = 0.9;
borderBackground.x = 5;
borderBackground.y = -5;
// Create title background with better contrast
var titleBackground = heroStatsContainer.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0
});
titleBackground.width = 360; // Better proportions
titleBackground.height = 55; // Optimal title bar height
titleBackground.tint = 0x2D3436; // Better contrast dark gray
titleBackground.alpha = 0.98;
titleBackground.x = -10;
titleBackground.y = 10;
// Add title with enhanced shadow effect
var titleShadow = new Text2('⚔ HERO STATUS ⚔', {
// Enhanced title with symbols
size: 34,
fill: 0x000000,
weight: 900
});
titleShadow.anchor.set(1.0, 0.5);
titleShadow.x = -25;
titleShadow.y = 39; // Centered in title bar
heroStatsContainer.addChild(titleShadow);
var titleText = new Text2('⚔ HERO STATUS ⚔', {
size: 34,
fill: 0xFDCB6E,
// Better golden color for title
weight: 900
});
titleText.anchor.set(1.0, 0.5);
titleText.x = -22;
titleText.y = 37; // Centered in title bar
heroStatsContainer.addChild(titleText);
// Create enhanced stat rows with better spacing and design
var statRows = [];
var statIcons = ['attackst', 'rangest', 'speedst', 'staminast'];
var statYPositions = [85, 135, 185, 235]; // Better compact spacing
var statLabels = ['DAMAGE', 'RANGE', 'SPEED', 'STAMINA']; // Full descriptive names
for (var i = 0; i < 4; i++) {
var statRow = new Container();
heroStatsContainer.addChild(statRow);
statRow.y = statYPositions[i];
statRows.push(statRow);
// Add subtle glow background for each stat row
var statGlow = statRow.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0.5
});
statGlow.width = 355;
statGlow.height = 42;
statGlow.tint = 0x636E72; // Better neutral glow
statGlow.alpha = 0.25;
statGlow.x = -12;
statGlow.y = 0;
// Add main stat background with better design
var statBg = statRow.attachAsset('notification', {
anchorX: 1.0,
anchorY: 0.5
});
statBg.width = 350;
statBg.height = 40;
statBg.tint = 0x2D3436; // Consistent with title
statBg.alpha = 0.9;
statBg.x = -15;
statBg.y = 0;
// Add icon background circle for better contrast
var iconBg = statRow.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
iconBg.width = 44;
iconBg.height = 44;
iconBg.tint = 0x74B9FF; // Vibrant blue background for icons
iconBg.alpha = 0.85;
iconBg.x = -45; // Better positioning for icon background
iconBg.y = 0;
// Add stat icon with enhanced positioning and proper coloring
var statIcon = statRow.attachAsset(statIcons[i], {
anchorX: 0.5,
anchorY: 0.5
});
statIcon.width = 36; // Proper icon size
statIcon.height = 36;
statIcon.x = -45; // Centered on icon background
statIcon.y = 0;
statIcon.tint = 0xFFFFFF; // Make icons white/visible instead of transparent
statIcon.alpha = 1.0; // Full opacity
// Add stat label with enhanced shadow
var statLabelShadow = new Text2(statLabels[i], {
size: 28,
fill: 0x000000,
weight: 800
});
statLabelShadow.anchor.set(1.0, 0.5);
statLabelShadow.x = -250;
statLabelShadow.y = 2;
statRow.addChild(statLabelShadow);
// Add stat label with better styling and colors
var statLabel = new Text2(statLabels[i], {
size: 28,
fill: 0xFFFFFF,
// White text for better visibility
weight: 800
});
statLabel.anchor.set(1.0, 0.5);
statLabel.x = -248;
statLabel.y = 0;
statRow.addChild(statLabel);
// Add stat value shadow with offset
var statValueShadow = new Text2('', {
size: 32,
fill: 0x000000,
weight: 900
});
statValueShadow.anchor.set(1.0, 0.5);
statValueShadow.x = -82;
statValueShadow.y = 2;
statRow.addChild(statValueShadow);
// Add stat value with color coding
var statValue = new Text2('', {
size: 32,
fill: 0xCCCCCC,
// Light gray for stat values
weight: 900
});
statValue.anchor.set(1.0, 0.5);
statValue.x = -80;
statValue.y = 0;
statRow.addChild(statValue);
}
// Update hero stats display with enhanced formatting and effects
function updateHeroStats() {
if (player && statRows.length >= 4) {
// Calculate stats with proper bounds checking and safe conversion
var damage = Math.max(0, Math.round(player.attackDamage || 0));
var range = Math.max(0, (player.attackRange || 0) / CELL_SIZE).toFixed(1);
var attacksPerSecond = Math.max(0, 60 / Math.max(1, player.attackCooldownMax || 1)).toFixed(1);
var stamina = Math.max(0, Math.round(player.stamina || 0));
var maxStamina = Math.max(1, Math.round(player.maxStamina || 1));
var staminaPercent = stamina / maxStamina;
// Enhanced stat values with consistent formatting and safe string conversion
var statValues = [damage.toString(), range.toString() + ' tiles', attacksPerSecond.toString() + '/sec', stamina.toString() + '/' + maxStamina.toString()];
// Use bright, consistent colors for all stats with better visibility
var statColors = [0xFFD700,
// Damage - bright gold
0x00BFFF,
// Range - bright blue
0x32CD32,
// Speed - bright green
0xFF6347 // Stamina - bright orange-red
];
var statLabels = ['DAMAGE', 'RANGE', 'SPEED', 'STAMINA'];
for (var i = 0; i < 4; i++) {
if (statRows[i] && statRows[i].children && statRows[i].children.length >= 8) {
// Ensure all text objects exist before updating
var labelShadow = statRows[i].children[4];
var label = statRows[i].children[5];
var valueShadow = statRows[i].children[6];
var value = statRows[i].children[7];
// Update label shadow (child index 4 in new layout) with consistent formatting
if (labelShadow && labelShadow.setText && statLabels[i]) {
labelShadow.setText(statLabels[i]);
labelShadow.tint = 0x000000;
labelShadow.alpha = 1.0;
}
// Update label (child index 5) with bright white text
if (label && label.setText && statLabels[i]) {
label.setText(statLabels[i]);
label.tint = 0xFFFFFF;
label.alpha = 1.0;
}
// Update value shadow (child index 6) with consistent black shadow
if (valueShadow && valueShadow.setText && statValues[i]) {
valueShadow.setText(statValues[i]);
valueShadow.tint = 0x000000;
valueShadow.alpha = 1.0;
}
// Update value with bright color coding (child index 7)
if (value && value.setText && statValues[i]) {
value.setText(statValues[i]);
value.tint = statColors[i];
value.alpha = 1.0;
}
// Add special visual effects for low stamina
if (i === 3 && staminaPercent < 0.3) {
// Flash stamina when critically low
if (LK.ticks % 60 < 30) {
// Flash every second
if (value) value.alpha = 0.6;
if (valueShadow) valueShadow.alpha = 0.6;
} else {
if (value) value.alpha = 1.0;
if (valueShadow) valueShadow.alpha = 1.0;
}
} else {
if (value) value.alpha = 1.0;
if (valueShadow) valueShadow.alpha = 1.0;
}
}
}
}
}
// Hide stats initially
heroStatsContainer.visible = false;
// Create base health UI display with heart icon
var baseHealthContainer = new Container();
LK.gui.topLeft.addChild(baseHealthContainer);
// Position container away from the top-left platform menu icon (avoid 100x100 px area)
baseHealthContainer.x = 120; // 120px from left edge to avoid platform menu
baseHealthContainer.y = 20; // 20px from top edge
// Create main background for base health UI
var baseHealthBackground = baseHealthContainer.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
baseHealthBackground.width = 280; // Compact width
baseHealthBackground.height = 80; // Compact height
baseHealthBackground.tint = 0x2C3E50; // Dark blue-gray background
baseHealthBackground.alpha = 0.9;
// Create decorative border
var baseHealthBorder = baseHealthContainer.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
baseHealthBorder.width = 290;
baseHealthBorder.height = 90;
baseHealthBorder.tint = 0x8B0000; // Dark red border for castle theme
baseHealthBorder.alpha = 0.8;
baseHealthBorder.x = -5;
baseHealthBorder.y = -5;
// Add health UI icon
var heartIcon = baseHealthContainer.attachAsset('healthui', {
anchorX: 0.5,
anchorY: 0.5
});
heartIcon.width = 50; // Appropriate size for compact UI
heartIcon.height = 50;
heartIcon.tint = 0xFF0000; // Red heart color
heartIcon.x = 35; // Position near left side
heartIcon.y = 40; // Center vertically
// Add castle health text with shadow
var baseHealthTextShadow = new Text2('', {
size: 32,
fill: 0x000000,
weight: 800
});
baseHealthTextShadow.anchor.set(0, 0.5);
baseHealthTextShadow.x = 72; // Position after heart icon + margin
baseHealthTextShadow.y = 42; // Slightly offset for shadow effect
baseHealthContainer.addChild(baseHealthTextShadow);
// Add main castle health text
var baseHealthText = new Text2('', {
size: 32,
fill: 0xFFFFFF,
weight: 800
});
baseHealthText.anchor.set(0, 0.5);
baseHealthText.x = 70; // Position after heart icon + margin
baseHealthText.y = 40; // Center vertically
baseHealthContainer.addChild(baseHealthText);
// Function to update kill count display
function updateKillCount() {
if (killCountValue && killCountValueShadow) {
var killText = killCount.toString();
killCountValue.setText(killText);
killCountValueShadow.setText(killText);
// Change color based on kill count milestones
if (killCount >= 100) {
killCountValue.tint = 0xFF1744; // Bright red for high kills
} else if (killCount >= 50) {
killCountValue.tint = 0xFF5722; // Orange-red for medium kills
} else if (killCount >= 25) {
killCountValue.tint = 0xFFD700; // Gold for decent kills
} else {
killCountValue.tint = 0xFFFFFF; // White for low kills
}
}
}
// Function to update base health display
function updateBaseHealth() {
if (baseCastle && baseHealthText && baseHealthTextShadow) {
var healthText = baseCastle.health + '/' + baseCastle.maxHealth;
baseHealthText.setText(healthText);
baseHealthTextShadow.setText(healthText);
// Change heart color based on health percentage
var healthPercent = baseCastle.health / baseCastle.maxHealth;
if (healthPercent > 0.6) {
heartIcon.tint = 0xFF0000; // Red for healthy
} else if (healthPercent > 0.3) {
heartIcon.tint = 0xFF6600; // Orange for damaged
} else {
heartIcon.tint = 0x990000; // Dark red for critical
}
// Change text color based on health
if (healthPercent > 0.6) {
baseHealthText.tint = 0x00FF00; // Green for healthy
} else if (healthPercent > 0.3) {
baseHealthText.tint = 0xFFFF00; // Yellow for damaged
} else {
baseHealthText.tint = 0xFF0000; // Red for critical
}
}
}
// Hide base health UI initially
baseHealthContainer.visible = false;
function initializeGameElements() {
// Show previously hidden UI elements
// waveIndicator.visible = true; // Removed - keep wave indicator hidden
nextWaveButton.visible = true;
shopButton.visible = true; // Show shop button when game starts
towerLayer.visible = true;
enemyLayer.visible = true;
goldImage.visible = true;
goldText.visible = true;
heroStatsContainer.visible = true; // Show hero stats when game starts
baseHealthContainer.visible = true; // Show base health when game starts
killCountContainer.visible = true; // Show kill count when game starts
// Reset kill count for new game
killCount = 0;
updateKillCount();
// Update gold text to show current gold amount
updateUI();
// Show source towers
for (var i = 0; i < sourceTowers.length; i++) {
sourceTowers[i].visible = true;
}
// Show player character
if (player) {
player.visible = true;
// Reset player stats to base values only (no upgrades applied)
player.attackDamage = 15; // Base damage
player.attackRange = CELL_SIZE * 1.0; // Base range
player.attackCooldownMax = 30; // Base cooldown
// No upgrades applied since they are all reset to 0
}
// Show spawn point
if (spawnPoint) {
spawnPoint.visible = true;
}
// Show base castle
if (baseCastle) {
baseCastle.visible = true;
}
// Initialize hero stats display
updateHeroStats();
// Any other game initialization that should happen when starting
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
// Initialize upgrade system with storage - reset everything to 0
var upgrades = {
damage: 0,
range: 0,
attackSpeed: 0,
stamina: 0,
castleHealth: 0,
castleAttack: 0
};
// Clear all stored upgrade values
storage.damageUpgrades = 0;
storage.rangeUpgrades = 0;
storage.attackSpeedUpgrades = 0;
storage.staminaUpgrades = 0;
storage.castleHealthUpgrades = 0;
storage.castleAttackUpgrades = 0;
// Function to reset game state for new games
function resetGameState() {
// Reset upgrades to zero and clear storage
upgrades = {
damage: 0,
range: 0,
attackSpeed: 0,
stamina: 0,
castleHealth: 0,
castleAttack: 0
};
// Clear all stored upgrade values
storage.damageUpgrades = 0;
storage.rangeUpgrades = 0;
storage.attackSpeedUpgrades = 0;
storage.staminaUpgrades = 0;
storage.castleHealthUpgrades = 0;
storage.castleAttackUpgrades = 0;
// Reset other game variables
gameStarted = false;
currentWave = 0;
killCount = 0; // Reset kill count
// Hide base health UI when resetting
if (baseHealthContainer) {
baseHealthContainer.visible = false;
}
// Hide kill count UI when resetting
if (killCountContainer) {
killCountContainer.visible = false;
}
waveTimer = 0;
waveInProgress = false;
waveSpawned = false;
enemies = [];
towers = [];
bullets = [];
gold = 60;
selectedTower = null;
}
// Upgrade costs and limits
var upgradeCosts = {
damage: 40,
range: 50,
attackSpeed: 40,
stamina: 50,
castleHealth: 35,
castleAttack: 70
};
var upgradeMaxLevels = {
damage: 3,
range: 3,
attackSpeed: 3,
stamina: 3,
castleHealth: 3,
castleAttack: 1
};
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
var gold = 60;
var lives = 20;
var score = 0;
var killCount = 0; // Track total kills
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 3000;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var goldImage = LK.getAsset('gold', {
anchorX: 0.5,
anchorY: 0.5
});
goldImage.visible = false; // Hide gold image initially
LK.gui.left.addChild(goldImage);
goldImage.x = 38; // Move 1 cm right (1cm * 37.8 pixels/cm)
goldImage.y = -378; // Move gold UI 10cm upper (0 - 378 pixels)
var goldText = new Text2('0', {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0, 0.5);
goldText.visible = false; // Hide gold text initially
LK.gui.left.addChild(goldText);
goldText.x = goldImage.x + goldImage.width / 2 + 10; // Position next to gold image with small gap
goldText.y = -378; // Match gold image y position (10cm upper)
function updateUI() {
goldText.setText(gold.toString());
}
function setGold(value) {
gold = value;
updateUI();
// Initialize base health display
updateBaseHealth();
}
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
// Create road path from spawn to base
function createRoadPath() {
// Clear existing path cells (set them to grass/floor)
for (var i = 0; i < grid.cells.length; i++) {
for (var j = 0; j < grid.cells[0].length; j++) {
var cell = grid.cells[i][j];
if (cell.type === 0) {
// Convert floor cells to walls first
cell.type = 1;
}
}
}
// Define the maze-like road path coordinates with longer turns
var roadPath = [
// Start from spawn point (column 11) - extended downward
{
x: 11,
y: 0
}, {
x: 11,
y: 1
}, {
x: 11,
y: 2
}, {
x: 11,
y: 3
}, {
x: 11,
y: 4
}, {
x: 11,
y: 5
}, {
x: 11,
y: 6
}, {
x: 11,
y: 7
}, {
x: 11,
y: 8
},
// First turn right - extended rightward
{
x: 12,
y: 8
}, {
x: 13,
y: 8
}, {
x: 14,
y: 8
}, {
x: 15,
y: 8
}, {
x: 16,
y: 8
}, {
x: 17,
y: 8
},
// Turn down - extended downward
{
x: 17,
y: 9
}, {
x: 17,
y: 10
}, {
x: 17,
y: 11
}, {
x: 17,
y: 12
}, {
x: 17,
y: 13
}, {
x: 17,
y: 14
},
// Turn left - extended leftward
{
x: 16,
y: 14
}, {
x: 15,
y: 14
}, {
x: 14,
y: 14
}, {
x: 13,
y: 14
}, {
x: 12,
y: 14
}, {
x: 11,
y: 14
}, {
x: 10,
y: 14
}, {
x: 9,
y: 14
}, {
x: 8,
y: 14
}, {
x: 7,
y: 14
},
// Turn down - extended downward
{
x: 7,
y: 15
}, {
x: 7,
y: 16
}, {
x: 7,
y: 17
}, {
x: 7,
y: 18
}, {
x: 7,
y: 19
},
// Turn right - extended rightward
{
x: 8,
y: 19
}, {
x: 9,
y: 19
}, {
x: 10,
y: 19
}, {
x: 11,
y: 19
}, {
x: 12,
y: 19
}, {
x: 13,
y: 19
}, {
x: 14,
y: 19
}, {
x: 15,
y: 19
}, {
x: 16,
y: 19
}, {
x: 17,
y: 19
}, {
x: 18,
y: 19
},
// Turn down - extended downward
{
x: 18,
y: 20
}, {
x: 18,
y: 21
}, {
x: 18,
y: 22
}, {
x: 18,
y: 23
}, {
x: 18,
y: 24
},
// Turn left - extended leftward
{
x: 17,
y: 24
}, {
x: 16,
y: 24
}, {
x: 15,
y: 24
}, {
x: 14,
y: 24
}, {
x: 13,
y: 24
}, {
x: 12,
y: 24
},
// Turn down - extended downward
{
x: 12,
y: 25
}, {
x: 12,
y: 26
}, {
x: 12,
y: 27
},
// Turn right towards base - extended rightward
{
x: 13,
y: 27
}, {
x: 14,
y: 27
}, {
x: 15,
y: 27
}, {
x: 16,
y: 27
}, {
x: 17,
y: 27
}, {
x: 18,
y: 27
}, {
x: 19,
y: 27
}, {
x: 20,
y: 27
}, {
x: 21,
y: 27
}];
// Set road tiles along the path
for (var i = 0; i < roadPath.length; i++) {
var pathPoint = roadPath[i];
var cell = grid.getCell(pathPoint.x, pathPoint.y);
if (cell) {
cell.type = 0; // Set as passable road
}
}
// Ensure spawn and goal areas remain correct
for (var i = 11 - 3; i <= 11 + 3; i++) {
var spawnCell = grid.getCell(i, 0);
if (spawnCell) {
spawnCell.type = 2; // Spawn
}
}
// Set castle area as goal (2x2 area around castle position)
var castleGridX = grid.cells.length - 3; // 2 cells from right edge
var castleGridY = grid.cells[0].length - 8; // 7 cells up from bottom
for (var ci = 0; ci < 2; ci++) {
for (var cj = 0; cj < 2; cj++) {
var castleCell = grid.getCell(castleGridX + ci, castleGridY + cj);
if (castleCell) {
castleCell.type = 3; // Set as goal
}
}
}
}
// Create the road path
createRoadPath();
grid.pathFind();
grid.renderDebug();
debugLayer.addChild(grid);
debugLayer.visible = true; // Show the debug layer so road tiles are visible
game.addChild(debugLayer);
towerLayer.visible = false; // Hide tower layer initially
enemyLayer.visible = false; // Hide enemy layer initially
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
function wouldBlockPath(gridX, gridY) {
// First check if any cell in the 2x2 tower footprint is a road tile (type 0)
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) {
// Cannot build on road tiles
return true;
}
}
}
// Check if any existing tower overlaps with the proposed tower position
for (var t = 0; t < towers.length; t++) {
var existingTower = towers[t];
var existingGridX = existingTower.gridX;
var existingGridY = existingTower.gridY;
// Check if the 2x2 footprints overlap
var overlapX = !(gridX + 2 <= existingGridX || gridX >= existingGridX + 2);
var overlapY = !(gridY + 2 <= existingGridY || gridY >= existingGridY + 2);
if (overlapX && overlapY) {
// Towers would overlap
return true;
}
}
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 = 10;
switch (towerType) {
case 'rapid':
cost = 20;
break;
case 'sniper':
cost = 30;
break;
case 'splash':
cost = 40;
break;
case 'slow':
cost = 50;
break;
case 'poison':
cost = 60;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
var tower = new Tower(towerType || 'default');
tower.placeOnGrid(gridX, gridY);
towerLayer.addChild(tower);
towers.push(tower);
setGold(gold - towerCost);
grid.pathFind();
grid.renderDebug();
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
var draggedTower = null;
var dragStartTime = 0;
game.down = function (x, y, obj) {
if (!gameStarted) {
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Record drag start time for distinguishing between drag and click
dragStartTime = LK.ticks;
// Handle contextual player actions based on distance
if (player && player.visible) {
// Check if clicking in a valid game area (not on UI elements)
var inGameArea = x >= grid.x && x <= grid.x + grid.cells.length * CELL_SIZE && y >= grid.y + CELL_SIZE * 4 && y <= grid.y + (grid.cells[0].length - 4) * CELL_SIZE;
if (inGameArea) {
// Calculate distance from player to tap location
var dx = x - player.x;
var dy = y - player.y;
var distanceToTap = Math.sqrt(dx * dx + dy * dy);
// If tapping close to hero (within attack range): attack
if (distanceToTap <= player.attackRange) {
// Trigger attack
if (player.attack()) {
// Attack successful - show feedback
var notification = game.addChild(new Notification("Slash attack!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
// Attack failed - not enough stamina
var notification = game.addChild(new Notification("Not enough stamina!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else {
// If tapping far from hero: move to that location
player.moveTo(x, y);
}
}
}
// Check if clicking on existing towers (no dragging allowed - towers are permanent)
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) {
// Store the clicked tower for upgrade menu only (no dragging)
draggedTower = tower;
return; // Exit early to prevent other interactions
}
}
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 player can afford this tower type first
if (gold >= getTowerCost(tower.towerType)) {
// Initialize dragging state
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.visible = true;
// Position the preview at the initial drag position and update its status
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
towerPreview.updatePlacementStatus();
} else {
// Show notification if can't afford
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
break;
}
}
};
game.move = function (x, y, obj) {
if (!gameStarted) {
// Handle main menu hover effects when game hasn't started
if (mainMenuContainer && mainMenuContainer.visible) {
// Get button references from the main menu container
var startButton = mainMenuContainer.children[2]; // third child after backgrounds
var optionsButton = mainMenuContainer.children[3]; // fourth child
var storyButton = mainMenuContainer.children[4]; // fifth child
var startText = startButton.children[1]; // text is second child after background
var optionsText = optionsButton.children[1]; // text is second child after background
var storyText = storyButton.children[1]; // text is second child after background
var hoverEffect = startButton.children[2]; // hover effect is third child
var optionsHoverEffect = optionsButton.children[2]; // hover effect is third child
var storyHoverEffect = storyButton.children[2]; // hover effect is third child
// Calculate start button bounds more precisely - use button background size
var buttonLeft = startButton.x - 175; // half of button background width (350/2)
var buttonRight = startButton.x + 175;
var buttonTop = startButton.y - 60; // half of button background height (120/2)
var buttonBottom = startButton.y + 60;
// Check if mouse is within start button bounds
var mouseInButton = x >= buttonLeft && x <= buttonRight && y >= buttonTop && y <= buttonBottom;
// Only show hover effect when mouse is precisely over the button
hoverEffect.visible = mouseInButton;
// Animate text color change with tween
if (mouseInButton) {
// Stop any existing tween on the text
tween.stop(startText, {
tint: true
});
// Tween to red color
tween(startText, {
tint: 0xFF0000
}, {
duration: 200,
easing: tween.easeOut
});
} else {
// Stop any existing tween on the text
tween.stop(startText, {
tint: true
});
// Tween back to white color
tween(startText, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
// Calculate options button bounds more precisely
var optionsButtonLeft = optionsButton.x - 175;
var optionsButtonRight = optionsButton.x + 175;
var optionsButtonTop = optionsButton.y - 60;
var optionsButtonBottom = optionsButton.y + 60;
// Check if mouse is within options button bounds
var mouseInOptionsButton = x >= optionsButtonLeft && x <= optionsButtonRight && y >= optionsButtonTop && y <= optionsButtonBottom;
// Only show hover effect when mouse is precisely over the options button
optionsHoverEffect.visible = mouseInOptionsButton;
// Calculate story button bounds more precisely
var storyButtonLeft = storyButton.x - 175;
var storyButtonRight = storyButton.x + 175;
var storyButtonTop = storyButton.y - 60;
var storyButtonBottom = storyButton.y + 60;
// Check if mouse is within story button bounds
var mouseInStoryButton = x >= storyButtonLeft && x <= storyButtonRight && y >= storyButtonTop && y <= storyButtonBottom;
// Only show hover effect when mouse is precisely over the story button
storyHoverEffect.visible = mouseInStoryButton;
// Animate options text color change with tween
if (mouseInOptionsButton) {
// Stop any existing tween on the options text
tween.stop(optionsText, {
tint: true
});
// Tween to green color
tween(optionsText, {
tint: 0x00FF00
}, {
duration: 200,
easing: tween.easeOut
});
} else {
// Stop any existing tween on the options text
tween.stop(optionsText, {
tint: true
});
// Tween back to white color
tween(optionsText, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
// Animate story text color change with tween
if (mouseInStoryButton) {
// Stop any existing tween on the story text
tween.stop(storyText, {
tint: true
});
// Tween to blue color
tween(storyText, {
tint: 0x4444FF
}, {
duration: 200,
easing: tween.easeOut
});
} else {
// Stop any existing tween on the story text
tween.stop(storyText, {
tint: true
});
// Tween back to white color
tween(storyText, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
} else {
// Hide hover effect when not in main menu - but check if elements exist first
if (mainMenuContainer && mainMenuContainer.children.length > 2) {
var startButton = mainMenuContainer.children[2];
var optionsButton = mainMenuContainer.children[3];
var storyButton = mainMenuContainer.children[4];
if (startButton && startButton.children.length > 2) {
startButton.children[2].visible = false; // hoverEffect
}
if (optionsButton && optionsButton.children.length > 2) {
optionsButton.children[2].visible = false; // optionsHoverEffect
}
if (storyButton && storyButton.children.length > 2) {
storyButton.children[2].visible = false; // storyHoverEffect
}
}
}
return;
}
// Handle tower dragging when game is started
if (isDragging && towerPreview.visible) {
// Shift the y position upward by 1.5 tiles to show preview above finger
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
// Force update the preview appearance after position change
towerPreview.updatePlacementStatus();
}
};
game.up = function (x, y, obj) {
if (!gameStarted) {
return;
}
var dragDuration = LK.ticks - dragStartTime;
var wasQuickClick = dragDuration < 10; // Less than ~1/6 second is considered a click
var clickedOnTower = false;
var clickedTower = null;
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;
clickedTower = tower;
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
// Only allow placing new towers from source towers (no tower repositioning)
if (!draggedTower && towerPreview.canPlace && towerPreview.hasEnoughGold) {
// This is placing a new tower from source towers
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else if (!towerPreview.hasEnoughGold) {
var notification = game.addChild(new Notification("Not enough gold!"));
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;
// For existing towers, show upgrade menu on quick click
if (draggedTower && wasQuickClick) {
clickedTower = draggedTower;
clickedOnTower = true;
}
draggedTower = null;
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
// Handle tower click for upgrade menu (only if it was a quick click)
if (clickedOnTower && clickedTower && wasQuickClick) {
// Trigger the tower's down method to show upgrade menu
clickedTower.down(x, y, obj);
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.visible = false; // Hide the wave indicator UI - permanently hidden
game.addChild(waveIndicator);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.visible = false; // Hide the next wave button
nextWaveButton.x = 189; // Move 5cm right (5cm * 37.8 pixels/cm)
nextWaveButton.y = 378; // Move 10cm down (10cm * 37.8 pixels/cm)
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
// Create shop button 3cm below next wave button
var shopContainer = new Container();
var shopButton = new Container();
var shopGraphics = shopButton.attachAsset('shop', {
anchorX: 0.5,
anchorY: 0.5
});
shopButton.x = 189; // Same X position as next wave button
shopButton.y = 378 + 113 + 113; // 6cm total below next wave button (6cm * 37.8 pixels/cm = 226 pixels)
shopButton.visible = false; // Hide initially until game starts
shopButton.down = function () {
if (!gameStarted) {
return;
}
// Show shop interface
showShopInterface();
};
shopContainer.addChild(shopButton);
game.addChild(shopContainer);
// Start playing main menu music immediately
LK.playMusic('MainMenuSt');
// Create main menu when game loads
createMainMenu();
var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
tower.visible = false; // Hide source towers initially
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
// Create spawn point image at the enemy spawn location
var spawnPoint = LK.getAsset('spawnpoint', {
anchorX: 0.5,
anchorY: 0.5
});
spawnPoint.x = grid.x + 11 * CELL_SIZE + CELL_SIZE / 2 - 38; // Center of spawn area (middle of 6 tiles), moved 1cm left (38 pixels)
spawnPoint.y = grid.y + 264 - 76; // Top of the grid where enemies spawn, moved 7cm (264 pixels) lower, then 2cm up (76 pixels)
spawnPoint.visible = false; // Hide initially until game starts
game.addChild(spawnPoint);
// Create base castle - positioned 7cm (264 pixels) up from bottom right
var baseCastle = new BaseCastle();
baseCastle.x = grid.x + (grid.cells.length - 2) * CELL_SIZE; // 2 cells from right edge
baseCastle.y = grid.y + (grid.cells[0].length - 7) * CELL_SIZE; // 7 cells up from bottom
baseCastle.visible = false; // Hide initially until game starts
game.addChild(baseCastle);
// Create player character
var player = new Player();
player.x = grid.x + grid.cells.length * CELL_SIZE / 2; // Center X of the map
player.y = grid.y + grid.cells[0].length * CELL_SIZE / 2; // Center Y of the map
player.visible = false; // Hide initially until game starts
game.addChild(player);
game.update = function () {
if (!gameStarted) {
return;
}
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);
// Apply difficulty scaling to enemy count
if (game.selectedDifficulty) {
var enemyCountMultiplier = 1.0;
switch (game.selectedDifficulty) {
case 'EASY':
enemyCountMultiplier = 0.7; // Easy - 30% fewer enemies (reduced difficulty)
break;
case 'MEDIUM':
enemyCountMultiplier = 1.1; // Medium - 10% more enemies (reduced difficulty)
break;
case 'HARD':
enemyCountMultiplier = 1.4; // Hard - 40% more enemies (reduced difficulty)
break;
case 'EXTREME':
enemyCountMultiplier = 1.8; // Extreme - 80% more enemies (reduced difficulty)
break;
}
enemyCount = Math.max(1, Math.round(enemyCount * enemyCountMultiplier));
}
// 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;
}
// Initialize enemy spawn tracking if not already set
if (!game.enemySpawnQueue) {
game.enemySpawnQueue = [];
game.enemySpawnTimer = 0;
game.enemySpawnDelay = 120; // 2 second delay (120 frames = 2 seconds at 60 FPS)
}
// Create spawn queue for this wave
for (var i = 0; i < enemyCount; i++) {
game.enemySpawnQueue.push({
waveType: waveType,
waveNumber: currentWave
});
}
}
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;
}
}
// Handle enemy spawning from queue one by one
if (game.enemySpawnQueue && game.enemySpawnQueue.length > 0) {
game.enemySpawnTimer++;
if (game.enemySpawnTimer >= game.enemySpawnDelay) {
game.enemySpawnTimer = 0;
// Spawn one enemy from the queue
var spawnData = game.enemySpawnQueue.shift();
var enemy = new Enemy(spawnData.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
var healthMultiplier = Math.pow(1.12, spawnData.waveNumber); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Position enemy exactly on top of spawnpoint image
var spawnX = 11; // Center column where spawnpoint is located
var spawnY = 0; // Exactly at the spawnpoint position
enemy.cellX = spawnX;
enemy.cellY = spawnY;
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
// Set enemy position to match spawnpoint exactly, then move 3cm lower and 1cm right
enemy.x = spawnPoint.x + 38; // Move 1cm right (1cm * 37.8 pixels/cm)
enemy.y = spawnPoint.y + 113; // Move 3cm lower (3cm * 37.8 pixels/cm)
enemy.waveNumber = spawnData.waveNumber;
enemies.push(enemy);
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Increment kill count
killCount++;
updateKillCount();
// Boss enemies give more gold and score
var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : Math.floor(1 + (enemy.waveNumber - 1) * 0.5);
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Update hero stats display
if (gameStarted && LK.ticks % 30 === 0) {
// Update every 0.5 seconds
updateHeroStats();
// Update base health display
updateBaseHealth();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
};
White circle with black outline. Blue background.. In-Game asset. 2d. High contrast. No shadows
pixel art hearth icon. In-Game asset. 2d. High contrast. No shadows
attack on titan world game background. In-Game asset. 2d. High contrast. No shadows
armor pixel art. In-Game asset. 2d. High contrast. No shadows
a hero man on the horse character image. In-Game asset. 2d. High contrast. No shadows
hero swordsman. In-Game asset. 2d. High contrast. No shadows
sniper girl hero. In-Game asset. 2d. High contrast. No shadows
swordsman hero. In-Game asset. 2d. High contrast. No shadows
2d hero swordsman pixel sprite. In-Game asset. 2d. High contrast. No shadows
2d hero swordsman pixel sprite. In-Game asset. 2d. High contrast. No shadows
2d hero swordsman pixel sprite. sending shockwaves. In-Game asset. 2d. High contrast. No shadows
swordsman hero girl. In-Game asset. 2d. High contrast. No shadows
pixel art, swordsman attack, send shockwaves. In-Game asset. 2d. High contrast. No shadows
a hero, channeling, pixel art. In-Game asset. 2d. High contrast. No shadows
colossus monster, pixel art, standing, attacking. In-Game asset. 2d. High contrast. No shadows
colossus monster, pixel art, punching the ground. In-Game asset. 2d. High contrast. No shadows
swordsgirl hero attack, sending shockwaves, pixel art, no background. In-Game asset. 2d. High contrast. No shadows
swords girl hero, standing, pixel art, no backgroun. In-Game asset. 2d. High contrast. No shadows
monster giant, pixel art, no background. In-Game asset. 2d. High contrast. No shadows
monster giant with wings, flying , pixel art, no background. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows
castle gun, siege gun, pixel art. In-Game asset. 2d. High contrast. No shadows
castle base, pixel art. In-Game asset. 2d. High contrast. No shadows