User prompt
başlangıçta 130 altın ver
User prompt
snake in yılanı, assets deki yilan resmine benzesin
User prompt
snake in yılanı şuan mor görünüyor. bunun yerine gözleri olan yeşil renkte hareket eden gerçekçi bir yılan figürü yap
User prompt
snake in yılanı daha büyük olsun
User prompt
snape ateş ettiğinde bir yılan oluşsun ve düşmanların üzerinde dolaşarak ateş etsin. ağzından alev çıksın
User prompt
harry ateş ettiğinde düşmanlar ikiye bölünsün ve çoğalsın ama canları yarım güçte olsun daha kolay ölsünler
User prompt
harmony nin yavaşlatma gücü dolduğunda karakterin yanında ingilizce ‘ üzerine bas ve yavaşlat’ yazısı çıksın
User prompt
100 resmi ekranın tamamını kaplasın
User prompt
neden karakterler büyüdü eski hallerine getir
User prompt
resimleri netleştir 100 resmindeki oyundaki resimleri örnek al kendi yapay zekanı da kullan
User prompt
100 resmini kendi yapay zekanı kullanarak biraz daha netleştir bozmuşsun
User prompt
100 resmini ekranın tamamına kaplat
User prompt
100 resmini hogwartsundderattack resminden sonra göster. ona tıkladıktan sonra oyun başlasın
User prompt
neden görünmüyor 100 resmi
User prompt
100 resmi hagwardunderattack resminden sonra görünsün
User prompt
2. sırada 100 resmi görünsün
User prompt
100 resmine tıklayınca hogwartsunderataack resmine geç
User prompt
100 resmini kendi yapay zekanı kullanarak ekranın tamamına kapla görüntü kalitesini yükselt yazıları netleştir
User prompt
hogwartsunderattact resminden önce 100 resmi görünsün
User prompt
oyun ilk açıldığında 100 resmini göster
User prompt
oyunda ekranı karartmayı iptal et
User prompt
oyunda bir sorun var düzgün ilerlemiyor
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'self.fastWaveWarningShown')' in or related to this line: 'if (isFastWave && !self.fastWaveWarningShown) {' Line Number: 3930
User prompt
Before you go to fast wave, show a warning that the enemies that will come are very fast, you can change the position of the characters, and you have a slowing feature with hormony. After this warning, wait three seconds and the enemies will come.
User prompt
Please fix the bug: 'TypeError: undefined is not an object (evaluating 'self.fastWaveWarningShown')' in or related to this line: 'if (currentWave === 2 && !self.fastWaveWarningShown) {' Line Number: 3914
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var BackgroundFlame = Container.expand(function (x, y) {
var self = Container.call(this);
self.x = x;
self.y = y;
var flameGraphics = self.attachAsset('flame', {
anchorX: 0.5,
anchorY: 1.0
});
// Random flame colors (orange to red)
var flameColors = [0xff6b35, 0xff4500, 0xff6347, 0xdc143c, 0xb22222];
flameGraphics.tint = flameColors[Math.floor(Math.random() * flameColors.length)];
// Random size variation
var sizeVariation = 0.5 + Math.random() * 0.8; // 0.5 to 1.3 scale
flameGraphics.scaleX = sizeVariation;
flameGraphics.scaleY = sizeVariation;
// Start invisible
self.alpha = 0;
self.scaleX = 0.1;
self.scaleY = 0.1;
// Animate appearance
var appearDuration = 800 + Math.random() * 600; // 800-1400ms
tween(self, {
alpha: 0.3 + Math.random() * 0.4,
// 0.3 to 0.7 alpha
scaleX: sizeVariation,
scaleY: sizeVariation
}, {
duration: appearDuration,
easing: tween.easeOut,
onFinish: function onFinish() {
// Flicker for a while
self.startFlickering();
}
});
self.startFlickering = function () {
if (!self.parent) return; // Stop if destroyed
// Random flicker duration
var flickerTime = 2000 + Math.random() * 3000; // 2-5 seconds
var flickerInterval = 80 + Math.random() * 120; // Flicker every 80-200ms
var flickerTimer = LK.setInterval(function () {
if (!self.parent) {
LK.clearInterval(flickerTimer);
return;
}
// Quick flicker animation
var currentAlpha = self.alpha;
var flickerAlpha = Math.max(0.1, currentAlpha - 0.15 + Math.random() * 0.3);
tween(self, {
alpha: flickerAlpha
}, {
duration: 60,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
tween(self, {
alpha: currentAlpha
}, {
duration: 60,
easing: tween.easeInOut
});
}
});
}, flickerInterval);
// Start disappearing after flicker time
LK.setTimeout(function () {
if (!self.parent) return;
LK.clearInterval(flickerTimer);
self.startDisappearing();
}, flickerTime);
};
self.startDisappearing = function () {
if (!self.parent) return; // Stop if destroyed
var disappearDuration = 600 + Math.random() * 400; // 600-1000ms
tween(self, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: disappearDuration,
easing: tween.easeIn,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
};
return self;
});
var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
var self = Container.call(this);
self.targetEnemy = targetEnemy;
self.damage = damage || 10;
self.speed = speed || 5;
self.x = startX;
self.y = startY;
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
// Store reference for customization
self.bulletGraphics = bulletGraphics;
self.update = function () {
if (!self.targetEnemy || !self.targetEnemy.parent) {
self.destroy();
return;
}
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < self.speed) {
// Apply damage to target enemy
self.targetEnemy.health -= self.damage;
if (self.targetEnemy.health <= 0) {
self.targetEnemy.health = 0;
} else {
self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
}
// If this is a sniper (Harry) bullet and enemy survives, split it
if (self.type === 'sniper' && self.targetEnemy.health > 0 && !self.targetEnemy.hasBeenSplit) {
// Mark this enemy as split to prevent infinite splitting
self.targetEnemy.hasBeenSplit = true;
// Create two new enemies with half the remaining health
var splitHealth = self.targetEnemy.health / 2;
for (var splitIndex = 0; splitIndex < 2; splitIndex++) {
var newEnemy = new Enemy(self.targetEnemy.type);
// Copy properties from original enemy
newEnemy.maxHealth = splitHealth;
newEnemy.health = splitHealth;
newEnemy.speed = self.targetEnemy.speed;
newEnemy.hasBeenSplit = true; // Prevent further splitting
newEnemy.waveNumber = self.targetEnemy.waveNumber;
// Copy status effects
if (self.targetEnemy.slowed) {
newEnemy.slowed = true;
newEnemy.slowDuration = self.targetEnemy.slowDuration;
newEnemy.originalSpeed = self.targetEnemy.originalSpeed;
newEnemy.speed = self.targetEnemy.speed;
}
if (self.targetEnemy.poisoned) {
newEnemy.poisoned = true;
newEnemy.poisonDuration = self.targetEnemy.poisonDuration;
newEnemy.poisonDamage = self.targetEnemy.poisonDamage;
}
// Position the new enemies slightly offset from original
var offsetX = splitIndex === 0 ? -0.5 : 0.5;
var offsetY = (Math.random() - 0.5) * 0.5;
newEnemy.cellX = self.targetEnemy.cellX;
newEnemy.cellY = self.targetEnemy.cellY;
newEnemy.currentCellX = self.targetEnemy.currentCellX + offsetX;
newEnemy.currentCellY = self.targetEnemy.currentCellY + offsetY;
newEnemy.x = grid.x + newEnemy.currentCellX * CELL_SIZE;
newEnemy.y = grid.y + newEnemy.currentCellY * CELL_SIZE;
// Copy flying target if applicable
if (self.targetEnemy.isFlying && self.targetEnemy.flyingTarget) {
newEnemy.flyingTarget = self.targetEnemy.flyingTarget;
}
// Add to appropriate layer
if (newEnemy.isFlying) {
enemyLayerTop.addChild(newEnemy);
if (newEnemy.shadow) {
enemyLayerMiddle.addChild(newEnemy.shadow);
}
} else {
enemyLayerBottom.addChild(newEnemy);
}
// Add to enemies array
enemies.push(newEnemy);
// Update health bar
newEnemy.healthBar.width = newEnemy.health / newEnemy.maxHealth * 70;
}
// Remove the original enemy
self.targetEnemy.health = 0;
}
// Apply special effects based on bullet type
if (self.type === 'splash') {
// Create visual splash effect
var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
game.addChild(splashEffect);
// Splash damage to nearby enemies
var splashRadius = CELL_SIZE * 1.5;
for (var i = 0; i < enemies.length; i++) {
var otherEnemy = enemies[i];
if (otherEnemy !== self.targetEnemy) {
var splashDx = otherEnemy.x - self.targetEnemy.x;
var splashDy = otherEnemy.y - self.targetEnemy.y;
var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
if (splashDistance <= splashRadius) {
// Apply splash damage (50% of original damage)
otherEnemy.health -= self.damage * 0.5;
if (otherEnemy.health <= 0) {
otherEnemy.health = 0;
} else {
otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
}
}
}
}
} else if (self.type === 'slow') {
// Prevent slow effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual slow effect
var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
game.addChild(slowEffect);
// Apply slow effect
// Make slow percentage scale with tower level (default 50%, up to 80% at max level)
var slowPct = 0.5;
if (self.sourceTowerLevel !== undefined) {
// Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
slowPct = slowLevels[idx];
}
if (!self.targetEnemy.slowed) {
self.targetEnemy.originalSpeed = self.targetEnemy.speed;
self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
self.targetEnemy.slowed = true;
self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
} else {
self.targetEnemy.slowDuration = 180; // Reset duration
}
}
} else if (self.type === 'poison') {
// Prevent poison effect on immune enemies
if (!self.targetEnemy.isImmune) {
// Create visual poison effect
var poisonEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'poison');
game.addChild(poisonEffect);
// Apply poison effect
self.targetEnemy.poisoned = true;
self.targetEnemy.poisonDamage = self.damage * 0.2; // 20% of original damage per tick
self.targetEnemy.poisonDuration = 300; // 5 seconds at 60 FPS
}
} else if (self.type === 'sniper') {
// Create visual critical hit effect for sniper
var sniperEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'sniper');
game.addChild(sniperEffect);
}
self.destroy();
} else {
var angle = Math.atan2(dy, dx);
self.x += Math.cos(angle) * self.speed;
self.y += Math.sin(angle) * self.speed;
}
};
return self;
});
var DebugCell = Container.expand(function () {
var self = Container.call(this);
var cellGraphics = self.attachAsset('cell', {
anchorX: 0.5,
anchorY: 0.5
});
// Remove the random tint to show the stone texture properly
cellGraphics.tint = 0xFFFFFF;
cellGraphics.alpha = 0.8;
var debugArrows = [];
var numberLabel = new Text2('0', {
size: 30,
fill: 0xFFFFFF,
weight: 800
});
numberLabel.anchor.set(.5, .5);
numberLabel.visible = false;
self.addChild(numberLabel);
self.update = function () {};
self.down = function () {
return;
if (self.cell.type == 0 || self.cell.type == 1) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
if (grid.pathFind()) {
self.cell.type = self.cell.type == 1 ? 0 : 1;
grid.pathFind();
var notification = game.addChild(new Notification("Path is blocked!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
grid.renderDebug();
}
};
self.removeArrows = function () {
while (debugArrows.length) {
self.removeChild(debugArrows.pop());
}
};
self.render = function (data) {
switch (data.type) {
case 0:
case 2:
{
if (data.pathId != pathId) {
self.removeArrows();
numberLabel.setText("-");
// Dark reddish stone for non-path cells
cellGraphics.tint = 0x8B4513;
cellGraphics.alpha = 0.5;
return;
}
numberLabel.visible = false;
var tint = Math.floor(data.score / maxScore * 0x88);
var towerInRangeHighlight = false;
if (selectedTower && data.towersInRange && data.towersInRange.indexOf(selectedTower) !== -1) {
towerInRangeHighlight = true;
cellGraphics.tint = 0x0088ff;
cellGraphics.alpha = 0.6;
} else {
// Apply subtle tinting to the stone texture based on path score
var pathTint = 0x88 - tint << 8 | tint;
// Blend the tint with the stone texture color
cellGraphics.tint = 0xD2B48C; // Light tan/stone color
cellGraphics.alpha = 0.7 + tint / 0x88 * 0.3; // Vary alpha based on path score
}
// Arrows removed - no visual indicators
break;
}
case 1:
{
self.removeArrows();
// Darker stone for walls
cellGraphics.tint = 0x654321;
cellGraphics.alpha = 0.9;
numberLabel.visible = false;
break;
}
case 3:
{
self.removeArrows();
// Goal cells with greenish stone tint
cellGraphics.tint = 0x8FBC8F;
cellGraphics.alpha = 0.8;
numberLabel.visible = false;
break;
}
}
// Number labels removed
};
});
// 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;
self.cellX = 0;
self.cellY = 0;
self.currentCellX = 0;
self.currentCellY = 0;
self.currentTarget = undefined;
self.maxHealth = 100;
self.health = self.maxHealth;
self.bulletsTargetingThis = [];
self.waveNumber = currentWave;
self.isFlying = false;
self.isImmune = false;
self.isBoss = false;
// Check if this is a boss wave
// Check if this is a boss wave
// Apply different stats based on enemy type
switch (self.type) {
case 'fast':
self.speed *= 6; // Three times as fast (was 2x, now 6x total)
self.maxHealth = 50; // Half health (was 100, now 50)
break;
case 'immune':
self.isImmune = true;
self.maxHealth = 80;
break;
case 'flying':
self.isFlying = true;
self.maxHealth = 80;
break;
case 'swarm':
self.maxHealth = 50; // Weaker enemies
break;
case 'normal':
default:
// Normal enemy uses default values
break;
}
if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
self.isBoss = true;
// Boss enemies have 20x health and are larger
self.maxHealth *= 20;
// Slower speed for bosses
self.speed = self.speed * 0.7;
}
// Make all enemies 25% faster
self.speed = self.speed * 1.25;
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
});
self.enemyGraphics = enemyGraphics; // Store reference for walking animation
// Scale up boss enemies
if (self.isBoss) {
enemyGraphics.scaleX = 1.8;
enemyGraphics.scaleY = 1.8;
}
// Dark wizard enemies with magical effects
// Different wizard types with distinct magical auras
/*switch (self.type) {
case 'fast':
enemyGraphics.tint = 0x3742fa; // Blue aura for fast dark wizards
break;
case 'immune':
enemyGraphics.tint = 0xff3838; // Red aura for immune dark wizards
break;
case 'flying':
enemyGraphics.tint = 0xf1c40f; // Golden aura for broom-riding wizards
break;
case 'swarm':
enemyGraphics.tint = 0x8c7ae6; // Purple aura for swarm dark wizards
break;
}*/
// Create shadow for flying enemies
if (self.isFlying) {
// Create a shadow container that will be added to the shadow layer
self.shadow = new Container();
// Clone the enemy graphics for the shadow
var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply shadow effect
shadowGraphics.tint = 0x000000; // Black shadow
shadowGraphics.alpha = 0.4; // Semi-transparent
// If this is a boss, scale up the shadow to match
if (self.isBoss) {
shadowGraphics.scaleX = 1.8;
shadowGraphics.scaleY = 1.8;
}
// Position shadow slightly offset
self.shadow.x = 20; // Offset right
self.shadow.y = 20; // Offset down
// Ensure shadow has the same rotation as the enemy
shadowGraphics.rotation = enemyGraphics.rotation;
}
var healthBarOutline = self.attachAsset('healthBarOutline', {
anchorX: 0,
anchorY: 0.5
});
var healthBarBG = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
var healthBar = self.attachAsset('healthBar', {
anchorX: 0,
anchorY: 0.5
});
healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
healthBarOutline.x = -healthBarOutline.width / 2;
healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
healthBar.tint = 0x00ff00;
healthBarBG.tint = 0xff0000;
self.healthBar = healthBar;
self.update = function () {
if (self.health <= 0) {
self.health = 0;
self.healthBar.width = 0;
}
// Handle slow effect
if (self.isImmune) {
// Immune enemies cannot be slowed or poisoned, clear any such effects
self.slowed = false;
self.slowEffect = false;
self.poisoned = false;
self.poisonEffect = false;
// Reset speed to original if needed
if (self.originalSpeed !== undefined) {
self.speed = self.originalSpeed;
}
} else {
// Handle slow effect
if (self.slowed) {
// Visual indication of slowed status
if (!self.slowEffect) {
self.slowEffect = true;
}
self.slowDuration--;
if (self.slowDuration <= 0) {
self.speed = self.originalSpeed;
self.slowed = false;
self.slowEffect = false;
// Only reset tint if not poisoned
if (!self.poisoned) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
// Handle poison effect
if (self.poisoned) {
// Visual indication of poisoned status
if (!self.poisonEffect) {
self.poisonEffect = true;
}
// Apply poison damage every 30 frames (twice per second)
if (LK.ticks % 30 === 0) {
self.health -= self.poisonDamage;
if (self.health <= 0) {
self.health = 0;
}
self.healthBar.width = self.health / self.maxHealth * 70;
}
self.poisonDuration--;
if (self.poisonDuration <= 0) {
self.poisoned = false;
self.poisonEffect = false;
// Only reset tint if not slowed
if (!self.slowed) {
enemyGraphics.tint = 0xFFFFFF; // Reset tint
}
}
}
}
// Set tint based on effect status
if (self.isImmune) {
enemyGraphics.tint = 0xFFFFFF;
} else if (self.poisoned && self.slowed) {
// Combine poison (0x00FFAA) and slow (0x9900FF) colors
// Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
enemyGraphics.tint = 0x4C7FD4;
} else if (self.poisoned) {
enemyGraphics.tint = 0x00FFAA;
} else if (self.slowed) {
enemyGraphics.tint = 0x9900FF;
} else {
enemyGraphics.tint = 0xFFFFFF;
}
// Add walking animation effect
if (!self.walkingAnimation && self.speed > 0) {
// Create a subtle bobbing effect for walking
var _createWalkingAnimation = function createWalkingAnimation() {
if (!self.parent) return; // Stop if enemy is destroyed
// Adjust animation speed based on enemy speed
var animDuration = self.slowed ? 400 : 200;
if (self.type === 'fast') animDuration = 100;
// Vertical bobbing motion
tween(enemyGraphics, {
y: -5
}, {
duration: animDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
tween(enemyGraphics, {
y: 0
}, {
duration: animDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
_createWalkingAnimation(); // Continue the animation loop
}
});
}
});
};
self.walkingAnimation = true;
_createWalkingAnimation();
}
if (self.currentTarget) {
var ox = self.currentTarget.x - self.currentCellX;
var oy = self.currentTarget.y - self.currentCellY;
if (ox !== 0 || oy !== 0) {
var angle = Math.atan2(oy, ox);
if (enemyGraphics.targetRotation === undefined) {
enemyGraphics.targetRotation = angle;
enemyGraphics.rotation = angle;
} else {
if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
tween.stop(enemyGraphics, {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemyGraphics.rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
enemyGraphics.targetRotation = angle;
tween(enemyGraphics, {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
}
}
healthBarOutline.y = healthBarBG.y = healthBar.y = -enemyGraphics.height / 2 - 10;
};
return self;
});
var GoldIndicator = Container.expand(function (value, x, y) {
var self = Container.call(this);
var shadowText = new Text2("+" + value, {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.addChild(shadowText);
var goldText = new Text2("+" + value, {
size: 45,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
self.addChild(goldText);
self.x = x;
self.y = y;
self.alpha = 0;
self.scaleX = 0.5;
self.scaleY = 0.5;
tween(self, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
y: y - 40
}, {
duration: 50,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
y: y - 80
}, {
duration: 600,
easing: tween.easeIn,
delay: 800,
onFinish: function onFinish() {
self.destroy();
}
});
}
});
return self;
});
var Grid = Container.expand(function (gridWidth, gridHeight) {
var self = Container.call(this);
self.cells = [];
self.spawns = [];
self.goals = [];
for (var i = 0; i < gridWidth; i++) {
self.cells[i] = [];
for (var j = 0; j < gridHeight; j++) {
self.cells[i][j] = {
score: 0,
pathId: 0,
towersInRange: []
};
}
}
/*
Cell Types
0: Transparent floor
1: Wall
2: Spawn
3: Goal
*/
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var cell = self.cells[i][j];
var cellType = i === 0 || i === gridWidth - 1 || j <= 4 || j >= gridHeight - 4 ? 1 : 0;
if (i > 11 - 3 && i <= 11 + 3) {
if (j === 0) {
cellType = 2;
self.spawns.push(cell);
} else if (j <= 4) {
cellType = 0;
} else if (j === gridHeight - 1) {
cellType = 3;
self.goals.push(cell);
} else if (j >= gridHeight - 4) {
cellType = 0;
}
}
cell.type = cellType;
cell.x = i;
cell.y = j;
cell.upLeft = self.cells[i - 1] && self.cells[i - 1][j - 1];
cell.up = self.cells[i - 1] && self.cells[i - 1][j];
cell.upRight = self.cells[i - 1] && self.cells[i - 1][j + 1];
cell.left = self.cells[i][j - 1];
cell.right = self.cells[i][j + 1];
cell.downLeft = self.cells[i + 1] && self.cells[i + 1][j - 1];
cell.down = self.cells[i + 1] && self.cells[i + 1][j];
cell.downRight = self.cells[i + 1] && self.cells[i + 1][j + 1];
cell.neighbors = [cell.upLeft, cell.up, cell.upRight, cell.right, cell.downRight, cell.down, cell.downLeft, cell.left];
cell.targets = [];
if (j > 3 && j <= gridHeight - 4) {
var debugCell = new DebugCell();
self.addChild(debugCell);
debugCell.cell = cell;
debugCell.x = i * CELL_SIZE;
debugCell.y = j * CELL_SIZE;
cell.debugCell = debugCell;
}
}
}
self.getCell = function (x, y) {
return self.cells[x] && self.cells[x][y];
};
self.pathFind = function () {
var before = new Date().getTime();
var toProcess = self.goals.concat([]);
maxScore = 0;
pathId += 1;
for (var a = 0; a < toProcess.length; a++) {
toProcess[a].pathId = pathId;
}
function processNode(node, targetValue, targetNode) {
if (node && node.type != 1) {
if (node.pathId < pathId || targetValue < node.score) {
node.targets = [targetNode];
} else if (node.pathId == pathId && targetValue == node.score) {
node.targets.push(targetNode);
}
if (node.pathId < pathId || targetValue < node.score) {
node.score = targetValue;
if (node.pathId != pathId) {
toProcess.push(node);
}
node.pathId = pathId;
if (targetValue > maxScore) {
maxScore = targetValue;
}
}
}
}
while (toProcess.length) {
var nodes = toProcess;
toProcess = [];
for (var a = 0; a < nodes.length; a++) {
var node = nodes[a];
var targetScore = node.score + 14142;
if (node.up && node.left && node.up.type != 1 && node.left.type != 1) {
processNode(node.upLeft, targetScore, node);
}
if (node.up && node.right && node.up.type != 1 && node.right.type != 1) {
processNode(node.upRight, targetScore, node);
}
if (node.down && node.right && node.down.type != 1 && node.right.type != 1) {
processNode(node.downRight, targetScore, node);
}
if (node.down && node.left && node.down.type != 1 && node.left.type != 1) {
processNode(node.downLeft, targetScore, node);
}
targetScore = node.score + 10000;
processNode(node.up, targetScore, node);
processNode(node.right, targetScore, node);
processNode(node.down, targetScore, node);
processNode(node.left, targetScore, node);
}
}
for (var a = 0; a < self.spawns.length; a++) {
if (self.spawns[a].pathId != pathId) {
console.warn("Spawn blocked");
return true;
}
}
for (var a = 0; a < enemies.length; a++) {
var enemy = enemies[a];
// Skip enemies that haven't entered the viewable area yet
if (enemy.currentCellY < 4) {
continue;
}
// Skip flying enemies from path check as they can fly over obstacles
if (enemy.isFlying) {
continue;
}
var target = self.getCell(enemy.cellX, enemy.cellY);
if (enemy.currentTarget) {
if (enemy.currentTarget.pathId != pathId) {
if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 1 ");
return true;
}
}
} else if (!target || target.pathId != pathId) {
console.warn("Enemy blocked 2");
return true;
}
}
console.log("Speed", new Date().getTime() - before);
};
self.renderDebug = function () {
for (var i = 0; i < gridWidth; i++) {
for (var j = 0; j < gridHeight; j++) {
var debugCell = self.cells[i][j].debugCell;
if (debugCell) {
debugCell.render(self.cells[i][j]);
}
}
}
};
self.updateEnemy = function (enemy) {
var cell = grid.getCell(enemy.cellX, enemy.cellY);
if (cell.type == 3) {
return true;
}
if (enemy.isFlying && enemy.shadow) {
enemy.shadow.x = enemy.x + 20; // Match enemy x-position + offset
enemy.shadow.y = enemy.y + 20; // Match enemy y-position + offset
// Match shadow rotation with enemy rotation
if (enemy.children[0] && enemy.shadow.children[0]) {
enemy.shadow.children[0].rotation = enemy.children[0].rotation;
}
}
// Check if the enemy has reached the entry area (y position is at least 5)
var hasReachedEntryArea = enemy.currentCellY >= 4;
// If enemy hasn't reached the entry area yet, just move down vertically
if (!hasReachedEntryArea) {
// Move directly downward
enemy.currentCellY += enemy.speed;
// Rotate enemy graphic to face downward (PI/2 radians = 90 degrees)
var angle = Math.PI / 2;
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update enemy's position
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// If enemy has now reached the entry area, update cell coordinates
if (enemy.currentCellY >= 4) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
}
return false;
}
// After reaching entry area, handle flying enemies differently
if (enemy.isFlying) {
// Flying enemies head straight to the closest goal
if (!enemy.flyingTarget) {
// Set flying target to the closest goal
enemy.flyingTarget = self.goals[0];
// Find closest goal if there are multiple
if (self.goals.length > 1) {
var closestDist = Infinity;
for (var i = 0; i < self.goals.length; i++) {
var goal = self.goals[i];
var dx = goal.x - enemy.cellX;
var dy = goal.y - enemy.cellY;
var dist = dx * dx + dy * dy;
if (dist < closestDist) {
closestDist = dist;
enemy.flyingTarget = goal;
}
}
}
}
// Move directly toward the goal
var ox = enemy.flyingTarget.x - enemy.currentCellX;
var oy = enemy.flyingTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
// Reached the goal
return true;
}
var angle = Math.atan2(oy, ox);
// Rotate enemy graphic to match movement direction
if (enemy.children[0] && enemy.children[0].targetRotation === undefined) {
enemy.children[0].targetRotation = angle;
enemy.children[0].rotation = angle;
} else if (enemy.children[0]) {
if (Math.abs(angle - enemy.children[0].targetRotation) > 0.05) {
tween.stop(enemy.children[0], {
rotation: true
});
// Calculate the shortest angle to rotate
var currentRotation = enemy.children[0].rotation;
var angleDiff = angle - currentRotation;
// Normalize angle difference to -PI to PI range for shortest path
while (angleDiff > Math.PI) {
angleDiff -= Math.PI * 2;
}
while (angleDiff < -Math.PI) {
angleDiff += Math.PI * 2;
}
// Set target rotation and animate to it
enemy.children[0].targetRotation = angle;
tween(enemy.children[0], {
rotation: currentRotation + angleDiff
}, {
duration: 250,
easing: tween.easeOut
});
}
}
// Update the cell position to track where the flying enemy is
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
// Update shadow position if this is a flying enemy
return false;
}
// Handle normal pathfinding enemies
if (!enemy.currentTarget) {
enemy.currentTarget = cell.targets[0];
}
if (enemy.currentTarget) {
if (cell.score < enemy.currentTarget.score) {
enemy.currentTarget = cell;
}
var ox = enemy.currentTarget.x - enemy.currentCellX;
var oy = enemy.currentTarget.y - enemy.currentCellY;
var dist = Math.sqrt(ox * ox + oy * oy);
if (dist < enemy.speed) {
enemy.cellX = Math.round(enemy.currentCellX);
enemy.cellY = Math.round(enemy.currentCellY);
enemy.currentTarget = undefined;
return;
}
var angle = Math.atan2(oy, ox);
enemy.currentCellX += Math.cos(angle) * enemy.speed;
enemy.currentCellY += Math.sin(angle) * enemy.speed;
}
enemy.x = grid.x + enemy.currentCellX * CELL_SIZE;
enemy.y = grid.y + enemy.currentCellY * CELL_SIZE;
};
});
var NextWaveButton = Container.expand(function () {
var self = Container.call(this);
var buttonBackground = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 300;
buttonBackground.height = 100;
buttonBackground.tint = 0x0088FF;
var buttonText = new Text2("Next Wave", {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
buttonText.anchor.set(0.5, 0.5);
self.addChild(buttonText);
self.enabled = false;
self.visible = false;
self.update = function () {
if (waveIndicator && waveIndicator.gameStarted && currentWave < totalWaves) {
self.enabled = true;
self.visible = true;
buttonBackground.tint = 0x0088FF;
self.alpha = 1;
} else {
self.enabled = false;
self.visible = false;
buttonBackground.tint = 0x888888;
self.alpha = 0.7;
}
};
self.down = function () {
if (!self.enabled) {
return;
}
if (waveIndicator.gameStarted && currentWave < totalWaves) {
currentWave++; // Increment to the next wave directly
waveTimer = 0; // Reset wave timer
waveInProgress = true;
waveSpawned = false;
// Get the type of the current wave (which is now the next wave)
var waveType = waveIndicator.getWaveTypeName(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) activated!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
};
return self;
});
var Notification = Container.expand(function (message) {
var self = Container.call(this);
var notificationGraphics = self.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
var notificationText = new Text2(message, {
size: 50,
fill: 0x000000,
weight: 800
});
notificationText.anchor.set(0.5, 0.5);
notificationGraphics.width = notificationText.width + 30;
self.addChild(notificationText);
self.alpha = 1;
self.fadeOutTime = 120; // Default fade out time
self.update = function () {
if (self.fadeOutTime > 0) {
self.fadeOutTime--;
self.alpha = Math.min(self.fadeOutTime / 120 * 2, 1);
} else {
self.destroy();
}
};
return self;
});
var SmokeParticle = Container.expand(function (startX, startY, targetX, targetY, color) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
// Create multiple smoke tendrils for realistic wispy smoke
var smokeTendrils = [];
var numTendrils = 3 + Math.floor(Math.random() * 3); // 3-5 tendrils per smoke
for (var i = 0; i < numTendrils; i++) {
var tendrilContainer = new Container();
self.addChild(tendrilContainer);
// Create elongated smoke shapes using multiple stretched circles
var numSegments = 4 + Math.floor(Math.random() * 3); // 4-6 segments per tendril
var segmentGraphics = [];
for (var j = 0; j < numSegments; j++) {
var segment = tendrilContainer.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
// Create elongated, thin smoke shapes
var baseWidth = 80 + Math.random() * 120 - j * 15; // Tapers towards end
var baseHeight = 250 + Math.random() * 350 + j * 50; // Gets longer
segment.width = baseWidth;
segment.height = baseHeight;
// Position segments to create continuous tendril
segment.y = -j * (baseHeight * 0.3); // Overlap segments
segment.x = (Math.random() - 0.5) * 30 * j; // Slight sideways drift
// Dark, horror-themed smoke colors
var smokeType = Math.random();
if (smokeType < 0.4) {
// Very dark grey-black smoke (40% chance)
var darkness = 0x08 + Math.floor(Math.random() * 0x18);
segment.tint = darkness << 16 | darkness << 8 | darkness;
} else if (smokeType < 0.6) {
// Dark purple-black smoke for horror effect (20% chance)
var purple = 0x20 + Math.floor(Math.random() * 0x20);
var darkness = 0x10 + Math.floor(Math.random() * 0x10);
segment.tint = purple << 16 | darkness << 8 | purple;
} else if (smokeType < 0.8) {
// Dark red-black smoke (20% chance)
var red = 0x30 + Math.floor(Math.random() * 0x20);
var darkness = 0x08 + Math.floor(Math.random() * 0x10);
segment.tint = red << 16 | darkness << 8 | darkness;
} else {
// Ashy grey smoke (20% chance)
var grayValue = 0x30 + Math.floor(Math.random() * 0x30);
segment.tint = grayValue << 16 | grayValue << 8 | grayValue;
}
// Vary alpha for depth - more transparent at the edges
segment.alpha = 0.4 - j * 0.08 - Math.abs(i - numTendrils / 2) * 0.1;
segment.blendMode = 1; // Additive blend for ethereal effect
segmentGraphics.push(segment);
}
// Random rotation for each tendril
tendrilContainer.rotation = (Math.random() - 0.5) * Math.PI * 0.4;
// Offset tendrils for natural spread
tendrilContainer.x = (i - numTendrils / 2) * 60 + (Math.random() - 0.5) * 40;
tendrilContainer.y = (Math.random() - 0.5) * 100;
smokeTendrils.push({
container: tendrilContainer,
segments: segmentGraphics,
baseRotation: tendrilContainer.rotation,
swaySpeed: 0.02 + Math.random() * 0.03,
swayAmount: 0.1 + Math.random() * 0.2
});
}
// Add dark wisps occasionally for extra horror effect
if (Math.random() < 0.4) {
var wispLayer = new Container();
self.addChild(wispLayer);
var wisp = wispLayer.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
wisp.width = 40 + Math.random() * 60;
wisp.height = 200 + Math.random() * 300;
// Very dark wisp
wisp.tint = 0x000000;
wisp.alpha = 0.3;
wisp.blendMode = 1;
wispLayer.x = (Math.random() - 0.5) * 150;
wispLayer.y = -100 - Math.random() * 200;
wispLayer.rotation = (Math.random() - 0.5) * Math.PI * 0.6;
}
// Start with 0 alpha and small scale
self.alpha = 0;
self.scaleX = 0.3;
self.scaleY = 0.2;
// Calculate movement direction with creeping motion
var dx = targetX - startX;
var dy = targetY - startY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Add strong upward bias and sideways drift for smoke
var windDrift = (Math.random() - 0.5) * 400;
var upwardForce = -150 - Math.random() * 100; // Strong upward movement
// Animate appearance with slow, creeping emergence
tween(self, {
alpha: 0.85,
scaleX: 0.8,
scaleY: 1.2,
x: startX + dx * 0.1 + windDrift * 0.2,
y: startY + dy * 0.1 + upwardForce * 0.3
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Continue creeping and growing
tween(self, {
alpha: 0.6,
scaleX: 1.2,
scaleY: 1.6,
x: startX + dx * 0.3 + windDrift * 0.6,
y: startY + dy * 0.3 + upwardForce * 0.7
}, {
duration: 2500,
easing: tween.linear,
onFinish: function onFinish() {
// Final dissipation
tween(self, {
alpha: 0,
scaleX: 1.5,
scaleY: 2.0,
x: startX + dx * 0.5 + windDrift,
y: startY + dy * 0.5 + upwardForce * 1.2
}, {
duration: 1500,
easing: tween.easeIn,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
}
});
}
});
// Animate individual tendrils for undulating, creepy movement
self.animationTime = 0;
self.update = function () {
if (!self.parent) return;
self.animationTime += 0.016; // Assuming 60 FPS
// Animate each tendril with swaying motion
for (var i = 0; i < smokeTendrils.length; i++) {
var tendril = smokeTendrils[i];
// Sinusoidal swaying motion
var swayAngle = Math.sin(self.animationTime * tendril.swaySpeed + i) * tendril.swayAmount;
tendril.container.rotation = tendril.baseRotation + swayAngle;
// Animate individual segments for more fluid motion
for (var j = 0; j < tendril.segments.length; j++) {
var segment = tendril.segments[j];
// Add slight pulsing to segments
var pulseFactor = 1 + Math.sin(self.animationTime * 0.5 + j * 0.3) * 0.05;
segment.scaleX = pulseFactor;
// Slight vertical bobbing
segment.y = -j * (segment.height * 0.3) + Math.sin(self.animationTime + j * 0.5) * 5;
}
}
};
return self;
});
var Snake = Container.expand(function (startX, startY, tower) {
var self = Container.call(this);
self.x = startX;
self.y = startY;
self.tower = tower;
self.speed = 3;
self.damage = 15;
self.fireRate = 30; // Fire every 0.5 seconds
self.lastFired = 0;
self.lifeTime = 300; // 5 seconds at 60 FPS
self.currentLifeTime = 0;
// Create snake body segments
self.segments = [];
var numSegments = 8; // More segments for smoother snake
for (var i = 0; i < numSegments; i++) {
var segment = new Container();
// Create snake segment using rangeCircle asset
var segmentGraphics = segment.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
// Make segments progressively smaller - increased base size
var segmentSize = 90 - i * 8; // Larger, more gradual taper
segmentGraphics.width = segmentSize;
segmentGraphics.height = segmentSize;
// Realistic green snake colors with pattern variation
var greenShades = [0x228B22, 0x2E8B57, 0x3CB371, 0x20B2AA, 0x008B8B];
var baseGreen = greenShades[i % greenShades.length];
// Add darker spots/pattern to some segments
if (i % 2 === 0 && i > 0) {
segmentGraphics.tint = 0x006400; // Dark green for pattern
} else {
segmentGraphics.tint = baseGreen;
}
segmentGraphics.alpha = 0.9;
segment.x = -i * 20; // Tighter spacing for realistic movement
segment.y = 0;
self.addChild(segment);
self.segments.push(segment);
}
// Create more detailed snake head
var head = self.segments[0];
// Remove the old head graphics first
head.removeChildAt(0);
// Main head shape (larger)
var headGraphics = head.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
headGraphics.width = 110; // Larger head
headGraphics.height = 100; // Slightly oval
headGraphics.tint = 0x228B22; // Forest green
// Add eyes to the head
var leftEyeContainer = new Container();
var leftEyeWhite = leftEyeContainer.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
leftEyeWhite.width = 25;
leftEyeWhite.height = 25;
leftEyeWhite.tint = 0xFFFFFF;
var leftEyePupil = leftEyeContainer.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
leftEyePupil.width = 15;
leftEyePupil.height = 15;
leftEyePupil.tint = 0x000000;
leftEyePupil.x = 2;
leftEyeContainer.x = -25;
leftEyeContainer.y = -10;
head.addChild(leftEyeContainer);
// Right eye
var rightEyeContainer = new Container();
var rightEyeWhite = rightEyeContainer.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rightEyeWhite.width = 25;
rightEyeWhite.height = 25;
rightEyeWhite.tint = 0xFFFFFF;
var rightEyePupil = rightEyeContainer.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rightEyePupil.width = 15;
rightEyePupil.height = 15;
rightEyePupil.tint = 0x000000;
rightEyePupil.x = 2;
rightEyeContainer.x = 25;
rightEyeContainer.y = -10;
head.addChild(rightEyeContainer);
// Add nostrils
var leftNostril = head.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
leftNostril.width = 8;
leftNostril.height = 8;
leftNostril.tint = 0x000000;
leftNostril.x = -10;
leftNostril.y = 15;
var rightNostril = head.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rightNostril.width = 8;
rightNostril.height = 8;
rightNostril.tint = 0x000000;
rightNostril.x = 10;
rightNostril.y = 15;
// Store eye references for animation
self.leftEyePupil = leftEyePupil;
self.rightEyePupil = rightEyePupil;
// Movement pattern variables
self.moveAngle = Math.random() * Math.PI * 2;
self.waveAmplitude = 30;
self.waveFrequency = 0.1;
self.time = 0;
self.findTarget = function () {
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
return closestEnemy;
};
self.shootFire = function (targetEnemy) {
if (!targetEnemy || !targetEnemy.parent) return;
// Create fire projectile from snake's mouth
var fireX = self.x + self.segments[0].x;
var fireY = self.y + self.segments[0].y;
// Create flame bullet
var flame = new Bullet(fireX, fireY, targetEnemy, self.damage, 8);
flame.type = 'snake_fire';
// Customize flame appearance
flame.bulletGraphics.tint = 0xFF4500; // Orange-red flame
flame.bulletGraphics.width = 40; // Increased from 25
flame.bulletGraphics.height = 40; // Increased from 25
// Add flame effect
tween(flame.bulletGraphics, {
scaleX: 1.2,
scaleY: 1.2,
alpha: 0.7
}, {
duration: 200,
easing: tween.easeInOut,
loop: true,
yoyo: true
});
game.addChild(flame);
bullets.push(flame);
targetEnemy.bulletsTargetingThis.push(flame);
};
self.update = function () {
self.currentLifeTime++;
self.time++;
// Destroy snake after lifetime expires
if (self.currentLifeTime >= self.lifeTime) {
self.destroy();
return;
}
// Find nearest enemy
var target = self.findTarget();
if (target) {
// Move towards target with snake-like motion
var dx = target.x - self.x;
var dy = target.y - self.y;
var targetAngle = Math.atan2(dy, dx);
// Smooth angle transition
var angleDiff = targetAngle - self.moveAngle;
while (angleDiff > Math.PI) angleDiff -= Math.PI * 2;
while (angleDiff < -Math.PI) angleDiff += Math.PI * 2;
self.moveAngle += angleDiff * 0.1;
// Move with sinusoidal pattern
var sineOffset = Math.sin(self.time * self.waveFrequency) * self.waveAmplitude;
self.x += Math.cos(self.moveAngle) * self.speed;
self.y += Math.sin(self.moveAngle) * self.speed;
// Apply perpendicular offset for snake-like movement
var perpAngle = self.moveAngle + Math.PI / 2;
self.x += Math.cos(perpAngle) * sineOffset * 0.02;
self.y += Math.sin(perpAngle) * sineOffset * 0.02;
// Fire at enemies
if (LK.ticks - self.lastFired >= self.fireRate) {
self.shootFire(target);
self.lastFired = LK.ticks;
}
} else {
// Wander if no target
self.x += Math.cos(self.moveAngle) * self.speed * 0.5;
self.y += Math.sin(self.moveAngle) * self.speed * 0.5;
// Random direction changes
if (Math.random() < 0.05) {
self.moveAngle += (Math.random() - 0.5) * Math.PI * 0.5;
}
}
// Update segment positions to follow head with snake-like motion
for (var i = 1; i < self.segments.length; i++) {
var prevSegment = self.segments[i - 1];
var segment = self.segments[i];
// Smooth following with delay - more fluid for realistic motion
var followSpeed = 0.25 - i * 0.02;
segment.x += (prevSegment.x - segment.x - 20) * followSpeed;
segment.y += (prevSegment.y - segment.y) * followSpeed;
// Add more complex oscillation for realistic slithering
var oscillation = Math.sin(self.time * 0.15 + i * 0.6) * (4 - i * 0.3);
var perpAngle = self.moveAngle + Math.PI / 2;
segment.x += Math.cos(perpAngle) * oscillation;
segment.y += Math.sin(perpAngle) * oscillation;
}
// Animate eyes to look in movement direction
if (self.leftEyePupil && self.rightEyePupil) {
var eyeOffset = Math.min(5, self.speed);
self.leftEyePupil.x = Math.cos(self.moveAngle) * eyeOffset;
self.leftEyePupil.y = Math.sin(self.moveAngle) * eyeOffset;
self.rightEyePupil.x = Math.cos(self.moveAngle) * eyeOffset;
self.rightEyePupil.y = Math.sin(self.moveAngle) * eyeOffset;
}
// Fade out near end of life
if (self.currentLifeTime > self.lifeTime - 60) {
self.alpha = (self.lifeTime - self.currentLifeTime) / 60;
}
};
return self;
});
var SourceTower = Container.expand(function (towerType) {
var self = Container.call(this);
self.towerType = towerType || 'default';
// Increase size of base for easier touch
var baseGraphics;
if (self.towerType === 'default') {
baseGraphics = self.attachAsset('1', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'rapid') {
baseGraphics = self.attachAsset('2', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'sniper') {
baseGraphics = self.attachAsset('3', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'splash') {
baseGraphics = self.attachAsset('4', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'slow') {
baseGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else if (self.towerType === 'poison') {
baseGraphics = self.attachAsset('6', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
} else {
baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
}
switch (self.towerType) {
case 'rapid':
// Don't tint if using image asset for rapid tower
break;
case 'sniper':
// Don't tint if using image asset for sniper tower
break;
case 'splash':
// Don't tint if using image asset for splash tower
break;
case 'slow':
// Don't tint if using image asset for slow tower
break;
case 'poison':
// Don't tint if using image asset for poison tower
break;
default:
// Don't tint if using image asset for default tower
break;
}
var towerCost = getTowerCost(self.towerType);
// Add shadow for tower type label
var towerDisplayNameShadow = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
if (self.towerType === 'rapid') {
towerDisplayNameShadow = 'Twins';
}
if (self.towerType === 'default') {
towerDisplayNameShadow = 'Student';
}
if (self.towerType === 'sniper') {
towerDisplayNameShadow = 'Harry';
}
if (self.towerType === 'splash') {
towerDisplayNameShadow = 'Harmony';
}
if (self.towerType === 'slow') {
towerDisplayNameShadow = 'Ron';
}
if (self.towerType === 'poison') {
towerDisplayNameShadow = 'Snape';
}
var typeLabelShadow = new Text2(towerDisplayNameShadow, {
size: 50,
fill: 0x000000,
weight: 800
});
typeLabelShadow.anchor.set(0.5, 0.5);
typeLabelShadow.x = 4;
typeLabelShadow.y = -20 + 4;
self.addChild(typeLabelShadow);
// Add tower type label
var towerDisplayName = self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1);
if (self.towerType === 'rapid') {
towerDisplayName = 'Twins';
}
if (self.towerType === 'default') {
towerDisplayName = 'Student';
}
if (self.towerType === 'sniper') {
towerDisplayName = 'Harry';
}
if (self.towerType === 'splash') {
towerDisplayName = 'Harmony';
}
if (self.towerType === 'slow') {
towerDisplayName = 'Ron';
}
if (self.towerType === 'poison') {
towerDisplayName = 'Snape';
}
var typeLabel = new Text2(towerDisplayName, {
size: 50,
fill: 0xFFFFFF,
weight: 800
});
typeLabel.anchor.set(0.5, 0.5);
typeLabel.y = -20; // Position above center of tower
self.addChild(typeLabel);
// Add cost shadow
var costLabelShadow = new Text2(towerCost, {
size: 50,
fill: 0x000000,
weight: 800
});
costLabelShadow.anchor.set(0.5, 0.5);
costLabelShadow.x = 4;
costLabelShadow.y = 24 + 12;
self.addChild(costLabelShadow);
// Add cost label
var costLabel = new Text2(towerCost, {
size: 50,
fill: 0xFFD700,
weight: 800
});
costLabel.anchor.set(0.5, 0.5);
costLabel.y = 20 + 12;
self.addChild(costLabel);
self.update = function () {
// Check if player can afford this tower
var canAfford = gold >= getTowerCost(self.towerType);
// Set opacity based on affordability
self.alpha = canAfford ? 1 : 0.5;
};
return self;
});
var Tower = Container.expand(function (id) {
var self = Container.call(this);
self.id = id || 'default';
self.level = 1;
self.maxLevel = 6;
self.gridX = 0;
self.gridY = 0;
self.range = 3 * CELL_SIZE;
self.specialAbilityCooldown = 0; // Cooldown for special abilities
// Standardized method to get the current range of the tower
self.getRange = function () {
// Always calculate range based on tower type and level
switch (self.id) {
case 'sniper':
// Sniper: base 5, +0.8 per level, but final upgrade gets a huge boost
if (self.level === self.maxLevel) {
return 12 * CELL_SIZE; // Significantly increased range for max level
}
return (5 + (self.level - 1) * 0.8) * CELL_SIZE;
case 'splash':
// Splash: base 2, +0.2 per level (max ~4 blocks at max level)
return (2 + (self.level - 1) * 0.2) * CELL_SIZE;
case 'rapid':
// Rapid: base 2.5, +0.5 per level
return (2.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'slow':
// Slow: base 3.5, +0.5 per level
return (3.5 + (self.level - 1) * 0.5) * CELL_SIZE;
case 'poison':
// Poison: base 3.2, +0.5 per level
return (3.2 + (self.level - 1) * 0.5) * CELL_SIZE;
default:
// Default: base 3, +0.5 per level
return (3 + (self.level - 1) * 0.5) * CELL_SIZE;
}
};
self.cellsInRange = [];
self.fireRate = 60;
self.bulletSpeed = 5;
self.damage = 10;
self.lastFired = 0;
self.targetEnemy = null;
switch (self.id) {
case 'rapid':
self.fireRate = 30;
self.damage = 5;
self.range = 2.5 * CELL_SIZE;
self.bulletSpeed = 7;
break;
case 'sniper':
self.fireRate = 90;
self.damage = 25;
self.range = 5 * CELL_SIZE;
self.bulletSpeed = 25;
break;
case 'splash':
self.fireRate = 75;
self.damage = 15;
self.range = 2 * CELL_SIZE;
self.bulletSpeed = 4;
break;
case 'slow':
self.fireRate = 50;
self.damage = 8;
self.range = 3.5 * CELL_SIZE;
self.bulletSpeed = 5;
break;
case 'poison':
self.fireRate = 70;
self.damage = 12;
self.range = 3.2 * CELL_SIZE;
self.bulletSpeed = 5;
break;
}
var baseGraphics;
if (self.id === 'default') {
baseGraphics = self.attachAsset('1', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'rapid') {
baseGraphics = self.attachAsset('2', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'sniper') {
baseGraphics = self.attachAsset('3', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'splash') {
baseGraphics = self.attachAsset('4', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'slow') {
baseGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else if (self.id === 'poison') {
baseGraphics = self.attachAsset('6', {
anchorX: 0.5,
anchorY: 0.5
});
// Ensure proper sizing for image asset
baseGraphics.width = CELL_SIZE * 2.5;
baseGraphics.height = CELL_SIZE * 2.5;
} else {
baseGraphics = self.attachAsset('tower', {
anchorX: 0.5,
anchorY: 0.5
});
}
// Store reference to base graphics
self.baseGraphics = baseGraphics;
switch (self.id) {
case 'rapid':
// Don't tint if using image asset for rapid tower
break;
case 'sniper':
// Don't tint if using image asset for sniper tower
break;
case 'splash':
// Don't tint if using image asset for splash tower
break;
case 'slow':
// Don't tint if using image asset for slow tower
break;
case 'poison':
// Don't tint if using image asset for poison tower
break;
default:
// Don't tint if using image asset for default tower
// Ensure default tower image is not tinted
if (self.id === 'default') {
baseGraphics.tint = 0xFFFFFF; // Pure white (no tint)
}
break;
}
var levelIndicators = [];
var maxDots = self.maxLevel;
var dotSpacing = baseGraphics.width / (maxDots + 1);
var dotSize = CELL_SIZE / 6;
for (var i = 0; i < maxDots; i++) {
var dot = new Container();
var outlineCircle = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
outlineCircle.width = dotSize + 4;
outlineCircle.height = dotSize + 4;
outlineCircle.tint = 0x000000;
var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
towerLevelIndicator.width = dotSize;
towerLevelIndicator.height = dotSize;
towerLevelIndicator.tint = 0xCCCCCC;
dot.x = -CELL_SIZE + dotSpacing * (i + 1);
dot.y = CELL_SIZE * 0.7;
self.addChild(dot);
levelIndicators.push(dot);
}
var gunContainer = new Container();
self.addChild(gunContainer);
var gunGraphics = gunContainer.attachAsset('defense', {
anchorX: 0.5,
anchorY: 0.5
});
// Make gun slightly smaller for default towers to not obscure the base
if (self.id === 'default') {
gunGraphics.scaleX = 0.8;
gunGraphics.scaleY = 0.8;
}
self.updateLevelIndicators = function () {
for (var i = 0; i < maxDots; i++) {
var dot = levelIndicators[i];
var towerLevelIndicator = dot.children[1];
if (i < self.level) {
towerLevelIndicator.tint = 0xFFFFFF;
} else {
switch (self.id) {
case 'rapid':
towerLevelIndicator.tint = 0x00AAFF;
break;
case 'sniper':
towerLevelIndicator.tint = 0xFF5500;
break;
case 'splash':
towerLevelIndicator.tint = 0x33CC00;
break;
case 'slow':
towerLevelIndicator.tint = 0x9900FF;
break;
case 'poison':
towerLevelIndicator.tint = 0x00FFAA;
break;
default:
towerLevelIndicator.tint = 0xAAAAAA;
}
}
}
};
self.updateLevelIndicators();
// Add cooldown indicator for harmony tower
if (self.id === 'splash') {
var cooldownIndicator = new Container();
self.cooldownIndicator = cooldownIndicator;
self.addChild(cooldownIndicator);
var cooldownBg = cooldownIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
cooldownBg.width = CELL_SIZE * 0.8;
cooldownBg.height = CELL_SIZE * 0.8;
cooldownBg.tint = 0x000000;
cooldownBg.alpha = 0.5;
var cooldownFill = cooldownIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
cooldownFill.width = CELL_SIZE * 0.7;
cooldownFill.height = CELL_SIZE * 0.7;
cooldownFill.tint = 0x00FF00;
self.cooldownFill = cooldownFill;
cooldownIndicator.y = -CELL_SIZE * 1.2;
}
self.refreshCellsInRange = function () {
for (var i = 0; i < self.cellsInRange.length; i++) {
var cell = self.cellsInRange[i];
var towerIndex = cell.towersInRange.indexOf(self);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
self.cellsInRange = [];
var rangeRadius = self.getRange() / CELL_SIZE;
var centerX = self.gridX + 1;
var centerY = self.gridY + 1;
var minI = Math.floor(centerX - rangeRadius - 0.5);
var maxI = Math.ceil(centerX + rangeRadius + 0.5);
var minJ = Math.floor(centerY - rangeRadius - 0.5);
var maxJ = Math.ceil(centerY + rangeRadius + 0.5);
for (var i = minI; i <= maxI; i++) {
for (var j = minJ; j <= maxJ; j++) {
var closestX = Math.max(i, Math.min(centerX, i + 1));
var closestY = Math.max(j, Math.min(centerY, j + 1));
var deltaX = closestX - centerX;
var deltaY = closestY - centerY;
var distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (distanceSquared <= rangeRadius * rangeRadius) {
var cell = grid.getCell(i, j);
if (cell) {
self.cellsInRange.push(cell);
cell.towersInRange.push(self);
}
}
}
}
grid.renderDebug();
};
self.getTotalValue = function () {
var baseTowerCost = getTowerCost(self.id);
var totalInvestment = baseTowerCost;
var baseUpgradeCost = baseTowerCost; // Upgrade cost now scales with base tower cost
for (var i = 1; i < self.level; i++) {
totalInvestment += Math.floor(baseUpgradeCost * Math.pow(2, i - 1));
}
return totalInvestment;
};
self.upgrade = function () {
if (self.level < self.maxLevel) {
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.id);
var upgradeCost;
// Make last upgrade level extra expensive
if (self.level === self.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1) * 3.5 / 2); // Half the cost for final upgrade
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.level - 1));
}
if (gold >= upgradeCost) {
setGold(gold - upgradeCost);
self.level++;
// No need to update self.range here; getRange() is now the source of truth
// Apply tower-specific upgrades based on type
if (self.id === 'rapid') {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade (double the effect)
self.fireRate = Math.max(4, 30 - self.level * 9); // double the effect
self.damage = 5 + self.level * 10; // double the effect
self.bulletSpeed = 7 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(15, 30 - self.level * 3); // Fast tower gets faster with upgrades
self.damage = 5 + self.level * 3;
self.bulletSpeed = 7 + self.level * 0.7;
}
} else {
if (self.level === self.maxLevel) {
// Extra powerful last upgrade for all other towers (double the effect)
self.fireRate = Math.max(5, 60 - self.level * 24); // double the effect
self.damage = 10 + self.level * 20; // double the effect
self.bulletSpeed = 5 + self.level * 2.4; // double the effect
} else {
self.fireRate = Math.max(20, 60 - self.level * 8);
self.damage = 10 + self.level * 5;
self.bulletSpeed = 5 + self.level * 0.5;
}
}
self.refreshCellsInRange();
self.updateLevelIndicators();
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 () {
// Update special ability cooldown
if (self.specialAbilityCooldown > 0) {
self.specialAbilityCooldown--;
}
// Update cooldown visual indicator for harmony tower
if (self.id === 'splash' && self.cooldownIndicator) {
if (self.specialAbilityCooldown > 0) {
// Show cooldown progress
self.cooldownIndicator.visible = true;
var cooldownProgress = 1 - self.specialAbilityCooldown / 600;
self.cooldownFill.scaleX = cooldownProgress;
self.cooldownFill.scaleY = cooldownProgress;
self.cooldownFill.tint = 0xFF0000; // Red when on cooldown
// Hide the ability ready text when on cooldown
if (self.abilityReadyText) {
self.abilityReadyText.visible = false;
}
} else {
// Ready to use
self.cooldownIndicator.visible = true;
self.cooldownFill.scaleX = 1;
self.cooldownFill.scaleY = 1;
self.cooldownFill.tint = 0x00FF00; // Green when ready
// Show the ability ready text
if (!self.abilityReadyText) {
// Create the text if it doesn't exist
self.abilityReadyText = new Container();
// Add shadow text
var shadowText = new Text2('Press to slow down', {
size: 45,
fill: 0x000000,
weight: 800
});
shadowText.anchor.set(0.5, 0.5);
shadowText.x = 2;
shadowText.y = 2;
self.abilityReadyText.addChild(shadowText);
// Add main text
var mainText = new Text2('Press to slow down', {
size: 45,
fill: 0xFFFFFF,
weight: 800
});
mainText.anchor.set(0.5, 0.5);
self.abilityReadyText.addChild(mainText);
// Position above the tower
self.abilityReadyText.y = -CELL_SIZE * 2;
self.addChild(self.abilityReadyText);
}
self.abilityReadyText.visible = true;
}
}
self.targetEnemy = self.findTarget();
if (self.targetEnemy) {
var dx = self.targetEnemy.x - self.x;
var dy = self.targetEnemy.y - self.y;
var angle = Math.atan2(dy, dx);
gunContainer.rotation = angle;
if (LK.ticks - self.lastFired >= self.fireRate) {
self.fire();
self.lastFired = LK.ticks;
}
}
// Flying behavior for students (default) towers
if (self.id === 'default') {
// Initialize flying properties if not already set
if (!self.isFlying) {
self.isFlying = true;
self.flightRadius = 200 + Math.random() * 300; // Random radius between 200-500
self.flightSpeed = 0.02 + Math.random() * 0.03; // Random speed
self.flightAngle = Math.random() * Math.PI * 2; // Random starting angle
self.flightCenterX = 300 + Math.random() * (2048 - 600); // Random center X across screen
self.flightCenterY = 400 + Math.random() * (2200 - 400); // Random center Y across screen
self.flightTimer = 0;
self.flightDuration = 1800 + Math.random() * 1200; // Flight duration 30-50 seconds
}
// Update flight position
self.flightTimer++;
self.flightAngle += self.flightSpeed;
// Calculate circular flight path
var targetX = self.flightCenterX + Math.cos(self.flightAngle) * self.flightRadius;
var targetY = self.flightCenterY + Math.sin(self.flightAngle) * self.flightRadius;
// Smoothly move to target position
var lerpFactor = 0.1;
self.x += (targetX - self.x) * lerpFactor;
self.y += (targetY - self.y) * lerpFactor;
// Change flight pattern periodically
if (self.flightTimer >= self.flightDuration) {
self.flightTimer = 0;
self.flightRadius = 200 + Math.random() * 300;
self.flightSpeed = 0.02 + Math.random() * 0.03;
self.flightCenterX = 300 + Math.random() * (2048 - 600);
self.flightCenterY = 400 + Math.random() * (2200 - 400);
self.flightDuration = 1800 + Math.random() * 1200;
}
// Add floating animation to the tower graphic
if (!self.floatingAnimation) {
self.floatingAnimation = true;
var _createFloatingAnimation2 = function _createFloatingAnimation() {
if (!self.parent) return;
tween(self.baseGraphics, {
y: -8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
tween(self.baseGraphics, {
y: 8
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (!self.parent) return;
_createFloatingAnimation2();
}
});
}
});
};
_createFloatingAnimation2();
}
} else {
// Original tower following behavior for non-students towers
if (!self.isMoving && !self.moveTimer) {
self.moveTimer = LK.ticks + Math.random() * 300 + 180; // Random interval 3-8 seconds
}
if (!self.isMoving && LK.ticks >= self.moveTimer) {
// Find closest enemy in range to follow
var closestEnemy = null;
var closestDistance = Infinity;
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.originalX;
var dy = enemy.y - self.originalY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Increase following range to 2.5x the attack range
if (distance <= self.getRange() * 2.5 && distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
}
if (closestEnemy) {
self.isMoving = true;
// Calculate movement towards enemy (but not too close)
var enemyDx = closestEnemy.x - self.originalX;
var enemyDy = closestEnemy.y - self.originalY;
var enemyDistance = Math.sqrt(enemyDx * enemyDx + enemyDy * enemyDy);
// Move 40% of the way towards the enemy (increased from 30%)
var moveDistance = Math.min(CELL_SIZE * 3, enemyDistance * 0.4);
var moveAngle = Math.atan2(enemyDy, enemyDx);
var newX = self.originalX + Math.cos(moveAngle) * moveDistance;
var newY = self.originalY + Math.sin(moveAngle) * moveDistance;
// Animate towards enemy
tween(self, {
x: newX,
y: newY
}, {
duration: 800,
easing: tween.easeOut,
onFinish: function onFinish() {
// Stay at new position for a moment
LK.setTimeout(function () {
// Return to original position
tween(self, {
x: self.originalX,
y: self.originalY
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.isMoving = false;
self.moveTimer = LK.ticks + Math.random() * 300 + 180; // Reset timer
}
});
}, 1000);
}
});
} else {
// No enemy found, reset timer
self.moveTimer = LK.ticks + Math.random() * 180 + 120; // Shorter wait if no target
}
}
}
};
self.down = function (x, y, obj) {
// Handle harmony tower special ability
if (self.id === 'splash' && self.specialAbilityCooldown === 0) {
// Activate special ability: slow all enemies to 50% for 5 seconds
var slowDuration = 300; // 5 seconds at 60 FPS
var slowPercentage = 0.5; // 50% speed
// Apply slow to all enemies
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
// Skip immune enemies
if (!enemy.isImmune) {
// Store original speed if not already slowed
if (!enemy.slowed) {
enemy.originalSpeed = enemy.speed;
}
enemy.speed = enemy.originalSpeed * slowPercentage;
enemy.slowed = true;
enemy.slowDuration = slowDuration;
// Create visual effect
var slowEffect = new EffectIndicator(enemy.x, enemy.y, 'slow');
game.addChild(slowEffect);
}
}
// Set cooldown to 10 seconds
self.specialAbilityCooldown = 600; // 10 seconds at 60 FPS
// Play harmony sound effect
LK.getSound('harmony').play();
// Create notification
var notification = game.addChild(new Notification("Harmony activated! All enemies slowed!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
// Visual feedback on the tower
tween(self, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
scaleX: 1,
scaleY: 1
}, {
duration: 200,
easing: tween.easeIn
});
}
});
return; // Don't open upgrade menu when using ability
}
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;
// For slow tower, pass level for scaling slow effect
if (self.id === 'slow') {
bullet.sourceTowerLevel = self.level;
}
// Customize bullet appearance based on tower type
switch (self.id) {
case 'default':
bullet.bulletGraphics.tint = 0xd2b48c; // Default tower bullet color
bullet.bulletGraphics.width = 30;
bullet.bulletGraphics.height = 30;
// Randomly play either 111 or abrakadabra sound when default tower fires
if (Math.random() < 0.5) {
LK.getSound('111').play();
} else {
LK.getSound('abrakadabra').play();
}
// Change default tower to alternate between image assets 11 and 1 after firing
if (self.baseGraphics) {
// Check current asset and switch
var currentAssetId = self.baseGraphics.texture ? '1' : '11';
if (self.lastUsedAsset === '1' || self.lastUsedAsset === undefined) {
// Switch to asset 11
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('11', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '11';
} else {
// Switch to asset 1
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('1', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '1';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'rapid':
bullet.bulletGraphics.tint = 0x87ceeb; // Bright sky blue bolt
bullet.bulletGraphics.width = 20;
bullet.bulletGraphics.height = 20;
// Play 222 sound when rapid tower fires
LK.getSound('222').play();
// Change rapid tower to alternate between image assets 22 and 2 after firing
if (self.baseGraphics) {
// Check current asset and switch
if (self.lastUsedAsset === '2' || self.lastUsedAsset === undefined) {
// Switch to asset 22
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('22', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '22';
} else {
// Switch to asset 2
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('2', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '2';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'sniper':
bullet.bulletGraphics.tint = 0xff6347; // Bright orange-red spell
bullet.bulletGraphics.width = 15;
bullet.bulletGraphics.height = 15;
// Play 33 sound when sniper tower fires
LK.getSound('33').play();
// Change sniper tower to alternate between image assets 3 and 32 after firing
if (self.baseGraphics) {
// Check current asset and switch
if (self.lastUsedAsset === '3' || self.lastUsedAsset === undefined) {
// Switch to asset 32
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('32', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '32';
} else {
// Switch to asset 3
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('3', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '3';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'splash':
bullet.bulletGraphics.tint = 0x90ee90; // Bright light green spell
bullet.bulletGraphics.width = 40;
bullet.bulletGraphics.height = 40;
// Play harmony sound when splash tower fires
LK.getSound('harmony').play();
// Change splash tower to use image asset 42 after firing
if (self.baseGraphics) {
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('42', {
anchorX: 0.5,
anchorY: 0.5
});
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'slow':
bullet.bulletGraphics.tint = 0xdda0dd; // Bright plum spell
bullet.bulletGraphics.width = 35;
bullet.bulletGraphics.height = 35;
// Play ron sound when slow tower fires
LK.getSound('ron').play();
// Change slow tower to alternate between image assets 5 and 52 after firing
if (self.baseGraphics) {
// Check current asset and switch
if (self.lastUsedAsset === '5' || self.lastUsedAsset === undefined) {
// Switch to asset 52
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('52', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '52';
} else {
// Switch to asset 5
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '5';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
case 'poison':
bullet.bulletGraphics.tint = 0x98fb98; // Bright pale green spell
bullet.bulletGraphics.width = 35;
bullet.bulletGraphics.height = 35;
// Play snape sound when poison tower fires
LK.getSound('snape').play();
// Create snake that shoots fire at enemies
var snakeX = self.x + Math.cos(gunContainer.rotation) * 60;
var snakeY = self.y + Math.sin(gunContainer.rotation) * 60;
var snake = new Snake(snakeX, snakeY, self);
game.addChild(snake);
snakes.push(snake);
// Change poison tower to alternate between image assets 6 and 62 after firing
if (self.baseGraphics) {
// Check current asset and switch
if (self.lastUsedAsset === '6' || self.lastUsedAsset === undefined) {
// Switch to asset 62
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('62', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '62';
} else {
// Switch to asset 6
self.removeChild(self.baseGraphics);
self.baseGraphics = self.attachAsset('6', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastUsedAsset = '6';
}
self.baseGraphics.width = CELL_SIZE * 2.5;
self.baseGraphics.height = CELL_SIZE * 2.5;
// Move base graphics to correct position in display list (before gun container)
self.setChildIndex(self.baseGraphics, 0);
}
break;
}
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;
self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
// Store original position for movement behavior
self.originalX = self.x;
self.originalY = self.y;
self.isMoving = false;
self.moveTimer = null;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 1;
}
}
}
self.refreshCellsInRange();
};
return self;
});
var TowerPreview = Container.expand(function () {
var self = Container.call(this);
var towerRange = 3;
var rangeInPixels = towerRange * CELL_SIZE;
self.towerType = 'default';
self.hasEnoughGold = true;
var rangeIndicator = new Container();
self.addChild(rangeIndicator);
var rangeGraphics = rangeIndicator.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3;
var previewGraphics = self.attachAsset('towerpreview', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.canPlace = false;
self.gridX = 0;
self.gridY = 0;
self.blockedByEnemy = false;
self.update = function () {
var previousHasEnoughGold = self.hasEnoughGold;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
// Only update appearance if the affordability status has changed
if (previousHasEnoughGold !== self.hasEnoughGold) {
self.updateAppearance();
}
};
self.updateAppearance = function () {
// Use Tower class to get the source of truth for range
var tempTower = new Tower(self.towerType);
var previewRange = tempTower.getRange();
// Clean up tempTower to avoid memory leaks
if (tempTower && tempTower.destroy) {
tempTower.destroy();
}
// Set range indicator using unified range logic
rangeGraphics.width = rangeGraphics.height = previewRange * 2;
// For default tower type, replace the preview graphics with image asset 1
if (self.towerType === 'default') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 1 as preview
previewGraphics = self.attachAsset('1', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'rapid') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 2 as preview
previewGraphics = self.attachAsset('2', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'sniper') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 3 as preview
previewGraphics = self.attachAsset('3', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'splash') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 4 as preview
previewGraphics = self.attachAsset('4', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'slow') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 5 as preview
previewGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else if (self.towerType === 'poison') {
// Remove existing preview graphics
self.removeChild(previewGraphics);
// Add image asset 5 as preview
previewGraphics = self.attachAsset('5', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
// Move preview graphics to correct position in display list (before range indicator)
self.setChildIndex(previewGraphics, 1);
} else {
// For non-default towers, ensure we're using the shape asset
if (previewGraphics.texture && previewGraphics.texture.baseTexture) {
// Already using an image, need to switch back to shape
self.removeChild(previewGraphics);
previewGraphics = self.attachAsset('towerpreview', {
anchorX: 0.5,
anchorY: 0.5
});
previewGraphics.width = CELL_SIZE * 2;
previewGraphics.height = CELL_SIZE * 2;
self.setChildIndex(previewGraphics, 1);
}
}
switch (self.towerType) {
case 'rapid':
// Don't tint if using image asset for rapid tower
break;
case 'sniper':
// Don't tint if using image asset for sniper tower
break;
case 'splash':
// Don't tint if using image asset for splash tower
break;
case 'slow':
// Don't tint if using image asset for slow tower
break;
case 'poison':
// Don't tint if using image asset for poison tower
break;
default:
// Don't tint if using image asset for default tower
if (self.towerType !== 'default' && self.towerType !== 'rapid' && self.towerType !== 'sniper' && self.towerType !== 'splash' && self.towerType !== 'slow' && self.towerType !== 'poison') {
previewGraphics.tint = 0xd2b48c; // Light tan
}
}
if (!self.canPlace || !self.hasEnoughGold) {
previewGraphics.tint = 0xFF0000;
}
};
self.updatePlacementStatus = function () {
var validGridPlacement = true;
if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
validGridPlacement = false;
} else {
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(self.gridX + i, self.gridY + j);
if (!cell || cell.type !== 0) {
validGridPlacement = false;
break;
}
}
if (!validGridPlacement) {
break;
}
}
}
self.blockedByEnemy = false;
if (validGridPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) {
continue;
}
// Only check non-flying enemies, flying enemies can pass over towers
if (!enemy.isFlying) {
if (enemy.cellX >= self.gridX && enemy.cellX < self.gridX + 2 && enemy.cellY >= self.gridY && enemy.cellY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
if (enemy.currentTarget) {
var targetX = enemy.currentTarget.x;
var targetY = enemy.currentTarget.y;
if (targetX >= self.gridX && targetX < self.gridX + 2 && targetY >= self.gridY && targetY < self.gridY + 2) {
self.blockedByEnemy = true;
break;
}
}
}
}
}
self.canPlace = validGridPlacement && !self.blockedByEnemy;
self.hasEnoughGold = gold >= getTowerCost(self.towerType);
self.updateAppearance();
};
self.checkPlacement = function () {
self.updatePlacementStatus();
};
self.snapToGrid = function (x, y) {
var gridPosX = x - grid.x;
var gridPosY = y - grid.y;
self.gridX = Math.floor(gridPosX / CELL_SIZE);
self.gridY = Math.floor(gridPosY / CELL_SIZE);
self.x = grid.x + self.gridX * CELL_SIZE + CELL_SIZE / 2;
self.y = grid.y + self.gridY * CELL_SIZE + CELL_SIZE / 2;
self.checkPlacement();
};
return self;
});
var UpgradeMenu = Container.expand(function (tower) {
var self = Container.call(this);
self.tower = tower;
self.y = 2732 + 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 towerTypeName = self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1);
if (self.tower.id === 'rapid') {
towerTypeName = 'Twins';
}
if (self.tower.id === 'default') {
towerTypeName = 'Student';
}
if (self.tower.id === 'sniper') {
towerTypeName = 'Harry';
}
if (self.tower.id === 'splash') {
towerTypeName = 'Harmony';
}
if (self.tower.id === 'slow') {
towerTypeName = 'Ron';
}
if (self.tower.id === 'poison') {
towerTypeName = 'Snape';
}
var towerTypeText = new Text2(towerTypeName + ' Tower', {
size: 80,
fill: 0xFFFFFF,
weight: 800
});
towerTypeText.anchor.set(0, 0);
towerTypeText.x = -840;
towerTypeText.y = -160;
self.addChild(towerTypeText);
var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
size: 70,
fill: 0xFFFFFF,
weight: 400
});
statsText.anchor.set(0, 0.5);
statsText.x = -840;
statsText.y = 50;
self.addChild(statsText);
var buttonsContainer = new Container();
buttonsContainer.x = 500;
self.addChild(buttonsContainer);
var upgradeButton = new Container();
buttonsContainer.addChild(upgradeButton);
var buttonBackground = upgradeButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
buttonBackground.width = 500;
buttonBackground.height = 150;
var isMaxLevel = self.tower.level >= self.tower.maxLevel;
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var upgradeCost;
if (isMaxLevel) {
upgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
buttonBackground.tint = isMaxLevel ? 0x888888 : 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 + '\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;
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cell.type = 0;
var towerIndex = cell.towersInRange.indexOf(self.tower);
if (towerIndex !== -1) {
cell.towersInRange.splice(towerIndex, 1);
}
}
}
}
if (selectedTower === self.tower) {
selectedTower = null;
}
var towerIndex = towers.indexOf(self.tower);
if (towerIndex !== -1) {
towers.splice(towerIndex, 1);
}
towerLayer.removeChild(self.tower);
grid.pathFind();
grid.renderDebug();
self.destroy();
for (var i = 0; i < game.children.length; i++) {
if (game.children[i].isTowerRange && game.children[i].tower === self.tower) {
game.removeChild(game.children[i]);
break;
}
}
};
closeButton.down = function (x, y, obj) {
hideUpgradeMenu(self);
selectedTower = null;
grid.renderDebug();
};
self.update = function () {
if (self.tower.level >= self.tower.maxLevel) {
if (buttonText.text !== 'Max Level') {
buttonText.setText('Max Level');
buttonBackground.tint = 0x888888;
}
return;
}
// Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
var baseUpgradeCost = getTowerCost(self.tower.id);
var currentUpgradeCost;
if (self.tower.level >= self.tower.maxLevel) {
currentUpgradeCost = 0;
} else if (self.tower.level === self.tower.maxLevel - 1) {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
} else {
currentUpgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
}
var canAfford = 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 () {
// Start button is now disabled - game starts automatically with countdown
};
for (var i = 0; i < totalWaves; i++) {
var marker = new Container();
var block = marker.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
block.width = blockWidth - 10;
block.height = 70 * 2;
// --- Begin new unified wave logic ---
var waveType = "normal";
var enemyType = "normal";
var enemyCount = 10;
var isBossWave = (i + 1) % 10 === 0;
// Ensure all types appear in early waves
if (i === 0) {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
} else if (i === 1) {
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if (i === 2) {
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if (i === 3) {
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if (i === 4) {
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else if (isBossWave) {
// Boss waves: cycle through all boss types, last boss is always flying
var bossTypes = ['normal', 'fast', 'immune', 'flying'];
var bossTypeIndex = Math.floor((i + 1) / 10) - 1;
if (i === totalWaves - 1) {
// Last boss is always flying
enemyType = 'flying';
waveType = "Boss Flying";
block.tint = 0xFFFF00;
} else {
enemyType = bossTypes[bossTypeIndex % bossTypes.length];
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
waveType = "Boss Normal";
break;
case 'fast':
block.tint = 0x00AAFF;
waveType = "Boss Fast";
break;
case 'immune':
block.tint = 0xAA0000;
waveType = "Boss Immune";
break;
case 'flying':
block.tint = 0xFFFF00;
waveType = "Boss Flying";
break;
}
}
enemyCount = 1;
// Make the wave indicator for boss waves stand out
// Set boss wave color to the color of the wave type
switch (enemyType) {
case 'normal':
block.tint = 0xAAAAAA;
break;
case 'fast':
block.tint = 0x00AAFF;
break;
case 'immune':
block.tint = 0xAA0000;
break;
case 'flying':
block.tint = 0xFFFF00;
break;
default:
block.tint = 0xFF0000;
break;
}
} else if ((i + 1) % 5 === 0) {
// Every 5th non-boss wave is fast
block.tint = 0x00AAFF;
waveType = "Fast";
enemyType = "fast";
enemyCount = 10;
} else if ((i + 1) % 4 === 0) {
// Every 4th non-boss wave is immune
block.tint = 0xAA0000;
waveType = "Immune";
enemyType = "immune";
enemyCount = 10;
} else if ((i + 1) % 7 === 0) {
// Every 7th non-boss wave is flying
block.tint = 0xFFFF00;
waveType = "Flying";
enemyType = "flying";
enemyCount = 10;
} else if ((i + 1) % 3 === 0) {
// Every 3rd non-boss wave is swarm
block.tint = 0xFF00FF;
waveType = "Swarm";
enemyType = "swarm";
enemyCount = 30;
} else {
block.tint = 0xAAAAAA;
waveType = "Normal";
enemyType = "normal";
enemyCount = 10;
}
// --- End new unified wave logic ---
// Mark boss waves with a special visual indicator
if (isBossWave && enemyType !== 'swarm') {
// Add a crown or some indicator to the wave marker for boss waves
var bossIndicator = marker.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
bossIndicator.width = 30;
bossIndicator.height = 30;
bossIndicator.tint = 0xFFD700; // Gold color
bossIndicator.y = -block.height / 2 - 15;
// Change the wave type text to indicate boss
waveType = "BOSS";
}
// Store the wave type and enemy count
self.waveTypes[i] = enemyType;
self.enemyCounts[i] = enemyCount;
// Add shadow for wave type - 30% smaller than before
var waveTypeShadow = new Text2(waveType, {
size: 56,
fill: 0x000000,
weight: 800
});
waveTypeShadow.anchor.set(0.5, 0.5);
waveTypeShadow.x = 4;
waveTypeShadow.y = 4;
marker.addChild(waveTypeShadow);
// Add wave type text - 30% smaller than before
var waveTypeText = new Text2(waveType, {
size: 56,
fill: 0xFFFFFF,
weight: 800
});
waveTypeText.anchor.set(0.5, 0.5);
waveTypeText.y = 0;
marker.addChild(waveTypeText);
// Add shadow for wave number - 20% larger than before
var waveNumShadow = new Text2((i + 1).toString(), {
size: 48,
fill: 0x000000,
weight: 800
});
waveNumShadow.anchor.set(1.0, 1.0);
waveNumShadow.x = blockWidth / 2 - 16 + 5;
waveNumShadow.y = block.height / 2 - 12 + 5;
marker.addChild(waveNumShadow);
// Main wave number text - 20% larger than before
var waveNum = new Text2((i + 1).toString(), {
size: 48,
fill: 0xFFFFFF,
weight: 800
});
waveNum.anchor.set(1.0, 1.0);
waveNum.x = blockWidth / 2 - 16;
waveNum.y = block.height / 2 - 12;
marker.addChild(waveNum);
marker.x = -self.indicatorWidth + (i + 1) * blockWidth;
self.addChild(marker);
self.waveMarkers.push(marker);
}
// Get wave type for a specific wave number
self.getWaveType = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return "normal";
}
// If this is a boss wave (waveNumber % 10 === 0), and the type is the same as lastBossType
// then we should return a different boss type
var waveType = self.waveTypes[waveNumber - 1];
return waveType;
};
// Get enemy count for a specific wave number
self.getEnemyCount = function (waveNumber) {
if (waveNumber < 1 || waveNumber > totalWaves) {
return 10;
}
return self.enemyCounts[waveNumber - 1];
};
// Get display name for a wave type
self.getWaveTypeName = function (waveNumber) {
var type = self.getWaveType(waveNumber);
var typeName = type.charAt(0).toUpperCase() + type.slice(1);
// Add boss prefix for boss waves (every 10th wave)
if (waveNumber % 10 === 0 && waveNumber > 0 && type !== 'swarm') {
typeName = "BOSS";
}
return typeName;
};
self.positionIndicator = new Container();
var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator.width = blockWidth - 10;
indicator.height = 16;
indicator.tint = 0xffad0e;
indicator.y = -65;
var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
indicator2.width = blockWidth - 10;
indicator2.height = 16;
indicator2.tint = 0xffad0e;
indicator2.y = 65;
var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
leftWall.width = 16;
leftWall.height = 146;
leftWall.tint = 0xffad0e;
leftWall.x = -(blockWidth - 16) / 2;
var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
anchorX: 0.5,
anchorY: 0.5
});
rightWall.width = 16;
rightWall.height = 146;
rightWall.tint = 0xffad0e;
rightWall.x = (blockWidth - 16) / 2;
self.addChild(self.positionIndicator);
self.update = function () {
var progress = waveTimer / nextWaveTime;
var moveAmount = (progress + currentWave) * blockWidth;
for (var i = 0; i < self.waveMarkers.length; i++) {
var marker = self.waveMarkers[i];
marker.x = -moveAmount + i * blockWidth;
}
self.positionIndicator.x = 0;
for (var i = 0; i < totalWaves + 1; i++) {
var marker = self.waveMarkers[i];
if (i === 0) {
continue;
}
var block = marker.children[0];
if (i - 1 < currentWave) {
block.alpha = .5;
}
}
self.handleWaveProgression = function () {
if (!self.gameStarted) {
return;
}
if (currentWave < totalWaves) {
waveTimer++;
if (waveTimer >= nextWaveTime) {
waveTimer = 0;
currentWave++;
waveInProgress = true;
waveSpawned = false;
if (currentWave != 1) {
var waveType = self.getWaveTypeName(currentWave);
var enemyCount = self.getEnemyCount(currentWave);
var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
}
}
};
self.handleWaveProgression();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x8B7355
});
/****
* Game Code
****/
var isHidingUpgradeMenu = false;
var gameStarted = false;
var startScreen = new Container();
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;
}
});
}
var CELL_SIZE = 76;
var pathId = 1;
var maxScore = 0;
var enemies = [];
var towers = [];
var bullets = [];
var defenses = [];
var snakes = [];
var selectedTower = null;
var gold = 200;
var lives = 5;
var score = 0;
var xp = 0;
var level = 1;
var currentWave = 0;
var totalWaves = 50;
var waveTimer = 0;
var waveInProgress = false;
var waveSpawned = false;
var nextWaveTime = 2000;
var sourceTower = null;
var enemiesToSpawn = 10; // Default number of enemies per wave
var backgroundFlames = [];
var flameSpawnTimer = 0;
var flameSpawnInterval = 180 + Math.random() * 240; // Spawn flame every 3-7 seconds
var lightningTimer = 0;
var lightningInterval = 900; // 15 seconds at 60 FPS
var groundShakeTimer = 0;
var groundShakeInterval = 1200; // 20 seconds at 60 FPS
var flameLayer = new Container();
var smokeLayer = new Container();
var smokeParticles = [];
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700,
weight: 800
});
goldText.anchor.set(0.5, 0.5);
var livesText = new Text2('Lives: ' + lives, {
size: 60,
fill: 0x00FF00,
weight: 800
});
livesText.anchor.set(0.5, 0.5);
var scoreText = new Text2('Score: ' + score, {
size: 60,
fill: 0xFF0000,
weight: 800
});
scoreText.anchor.set(0.5, 0.5);
var xpText = new Text2('XP: ' + xp, {
size: 60,
fill: 0x9400D3,
weight: 800
});
xpText.anchor.set(0.5, 0.5);
var levelText = new Text2('Level: ' + level, {
size: 60,
fill: 0x00CED1,
weight: 800
});
levelText.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);
LK.gui.top.addChild(xpText);
LK.gui.top.addChild(levelText);
livesText.x = 0;
livesText.y = topMargin;
goldText.x = -spacing;
goldText.y = topMargin;
scoreText.x = spacing;
scoreText.y = topMargin;
xpText.x = 0;
xpText.y = topMargin + 80;
levelText.x = 0;
levelText.y = topMargin + 160;
function updateUI() {
goldText.setText('Gold: ' + gold);
livesText.setText('Lives: ' + lives);
scoreText.setText('Score: ' + score);
xpText.setText('XP: ' + xp);
levelText.setText('Level: ' + level);
}
function setGold(value) {
gold = value;
updateUI();
}
var debugLayer = new Container();
var towerLayer = new Container();
// Create three separate layers for enemy hierarchy
var enemyLayerBottom = new Container(); // For normal enemies
var enemyLayerMiddle = new Container(); // For shadows
var enemyLayerTop = new Container(); // For flying enemies
var enemyLayer = new Container(); // Main container to hold all enemy layers
// Add layers in correct order (bottom first, then middle for shadows, then top)
enemyLayer.addChild(enemyLayerBottom);
enemyLayer.addChild(enemyLayerMiddle);
enemyLayer.addChild(enemyLayerTop);
var grid = new Grid(24, 29 + 6);
grid.x = 150;
grid.y = 200 - CELL_SIZE * 4;
grid.pathFind();
grid.renderDebug();
// Add background image
var backgroundImage = LK.getAsset('arkaplan', {
anchorX: 0,
anchorY: 0,
scaleX: 1,
scaleY: 1
});
backgroundImage.x = 0;
backgroundImage.y = 0;
game.addChild(backgroundImage);
debugLayer.addChild(grid);
game.addChild(debugLayer);
game.addChild(flameLayer);
game.addChild(smokeLayer);
game.addChild(towerLayer);
game.addChild(enemyLayer);
var offset = 0;
var towerPreview = new TowerPreview();
game.addChild(towerPreview);
towerPreview.visible = false;
var isDragging = false;
var draggingTower = null;
var dragOffset = {
x: 0,
y: 0
};
function wouldBlockPath(gridX, gridY) {
var cells = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(gridX + i, gridY + j);
if (cell) {
cells.push({
cell: cell,
originalType: cell.type
});
cell.type = 1;
}
}
}
var blocked = grid.pathFind();
for (var i = 0; i < cells.length; i++) {
cells[i].cell.type = cells[i].originalType;
}
grid.pathFind();
grid.renderDebug();
return blocked;
}
function getTowerCost(towerType) {
var cost = 5;
switch (towerType) {
case 'rapid':
cost = 15;
break;
case 'sniper':
cost = 25;
break;
case 'splash':
cost = 35;
break;
case 'slow':
cost = 45;
break;
case 'poison':
cost = 55;
break;
}
return cost;
}
function getTowerSellValue(totalValue) {
return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
}
function placeTower(gridX, gridY, towerType) {
var towerCost = getTowerCost(towerType);
if (gold >= towerCost) {
// Ensure towerType is 'default' if not specified
var actualTowerType = towerType || 'default';
var tower = new Tower(actualTowerType);
// Apply level 2 fire rate bonus if applicable
if (level >= 2) {
tower.fireRate = 30; // 0.5 second attack interval
}
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) {
if (!gameStarted) {
return;
}
var upgradeMenuVisible = game.children.some(function (child) {
return child instanceof UpgradeMenu;
});
if (upgradeMenuVisible) {
return;
}
// Check if clicking on an existing tower first
for (var i = 0; i < towers.length; i++) {
var tower = towers[i];
if (x >= tower.x - tower.baseGraphics.width / 2 && x <= tower.x + tower.baseGraphics.width / 2 && y >= tower.y - tower.baseGraphics.height / 2 && y <= tower.y + tower.baseGraphics.height / 2) {
// Start dragging the existing tower
draggingTower = tower;
isDragging = true;
dragOffset.x = x - tower.x;
dragOffset.y = y - tower.y;
// Clear the tower's grid cells
for (var j = 0; j < 2; j++) {
for (var k = 0; k < 2; k++) {
var cell = grid.getCell(tower.gridX + j, tower.gridY + k);
if (cell) {
cell.type = 0;
}
}
}
// Show visual feedback that tower is being dragged
tower.alpha = 0.7;
// Remove tower from towers in range for all cells
tower.refreshCellsInRange();
grid.pathFind();
grid.renderDebug();
return;
}
}
// Check source towers for creating new towers
for (var i = 0; i < sourceTowers.length; i++) {
var tower = sourceTowers[i];
if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
towerPreview.visible = true;
isDragging = true;
towerPreview.towerType = tower.towerType;
towerPreview.updateAppearance();
// Apply the same offset as in move handler to ensure consistency when starting drag
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
break;
}
}
};
game.move = function (x, y, obj) {
if (!gameStarted) {
return;
}
if (isDragging) {
if (draggingTower) {
// Move the existing tower
draggingTower.x = x - dragOffset.x;
draggingTower.y = y - dragOffset.y;
} else {
// Shift the y position upward by 1.5 tiles to show preview above finger
towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
}
}
};
game.up = function (x, y, obj) {
if (!gameStarted) {
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 (draggingTower) {
// Handle existing tower repositioning
var gridPosX = draggingTower.x - grid.x;
var gridPosY = draggingTower.y - grid.y;
var newGridX = Math.floor(gridPosX / CELL_SIZE);
var newGridY = Math.floor(gridPosY / CELL_SIZE);
// Check if the new position is valid
var validPlacement = true;
var blockedByEnemy = false;
// Check bounds
if (newGridY <= 4 || newGridY + 1 >= grid.cells[0].length - 4) {
validPlacement = false;
} else {
// Check if cells are available
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(newGridX + i, newGridY + j);
if (!cell || cell.type !== 0) {
validPlacement = false;
break;
}
}
if (!validPlacement) break;
}
// Check for enemies in the way
if (validPlacement) {
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (enemy.currentCellY < 4) continue;
if (!enemy.isFlying) {
if (enemy.cellX >= newGridX && enemy.cellX < newGridX + 2 && enemy.cellY >= newGridY && enemy.cellY < newGridY + 2) {
blockedByEnemy = true;
break;
}
}
}
}
}
// Check if placement would block path
var wouldBlock = false;
if (validPlacement && !blockedByEnemy) {
wouldBlock = wouldBlockPath(newGridX, newGridY);
}
if (validPlacement && !blockedByEnemy && !wouldBlock) {
// Place tower in new position
draggingTower.placeOnGrid(newGridX, newGridY);
draggingTower.alpha = 1;
var notification = game.addChild(new Notification("Tower moved!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else {
// Return tower to original position
draggingTower.x = grid.x + draggingTower.gridX * CELL_SIZE + CELL_SIZE / 2;
draggingTower.y = grid.y + draggingTower.gridY * CELL_SIZE + CELL_SIZE / 2;
draggingTower.alpha = 1;
// Restore grid cells
for (var i = 0; i < 2; i++) {
for (var j = 0; j < 2; j++) {
var cell = grid.getCell(draggingTower.gridX + i, draggingTower.gridY + j);
if (cell) {
cell.type = 1;
}
}
}
draggingTower.refreshCellsInRange();
if (!validPlacement) {
var notification = game.addChild(new Notification("Cannot move tower here!"));
} else if (blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot move: Enemy in the way!"));
} else if (wouldBlock) {
var notification = game.addChild(new Notification("Cannot move: Would block path!"));
}
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
draggingTower = null;
grid.pathFind();
grid.renderDebug();
} else {
// Handle new tower placement
if (towerPreview.canPlace) {
if (!wouldBlockPath(towerPreview.gridX, towerPreview.gridY)) {
placeTower(towerPreview.gridX, towerPreview.gridY, towerPreview.towerType);
} else {
var notification = game.addChild(new Notification("Tower would block the path!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
} else if (towerPreview.blockedByEnemy) {
var notification = game.addChild(new Notification("Cannot build: Enemy in the way!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
} else if (towerPreview.visible) {
var notification = game.addChild(new Notification("Cannot build here!"));
notification.x = 2048 / 2;
notification.y = grid.height - 50;
}
towerPreview.visible = false;
}
if (isDragging) {
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
}
}
};
var waveIndicator = new WaveIndicator();
waveIndicator.x = 2048 / 2;
waveIndicator.y = 2732 - 80;
game.addChild(waveIndicator);
var nextWaveButtonContainer = new Container();
var nextWaveButton = new NextWaveButton();
nextWaveButton.x = 2048 - 200;
nextWaveButton.y = 2732 - 100 + 20;
nextWaveButtonContainer.addChild(nextWaveButton);
game.addChild(nextWaveButtonContainer);
// Create start screen with Hogwarts under attack image
var startScreenBackground = startScreen.attachAsset('hogwartsAttack', {
anchorX: 0.5,
anchorY: 0.5
});
startScreenBackground.x = 1024;
startScreenBackground.y = 1366; // Center vertically
// Image is already 2048x2732, perfect fit
startScreenBackground.scaleX = 1;
startScreenBackground.scaleY = 1;
// Title text removed as requested
// Subtitle removed as requested
// Add instruction text to tap anywhere
var tapInstructionShadow = new Text2("Tap anywhere to start", {
size: 80,
fill: 0x000000,
weight: 600
});
tapInstructionShadow.anchor.set(0.5, 0.5);
tapInstructionShadow.x = 1024 + 4;
tapInstructionShadow.y = 2732 - 600 + 4;
startScreen.addChild(tapInstructionShadow);
var tapInstruction = new Text2("Tap anywhere to start", {
size: 80,
fill: 0xFFD700,
weight: 600
});
tapInstruction.anchor.set(0.5, 0.5);
tapInstruction.x = 1024;
tapInstruction.y = 2732 - 600;
startScreen.addChild(tapInstruction);
// Add pulsing animation to instruction text
var _pulseAnimation = function pulseAnimation() {
if (tapInstruction.parent) {
tween(tapInstruction, {
alpha: 0.4,
scaleX: 0.95,
scaleY: 0.95
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (tapInstruction.parent) {
tween(tapInstruction, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: _pulseAnimation
});
}
}
});
}
};
_pulseAnimation();
// Make entire start screen clickable
startScreen.down = function () {
// Remove start screen
game.removeChild(startScreen);
// Start countdown directly
startCountdown();
};
// Function to start countdown
function startCountdown() {
// Add 100 image after hogwartsAttack
var image100Container = new Container();
game.addChild(image100Container);
var image100 = image100Container.attachAsset('100', {
anchorX: 0.5,
anchorY: 0.5
});
// Scale to fill the entire screen
image100.width = 2048;
image100.height = 2732;
image100Container.x = 2048 / 2;
image100Container.y = 2732 / 2;
image100Container.alpha = 0;
// Ensure it's on top of other elements
game.setChildIndex(image100Container, game.children.length - 1);
// Fade in the 100 image
tween(image100Container, {
alpha: 1
}, {
duration: 500,
easing: tween.easeOut
});
// Make 100 image clickable to start the game
image100Container.down = function () {
// Fade out and start game
tween(image100Container, {
alpha: 0
}, {
duration: 500,
easing: tween.easeIn,
onFinish: function onFinish() {
if (image100Container.parent) {
image100Container.destroy();
}
// Start the actual game after clicking 100
gameStarted = true;
// Update wave indicator to show game has started
waveIndicator.gameStarted = true;
currentWave = 0;
waveTimer = nextWaveTime;
// Update start marker visual
var startMarker = waveIndicator.waveMarkers[0];
if (startMarker && startMarker.children[0]) {
startMarker.children[0].tint = 0x00FF00;
}
if (startMarker && startMarker.children[1]) {
startMarker.children[1].setText("Started!");
}
if (startMarker && startMarker.children[2]) {
startMarker.children[2].setText("Started!");
startMarker.children[2].x = 4;
startMarker.children[2].y = 4;
}
// Add wizard movement warning
var movementWarning = game.addChild(new Notification("Wizards can be moved by dragging them!"));
movementWarning.x = 2048 / 2;
movementWarning.y = 2732 / 2;
// Make the warning last longer than normal notifications
movementWarning.fadeOutTime = 300; // 5 seconds at 60 FPS
}
});
};
}
// Add start screen to game
game.addChild(startScreen);
// Play exciting music on start screen
LK.playMusic('g1', {
loop: true
});
// Wizard movement warning and music are now handled in the start screen button handler
// Create restart button
var restartButton = new Container();
var restartBackground = restartButton.attachAsset('notification', {
anchorX: 0.5,
anchorY: 0.5
});
restartBackground.width = 300;
restartBackground.height = 120;
restartBackground.tint = 0xFF6B6B;
var restartText = new Text2("Restart", {
size: 60,
fill: 0xFFFFFF,
weight: 800
});
restartText.anchor.set(0.5, 0.5);
restartButton.addChild(restartText);
// Position in top left area but outside the reserved 100x100 zone
restartButton.x = 250; // Adjusted position for larger button
restartButton.y = 140; // Adjusted position for larger button
restartButton.down = function () {
// Reset all game variables to initial state
gold = 200;
lives = 20;
score = 0;
xp = 0;
level = 1;
currentWave = 0;
waveTimer = 0;
waveInProgress = false;
waveSpawned = false;
selectedTower = null;
isDragging = false;
draggingTower = null;
// Clear all enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
}
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
}
enemies = [];
// Clear all bullets
for (var i = bullets.length - 1; i >= 0; i--) {
if (bullets[i].parent) {
bullets[i].destroy();
}
}
bullets = [];
// Clear all towers
for (var i = towers.length - 1; i >= 0; i--) {
var tower = towers[i];
// Reset grid cells
for (var j = 0; j < 2; j++) {
for (var k = 0; k < 2; k++) {
var cell = grid.getCell(tower.gridX + j, tower.gridY + k);
if (cell) {
cell.type = 0;
}
}
}
towerLayer.removeChild(tower);
}
towers = [];
// Clear any upgrade menus
var upgradeMenus = game.children.filter(function (child) {
return child instanceof UpgradeMenu;
});
for (var i = 0; i < upgradeMenus.length; i++) {
upgradeMenus[i].destroy();
}
// Clear any range indicators
for (var i = game.children.length - 1; i >= 0; i--) {
if (game.children[i].isTowerRange) {
game.removeChild(game.children[i]);
}
}
// Clear background flames
for (var i = backgroundFlames.length - 1; i >= 0; i--) {
if (backgroundFlames[i].parent) {
backgroundFlames[i].destroy();
}
}
backgroundFlames = [];
// Clear smoke particles
for (var i = smokeParticles.length - 1; i >= 0; i--) {
if (smokeParticles[i].parent) {
smokeParticles[i].destroy();
}
}
smokeParticles = [];
flameSpawnTimer = 0;
lightningTimer = 0;
groundShakeTimer = 0;
// Reset wave indicator
waveIndicator.gameStarted = false;
var startMarker = waveIndicator.waveMarkers[0];
if (startMarker && startMarker.children[0]) {
startMarker.children[0].tint = 0x00AA00;
}
if (startMarker && startMarker.children[1]) {
startMarker.children[1].setText("Start Game");
}
if (startMarker && startMarker.children[2]) {
startMarker.children[2].setText("Start Game");
}
// Reset grid
grid.pathFind();
grid.renderDebug();
// Update UI
updateUI();
// Hide tower preview
towerPreview.visible = false;
var notification = game.addChild(new Notification("Game restarted!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
};
game.addChild(restartButton);
var towerTypes = ['default', 'rapid', 'sniper', 'splash', 'slow', 'poison'];
var sourceTowers = [];
var towerSpacing = 300; // Increase spacing for larger towers
var startX = 2048 / 2 - towerTypes.length * towerSpacing / 2 + towerSpacing / 2;
var towerY = 2732 - CELL_SIZE * 3 - 90;
for (var i = 0; i < towerTypes.length; i++) {
var tower = new SourceTower(towerTypes[i]);
tower.x = startX + i * towerSpacing;
tower.y = towerY;
towerLayer.addChild(tower);
sourceTowers.push(tower);
}
sourceTower = null;
enemiesToSpawn = 10;
game.update = function () {
if (!gameStarted) {
return;
}
if (waveInProgress) {
if (!waveSpawned) {
waveSpawned = true;
// Get wave type and enemy count from the wave indicator
var waveType = waveIndicator.getWaveType(currentWave);
var enemyCount = waveIndicator.getEnemyCount(currentWave);
// Check if this is a boss wave
var isBossWave = currentWave % 10 === 0 && currentWave > 0;
if (isBossWave && waveType !== 'swarm') {
// Boss waves have just 1 enemy regardless of what the wave indicator says
enemyCount = 1;
// Show boss announcement
var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️"));
notification.x = 2048 / 2;
notification.y = grid.height - 200;
}
// Spawn the appropriate number of enemies
for (var i = 0; i < enemyCount; i++) {
var enemy = new Enemy(waveType);
// Play monster sound when enemy spawns
LK.getSound('ron').play();
// Add enemy to the appropriate layer based on type
if (enemy.isFlying) {
// Add flying enemy to the top layer
enemyLayerTop.addChild(enemy);
// If it's a flying enemy, add its shadow to the middle layer
if (enemy.shadow) {
enemyLayerMiddle.addChild(enemy.shadow);
}
} else {
// Add normal/ground enemies to the bottom layer
enemyLayerBottom.addChild(enemy);
}
// Scale difficulty with wave number but don't apply to boss
// as bosses already have their health multiplier
// Use exponential scaling for health
var healthMultiplier = Math.pow(1.12, currentWave); // ~20% increase per wave
enemy.maxHealth = Math.round(enemy.maxHealth * healthMultiplier);
enemy.health = enemy.maxHealth;
// Increment speed slightly with wave number
//enemy.speed = enemy.speed + currentWave * 0.002;
// All enemy types now spawn in the middle 6 tiles at the top spacing
var gridWidth = 24;
var midPoint = Math.floor(gridWidth / 2); // 12
// Find a column that isn't occupied by another enemy that's not yet in view
var availableColumns = [];
for (var col = midPoint - 3; col < midPoint + 3; col++) {
var columnOccupied = false;
// Check if any enemy is already in this column but not yet in view
for (var e = 0; e < enemies.length; e++) {
if (enemies[e].cellX === col && enemies[e].currentCellY < 4) {
columnOccupied = true;
break;
}
}
if (!columnOccupied) {
availableColumns.push(col);
}
}
// If all columns are occupied, use original random method
var spawnX;
if (availableColumns.length > 0) {
// Choose a random unoccupied column
spawnX = availableColumns[Math.floor(Math.random() * availableColumns.length)];
} else {
// Fallback to random if all columns are occupied
spawnX = midPoint - 3 + Math.floor(Math.random() * 6); // x from 9 to 14
}
var spawnY = -1 - Math.random() * 5; // Random distance above the grid for spreading
enemy.cellX = spawnX;
enemy.cellY = 5; // Position after entry
enemy.currentCellX = spawnX;
enemy.currentCellY = spawnY;
enemy.waveNumber = currentWave;
enemies.push(enemy);
}
}
var currentWaveEnemiesRemaining = false;
for (var i = 0; i < enemies.length; i++) {
if (enemies[i].waveNumber === currentWave) {
currentWaveEnemiesRemaining = true;
break;
}
}
if (waveSpawned && !currentWaveEnemiesRemaining) {
waveInProgress = false;
waveSpawned = false;
}
}
for (var a = enemies.length - 1; a >= 0; a--) {
var enemy = enemies[a];
if (enemy.health <= 0) {
for (var i = 0; i < enemy.bulletsTargetingThis.length; i++) {
var bullet = enemy.bulletsTargetingThis[i];
bullet.targetEnemy = null;
}
// Boss enemies give more gold and score
var goldEarned = enemy.isBoss ? Math.floor(50 + (enemy.waveNumber - 1) * 5) : 5;
var goldIndicator = new GoldIndicator(goldEarned, enemy.x, enemy.y);
game.addChild(goldIndicator);
setGold(gold + goldEarned);
// Give more score for defeating a boss
var scoreValue = enemy.isBoss ? 100 : 5;
score += scoreValue;
// Increase XP by 10 when enemy is defeated
xp += 10;
// Check for level up
if (xp >= 100 && level < 2) {
level = 2;
xp = 0; // Reset XP to 0 when reaching level 2
// Set attack interval to 0.5 seconds (30 frames at 60 FPS) for all towers
for (var i = 0; i < towers.length; i++) {
towers[i].fireRate = 30;
}
// Show level up notification
var levelUpNotification = game.addChild(new Notification("LEVEL UP! Attack speed increased!"));
levelUpNotification.x = 2048 / 2;
levelUpNotification.y = 2732 / 2;
levelUpNotification.fadeOutTime = 240; // 4 seconds
}
// Add a notification for boss defeat
if (enemy.isBoss) {
var notification = game.addChild(new Notification("Boss defeated! +" + goldEarned + " gold!"));
notification.x = 2048 / 2;
notification.y = grid.height - 150;
}
updateUI();
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
continue;
}
if (grid.updateEnemy(enemy)) {
// Clean up shadow if it's a flying enemy
if (enemy.isFlying && enemy.shadow) {
enemyLayerMiddle.removeChild(enemy.shadow);
enemy.shadow = null;
}
// Remove enemy from the appropriate layer
if (enemy.isFlying) {
enemyLayerTop.removeChild(enemy);
} else {
enemyLayerBottom.removeChild(enemy);
}
enemies.splice(a, 1);
lives = Math.max(0, lives - 1);
updateUI();
if (lives <= 0) {
LK.showGameOver();
}
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i].parent) {
if (bullets[i].targetEnemy) {
var targetEnemy = bullets[i].targetEnemy;
var bulletIndex = targetEnemy.bulletsTargetingThis.indexOf(bullets[i]);
if (bulletIndex !== -1) {
targetEnemy.bulletsTargetingThis.splice(bulletIndex, 1);
}
}
bullets.splice(i, 1);
}
}
// Clean up destroyed snakes
for (var i = snakes.length - 1; i >= 0; i--) {
if (!snakes[i].parent) {
snakes.splice(i, 1);
}
}
if (towerPreview.visible) {
towerPreview.checkPlacement();
}
// Background flame spawning logic
if (gameStarted) {
flameSpawnTimer++;
if (flameSpawnTimer >= flameSpawnInterval) {
flameSpawnTimer = 0;
flameSpawnInterval = 180 + Math.random() * 240; // Reset interval
// Spawn a new flame at random position
var flameX = 100 + Math.random() * (2048 - 200); // Avoid edges
var flameY = 300 + Math.random() * (2200 - 300); // Avoid UI areas
var flame = new BackgroundFlame(flameX, flameY);
flameLayer.addChild(flame);
backgroundFlames.push(flame);
}
// Clean up destroyed flames from array
for (var i = backgroundFlames.length - 1; i >= 0; i--) {
if (!backgroundFlames[i].parent) {
backgroundFlames.splice(i, 1);
}
}
}
// Lightning effect every 15 seconds
if (gameStarted) {
lightningTimer++;
if (lightningTimer >= lightningInterval) {
lightningTimer = 0;
// Play pat sound for explosion
LK.getSound('pat').play();
// Play lightning sound
LK.getSound('lightning').play();
// Create smoke particles from edges without dark overlay
var smokeCount = 80; // Even more particles for realistic creeping fog
var centerX = 1024;
var centerY = 1366;
// Create initial wave of smoke with more focused origin points (like fire sources)
for (var i = 0; i < smokeCount; i++) {
var side = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
var startX, startY;
// Create clusters of smoke from specific "burning" points
var clusterOffset = Math.floor(i / 8) * 300; // Larger clusters
switch (side) {
case 0:
// Top edge - create fire source clusters
startX = (clusterOffset + Math.random() * 200) % 2048;
startY = -200 - Math.random() * 100;
break;
case 1:
// Right edge - create fire source clusters
startX = 2248 + Math.random() * 100;
startY = (clusterOffset + Math.random() * 200) % 2732;
break;
case 2:
// Bottom edge - create fire source clusters
startX = (clusterOffset + Math.random() * 200) % 2048;
startY = 2932 + Math.random() * 100;
break;
case 3:
// Left edge - create fire source clusters
startX = -200 - Math.random() * 100;
startY = (clusterOffset + Math.random() * 200) % 2732;
break;
}
// Create smoke with varied delays for waves of fog
LK.setTimeout(function (x, y) {
return function () {
// Vary the target point for more organic movement
// Smoke creeps inward with strong upward bias
var inwardAngle = Math.atan2(centerY - y, centerX - x);
var spreadAngle = inwardAngle + (Math.random() - 0.5) * Math.PI * 0.5;
var spreadDistance = 300 + Math.random() * 600;
var targetX = x + Math.cos(spreadAngle) * spreadDistance;
var targetY = y + Math.sin(spreadAngle) * spreadDistance - 200; // Strong upward bias
var smoke = new SmokeParticle(x, y, targetX, targetY);
smokeLayer.addChild(smoke);
smokeParticles.push(smoke);
};
}(startX, startY), Math.random() * 1200); // Staggered over 1200ms for more gradual effect
}
// Create second wave of smoke for continuous burning effect
LK.setTimeout(function () {
for (var i = 0; i < smokeCount * 0.75; i++) {
var side = Math.floor(Math.random() * 4);
var startX, startY;
// More concentrated fire sources for second wave
var fireSourceX = Math.floor(Math.random() * 5) * 400 + 200; // 5 main fire sources
var fireSourceY = Math.floor(Math.random() * 5) * 546 + 273;
switch (side) {
case 0:
startX = fireSourceX + (Math.random() - 0.5) * 250;
startY = -150 - Math.random() * 100;
break;
case 1:
startX = 2198 + Math.random() * 100;
startY = fireSourceY + (Math.random() - 0.5) * 250;
break;
case 2:
startX = fireSourceX + (Math.random() - 0.5) * 250;
startY = 2882 + Math.random() * 100;
break;
case 3:
startX = -150 - Math.random() * 100;
startY = fireSourceY + (Math.random() - 0.5) * 250;
break;
}
// Create more varied smoke movement patterns
LK.setTimeout(function (x, y) {
return function () {
// Smoke rises and creeps in a more realistic pattern
var riseAngle = -Math.PI / 2 + (Math.random() - 0.5) * Math.PI / 2; // Mostly upward with variation
var creepDistance = 400 + Math.random() * 600;
var targetX = x + Math.cos(riseAngle) * creepDistance * 0.6;
var targetY = y + Math.sin(riseAngle) * creepDistance - 100;
var smoke = new SmokeParticle(x, y, targetX, targetY);
smokeLayer.addChild(smoke);
smokeParticles.push(smoke);
};
}(startX, startY), Math.random() * 800);
}
}, 800);
}
}
// Ground shake effect every 20 seconds
if (gameStarted) {
groundShakeTimer++;
if (groundShakeTimer >= groundShakeInterval) {
groundShakeTimer = 0;
// Shake the ground (game container)
var shakeIntensity = 15;
var originalX = game.x;
var originalY = game.y;
// Create shake sequence
var _shakeSequence = function shakeSequence(count) {
if (count <= 0) {
// Return to original position
tween(game, {
x: originalX,
y: originalY
}, {
duration: 100,
easing: tween.easeOut
});
return;
}
// Random shake offset
var shakeX = originalX + (Math.random() - 0.5) * shakeIntensity;
var shakeY = originalY + (Math.random() - 0.5) * shakeIntensity;
tween(game, {
x: shakeX,
y: shakeY
}, {
duration: 50,
easing: tween.easeInOut,
onFinish: function onFinish() {
_shakeSequence(count - 1);
}
});
};
// Start shake sequence with 8 shakes
_shakeSequence(8);
}
}
if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
LK.showYouWin();
}
}; ===================================================================
--- original.js
+++ change.js
@@ -1271,37 +1271,109 @@
self.lifeTime = 300; // 5 seconds at 60 FPS
self.currentLifeTime = 0;
// Create snake body segments
self.segments = [];
- var numSegments = 5;
+ var numSegments = 8; // More segments for smoother snake
for (var i = 0; i < numSegments; i++) {
var segment = new Container();
// Create snake segment using rangeCircle asset
var segmentGraphics = segment.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
// Make segments progressively smaller - increased base size
- var segmentSize = 80 - i * 10; // Doubled from 40 - i * 5
+ var segmentSize = 90 - i * 8; // Larger, more gradual taper
segmentGraphics.width = segmentSize;
segmentGraphics.height = segmentSize;
- // Dark green/purple color for snake
- segmentGraphics.tint = 0x4B0082;
- segmentGraphics.alpha = 0.8;
- segment.x = -i * 25; // Increased spacing from 15 to 25
+ // Realistic green snake colors with pattern variation
+ var greenShades = [0x228B22, 0x2E8B57, 0x3CB371, 0x20B2AA, 0x008B8B];
+ var baseGreen = greenShades[i % greenShades.length];
+ // Add darker spots/pattern to some segments
+ if (i % 2 === 0 && i > 0) {
+ segmentGraphics.tint = 0x006400; // Dark green for pattern
+ } else {
+ segmentGraphics.tint = baseGreen;
+ }
+ segmentGraphics.alpha = 0.9;
+ segment.x = -i * 20; // Tighter spacing for realistic movement
segment.y = 0;
self.addChild(segment);
self.segments.push(segment);
}
- // Add snake head
+ // Create more detailed snake head
var head = self.segments[0];
+ // Remove the old head graphics first
+ head.removeChildAt(0);
+ // Main head shape (larger)
var headGraphics = head.attachAsset('rangeCircle', {
anchorX: 0.5,
anchorY: 0.5
});
- headGraphics.width = 100; // Doubled from 50
- headGraphics.height = 100; // Doubled from 50
- headGraphics.tint = 0x2F004D;
+ headGraphics.width = 110; // Larger head
+ headGraphics.height = 100; // Slightly oval
+ headGraphics.tint = 0x228B22; // Forest green
+ // Add eyes to the head
+ var leftEyeContainer = new Container();
+ var leftEyeWhite = leftEyeContainer.attachAsset('rangeCircle', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ leftEyeWhite.width = 25;
+ leftEyeWhite.height = 25;
+ leftEyeWhite.tint = 0xFFFFFF;
+ var leftEyePupil = leftEyeContainer.attachAsset('rangeCircle', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ leftEyePupil.width = 15;
+ leftEyePupil.height = 15;
+ leftEyePupil.tint = 0x000000;
+ leftEyePupil.x = 2;
+ leftEyeContainer.x = -25;
+ leftEyeContainer.y = -10;
+ head.addChild(leftEyeContainer);
+ // Right eye
+ var rightEyeContainer = new Container();
+ var rightEyeWhite = rightEyeContainer.attachAsset('rangeCircle', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ rightEyeWhite.width = 25;
+ rightEyeWhite.height = 25;
+ rightEyeWhite.tint = 0xFFFFFF;
+ var rightEyePupil = rightEyeContainer.attachAsset('rangeCircle', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ rightEyePupil.width = 15;
+ rightEyePupil.height = 15;
+ rightEyePupil.tint = 0x000000;
+ rightEyePupil.x = 2;
+ rightEyeContainer.x = 25;
+ rightEyeContainer.y = -10;
+ head.addChild(rightEyeContainer);
+ // Add nostrils
+ var leftNostril = head.attachAsset('rangeCircle', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ leftNostril.width = 8;
+ leftNostril.height = 8;
+ leftNostril.tint = 0x000000;
+ leftNostril.x = -10;
+ leftNostril.y = 15;
+ var rightNostril = head.attachAsset('rangeCircle', {
+ anchorX: 0.5,
+ anchorY: 0.5
+ });
+ rightNostril.width = 8;
+ rightNostril.height = 8;
+ rightNostril.tint = 0x000000;
+ rightNostril.x = 10;
+ rightNostril.y = 15;
+ // Store eye references for animation
+ self.leftEyePupil = leftEyePupil;
+ self.rightEyePupil = rightEyePupil;
// Movement pattern variables
self.moveAngle = Math.random() * Math.PI * 2;
self.waveAmplitude = 30;
self.waveFrequency = 0.1;
@@ -1393,15 +1465,26 @@
// Update segment positions to follow head with snake-like motion
for (var i = 1; i < self.segments.length; i++) {
var prevSegment = self.segments[i - 1];
var segment = self.segments[i];
- // Smooth following with delay
- var followSpeed = 0.3 - i * 0.04;
- segment.x += (prevSegment.x - segment.x - 25) * followSpeed; // Increased from 15 to 25
+ // Smooth following with delay - more fluid for realistic motion
+ var followSpeed = 0.25 - i * 0.02;
+ segment.x += (prevSegment.x - segment.x - 20) * followSpeed;
segment.y += (prevSegment.y - segment.y) * followSpeed;
- // Add slight oscillation
- segment.y += Math.sin(self.time * 0.1 + i * 0.5) * 3; // Increased from 2 to 3
+ // Add more complex oscillation for realistic slithering
+ var oscillation = Math.sin(self.time * 0.15 + i * 0.6) * (4 - i * 0.3);
+ var perpAngle = self.moveAngle + Math.PI / 2;
+ segment.x += Math.cos(perpAngle) * oscillation;
+ segment.y += Math.sin(perpAngle) * oscillation;
}
+ // Animate eyes to look in movement direction
+ if (self.leftEyePupil && self.rightEyePupil) {
+ var eyeOffset = Math.min(5, self.speed);
+ self.leftEyePupil.x = Math.cos(self.moveAngle) * eyeOffset;
+ self.leftEyePupil.y = Math.sin(self.moveAngle) * eyeOffset;
+ self.rightEyePupil.x = Math.cos(self.moveAngle) * eyeOffset;
+ self.rightEyePupil.y = Math.sin(self.moveAngle) * eyeOffset;
+ }
// Fade out near end of life
if (self.currentLifeTime > self.lifeTime - 60) {
self.alpha = (self.lifeTime - self.currentLifeTime) / 60;
}
ışıklı büyü ışıltısı. In-Game asset. 2d. High contrast. No shadows
3 boyutlu alev
elinde asa olsun ve sinirli olsun
harry potter evreninde kötü bir yaratık. In-Game asset. 2d. High contrast. No shadows
harry potter temasında korkutucu bir yılan yeşil renkte. In-Game asset. 2d. High contrast. No shadows