/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Enemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Use default hitbox
// Enemy properties
self.health = 1;
self.speed = 2;
self.scoreValue = 100;
self.fireRate = 120; // Ticks between shots
self.lastShot = Math.floor(Math.random() * 60); // Randomize initial shot timer
self.movementPattern = Math.floor(Math.random() * 3); // 0: straight, 1: zigzag, 2: circular
self.movementCounter = 0;
self.lastY = 0; // Track last Y position to detect reaching bottom
self.firingMode = false; // Track if enemy is in enhanced firing mode
self.firingModeStartTime = 0; // When enhanced firing mode started
// Dodging properties
self.canDodge = true;
self.lastDodgeTime = 0;
self.isDodging = false;
self.dodgeCooldown = 300; // 5 seconds at 60fps
self.dodgeDuration = 30; // Half a second dodge movement
self.dodgeCounter = 0;
self.dodgeDirection = 0; // Will be set when dodging
self.dodgeSpeed = 10; // Fast dodge movement
// Initial target position (will be set during movement)
self.targetX = null;
self.update = function () {
// Store last Y position for edge detection
if (self.lastY === undefined) {
self.lastY = self.y;
}
// Basic downward movement
self.y += self.speed;
// Check if enemy just reached bottom edge of game area
var bottomThreshold = GAME_HEIGHT - 200;
if (self.lastY < bottomThreshold && self.y >= bottomThreshold && !self.firingMode) {
// Enemy reached bottom edge - activate enhanced firing mode
self.firingMode = true;
self.firingModeStartTime = LK.ticks;
// Store original fire rate before enhancement
self.originalFireRate = self.fireRate;
// Increase fire rate and set player as target
self.fireRate = 30; // Much faster fire rate
// Flash red to indicate enhanced firing mode
LK.effects.flashObject(self, 0xff0000, 500);
// Start aiming at player
self.targetPlayer = true;
}
// Handle enhanced firing mode behavior
if (self.firingMode) {
// Track number of shots in enhanced mode
if (self.enhancedShotsCount === undefined) {
self.enhancedShotsCount = 0;
}
// Self-destruct after 10 shots instead of time-based
if (self.enhancedShotsCount >= 10) {
// Create explosion effect
var explosion = new Explosion();
explosion.x = self.x;
explosion.y = self.y;
game.addChild(explosion);
explosions.push(explosion);
// Play explosion sound
LK.getSound('explosion').play();
// Add score for self-destruction
score += self.scoreValue;
LK.setScore(score);
updateScoreDisplay();
// Remove the enemy
self.destroy();
return true; // Signal to remove from enemies array
}
// Aim at player during enhanced firing mode
if (player && self.targetPlayer) {
// Calculate direction to player
var diffX = player.x - self.x;
var diffY = player.y - self.y;
// Move toward player with increased precision
self.x += diffX * 0.02;
// Apply a pulsating movement for visual effect
self.y += Math.sin(LK.ticks * 0.1) * 1.5;
return;
}
}
// Apply regular movement pattern when not in enhanced firing mode
if (self.movementPattern === 1) {
// Zigzag with occasional side movement
self.x += Math.sin(self.movementCounter * 0.05) * 3;
// Occasionally move to a different horizontal position
if (self.movementCounter % 500 === 0) {
// Choose a new target position on the opposite side
self.targetX = self.x < GAME_WIDTH / 2 ? GAME_WIDTH * 0.7 + Math.random() * GAME_WIDTH * 0.2 : GAME_WIDTH * 0.1 + Math.random() * GAME_WIDTH * 0.2;
}
if (self.targetX) {
var diffX = self.targetX - self.x;
if (Math.abs(diffX) > 10) {
self.x += diffX * 0.03;
}
}
self.movementCounter++;
} else if (self.movementPattern === 2) {
// Circular with occasional repositioning
self.x += Math.sin(self.movementCounter * 0.03) * 4;
// Occasionally reset position to avoid grouping
if (self.movementCounter % 700 === 0) {
// Move to random position on screen
self.targetX = Math.random() * GAME_WIDTH * 0.8 + GAME_WIDTH * 0.1;
}
// Move toward target if exists
if (self.targetX) {
var diffX = self.targetX - self.x;
if (Math.abs(diffX) > 10) {
self.x += diffX * 0.02;
}
}
self.movementCounter++;
} else {
// Add movement for straight pattern (pattern 0)
if (self.movementCounter % 900 === 0) {
// Occasional side movement
self.targetX = Math.random() * GAME_WIDTH * 0.8 + GAME_WIDTH * 0.1;
}
// Move toward target if exists
if (self.targetX) {
var diffX = self.targetX - self.x;
if (Math.abs(diffX) > 10) {
self.x += diffX * 0.01;
}
}
self.movementCounter++;
}
// Check for nearby bullets and dodge if possible
if (self.canDodge && !self.isDodging && LK.ticks - self.lastDodgeTime >= self.dodgeCooldown) {
// Look for nearby bullets
for (var i = 0; i < playerBullets.length; i++) {
var bullet = playerBullets[i];
// Skip bullets not moving toward the enemy (horizontal bullets, or downward bullets)
if (bullet.direction !== 'up') continue;
// Calculate distance between bullet and enemy
var dx = bullet.x - self.x;
var dy = bullet.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If bullet is close but not too close, and is above the enemy (coming toward it)
if (distance < 150 && Math.abs(dx) < 100 && bullet.y < self.y && bullet.y > self.y - 300) {
// Start dodging!
self.isDodging = true;
self.dodgeCounter = 0;
// Determine dodge direction (away from bullet)
self.dodgeDirection = dx > 0 ? -1 : 1; // Move left if bullet is on right, right if bullet is on left
self.lastDodgeTime = LK.ticks;
// Flash enemy green to indicate dodge
LK.effects.flashObject(self, 0x00ff00, 300);
break;
}
}
}
// Handle active dodging movement
if (self.isDodging) {
self.dodgeCounter++;
// Move rapidly in dodge direction
self.x += self.dodgeDirection * self.dodgeSpeed;
// Constrain to screen bounds
if (self.x < 50) {
self.x = 50;
self.dodgeDirection = 1; // Reverse direction if hitting edge
} else if (self.x > GAME_WIDTH - 50) {
self.x = GAME_WIDTH - 50;
self.dodgeDirection = -1; // Reverse direction if hitting edge
}
// End dodge after duration
if (self.dodgeCounter >= self.dodgeDuration) {
self.isDodging = false;
}
}
// Update lastY for next frame
self.lastY = self.y;
};
// Take damage and check if destroyed
self.takeDamage = function (amount) {
self.health -= amount;
if (self.health <= 0) {
return true; // Enemy is destroyed
}
return false;
};
// Use default intersection method
return self;
});
var EnemyBullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('enemyBullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 7; // Moving downward
self.update = function () {
if (self.isAimed && self.speedX !== undefined && self.speedY !== undefined) {
// Move along calculated trajectory for aimed bullets
self.x += self.speedX;
self.y += self.speedY;
} else {
// Default downward movement
self.y += self.speed;
}
};
// Use default intersection method
return self;
});
var Explosion = Container.expand(function () {
var self = Container.call(this);
var explosionGraphics = self.attachAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Explosion duration
self.duration = 30; // frames
self.currentFrame = 0;
// Scale explosion from small to large then fade
explosionGraphics.scale.x = 0.1;
explosionGraphics.scale.y = 0.1;
self.update = function () {
self.currentFrame++;
// Expand and then fade
if (self.currentFrame < 15) {
explosionGraphics.scale.x += 0.1;
explosionGraphics.scale.y += 0.1;
} else {
explosionGraphics.alpha -= 0.08;
}
// Remove when animation complete
if (self.currentFrame >= self.duration) {
self.destroy();
return true;
}
return false;
};
return self;
});
var FirePool = Container.expand(function () {
var self = Container.call(this);
var fireGraphics = self.attachAsset('firePool', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.7
});
// Fire pool duration (4 seconds at 60fps)
self.duration = 240;
self.currentFrame = 0;
// Pulsate effect
self.pulsateDirection = 1;
self.pulsateAmount = 0;
self.update = function () {
self.currentFrame++;
// Pulsate effect
self.pulsateAmount += 0.02 * self.pulsateDirection;
if (self.pulsateAmount > 0.2 || self.pulsateAmount < -0.2) {
self.pulsateDirection *= -1;
}
fireGraphics.scale.x = 1 + self.pulsateAmount;
fireGraphics.scale.y = 1 + self.pulsateAmount;
// Fade out at the end
if (self.currentFrame > 150) {
fireGraphics.alpha -= 0.03;
}
// Check for enemies touching the fire pool
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.intersects(self)) {
// Enemy is instantly destroyed by fire
score += enemy.scoreValue;
LK.setScore(score);
updateScoreDisplay();
LK.effects.flashObject(enemy, 0xff0000, 300);
// Play explosion sound when enemy is destroyed by fire pool
LK.getSound('firePoolDestroy').play();
enemy.destroy();
enemies.splice(i, 1);
}
}
// Remove when duration complete
if (self.currentFrame >= self.duration) {
self.destroy();
return true;
}
return false;
};
return self;
});
var HealthBar = Container.expand(function () {
var self = Container.call(this);
// Health bar properties
self.maxHealth = 4;
self.currentHealth = 4;
self.width = 140;
self.height = 20;
// Create background bar (gray)
var background = self.attachAsset('healthbar', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
// Adjust to match bar width
scaleY: 1,
// Adjust to match bar height
tint: 0x666666 // Gray color
});
// Create foreground bar (health indicator - green)
var foreground = self.attachAsset('healthbar', {
anchorX: 0,
anchorY: 0.5,
scaleX: 1,
// Adjust to match bar width
scaleY: 1,
// Adjust to match bar height
tint: 0x00FF00 // Green color
});
// Center the foreground bar
foreground.x = -background.width / 2;
// Update health bar visual based on current health
self.updateHealth = function (health) {
self.currentHealth = health;
// Update the scale of the foreground bar based on current health
var healthPercentage = Math.max(self.currentHealth / self.maxHealth, 0);
// Scale should decrease as health decreases, not increase
// Ensure that we don't go to 0 until health is actually 0
if (self.currentHealth > 0) {
// For non-zero health, ensure we have at least a small visible portion of health bar
foreground.scale.x = Math.max(healthPercentage, 0.05);
} else {
// Only when health is truly zero, set scale to zero
foreground.scale.x = 0;
}
// Change color based on health level
if (healthPercentage > 0.6) {
foreground.tint = 0x00FF00; // Green
} else if (healthPercentage > 0.3) {
foreground.tint = 0xFFFF00; // Yellow
} else {
foreground.tint = 0xFF0000; // Red
}
};
return self;
});
var Missile = Container.expand(function () {
var self = Container.call(this);
var missileGraphics = self.attachAsset('missile', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.speed = -10; // Moving upward
self.targetX = null;
self.targetY = null; // Add targetY for better homing
self.turnSpeed = 0.25; // Significantly increased turn speed for better tracking
self.angle = 0; // For side missiles
self.update = function () {
// Basic upward movement
self.y += self.speed;
// Enhanced target tracking if we have a target
if (self.targetX && self.targetY) {
// Calculate distance to target
var diffX = self.targetX - self.x;
var diffY = self.targetY - self.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
// Track frames since launch for initial trajectory control
if (self.initialDirectionFrames === undefined) {
self.initialDirectionFrames = 0;
}
// For the first 20 frames, maintain initial trajectory with very minimal homing
// This ensures missiles start with distinctive paths
if (self.initialDirectionFrames < 20) {
self.initialDirectionFrames++;
// Initial direction based on missile type
if (self.initialDirection === 'left-up') {
// Left missile veers slightly left
self.x -= 2;
self.y += self.speed * 0.2; // Slow vertical movement slightly
} else if (self.initialDirection === 'right-up') {
// Right missile veers slightly right
self.x += 2;
self.y += self.speed * 0.2; // Slow vertical movement slightly
} else {
// Center missile goes straight up
// No horizontal movement, just straight up
}
} else {
// After initial frames, apply enhanced homing behavior
// Use a larger factor for more effective tracking while keeping realistic movement
var homingFactor = self.turnSpeed * 0.3; // Significantly increased factor for better homing
// Move more noticeably toward target x position
self.x += diffX * homingFactor;
// Also add vertical homing for more direct targeting
self.y += diffY * (homingFactor * 0.4); // Add vertical component to homing
// Apply slight angular adjustment to maintain separation
// This ensures missiles maintain some of their initial trajectory
if (self.angle) {
self.x += Math.sin(self.angle) * 0.2; // Further reduced angular influence for better homing
}
}
// Improved y-speed adjustment if target is far above or below
if (diffY < -150) {
// Increased speed adjustment for better vertical tracking
self.y += self.speed * 0.06; // More aggressive speed adjustment
} else if (diffY > 150) {
// Increased adjustment if target is below missile
self.y += self.speed * -0.04; // More noticeable slowdown
}
}
};
// Create explosion when hitting a target or edge
self.explode = function () {
var explosion = new Explosion();
explosion.x = self.x;
explosion.y = self.y;
game.addChild(explosion);
// Create fire pool after explosion
var firePool = new FirePool();
firePool.x = self.x;
firePool.y = self.y;
game.addChild(firePool);
firePools.push(firePool);
// Play explosion sound
LK.getSound('explosion').play();
// Damage enemies in area
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If enemy is within explosion radius
if (distance < 150) {
// Enemy is destroyed by area damage
score += enemy.scoreValue;
LK.setScore(score);
updateScoreDisplay();
LK.effects.flashObject(enemy, 0xff0000, 300);
enemy.destroy();
enemies.splice(i, 1);
}
}
return true;
};
return self;
});
var PlayerBullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('playerBullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = -20; // Moving upward (increased speed)
self.power = 1;
self.direction = 'up'; // up, down, left, right
self.isPiercing = false;
self.isBouncing = false;
self.bounceCount = 0;
self.maxBounces = 3;
// Enemy bullet destruction functionality removed
self.update = function () {
// Move bullet based on direction
if (self.direction === 'up') {
self.y += self.speed;
// Check for bouncing against top edge
if (self.isBouncing && self.y < 0 && self.bounceCount < self.maxBounces) {
self.direction = 'down';
self.bounceCount++;
}
} else if (self.direction === 'down') {
self.y -= self.speed; // Note: speed is negative, so this is adding a positive value
// Check for bouncing against bottom edge
if (self.isBouncing && self.y > GAME_HEIGHT && self.bounceCount < self.maxBounces) {
self.direction = 'up';
self.bounceCount++;
}
} else if (self.direction === 'left') {
self.x += self.speed;
// Check for bouncing against left edge
if (self.isBouncing && self.x < 0 && self.bounceCount < self.maxBounces) {
self.direction = 'right';
self.bounceCount++;
}
} else if (self.direction === 'right') {
self.x -= self.speed; // Note: speed is negative, so this is adding a positive value
// Check for bouncing against right edge
if (self.isBouncing && self.x > GAME_WIDTH && self.bounceCount < self.maxBounces) {
self.direction = 'left';
self.bounceCount++;
}
}
};
// Method to create explosion when player bullet is destroyed
self.explode = function () {
var explosion = new Explosion();
explosion.x = self.x;
explosion.y = self.y;
// Make player bullet explosion slightly smaller
explosion.scale.x = 0.6;
explosion.scale.y = 0.6;
game.addChild(explosion);
explosions.push(explosion);
// Play explosion sound
LK.getSound('explosion').play();
};
// Use default intersection method
return self;
});
var PlayerShip = Container.expand(function () {
var self = Container.call(this);
// Visual representation
var shipGraphics = self.attachAsset('playerShip', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
});
// Use default hitbox
// Health visual (initially invisible)
var shieldGraphics = self.attachAsset('health', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
scaleX: 1.2,
scaleY: 1.2
});
// Final life warning text
var finalLifeText = new Text2('FINAL LIFE', {
size: 50,
fill: 0xFF0000
});
finalLifeText.anchor.set(0.5, 0.5);
finalLifeText.y = -110;
finalLifeText.alpha = 0;
self.addChild(finalLifeText);
// Player properties
self.speed = 10;
self.fireRate = 20; // Ticks between shots
self.lastShot = 0;
self.health = 4; // Player starts with 4 health
self.healthBar = new HealthBar();
self.healthBar.y = -80; // Position above player
self.healthBar.x = 0; // Perfectly center the healthbar with the ship
self.addChild(self.healthBar);
self.shieldActive = false;
self.powerUpActive = false;
self.quadBulletsActive = false;
self.rotationSpeed = 0;
self.finalLifeWarningActive = false;
// Physics properties for smooth movement
self.targetX = null;
self.targetY = null;
self.isMoving = false;
self.moveTween = null;
// Update method for rotation and missile text positioning
self.update = function () {
if (self.quadBulletsActive) {
shipGraphics.rotation += self.rotationSpeed;
}
// Keep missile ready text below the ship
if (missileReadyText && missileReadyText.visible) {
missileReadyText.x = self.x;
missileReadyText.y = self.y + 110;
}
};
// Use default intersection method
// Shield activation
self.activateShield = function () {
self.shieldActive = true;
shieldGraphics.alpha = 0.5;
// Play shield activation sound effect
LK.getSound('shieldActivation').play();
// Shield times out after 5 seconds
LK.setTimeout(function () {
self.shieldActive = false;
shieldGraphics.alpha = 0;
}, 5000);
};
// Power-up activation
self.activatePowerUp = function () {
self.powerUpActive = true;
shipGraphics.tint = 0xf1c40f; // Yellow tint for power-up
self.fireRate = 10; // Faster firing
// Power-up times out after 7 seconds
LK.setTimeout(function () {
self.powerUpActive = false;
shipGraphics.tint = 0xFFFFFF;
self.fireRate = 20;
}, 7000);
};
// Quad bullets activation
self.activateQuadBullets = function () {
self.quadBulletsActive = true;
shipGraphics.tint = 0xe74c3c; // Red tint for quad bullets
self.fireRate = 15; // Medium firing speed
self.rotationSpeed = 0.05; // Start spinning
// Quad bullets time out after 8 seconds
LK.setTimeout(function () {
self.quadBulletsActive = false;
shipGraphics.tint = 0xFFFFFF;
self.fireRate = 20;
self.rotationSpeed = 0;
shipGraphics.rotation = 0; // Reset rotation
}, 8000);
};
return self;
});
var PowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('powerUp', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.speed = 3;
// Add quad bullet type with 1/3 chance
var rand = Math.random();
if (rand < 0.33) {
self.type = 'health';
} else if (rand < 0.66) {
self.type = 'weapon';
} else {
self.type = 'quadBullets';
}
// Set color based on type
if (self.type === 'health') {
powerUpGraphics.tint = 0x2ecc71; // Green for health
} else if (self.type === 'weapon') {
powerUpGraphics.tint = 0xf1c40f; // Yellow for weapon
} else {
powerUpGraphics.tint = 0xe74c3c; // Red for quad bullets
}
self.update = function () {
self.y += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game constants
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var SPAWN_ENEMY_INTERVAL = 60; // Spawn enemy every 60 ticks initially
var SPAWN_POWERUP_INTERVAL = 600; // Spawn power-up every 600 ticks
var MIN_SPAWN_INTERVAL = 20; // Minimum spawn rate as difficulty increases
// Game state variables
var player;
var playerBullets = [];
var enemies = [];
var enemyBullets = [];
var powerUps = [];
var missiles = [];
var explosions = [];
var firePools = [];
var gameLevel = 1;
var spawnCounter = 0;
var powerUpCounter = 0;
var isGameActive = true;
var score = 0;
var lastMissileTime = 0;
var missileReadyText = new Text2('MISSILE READY', {
size: 40,
fill: 0xFF0000
});
// Initialize score display
var scoreTxt = new Text2('SCORE: 0', {
size: 60,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
scoreTxt.y = 30;
// Initialize level display
var levelTxt = new Text2('LEVEL: 1', {
size: 60,
fill: 0xFFFFFF
});
levelTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(levelTxt);
levelTxt.y = 100;
// Create player ship
function initializeGame() {
player = new PlayerShip();
player.x = GAME_WIDTH / 2;
player.y = GAME_HEIGHT - 200;
game.addChild(player);
// Reset arrays
playerBullets = [];
enemies = [];
enemyBullets = [];
powerUps = [];
missiles = [];
explosions = [];
firePools = [];
// Reset game state
gameLevel = 1;
spawnCounter = 0;
powerUpCounter = 0;
isGameActive = true;
lastMissileTime = 0;
// Initialize missile ready text
missileReadyText.anchor.set(0.5, 0.5);
missileReadyText.alpha = 1;
missileReadyText.visible = true;
// Set initial missile time to be ready at game start
lastMissileTime = LK.ticks - 600;
// Position text below the player ship
game.addChild(missileReadyText);
// Flash the text immediately
var _flashMissileText = function flashMissileText() {
tween(missileReadyText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tween(missileReadyText, {
alpha: 1
}, {
duration: 500,
onFinish: _flashMissileText
});
}
});
};
_flashMissileText();
// Reset score and update display
LK.setScore(0);
score = 0;
updateScoreDisplay();
updateLevelDisplay();
// Play background music when game starts
LK.playMusic('gameMusic', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
}
// Initialize the game
initializeGame();
// Update score display
function updateScoreDisplay() {
scoreTxt.setText('SCORE: ' + score);
}
// Update level display
function updateLevelDisplay() {
levelTxt.setText('LEVEL: ' + gameLevel);
}
// Player shooting
function playerShoot() {
// Check fire rate cooldown
if (LK.ticks - player.lastShot < player.fireRate) {
return;
}
// Always fire dual cannons (left and right)
var bulletLeft = new PlayerBullet();
bulletLeft.x = player.x - 20;
bulletLeft.y = player.y - 40;
bulletLeft.direction = 'up';
game.addChild(bulletLeft);
playerBullets.push(bulletLeft);
var bulletRight = new PlayerBullet();
bulletRight.x = player.x + 20;
bulletRight.y = player.y - 40;
bulletRight.direction = 'up';
game.addChild(bulletRight);
playerBullets.push(bulletRight);
// Triple bullets if weapon power-up is active
if (player.powerUpActive) {
var bulletCenter = new PlayerBullet();
bulletCenter.x = player.x;
bulletCenter.y = player.y - 50;
bulletCenter.direction = 'up';
game.addChild(bulletCenter);
playerBullets.push(bulletCenter);
}
// Quad-directional bullets if quad bullets power-up is active
if (player.quadBulletsActive) {
// Up bullet already covered
// Down bullet
var bulletDown = new PlayerBullet();
bulletDown.x = player.x;
bulletDown.y = player.y + 40;
bulletDown.direction = 'down';
bulletDown.isPiercing = true;
bulletDown.isBouncing = true;
game.addChild(bulletDown);
playerBullets.push(bulletDown);
// Left bullet
var bulletLeft = new PlayerBullet();
bulletLeft.x = player.x - 40;
bulletLeft.y = player.y;
bulletLeft.direction = 'left';
bulletLeft.isPiercing = true;
bulletLeft.isBouncing = true;
game.addChild(bulletLeft);
playerBullets.push(bulletLeft);
// Right bullet
var bulletRight = new PlayerBullet();
bulletRight.x = player.x + 40;
bulletRight.y = player.y;
bulletRight.direction = 'right';
bulletRight.isPiercing = true;
bulletRight.isBouncing = true;
game.addChild(bulletRight);
playerBullets.push(bulletRight);
}
// Play sound and reset shot timer
LK.getSound('playerShoot').play();
player.lastShot = LK.ticks;
}
// Enemy shooting
function enemyShoot(enemy) {
if (LK.ticks - enemy.lastShot < enemy.fireRate) {
return;
}
var bullet = new EnemyBullet();
bullet.x = enemy.x;
bullet.y = enemy.y + 40;
// Check if enemy is in enhanced firing mode
if (enemy.firingMode) {
// Enhanced bullets are faster and aim at player
bullet.speed = 12; // Increased speed
// Calculate direction to player for aiming
if (player) {
// Get angle to player
var dx = player.x - enemy.x;
var dy = player.y - enemy.y;
var angle = Math.atan2(dy, dx);
// Set bullet velocity components based on angle
bullet.speedX = Math.cos(angle) * bullet.speed;
bullet.speedY = Math.sin(angle) * bullet.speed;
bullet.isAimed = true;
}
// Count shots in enhanced mode
if (enemy.enhancedShotsCount === undefined) {
enemy.enhancedShotsCount = 0;
}
enemy.enhancedShotsCount++;
}
game.addChild(bullet);
enemyBullets.push(bullet);
LK.getSound('enemyShoot').play();
enemy.lastShot = LK.ticks;
}
// Spawn new enemy
function spawnEnemy() {
var enemy = new Enemy();
// Distribute enemies better across the screen width
// Check if we have other enemies and avoid spawning too close to them
if (enemies.length > 0) {
// Find a position away from other enemies
var attempts = 0;
var potentialX;
var validPosition = false;
while (!validPosition && attempts < 10) {
potentialX = Math.random() * (GAME_WIDTH - 100) + 50;
validPosition = true;
// Check distance from existing enemies
for (var i = 0; i < enemies.length; i++) {
if (Math.abs(enemies[i].x - potentialX) < 150) {
validPosition = false;
break;
}
}
attempts++;
}
enemy.x = validPosition ? potentialX : Math.random() * (GAME_WIDTH - 100) + 50;
} else {
enemy.x = Math.random() * (GAME_WIDTH - 100) + 50;
}
enemy.y = -50;
// Increase difficulty with levels
if (gameLevel > 1) {
enemy.speed = Math.min(2 + gameLevel * 0.5, 6);
enemy.health = Math.min(1 + Math.floor(gameLevel / 3), 3);
}
if (gameLevel > 5) {
enemy.fireRate = Math.max(120 - gameLevel * 5, 60);
}
game.addChild(enemy);
enemies.push(enemy);
}
// Spawn power-up
function spawnPowerUp() {
var powerUp = new PowerUp();
powerUp.x = Math.random() * (GAME_WIDTH - 100) + 50;
powerUp.y = -50;
game.addChild(powerUp);
powerUps.push(powerUp);
}
// Check level progress
function checkLevelProgress() {
// Advance level based on score
var shouldAdvanceLevel = Math.floor(score / 1000) + 1;
if (shouldAdvanceLevel > gameLevel) {
gameLevel = shouldAdvanceLevel;
updateLevelDisplay();
// Flash screen to indicate level up
LK.effects.flashScreen(0x3498db, 500);
// Play level up sound
LK.getSound('levelUp').play();
}
}
// Handle collisions
function handleCollisions() {
// Check player bullets hitting enemies
for (var i = playerBullets.length - 1; i >= 0; i--) {
var bullet = playerBullets[i];
var bulletHit = false;
// Enemy bullet destruction completely removed
// Skip all bullet collision detection logic
// If player bullet was destroyed by enemy bullets, skip enemy collision check
if (bulletHit) {
continue;
}
// Now check for player bullet hitting enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (bullet.intersects(enemy)) {
// Enemy takes damage
if (enemy.takeDamage(bullet.power)) {
// Enemy destroyed
score += enemy.scoreValue;
LK.setScore(score);
updateScoreDisplay();
LK.getSound('explosion').play();
LK.effects.flashObject(enemy, 0xff0000, 300);
// Create explosion animation at enemy position
var explosion = new Explosion();
explosion.x = enemy.x;
explosion.y = enemy.y;
game.addChild(explosion);
explosions.push(explosion);
// Remove enemy
enemy.destroy();
enemies.splice(j, 1);
}
// Only remove non-piercing bullets on hit
if (!bullet.isPiercing) {
bullet.explode(); // Add explosion effect when bullet is destroyed
bullet.destroy();
playerBullets.splice(i, 1);
bulletHit = true;
break;
}
}
}
// If a non-piercing bullet hit something, skip to the next bullet
if (bulletHit) {
continue;
}
}
// Check enemy bullets hitting player
for (var i = enemyBullets.length - 1; i >= 0; i--) {
var bullet = enemyBullets[i];
// Get center points of both objects for more precise collision detection
var bulletCenterX = bullet.x;
var bulletCenterY = bullet.y;
var playerCenterX = player.x;
var playerCenterY = player.y;
// Calculate distance between centers
var dx = bulletCenterX - playerCenterX;
var dy = bulletCenterY - playerCenterY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Get the combined radius (half the width of both objects for a more precise collision)
var bulletRadius = bullet.width / 3.5; // Much smaller hitbox for accurate collision
var playerRadius = player.width / 3.5; // Much smaller hitbox for accurate collision
var combinedRadius = bulletRadius + playerRadius;
// Only register hit if the bullet is actually touching the player (more precise collision)
if (distance < combinedRadius) {
// Player hit
bullet.destroy();
enemyBullets.splice(i, 1);
if (player.shieldActive) {
// Shield absorbs the hit
// Play explosion sound for shield hit
LK.getSound('explosion').play();
player.shieldActive = false;
var shield = player.getChildAt(1);
shield.alpha = 0;
} else {
// Play sound when player gets hit by enemy bullet
LK.getSound('playerHit').play();
// Reduce player health
player.health--;
player.healthBar.updateHealth(player.health);
// Flash player to indicate damage
LK.effects.flashObject(player, 0xff0000, 300);
// Check if player health reaches 1 (final life)
if (player.health === 1 && !player.finalLifeWarningActive) {
player.finalLifeWarningActive = true;
// Hide health bar completely
player.healthBar.visible = false;
// Show and animate "Final Life" text
var finalLifeText = player.getChildAt(2); // Get the final life text
finalLifeText.alpha = 1;
// Create flashing effect for the text
var _flashTween2 = function flashTween() {
tween(finalLifeText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tween(finalLifeText, {
alpha: 1
}, {
duration: 500,
onFinish: _flashTween2
});
}
});
};
_flashTween2(); // Start the flashing animation
}
// Check if player is out of health
if (player.health <= 0) {
// Game over
LK.effects.flashScreen(0xff0000, 1000);
isGameActive = false;
// Stop music with fade out
LK.playMusic('gameMusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// Play game over sound effect
LK.getSound('gameOver').play();
LK.showGameOver();
break;
}
}
}
}
// Check enemies colliding with player
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.intersects(player)) {
if (player.shieldActive) {
// Shield absorbs the hit
// Play collision sound when enemy collides with shield
LK.getSound('shieldCollision').play();
enemy.destroy();
enemies.splice(i, 1);
// Shield gets depleted
player.shieldActive = false;
var shield = player.getChildAt(1);
shield.alpha = 0;
score += enemy.scoreValue;
LK.setScore(score);
updateScoreDisplay();
} else {
// Reduce player health by 2 (collision is more dangerous)
player.health -= 2;
player.healthBar.updateHealth(player.health);
// Play collision sound when player collides with enemy
LK.getSound('playerCollision').play();
// Remove the enemy
enemy.destroy();
enemies.splice(i, 1);
// Flash player to indicate damage
LK.effects.flashObject(player, 0xff0000, 300);
// Check if player health reaches 1 (final life)
if (player.health === 1 && !player.finalLifeWarningActive) {
player.finalLifeWarningActive = true;
// Hide health bar completely
player.healthBar.visible = false;
// Show and animate "Final Life" text
var finalLifeText = player.getChildAt(2); // Get the final life text
finalLifeText.alpha = 1;
// Create flashing effect for the text
var _flashTween2 = function _flashTween() {
tween(finalLifeText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tween(finalLifeText, {
alpha: 1
}, {
duration: 500,
onFinish: _flashTween2
});
}
});
};
_flashTween2(); // Start the flashing animation
}
// Check if player is out of health
if (player.health <= 0) {
// Game over
LK.effects.flashScreen(0xff0000, 1000);
isGameActive = false;
LK.showGameOver();
break;
}
}
}
}
// Check player collecting power-ups
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
if (powerUp.intersects(player)) {
LK.getSound('powerUp').play();
// Apply power-up effect
if (powerUp.type === 'health') {
// Health regeneration logic
if (player.health < player.healthBar.maxHealth) {
// Add 1 health point
player.health = Math.min(player.health + 1, player.healthBar.maxHealth);
// If player was at final life, restore health bar and remove warning
if (player.finalLifeWarningActive && player.health > 1) {
player.finalLifeWarningActive = false;
player.healthBar.visible = true;
// Get the final life text and hide it
var finalLifeText = player.getChildAt(2);
// Stop any existing tweens on the final life text
tween.stop(finalLifeText);
finalLifeText.alpha = 0;
}
// Update health bar
player.healthBar.updateHealth(player.health);
}
} else if (powerUp.type === 'weapon') {
player.activatePowerUp();
} else if (powerUp.type === 'quadBullets') {
player.activateQuadBullets();
}
// Remove power-up
powerUp.destroy();
powerUps.splice(i, 1);
}
}
}
// Clean up off-screen objects
function cleanupOffscreenObjects() {
// Clean up missiles that went off screen
for (var i = missiles.length - 1; i >= 0; i--) {
var missile = missiles[i];
if (missile.y < -50 || missile.y > GAME_HEIGHT + 50 || missile.x < -50 || missile.x > GAME_WIDTH + 50) {
missile.explode();
missile.destroy();
missiles.splice(i, 1);
}
}
// Clean up bullets that went off screen
for (var i = playerBullets.length - 1; i >= 0; i--) {
var bullet = playerBullets[i];
// Check if bullet is off screen based on direction (unless it's bouncing)
if (!bullet.isBouncing) {
if (bullet.direction === 'up' && bullet.y < -50 || bullet.direction === 'down' && bullet.y > GAME_HEIGHT + 50 || bullet.direction === 'left' && bullet.x < -50 || bullet.direction === 'right' && bullet.x > GAME_WIDTH + 50) {
bullet.destroy();
playerBullets.splice(i, 1);
}
} else if (bullet.bounceCount >= bullet.maxBounces) {
// Add explosion for bouncing bullets that reached max bounce count
bullet.explode();
// Play explosion sound when bullet is removed at max bounce count
LK.getSound('bulletExplosion').play();
bullet.destroy();
playerBullets.splice(i, 1);
}
}
for (var i = enemyBullets.length - 1; i >= 0; i--) {
if (enemyBullets[i].y > GAME_HEIGHT + 50) {
enemyBullets[i].destroy();
enemyBullets.splice(i, 1);
}
}
// Clean up enemies that went off screen
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i].y > GAME_HEIGHT + 100) {
enemies[i].destroy();
enemies.splice(i, 1);
}
}
// Clean up power-ups that went off screen
for (var i = powerUps.length - 1; i >= 0; i--) {
if (powerUps[i].y > GAME_HEIGHT + 50) {
powerUps[i].destroy();
powerUps.splice(i, 1);
}
}
}
// Player movement
var isDragging = false;
game.down = function (x, y) {
// Fire missile on left click if cooldown is ready
if (LK.ticks - lastMissileTime >= 600) {
// 10 seconds at 60fps
// Find closest enemy as target first
var closestEnemy = null;
var closestDistance = Number.MAX_VALUE;
var potentialTargets = [];
// Find up to 6 potential targets
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - player.x;
var dy = enemy.y - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
// Add to potential targets list (sort by proximity later)
potentialTargets.push({
enemy: enemy,
distance: distance
});
}
// Sort targets by distance
potentialTargets.sort(function (a, b) {
return a.distance - b.distance;
});
// Launch three missiles from the same vertical position but different horizontal positions
// Center missile - positioned exactly in the center of player ship
var centerMissile = new Missile();
centerMissile.x = player.x; // Center horizontally with player
centerMissile.y = player.y - 60; // Launch from front center of player ship
centerMissile.turnSpeed = 0.05; // Very slight homing for center missile
centerMissile.initialDirection = 'up'; // Make sure it exits straight up first
// Left missile - positioned to the left of center missile
var leftMissile = new Missile();
leftMissile.x = player.x - 30; // Position left of the player
leftMissile.y = player.y - 60; // Same vertical position as center missile
leftMissile.angle = -0.3; // Slight left angle for initial trajectory
leftMissile.turnSpeed = 0.07; // Reduced homing for more separation
leftMissile.initialDirection = 'left-up'; // Make sure it exits left-up first
// Right missile - positioned to the right of center missile
var rightMissile = new Missile();
rightMissile.x = player.x + 30; // Position right of the player
rightMissile.y = player.y - 60; // Same vertical position as center missile
rightMissile.angle = 0.3; // Slight right angle for initial trajectory
rightMissile.turnSpeed = 0.07; // Reduced homing for more separation
rightMissile.initialDirection = 'right-up'; // Make sure it exits right-up first
// If we found a target, set it for missiles with different offsets to avoid pairing
if (closestEnemy) {
// Set slightly different targets for each missile to avoid pairing
centerMissile.targetX = closestEnemy.x;
centerMissile.targetY = closestEnemy.y;
// For side missiles, either use different enemies or offset from main target
if (potentialTargets.length > 1) {
leftMissile.targetX = potentialTargets[Math.min(1, potentialTargets.length - 1)].enemy.x - 50;
leftMissile.targetY = potentialTargets[Math.min(1, potentialTargets.length - 1)].enemy.y;
} else {
// If not enough targets, offset significantly from main target
leftMissile.targetX = closestEnemy.x - 150;
leftMissile.targetY = closestEnemy.y;
}
if (potentialTargets.length > 2) {
rightMissile.targetX = potentialTargets[Math.min(2, potentialTargets.length - 1)].enemy.x + 50;
rightMissile.targetY = potentialTargets[Math.min(2, potentialTargets.length - 1)].enemy.y;
} else {
// If not enough targets, offset significantly from main target
rightMissile.targetX = closestEnemy.x + 150;
rightMissile.targetY = closestEnemy.y;
}
}
// Add first wave of missiles to game
game.addChild(centerMissile);
missiles.push(centerMissile);
game.addChild(leftMissile);
missiles.push(leftMissile);
game.addChild(rightMissile);
missiles.push(rightMissile);
// Create a second wave of missiles with slightly different launch positions
// Second center missile
var centerMissile2 = new Missile();
centerMissile2.x = player.x;
centerMissile2.y = player.y - 30; // Slightly higher than first wave
centerMissile2.turnSpeed = 0.06;
centerMissile2.initialDirection = 'up';
// Second left missile
var leftMissile2 = new Missile();
leftMissile2.x = player.x - 40; // Different horizontal offset
leftMissile2.y = player.y - 30;
leftMissile2.angle = -0.4; // Different initial angle
leftMissile2.turnSpeed = 0.08;
leftMissile2.initialDirection = 'left-up';
// Second right missile
var rightMissile2 = new Missile();
rightMissile2.x = player.x + 40; // Different horizontal offset
rightMissile2.y = player.y - 30;
rightMissile2.angle = 0.4; // Different initial angle
rightMissile2.turnSpeed = 0.08;
rightMissile2.initialDirection = 'right-up';
// Set targets for second wave
if (closestEnemy) {
// Set slightly different targets for second wave missiles
if (potentialTargets.length > 3) {
centerMissile2.targetX = potentialTargets[3].enemy.x;
centerMissile2.targetY = potentialTargets[3].enemy.y;
} else {
centerMissile2.targetX = closestEnemy.x + 50;
centerMissile2.targetY = closestEnemy.y + 50;
}
if (potentialTargets.length > 4) {
leftMissile2.targetX = potentialTargets[4].enemy.x - 70;
leftMissile2.targetY = potentialTargets[4].enemy.y;
} else {
leftMissile2.targetX = closestEnemy.x - 200;
leftMissile2.targetY = closestEnemy.y + 50;
}
if (potentialTargets.length > 5) {
rightMissile2.targetX = potentialTargets[5].enemy.x + 70;
rightMissile2.targetY = potentialTargets[5].enemy.y;
} else {
rightMissile2.targetX = closestEnemy.x + 200;
rightMissile2.targetY = closestEnemy.y + 50;
}
}
// Add second wave of missiles to game
game.addChild(centerMissile2);
missiles.push(centerMissile2);
game.addChild(leftMissile2);
missiles.push(leftMissile2);
game.addChild(rightMissile2);
missiles.push(rightMissile2);
// Play missile launch sound
LK.getSound('missileLaunch').play();
// Reset cooldown
lastMissileTime = LK.ticks;
// Hide missile ready text and stop any active tweens
tween.stop(missileReadyText);
missileReadyText.visible = false;
} //[8F]
// Handle regular player movement
var targetX = x;
var targetY = y;
// Clamp target position to game bounds
if (targetX < 50) {
targetX = 50;
}
if (targetX > GAME_WIDTH - 50) {
targetX = GAME_WIDTH - 50;
}
if (targetY < 50) {
targetY = 50;
}
if (targetY > GAME_HEIGHT - 50) {
targetY = GAME_HEIGHT - 50;
}
player.targetX = targetX;
player.targetY = targetY;
player.isMoving = true;
// Also shoot when player taps
playerShoot();
};
game.move = function (x, y) {
if (isGameActive) {
// Clamp target position to game bounds
var targetX = x;
var targetY = y;
if (targetX < 50) {
targetX = 50;
}
if (targetX > GAME_WIDTH - 50) {
targetX = GAME_WIDTH - 50;
}
if (targetY < 50) {
targetY = 50;
}
if (targetY > GAME_HEIGHT - 50) {
targetY = GAME_HEIGHT - 50;
}
// Update target position
player.targetX = targetX;
player.targetY = targetY;
// For better tracking during fast mouse movements, directly update position
player.x = player.targetX;
player.y = player.targetY;
player.isMoving = true; //{2K}{2L}{2M}{2N}
}
};
game.up = function () {
// Keep the ship moving toward last target point
};
// Main game update loop
game.update = function () {
if (!isGameActive) {
return;
}
// Update missile ready text
if (LK.ticks - lastMissileTime >= 600) {
// 10 seconds cooldown
if (!missileReadyText.visible) {
missileReadyText.visible = true;
missileReadyText.x = player.x;
missileReadyText.y = player.y + 110;
missileReadyText.alpha = 1;
// Flash the text when ready
var _flashMissileText2 = function flashMissileText() {
tween(missileReadyText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tween(missileReadyText, {
alpha: 1
}, {
duration: 500,
onFinish: _flashMissileText2
});
}
});
};
_flashMissileText2();
}
}
// Spawn enemies
spawnCounter++;
var currentSpawnRate = Math.max(SPAWN_ENEMY_INTERVAL - gameLevel * 5, MIN_SPAWN_INTERVAL);
if (spawnCounter >= currentSpawnRate) {
spawnEnemy();
spawnCounter = 0;
}
// Spawn power-ups
powerUpCounter++;
if (powerUpCounter >= SPAWN_POWERUP_INTERVAL) {
spawnPowerUp();
powerUpCounter = 0;
}
// Make player shoot automatically
if (player.isMoving) {
playerShoot();
}
// Update enemies and their shooting
for (var i = enemies.length - 1; i >= 0; i--) {
// Check if the enemy should be removed (returned true from update)
if (enemies[i].update && enemies[i].update() === true) {
enemies.splice(i, 1);
continue;
}
enemyShoot(enemies[i]);
}
// Update missiles
for (var i = missiles.length - 1; i >= 0; i--) {
var missile = missiles[i];
// If missile doesn't have a target or target is destroyed, find new target
if ((missile.targetX === null || missile.targetY === null) && enemies.length > 0) {
// Find closest enemy as new target
var closestEnemy = null;
var closestDistance = Number.MAX_VALUE;
// Check all enemies for potential targets
for (var k = 0; k < enemies.length; k++) {
var dx = enemies[k].x - missile.x;
var dy = enemies[k].y - missile.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemies[k];
}
}
// If we found a target, set it with slight offset based on missile type
if (closestEnemy) {
// Add slightly different offsets to each missile's target to maintain separation
// We can determine missile "type" by its position relative to player
var offsetX = 0;
var offsetY = 0; // Adding vertical offset for better separation
// Using angle to identify missile type
if (missile.angle < 0) {
// Left missile
offsetX = -100; // Increased horizontal offset
offsetY = -20; // Slight vertical offset
} else if (missile.angle > 0) {
// Right missile
offsetX = 100; // Increased horizontal offset
offsetY = -20; // Slight vertical offset
} else {
// Center missile
offsetY = -50; // Different vertical offset for center missile
}
missile.targetX = closestEnemy.x + offsetX;
missile.targetY = closestEnemy.y + offsetY;
}
}
// Check if missile hits an enemy
var missileHit = false;
for (var j = enemies.length - 1; j >= 0; j--) {
if (missile.intersects(enemies[j])) {
missile.explode();
missile.destroy();
missiles.splice(i, 1);
missileHit = true;
break;
}
}
// Check if missile is off screen
if (!missileHit && (missile.y < -50 || missile.y > GAME_HEIGHT + 50 || missile.x < -50 || missile.x > GAME_WIDTH + 50)) {
missile.explode();
missile.destroy();
missiles.splice(i, 1);
}
}
// Update explosions
for (var i = explosions.length - 1; i >= 0; i--) {
if (explosions[i].update()) {
explosions.splice(i, 1);
}
}
// Update fire pools
for (var i = firePools.length - 1; i >= 0; i--) {
if (firePools[i].update()) {
firePools.splice(i, 1);
}
}
// Check collisions
handleCollisions();
// Clean up off-screen objects
cleanupOffscreenObjects();
// Check level progression
checkLevelProgress();
// Win condition check
if (score >= 10000) {
// Play win sound effect
LK.getSound('winSound').play();
// Stop music with fade out
LK.playMusic('gameMusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
LK.showYouWin();
isGameActive = false;
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var Enemy = Container.expand(function () {
var self = Container.call(this);
var enemyGraphics = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
// Use default hitbox
// Enemy properties
self.health = 1;
self.speed = 2;
self.scoreValue = 100;
self.fireRate = 120; // Ticks between shots
self.lastShot = Math.floor(Math.random() * 60); // Randomize initial shot timer
self.movementPattern = Math.floor(Math.random() * 3); // 0: straight, 1: zigzag, 2: circular
self.movementCounter = 0;
self.lastY = 0; // Track last Y position to detect reaching bottom
self.firingMode = false; // Track if enemy is in enhanced firing mode
self.firingModeStartTime = 0; // When enhanced firing mode started
// Dodging properties
self.canDodge = true;
self.lastDodgeTime = 0;
self.isDodging = false;
self.dodgeCooldown = 300; // 5 seconds at 60fps
self.dodgeDuration = 30; // Half a second dodge movement
self.dodgeCounter = 0;
self.dodgeDirection = 0; // Will be set when dodging
self.dodgeSpeed = 10; // Fast dodge movement
// Initial target position (will be set during movement)
self.targetX = null;
self.update = function () {
// Store last Y position for edge detection
if (self.lastY === undefined) {
self.lastY = self.y;
}
// Basic downward movement
self.y += self.speed;
// Check if enemy just reached bottom edge of game area
var bottomThreshold = GAME_HEIGHT - 200;
if (self.lastY < bottomThreshold && self.y >= bottomThreshold && !self.firingMode) {
// Enemy reached bottom edge - activate enhanced firing mode
self.firingMode = true;
self.firingModeStartTime = LK.ticks;
// Store original fire rate before enhancement
self.originalFireRate = self.fireRate;
// Increase fire rate and set player as target
self.fireRate = 30; // Much faster fire rate
// Flash red to indicate enhanced firing mode
LK.effects.flashObject(self, 0xff0000, 500);
// Start aiming at player
self.targetPlayer = true;
}
// Handle enhanced firing mode behavior
if (self.firingMode) {
// Track number of shots in enhanced mode
if (self.enhancedShotsCount === undefined) {
self.enhancedShotsCount = 0;
}
// Self-destruct after 10 shots instead of time-based
if (self.enhancedShotsCount >= 10) {
// Create explosion effect
var explosion = new Explosion();
explosion.x = self.x;
explosion.y = self.y;
game.addChild(explosion);
explosions.push(explosion);
// Play explosion sound
LK.getSound('explosion').play();
// Add score for self-destruction
score += self.scoreValue;
LK.setScore(score);
updateScoreDisplay();
// Remove the enemy
self.destroy();
return true; // Signal to remove from enemies array
}
// Aim at player during enhanced firing mode
if (player && self.targetPlayer) {
// Calculate direction to player
var diffX = player.x - self.x;
var diffY = player.y - self.y;
// Move toward player with increased precision
self.x += diffX * 0.02;
// Apply a pulsating movement for visual effect
self.y += Math.sin(LK.ticks * 0.1) * 1.5;
return;
}
}
// Apply regular movement pattern when not in enhanced firing mode
if (self.movementPattern === 1) {
// Zigzag with occasional side movement
self.x += Math.sin(self.movementCounter * 0.05) * 3;
// Occasionally move to a different horizontal position
if (self.movementCounter % 500 === 0) {
// Choose a new target position on the opposite side
self.targetX = self.x < GAME_WIDTH / 2 ? GAME_WIDTH * 0.7 + Math.random() * GAME_WIDTH * 0.2 : GAME_WIDTH * 0.1 + Math.random() * GAME_WIDTH * 0.2;
}
if (self.targetX) {
var diffX = self.targetX - self.x;
if (Math.abs(diffX) > 10) {
self.x += diffX * 0.03;
}
}
self.movementCounter++;
} else if (self.movementPattern === 2) {
// Circular with occasional repositioning
self.x += Math.sin(self.movementCounter * 0.03) * 4;
// Occasionally reset position to avoid grouping
if (self.movementCounter % 700 === 0) {
// Move to random position on screen
self.targetX = Math.random() * GAME_WIDTH * 0.8 + GAME_WIDTH * 0.1;
}
// Move toward target if exists
if (self.targetX) {
var diffX = self.targetX - self.x;
if (Math.abs(diffX) > 10) {
self.x += diffX * 0.02;
}
}
self.movementCounter++;
} else {
// Add movement for straight pattern (pattern 0)
if (self.movementCounter % 900 === 0) {
// Occasional side movement
self.targetX = Math.random() * GAME_WIDTH * 0.8 + GAME_WIDTH * 0.1;
}
// Move toward target if exists
if (self.targetX) {
var diffX = self.targetX - self.x;
if (Math.abs(diffX) > 10) {
self.x += diffX * 0.01;
}
}
self.movementCounter++;
}
// Check for nearby bullets and dodge if possible
if (self.canDodge && !self.isDodging && LK.ticks - self.lastDodgeTime >= self.dodgeCooldown) {
// Look for nearby bullets
for (var i = 0; i < playerBullets.length; i++) {
var bullet = playerBullets[i];
// Skip bullets not moving toward the enemy (horizontal bullets, or downward bullets)
if (bullet.direction !== 'up') continue;
// Calculate distance between bullet and enemy
var dx = bullet.x - self.x;
var dy = bullet.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If bullet is close but not too close, and is above the enemy (coming toward it)
if (distance < 150 && Math.abs(dx) < 100 && bullet.y < self.y && bullet.y > self.y - 300) {
// Start dodging!
self.isDodging = true;
self.dodgeCounter = 0;
// Determine dodge direction (away from bullet)
self.dodgeDirection = dx > 0 ? -1 : 1; // Move left if bullet is on right, right if bullet is on left
self.lastDodgeTime = LK.ticks;
// Flash enemy green to indicate dodge
LK.effects.flashObject(self, 0x00ff00, 300);
break;
}
}
}
// Handle active dodging movement
if (self.isDodging) {
self.dodgeCounter++;
// Move rapidly in dodge direction
self.x += self.dodgeDirection * self.dodgeSpeed;
// Constrain to screen bounds
if (self.x < 50) {
self.x = 50;
self.dodgeDirection = 1; // Reverse direction if hitting edge
} else if (self.x > GAME_WIDTH - 50) {
self.x = GAME_WIDTH - 50;
self.dodgeDirection = -1; // Reverse direction if hitting edge
}
// End dodge after duration
if (self.dodgeCounter >= self.dodgeDuration) {
self.isDodging = false;
}
}
// Update lastY for next frame
self.lastY = self.y;
};
// Take damage and check if destroyed
self.takeDamage = function (amount) {
self.health -= amount;
if (self.health <= 0) {
return true; // Enemy is destroyed
}
return false;
};
// Use default intersection method
return self;
});
var EnemyBullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('enemyBullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 7; // Moving downward
self.update = function () {
if (self.isAimed && self.speedX !== undefined && self.speedY !== undefined) {
// Move along calculated trajectory for aimed bullets
self.x += self.speedX;
self.y += self.speedY;
} else {
// Default downward movement
self.y += self.speed;
}
};
// Use default intersection method
return self;
});
var Explosion = Container.expand(function () {
var self = Container.call(this);
var explosionGraphics = self.attachAsset('explosion', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.8
});
// Explosion duration
self.duration = 30; // frames
self.currentFrame = 0;
// Scale explosion from small to large then fade
explosionGraphics.scale.x = 0.1;
explosionGraphics.scale.y = 0.1;
self.update = function () {
self.currentFrame++;
// Expand and then fade
if (self.currentFrame < 15) {
explosionGraphics.scale.x += 0.1;
explosionGraphics.scale.y += 0.1;
} else {
explosionGraphics.alpha -= 0.08;
}
// Remove when animation complete
if (self.currentFrame >= self.duration) {
self.destroy();
return true;
}
return false;
};
return self;
});
var FirePool = Container.expand(function () {
var self = Container.call(this);
var fireGraphics = self.attachAsset('firePool', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.7
});
// Fire pool duration (4 seconds at 60fps)
self.duration = 240;
self.currentFrame = 0;
// Pulsate effect
self.pulsateDirection = 1;
self.pulsateAmount = 0;
self.update = function () {
self.currentFrame++;
// Pulsate effect
self.pulsateAmount += 0.02 * self.pulsateDirection;
if (self.pulsateAmount > 0.2 || self.pulsateAmount < -0.2) {
self.pulsateDirection *= -1;
}
fireGraphics.scale.x = 1 + self.pulsateAmount;
fireGraphics.scale.y = 1 + self.pulsateAmount;
// Fade out at the end
if (self.currentFrame > 150) {
fireGraphics.alpha -= 0.03;
}
// Check for enemies touching the fire pool
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.intersects(self)) {
// Enemy is instantly destroyed by fire
score += enemy.scoreValue;
LK.setScore(score);
updateScoreDisplay();
LK.effects.flashObject(enemy, 0xff0000, 300);
// Play explosion sound when enemy is destroyed by fire pool
LK.getSound('firePoolDestroy').play();
enemy.destroy();
enemies.splice(i, 1);
}
}
// Remove when duration complete
if (self.currentFrame >= self.duration) {
self.destroy();
return true;
}
return false;
};
return self;
});
var HealthBar = Container.expand(function () {
var self = Container.call(this);
// Health bar properties
self.maxHealth = 4;
self.currentHealth = 4;
self.width = 140;
self.height = 20;
// Create background bar (gray)
var background = self.attachAsset('healthbar', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
// Adjust to match bar width
scaleY: 1,
// Adjust to match bar height
tint: 0x666666 // Gray color
});
// Create foreground bar (health indicator - green)
var foreground = self.attachAsset('healthbar', {
anchorX: 0,
anchorY: 0.5,
scaleX: 1,
// Adjust to match bar width
scaleY: 1,
// Adjust to match bar height
tint: 0x00FF00 // Green color
});
// Center the foreground bar
foreground.x = -background.width / 2;
// Update health bar visual based on current health
self.updateHealth = function (health) {
self.currentHealth = health;
// Update the scale of the foreground bar based on current health
var healthPercentage = Math.max(self.currentHealth / self.maxHealth, 0);
// Scale should decrease as health decreases, not increase
// Ensure that we don't go to 0 until health is actually 0
if (self.currentHealth > 0) {
// For non-zero health, ensure we have at least a small visible portion of health bar
foreground.scale.x = Math.max(healthPercentage, 0.05);
} else {
// Only when health is truly zero, set scale to zero
foreground.scale.x = 0;
}
// Change color based on health level
if (healthPercentage > 0.6) {
foreground.tint = 0x00FF00; // Green
} else if (healthPercentage > 0.3) {
foreground.tint = 0xFFFF00; // Yellow
} else {
foreground.tint = 0xFF0000; // Red
}
};
return self;
});
var Missile = Container.expand(function () {
var self = Container.call(this);
var missileGraphics = self.attachAsset('missile', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.speed = -10; // Moving upward
self.targetX = null;
self.targetY = null; // Add targetY for better homing
self.turnSpeed = 0.25; // Significantly increased turn speed for better tracking
self.angle = 0; // For side missiles
self.update = function () {
// Basic upward movement
self.y += self.speed;
// Enhanced target tracking if we have a target
if (self.targetX && self.targetY) {
// Calculate distance to target
var diffX = self.targetX - self.x;
var diffY = self.targetY - self.y;
var dist = Math.sqrt(diffX * diffX + diffY * diffY);
// Track frames since launch for initial trajectory control
if (self.initialDirectionFrames === undefined) {
self.initialDirectionFrames = 0;
}
// For the first 20 frames, maintain initial trajectory with very minimal homing
// This ensures missiles start with distinctive paths
if (self.initialDirectionFrames < 20) {
self.initialDirectionFrames++;
// Initial direction based on missile type
if (self.initialDirection === 'left-up') {
// Left missile veers slightly left
self.x -= 2;
self.y += self.speed * 0.2; // Slow vertical movement slightly
} else if (self.initialDirection === 'right-up') {
// Right missile veers slightly right
self.x += 2;
self.y += self.speed * 0.2; // Slow vertical movement slightly
} else {
// Center missile goes straight up
// No horizontal movement, just straight up
}
} else {
// After initial frames, apply enhanced homing behavior
// Use a larger factor for more effective tracking while keeping realistic movement
var homingFactor = self.turnSpeed * 0.3; // Significantly increased factor for better homing
// Move more noticeably toward target x position
self.x += diffX * homingFactor;
// Also add vertical homing for more direct targeting
self.y += diffY * (homingFactor * 0.4); // Add vertical component to homing
// Apply slight angular adjustment to maintain separation
// This ensures missiles maintain some of their initial trajectory
if (self.angle) {
self.x += Math.sin(self.angle) * 0.2; // Further reduced angular influence for better homing
}
}
// Improved y-speed adjustment if target is far above or below
if (diffY < -150) {
// Increased speed adjustment for better vertical tracking
self.y += self.speed * 0.06; // More aggressive speed adjustment
} else if (diffY > 150) {
// Increased adjustment if target is below missile
self.y += self.speed * -0.04; // More noticeable slowdown
}
}
};
// Create explosion when hitting a target or edge
self.explode = function () {
var explosion = new Explosion();
explosion.x = self.x;
explosion.y = self.y;
game.addChild(explosion);
// Create fire pool after explosion
var firePool = new FirePool();
firePool.x = self.x;
firePool.y = self.y;
game.addChild(firePool);
firePools.push(firePool);
// Play explosion sound
LK.getSound('explosion').play();
// Damage enemies in area
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
var dx = enemy.x - self.x;
var dy = enemy.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// If enemy is within explosion radius
if (distance < 150) {
// Enemy is destroyed by area damage
score += enemy.scoreValue;
LK.setScore(score);
updateScoreDisplay();
LK.effects.flashObject(enemy, 0xff0000, 300);
enemy.destroy();
enemies.splice(i, 1);
}
}
return true;
};
return self;
});
var PlayerBullet = Container.expand(function () {
var self = Container.call(this);
var bulletGraphics = self.attachAsset('playerBullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = -20; // Moving upward (increased speed)
self.power = 1;
self.direction = 'up'; // up, down, left, right
self.isPiercing = false;
self.isBouncing = false;
self.bounceCount = 0;
self.maxBounces = 3;
// Enemy bullet destruction functionality removed
self.update = function () {
// Move bullet based on direction
if (self.direction === 'up') {
self.y += self.speed;
// Check for bouncing against top edge
if (self.isBouncing && self.y < 0 && self.bounceCount < self.maxBounces) {
self.direction = 'down';
self.bounceCount++;
}
} else if (self.direction === 'down') {
self.y -= self.speed; // Note: speed is negative, so this is adding a positive value
// Check for bouncing against bottom edge
if (self.isBouncing && self.y > GAME_HEIGHT && self.bounceCount < self.maxBounces) {
self.direction = 'up';
self.bounceCount++;
}
} else if (self.direction === 'left') {
self.x += self.speed;
// Check for bouncing against left edge
if (self.isBouncing && self.x < 0 && self.bounceCount < self.maxBounces) {
self.direction = 'right';
self.bounceCount++;
}
} else if (self.direction === 'right') {
self.x -= self.speed; // Note: speed is negative, so this is adding a positive value
// Check for bouncing against right edge
if (self.isBouncing && self.x > GAME_WIDTH && self.bounceCount < self.maxBounces) {
self.direction = 'left';
self.bounceCount++;
}
}
};
// Method to create explosion when player bullet is destroyed
self.explode = function () {
var explosion = new Explosion();
explosion.x = self.x;
explosion.y = self.y;
// Make player bullet explosion slightly smaller
explosion.scale.x = 0.6;
explosion.scale.y = 0.6;
game.addChild(explosion);
explosions.push(explosion);
// Play explosion sound
LK.getSound('explosion').play();
};
// Use default intersection method
return self;
});
var PlayerShip = Container.expand(function () {
var self = Container.call(this);
// Visual representation
var shipGraphics = self.attachAsset('playerShip', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.2,
scaleY: 1.2
});
// Use default hitbox
// Health visual (initially invisible)
var shieldGraphics = self.attachAsset('health', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
scaleX: 1.2,
scaleY: 1.2
});
// Final life warning text
var finalLifeText = new Text2('FINAL LIFE', {
size: 50,
fill: 0xFF0000
});
finalLifeText.anchor.set(0.5, 0.5);
finalLifeText.y = -110;
finalLifeText.alpha = 0;
self.addChild(finalLifeText);
// Player properties
self.speed = 10;
self.fireRate = 20; // Ticks between shots
self.lastShot = 0;
self.health = 4; // Player starts with 4 health
self.healthBar = new HealthBar();
self.healthBar.y = -80; // Position above player
self.healthBar.x = 0; // Perfectly center the healthbar with the ship
self.addChild(self.healthBar);
self.shieldActive = false;
self.powerUpActive = false;
self.quadBulletsActive = false;
self.rotationSpeed = 0;
self.finalLifeWarningActive = false;
// Physics properties for smooth movement
self.targetX = null;
self.targetY = null;
self.isMoving = false;
self.moveTween = null;
// Update method for rotation and missile text positioning
self.update = function () {
if (self.quadBulletsActive) {
shipGraphics.rotation += self.rotationSpeed;
}
// Keep missile ready text below the ship
if (missileReadyText && missileReadyText.visible) {
missileReadyText.x = self.x;
missileReadyText.y = self.y + 110;
}
};
// Use default intersection method
// Shield activation
self.activateShield = function () {
self.shieldActive = true;
shieldGraphics.alpha = 0.5;
// Play shield activation sound effect
LK.getSound('shieldActivation').play();
// Shield times out after 5 seconds
LK.setTimeout(function () {
self.shieldActive = false;
shieldGraphics.alpha = 0;
}, 5000);
};
// Power-up activation
self.activatePowerUp = function () {
self.powerUpActive = true;
shipGraphics.tint = 0xf1c40f; // Yellow tint for power-up
self.fireRate = 10; // Faster firing
// Power-up times out after 7 seconds
LK.setTimeout(function () {
self.powerUpActive = false;
shipGraphics.tint = 0xFFFFFF;
self.fireRate = 20;
}, 7000);
};
// Quad bullets activation
self.activateQuadBullets = function () {
self.quadBulletsActive = true;
shipGraphics.tint = 0xe74c3c; // Red tint for quad bullets
self.fireRate = 15; // Medium firing speed
self.rotationSpeed = 0.05; // Start spinning
// Quad bullets time out after 8 seconds
LK.setTimeout(function () {
self.quadBulletsActive = false;
shipGraphics.tint = 0xFFFFFF;
self.fireRate = 20;
self.rotationSpeed = 0;
shipGraphics.rotation = 0; // Reset rotation
}, 8000);
};
return self;
});
var PowerUp = Container.expand(function () {
var self = Container.call(this);
var powerUpGraphics = self.attachAsset('powerUp', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.speed = 3;
// Add quad bullet type with 1/3 chance
var rand = Math.random();
if (rand < 0.33) {
self.type = 'health';
} else if (rand < 0.66) {
self.type = 'weapon';
} else {
self.type = 'quadBullets';
}
// Set color based on type
if (self.type === 'health') {
powerUpGraphics.tint = 0x2ecc71; // Green for health
} else if (self.type === 'weapon') {
powerUpGraphics.tint = 0xf1c40f; // Yellow for weapon
} else {
powerUpGraphics.tint = 0xe74c3c; // Red for quad bullets
}
self.update = function () {
self.y += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game constants
var GAME_WIDTH = 2048;
var GAME_HEIGHT = 2732;
var SPAWN_ENEMY_INTERVAL = 60; // Spawn enemy every 60 ticks initially
var SPAWN_POWERUP_INTERVAL = 600; // Spawn power-up every 600 ticks
var MIN_SPAWN_INTERVAL = 20; // Minimum spawn rate as difficulty increases
// Game state variables
var player;
var playerBullets = [];
var enemies = [];
var enemyBullets = [];
var powerUps = [];
var missiles = [];
var explosions = [];
var firePools = [];
var gameLevel = 1;
var spawnCounter = 0;
var powerUpCounter = 0;
var isGameActive = true;
var score = 0;
var lastMissileTime = 0;
var missileReadyText = new Text2('MISSILE READY', {
size: 40,
fill: 0xFF0000
});
// Initialize score display
var scoreTxt = new Text2('SCORE: 0', {
size: 60,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
scoreTxt.y = 30;
// Initialize level display
var levelTxt = new Text2('LEVEL: 1', {
size: 60,
fill: 0xFFFFFF
});
levelTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(levelTxt);
levelTxt.y = 100;
// Create player ship
function initializeGame() {
player = new PlayerShip();
player.x = GAME_WIDTH / 2;
player.y = GAME_HEIGHT - 200;
game.addChild(player);
// Reset arrays
playerBullets = [];
enemies = [];
enemyBullets = [];
powerUps = [];
missiles = [];
explosions = [];
firePools = [];
// Reset game state
gameLevel = 1;
spawnCounter = 0;
powerUpCounter = 0;
isGameActive = true;
lastMissileTime = 0;
// Initialize missile ready text
missileReadyText.anchor.set(0.5, 0.5);
missileReadyText.alpha = 1;
missileReadyText.visible = true;
// Set initial missile time to be ready at game start
lastMissileTime = LK.ticks - 600;
// Position text below the player ship
game.addChild(missileReadyText);
// Flash the text immediately
var _flashMissileText = function flashMissileText() {
tween(missileReadyText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tween(missileReadyText, {
alpha: 1
}, {
duration: 500,
onFinish: _flashMissileText
});
}
});
};
_flashMissileText();
// Reset score and update display
LK.setScore(0);
score = 0;
updateScoreDisplay();
updateLevelDisplay();
// Play background music when game starts
LK.playMusic('gameMusic', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
}
// Initialize the game
initializeGame();
// Update score display
function updateScoreDisplay() {
scoreTxt.setText('SCORE: ' + score);
}
// Update level display
function updateLevelDisplay() {
levelTxt.setText('LEVEL: ' + gameLevel);
}
// Player shooting
function playerShoot() {
// Check fire rate cooldown
if (LK.ticks - player.lastShot < player.fireRate) {
return;
}
// Always fire dual cannons (left and right)
var bulletLeft = new PlayerBullet();
bulletLeft.x = player.x - 20;
bulletLeft.y = player.y - 40;
bulletLeft.direction = 'up';
game.addChild(bulletLeft);
playerBullets.push(bulletLeft);
var bulletRight = new PlayerBullet();
bulletRight.x = player.x + 20;
bulletRight.y = player.y - 40;
bulletRight.direction = 'up';
game.addChild(bulletRight);
playerBullets.push(bulletRight);
// Triple bullets if weapon power-up is active
if (player.powerUpActive) {
var bulletCenter = new PlayerBullet();
bulletCenter.x = player.x;
bulletCenter.y = player.y - 50;
bulletCenter.direction = 'up';
game.addChild(bulletCenter);
playerBullets.push(bulletCenter);
}
// Quad-directional bullets if quad bullets power-up is active
if (player.quadBulletsActive) {
// Up bullet already covered
// Down bullet
var bulletDown = new PlayerBullet();
bulletDown.x = player.x;
bulletDown.y = player.y + 40;
bulletDown.direction = 'down';
bulletDown.isPiercing = true;
bulletDown.isBouncing = true;
game.addChild(bulletDown);
playerBullets.push(bulletDown);
// Left bullet
var bulletLeft = new PlayerBullet();
bulletLeft.x = player.x - 40;
bulletLeft.y = player.y;
bulletLeft.direction = 'left';
bulletLeft.isPiercing = true;
bulletLeft.isBouncing = true;
game.addChild(bulletLeft);
playerBullets.push(bulletLeft);
// Right bullet
var bulletRight = new PlayerBullet();
bulletRight.x = player.x + 40;
bulletRight.y = player.y;
bulletRight.direction = 'right';
bulletRight.isPiercing = true;
bulletRight.isBouncing = true;
game.addChild(bulletRight);
playerBullets.push(bulletRight);
}
// Play sound and reset shot timer
LK.getSound('playerShoot').play();
player.lastShot = LK.ticks;
}
// Enemy shooting
function enemyShoot(enemy) {
if (LK.ticks - enemy.lastShot < enemy.fireRate) {
return;
}
var bullet = new EnemyBullet();
bullet.x = enemy.x;
bullet.y = enemy.y + 40;
// Check if enemy is in enhanced firing mode
if (enemy.firingMode) {
// Enhanced bullets are faster and aim at player
bullet.speed = 12; // Increased speed
// Calculate direction to player for aiming
if (player) {
// Get angle to player
var dx = player.x - enemy.x;
var dy = player.y - enemy.y;
var angle = Math.atan2(dy, dx);
// Set bullet velocity components based on angle
bullet.speedX = Math.cos(angle) * bullet.speed;
bullet.speedY = Math.sin(angle) * bullet.speed;
bullet.isAimed = true;
}
// Count shots in enhanced mode
if (enemy.enhancedShotsCount === undefined) {
enemy.enhancedShotsCount = 0;
}
enemy.enhancedShotsCount++;
}
game.addChild(bullet);
enemyBullets.push(bullet);
LK.getSound('enemyShoot').play();
enemy.lastShot = LK.ticks;
}
// Spawn new enemy
function spawnEnemy() {
var enemy = new Enemy();
// Distribute enemies better across the screen width
// Check if we have other enemies and avoid spawning too close to them
if (enemies.length > 0) {
// Find a position away from other enemies
var attempts = 0;
var potentialX;
var validPosition = false;
while (!validPosition && attempts < 10) {
potentialX = Math.random() * (GAME_WIDTH - 100) + 50;
validPosition = true;
// Check distance from existing enemies
for (var i = 0; i < enemies.length; i++) {
if (Math.abs(enemies[i].x - potentialX) < 150) {
validPosition = false;
break;
}
}
attempts++;
}
enemy.x = validPosition ? potentialX : Math.random() * (GAME_WIDTH - 100) + 50;
} else {
enemy.x = Math.random() * (GAME_WIDTH - 100) + 50;
}
enemy.y = -50;
// Increase difficulty with levels
if (gameLevel > 1) {
enemy.speed = Math.min(2 + gameLevel * 0.5, 6);
enemy.health = Math.min(1 + Math.floor(gameLevel / 3), 3);
}
if (gameLevel > 5) {
enemy.fireRate = Math.max(120 - gameLevel * 5, 60);
}
game.addChild(enemy);
enemies.push(enemy);
}
// Spawn power-up
function spawnPowerUp() {
var powerUp = new PowerUp();
powerUp.x = Math.random() * (GAME_WIDTH - 100) + 50;
powerUp.y = -50;
game.addChild(powerUp);
powerUps.push(powerUp);
}
// Check level progress
function checkLevelProgress() {
// Advance level based on score
var shouldAdvanceLevel = Math.floor(score / 1000) + 1;
if (shouldAdvanceLevel > gameLevel) {
gameLevel = shouldAdvanceLevel;
updateLevelDisplay();
// Flash screen to indicate level up
LK.effects.flashScreen(0x3498db, 500);
// Play level up sound
LK.getSound('levelUp').play();
}
}
// Handle collisions
function handleCollisions() {
// Check player bullets hitting enemies
for (var i = playerBullets.length - 1; i >= 0; i--) {
var bullet = playerBullets[i];
var bulletHit = false;
// Enemy bullet destruction completely removed
// Skip all bullet collision detection logic
// If player bullet was destroyed by enemy bullets, skip enemy collision check
if (bulletHit) {
continue;
}
// Now check for player bullet hitting enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
if (bullet.intersects(enemy)) {
// Enemy takes damage
if (enemy.takeDamage(bullet.power)) {
// Enemy destroyed
score += enemy.scoreValue;
LK.setScore(score);
updateScoreDisplay();
LK.getSound('explosion').play();
LK.effects.flashObject(enemy, 0xff0000, 300);
// Create explosion animation at enemy position
var explosion = new Explosion();
explosion.x = enemy.x;
explosion.y = enemy.y;
game.addChild(explosion);
explosions.push(explosion);
// Remove enemy
enemy.destroy();
enemies.splice(j, 1);
}
// Only remove non-piercing bullets on hit
if (!bullet.isPiercing) {
bullet.explode(); // Add explosion effect when bullet is destroyed
bullet.destroy();
playerBullets.splice(i, 1);
bulletHit = true;
break;
}
}
}
// If a non-piercing bullet hit something, skip to the next bullet
if (bulletHit) {
continue;
}
}
// Check enemy bullets hitting player
for (var i = enemyBullets.length - 1; i >= 0; i--) {
var bullet = enemyBullets[i];
// Get center points of both objects for more precise collision detection
var bulletCenterX = bullet.x;
var bulletCenterY = bullet.y;
var playerCenterX = player.x;
var playerCenterY = player.y;
// Calculate distance between centers
var dx = bulletCenterX - playerCenterX;
var dy = bulletCenterY - playerCenterY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Get the combined radius (half the width of both objects for a more precise collision)
var bulletRadius = bullet.width / 3.5; // Much smaller hitbox for accurate collision
var playerRadius = player.width / 3.5; // Much smaller hitbox for accurate collision
var combinedRadius = bulletRadius + playerRadius;
// Only register hit if the bullet is actually touching the player (more precise collision)
if (distance < combinedRadius) {
// Player hit
bullet.destroy();
enemyBullets.splice(i, 1);
if (player.shieldActive) {
// Shield absorbs the hit
// Play explosion sound for shield hit
LK.getSound('explosion').play();
player.shieldActive = false;
var shield = player.getChildAt(1);
shield.alpha = 0;
} else {
// Play sound when player gets hit by enemy bullet
LK.getSound('playerHit').play();
// Reduce player health
player.health--;
player.healthBar.updateHealth(player.health);
// Flash player to indicate damage
LK.effects.flashObject(player, 0xff0000, 300);
// Check if player health reaches 1 (final life)
if (player.health === 1 && !player.finalLifeWarningActive) {
player.finalLifeWarningActive = true;
// Hide health bar completely
player.healthBar.visible = false;
// Show and animate "Final Life" text
var finalLifeText = player.getChildAt(2); // Get the final life text
finalLifeText.alpha = 1;
// Create flashing effect for the text
var _flashTween2 = function flashTween() {
tween(finalLifeText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tween(finalLifeText, {
alpha: 1
}, {
duration: 500,
onFinish: _flashTween2
});
}
});
};
_flashTween2(); // Start the flashing animation
}
// Check if player is out of health
if (player.health <= 0) {
// Game over
LK.effects.flashScreen(0xff0000, 1000);
isGameActive = false;
// Stop music with fade out
LK.playMusic('gameMusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// Play game over sound effect
LK.getSound('gameOver').play();
LK.showGameOver();
break;
}
}
}
}
// Check enemies colliding with player
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
if (enemy.intersects(player)) {
if (player.shieldActive) {
// Shield absorbs the hit
// Play collision sound when enemy collides with shield
LK.getSound('shieldCollision').play();
enemy.destroy();
enemies.splice(i, 1);
// Shield gets depleted
player.shieldActive = false;
var shield = player.getChildAt(1);
shield.alpha = 0;
score += enemy.scoreValue;
LK.setScore(score);
updateScoreDisplay();
} else {
// Reduce player health by 2 (collision is more dangerous)
player.health -= 2;
player.healthBar.updateHealth(player.health);
// Play collision sound when player collides with enemy
LK.getSound('playerCollision').play();
// Remove the enemy
enemy.destroy();
enemies.splice(i, 1);
// Flash player to indicate damage
LK.effects.flashObject(player, 0xff0000, 300);
// Check if player health reaches 1 (final life)
if (player.health === 1 && !player.finalLifeWarningActive) {
player.finalLifeWarningActive = true;
// Hide health bar completely
player.healthBar.visible = false;
// Show and animate "Final Life" text
var finalLifeText = player.getChildAt(2); // Get the final life text
finalLifeText.alpha = 1;
// Create flashing effect for the text
var _flashTween2 = function _flashTween() {
tween(finalLifeText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tween(finalLifeText, {
alpha: 1
}, {
duration: 500,
onFinish: _flashTween2
});
}
});
};
_flashTween2(); // Start the flashing animation
}
// Check if player is out of health
if (player.health <= 0) {
// Game over
LK.effects.flashScreen(0xff0000, 1000);
isGameActive = false;
LK.showGameOver();
break;
}
}
}
}
// Check player collecting power-ups
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
if (powerUp.intersects(player)) {
LK.getSound('powerUp').play();
// Apply power-up effect
if (powerUp.type === 'health') {
// Health regeneration logic
if (player.health < player.healthBar.maxHealth) {
// Add 1 health point
player.health = Math.min(player.health + 1, player.healthBar.maxHealth);
// If player was at final life, restore health bar and remove warning
if (player.finalLifeWarningActive && player.health > 1) {
player.finalLifeWarningActive = false;
player.healthBar.visible = true;
// Get the final life text and hide it
var finalLifeText = player.getChildAt(2);
// Stop any existing tweens on the final life text
tween.stop(finalLifeText);
finalLifeText.alpha = 0;
}
// Update health bar
player.healthBar.updateHealth(player.health);
}
} else if (powerUp.type === 'weapon') {
player.activatePowerUp();
} else if (powerUp.type === 'quadBullets') {
player.activateQuadBullets();
}
// Remove power-up
powerUp.destroy();
powerUps.splice(i, 1);
}
}
}
// Clean up off-screen objects
function cleanupOffscreenObjects() {
// Clean up missiles that went off screen
for (var i = missiles.length - 1; i >= 0; i--) {
var missile = missiles[i];
if (missile.y < -50 || missile.y > GAME_HEIGHT + 50 || missile.x < -50 || missile.x > GAME_WIDTH + 50) {
missile.explode();
missile.destroy();
missiles.splice(i, 1);
}
}
// Clean up bullets that went off screen
for (var i = playerBullets.length - 1; i >= 0; i--) {
var bullet = playerBullets[i];
// Check if bullet is off screen based on direction (unless it's bouncing)
if (!bullet.isBouncing) {
if (bullet.direction === 'up' && bullet.y < -50 || bullet.direction === 'down' && bullet.y > GAME_HEIGHT + 50 || bullet.direction === 'left' && bullet.x < -50 || bullet.direction === 'right' && bullet.x > GAME_WIDTH + 50) {
bullet.destroy();
playerBullets.splice(i, 1);
}
} else if (bullet.bounceCount >= bullet.maxBounces) {
// Add explosion for bouncing bullets that reached max bounce count
bullet.explode();
// Play explosion sound when bullet is removed at max bounce count
LK.getSound('bulletExplosion').play();
bullet.destroy();
playerBullets.splice(i, 1);
}
}
for (var i = enemyBullets.length - 1; i >= 0; i--) {
if (enemyBullets[i].y > GAME_HEIGHT + 50) {
enemyBullets[i].destroy();
enemyBullets.splice(i, 1);
}
}
// Clean up enemies that went off screen
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i].y > GAME_HEIGHT + 100) {
enemies[i].destroy();
enemies.splice(i, 1);
}
}
// Clean up power-ups that went off screen
for (var i = powerUps.length - 1; i >= 0; i--) {
if (powerUps[i].y > GAME_HEIGHT + 50) {
powerUps[i].destroy();
powerUps.splice(i, 1);
}
}
}
// Player movement
var isDragging = false;
game.down = function (x, y) {
// Fire missile on left click if cooldown is ready
if (LK.ticks - lastMissileTime >= 600) {
// 10 seconds at 60fps
// Find closest enemy as target first
var closestEnemy = null;
var closestDistance = Number.MAX_VALUE;
var potentialTargets = [];
// Find up to 6 potential targets
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
var dx = enemy.x - player.x;
var dy = enemy.y - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemy;
}
// Add to potential targets list (sort by proximity later)
potentialTargets.push({
enemy: enemy,
distance: distance
});
}
// Sort targets by distance
potentialTargets.sort(function (a, b) {
return a.distance - b.distance;
});
// Launch three missiles from the same vertical position but different horizontal positions
// Center missile - positioned exactly in the center of player ship
var centerMissile = new Missile();
centerMissile.x = player.x; // Center horizontally with player
centerMissile.y = player.y - 60; // Launch from front center of player ship
centerMissile.turnSpeed = 0.05; // Very slight homing for center missile
centerMissile.initialDirection = 'up'; // Make sure it exits straight up first
// Left missile - positioned to the left of center missile
var leftMissile = new Missile();
leftMissile.x = player.x - 30; // Position left of the player
leftMissile.y = player.y - 60; // Same vertical position as center missile
leftMissile.angle = -0.3; // Slight left angle for initial trajectory
leftMissile.turnSpeed = 0.07; // Reduced homing for more separation
leftMissile.initialDirection = 'left-up'; // Make sure it exits left-up first
// Right missile - positioned to the right of center missile
var rightMissile = new Missile();
rightMissile.x = player.x + 30; // Position right of the player
rightMissile.y = player.y - 60; // Same vertical position as center missile
rightMissile.angle = 0.3; // Slight right angle for initial trajectory
rightMissile.turnSpeed = 0.07; // Reduced homing for more separation
rightMissile.initialDirection = 'right-up'; // Make sure it exits right-up first
// If we found a target, set it for missiles with different offsets to avoid pairing
if (closestEnemy) {
// Set slightly different targets for each missile to avoid pairing
centerMissile.targetX = closestEnemy.x;
centerMissile.targetY = closestEnemy.y;
// For side missiles, either use different enemies or offset from main target
if (potentialTargets.length > 1) {
leftMissile.targetX = potentialTargets[Math.min(1, potentialTargets.length - 1)].enemy.x - 50;
leftMissile.targetY = potentialTargets[Math.min(1, potentialTargets.length - 1)].enemy.y;
} else {
// If not enough targets, offset significantly from main target
leftMissile.targetX = closestEnemy.x - 150;
leftMissile.targetY = closestEnemy.y;
}
if (potentialTargets.length > 2) {
rightMissile.targetX = potentialTargets[Math.min(2, potentialTargets.length - 1)].enemy.x + 50;
rightMissile.targetY = potentialTargets[Math.min(2, potentialTargets.length - 1)].enemy.y;
} else {
// If not enough targets, offset significantly from main target
rightMissile.targetX = closestEnemy.x + 150;
rightMissile.targetY = closestEnemy.y;
}
}
// Add first wave of missiles to game
game.addChild(centerMissile);
missiles.push(centerMissile);
game.addChild(leftMissile);
missiles.push(leftMissile);
game.addChild(rightMissile);
missiles.push(rightMissile);
// Create a second wave of missiles with slightly different launch positions
// Second center missile
var centerMissile2 = new Missile();
centerMissile2.x = player.x;
centerMissile2.y = player.y - 30; // Slightly higher than first wave
centerMissile2.turnSpeed = 0.06;
centerMissile2.initialDirection = 'up';
// Second left missile
var leftMissile2 = new Missile();
leftMissile2.x = player.x - 40; // Different horizontal offset
leftMissile2.y = player.y - 30;
leftMissile2.angle = -0.4; // Different initial angle
leftMissile2.turnSpeed = 0.08;
leftMissile2.initialDirection = 'left-up';
// Second right missile
var rightMissile2 = new Missile();
rightMissile2.x = player.x + 40; // Different horizontal offset
rightMissile2.y = player.y - 30;
rightMissile2.angle = 0.4; // Different initial angle
rightMissile2.turnSpeed = 0.08;
rightMissile2.initialDirection = 'right-up';
// Set targets for second wave
if (closestEnemy) {
// Set slightly different targets for second wave missiles
if (potentialTargets.length > 3) {
centerMissile2.targetX = potentialTargets[3].enemy.x;
centerMissile2.targetY = potentialTargets[3].enemy.y;
} else {
centerMissile2.targetX = closestEnemy.x + 50;
centerMissile2.targetY = closestEnemy.y + 50;
}
if (potentialTargets.length > 4) {
leftMissile2.targetX = potentialTargets[4].enemy.x - 70;
leftMissile2.targetY = potentialTargets[4].enemy.y;
} else {
leftMissile2.targetX = closestEnemy.x - 200;
leftMissile2.targetY = closestEnemy.y + 50;
}
if (potentialTargets.length > 5) {
rightMissile2.targetX = potentialTargets[5].enemy.x + 70;
rightMissile2.targetY = potentialTargets[5].enemy.y;
} else {
rightMissile2.targetX = closestEnemy.x + 200;
rightMissile2.targetY = closestEnemy.y + 50;
}
}
// Add second wave of missiles to game
game.addChild(centerMissile2);
missiles.push(centerMissile2);
game.addChild(leftMissile2);
missiles.push(leftMissile2);
game.addChild(rightMissile2);
missiles.push(rightMissile2);
// Play missile launch sound
LK.getSound('missileLaunch').play();
// Reset cooldown
lastMissileTime = LK.ticks;
// Hide missile ready text and stop any active tweens
tween.stop(missileReadyText);
missileReadyText.visible = false;
} //[8F]
// Handle regular player movement
var targetX = x;
var targetY = y;
// Clamp target position to game bounds
if (targetX < 50) {
targetX = 50;
}
if (targetX > GAME_WIDTH - 50) {
targetX = GAME_WIDTH - 50;
}
if (targetY < 50) {
targetY = 50;
}
if (targetY > GAME_HEIGHT - 50) {
targetY = GAME_HEIGHT - 50;
}
player.targetX = targetX;
player.targetY = targetY;
player.isMoving = true;
// Also shoot when player taps
playerShoot();
};
game.move = function (x, y) {
if (isGameActive) {
// Clamp target position to game bounds
var targetX = x;
var targetY = y;
if (targetX < 50) {
targetX = 50;
}
if (targetX > GAME_WIDTH - 50) {
targetX = GAME_WIDTH - 50;
}
if (targetY < 50) {
targetY = 50;
}
if (targetY > GAME_HEIGHT - 50) {
targetY = GAME_HEIGHT - 50;
}
// Update target position
player.targetX = targetX;
player.targetY = targetY;
// For better tracking during fast mouse movements, directly update position
player.x = player.targetX;
player.y = player.targetY;
player.isMoving = true; //{2K}{2L}{2M}{2N}
}
};
game.up = function () {
// Keep the ship moving toward last target point
};
// Main game update loop
game.update = function () {
if (!isGameActive) {
return;
}
// Update missile ready text
if (LK.ticks - lastMissileTime >= 600) {
// 10 seconds cooldown
if (!missileReadyText.visible) {
missileReadyText.visible = true;
missileReadyText.x = player.x;
missileReadyText.y = player.y + 110;
missileReadyText.alpha = 1;
// Flash the text when ready
var _flashMissileText2 = function flashMissileText() {
tween(missileReadyText, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tween(missileReadyText, {
alpha: 1
}, {
duration: 500,
onFinish: _flashMissileText2
});
}
});
};
_flashMissileText2();
}
}
// Spawn enemies
spawnCounter++;
var currentSpawnRate = Math.max(SPAWN_ENEMY_INTERVAL - gameLevel * 5, MIN_SPAWN_INTERVAL);
if (spawnCounter >= currentSpawnRate) {
spawnEnemy();
spawnCounter = 0;
}
// Spawn power-ups
powerUpCounter++;
if (powerUpCounter >= SPAWN_POWERUP_INTERVAL) {
spawnPowerUp();
powerUpCounter = 0;
}
// Make player shoot automatically
if (player.isMoving) {
playerShoot();
}
// Update enemies and their shooting
for (var i = enemies.length - 1; i >= 0; i--) {
// Check if the enemy should be removed (returned true from update)
if (enemies[i].update && enemies[i].update() === true) {
enemies.splice(i, 1);
continue;
}
enemyShoot(enemies[i]);
}
// Update missiles
for (var i = missiles.length - 1; i >= 0; i--) {
var missile = missiles[i];
// If missile doesn't have a target or target is destroyed, find new target
if ((missile.targetX === null || missile.targetY === null) && enemies.length > 0) {
// Find closest enemy as new target
var closestEnemy = null;
var closestDistance = Number.MAX_VALUE;
// Check all enemies for potential targets
for (var k = 0; k < enemies.length; k++) {
var dx = enemies[k].x - missile.x;
var dy = enemies[k].y - missile.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
closestEnemy = enemies[k];
}
}
// If we found a target, set it with slight offset based on missile type
if (closestEnemy) {
// Add slightly different offsets to each missile's target to maintain separation
// We can determine missile "type" by its position relative to player
var offsetX = 0;
var offsetY = 0; // Adding vertical offset for better separation
// Using angle to identify missile type
if (missile.angle < 0) {
// Left missile
offsetX = -100; // Increased horizontal offset
offsetY = -20; // Slight vertical offset
} else if (missile.angle > 0) {
// Right missile
offsetX = 100; // Increased horizontal offset
offsetY = -20; // Slight vertical offset
} else {
// Center missile
offsetY = -50; // Different vertical offset for center missile
}
missile.targetX = closestEnemy.x + offsetX;
missile.targetY = closestEnemy.y + offsetY;
}
}
// Check if missile hits an enemy
var missileHit = false;
for (var j = enemies.length - 1; j >= 0; j--) {
if (missile.intersects(enemies[j])) {
missile.explode();
missile.destroy();
missiles.splice(i, 1);
missileHit = true;
break;
}
}
// Check if missile is off screen
if (!missileHit && (missile.y < -50 || missile.y > GAME_HEIGHT + 50 || missile.x < -50 || missile.x > GAME_WIDTH + 50)) {
missile.explode();
missile.destroy();
missiles.splice(i, 1);
}
}
// Update explosions
for (var i = explosions.length - 1; i >= 0; i--) {
if (explosions[i].update()) {
explosions.splice(i, 1);
}
}
// Update fire pools
for (var i = firePools.length - 1; i >= 0; i--) {
if (firePools[i].update()) {
firePools.splice(i, 1);
}
}
// Check collisions
handleCollisions();
// Clean up off-screen objects
cleanupOffscreenObjects();
// Check level progression
checkLevelProgress();
// Win condition check
if (score >= 10000) {
// Play win sound effect
LK.getSound('winSound').play();
// Stop music with fade out
LK.playMusic('gameMusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
LK.showYouWin();
isGameActive = false;
}
};
An enemy spaceship in a topdown shooter. In-Game asset. 2d. High contrast. No shadows
A player's spaceship in a topdown shooter (mainly blue in colour). In-Game asset. 2d. High contrast. No shadows
A powerup in a top-down shooter (icon of powerup no text). In-Game asset. 2d. High contrast. No shadows
Health Powerup in 2d space shooter (top down). In-Game asset. 2d. High contrast. No shadows
An explosion which contains debris of a spaceship. In-Game asset. 2d. High contrast. No shadows
pool of fire with top down view. In-Game asset. 2d. High contrast. No shadows
Make a small circle that looks like a laser (red colour). In-Game asset. 2d. High contrast. No shadows
Make a very small circle that looks like a laser (blue colour). In-Game asset. 2d. High contrast. No shadows. In-Game asset. 2d. High contrast. No shadows
A missile. In-Game asset. 2d. High contrast. No shadows