User prompt
Increase number of available ships to 4
User prompt
As the game goes on, make asteroids more frequent
User prompt
Allow defensive ships to be selected after placement but not moved. Instead, when selected after placement, allow the player to select any point in the solar system and have the lasers shoot toward that point. If the lasers hit an asteroid, destroy the asteroid. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Only allow defensive ships to be placed once and not moved again.
User prompt
When selected, allow defensive ships to be dragged to a location in the solar system. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Increase size of defensive ships
User prompt
Allow defensive ships to be selected. When selected, make the asset more transparent so the player knows it has been selected. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Put a defensive ship below the score/lives display
User prompt
Create new asset for defensive ships
User prompt
Create a new asset for ships instead of using bullet asset
User prompt
Allow ships to be selected and then dragged when selected
User prompt
Allow ships to be dragged to place
User prompt
Increase size of ships
User prompt
Below score and lives, put 4 ships that can be selected and then placed about the solar system. These ships can shoot lasers and destroy asteroids. When selected after being placed, and then a point in the solar system is clicked, the ship turns and the lasers point in the direction of that point. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Center the score and lives displays
User prompt
Put the lives display next to the score
User prompt
Do not sway asteroid trajectory
User prompt
Only flash red when Earth is hit ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Give the player four “lives”, displayed in the bottom right corner
User prompt
Add the other four planets too
User prompt
Remove the defensive ships
Code edit (1 edits merged)
Please save this source code
User prompt
Solar System Defender
User prompt
Instead of earth at the center, put the Sun at the center. The goal is still to protect the Earth but all of the planets are in orbit.
Initial prompt
I have an idea for a tower defense-style game, but it’s space themed. So asteroids fly through the solar system and the player has to deflect them from hitting earth.
/**** * 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;
}
}
}
};