/****
* 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
@@ -5,2675 +5,1451 @@
/****
* Classes
****/
-var Bullet = Container.expand(function (startX, startY, targetEnemy, damage, speed) {
+var Bullet = Container.expand(function (type, startX, startY, targetX, targetY, damage) {
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', {
+ 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 () {
- if (!self.targetEnemy || !self.targetEnemy.parent) {
- self.destroy();
- return;
- }
- var dx = self.targetEnemy.x - self.x;
- var dy = self.targetEnemy.y - self.y;
- var distance = Math.sqrt(dx * dx + dy * dy);
- if (distance < self.speed) {
- // Apply damage to target enemy
- self.targetEnemy.health -= self.damage;
- if (self.targetEnemy.health <= 0) {
- self.targetEnemy.health = 0;
- } else {
- self.targetEnemy.healthBar.width = self.targetEnemy.health / self.targetEnemy.maxHealth * 70;
- }
- // Apply special effects based on bullet type
- if (self.type === 'splash') {
- // Create visual splash effect
- var splashEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'splash');
- game.addChild(splashEffect);
- // Splash damage to nearby enemies
- var splashRadius = CELL_SIZE * 1.5;
- for (var i = 0; i < enemies.length; i++) {
- var otherEnemy = enemies[i];
- if (otherEnemy !== self.targetEnemy) {
- var splashDx = otherEnemy.x - self.targetEnemy.x;
- var splashDy = otherEnemy.y - self.targetEnemy.y;
- var splashDistance = Math.sqrt(splashDx * splashDx + splashDy * splashDy);
- if (splashDistance <= splashRadius) {
- // Apply splash damage (50% of original damage)
- otherEnemy.health -= self.damage * 0.5;
- if (otherEnemy.health <= 0) {
- otherEnemy.health = 0;
- } else {
- otherEnemy.healthBar.width = otherEnemy.health / otherEnemy.maxHealth * 70;
- }
- }
- }
- }
- } else if (self.type === 'slow') {
- // Prevent slow effect on immune enemies
- if (!self.targetEnemy.isImmune) {
- // Create visual slow effect
- var slowEffect = new EffectIndicator(self.targetEnemy.x, self.targetEnemy.y, 'slow');
- game.addChild(slowEffect);
- // Apply slow effect
- // Make slow percentage scale with tower level (default 50%, up to 80% at max level)
- var slowPct = 0.5;
- if (self.sourceTowerLevel !== undefined) {
- // Scale: 50% at level 1, 60% at 2, 65% at 3, 70% at 4, 75% at 5, 80% at 6
- var slowLevels = [0.5, 0.6, 0.65, 0.7, 0.75, 0.8];
- var idx = Math.max(0, Math.min(5, self.sourceTowerLevel - 1));
- slowPct = slowLevels[idx];
- }
- if (!self.targetEnemy.slowed) {
- self.targetEnemy.originalSpeed = self.targetEnemy.speed;
- self.targetEnemy.speed *= 1 - slowPct; // Slow by X%
- self.targetEnemy.slowed = true;
- self.targetEnemy.slowDuration = 180; // 3 seconds at 60 FPS
- } else {
- self.targetEnemy.slowDuration = 180; // Reset duration
- }
- }
- } else if (self.type === '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;
- }
+ self.x += self.velocityX;
+ self.y += self.velocityY;
};
return self;
});
-var DebugCell = Container.expand(function () {
+var DoublePlasmaCannon = Container.expand(function (gridX, gridY) {
var self = Container.call(this);
- var cellGraphics = self.attachAsset('cell', {
+ // 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.5
+ anchorY: 0.3 // Adjusted to position base higher than center for better cannon alignment with 200x150 sprite
});
- cellGraphics.tint = Math.random() * 0xffffff;
- var debugArrows = [];
- var numberLabel = new Text2('0', {
- size: 30,
- fill: 0xFFFFFF,
- weight: 800
- });
- numberLabel.anchor.set(.5, .5);
- self.addChild(numberLabel);
- self.update = function () {};
- self.down = function () {
- return;
- if (self.cell.type == 0 || self.cell.type == 1) {
- self.cell.type = self.cell.type == 1 ? 0 : 1;
- if (grid.pathFind()) {
- self.cell.type = self.cell.type == 1 ? 0 : 1;
- grid.pathFind();
- var notification = game.addChild(new Notification("Path is blocked!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 50;
- }
- grid.renderDebug();
- }
- };
- self.removeArrows = function () {
- while (debugArrows.length) {
- self.removeChild(debugArrows.pop());
- }
- };
- self.render = function (data) {
- switch (data.type) {
- case 0:
- case 2:
- {
- if (data.pathId != pathId) {
- self.removeArrows();
- numberLabel.setText("-");
- cellGraphics.tint = 0x880000;
- return;
- }
- numberLabel.visible = true;
- 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;
- } else {
- cellGraphics.tint = 0x88 - tint << 8 | tint;
- }
- while (debugArrows.length > data.targets.length) {
- self.removeChild(debugArrows.pop());
- }
- for (var a = 0; a < data.targets.length; a++) {
- var destination = data.targets[a];
- var ox = destination.x - data.x;
- var oy = destination.y - data.y;
- var angle = Math.atan2(oy, ox);
- if (!debugArrows[a]) {
- debugArrows[a] = LK.getAsset('arrow', {
- anchorX: -.5,
- anchorY: 0.5
- });
- debugArrows[a].alpha = .5;
- self.addChildAt(debugArrows[a], 1);
- }
- debugArrows[a].rotation = angle;
- }
- break;
- }
- case 1:
- {
- self.removeArrows();
- cellGraphics.tint = 0xaaaaaa;
- numberLabel.visible = false;
- break;
- }
- case 3:
- {
- self.removeArrows();
- cellGraphics.tint = 0x008800;
- numberLabel.visible = false;
- break;
- }
- }
- numberLabel.setText(Math.floor(data.score / 1000) / 10);
- };
-});
-// This update method was incorrectly placed here and should be removed
-var EffectIndicator = Container.expand(function (x, y, type) {
- var self = Container.call(this);
- self.x = x;
- self.y = y;
- var effectGraphics = self.attachAsset('rangeCircle', {
+ // 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.5
+ anchorY: 0.3 // Adjusted anchor point to better center during rotation for 100x163.06 sprite
});
- 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
+ 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: 300,
- easing: tween.easeIn,
- onFinish: function onFinish() {
- self.destroy();
- }
+ duration: 100,
+ easing: tween.easeInOut
});
- }
- });
- return self;
-});
-// Base enemy class for common functionality
-var Enemy = Container.expand(function (type) {
- var self = Container.call(this);
- self.type = type || 'normal';
- self.speed = .01;
- self.cellX = 0;
- self.cellY = 0;
- self.currentCellX = 0;
- self.currentCellY = 0;
- self.currentTarget = undefined;
- self.maxHealth = 100;
- self.health = self.maxHealth;
- self.bulletsTargetingThis = [];
- self.waveNumber = currentWave;
- self.isFlying = false;
- self.isImmune = false;
- self.isBoss = false;
- // Check if this is a boss wave
- // Check if this is a boss wave
- // Apply different stats based on enemy type
- switch (self.type) {
- case 'fast':
- self.speed *= 2; // Twice as fast
- self.maxHealth = 100;
- break;
- case 'immune':
- self.isImmune = true;
- self.maxHealth = 80;
- break;
- case 'flying':
- self.isFlying = true;
- self.maxHealth = 80;
- break;
- case 'swarm':
- self.maxHealth = 50; // Weaker enemies
- break;
- case 'normal':
- default:
- // Normal enemy uses default values
- break;
- }
- if (currentWave % 10 === 0 && currentWave > 0 && type !== 'swarm') {
- self.isBoss = true;
- // Boss enemies have 20x health and are larger
- self.maxHealth *= 20;
- // Slower speed for bosses
- self.speed = self.speed * 0.7;
- }
- self.health = self.maxHealth;
- // Get appropriate asset for this enemy type
- var assetId = 'enemy';
- if (self.type !== 'normal') {
- assetId = 'enemy_' + self.type;
- }
- var enemyGraphics = self.attachAsset(assetId, {
- anchorX: 0.5,
- anchorY: 0.5
- });
- // Scale up boss enemies
- if (self.isBoss) {
- enemyGraphics.scaleX = 1.8;
- enemyGraphics.scaleY = 1.8;
- }
- // Fall back to regular enemy asset if specific type asset not found
- // Apply tint to differentiate enemy types
- /*switch (self.type) {
- case 'fast':
- enemyGraphics.tint = 0x00AAFF; // Blue for fast enemies
- break;
- case 'immune':
- enemyGraphics.tint = 0xAA0000; // Red for immune enemies
- break;
- case 'flying':
- enemyGraphics.tint = 0xFFFF00; // Yellow for flying enemies
- break;
- case 'swarm':
- enemyGraphics.tint = 0xFF00FF; // Pink for swarm enemies
- break;
- }*/
- // Create shadow for flying enemies
- if (self.isFlying) {
- // Create a shadow container that will be added to the shadow layer
- self.shadow = new Container();
- // Clone the enemy graphics for the shadow
- var shadowGraphics = self.shadow.attachAsset(assetId || 'enemy', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- // Apply shadow effect
- shadowGraphics.tint = 0x000000; // Black shadow
- shadowGraphics.alpha = 0.4; // Semi-transparent
- // If this is a boss, scale up the shadow to match
- if (self.isBoss) {
- shadowGraphics.scaleX = 1.8;
- shadowGraphics.scaleY = 1.8;
- }
- // Position shadow slightly offset
- self.shadow.x = 20; // Offset right
- self.shadow.y = 20; // Offset down
- // Ensure shadow has the same rotation as the enemy
- shadowGraphics.rotation = enemyGraphics.rotation;
- }
- var healthBarOutline = self.attachAsset('healthBarOutline', {
- anchorX: 0,
- anchorY: 0.5
- });
- var healthBarBG = self.attachAsset('healthBar', {
- anchorX: 0,
- anchorY: 0.5
- });
- var healthBar = self.attachAsset('healthBar', {
- anchorX: 0,
- anchorY: 0.5
- });
- healthBarBG.y = healthBarOutline.y = healthBar.y = -enemyGraphics.height / 2 - 10;
- healthBarOutline.x = -healthBarOutline.width / 2;
- healthBarBG.x = healthBar.x = -healthBar.width / 2 - .5;
- healthBar.tint = 0x00ff00;
- healthBarBG.tint = 0xff0000;
- self.healthBar = healthBar;
- self.update = function () {
- if (self.health <= 0) {
- self.health = 0;
- self.healthBar.width = 0;
- }
- // Handle slow effect
- if (self.isImmune) {
- // Immune enemies cannot be slowed or 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;
+ // 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;
}
- 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
- }
+ if (self.chargingLoopTimeout) {
+ LK.clearTimeout(self.chargingLoopTimeout);
+ self.chargingLoopTimeout = null;
}
+ safePlayAudio('ChargingCompletePlasmaCannon', 'DoublePlasmaCannon ready audio failed');
}
- // 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
- }
- }
- }
+ return; // Exit early during reload - no other functionality
}
- // Set tint based on effect status
- if (self.isImmune) {
- enemyGraphics.tint = 0xFFFFFF;
- } else if (self.poisoned && self.slowed) {
- // Combine poison (0x00FFAA) and slow (0x9900FF) colors
- // Simple average: R: (0+153)/2=76, G: (255+0)/2=127, B: (170+255)/2=212
- enemyGraphics.tint = 0x4C7FD4;
- } else if (self.poisoned) {
- enemyGraphics.tint = 0x00FFAA;
- } else if (self.slowed) {
- enemyGraphics.tint = 0x9900FF;
- } else {
- enemyGraphics.tint = 0xFFFFFF;
- }
- if (self.currentTarget) {
- var ox = self.currentTarget.x - self.currentCellX;
- var oy = self.currentTarget.y - self.currentCellY;
- if (ox !== 0 || oy !== 0) {
- var angle = Math.atan2(oy, ox);
- if (enemyGraphics.targetRotation === undefined) {
- enemyGraphics.targetRotation = angle;
- enemyGraphics.rotation = angle;
- } else {
- if (Math.abs(angle - enemyGraphics.targetRotation) > 0.05) {
- tween.stop(enemyGraphics, {
- rotation: true
- });
- // Calculate the shortest angle to rotate
- var currentRotation = enemyGraphics.rotation;
- var angleDiff = angle - currentRotation;
- // Normalize angle difference to -PI to PI range for shortest path
- while (angleDiff > Math.PI) {
- angleDiff -= Math.PI * 2;
- }
- while (angleDiff < -Math.PI) {
- angleDiff += Math.PI * 2;
- }
- enemyGraphics.targetRotation = angle;
- tween(enemyGraphics, {
- rotation: currentRotation + angleDiff
- }, {
- duration: 250,
- easing: tween.easeOut
- });
+ // 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;
}
- 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;
- }
+ 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;
}
- 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.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;
}
- }
- }
- 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 (angleDiff < -Math.PI) {
+ angleDiff += 2 * Math.PI;
}
- }
- 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;
+ // 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;
}
}
- } 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;
+ 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;
+ }
}
- 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
- });
+ });
+ } 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;
}
}
- // 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);
+ // 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
+ });
}
- 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;
+ // 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;
}
- // 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;
+ } 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
+ });
}
- 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;
+ // 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;
}
- while (angleDiff < -Math.PI) {
- angleDiff += Math.PI * 2;
+ 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;
+ }
+ }
+ });
+ }
}
- // 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 idle action timer
+ self.idleActionTimer++;
}
- // 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;
- var fadeOutTime = 120;
- self.update = function () {
- if (fadeOutTime > 0) {
- fadeOutTime--;
- self.alpha = Math.min(fadeOutTime / 120 * 2, 1);
- } else {
- self.destroy();
- }
- };
- return self;
-});
-var SourceTower = Container.expand(function (towerType) {
- var self = Container.call(this);
- self.towerType = towerType || 'default';
- // Increase size of base for easier touch
- var baseGraphics = self.attachAsset('tower', {
- anchorX: 0.5,
- anchorY: 0.5,
- scaleX: 1.3,
- scaleY: 1.3
- });
- switch (self.towerType) {
- case 'rapid':
- baseGraphics.tint = 0x00AAFF;
- break;
- case 'sniper':
- baseGraphics.tint = 0xFF5500;
- break;
- case 'splash':
- baseGraphics.tint = 0x33CC00;
- break;
- case 'slow':
- baseGraphics.tint = 0x9900FF;
- break;
- case 'poison':
- baseGraphics.tint = 0x00FFAA;
- break;
- default:
- baseGraphics.tint = 0xAAAAAA;
- }
- var towerCost = getTowerCost(self.towerType);
- // Add shadow for tower type label
- var typeLabelShadow = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
- 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 typeLabel = new Text2(self.towerType.charAt(0).toUpperCase() + self.towerType.slice(1), {
- 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;
- // 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
+ // 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
}
- 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 = self.attachAsset('tower', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- switch (self.id) {
- case 'rapid':
- baseGraphics.tint = 0x00AAFF;
- break;
- case 'sniper':
- baseGraphics.tint = 0xFF5500;
- break;
- case 'splash':
- baseGraphics.tint = 0x33CC00;
- break;
- case 'slow':
- baseGraphics.tint = 0x9900FF;
- break;
- case 'poison':
- baseGraphics.tint = 0x00FFAA;
- break;
- default:
- baseGraphics.tint = 0xAAAAAA;
- }
- var levelIndicators = [];
- var maxDots = self.maxLevel;
- var dotSpacing = baseGraphics.width / (maxDots + 1);
- var dotSize = CELL_SIZE / 6;
- for (var i = 0; i < maxDots; i++) {
- var dot = new Container();
- var outlineCircle = dot.attachAsset('towerLevelIndicator', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- outlineCircle.width = dotSize + 4;
- outlineCircle.height = dotSize + 4;
- outlineCircle.tint = 0x000000;
- var towerLevelIndicator = dot.attachAsset('towerLevelIndicator', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- towerLevelIndicator.width = dotSize;
- towerLevelIndicator.height = dotSize;
- towerLevelIndicator.tint = 0xCCCCCC;
- dot.x = -CELL_SIZE + dotSpacing * (i + 1);
- 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
- });
- 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();
- 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);
- }
+ // 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;
}
- }
- }
- 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;
- }
+ while (angleDiff < -Math.PI) {
+ angleDiff += 2 * Math.PI;
}
- self.refreshCellsInRange();
- self.updateLevelIndicators();
- if (self.level > 1) {
- var levelDot = levelIndicators[self.level - 1].children[1];
- tween(levelDot, {
- scaleX: 1.5,
- scaleY: 1.5
+ // 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: 300,
- easing: tween.elasticOut,
+ duration: 500,
+ easing: tween.easeInOut,
onFinish: function onFinish() {
- tween(levelDot, {
- scaleX: 1,
- scaleY: 1
- }, {
- duration: 200,
- easing: tween.easeOut
- });
+ self.isRotating = false;
+ if (self.rotatingLoopInterval) {
+ LK.clearInterval(self.rotatingLoopInterval);
+ self.rotatingLoopInterval = null;
+ }
}
});
}
- 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;
+ // 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 () {
- var closestEnemy = null;
- var closestScore = Infinity;
+ // 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);
- // 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 (distance <= self.range) {
+ return enemy;
}
}
- if (!closestEnemy) {
- self.targetEnemy = null;
- }
- return closestEnemy;
+ return null;
};
- self.update = function () {
- self.targetEnemy = self.findTarget();
- if (self.targetEnemy) {
- var dx = self.targetEnemy.x - self.x;
- var dy = self.targetEnemy.y - self.y;
- var angle = Math.atan2(dy, dx);
- gunContainer.rotation = angle;
- if (LK.ticks - self.lastFired >= self.fireRate) {
- self.fire();
- self.lastFired = LK.ticks;
+ self.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;
}
}
- };
- self.down = function (x, y, obj) {
- var existingMenus = game.children.filter(function (child) {
- return child instanceof UpgradeMenu;
+ // 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
});
- 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;
+ // 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
+ });
}
- }
- 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
+ // Add glowing effect to both plasma bullets
+ tween(bullet1.children[0], {
+ tint: 0x80ff80
}, {
- duration: 200,
- easing: tween.backOut
+ duration: 100
});
- grid.renderDebug();
+ tween(bullet2.children[0], {
+ tint: 0x80ff80
+ }, {
+ duration: 100
+ });
};
- 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 'rapid':
- bullet.children[0].tint = 0x00AAFF;
- bullet.children[0].width = 20;
- bullet.children[0].height = 20;
- break;
- case 'sniper':
- bullet.children[0].tint = 0xFF5500;
- bullet.children[0].width = 15;
- bullet.children[0].height = 15;
- break;
- case 'splash':
- bullet.children[0].tint = 0x33CC00;
- bullet.children[0].width = 40;
- bullet.children[0].height = 40;
- break;
- case 'slow':
- bullet.children[0].tint = 0x9900FF;
- bullet.children[0].width = 35;
- bullet.children[0].height = 35;
- break;
- case 'poison':
- bullet.children[0].tint = 0x00FFAA;
- bullet.children[0].width = 35;
- bullet.children[0].height = 35;
- break;
- }
- game.addChild(bullet);
- bullets.push(bullet);
- self.targetEnemy.bulletsTargetingThis.push(bullet);
- // --- Fire recoil effect for gunContainer ---
- // Stop any ongoing recoil tweens before starting a new one
- tween.stop(gunContainer, {
- x: true,
- y: true,
- scaleX: true,
- scaleY: true
- });
- // Always use the original resting position for recoil, never accumulate offset
- if (gunContainer._restX === undefined) {
- gunContainer._restX = 0;
- }
- if (gunContainer._restY === undefined) {
- gunContainer._restY = 0;
- }
- if (gunContainer._restScaleX === undefined) {
- gunContainer._restScaleX = 1;
- }
- if (gunContainer._restScaleY === undefined) {
- gunContainer._restScaleY = 1;
- }
- // Reset to resting position before animating (in case of interrupted tweens)
- gunContainer.x = gunContainer._restX;
- gunContainer.y = gunContainer._restY;
- gunContainer.scaleX = gunContainer._restScaleX;
- gunContainer.scaleY = gunContainer._restScaleY;
- // Calculate recoil offset (recoil back along the gun's rotation)
- var recoilDistance = 8;
- var recoilX = -Math.cos(gunContainer.rotation) * recoilDistance;
- var recoilY = -Math.sin(gunContainer.rotation) * recoilDistance;
- // Animate recoil back from the resting position
- tween(gunContainer, {
- x: gunContainer._restX + recoilX,
- y: gunContainer._restY + recoilY
- }, {
- duration: 60,
- easing: tween.cubicOut,
- onFinish: function onFinish() {
- // Animate return to original position/scale
- tween(gunContainer, {
- x: gunContainer._restX,
- y: gunContainer._restY
- }, {
- duration: 90,
- easing: tween.cubicIn
- });
- }
- });
- }
- }
- };
- self.placeOnGrid = function (gridX, gridY) {
- self.gridX = gridX;
- self.gridY = gridY;
- self.x = grid.x + gridX * CELL_SIZE + CELL_SIZE / 2;
- self.y = grid.y + gridY * CELL_SIZE + CELL_SIZE / 2;
- for (var i = 0; i < 2; i++) {
- for (var j = 0; j < 2; j++) {
- var cell = grid.getCell(gridX + i, gridY + j);
- if (cell) {
- cell.type = 1;
- }
- }
- }
- self.refreshCellsInRange();
- };
return self;
});
-var TowerPreview = Container.expand(function () {
+var Enemy = Container.expand(function (type) {
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', {
+ var enemyGraphic = self.attachAsset(type, {
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.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.updateAppearance = function () {
- // Use Tower class to get the source of truth for range
- var tempTower = new Tower(self.towerType);
- var previewRange = tempTower.getRange();
- // Clean up tempTower to avoid memory leaks
- if (tempTower && tempTower.destroy) {
- tempTower.destroy();
- }
- // Set range indicator using unified range logic
- rangeGraphics.width = rangeGraphics.height = previewRange * 2;
- switch (self.towerType) {
- case 'rapid':
- previewGraphics.tint = 0x00AAFF;
- break;
- case 'sniper':
- previewGraphics.tint = 0xFF5500;
- break;
- case 'splash':
- previewGraphics.tint = 0x33CC00;
- break;
- case 'slow':
- previewGraphics.tint = 0x9900FF;
- break;
- case 'poison':
- previewGraphics.tint = 0x00FFAA;
- break;
- default:
- previewGraphics.tint = 0xAAAAAA;
- }
- if (!self.canPlace || !self.hasEnoughGold) {
- previewGraphics.tint = 0xFF0000;
- }
- };
- self.updatePlacementStatus = function () {
- var validGridPlacement = true;
- if (self.gridY <= 4 || self.gridY + 1 >= grid.cells[0].length - 4) {
- validGridPlacement = false;
- } else {
- 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.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;
}
}
- 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 Tower = Container.expand(function (type, gridX, gridY) {
var self = Container.call(this);
- self.tower = tower;
- self.y = 2732 + 225;
- var menuBackground = self.attachAsset('notification', {
+ var towerGraphic = self.attachAsset(type, {
anchorX: 0.5,
anchorY: 0.5
});
- menuBackground.width = 2048;
- menuBackground.height = 500;
- menuBackground.tint = 0x444444;
- menuBackground.alpha = 0.9;
- var towerTypeText = new Text2(self.tower.id.charAt(0).toUpperCase() + self.tower.id.slice(1) + ' Tower', {
- size: 80,
- fill: 0xFFFFFF,
- weight: 800
- });
- towerTypeText.anchor.set(0, 0);
- towerTypeText.x = -840;
- towerTypeText.y = -160;
- self.addChild(towerTypeText);
- var statsText = new Text2('Level: ' + self.tower.level + '/' + self.tower.maxLevel + '\nDamage: ' + self.tower.damage + '\nFire Rate: ' + (60 / self.tower.fireRate).toFixed(1) + '/s', {
- size: 70,
- fill: 0xFFFFFF,
- weight: 400
- });
- statsText.anchor.set(0, 0.5);
- statsText.x = -840;
- statsText.y = 50;
- self.addChild(statsText);
- var buttonsContainer = new Container();
- buttonsContainer.x = 500;
- self.addChild(buttonsContainer);
- var upgradeButton = new Container();
- buttonsContainer.addChild(upgradeButton);
- var buttonBackground = upgradeButton.attachAsset('notification', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- buttonBackground.width = 500;
- buttonBackground.height = 150;
- var isMaxLevel = self.tower.level >= self.tower.maxLevel;
- // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
- var baseUpgradeCost = getTowerCost(self.tower.id);
- var upgradeCost;
- if (isMaxLevel) {
- upgradeCost = 0;
- } else if (self.tower.level === self.tower.maxLevel - 1) {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
- } else {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
+ 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;
}
- 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;
+ self.getBulletType = function () {
+ if (self.towerType === 'tacticalRecruit') {
+ return 'basicBullet';
}
- if (self.tower.upgrade()) {
- // Exponential upgrade cost: base cost * (2 ^ (level-1)), scaled by tower base cost
- var baseUpgradeCost = getTowerCost(self.tower.id);
- if (self.tower.level >= self.tower.maxLevel) {
- upgradeCost = 0;
- } else if (self.tower.level === self.tower.maxLevel - 1) {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1) * 3.5 / 2);
- } else {
- upgradeCost = Math.floor(baseUpgradeCost * Math.pow(2, self.tower.level - 1));
+ if (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;
}
- 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');
+ while (angleDiff < -Math.PI) {
+ angleDiff += 2 * Math.PI;
}
- 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;
- }
+ // 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
+ });
}
- 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
+ } 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
});
- rangeGraphics.width = rangeGraphics.height = self.tower.getRange() * 2;
- rangeGraphics.alpha = 0.3;
+ tween(towerGraphic, {
+ rotation: originalAngle
+ }, {
+ duration: 300,
+ easing: tween.easeOut
+ });
}
- 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);
- }
- }
- }
+ // Debug logging for tacticalRecruit
+ if (self.towerType === 'tacticalRecruit') {
+ console.log('tacticalRecruit update - target found:', self.currentTarget ? 'yes' : 'no', 'lastShot:', self.lastShot, 'fireRate:', self.fireRate);
}
- 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;
+ // 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;
}
};
- 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;
+ 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
}
- 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 closestTarget = null;
+ var closestDistance = Infinity;
+ if (self.towerType === 'tacticalRecruit') {
+ console.log('tacticalRecruit findTarget - checking', enemies.length, 'enemies, range:', self.range);
}
- var canAfford = gold >= currentUpgradeCost;
- buttonBackground.tint = canAfford ? 0x00AA00 : 0x888888;
- var newText = 'Upgrade: ' + currentUpgradeCost + ' gold';
- if (buttonText.text !== newText) {
- buttonText.setText(newText);
- }
- };
- return self;
-});
-var WaveIndicator = Container.expand(function () {
- var self = Container.call(this);
- self.gameStarted = false;
- self.waveMarkers = [];
- self.waveTypes = [];
- self.enemyCounts = [];
- self.indicatorWidth = 0;
- self.lastBossType = null; // Track the last boss type to avoid repeating
- var blockWidth = 400;
- var totalBlocksWidth = blockWidth * totalWaves;
- var startMarker = new Container();
- var startBlock = startMarker.attachAsset('notification', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- startBlock.width = blockWidth - 10;
- startBlock.height = 70 * 2;
- startBlock.tint = 0x00AA00;
- // Add shadow for start text
- var startTextShadow = new Text2("Start Game", {
- size: 50,
- fill: 0x000000,
- weight: 800
- });
- startTextShadow.anchor.set(0.5, 0.5);
- startTextShadow.x = 4;
- startTextShadow.y = 4;
- startMarker.addChild(startTextShadow);
- var startText = new Text2("Start Game", {
- size: 50,
- fill: 0xFFFFFF,
- weight: 800
- });
- startText.anchor.set(0.5, 0.5);
- startMarker.addChild(startText);
- startMarker.x = -self.indicatorWidth;
- self.addChild(startMarker);
- self.waveMarkers.push(startMarker);
- startMarker.down = function () {
- if (!self.gameStarted) {
- self.gameStarted = true;
- currentWave = 0;
- waveTimer = nextWaveTime;
- startBlock.tint = 0x00FF00;
- startText.setText("Started!");
- startTextShadow.setText("Started!");
- // Make sure shadow position remains correct after text change
- startTextShadow.x = 4;
- startTextShadow.y = 4;
- var notification = game.addChild(new Notification("Game started! Wave 1 incoming!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 150;
- }
- };
- for (var i = 0; i < totalWaves; i++) {
- var marker = new Container();
- var block = marker.attachAsset('notification', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- block.width = blockWidth - 10;
- block.height = 70 * 2;
- // --- Begin new unified wave logic ---
- var waveType = "normal";
- var enemyType = "normal";
- var enemyCount = 10;
- var isBossWave = (i + 1) % 10 === 0;
- // Ensure all types appear in early waves
- if (i === 0) {
- block.tint = 0xAAAAAA;
- waveType = "Normal";
- enemyType = "normal";
- enemyCount = 10;
- } else if (i === 1) {
- block.tint = 0x00AAFF;
- waveType = "Fast";
- enemyType = "fast";
- enemyCount = 10;
- } else if (i === 2) {
- block.tint = 0xAA0000;
- waveType = "Immune";
- enemyType = "immune";
- enemyCount = 10;
- } else if (i === 3) {
- block.tint = 0xFFFF00;
- waveType = "Flying";
- enemyType = "flying";
- enemyCount = 10;
- } else if (i === 4) {
- block.tint = 0xFF00FF;
- waveType = "Swarm";
- enemyType = "swarm";
- enemyCount = 30;
- } else if (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;
+ 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);
}
}
- 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
+ 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'
});
- 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";
+ return;
}
- // 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";
+ // 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;
+ }
}
- // 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;
+ // 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);
}
- 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";
+ // 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);
}
- return typeName;
+ safePlayAudio('shoot', 'Tower shoot audio failed');
};
- self.positionIndicator = new Container();
- var indicator = self.positionIndicator.attachAsset('towerLevelIndicator', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- indicator.width = blockWidth - 10;
- indicator.height = 16;
- indicator.tint = 0xffad0e;
- indicator.y = -65;
- var indicator2 = self.positionIndicator.attachAsset('towerLevelIndicator', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- indicator2.width = blockWidth - 10;
- indicator2.height = 16;
- indicator2.tint = 0xffad0e;
- indicator2.y = 65;
- var leftWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- leftWall.width = 16;
- leftWall.height = 146;
- leftWall.tint = 0xffad0e;
- leftWall.x = -(blockWidth - 16) / 2;
- var rightWall = self.positionIndicator.attachAsset('towerLevelIndicator', {
- anchorX: 0.5,
- anchorY: 0.5
- });
- rightWall.width = 16;
- rightWall.height = 146;
- rightWall.tint = 0xffad0e;
- rightWall.x = (blockWidth - 16) / 2;
- self.addChild(self.positionIndicator);
- self.update = function () {
- var progress = waveTimer / nextWaveTime;
- var moveAmount = (progress + currentWave) * blockWidth;
- for (var i = 0; i < self.waveMarkers.length; i++) {
- var marker = self.waveMarkers[i];
- marker.x = -moveAmount + i * blockWidth;
- }
- self.positionIndicator.x = 0;
- for (var i = 0; i < totalWaves + 1; i++) {
- var marker = self.waveMarkers[i];
- if (i === 0) {
- continue;
- }
- var block = marker.children[0];
- if (i - 1 < currentWave) {
- block.alpha = .5;
- }
- }
- self.handleWaveProgression = function () {
- if (!self.gameStarted) {
- return;
- }
- if (currentWave < totalWaves) {
- waveTimer++;
- if (waveTimer >= nextWaveTime) {
- waveTimer = 0;
- currentWave++;
- waveInProgress = true;
- waveSpawned = false;
- if (currentWave != 1) {
- var waveType = self.getWaveTypeName(currentWave);
- var enemyCount = self.getEnemyCount(currentWave);
- var notification = game.addChild(new Notification("Wave " + currentWave + " (" + waveType + " - " + enemyCount + " enemies) incoming!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 150;
- }
- }
- }
- };
- self.handleWaveProgression();
- };
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
- backgroundColor: 0x333333
+ backgroundColor: 0x1a1a2e
});
/****
* Game Code
****/
-var isHidingUpgradeMenu = false;
-function hideUpgradeMenu(menu) {
- if (isHidingUpgradeMenu) {
+// 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;
}
- isHidingUpgradeMenu = true;
- tween(menu, {
- y: 2732 + 225
- }, {
- duration: 150,
- easing: tween.easeIn,
- onFinish: function onFinish() {
- menu.destroy();
- isHidingUpgradeMenu = false;
+ 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);
}
-var CELL_SIZE = 76;
-var pathId = 1;
-var maxScore = 0;
-var enemies = [];
-var towers = [];
-var bullets = [];
-var defenses = [];
-var selectedTower = null;
-var gold = 80;
-var lives = 20;
-var score = 0;
-var currentWave = 0;
-var totalWaves = 50;
-var waveTimer = 0;
-var waveInProgress = false;
-var waveSpawned = false;
-var nextWaveTime = 12000 / 2;
-var sourceTower = null;
-var enemiesToSpawn = 10; // Default number of enemies per wave
+// 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,
- weight: 800
+ fill: 0xFFD700
});
-goldText.anchor.set(0.5, 0.5);
-var livesText = new Text2('Lives: ' + lives, {
+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: 0x00FF00,
- weight: 800
+ fill: 0xFF4500
});
-livesText.anchor.set(0.5, 0.5);
-var scoreText = new Text2('Score: ' + score, {
+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: 0xFF0000,
- weight: 800
+ fill: 0x00BFFF
});
-scoreText.anchor.set(0.5, 0.5);
-var topMargin = 50;
-var centerX = 2048 / 2;
-var spacing = 400;
-LK.gui.top.addChild(goldText);
-LK.gui.top.addChild(livesText);
-LK.gui.top.addChild(scoreText);
-livesText.x = 0;
-livesText.y = topMargin;
-goldText.x = -spacing;
-goldText.y = topMargin;
-scoreText.x = spacing;
-scoreText.y = topMargin;
-function updateUI() {
- goldText.setText('Gold: ' + gold);
- livesText.setText('Lives: ' + lives);
- scoreText.setText('Score: ' + score);
+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;
+ }
}
-function setGold(value) {
- gold = value;
- updateUI();
+// 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);
}
-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();
-debugLayer.addChild(grid);
-game.addChild(debugLayer);
-game.addChild(towerLayer);
-game.addChild(enemyLayer);
-var offset = 0;
-var towerPreview = new TowerPreview();
-game.addChild(towerPreview);
-towerPreview.visible = false;
-var isDragging = false;
-function wouldBlockPath(gridX, gridY) {
- 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;
+// 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]);
+ }
}
- 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;
+ return enemyList;
}
-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;
+function startNextWave() {
+ if (waveInProgress || gameEnded || waveBreakTime > 0) {
+ return;
}
- return cost;
+ 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 getTowerSellValue(totalValue) {
- return waveIndicator && waveIndicator.gameStarted ? Math.floor(totalValue * 0.6) : totalValue;
-}
-function placeTower(gridX, gridY, towerType) {
- var towerCost = getTowerCost(towerType);
- if (gold >= towerCost) {
- var tower = new Tower(towerType || 'default');
- tower.placeOnGrid(gridX, gridY);
- towerLayer.addChild(tower);
- towers.push(tower);
- setGold(gold - towerCost);
- grid.pathFind();
- grid.renderDebug();
- return true;
- } else {
- var notification = game.addChild(new Notification("Not enough gold!"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 50;
- return false;
+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++;
}
}
-game.down = function (x, y, obj) {
- var upgradeMenuVisible = game.children.some(function (child) {
- return child instanceof UpgradeMenu;
- });
- if (upgradeMenuVisible) {
- return;
- }
- for (var i = 0; i < sourceTowers.length; i++) {
- var tower = sourceTowers[i];
- if (x >= tower.x - tower.width / 2 && x <= tower.x + tower.width / 2 && y >= tower.y - tower.height / 2 && y <= tower.y + tower.height / 2) {
- 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;
+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;
}
}
-};
-game.move = function (x, y, obj) {
- if (isDragging) {
- // Shift the y position upward by 1.5 tiles to show preview above finger
- towerPreview.snapToGrid(x, y - CELL_SIZE * 1.5);
- }
-};
-game.up = function (x, y, obj) {
- var clickedOnTower = false;
- for (var i = 0; i < towers.length; i++) {
- var tower = towers[i];
- var towerLeft = tower.x - tower.width / 2;
- var towerRight = tower.x + tower.width / 2;
- var towerTop = tower.y - tower.height / 2;
- var towerBottom = tower.y + tower.height / 2;
- if (x >= towerLeft && x <= towerRight && y >= towerTop && y <= towerBottom) {
- clickedOnTower = true;
- break;
- }
- }
- 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]);
+}
+// 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;
}
- selectedTower = null;
- grid.renderDebug();
}
}
- if (isDragging) {
- isDragging = false;
- 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;
+ // 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;
}
- } 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);
-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 (waveInProgress) {
- if (!waveSpawned) {
- waveSpawned = true;
- // Get wave type and enemy count from the wave indicator
- var waveType = waveIndicator.getWaveType(currentWave);
- var enemyCount = waveIndicator.getEnemyCount(currentWave);
- // Check if this is a boss wave
- var isBossWave = currentWave % 10 === 0 && currentWave > 0;
- if (isBossWave && waveType !== 'swarm') {
- // Boss waves have just 1 enemy regardless of what the wave indicator says
- enemyCount = 1;
- // Show boss announcement
- var notification = game.addChild(new Notification("⚠️ BOSS WAVE! ⚠️"));
- notification.x = 2048 / 2;
- notification.y = grid.height - 200;
- }
- // Spawn the appropriate number of enemies
- for (var i = 0; i < enemyCount; i++) {
- var enemy = new Enemy(waveType);
- // Add enemy to the appropriate layer based on type
- if (enemy.isFlying) {
- // Add flying enemy to the top layer
- enemyLayerTop.addChild(enemy);
- // If it's a flying enemy, add its shadow to the middle layer
- if (enemy.shadow) {
- enemyLayerMiddle.addChild(enemy.shadow);
- }
+ 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 {
- // Add normal/ground enemies to the bottom layer
- enemyLayerBottom.addChild(enemy);
+ tower = new Tower(selectedTowerType, i % 3, Math.floor(i / 3));
}
- // 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);
- }
+ // Validate tower was created successfully
+ if (!tower) {
+ console.error('Failed to create tower of type:', selectedTowerType);
+ return;
}
- // 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
+ 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;
}
- 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);
+ 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;
}
- var currentWaveEnemiesRemaining = false;
- for (var i = 0; i < enemies.length; i++) {
- if (enemies[i].waveNumber === currentWave) {
- currentWaveEnemiesRemaining = true;
- break;
- }
+ }
+ // 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;
}
- if (waveSpawned && !currentWaveEnemiesRemaining) {
- waveInProgress = false;
- waveSpawned = 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;
}
}
- 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) : Math.floor(1 + (enemy.waveNumber - 1) * 0.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;
- // 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;
+ // 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);
}
- 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) {
+ }
+ // 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--) {
- 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);
+ 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);
}
}
- if (towerPreview.visible) {
- towerPreview.checkPlacement();
+ // 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);
+ }
+ }
}
- if (currentWave >= totalWaves && enemies.length === 0 && !waveInProgress) {
- LK.showYouWin();
- }
+ // Check wave completion
+ checkWaveCompletion();
+ // Update UI
+ updateNextWaveButton();
};
\ No newline at end of file