User prompt
basenin 10 hücre üst tarafına siyah renkli hiçbir şey koyulamayan road ya da tower farketmeksizin hiçbir şeyin yerleşemediği şeridi oyuncu basenin 10 hücre üstüne koy
User prompt
basenin 9 hücre altındaki siyah hiçbir şeyin koyulamadığı şerit statik olsun asla o siyah şerit dışında bir yere kıpırdamasın base yeri değişse de değşimese de o siyah şerit orada dursun
Code edit (1 edits merged)
Please save this source code
User prompt
updateForbiddenZone() fonksiyonunu zaten kuleler için çağırıyorsunuz. Aynı anda türü de “zemin”e çevirirseniz yol algoritması hiç bakmadan red olur: js Kopyala Düzenle c.isForbiddenZone = true; c.type = TYPE_BLOCKED; // yeni sabit, walkable = false Böylece hem RoadPreview hem path-finding otomatik olarak o kareleri geçilemez sayar; ek kontrolden kurtulursunuz. Uyguladıktan sonra: Tower ve Road ikisi de UI satırına konamaz. Düşman path-finding eski yolu bozmadan çalışır; çünkü TYPE_BLOCKED hücrelere hiç adım atmazlar. Bu tek satır ekleme yol-yerleştirme boşluğunu kapatır.
User prompt
Neden yol (UI çubuğunun üstündeki satıra) hâlâ yerleşebiliyor? TowerPreview/Tower.placeOnGrid yol-altı satırı kontrol ederken cell.isForbiddenZone bayrağını okuyor. RoadPreview ise yalnızca: js Kopyala Düzenle if (!cell || cell.type !== TYPE_WALL) self.canPlace = false; ...şeklinde bir duvar testi yapıyor; isForbiddenZone hiç sorgulanmıyor. Bu yüzden “yasak satır” duvar hücresi olsa bile Basic/Slow/Poison Road rahatça konabiliyor. Hızlı çözüm - aynı bayrağı RoadPreview’de de kullan diff Kopyala Düzenle // RoadPreview.updatePlacementStatus veya benzer fonksiyon - if (!cell || cell.type !== TYPE_WALL) { + if ( + !cell || + cell.type !== TYPE_WALL || + cell.isForbiddenZone // ← yasak satır/yama + ) { self.canPlace = false; return; } Aynı ek satırı SlowRoadPreview ve PoisonRoadPreview sınıflarına da kopyalayın.
User prompt
Sorun yasak satırın (base + 9) yalnızca oyun ilk yüklendiğinde işaretlenmesi. Yeni bir yol ≡ yeni bir “base” eklediğiniz her seferinde base aşağı kayıyor, ama isForbiddenZone bayrakları güncellenmiyor. RoadPreview/TowerPreview hâlâ ilk kurulan satırı “yasak” sanıyor; oysa gerçek yasak satır artık yenisinin altında kalıyor ve orada yol yerleştirilmesine engel olunmuyor. // başlangıçta bir kez çalışıyor var baseCell = pathPoints[pathPoints.length - 1]; var gridYBelow = baseCell.y + 9; for (var x = 0; x < grid.cells.length; x++) { var cell = grid.getCell(x, gridYBelow); cell.isForbiddenZone = true; // ← 1) sadece bir kez } Preview’lerde ise sadece bu bayrağa bakılıyor: if (cell && cell.isForbiddenZone) { self.canPlace = false; } Base’i her uzattığınızda yeni satıra yeniden isForbiddenZone vermediğiniz için yol konabiliyor. Çözüm Yasak satırı her base değiştiğinde yeniden hesapla. function updateForbiddenZone() { // önce tüm eski işaretleri temizle grid.cells.forEach(col => col.forEach(c => { delete c.isForbiddenZone; })); const base = pathPoints[pathPoints.length - 1]; const forbidY = base.y + 9; for (let x = 0; x < grid.cells.length; x++) { const c = grid.getCell(x, forbidY); if (c) c.isForbiddenZone = true; } } Yol eklendiğinde (base değiştiğinde), yani şu bloğun hemen sonuna çağır: js Kopyala Düzenle // ...cell.type = TYPE_GOAL; pathPoints.push(cell); updateForbiddenZone(); // ← ekle grid.pathFind(); Kule yerleştirme kontrolü zaten cell.isForbiddenZone kullandığı için ek düzeltme gerekmez; yeni satır her seferinde doğru bayraklanacağından hem kule hem yol o satıra konamaz. Böylece base nereye taşınırsa taşınsın daima onun 9 hücre altındaki bütün satır yasak kalır ve “sağdan sola hiçbir yere yol konamıyor” kuralı gerçekleşir.
User prompt
base bulunan yerin 9 cell altına tower konulamıyor ancak road konulabiliyor road konulamaması lazım bunu fixle base bulunan yerin 9 cell alt kısmındaki satırda sağdan sola hiçbir yere road konulamaması lazım
User prompt
base bulunan yerin 9 cell altına road konulamaması lazım,tower konulamıyor road da konulamaması lazım bunu sağla şu an tower konulamıyor road konulabiliyor,hem tower hem road konulamaması lazım
User prompt
base bulunan yerin 9 cell alt kısmının tüm yatay soldan sağa her cell rengini siyah yap ve siyah olan cell e road base ya da tower kurmayı yasakla
User prompt
base bulunan yerin 8 cell alt kısmının cell rengini siyah yap
User prompt
base bulunan yerin 8 cell alt kısmına bir adet a texti yaz
User prompt
play placingtowerandroad sound when a road or a tower placed
User prompt
play hpgainsound when player base healthpoint increased for example if player base touch 10hp asset player base hp increases by 10 for cases like that play hpgain sound ,play damagetotower sound when player base hp decreases for example if an enemy touches player base player base gets damage and loses some hp in cases like that play damagetotower sound
User prompt
play stickywalk when an enemy is spawned and play it like a loop until it dies
User prompt
use cannon sound for normal tower ,use rapid cannon sound for quick tower,use splash cannon sound for splash tower when a tower shots play their own sound
User prompt
when a regular or boss enemy dies play slimewalking sound
User prompt
10hp asseti 111 pixel yukarı taşı bulunduğu konumdan
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 1; // Use tower-specific damage
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function (dt) {
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);
// Use scaledDt for consistent movement
var scaledSpeed = self.speed * (dt || scaledDt);
if (distance < scaledSpeed) {
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create visual splash effect
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
// Splash damage to nearby enemies
var splashRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var splashDx = otherEnemy.x - self.targetEnemy.x;
var splashDy = otherEnemy.y - self.targetEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage, minimum 1)
var splashDamage = Math.max(1, Math.floor(self.damage * 0.5));
otherEnemy.health -= splashDamage;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
// Use scaledDt for consistent movement
var scaledSpeed = self.speed * (dt || scaledDt);
self.x += Math.cos(angle) * scaledSpeed;
self.y += Math.sin(angle) * scaledSpeed;
}
};
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: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case TYPE_ROAD:
case TYPE_SLOW_ROAD:
case TYPE_POISON_ROAD:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.visible = false; // Hide numbers for decorative roads
cellGraphics.tint = 0x880000;
return;
}
numberLabel.visible = false; // Hide numbers to make roads naked
// Replace cell graphics with appropriate road asset
if (!cellGraphics.isRoadAsset) {
self.removeChild(cellGraphics);
cellGraphics = self.attachAsset('roadNormal', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.isRoadAsset = true;
}
// Apply road texture and rotation based on connections
renderRoadCell(cellGraphics, self.cell.x, self.cell.y);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
} else {
// Reset to default road appearance
cellGraphics.tint = 0xFFFFFF;
}
// Remove all arrows - roads should be naked
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
break;
}
case 2:
{
// spawn - enemies can walk on it but it doesn't count as road for scoring
self.removeArrows();
// Replace cell graphics with enemyspawns asset
if (!cellGraphics.isSpawnAsset) {
self.removeChild(cellGraphics);
cellGraphics = self.attachAsset('enemyspawns', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.isSpawnAsset = true;
}
cellGraphics.tint = 0xFFFFFF; // Reset tint for spawn asset
numberLabel.visible = false;
// Don't show score numbers for spawn point even though enemies use it for pathfinding
break;
}
case 1:
{
self.removeArrows();
cellGraphics.tint = 0xaaaaaa;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
// Replace cell graphics with playerbase asset
if (!cellGraphics.isBaseAsset) {
self.removeChild(cellGraphics);
cellGraphics = self.attachAsset('playerbase', {
anchorX: 0.5,
anchorY: 0.5
});
cellGraphics.isBaseAsset = true;
}
cellGraphics.tint = 0xFFFFFF; // Reset tint for base asset
numberLabel.visible = false;
break;
}
case 4:
{
// tower body
self.removeArrows();
cellGraphics.tint = 0x555555; // dark gray
numberLabel.visible = false;
break;
}
}
numberLabel.setText(Math.floor(data.score / 10000));
};
});
// 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;
}
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 * 1.05 * 1.2; // 5% faster + 20% additional increase = 26% faster total
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
// Initialize with base HP from table
self.maxHealth = HP_TABLE[self.type] || HP_TABLE.normal;
self.health = self.maxHealth;
self.poisonTimer = 0; // For poison road damage tracking
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
// Start playing stickywalk sound in loop for this enemy
self.walkingSound = LK.getSound('stickywalk');
self.walkingSound.play({
loop: true
});
// 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 * 1.2; // Twice as fast + 20% increase
self.maxHealth = HP_TABLE.fast;
break;
case 'immune':
self.isImmune = true;
self.maxHealth = HP_TABLE.immune;
self.speed *= 0.9 * 1.2; // 10% slower + 20% increase = 8% faster overall
break;
case 'flying':
self.isFlying = true;
self.maxHealth = Math.floor(HP_TABLE.flying * 1.1); // 10% more HP
self.speed *= 1.1 * 1.2; // 10% faster + 20% increase
break;
case 'swarm':
self.maxHealth = HP_TABLE.swarm;
self.speed *= 1.4; // 40% faster movement
break;
case 'normal':
default:
self.maxHealth = HP_TABLE.normal;
// Base speed already includes 20% increase in initialization
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies use specific HP values per wave
var bossNumber = currentWave / 10; // 1st boss = wave 10 (bossNumber = 1), 2nd boss = wave 20 (bossNumber = 2), etc.
// New boss HP values with 20% increase: Wave 10: 480, Wave 20: 780, Wave 30: 960, Wave 40: 1200, Wave 50: special
var bossHPValues = [480, 780, 960, 1200, 1200]; // HP for waves 10, 20, 30, 40, 50 (20% increase applied: 400*1.2=480, 650*1.2=780, 800*1.2=960, 1000*1.2=1200)
var baseHP = bossHPValues[Math.min(bossNumber - 1, 4)]; // Cap at index 4 (wave 50)
self.maxHealth = baseHP;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
self.health = self.maxHealth;
// Get appropriate asset for this enemy type
var assetId = 'enemy';
if (self.type !== 'normal') {
assetId = 'enemy_' + self.type;
}
var enemyGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Add scaling animation to all enemies
// Start with random scale between 0.8 and 1.2
var initialScale = 0.8 + Math.random() * 0.4; // Random between 0.8 and 1.2
enemyGraphics.scaleX = initialScale;
enemyGraphics.scaleY = initialScale;
// Create continuous scaling animation
var scaleMin = 0.8;
var scaleMax = 1.2;
var _animateScale = function animateScale() {
var targetScale = scaleMin + Math.random() * (scaleMax - scaleMin);
var duration = 500 + Math.random() * 250; // 0.5-0.75 seconds
tween(enemyGraphics, {
scaleX: targetScale,
scaleY: targetScale
}, {
duration: duration,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (enemyGraphics && enemyGraphics.parent) {
_animateScale(); // Continue animation if enemy still exists
}
}
});
};
// Start the scaling animation
_animateScale();
// Scale up boss enemies (applied on top of the animation)
if (self.isBoss) {
// For bosses, scale the animation range up
scaleMin = 1.4; // 0.8 * 1.8 = 1.44, rounded to 1.4
scaleMax = 2.2; // 1.2 * 1.8 = 2.16, rounded to 2.2
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Fall back to regular enemy asset if specific type asset not found
// Apply tint to differentiate enemy types
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
break;
case 'immune':
enemyGraphics.tint = 0xAA0000; // Red for immune enemies
break;
case 'flying':
enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
break;
case 'swarm':
enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// Add scaling animation to shadow that matches the main enemy
var shadowScaleMin = 0.8;
var shadowScaleMax = 1.2;
var _animateShadowScale = function animateShadowScale() {
var targetScale = shadowScaleMin + Math.random() * (shadowScaleMax - shadowScaleMin);
var duration = 500 + Math.random() * 250; // 0.5-0.75 seconds
tween(shadowGraphics, {
scaleX: targetScale,
scaleY: targetScale
}, {
duration: duration,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (shadowGraphics && shadowGraphics.parent) {
_animateShadowScale(); // Continue animation if shadow still exists
}
}
});
};
// Start the shadow scaling animation
_animateShadowScale();
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
// For boss shadows, scale the animation range up
shadowScaleMin = 1.4;
shadowScaleMax = 2.2;
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 (dt) {
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 -= dt || scaledDt;
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;
}
// Initialize poison timer if not exists
if (!self.poisonTimer) {
self.poisonTimer = 0;
}
self.poisonTimer += dt || scaledDt;
// Apply poison damage every 30 units (twice per second at 1x speed)
if (self.poisonTimer >= 30) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
self.poisonTimer = 0;
}
self.poisonDuration -= dt || scaledDt;
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);
// If gold is less than 1 but more than 0, display as 1
var displayValue = value > 0 && value < 1 ? 1 : value;
// Format the display text properly for positive and negative values
var displayText = displayValue >= 0 ? "+" + displayValue : displayValue.toString();
var shadowText = new Text2(displayText, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2(displayText, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
// Calculate actual grid height accounting for bars
var GRID_H_PX = 2732 - TOP_BAR_H - BOTTOM_BAR_H;
var actualRows = Math.floor(GRID_H_PX / CELL_SIZE);
gridHeight = Math.min(gridHeight, actualRows);
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];
// Default all cells to walls
var cellType = 1;
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;
}
}
}
// Set spawn at center (10, 12)
var centerCell = self.cells[10] && self.cells[10][12];
if (centerCell) {
centerCell.type = 2;
self.spawns = [centerCell];
}
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 != TYPE_WALL && !node.hasTower) {
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];
// Only process cardinal directions (up, down, left, right)
var 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;
} else {
// Limit spawn to only target the spawn exit (down direction)
self.spawns[a].targets = [spawnExit];
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// 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]);
}
}
}
};
// Re-render all road cells with updated corner/straight textures
self.forEachRoadCell = function (renderFunc) {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
if (cell && (cell.type === TYPE_ROAD || cell.type === TYPE_SLOW_ROAD || cell.type === TYPE_POISON_ROAD)) {
var debugCell = cell.debugCell;
if (debugCell && debugCell.children[0]) {
renderFunc(debugCell.children[0], i, j);
}
}
}
}
};
self.poisonRoadParticles = {}; // Track particles per poison road cell
self.slowRoadParticles = {}; // Track particles per slow road cell
self.spawnPotionParticle = function (cellX, cellY) {
var key = cellX + ',' + cellY;
if (!self.poisonRoadParticles[key]) {
self.poisonRoadParticles[key] = [];
}
// Clean up destroyed particles FIRST (before checking limit)
// Check both parent !== null AND !destroyed flag
self.poisonRoadParticles[key] = self.poisonRoadParticles[key].filter(function (p) {
return p.parent !== null && !p.destroyed;
});
// Check if we can spawn more particles (max 60 per cell - increased from 30)
if (self.poisonRoadParticles[key].length >= 60) {
return;
}
// Random position within the cell - use self.x instead of grid.x since we're inside the Grid class
var particleX = self.x + cellX * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8;
var particleY = self.y + cellY * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8;
var particle = new PotionParticle(particleX, particleY);
game.addChild(particle);
self.poisonRoadParticles[key].push(particle);
};
self.spawnSlowParticle = function (cellX, cellY) {
var key = cellX + ',' + cellY;
if (!self.slowRoadParticles[key]) {
self.slowRoadParticles[key] = [];
}
// Clean up destroyed particles FIRST (before checking limit)
// Check both parent !== null AND !destroyed flag
self.slowRoadParticles[key] = self.slowRoadParticles[key].filter(function (p) {
return p.parent !== null && !p.destroyed;
});
// Check if we can spawn more particles (max 60 per cell)
if (self.slowRoadParticles[key].length >= 60) {
return;
}
// Random position within the cell - use self.x instead of grid.x since we're inside the Grid class
var particleX = self.x + cellX * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8;
var particleY = self.y + cellY * CELL_SIZE + (Math.random() - 0.5) * CELL_SIZE * 0.8;
var particle = new SlowParticle(particleX, particleY);
game.addChild(particle);
self.slowRoadParticles[key].push(particle);
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Handle flying enemies differently - they follow the road but don't get poison effects
if (enemy.isFlying) {
// Use the same pathfinding system as ground enemies but ignore poison effects
// Handle normal pathfinding enemies using pathPoints array
if (!enemy.currentTarget) {
if (enemy.cellPathIndex === undefined) {
enemy.cellPathIndex = 1; // Start with spawn exit
}
if (enemy.cellPathIndex < pathPoints.length) {
enemy.currentTarget = pathPoints[enemy.cellPathIndex];
}
}
if (enemy.currentTarget) {
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
// Calculate local speed for this enemy
var speed = enemy.speed;
// Flying enemies are NOT affected by slow roads, so don't check for slow road
// Scale by game speed (for fast forward/pause)
var scaledSpeed = speed * gameSpeed;
if (dist < scaledSpeed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Move to next point in path
enemy.cellPathIndex++;
if (enemy.cellPathIndex < pathPoints.length) {
enemy.currentTarget = pathPoints[enemy.cellPathIndex];
} else {
enemy.currentTarget = undefined;
// Reached the goal
return true;
}
} else {
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
});
}
}
enemy.currentCellX += Math.cos(angle) * scaledSpeed;
enemy.currentCellY += Math.sin(angle) * scaledSpeed;
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Flying enemies are NOT affected by poison roads, so skip poison damage section
return false;
}
// Apply road effects for ground enemies
if (!enemy.isFlying) {
var currentCell = grid.getCell(Math.floor(enemy.currentCellX), Math.floor(enemy.currentCellY));
if (currentCell) {
// Apply poison damage
if (currentCell.type === TYPE_POISON_ROAD) {
enemy.poisonTimer += gameSpeed;
if (enemy.poisonTimer >= 60) {
// Once per second at 60 FPS
enemy.health -= 0.5;
enemy.poisonTimer = 0;
if (enemy.health <= 0) {
enemy.health = 0;
}
// Update health bar
if (enemy.healthBar) {
enemy.healthBar.width = enemy.health / enemy.maxHealth * 70;
}
}
} else {
enemy.poisonTimer = 0;
}
}
}
// Handle normal pathfinding enemies using pathPoints array
if (!enemy.currentTarget) {
if (enemy.cellPathIndex === undefined) {
enemy.cellPathIndex = 1; // Start with spawn exit
}
if (enemy.cellPathIndex < pathPoints.length) {
enemy.currentTarget = pathPoints[enemy.cellPathIndex];
}
}
if (enemy.currentTarget) {
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
// Calculate local speed for this enemy
var speed = enemy.speed;
// Check if enemy is on slow road
var currentCell = grid.getCell(Math.floor(enemy.currentCellX), Math.floor(enemy.currentCellY));
if (currentCell && currentCell.type === TYPE_SLOW_ROAD) {
speed *= SLOW_MUL; // Apply slow multiplier
}
// Scale by game speed (for fast forward/pause)
var scaledSpeed = speed * gameSpeed;
if (dist < scaledSpeed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
// Move to next point in path
enemy.cellPathIndex++;
if (enemy.cellPathIndex < pathPoints.length) {
enemy.currentTarget = pathPoints[enemy.cellPathIndex];
} else {
enemy.currentTarget = undefined;
}
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * scaledSpeed;
enemy.currentCellY += Math.sin(angle) * scaledSpeed;
}
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);
// Create outline first (behind the main button)
var buttonOutline = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonOutline.width = 450 + 8; // 50% bigger + outline thickness
buttonOutline.height = 150 + 8; // 50% bigger + outline thickness
buttonOutline.tint = 0x000000; // Black outline
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 450; // 50% bigger (300 * 1.5)
buttonBackground.height = 150; // 50% bigger (100 * 1.5)
buttonBackground.tint = 0xFFF8DC; // Cream white color
var buttonText = new Text2("Next Wave", {
size: 75,
//{8q} // 50% bigger text (50 * 1.5)
fill: 0x000000,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.pulseStartTime = 0;
self.isPulsing = false;
self.update = function () {
var shouldBeEnabled = waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves && waveIsComplete;
var shouldBeVisible = waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves && waveIsComplete;
// Handle visibility changes with tween effects
if (shouldBeVisible && !self.visible) {
// Show button with zoom in effect
self.visible = true;
self.scaleX = 0.1;
self.scaleY = 0.1;
self.alpha = 0;
self.pulseStartTime = LK.ticks;
self.isPulsing = true;
tween(self, {
scaleX: 1,
scaleY: 1,
alpha: 1
}, {
duration: 300,
easing: tween.elasticOut
});
} else if (!shouldBeVisible && self.visible) {
// Hide button with zoom out effect
self.isPulsing = false;
tween(self, {
scaleX: 0.1,
scaleY: 0.1,
alpha: 0
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
self.visible = false;
}
});
}
// Add continuous zoom in/out animation every 3 seconds when visible
if (self.visible && self.isPulsing) {
var timeSincePulse = LK.ticks - self.pulseStartTime;
// 3 seconds = 180 ticks at 60 FPS
if (timeSincePulse % 180 === 0 && timeSincePulse > 0) {
// Zoom out then zoom in
tween(self, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 400,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.easeInOut
});
}
});
}
}
if (shouldBeEnabled) {
self.enabled = true;
buttonBackground.tint = 0xFFF8DC; // Cream white color
} else {
self.enabled = false;
buttonBackground.tint = 0x888888;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves && waveIsComplete) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Reset wave completion tracking for new wave
waveIsComplete = false;
totalEnemiesSpawnedThisWave = 0;
totalEnemiesDeadThisWave = 0;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
var fadeOutTime = 120;
self.update = function () {
if (fadeOutTime > 0) {
fadeOutTime--;
self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var PoisonRoadPreview = Container.expand(function () {
var self = Container.call(this);
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
// Create purple semi-transparent square
var previewGraphics = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.tint = 0x8B4513; // Brown for poison road (same as normal road)
previewGraphics.alpha = 0.5;
previewGraphics.width = CELL_SIZE;
previewGraphics.height = CELL_SIZE;
self.addChild(previewGraphics);
// Add pulsing effect
self.pulsingTween = null;
self.startPulsing = function () {
if (self.pulsingTween) {
return;
}
self.pulsingTween = tween(previewGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
};
self.stopPulsing = function () {
if (self.pulsingTween) {
tween.stop(previewGraphics, {
alpha: true
});
self.pulsingTween = null;
previewGraphics.alpha = 0.5;
}
};
// Start pulsing immediately
self.startPulsing();
self.updatePlacementStatus = function () {
self.canPlace = false;
// Check if placement would be in bottom bar
if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) {
self.canPlace = false;
return;
}
var canAfford = gold >= POISON_ROAD_COST;
// Check if this cell is valid for road placement
var cell = grid.getCell(self.gridX, self.gridY);
// Prevent poison road placement on forbidden zone (row 9 below base)
if (!cell || cell.type !== TYPE_WALL || cell.hasTower || cell && cell.isForbiddenZone) {
self.canPlace = false;
} else if (!canAfford) {
self.canPlace = false;
} else {
// New yol bloğunun merkez koordinatı
var cx = self.gridX + 0.5;
var cy = self.gridY + 0.5;
var spawn = pathPoints[0]; // First point is spawn
var spawnCenter = {
x: spawn.x + 0.5,
y: spawn.y + 0.5
};
var base = pathPoints[pathPoints.length - 1];
var baseCenter = {
x: base.x + 0.5,
y: base.y + 0.5
};
// Spawn edge restrictions removed - roads can be placed adjacent to any spawn edge
// Check base edge restrictions
var isBaseEdge = sharesEdge({
x: cx,
y: cy
}, baseCenter);
if (isBaseEdge) {
var last = pathPoints[pathPoints.length - 1]; // base centre
var prev = pathPoints[pathPoints.length - 2]; // önceki yol
var backDir = getDirectionIndex(last.x, last.y, prev.x, prev.y);
var dirHere = getDirectionIndex(baseCenter.x, baseCenter.y, cx, cy);
if (dirHere === backDir) {
// SADECE geri yasak
self.canPlace = false;
return;
}
// yan veya ileri 90° → serbest
}
// Check if adjacent to any existing road (forbidden - only base allowed)
var dirsRoad = [[0, -1], [1, 0], [0, 1], [-1, 0]];
var adjacentToRoad = false;
for (var d = 0; d < 4; d++) {
var n = grid.getCell(self.gridX + dirsRoad[d][0], self.gridY + dirsRoad[d][1]);
if (n && (n.type === TYPE_ROAD || n.type === TYPE_SLOW_ROAD || n.type === TYPE_POISON_ROAD)) {
adjacentToRoad = true;
break;
}
}
// Roads can only be placed adjacent to base
if (adjacentToRoad) {
self.canPlace = false;
} else {
// Check if cell is adjacent to the base (end of current path)
var base = pathPoints[pathPoints.length - 1];
var isAdjacentToBase = false;
var baseNeighbors = [{
x: base.x,
y: base.y - 1
}, {
x: base.x + 1,
y: base.y
}, {
x: base.x,
y: base.y + 1
}, {
x: base.x - 1,
y: base.y
}];
for (var i = 0; i < baseNeighbors.length; i++) {
if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) {
isAdjacentToBase = true;
break;
}
}
// Check if this would be going backwards
var isBackwards = false;
if (isAdjacentToBase && pathPoints.length >= 2) {
var previousPoint = pathPoints[pathPoints.length - 2];
var currentDirection = {
x: base.x - previousPoint.x,
y: base.y - previousPoint.y
};
var proposedDirection = {
x: self.gridX - base.x,
y: self.gridY - base.y
};
if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) {
isBackwards = true;
}
}
self.canPlace = isAdjacentToBase && !isBackwards;
}
}
// Update appearance
if (self.canPlace) {
previewGraphics.tint = 0x00FF00; // Green for placeable
self.visible = true;
} else {
previewGraphics.tint = 0xFF0000; // Red for not placeable
self.visible = true;
}
};
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 - 72 + 10 + 4 + 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2;
self.updatePlacementStatus();
};
return self;
});
var PotionParticle = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
var particleGraphics = self.attachAsset('potionparticle', {
anchorX: 0.5,
anchorY: 0.5
});
// Start with random alpha
self.alpha = 0.3 + Math.random() * 0.5;
// Random horizontal offset for zigzag
self.zigzagAmplitude = 5 + Math.random() * 5;
self.zigzagFrequency = 2 + Math.random() * 2;
self.startX = x;
self.lifetime = 0;
// Animate upward movement with zigzag
tween(self, {
y: y - 80,
alpha: 0
}, {
duration: 2000,
// Reduced from 3000ms to 2000ms
easing: tween.linear,
onFinish: function onFinish() {
// Remove from parent before destroying
if (self.parent) {
self.parent.removeChild(self);
}
self.destroy();
}
});
self.update = function () {
self.lifetime += 1;
// Create zigzag pattern
var zigzagOffset = Math.sin(self.lifetime * 0.1 * self.zigzagFrequency) * self.zigzagAmplitude;
self.x = self.startX + zigzagOffset;
};
return self;
});
var RoadPreview = Container.expand(function () {
var self = Container.call(this);
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
// Create simple brown semi-transparent square
var previewGraphics = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.tint = 0x8B4513; // Brown for road
previewGraphics.alpha = 0.5;
previewGraphics.width = CELL_SIZE;
previewGraphics.height = CELL_SIZE;
self.addChild(previewGraphics);
// Add pulsing effect - start with null reference
self.pulsingTween = null;
self.startPulsing = function () {
if (self.pulsingTween) {
return;
}
self.pulsingTween = tween(previewGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
};
self.stopPulsing = function () {
if (self.pulsingTween) {
tween.stop(previewGraphics, {
alpha: true
});
self.pulsingTween = null;
previewGraphics.alpha = 0.5;
}
};
// Start pulsing immediately
self.startPulsing();
self.updatePlacementStatus = function () {
self.canPlace = false;
// Check if placement would be in bottom bar
if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) {
self.canPlace = false;
return;
}
var canAfford = gold >= ROAD_COST;
// Check if this cell is valid for road placement
var cell = grid.getCell(self.gridX, self.gridY);
// Prevent road placement on forbidden zone (row 9 below base)
if (!cell || cell.type !== 1 || cell.hasTower || cell && cell.isForbiddenZone) {
// Can't place on non-wall cells, cells with towers, or forbidden zone
self.canPlace = false;
} else if (!canAfford) {
// Not enough gold
self.canPlace = false;
} else {
// New road bloğunun merkez koordinatı
var cx = self.gridX + 0.5;
var cy = self.gridY + 0.5;
var spawn = pathPoints[0]; // First point is spawn
var spawnCenter = {
x: spawn.x + 0.5,
y: spawn.y + 0.5
};
var base = pathPoints[pathPoints.length - 1];
var baseCenter = {
x: base.x + 0.5,
y: base.y + 0.5
};
// Spawn edge restrictions removed - roads can be placed adjacent to any spawn edge
// Check base edge restrictions
var isBaseEdge = sharesEdge({
x: cx,
y: cy
}, baseCenter);
if (isBaseEdge) {
var last = pathPoints[pathPoints.length - 1]; // base centre
var prev = pathPoints[pathPoints.length - 2]; // önceki yol
var backDir = getDirectionIndex(last.x, last.y, prev.x, prev.y);
var dirHere = getDirectionIndex(baseCenter.x, baseCenter.y, cx, cy);
if (dirHere === backDir) {
// SADECE geri yasak
self.canPlace = false;
return;
}
// yan veya ileri 90° → serbest
}
// Check if adjacent to any existing road (forbidden - only base allowed)
var dirsRoad = [[0, -1], [1, 0], [0, 1], [-1, 0]];
var adjacentToRoad = false;
for (var d = 0; d < 4; d++) {
var n = grid.getCell(self.gridX + dirsRoad[d][0], self.gridY + dirsRoad[d][1]);
if (n && (n.type === TYPE_ROAD || n.type === TYPE_SLOW_ROAD || n.type === TYPE_POISON_ROAD)) {
// bitişik ROAD tespit
adjacentToRoad = true;
break;
}
}
// Roads can only be placed adjacent to base
if (adjacentToRoad) {
self.canPlace = false;
} else {
// Check if cell is adjacent to the base (end of current path)
var base = pathPoints[pathPoints.length - 1];
var isAdjacentToBase = false;
// Check 4-directional neighbors from base
var baseNeighbors = [{
x: base.x,
y: base.y - 1
},
// up
{
x: base.x + 1,
y: base.y
},
// right
{
x: base.x,
y: base.y + 1
},
// down
{
x: base.x - 1,
y: base.y
} // left
];
for (var i = 0; i < baseNeighbors.length; i++) {
if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) {
isAdjacentToBase = true;
break;
}
}
// Check if this would be going backwards
var isBackwards = false;
if (isAdjacentToBase && pathPoints.length >= 2) {
var previousPoint = pathPoints[pathPoints.length - 2];
var currentDirection = {
x: base.x - previousPoint.x,
y: base.y - previousPoint.y
};
var proposedDirection = {
x: self.gridX - base.x,
y: self.gridY - base.y
};
// Check if proposed direction is opposite to current direction
if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) {
isBackwards = true;
}
}
self.canPlace = isAdjacentToBase && !isBackwards;
}
}
// Update appearance - green for placeable, red for not placeable
if (self.canPlace) {
previewGraphics.tint = 0x00FF00; // Green for placeable
self.visible = true;
} else {
previewGraphics.tint = 0xFF0000; // Red for not placeable
self.visible = true;
}
};
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 - 72 + 10 + 4 + 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2;
self.updatePlacementStatus();
};
return self;
});
var SlowParticle = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
var particleGraphics = self.attachAsset('slowparticle', {
anchorX: 0.5,
anchorY: 0.5
});
// Start with random alpha
self.alpha = 0.3 + Math.random() * 0.5;
// Random horizontal offset for zigzag
self.zigzagAmplitude = 5 + Math.random() * 5;
self.zigzagFrequency = 2 + Math.random() * 2;
self.startX = x;
self.lifetime = 0;
// Animate upward movement with zigzag
tween(self, {
y: y - 80,
alpha: 0
}, {
duration: 2000,
// Same duration as poison particles
easing: tween.linear,
onFinish: function onFinish() {
// Remove from parent before destroying
if (self.parent) {
self.parent.removeChild(self);
}
self.destroy();
}
});
self.update = function () {
self.lifetime += 1;
// Create zigzag pattern
var zigzagOffset = Math.sin(self.lifetime * 0.1 * self.zigzagFrequency) * self.zigzagAmplitude;
self.x = self.startX + zigzagOffset;
};
return self;
});
var SlowRoadPreview = Container.expand(function () {
var self = Container.call(this);
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
// Create ice-blue semi-transparent square
var previewGraphics = LK.getAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.tint = 0x8B4513; // Brown for slow road (same as normal road)
previewGraphics.alpha = 0.5;
previewGraphics.width = CELL_SIZE;
previewGraphics.height = CELL_SIZE;
self.addChild(previewGraphics);
// Add pulsing effect
self.pulsingTween = null;
self.startPulsing = function () {
if (self.pulsingTween) {
return;
}
self.pulsingTween = tween(previewGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
};
self.stopPulsing = function () {
if (self.pulsingTween) {
tween.stop(previewGraphics, {
alpha: true
});
self.pulsingTween = null;
previewGraphics.alpha = 0.5;
}
};
// Start pulsing immediately
self.startPulsing();
self.updatePlacementStatus = function () {
self.canPlace = false;
// Check if placement would be in bottom bar
if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) {
self.canPlace = false;
return;
}
var canAfford = gold >= SLOW_ROAD_COST;
// Check if this cell is valid for road placement
var cell = grid.getCell(self.gridX, self.gridY);
// Prevent slow road placement on forbidden zone (row 9 below base)
if (!cell || cell.type !== TYPE_WALL || cell.hasTower || cell && cell.isForbiddenZone) {
self.canPlace = false;
} else if (!canAfford) {
self.canPlace = false;
} else {
// New yol bloğunun merkez koordinatı
var cx = self.gridX + 0.5;
var cy = self.gridY + 0.5;
var spawn = pathPoints[0]; // First point is spawn
var spawnCenter = {
x: spawn.x + 0.5,
y: spawn.y + 0.5
};
var base = pathPoints[pathPoints.length - 1];
var baseCenter = {
x: base.x + 0.5,
y: base.y + 0.5
};
// Spawn edge restrictions removed - roads can be placed adjacent to any spawn edge
// Check base edge restrictions
var isBaseEdge = sharesEdge({
x: cx,
y: cy
}, baseCenter);
if (isBaseEdge) {
var last = pathPoints[pathPoints.length - 1]; // base centre
var prev = pathPoints[pathPoints.length - 2]; // önceki yol
var backDir = getDirectionIndex(last.x, last.y, prev.x, prev.y);
var dirHere = getDirectionIndex(baseCenter.x, baseCenter.y, cx, cy);
if (dirHere === backDir) {
// SADECE geri yasak
self.canPlace = false;
return;
}
// yan veya ileri 90° → serbest
}
// Check if adjacent to any existing road (forbidden - only base allowed)
var dirsRoad = [[0, -1], [1, 0], [0, 1], [-1, 0]];
var adjacentToRoad = false;
for (var d = 0; d < 4; d++) {
var n = grid.getCell(self.gridX + dirsRoad[d][0], self.gridY + dirsRoad[d][1]);
if (n && (n.type === TYPE_ROAD || n.type === TYPE_SLOW_ROAD || n.type === TYPE_POISON_ROAD)) {
adjacentToRoad = true;
break;
}
}
// Roads can only be placed adjacent to base
if (adjacentToRoad) {
self.canPlace = false;
} else {
// Check if cell is adjacent to the base (end of current path)
var base = pathPoints[pathPoints.length - 1];
var isAdjacentToBase = false;
var baseNeighbors = [{
x: base.x,
y: base.y - 1
}, {
x: base.x + 1,
y: base.y
}, {
x: base.x,
y: base.y + 1
}, {
x: base.x - 1,
y: base.y
}];
for (var i = 0; i < baseNeighbors.length; i++) {
if (baseNeighbors[i].x === self.gridX && baseNeighbors[i].y === self.gridY) {
isAdjacentToBase = true;
break;
}
}
// Check if this would be going backwards
var isBackwards = false;
if (isAdjacentToBase && pathPoints.length >= 2) {
var previousPoint = pathPoints[pathPoints.length - 2];
var currentDirection = {
x: base.x - previousPoint.x,
y: base.y - previousPoint.y
};
var proposedDirection = {
x: self.gridX - base.x,
y: self.gridY - base.y
};
if (currentDirection.x === -proposedDirection.x && currentDirection.y === -proposedDirection.y) {
isBackwards = true;
}
}
self.canPlace = isAdjacentToBase && !isBackwards;
}
}
// Update appearance
if (self.canPlace) {
previewGraphics.tint = 0x00FF00; // Green for placeable
self.visible = true;
} else {
previewGraphics.tint = 0xFF0000; // Red for not placeable
self.visible = true;
}
};
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 - 72 + 10 + 4 + 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2;
self.updatePlacementStatus();
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'basic';
// Increase size of base for easier touch
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
baseGraphics.width = CELL_SIZE * 1.3 * 1.2;
baseGraphics.height = CELL_SIZE * 1.3 * 1.2;
switch (self.towerType) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var towerCost = getTowerCost(self.towerType);
// Get display name for tower type
var displayName;
switch (self.towerType) {
case 'basic':
displayName = ' Standard\n Cannon';
break;
case 'rapid':
displayName = ' Quick\nCannon';
break;
case 'splash':
displayName = ' Splash\nCannon';
break;
default:
displayName = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
}
// Add shadow for tower type label
var typeLabelShadow = new Text2(displayName, {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = 0 + 4;
self.addChild(typeLabelShadow);
var typeLabel = new Text2(displayName, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = 0; // Position at center of tower
self.addChild(typeLabel);
// Add coin image shadow
var coinImageShadow = self.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
coinImageShadow.width = 40;
coinImageShadow.height = 40;
coinImageShadow.x = -25 + 4;
coinImageShadow.y = 99 + 12 + 15;
coinImageShadow.tint = 0x000000;
coinImageShadow.alpha = 0.5;
// Add cost shadow
var costLabelShadow = new Text2(' ' + towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 15 + 4;
costLabelShadow.y = 99 + 12 + 15;
self.addChild(costLabelShadow);
// Add coin image
var coinImage = self.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
coinImage.width = 40;
coinImage.height = 40;
coinImage.x = -25;
coinImage.y = 95 + 12 + 15;
// Add cost label
var costLabel = new Text2(' ' + towerCost, {
size: 50,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.x = 15;
costLabel.y = 95 + 12 + 15;
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 || 'basic';
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 '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;
default:
// Basic: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60 * 1.10; // 10% slower fire rate
self.bulletSpeed = 5;
self.damage = 1; // Default tower damage (will be set based on type)
self.lastFired = 0;
self.targetEnemy = null;
// Set initial fire rate based on tower type and level
self.updateFireRate = function () {
switch (self.id) {
case 'rapid':
// Rapid tower fire rate values per second: 1.25, 1.5, 1.75, 2.0, 2.25, 3.5
var rapidRates = [1.25, 1.5, 1.75, 2.0, 2.25, 3.5];
var rapidRate = rapidRates[Math.min(self.level - 1, 5)]; // cap at level 6
self.fireRate = 60 / rapidRate; // convert to ticks between shots
break;
case 'splash':
// Splash tower fire rate values per second: 0.65, 0.9, 1.1, 1.3, 1.5, 1.75
var splashRates = [0.65, 0.9, 1.1, 1.3, 1.5, 1.75];
var splashRate = splashRates[Math.min(self.level - 1, 5)]; // cap at level 6
self.fireRate = 60 / splashRate; // convert to ticks between shots
break;
default:
// basic tower
// Basic tower fire rate values per second: 1.0, 1.1, 1.3, 1.5, 1.7, 2.0
var basicRate = basicRates[Math.min(self.level - 1, 5)]; // cap at level 6
self.fireRate = 60 / basicRate; // convert to ticks between shots
break;
}
};
switch (self.id) {
case 'rapid':
self.fireRate = 60 / 1.25; // 1.25 shots/second for level 1
self.damage = 1.0; // Base damage for rapid tower
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'splash':
self.fireRate = 60 / 0.65; // 0.65 shots/second for level 1
self.damage = 1.2; // Base damage for splash tower
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
default:
// basic tower
self.fireRate = 60 / 0.75; // 0.75 shots/second for level 1
self.damage = 0.9; // Base damage for basic tower
self.range = 3 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
// Set initial fire rate
self.updateFireRate();
var baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
baseGraphics.width = CELL_SIZE;
baseGraphics.height = CELL_SIZE;
switch (self.id) {
case 'rapid':
baseGraphics.tint = 0x00AAFF;
break;
case 'splash':
baseGraphics.tint = 0x33CC00;
break;
default:
baseGraphics.tint = 0xAAAAAA;
}
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1) + 50;
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
var gunAsset = self.id === 'rapid' ? 'rapidtowercannon' : self.id === 'splash' ? 'splashcannontower' : 'defense';
var gunGraphics = gunContainer.attachAsset(gunAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
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++;
// Increase damage by 0.1 per level
self.damage += 0.1;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
// Update fire rate using the new unified system for all tower types
self.updateFireRate();
// Update bullet speed for all tower types
if (self.id === 'rapid') {
self.bulletSpeed = 7 + self.level * 0.7;
} else if (self.id === 'splash') {
self.bulletSpeed = 5 + self.level * 0.5;
} else {
// Basic tower
self.bulletSpeed = 5 + self.level * 0.5;
}
self.refreshCellsInRange();
self.updateLevelIndicators();
if (self.level > 1) {
var levelDot = levelIndicators[self.level - 1].children[1];
tween(levelDot, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
tween(levelDot, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
return true;
} else {
var notification = game.addChild(new Notification("Not enough gold to upgrade!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return false;
}
}
return false;
};
self.findTarget = function () {
var closestEnemy = null;
var closestScore = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if enemy is in range
if (distance <= self.getRange()) {
// Handle flying enemies differently - they can be targeted regardless of path
if (enemy.isFlying) {
// For flying enemies, prioritize by distance to the goal
if (enemy.flyingTarget) {
var goalX = enemy.flyingTarget.x;
var goalY = enemy.flyingTarget.y;
var distToGoal = Math.sqrt((goalX - enemy.cellX) * (goalX - enemy.cellX) + (goalY - enemy.cellY) * (goalY - enemy.cellY));
// Use distance to goal as score
if (distToGoal < closestScore) {
closestScore = distToGoal;
closestEnemy = enemy;
}
} else {
// If no flying target yet (shouldn't happen), prioritize by distance to tower
if (distance < closestScore) {
closestScore = distance;
closestEnemy = enemy;
}
}
} else {
// For ground enemies, 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 (dt) {
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
// Use cooldown system with scaledDt
if (!self.cooldown) {
self.cooldown = 0;
}
self.cooldown -= dt || scaledDt;
if (self.cooldown <= 0) {
self.fire();
self.cooldown = self.fireRate;
}
}
};
self.down = function (x, y, obj) {
var existingMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
var hasOwnMenu = false;
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self) {
rangeCircle = game.children[i];
break;
}
}
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hasOwnMenu = true;
break;
}
}
if (hasOwnMenu) {
for (var i = 0; i < existingMenus.length; i++) {
if (existingMenus[i].tower === self) {
hideUpgradeMenu(existingMenus[i]);
}
}
if (rangeCircle) {
game.removeChild(rangeCircle);
}
selectedTower = null;
grid.renderDebug();
return;
}
for (var i = 0; i < existingMenus.length; i++) {
existingMenus[i].destroy();
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = self;
var rangeIndicator = new Container();
rangeIndicator.isTowerRange = true;
rangeIndicator.tower = self;
game.addChild(rangeIndicator);
rangeIndicator.x = self.x;
rangeIndicator.y = self.y;
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.getRange() * 2;
rangeGraphics.alpha = 0.3;
var upgradeMenu = new UpgradeMenu(self);
game.addChild(upgradeMenu);
upgradeMenu.x = 2048 / 2;
tween(upgradeMenu, {
y: 2732 - 225
}, {
duration: 200,
easing: tween.backOut
});
grid.renderDebug();
};
self.isInRange = function (enemy) {
if (!enemy) {
return false;
}
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
return distance <= self.getRange();
};
self.fire = function () {
if (self.targetEnemy) {
var potentialDamage = 0;
for (var i = 0; i < self.targetEnemy.bulletsTargetingThis.length; i++) {
potentialDamage += self.targetEnemy.bulletsTargetingThis[i].damage;
}
if (self.targetEnemy.health > potentialDamage) {
var bulletX = self.x + Math.cos(gunContainer.rotation) * 40;
var bulletY = self.y + Math.sin(gunContainer.rotation) * 40;
var bullet = new Bullet(bulletX, bulletY, self.targetEnemy, self.damage, self.bulletSpeed);
// Set bullet type based on tower type
bullet.type = self.id;
// Play appropriate sound based on tower type
switch (self.id) {
case 'rapid':
bullet.children[0].tint = 0x00AAFF;
bullet.children[0].width = 20;
bullet.children[0].height = 20;
LK.getSound('rapidcannonsound').play();
break;
case 'splash':
bullet.children[0].tint = 0x33CC00;
bullet.children[0].width = 40 * 0.65;
bullet.children[0].height = 40 * 0.65;
LK.getSound('splashcannonsound').play();
break;
default:
LK.getSound('cannonsound').play();
}
game.addChild(bullet);
bullets.push(bullet);
self.targetEnemy.bulletsTargetingThis.push(bullet);
// --- Fire recoil effect for gunContainer ---
// Stop any ongoing recoil tweens before starting a new one
tween.stop(gunContainer, {
x: true,
y: true,
scaleX: true,
scaleY: true
});
// Always use the original resting position for recoil, never accumulate offset
if (gunContainer._restX === undefined) {
gunContainer._restX = 0;
}
if (gunContainer._restY === undefined) {
gunContainer._restY = 0;
}
if (gunContainer._restScaleX === undefined) {
gunContainer._restScaleX = 1;
}
if (gunContainer._restScaleY === undefined) {
gunContainer._restScaleY = 1;
}
// Reset to resting position before animating (in case of interrupted tweens)
gunContainer.x = gunContainer._restX;
gunContainer.y = gunContainer._restY;
gunContainer.scaleX = gunContainer._restScaleX;
gunContainer.scaleY = gunContainer._restScaleY;
// Calculate recoil offset (recoil back along the gun's rotation)
var recoilDistance = 8;
var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
// Animate recoil back from the resting position
tween(gunContainer, {
x: gunContainer._restX + recoilX,
y: gunContainer._restY + recoilY
}, {
duration: 60,
easing: tween.cubicOut,
onFinish: function onFinish() {
// Animate return to original position/scale
tween(gunContainer, {
x: gunContainer._restX,
y: gunContainer._restY
}, {
duration: 90,
easing: tween.cubicIn
});
}
});
}
}
};
self.placeOnGrid = function (gridX, gridY) {
self.gridX = gridX;
self.gridY = gridY;
// Position at exact grid cell center with offset of 56 pixels up and 56 pixels left (moved 14 pixels down and 12 pixels right total)
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2 - 58;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2 - 56;
var cell = grid.getCell(gridX, gridY);
if (cell) {
cell.type = 4; // tower type
cell.hasTower = true; // mark as occupied
}
self.refreshCellsInRange();
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'basic';
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;
previewGraphics.height = CELL_SIZE;
// Add pulsing effect to preview
self.pulsingTween = null;
self.startPulsing = function () {
if (self.pulsingTween) {
return;
}
self.pulsingTween = tween(previewGraphics, {
alpha: 0.8
}, {
duration: 500,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
};
self.stopPulsing = function () {
if (self.pulsingTween) {
tween.stop(previewGraphics, {
alpha: true
});
self.pulsingTween = null;
previewGraphics.alpha = 0.5;
}
};
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
} else {
// Green tint for valid placement
switch (self.towerType) {
case 'rapid':
previewGraphics.tint = 0x00AAFF;
break;
case 'splash':
previewGraphics.tint = 0x33CC00;
break;
default:
previewGraphics.tint = 0xAAAAAA;
}
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
// Check if placement would be in bottom bar
if (self.y + CELL_SIZE / 2 > 2732 + BOTTOM_BAR_H) {
validGridPlacement = false;
self.canPlace = false;
}
if (self.gridY <= 4 || self.gridY >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
var cell = grid.getCell(self.gridX, self.gridY);
// Towers can only be placed on wall cells (type 1) and must not have towers
if (!cell || cell.type !== 1 || cell.hasTower || cell.isForbiddenZone) {
validGridPlacement = false;
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX === self.gridX && enemy.cellY === self.gridY) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX === self.gridX && targetY === self.gridY) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
// Check if tower touches a road
var touchesRoad = false;
if (validGridPlacement) {
var dirs = [[0, -1], [1, 0], [0, 1], [-1, 0]]; // 4 directions
for (var d = 0; d < dirs.length; d++) {
var nx = self.gridX + dirs[d][0];
var ny = self.gridY + dirs[d][1];
var nCell = grid.getCell(nx, ny);
if (nCell && (nCell.type === TYPE_ROAD || nCell.type === TYPE_SLOW_ROAD || nCell.type === TYPE_POISON_ROAD || nCell.type === TYPE_SPAWN || nCell.type === TYPE_GOAL)) {
touchesRoad = true;
break;
}
}
}
self.canPlace = validGridPlacement && touchesRoad && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
// Position at exact grid cell center - same as Tower
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2 - 72 + 10 + 4 + 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2 - 66 + 5 + 3 + 2;
self.checkPlacement();
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 225;
var menuBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
menuBackground.width = 2048;
menuBackground.height = 500;
menuBackground.tint = 0x444444;
menuBackground.alpha = 0.9;
var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage.toFixed(1) + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : gold >= upgradeCost ? 0x00AA00 : 0x888888;
var buttonText = new Text2(isMaxLevel ? 'Max Level' : 'Upgrade: ' + upgradeCost + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
upgradeButton.addChild(buttonText);
var sellButton = new Container();
buttonsContainer.addChild(sellButton);
var sellButtonBackground = sellButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
sellButtonBackground.width = 500;
sellButtonBackground.height = 150;
sellButtonBackground.tint = 0xCC0000;
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
var sellButtonText = new Text2('Sell: +' + sellValue + ' gold', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
sellButtonText.anchor.set(0.5, 0.5);
sellButton.addChild(sellButtonText);
upgradeButton.y = -85;
sellButton.y = 85;
var closeButton = new Container();
self.addChild(closeButton);
var closeBackground = closeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
closeBackground.width = 90;
closeBackground.height = 90;
closeBackground.tint = 0xAA0000;
var closeText = new Text2('X', {
size: 68,
fill: 0xFFFFFF,
weight: 800
});
closeText.anchor.set(0.5, 0.5);
closeButton.addChild(closeText);
closeButton.x = menuBackground.width / 2 - 57;
closeButton.y = -menuBackground.height / 2 + 57;
upgradeButton.down = function (x, y, obj) {
if (self.tower.level >= self.tower.maxLevel) {
var notification = game.addChild(new Notification("Tower is already at max level!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
return;
}
if (self.tower.upgrade()) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
if (self.tower.level >= self.tower.maxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
statsText.setText('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage.toFixed(1) + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s');
buttonText.setText('Upgrade: ' + upgradeCost + ' gold');
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = Math.floor(totalInvestment * 0.6);
sellButtonText.setText('Sell: +' + sellValue + ' gold');
if (self.tower.level >= self.tower.maxLevel) {
buttonBackground.tint = 0x888888;
buttonText.setText('Max Level');
}
var rangeCircle = null;
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
rangeCircle = game.children[i];
break;
}
}
if (rangeCircle) {
var rangeGraphics = rangeCircle.children[0];
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
} else {
var newRangeIndicator = new Container();
newRangeIndicator.isTowerRange = true;
newRangeIndicator.tower = self.tower;
game.addChildAt(newRangeIndicator, 0);
newRangeIndicator.x = self.tower.x;
newRangeIndicator.y = self.tower.y;
var rangeGraphics = newRangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
rangeGraphics.alpha = 0.3;
}
tween(self, {
scaleX: 1.05,
scaleY: 1.05
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
sellButton.down = function (x, y, obj) {
var totalInvestment = self.tower.getTotalValue ? self.tower.getTotalValue() : 0;
var sellValue = getTowerSellValue(totalInvestment);
setGold(gold + sellValue);
var notification = game.addChild(new Notification("Tower sold for " + sellValue + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
var gridX = self.tower.gridX;
var gridY = self.tower.gridY;
var cell = grid.getCell(gridX, gridY);
if (cell) {
cell.type = 1; // reset to wall
cell.hasTower = false; // mark as free
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();
if (typeof window.fixSpawnTargets === 'function') {
window.fixSpawnTargets();
}
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = gold >= currentUpgradeCost;
buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
if (buttonText.text !== newText) {
buttonText.setText(newText);
}
};
return self;
});
var WaveIndicator = Container.expand(function () {
var self = Container.call(this);
self.gameStarted = false;
self.waveMarkers = [];
self.waveTypes = [];
self.enemyCounts = [];
self.indicatorWidth = 0;
self.lastBossType = null; // Track the last boss type to avoid repeating
var blockWidth = 400;
var totalBlocksWidth = blockWidth * totalWaves;
var startMarker = new Container();
var startBlock = startMarker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
startBlock.width = blockWidth - 10;
startBlock.height = 70 * 2;
startBlock.tint = 0x00AA00;
// Add shadow for start text
var startTextShadow = new Text2("Start Game", {
size: 50,
fill: 0x000000,
weight: 800
});
startTextShadow.anchor.set(0.5, 0.5);
startTextShadow.x = 4;
startTextShadow.y = 4;
startMarker.addChild(startTextShadow);
var startText = new Text2("Start Game", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
startText.anchor.set(0.5, 0.5);
startMarker.addChild(startText);
startMarker.x = -self.indicatorWidth;
self.addChild(startMarker);
self.waveMarkers.push(startMarker);
startMarker.down = function () {
if (!self.gameStarted) {
self.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
startBlock.tint = 0x00FF00;
startText.setText("Started!");
startTextShadow.setText("Started!");
// Make sure shadow position remains correct after text change
startTextShadow.x = 4;
startTextShadow.y = 4;
var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (i === 5) {
// Wave 6 - Mini Boss
block.tint = 0xFF4500;
waveType = "Mini Boss";
enemyType = "swarm";
enemyCount = 1;
} 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;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
// Only handle wave progression if game is not paused
if (gameSpeed > 0) {
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
// Scale wave timer by game speed
// When gameSpeed is 2x, timer progresses 2x faster
// When gameSpeed is 0.5x, timer progresses 2x slower
waveTimer += gameSpeed; // Direct multiplication by game speed
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
var isHidingUpgradeMenu = false;
function hideUpgradeMenu(menu) {
if (isHidingUpgradeMenu) {
return;
}
isHidingUpgradeMenu = true;
tween(menu, {
y: 2732 + 225
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
menu.destroy();
isHidingUpgradeMenu = false;
}
});
}
window.fixSpawnTargets = function fixSpawnTargets() {
if (!grid || !grid.spawns || !grid.spawns[0]) {
return;
} // koruma
var spawn = grid.spawns[0];
var spawnExit = grid.getCell(spawn.x, spawn.y + 1);
spawn.targets = [spawnExit]; // yalnız aşağı
};
function isRoad(cellType) {
return cellType === TYPE_ROAD || cellType === TYPE_SLOW_ROAD || cellType === TYPE_POISON_ROAD || cellType === TYPE_SPAWN || cellType === TYPE_GOAL;
}
// Classify block and return detailed info including directions
function classifyBlock(x, y) {
var kind = classifyRoadCell(x, y);
var dirIn = 0; // Default up
var dirOut = 0; // Default up
var rotation = 0;
if (kind === 'corner') {
var _grid$getCell5, _grid$getCell6, _grid$getCell7, _grid$getCell8;
var up = isRoad((_grid$getCell5 = grid.getCell(x, y - 1)) === null || _grid$getCell5 === void 0 ? void 0 : _grid$getCell5.type);
var dn = isRoad((_grid$getCell6 = grid.getCell(x, y + 1)) === null || _grid$getCell6 === void 0 ? void 0 : _grid$getCell6.type);
var lt = isRoad((_grid$getCell7 = grid.getCell(x - 1, y)) === null || _grid$getCell7 === void 0 ? void 0 : _grid$getCell7.type);
var rt = isRoad((_grid$getCell8 = grid.getCell(x + 1, y)) === null || _grid$getCell8 === void 0 ? void 0 : _grid$getCell8.type);
// Determine directions and rotation
if (up && rt) {
dirIn = 2;
dirOut = 0;
rotation = 0; // ┘ (from down to up)
} else if (rt && dn) {
dirIn = 3;
dirOut = 1;
rotation = Math.PI / 2; // └ (from left to right)
} else if (dn && lt) {
dirIn = 0;
dirOut = 2;
rotation = Math.PI; // ┌ (from up to down)
} else if (lt && up) {
dirIn = 1;
dirOut = 3;
rotation = -Math.PI / 2; // ┐ (from right to left)
}
}
return {
kind: kind,
dirIn: dirIn,
dirOut: dirOut,
rotation: rotation
};
}
// Classify road cell as corner or straight based on connections
function classifyRoadCell(x, y) {
var _grid$getCell, _grid$getCell2, _grid$getCell3, _grid$getCell4;
var n = (_grid$getCell = grid.getCell(x, y - 1)) === null || _grid$getCell === void 0 ? void 0 : _grid$getCell.type; // up
var s = (_grid$getCell2 = grid.getCell(x, y + 1)) === null || _grid$getCell2 === void 0 ? void 0 : _grid$getCell2.type; // down
var w = (_grid$getCell3 = grid.getCell(x - 1, y)) === null || _grid$getCell3 === void 0 ? void 0 : _grid$getCell3.type; // left
var e = (_grid$getCell4 = grid.getCell(x + 1, y)) === null || _grid$getCell4 === void 0 ? void 0 : _grid$getCell4.type; // right
var up = isRoad(n);
var dn = isRoad(s);
var lt = isRoad(w);
var rt = isRoad(e);
// Corner = at least one vertical AND at least one horizontal connection
if ((up || dn) && (lt || rt)) {
return 'corner';
}
return 'straight';
}
// Render road cell with appropriate texture and rotation
function renderRoadCell(cellSprite, x, y) {
var c = grid.getCell(x, y);
// ① If already marked as corner, use that directly
if (c.kind === 'corner') {
var cornerAsset = LK.getAsset('roadCorner', {});
cellSprite.texture = cornerAsset.texture;
cellSprite.rotation = c.cachedRotation || 0; // Use stored rotation
return; // Done
}
// ② Otherwise continue with automatic analysis (straight/corner detection)
var _grid$getCell5, _grid$getCell6, _grid$getCell7, _grid$getCell8;
var up = isRoad((_grid$getCell5 = grid.getCell(x, y - 1)) === null || _grid$getCell5 === void 0 ? void 0 : _grid$getCell5.type);
var dn = isRoad((_grid$getCell6 = grid.getCell(x, y + 1)) === null || _grid$getCell6 === void 0 ? void 0 : _grid$getCell6.type);
var lt = isRoad((_grid$getCell7 = grid.getCell(x - 1, y)) === null || _grid$getCell7 === void 0 ? void 0 : _grid$getCell7.type);
var rt = isRoad((_grid$getCell8 = grid.getCell(x + 1, y)) === null || _grid$getCell8 === void 0 ? void 0 : _grid$getCell8.type);
// Count connections to determine if this is a corner
var connectionCount = 0;
var hasVertical = up || dn;
var hasHorizontal = lt || rt;
if (up) {
connectionCount++;
}
if (dn) {
connectionCount++;
}
if (lt) {
connectionCount++;
}
if (rt) {
connectionCount++;
}
// Use corner asset if we have exactly 2 connections AND they form a corner (one vertical + one horizontal)
var isCorner = connectionCount === 2 && hasVertical && hasHorizontal;
if (isCorner) {
var cornerAsset = LK.getAsset('roadCorner', {});
cellSprite.texture = cornerAsset.texture;
// Set rotation based on which two directions are connected
if (up && rt) {
cellSprite.rotation = 0;
} // ┘ (up + right)
else if (rt && dn) {
cellSprite.rotation = Math.PI / 2;
} // └ (right + down)
else if (dn && lt) {
cellSprite.rotation = Math.PI;
} // ┌ (down + left)
else if (lt && up) {
cellSprite.rotation = -Math.PI / 2;
} // ┐ (left + up)
else {
cellSprite.rotation = 0;
} // fallback
} else {
var normalAsset = LK.getAsset('roadNormal', {});
cellSprite.texture = normalAsset.texture;
cellSprite.rotation = 0;
}
}
var CELL_SIZE = 114;
var ROAD_COST = 5;
var TOP_BAR_H = 38;
var BOTTOM_BAR_H = -260;
// Bullet damage constant
var BULLET_DAMAGE = 1;
// Enemy HP table
var HP_TABLE = {
normal: 6,
fast: 6,
flying: 6,
immune: 12,
swarm: 3,
boss: 50
};
// Road type constants
var TYPE_ROAD = 0;
var TYPE_WALL = 1;
var TYPE_SPAWN = 2;
var TYPE_GOAL = 3;
var TYPE_TOWER = 4;
var TYPE_SLOW_ROAD = 5;
var TYPE_POISON_ROAD = 6;
var TYPE_BLOCKED = 99; // Not walkable, not buildable (forbidden zone)
// Slow road multiplier
var SLOW_MUL = 0.75; // 25% slower
// Road costs
var SLOW_ROAD_COST = 10;
var POISON_ROAD_COST = 15;
// Direction constants for spawn/base edge filtering
var DIRS = [{
dx: 0,
dy: -1
},
// up (index 0)
{
dx: 1,
dy: 0
},
// right (index 1)
{
dx: 0,
dy: 1
},
// down (index 2)
{
dx: -1,
dy: 0
} // left (index 3)
];
var SPAWN_EXIT_DIR = 2; // index 2 → down
// Helper functions for spawn/base edge filtering
function sharesEdge(a, b) {
return Math.abs(a.x - b.x) === 1 && a.y === b.y || Math.abs(a.y - b.y) === 1 && a.x === b.x;
}
function getDirectionIndex(fromX, fromY, toX, toY) {
if (toX > fromX) {
return 1;
} // right
if (toX < fromX) {
return 3;
} // left
if (toY > fromY) {
return 2;
} // down
return 0; // up
}
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var selectedTower = null;
var gold = 80;
var hiddenGoldFraction = 0; // Track decimal parts of gold earnings
var lives = 100;
var score = 0;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 12000 / 2;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
// Wave spawning state tracking
var enemiesSpawnedThisWave = 0;
var lastEnemySpawnTime = 0;
var enemySpawnDelay = 78; // Default 1.3 seconds at 60 FPS (60 * 1.3 = 78)
var totalEnemiesToSpawnThisWave = 0;
var currentWaveType = 'normal';
// Wave completion tracking
var totalEnemiesSpawnedThisWave = 0;
var totalEnemiesDeadThisWave = 0;
var waveIsComplete = false;
// Function to get spawn delay based on enemy type
function getEnemySpawnDelay(enemyType) {
switch (enemyType) {
case 'normal':
return 51;
// 0.85 seconds (60 * 0.85 = 51)
case 'fast':
return 51;
// 0.85 seconds (60 * 0.85 = 51)
case 'swarm':
return 51;
// 0.85 seconds (60 * 0.85 = 51)
case 'immune':
return 51;
// 0.85 seconds (60 * 0.85 = 51)
case 'flying':
return 51;
// 0.85 seconds (60 * 0.85 = 51)
default:
return 51;
// 0.85 seconds (60 * 0.85 = 51)
}
}
// Game speed control
var gameSpeed = 1;
var gameSpeedFactors = [1, 2, 5, 0.5, 0];
var currentSpeedIndex = 0;
// Global scaled delta time
var scaledDt = 1; // Initialize with default value
// Basic tower fire rate values per second: 0.75, 1.0, 1.2, 1.5, 1.7, 2.0
var basicRates = [0.75, 1.0, 1.2, 1.5, 1.7, 2.0];
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('HP: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
// Create health bar
var healthBarContainer = new Container();
healthBarContainer.visible = true; // Ensure container is visible
var healthBarBg = healthBarContainer.attachAsset('healthBar', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarBg.width = 106.9; // 5% smaller than 112.5 (112.5 * 0.95)
healthBarBg.height = 10.7; // 5% smaller than 11.25 (11.25 * 0.95)
healthBarBg.tint = 0x444444; // Dark gray background
healthBarBg.visible = true; // Ensure background is visible
var healthBarFill = healthBarContainer.attachAsset('healthBar', {
anchorX: 0.5,
anchorY: 0.5
});
healthBarFill.width = 106.9; // 5% smaller than 112.5 (112.5 * 0.95)
healthBarFill.height = 10.7; // 5% smaller than 11.25 (11.25 * 0.95)
healthBarFill.tint = 0x00FF00; // Green fill
healthBarFill.visible = true; // Ensure fill is visible
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var topMargin = 50;
var centerX = 2048 / 2;
var spacing = 400;
LK.gui.top.addChild(goldText);
LK.gui.top.addChild(livesText);
LK.gui.top.addChild(scoreText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
var speedText = new Text2('1×', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
speedText.anchor.set(0.5, 0.5);
speedText.x = spacing + 30 + 200;
speedText.y = topMargin;
LK.gui.top.addChild(speedText);
speedText.down = function () {
currentSpeedIndex = (currentSpeedIndex + 1) % gameSpeedFactors.length;
gameSpeed = gameSpeedFactors[currentSpeedIndex];
if (gameSpeed === 0) {
speedText.setText('⏸');
} else if (gameSpeed === 0.5) {
speedText.setText('0.5×');
} else if (gameSpeed === 1) {
speedText.setText('1×');
} else if (gameSpeed === 2) {
speedText.setText('2×');
} else if (gameSpeed === 5) {
speedText.setText('5×');
} else {
speedText.setText(gameSpeed + 'x');
}
};
function updateHealthBarPos() {
if (pathPoints && pathPoints.length > 0) {
var base = pathPoints[pathPoints.length - 1]; // Last point is always the base
healthBarContainer.x = grid.x + base.x * CELL_SIZE + CELL_SIZE / 2 - 58; // 58 pixels left (59 - 1)
healthBarContainer.y = grid.y + base.y * CELL_SIZE + CELL_SIZE + 8 - 43; // 43 pixels up from 8px below base (40 + 3)
}
}
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('HP: ' + Math.floor(lives));
scoreText.setText('Score: ' + score);
// Update health bar
var maxDisplayHP = Math.max(100, lives); // Dynamic max HP based on current HP if above 100
var healthPercentage = Math.min(1, lives / maxDisplayHP); // Cap percentage at 100% for display
healthBarFill.width = 106.9 * healthPercentage; // Use new smaller size (106.9 instead of 112.5)
// Change health bar color based on HP level
if (lives > 100) {
healthBarFill.tint = 0x00FFFF; // Cyan for above 100 HP
} else {
healthBarFill.tint = 0x00FF00; // Green for normal HP
}
// Align health bar fill to the left by adjusting anchor and position
healthBarFill.anchorX = 0; // Change anchor to left
healthBarFill.x = healthBarBg.x - healthBarBg.width / 2; // Position at left edge of background
}
function setGold(value) {
gold = value;
updateUI();
}
function addGoldWithFraction(amount) {
// Add the exact amount including decimals to hidden tracking
hiddenGoldFraction += amount;
// Extract whole number part and add to displayed gold
var wholeGold = Math.floor(hiddenGoldFraction);
if (wholeGold > 0) {
setGold(gold + wholeGold);
hiddenGoldFraction -= wholeGold; // Keep only the decimal part
}
}
// Create bottom bar
var bottomBar = new Container();
var bottomBarGraphics = bottomBar.attachAsset('notification', {
anchorX: 0,
anchorY: 0
});
bottomBarGraphics.width = 2048;
bottomBarGraphics.height = Math.abs(BOTTOM_BAR_H);
bottomBarGraphics.tint = 0x222222;
bottomBar.y = 2732 - BOTTOM_BAR_H;
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 = 60;
grid.y = 200 - CELL_SIZE * 4;
// Initialize the dynamic path
function buildPath(spawnX, spawnY) {
var pts = [];
pts.push({
x: spawnX,
y: spawnY
}); // spawn
pts.push({
x: spawnX,
y: spawnY + 1
}); // 1 down
for (var k = 1; k <= 3; k++) {
// 3 left
pts.push({
x: spawnX - k,
y: spawnY + 1
});
}
return pts; // last element is base
}
var pathPoints = buildPath(10, 12);
// Define spawn and spawn exit for enemy pathfinding
var spawn = grid.getCell(10, 12);
var spawnExit = grid.getCell(10, 13);
// Mark path cells in grid
for (var i = 0; i < pathPoints.length; i++) {
var point = pathPoints[i];
var cell = grid.getCell(point.x, point.y);
if (cell) {
if (i === 0) {
cell.type = 2; // spawn
} else if (i === pathPoints.length - 1) {
cell.type = 3; // goal
grid.goals.push(cell); // Add base to goals array
} else {
cell.type = TYPE_ROAD; // path
}
// Store sprite reference and block info for initial path (center cell)
var blockInfo = classifyBlock(cell.x, cell.y);
if (cell.debugCell && cell.debugCell.children[0]) {
// Only store sprite reference if this is the center cell
var HALF = 1; // Block center offset
if (cell.x % 2 === 0 && cell.y % 2 === 1) {
// Center cell for our grid layout
cell.sprite = cell.debugCell.children[0];
cell.kind = blockInfo.kind;
cell.dirIn = blockInfo.dirIn;
cell.dirOut = blockInfo.dirOut;
}
}
}
}
// --- Ava: Static forbidden zone strip that never moves ---
function updateForbiddenZone() {
// Clear all previous forbidden zone flags and reset tint if needed
for (var i = 0; i < grid.cells.length; i++) {
for (var j = 0; j < grid.cells[i].length; j++) {
var cell = grid.getCell(i, j);
if (cell) {
if (cell.isForbiddenZone) {
cell.isForbiddenZone = false;
// Restore tint if it was set to black before
if (cell.debugCell && cell.debugCell.children[0]) {
// Only reset if not a forbidden zone anymore and not a road/base
if (cell.type === TYPE_WALL) {
cell.debugCell.children[0].tint = 0x2e8500;
}
}
}
}
}
}
// Mark static forbidden zone row at fixed position (row 22) - 9 cells below base
var staticForbiddenRow = 22; // Fixed row position that never changes
for (var i = 0; i < grid.cells.length; i++) {
var cell = grid.getCell(i, staticForbiddenRow);
if (cell && cell.debugCell && cell.debugCell.children[0]) {
// Set color to black
cell.debugCell.children[0].tint = 0x000000;
// Mark as forbidden for road/tower
cell.isForbiddenZone = true;
cell.type = 99; // TYPE_BLOCKED: not walkable, not buildable
} else if (cell) {
// Even if no debugCell, still mark as forbidden
cell.isForbiddenZone = true;
cell.type = 99; // TYPE_BLOCKED: not walkable, not buildable
}
}
// Mark static forbidden zone row 10 cells above base at fixed position (row 3)
var staticForbiddenRowAbove = 3; // Fixed row position 10 cells above base
for (var i = 0; i < grid.cells.length; i++) {
var cell = grid.getCell(i, staticForbiddenRowAbove);
if (cell && cell.debugCell && cell.debugCell.children[0]) {
// Set color to black
cell.debugCell.children[0].tint = 0x000000;
// Mark as forbidden for road/tower
cell.isForbiddenZone = true;
cell.type = 99; // TYPE_BLOCKED: not walkable, not buildable
} else if (cell) {
// Even if no debugCell, still mark as forbidden
cell.isForbiddenZone = true;
cell.type = 99; // TYPE_BLOCKED: not walkable, not buildable
}
}
}
// Initial forbidden zone marking
updateForbiddenZone();
grid.pathFind();
// Fix spawn targets to only go downward
if (typeof window.fixSpawnTargets === 'function') {
window.fixSpawnTargets();
}
grid.renderDebug();
// Position health bar below player base (at end of path) - moved here after pathPoints is initialized
// NOTE: This will be executed after uiLayer is created
// Update sprite references and block info after initial rendering
for (var i = 0; i < pathPoints.length; i++) {
var point = pathPoints[i];
var cell = grid.getCell(point.x, point.y);
if (cell && cell.debugCell && cell.debugCell.children[0]) {
var blockInfo = classifyBlock(cell.x, cell.y);
// Only store sprite reference if this is the center cell
var HALF = 1; // Block center offset
if (cell.x % 2 === 0 && cell.y % 2 === 1) {
// Center cell for our grid layout
cell.sprite = cell.debugCell.children[0];
cell.kind = blockInfo.kind;
cell.dirIn = blockInfo.dirIn;
cell.dirOut = blockInfo.dirOut;
}
}
}
debugLayer.addChild(grid);
game.addChild(bottomBar);
game.addChild(debugLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
// Add UI overlay layer that renders on top of everything
var uiLayer = new Container();
game.addChild(uiLayer);
// Position health bar below player base (at end of path) - moved here after uiLayer is created
if (pathPoints && pathPoints.length > 0) {
var basePoint = pathPoints[pathPoints.length - 1];
// Add health bar to UI layer instead of base cell
uiLayer.addChild(healthBarContainer);
// Position health bar in world coordinates
healthBarContainer.x = grid.x + basePoint.x * CELL_SIZE + CELL_SIZE / 2;
healthBarContainer.y = grid.y + basePoint.y * CELL_SIZE + CELL_SIZE + 8; // 8px below base
// Ensure health bar is visible
healthBarContainer.visible = true;
healthBarBg.visible = true;
healthBarFill.visible = true;
}
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var roadPreview = new RoadPreview();
game.addChild(roadPreview);
roadPreview.visible = false;
var slowRoadPreview = new SlowRoadPreview();
game.addChild(slowRoadPreview);
slowRoadPreview.visible = false;
var poisonRoadPreview = new PoisonRoadPreview();
game.addChild(poisonRoadPreview);
poisonRoadPreview.visible = false;
var isDragging = false;
var dragType = null; // 'tower' or 'road'
function wouldBlockPath(gridX, gridY) {
// Always return false as enemies can only walk on road tiles
return false;
}
function getTowerCost(towerType) {
switch (towerType) {
case 'rapid':
return 15;
case 'splash':
return 25;
default:
return 10;
}
}
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 || 'basic');
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;
}
}
game.down = function (x, y, obj) {
// Check if menu2 is visible and click is outside menu button
if (menu2Popup) {
// Check if click is on menu button
var menuButtonBounds = {
left: menuButton.x - 50,
right: menuButton.x + 50,
top: menuButton.y - 50,
bottom: menuButton.y + 50
};
var clickedOnMenuButton = x >= menuButtonBounds.left && x <= menuButtonBounds.right && y >= menuButtonBounds.top && y <= menuButtonBounds.bottom;
if (!clickedOnMenuButton) {
// Close menu2 with fade out animation
tween(menu2Popup, {
alpha: 0
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
if (menu2Popup && menu2Popup.parent) {
menu2Popup.parent.removeChild(menu2Popup);
}
menu2Popup = null;
}
});
}
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Check if clicking on road sources
for (var i = 0; i < roadSources.length; i++) {
var source = roadSources[i];
if (x >= source.x - source.width / 2 && x <= source.x + source.width / 2 && y >= source.y - source.height / 2 && y <= source.y + source.height / 2) {
isDragging = true;
if (source.data.type === 'road') {
roadPreview.visible = true;
dragType = 'road';
roadPreview.snapToGrid(x, y);
} else if (source.data.type === 'slowRoad') {
slowRoadPreview.visible = true;
dragType = 'slowRoad';
slowRoadPreview.snapToGrid(x, y);
} else if (source.data.type === 'poisonRoad') {
poisonRoadPreview.visible = true;
dragType = 'poisonRoad';
poisonRoadPreview.snapToGrid(x, y);
}
return;
}
}
// Check tower sources
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
towerPreview.visible = true;
isDragging = true;
dragType = 'tower';
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
towerPreview.startPulsing();
// Apply the same offset as in move handler to ensure consistency when starting drag
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
break;
}
}
};
game.move = function (x, y, obj) {
if (isDragging) {
// Shift the y position upward by 1.5 tiles to show preview above finger
if (dragType === 'road') {
roadPreview.snapToGrid(x, y);
} else if (dragType === 'slowRoad') {
slowRoadPreview.snapToGrid(x, y);
} else if (dragType === 'poisonRoad') {
poisonRoadPreview.snapToGrid(x, y);
} else {
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
}
}
};
game.up = function (x, y, obj) {
// Ignore clicks in bottom bar area
if (y > 2732 + BOTTOM_BAR_H) {
return;
}
var clickedOnTower = false;
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
var towerLeft = tower.x - tower.width / 2;
var towerRight = tower.x + tower.width / 2;
var towerTop = tower.y - tower.height / 2;
var towerBottom = tower.y + tower.height / 2;
if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
clickedOnTower = true;
break;
}
}
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenus.length > 0 && !isDragging && !clickedOnTower) {
var clickedOnMenu = false;
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
var menuWidth = 2048;
var menuHeight = 450;
var menuLeft = menu.x - menuWidth / 2;
var menuRight = menu.x + menuWidth / 2;
var menuTop = menu.y - menuHeight / 2;
var menuBottom = menu.y + menuHeight / 2;
if (x >= menuLeft && x <= menuRight && y >= menuTop && y <= menuBottom) {
clickedOnMenu = true;
break;
}
}
if (!clickedOnMenu) {
for (var i = 0; i < upgradeMenus.length; i++) {
var menu = upgradeMenus[i];
hideUpgradeMenu(menu);
}
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
selectedTower = null;
grid.renderDebug();
}
}
if (isDragging) {
isDragging = false;
if (dragType === 'road' || dragType === 'slowRoad' || dragType === 'poisonRoad') {
// Handle road placement
var preview, cost, roadType;
if (dragType === 'road') {
preview = roadPreview;
cost = ROAD_COST;
roadType = TYPE_ROAD;
} else if (dragType === 'slowRoad') {
preview = slowRoadPreview;
cost = SLOW_ROAD_COST;
roadType = TYPE_SLOW_ROAD;
} else if (dragType === 'poisonRoad') {
preview = poisonRoadPreview;
cost = POISON_ROAD_COST;
roadType = TYPE_POISON_ROAD;
}
if (preview.canPlace && gold >= cost) {
var cell = grid.getCell(preview.gridX, preview.gridY);
if (cell) {
// Deduct gold
setGold(gold - cost);
// Check if touching base to determine if this extends the path
var base = pathPoints[pathPoints.length - 1];
var touchingBase = sharesEdge({
x: preview.gridX + 0.5,
y: preview.gridY + 0.5
}, {
x: base.x + 0.5,
y: base.y + 0.5
});
if (touchingBase) {
// L-turn detection and corner conversion
if (pathPoints.length >= 2) {
// Direction mapping: 0=up, 1=right, 2=down, 3=left
var getDirection = function getDirection(dx, dy) {
if (dx > 0) {
return 1;
} // →
if (dx < 0) {
return 3;
} // ←
if (dy > 0) {
return 2;
} // ↓
return 0; // ↑
};
var prevCell = grid.getCell(pathPoints[pathPoints.length - 2].x, pathPoints[pathPoints.length - 2].y); // A (old base)
var newCell = {
x: preview.gridX,
y: preview.gridY
}; // B (new base)
// Calculate direction vectors
var dx = newCell.x - prevCell.x;
var dy = newCell.y - prevCell.y;
var newDir = getDirection(dx, dy); // prevCell's exit direction
var oldDir = prevCell.dirIn || 0; // prevCell's entry direction
// Check if this is a turn (different axes: horizontal vs vertical)
var isTurn = oldDir % 2 !== newDir % 2;
if (isTurn && prevCell.sprite) {
// Update cell data for corner
prevCell.kind = 'corner';
prevCell.dirOut = newDir;
// Break PIXI texture cache
prevCell.sprite.texture = LK.getAsset('cell', {}).texture;
// Apply corner texture
prevCell.sprite.texture = LK.getAsset('roadCorner', {}).texture;
// Apply correct rotation based on turn direction
var rotationMap = {};
rotationMap['2-0'] = 0; // ↓ then ↑ (┘)
rotationMap['3-1'] = Math.PI / 2; // ← then → (└)
rotationMap['0-2'] = Math.PI; // ↑ then ↓ (┌)
rotationMap['1-3'] = -Math.PI / 2; // → then ← (┐)
var rotationKey = oldDir + '-' + newDir;
var rotation = rotationMap[rotationKey] || 0;
prevCell.sprite.rotation = rotation;
// Cache the rotation for future renders
prevCell.cachedRotation = rotation;
}
}
// Extending the main path - old behavior
var oldBase = grid.goals[0];
oldBase.type = TYPE_ROAD; // convert to normal road
cell.type = TYPE_GOAL; // temporarily make this the new base
cell.hasTower = false; // ensure it's not marked as having a tower
grid.goals[0] = cell; // update goals array
// Update pathPoints array for debug arrows
pathPoints.push({
x: cell.x,
y: cell.y
});
// Update forbidden zone row after base moves
updateForbiddenZone();
// Update health bar position (it stays in uiLayer)
updateHealthBarPos();
// Now convert the new base to the desired road type
oldBase.type = roadType;
// Store sprite reference and block info for the newly placed block (center cell)
var blockInfo = classifyBlock(cell.x, cell.y);
if (cell.debugCell && cell.debugCell.children[0]) {
// Only store sprite reference if this is the center cell (for 2x2 blocks)
var HALF = 1; // Block center offset
if (cell.x % 2 === 0 && cell.y % 2 === 1) {
// Center cell for our grid layout
cell.sprite = cell.debugCell.children[0];
cell.kind = blockInfo.kind;
cell.dirIn = blockInfo.dirIn;
cell.dirOut = blockInfo.dirOut;
}
}
} else {
// Decorative road next to spawn - don't extend path
cell.type = roadType;
cell.hasTower = false; // ensure it's not marked as having a tower
// Store sprite reference and block info for decorative road (center cell)
var blockInfo = classifyBlock(cell.x, cell.y);
if (cell.debugCell && cell.debugCell.children[0]) {
// Only store sprite reference if this is the center cell
var HALF = 1; // Block center offset
if (cell.x % 2 === 0 && cell.y % 2 === 1) {
// Center cell for our grid layout
cell.sprite = cell.debugCell.children[0];
cell.kind = blockInfo.kind;
cell.dirIn = blockInfo.dirIn;
cell.dirOut = blockInfo.dirOut;
}
}
}
// Recalculate path and redraw
grid.pathFind();
// Fix spawn targets to only go downward
if (typeof window.fixSpawnTargets === 'function') {
window.fixSpawnTargets();
}
grid.renderDebug();
// Play placingtowerandroad sound when road is placed
LK.getSound('placingtowerandroad').play();
// Show gold deduction
var goldIndicator = new GoldIndicator(-cost, preview.x, preview.y);
game.addChild(goldIndicator);
}
} else if (gold < cost) {
var notification = game.addChild(new Notification("Not enough gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
// Check specific reasons for failure
var base = pathPoints[pathPoints.length - 1];
var isAdjacentToBase = false;
var baseNeighbors = [{
x: base.x,
y: base.y - 1
},
// up
{
x: base.x + 1,
y: base.y
},
// right
{
x: base.x,
y: base.y + 1
},
// down
{
x: base.x - 1,
y: base.y
} // left
];
for (var i = 0; i < baseNeighbors.length; i++) {
if (baseNeighbors[i].x === preview.gridX && baseNeighbors[i].y === preview.gridY) {
isAdjacentToBase = true;
break;
}
}
if (!isAdjacentToBase) {
var notification = game.addChild(new Notification("Road must connect to Base!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
var notification = game.addChild(new Notification("Road can't go backwards!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
}
roadPreview.visible = false;
slowRoadPreview.visible = false;
poisonRoadPreview.visible = false;
} else {
// Handle tower placement
if (towerPreview.canPlace) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
var towerCost = getTowerCost(towerPreview.towerType);
var wasPlaced = placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
if (wasPlaced) {
// Play placingtowerandroad sound when tower is placed
LK.getSound('placingtowerandroad').play();
// Show gold deduction for tower
var goldIndicator = new GoldIndicator(-towerCost, towerPreview.x, towerPreview.y);
game.addChild(goldIndicator);
}
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
towerPreview.stopPulsing();
}
dragType = null;
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - BOTTOM_BAR_H - 80 - 320 + 30 + 36;
game.addChild(waveIndicator);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200 - 500 - 300 - 10 - 7; // Move 817 pixels left total (500 + 300 + 10 + 7)
nextWaveButton.y = 2732 - BOTTOM_BAR_H - 100 + 20 - 320 + 30 - 400 - 200 + 100 - 4 + 30 + 40; // Move 434 pixels up total (400 + 200 - 100 + 4 - 30 - 40)
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
var buildItems = ['basic', 'rapid', 'splash'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
// Account for road buttons in spacing calculation
var totalItems = buildItems.length + 3; // +3 for road types
var startX = 2048 / 2 - totalItems * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - BOTTOM_BAR_H - CELL_SIZE * 3 - 90 - 150 - 50 - 15 + 10;
// Create road source buttons
var roadSources = [];
// Normal road
var roadSource = new Container();
roadSource.interactive = true;
roadSource.buttonMode = true;
roadSource.data = {
type: "road"
};
var roadBg = roadSource.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
roadBg.width = CELL_SIZE * 1.3 * 1.2;
roadBg.height = CELL_SIZE * 1.3 * 1.2;
roadBg.tint = 0xF4A460; // Sand yellow color for road
// Add shadow for road type label
var roadTypeLabelShadow = new Text2('Basic\nRoad', {
size: 50,
fill: 0x000000,
weight: 800
});
roadTypeLabelShadow.anchor.set(0.5, 0.5);
roadTypeLabelShadow.x = 4;
roadTypeLabelShadow.y = 4;
roadSource.addChild(roadTypeLabelShadow);
// Add road type label
var roadTypeLabel = new Text2('Basic\nRoad', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
roadTypeLabel.anchor.set(0.5, 0.5);
roadTypeLabel.y = 0;
roadSource.addChild(roadTypeLabel);
// Add coin image
var roadCoinImage = roadSource.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
roadCoinImage.width = 40;
roadCoinImage.height = 40;
roadCoinImage.x = -25;
roadCoinImage.y = 105 + 15;
// Add cost label
var roadCostLabel = new Text2(' ' + ROAD_COST.toString(), {
size: 50,
fill: 0xFFD700,
weight: 800
});
roadCostLabel.anchor.set(0.5, 0.5);
roadCostLabel.x = 15;
roadCostLabel.y = 105 + 15;
roadSource.addChild(roadCostLabel);
roadSource.x = startX;
roadSource.y = towerY + 50;
roadSource.update = function () {
var canAfford = gold >= ROAD_COST;
roadSource.alpha = canAfford ? 1 : 0.5;
};
towerLayer.addChild(roadSource);
roadSources.push(roadSource);
// Slow road
var slowRoadSource = new Container();
slowRoadSource.interactive = true;
slowRoadSource.buttonMode = true;
slowRoadSource.data = {
type: "slowRoad"
};
var slowRoadBg = slowRoadSource.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
slowRoadBg.width = CELL_SIZE * 1.3 * 1.2;
slowRoadBg.height = CELL_SIZE * 1.3 * 1.2;
slowRoadBg.tint = 0xF4A460; // Sand yellow for slow road
// Add shadow for slow road type label
var slowRoadTypeLabelShadow = new Text2('Slow\nRoad', {
size: 50,
fill: 0x000000,
weight: 800
});
slowRoadTypeLabelShadow.anchor.set(0.5, 0.5);
slowRoadTypeLabelShadow.x = 4;
slowRoadTypeLabelShadow.y = 4;
slowRoadSource.addChild(slowRoadTypeLabelShadow);
// Add road type label
var slowRoadTypeLabel = new Text2('Slow\nRoad', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
slowRoadTypeLabel.anchor.set(0.5, 0.5);
slowRoadTypeLabel.y = 0;
slowRoadSource.addChild(slowRoadTypeLabel);
// Add coin image
var slowRoadCoinImage = slowRoadSource.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
slowRoadCoinImage.width = 40;
slowRoadCoinImage.height = 40;
slowRoadCoinImage.x = -25;
slowRoadCoinImage.y = 105 + 15;
// Add cost label
var slowRoadCostLabel = new Text2(' ' + SLOW_ROAD_COST.toString(), {
size: 50,
fill: 0xFFD700,
weight: 800
});
slowRoadCostLabel.anchor.set(0.5, 0.5);
slowRoadCostLabel.x = 15;
slowRoadCostLabel.y = 105 + 15;
slowRoadSource.addChild(slowRoadCostLabel);
slowRoadSource.x = startX + towerSpacing;
slowRoadSource.y = towerY + 50;
slowRoadSource.update = function () {
var canAfford = gold >= SLOW_ROAD_COST;
slowRoadSource.alpha = canAfford ? 1 : 0.5;
};
towerLayer.addChild(slowRoadSource);
roadSources.push(slowRoadSource);
// Poison road
var poisonRoadSource = new Container();
poisonRoadSource.interactive = true;
poisonRoadSource.buttonMode = true;
poisonRoadSource.data = {
type: "poisonRoad"
};
var poisonRoadBg = poisonRoadSource.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
poisonRoadBg.width = CELL_SIZE * 1.3 * 1.2;
poisonRoadBg.height = CELL_SIZE * 1.3 * 1.2;
poisonRoadBg.tint = 0xF4A460; // Sand yellow for poison road
// Add shadow for poison road type label
var poisonRoadTypeLabelShadow = new Text2('Poison\n Road', {
size: 50,
fill: 0x000000,
weight: 800
});
poisonRoadTypeLabelShadow.anchor.set(0.5, 0.5);
poisonRoadTypeLabelShadow.x = 4;
poisonRoadTypeLabelShadow.y = 4;
poisonRoadSource.addChild(poisonRoadTypeLabelShadow);
// Add road type label
var poisonRoadTypeLabel = new Text2('Poison\n Road', {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
poisonRoadTypeLabel.anchor.set(0.5, 0.5);
poisonRoadTypeLabel.y = 0;
poisonRoadSource.addChild(poisonRoadTypeLabel);
// Add coin image
var poisonRoadCoinImage = poisonRoadSource.attachAsset('goldimage', {
anchorX: 0.5,
anchorY: 0.5
});
poisonRoadCoinImage.width = 40;
poisonRoadCoinImage.height = 40;
poisonRoadCoinImage.x = -25;
poisonRoadCoinImage.y = 105 + 15;
// Add cost label
var poisonRoadCostLabel = new Text2(' ' + POISON_ROAD_COST.toString(), {
size: 50,
fill: 0xFFD700,
weight: 800
});
poisonRoadCostLabel.anchor.set(0.5, 0.5);
poisonRoadCostLabel.x = 15;
poisonRoadCostLabel.y = 105 + 15;
poisonRoadSource.addChild(poisonRoadCostLabel);
poisonRoadSource.x = startX + towerSpacing * 2;
poisonRoadSource.y = towerY + 50;
poisonRoadSource.update = function () {
var canAfford = gold >= POISON_ROAD_COST;
poisonRoadSource.alpha = canAfford ? 1 : 0.5;
};
towerLayer.addChild(poisonRoadSource);
roadSources.push(poisonRoadSource);
// Add "ROADS" section header
var roadsHeaderText = new Text2('ROADS', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
roadsHeaderText.anchor.set(0.5, 0.5);
roadsHeaderText.x = startX + towerSpacing; // Center over the road items
roadsHeaderText.y = towerY - 130 + 50 - 30; // Position above the road items
towerLayer.addChild(roadsHeaderText);
// Add dash between poison and default
var dashText = new Text2('-', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
dashText.anchor.set(0.5, 0.5);
dashText.x = startX + towerSpacing * 2.5;
dashText.y = towerY + 50;
towerLayer.addChild(dashText);
// Add "TOWERS" section header
var towersHeaderText = new Text2('TOWERS', {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
towersHeaderText.anchor.set(0.5, 0.5);
towersHeaderText.x = startX + towerSpacing * 4; // Center over the tower items
towersHeaderText.y = towerY - 130 + 50 - 30; // Position above the tower items
towerLayer.addChild(towersHeaderText);
// Add menu button near TOWERS text
var menuButton = towerLayer.attachAsset('menu_button', {
anchorX: 0.5,
anchorY: 0.5
});
menuButton.x = towersHeaderText.x + 200 + 270 + 15; // Position to the right of TOWERS text, moved 270 pixels right, then 15 pixels more right
menuButton.y = towersHeaderText.y + 15; // Same vertical position as TOWERS text, then 15 pixels down
menuButton.visible = true; // Make menu button visible
// Variable to track menu2 popup
var menu2Popup = null;
// Add click handler for menu button
menuButton.down = function () {
// Show intro assets when menu button is clicked
if (!introAsset.parent) {
LK.gui.center.addChild(introAsset);
}
if (!intro2Asset.parent) {
LK.gui.center.addChild(intro2Asset);
}
if (!intro3Asset.parent) {
LK.gui.center.addChild(intro3Asset);
}
if (!intro4Asset.parent) {
LK.gui.center.addChild(intro4Asset);
}
if (!intro5Asset.parent) {
LK.gui.center.addChild(intro5Asset);
}
};
// Create tower sources (offset by 3 to account for road types)
for (var i = 0; i < buildItems.length; i++) {
var tower = new SourceTower(buildItems[i]);
tower.x = startX + (i + 3) * towerSpacing;
tower.y = towerY + 50;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
// Create intro assets but don't add them to the scene initially
var introAsset = LK.getAsset('Intro', {
anchorX: 0.5,
anchorY: 0.5
});
var intro2Asset = LK.getAsset('intro2', {
anchorX: 0.5,
anchorY: 0.5
});
var intro3Asset = LK.getAsset('intro3', {
anchorX: 0.5,
anchorY: 0.5
});
var intro4Asset = LK.getAsset('intro4', {
anchorX: 0.5,
anchorY: 0.5
});
var intro5Asset = LK.getAsset('intro5', {
anchorX: 0.5,
anchorY: 0.5
});
// LK.gui.center automatically positions at center, so no need to set x,y coordinates
// Intro assets remain visible until clicked
// Add click handlers to make intro assets disappear when clicked
LK.gui.center.down = function (x, y, obj) {
// Check which intro asset is currently visible and remove it
if (intro5Asset.parent) {
// intro5 is visible, remove it
intro5Asset.parent.removeChild(intro5Asset);
} else if (intro4Asset.parent) {
// intro4 is visible, remove it
intro4Asset.parent.removeChild(intro4Asset);
} else if (intro3Asset.parent) {
// intro3 is visible, remove it
intro3Asset.parent.removeChild(intro3Asset);
} else if (intro2Asset.parent) {
// intro2 is visible, remove it
intro2Asset.parent.removeChild(intro2Asset);
} else if (introAsset.parent) {
// intro is visible, remove it
introAsset.parent.removeChild(introAsset);
}
};
// Start playing background music in loop
LK.playMusic('background');
game.update = function () {
// Calculate global scaledDt
scaledDt = 1 * gameSpeed; // 0.5, 1, 3, 5, etc.
// Apply game speed (skip updates if paused)
if (gameSpeed === 0) {
return; // Game is paused
}
// Scale time-based updates by game speed
var speedMultiplier = gameSpeed;
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
// Reset wave completion tracking
totalEnemiesSpawnedThisWave = 0;
totalEnemiesDeadThisWave = 0;
waveIsComplete = false;
// Get wave type and enemy count from the wave indicator
currentWaveType = waveIndicator.getWaveType(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
// Check if this is a boss wave
var isBossWave = currentWave % 10 === 0 && currentWave > 0;
// Always get the enemy count from wave indicator first
enemyCount = waveIndicator.getEnemyCount(currentWave);
// Only override for boss waves if it's NOT a swarm wave
if (currentWave === 50) {
// Wave 50 spawns 4 bosses (all previous bosses)
enemyCount = 4;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ FINAL BOSS WAVE! ALL BOSSES! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
} else if (isBossWave && currentWaveType !== '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;
} else if (currentWaveType === 'swarm') {
// For wave 6 (mini boss), spawn only 1 enemy
if (currentWave === 6) {
enemyCount = 1;
} else {
// Other swarm waves get 30 enemies
enemyCount = 30;
}
}
// Initialize wave spawning variables
totalEnemiesToSpawnThisWave = enemyCount;
enemiesSpawnedThisWave = 0;
lastEnemySpawnTime = LK.ticks - getEnemySpawnDelay(currentWaveType); // Allow immediate first spawn
}
// Handle delayed enemy spawning with dynamic delays based on enemy type
if (waveSpawned && enemiesSpawnedThisWave < totalEnemiesToSpawnThisWave) {
var currentEnemySpawnDelay = getEnemySpawnDelay(currentWaveType);
// Check if this is a mini-boss wave (waves 11+ but not major boss waves)
var isMajorBossWave = currentWave % 10 === 0 && currentWave > 0;
var isMiniBossWave = currentWave > 10 && !isMajorBossWave;
// Apply 10% faster spawn rate for mini-boss waves
if (isMiniBossWave) {
currentEnemySpawnDelay = Math.floor(currentEnemySpawnDelay * 0.9); // 10% faster spawning
}
// Scale the spawn delay by game speed to maintain consistent real-time intervals
var scaledSpawnDelay = Math.floor(currentEnemySpawnDelay / gameSpeed);
if (LK.ticks - lastEnemySpawnTime >= scaledSpawnDelay) {
// Check if this is the first enemy spawn and determine the enemy type
var enemyType = currentWaveType;
// Handle wave 50 special spawning sequence
if (currentWave === 50) {
// Spawn bosses in sequence: normal (wave 10), fast (wave 20), immune (wave 30), flying (wave 40)
if (enemiesSpawnedThisWave === 0) {
enemyType = 'normal'; // Wave 10 boss
} else if (enemiesSpawnedThisWave === 1) {
enemyType = 'fast'; // Wave 20 boss
} else if (enemiesSpawnedThisWave === 2) {
enemyType = 'immune'; // Wave 30 boss
} else if (enemiesSpawnedThisWave === 3) {
enemyType = 'flying'; // Wave 40 boss
}
} else if (enemiesSpawnedThisWave === 0) {
// Override enemy type for first spawn in specific wave ranges
if (currentWave >= 6 && currentWave <= 9) {
enemyType = 'swarm'; // Mini Boss
} else if (currentWave >= 11 && currentWave <= 19) {
enemyType = 'normal'; // Wave 10 Boss (as first enemy)
} else if (currentWave >= 21 && currentWave <= 29) {
enemyType = 'fast'; // Wave 20 Boss (as first enemy)
} else if (currentWave >= 31 && currentWave <= 39) {
enemyType = 'immune'; // Wave 30 Boss (as first enemy)
} else if (currentWave >= 41 && currentWave <= 49) {
enemyType = 'flying'; // Wave 40 Boss (as first enemy)
}
}
// Spawn one enemy
var enemy = new Enemy(enemyType);
// Handle wave 50 special bosses
if (currentWave === 50) {
enemy.isBoss = true;
// Set HP based on which boss in the sequence (20% increase)
if (enemiesSpawnedThisWave === 0) {
enemy.maxHealth = 480; // Wave 10 boss HP (400 * 1.2)
} else if (enemiesSpawnedThisWave === 1) {
enemy.maxHealth = 780; // Wave 20 boss HP (650 * 1.2)
} else if (enemiesSpawnedThisWave === 2) {
enemy.maxHealth = 960; // Wave 30 boss HP (800 * 1.2)
} else if (enemiesSpawnedThisWave === 3) {
enemy.maxHealth = 1200; // Wave 40 boss HP (1000 * 1.2)
}
enemy.health = enemy.maxHealth;
enemy.speed = enemy.speed * 0.7; // Boss speed
// Scale up the enemy graphics like a boss
if (enemy.children[0]) {
enemy.children[0].scaleX = 1.8;
enemy.children[0].scaleY = 1.8;
}
// Scale up shadow for flying bosses
if (enemy.isFlying && enemy.shadow && enemy.shadow.children[0]) {
enemy.shadow.children[0].scaleX = 1.8;
enemy.shadow.children[0].scaleY = 1.8;
}
} else if (enemiesSpawnedThisWave === 0) {
// Apply boss scaling to graphics for special first enemies
if (currentWave >= 6 && currentWave <= 9 || currentWave >= 11 && currentWave <= 19 || currentWave >= 21 && currentWave <= 29 || currentWave >= 31 && currentWave <= 39 || currentWave >= 41 && currentWave <= 49) {
// Scale up the enemy graphics like a boss
if (enemy.children[0]) {
enemy.children[0].scaleX = 1.8;
enemy.children[0].scaleY = 1.8;
}
// Scale up shadow for flying bosses
if (enemy.isFlying && enemy.shadow && enemy.shadow.children[0]) {
enemy.shadow.children[0].scaleX = 1.8;
enemy.shadow.children[0].scaleY = 1.8;
}
}
}
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use new HP formula: base HP + (wave number * 2)
if (enemy.isBoss) {
// No scaling for bosses - they use fixed HP values
} else if (enemiesSpawnedThisWave === 0) {
// Check if this is a special first enemy spawn
if (currentWave >= 6 && currentWave <= 9) {
// Mini boss (wave 6-9 first enemy) - Use 85 HP + wave number * 2
enemy.maxHealth = 85 + currentWave * 2;
enemy.isBoss = true; // Mark as boss for gold/score rewards
} else if (currentWave >= 11 && currentWave <= 19) {
// Wave 10 boss stats for first enemy
enemy.maxHealth = 275; // Wave 10 boss HP
enemy.speed = enemy.speed * 0.7; // Boss speed
enemy.isBoss = true; // Mark as boss
} else if (currentWave >= 21 && currentWave <= 29) {
// Wave 20 boss stats for first enemy
enemy.maxHealth = 400; // Wave 20 boss HP
enemy.speed = enemy.speed * 0.7; // Boss speed
enemy.isBoss = true; // Mark as boss
} else if (currentWave >= 31 && currentWave <= 39) {
// Wave 30 boss stats for first enemy
enemy.maxHealth = 525; // Wave 30 boss HP
enemy.speed = enemy.speed * 0.7; // Boss speed
enemy.isBoss = true; // Mark as boss
} else if (currentWave >= 41 && currentWave <= 49) {
// Wave 40 boss stats for first enemy
enemy.maxHealth = 650; // Wave 40 boss HP
enemy.speed = enemy.speed * 0.7; // Boss speed
enemy.isBoss = true; // Mark as boss
} else {
// Other first enemies get regular scaling: base HP + wave number * 2
enemy.maxHealth = enemy.maxHealth + currentWave * 2;
}
} else {
// Regular enemies: base HP + wave number * 2
enemy.maxHealth = enemy.maxHealth + currentWave * 2;
// Make all enemies 10% faster in mini-boss waves
if (isMiniBossWave) {
enemy.speed = enemy.speed * 1.1;
}
}
// HP is now set, update health to match
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// Spawn at the defined spawn point (center of grid)
var spawnPoint = pathPoints[0];
enemy.cellX = spawnPoint.x;
enemy.cellY = spawnPoint.y;
enemy.currentCellX = spawnPoint.x;
enemy.currentCellY = spawnPoint.y;
enemy.x = grid.x + spawnPoint.x * CELL_SIZE + CELL_SIZE / 2;
enemy.y = grid.y + spawnPoint.y * CELL_SIZE + CELL_SIZE / 2;
enemy.waveNumber = currentWave;
// Initialize enemy pathfinding - start with spawn exit as first target
enemy.currentTarget = spawnExit;
enemy.cellPathIndex = 1; // Index into pathPoints array
enemies.push(enemy);
// Update spawn tracking
enemiesSpawnedThisWave++;
totalEnemiesSpawnedThisWave++;
lastEnemySpawnTime = LK.ticks;
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
// Check if wave is complete: all enemies spawned AND all enemies dead
if (waveSpawned && enemiesSpawnedThisWave >= totalEnemiesToSpawnThisWave && totalEnemiesDeadThisWave >= totalEnemiesSpawnedThisWave) {
if (!waveIsComplete) {
waveIsComplete = true;
var notification = game.addChild(new Notification("Wave " + currentWave + " Complete! Next wave unlocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
waveInProgress = false;
waveSpawned = false;
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
enemy.update(scaledDt);
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Stop the walking sound loop when enemy dies
if (enemy.walkingSound) {
enemy.walkingSound.stop();
}
// Play slimewalking sound when enemy dies
LK.getSound('slimewalking').play();
// Track enemy death for wave completion
if (enemy.waveNumber === currentWave) {
totalEnemiesDeadThisWave++;
}
// Updated gold earning mechanism
var goldEarned;
var actualGoldValue; // Keep track of exact value including decimals
// Check if this is a special boss spawn that should give normal enemy gold
var isSpecialBossSpawn = false;
if (enemy.isBoss && (enemy.waveNumber >= 11 && enemy.waveNumber <= 19 || enemy.waveNumber >= 21 && enemy.waveNumber <= 29 || enemy.waveNumber >= 31 && enemy.waveNumber <= 39 || enemy.waveNumber >= 41 && enemy.waveNumber <= 49)) {
isSpecialBossSpawn = true;
}
if (enemy.isBoss && !isSpecialBossSpawn) {
// Real boss enemies: Fixed gold values per boss
if (enemy.waveNumber === 10) {
actualGoldValue = 50;
} else if (enemy.waveNumber === 20) {
actualGoldValue = 75;
} else if (enemy.waveNumber === 30) {
actualGoldValue = 100;
} else if (enemy.waveNumber === 40) {
actualGoldValue = 150;
} else if (enemy.waveNumber === 50) {
actualGoldValue = 200;
} else {
// Fallback for any other boss waves
actualGoldValue = 50;
}
goldEarned = Math.floor(actualGoldValue);
} else {
// Regular enemies and special boss spawns: base gold × (1 + wave number * 0.1) × enemy type multiplier
// Base gold calculation: 1 × (1 + wave number * 0.1)
var baseGoldValue = 1 * (1 + enemy.waveNumber * 0.1);
// Apply enemy type multipliers
var goldMultiplier;
switch (enemy.type) {
case 'swarm':
goldMultiplier = 0.5;
break;
case 'normal':
goldMultiplier = 1.0;
break;
case 'flying':
goldMultiplier = 1.0;
break;
case 'fast':
goldMultiplier = 1.0;
break;
case 'immune':
goldMultiplier = 1.0;
break;
default:
goldMultiplier = 1.0; // Default to 1.0 multiplier
break;
}
actualGoldValue = baseGoldValue * goldMultiplier;
goldEarned = Math.floor(actualGoldValue);
}
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
addGoldWithFraction(actualGoldValue);
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Stop the walking sound loop when enemy reaches base
if (enemy.walkingSound) {
enemy.walkingSound.stop();
}
// Track enemy death for wave completion (even if they reach base)
if (enemy.waveNumber === currentWave) {
totalEnemiesDeadThisWave++;
}
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
lives = Math.max(0, lives - enemy.health);
// Play damagetotower sound when base HP decreases
LK.getSound('damagetotower').play();
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
// Update towers with scaledDt
for (var i = 0; i < towers.length; i++) {
towers[i].update(scaledDt);
}
// Update bullets with scaledDt
for (var i = 0; i < bullets.length; i++) {
bullets[i].update(scaledDt);
}
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();
}
if (roadPreview.visible) {
roadPreview.updatePlacementStatus();
}
if (slowRoadPreview.visible) {
slowRoadPreview.updatePlacementStatus();
}
if (poisonRoadPreview.visible) {
poisonRoadPreview.updatePlacementStatus();
}
// Update road source buttons
for (var i = 0; i < roadSources.length; i++) {
roadSources[i].update();
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
// Spawn poison particles on poison roads continuously
// No need to check for game over - particles have their own lifetime
if (LK.ticks % 30 === 0) {
// Spawn particles every 0.5 seconds
for (var i = 0; i < grid.cells.length; i++) {
for (var j = 0; j < grid.cells[i].length; j++) {
var cell = grid.cells[i][j];
if (cell && cell.type === TYPE_POISON_ROAD) {
// 100% chance to spawn a particle on each poison road cell
grid.spawnPotionParticle(i, j);
}
if (cell && cell.type === TYPE_SLOW_ROAD) {
// 100% chance to spawn a particle on each slow road cell
grid.spawnSlowParticle(i, j);
}
}
}
}
// Check collision between base and 10hp asset
if (hpAsset && hpAsset.parent && pathPoints && pathPoints.length > 0) {
var base = pathPoints[pathPoints.length - 1]; // Get base position
var baseWorldX = grid.x + base.x * CELL_SIZE + CELL_SIZE / 2;
var baseWorldY = grid.y + base.y * CELL_SIZE + CELL_SIZE / 2;
// Calculate distance between base and 10hp asset
var dx = hpAsset.x - baseWorldX;
var dy = hpAsset.y - baseWorldY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Check if they're touching (within CELL_SIZE distance)
if (distance < CELL_SIZE) {
// Remove the 10hp asset
game.removeChild(hpAsset);
hpAsset = null;
// Add 10 HP to player base (increase current HP by 10)
lives = lives + 10; // Remove the cap to allow HP to go above 100
// Play hpgain sound when base HP increases
LK.getSound('hpgain').play();
updateUI();
// Show notification with current HP value
var notification = game.addChild(new Notification("Base healed! +10 HP (Total: " + Math.floor(lives) + " HP)"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
// Update health bar position every frame
updateHealthBarPos();
};
// Variable to track the 10hp asset
var hpAsset = null;
// Function to spawn 10hp asset at a random valid position around the base
function spawn10HPAsset() {
var baseX = 7; // Base X position
var baseY = 13; // Base Y position
var maxAttempts = 100;
var placed = false;
for (var attempt = 0; attempt < maxAttempts && !placed; attempt++) {
// Generate random position within elliptical area
// 6 cells left/right, 8 cells up/down
var randomX = baseX + Math.floor(Math.random() * 13) - 6; // -6 to +6
var randomY = baseY + Math.floor(Math.random() * 17) - 8; // -8 to +8
// Check if position is within elliptical bounds
var normalizedX = (randomX - baseX) / 6;
var normalizedY = (randomY - baseY) / 8;
var distanceSquared = normalizedX * normalizedX + normalizedY * normalizedY;
if (distanceSquared <= 1) {
// Within ellipse
var cell = grid.getCell(randomX, randomY);
if (cell && cell.type === TYPE_WALL && !cell.hasTower) {
// Valid position found - create 10hp asset
hpAsset = game.attachAsset('10hp', {
anchorX: 0.5,
anchorY: 0.5
});
// Position at center of cell, adjusted 166 pixels up and 55 pixels left (moved 111 pixels up)
hpAsset.x = grid.x + randomX * CELL_SIZE + CELL_SIZE / 2 - 55; // -75 + 20 = -55
hpAsset.y = grid.y + randomY * CELL_SIZE + CELL_SIZE / 2 - 166; // -55 - 111 = -166
placed = true;
}
}
}
}
// Spawn the 10hp asset at game start
spawn10HPAsset(); ===================================================================
--- original.js
+++ change.js
@@ -3516,9 +3516,9 @@
}
}
}
}
- // Mark static forbidden zone row at fixed position (row 22)
+ // Mark static forbidden zone row at fixed position (row 22) - 9 cells below base
var staticForbiddenRow = 22; // Fixed row position that never changes
for (var i = 0; i < grid.cells.length; i++) {
var cell = grid.getCell(i, staticForbiddenRow);
if (cell && cell.debugCell && cell.debugCell.children[0]) {
@@ -3532,8 +3532,24 @@
cell.isForbiddenZone = true;
cell.type = 99; // TYPE_BLOCKED: not walkable, not buildable
}
}
+ // Mark static forbidden zone row 10 cells above base at fixed position (row 3)
+ var staticForbiddenRowAbove = 3; // Fixed row position 10 cells above base
+ for (var i = 0; i < grid.cells.length; i++) {
+ var cell = grid.getCell(i, staticForbiddenRowAbove);
+ if (cell && cell.debugCell && cell.debugCell.children[0]) {
+ // Set color to black
+ cell.debugCell.children[0].tint = 0x000000;
+ // Mark as forbidden for road/tower
+ cell.isForbiddenZone = true;
+ cell.type = 99; // TYPE_BLOCKED: not walkable, not buildable
+ } else if (cell) {
+ // Even if no debugCell, still mark as forbidden
+ cell.isForbiddenZone = true;
+ cell.type = 99; // TYPE_BLOCKED: not walkable, not buildable
+ }
+ }
}
// Initial forbidden zone marking
updateForbiddenZone();
grid.pathFind();
17. century tower castle square shaped 1x1 medeval military base,grey coloured top-down look , simple design, medieval-semirealistic.. In-Game asset. 2d. High contrast. No shadows
17.th century cannon bullet semirealistic medieval cannon bullet. In-Game asset. 2d. High contrast. No shadows
top down look for basic cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
red coloured top down look for quick cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows.
armor grey coloured top down look for armoured basic cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
swarm enemy light green coloured top down look for quick cute semi realistic round shaped anime enemy for basic animation like enemy. In-Game asset. 2d. High contrast. No shadows.
snow white
top down look square shaped rock. In-Game asset. 2d. High contrast. No shadows
top down look square shaped obsidian rock. In-Game asset. 2d. High contrast. No shadows