/**** * 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