/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Asteroid = Container.expand(function () { var self = Container.call(this); var asteroidGraphics = self.attachAsset('asteroid', { anchorX: 0.5, anchorY: 0.5 }); // Randomize asteroid size between 45x45 and 70x70 // Base asset is 60x60, so scale range is 0.75 to 1.17 var randomScale = 0.75 + Math.random() * 0.42; // Random value between 0.75 and 1.17 asteroidGraphics.scaleX = randomScale; asteroidGraphics.scaleY = randomScale; self.velocityX = 0; self.velocityY = 0; self.targetPlanet = null; self.update = function () { self.x += self.velocityX; self.y += self.velocityY; asteroidGraphics.rotation += 0.05; }; return self; }); var Bullet = Container.expand(function () { var self = Container.call(this); var bulletGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.velocityX = 0; self.velocityY = 0; self.update = function () { self.x += self.velocityX; self.y += self.velocityY; }; return self; }); var Laser = Container.expand(function () { var self = Container.call(this); var laserGraphics = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); laserGraphics.tint = 0x00ff00; // Green laser color laserGraphics.scaleX = 0.5; // Make laser thinner laserGraphics.scaleY = 2; // Make laser longer self.velocityX = 0; self.velocityY = 0; self.update = function () { self.x += self.velocityX; self.y += self.velocityY; }; return self; }); var OrbitLine = Container.expand(function (radius) { var self = Container.call(this); self.orbitRadius = radius; // Create a single connected circular line using small line segments var numSegments = Math.max(16, Math.floor(radius * 0.1)); // Fewer segments for better performance var twoPi = Math.PI * 2; // Cache the calculation for (var i = 0; i < numSegments; i++) { var angle1 = i / numSegments * twoPi; var angle2 = (i + 1) / numSegments * twoPi; var x1 = Math.cos(angle1) * radius; var y1 = Math.sin(angle1) * radius; var x2 = Math.cos(angle2) * radius; var y2 = Math.sin(angle2) * radius; // Calculate segment length and angle var segmentLength = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); var segmentAngle = Math.atan2(y2 - y1, x2 - x1); // Create line segment var lineSegment = self.attachAsset('orbitLine', { anchorX: 0, anchorY: 0.5, alpha: 0.4, scaleX: segmentLength / 2, // Scale to match segment length scaleY: 1 }); // Position and rotate the segment lineSegment.x = x1; lineSegment.y = y1; lineSegment.rotation = segmentAngle; } return self; }); var Planet = Container.expand(function (size, color, orbitRadius, orbitSpeed) { var self = Container.call(this); self.orbitRadius = orbitRadius; self.orbitSpeed = orbitSpeed; self.angle = Math.random() * Math.PI * 2; var assetName = 'mercury'; if (color === 0x4169e1) { assetName = 'earth'; } else if (color === 0xff4500) { assetName = 'mars'; } else if (color === 0xffc649) { assetName = 'venus'; } else if (color === 0xd2691e) { assetName = 'jupiter'; } else if (color === 0xffd700) { assetName = 'saturn'; } else if (color === 0x40e0d0) { assetName = 'uranus'; } else if (color === 0x0080ff) { assetName = 'neptune'; } var planetGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); self.update = function () { self.angle += self.orbitSpeed; var cosAngle = Math.cos(self.angle); var sinAngle = Math.sin(self.angle); self.x = sunX + cosAngle * self.orbitRadius; self.y = sunY + sinAngle * self.orbitRadius; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000011 }); /**** * Game Code ****/ // Show instructions before game starts var instructionText = new Text2(" How to play:\n\nGoal:\n- protect the Earth from asteroid collisions\n- you will be given a spaceship that shoots lasers\n- you will use these lasers to destroy asteroids\n- if the Earth gets hit with 4 asteroids, you lose\n- if you have multiple spaceships, they will all fire where you choose\n\nPoints:\n- every asteroid you destroy will earn you five points\n- every 50 points, you will recieve a new spaceship you can put in the solar system\n- if you reach 200 points, that interval will increase to every 100 points\n- with a price of 75 points, you can buy a laser upgrade that randomly applies to one ship in play, giving it the ability to shoot two lasers instead of one\n\nAsteroids:\n- asteroids are your enimies\n- they can be destroyed with one laser hit\n- they will come from random directions, so be aware of all sides of the screen\n- planets are you friends, all planets other than Earth deflect asteroids\n- if a ship gets hit with an asteroid, it will be removed, so keep a lookout for your ships\n\nControls:\n- select a spaceship until it shrinks, then click and drag it to move it, release to place it\n- click any point on the sreen to fire lasers in that direction\n- if you have multiple spaceships, they will all fire where you choose", { size: 60, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 1600 }); instructionText.anchor.set(0.5, 0.5); instructionText.x = 2048 / 2; instructionText.y = 2732 / 2 - 100; game.addChild(instructionText); // Add separate "Got it!" button text var gotItText = new Text2("Got it!", { size: 80, fill: 0xFFFFFF }); gotItText.anchor.set(0.5, 0.5); gotItText.x = 2048 / 2; gotItText.y = 2732 - 200; // Position below instructions, above bottom of screen game.addChild(gotItText); // Add spinning earth to top right corner of instructions var instructionEarth = LK.getAsset('earth', { anchorX: 0.5, anchorY: 0.5, scaleX: 3, scaleY: 3, x: 2048 - 200, y: 200 }); game.addChild(instructionEarth); // Add defensive ship next to spinning earth var instructionShip = LK.getAsset('defensiveShip', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5, x: 2048 - 450, y: 200 }); game.addChild(instructionShip); // Start spinning animation function spinEarth() { tween(instructionEarth, { rotation: instructionEarth.rotation + Math.PI * 2 }, { duration: 3000, easing: tween.linear, onFinish: function onFinish() { if (instructionsVisible) { spinEarth(); } } }); } spinEarth(); // Add click handler to dismiss instructions var instructionsVisible = true; var instructionAsteroids = []; var instructionAsteroidSpawnTimer = 0; game.down = function (x, y, obj) { if (instructionsVisible) { // Check if "Got it!" button was clicked var gotItClickArea = { x: gotItText.x - gotItText.width / 2 - 50, y: gotItText.y - gotItText.height / 2 - 25, width: gotItText.width + 100, height: gotItText.height + 50 }; if (x >= gotItClickArea.x && x <= gotItClickArea.x + gotItClickArea.width && y >= gotItClickArea.y && y <= gotItClickArea.y + gotItClickArea.height) { // Hide instructions and start the game instructionText.destroy(); gotItText.destroy(); instructionEarth.destroy(); instructionShip.destroy(); // Clean up instruction asteroids for (var i = 0; i < instructionAsteroids.length; i++) { instructionAsteroids[i].destroy(); } instructionAsteroids = []; instructionsVisible = false; // Initialize all game assets now initializeGameAssets(); // Re-define the game.down handler for normal gameplay } return; } // Check if any improve lasers text was clicked for (var k = 0; k < defensiveShips.length; k++) { var ship = defensiveShips[k]; if (ship.visible && ship.improveLasersText.visible) { // Convert coordinates to check if improve lasers text was clicked var improveLasersClickArea = { x: ship.improveLasersText.x - ship.improveLasersText.width, y: ship.improveLasersText.y - ship.improveLasersText.height / 2, width: ship.improveLasersText.width, height: ship.improveLasersText.height }; // Convert click coordinates to GUI space var guiPos = LK.gui.topRight.toLocal(game.toGlobal({ x: x, y: y })); if (guiPos.x >= improveLasersClickArea.x && guiPos.x <= improveLasersClickArea.x + improveLasersClickArea.width && guiPos.y >= improveLasersClickArea.y && guiPos.y <= improveLasersClickArea.y + improveLasersClickArea.height) { // Check if player has enough points to use improve lasers if (LK.getScore() < 75) { return; // Not enough points, don't process the upgrade } // Find all placed ships that don't already have improved lasers var upgradeableShips = []; for (var j = 0; j < defensiveShips.length; j++) { if (defensiveShips[j].placed && !defensiveShips[j].hasImprovedLasers) { upgradeableShips.push(defensiveShips[j]); } } // Randomly select one ship to upgrade if (upgradeableShips.length > 0) { // Deduct 75 points from score LK.setScore(LK.getScore() - 75); scoreTxt.setText('Score: ' + LK.getScore()); var randomIndex = Math.floor(Math.random() * upgradeableShips.length); var selectedShip = upgradeableShips[randomIndex]; selectedShip.hasImprovedLasers = true; // Visual feedback - flash the upgraded ship tween(selectedShip, { tint: 0x00ff00 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { tween(selectedShip, { tint: 0xffffff }, { duration: 500, easing: tween.easeOut }); } }); } return; // Don't fire lasers when clicking improve lasers } } } // Check if laser firing is on cooldown if (laserCooldownActive) { return; } // Make all placed ships fire lasers at the clicked point for (var k = 0; k < defensiveShips.length; k++) { var ship = defensiveShips[k]; // Only fire from ships that are placed if (!ship.placed) { continue; } // Calculate direction from this ship to clicked position var dx = x - ship.x; var dy = y - ship.y; var distance = Math.sqrt(dx * dx + dy * dy); // Calculate rotation angle to face the target point var targetRotation = Math.atan2(dy, dx) + Math.PI / 2; // Add PI/2 since ship top should face target // Rotate the ship to face the target tween(ship, { rotation: targetRotation }, { duration: 200, easing: tween.easeOut }); var speed = 8; var normalizedDx = 0; var normalizedDy = 0; if (distance > 0) { normalizedDx = dx / distance; normalizedDy = dy / distance; } // Fire one or two lasers depending on ship upgrade if (ship.hasImprovedLasers) { // Fire two lasers with slight spread var spreadAngle = 0.1; // Radians for spread // First laser (slightly left) var laser1 = new Laser(); laser1.x = ship.x; laser1.y = ship.y; var angle1 = Math.atan2(normalizedDy, normalizedDx) - spreadAngle; laser1.velocityX = Math.cos(angle1) * speed; laser1.velocityY = Math.sin(angle1) * speed; lasers.push(laser1); game.addChild(laser1); // Second laser (slightly right) var laser2 = new Laser(); laser2.x = ship.x; laser2.y = ship.y; var angle2 = Math.atan2(normalizedDy, normalizedDx) + spreadAngle; laser2.velocityX = Math.cos(angle2) * speed; laser2.velocityY = Math.sin(angle2) * speed; lasers.push(laser2); game.addChild(laser2); } else { // Fire single laser var laser = new Laser(); laser.x = ship.x; laser.y = ship.y; laser.velocityX = normalizedDx * speed; laser.velocityY = normalizedDy * speed; lasers.push(laser); game.addChild(laser); } } // Start laser cooldown for 0.2 seconds laserCooldownActive = true; var cooldownTarget = {}; // Dummy object for tween tween(cooldownTarget, { dummy: 1 }, { duration: 200, onFinish: function onFinish() { laserCooldownActive = false; } }); }; // Define variables that will be initialized after instructions var sunX, sunY, sun, planets, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune; var orbitLines, bullets, lasers, asteroids, asteroidSpawnTimer, lives; var scoreTxt, livesTxt, defensiveShips, selectedShipIndex, isDragging; var totalShips, availableShips, lastScoreCheck; var laserCooldownActive = false; // Function to initialize game assets function initializeGameAssets() { sunX = 2048 / 2; sunY = 2732 / 2; sun = game.addChild(LK.getAsset('sun', { anchorX: 0.5, anchorY: 0.5, x: sunX, y: sunY })); planets = []; mercury = new Planet(50, 0x8c7853, 150, 0.015); venus = new Planet(70, 0xffc649, 200, 0.012); earth = new Planet(80, 0x4169e1, 300, 0.008); mars = new Planet(60, 0xff4500, 450, 0.005); jupiter = new Planet(120, 0xd2691e, 600, 0.003); saturn = new Planet(100, 0xffd700, 750, 0.002); uranus = new Planet(90, 0x40e0d0, 900, 0.0015); neptune = new Planet(85, 0x0080ff, 1050, 0.001); planets.push(mercury); planets.push(venus); planets.push(earth); planets.push(mars); planets.push(jupiter); planets.push(saturn); planets.push(uranus); planets.push(neptune); // Create orbit lines for each planet orbitLines = []; var planetOrbits = [150, 200, 300, 450, 600, 750, 900, 1050]; // Mercury to Neptune orbit radii for (var i = 0; i < planetOrbits.length; i++) { var orbitLine = new OrbitLine(planetOrbits[i]); orbitLine.x = sunX; orbitLine.y = sunY; orbitLines.push(orbitLine); game.addChild(orbitLine); } for (var i = 0; i < planets.length; i++) { game.addChild(planets[i]); } bullets = []; lasers = []; asteroids = []; asteroidSpawnTimer = 0; lives = 4; scoreTxt = new Text2('Score: 0', { size: 60, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); scoreTxt.x = 0; LK.gui.top.addChild(scoreTxt); livesTxt = new Text2('Lives: 4', { size: 60, fill: 0xFFFFFF }); livesTxt.anchor.set(0.5, 0); livesTxt.x = 0; livesTxt.y = 70; LK.gui.top.addChild(livesTxt); defensiveShips = []; selectedShipIndex = -1; isDragging = false; totalShips = 4; // Maximum number of ships availableShips = 1; // Start with 1 ship available lastScoreCheck = 0; // Track score for adding new ships // Create 4 defensive ships but only show the available ones for (var i = 0; i < totalShips; i++) { var defensiveShip = LK.getAsset('defensiveShip', { anchorX: 0.5, anchorY: 0.5, x: -80, // Position all ships in the same spot in top right corner y: 80 }); defensiveShip.shipIndex = i; defensiveShip.placed = false; defensiveShip.selected = false; defensiveShip.requiresConfirmation = false; defensiveShip.hasImprovedLasers = false; // Only show ships that are available defensiveShip.visible = i < availableShips; defensiveShips.push(defensiveShip); LK.gui.topRight.addChild(defensiveShip); // Create "Improve Lasers" text for this ship var improveLasersText = new Text2('Improve Lasers', { size: 44, fill: 0xFFFFFF }); improveLasersText.anchor.set(1, 0.5); // Right anchor to position to left of ship improveLasersText.x = -140; // Position further to the left of the ship improveLasersText.y = 80; improveLasersText.visible = i < availableShips && LK.getScore() >= 75; defensiveShip.improveLasersText = improveLasersText; // Store reference on ship LK.gui.topRight.addChild(improveLasersText); // Create cost text below improve lasers var costText = new Text2('Cost: 75 Points', { size: 32, fill: 0xcccccc }); costText.anchor.set(1, 0.5); // Right anchor to position to left of ship costText.x = -140; // Position further to the left of the ship costText.y = 110; // Position below improve lasers text costText.visible = i < availableShips && LK.getScore() >= 75; defensiveShip.costText = costText; // Store reference on ship LK.gui.topRight.addChild(costText); } // Add down handler to each defensive ship for (var i = 0; i < defensiveShips.length; i++) { defensiveShips[i].down = function (x, y, obj) { var ship = this; var shipIndex = ship.shipIndex; // Only allow interaction with visible/available ships if (!ship.visible) { return; } if (ship.placed) { // Ship has been placed, allow selection for shooting // Deselect all other ships for (var j = 0; j < defensiveShips.length; j++) { if (j !== shipIndex && defensiveShips[j].selected) { defensiveShips[j].selected = false; tween(defensiveShips[j], { alpha: 1.0 }, { duration: 200 }); } } if (!ship.selected) { ship.selected = true; selectedShipIndex = shipIndex; tween(ship, { alpha: 0.7 }, { duration: 200 }); } // Removed else block to prevent manual dis-selection of placed ships return; } // Ship not placed yet - check if it needs confirmation or can start dragging if (!ship.requiresConfirmation) { // First click - select ship and require confirmation // Deselect all other ships for (var j = 0; j < defensiveShips.length; j++) { if (j !== shipIndex) { defensiveShips[j].selected = false; defensiveShips[j].requiresConfirmation = false; tween(defensiveShips[j], { alpha: 1.0 }, { duration: 200 }); } } // Select this ship and mark it as requiring confirmation ship.selected = true; ship.requiresConfirmation = true; selectedShipIndex = shipIndex; tween(ship, { alpha: 0.7 }, { duration: 200 }); } else { // Second click - start dragging isDragging = true; // Store original scale before moving var originalScaleX = ship.scaleX; var originalScaleY = ship.scaleY; // No need to shift ships since they're all in the same position // Move defensive ship to game area for dragging var gamePos = game.toLocal(LK.gui.topRight.toGlobal({ x: ship.x, y: ship.y })); LK.gui.topRight.removeChild(ship); game.addChild(ship); ship.x = gamePos.x; ship.y = gamePos.y; // Restore original scale after moving to game area ship.scaleX = originalScaleX; ship.scaleY = originalScaleY; } }; } } game.move = function (x, y, obj) { // Move ship to cursor position during dragging if (isDragging && selectedShipIndex >= 0) { var ship = defensiveShips[selectedShipIndex]; ship.x = x; ship.y = y; } }; game.up = function (x, y, obj) { if (isDragging && selectedShipIndex >= 0) { isDragging = false; // Defensive ship is now placed in the solar system var placedShip = defensiveShips[selectedShipIndex]; // Check if ship is off-screen and needs to be moved back to available ships area var shipHalfWidth = 40; // Half width of defensive ship (80/2) var shipHalfHeight = 55; // Half height of defensive ship (110/2) var isOffScreen = placedShip.x - shipHalfWidth < 0 || placedShip.x + shipHalfWidth > 2048 || placedShip.y - shipHalfHeight < 0 || placedShip.y + shipHalfHeight > 2732; if (isOffScreen) { // Move ship back to available ships area game.removeChild(placedShip); LK.gui.topRight.addChild(placedShip); // Place ship back at the single spawn position in top right corner placedShip.x = -80; placedShip.y = 80; placedShip.placed = false; } else { placedShip.placed = true; } placedShip.selected = false; selectedShipIndex = -1; tween(placedShip, { alpha: 1.0 }, { duration: 200 }); } }; game.update = function () { // Don't update game while instructions are visible if (instructionsVisible) { // Spawn asteroids for instruction demonstration instructionAsteroidSpawnTimer++; if (instructionAsteroidSpawnTimer > 90) { // Spawn every 1.5 seconds var asteroid = new Asteroid(); // Randomly choose left or bottom side to spawn from var spawnSide = Math.random() < 0.5 ? 'left' : 'bottom'; if (spawnSide === 'left') { // Spawn from left side at random Y position asteroid.x = -50; asteroid.y = Math.random() * 2732; } else { // Spawn from bottom side at random X position asteroid.x = Math.random() * 2048; asteroid.y = 2732 + 50; } // Calculate direction toward instruction earth var dx = instructionEarth.x - asteroid.x; var dy = instructionEarth.y - asteroid.y; var distance = Math.sqrt(dx * dx + dy * dy); var speed = 2; if (distance > 0) { asteroid.velocityX = dx / distance * speed; asteroid.velocityY = dy / distance * speed; } instructionAsteroids.push(asteroid); game.addChild(asteroid); instructionAsteroidSpawnTimer = 0; } // Count active instruction lasers var activeLasers = 0; for (var m = 0; m < instructionAsteroids.length; m++) { if (instructionAsteroids[m] instanceof Laser) { activeLasers++; } } // Auto-fire from instruction ship every 45 frames (0.75 seconds) - only if no active laser if (instructionAsteroidSpawnTimer % 45 === 0 && instructionAsteroids.length > 0 && activeLasers === 0) { // Find closest asteroid to instruction ship var closestAsteroid = null; var closestDistance = Infinity; for (var k = 0; k < instructionAsteroids.length; k++) { var asteroid = instructionAsteroids[k]; if (asteroid instanceof Asteroid) { var dx = asteroid.x - instructionShip.x; var dy = asteroid.y - instructionShip.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < closestDistance) { closestDistance = distance; closestAsteroid = asteroid; } } } // Fire laser at closest asteroid only if it's visible on screen and not targeting earth if (closestAsteroid && closestAsteroid.x >= -50 && closestAsteroid.x <= 2098 && closestAsteroid.y >= -50 && closestAsteroid.y <= 2782) { // Check if firing would hit the earth - don't fire if trajectory passes too close to earth var dx = closestAsteroid.x - instructionShip.x; var dy = closestAsteroid.y - instructionShip.y; var distance = Math.sqrt(dx * dx + dy * dy); // Calculate predicted asteroid position var laserSpeed = 8; var timeToTarget = distance / laserSpeed; var predictedX = closestAsteroid.x + closestAsteroid.velocityX * timeToTarget; var predictedY = closestAsteroid.y + closestAsteroid.velocityY * timeToTarget; // Check if laser trajectory would pass too close to earth var earthDx = instructionEarth.x - instructionShip.x; var earthDy = instructionEarth.y - instructionShip.y; var targetDx = predictedX - instructionShip.x; var targetDy = predictedY - instructionShip.y; // Calculate angle between earth direction and target direction var earthAngle = Math.atan2(earthDy, earthDx); var targetAngle = Math.atan2(targetDy, targetDx); var angleDiff = Math.abs(earthAngle - targetAngle); if (angleDiff > Math.PI) { angleDiff = 2 * Math.PI - angleDiff; } // Normalize to shortest angle // Only fire if angle difference is greater than 0.3 radians (~17 degrees) to avoid hitting earth if (angleDiff > 0.3) { var instructionLaser = new Laser(); instructionLaser.x = instructionShip.x; instructionLaser.y = instructionShip.y; // Calculate direction to predicted position var laserDx = predictedX - instructionLaser.x; var laserDy = predictedY - instructionLaser.y; var laserDistance = Math.sqrt(laserDx * laserDx + laserDy * laserDy); // Add aiming inaccuracy - larger error at longer distances var aimError = laserDistance / 300 * 0.15; // Reduced error for better accuracy var randomAngleOffset = (Math.random() - 0.5) * aimError; // Random offset between -aimError/2 and +aimError/2 var baseAngle = Math.atan2(laserDy, laserDx); var aimAngle = baseAngle + randomAngleOffset; var speed = 8; if (laserDistance > 0) { instructionLaser.velocityX = Math.cos(aimAngle) * speed; instructionLaser.velocityY = Math.sin(aimAngle) * speed; } // Rotate ship to face target (still accurate rotation for visual effect) var targetRotation = Math.atan2(laserDy, laserDx) + Math.PI / 2; tween(instructionShip, { rotation: targetRotation }, { duration: 200, easing: tween.easeOut }); instructionAsteroids.push(instructionLaser); // Reuse instructionAsteroids array for cleanup game.addChild(instructionLaser); } } } // Update instruction asteroids and lasers for (var i = instructionAsteroids.length - 1; i >= 0; i--) { var object = instructionAsteroids[i]; object.update(); // Check if it's a laser hitting an asteroid if (object instanceof Laser) { var laserHit = false; for (var j = instructionAsteroids.length - 1; j >= 0 && !laserHit; j--) { var target = instructionAsteroids[j]; if (target instanceof Asteroid && object.intersects(target)) { object.destroy(); instructionAsteroids.splice(i, 1); target.destroy(); instructionAsteroids.splice(j > i ? j - 1 : j, 1); laserHit = true; if (j < i) { i--; } // Adjust index since we removed an element before current break; } } // Remove laser if off-screen and not hit if (!laserHit && (object.x < -100 || object.x > 2148 || object.y < -100 || object.y > 2832)) { object.destroy(); instructionAsteroids.splice(i, 1); } } else if (object instanceof Asteroid) { // Remove asteroids that are off-screen or hit the earth if (object.x < -100 || object.x > 2148 || object.y < -100 || object.y > 2832) { object.destroy(); instructionAsteroids.splice(i, 1); } else { // Check if asteroid hits instruction earth (simple distance check) var dx = object.x - instructionEarth.x; var dy = object.y - instructionEarth.y; var distanceSquared = dx * dx + dy * dy; if (distanceSquared < 10000) { // Collision distance - flash earth dim red tween(instructionEarth, { tint: 0x664444 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(instructionEarth, { tint: 0xFFFFFF }, { duration: 300, easing: tween.easeOut }); } }); object.destroy(); instructionAsteroids.splice(i, 1); } } } } return; } // Check if we should add a new ship (every 50 points up to 200, then every 100 points) var currentScore = LK.getScore(); var newAvailableShips; if (currentScore < 200) { // Every 50 points up to 200 newAvailableShips = Math.min(totalShips, 1 + Math.floor(currentScore / 50)); } else { // After 200: base ships from first 200 points plus additional ships every 100 points starting from 200 var baseShips = 1 + Math.floor(200 / 50); // Ships earned up to 200 points (5 total: 1 + 4) var additionalShips = Math.floor((currentScore - 200) / 100); // Additional ships every 100 points after 200 newAvailableShips = Math.min(totalShips, baseShips + additionalShips); } if (newAvailableShips > availableShips) { availableShips = newAvailableShips; // Make the newly available ship visible for (var k = 0; k < availableShips; k++) { if (!defensiveShips[k].visible) { defensiveShips[k].visible = true; defensiveShips[k].improveLasersText.visible = currentScore >= 75; defensiveShips[k].costText.visible = currentScore >= 75; } } } // Update improve lasers text visibility based on score (already using cached currentScore) for (var k = 0; k < availableShips; k++) { if (defensiveShips[k].visible) { defensiveShips[k].improveLasersText.visible = currentScore >= 75; defensiveShips[k].costText.visible = currentScore >= 75; } } for (var i = 0; i < planets.length; i++) { planets[i].update(); } // Check if at least one spaceship is placed before spawning asteroids var hasPlacedShip = false; for (var k = 0; k < defensiveShips.length; k++) { if (defensiveShips[k].placed) { hasPlacedShip = true; break; } } // Only spawn asteroids if a spaceship is placed if (hasPlacedShip) { asteroidSpawnTimer++; // Calculate spawn frequency based on score - gradual increase after score 50 var baseSpawnDelay = 120; var minSpawnDelay = 20; // Higher minimum for less dramatic increase var cachedScore = LK.getScore(); // Cache score to avoid multiple calls var currentSpawnDelay; if (cachedScore < 50) { // Normal progression before score 50 var scoreSpeedUp = Math.floor(cachedScore / 100); currentSpawnDelay = Math.max(60, baseSpawnDelay - scoreSpeedUp * 10); } else { // Gradual increase after score 50 - slower progression var excessScore = cachedScore - 50; var linearSpeedUp = Math.floor(excessScore / 25); // Reduced frequency: every 25 points instead of 10 currentSpawnDelay = Math.max(minSpawnDelay, 60 - linearSpeedUp * 1); // Smaller reduction: 1 frame instead of 2 } if (asteroidSpawnTimer > currentSpawnDelay) { var asteroid = new Asteroid(); // Spawn only from bottom and left sides (angles between PI and 2*PI) var spawnAngle = Math.PI + Math.random() * Math.PI; var spawnDistance = 1500; var cosSpawn = Math.cos(spawnAngle); var sinSpawn = Math.sin(spawnAngle); asteroid.x = sunX + cosSpawn * spawnDistance; asteroid.y = sunY + sinSpawn * spawnDistance; // Set initial velocity toward Earth's current position var dx = earth.x - asteroid.x; var dy = earth.y - asteroid.y; var distanceSquared = dx * dx + dy * dy; if (distanceSquared > 0) { var distance = Math.sqrt(distanceSquared); // Increase speed based on score after reaching 50 points - much more gradual var baseSpeed = 2; var speedMultiplier = 1; if (cachedScore >= 50) { // Very gradual speed increase: 0.2% per point after 50, capped at 2x speed var speedIncrease = (cachedScore - 50) * 0.002; // Reduced from 0.02 to 0.002 speedMultiplier = Math.min(2.0, 1 + speedIncrease); // Cap at 2x speed (reached at score 550) } var speed = baseSpeed * speedMultiplier / distance; // Cache division asteroid.velocityX = dx * speed; asteroid.velocityY = dy * speed; } asteroids.push(asteroid); game.addChild(asteroid); asteroidSpawnTimer = 0; } } for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; if (bullet.x < -50 || bullet.x > 2098 || bullet.y < -50 || bullet.y > 2782) { bullet.destroy(); bullets.splice(i, 1); continue; } for (var j = asteroids.length - 1; j >= 0; j--) { if (bullet.intersects(asteroids[j])) { LK.setScore(LK.getScore() + 5); scoreTxt.setText('Score: ' + LK.getScore()); bullet.destroy(); bullets.splice(i, 1); asteroids[j].destroy(); asteroids.splice(j, 1); break; } } } // Update lasers for (var i = lasers.length - 1; i >= 0; i--) { var laser = lasers[i]; laser.update(); if (laser.x < -50 || laser.x > 2098 || laser.y < -50 || laser.y > 2782) { laser.destroy(); lasers.splice(i, 1); continue; } // Check laser collision with asteroids var laserHit = false; for (var j = asteroids.length - 1; j >= 0 && !laserHit; j--) { var asteroid = asteroids[j]; // Quick distance check before expensive intersects call var dx = laser.x - asteroid.x; var dy = laser.y - asteroid.y; var distanceSquared = dx * dx + dy * dy; if (distanceSquared < 900 && laser.intersects(asteroid)) { // 30*30 rough collision area LK.setScore(LK.getScore() + 5); scoreTxt.setText('Score: ' + LK.getScore()); laser.destroy(); lasers.splice(i, 1); asteroid.destroy(); asteroids.splice(j, 1); laserHit = true; break; } } } for (var i = asteroids.length - 1; i >= 0; i--) { var asteroid = asteroids[i]; asteroid.update(); var asteroidHit = false; // Cache asteroid position relative to sun var dx = asteroid.x - sunX; var dy = asteroid.y - sunY; var sunDistanceSquared = dx * dx + dy * dy; // Check sun collision first (most common) if (sunDistanceSquared < 14400) { // 120 * 120 = 14400 asteroid.destroy(); asteroids.splice(i, 1); continue; } // Early exit if asteroid is far from all planets (optimization) var minPlanetDistance = 200; // Approximate max planet radius + safety margin if (sunDistanceSquared > 1440000) { // 1200 * 1200 - beyond Neptune's orbit continue; } // Count placed ships to determine if immunity should apply var placedShipsCount = 0; for (var m = 0; m < defensiveShips.length; m++) { if (defensiveShips[m].placed) { placedShipsCount++; } } // Check asteroid collision with defensive ships first (only if more than one ship is placed) if (placedShipsCount > 1) { for (var k = 0; k < defensiveShips.length && !asteroidHit; k++) { var ship = defensiveShips[k]; if (ship.placed && asteroid.intersects(ship)) { // Flash ship dim red tween(ship, { tint: 0x664444 }, { duration: 300, easing: tween.easeOut, onFinish: function onFinish() { tween(ship, { tint: 0xFFFFFF }, { duration: 300, easing: tween.easeOut }); } }); // Remove the ship ship.destroy(); defensiveShips.splice(k, 1); // Adjust availableShips count if (availableShips > defensiveShips.length) { availableShips = defensiveShips.length; } asteroid.destroy(); asteroids.splice(i, 1); asteroidHit = true; break; } } } // Only check planet collisions if not destroyed by sun or ships for (var j = 0; j < planets.length && !asteroidHit; j++) { var planet = planets[j]; // Quick distance check before expensive intersects call var planetDx = asteroid.x - planet.x; var planetDy = asteroid.y - planet.y; var planetDistanceSquared = planetDx * planetDx + planetDy * planetDy; if (planetDistanceSquared < 10000 && asteroid.intersects(planet)) { // 100*100 rough collision area if (planet === earth) { LK.effects.flashScreen(0x440000, 1000); lives--; livesTxt.setText('Lives: ' + lives); if (lives <= 0) { LK.showGameOver(); return; } } asteroid.destroy(); asteroids.splice(i, 1); asteroidHit = true; break; } } } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Asteroid = Container.expand(function () {
var self = Container.call(this);
var asteroidGraphics = self.attachAsset('asteroid', {
anchorX: 0.5,
anchorY: 0.5
});
// Randomize asteroid size between 45x45 and 70x70
// Base asset is 60x60, so scale range is 0.75 to 1.17
var randomScale = 0.75 + Math.random() * 0.42; // Random value between 0.75 and 1.17
asteroidGraphics.scaleX = randomScale;
asteroidGraphics.scaleY = randomScale;
self.velocityX = 0;
self.velocityY = 0;
self.targetPlanet = null;
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
asteroidGraphics.rotation += 0.05;
};
return self;
});
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
};
return self;
});
var Laser = Container.expand(function () {
var self = Container.call(this);
var laserGraphics = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
laserGraphics.tint = 0x00ff00; // Green laser color
laserGraphics.scaleX = 0.5; // Make laser thinner
laserGraphics.scaleY = 2; // Make laser longer
self.velocityX = 0;
self.velocityY = 0;
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
};
return self;
});
var OrbitLine = Container.expand(function (radius) {
var self = Container.call(this);
self.orbitRadius = radius;
// Create a single connected circular line using small line segments
var numSegments = Math.max(16, Math.floor(radius * 0.1)); // Fewer segments for better performance
var twoPi = Math.PI * 2; // Cache the calculation
for (var i = 0; i < numSegments; i++) {
var angle1 = i / numSegments * twoPi;
var angle2 = (i + 1) / numSegments * twoPi;
var x1 = Math.cos(angle1) * radius;
var y1 = Math.sin(angle1) * radius;
var x2 = Math.cos(angle2) * radius;
var y2 = Math.sin(angle2) * radius;
// Calculate segment length and angle
var segmentLength = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
var segmentAngle = Math.atan2(y2 - y1, x2 - x1);
// Create line segment
var lineSegment = self.attachAsset('orbitLine', {
anchorX: 0,
anchorY: 0.5,
alpha: 0.4,
scaleX: segmentLength / 2,
// Scale to match segment length
scaleY: 1
});
// Position and rotate the segment
lineSegment.x = x1;
lineSegment.y = y1;
lineSegment.rotation = segmentAngle;
}
return self;
});
var Planet = Container.expand(function (size, color, orbitRadius, orbitSpeed) {
var self = Container.call(this);
self.orbitRadius = orbitRadius;
self.orbitSpeed = orbitSpeed;
self.angle = Math.random() * Math.PI * 2;
var assetName = 'mercury';
if (color === 0x4169e1) {
assetName = 'earth';
} else if (color === 0xff4500) {
assetName = 'mars';
} else if (color === 0xffc649) {
assetName = 'venus';
} else if (color === 0xd2691e) {
assetName = 'jupiter';
} else if (color === 0xffd700) {
assetName = 'saturn';
} else if (color === 0x40e0d0) {
assetName = 'uranus';
} else if (color === 0x0080ff) {
assetName = 'neptune';
}
var planetGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
self.angle += self.orbitSpeed;
var cosAngle = Math.cos(self.angle);
var sinAngle = Math.sin(self.angle);
self.x = sunX + cosAngle * self.orbitRadius;
self.y = sunY + sinAngle * self.orbitRadius;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000011
});
/****
* Game Code
****/
// Show instructions before game starts
var instructionText = new Text2(" How to play:\n\nGoal:\n- protect the Earth from asteroid collisions\n- you will be given a spaceship that shoots lasers\n- you will use these lasers to destroy asteroids\n- if the Earth gets hit with 4 asteroids, you lose\n- if you have multiple spaceships, they will all fire where you choose\n\nPoints:\n- every asteroid you destroy will earn you five points\n- every 50 points, you will recieve a new spaceship you can put in the solar system\n- if you reach 200 points, that interval will increase to every 100 points\n- with a price of 75 points, you can buy a laser upgrade that randomly applies to one ship in play, giving it the ability to shoot two lasers instead of one\n\nAsteroids:\n- asteroids are your enimies\n- they can be destroyed with one laser hit\n- they will come from random directions, so be aware of all sides of the screen\n- planets are you friends, all planets other than Earth deflect asteroids\n- if a ship gets hit with an asteroid, it will be removed, so keep a lookout for your ships\n\nControls:\n- select a spaceship until it shrinks, then click and drag it to move it, release to place it\n- click any point on the sreen to fire lasers in that direction\n- if you have multiple spaceships, they will all fire where you choose", {
size: 60,
fill: 0xFFFFFF,
wordWrap: true,
wordWrapWidth: 1600
});
instructionText.anchor.set(0.5, 0.5);
instructionText.x = 2048 / 2;
instructionText.y = 2732 / 2 - 100;
game.addChild(instructionText);
// Add separate "Got it!" button text
var gotItText = new Text2("Got it!", {
size: 80,
fill: 0xFFFFFF
});
gotItText.anchor.set(0.5, 0.5);
gotItText.x = 2048 / 2;
gotItText.y = 2732 - 200; // Position below instructions, above bottom of screen
game.addChild(gotItText);
// Add spinning earth to top right corner of instructions
var instructionEarth = LK.getAsset('earth', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 3,
x: 2048 - 200,
y: 200
});
game.addChild(instructionEarth);
// Add defensive ship next to spinning earth
var instructionShip = LK.getAsset('defensiveShip', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5,
x: 2048 - 450,
y: 200
});
game.addChild(instructionShip);
// Start spinning animation
function spinEarth() {
tween(instructionEarth, {
rotation: instructionEarth.rotation + Math.PI * 2
}, {
duration: 3000,
easing: tween.linear,
onFinish: function onFinish() {
if (instructionsVisible) {
spinEarth();
}
}
});
}
spinEarth();
// Add click handler to dismiss instructions
var instructionsVisible = true;
var instructionAsteroids = [];
var instructionAsteroidSpawnTimer = 0;
game.down = function (x, y, obj) {
if (instructionsVisible) {
// Check if "Got it!" button was clicked
var gotItClickArea = {
x: gotItText.x - gotItText.width / 2 - 50,
y: gotItText.y - gotItText.height / 2 - 25,
width: gotItText.width + 100,
height: gotItText.height + 50
};
if (x >= gotItClickArea.x && x <= gotItClickArea.x + gotItClickArea.width && y >= gotItClickArea.y && y <= gotItClickArea.y + gotItClickArea.height) {
// Hide instructions and start the game
instructionText.destroy();
gotItText.destroy();
instructionEarth.destroy();
instructionShip.destroy();
// Clean up instruction asteroids
for (var i = 0; i < instructionAsteroids.length; i++) {
instructionAsteroids[i].destroy();
}
instructionAsteroids = [];
instructionsVisible = false;
// Initialize all game assets now
initializeGameAssets();
// Re-define the game.down handler for normal gameplay
}
return;
}
// Check if any improve lasers text was clicked
for (var k = 0; k < defensiveShips.length; k++) {
var ship = defensiveShips[k];
if (ship.visible && ship.improveLasersText.visible) {
// Convert coordinates to check if improve lasers text was clicked
var improveLasersClickArea = {
x: ship.improveLasersText.x - ship.improveLasersText.width,
y: ship.improveLasersText.y - ship.improveLasersText.height / 2,
width: ship.improveLasersText.width,
height: ship.improveLasersText.height
};
// Convert click coordinates to GUI space
var guiPos = LK.gui.topRight.toLocal(game.toGlobal({
x: x,
y: y
}));
if (guiPos.x >= improveLasersClickArea.x && guiPos.x <= improveLasersClickArea.x + improveLasersClickArea.width && guiPos.y >= improveLasersClickArea.y && guiPos.y <= improveLasersClickArea.y + improveLasersClickArea.height) {
// Check if player has enough points to use improve lasers
if (LK.getScore() < 75) {
return; // Not enough points, don't process the upgrade
}
// Find all placed ships that don't already have improved lasers
var upgradeableShips = [];
for (var j = 0; j < defensiveShips.length; j++) {
if (defensiveShips[j].placed && !defensiveShips[j].hasImprovedLasers) {
upgradeableShips.push(defensiveShips[j]);
}
}
// Randomly select one ship to upgrade
if (upgradeableShips.length > 0) {
// Deduct 75 points from score
LK.setScore(LK.getScore() - 75);
scoreTxt.setText('Score: ' + LK.getScore());
var randomIndex = Math.floor(Math.random() * upgradeableShips.length);
var selectedShip = upgradeableShips[randomIndex];
selectedShip.hasImprovedLasers = true;
// Visual feedback - flash the upgraded ship
tween(selectedShip, {
tint: 0x00ff00
}, {
duration: 500,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(selectedShip, {
tint: 0xffffff
}, {
duration: 500,
easing: tween.easeOut
});
}
});
}
return; // Don't fire lasers when clicking improve lasers
}
}
}
// Check if laser firing is on cooldown
if (laserCooldownActive) {
return;
}
// Make all placed ships fire lasers at the clicked point
for (var k = 0; k < defensiveShips.length; k++) {
var ship = defensiveShips[k];
// Only fire from ships that are placed
if (!ship.placed) {
continue;
}
// Calculate direction from this ship to clicked position
var dx = x - ship.x;
var dy = y - ship.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Calculate rotation angle to face the target point
var targetRotation = Math.atan2(dy, dx) + Math.PI / 2; // Add PI/2 since ship top should face target
// Rotate the ship to face the target
tween(ship, {
rotation: targetRotation
}, {
duration: 200,
easing: tween.easeOut
});
var speed = 8;
var normalizedDx = 0;
var normalizedDy = 0;
if (distance > 0) {
normalizedDx = dx / distance;
normalizedDy = dy / distance;
}
// Fire one or two lasers depending on ship upgrade
if (ship.hasImprovedLasers) {
// Fire two lasers with slight spread
var spreadAngle = 0.1; // Radians for spread
// First laser (slightly left)
var laser1 = new Laser();
laser1.x = ship.x;
laser1.y = ship.y;
var angle1 = Math.atan2(normalizedDy, normalizedDx) - spreadAngle;
laser1.velocityX = Math.cos(angle1) * speed;
laser1.velocityY = Math.sin(angle1) * speed;
lasers.push(laser1);
game.addChild(laser1);
// Second laser (slightly right)
var laser2 = new Laser();
laser2.x = ship.x;
laser2.y = ship.y;
var angle2 = Math.atan2(normalizedDy, normalizedDx) + spreadAngle;
laser2.velocityX = Math.cos(angle2) * speed;
laser2.velocityY = Math.sin(angle2) * speed;
lasers.push(laser2);
game.addChild(laser2);
} else {
// Fire single laser
var laser = new Laser();
laser.x = ship.x;
laser.y = ship.y;
laser.velocityX = normalizedDx * speed;
laser.velocityY = normalizedDy * speed;
lasers.push(laser);
game.addChild(laser);
}
}
// Start laser cooldown for 0.2 seconds
laserCooldownActive = true;
var cooldownTarget = {}; // Dummy object for tween
tween(cooldownTarget, {
dummy: 1
}, {
duration: 200,
onFinish: function onFinish() {
laserCooldownActive = false;
}
});
};
// Define variables that will be initialized after instructions
var sunX, sunY, sun, planets, mercury, venus, earth, mars, jupiter, saturn, uranus, neptune;
var orbitLines, bullets, lasers, asteroids, asteroidSpawnTimer, lives;
var scoreTxt, livesTxt, defensiveShips, selectedShipIndex, isDragging;
var totalShips, availableShips, lastScoreCheck;
var laserCooldownActive = false;
// Function to initialize game assets
function initializeGameAssets() {
sunX = 2048 / 2;
sunY = 2732 / 2;
sun = game.addChild(LK.getAsset('sun', {
anchorX: 0.5,
anchorY: 0.5,
x: sunX,
y: sunY
}));
planets = [];
mercury = new Planet(50, 0x8c7853, 150, 0.015);
venus = new Planet(70, 0xffc649, 200, 0.012);
earth = new Planet(80, 0x4169e1, 300, 0.008);
mars = new Planet(60, 0xff4500, 450, 0.005);
jupiter = new Planet(120, 0xd2691e, 600, 0.003);
saturn = new Planet(100, 0xffd700, 750, 0.002);
uranus = new Planet(90, 0x40e0d0, 900, 0.0015);
neptune = new Planet(85, 0x0080ff, 1050, 0.001);
planets.push(mercury);
planets.push(venus);
planets.push(earth);
planets.push(mars);
planets.push(jupiter);
planets.push(saturn);
planets.push(uranus);
planets.push(neptune);
// Create orbit lines for each planet
orbitLines = [];
var planetOrbits = [150, 200, 300, 450, 600, 750, 900, 1050]; // Mercury to Neptune orbit radii
for (var i = 0; i < planetOrbits.length; i++) {
var orbitLine = new OrbitLine(planetOrbits[i]);
orbitLine.x = sunX;
orbitLine.y = sunY;
orbitLines.push(orbitLine);
game.addChild(orbitLine);
}
for (var i = 0; i < planets.length; i++) {
game.addChild(planets[i]);
}
bullets = [];
lasers = [];
asteroids = [];
asteroidSpawnTimer = 0;
lives = 4;
scoreTxt = new Text2('Score: 0', {
size: 60,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
scoreTxt.x = 0;
LK.gui.top.addChild(scoreTxt);
livesTxt = new Text2('Lives: 4', {
size: 60,
fill: 0xFFFFFF
});
livesTxt.anchor.set(0.5, 0);
livesTxt.x = 0;
livesTxt.y = 70;
LK.gui.top.addChild(livesTxt);
defensiveShips = [];
selectedShipIndex = -1;
isDragging = false;
totalShips = 4; // Maximum number of ships
availableShips = 1; // Start with 1 ship available
lastScoreCheck = 0; // Track score for adding new ships
// Create 4 defensive ships but only show the available ones
for (var i = 0; i < totalShips; i++) {
var defensiveShip = LK.getAsset('defensiveShip', {
anchorX: 0.5,
anchorY: 0.5,
x: -80,
// Position all ships in the same spot in top right corner
y: 80
});
defensiveShip.shipIndex = i;
defensiveShip.placed = false;
defensiveShip.selected = false;
defensiveShip.requiresConfirmation = false;
defensiveShip.hasImprovedLasers = false;
// Only show ships that are available
defensiveShip.visible = i < availableShips;
defensiveShips.push(defensiveShip);
LK.gui.topRight.addChild(defensiveShip);
// Create "Improve Lasers" text for this ship
var improveLasersText = new Text2('Improve Lasers', {
size: 44,
fill: 0xFFFFFF
});
improveLasersText.anchor.set(1, 0.5); // Right anchor to position to left of ship
improveLasersText.x = -140; // Position further to the left of the ship
improveLasersText.y = 80;
improveLasersText.visible = i < availableShips && LK.getScore() >= 75;
defensiveShip.improveLasersText = improveLasersText; // Store reference on ship
LK.gui.topRight.addChild(improveLasersText);
// Create cost text below improve lasers
var costText = new Text2('Cost: 75 Points', {
size: 32,
fill: 0xcccccc
});
costText.anchor.set(1, 0.5); // Right anchor to position to left of ship
costText.x = -140; // Position further to the left of the ship
costText.y = 110; // Position below improve lasers text
costText.visible = i < availableShips && LK.getScore() >= 75;
defensiveShip.costText = costText; // Store reference on ship
LK.gui.topRight.addChild(costText);
}
// Add down handler to each defensive ship
for (var i = 0; i < defensiveShips.length; i++) {
defensiveShips[i].down = function (x, y, obj) {
var ship = this;
var shipIndex = ship.shipIndex;
// Only allow interaction with visible/available ships
if (!ship.visible) {
return;
}
if (ship.placed) {
// Ship has been placed, allow selection for shooting
// Deselect all other ships
for (var j = 0; j < defensiveShips.length; j++) {
if (j !== shipIndex && defensiveShips[j].selected) {
defensiveShips[j].selected = false;
tween(defensiveShips[j], {
alpha: 1.0
}, {
duration: 200
});
}
}
if (!ship.selected) {
ship.selected = true;
selectedShipIndex = shipIndex;
tween(ship, {
alpha: 0.7
}, {
duration: 200
});
}
// Removed else block to prevent manual dis-selection of placed ships
return;
}
// Ship not placed yet - check if it needs confirmation or can start dragging
if (!ship.requiresConfirmation) {
// First click - select ship and require confirmation
// Deselect all other ships
for (var j = 0; j < defensiveShips.length; j++) {
if (j !== shipIndex) {
defensiveShips[j].selected = false;
defensiveShips[j].requiresConfirmation = false;
tween(defensiveShips[j], {
alpha: 1.0
}, {
duration: 200
});
}
}
// Select this ship and mark it as requiring confirmation
ship.selected = true;
ship.requiresConfirmation = true;
selectedShipIndex = shipIndex;
tween(ship, {
alpha: 0.7
}, {
duration: 200
});
} else {
// Second click - start dragging
isDragging = true;
// Store original scale before moving
var originalScaleX = ship.scaleX;
var originalScaleY = ship.scaleY;
// No need to shift ships since they're all in the same position
// Move defensive ship to game area for dragging
var gamePos = game.toLocal(LK.gui.topRight.toGlobal({
x: ship.x,
y: ship.y
}));
LK.gui.topRight.removeChild(ship);
game.addChild(ship);
ship.x = gamePos.x;
ship.y = gamePos.y;
// Restore original scale after moving to game area
ship.scaleX = originalScaleX;
ship.scaleY = originalScaleY;
}
};
}
}
game.move = function (x, y, obj) {
// Move ship to cursor position during dragging
if (isDragging && selectedShipIndex >= 0) {
var ship = defensiveShips[selectedShipIndex];
ship.x = x;
ship.y = y;
}
};
game.up = function (x, y, obj) {
if (isDragging && selectedShipIndex >= 0) {
isDragging = false;
// Defensive ship is now placed in the solar system
var placedShip = defensiveShips[selectedShipIndex];
// Check if ship is off-screen and needs to be moved back to available ships area
var shipHalfWidth = 40; // Half width of defensive ship (80/2)
var shipHalfHeight = 55; // Half height of defensive ship (110/2)
var isOffScreen = placedShip.x - shipHalfWidth < 0 || placedShip.x + shipHalfWidth > 2048 || placedShip.y - shipHalfHeight < 0 || placedShip.y + shipHalfHeight > 2732;
if (isOffScreen) {
// Move ship back to available ships area
game.removeChild(placedShip);
LK.gui.topRight.addChild(placedShip);
// Place ship back at the single spawn position in top right corner
placedShip.x = -80;
placedShip.y = 80;
placedShip.placed = false;
} else {
placedShip.placed = true;
}
placedShip.selected = false;
selectedShipIndex = -1;
tween(placedShip, {
alpha: 1.0
}, {
duration: 200
});
}
};
game.update = function () {
// Don't update game while instructions are visible
if (instructionsVisible) {
// Spawn asteroids for instruction demonstration
instructionAsteroidSpawnTimer++;
if (instructionAsteroidSpawnTimer > 90) {
// Spawn every 1.5 seconds
var asteroid = new Asteroid();
// Randomly choose left or bottom side to spawn from
var spawnSide = Math.random() < 0.5 ? 'left' : 'bottom';
if (spawnSide === 'left') {
// Spawn from left side at random Y position
asteroid.x = -50;
asteroid.y = Math.random() * 2732;
} else {
// Spawn from bottom side at random X position
asteroid.x = Math.random() * 2048;
asteroid.y = 2732 + 50;
}
// Calculate direction toward instruction earth
var dx = instructionEarth.x - asteroid.x;
var dy = instructionEarth.y - asteroid.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var speed = 2;
if (distance > 0) {
asteroid.velocityX = dx / distance * speed;
asteroid.velocityY = dy / distance * speed;
}
instructionAsteroids.push(asteroid);
game.addChild(asteroid);
instructionAsteroidSpawnTimer = 0;
}
// Count active instruction lasers
var activeLasers = 0;
for (var m = 0; m < instructionAsteroids.length; m++) {
if (instructionAsteroids[m] instanceof Laser) {
activeLasers++;
}
}
// Auto-fire from instruction ship every 45 frames (0.75 seconds) - only if no active laser
if (instructionAsteroidSpawnTimer % 45 === 0 && instructionAsteroids.length > 0 && activeLasers === 0) {
// Find closest asteroid to instruction ship
var closestAsteroid = null;
var closestDistance = Infinity;
for (var k = 0; k < instructionAsteroids.length; k++) {
var asteroid = instructionAsteroids[k];
if (asteroid instanceof Asteroid) {
var dx = asteroid.x - instructionShip.x;
var dy = asteroid.y - instructionShip.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestAsteroid = asteroid;
}
}
}
// Fire laser at closest asteroid only if it's visible on screen and not targeting earth
if (closestAsteroid && closestAsteroid.x >= -50 && closestAsteroid.x <= 2098 && closestAsteroid.y >= -50 && closestAsteroid.y <= 2782) {
// Check if firing would hit the earth - don't fire if trajectory passes too close to earth
var dx = closestAsteroid.x - instructionShip.x;
var dy = closestAsteroid.y - instructionShip.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Calculate predicted asteroid position
var laserSpeed = 8;
var timeToTarget = distance / laserSpeed;
var predictedX = closestAsteroid.x + closestAsteroid.velocityX * timeToTarget;
var predictedY = closestAsteroid.y + closestAsteroid.velocityY * timeToTarget;
// Check if laser trajectory would pass too close to earth
var earthDx = instructionEarth.x - instructionShip.x;
var earthDy = instructionEarth.y - instructionShip.y;
var targetDx = predictedX - instructionShip.x;
var targetDy = predictedY - instructionShip.y;
// Calculate angle between earth direction and target direction
var earthAngle = Math.atan2(earthDy, earthDx);
var targetAngle = Math.atan2(targetDy, targetDx);
var angleDiff = Math.abs(earthAngle - targetAngle);
if (angleDiff > Math.PI) {
angleDiff = 2 * Math.PI - angleDiff;
} // Normalize to shortest angle
// Only fire if angle difference is greater than 0.3 radians (~17 degrees) to avoid hitting earth
if (angleDiff > 0.3) {
var instructionLaser = new Laser();
instructionLaser.x = instructionShip.x;
instructionLaser.y = instructionShip.y;
// Calculate direction to predicted position
var laserDx = predictedX - instructionLaser.x;
var laserDy = predictedY - instructionLaser.y;
var laserDistance = Math.sqrt(laserDx * laserDx + laserDy * laserDy);
// Add aiming inaccuracy - larger error at longer distances
var aimError = laserDistance / 300 * 0.15; // Reduced error for better accuracy
var randomAngleOffset = (Math.random() - 0.5) * aimError; // Random offset between -aimError/2 and +aimError/2
var baseAngle = Math.atan2(laserDy, laserDx);
var aimAngle = baseAngle + randomAngleOffset;
var speed = 8;
if (laserDistance > 0) {
instructionLaser.velocityX = Math.cos(aimAngle) * speed;
instructionLaser.velocityY = Math.sin(aimAngle) * speed;
}
// Rotate ship to face target (still accurate rotation for visual effect)
var targetRotation = Math.atan2(laserDy, laserDx) + Math.PI / 2;
tween(instructionShip, {
rotation: targetRotation
}, {
duration: 200,
easing: tween.easeOut
});
instructionAsteroids.push(instructionLaser); // Reuse instructionAsteroids array for cleanup
game.addChild(instructionLaser);
}
}
}
// Update instruction asteroids and lasers
for (var i = instructionAsteroids.length - 1; i >= 0; i--) {
var object = instructionAsteroids[i];
object.update();
// Check if it's a laser hitting an asteroid
if (object instanceof Laser) {
var laserHit = false;
for (var j = instructionAsteroids.length - 1; j >= 0 && !laserHit; j--) {
var target = instructionAsteroids[j];
if (target instanceof Asteroid && object.intersects(target)) {
object.destroy();
instructionAsteroids.splice(i, 1);
target.destroy();
instructionAsteroids.splice(j > i ? j - 1 : j, 1);
laserHit = true;
if (j < i) {
i--;
} // Adjust index since we removed an element before current
break;
}
}
// Remove laser if off-screen and not hit
if (!laserHit && (object.x < -100 || object.x > 2148 || object.y < -100 || object.y > 2832)) {
object.destroy();
instructionAsteroids.splice(i, 1);
}
} else if (object instanceof Asteroid) {
// Remove asteroids that are off-screen or hit the earth
if (object.x < -100 || object.x > 2148 || object.y < -100 || object.y > 2832) {
object.destroy();
instructionAsteroids.splice(i, 1);
} else {
// Check if asteroid hits instruction earth (simple distance check)
var dx = object.x - instructionEarth.x;
var dy = object.y - instructionEarth.y;
var distanceSquared = dx * dx + dy * dy;
if (distanceSquared < 10000) {
// Collision distance - flash earth dim red
tween(instructionEarth, {
tint: 0x664444
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(instructionEarth, {
tint: 0xFFFFFF
}, {
duration: 300,
easing: tween.easeOut
});
}
});
object.destroy();
instructionAsteroids.splice(i, 1);
}
}
}
}
return;
}
// Check if we should add a new ship (every 50 points up to 200, then every 100 points)
var currentScore = LK.getScore();
var newAvailableShips;
if (currentScore < 200) {
// Every 50 points up to 200
newAvailableShips = Math.min(totalShips, 1 + Math.floor(currentScore / 50));
} else {
// After 200: base ships from first 200 points plus additional ships every 100 points starting from 200
var baseShips = 1 + Math.floor(200 / 50); // Ships earned up to 200 points (5 total: 1 + 4)
var additionalShips = Math.floor((currentScore - 200) / 100); // Additional ships every 100 points after 200
newAvailableShips = Math.min(totalShips, baseShips + additionalShips);
}
if (newAvailableShips > availableShips) {
availableShips = newAvailableShips;
// Make the newly available ship visible
for (var k = 0; k < availableShips; k++) {
if (!defensiveShips[k].visible) {
defensiveShips[k].visible = true;
defensiveShips[k].improveLasersText.visible = currentScore >= 75;
defensiveShips[k].costText.visible = currentScore >= 75;
}
}
}
// Update improve lasers text visibility based on score (already using cached currentScore)
for (var k = 0; k < availableShips; k++) {
if (defensiveShips[k].visible) {
defensiveShips[k].improveLasersText.visible = currentScore >= 75;
defensiveShips[k].costText.visible = currentScore >= 75;
}
}
for (var i = 0; i < planets.length; i++) {
planets[i].update();
}
// Check if at least one spaceship is placed before spawning asteroids
var hasPlacedShip = false;
for (var k = 0; k < defensiveShips.length; k++) {
if (defensiveShips[k].placed) {
hasPlacedShip = true;
break;
}
}
// Only spawn asteroids if a spaceship is placed
if (hasPlacedShip) {
asteroidSpawnTimer++;
// Calculate spawn frequency based on score - gradual increase after score 50
var baseSpawnDelay = 120;
var minSpawnDelay = 20; // Higher minimum for less dramatic increase
var cachedScore = LK.getScore(); // Cache score to avoid multiple calls
var currentSpawnDelay;
if (cachedScore < 50) {
// Normal progression before score 50
var scoreSpeedUp = Math.floor(cachedScore / 100);
currentSpawnDelay = Math.max(60, baseSpawnDelay - scoreSpeedUp * 10);
} else {
// Gradual increase after score 50 - slower progression
var excessScore = cachedScore - 50;
var linearSpeedUp = Math.floor(excessScore / 25); // Reduced frequency: every 25 points instead of 10
currentSpawnDelay = Math.max(minSpawnDelay, 60 - linearSpeedUp * 1); // Smaller reduction: 1 frame instead of 2
}
if (asteroidSpawnTimer > currentSpawnDelay) {
var asteroid = new Asteroid();
// Spawn only from bottom and left sides (angles between PI and 2*PI)
var spawnAngle = Math.PI + Math.random() * Math.PI;
var spawnDistance = 1500;
var cosSpawn = Math.cos(spawnAngle);
var sinSpawn = Math.sin(spawnAngle);
asteroid.x = sunX + cosSpawn * spawnDistance;
asteroid.y = sunY + sinSpawn * spawnDistance;
// Set initial velocity toward Earth's current position
var dx = earth.x - asteroid.x;
var dy = earth.y - asteroid.y;
var distanceSquared = dx * dx + dy * dy;
if (distanceSquared > 0) {
var distance = Math.sqrt(distanceSquared);
// Increase speed based on score after reaching 50 points - much more gradual
var baseSpeed = 2;
var speedMultiplier = 1;
if (cachedScore >= 50) {
// Very gradual speed increase: 0.2% per point after 50, capped at 2x speed
var speedIncrease = (cachedScore - 50) * 0.002; // Reduced from 0.02 to 0.002
speedMultiplier = Math.min(2.0, 1 + speedIncrease); // Cap at 2x speed (reached at score 550)
}
var speed = baseSpeed * speedMultiplier / distance; // Cache division
asteroid.velocityX = dx * speed;
asteroid.velocityY = dy * speed;
}
asteroids.push(asteroid);
game.addChild(asteroid);
asteroidSpawnTimer = 0;
}
}
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
if (bullet.x < -50 || bullet.x > 2098 || bullet.y < -50 || bullet.y > 2782) {
bullet.destroy();
bullets.splice(i, 1);
continue;
}
for (var j = asteroids.length - 1; j >= 0; j--) {
if (bullet.intersects(asteroids[j])) {
LK.setScore(LK.getScore() + 5);
scoreTxt.setText('Score: ' + LK.getScore());
bullet.destroy();
bullets.splice(i, 1);
asteroids[j].destroy();
asteroids.splice(j, 1);
break;
}
}
}
// Update lasers
for (var i = lasers.length - 1; i >= 0; i--) {
var laser = lasers[i];
laser.update();
if (laser.x < -50 || laser.x > 2098 || laser.y < -50 || laser.y > 2782) {
laser.destroy();
lasers.splice(i, 1);
continue;
}
// Check laser collision with asteroids
var laserHit = false;
for (var j = asteroids.length - 1; j >= 0 && !laserHit; j--) {
var asteroid = asteroids[j];
// Quick distance check before expensive intersects call
var dx = laser.x - asteroid.x;
var dy = laser.y - asteroid.y;
var distanceSquared = dx * dx + dy * dy;
if (distanceSquared < 900 && laser.intersects(asteroid)) {
// 30*30 rough collision area
LK.setScore(LK.getScore() + 5);
scoreTxt.setText('Score: ' + LK.getScore());
laser.destroy();
lasers.splice(i, 1);
asteroid.destroy();
asteroids.splice(j, 1);
laserHit = true;
break;
}
}
}
for (var i = asteroids.length - 1; i >= 0; i--) {
var asteroid = asteroids[i];
asteroid.update();
var asteroidHit = false;
// Cache asteroid position relative to sun
var dx = asteroid.x - sunX;
var dy = asteroid.y - sunY;
var sunDistanceSquared = dx * dx + dy * dy;
// Check sun collision first (most common)
if (sunDistanceSquared < 14400) {
// 120 * 120 = 14400
asteroid.destroy();
asteroids.splice(i, 1);
continue;
}
// Early exit if asteroid is far from all planets (optimization)
var minPlanetDistance = 200; // Approximate max planet radius + safety margin
if (sunDistanceSquared > 1440000) {
// 1200 * 1200 - beyond Neptune's orbit
continue;
}
// Count placed ships to determine if immunity should apply
var placedShipsCount = 0;
for (var m = 0; m < defensiveShips.length; m++) {
if (defensiveShips[m].placed) {
placedShipsCount++;
}
}
// Check asteroid collision with defensive ships first (only if more than one ship is placed)
if (placedShipsCount > 1) {
for (var k = 0; k < defensiveShips.length && !asteroidHit; k++) {
var ship = defensiveShips[k];
if (ship.placed && asteroid.intersects(ship)) {
// Flash ship dim red
tween(ship, {
tint: 0x664444
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(ship, {
tint: 0xFFFFFF
}, {
duration: 300,
easing: tween.easeOut
});
}
});
// Remove the ship
ship.destroy();
defensiveShips.splice(k, 1);
// Adjust availableShips count
if (availableShips > defensiveShips.length) {
availableShips = defensiveShips.length;
}
asteroid.destroy();
asteroids.splice(i, 1);
asteroidHit = true;
break;
}
}
}
// Only check planet collisions if not destroyed by sun or ships
for (var j = 0; j < planets.length && !asteroidHit; j++) {
var planet = planets[j];
// Quick distance check before expensive intersects call
var planetDx = asteroid.x - planet.x;
var planetDy = asteroid.y - planet.y;
var planetDistanceSquared = planetDx * planetDx + planetDy * planetDy;
if (planetDistanceSquared < 10000 && asteroid.intersects(planet)) {
// 100*100 rough collision area
if (planet === earth) {
LK.effects.flashScreen(0x440000, 1000);
lives--;
livesTxt.setText('Lives: ' + lives);
if (lives <= 0) {
LK.showGameOver();
return;
}
}
asteroid.destroy();
asteroids.splice(i, 1);
asteroidHit = true;
break;
}
}
}
};