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