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