/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
lives: 3,
maxLives: 3,
highestLevel: 1
});
/****
* Classes
****/
var Boss = Container.expand(function () {
var self = Container.call(this);
// Randomly select boss asset
var bossAssets = ['boss', 'boss2', 'boss3', 'boss4'];
var selectedAsset = bossAssets[Math.floor(Math.random() * bossAssets.length)];
var bossGraphics = self.attachAsset(selectedAsset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.5,
scaleY: 2.5
});
self.health = 50;
self.maxHealth = 50;
self.shootTimer = 0;
// Shooting rate based on current level - faster at higher levels
self.shootRate = Math.max(30, 90 - (currentLevel - 1) * 5); // Gets faster each level
self.angle = 0;
self.orbitDistance = 800; // Boss orbits at much larger radius than player (was 600)
self.centerX = 2048 / 2;
self.centerY = 2732 / 2;
// Rotation speed based on current level - faster at higher levels
self.rotationSpeed = 0.02 + (currentLevel - 1) * 0.005;
self.bullets = [];
self.glowPhase = 0;
self.update = function () {
// Update level-based properties dynamically
self.shootRate = Math.max(30, 90 - (currentLevel - 1) * 5); // Gets faster each level
self.rotationSpeed = 0.02 + (currentLevel - 1) * 0.005; // Gets faster each level
// Orbital movement around center
self.angle += self.rotationSpeed;
self.x = self.centerX + Math.cos(self.angle) * self.orbitDistance;
self.y = self.centerY + Math.sin(self.angle) * self.orbitDistance;
// Visual effects - glow and rotation to face outward
self.glowPhase += 0.1;
var glowIntensity = Math.sin(self.glowPhase) * 0.3 + 0.7;
bossGraphics.alpha = glowIntensity;
// Rotate boss to face outward from center (head toward edge, feet toward center)
var angleToCenter = Math.atan2(self.centerY - self.y, self.centerX - self.x);
bossGraphics.rotation = angleToCenter + Math.PI; // Add PI to face away from center
// Remove color tinting to show original boss colors
// Shooting mechanics
self.shootTimer++;
if (self.shootTimer >= self.shootRate) {
self.shoot();
self.shootTimer = 0;
}
// Clean up destroyed bullets
for (var i = self.bullets.length - 1; i >= 0; i--) {
var bullet = self.bullets[i];
if (bullet.age >= bullet.lifetime) {
self.bullets.splice(i, 1);
}
}
};
self.shoot = function () {
// Create bullet that shoots toward center like player
var bullet = new BossBullet();
bullet.x = self.x;
bullet.y = self.y;
// Always shoot toward center (moon) like player does
var dx = self.centerX - self.x;
var dy = self.centerY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
bullet.velocityX = dx / distance * bullet.speed;
bullet.velocityY = dy / distance * bullet.speed;
self.bullets.push(bullet);
game.addChild(bullet);
LK.getSound('bossShoot').play();
};
self.takeDamage = function (damage) {
self.health -= damage;
// Flash effect when taking damage (scale only, no color change)
tween(bossGraphics, {
scaleX: 2.8,
scaleY: 2.8
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(bossGraphics, {
scaleX: 2.5,
scaleY: 2.5
}, {
duration: 100,
easing: tween.easeIn
});
}
});
if (self.health <= 0) {
self.destroy();
return true; // Boss defeated
}
return false;
};
return self;
});
var BossBullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('bossBullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.speed = 8; // Same speed as player bullets
self.velocityX = 0;
self.velocityY = 0;
self.centerX = 2048 / 2;
self.centerY = 2732 / 2;
self.update = function () {
// Move in straight line toward center like player bullets
self.x += self.velocityX;
self.y += self.velocityY;
// Rotate bullet to point toward center (tip toward center, tail toward edge)
bulletGraphics.rotation = Math.atan2(self.velocityY, self.velocityX);
// Remove bullet if it goes off screen or reaches center
var distanceToCenter = Math.sqrt((self.x - self.centerX) * (self.x - self.centerX) + (self.y - self.centerY) * (self.y - self.centerY));
if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782 || distanceToCenter < 80) {
self.destroy();
}
};
return self;
});
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('playerBullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 3
});
self.speed = 12;
self.velocityX = 0;
self.velocityY = 0;
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
// Rotate bullet to point away from center (tip toward edge, tail toward center)
bulletGraphics.rotation = Math.atan2(self.velocityY, self.velocityX);
// Remove bullet if it goes off screen
if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782) {
self.destroy();
}
};
return self;
});
var FireParticle = Container.expand(function () {
var self = Container.call(this);
var particleGraphics = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
scaleY: 0.6,
tint: 0xff4500
});
self.life = 1.0;
self.velocityX = 0;
self.velocityY = 0;
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
self.life -= 0.05;
particleGraphics.alpha = self.life;
particleGraphics.scaleX = self.life * 0.6;
particleGraphics.scaleY = self.life * 0.6;
if (self.life <= 0) {
self.destroy();
}
};
return self;
});
var ImmunityPowerUp = Container.expand(function () {
var self = Container.call(this);
var shieldGraphics = self.attachAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5,
tint: 0x00ffff
});
// Rotation and glow effect
self.rotationSpeed = 0.08;
self.glowPhase = 0;
self.glowSpeed = 0.12;
self.baseScale = 1.2;
self.lifetime = 600; // 10 seconds at 60fps
self.age = 0;
self.update = function () {
// Rotate the shield
shieldGraphics.rotation += self.rotationSpeed;
// Glowing effect
self.glowPhase += self.glowSpeed;
var glowFactor = 1 + Math.sin(self.glowPhase) * 0.3;
shieldGraphics.scaleX = self.baseScale * glowFactor;
shieldGraphics.scaleY = self.baseScale * glowFactor;
// Cycle through cyan and blue tints
var tintPhase = Math.sin(self.glowPhase * 0.5) * 0.5 + 0.5;
var r = 0;
var g = Math.floor(255 * (0.6 + tintPhase * 0.4));
var b = 255;
shieldGraphics.tint = r << 16 | g << 8 | b;
// Age and fade out near end of lifetime
self.age++;
if (self.age > self.lifetime * 0.7) {
var fadeProgress = (self.age - self.lifetime * 0.7) / (self.lifetime * 0.3);
shieldGraphics.alpha = 1 - fadeProgress;
}
// Remove when lifetime expires
if (self.age >= self.lifetime) {
self.destroy();
}
};
return self;
});
var LifePowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('heart', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2,
tint: 0xff1493
});
// Rotation and pulsing effect
self.rotationSpeed = 0.05;
self.pulsePhase = 0;
self.pulseSpeed = 0.1;
self.baseScale = 0.8;
self.lifetime = 600; // 10 seconds at 60fps
self.age = 0;
self.update = function () {
// Rotate the power-up
powerUpGraphics.rotation += self.rotationSpeed;
// Pulsing scale effect
self.pulsePhase += self.pulseSpeed;
var pulseFactor = 1 + Math.sin(self.pulsePhase) * 0.2;
powerUpGraphics.scaleX = self.baseScale * pulseFactor;
powerUpGraphics.scaleY = self.baseScale * pulseFactor;
// Age and fade out near end of lifetime
self.age++;
if (self.age > self.lifetime * 0.7) {
var fadeProgress = (self.age - self.lifetime * 0.7) / (self.lifetime * 0.3);
powerUpGraphics.alpha = 1 - fadeProgress;
}
// Remove when lifetime expires
if (self.age >= self.lifetime) {
self.destroy();
}
};
return self;
});
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obstacleGraphics = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
self.speed = 2;
self.targetX = 2048 / 2;
self.targetY = 2732 / 2;
self.update = function () {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Calculate scale based on distance from center, but only start scaling after passing player orbit
var playerOrbitDistance = 400; // Player orbit distance from center
var maxDistance = Math.sqrt(2048 / 2 * (2048 / 2) + 2732 / 2 * (2732 / 2)); // Max distance from center to corner
var minDistance = 80; // Stop before touching the moon (moon radius ~60px)
var scale;
if (distance > playerOrbitDistance) {
// Maintain original size until reaching player orbit
scale = 2.0; // Original obstacle scale
} else {
// Start scaling only after passing player orbit
var normalizedDistance = Math.max(0, Math.min(1, (distance - minDistance) / (playerOrbitDistance - minDistance)));
scale = 0.15 + normalizedDistance * 1.85; // Scale from 0.15 to 2.0 within player orbit
}
// Apply scale with tween for smooth transition
tween(obstacleGraphics, {
scaleX: scale,
scaleY: scale
}, {
duration: 100,
easing: tween.easeOut
});
// Always move toward center - obstacles will disappear before reaching moon
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
// Rotate obstacle continuously as it moves
obstacleGraphics.rotation += 0.1;
};
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
// Make it look more like a spaceship by rotating it to point upward
playerGraphics.rotation = -Math.PI * 1.5; // Rotated an additional 90 degrees
self.speed = 8;
self.targetX = self.x;
self.targetY = self.y;
self.lastX = self.x;
self.lastY = self.y;
self.fireParticles = [];
self.bullets = [];
self.shootCooldown = 0;
self.tripleShotTimer = 0;
self.tripleShotDuration = 900; // 15 seconds at 60fps
self.hasTripleShot = false;
self.engineSoundPlaying = false;
self.update = function () {
// Store last position for movement detection
self.lastX = self.x;
self.lastY = self.y;
// Smooth movement toward target position
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var isMoving = Math.abs(dx) > 1 || Math.abs(dy) > 1;
if (isMoving) {
self.x += dx * 0.15;
self.y += dy * 0.15;
// Play engine sound when moving
if (!self.engineSoundPlaying) {
self.engineSound = LK.getSound('engine');
self.engineSound.play();
self.engineSoundPlaying = true;
}
} else {
// Stop engine sound when not moving
if (self.engineSoundPlaying) {
if (self.engineSound) {
self.engineSound.stop();
}
self.engineSoundPlaying = false;
}
}
// Calculate angle to center and rotate spaceship to point toward it
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var angleToCenter = Math.atan2(centerY - self.y, centerX - self.x);
var targetRotation = angleToCenter - Math.PI / 2; // Point toward center
// Smooth rotation toward target angle using tween
tween(playerGraphics, {
rotation: targetRotation
}, {
duration: 100,
easing: tween.easeOut
});
// Create fire particles when moving
if (isMoving) {
var particle = new FireParticle();
// Always position particle from the left side of player (consistent side)
var leftSideOffsetX = -35; // Fixed offset to the left
var leftSideOffsetY = 0;
particle.x = self.x + leftSideOffsetX;
particle.y = self.y + leftSideOffsetY;
// Add velocity toward the left side (consistent direction)
particle.velocityX = -2 - Math.random() * 2;
particle.velocityY = 0;
// Add some random spread
particle.velocityX += (Math.random() - 0.5) * 2;
particle.velocityY += (Math.random() - 0.5) * 2;
self.fireParticles.push(particle);
game.addChild(particle);
}
// Clean up dead particles
for (var i = self.fireParticles.length - 1; i >= 0; i--) {
if (self.fireParticles[i].life <= 0) {
self.fireParticles.splice(i, 1);
}
}
// Update shooting cooldown
if (self.shootCooldown > 0) {
self.shootCooldown--;
}
// Update triple shot timer
if (self.tripleShotTimer > 0) {
self.tripleShotTimer--;
if (self.tripleShotTimer <= 0) {
self.hasTripleShot = false;
}
}
// Clean up bullets that are destroyed
for (var i = self.bullets.length - 1; i >= 0; i--) {
var bullet = self.bullets[i];
if (bullet.destroyed || bullet.x < -50 || bullet.x > 2098 || bullet.y < -50 || bullet.y > 2782) {
self.bullets.splice(i, 1);
}
}
};
self.moveTo = function (x, y) {
// Keep player within central area bounds
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var maxDistance = 400;
// Always constrain player to move only on the circular path at maxDistance
var angle = Math.atan2(y - centerY, x - centerX);
self.targetX = centerX + Math.cos(angle) * maxDistance;
self.targetY = centerY + Math.sin(angle) * maxDistance;
};
self.shoot = function () {
if (self.shootCooldown <= 0) {
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var dx = self.x - centerX; // Reversed direction
var dy = self.y - centerY; // Reversed direction
var distance = Math.sqrt(dx * dx + dy * dy);
if (self.hasTripleShot) {
// Fire three bullets with spread
var angles = [-0.3, 0, 0.3]; // 30 degree spread
var baseAngle = Math.atan2(dy, dx);
for (var i = 0; i < angles.length; i++) {
var bullet = new Bullet();
bullet.x = self.x;
bullet.y = self.y;
var angle = baseAngle + angles[i];
bullet.velocityX = Math.cos(angle) * bullet.speed;
bullet.velocityY = Math.sin(angle) * bullet.speed;
self.bullets.push(bullet);
game.addChild(bullet);
}
} else {
// Normal single bullet
var bullet = new Bullet();
bullet.x = self.x;
bullet.y = self.y;
bullet.velocityX = dx / distance * bullet.speed;
bullet.velocityY = dy / distance * bullet.speed;
self.bullets.push(bullet);
game.addChild(bullet);
}
LK.getSound('shoot').play();
self.shootCooldown = 15; // Cooldown between shots
}
};
return self;
});
var Star = Container.expand(function () {
var self = Container.call(this);
var starGraphics = self.attachAsset('star', {
anchorX: 0.5,
anchorY: 0.5
});
// Random star properties
self.twinkleSpeed = 0.02 + Math.random() * 0.03;
self.twinklePhase = Math.random() * Math.PI * 2;
self.baseAlpha = 0.5 + Math.random() * 0.5;
self.isFlashing = false;
self.flashTimer = 0;
self.nextFlashTime = Math.random() * 300 + 120; // Random time between 2-7 seconds
self.updateScale = function () {
// Calculate distance from center of screen
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var dx = self.x - centerX;
var dy = self.y - centerY;
var distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
// Maximum possible distance from center to corner
var maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
// Scale factor: 3.0 at center, 8.0 at edges (much larger stars)
var normalizedDistance = distanceFromCenter / maxDistance;
var scale = 3.0 + normalizedDistance * 5.0;
starGraphics.scaleX = scale;
starGraphics.scaleY = scale;
};
self.update = function () {
// Orbit around center matching player's movement direction and speed
var centerX = 2048 / 2;
var centerY = 2732 / 2;
// Calculate player's movement direction
var playerDx = player.x - player.lastX;
var playerDy = player.y - player.lastY;
var playerMovement = Math.sqrt(playerDx * playerDx + playerDy * playerDy);
// Determine rotation direction based on player's orbital movement
var playerAngle = Math.atan2(player.y - centerY, player.x - centerX);
var playerLastAngle = Math.atan2(player.lastY - centerY, player.lastX - centerX);
var angleDiff = playerAngle - playerLastAngle;
// Handle angle wrapping around π/-π boundary
if (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
if (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
// Only rotate stars when player is actually moving
var rotationSpeed = 0;
if (playerMovement > 0.5) {
// Player is moving
rotationSpeed = angleDiff; // Match player's angular velocity
}
// Calculate current angle and distance from center
var dx = self.x - centerX;
var dy = self.y - centerY;
var currentDistance = Math.sqrt(dx * dx + dy * dy);
var currentAngle = Math.atan2(dy, dx);
// Rotate around center matching player's rotation
currentAngle += rotationSpeed;
// Update position maintaining same distance from center
self.x = centerX + Math.cos(currentAngle) * currentDistance;
self.y = centerY + Math.sin(currentAngle) * currentDistance;
// Normal twinkling effect
if (!self.isFlashing) {
self.twinklePhase += self.twinkleSpeed;
starGraphics.alpha = self.baseAlpha + Math.sin(self.twinklePhase) * 0.3;
starGraphics.tint = 0xffffff;
// Check if it's time for a colored flash
self.flashTimer++;
if (self.flashTimer >= self.nextFlashTime) {
self.isFlashing = true;
self.flashTimer = 0;
// Randomly choose between yellow and blue flash
var flashColor = Math.random() < 0.5 ? 0xffff00 : 0x00aaff;
// Start colored flash tween
tween(starGraphics, {
tint: flashColor
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Fade back to white
tween(starGraphics, {
tint: 0xffffff
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
self.isFlashing = false;
self.nextFlashTime = Math.random() * 300 + 120; // Next flash in 2-7 seconds
}
});
}
});
}
} else {
// During flash, maintain bright alpha
starGraphics.alpha = 1.0;
}
// Update scale based on distance from center
self.updateScale();
};
return self;
});
var TripleShotPowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('tripleShotPowerUp', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2,
tint: 0xffa500
});
// Rotation and pulsing effect
self.rotationSpeed = 0.06;
self.pulsePhase = 0;
self.pulseSpeed = 0.08;
self.baseScale = 1.0;
self.lifetime = 600; // 10 seconds at 60fps
self.age = 0;
self.update = function () {
// Rotate the power-up
powerUpGraphics.rotation += self.rotationSpeed;
// Pulsing scale effect
self.pulsePhase += self.pulseSpeed;
var pulseFactor = 1 + Math.sin(self.pulsePhase) * 0.3;
powerUpGraphics.scaleX = self.baseScale * pulseFactor;
powerUpGraphics.scaleY = self.baseScale * pulseFactor;
// Cycle through orange and yellow tints
var tintPhase = Math.sin(self.pulsePhase * 0.7) * 0.5 + 0.5;
var r = 255;
var g = Math.floor(160 + tintPhase * 95);
var b = 0;
powerUpGraphics.tint = r << 16 | g << 8 | b;
// Age and fade out near end of lifetime
self.age++;
if (self.age > self.lifetime * 0.7) {
var fadeProgress = (self.age - self.lifetime * 0.7) / (self.lifetime * 0.3);
powerUpGraphics.alpha = 1 - fadeProgress;
}
// Remove when lifetime expires
if (self.age >= self.lifetime) {
self.destroy();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a2e
});
/****
* Game Code
****/
var player;
var obstacles = [];
var bullets = [];
var stars = [];
var powerUps = [];
var gameStarted = false;
var spawnTimer = 0;
var spawnRate = 120; // Initial spawn rate (ticks between spawns)
var difficultyTimer = 0;
var baseObstacleSpeed = 2;
var currentLives = storage.lives || 3;
var maxLives = storage.maxLives || 3;
var invulnerabilityTimer = 0;
var invulnerabilityDuration = 120; // 2 seconds at 60fps
var powerUpSpawnTimer = 0;
var powerUpSpawnRate = 1800; // Spawn power-up every 30 seconds at 60fps
var timeScoreTimer = 0;
var timeScoreRate = 60; // Award points every second (60 ticks)
var immunityTimer = 0;
var immunityDuration = 600; // 10 seconds at 60fps
var isImmune = false;
var currentLevel = 1;
var levelTimer = 0;
var levelDuration = 1800; // 30 seconds per level at 60fps
var baseSpawnRate = 120;
var minSpawnRate = 20;
var bosses = [];
var bossBullets = [];
var bossSpawnTimer = 0;
var bossSpawnRate = 3600; // Spawn boss every 60 seconds
var bossActive = false;
// Create starfield background
function createStarfield() {
for (var i = 0; i < 300; i++) {
// Double the number of stars for more expansive field
var star = new Star();
// Expand star generation area beyond screen bounds for wider coverage
var expandedWidth = 2048 * 1.8; // 80% larger width
var expandedHeight = 2732 * 1.8; // 80% larger height
var offsetX = (expandedWidth - 2048) / 2; // Center the expanded area
var offsetY = (expandedHeight - 2732) / 2;
star.x = -offsetX + Math.random() * expandedWidth;
star.y = -offsetY + Math.random() * expandedHeight;
// Apply initial scaling based on position
star.updateScale();
stars.push(star);
game.addChild(star);
}
}
// Initialize starfield
createStarfield();
// Create center moon indicator
var centerIndicator = LK.getAsset('moon', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2,
alpha: 0.8
});
centerIndicator.x = 2048 / 2;
centerIndicator.y = 2732 / 2;
game.addChild(centerIndicator);
// Start continuous moon rotation
tween(centerIndicator, {
rotation: Math.PI * 2
}, {
duration: 8000,
easing: tween.linear,
onFinish: function onFinish() {
// Reset rotation and start again for infinite loop
centerIndicator.rotation = 0;
tween(centerIndicator, {
rotation: Math.PI * 2
}, {
duration: 8000,
easing: tween.linear,
onFinish: arguments.callee // Reference to this same function for infinite loop
});
}
});
// Create sparkle effects container for moon impact glimmers
var moonSparkles = [];
function createMoonSparkle(impactX, impactY) {
// Random initial size for sparkles - vary between small, medium and large
var initialSize = 4 + Math.random() * 12; // Range from 4 to 16
// Random sparkle color - white, yellow, orange, or red
var sparkleColors = [0xffffff, 0xffff00, 0xff8800, 0xff0000];
var randomColor = sparkleColors[Math.floor(Math.random() * sparkleColors.length)];
var sparkle = LK.getAsset('star', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: initialSize,
scaleY: initialSize,
alpha: 0,
tint: randomColor
});
// Position sparkle at the impact location on moon's surface
sparkle.x = impactX;
sparkle.y = impactY;
game.addChild(sparkle);
moonSparkles.push(sparkle);
// Calculate target sizes - make them grow to 1.5-2x their initial size
var maxSize = initialSize * (1.5 + Math.random() * 0.5);
var endSize = initialSize * 0.3;
// Sparkle animation - fade in, scale up, then fade out (much faster)
tween(sparkle, {
alpha: 1,
scaleX: maxSize,
scaleY: maxSize
}, {
duration: 120,
// Much faster - was 300ms, now 120ms
easing: tween.easeOut,
onFinish: function onFinish() {
tween(sparkle, {
alpha: 0,
scaleX: endSize,
scaleY: endSize
}, {
duration: 180,
// Much faster - was 400ms, now 180ms
easing: tween.easeIn,
onFinish: function onFinish() {
sparkle.destroy();
// Remove from array
for (var i = moonSparkles.length - 1; i >= 0; i--) {
if (moonSparkles[i] === sparkle) {
moonSparkles.splice(i, 1);
break;
}
}
}
});
}
});
}
// Create player
player = new Player();
player.x = 2048 / 2;
player.y = 2732 / 2;
game.addChild(player);
// Score display
var scoreTxt = new Text2('0', {
size: 80,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// High score display
var highScore = storage.highScore || 0;
var highScoreTxt = new Text2('Best: ' + highScore, {
size: 60,
fill: 0xFFD700
});
highScoreTxt.anchor.set(1, 0);
highScoreTxt.y = 0;
LK.gui.topRight.addChild(highScoreTxt);
// Level display (below score)
var levelTxt = new Text2('Level 1', {
size: 60,
fill: 0x00FF88
});
levelTxt.anchor.set(0.5, 0);
levelTxt.x = 0;
levelTxt.y = 100;
LK.gui.top.addChild(levelTxt);
// Level record display (below high score)
var highestLevel = storage.highestLevel || 1;
var levelRecordTxt = new Text2('Best Level: ' + highestLevel, {
size: 60,
fill: 0x00FF88
});
levelRecordTxt.anchor.set(1, 0);
levelRecordTxt.x = 0;
levelRecordTxt.y = 80;
LK.gui.topRight.addChild(levelRecordTxt);
// Lives display with player icons
var livesContainer = new Container();
var lifeIcons = [];
function updateLivesDisplay() {
// Clear existing icons and text
for (var i = 0; i < lifeIcons.length; i++) {
lifeIcons[i].destroy();
}
lifeIcons = [];
if (livesContainer.livesText) {
livesContainer.livesText.destroy();
}
// Create single life icon using heart asset
var lifeIcon = LK.getAsset('heartIcon', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2,
tint: 0xff4444
});
lifeIcon.x = 40; // Position icon from left edge
lifeIcon.y = 0;
lifeIcons.push(lifeIcon);
livesContainer.addChild(lifeIcon);
// Create lives number text
var livesText = new Text2('x' + currentLives, {
size: 60,
fill: 0xFFFFFF
});
livesText.anchor.set(0, 0.5);
livesText.x = 80; // Position text next to icon
livesText.y = 0;
livesContainer.livesText = livesText;
livesContainer.addChild(livesText);
}
livesContainer.x = 0;
livesContainer.y = 0;
LK.gui.bottomLeft.addChild(livesContainer);
updateLivesDisplay();
// Create shoot button in bottom right
var shootButtonContainer = new Container();
var shootButton = LK.getAsset('shootButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 4,
scaleY: 4,
alpha: 0.8
});
shootButtonContainer.addChild(shootButton);
shootButtonContainer.x = -120;
shootButtonContainer.y = -120;
LK.gui.bottomRight.addChild(shootButtonContainer);
// Add shoot button touch handler
shootButtonContainer.down = function (x, y, obj) {
if (gameStarted) {
player.shoot();
// Visual feedback - briefly scale and flash the button
tween(shootButton, {
scaleX: 5,
scaleY: 5,
alpha: 1.0
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(shootButton, {
scaleX: 4,
scaleY: 4,
alpha: 0.8
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
// Game instructions
var instructionTxt = new Text2('Touch to move around the center', {
size: 60,
fill: 0xCCCCCC
});
instructionTxt.anchor.set(0.5, 0.5);
instructionTxt.x = 2048 / 2;
instructionTxt.y = 2732 / 2 + 80;
game.addChild(instructionTxt);
function updateLives(newLives) {
currentLives = Math.max(0, newLives); // Remove max lives constraint
storage.lives = currentLives;
updateLivesDisplay();
if (currentLives <= 0) {
LK.showGameOver();
}
}
function spawnObstacle() {
var obstacle = new Obstacle();
// Random spawn position along screen edges
var edge = Math.floor(Math.random() * 4);
switch (edge) {
case 0:
// Top edge
obstacle.x = Math.random() * 2048;
obstacle.y = -50;
break;
case 1:
// Right edge
obstacle.x = 2048 + 50;
obstacle.y = Math.random() * 2732;
break;
case 2:
// Bottom edge
obstacle.x = Math.random() * 2048;
obstacle.y = 2732 + 50;
break;
case 3:
// Left edge
obstacle.x = -50;
obstacle.y = Math.random() * 2732;
break;
}
// Add random size variation to obstacles
var randomScale = 0.5 + Math.random() * 1.5; // Random scale between 0.5x and 2x
obstacle.scaleX = randomScale;
obstacle.scaleY = randomScale;
// Set speed based on current level difficulty
obstacle.speed = baseObstacleSpeed + Math.random() * 1; // Add slight random variation
obstacles.push(obstacle);
game.addChild(obstacle);
}
function spawnPowerUp() {
// Always spawn power-ups (no maximum lives check)
// Randomly choose between life, immunity, and triple shot power-ups (33% each)
var randomValue = Math.random();
var powerUp;
if (randomValue < 0.33) {
powerUp = new LifePowerUp();
} else if (randomValue < 0.66) {
powerUp = new ImmunityPowerUp();
} else {
powerUp = new TripleShotPowerUp();
}
// Spawn in a safe area around the player's orbit
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var angle = Math.random() * Math.PI * 2;
var distance = 300 + Math.random() * 200; // Between player orbit and center
powerUp.x = centerX + Math.cos(angle) * distance;
powerUp.y = centerY + Math.sin(angle) * distance;
powerUps.push(powerUp);
game.addChild(powerUp);
}
function spawnBoss() {
if (!bossActive) {
var boss = new Boss();
// Start boss at a random position in its orbit
boss.angle = Math.random() * Math.PI * 2;
boss.x = boss.centerX + Math.cos(boss.angle) * boss.orbitDistance;
boss.y = boss.centerY + Math.sin(boss.angle) * boss.orbitDistance;
bosses.push(boss);
game.addChild(boss);
bossActive = true;
// Flash screen orange to indicate boss spawn
LK.effects.flashScreen(0xff4400, 1000);
}
}
function checkCollisions() {
// Check power-up collisions
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
// Initialize collision tracking if not present
if (powerUp.lastIntersecting === undefined) {
powerUp.lastIntersecting = false;
}
var currentIntersecting = player.intersects(powerUp);
// Check if collection just started (transition from false to true)
if (!powerUp.lastIntersecting && currentIntersecting) {
// Check if it's a life power-up or immunity power-up
if (powerUp instanceof LifePowerUp) {
// Always add life (no maximum limit)
// Play power-up sound when gaining a life
LK.getSound('lifeup').play();
updateLives(currentLives + 1);
LK.effects.flashScreen(0x00ff00, 300); // Green flash for power-up
} else if (powerUp instanceof ImmunityPowerUp) {
// Activate immunity
isImmune = true;
immunityTimer = immunityDuration;
// Play power-up sound
LK.getSound('powerup').play();
LK.effects.flashScreen(0x00ffff, 500); // Cyan flash for immunity
// Add glowing effect to player during immunity
tween(player, {
tint: 0x00ffff
}, {
duration: 200,
easing: tween.easeOut
});
} else if (powerUp instanceof TripleShotPowerUp) {
// Activate triple shot
player.hasTripleShot = true;
player.tripleShotTimer = player.tripleShotDuration;
// Play power-up sound
LK.getSound('powerup').play();
LK.effects.flashScreen(0xffa500, 500); // Orange flash for triple shot
// Add orange glow effect to player during triple shot
tween(player, {
tint: 0xffa500
}, {
duration: 200,
easing: tween.easeOut
});
}
// Create collection effect with tween
tween(powerUp, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
powerUp.destroy();
}
});
powerUps.splice(i, 1);
continue; // Skip further checks for this power-up
}
// Update last intersecting state
powerUp.lastIntersecting = currentIntersecting;
}
// Clean up expired power-ups
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
if (powerUp.age >= powerUp.lifetime) {
powerUps.splice(i, 1);
}
}
// Check boss-obstacle collisions (bosses are immune to obstacles)
for (var i = obstacles.length - 1; i >= 0; i--) {
var obstacle = obstacles[i];
for (var j = bosses.length - 1; j >= 0; j--) {
var boss = bosses[j];
if (obstacle.intersects(boss)) {
// Boss destroys obstacle without taking damage
obstacle.destroy();
obstacles.splice(i, 1);
break;
}
}
}
// Only check collisions if not invulnerable and not immune
if (invulnerabilityTimer <= 0 && !isImmune) {
for (var i = obstacles.length - 1; i >= 0; i--) {
var obstacle = obstacles[i];
// Initialize collision tracking if not present
if (obstacle.lastIntersecting === undefined) {
obstacle.lastIntersecting = false;
}
// Create smaller collision area (75% of obstacle size) for more forgiving gameplay
var collisionScale = 0.75;
var originalScaleX = obstacle.scaleX;
var originalScaleY = obstacle.scaleY;
// Temporarily reduce obstacle scale for collision detection
obstacle.scaleX = originalScaleX * collisionScale;
obstacle.scaleY = originalScaleY * collisionScale;
var currentIntersecting = player.intersects(obstacle);
// Restore original scale
obstacle.scaleX = originalScaleX;
obstacle.scaleY = originalScaleY;
// Check if collision just started (transition from false to true)
if (!obstacle.lastIntersecting && currentIntersecting) {
if (isImmune) {
// When immune, destroy obstacle and gain points
LK.setScore(LK.getScore() + 10);
scoreTxt.setText(LK.getScore());
// Update high score if current score is higher
if (LK.getScore() > highScore) {
highScore = LK.getScore();
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
// Create destruction effect
tween(obstacle, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
obstacle.destroy();
}
});
obstacles.splice(i, 1);
} else {
// Normal collision behavior when not immune
// Play collision sound immediately when collision starts
LK.getSound('collision').play();
// Also play hit sound for physical collision
LK.getSound('hit').play();
LK.effects.flashScreen(0xff0000, 500);
// Reduce life instead of immediate game over
updateLives(currentLives - 1);
// Set invulnerability period
invulnerabilityTimer = invulnerabilityDuration;
// Remove the obstacle that hit the player
obstacle.destroy();
obstacles.splice(i, 1);
}
break; // Exit loop after first collision
}
// Update last intersecting state
obstacle.lastIntersecting = currentIntersecting;
}
}
// Check player bullet-boss collisions
for (var i = bosses.length - 1; i >= 0; i--) {
var boss = bosses[i];
for (var j = player.bullets.length - 1; j >= 0; j--) {
var bullet = player.bullets[j];
if (bullet.intersects(boss)) {
// Play hit sound when bullet hits boss
LK.getSound('hit').play();
// Boss takes damage from player bullet
var bossDefeated = boss.takeDamage(10); // 10 damage per bullet
// Remove bullet
bullet.destroy();
player.bullets.splice(j, 1);
if (bossDefeated) {
// Award 500 points for defeating boss
LK.setScore(LK.getScore() + 500);
scoreTxt.setText(LK.getScore());
// Update high score if current score is higher
if (LK.getScore() > highScore) {
highScore = LK.getScore();
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
// Remove boss from array
bosses.splice(i, 1);
bossActive = false;
// Create victory effect
LK.effects.flashScreen(0x00ff00, 1000); // Green flash for boss defeat
}
break;
}
}
}
// Check boss bullet-player collisions (only if not immune and not invulnerable)
if (invulnerabilityTimer <= 0 && !isImmune) {
for (var i = bosses.length - 1; i >= 0; i--) {
var boss = bosses[i];
for (var j = boss.bullets.length - 1; j >= 0; j--) {
var bossBullet = boss.bullets[j];
if (bossBullet.lastIntersecting === undefined) {
bossBullet.lastIntersecting = false;
}
var currentIntersecting = player.intersects(bossBullet);
if (!bossBullet.lastIntersecting && currentIntersecting) {
// Play collision sound
LK.getSound('collision').play();
// Also play hit sound for physical collision
LK.getSound('hit').play();
LK.effects.flashScreen(0xff0000, 500);
// Reduce life
updateLives(currentLives - 1);
// Set invulnerability period
invulnerabilityTimer = invulnerabilityDuration;
// Remove boss bullet
bossBullet.destroy();
boss.bullets.splice(j, 1);
break;
}
bossBullet.lastIntersecting = currentIntersecting;
}
}
}
// Check bullet-obstacle collisions
for (var i = obstacles.length - 1; i >= 0; i--) {
var obstacle = obstacles[i];
var hitByBullet = false;
for (var j = player.bullets.length - 1; j >= 0; j--) {
var bullet = player.bullets[j];
// Create smaller collision area (75% of obstacle size) for bullet collisions
var collisionScale = 0.75;
var originalScaleX = obstacle.scaleX;
var originalScaleY = obstacle.scaleY;
// Temporarily reduce obstacle scale for collision detection
obstacle.scaleX = originalScaleX * collisionScale;
obstacle.scaleY = originalScaleY * collisionScale;
var bulletHit = bullet.intersects(obstacle);
// Restore original scale
obstacle.scaleX = originalScaleX;
obstacle.scaleY = originalScaleY;
if (bulletHit) {
// Play hit sound when bullet hits obstacle
LK.getSound('hit').play();
// Add 10 points for destroying obstacle
LK.setScore(LK.getScore() + 10);
scoreTxt.setText(LK.getScore());
// Update high score if current score is higher
if (LK.getScore() > highScore) {
highScore = LK.getScore();
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
// Create destruction effect
tween(obstacle, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
obstacle.destroy();
}
});
// Remove bullet
bullet.destroy();
player.bullets.splice(j, 1);
// Remove obstacle
obstacles.splice(i, 1);
hitByBullet = true;
break;
}
}
if (hitByBullet) continue;
}
// Bullets now pass through the moon without collision detection
// Clean up obstacles that reach close to the moon - make them disappear with effect
for (var i = obstacles.length - 1; i >= 0; i--) {
var obstacle = obstacles[i];
var distanceToCenter = Math.sqrt((obstacle.x - 2048 / 2) * (obstacle.x - 2048 / 2) + (obstacle.y - 2732 / 2) * (obstacle.y - 2732 / 2));
if (distanceToCenter < 80) {
// Moon collision radius
// Create impact sparkle at obstacle's current position
createMoonSparkle(obstacle.x, obstacle.y);
// Make obstacle disappear with fade effect when touching the moon
tween(obstacle, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
obstacle.destroy();
}
});
obstacles.splice(i, 1);
}
}
}
function updateDifficulty() {
difficultyTimer++;
levelTimer++;
// Level progression - advance level every 30 seconds
if (levelTimer >= levelDuration) {
currentLevel++;
levelTimer = 0;
levelTxt.setText('Level ' + currentLevel);
// Update level record if current level is higher
if (currentLevel > highestLevel) {
highestLevel = currentLevel;
storage.highestLevel = highestLevel;
levelRecordTxt.setText('Best Level: ' + highestLevel);
}
// Flash level up effect
LK.effects.flashScreen(0x00FF88, 800); // Green flash for level up
tween(levelTxt, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(levelTxt, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
// Progressive difficulty based on level
var levelMultiplier = 1 + (currentLevel - 1) * 0.3; // 30% increase per level
baseObstacleSpeed = 2 * levelMultiplier;
// Decrease spawn rate based on level (more frequent spawning)
spawnRate = Math.max(minSpawnRate, baseSpawnRate - (currentLevel - 1) * 8);
// Slightly increase power-up spawn rate at higher levels
powerUpSpawnRate = Math.max(900, 1800 - (currentLevel - 1) * 60); // Faster power-ups at higher levels
}
game.down = function (x, y, obj) {
if (!gameStarted) {
gameStarted = true;
instructionTxt.destroy();
// Start background music
LK.playMusic('backgroundMusic');
}
player.moveTo(x, y);
};
game.move = function (x, y, obj) {
if (gameStarted) {
player.moveTo(x, y);
}
};
game.update = function () {
if (!gameStarted) return;
// Update immunity timer
if (immunityTimer > 0) {
immunityTimer--;
// Make player glow cyan during immunity
var glowIntensity = Math.sin(LK.ticks * 0.3) * 0.5 + 0.5;
player.alpha = 0.7 + glowIntensity * 0.3;
// End immunity when timer expires
if (immunityTimer <= 0) {
isImmune = false;
// Only reset color if no other power-ups are active
if (!player.hasTripleShot) {
// Fade player back to normal color
tween(player, {
tint: 0xffffff,
alpha: 1.0
}, {
duration: 500,
easing: tween.easeOut
});
}
}
}
// Update triple shot visual effect
if (player.hasTripleShot && !isImmune) {
// Make player glow orange during triple shot
var glowIntensity = Math.sin(LK.ticks * 0.2) * 0.3 + 0.7;
player.alpha = glowIntensity;
// End triple shot when timer expires
if (player.tripleShotTimer <= 0) {
// Fade player back to normal color
tween(player, {
tint: 0xffffff,
alpha: 1.0
}, {
duration: 500,
easing: tween.easeOut
});
}
}
// Update invulnerability timer
if (invulnerabilityTimer > 0) {
invulnerabilityTimer--;
// Make player flash during invulnerability (only if not immune)
if (!isImmune) {
player.alpha = invulnerabilityTimer % 10 < 5 ? 0.5 : 1.0;
}
} else if (!isImmune) {
player.alpha = 1.0;
}
// Update score display (score is now updated in collision detection)
scoreTxt.setText(LK.getScore());
// Update high score if current score is higher
if (currentScore > highScore) {
highScore = currentScore;
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
// Spawn obstacles
spawnTimer++;
if (spawnTimer >= spawnRate) {
spawnObstacle();
spawnTimer = 0;
}
// Spawn power-ups
powerUpSpawnTimer++;
if (powerUpSpawnTimer >= powerUpSpawnRate) {
spawnPowerUp();
powerUpSpawnTimer = 0;
}
// Spawn bosses
bossSpawnTimer++;
if (bossSpawnTimer >= bossSpawnRate) {
spawnBoss();
bossSpawnTimer = 0;
}
// Update difficulty
updateDifficulty();
// Time-based scoring - award points for staying alive
timeScoreTimer++;
if (timeScoreTimer >= timeScoreRate) {
LK.setScore(LK.getScore() + 1);
timeScoreTimer = 0;
}
// Manual shooting only - removed auto-shoot
// Check for collisions
checkCollisions();
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
lives: 3,
maxLives: 3,
highestLevel: 1
});
/****
* Classes
****/
var Boss = Container.expand(function () {
var self = Container.call(this);
// Randomly select boss asset
var bossAssets = ['boss', 'boss2', 'boss3', 'boss4'];
var selectedAsset = bossAssets[Math.floor(Math.random() * bossAssets.length)];
var bossGraphics = self.attachAsset(selectedAsset, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.5,
scaleY: 2.5
});
self.health = 50;
self.maxHealth = 50;
self.shootTimer = 0;
// Shooting rate based on current level - faster at higher levels
self.shootRate = Math.max(30, 90 - (currentLevel - 1) * 5); // Gets faster each level
self.angle = 0;
self.orbitDistance = 800; // Boss orbits at much larger radius than player (was 600)
self.centerX = 2048 / 2;
self.centerY = 2732 / 2;
// Rotation speed based on current level - faster at higher levels
self.rotationSpeed = 0.02 + (currentLevel - 1) * 0.005;
self.bullets = [];
self.glowPhase = 0;
self.update = function () {
// Update level-based properties dynamically
self.shootRate = Math.max(30, 90 - (currentLevel - 1) * 5); // Gets faster each level
self.rotationSpeed = 0.02 + (currentLevel - 1) * 0.005; // Gets faster each level
// Orbital movement around center
self.angle += self.rotationSpeed;
self.x = self.centerX + Math.cos(self.angle) * self.orbitDistance;
self.y = self.centerY + Math.sin(self.angle) * self.orbitDistance;
// Visual effects - glow and rotation to face outward
self.glowPhase += 0.1;
var glowIntensity = Math.sin(self.glowPhase) * 0.3 + 0.7;
bossGraphics.alpha = glowIntensity;
// Rotate boss to face outward from center (head toward edge, feet toward center)
var angleToCenter = Math.atan2(self.centerY - self.y, self.centerX - self.x);
bossGraphics.rotation = angleToCenter + Math.PI; // Add PI to face away from center
// Remove color tinting to show original boss colors
// Shooting mechanics
self.shootTimer++;
if (self.shootTimer >= self.shootRate) {
self.shoot();
self.shootTimer = 0;
}
// Clean up destroyed bullets
for (var i = self.bullets.length - 1; i >= 0; i--) {
var bullet = self.bullets[i];
if (bullet.age >= bullet.lifetime) {
self.bullets.splice(i, 1);
}
}
};
self.shoot = function () {
// Create bullet that shoots toward center like player
var bullet = new BossBullet();
bullet.x = self.x;
bullet.y = self.y;
// Always shoot toward center (moon) like player does
var dx = self.centerX - self.x;
var dy = self.centerY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
bullet.velocityX = dx / distance * bullet.speed;
bullet.velocityY = dy / distance * bullet.speed;
self.bullets.push(bullet);
game.addChild(bullet);
LK.getSound('bossShoot').play();
};
self.takeDamage = function (damage) {
self.health -= damage;
// Flash effect when taking damage (scale only, no color change)
tween(bossGraphics, {
scaleX: 2.8,
scaleY: 2.8
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(bossGraphics, {
scaleX: 2.5,
scaleY: 2.5
}, {
duration: 100,
easing: tween.easeIn
});
}
});
if (self.health <= 0) {
self.destroy();
return true; // Boss defeated
}
return false;
};
return self;
});
var BossBullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('bossBullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.speed = 8; // Same speed as player bullets
self.velocityX = 0;
self.velocityY = 0;
self.centerX = 2048 / 2;
self.centerY = 2732 / 2;
self.update = function () {
// Move in straight line toward center like player bullets
self.x += self.velocityX;
self.y += self.velocityY;
// Rotate bullet to point toward center (tip toward center, tail toward edge)
bulletGraphics.rotation = Math.atan2(self.velocityY, self.velocityX);
// Remove bullet if it goes off screen or reaches center
var distanceToCenter = Math.sqrt((self.x - self.centerX) * (self.x - self.centerX) + (self.y - self.centerY) * (self.y - self.centerY));
if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782 || distanceToCenter < 80) {
self.destroy();
}
};
return self;
});
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('playerBullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 3
});
self.speed = 12;
self.velocityX = 0;
self.velocityY = 0;
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
// Rotate bullet to point away from center (tip toward edge, tail toward center)
bulletGraphics.rotation = Math.atan2(self.velocityY, self.velocityX);
// Remove bullet if it goes off screen
if (self.x < -50 || self.x > 2098 || self.y < -50 || self.y > 2782) {
self.destroy();
}
};
return self;
});
var FireParticle = Container.expand(function () {
var self = Container.call(this);
var particleGraphics = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.6,
scaleY: 0.6,
tint: 0xff4500
});
self.life = 1.0;
self.velocityX = 0;
self.velocityY = 0;
self.update = function () {
self.x += self.velocityX;
self.y += self.velocityY;
self.life -= 0.05;
particleGraphics.alpha = self.life;
particleGraphics.scaleX = self.life * 0.6;
particleGraphics.scaleY = self.life * 0.6;
if (self.life <= 0) {
self.destroy();
}
};
return self;
});
var ImmunityPowerUp = Container.expand(function () {
var self = Container.call(this);
var shieldGraphics = self.attachAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5,
tint: 0x00ffff
});
// Rotation and glow effect
self.rotationSpeed = 0.08;
self.glowPhase = 0;
self.glowSpeed = 0.12;
self.baseScale = 1.2;
self.lifetime = 600; // 10 seconds at 60fps
self.age = 0;
self.update = function () {
// Rotate the shield
shieldGraphics.rotation += self.rotationSpeed;
// Glowing effect
self.glowPhase += self.glowSpeed;
var glowFactor = 1 + Math.sin(self.glowPhase) * 0.3;
shieldGraphics.scaleX = self.baseScale * glowFactor;
shieldGraphics.scaleY = self.baseScale * glowFactor;
// Cycle through cyan and blue tints
var tintPhase = Math.sin(self.glowPhase * 0.5) * 0.5 + 0.5;
var r = 0;
var g = Math.floor(255 * (0.6 + tintPhase * 0.4));
var b = 255;
shieldGraphics.tint = r << 16 | g << 8 | b;
// Age and fade out near end of lifetime
self.age++;
if (self.age > self.lifetime * 0.7) {
var fadeProgress = (self.age - self.lifetime * 0.7) / (self.lifetime * 0.3);
shieldGraphics.alpha = 1 - fadeProgress;
}
// Remove when lifetime expires
if (self.age >= self.lifetime) {
self.destroy();
}
};
return self;
});
var LifePowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('heart', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2,
tint: 0xff1493
});
// Rotation and pulsing effect
self.rotationSpeed = 0.05;
self.pulsePhase = 0;
self.pulseSpeed = 0.1;
self.baseScale = 0.8;
self.lifetime = 600; // 10 seconds at 60fps
self.age = 0;
self.update = function () {
// Rotate the power-up
powerUpGraphics.rotation += self.rotationSpeed;
// Pulsing scale effect
self.pulsePhase += self.pulseSpeed;
var pulseFactor = 1 + Math.sin(self.pulsePhase) * 0.2;
powerUpGraphics.scaleX = self.baseScale * pulseFactor;
powerUpGraphics.scaleY = self.baseScale * pulseFactor;
// Age and fade out near end of lifetime
self.age++;
if (self.age > self.lifetime * 0.7) {
var fadeProgress = (self.age - self.lifetime * 0.7) / (self.lifetime * 0.3);
powerUpGraphics.alpha = 1 - fadeProgress;
}
// Remove when lifetime expires
if (self.age >= self.lifetime) {
self.destroy();
}
};
return self;
});
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var obstacleGraphics = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
self.speed = 2;
self.targetX = 2048 / 2;
self.targetY = 2732 / 2;
self.update = function () {
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Calculate scale based on distance from center, but only start scaling after passing player orbit
var playerOrbitDistance = 400; // Player orbit distance from center
var maxDistance = Math.sqrt(2048 / 2 * (2048 / 2) + 2732 / 2 * (2732 / 2)); // Max distance from center to corner
var minDistance = 80; // Stop before touching the moon (moon radius ~60px)
var scale;
if (distance > playerOrbitDistance) {
// Maintain original size until reaching player orbit
scale = 2.0; // Original obstacle scale
} else {
// Start scaling only after passing player orbit
var normalizedDistance = Math.max(0, Math.min(1, (distance - minDistance) / (playerOrbitDistance - minDistance)));
scale = 0.15 + normalizedDistance * 1.85; // Scale from 0.15 to 2.0 within player orbit
}
// Apply scale with tween for smooth transition
tween(obstacleGraphics, {
scaleX: scale,
scaleY: scale
}, {
duration: 100,
easing: tween.easeOut
});
// Always move toward center - obstacles will disappear before reaching moon
self.x += dx / distance * self.speed;
self.y += dy / distance * self.speed;
// Rotate obstacle continuously as it moves
obstacleGraphics.rotation += 0.1;
};
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
// Make it look more like a spaceship by rotating it to point upward
playerGraphics.rotation = -Math.PI * 1.5; // Rotated an additional 90 degrees
self.speed = 8;
self.targetX = self.x;
self.targetY = self.y;
self.lastX = self.x;
self.lastY = self.y;
self.fireParticles = [];
self.bullets = [];
self.shootCooldown = 0;
self.tripleShotTimer = 0;
self.tripleShotDuration = 900; // 15 seconds at 60fps
self.hasTripleShot = false;
self.engineSoundPlaying = false;
self.update = function () {
// Store last position for movement detection
self.lastX = self.x;
self.lastY = self.y;
// Smooth movement toward target position
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var isMoving = Math.abs(dx) > 1 || Math.abs(dy) > 1;
if (isMoving) {
self.x += dx * 0.15;
self.y += dy * 0.15;
// Play engine sound when moving
if (!self.engineSoundPlaying) {
self.engineSound = LK.getSound('engine');
self.engineSound.play();
self.engineSoundPlaying = true;
}
} else {
// Stop engine sound when not moving
if (self.engineSoundPlaying) {
if (self.engineSound) {
self.engineSound.stop();
}
self.engineSoundPlaying = false;
}
}
// Calculate angle to center and rotate spaceship to point toward it
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var angleToCenter = Math.atan2(centerY - self.y, centerX - self.x);
var targetRotation = angleToCenter - Math.PI / 2; // Point toward center
// Smooth rotation toward target angle using tween
tween(playerGraphics, {
rotation: targetRotation
}, {
duration: 100,
easing: tween.easeOut
});
// Create fire particles when moving
if (isMoving) {
var particle = new FireParticle();
// Always position particle from the left side of player (consistent side)
var leftSideOffsetX = -35; // Fixed offset to the left
var leftSideOffsetY = 0;
particle.x = self.x + leftSideOffsetX;
particle.y = self.y + leftSideOffsetY;
// Add velocity toward the left side (consistent direction)
particle.velocityX = -2 - Math.random() * 2;
particle.velocityY = 0;
// Add some random spread
particle.velocityX += (Math.random() - 0.5) * 2;
particle.velocityY += (Math.random() - 0.5) * 2;
self.fireParticles.push(particle);
game.addChild(particle);
}
// Clean up dead particles
for (var i = self.fireParticles.length - 1; i >= 0; i--) {
if (self.fireParticles[i].life <= 0) {
self.fireParticles.splice(i, 1);
}
}
// Update shooting cooldown
if (self.shootCooldown > 0) {
self.shootCooldown--;
}
// Update triple shot timer
if (self.tripleShotTimer > 0) {
self.tripleShotTimer--;
if (self.tripleShotTimer <= 0) {
self.hasTripleShot = false;
}
}
// Clean up bullets that are destroyed
for (var i = self.bullets.length - 1; i >= 0; i--) {
var bullet = self.bullets[i];
if (bullet.destroyed || bullet.x < -50 || bullet.x > 2098 || bullet.y < -50 || bullet.y > 2782) {
self.bullets.splice(i, 1);
}
}
};
self.moveTo = function (x, y) {
// Keep player within central area bounds
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var maxDistance = 400;
// Always constrain player to move only on the circular path at maxDistance
var angle = Math.atan2(y - centerY, x - centerX);
self.targetX = centerX + Math.cos(angle) * maxDistance;
self.targetY = centerY + Math.sin(angle) * maxDistance;
};
self.shoot = function () {
if (self.shootCooldown <= 0) {
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var dx = self.x - centerX; // Reversed direction
var dy = self.y - centerY; // Reversed direction
var distance = Math.sqrt(dx * dx + dy * dy);
if (self.hasTripleShot) {
// Fire three bullets with spread
var angles = [-0.3, 0, 0.3]; // 30 degree spread
var baseAngle = Math.atan2(dy, dx);
for (var i = 0; i < angles.length; i++) {
var bullet = new Bullet();
bullet.x = self.x;
bullet.y = self.y;
var angle = baseAngle + angles[i];
bullet.velocityX = Math.cos(angle) * bullet.speed;
bullet.velocityY = Math.sin(angle) * bullet.speed;
self.bullets.push(bullet);
game.addChild(bullet);
}
} else {
// Normal single bullet
var bullet = new Bullet();
bullet.x = self.x;
bullet.y = self.y;
bullet.velocityX = dx / distance * bullet.speed;
bullet.velocityY = dy / distance * bullet.speed;
self.bullets.push(bullet);
game.addChild(bullet);
}
LK.getSound('shoot').play();
self.shootCooldown = 15; // Cooldown between shots
}
};
return self;
});
var Star = Container.expand(function () {
var self = Container.call(this);
var starGraphics = self.attachAsset('star', {
anchorX: 0.5,
anchorY: 0.5
});
// Random star properties
self.twinkleSpeed = 0.02 + Math.random() * 0.03;
self.twinklePhase = Math.random() * Math.PI * 2;
self.baseAlpha = 0.5 + Math.random() * 0.5;
self.isFlashing = false;
self.flashTimer = 0;
self.nextFlashTime = Math.random() * 300 + 120; // Random time between 2-7 seconds
self.updateScale = function () {
// Calculate distance from center of screen
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var dx = self.x - centerX;
var dy = self.y - centerY;
var distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
// Maximum possible distance from center to corner
var maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
// Scale factor: 3.0 at center, 8.0 at edges (much larger stars)
var normalizedDistance = distanceFromCenter / maxDistance;
var scale = 3.0 + normalizedDistance * 5.0;
starGraphics.scaleX = scale;
starGraphics.scaleY = scale;
};
self.update = function () {
// Orbit around center matching player's movement direction and speed
var centerX = 2048 / 2;
var centerY = 2732 / 2;
// Calculate player's movement direction
var playerDx = player.x - player.lastX;
var playerDy = player.y - player.lastY;
var playerMovement = Math.sqrt(playerDx * playerDx + playerDy * playerDy);
// Determine rotation direction based on player's orbital movement
var playerAngle = Math.atan2(player.y - centerY, player.x - centerX);
var playerLastAngle = Math.atan2(player.lastY - centerY, player.lastX - centerX);
var angleDiff = playerAngle - playerLastAngle;
// Handle angle wrapping around π/-π boundary
if (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
if (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
// Only rotate stars when player is actually moving
var rotationSpeed = 0;
if (playerMovement > 0.5) {
// Player is moving
rotationSpeed = angleDiff; // Match player's angular velocity
}
// Calculate current angle and distance from center
var dx = self.x - centerX;
var dy = self.y - centerY;
var currentDistance = Math.sqrt(dx * dx + dy * dy);
var currentAngle = Math.atan2(dy, dx);
// Rotate around center matching player's rotation
currentAngle += rotationSpeed;
// Update position maintaining same distance from center
self.x = centerX + Math.cos(currentAngle) * currentDistance;
self.y = centerY + Math.sin(currentAngle) * currentDistance;
// Normal twinkling effect
if (!self.isFlashing) {
self.twinklePhase += self.twinkleSpeed;
starGraphics.alpha = self.baseAlpha + Math.sin(self.twinklePhase) * 0.3;
starGraphics.tint = 0xffffff;
// Check if it's time for a colored flash
self.flashTimer++;
if (self.flashTimer >= self.nextFlashTime) {
self.isFlashing = true;
self.flashTimer = 0;
// Randomly choose between yellow and blue flash
var flashColor = Math.random() < 0.5 ? 0xffff00 : 0x00aaff;
// Start colored flash tween
tween(starGraphics, {
tint: flashColor
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Fade back to white
tween(starGraphics, {
tint: 0xffffff
}, {
duration: 400,
easing: tween.easeIn,
onFinish: function onFinish() {
self.isFlashing = false;
self.nextFlashTime = Math.random() * 300 + 120; // Next flash in 2-7 seconds
}
});
}
});
}
} else {
// During flash, maintain bright alpha
starGraphics.alpha = 1.0;
}
// Update scale based on distance from center
self.updateScale();
};
return self;
});
var TripleShotPowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('tripleShotPowerUp', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2,
tint: 0xffa500
});
// Rotation and pulsing effect
self.rotationSpeed = 0.06;
self.pulsePhase = 0;
self.pulseSpeed = 0.08;
self.baseScale = 1.0;
self.lifetime = 600; // 10 seconds at 60fps
self.age = 0;
self.update = function () {
// Rotate the power-up
powerUpGraphics.rotation += self.rotationSpeed;
// Pulsing scale effect
self.pulsePhase += self.pulseSpeed;
var pulseFactor = 1 + Math.sin(self.pulsePhase) * 0.3;
powerUpGraphics.scaleX = self.baseScale * pulseFactor;
powerUpGraphics.scaleY = self.baseScale * pulseFactor;
// Cycle through orange and yellow tints
var tintPhase = Math.sin(self.pulsePhase * 0.7) * 0.5 + 0.5;
var r = 255;
var g = Math.floor(160 + tintPhase * 95);
var b = 0;
powerUpGraphics.tint = r << 16 | g << 8 | b;
// Age and fade out near end of lifetime
self.age++;
if (self.age > self.lifetime * 0.7) {
var fadeProgress = (self.age - self.lifetime * 0.7) / (self.lifetime * 0.3);
powerUpGraphics.alpha = 1 - fadeProgress;
}
// Remove when lifetime expires
if (self.age >= self.lifetime) {
self.destroy();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a2e
});
/****
* Game Code
****/
var player;
var obstacles = [];
var bullets = [];
var stars = [];
var powerUps = [];
var gameStarted = false;
var spawnTimer = 0;
var spawnRate = 120; // Initial spawn rate (ticks between spawns)
var difficultyTimer = 0;
var baseObstacleSpeed = 2;
var currentLives = storage.lives || 3;
var maxLives = storage.maxLives || 3;
var invulnerabilityTimer = 0;
var invulnerabilityDuration = 120; // 2 seconds at 60fps
var powerUpSpawnTimer = 0;
var powerUpSpawnRate = 1800; // Spawn power-up every 30 seconds at 60fps
var timeScoreTimer = 0;
var timeScoreRate = 60; // Award points every second (60 ticks)
var immunityTimer = 0;
var immunityDuration = 600; // 10 seconds at 60fps
var isImmune = false;
var currentLevel = 1;
var levelTimer = 0;
var levelDuration = 1800; // 30 seconds per level at 60fps
var baseSpawnRate = 120;
var minSpawnRate = 20;
var bosses = [];
var bossBullets = [];
var bossSpawnTimer = 0;
var bossSpawnRate = 3600; // Spawn boss every 60 seconds
var bossActive = false;
// Create starfield background
function createStarfield() {
for (var i = 0; i < 300; i++) {
// Double the number of stars for more expansive field
var star = new Star();
// Expand star generation area beyond screen bounds for wider coverage
var expandedWidth = 2048 * 1.8; // 80% larger width
var expandedHeight = 2732 * 1.8; // 80% larger height
var offsetX = (expandedWidth - 2048) / 2; // Center the expanded area
var offsetY = (expandedHeight - 2732) / 2;
star.x = -offsetX + Math.random() * expandedWidth;
star.y = -offsetY + Math.random() * expandedHeight;
// Apply initial scaling based on position
star.updateScale();
stars.push(star);
game.addChild(star);
}
}
// Initialize starfield
createStarfield();
// Create center moon indicator
var centerIndicator = LK.getAsset('moon', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2,
alpha: 0.8
});
centerIndicator.x = 2048 / 2;
centerIndicator.y = 2732 / 2;
game.addChild(centerIndicator);
// Start continuous moon rotation
tween(centerIndicator, {
rotation: Math.PI * 2
}, {
duration: 8000,
easing: tween.linear,
onFinish: function onFinish() {
// Reset rotation and start again for infinite loop
centerIndicator.rotation = 0;
tween(centerIndicator, {
rotation: Math.PI * 2
}, {
duration: 8000,
easing: tween.linear,
onFinish: arguments.callee // Reference to this same function for infinite loop
});
}
});
// Create sparkle effects container for moon impact glimmers
var moonSparkles = [];
function createMoonSparkle(impactX, impactY) {
// Random initial size for sparkles - vary between small, medium and large
var initialSize = 4 + Math.random() * 12; // Range from 4 to 16
// Random sparkle color - white, yellow, orange, or red
var sparkleColors = [0xffffff, 0xffff00, 0xff8800, 0xff0000];
var randomColor = sparkleColors[Math.floor(Math.random() * sparkleColors.length)];
var sparkle = LK.getAsset('star', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: initialSize,
scaleY: initialSize,
alpha: 0,
tint: randomColor
});
// Position sparkle at the impact location on moon's surface
sparkle.x = impactX;
sparkle.y = impactY;
game.addChild(sparkle);
moonSparkles.push(sparkle);
// Calculate target sizes - make them grow to 1.5-2x their initial size
var maxSize = initialSize * (1.5 + Math.random() * 0.5);
var endSize = initialSize * 0.3;
// Sparkle animation - fade in, scale up, then fade out (much faster)
tween(sparkle, {
alpha: 1,
scaleX: maxSize,
scaleY: maxSize
}, {
duration: 120,
// Much faster - was 300ms, now 120ms
easing: tween.easeOut,
onFinish: function onFinish() {
tween(sparkle, {
alpha: 0,
scaleX: endSize,
scaleY: endSize
}, {
duration: 180,
// Much faster - was 400ms, now 180ms
easing: tween.easeIn,
onFinish: function onFinish() {
sparkle.destroy();
// Remove from array
for (var i = moonSparkles.length - 1; i >= 0; i--) {
if (moonSparkles[i] === sparkle) {
moonSparkles.splice(i, 1);
break;
}
}
}
});
}
});
}
// Create player
player = new Player();
player.x = 2048 / 2;
player.y = 2732 / 2;
game.addChild(player);
// Score display
var scoreTxt = new Text2('0', {
size: 80,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// High score display
var highScore = storage.highScore || 0;
var highScoreTxt = new Text2('Best: ' + highScore, {
size: 60,
fill: 0xFFD700
});
highScoreTxt.anchor.set(1, 0);
highScoreTxt.y = 0;
LK.gui.topRight.addChild(highScoreTxt);
// Level display (below score)
var levelTxt = new Text2('Level 1', {
size: 60,
fill: 0x00FF88
});
levelTxt.anchor.set(0.5, 0);
levelTxt.x = 0;
levelTxt.y = 100;
LK.gui.top.addChild(levelTxt);
// Level record display (below high score)
var highestLevel = storage.highestLevel || 1;
var levelRecordTxt = new Text2('Best Level: ' + highestLevel, {
size: 60,
fill: 0x00FF88
});
levelRecordTxt.anchor.set(1, 0);
levelRecordTxt.x = 0;
levelRecordTxt.y = 80;
LK.gui.topRight.addChild(levelRecordTxt);
// Lives display with player icons
var livesContainer = new Container();
var lifeIcons = [];
function updateLivesDisplay() {
// Clear existing icons and text
for (var i = 0; i < lifeIcons.length; i++) {
lifeIcons[i].destroy();
}
lifeIcons = [];
if (livesContainer.livesText) {
livesContainer.livesText.destroy();
}
// Create single life icon using heart asset
var lifeIcon = LK.getAsset('heartIcon', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2,
tint: 0xff4444
});
lifeIcon.x = 40; // Position icon from left edge
lifeIcon.y = 0;
lifeIcons.push(lifeIcon);
livesContainer.addChild(lifeIcon);
// Create lives number text
var livesText = new Text2('x' + currentLives, {
size: 60,
fill: 0xFFFFFF
});
livesText.anchor.set(0, 0.5);
livesText.x = 80; // Position text next to icon
livesText.y = 0;
livesContainer.livesText = livesText;
livesContainer.addChild(livesText);
}
livesContainer.x = 0;
livesContainer.y = 0;
LK.gui.bottomLeft.addChild(livesContainer);
updateLivesDisplay();
// Create shoot button in bottom right
var shootButtonContainer = new Container();
var shootButton = LK.getAsset('shootButton', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 4,
scaleY: 4,
alpha: 0.8
});
shootButtonContainer.addChild(shootButton);
shootButtonContainer.x = -120;
shootButtonContainer.y = -120;
LK.gui.bottomRight.addChild(shootButtonContainer);
// Add shoot button touch handler
shootButtonContainer.down = function (x, y, obj) {
if (gameStarted) {
player.shoot();
// Visual feedback - briefly scale and flash the button
tween(shootButton, {
scaleX: 5,
scaleY: 5,
alpha: 1.0
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(shootButton, {
scaleX: 4,
scaleY: 4,
alpha: 0.8
}, {
duration: 100,
easing: tween.easeIn
});
}
});
}
};
// Game instructions
var instructionTxt = new Text2('Touch to move around the center', {
size: 60,
fill: 0xCCCCCC
});
instructionTxt.anchor.set(0.5, 0.5);
instructionTxt.x = 2048 / 2;
instructionTxt.y = 2732 / 2 + 80;
game.addChild(instructionTxt);
function updateLives(newLives) {
currentLives = Math.max(0, newLives); // Remove max lives constraint
storage.lives = currentLives;
updateLivesDisplay();
if (currentLives <= 0) {
LK.showGameOver();
}
}
function spawnObstacle() {
var obstacle = new Obstacle();
// Random spawn position along screen edges
var edge = Math.floor(Math.random() * 4);
switch (edge) {
case 0:
// Top edge
obstacle.x = Math.random() * 2048;
obstacle.y = -50;
break;
case 1:
// Right edge
obstacle.x = 2048 + 50;
obstacle.y = Math.random() * 2732;
break;
case 2:
// Bottom edge
obstacle.x = Math.random() * 2048;
obstacle.y = 2732 + 50;
break;
case 3:
// Left edge
obstacle.x = -50;
obstacle.y = Math.random() * 2732;
break;
}
// Add random size variation to obstacles
var randomScale = 0.5 + Math.random() * 1.5; // Random scale between 0.5x and 2x
obstacle.scaleX = randomScale;
obstacle.scaleY = randomScale;
// Set speed based on current level difficulty
obstacle.speed = baseObstacleSpeed + Math.random() * 1; // Add slight random variation
obstacles.push(obstacle);
game.addChild(obstacle);
}
function spawnPowerUp() {
// Always spawn power-ups (no maximum lives check)
// Randomly choose between life, immunity, and triple shot power-ups (33% each)
var randomValue = Math.random();
var powerUp;
if (randomValue < 0.33) {
powerUp = new LifePowerUp();
} else if (randomValue < 0.66) {
powerUp = new ImmunityPowerUp();
} else {
powerUp = new TripleShotPowerUp();
}
// Spawn in a safe area around the player's orbit
var centerX = 2048 / 2;
var centerY = 2732 / 2;
var angle = Math.random() * Math.PI * 2;
var distance = 300 + Math.random() * 200; // Between player orbit and center
powerUp.x = centerX + Math.cos(angle) * distance;
powerUp.y = centerY + Math.sin(angle) * distance;
powerUps.push(powerUp);
game.addChild(powerUp);
}
function spawnBoss() {
if (!bossActive) {
var boss = new Boss();
// Start boss at a random position in its orbit
boss.angle = Math.random() * Math.PI * 2;
boss.x = boss.centerX + Math.cos(boss.angle) * boss.orbitDistance;
boss.y = boss.centerY + Math.sin(boss.angle) * boss.orbitDistance;
bosses.push(boss);
game.addChild(boss);
bossActive = true;
// Flash screen orange to indicate boss spawn
LK.effects.flashScreen(0xff4400, 1000);
}
}
function checkCollisions() {
// Check power-up collisions
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
// Initialize collision tracking if not present
if (powerUp.lastIntersecting === undefined) {
powerUp.lastIntersecting = false;
}
var currentIntersecting = player.intersects(powerUp);
// Check if collection just started (transition from false to true)
if (!powerUp.lastIntersecting && currentIntersecting) {
// Check if it's a life power-up or immunity power-up
if (powerUp instanceof LifePowerUp) {
// Always add life (no maximum limit)
// Play power-up sound when gaining a life
LK.getSound('lifeup').play();
updateLives(currentLives + 1);
LK.effects.flashScreen(0x00ff00, 300); // Green flash for power-up
} else if (powerUp instanceof ImmunityPowerUp) {
// Activate immunity
isImmune = true;
immunityTimer = immunityDuration;
// Play power-up sound
LK.getSound('powerup').play();
LK.effects.flashScreen(0x00ffff, 500); // Cyan flash for immunity
// Add glowing effect to player during immunity
tween(player, {
tint: 0x00ffff
}, {
duration: 200,
easing: tween.easeOut
});
} else if (powerUp instanceof TripleShotPowerUp) {
// Activate triple shot
player.hasTripleShot = true;
player.tripleShotTimer = player.tripleShotDuration;
// Play power-up sound
LK.getSound('powerup').play();
LK.effects.flashScreen(0xffa500, 500); // Orange flash for triple shot
// Add orange glow effect to player during triple shot
tween(player, {
tint: 0xffa500
}, {
duration: 200,
easing: tween.easeOut
});
}
// Create collection effect with tween
tween(powerUp, {
scaleX: 2,
scaleY: 2,
alpha: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
powerUp.destroy();
}
});
powerUps.splice(i, 1);
continue; // Skip further checks for this power-up
}
// Update last intersecting state
powerUp.lastIntersecting = currentIntersecting;
}
// Clean up expired power-ups
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
if (powerUp.age >= powerUp.lifetime) {
powerUps.splice(i, 1);
}
}
// Check boss-obstacle collisions (bosses are immune to obstacles)
for (var i = obstacles.length - 1; i >= 0; i--) {
var obstacle = obstacles[i];
for (var j = bosses.length - 1; j >= 0; j--) {
var boss = bosses[j];
if (obstacle.intersects(boss)) {
// Boss destroys obstacle without taking damage
obstacle.destroy();
obstacles.splice(i, 1);
break;
}
}
}
// Only check collisions if not invulnerable and not immune
if (invulnerabilityTimer <= 0 && !isImmune) {
for (var i = obstacles.length - 1; i >= 0; i--) {
var obstacle = obstacles[i];
// Initialize collision tracking if not present
if (obstacle.lastIntersecting === undefined) {
obstacle.lastIntersecting = false;
}
// Create smaller collision area (75% of obstacle size) for more forgiving gameplay
var collisionScale = 0.75;
var originalScaleX = obstacle.scaleX;
var originalScaleY = obstacle.scaleY;
// Temporarily reduce obstacle scale for collision detection
obstacle.scaleX = originalScaleX * collisionScale;
obstacle.scaleY = originalScaleY * collisionScale;
var currentIntersecting = player.intersects(obstacle);
// Restore original scale
obstacle.scaleX = originalScaleX;
obstacle.scaleY = originalScaleY;
// Check if collision just started (transition from false to true)
if (!obstacle.lastIntersecting && currentIntersecting) {
if (isImmune) {
// When immune, destroy obstacle and gain points
LK.setScore(LK.getScore() + 10);
scoreTxt.setText(LK.getScore());
// Update high score if current score is higher
if (LK.getScore() > highScore) {
highScore = LK.getScore();
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
// Create destruction effect
tween(obstacle, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
obstacle.destroy();
}
});
obstacles.splice(i, 1);
} else {
// Normal collision behavior when not immune
// Play collision sound immediately when collision starts
LK.getSound('collision').play();
// Also play hit sound for physical collision
LK.getSound('hit').play();
LK.effects.flashScreen(0xff0000, 500);
// Reduce life instead of immediate game over
updateLives(currentLives - 1);
// Set invulnerability period
invulnerabilityTimer = invulnerabilityDuration;
// Remove the obstacle that hit the player
obstacle.destroy();
obstacles.splice(i, 1);
}
break; // Exit loop after first collision
}
// Update last intersecting state
obstacle.lastIntersecting = currentIntersecting;
}
}
// Check player bullet-boss collisions
for (var i = bosses.length - 1; i >= 0; i--) {
var boss = bosses[i];
for (var j = player.bullets.length - 1; j >= 0; j--) {
var bullet = player.bullets[j];
if (bullet.intersects(boss)) {
// Play hit sound when bullet hits boss
LK.getSound('hit').play();
// Boss takes damage from player bullet
var bossDefeated = boss.takeDamage(10); // 10 damage per bullet
// Remove bullet
bullet.destroy();
player.bullets.splice(j, 1);
if (bossDefeated) {
// Award 500 points for defeating boss
LK.setScore(LK.getScore() + 500);
scoreTxt.setText(LK.getScore());
// Update high score if current score is higher
if (LK.getScore() > highScore) {
highScore = LK.getScore();
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
// Remove boss from array
bosses.splice(i, 1);
bossActive = false;
// Create victory effect
LK.effects.flashScreen(0x00ff00, 1000); // Green flash for boss defeat
}
break;
}
}
}
// Check boss bullet-player collisions (only if not immune and not invulnerable)
if (invulnerabilityTimer <= 0 && !isImmune) {
for (var i = bosses.length - 1; i >= 0; i--) {
var boss = bosses[i];
for (var j = boss.bullets.length - 1; j >= 0; j--) {
var bossBullet = boss.bullets[j];
if (bossBullet.lastIntersecting === undefined) {
bossBullet.lastIntersecting = false;
}
var currentIntersecting = player.intersects(bossBullet);
if (!bossBullet.lastIntersecting && currentIntersecting) {
// Play collision sound
LK.getSound('collision').play();
// Also play hit sound for physical collision
LK.getSound('hit').play();
LK.effects.flashScreen(0xff0000, 500);
// Reduce life
updateLives(currentLives - 1);
// Set invulnerability period
invulnerabilityTimer = invulnerabilityDuration;
// Remove boss bullet
bossBullet.destroy();
boss.bullets.splice(j, 1);
break;
}
bossBullet.lastIntersecting = currentIntersecting;
}
}
}
// Check bullet-obstacle collisions
for (var i = obstacles.length - 1; i >= 0; i--) {
var obstacle = obstacles[i];
var hitByBullet = false;
for (var j = player.bullets.length - 1; j >= 0; j--) {
var bullet = player.bullets[j];
// Create smaller collision area (75% of obstacle size) for bullet collisions
var collisionScale = 0.75;
var originalScaleX = obstacle.scaleX;
var originalScaleY = obstacle.scaleY;
// Temporarily reduce obstacle scale for collision detection
obstacle.scaleX = originalScaleX * collisionScale;
obstacle.scaleY = originalScaleY * collisionScale;
var bulletHit = bullet.intersects(obstacle);
// Restore original scale
obstacle.scaleX = originalScaleX;
obstacle.scaleY = originalScaleY;
if (bulletHit) {
// Play hit sound when bullet hits obstacle
LK.getSound('hit').play();
// Add 10 points for destroying obstacle
LK.setScore(LK.getScore() + 10);
scoreTxt.setText(LK.getScore());
// Update high score if current score is higher
if (LK.getScore() > highScore) {
highScore = LK.getScore();
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
// Create destruction effect
tween(obstacle, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
obstacle.destroy();
}
});
// Remove bullet
bullet.destroy();
player.bullets.splice(j, 1);
// Remove obstacle
obstacles.splice(i, 1);
hitByBullet = true;
break;
}
}
if (hitByBullet) continue;
}
// Bullets now pass through the moon without collision detection
// Clean up obstacles that reach close to the moon - make them disappear with effect
for (var i = obstacles.length - 1; i >= 0; i--) {
var obstacle = obstacles[i];
var distanceToCenter = Math.sqrt((obstacle.x - 2048 / 2) * (obstacle.x - 2048 / 2) + (obstacle.y - 2732 / 2) * (obstacle.y - 2732 / 2));
if (distanceToCenter < 80) {
// Moon collision radius
// Create impact sparkle at obstacle's current position
createMoonSparkle(obstacle.x, obstacle.y);
// Make obstacle disappear with fade effect when touching the moon
tween(obstacle, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 200,
easing: tween.easeIn,
onFinish: function onFinish() {
obstacle.destroy();
}
});
obstacles.splice(i, 1);
}
}
}
function updateDifficulty() {
difficultyTimer++;
levelTimer++;
// Level progression - advance level every 30 seconds
if (levelTimer >= levelDuration) {
currentLevel++;
levelTimer = 0;
levelTxt.setText('Level ' + currentLevel);
// Update level record if current level is higher
if (currentLevel > highestLevel) {
highestLevel = currentLevel;
storage.highestLevel = highestLevel;
levelRecordTxt.setText('Best Level: ' + highestLevel);
}
// Flash level up effect
LK.effects.flashScreen(0x00FF88, 800); // Green flash for level up
tween(levelTxt, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(levelTxt, {
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200,
easing: tween.easeIn
});
}
});
}
// Progressive difficulty based on level
var levelMultiplier = 1 + (currentLevel - 1) * 0.3; // 30% increase per level
baseObstacleSpeed = 2 * levelMultiplier;
// Decrease spawn rate based on level (more frequent spawning)
spawnRate = Math.max(minSpawnRate, baseSpawnRate - (currentLevel - 1) * 8);
// Slightly increase power-up spawn rate at higher levels
powerUpSpawnRate = Math.max(900, 1800 - (currentLevel - 1) * 60); // Faster power-ups at higher levels
}
game.down = function (x, y, obj) {
if (!gameStarted) {
gameStarted = true;
instructionTxt.destroy();
// Start background music
LK.playMusic('backgroundMusic');
}
player.moveTo(x, y);
};
game.move = function (x, y, obj) {
if (gameStarted) {
player.moveTo(x, y);
}
};
game.update = function () {
if (!gameStarted) return;
// Update immunity timer
if (immunityTimer > 0) {
immunityTimer--;
// Make player glow cyan during immunity
var glowIntensity = Math.sin(LK.ticks * 0.3) * 0.5 + 0.5;
player.alpha = 0.7 + glowIntensity * 0.3;
// End immunity when timer expires
if (immunityTimer <= 0) {
isImmune = false;
// Only reset color if no other power-ups are active
if (!player.hasTripleShot) {
// Fade player back to normal color
tween(player, {
tint: 0xffffff,
alpha: 1.0
}, {
duration: 500,
easing: tween.easeOut
});
}
}
}
// Update triple shot visual effect
if (player.hasTripleShot && !isImmune) {
// Make player glow orange during triple shot
var glowIntensity = Math.sin(LK.ticks * 0.2) * 0.3 + 0.7;
player.alpha = glowIntensity;
// End triple shot when timer expires
if (player.tripleShotTimer <= 0) {
// Fade player back to normal color
tween(player, {
tint: 0xffffff,
alpha: 1.0
}, {
duration: 500,
easing: tween.easeOut
});
}
}
// Update invulnerability timer
if (invulnerabilityTimer > 0) {
invulnerabilityTimer--;
// Make player flash during invulnerability (only if not immune)
if (!isImmune) {
player.alpha = invulnerabilityTimer % 10 < 5 ? 0.5 : 1.0;
}
} else if (!isImmune) {
player.alpha = 1.0;
}
// Update score display (score is now updated in collision detection)
scoreTxt.setText(LK.getScore());
// Update high score if current score is higher
if (currentScore > highScore) {
highScore = currentScore;
storage.highScore = highScore;
highScoreTxt.setText('Best: ' + highScore);
}
// Spawn obstacles
spawnTimer++;
if (spawnTimer >= spawnRate) {
spawnObstacle();
spawnTimer = 0;
}
// Spawn power-ups
powerUpSpawnTimer++;
if (powerUpSpawnTimer >= powerUpSpawnRate) {
spawnPowerUp();
powerUpSpawnTimer = 0;
}
// Spawn bosses
bossSpawnTimer++;
if (bossSpawnTimer >= bossSpawnRate) {
spawnBoss();
bossSpawnTimer = 0;
}
// Update difficulty
updateDifficulty();
// Time-based scoring - award points for staying alive
timeScoreTimer++;
if (timeScoreTimer >= timeScoreRate) {
LK.setScore(LK.getScore() + 1);
timeScoreTimer = 0;
}
// Manual shooting only - removed auto-shoot
// Check for collisions
checkCollisions();
};
Botón rojo con dibujo de bala en el interior. In-Game asset. 2d. High contrast. No shadows
Planeta tierra. In-Game asset. 2d. High contrast. No shadows
Rodeado de un corazón rosa
Escudo azul. In-Game asset. 2d. High contrast. No shadows
Boss espacial. In-Game asset. 2d. High contrast. No shadows
Bala azul. In-Game asset. 2d. High contrast. No shadows
Amarillo