/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Bullet = Container.expand(function (type, startX, startY, targetX, targetY, damage) {
var self = Container.call(this);
var bulletGraphic = self.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
// Validate input coordinates
if (isNaN(startX) || isNaN(startY) || isNaN(targetX) || isNaN(targetY)) {
console.error('Bullet constructor received NaN coordinates:', {
startX: startX,
startY: startY,
targetX: targetX,
targetY: targetY
});
// Use fallback coordinates to prevent NaN propagation
startX = startX || 0;
startY = startY || 0;
targetX = targetX || 0;
targetY = targetY || 0;
}
self.x = startX;
self.y = startY;
self.damage = damage;
self.speed = 8;
self.targetX = targetX;
self.targetY = targetY;
var dx = targetX - startX;
var dy = targetY - startY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Calculate bullet trajectory angle for proper sprite rotation
var trajectoryAngle = Math.atan2(dy, dx);
// Set bullet graphic rotation to match trajectory direction
bulletGraphic.rotation = trajectoryAngle;
// Prevent division by zero and NaN calculations
if (distance === 0 || isNaN(distance)) {
console.warn('Bullet distance is zero or NaN, using default velocity');
self.velocityX = 1; // Default horizontal movement
self.velocityY = 0;
} else {
self.velocityX = dx / distance * self.speed;
self.velocityY = dy / distance * self.speed;
}
// Final validation to ensure velocities are not NaN
if (isNaN(self.velocityX) || isNaN(self.velocityY)) {
console.error('Bullet velocities are NaN, using fallback');
self.velocityX = 1;
self.velocityY = 0;
}
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
};
return self;
});
var DoublePlasmaCannon = Container.expand(function (gridX, gridY) {
var self = Container.call(this);
// Create static base - adjust anchor to position base higher for better cannon alignment
// Base is 200x150, adjust anchor Y to position base higher than center
var base = self.attachAsset('CanonDoublePlasmaBase', {
anchorX: 0.5,
anchorY: 0.3 // Adjusted to position base higher than center for better cannon alignment with 200x150 sprite
});
// Create rotating cannon part - position correctly on top of base
// Base is 200x150, cannon is 100x163.06, adjust anchor to position cannon higher in sprite
// Adjusting anchor to 0.3 to better center the cannon sprite (100x163.06)
var cannon = self.attachAsset('CanonDoublePlasma', {
anchorX: 0.5,
anchorY: 0.3 // Adjusted anchor point to better center during rotation for 100x163.06 sprite
});
self.towerType = 'doublePlasmaCannon';
self.gridX = gridX;
self.gridY = gridY;
self.range = 360;
self.damage = 75; // Increased damage for plasma rounds
self.fireRate = 30; // Faster fire rate between shots when loaded
self.lastShot = 0;
self.cost = 200;
self.currentTarget = null;
self.noTargetTimer = 0; // Timer to track how long without target
// Heavy weapon system properties
self.isReloading = true; // Start in reload phase
self.reloadTime = 1200; // 20 seconds at 60fps
self.reloadTimer = 0;
self.maxAmmo = 10;
self.currentAmmo = 0;
self.isActive = false; // Can only target when loaded and ready
self.isRotating = false; // Track if cannon is currently rotating
self.isAligned = false; // Track if cannon is aligned with target
self.wasRotating = false; // Track previous rotation state for sound triggers
self.rotatingStartPlayed = false; // Track if start sound has been played
self.rotatingLoopInterval = null; // Store rotation loop sound interval
// Random movement properties for idle behavior
self.idleMovementTimer = 0; // Timer for random movements
self.currentIdleAction = null; // Current idle action being performed
self.idleActionDuration = 0; // How long current action should last
self.idleActionTimer = 0; // Timer for current action
self.lastRotationState = false; // More precise rotation state tracking
self.rotationSoundDelay = 0; // Delay counter for better sound timing
self.getBulletType = function () {
return 'plasmaBullet';
};
self.update = function () {
// Heavy weapon reload system
if (self.isReloading) {
self.reloadTimer++;
// No targeting or rotation during reload
self.currentTarget = null;
self.noTargetTimer = 0;
// Visual feedback during reload - gradual blue charging effect
var reloadProgress = self.reloadTimer / self.reloadTime; // 0 to 1
var blueIntensity = Math.floor(reloadProgress * 127); // 0 to 127
// Create gradual blue tint - more blue as charging progresses
var redComponent = Math.floor(255 - blueIntensity * 0.7);
var greenComponent = Math.floor(255 - blueIntensity * 0.3);
var blueComponent = 255;
var chargeTint = redComponent << 16 | greenComponent << 8 | blueComponent;
tween.stop(cannon, {
tint: true
});
tween(cannon, {
tint: chargeTint // Gradual blue charging effect
}, {
duration: 100,
easing: tween.easeInOut
});
// Check if reload is complete
if (self.reloadTimer >= self.reloadTime) {
self.isReloading = false;
self.isActive = true;
self.currentAmmo = self.maxAmmo;
self.reloadTimer = 0;
// Stop charging sound loop
if (self.chargingLoopInterval) {
LK.clearInterval(self.chargingLoopInterval);
self.chargingLoopInterval = null;
}
if (self.chargingLoopTimeout) {
LK.clearTimeout(self.chargingLoopTimeout);
self.chargingLoopTimeout = null;
}
safePlayAudio('ChargingCompletePlasmaCannon', 'DoublePlasmaCannon ready audio failed');
}
return; // Exit early during reload - no other functionality
}
// Only proceed with normal behavior if loaded and active
if (!self.isActive || self.currentAmmo <= 0) {
// Start reload cycle
self.isReloading = true;
self.isActive = false;
self.reloadTimer = 0;
safePlayAudio('ChargingPlasmaCannon', 'DoublePlasmaCannon charging audio failed');
// Start looping the 2-4 second fragment immediately at 2 seconds
self.chargingLoopTimeout = LK.setTimeout(function () {
self.chargingLoopInterval = LK.setInterval(function () {
if (self.isReloading) {
safePlayAudio('ChargingPlasmaCannonLoop', 'DoublePlasmaCannon charging loop audio failed');
}
}, 2000); // Loop the 2-second fragment continuously
}, 2000); // Start loop immediately at 2 seconds, not after the full sound completes
return;
}
self.lastShot++;
// Find target and rotate cannon towards it (only when active)
var target = self.findTarget();
if (target) {
// Check if target changed - if so, reset alignment
if (self.currentTarget !== target) {
self.isAligned = false;
}
self.currentTarget = target;
self.noTargetTimer = 0; // Reset timer when target is found
// Calculate angle to target
var dx = target.x - self.x;
var dy = target.y - self.y;
var targetAngle = Math.atan2(dy, dx);
// FIX: Add 90-degree offset because sprite points UP instead of RIGHT
var spriteRotationAngle = targetAngle - Math.PI / 2;
// Smooth rotation using tween
var currentAngle = cannon.rotation;
var angleDiff = spriteRotationAngle - currentAngle;
// Normalize angle difference to shortest path
while (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
while (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Only tween if significant angle difference
if (Math.abs(angleDiff) > 0.1) {
// Check if this is the start of rotation (transition from not rotating to rotating)
if (!self.isRotating && !self.lastRotationState) {
// Play start rotation sound immediately
safePlayAudio('RotatingStartPlasmaCannon', 'DoublePlasmaCannon rotation start audio failed');
self.rotatingStartPlayed = true;
// Set delay before starting loop sound for better synchronization
self.rotationSoundDelay = 25; // Slightly shorter delay for responsiveness
// Clear any existing rotation loop
if (self.rotatingLoopInterval) {
LK.clearInterval(self.rotatingLoopInterval);
self.rotatingLoopInterval = null;
}
}
self.isRotating = true;
self.isAligned = false;
tween.stop(cannon, {
rotation: true
});
tween(cannon, {
rotation: spriteRotationAngle
}, {
duration: 400,
easing: tween.linear,
onFinish: function onFinish() {
self.isRotating = false;
self.isAligned = true;
// Stop rotation loop sound when rotation finishes
if (self.rotatingLoopInterval) {
LK.clearInterval(self.rotatingLoopInterval);
self.rotatingLoopInterval = null;
}
}
});
} else {
// Already aligned
self.isRotating = false;
self.isAligned = true;
// Stop rotation sounds if cannon stops rotating
if (self.rotatingLoopInterval) {
LK.clearInterval(self.rotatingLoopInterval);
self.rotatingLoopInterval = null;
}
}
// Visual charging feedback based on fire rate progress
var chargeProgress = self.lastShot / self.fireRate;
if (chargeProgress < 1.0) {
// Charging phase - gradually build up blue tint
var blueIntensity = Math.floor(chargeProgress * 127); // 0 to 127
// Create proper blue tint by reducing red and green components
var redComponent = Math.floor(255 - blueIntensity);
var greenComponent = Math.floor(255 - blueIntensity * 0.5);
var blueComponent = 255;
var chargeTint = redComponent << 16 | greenComponent << 8 | blueComponent;
// Stop any existing tint animations
tween.stop(cannon, {
tint: true
});
// Apply charging tint
tween(cannon, {
tint: chargeTint
}, {
duration: 100,
easing: tween.easeInOut
});
} else {
// Ready phase - steady blue glow (indicates loaded and ready)
tween.stop(cannon, {
tint: true
});
tween(cannon, {
tint: 0x80BFFF
}, {
duration: 100,
easing: tween.easeInOut
});
}
// Shoot if ready, has ammo, and cannon is properly aligned
if (self.lastShot >= self.fireRate && self.currentAmmo > 0 && self.isAligned && !self.isRotating) {
// Firing phase - brief bright flash
tween.stop(cannon, {
tint: true
});
tween(cannon, {
tint: 0x40A0FF
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
// Return to ammo-based blue intensity after firing
if (self.currentAmmo > 0) {
// Calculate new blue intensity based on remaining ammo after this shot
var ammoAfterShot = self.currentAmmo - 1;
if (ammoAfterShot > 0) {
var ammoProgress = ammoAfterShot / self.maxAmmo;
var blueIntensity = Math.floor(ammoProgress * 127 + 60);
var redComponent = Math.floor(255 - blueIntensity * 0.7);
var greenComponent = Math.floor(255 - blueIntensity * 0.3);
var blueComponent = 255;
var ammoTint = redComponent << 16 | greenComponent << 8 | blueComponent;
tween(cannon, {
tint: ammoTint // Dimmer blue based on remaining ammo
}, {
duration: 200,
easing: tween.easeOut
});
} else {
// Last shot - fade to normal
tween(cannon, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
} else {
tween(cannon, {
tint: 0xFFFFFF
}, {
duration: 200,
easing: tween.easeOut
});
}
}
});
self.shoot(target);
self.currentAmmo--; // Consume ammo
self.lastShot = 0;
}
} else {
// No target found - increment timer and implement random idle behavior
self.currentTarget = null;
self.isAligned = false; // Reset alignment when no target
self.noTargetTimer++;
self.idleMovementTimer++;
// Keep blue glow when loaded but no target (ready state) - intensity based on remaining ammo
if (self.currentAmmo > 0) {
// Calculate blue intensity based on remaining ammo (more ammo = more blue)
var ammoProgress = self.currentAmmo / self.maxAmmo; // 0 to 1
var blueIntensity = Math.floor(ammoProgress * 127 + 60); // 60 to 187 (ensures visible blue)
var redComponent = Math.floor(255 - blueIntensity * 0.7);
var greenComponent = Math.floor(255 - blueIntensity * 0.3);
var blueComponent = 255;
var ammoTint = redComponent << 16 | greenComponent << 8 | blueComponent;
tween.stop(cannon, {
tint: true
});
tween(cannon, {
tint: ammoTint // Blue intensity reflects remaining ammo
}, {
duration: 300,
easing: tween.easeOut
});
} else {
// Reset tint when no ammo
tween.stop(cannon, {
tint: true
});
tween(cannon, {
tint: 0xFFFFFF
}, {
duration: 300,
easing: tween.easeOut
});
}
// Random idle movement behavior - starts after 3 seconds without target
if (self.idleMovementTimer >= 180) {
// 3 seconds at 60fps
// Check if we need to start a new idle action
if (!self.currentIdleAction || self.idleActionTimer >= self.idleActionDuration) {
// Randomly choose an idle action every 2-5 seconds
var randomActions = ['rotateLeft', 'rotateRight', 'rotateUp', 'rotateDown', 'returnToCenter'];
self.currentIdleAction = randomActions[Math.floor(Math.random() * randomActions.length)];
self.idleActionDuration = 120 + Math.floor(Math.random() * 180); // 2-5 seconds
self.idleActionTimer = 0;
// Execute the chosen idle action
var targetAngle = cannon.rotation;
var shouldRotate = false;
switch (self.currentIdleAction) {
case 'rotateLeft':
targetAngle = cannon.rotation - Math.PI / 3; // 60 degrees left
shouldRotate = true;
break;
case 'rotateRight':
targetAngle = cannon.rotation + Math.PI / 3; // 60 degrees right
shouldRotate = true;
break;
case 'rotateUp':
targetAngle = -Math.PI / 2; // Point up
shouldRotate = true;
break;
case 'rotateDown':
targetAngle = Math.PI / 2; // Point down
shouldRotate = true;
break;
case 'returnToCenter':
targetAngle = -Math.PI / 2; // Return to original position
shouldRotate = true;
break;
}
if (shouldRotate) {
// Normalize target angle
while (targetAngle > Math.PI) {
targetAngle -= 2 * Math.PI;
}
while (targetAngle < -Math.PI) {
targetAngle += 2 * Math.PI;
}
var currentAngle = cannon.rotation;
var angleDiff = targetAngle - currentAngle;
// Normalize angle difference to shortest path
while (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
while (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Only rotate if significant difference
if (Math.abs(angleDiff) > 0.1) {
// Improved rotation sound logic with better timing
if (!self.isRotating && !self.lastRotationState) {
// Start rotation sound immediately
safePlayAudio('RotatingStartPlasmaCannon', 'DoublePlasmaCannon idle rotation start audio failed');
// Set delay before starting loop sound
self.rotationSoundDelay = 30; // 0.5 second delay at 60fps
// Clear any existing rotation loop
if (self.rotatingLoopInterval) {
LK.clearInterval(self.rotatingLoopInterval);
self.rotatingLoopInterval = null;
}
}
self.isRotating = true;
tween.stop(cannon, {
rotation: true
});
tween(cannon, {
rotation: targetAngle
}, {
duration: 600 + Math.floor(Math.random() * 400),
// 0.6-1.0 seconds for natural variation
easing: tween.easeInOut,
onFinish: function onFinish() {
self.isRotating = false;
// Stop rotation loop sound when rotation finishes
if (self.rotatingLoopInterval) {
LK.clearInterval(self.rotatingLoopInterval);
self.rotatingLoopInterval = null;
}
}
});
}
}
}
// Update idle action timer
self.idleActionTimer++;
}
// Handle rotation sound delay and looping for idle movement
if (self.isRotating && self.rotationSoundDelay > 0) {
self.rotationSoundDelay--;
if (self.rotationSoundDelay === 0 && !self.rotatingLoopInterval) {
// Start the rotation loop sound after delay
self.rotatingLoopInterval = LK.setInterval(function () {
if (self.isRotating) {
safePlayAudio('RotatingPlasmaCannon', 'DoublePlasmaCannon idle rotation loop audio failed');
}
}, 600); // Play every 600ms for a more natural feel
}
}
// Fallback: Return to original position after 8 seconds of no target
if (self.noTargetTimer >= 480 && !self.isRotating) {
// 8 seconds at 60fps
var originalAngle = -Math.PI / 2; // Pointing up (original position)
var currentAngle = cannon.rotation;
var angleDiff = originalAngle - currentAngle;
// Normalize angle difference to shortest path
while (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
while (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Only rotate if significant difference
if (Math.abs(angleDiff) > 0.1) {
if (!self.isRotating && !self.lastRotationState) {
safePlayAudio('RotatingStartPlasmaCannon', 'DoublePlasmaCannon return rotation start audio failed');
self.rotationSoundDelay = 30;
if (self.rotatingLoopInterval) {
LK.clearInterval(self.rotatingLoopInterval);
self.rotatingLoopInterval = null;
}
}
self.isRotating = true;
self.currentIdleAction = null; // Reset idle action
tween.stop(cannon, {
rotation: true
});
tween(cannon, {
rotation: originalAngle
}, {
duration: 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
self.isRotating = false;
if (self.rotatingLoopInterval) {
LK.clearInterval(self.rotatingLoopInterval);
self.rotatingLoopInterval = null;
}
}
});
}
}
}
// Update rotation state tracking for better sound synchronization
self.wasRotating = self.lastRotationState;
self.lastRotationState = self.isRotating;
// Handle rotation sound delay for better timing
if (self.isRotating && self.rotationSoundDelay > 0) {
self.rotationSoundDelay--;
if (self.rotationSoundDelay === 0 && !self.rotatingLoopInterval) {
// Start the rotation loop sound after proper delay
self.rotatingLoopInterval = LK.setInterval(function () {
if (self.isRotating) {
safePlayAudio('RotatingPlasmaCannon', 'DoublePlasmaCannon rotation loop audio failed');
}
}, 500); // Consistent timing with target tracking
}
}
};
self.findTarget = function () {
// Only target enemies when active (not reloading)
if (!self.isActive || self.isReloading || self.currentAmmo <= 0) {
return null;
}
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.range) {
return enemy;
}
}
return null;
};
self.shoot = function (target) {
// Calculate enemy movement prediction for accurate shooting
var enemyVelocityX = 0;
var enemyVelocityY = 0;
// Get enemy movement direction from their path
if (target.pathIndex < enemyPath.length - 1) {
var nextPoint = enemyPath[target.pathIndex + 1];
var dx = nextPoint.x - target.x;
var dy = nextPoint.y - target.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
enemyVelocityX = dx / distance * target.speed;
enemyVelocityY = dy / distance * target.speed;
}
}
// Calculate bullet travel time and predict enemy position
var bulletSpeed = 8; // Match bullet speed from Bullet class
var distanceToEnemy = Math.sqrt((target.x - self.x) * (target.x - self.x) + (target.y - self.y) * (target.y - self.y));
var bulletTravelTime = distanceToEnemy / bulletSpeed;
// Predict where enemy will be when bullet arrives
var predictedX = target.x + enemyVelocityX * bulletTravelTime;
var predictedY = target.y + enemyVelocityY * bulletTravelTime;
// Calculate cannon corner positions for dual shooting from sprite corners
// Get current cannon rotation angle
var cannonAngle = cannon.rotation;
// Calculate the corners of the cannon sprite
// Cannon sprite is 100x163.06, with anchor at 0.5, 0.3
var spriteWidth = 100;
var spriteHeight = 163.06;
var anchorX = 0.5;
var anchorY = 0.3;
// Calculate local corner positions relative to anchor point - shooting from rear corners
var leftCornerLocalX = -spriteWidth * anchorX + 20; // Slightly inward from edge
var rightCornerLocalX = spriteWidth * (1 - anchorX) - 20; // Slightly inward from edge
var cornerLocalY = spriteHeight * (1 - anchorY) - 30; // Near the rear end of cannon (opposite side)
// Transform local positions to world coordinates with rotation
var cosAngle = Math.cos(cannonAngle);
var sinAngle = Math.sin(cannonAngle);
// Left corner world position
var leftBarrelX = self.x + (leftCornerLocalX * cosAngle - cornerLocalY * sinAngle);
var leftBarrelY = self.y + (leftCornerLocalX * sinAngle + cornerLocalY * cosAngle);
// Right corner world position
var rightBarrelX = self.x + (rightCornerLocalX * cosAngle - cornerLocalY * sinAngle);
var rightBarrelY = self.y + (rightCornerLocalX * sinAngle + cornerLocalY * cosAngle);
// Fire two bullets - one from each barrel
var bullet1 = new Bullet(self.getBulletType(), leftBarrelX, leftBarrelY, predictedX, predictedY, self.damage);
var bullet2 = new Bullet(self.getBulletType(), rightBarrelX, rightBarrelY, predictedX, predictedY, self.damage);
bullets.push(bullet1);
bullets.push(bullet2);
game.addChild(bullet1);
game.addChild(bullet2);
safePlayAudio('BulletPlasmaCannon', 'DoublePlasmaCannon shoot audio failed');
// Add recoil effect - move cannon sprite backward slightly then return
var recoilDistance = 8; // Small backward movement
var currentX = cannon.x;
var currentY = cannon.y;
// Calculate recoil direction based on cannon's current rotation
// Since sprite points UP by default and we add -PI/2 offset, we need to account for that
var actualCannonDirection = cannon.rotation + Math.PI / 2; // Convert back to actual direction
var recoilAngle = actualCannonDirection + Math.PI; // Opposite to firing direction
var recoilX = currentX + Math.cos(recoilAngle) * recoilDistance;
var recoilY = currentY + Math.sin(recoilAngle) * recoilDistance;
// Stop any existing position tweens on cannon sprite
tween.stop(cannon, {
x: true,
y: true
});
// Quick recoil backward then return to original position
tween(cannon, {
x: recoilX,
y: recoilY
}, {
duration: 80,
easing: tween.easeOut,
onFinish: function onFinish() {
// Return to original position
tween(cannon, {
x: currentX,
y: currentY
}, {
duration: 120,
easing: tween.easeInOut
});
}
});
// Add glowing effect to both plasma bullets
tween(bullet1.children[0], {
tint: 0x80ff80
}, {
duration: 100
});
tween(bullet2.children[0], {
tint: 0x80ff80
}, {
duration: 100
});
};
return self;
});
var Enemy = Container.expand(function (type) {
var self = Container.call(this);
var enemyGraphic = self.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
self.enemyType = type;
self.pathIndex = 0;
self.speed = 1;
self.maxHealth = 100;
self.health = self.maxHealth;
self.goldValue = 10;
// Set enemy properties based on type
if (type === 'alienGrunt') {
self.health = 80;
self.maxHealth = 80;
self.speed = 1.5;
self.goldValue = 8;
} else if (type === 'shieldedWalker') {
self.health = 120;
self.maxHealth = 120;
self.speed = 1;
self.goldValue = 12;
} else if (type === 'agileCommander') {
self.health = 60;
self.maxHealth = 60;
self.speed = 2.5;
self.goldValue = 15;
} else if (type === 'armoredColossus') {
self.health = 200;
self.maxHealth = 200;
self.speed = 0.8;
self.goldValue = 25;
} else if (type === 'flyingDrone') {
self.health = 70;
self.maxHealth = 70;
self.speed = 2;
self.goldValue = 18;
}
self.takeDamage = function (damage) {
self.health -= damage;
if (self.health <= 0) {
return true; // Enemy is dead
}
return false;
};
self.update = function () {
if (self.pathIndex < enemyPath.length - 1) {
var currentTarget = enemyPath[self.pathIndex + 1];
var dx = currentTarget.x - self.x;
var dy = currentTarget.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 5) {
self.pathIndex++;
} else {
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
}
}
};
return self;
});
var Tower = Container.expand(function (type, gridX, gridY) {
var self = Container.call(this);
var towerGraphic = self.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
self.towerType = type;
self.gridX = gridX;
self.gridY = gridY;
self.range = 150;
self.damage = 25;
self.fireRate = 60; // frames between shots
self.lastShot = 0;
self.cost = 75;
self.currentTarget = null; // Store current target for continuous tracking
self.targetDirection = null; // Store targeting calculations for accurate shooting
// Set tower properties based on type
if (type === 'tacticalRecruit') {
self.range = 360;
self.damage = 20;
self.fireRate = 45;
self.cost = 50;
} else if (type === 'armoredElite') {
self.range = 360;
self.damage = 40;
self.fireRate = 90;
self.cost = 100;
} else if (type === 'rotatingTurret') {
self.range = 360;
self.damage = 15;
self.fireRate = 30;
self.cost = 75;
} else if (type === 'magneticCannon') {
self.range = 360;
self.damage = 60;
self.fireRate = 120;
self.cost = 150;
}
self.getBulletType = function () {
if (self.towerType === 'tacticalRecruit') {
return 'basicBullet';
}
if (self.towerType === 'armoredElite') {
return 'heavyBullet';
}
if (self.towerType === 'rotatingTurret') {
return 'turretBullet';
}
if (self.towerType === 'magneticCannon') {
return 'magneticBullet';
}
if (self.towerType === 'doublePlasmaCannon') {
return 'plasmaBullet';
}
return 'basicBullet';
};
self.update = function () {
self.lastShot++;
// Find and track target EVERY frame (like DoublePlasmaCannon)
var target = self.findTarget();
if (target) {
self.currentTarget = target;
// Calculate direction to target (standardized like DoublePlasmaCannon)
var dx = target.x - self.x;
var dy = target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var targetAngle = Math.atan2(dy, dx);
// Store targeting info for accurate shooting
self.targetDirection = {
dx: dx,
dy: dy,
distance: distance,
angle: targetAngle
};
// Add visual rotation like DoublePlasmaCannon
var currentAngle = towerGraphic.rotation;
var angleDiff = targetAngle - currentAngle;
// Normalize angle difference to shortest path
while (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
while (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Only tween if significant angle difference
if (Math.abs(angleDiff) > 0.1) {
tween.stop(towerGraphic, {
rotation: true
});
tween(towerGraphic, {
rotation: currentAngle + angleDiff
}, {
duration: 200,
easing: tween.easeOut
});
}
} else {
self.currentTarget = null; // Clear invalid target
self.targetDirection = null;
// Return tower to original position (pointing up) when no target
var originalAngle = -Math.PI / 2; // Pointing up (original position)
var currentAngle = towerGraphic.rotation;
var angleDiff = originalAngle - currentAngle;
// Normalize angle difference to shortest path
while (angleDiff > Math.PI) {
angleDiff -= 2 * Math.PI;
}
while (angleDiff < -Math.PI) {
angleDiff += 2 * Math.PI;
}
// Only tween if significant angle difference
if (Math.abs(angleDiff) > 0.1) {
tween.stop(towerGraphic, {
rotation: true
});
tween(towerGraphic, {
rotation: originalAngle
}, {
duration: 300,
easing: tween.easeOut
});
}
}
// Debug logging for tacticalRecruit
if (self.towerType === 'tacticalRecruit') {
console.log('tacticalRecruit update - target found:', self.currentTarget ? 'yes' : 'no', 'lastShot:', self.lastShot, 'fireRate:', self.fireRate);
}
// Shoot at current target when ready
if (self.currentTarget && self.lastShot >= self.fireRate) {
if (self.towerType === 'tacticalRecruit') {
console.log('tacticalRecruit shooting at target:', self.currentTarget.enemyType);
}
self.shoot(self.currentTarget);
self.lastShot = 0;
}
};
self.findTarget = function () {
// Check if current target is still valid first (like DoublePlasmaCannon logic)
if (self.currentTarget) {
var dx = self.currentTarget.x - self.x;
var dy = self.currentTarget.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.range && self.currentTarget.health > 0) {
return self.currentTarget; // Keep tracking same enemy
}
}
var closestTarget = null;
var closestDistance = Infinity;
if (self.towerType === 'tacticalRecruit') {
console.log('tacticalRecruit findTarget - checking', enemies.length, 'enemies, range:', self.range);
}
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (self.towerType === 'tacticalRecruit') {
console.log('tacticalRecruit checking enemy', i, 'distance:', distance, 'range:', self.range);
}
if (distance <= self.range && distance < closestDistance) {
closestTarget = enemy;
closestDistance = distance;
if (self.towerType === 'tacticalRecruit') {
console.log('tacticalRecruit found closer target! distance:', distance);
}
}
}
return closestTarget;
};
self.shoot = function (target) {
// Validate target and coordinates before shooting
if (!target || isNaN(target.x) || isNaN(target.y) || isNaN(self.x) || isNaN(self.y)) {
console.error('Tower shoot - invalid coordinates:', {
tower: {
x: self.x,
y: self.y
},
target: target ? {
x: target.x,
y: target.y
} : 'null target'
});
return;
}
// Calculate enemy movement prediction for accurate shooting
var enemyVelocityX = 0;
var enemyVelocityY = 0;
// Get enemy movement direction from their path
if (target.pathIndex < enemyPath.length - 1) {
var nextPoint = enemyPath[target.pathIndex + 1];
var dx = nextPoint.x - target.x;
var dy = nextPoint.y - target.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
enemyVelocityX = dx / distance * target.speed;
enemyVelocityY = dy / distance * target.speed;
}
}
// Calculate bullet travel time and predict enemy position
var bulletSpeed = 8; // Match bullet speed from Bullet class
var distanceToEnemy = Math.sqrt((target.x - self.x) * (target.x - self.x) + (target.y - self.y) * (target.y - self.y));
var bulletTravelTime = distanceToEnemy / bulletSpeed;
// Predict where enemy will be when bullet arrives
var predictedX = target.x + enemyVelocityX * bulletTravelTime;
var predictedY = target.y + enemyVelocityY * bulletTravelTime;
if (self.towerType === 'tacticalRecruit') {
console.log('tacticalRecruit shooting! Tower pos:', self.x, self.y, 'Target pos:', target.x, target.y, 'Predicted pos:', predictedX, predictedY, 'Bullet type:', self.getBulletType(), 'damage:', self.damage);
}
// Shoot at predicted position instead of current position
var bullet = new Bullet(self.getBulletType(), self.x, self.y, predictedX, predictedY, self.damage);
bullets.push(bullet);
game.addChild(bullet);
if (self.towerType === 'tacticalRecruit') {
console.log('tacticalRecruit bullet created and added, bullets array length:', bullets.length);
}
safePlayAudio('shoot', 'Tower shoot audio failed');
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a2e
});
/****
* Game Code
****/
// Utility functions for stability
function isValidNumber(num) {
return typeof num === 'number' && !isNaN(num) && num !== Infinity && num !== -Infinity;
}
function safeAssign(obj, property, value, fallback) {
obj[property] = isValidNumber(value) ? value : fallback;
}
// Game state variables
var gold = 200;
var baseHealth = 20;
var currentWave = 1;
var totalWaves = 20;
var waveInProgress = false;
var waveCompleted = false;
var waveBreakTime = 0;
var gameEnded = false;
var enemiesSpawned = 0;
var enemiesToSpawn = [];
var spawnTimer = 0;
var waveStartTime = 0;
// Resource management limits
var maxBullets = 50;
var maxEnemies = 30;
var audioEnabled = true;
// Game object arrays
var towers = [];
var enemies = [];
var bullets = [];
var towerZones = [];
// Safe audio playback function
function safePlayAudio(soundId, fallbackMessage) {
if (!audioEnabled) {
return;
}
try {
var sound = LK.getSound(soundId);
if (sound && typeof sound.play === 'function') {
sound.play();
}
} catch (error) {
console.warn(fallbackMessage || 'Audio playback failed:', error);
// Disable audio temporarily on multiple failures
audioEnabled = false;
LK.setTimeout(function () {
audioEnabled = true;
}, 5000);
}
}
// Enemy path - creating a simple path from left to right with some turns
var enemyPath = [{
x: 0,
y: 1366
},
// Start off screen left
{
x: 300,
y: 1366
}, {
x: 300,
y: 1000
}, {
x: 800,
y: 1000
}, {
x: 800,
y: 1600
}, {
x: 1200,
y: 1600
}, {
x: 1200,
y: 800
}, {
x: 1700,
y: 800
}, {
x: 1700,
y: 1366
}, {
x: 2100,
y: 1366
} // End off screen right (base location)
];
// Draw path
for (var i = 0; i < enemyPath.length - 1; i++) {
var pathSegment = LK.getAsset('pathTile', {
anchorX: 0.5,
anchorY: 0.5
});
pathSegment.x = enemyPath[i].x;
pathSegment.y = enemyPath[i].y;
pathSegment.alpha = 0.3;
game.addChild(pathSegment);
}
// Create base
var base = LK.getAsset('base', {
anchorX: 0.5,
anchorY: 0.5
});
base.x = enemyPath[enemyPath.length - 1].x;
base.y = enemyPath[enemyPath.length - 1].y;
game.addChild(base);
// Create tower placement zones
var towerGrid = [{
x: 500,
y: 800
}, {
x: 500,
y: 1200
}, {
x: 500,
y: 1800
}, {
x: 1000,
y: 600
}, {
x: 1000,
y: 1200
}, {
x: 1000,
y: 1800
}, {
x: 1400,
y: 600
}, {
x: 1400,
y: 1000
}, {
x: 1400,
y: 1400
}];
for (var i = 0; i < towerGrid.length; i++) {
// Validate grid coordinates before creating zone
var gridX = towerGrid[i].x;
var gridY = towerGrid[i].y;
if (!isValidNumber(gridX) || !isValidNumber(gridY)) {
console.error('Invalid tower grid coordinates at index', i, ':', gridX, gridY);
continue;
}
var zone = LK.getAsset('towerZone', {
anchorX: 0.5,
anchorY: 0.5
});
safeAssign(zone, 'x', gridX, 500);
safeAssign(zone, 'y', gridY, 800);
zone.alpha = 0.2;
zone.gridIndex = i;
zone.occupied = false;
towerZones.push(zone);
game.addChild(zone);
}
// UI Elements
var goldText = new Text2('Gold: ' + gold, {
size: 60,
fill: 0xFFD700
});
goldText.anchor.set(0, 0);
goldText.x = 120;
goldText.y = 120;
LK.gui.topLeft.addChild(goldText);
var healthText = new Text2('Base Health: ' + baseHealth, {
size: 60,
fill: 0xFF4500
});
healthText.anchor.set(0.5, 0);
healthText.x = 0;
healthText.y = 120;
LK.gui.top.addChild(healthText);
var waveText = new Text2('Wave: ' + currentWave + '/' + totalWaves, {
size: 60,
fill: 0x00BFFF
});
waveText.anchor.set(1, 0);
waveText.x = -20;
waveText.y = 120;
LK.gui.topRight.addChild(waveText);
var nextWaveButton = new Text2('Start Wave 1', {
size: 70,
fill: 0x00FF00
});
nextWaveButton.anchor.set(0.5, 1);
nextWaveButton.x = 0;
nextWaveButton.y = -100;
LK.gui.bottom.addChild(nextWaveButton);
// Function to update button text
function updateNextWaveButton() {
if (gameEnded) {
nextWaveButton.setText('Game Over');
nextWaveButton.tint = 0xFF0000;
nextWaveButton.alpha = 0.5;
} else if (currentWave > totalWaves) {
nextWaveButton.setText('Victory!');
nextWaveButton.tint = 0xFFD700;
nextWaveButton.alpha = 0.5;
} else if (waveInProgress) {
nextWaveButton.setText('Wave ' + currentWave + ' in Progress');
nextWaveButton.tint = 0x888888;
nextWaveButton.alpha = 0.5;
} else if (waveBreakTime > 0) {
var seconds = Math.ceil(waveBreakTime / 60);
nextWaveButton.setText('Next Wave in ' + seconds + 's');
nextWaveButton.tint = 0xFFFF00;
nextWaveButton.alpha = 0.7;
} else {
nextWaveButton.setText('Start Wave ' + currentWave);
nextWaveButton.tint = 0x00FF00;
nextWaveButton.alpha = 1.0;
}
}
// Tower selection UI
var towerButtons = [];
var towerTypes = ['tacticalRecruit', 'armoredElite', 'rotatingTurret', 'magneticCannon', 'CanonDoublePlasmaBase'];
var towerCosts = [50, 100, 75, 150, 200];
var selectedTowerType = null;
for (var i = 0; i < towerTypes.length; i++) {
var button = LK.getAsset(towerTypes[i], {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8
});
button.x = 200 + i * 120;
button.y = 2600;
button.towerType = towerTypes[i];
button.cost = towerCosts[i];
button.alpha = 0.7;
towerButtons.push(button);
game.addChild(button);
var costText = new Text2('$' + towerCosts[i], {
size: 40,
fill: 0xFFFFFF
});
costText.anchor.set(0.5, 0);
costText.x = button.x;
costText.y = button.y + 60;
game.addChild(costText);
}
// Wave configuration
function generateWave(waveNumber) {
var enemyList = [];
var enemyTypes = ['alienGrunt', 'shieldedWalker', 'agileCommander', 'armoredColossus', 'flyingDrone'];
// Base enemy count increases with wave number
var baseCount = Math.min(3 + Math.floor(waveNumber * 1.5), 25);
// Early waves have simpler enemies
if (waveNumber <= 3) {
for (var i = 0; i < baseCount; i++) {
enemyList.push('alienGrunt');
}
} else if (waveNumber <= 7) {
// Mix of basic enemies
for (var i = 0; i < baseCount; i++) {
var rand = Math.random();
if (rand < 0.6) {
enemyList.push('alienGrunt');
} else if (rand < 0.9) {
enemyList.push('shieldedWalker');
} else {
enemyList.push('agileCommander');
}
}
} else if (waveNumber <= 15) {
// More variety and difficulty
for (var i = 0; i < baseCount; i++) {
var rand = Math.random();
if (rand < 0.3) {
enemyList.push('alienGrunt');
} else if (rand < 0.5) {
enemyList.push('shieldedWalker');
} else if (rand < 0.7) {
enemyList.push('agileCommander');
} else if (rand < 0.9) {
enemyList.push('armoredColossus');
} else {
enemyList.push('flyingDrone');
}
}
} else {
// Final waves - all enemy types
for (var i = 0; i < baseCount; i++) {
var typeIndex = Math.floor(Math.random() * enemyTypes.length);
enemyList.push(enemyTypes[typeIndex]);
}
}
return enemyList;
}
function startNextWave() {
if (waveInProgress || gameEnded || waveBreakTime > 0) {
return;
}
console.log("Starting wave " + currentWave);
enemiesToSpawn = generateWave(currentWave);
waveInProgress = true;
waveCompleted = false;
enemiesSpawned = 0;
spawnTimer = 1;
waveStartTime = LK.ticks;
// Spawn first enemy immediately
spawnEnemy();
// Update wave text
waveText.setText('Wave: ' + currentWave + '/' + totalWaves);
// Immediate visual feedback
updateNextWaveButton();
}
function spawnEnemy() {
if (enemiesSpawned < enemiesToSpawn.length && !gameEnded) {
var enemyType = enemiesToSpawn[enemiesSpawned];
var enemy = new Enemy(enemyType);
enemy.x = enemyPath[0].x;
enemy.y = enemyPath[0].y;
enemies.push(enemy);
game.addChild(enemy);
enemiesSpawned++;
}
}
function checkWaveCompletion() {
// Wave is complete when all enemies have been spawned and destroyed
if (enemiesSpawned >= enemiesToSpawn.length && enemies.length === 0 && waveInProgress) {
waveInProgress = false;
waveCompleted = true;
waveBreakTime = 300; // 5 seconds at 60fps
// Wave completion bonus
gold += 25;
goldText.setText('Gold: ' + gold);
currentWave++;
if (currentWave > totalWaves) {
gameEnded = true;
LK.showYouWin();
return;
}
}
}
// Event handlers
game.down = function (x, y, obj) {
// Check tower button clicks
for (var i = 0; i < towerButtons.length; i++) {
var button = towerButtons[i];
var dx = x - button.x;
var dy = y - button.y;
if (Math.abs(dx) < 50 && Math.abs(dy) < 50) {
if (gold >= button.cost) {
selectedTowerType = button.towerType;
// Highlight selected tower
for (var j = 0; j < towerButtons.length; j++) {
towerButtons[j].alpha = j === i ? 1.0 : 0.7;
}
return;
}
}
}
// Check tower zone clicks
for (var i = 0; i < towerZones.length; i++) {
var zone = towerZones[i];
var dx = x - zone.x;
var dy = y - zone.y;
if (Math.abs(dx) < 50 && Math.abs(dy) < 50 && !zone.occupied && selectedTowerType) {
var towerCost = 50;
if (selectedTowerType === 'armoredElite') {
towerCost = 100;
} else if (selectedTowerType === 'rotatingTurret') {
towerCost = 75;
} else if (selectedTowerType === 'magneticCannon') {
towerCost = 150;
} else if (selectedTowerType === 'CanonDoublePlasmaBase') {
towerCost = 200;
}
if (gold >= towerCost) {
// Validate zone coordinates before tower creation
if (!isValidNumber(zone.x) || !isValidNumber(zone.y)) {
console.error('Invalid zone coordinates for tower placement:', zone.x, zone.y);
return;
}
var tower;
if (selectedTowerType === 'CanonDoublePlasmaBase') {
tower = new DoublePlasmaCannon(i % 3, Math.floor(i / 3));
} else {
tower = new Tower(selectedTowerType, i % 3, Math.floor(i / 3));
}
// Validate tower was created successfully
if (!tower) {
console.error('Failed to create tower of type:', selectedTowerType);
return;
}
safeAssign(tower, 'x', zone.x, 500);
safeAssign(tower, 'y', zone.y, 800);
// Final validation of tower position
if (!isValidNumber(tower.x) || !isValidNumber(tower.y)) {
console.error('Tower position became NaN after assignment:', tower.x, tower.y);
return;
}
towers.push(tower);
game.addChild(tower);
zone.occupied = true;
gold -= towerCost;
goldText.setText('Gold: ' + gold);
safePlayAudio('placeTower', 'Tower placement audio failed');
selectedTowerType = null;
// Reset tower button highlights
for (var j = 0; j < towerButtons.length; j++) {
towerButtons[j].alpha = 0.7;
}
}
return;
}
}
// Check next wave button click
// Button is positioned at bottom center of screen
var buttonDx = x - 1024; // Center of screen width (2048/2)
var buttonDy = y - 2500; // Near bottom of screen
if (Math.abs(buttonDx) < 200 && Math.abs(buttonDy) < 60 && !waveInProgress && waveBreakTime === 0 && currentWave <= totalWaves && !gameEnded) {
console.log("Next wave button clicked!");
startNextWave();
}
};
// Game update loop
game.update = function () {
if (gameEnded) {
return;
}
// Handle wave break timer
if (waveBreakTime > 0) {
waveBreakTime--;
if (waveBreakTime === 0) {
waveCompleted = false;
}
}
// Spawn enemies during active wave with limits
if (waveInProgress && !gameEnded && enemies.length < maxEnemies) {
spawnTimer++;
// Spawn enemies every 48 frames (0.8 seconds at 60fps) after the first one
if (spawnTimer >= 48) {
spawnEnemy();
spawnTimer = 0;
}
}
// Update towers with rate limiting
for (var i = 0; i < towers.length; i++) {
try {
towers[i].update();
} catch (error) {
console.error('Tower update failed for tower', i, ':', error);
}
}
// Update enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemy.update();
// Check if enemy reached base
if (enemy.pathIndex >= enemyPath.length - 1) {
baseHealth--;
healthText.setText('Base Health: ' + baseHealth);
enemy.destroy();
enemies.splice(i, 1);
if (baseHealth <= 0) {
gameEnded = true;
LK.showGameOver();
return;
}
}
}
// Update bullets with resource limits
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
// Validate bullet exists and has valid position
if (!bullet || !isValidNumber(bullet.x) || !isValidNumber(bullet.y)) {
if (bullet) {
bullet.destroy();
}
bullets.splice(i, 1);
continue;
}
try {
bullet.update();
} catch (error) {
console.error('Bullet update failed:', error);
bullet.destroy();
bullets.splice(i, 1);
continue;
}
// Check bullet collision with enemies
var hit = false;
for (var j = 0; j < enemies.length; j++) {
var enemy = enemies[j];
if (!enemy || !isValidNumber(enemy.x) || !isValidNumber(enemy.y)) {
continue;
}
var dx = bullet.x - enemy.x;
var dy = bullet.y - enemy.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (isValidNumber(distance) && distance < 30) {
if (enemy.takeDamage(bullet.damage)) {
// Enemy died
gold += enemy.goldValue;
goldText.setText('Gold: ' + gold);
safePlayAudio('enemyDeath', 'Enemy death audio failed');
enemy.destroy();
enemies.splice(j, 1);
}
bullet.destroy();
bullets.splice(i, 1);
hit = true;
break;
}
}
// Remove bullets that go off screen
if (!hit && (bullet.x < -50 || bullet.x > 2098 || bullet.y < -50 || bullet.y > 2782)) {
bullet.destroy();
bullets.splice(i, 1);
}
}
// Limit total bullets for performance
if (bullets.length > maxBullets) {
var excessBullets = bullets.length - maxBullets;
for (var k = 0; k < excessBullets; k++) {
if (bullets[k]) {
bullets[k].destroy();
bullets.splice(k, 1);
}
}
}
// Check wave completion
checkWaveCompletion();
// Update UI
updateNextWaveButton();
}; ===================================================================
--- original.js
+++ change.js