/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// New armored enemy ship: slower, more health, different sprite
var ArmoredEnemyShip = Container.expand(function () {
var self = Container.call(this);
self.fireCooldown = 120; // Fires more often than normal enemy
self.fireTimer = Math.random() * self.fireCooldown;
self.enemyProjectileSpawnPoint = {
x: 0,
y: 0
};
self.hit = function () {
if (self.isDestroyed) return;
self.health -= 1;
if (self.health <= 0) {
self.isDestroyed = true;
} else {
// Flash magenta on hit
if (self.shipSprite) LK.effects.flashObject(self.shipSprite, 0xe011a9, 80);
}
};
self.update = function () {
if (self.isOffScreen || self.isDestroyed) return;
if (typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
// Target the player ship
if (typeof playerShip !== 'undefined' && playerShip && !playerShip.isDestroyed) {
var dx = playerShip.x - self.x;
var dy = playerShip.y - self.y;
var targetAngle = Math.atan2(dy, dx);
self.angle = targetAngle;
if (self.shipSprite) self.shipSprite.rotation = self.angle + Math.PI / 2;
}
self.lastX = self.x;
self.lastY = self.y;
// Armored enemy moves slower
var moveX = Math.cos(self.angle) * self.speed;
var moveY = Math.sin(self.angle) * self.speed;
if (typeof playerShip !== 'undefined' && playerShip && !playerShip.isDestroyed) {
var dxToPlayer = self.x + moveX - playerShip.x;
var dyToPlayer = self.y + moveY - playerShip.y;
var distToPlayer = Math.sqrt(dxToPlayer * dxToPlayer + dyToPlayer * dyToPlayer);
if (distToPlayer < 120) {
moveX = 0;
moveY = 0;
}
}
self.x += moveX;
self.y += moveY;
var shipAsset = self.shipSprite;
if (shipAsset) {
var noseDistance = shipAsset.height / 2;
self.enemyProjectileSpawnPoint.x = self.x + Math.cos(self.angle) * noseDistance;
self.enemyProjectileSpawnPoint.y = self.y + Math.sin(self.angle) * noseDistance;
}
self.fireTimer--;
if (self.fireTimer <= 0) {
if (typeof EnemyProjectile !== 'undefined' && typeof enemyProjectiles !== 'undefined' && game && typeof game.addChild === 'function') {
var newProjectile = new EnemyProjectile(self.angle);
newProjectile.x = self.enemyProjectileSpawnPoint.x;
newProjectile.y = self.enemyProjectileSpawnPoint.y;
enemyProjectiles.push(newProjectile);
game.addChild(newProjectile);
self.fireTimer = self.fireCooldown;
}
}
var gameWidth = 2048;
var gameHeight = 2732;
var marginWidth = self.shipSprite && self.shipSprite.width ? self.shipSprite.width / 2 + 50 : 100;
var marginHeight = self.shipSprite && self.shipSprite.height ? self.shipSprite.height / 2 + 50 : 100;
if (self.x < -marginWidth || self.x > gameWidth + marginWidth || self.y < -marginHeight || self.y > gameHeight + marginHeight) {
self.isOffScreen = true;
}
};
// Use a magenta box for armored enemy
self.shipSprite = self.attachAsset('Armoredenyme', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = Math.PI / 2;
if (self.shipSprite) self.shipSprite.rotation = self.angle + Math.PI / 2;
self.speed = 2.5; // Slower than normal enemy
self.isOffScreen = false;
self.lastX = self.x;
self.lastY = self.y;
self.health = 3; // Armored enemy starts with 3 HP
self.isDestroyed = false;
return self;
});
var BossCannon = Container.expand(function (parentBoss) {
var self = Container.call(this);
self.parentBoss = parentBoss;
self.health = 15; // Cannons are a bit tough
self.isDestroyed = false;
self.fireCooldown = 90; // Fires every 1.5 seconds
self.fireTimer = Math.random() * self.fireCooldown;
self.cannonSprite = self.attachAsset('Bossgun', {
anchorX: 0.5,
anchorY: 0.5
});
self.hit = function (damage) {
if (self.isDestroyed) return;
self.health -= damage;
LK.effects.flashObject(self.cannonSprite, 0xff8888, 100);
if (self.health <= 0) {
self.isDestroyed = true;
self.visible = false;
// Play a smaller explosion for cannon destruction
var explosionX = self.parentBoss.x + self.x;
var explosionY = self.parentBoss.y + self.y;
var boomImg = LK.getAsset('Boom3', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
y: explosionY
});
game.addChild(boomImg);
LK.setTimeout(function () {
if (boomImg && typeof boomImg.destroy === 'function') boomImg.destroy();
}, 500);
LK.getSound('Boom').play();
}
};
self.update = function () {
if (self.isDestroyed || typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
// Aim at player
if (playerShip && !playerShip.isDestroyed) {
var globalCannonX = self.parentBoss.x + self.x;
var globalCannonY = self.parentBoss.y + self.y;
var dx = playerShip.x - globalCannonX;
var dy = playerShip.y - globalCannonY;
var angleToPlayer = Math.atan2(dy, dx);
self.cannonSprite.rotation = angleToPlayer + Math.PI / 2; // Assuming sprite points "up"
self.fireTimer--;
if (self.fireTimer <= 0) {
var noseOffset = self.cannonSprite.height / 2;
var projSpawnX = globalCannonX + Math.cos(angleToPlayer) * noseOffset;
var projSpawnY = globalCannonY + Math.sin(angleToPlayer) * noseOffset;
var newProjectile = new EnemyProjectile(angleToPlayer);
newProjectile.x = projSpawnX;
newProjectile.y = projSpawnY;
enemyProjectiles.push(newProjectile);
game.addChild(newProjectile);
self.fireTimer = self.fireCooldown;
}
}
};
return self;
});
var BossEnemy = Container.expand(function () {
var self = Container.call(this);
self.isDestroyed = false;
self.mainBody = new BossMainBody(self);
self.addChild(self.mainBody);
self.mainBody.x = 0;
self.mainBody.y = 0;
self.leftCannon = new BossCannon(self);
self.addChild(self.leftCannon);
self.rightCannon = new BossCannon(self);
self.addChild(self.rightCannon);
// Position cannons relative to the main body
// Ensure sprites are attached to get dimensions
var mainBodyWidth = self.mainBody.bodySprite.width;
var cannonWidth = self.leftCannon.cannonSprite.width; // Both cannons use same asset
self.leftCannon.x = -(mainBodyWidth / 2) - cannonWidth / 2 - 20; // 20px spacing
self.leftCannon.y = 0;
self.rightCannon.x = mainBodyWidth / 2 + cannonWidth / 2 + 20; // 20px spacing
self.rightCannon.y = 0;
self.update = function () {
if (self.isDestroyed || typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
if (self.mainBody && !self.mainBody.isDestroyed) {
self.mainBody.update();
}
if (self.leftCannon && !self.leftCannon.isDestroyed) {
self.leftCannon.update();
}
if (self.rightCannon && !self.rightCannon.isDestroyed) {
self.rightCannon.update();
}
};
return self;
});
var BossMainBody = Container.expand(function (parentBoss) {
var self = Container.call(this);
self.parentBoss = parentBoss;
self.health = 50; // Main body is very tough
self.isDestroyed = false;
self.bodySprite = self.attachAsset('BossBodyAsset', {
anchorX: 0.5,
anchorY: 0.5
});
self.hit = function (damage) {
if (self.isDestroyed) return;
self.health -= damage;
LK.effects.flashObject(self.bodySprite, 0xff6666, 150);
if (self.health <= 0) {
self.isDestroyed = true;
self.visible = false;
self.parentBoss.isDestroyed = true; // Main body destroyed means boss is defeated
// Trigger larger explosion sequence for boss defeat in BossEnemy or game.update
}
};
self.update = function () {
if (self.isDestroyed || typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
// Potential movement or special attacks later
};
return self;
});
var EnemyProjectile = Container.expand(function (fireAngle) {
var self = Container.call(this);
self.bulletSprite = self.attachAsset('enemyBulletSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = fireAngle; // Angle of movement
self.speed = 10; // Enemy projectiles are a bit slower
if (self.bulletSprite) {
// Assuming the bullet shape is wider than tall, rotating by angle aligns its length with movement.
self.bulletSprite.rotation = self.angle;
}
self.isOffScreen = false;
self.update = function () {
if (self.isOffScreen) return;
if (typeof freezeEnemies !== 'undefined' && freezeEnemies) return; // Freeze enemy projectiles during card choice
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
var gameWidth = 2048;
var gameHeight = 2732;
// Margin based on projectile size to ensure it's fully off-screen
var margin = 50; // Default margin
if (self.bulletSprite && (self.bulletSprite.width || self.bulletSprite.height)) {
margin = Math.max(self.bulletSprite.width || 0, self.bulletSprite.height || 0) / 2 + 50;
}
if (self.x < -margin || self.x > gameWidth + margin || self.y < -margin || self.y > gameHeight + margin) {
self.isOffScreen = true;
}
};
return self;
});
// Orange rectangular bullet
var EnemyShip = Container.expand(function () {
var self = Container.call(this);
self.fireCooldown = 180; // Fire every 3 seconds (180 ticks at 60FPS)
self.fireTimer = Math.random() * self.fireCooldown; // Stagger initial firing
self.enemyProjectileSpawnPoint = {
x: 0,
y: 0
};
self.hit = function () {
if (self.isDestroyed) return; // Already destroyed
self.health--;
if (self.health <= 0) {
self.isDestroyed = true;
// Optionally, trigger a small visual effect here like a flash
// LK.effects.flashObject(self, 0xffffff, 100);
} else {
// Optionally, visual effect for taking damage but not destroyed
// LK.effects.flashObject(self, 0xffaaaa, 50);
}
};
self.update = function () {
if (self.isOffScreen || self.isDestroyed) return; // Don't update if off-screen or destroyed
if (typeof freezeEnemies !== 'undefined' && freezeEnemies) return; // Freeze all enemy movement and firing during card choice
// Target the player ship
if (typeof playerShip !== 'undefined' && playerShip && !playerShip.isDestroyed) {
var dx = playerShip.x - self.x;
var dy = playerShip.y - self.y;
// Calculate angle towards player
var targetAngle = Math.atan2(dy, dx);
// Update the enemy's angle to face the player
self.angle = targetAngle;
// Update sprite rotation to match the new angle
// Assuming the ship sprite is designed "pointing up" (nose along its local -Y axis or top)
// visual rotation = world angle + PI/2.
if (self.shipSprite) {
self.shipSprite.rotation = self.angle + Math.PI / 2;
}
}
// If playerShip is not available or is destroyed, the enemy will continue in its current self.angle.
self.lastX = self.x;
self.lastY = self.y;
// Prevent enemy from moving too close to the player
var safeDistance = 120; // Minimum allowed distance between enemy and player
var moveX = Math.cos(self.angle) * self.speed;
var moveY = Math.sin(self.angle) * self.speed;
if (typeof playerShip !== 'undefined' && playerShip && !playerShip.isDestroyed) {
var dxToPlayer = self.x + moveX - playerShip.x;
var dyToPlayer = self.y + moveY - playerShip.y;
var distToPlayer = Math.sqrt(dxToPlayer * dxToPlayer + dyToPlayer * dyToPlayer);
if (distToPlayer < safeDistance) {
// If moving would bring us too close, do not move this frame
moveX = 0;
moveY = 0;
}
}
self.x += moveX;
self.y += moveY;
// Update enemy projectile spawn point
var shipAsset = self.shipSprite;
if (shipAsset) {
var noseDistance = shipAsset.height / 2; // Assuming front is along height axis from center
self.enemyProjectileSpawnPoint.x = self.x + Math.cos(self.angle) * noseDistance;
self.enemyProjectileSpawnPoint.y = self.y + Math.sin(self.angle) * noseDistance;
}
// Firing logic
self.fireTimer--;
if (self.fireTimer <= 0) {
if (typeof EnemyProjectile !== 'undefined' && typeof enemyProjectiles !== 'undefined' && game && typeof game.addChild === 'function') {
var newProjectile = new EnemyProjectile(self.angle); // Fire in the direction the ship is moving
newProjectile.x = self.enemyProjectileSpawnPoint.x;
newProjectile.y = self.enemyProjectileSpawnPoint.y;
enemyProjectiles.push(newProjectile);
game.addChild(newProjectile);
self.fireTimer = self.fireCooldown; // Reset cooldown
}
}
// Generalized off-screen check
var gameWidth = 2048;
var gameHeight = 2732;
var marginWidth = self.shipSprite && self.shipSprite.width ? self.shipSprite.width / 2 + 50 : 100;
var marginHeight = self.shipSprite && self.shipSprite.height ? self.shipSprite.height / 2 + 50 : 100;
if (self.x < -marginWidth || self.x > gameWidth + marginWidth || self.y < -marginHeight || self.y > gameHeight + marginHeight) {
self.isOffScreen = true;
}
};
self.shipSprite = self.attachAsset('enemyShipSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = Math.PI / 2; // Default angle: moving downwards (positive Y direction)
if (self.shipSprite) {
self.shipSprite.rotation = self.angle + Math.PI / 2;
}
self.speed = 4;
self.isOffScreen = false;
self.lastX = self.x;
self.lastY = self.y;
self.health = 1;
self.isDestroyed = false;
return self;
});
var FireButton = Container.expand(function () {
var self = Container.call(this);
self.buttonSprite = self.attachAsset('fireButtonSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastFireTick = -1000; // Track last tick when fired
self.down = function (x, y, obj) {
// Prevent holding down for auto-fire: only allow firing if enough time has passed since last fire
if (typeof LK !== 'undefined' && typeof LK.ticks !== 'undefined') {
if (typeof self.lastFireTick === 'undefined') self.lastFireTick = -1000;
// Only allow firing if at least fireButtonCooldown ticks have passed since last fire
if (typeof fireButtonCooldown === 'undefined') fireButtonCooldown = 30;
if (LK.ticks - self.lastFireTick < fireButtonCooldown) return;
}
// Only fire if player has ammo and not holding down for auto-fire
if (typeof playerAmmo !== 'undefined' && playerAmmo > 0) {
if (playerShip && typeof playerShip.currentAngle !== 'undefined' && typeof projectileSpawnPoint !== 'undefined' && playerProjectiles && PlayerProjectile) {
if (typeof playerSplitShotActive !== 'undefined' && playerSplitShotActive) {
var spreadAngle = Math.PI / 10; // 18 degrees spread for each projectile from center
// Projectile 1 (left/upward component of spread)
var proj1 = new PlayerProjectile(playerShip.currentAngle - spreadAngle);
proj1.x = projectileSpawnPoint.x;
proj1.y = projectileSpawnPoint.y;
playerProjectiles.push(proj1);
game.addChild(proj1);
// Projectile 2 (right/downward component of spread)
var proj2 = new PlayerProjectile(playerShip.currentAngle + spreadAngle);
proj2.x = projectileSpawnPoint.x;
proj2.y = projectileSpawnPoint.y;
playerProjectiles.push(proj2);
game.addChild(proj2);
} else {
// Single projectile (default behavior)
var newProjectile = new PlayerProjectile(playerShip.currentAngle);
newProjectile.x = projectileSpawnPoint.x;
newProjectile.y = projectileSpawnPoint.y;
playerProjectiles.push(newProjectile);
game.addChild(newProjectile);
}
playerAmmo = Math.max(0, playerAmmo - 1); // Decrease ammo by 1, never below 0
// Removed ammo counter update as per requirements
if (typeof LK !== 'undefined' && typeof LK.ticks !== 'undefined') {
self.lastFireTick = LK.ticks;
}
// Play shot sound when firing
if (typeof LK !== 'undefined' && typeof LK.getSound === 'function') {
var shotSound = LK.getSound('Shot');
if (shotSound && typeof shotSound.play === 'function') {
shotSound.play();
}
}
}
}
};
return self;
});
var HealthRestore = Container.expand(function () {
var self = Container.call(this);
self.restoreSprite = self.attachAsset('healthRestoreSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Check for collision with player ship
if (playerShip && !playerShip.isDestroyed && self.intersects(playerShip)) {
if (playerShip.health < 3) {
playerShip.health = Math.min(playerShip.health + 1, 3); // Restore health, max 3
if (playerShip.shipSprite) {
LK.effects.flashObject(playerShip.shipSprite, 0x00ff00, 150); // Flash green on health restore
}
// Update hearts display
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < playerShip.health;
}
}
self.destroy();
}
};
return self;
});
var Heart = Container.expand(function () {
var self = Container.call(this);
self.heartSprite = self.attachAsset('heartSprite', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
return self;
});
// PlayerShip class to handle player ship logic and projectile spawn point calculation
var HomingMissile = Container.expand(function (initialAngle) {
var self = Container.call(this);
self.missileSprite = self.attachAsset('HomingMissileSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = initialAngle;
self.speed = 7;
self.turnRate = 0.05; // Radians per tick
self.isOffScreen = false;
self.lastX = self.x;
self.lastY = self.y;
if (self.missileSprite) {
// Ensure sprite exists before setting rotation
self.missileSprite.rotation = self.angle; // Assuming sprite points right (0 rad) initially
}
self.update = function () {
if (self.isOffScreen || typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
if (playerShip && !playerShip.isDestroyed) {
var targetAngle = Math.atan2(playerShip.y - self.y, playerShip.x - self.x);
var angleDiff = targetAngle - self.angle;
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
var turnAmount = Math.sign(angleDiff) * Math.min(self.turnRate, Math.abs(angleDiff));
self.angle += turnAmount;
}
if (self.missileSprite) self.missileSprite.rotation = self.angle;
self.lastX = self.x;
self.lastY = self.y;
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
var gameWidth = 2048;
var gameHeight = 2732;
var margin = (self.missileSprite && self.missileSprite.width ? self.missileSprite.width / 2 : 25) + 50;
if (self.x < -margin || self.x > gameWidth + margin || self.y < -margin || self.y > gameHeight + margin) {
self.isOffScreen = true;
}
};
return self;
});
// Red circular fire button
var Joystick = Container.expand(function () {
var self = Container.call(this);
self.baseSprite = self.attachAsset('joystickBaseSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.knobSprite = self.attachAsset('joystickKnobSprite', {
anchorX: 0.5,
anchorY: 0.5
});
// Maximum distance the knob's center can move from the base's center
self.radius = self.baseSprite.width / 2 - self.knobSprite.width / 2;
if (self.radius <= 0) {
// Ensure a sensible minimum radius
self.radius = self.baseSprite.width > 0 ? self.baseSprite.width / 4 : 50; // Use 1/4 of base width or 50 if base has no width
}
self.isDragging = false;
self.inputVector = {
x: 0,
y: 0
}; // Normalized output (-1 to 1)
self.handleDown = function (localX, localY) {
// localX, localY are relative to the joystick's center (its origin)
// Check if the touch is within the larger base area to start dragging
var distSqFromCenter = localX * localX + localY * localY;
if (distSqFromCenter <= self.baseSprite.width / 2 * (self.baseSprite.width / 2)) {
self.isDragging = true;
self.handleMove(localX, localY); // Snap knob to initial touch position
return true; // Indicates joystick took control
}
return false; // Joystick not activated
};
self.handleMove = function (localX, localY) {
if (!self.isDragging) return;
var dist = Math.sqrt(localX * localX + localY * localY);
if (dist > self.radius) {
// Normalize and scale to radius if touch is outside draggable area
self.knobSprite.x = localX / dist * self.radius;
self.knobSprite.y = localY / dist * self.radius;
} else {
self.knobSprite.x = localX;
self.knobSprite.y = localY;
}
// Calculate normalized input vector
if (self.radius > 0) {
self.inputVector.x = self.knobSprite.x / self.radius;
self.inputVector.y = self.knobSprite.y / self.radius;
} else {
// Avoid division by zero if radius is zero
self.inputVector.x = 0;
self.inputVector.y = 0;
}
};
self.handleUp = function () {
if (self.isDragging) {
self.isDragging = false;
// Reset knob to center and clear input vector
self.knobSprite.x = 0;
self.knobSprite.y = 0;
self.inputVector.x = 0;
self.inputVector.y = 0;
}
};
self.getInput = function () {
return self.inputVector;
};
self.isActive = function () {
return self.isDragging;
};
return self;
});
// Projectile class for player's bullets
var PlayerProjectile = Container.expand(function (fireAngle) {
var self = Container.call(this);
// Attach bullet sprite
self.bulletSprite = self.attachAsset('playerBulletSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = fireAngle; // Store the firing angle
self.speed = 20; // Projectile speed magnitude
// The playerBulletSprite has orientation:1, meaning it's rotated 90deg clockwise.
// If original was thin vertical, it's now thin horizontal, "pointing" along its X-axis.
// So, self.angle directly applies.
self.bulletSprite.rotation = self.angle;
self.isOffScreen = false; // Flag to indicate if projectile is off-screen
// Update method to move projectile
self.update = function () {
if (self.isOffScreen) return; // Don't update if already marked off-screen
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
// Check if off-screen
var gameWidth = 2048;
var gameHeight = 2732;
// Margin based on projectile size to ensure it's fully off-screen
var margin = Math.max(self.bulletSprite.width, self.bulletSprite.height) / 2 + 50;
if (self.x < -margin || self.x > gameWidth + margin || self.y < -margin || self.y > gameHeight + margin) {
self.isOffScreen = true;
}
}; //{M} // Note: Original L was self.destroy related, removed.
return self;
});
var PlayerShip = Container.expand(function () {
var self = Container.call(this);
self.health = 3; // Player can take a few hits
self._fractionalHealth = self.health; // For fractional damage (e.g., 0.5 from armored enemy)
self.isDestroyed = false;
self.hit = function () {
if (self.isDestroyed) return;
if (typeof self._fractionalHealth === 'undefined') self._fractionalHealth = self.health;
self._fractionalHealth -= 1;
self.health = Math.floor(self._fractionalHealth);
LK.effects.flashObject(self.shipSprite || self, 0xff0000, 150); // Flash red on hit (flash sprite if available)
// Update hearts display
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < self.health;
}
if (self._fractionalHealth <= 0) {
self.isDestroyed = true;
// Game over will be triggered in game.update
}
};
// Attach player ship sprite and set reference for spawn point calculation
self.shipSprite = self.attachAsset('playerShipSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.moveSpeed = 10; // Pixels per tick for movement speed
// currentAngle is the direction the ship moves and fires projectiles.
// 0 radians = right, -PI/2 = up (screen coordinates).
self.currentAngle = -Math.PI / 2; // Initial angle: pointing up.
// playerShipSprite is assumed to be designed pointing "up" (its nose along its local -Y axis).
// To make the sprite's nose align with currentAngle, its visual rotation needs adjustment.
// Visual rotation = currentAngle + Math.PI / 2.
// E.g., currentAngle = -PI/2 (up) => visual rotation = 0.
// E.g., currentAngle = 0 (right) => visual rotation = PI/2.
self.shipSprite.rotation = self.currentAngle + Math.PI / 2;
// Initialize projectile spawn point (remains important)
// This will be updated relative to the ship's current position in self.update()
// Initial dummy values, will be set correctly on first update.
projectileSpawnPoint.x = 0;
projectileSpawnPoint.y = 0;
self.applyJoystickInput = function (inputX, inputY) {
// Update angle only if joystick provides directional input
if (inputX !== 0 || inputY !== 0) {
self.currentAngle = Math.atan2(inputY, inputX);
self.shipSprite.rotation = self.currentAngle + Math.PI / 2;
}
self.x += inputX * self.moveSpeed;
self.y += inputY * self.moveSpeed;
// Boundary checks to keep ship on screen
var halfWidth = self.shipSprite.width / 2;
var halfHeight = self.shipSprite.height / 2;
var gameWidth = 2048;
var gameHeight = 2732;
var topSafeMargin = 100; // Top 100px area is reserved
if (self.x - halfWidth < 0) self.x = halfWidth;
if (self.x + halfWidth > gameWidth) self.x = gameWidth - halfWidth;
if (self.y - halfHeight < topSafeMargin) self.y = topSafeMargin + halfHeight;
if (self.y + halfHeight > gameHeight) self.y = gameHeight - halfHeight;
};
// Update projectile spawn point every frame based on player ship position and rotation
self.update = function () {
var shipAsset = self.shipSprite;
if (!shipAsset) return;
// Calculate spawn point at the tip of the ship, considering its currentAngle.
// The "nose" is shipAsset.height / 2 distance from the center.
var noseDistance = shipAsset.height / 2;
projectileSpawnPoint.x = self.x + Math.cos(self.currentAngle) * noseDistance;
projectileSpawnPoint.y = self.y + Math.sin(self.currentAngle) * noseDistance;
};
return self;
});
var PlayerTrail = Container.expand(function () {
var self = Container.call(this);
self.trailSprite = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.4,
scaleY: 0.4,
tint: 0xff3333
});
self.lifeTime = 60; // Trail segment lasts 1 second at 60fps
self.currentLife = self.lifeTime;
self.update = function () {
self.currentLife--;
if (self.currentLife <= 0) {
self.isExpired = true;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
//Only include the plugins you need to create the game.
// Placeholder ID
// Placeholder ID
// Placeholder ID
// Placeholder ID
// Placeholder ID
// Projectile spawn point (relative to player ship center)
// Will be updated in PlayerShip class update
var projectileSpawnPoint = {
x: 0,
y: 0
};
var joystick; // Declare joystick instance
// Array to keep track of all player projectiles
var playerProjectiles = [];
// Array to keep track of player trail segments
var playerTrailSegments = [];
var trailSpawnTimer = 0;
// Array to keep track of enemy trail segments
var enemyTrailSegments = [];
var enemyTrailSpawnTimer = 0;
// Player projectile damage (can be doubled by Cardsttsck)
var playerProjectileDamage = 1;
// Player ammo count (start with 10). Player cannot fire forever: must wait for ammo to restore and cannot hold fire for auto-fire.
var playerAmmo = 10;
// Create and add the game background
// You can change the background by updating the ID in the Assets section for 'game_background_image'
var gameBackground = LK.getAsset('game_background_image', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(gameBackground);
// Add colorful stars to the background
var starColors = [0xFFFFFF, 0xFF3333, 0x33FF33, 0x3333FF, 0xFFFF33, 0xFF33FF, 0x33FFFF, 0xFFAA33, 0xAA33FF, 0x33AAFF];
for (var i = 0; i < 50; i++) {
var star = LK.getAsset('Star', {
anchorX: 0.5,
anchorY: 0.5,
x: Math.random() * 2048,
y: Math.random() * 2732,
tint: starColors[Math.floor(Math.random() * starColors.length)]
});
// 30% chance for a star to have disappearing/reappearing effect instead of twinkling
if (Math.random() < 0.3) {
// Create disappearing and reappearing effect for some stars
var disappearDelay = 2000 + Math.random() * 8000; // Random delay 2-10 seconds before first disappear
LK.setTimeout(function () {
function startDisappearCycle() {
// Disappear quickly
tween(star, {
alpha: 0
}, {
duration: 200 + Math.random() * 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Stay invisible for a random time
var invisibleTime = 500 + Math.random() * 2000; // 0.5-2.5 seconds invisible
LK.setTimeout(function () {
// Reappear
tween(star, {
alpha: 1.0
}, {
duration: 300 + Math.random() * 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Wait before next disappear cycle
var nextCycleDelay = 3000 + Math.random() * 7000; // 3-10 seconds before next cycle
LK.setTimeout(function () {
startDisappearCycle(); // Start the cycle again
}, nextCycleDelay);
}
});
}, invisibleTime);
}
});
}
startDisappearCycle();
}, disappearDelay);
} else {
// Add normal twinkling animation to other stars
tween(star, {
alpha: 0.3
}, {
duration: 1000 + Math.random() * 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(star, {
alpha: 1.0
}, {
duration: 1000 + Math.random() * 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Create infinite loop by calling the twinkling again
tween(star, {
alpha: 0.3
}, {
duration: 1000 + Math.random() * 1000,
easing: tween.easeInOut
});
}
});
}
});
}
game.addChild(star);
}
// Adding it here as one of the first children to 'game' ensures it's in the background.
var maxPlayerAmmo = 10; // Maximum player ammo capacity
// Fire button cooldown in ticks (default 30, can be halved by Card1)
var fireButtonCooldown = 30;
// Array to keep track of all enemy ships
var enemyShips = [];
// Array to keep track of all enemy projectiles
var enemyProjectiles = [];
// Global flag: freeze all enemy movement and firing during card choice
var freezeEnemies = false;
var playerSplitShotActive = false; // Flag for Asplitmind card: player fires two projectiles
// Create player ship and add to game
var playerShip = new PlayerShip();
// Apply saved skin selection
if (selectedSkin && selectedSkin !== 'playerShipSprite') {
var oldRotation = playerShip.shipSprite.rotation;
playerShip.shipSprite.destroy();
playerShip.shipSprite = playerShip.attachAsset(selectedSkin, {
anchorX: 0.5,
anchorY: 0.5
});
playerShip.shipSprite.rotation = oldRotation;
}
game.addChild(playerShip);
// Display hearts for player health
var hearts = [];
for (var i = 0; i < playerShip.health; i++) {
var heart = new Heart();
heart.x = i * 120 + 120; // Position hearts with increased spacing
heart.y = 60; // Top-left corner
LK.gui.topLeft.addChild(heart);
hearts.push(heart);
}
// Load saved money from storage
var money = storage.money || 0;
var moneyTxt = new Text2(money + " $", {
size: 120,
fill: 0xFFE066,
font: "monospace, 'Press Start 2P', 'VT323', 'Courier New', Courier, monospace",
// pixel/retro style
align: "right"
});
moneyTxt.anchor.set(1, 0); // Right-top anchor
moneyTxt.x = -60; // Padding from right edge
moneyTxt.y = 200; // Move money text down to make room for settings
LK.gui.topRight.addChild(moneyTxt);
// Settings icon in top-right corner
var settingsIcon = new Text2("⚙", {
size: 80,
fill: 0xFFFFFF,
align: "right"
});
settingsIcon.anchor.set(1, 0); // Right-top anchor
settingsIcon.x = -60; // Padding from right edge
settingsIcon.y = 60; // Top padding
settingsIcon.interactive = true;
settingsIcon.down = function (x, y, obj) {
// Open ship skin selection menu
if (game._skinMenuActive) return; // Prevent multiple menus
openSkinSelectionMenu();
};
// Ship skin selection menu functionality
var selectedSkin = storage.selectedSkin || 'shipSkin1';
// Initialize owned skins if not exists (default and heavy are free)
if (!storage.ownedSkins) {
storage.ownedSkins = ['shipSkin1', 'shipSkin3'];
}
// Initialize trail setting if not exists (enabled by default)
var trailEnabled = storage.trailEnabled !== undefined ? storage.trailEnabled : true;
// Initialize trail color if not exists (default to red)
var trailColor = storage.trailColor || 0xff3333;
// Ensure selected skin is valid (not Fighter skin)
if (selectedSkin === 'shipSkin2') {
selectedSkin = 'shipSkin1';
storage.selectedSkin = selectedSkin;
}
function openSkinSelectionMenu() {
game._skinMenuActive = true;
// Freeze all enemy movement and firing like card selection
freezeEnemies = true;
// Create overlay for skin selection
var skinOverlay = new Container();
skinOverlay.name = "skinSelectionOverlay";
// Semi-transparent background
var skinBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 20,
scaleY: 20,
x: 2048 / 2,
y: 2732 / 2,
tint: 0x000000,
alpha: 0.8
});
skinOverlay.addChild(skinBg);
// Title text
var titleText = new Text2("SELECT SHIP SKIN", {
size: 100,
fill: 0xFFFFFF,
align: "center"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 2732 / 2 - 400;
skinOverlay.addChild(titleText);
// Available ship skins
var availableSkins = [{
id: 'shipSkin1',
name: 'Default',
cost: 0
}, {
id: 'shipSkin3',
name: 'Heavy',
cost: 0
}, {
id: 'skinfire',
name: 'Ghost',
cost: 5
}, {
id: 'skinnlood',
name: 'Blood',
cost: 8
}, {
id: 'Skinrocet',
name: 'Rocket',
cost: 12
}];
// Position skins horizontally
var skinPositions = [{
x: 2048 / 2 - 540,
y: 2732 / 2
}, {
x: 2048 / 2 - 270,
y: 2732 / 2
}, {
x: 2048 / 2,
y: 2732 / 2
}, {
x: 2048 / 2 + 270,
y: 2732 / 2
}, {
x: 2048 / 2 + 540,
y: 2732 / 2
}];
for (var skinIdx = 0; skinIdx < availableSkins.length; skinIdx++) {
var skinData = availableSkins[skinIdx];
var skinContainer = new Container();
// Ship preview
var shipPreview = LK.getAsset(skinData.id, {
anchorX: 0.5,
anchorY: 0.5,
x: skinPositions[skinIdx].x,
y: skinPositions[skinIdx].y,
scaleX: 1.5,
scaleY: 1.5
});
skinContainer.addChild(shipPreview);
// Selection border for currently selected skin
if (skinData.id === selectedSkin) {
var selectionBorder = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: skinPositions[skinIdx].x,
y: skinPositions[skinIdx].y,
scaleX: 3,
scaleY: 3,
tint: 0x00FF00,
alpha: 0.3
});
skinContainer.addChild(selectionBorder);
}
// Skin name text
var skinNameText = new Text2(skinData.name, {
size: 60,
fill: 0xFFFFFF,
align: "center"
});
skinNameText.anchor.set(0.5, 0.5);
skinNameText.x = skinPositions[skinIdx].x;
skinNameText.y = skinPositions[skinIdx].y + 200;
skinContainer.addChild(skinNameText);
// Cost text - only show if skin is not owned
var ownedSkins = storage.ownedSkins || ['shipSkin1', 'shipSkin3'];
var isOwned = ownedSkins.indexOf(skinData.id) !== -1 || skinData.cost === 0;
if (!isOwned) {
var costText = new Text2(skinData.cost > 0 ? skinData.cost + " $" : "FREE", {
size: 40,
fill: skinData.cost > 0 ? 0xFFE066 : 0x00FF00,
align: "center"
});
costText.anchor.set(0.5, 0.5);
costText.x = skinPositions[skinIdx].x;
costText.y = skinPositions[skinIdx].y + 270;
skinContainer.addChild(costText);
}
// If player doesn't have enough money, make skin appear unavailable
if (skinData.cost > money) {
shipPreview.tint = 0x666666;
skinNameText.tint = 0x666666;
costText.fill = 0xFF4444;
}
skinContainer.interactive = true;
// Use closure to capture skin data
(function (currentSkinData) {
skinContainer.down = function (x_down, y_down, obj_down) {
if (!game._skinMenuActive) return;
// Check if skin costs money and player has enough
if (currentSkinData.cost > 0 && money < currentSkinData.cost) {
// Flash red to indicate insufficient funds
LK.effects.flashScreen(0xFF0000, 300);
return;
}
// Check if skin is already owned (either free or already purchased)
var ownedSkins = storage.ownedSkins || ['shipSkin1', 'shipSkin3']; // Default and Heavy are free
if (currentSkinData.cost > 0 && ownedSkins.indexOf(currentSkinData.id) === -1) {
// Purchase the skin
money -= currentSkinData.cost;
storage.money = money; // Save money to storage
moneyTxt.setText(money + " $");
ownedSkins.push(currentSkinData.id);
storage.ownedSkins = ownedSkins;
}
// Update selected skin
selectedSkin = currentSkinData.id;
storage.selectedSkin = selectedSkin;
// Update player ship sprite
if (playerShip && playerShip.shipSprite) {
var oldRotation = playerShip.shipSprite.rotation;
playerShip.shipSprite.destroy();
playerShip.shipSprite = playerShip.attachAsset(selectedSkin, {
anchorX: 0.5,
anchorY: 0.5
});
playerShip.shipSprite.rotation = oldRotation;
}
closeSkinSelectionMenu(skinOverlay);
};
})(skinData);
skinOverlay.addChild(skinContainer);
}
// Trail toggle button
var trailToggleText = trailEnabled ? "TRAIL: ON" : "TRAIL: OFF";
var trailToggleButton = new Text2(trailToggleText, {
size: 80,
fill: trailEnabled ? 0x00FF00 : 0xFF4444,
align: "center"
});
trailToggleButton.anchor.set(0.5, 0.5);
trailToggleButton.x = 2048 / 2 - 300;
trailToggleButton.y = 2732 / 2 + 400;
trailToggleButton.interactive = true;
trailToggleButton.down = function () {
if (!game._skinMenuActive) return;
// Toggle trail setting
trailEnabled = !trailEnabled;
storage.trailEnabled = trailEnabled;
// Update button text and color
trailToggleButton.setText(trailEnabled ? "TRAIL: ON" : "TRAIL: OFF");
trailToggleButton.fill = trailEnabled ? 0x00FF00 : 0xFF4444;
};
skinOverlay.addChild(trailToggleButton);
// Trail color selection section
var trailColorTitle = new Text2("TRAIL COLOR", {
size: 60,
fill: 0xFFFFFF,
align: "center"
});
trailColorTitle.anchor.set(0.5, 0.5);
trailColorTitle.x = 2048 / 2 + 300;
trailColorTitle.y = 2732 / 2 + 350;
skinOverlay.addChild(trailColorTitle);
// Available trail colors
var trailColors = [{
name: "RED",
color: 0xff3333
}, {
name: "BLUE",
color: 0x3333ff
}, {
name: "GREEN",
color: 0x33ff33
}, {
name: "YELLOW",
color: 0xffff33
}, {
name: "PURPLE",
color: 0xff33ff
}, {
name: "ORANGE",
color: 0xff6600
}];
// Get current trail color from storage or default to red
var currentTrailColor = storage.trailColor || 0xff3333;
// Display trail color options in a 3x2 grid
for (var colorIdx = 0; colorIdx < trailColors.length; colorIdx++) {
var colorData = trailColors[colorIdx];
var colorContainer = new Container();
// Calculate position in 3x2 grid
var col = colorIdx % 3;
var row = Math.floor(colorIdx / 3);
var colorX = 2048 / 2 + 150 + col * 100;
var colorY = 2732 / 2 + 420 + row * 80;
// Color preview circle
var colorPreview = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: colorX,
y: colorY,
scaleX: 0.8,
scaleY: 0.8,
tint: colorData.color
});
colorContainer.addChild(colorPreview);
// Selection border for currently selected color
if (colorData.color === currentTrailColor) {
var colorBorder = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: colorX,
y: colorY,
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF,
alpha: 0.5
});
colorContainer.addChild(colorBorder);
}
colorContainer.interactive = true;
// Use closure to capture color data
(function (currentColorData) {
colorContainer.down = function () {
if (!game._skinMenuActive) return;
// Update trail color
currentTrailColor = currentColorData.color;
storage.trailColor = currentTrailColor;
// Refresh the menu to show new selection
if (game._skinMenuActive) {
closeSkinSelectionMenu(skinOverlay);
openSkinSelectionMenu();
}
};
})(colorData);
skinOverlay.addChild(colorContainer);
}
// Close button
var closeButton = new Text2("✕", {
size: 120,
fill: 0xFF4444,
align: "center"
});
closeButton.anchor.set(0.5, 0.5);
closeButton.x = 2048 / 2 + 600;
closeButton.y = 2732 / 2 - 400;
closeButton.interactive = true;
closeButton.down = function () {
if (!game._skinMenuActive) return;
closeSkinSelectionMenu(skinOverlay);
};
skinOverlay.addChild(closeButton);
game.addChild(skinOverlay);
}
function closeSkinSelectionMenu(overlay) {
game._skinMenuActive = false;
// Resume game like card selection
freezeEnemies = false;
if (overlay && typeof overlay.destroy === 'function') {
overlay.destroy();
}
}
LK.gui.topRight.addChild(settingsIcon);
// Create black hole in top right corner
var blackHole = LK.getAsset('blackHole', {
anchorX: 0.5,
anchorY: 0.5
});
blackHole.x = -120; // Position from right edge
blackHole.y = 120; // Position from top edge
LK.gui.topRight.addChild(blackHole);
// Add swirling animation to black hole
tween(blackHole, {
rotation: Math.PI * 2
}, {
duration: 2000,
easing: tween.easeLinear,
loop: true
});
var enemiesKilled = 0; // Counter for killed enemies, to trigger boss
var bossEnemyInstance = null; // Holds the boss instance
var bossSpawned = false; // Flag to ensure boss spawns only once per game
var homingMissiles = []; // Array for new boss's homing missiles
var giantBossEnemyInstance = null; // Instance for the new giant boss
var giantBossSpawned = false; // Flag for the new giant boss
// Ammo counter removed as per requirements
// Center player ship horizontally, place near bottom
playerShip.x = 2048 / 2;
playerShip.y = 2732 - 350;
// Create Joystick
joystick = new Joystick();
game.addChild(joystick);
// Set initial position (e.g., 0,0), though it will jump to touch.
joystick.x = 0;
joystick.y = 0;
// Enemy spawn multiplier (default 1, can be set to 3 for 5 seconds after Card2)
game._enemySpawnMultiplier = 1;
game._enemySpawnMultiplierTimeout = null;
// Create and add Enemy Ships
var enemyShip1 = new EnemyShip();
var enemyShipAssetHeight1 = enemyShip1.shipSprite && enemyShip1.shipSprite.height ? enemyShip1.shipSprite.height : 146; // Default to asset height
enemyShip1.x = 2048 / 3;
enemyShip1.y = -(enemyShipAssetHeight1 / 2) - 50; // Start off-screen at the top
enemyShip1.lastX = enemyShip1.x; // Initialize lastX to the actual starting x
enemyShip1.lastY = enemyShip1.y; // Initialize lastY to the actual starting y
game.addChild(enemyShip1);
enemyShips.push(enemyShip1);
var enemyShip2 = new EnemyShip();
var enemyShipAssetHeight2 = enemyShip2.shipSprite && enemyShip2.shipSprite.height ? enemyShip2.shipSprite.height : 146; // Default to asset height
enemyShip2.x = 2048 * 2 / 3;
enemyShip2.y = -(enemyShipAssetHeight2 / 2) - 150; // Start off-screen, staggered further up
enemyShip2.lastX = enemyShip2.x; // Initialize lastX to the actual starting x
enemyShip2.lastY = enemyShip2.y; // Initialize lastY to the actual starting y
game.addChild(enemyShip2);
enemyShips.push(enemyShip2);
// Game event handlers
// Create and position Fire Button
var fireButton = new FireButton();
LK.gui.bottomLeft.addChild(fireButton);
// Position the fire button visually in the bottom-left corner with some margin
// The buttonSprite is 200x200 and anchored at its center (0.5, 0.5)
// fireButton (Container) is added to LK.gui.bottomLeft, positioning its top-left (0,0) at screen bottom-left.
// Adjust x and y to make the button appear correctly.
var buttonMargin = 50; // 50px margin from screen edges
fireButton.x = fireButton.buttonSprite.width / 2 + buttonMargin;
fireButton.y = -fireButton.buttonSprite.height / 2 - buttonMargin; // Negative Y moves up from the bottom edge
game.down = function (x, y, obj) {
if (game._cardChoiceActive || game._skinMenuActive) {
// Block joystick and ship movement while card choice or skin menu is active
return;
}
// x, y are global game coordinates
// Move the joystick to the touch position
joystick.x = x;
joystick.y = y;
joystick.visible = true;
// Activate the joystick. Since the joystick's origin is now at the touch point (x,y),
// the local coordinates for the touch within the joystick are (0,0).
// This centers the knob under the finger and initiates dragging.
joystick.handleDown(0, 0); // Pass (0,0) as local coordinates
// Note: With this "floating joystick" implementation, the joystick activates on any screen press.
// The previous mechanic, where tapping away from a fixed joystick fired projectiles,
// is superseded. Further design would be needed for a separate firing mechanism.
};
game.move = function (x, y, obj) {
if (game._cardChoiceActive || game._skinMenuActive) {
// Block joystick movement while card choice or skin menu is active
return;
}
if (joystick.isActive()) {
var joystickLocalPos = joystick.toLocal({
x: x,
y: y
});
joystick.handleMove(joystickLocalPos.x, joystickLocalPos.y);
}
};
game.up = function (x, y, obj) {
if (game._cardChoiceActive || game._skinMenuActive) {
// Block joystick release while card choice or skin menu is active
return;
}
if (joystick.isActive()) {
joystick.handleUp(); // Resets knob, inputVector, and isDragging flag
joystick.visible = false; // Hide the joystick when the touch is released
}
};
// Update game state
var ammoRestoreCooldown = 0; // Ticks until next ammo restore
// Start with space music
LK.playMusic('Space');
game.update = function () {
// Prevent all game logic and object updates while card choice or skin menu overlay is active
if (game._cardChoiceActive || game._skinMenuActive) {
// Only allow overlay input, skip all updates and movement
return;
}
// Restore ammo if not full, with a delay (e.g., 30 ticks = 0.5s)
if (typeof playerAmmo !== 'undefined') {
if (typeof maxPlayerAmmo === 'undefined') maxPlayerAmmo = 10; // Defensive: ensure maxPlayerAmmo is defined
if (playerAmmo < maxPlayerAmmo) {
// Use maxPlayerAmmo instead of hardcoded 10
if (typeof ammoRestoreCooldown === 'undefined') ammoRestoreCooldown = 0;
if (ammoRestoreCooldown > 0) {
ammoRestoreCooldown--;
} else {
playerAmmo += 1;
// Removed ammo counter update as per requirements
ammoRestoreCooldown = 30; // 0.5s at 60FPS
}
} else {
// Player is at or above max ammo.
// Ensure current ammo doesn't exceed new max if it somehow did, though unlikely with current regen.
playerAmmo = Math.min(playerAmmo, maxPlayerAmmo);
ammoRestoreCooldown = 0;
}
}
// Apply joystick input to player ship movement
if (joystick && playerShip && playerShip.applyJoystickInput) {
var input = joystick.getInput();
playerShip.applyJoystickInput(input.x, input.y);
}
// Update player ship (e.g., to recalculate projectileSpawnPoint after moving)
if (playerShip && playerShip.update) {
playerShip.update();
}
// Create trail segments behind player
trailSpawnTimer++;
if (trailSpawnTimer >= 3 && playerShip && !playerShip.isDestroyed && trailEnabled) {
var trailSegment = new PlayerTrail();
trailSegment.x = playerShip.x;
trailSegment.y = playerShip.y;
// Get current trail color from storage
var currentTrailColor = storage.trailColor || 0xff3333;
// Add bright pulsing effect to make trail more vibrant with selected color
if (trailSegment.trailSprite) {
// Calculate a lighter version of the selected color for pulsing effect
var lighterColor = currentTrailColor;
// Add brightness by increasing RGB values proportionally
var r = Math.min(255, (currentTrailColor >> 16 & 0xFF) + 50);
var g = Math.min(255, (currentTrailColor >> 8 & 0xFF) + 50);
var b = Math.min(255, (currentTrailColor & 0xFF) + 50);
lighterColor = r << 16 | g << 8 | b;
trailSegment.trailSprite.tint = currentTrailColor;
tween(trailSegment.trailSprite, {
tint: lighterColor
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(trailSegment.trailSprite, {
tint: currentTrailColor
}, {
duration: 200,
easing: tween.easeInOut
});
}
});
}
game.addChild(trailSegment);
playerTrailSegments.push(trailSegment);
trailSpawnTimer = 0;
}
// Create trail segments behind enemies
enemyTrailSpawnTimer++;
if (enemyTrailSpawnTimer >= 4) {
// Create trails for all active enemies
for (var e = 0; e < enemyShips.length; e++) {
var enemy = enemyShips[e];
if (!enemy.isDestroyed && !enemy.isOffScreen) {
var enemyTrailSegment = new PlayerTrail();
enemyTrailSegment.x = enemy.x;
enemyTrailSegment.y = enemy.y;
// Make enemy trails orange/yellow color
if (enemyTrailSegment.trailSprite) {
enemyTrailSegment.trailSprite.tint = 0xff6600;
tween(enemyTrailSegment.trailSprite, {
tint: 0xffaa33
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(enemyTrailSegment.trailSprite, {
tint: 0xff6600
}, {
duration: 200,
easing: tween.easeInOut
});
}
});
}
game.addChild(enemyTrailSegment);
enemyTrailSegments.push(enemyTrailSegment);
}
}
enemyTrailSpawnTimer = 0;
}
// Update and clean up trail segments
for (var t = playerTrailSegments.length - 1; t >= 0; t--) {
var trailSeg = playerTrailSegments[t];
if (trailSeg.update) {
trailSeg.update();
}
// Calculate alpha based on remaining life (1.0 to 0.0)
var alpha = trailSeg.currentLife / trailSeg.lifeTime;
if (trailSeg.trailSprite) {
trailSeg.trailSprite.alpha = Math.max(0.3, alpha); // Keep minimum visibility of 0.3
}
if (trailSeg.isExpired) {
trailSeg.destroy();
playerTrailSegments.splice(t, 1);
}
}
// Update and clean up enemy trail segments
for (var et = enemyTrailSegments.length - 1; et >= 0; et--) {
var enemyTrailSeg = enemyTrailSegments[et];
if (enemyTrailSeg.update) {
enemyTrailSeg.update();
}
// Calculate alpha based on remaining life (1.0 to 0.0)
var enemyAlpha = enemyTrailSeg.currentLife / enemyTrailSeg.lifeTime;
if (enemyTrailSeg.trailSprite) {
enemyTrailSeg.trailSprite.alpha = Math.max(0.2, enemyAlpha); // Slightly more transparent than player trails
}
if (enemyTrailSeg.isExpired) {
enemyTrailSeg.destroy();
enemyTrailSegments.splice(et, 1);
}
}
// Update and clean up projectiles, and check for collisions with enemies
for (var i = playerProjectiles.length - 1; i >= 0; i--) {
var proj = playerProjectiles[i];
if (proj.update) {
proj.update(); // Call projectile's own update logic (movement)
}
// Check if the projectile flagged itself as off-screen AFTER moving
if (proj.isOffScreen) {
proj.destroy(); // Destroy the projectile
playerProjectiles.splice(i, 1); // Remove from the tracking array
continue; // Move to the next projectile
}
// Collision detection with enemy ships
for (var k = enemyShips.length - 1; k >= 0; k--) {
var enemy = enemyShips[k];
// Skip already destroyed or off-screen enemies for collision checks
if (enemy.isDestroyed || enemy.isOffScreen) {
continue;
}
if (proj.intersects(enemy)) {
// Use playerProjectileDamage if defined, else default to 1
var dmg = typeof playerProjectileDamage !== 'undefined' ? playerProjectileDamage : 1;
if (typeof enemy.health === 'number') {
enemy.health -= dmg;
if (enemy.health <= 0) {
enemy.isDestroyed = true;
} else {
if (typeof enemy.hit === 'function') enemy.hit();
}
} else {
if (typeof enemy.hit === 'function') enemy.hit();
}
proj.destroy(); // Projectile is consumed on hit
playerProjectiles.splice(i, 1); // Remove projectile
// If enemy.isDestroyed became true, it will be handled in the enemyShips loop below.
// Score will be awarded there too.
break; // Projectile is gone, no need to check against other enemies
}
}
// If projectile still exists, check collision with boss parts
if (!proj.isOffScreen && bossEnemyInstance && !bossEnemyInstance.isDestroyed) {
var bossPartsToCheck = [];
if (bossEnemyInstance.mainBody && !bossEnemyInstance.mainBody.isDestroyed) {
bossPartsToCheck.push(bossEnemyInstance.mainBody);
}
if (bossEnemyInstance.leftCannon && !bossEnemyInstance.leftCannon.isDestroyed) {
bossPartsToCheck.push(bossEnemyInstance.leftCannon);
}
if (bossEnemyInstance.rightCannon && !bossEnemyInstance.rightCannon.isDestroyed) {
bossPartsToCheck.push(bossEnemyInstance.rightCannon);
}
for (var bp_idx = 0; bp_idx < bossPartsToCheck.length; bp_idx++) {
var part = bossPartsToCheck[bp_idx];
if (proj.intersects(part)) {
var dmg = typeof playerProjectileDamage !== 'undefined' ? playerProjectileDamage : 1;
part.hit(dmg); // Part handles its own health and destruction visuals
if (part.isDestroyed) {
// Award points/money for destroying a part
if (part instanceof BossCannon) {
LK.setScore(LK.getScore() + 50); // More points for a cannon
money += 25;
storage.money = money; // Save money to storage
if (typeof moneyTxt !== 'undefined' && moneyTxt.setText) {
moneyTxt.setText(money + " $");
}
}
// Main body destruction score is handled when boss is fully defeated
}
proj.destroy(); // Projectile is consumed
playerProjectiles.splice(i, 1);
break; // Projectile is gone, stop checking this projectile against other boss parts
}
}
}
// If proj was spliced (either by hitting enemy or boss part), loop continues correctly.
}
// Update and clean up enemy projectiles, and check for collisions with player
for (var l = enemyProjectiles.length - 1; l >= 0; l--) {
var eProj = enemyProjectiles[l];
if (eProj.update) {
eProj.update(); // Call projectile's own update logic (movement)
}
// Check if the projectile flagged itself as off-screen AFTER moving
if (eProj.isOffScreen) {
if (typeof eProj.destroy === 'function') eProj.destroy();
enemyProjectiles.splice(l, 1); // Remove from the tracking array
continue; // Move to the next projectile
}
// Collision detection with player ship
// Ensure playerShip exists and is not already destroyed before checking intersection
if (playerShip && !playerShip.isDestroyed && typeof eProj.intersects === 'function' && eProj.intersects(playerShip)) {
// Check if the projectile's parent is an ArmoredEnemyShip (for 0.5 damage)
var isArmored = false;
if (typeof enemyShips !== 'undefined') {
for (var ai = 0; ai < enemyShips.length; ai++) {
var es = enemyShips[ai];
if (es instanceof ArmoredEnemyShip && es.enemyProjectileSpawnPoint && Math.abs(eProj.x - es.enemyProjectileSpawnPoint.x) < 2 && Math.abs(eProj.y - es.enemyProjectileSpawnPoint.y) < 2) {
isArmored = true;
break;
}
}
}
if (isArmored) {
// Deal 0.5 damage: use a fractional health system
if (typeof playerShip.health === 'number') {
if (typeof playerShip._fractionalHealth === 'undefined') playerShip._fractionalHealth = playerShip.health;
playerShip._fractionalHealth -= 0.5;
if (playerShip._fractionalHealth <= 0) {
playerShip.health = 0;
playerShip.isDestroyed = true;
if (playerShip.shipSprite) playerShip.shipSprite.visible = false;else if (typeof playerShip.visible !== 'undefined') playerShip.visible = false;
LK.showGameOver();
} else {
playerShip.health = Math.floor(playerShip._fractionalHealth);
LK.effects.flashObject(playerShip.shipSprite || playerShip, 0xff0000, 150);
// Update hearts display
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < playerShip.health;
}
}
}
} else {
if (typeof playerShip.hit === 'function') playerShip.hit(); // Player takes damage
if (playerShip.isDestroyed) {
if (playerShip.shipSprite) playerShip.shipSprite.visible = false;else if (typeof playerShip.visible !== 'undefined') playerShip.visible = false;
LK.showGameOver();
}
}
if (typeof eProj.destroy === 'function') eProj.destroy(); // Projectile is consumed on hit
enemyProjectiles.splice(l, 1); // Remove projectile
// If projectile hit player, it's gone. No need to check further for this projectile.
}
}
// Update and clean up enemy ships
// This loop should still run even if player is destroyed, to clean up existing enemies,
// though new game state might prevent further updates if LK.showGameOver() is effective immediately.
var _loop = function _loop() {
enemy = enemyShips[j]; // First, check if the enemy ship is marked as destroyed (e.g., by a projectile hit)
if (enemy.isDestroyed) {
// Capture enemy's current position for the explosion sequence
var explosionX = enemy.x;
var explosionY = enemy.y;
// Show Boom image at the captured position
var localBoomImg1 = LK.getAsset('Boom', {
// Use a new local variable for the first boom image
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X coordinate
y: explosionY // Use captured Y coordinate
});
game.addChild(localBoomImg1);
// Remove Boom image after a short delay (e.g., 125ms), then show Boom2 at the same position
LK.setTimeout(function () {
if (localBoomImg1 && typeof localBoomImg1.destroy === 'function') localBoomImg1.destroy(); // Destroy the correct local image
// Show Boom2 image at the same captured position
var boom2Img = LK.getAsset('Boom2', {
// This was already var, so local, good.
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom2Img);
// Remove Boom2 image after a short delay (e.g., 125ms), then show Boom3 at the same position
LK.setTimeout(function () {
if (boom2Img && typeof boom2Img.destroy === 'function') boom2Img.destroy();
// Show Boom3 image at the same captured position
var boom3Img = LK.getAsset('Boom3', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom3Img);
// Remove Boom3 image after a short delay (e.g., 125ms), then show Boom4 at the same position
LK.setTimeout(function () {
if (boom3Img && typeof boom3Img.destroy === 'function') boom3Img.destroy();
// Show Boom4 image at the same captured position
var boom4Img = LK.getAsset('Boom4', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom4Img);
// Remove Boom4 image after a short delay (e.g., 125ms), then show Boom5 at the same position
LK.setTimeout(function () {
if (boom4Img && typeof boom4Img.destroy === 'function') boom4Img.destroy();
// Show Boom5 image at the same captured position
var boom5Img = LK.getAsset('Boom5', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom5Img);
// Remove Boom5 image after a short delay (e.g., 125ms), then show Boom6 at the same position
LK.setTimeout(function () {
if (boom5Img && typeof boom5Img.destroy === 'function') boom5Img.destroy();
// Show Boom6 image at the same captured position
var boom6Img = LK.getAsset('Boom6', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom6Img);
// Remove Boom6 image after a short delay (e.g., 125ms)
LK.setTimeout(function () {
if (boom6Img && typeof boom6Img.destroy === 'function') boom6Img.destroy();
}, 125);
}, 125);
}, 125);
}, 125);
}, 125);
}, 125);
enemy.destroy(); // Destroy the enemy ship
enemyShips.splice(j, 1); // Remove from the tracking array
LK.setScore(LK.getScore() + 10); // Award score for destroying an enemy
money += 5; // Add 5 money per enemy destroyed
storage.money = money; // Save money to storage
if (typeof moneyTxt !== 'undefined' && moneyTxt.setText) {
moneyTxt.setText(money + " $");
}
LK.getSound('Boom').play(); // Play 'Boom' sound effect
// 25% chance to drop a health restoration
if (Math.random() < 0.25) {
healthRestore = new HealthRestore();
healthRestore.x = enemy.x;
healthRestore.y = enemy.y;
game.addChild(healthRestore);
}
// --- Card choice after 3 kills ---
enemiesKilled++;
// --- Boss Spawn Logic ---
// Spawn original boss after 10 kills
if (!bossSpawned && bossEnemyInstance === null && enemiesKilled >= 10) {
bossEnemyInstance = new BossEnemy();
bossEnemyInstance.x = 2048 / 2;
bossEnemyInstance.y = 450;
game.addChild(bossEnemyInstance);
bossSpawned = true;
LK.playMusic('Boss');
}
// --- End Boss Spawn Logic ---
if (enemiesKilled > 0 && enemiesKilled % 3 === 0 && !game._cardChoiceActive) {
// Helper to finish card choice and resume game
var finishCardChoice = function finishCardChoice() {
if (!game._cardChoiceActive) return;
game._cardChoiceActive = false;
freezeEnemies = false;
if (cardOverlay && typeof cardOverlay.destroy === 'function') cardOverlay.destroy();
// Restore input
game.down = oldGameDown;
game.move = oldGameMove;
game.up = oldGameUp;
// Resume game logic
// No need to call LK.resumeGame(); LK engine will resume game after overlay is destroyed
};
game._cardChoiceActive = true;
// Freeze all enemy movement and firing
freezeEnemies = true;
// --- Block card selection for 2 seconds ---
game._cardChoiceBlock = true;
LK.setTimeout(function () {
game._cardChoiceBlock = false;
}, 2000);
// Pause game logic is handled by LK.showGameOver() and LK.resumeGame(), no need to call LK.pause() here
// Create overlay for card choice
cardOverlay = new Container();
cardOverlay.name = "cardChoiceOverlay";
// Semi-transparent background
bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 20,
scaleY: 20,
x: 2048 / 2,
y: 2732 / 2,
tint: 0x000000,
alpha: 0.7
});
cardOverlay.addChild(bg);
// --- Define all available cards and their effects ---
var allAvailableCards = [{
key: "card1_ammo_firerate",
// Card1: More Ammo, Faster Fire Rate
asset: "Card1",
applyEffect: function applyEffect() {
if (typeof maxPlayerAmmo === 'undefined') maxPlayerAmmo = 10; // Defensive initialization
maxPlayerAmmo += 2; // Increase maximum ammo capacity
playerAmmo = Math.min(playerAmmo + 2, maxPlayerAmmo); // Add 2 to current ammo, capped by new maxPlayerAmmo
fireButtonCooldown = Math.max(1, Math.floor(fireButtonCooldown / 2)); // Halve cooldown, min 1
}
}, {
key: "card2_speed_spawn_heal",
// Card2: Speed Boost, Enemy Spawn Boost, Heal 1 HP
asset: "Csrd2",
applyEffect: function applyEffect() {
if (playerShip) {
if (typeof playerShip._originalMoveSpeed === 'undefined') {
playerShip._originalMoveSpeed = playerShip.moveSpeed;
}
playerShip.moveSpeed = playerShip._originalMoveSpeed * 2;
// Speed boost is now permanent, timeout removed.
// Heal 1 HP if not at max health
var currentMaxHealth = playerShip.maxHealth || 3;
if (playerShip.health < currentMaxHealth) {
playerShip.health = Math.min(playerShip.health + 1, currentMaxHealth);
if (typeof playerShip._fractionalHealth !== 'undefined') {
// Ensure fractional health is updated
playerShip._fractionalHealth = playerShip.health;
}
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < playerShip.health;
}
}
}
// Removed enemy spawn multiplier effect
}
}, {
key: "cardhp_maxhp_heal",
// Cardhp: Increase Max HP, Heal 1 HP
asset: "Cardhp",
applyEffect: function applyEffect() {
if (playerShip) {
if (typeof playerShip.maxHealth === 'undefined') {
playerShip.maxHealth = 3;
}
playerShip.maxHealth += 1;
playerShip.health = Math.min(playerShip.health + 1, playerShip.maxHealth); // Heal 1 HP up to new max
if (typeof playerShip._fractionalHealth !== 'undefined') {
// Ensure fractional health is updated
playerShip._fractionalHealth = playerShip.health;
}
// Add a new heart icon if needed (maxHealth might exceed initial hearts array length)
// Ensure heart icons match current maxHealth and health
while (hearts.length < playerShip.maxHealth) {
var newHeart = new Heart();
newHeart.x = hearts.length * 120 + 120;
newHeart.y = 60;
LK.gui.topLeft.addChild(newHeart);
hearts.push(newHeart);
}
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < playerShip.health;
}
}
}
}, {
key: "cardsttsck_doubledamage",
// Cardsttsck: Double Projectile Damage
asset: "Cardsttsck",
applyEffect: function applyEffect() {
if (typeof playerProjectileDamage === 'undefined') {
playerProjectileDamage = 1;
}
playerProjectileDamage *= 2;
}
}, {
// Adding new card Asplitmind
key: "asplitmind_split_shot",
asset: "Asplitmind",
applyEffect: function applyEffect() {
// playerSplitShotActive is globally defined and initialized to false.
// This effect makes the split shot permanently true.
playerSplitShotActive = true;
}
}];
// --- Shuffle and select 3 cards to display ---
// Fisher-Yates shuffle
for (var k = allAvailableCards.length - 1; k > 0; k--) {
var j_shuffle = Math.floor(Math.random() * (k + 1));
var tempCard = allAvailableCards[k];
allAvailableCards[k] = allAvailableCards[j_shuffle];
allAvailableCards[j_shuffle] = tempCard;
}
var cardsToDisplay = allAvailableCards.slice(0, 3); // Get the first 3 cards
// --- Position and display selected cards ---
var cardPositions = [{
x: 2048 / 2 - 450,
y: 2732 / 2
},
// Left card
{
x: 2048 / 2,
y: 2732 / 2
},
// Middle card
{
x: 2048 / 2 + 450,
y: 2732 / 2
} // Right card
];
for (var cardIdx = 0; cardIdx < cardsToDisplay.length; cardIdx++) {
var cardData = cardsToDisplay[cardIdx];
var cardContainer = new Container(); // Create a new container for each card
// The image itself is positioned globally based on cardPositions
var cardImg = LK.getAsset(cardData.asset, {
anchorX: 0.5,
anchorY: 0.5,
x: cardPositions[cardIdx].x,
y: cardPositions[cardIdx].y
});
cardContainer.addChild(cardImg);
// The container itself can be at 0,0 as its child (the image) is globally positioned.
// However, for consistency with how interactives are often handled,
// we can also set the container's position if we wanted the image local to it.
// For this setup, global positioning of the image is fine.
cardContainer.x = 0;
cardContainer.y = 0;
cardContainer.interactive = true;
// Use a closure to correctly capture cardData for each event handler
(function (currentCardData) {
cardContainer.down = function (x_down, y_down, obj_down) {
if (!game._cardChoiceActive || game._cardChoiceBlock) return;
currentCardData.applyEffect();
finishCardChoice();
};
})(cardData);
cardOverlay.addChild(cardContainer);
}
// Add overlay to game
game.addChild(cardOverlay);
// Block all input except card choice
oldGameDown = game.down;
oldGameMove = game.move;
oldGameUp = game.up;
game.down = function (x, y, obj) {
if (game._cardChoiceBlock) return;
// Defensive: Only forward to cards that exist in overlay
if (cardOverlay && cardOverlay.children) {
for (var i = 0; i < cardOverlay.children.length; i++) {
var child = cardOverlay.children[i];
if (child && child.interactive && typeof child.down === "function" && child.children && child.children.length > 0) {
var img = child.children[0];
if (img && typeof child.toLocal === "function") {
var local = child.toLocal({
x: x,
y: y
});
if (img.width && img.height && Math.abs(local.x) < img.width / 2 && Math.abs(local.y) < img.height / 2) {
child.down(x, y, obj);
return;
}
}
}
}
}
};
game.move = function () {};
game.up = function () {};
}
// }
// if (typeof scoreTxt !== 'undefined' && scoreTxt && scoreTxt.setText) {
// scoreTxt.setText(LK.getScore());
// }
return 1; // continue
// Move to the next enemy ship in the list
}
if (enemy.update) {
enemy.update(); // Call enemy ship's own update logic (movement, etc.)
}
// Then, check if the enemy ship flagged itself as off-screen (if not already destroyed)
if (enemy.isOffScreen) {
enemy.destroy(); // Destroy the enemy ship
enemyShips.splice(j, 1); // Remove from the tracking array
// No score for merely going off-screen, unless desired.
}
},
enemy,
boomImg,
healthRestore,
cardOverlay,
bg,
card1,
card1Bg,
card1Text,
card2,
card2Bg,
card2Text,
oldGameDown,
oldGameMove,
oldGameUp;
for (var j = enemyShips.length - 1; j >= 0; j--) {
if (_loop()) continue;
}
// Increase the number of enemies over time
// Spawn regular enemies only if NEITHER boss is currently active
var originalBossActive = bossEnemyInstance && !bossEnemyInstance.isDestroyed;
var giantBossActive = giantBossEnemyInstance && !giantBossEnemyInstance.isDestroyed;
if (!originalBossActive && !giantBossActive && LK.ticks % Math.max(30, 150 - Math.floor(LK.ticks / 300)) === 0) {
// Determine multiplier (default 1, or 3x if card2 effect active)
var multiplier = typeof game._enemySpawnMultiplier !== 'undefined' && game._enemySpawnMultiplier > 1 ? game._enemySpawnMultiplier : 1;
for (var spawnIdx = 0; spawnIdx < multiplier; spawnIdx++) {
// 25% chance to spawn armored enemy, else normal
var newEnemyShip;
if (Math.random() < 0.25 && typeof ArmoredEnemyShip !== 'undefined') {
newEnemyShip = new ArmoredEnemyShip();
} else {
newEnemyShip = new EnemyShip();
}
var enemyShipAssetHeight = newEnemyShip.shipSprite && newEnemyShip.shipSprite.height ? newEnemyShip.shipSprite.height : 146;
newEnemyShip.x = Math.random() * 2048;
newEnemyShip.y = -(enemyShipAssetHeight / 2) - 50;
newEnemyShip.lastX = newEnemyShip.x;
newEnemyShip.lastY = newEnemyShip.y;
game.addChild(newEnemyShip);
enemyShips.push(newEnemyShip);
}
}
// --- Boss Update and Defeat Logic ---
if (bossEnemyInstance) {
if (bossEnemyInstance.isDestroyed) {
// Duration each explosion frame is visible (ms)
// Helper function to manage the chained explosion sequence
var _showNextExplosion = function showNextExplosion(index, previousExplosionAsset) {
// Clean up the previous explosion asset from the screen
if (previousExplosionAsset && typeof previousExplosionAsset.destroy === 'function') {
previousExplosionAsset.destroy();
}
// Check if there are more explosion frames to show
if (index < explosionAssets.length) {
var assetId = explosionAssets[index];
var currentExplosion = LK.getAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
y: explosionY,
scaleX: 1.5,
scaleY: 1.5 // Make boss explosions bigger
});
game.addChild(currentExplosion);
LK.getSound('Boom').play(); // Play sound for each frame
LK.setTimeout(function () {
_showNextExplosion(index + 1, currentExplosion);
}, explosionAssetDuration);
} else {
// All explosion frames for the first boss are done
bossEnemyInstance = null; // Officially nullify the global reference after explosion
// Check if giant boss is NOT active (or not spawned / already destroyed)
if (!giantBossEnemyInstance || giantBossEnemyInstance.isDestroyed) {
LK.stopMusic(); // Stop current music first
LK.playMusic('Space'); // Resume normal gameplay music
}
// If giant boss IS active, its spawn logic would have (or will) set 'Boss' music.
}
}; // End of _showNextExplosion definition
// Boss has been defeated (main body destroyed)
LK.setScore(LK.getScore() + 250); // Big score for defeating the boss
money += 100;
storage.money = money; // Save money to storage
if (typeof moneyTxt !== 'undefined' && moneyTxt.setText) {
moneyTxt.setText(money + " $");
}
// Grand explosion for boss defeat
var explosionX = bossEnemyInstance.x + (bossEnemyInstance.mainBody ? bossEnemyInstance.mainBody.x : 0);
var explosionY = bossEnemyInstance.y + (bossEnemyInstance.mainBody ? bossEnemyInstance.mainBody.y : 0);
// Grand explosion for boss defeat is handled by a chained sequence
// Define explosion parameters (position is already defined above this block)
var explosionAssets = ['Boom', 'Boom2', 'Boom3', 'Boom4', 'Boom5', 'Boom6'];
var explosionAssetDuration = 150;
if (explosionAssets.length > 0) {
// Initial call to start the sequence with the first asset (index 0)
// There's no previousExplosionAsset for the first frame.
_showNextExplosion(0, null);
}
bossEnemyInstance.destroy(); // Remove boss from game (destroys the actual game object)
// Note: bossEnemyInstance = null; and LK.playMusic('Space'); have been moved into _showNextExplosion
// Optionally, trigger win condition or next phase
// LK.showYouWin();
} else {
bossEnemyInstance.update(); // Update active boss
}
}
};
// Music will be managed by game state - boss music during boss fights, space music otherwise
;
; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// New armored enemy ship: slower, more health, different sprite
var ArmoredEnemyShip = Container.expand(function () {
var self = Container.call(this);
self.fireCooldown = 120; // Fires more often than normal enemy
self.fireTimer = Math.random() * self.fireCooldown;
self.enemyProjectileSpawnPoint = {
x: 0,
y: 0
};
self.hit = function () {
if (self.isDestroyed) return;
self.health -= 1;
if (self.health <= 0) {
self.isDestroyed = true;
} else {
// Flash magenta on hit
if (self.shipSprite) LK.effects.flashObject(self.shipSprite, 0xe011a9, 80);
}
};
self.update = function () {
if (self.isOffScreen || self.isDestroyed) return;
if (typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
// Target the player ship
if (typeof playerShip !== 'undefined' && playerShip && !playerShip.isDestroyed) {
var dx = playerShip.x - self.x;
var dy = playerShip.y - self.y;
var targetAngle = Math.atan2(dy, dx);
self.angle = targetAngle;
if (self.shipSprite) self.shipSprite.rotation = self.angle + Math.PI / 2;
}
self.lastX = self.x;
self.lastY = self.y;
// Armored enemy moves slower
var moveX = Math.cos(self.angle) * self.speed;
var moveY = Math.sin(self.angle) * self.speed;
if (typeof playerShip !== 'undefined' && playerShip && !playerShip.isDestroyed) {
var dxToPlayer = self.x + moveX - playerShip.x;
var dyToPlayer = self.y + moveY - playerShip.y;
var distToPlayer = Math.sqrt(dxToPlayer * dxToPlayer + dyToPlayer * dyToPlayer);
if (distToPlayer < 120) {
moveX = 0;
moveY = 0;
}
}
self.x += moveX;
self.y += moveY;
var shipAsset = self.shipSprite;
if (shipAsset) {
var noseDistance = shipAsset.height / 2;
self.enemyProjectileSpawnPoint.x = self.x + Math.cos(self.angle) * noseDistance;
self.enemyProjectileSpawnPoint.y = self.y + Math.sin(self.angle) * noseDistance;
}
self.fireTimer--;
if (self.fireTimer <= 0) {
if (typeof EnemyProjectile !== 'undefined' && typeof enemyProjectiles !== 'undefined' && game && typeof game.addChild === 'function') {
var newProjectile = new EnemyProjectile(self.angle);
newProjectile.x = self.enemyProjectileSpawnPoint.x;
newProjectile.y = self.enemyProjectileSpawnPoint.y;
enemyProjectiles.push(newProjectile);
game.addChild(newProjectile);
self.fireTimer = self.fireCooldown;
}
}
var gameWidth = 2048;
var gameHeight = 2732;
var marginWidth = self.shipSprite && self.shipSprite.width ? self.shipSprite.width / 2 + 50 : 100;
var marginHeight = self.shipSprite && self.shipSprite.height ? self.shipSprite.height / 2 + 50 : 100;
if (self.x < -marginWidth || self.x > gameWidth + marginWidth || self.y < -marginHeight || self.y > gameHeight + marginHeight) {
self.isOffScreen = true;
}
};
// Use a magenta box for armored enemy
self.shipSprite = self.attachAsset('Armoredenyme', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = Math.PI / 2;
if (self.shipSprite) self.shipSprite.rotation = self.angle + Math.PI / 2;
self.speed = 2.5; // Slower than normal enemy
self.isOffScreen = false;
self.lastX = self.x;
self.lastY = self.y;
self.health = 3; // Armored enemy starts with 3 HP
self.isDestroyed = false;
return self;
});
var BossCannon = Container.expand(function (parentBoss) {
var self = Container.call(this);
self.parentBoss = parentBoss;
self.health = 15; // Cannons are a bit tough
self.isDestroyed = false;
self.fireCooldown = 90; // Fires every 1.5 seconds
self.fireTimer = Math.random() * self.fireCooldown;
self.cannonSprite = self.attachAsset('Bossgun', {
anchorX: 0.5,
anchorY: 0.5
});
self.hit = function (damage) {
if (self.isDestroyed) return;
self.health -= damage;
LK.effects.flashObject(self.cannonSprite, 0xff8888, 100);
if (self.health <= 0) {
self.isDestroyed = true;
self.visible = false;
// Play a smaller explosion for cannon destruction
var explosionX = self.parentBoss.x + self.x;
var explosionY = self.parentBoss.y + self.y;
var boomImg = LK.getAsset('Boom3', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
y: explosionY
});
game.addChild(boomImg);
LK.setTimeout(function () {
if (boomImg && typeof boomImg.destroy === 'function') boomImg.destroy();
}, 500);
LK.getSound('Boom').play();
}
};
self.update = function () {
if (self.isDestroyed || typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
// Aim at player
if (playerShip && !playerShip.isDestroyed) {
var globalCannonX = self.parentBoss.x + self.x;
var globalCannonY = self.parentBoss.y + self.y;
var dx = playerShip.x - globalCannonX;
var dy = playerShip.y - globalCannonY;
var angleToPlayer = Math.atan2(dy, dx);
self.cannonSprite.rotation = angleToPlayer + Math.PI / 2; // Assuming sprite points "up"
self.fireTimer--;
if (self.fireTimer <= 0) {
var noseOffset = self.cannonSprite.height / 2;
var projSpawnX = globalCannonX + Math.cos(angleToPlayer) * noseOffset;
var projSpawnY = globalCannonY + Math.sin(angleToPlayer) * noseOffset;
var newProjectile = new EnemyProjectile(angleToPlayer);
newProjectile.x = projSpawnX;
newProjectile.y = projSpawnY;
enemyProjectiles.push(newProjectile);
game.addChild(newProjectile);
self.fireTimer = self.fireCooldown;
}
}
};
return self;
});
var BossEnemy = Container.expand(function () {
var self = Container.call(this);
self.isDestroyed = false;
self.mainBody = new BossMainBody(self);
self.addChild(self.mainBody);
self.mainBody.x = 0;
self.mainBody.y = 0;
self.leftCannon = new BossCannon(self);
self.addChild(self.leftCannon);
self.rightCannon = new BossCannon(self);
self.addChild(self.rightCannon);
// Position cannons relative to the main body
// Ensure sprites are attached to get dimensions
var mainBodyWidth = self.mainBody.bodySprite.width;
var cannonWidth = self.leftCannon.cannonSprite.width; // Both cannons use same asset
self.leftCannon.x = -(mainBodyWidth / 2) - cannonWidth / 2 - 20; // 20px spacing
self.leftCannon.y = 0;
self.rightCannon.x = mainBodyWidth / 2 + cannonWidth / 2 + 20; // 20px spacing
self.rightCannon.y = 0;
self.update = function () {
if (self.isDestroyed || typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
if (self.mainBody && !self.mainBody.isDestroyed) {
self.mainBody.update();
}
if (self.leftCannon && !self.leftCannon.isDestroyed) {
self.leftCannon.update();
}
if (self.rightCannon && !self.rightCannon.isDestroyed) {
self.rightCannon.update();
}
};
return self;
});
var BossMainBody = Container.expand(function (parentBoss) {
var self = Container.call(this);
self.parentBoss = parentBoss;
self.health = 50; // Main body is very tough
self.isDestroyed = false;
self.bodySprite = self.attachAsset('BossBodyAsset', {
anchorX: 0.5,
anchorY: 0.5
});
self.hit = function (damage) {
if (self.isDestroyed) return;
self.health -= damage;
LK.effects.flashObject(self.bodySprite, 0xff6666, 150);
if (self.health <= 0) {
self.isDestroyed = true;
self.visible = false;
self.parentBoss.isDestroyed = true; // Main body destroyed means boss is defeated
// Trigger larger explosion sequence for boss defeat in BossEnemy or game.update
}
};
self.update = function () {
if (self.isDestroyed || typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
// Potential movement or special attacks later
};
return self;
});
var EnemyProjectile = Container.expand(function (fireAngle) {
var self = Container.call(this);
self.bulletSprite = self.attachAsset('enemyBulletSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = fireAngle; // Angle of movement
self.speed = 10; // Enemy projectiles are a bit slower
if (self.bulletSprite) {
// Assuming the bullet shape is wider than tall, rotating by angle aligns its length with movement.
self.bulletSprite.rotation = self.angle;
}
self.isOffScreen = false;
self.update = function () {
if (self.isOffScreen) return;
if (typeof freezeEnemies !== 'undefined' && freezeEnemies) return; // Freeze enemy projectiles during card choice
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
var gameWidth = 2048;
var gameHeight = 2732;
// Margin based on projectile size to ensure it's fully off-screen
var margin = 50; // Default margin
if (self.bulletSprite && (self.bulletSprite.width || self.bulletSprite.height)) {
margin = Math.max(self.bulletSprite.width || 0, self.bulletSprite.height || 0) / 2 + 50;
}
if (self.x < -margin || self.x > gameWidth + margin || self.y < -margin || self.y > gameHeight + margin) {
self.isOffScreen = true;
}
};
return self;
});
// Orange rectangular bullet
var EnemyShip = Container.expand(function () {
var self = Container.call(this);
self.fireCooldown = 180; // Fire every 3 seconds (180 ticks at 60FPS)
self.fireTimer = Math.random() * self.fireCooldown; // Stagger initial firing
self.enemyProjectileSpawnPoint = {
x: 0,
y: 0
};
self.hit = function () {
if (self.isDestroyed) return; // Already destroyed
self.health--;
if (self.health <= 0) {
self.isDestroyed = true;
// Optionally, trigger a small visual effect here like a flash
// LK.effects.flashObject(self, 0xffffff, 100);
} else {
// Optionally, visual effect for taking damage but not destroyed
// LK.effects.flashObject(self, 0xffaaaa, 50);
}
};
self.update = function () {
if (self.isOffScreen || self.isDestroyed) return; // Don't update if off-screen or destroyed
if (typeof freezeEnemies !== 'undefined' && freezeEnemies) return; // Freeze all enemy movement and firing during card choice
// Target the player ship
if (typeof playerShip !== 'undefined' && playerShip && !playerShip.isDestroyed) {
var dx = playerShip.x - self.x;
var dy = playerShip.y - self.y;
// Calculate angle towards player
var targetAngle = Math.atan2(dy, dx);
// Update the enemy's angle to face the player
self.angle = targetAngle;
// Update sprite rotation to match the new angle
// Assuming the ship sprite is designed "pointing up" (nose along its local -Y axis or top)
// visual rotation = world angle + PI/2.
if (self.shipSprite) {
self.shipSprite.rotation = self.angle + Math.PI / 2;
}
}
// If playerShip is not available or is destroyed, the enemy will continue in its current self.angle.
self.lastX = self.x;
self.lastY = self.y;
// Prevent enemy from moving too close to the player
var safeDistance = 120; // Minimum allowed distance between enemy and player
var moveX = Math.cos(self.angle) * self.speed;
var moveY = Math.sin(self.angle) * self.speed;
if (typeof playerShip !== 'undefined' && playerShip && !playerShip.isDestroyed) {
var dxToPlayer = self.x + moveX - playerShip.x;
var dyToPlayer = self.y + moveY - playerShip.y;
var distToPlayer = Math.sqrt(dxToPlayer * dxToPlayer + dyToPlayer * dyToPlayer);
if (distToPlayer < safeDistance) {
// If moving would bring us too close, do not move this frame
moveX = 0;
moveY = 0;
}
}
self.x += moveX;
self.y += moveY;
// Update enemy projectile spawn point
var shipAsset = self.shipSprite;
if (shipAsset) {
var noseDistance = shipAsset.height / 2; // Assuming front is along height axis from center
self.enemyProjectileSpawnPoint.x = self.x + Math.cos(self.angle) * noseDistance;
self.enemyProjectileSpawnPoint.y = self.y + Math.sin(self.angle) * noseDistance;
}
// Firing logic
self.fireTimer--;
if (self.fireTimer <= 0) {
if (typeof EnemyProjectile !== 'undefined' && typeof enemyProjectiles !== 'undefined' && game && typeof game.addChild === 'function') {
var newProjectile = new EnemyProjectile(self.angle); // Fire in the direction the ship is moving
newProjectile.x = self.enemyProjectileSpawnPoint.x;
newProjectile.y = self.enemyProjectileSpawnPoint.y;
enemyProjectiles.push(newProjectile);
game.addChild(newProjectile);
self.fireTimer = self.fireCooldown; // Reset cooldown
}
}
// Generalized off-screen check
var gameWidth = 2048;
var gameHeight = 2732;
var marginWidth = self.shipSprite && self.shipSprite.width ? self.shipSprite.width / 2 + 50 : 100;
var marginHeight = self.shipSprite && self.shipSprite.height ? self.shipSprite.height / 2 + 50 : 100;
if (self.x < -marginWidth || self.x > gameWidth + marginWidth || self.y < -marginHeight || self.y > gameHeight + marginHeight) {
self.isOffScreen = true;
}
};
self.shipSprite = self.attachAsset('enemyShipSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = Math.PI / 2; // Default angle: moving downwards (positive Y direction)
if (self.shipSprite) {
self.shipSprite.rotation = self.angle + Math.PI / 2;
}
self.speed = 4;
self.isOffScreen = false;
self.lastX = self.x;
self.lastY = self.y;
self.health = 1;
self.isDestroyed = false;
return self;
});
var FireButton = Container.expand(function () {
var self = Container.call(this);
self.buttonSprite = self.attachAsset('fireButtonSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.lastFireTick = -1000; // Track last tick when fired
self.down = function (x, y, obj) {
// Prevent holding down for auto-fire: only allow firing if enough time has passed since last fire
if (typeof LK !== 'undefined' && typeof LK.ticks !== 'undefined') {
if (typeof self.lastFireTick === 'undefined') self.lastFireTick = -1000;
// Only allow firing if at least fireButtonCooldown ticks have passed since last fire
if (typeof fireButtonCooldown === 'undefined') fireButtonCooldown = 30;
if (LK.ticks - self.lastFireTick < fireButtonCooldown) return;
}
// Only fire if player has ammo and not holding down for auto-fire
if (typeof playerAmmo !== 'undefined' && playerAmmo > 0) {
if (playerShip && typeof playerShip.currentAngle !== 'undefined' && typeof projectileSpawnPoint !== 'undefined' && playerProjectiles && PlayerProjectile) {
if (typeof playerSplitShotActive !== 'undefined' && playerSplitShotActive) {
var spreadAngle = Math.PI / 10; // 18 degrees spread for each projectile from center
// Projectile 1 (left/upward component of spread)
var proj1 = new PlayerProjectile(playerShip.currentAngle - spreadAngle);
proj1.x = projectileSpawnPoint.x;
proj1.y = projectileSpawnPoint.y;
playerProjectiles.push(proj1);
game.addChild(proj1);
// Projectile 2 (right/downward component of spread)
var proj2 = new PlayerProjectile(playerShip.currentAngle + spreadAngle);
proj2.x = projectileSpawnPoint.x;
proj2.y = projectileSpawnPoint.y;
playerProjectiles.push(proj2);
game.addChild(proj2);
} else {
// Single projectile (default behavior)
var newProjectile = new PlayerProjectile(playerShip.currentAngle);
newProjectile.x = projectileSpawnPoint.x;
newProjectile.y = projectileSpawnPoint.y;
playerProjectiles.push(newProjectile);
game.addChild(newProjectile);
}
playerAmmo = Math.max(0, playerAmmo - 1); // Decrease ammo by 1, never below 0
// Removed ammo counter update as per requirements
if (typeof LK !== 'undefined' && typeof LK.ticks !== 'undefined') {
self.lastFireTick = LK.ticks;
}
// Play shot sound when firing
if (typeof LK !== 'undefined' && typeof LK.getSound === 'function') {
var shotSound = LK.getSound('Shot');
if (shotSound && typeof shotSound.play === 'function') {
shotSound.play();
}
}
}
}
};
return self;
});
var HealthRestore = Container.expand(function () {
var self = Container.call(this);
self.restoreSprite = self.attachAsset('healthRestoreSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
// Check for collision with player ship
if (playerShip && !playerShip.isDestroyed && self.intersects(playerShip)) {
if (playerShip.health < 3) {
playerShip.health = Math.min(playerShip.health + 1, 3); // Restore health, max 3
if (playerShip.shipSprite) {
LK.effects.flashObject(playerShip.shipSprite, 0x00ff00, 150); // Flash green on health restore
}
// Update hearts display
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < playerShip.health;
}
}
self.destroy();
}
};
return self;
});
var Heart = Container.expand(function () {
var self = Container.call(this);
self.heartSprite = self.attachAsset('heartSprite', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
return self;
});
// PlayerShip class to handle player ship logic and projectile spawn point calculation
var HomingMissile = Container.expand(function (initialAngle) {
var self = Container.call(this);
self.missileSprite = self.attachAsset('HomingMissileSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = initialAngle;
self.speed = 7;
self.turnRate = 0.05; // Radians per tick
self.isOffScreen = false;
self.lastX = self.x;
self.lastY = self.y;
if (self.missileSprite) {
// Ensure sprite exists before setting rotation
self.missileSprite.rotation = self.angle; // Assuming sprite points right (0 rad) initially
}
self.update = function () {
if (self.isOffScreen || typeof freezeEnemies !== 'undefined' && freezeEnemies) return;
if (playerShip && !playerShip.isDestroyed) {
var targetAngle = Math.atan2(playerShip.y - self.y, playerShip.x - self.x);
var angleDiff = targetAngle - self.angle;
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
var turnAmount = Math.sign(angleDiff) * Math.min(self.turnRate, Math.abs(angleDiff));
self.angle += turnAmount;
}
if (self.missileSprite) self.missileSprite.rotation = self.angle;
self.lastX = self.x;
self.lastY = self.y;
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
var gameWidth = 2048;
var gameHeight = 2732;
var margin = (self.missileSprite && self.missileSprite.width ? self.missileSprite.width / 2 : 25) + 50;
if (self.x < -margin || self.x > gameWidth + margin || self.y < -margin || self.y > gameHeight + margin) {
self.isOffScreen = true;
}
};
return self;
});
// Red circular fire button
var Joystick = Container.expand(function () {
var self = Container.call(this);
self.baseSprite = self.attachAsset('joystickBaseSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.knobSprite = self.attachAsset('joystickKnobSprite', {
anchorX: 0.5,
anchorY: 0.5
});
// Maximum distance the knob's center can move from the base's center
self.radius = self.baseSprite.width / 2 - self.knobSprite.width / 2;
if (self.radius <= 0) {
// Ensure a sensible minimum radius
self.radius = self.baseSprite.width > 0 ? self.baseSprite.width / 4 : 50; // Use 1/4 of base width or 50 if base has no width
}
self.isDragging = false;
self.inputVector = {
x: 0,
y: 0
}; // Normalized output (-1 to 1)
self.handleDown = function (localX, localY) {
// localX, localY are relative to the joystick's center (its origin)
// Check if the touch is within the larger base area to start dragging
var distSqFromCenter = localX * localX + localY * localY;
if (distSqFromCenter <= self.baseSprite.width / 2 * (self.baseSprite.width / 2)) {
self.isDragging = true;
self.handleMove(localX, localY); // Snap knob to initial touch position
return true; // Indicates joystick took control
}
return false; // Joystick not activated
};
self.handleMove = function (localX, localY) {
if (!self.isDragging) return;
var dist = Math.sqrt(localX * localX + localY * localY);
if (dist > self.radius) {
// Normalize and scale to radius if touch is outside draggable area
self.knobSprite.x = localX / dist * self.radius;
self.knobSprite.y = localY / dist * self.radius;
} else {
self.knobSprite.x = localX;
self.knobSprite.y = localY;
}
// Calculate normalized input vector
if (self.radius > 0) {
self.inputVector.x = self.knobSprite.x / self.radius;
self.inputVector.y = self.knobSprite.y / self.radius;
} else {
// Avoid division by zero if radius is zero
self.inputVector.x = 0;
self.inputVector.y = 0;
}
};
self.handleUp = function () {
if (self.isDragging) {
self.isDragging = false;
// Reset knob to center and clear input vector
self.knobSprite.x = 0;
self.knobSprite.y = 0;
self.inputVector.x = 0;
self.inputVector.y = 0;
}
};
self.getInput = function () {
return self.inputVector;
};
self.isActive = function () {
return self.isDragging;
};
return self;
});
// Projectile class for player's bullets
var PlayerProjectile = Container.expand(function (fireAngle) {
var self = Container.call(this);
// Attach bullet sprite
self.bulletSprite = self.attachAsset('playerBulletSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.angle = fireAngle; // Store the firing angle
self.speed = 20; // Projectile speed magnitude
// The playerBulletSprite has orientation:1, meaning it's rotated 90deg clockwise.
// If original was thin vertical, it's now thin horizontal, "pointing" along its X-axis.
// So, self.angle directly applies.
self.bulletSprite.rotation = self.angle;
self.isOffScreen = false; // Flag to indicate if projectile is off-screen
// Update method to move projectile
self.update = function () {
if (self.isOffScreen) return; // Don't update if already marked off-screen
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
// Check if off-screen
var gameWidth = 2048;
var gameHeight = 2732;
// Margin based on projectile size to ensure it's fully off-screen
var margin = Math.max(self.bulletSprite.width, self.bulletSprite.height) / 2 + 50;
if (self.x < -margin || self.x > gameWidth + margin || self.y < -margin || self.y > gameHeight + margin) {
self.isOffScreen = true;
}
}; //{M} // Note: Original L was self.destroy related, removed.
return self;
});
var PlayerShip = Container.expand(function () {
var self = Container.call(this);
self.health = 3; // Player can take a few hits
self._fractionalHealth = self.health; // For fractional damage (e.g., 0.5 from armored enemy)
self.isDestroyed = false;
self.hit = function () {
if (self.isDestroyed) return;
if (typeof self._fractionalHealth === 'undefined') self._fractionalHealth = self.health;
self._fractionalHealth -= 1;
self.health = Math.floor(self._fractionalHealth);
LK.effects.flashObject(self.shipSprite || self, 0xff0000, 150); // Flash red on hit (flash sprite if available)
// Update hearts display
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < self.health;
}
if (self._fractionalHealth <= 0) {
self.isDestroyed = true;
// Game over will be triggered in game.update
}
};
// Attach player ship sprite and set reference for spawn point calculation
self.shipSprite = self.attachAsset('playerShipSprite', {
anchorX: 0.5,
anchorY: 0.5
});
self.moveSpeed = 10; // Pixels per tick for movement speed
// currentAngle is the direction the ship moves and fires projectiles.
// 0 radians = right, -PI/2 = up (screen coordinates).
self.currentAngle = -Math.PI / 2; // Initial angle: pointing up.
// playerShipSprite is assumed to be designed pointing "up" (its nose along its local -Y axis).
// To make the sprite's nose align with currentAngle, its visual rotation needs adjustment.
// Visual rotation = currentAngle + Math.PI / 2.
// E.g., currentAngle = -PI/2 (up) => visual rotation = 0.
// E.g., currentAngle = 0 (right) => visual rotation = PI/2.
self.shipSprite.rotation = self.currentAngle + Math.PI / 2;
// Initialize projectile spawn point (remains important)
// This will be updated relative to the ship's current position in self.update()
// Initial dummy values, will be set correctly on first update.
projectileSpawnPoint.x = 0;
projectileSpawnPoint.y = 0;
self.applyJoystickInput = function (inputX, inputY) {
// Update angle only if joystick provides directional input
if (inputX !== 0 || inputY !== 0) {
self.currentAngle = Math.atan2(inputY, inputX);
self.shipSprite.rotation = self.currentAngle + Math.PI / 2;
}
self.x += inputX * self.moveSpeed;
self.y += inputY * self.moveSpeed;
// Boundary checks to keep ship on screen
var halfWidth = self.shipSprite.width / 2;
var halfHeight = self.shipSprite.height / 2;
var gameWidth = 2048;
var gameHeight = 2732;
var topSafeMargin = 100; // Top 100px area is reserved
if (self.x - halfWidth < 0) self.x = halfWidth;
if (self.x + halfWidth > gameWidth) self.x = gameWidth - halfWidth;
if (self.y - halfHeight < topSafeMargin) self.y = topSafeMargin + halfHeight;
if (self.y + halfHeight > gameHeight) self.y = gameHeight - halfHeight;
};
// Update projectile spawn point every frame based on player ship position and rotation
self.update = function () {
var shipAsset = self.shipSprite;
if (!shipAsset) return;
// Calculate spawn point at the tip of the ship, considering its currentAngle.
// The "nose" is shipAsset.height / 2 distance from the center.
var noseDistance = shipAsset.height / 2;
projectileSpawnPoint.x = self.x + Math.cos(self.currentAngle) * noseDistance;
projectileSpawnPoint.y = self.y + Math.sin(self.currentAngle) * noseDistance;
};
return self;
});
var PlayerTrail = Container.expand(function () {
var self = Container.call(this);
self.trailSprite = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.4,
scaleY: 0.4,
tint: 0xff3333
});
self.lifeTime = 60; // Trail segment lasts 1 second at 60fps
self.currentLife = self.lifeTime;
self.update = function () {
self.currentLife--;
if (self.currentLife <= 0) {
self.isExpired = true;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
//Only include the plugins you need to create the game.
// Placeholder ID
// Placeholder ID
// Placeholder ID
// Placeholder ID
// Placeholder ID
// Projectile spawn point (relative to player ship center)
// Will be updated in PlayerShip class update
var projectileSpawnPoint = {
x: 0,
y: 0
};
var joystick; // Declare joystick instance
// Array to keep track of all player projectiles
var playerProjectiles = [];
// Array to keep track of player trail segments
var playerTrailSegments = [];
var trailSpawnTimer = 0;
// Array to keep track of enemy trail segments
var enemyTrailSegments = [];
var enemyTrailSpawnTimer = 0;
// Player projectile damage (can be doubled by Cardsttsck)
var playerProjectileDamage = 1;
// Player ammo count (start with 10). Player cannot fire forever: must wait for ammo to restore and cannot hold fire for auto-fire.
var playerAmmo = 10;
// Create and add the game background
// You can change the background by updating the ID in the Assets section for 'game_background_image'
var gameBackground = LK.getAsset('game_background_image', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(gameBackground);
// Add colorful stars to the background
var starColors = [0xFFFFFF, 0xFF3333, 0x33FF33, 0x3333FF, 0xFFFF33, 0xFF33FF, 0x33FFFF, 0xFFAA33, 0xAA33FF, 0x33AAFF];
for (var i = 0; i < 50; i++) {
var star = LK.getAsset('Star', {
anchorX: 0.5,
anchorY: 0.5,
x: Math.random() * 2048,
y: Math.random() * 2732,
tint: starColors[Math.floor(Math.random() * starColors.length)]
});
// 30% chance for a star to have disappearing/reappearing effect instead of twinkling
if (Math.random() < 0.3) {
// Create disappearing and reappearing effect for some stars
var disappearDelay = 2000 + Math.random() * 8000; // Random delay 2-10 seconds before first disappear
LK.setTimeout(function () {
function startDisappearCycle() {
// Disappear quickly
tween(star, {
alpha: 0
}, {
duration: 200 + Math.random() * 300,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Stay invisible for a random time
var invisibleTime = 500 + Math.random() * 2000; // 0.5-2.5 seconds invisible
LK.setTimeout(function () {
// Reappear
tween(star, {
alpha: 1.0
}, {
duration: 300 + Math.random() * 500,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Wait before next disappear cycle
var nextCycleDelay = 3000 + Math.random() * 7000; // 3-10 seconds before next cycle
LK.setTimeout(function () {
startDisappearCycle(); // Start the cycle again
}, nextCycleDelay);
}
});
}, invisibleTime);
}
});
}
startDisappearCycle();
}, disappearDelay);
} else {
// Add normal twinkling animation to other stars
tween(star, {
alpha: 0.3
}, {
duration: 1000 + Math.random() * 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(star, {
alpha: 1.0
}, {
duration: 1000 + Math.random() * 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Create infinite loop by calling the twinkling again
tween(star, {
alpha: 0.3
}, {
duration: 1000 + Math.random() * 1000,
easing: tween.easeInOut
});
}
});
}
});
}
game.addChild(star);
}
// Adding it here as one of the first children to 'game' ensures it's in the background.
var maxPlayerAmmo = 10; // Maximum player ammo capacity
// Fire button cooldown in ticks (default 30, can be halved by Card1)
var fireButtonCooldown = 30;
// Array to keep track of all enemy ships
var enemyShips = [];
// Array to keep track of all enemy projectiles
var enemyProjectiles = [];
// Global flag: freeze all enemy movement and firing during card choice
var freezeEnemies = false;
var playerSplitShotActive = false; // Flag for Asplitmind card: player fires two projectiles
// Create player ship and add to game
var playerShip = new PlayerShip();
// Apply saved skin selection
if (selectedSkin && selectedSkin !== 'playerShipSprite') {
var oldRotation = playerShip.shipSprite.rotation;
playerShip.shipSprite.destroy();
playerShip.shipSprite = playerShip.attachAsset(selectedSkin, {
anchorX: 0.5,
anchorY: 0.5
});
playerShip.shipSprite.rotation = oldRotation;
}
game.addChild(playerShip);
// Display hearts for player health
var hearts = [];
for (var i = 0; i < playerShip.health; i++) {
var heart = new Heart();
heart.x = i * 120 + 120; // Position hearts with increased spacing
heart.y = 60; // Top-left corner
LK.gui.topLeft.addChild(heart);
hearts.push(heart);
}
// Load saved money from storage
var money = storage.money || 0;
var moneyTxt = new Text2(money + " $", {
size: 120,
fill: 0xFFE066,
font: "monospace, 'Press Start 2P', 'VT323', 'Courier New', Courier, monospace",
// pixel/retro style
align: "right"
});
moneyTxt.anchor.set(1, 0); // Right-top anchor
moneyTxt.x = -60; // Padding from right edge
moneyTxt.y = 200; // Move money text down to make room for settings
LK.gui.topRight.addChild(moneyTxt);
// Settings icon in top-right corner
var settingsIcon = new Text2("⚙", {
size: 80,
fill: 0xFFFFFF,
align: "right"
});
settingsIcon.anchor.set(1, 0); // Right-top anchor
settingsIcon.x = -60; // Padding from right edge
settingsIcon.y = 60; // Top padding
settingsIcon.interactive = true;
settingsIcon.down = function (x, y, obj) {
// Open ship skin selection menu
if (game._skinMenuActive) return; // Prevent multiple menus
openSkinSelectionMenu();
};
// Ship skin selection menu functionality
var selectedSkin = storage.selectedSkin || 'shipSkin1';
// Initialize owned skins if not exists (default and heavy are free)
if (!storage.ownedSkins) {
storage.ownedSkins = ['shipSkin1', 'shipSkin3'];
}
// Initialize trail setting if not exists (enabled by default)
var trailEnabled = storage.trailEnabled !== undefined ? storage.trailEnabled : true;
// Initialize trail color if not exists (default to red)
var trailColor = storage.trailColor || 0xff3333;
// Ensure selected skin is valid (not Fighter skin)
if (selectedSkin === 'shipSkin2') {
selectedSkin = 'shipSkin1';
storage.selectedSkin = selectedSkin;
}
function openSkinSelectionMenu() {
game._skinMenuActive = true;
// Freeze all enemy movement and firing like card selection
freezeEnemies = true;
// Create overlay for skin selection
var skinOverlay = new Container();
skinOverlay.name = "skinSelectionOverlay";
// Semi-transparent background
var skinBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 20,
scaleY: 20,
x: 2048 / 2,
y: 2732 / 2,
tint: 0x000000,
alpha: 0.8
});
skinOverlay.addChild(skinBg);
// Title text
var titleText = new Text2("SELECT SHIP SKIN", {
size: 100,
fill: 0xFFFFFF,
align: "center"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 2732 / 2 - 400;
skinOverlay.addChild(titleText);
// Available ship skins
var availableSkins = [{
id: 'shipSkin1',
name: 'Default',
cost: 0
}, {
id: 'shipSkin3',
name: 'Heavy',
cost: 0
}, {
id: 'skinfire',
name: 'Ghost',
cost: 5
}, {
id: 'skinnlood',
name: 'Blood',
cost: 8
}, {
id: 'Skinrocet',
name: 'Rocket',
cost: 12
}];
// Position skins horizontally
var skinPositions = [{
x: 2048 / 2 - 540,
y: 2732 / 2
}, {
x: 2048 / 2 - 270,
y: 2732 / 2
}, {
x: 2048 / 2,
y: 2732 / 2
}, {
x: 2048 / 2 + 270,
y: 2732 / 2
}, {
x: 2048 / 2 + 540,
y: 2732 / 2
}];
for (var skinIdx = 0; skinIdx < availableSkins.length; skinIdx++) {
var skinData = availableSkins[skinIdx];
var skinContainer = new Container();
// Ship preview
var shipPreview = LK.getAsset(skinData.id, {
anchorX: 0.5,
anchorY: 0.5,
x: skinPositions[skinIdx].x,
y: skinPositions[skinIdx].y,
scaleX: 1.5,
scaleY: 1.5
});
skinContainer.addChild(shipPreview);
// Selection border for currently selected skin
if (skinData.id === selectedSkin) {
var selectionBorder = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: skinPositions[skinIdx].x,
y: skinPositions[skinIdx].y,
scaleX: 3,
scaleY: 3,
tint: 0x00FF00,
alpha: 0.3
});
skinContainer.addChild(selectionBorder);
}
// Skin name text
var skinNameText = new Text2(skinData.name, {
size: 60,
fill: 0xFFFFFF,
align: "center"
});
skinNameText.anchor.set(0.5, 0.5);
skinNameText.x = skinPositions[skinIdx].x;
skinNameText.y = skinPositions[skinIdx].y + 200;
skinContainer.addChild(skinNameText);
// Cost text - only show if skin is not owned
var ownedSkins = storage.ownedSkins || ['shipSkin1', 'shipSkin3'];
var isOwned = ownedSkins.indexOf(skinData.id) !== -1 || skinData.cost === 0;
if (!isOwned) {
var costText = new Text2(skinData.cost > 0 ? skinData.cost + " $" : "FREE", {
size: 40,
fill: skinData.cost > 0 ? 0xFFE066 : 0x00FF00,
align: "center"
});
costText.anchor.set(0.5, 0.5);
costText.x = skinPositions[skinIdx].x;
costText.y = skinPositions[skinIdx].y + 270;
skinContainer.addChild(costText);
}
// If player doesn't have enough money, make skin appear unavailable
if (skinData.cost > money) {
shipPreview.tint = 0x666666;
skinNameText.tint = 0x666666;
costText.fill = 0xFF4444;
}
skinContainer.interactive = true;
// Use closure to capture skin data
(function (currentSkinData) {
skinContainer.down = function (x_down, y_down, obj_down) {
if (!game._skinMenuActive) return;
// Check if skin costs money and player has enough
if (currentSkinData.cost > 0 && money < currentSkinData.cost) {
// Flash red to indicate insufficient funds
LK.effects.flashScreen(0xFF0000, 300);
return;
}
// Check if skin is already owned (either free or already purchased)
var ownedSkins = storage.ownedSkins || ['shipSkin1', 'shipSkin3']; // Default and Heavy are free
if (currentSkinData.cost > 0 && ownedSkins.indexOf(currentSkinData.id) === -1) {
// Purchase the skin
money -= currentSkinData.cost;
storage.money = money; // Save money to storage
moneyTxt.setText(money + " $");
ownedSkins.push(currentSkinData.id);
storage.ownedSkins = ownedSkins;
}
// Update selected skin
selectedSkin = currentSkinData.id;
storage.selectedSkin = selectedSkin;
// Update player ship sprite
if (playerShip && playerShip.shipSprite) {
var oldRotation = playerShip.shipSprite.rotation;
playerShip.shipSprite.destroy();
playerShip.shipSprite = playerShip.attachAsset(selectedSkin, {
anchorX: 0.5,
anchorY: 0.5
});
playerShip.shipSprite.rotation = oldRotation;
}
closeSkinSelectionMenu(skinOverlay);
};
})(skinData);
skinOverlay.addChild(skinContainer);
}
// Trail toggle button
var trailToggleText = trailEnabled ? "TRAIL: ON" : "TRAIL: OFF";
var trailToggleButton = new Text2(trailToggleText, {
size: 80,
fill: trailEnabled ? 0x00FF00 : 0xFF4444,
align: "center"
});
trailToggleButton.anchor.set(0.5, 0.5);
trailToggleButton.x = 2048 / 2 - 300;
trailToggleButton.y = 2732 / 2 + 400;
trailToggleButton.interactive = true;
trailToggleButton.down = function () {
if (!game._skinMenuActive) return;
// Toggle trail setting
trailEnabled = !trailEnabled;
storage.trailEnabled = trailEnabled;
// Update button text and color
trailToggleButton.setText(trailEnabled ? "TRAIL: ON" : "TRAIL: OFF");
trailToggleButton.fill = trailEnabled ? 0x00FF00 : 0xFF4444;
};
skinOverlay.addChild(trailToggleButton);
// Trail color selection section
var trailColorTitle = new Text2("TRAIL COLOR", {
size: 60,
fill: 0xFFFFFF,
align: "center"
});
trailColorTitle.anchor.set(0.5, 0.5);
trailColorTitle.x = 2048 / 2 + 300;
trailColorTitle.y = 2732 / 2 + 350;
skinOverlay.addChild(trailColorTitle);
// Available trail colors
var trailColors = [{
name: "RED",
color: 0xff3333
}, {
name: "BLUE",
color: 0x3333ff
}, {
name: "GREEN",
color: 0x33ff33
}, {
name: "YELLOW",
color: 0xffff33
}, {
name: "PURPLE",
color: 0xff33ff
}, {
name: "ORANGE",
color: 0xff6600
}];
// Get current trail color from storage or default to red
var currentTrailColor = storage.trailColor || 0xff3333;
// Display trail color options in a 3x2 grid
for (var colorIdx = 0; colorIdx < trailColors.length; colorIdx++) {
var colorData = trailColors[colorIdx];
var colorContainer = new Container();
// Calculate position in 3x2 grid
var col = colorIdx % 3;
var row = Math.floor(colorIdx / 3);
var colorX = 2048 / 2 + 150 + col * 100;
var colorY = 2732 / 2 + 420 + row * 80;
// Color preview circle
var colorPreview = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: colorX,
y: colorY,
scaleX: 0.8,
scaleY: 0.8,
tint: colorData.color
});
colorContainer.addChild(colorPreview);
// Selection border for currently selected color
if (colorData.color === currentTrailColor) {
var colorBorder = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: colorX,
y: colorY,
scaleX: 1.0,
scaleY: 1.0,
tint: 0xFFFFFF,
alpha: 0.5
});
colorContainer.addChild(colorBorder);
}
colorContainer.interactive = true;
// Use closure to capture color data
(function (currentColorData) {
colorContainer.down = function () {
if (!game._skinMenuActive) return;
// Update trail color
currentTrailColor = currentColorData.color;
storage.trailColor = currentTrailColor;
// Refresh the menu to show new selection
if (game._skinMenuActive) {
closeSkinSelectionMenu(skinOverlay);
openSkinSelectionMenu();
}
};
})(colorData);
skinOverlay.addChild(colorContainer);
}
// Close button
var closeButton = new Text2("✕", {
size: 120,
fill: 0xFF4444,
align: "center"
});
closeButton.anchor.set(0.5, 0.5);
closeButton.x = 2048 / 2 + 600;
closeButton.y = 2732 / 2 - 400;
closeButton.interactive = true;
closeButton.down = function () {
if (!game._skinMenuActive) return;
closeSkinSelectionMenu(skinOverlay);
};
skinOverlay.addChild(closeButton);
game.addChild(skinOverlay);
}
function closeSkinSelectionMenu(overlay) {
game._skinMenuActive = false;
// Resume game like card selection
freezeEnemies = false;
if (overlay && typeof overlay.destroy === 'function') {
overlay.destroy();
}
}
LK.gui.topRight.addChild(settingsIcon);
// Create black hole in top right corner
var blackHole = LK.getAsset('blackHole', {
anchorX: 0.5,
anchorY: 0.5
});
blackHole.x = -120; // Position from right edge
blackHole.y = 120; // Position from top edge
LK.gui.topRight.addChild(blackHole);
// Add swirling animation to black hole
tween(blackHole, {
rotation: Math.PI * 2
}, {
duration: 2000,
easing: tween.easeLinear,
loop: true
});
var enemiesKilled = 0; // Counter for killed enemies, to trigger boss
var bossEnemyInstance = null; // Holds the boss instance
var bossSpawned = false; // Flag to ensure boss spawns only once per game
var homingMissiles = []; // Array for new boss's homing missiles
var giantBossEnemyInstance = null; // Instance for the new giant boss
var giantBossSpawned = false; // Flag for the new giant boss
// Ammo counter removed as per requirements
// Center player ship horizontally, place near bottom
playerShip.x = 2048 / 2;
playerShip.y = 2732 - 350;
// Create Joystick
joystick = new Joystick();
game.addChild(joystick);
// Set initial position (e.g., 0,0), though it will jump to touch.
joystick.x = 0;
joystick.y = 0;
// Enemy spawn multiplier (default 1, can be set to 3 for 5 seconds after Card2)
game._enemySpawnMultiplier = 1;
game._enemySpawnMultiplierTimeout = null;
// Create and add Enemy Ships
var enemyShip1 = new EnemyShip();
var enemyShipAssetHeight1 = enemyShip1.shipSprite && enemyShip1.shipSprite.height ? enemyShip1.shipSprite.height : 146; // Default to asset height
enemyShip1.x = 2048 / 3;
enemyShip1.y = -(enemyShipAssetHeight1 / 2) - 50; // Start off-screen at the top
enemyShip1.lastX = enemyShip1.x; // Initialize lastX to the actual starting x
enemyShip1.lastY = enemyShip1.y; // Initialize lastY to the actual starting y
game.addChild(enemyShip1);
enemyShips.push(enemyShip1);
var enemyShip2 = new EnemyShip();
var enemyShipAssetHeight2 = enemyShip2.shipSprite && enemyShip2.shipSprite.height ? enemyShip2.shipSprite.height : 146; // Default to asset height
enemyShip2.x = 2048 * 2 / 3;
enemyShip2.y = -(enemyShipAssetHeight2 / 2) - 150; // Start off-screen, staggered further up
enemyShip2.lastX = enemyShip2.x; // Initialize lastX to the actual starting x
enemyShip2.lastY = enemyShip2.y; // Initialize lastY to the actual starting y
game.addChild(enemyShip2);
enemyShips.push(enemyShip2);
// Game event handlers
// Create and position Fire Button
var fireButton = new FireButton();
LK.gui.bottomLeft.addChild(fireButton);
// Position the fire button visually in the bottom-left corner with some margin
// The buttonSprite is 200x200 and anchored at its center (0.5, 0.5)
// fireButton (Container) is added to LK.gui.bottomLeft, positioning its top-left (0,0) at screen bottom-left.
// Adjust x and y to make the button appear correctly.
var buttonMargin = 50; // 50px margin from screen edges
fireButton.x = fireButton.buttonSprite.width / 2 + buttonMargin;
fireButton.y = -fireButton.buttonSprite.height / 2 - buttonMargin; // Negative Y moves up from the bottom edge
game.down = function (x, y, obj) {
if (game._cardChoiceActive || game._skinMenuActive) {
// Block joystick and ship movement while card choice or skin menu is active
return;
}
// x, y are global game coordinates
// Move the joystick to the touch position
joystick.x = x;
joystick.y = y;
joystick.visible = true;
// Activate the joystick. Since the joystick's origin is now at the touch point (x,y),
// the local coordinates for the touch within the joystick are (0,0).
// This centers the knob under the finger and initiates dragging.
joystick.handleDown(0, 0); // Pass (0,0) as local coordinates
// Note: With this "floating joystick" implementation, the joystick activates on any screen press.
// The previous mechanic, where tapping away from a fixed joystick fired projectiles,
// is superseded. Further design would be needed for a separate firing mechanism.
};
game.move = function (x, y, obj) {
if (game._cardChoiceActive || game._skinMenuActive) {
// Block joystick movement while card choice or skin menu is active
return;
}
if (joystick.isActive()) {
var joystickLocalPos = joystick.toLocal({
x: x,
y: y
});
joystick.handleMove(joystickLocalPos.x, joystickLocalPos.y);
}
};
game.up = function (x, y, obj) {
if (game._cardChoiceActive || game._skinMenuActive) {
// Block joystick release while card choice or skin menu is active
return;
}
if (joystick.isActive()) {
joystick.handleUp(); // Resets knob, inputVector, and isDragging flag
joystick.visible = false; // Hide the joystick when the touch is released
}
};
// Update game state
var ammoRestoreCooldown = 0; // Ticks until next ammo restore
// Start with space music
LK.playMusic('Space');
game.update = function () {
// Prevent all game logic and object updates while card choice or skin menu overlay is active
if (game._cardChoiceActive || game._skinMenuActive) {
// Only allow overlay input, skip all updates and movement
return;
}
// Restore ammo if not full, with a delay (e.g., 30 ticks = 0.5s)
if (typeof playerAmmo !== 'undefined') {
if (typeof maxPlayerAmmo === 'undefined') maxPlayerAmmo = 10; // Defensive: ensure maxPlayerAmmo is defined
if (playerAmmo < maxPlayerAmmo) {
// Use maxPlayerAmmo instead of hardcoded 10
if (typeof ammoRestoreCooldown === 'undefined') ammoRestoreCooldown = 0;
if (ammoRestoreCooldown > 0) {
ammoRestoreCooldown--;
} else {
playerAmmo += 1;
// Removed ammo counter update as per requirements
ammoRestoreCooldown = 30; // 0.5s at 60FPS
}
} else {
// Player is at or above max ammo.
// Ensure current ammo doesn't exceed new max if it somehow did, though unlikely with current regen.
playerAmmo = Math.min(playerAmmo, maxPlayerAmmo);
ammoRestoreCooldown = 0;
}
}
// Apply joystick input to player ship movement
if (joystick && playerShip && playerShip.applyJoystickInput) {
var input = joystick.getInput();
playerShip.applyJoystickInput(input.x, input.y);
}
// Update player ship (e.g., to recalculate projectileSpawnPoint after moving)
if (playerShip && playerShip.update) {
playerShip.update();
}
// Create trail segments behind player
trailSpawnTimer++;
if (trailSpawnTimer >= 3 && playerShip && !playerShip.isDestroyed && trailEnabled) {
var trailSegment = new PlayerTrail();
trailSegment.x = playerShip.x;
trailSegment.y = playerShip.y;
// Get current trail color from storage
var currentTrailColor = storage.trailColor || 0xff3333;
// Add bright pulsing effect to make trail more vibrant with selected color
if (trailSegment.trailSprite) {
// Calculate a lighter version of the selected color for pulsing effect
var lighterColor = currentTrailColor;
// Add brightness by increasing RGB values proportionally
var r = Math.min(255, (currentTrailColor >> 16 & 0xFF) + 50);
var g = Math.min(255, (currentTrailColor >> 8 & 0xFF) + 50);
var b = Math.min(255, (currentTrailColor & 0xFF) + 50);
lighterColor = r << 16 | g << 8 | b;
trailSegment.trailSprite.tint = currentTrailColor;
tween(trailSegment.trailSprite, {
tint: lighterColor
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(trailSegment.trailSprite, {
tint: currentTrailColor
}, {
duration: 200,
easing: tween.easeInOut
});
}
});
}
game.addChild(trailSegment);
playerTrailSegments.push(trailSegment);
trailSpawnTimer = 0;
}
// Create trail segments behind enemies
enemyTrailSpawnTimer++;
if (enemyTrailSpawnTimer >= 4) {
// Create trails for all active enemies
for (var e = 0; e < enemyShips.length; e++) {
var enemy = enemyShips[e];
if (!enemy.isDestroyed && !enemy.isOffScreen) {
var enemyTrailSegment = new PlayerTrail();
enemyTrailSegment.x = enemy.x;
enemyTrailSegment.y = enemy.y;
// Make enemy trails orange/yellow color
if (enemyTrailSegment.trailSprite) {
enemyTrailSegment.trailSprite.tint = 0xff6600;
tween(enemyTrailSegment.trailSprite, {
tint: 0xffaa33
}, {
duration: 200,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(enemyTrailSegment.trailSprite, {
tint: 0xff6600
}, {
duration: 200,
easing: tween.easeInOut
});
}
});
}
game.addChild(enemyTrailSegment);
enemyTrailSegments.push(enemyTrailSegment);
}
}
enemyTrailSpawnTimer = 0;
}
// Update and clean up trail segments
for (var t = playerTrailSegments.length - 1; t >= 0; t--) {
var trailSeg = playerTrailSegments[t];
if (trailSeg.update) {
trailSeg.update();
}
// Calculate alpha based on remaining life (1.0 to 0.0)
var alpha = trailSeg.currentLife / trailSeg.lifeTime;
if (trailSeg.trailSprite) {
trailSeg.trailSprite.alpha = Math.max(0.3, alpha); // Keep minimum visibility of 0.3
}
if (trailSeg.isExpired) {
trailSeg.destroy();
playerTrailSegments.splice(t, 1);
}
}
// Update and clean up enemy trail segments
for (var et = enemyTrailSegments.length - 1; et >= 0; et--) {
var enemyTrailSeg = enemyTrailSegments[et];
if (enemyTrailSeg.update) {
enemyTrailSeg.update();
}
// Calculate alpha based on remaining life (1.0 to 0.0)
var enemyAlpha = enemyTrailSeg.currentLife / enemyTrailSeg.lifeTime;
if (enemyTrailSeg.trailSprite) {
enemyTrailSeg.trailSprite.alpha = Math.max(0.2, enemyAlpha); // Slightly more transparent than player trails
}
if (enemyTrailSeg.isExpired) {
enemyTrailSeg.destroy();
enemyTrailSegments.splice(et, 1);
}
}
// Update and clean up projectiles, and check for collisions with enemies
for (var i = playerProjectiles.length - 1; i >= 0; i--) {
var proj = playerProjectiles[i];
if (proj.update) {
proj.update(); // Call projectile's own update logic (movement)
}
// Check if the projectile flagged itself as off-screen AFTER moving
if (proj.isOffScreen) {
proj.destroy(); // Destroy the projectile
playerProjectiles.splice(i, 1); // Remove from the tracking array
continue; // Move to the next projectile
}
// Collision detection with enemy ships
for (var k = enemyShips.length - 1; k >= 0; k--) {
var enemy = enemyShips[k];
// Skip already destroyed or off-screen enemies for collision checks
if (enemy.isDestroyed || enemy.isOffScreen) {
continue;
}
if (proj.intersects(enemy)) {
// Use playerProjectileDamage if defined, else default to 1
var dmg = typeof playerProjectileDamage !== 'undefined' ? playerProjectileDamage : 1;
if (typeof enemy.health === 'number') {
enemy.health -= dmg;
if (enemy.health <= 0) {
enemy.isDestroyed = true;
} else {
if (typeof enemy.hit === 'function') enemy.hit();
}
} else {
if (typeof enemy.hit === 'function') enemy.hit();
}
proj.destroy(); // Projectile is consumed on hit
playerProjectiles.splice(i, 1); // Remove projectile
// If enemy.isDestroyed became true, it will be handled in the enemyShips loop below.
// Score will be awarded there too.
break; // Projectile is gone, no need to check against other enemies
}
}
// If projectile still exists, check collision with boss parts
if (!proj.isOffScreen && bossEnemyInstance && !bossEnemyInstance.isDestroyed) {
var bossPartsToCheck = [];
if (bossEnemyInstance.mainBody && !bossEnemyInstance.mainBody.isDestroyed) {
bossPartsToCheck.push(bossEnemyInstance.mainBody);
}
if (bossEnemyInstance.leftCannon && !bossEnemyInstance.leftCannon.isDestroyed) {
bossPartsToCheck.push(bossEnemyInstance.leftCannon);
}
if (bossEnemyInstance.rightCannon && !bossEnemyInstance.rightCannon.isDestroyed) {
bossPartsToCheck.push(bossEnemyInstance.rightCannon);
}
for (var bp_idx = 0; bp_idx < bossPartsToCheck.length; bp_idx++) {
var part = bossPartsToCheck[bp_idx];
if (proj.intersects(part)) {
var dmg = typeof playerProjectileDamage !== 'undefined' ? playerProjectileDamage : 1;
part.hit(dmg); // Part handles its own health and destruction visuals
if (part.isDestroyed) {
// Award points/money for destroying a part
if (part instanceof BossCannon) {
LK.setScore(LK.getScore() + 50); // More points for a cannon
money += 25;
storage.money = money; // Save money to storage
if (typeof moneyTxt !== 'undefined' && moneyTxt.setText) {
moneyTxt.setText(money + " $");
}
}
// Main body destruction score is handled when boss is fully defeated
}
proj.destroy(); // Projectile is consumed
playerProjectiles.splice(i, 1);
break; // Projectile is gone, stop checking this projectile against other boss parts
}
}
}
// If proj was spliced (either by hitting enemy or boss part), loop continues correctly.
}
// Update and clean up enemy projectiles, and check for collisions with player
for (var l = enemyProjectiles.length - 1; l >= 0; l--) {
var eProj = enemyProjectiles[l];
if (eProj.update) {
eProj.update(); // Call projectile's own update logic (movement)
}
// Check if the projectile flagged itself as off-screen AFTER moving
if (eProj.isOffScreen) {
if (typeof eProj.destroy === 'function') eProj.destroy();
enemyProjectiles.splice(l, 1); // Remove from the tracking array
continue; // Move to the next projectile
}
// Collision detection with player ship
// Ensure playerShip exists and is not already destroyed before checking intersection
if (playerShip && !playerShip.isDestroyed && typeof eProj.intersects === 'function' && eProj.intersects(playerShip)) {
// Check if the projectile's parent is an ArmoredEnemyShip (for 0.5 damage)
var isArmored = false;
if (typeof enemyShips !== 'undefined') {
for (var ai = 0; ai < enemyShips.length; ai++) {
var es = enemyShips[ai];
if (es instanceof ArmoredEnemyShip && es.enemyProjectileSpawnPoint && Math.abs(eProj.x - es.enemyProjectileSpawnPoint.x) < 2 && Math.abs(eProj.y - es.enemyProjectileSpawnPoint.y) < 2) {
isArmored = true;
break;
}
}
}
if (isArmored) {
// Deal 0.5 damage: use a fractional health system
if (typeof playerShip.health === 'number') {
if (typeof playerShip._fractionalHealth === 'undefined') playerShip._fractionalHealth = playerShip.health;
playerShip._fractionalHealth -= 0.5;
if (playerShip._fractionalHealth <= 0) {
playerShip.health = 0;
playerShip.isDestroyed = true;
if (playerShip.shipSprite) playerShip.shipSprite.visible = false;else if (typeof playerShip.visible !== 'undefined') playerShip.visible = false;
LK.showGameOver();
} else {
playerShip.health = Math.floor(playerShip._fractionalHealth);
LK.effects.flashObject(playerShip.shipSprite || playerShip, 0xff0000, 150);
// Update hearts display
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < playerShip.health;
}
}
}
} else {
if (typeof playerShip.hit === 'function') playerShip.hit(); // Player takes damage
if (playerShip.isDestroyed) {
if (playerShip.shipSprite) playerShip.shipSprite.visible = false;else if (typeof playerShip.visible !== 'undefined') playerShip.visible = false;
LK.showGameOver();
}
}
if (typeof eProj.destroy === 'function') eProj.destroy(); // Projectile is consumed on hit
enemyProjectiles.splice(l, 1); // Remove projectile
// If projectile hit player, it's gone. No need to check further for this projectile.
}
}
// Update and clean up enemy ships
// This loop should still run even if player is destroyed, to clean up existing enemies,
// though new game state might prevent further updates if LK.showGameOver() is effective immediately.
var _loop = function _loop() {
enemy = enemyShips[j]; // First, check if the enemy ship is marked as destroyed (e.g., by a projectile hit)
if (enemy.isDestroyed) {
// Capture enemy's current position for the explosion sequence
var explosionX = enemy.x;
var explosionY = enemy.y;
// Show Boom image at the captured position
var localBoomImg1 = LK.getAsset('Boom', {
// Use a new local variable for the first boom image
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X coordinate
y: explosionY // Use captured Y coordinate
});
game.addChild(localBoomImg1);
// Remove Boom image after a short delay (e.g., 125ms), then show Boom2 at the same position
LK.setTimeout(function () {
if (localBoomImg1 && typeof localBoomImg1.destroy === 'function') localBoomImg1.destroy(); // Destroy the correct local image
// Show Boom2 image at the same captured position
var boom2Img = LK.getAsset('Boom2', {
// This was already var, so local, good.
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom2Img);
// Remove Boom2 image after a short delay (e.g., 125ms), then show Boom3 at the same position
LK.setTimeout(function () {
if (boom2Img && typeof boom2Img.destroy === 'function') boom2Img.destroy();
// Show Boom3 image at the same captured position
var boom3Img = LK.getAsset('Boom3', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom3Img);
// Remove Boom3 image after a short delay (e.g., 125ms), then show Boom4 at the same position
LK.setTimeout(function () {
if (boom3Img && typeof boom3Img.destroy === 'function') boom3Img.destroy();
// Show Boom4 image at the same captured position
var boom4Img = LK.getAsset('Boom4', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom4Img);
// Remove Boom4 image after a short delay (e.g., 125ms), then show Boom5 at the same position
LK.setTimeout(function () {
if (boom4Img && typeof boom4Img.destroy === 'function') boom4Img.destroy();
// Show Boom5 image at the same captured position
var boom5Img = LK.getAsset('Boom5', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom5Img);
// Remove Boom5 image after a short delay (e.g., 125ms), then show Boom6 at the same position
LK.setTimeout(function () {
if (boom5Img && typeof boom5Img.destroy === 'function') boom5Img.destroy();
// Show Boom6 image at the same captured position
var boom6Img = LK.getAsset('Boom6', {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
// Use captured X
y: explosionY // Use captured Y
});
game.addChild(boom6Img);
// Remove Boom6 image after a short delay (e.g., 125ms)
LK.setTimeout(function () {
if (boom6Img && typeof boom6Img.destroy === 'function') boom6Img.destroy();
}, 125);
}, 125);
}, 125);
}, 125);
}, 125);
}, 125);
enemy.destroy(); // Destroy the enemy ship
enemyShips.splice(j, 1); // Remove from the tracking array
LK.setScore(LK.getScore() + 10); // Award score for destroying an enemy
money += 5; // Add 5 money per enemy destroyed
storage.money = money; // Save money to storage
if (typeof moneyTxt !== 'undefined' && moneyTxt.setText) {
moneyTxt.setText(money + " $");
}
LK.getSound('Boom').play(); // Play 'Boom' sound effect
// 25% chance to drop a health restoration
if (Math.random() < 0.25) {
healthRestore = new HealthRestore();
healthRestore.x = enemy.x;
healthRestore.y = enemy.y;
game.addChild(healthRestore);
}
// --- Card choice after 3 kills ---
enemiesKilled++;
// --- Boss Spawn Logic ---
// Spawn original boss after 10 kills
if (!bossSpawned && bossEnemyInstance === null && enemiesKilled >= 10) {
bossEnemyInstance = new BossEnemy();
bossEnemyInstance.x = 2048 / 2;
bossEnemyInstance.y = 450;
game.addChild(bossEnemyInstance);
bossSpawned = true;
LK.playMusic('Boss');
}
// --- End Boss Spawn Logic ---
if (enemiesKilled > 0 && enemiesKilled % 3 === 0 && !game._cardChoiceActive) {
// Helper to finish card choice and resume game
var finishCardChoice = function finishCardChoice() {
if (!game._cardChoiceActive) return;
game._cardChoiceActive = false;
freezeEnemies = false;
if (cardOverlay && typeof cardOverlay.destroy === 'function') cardOverlay.destroy();
// Restore input
game.down = oldGameDown;
game.move = oldGameMove;
game.up = oldGameUp;
// Resume game logic
// No need to call LK.resumeGame(); LK engine will resume game after overlay is destroyed
};
game._cardChoiceActive = true;
// Freeze all enemy movement and firing
freezeEnemies = true;
// --- Block card selection for 2 seconds ---
game._cardChoiceBlock = true;
LK.setTimeout(function () {
game._cardChoiceBlock = false;
}, 2000);
// Pause game logic is handled by LK.showGameOver() and LK.resumeGame(), no need to call LK.pause() here
// Create overlay for card choice
cardOverlay = new Container();
cardOverlay.name = "cardChoiceOverlay";
// Semi-transparent background
bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 20,
scaleY: 20,
x: 2048 / 2,
y: 2732 / 2,
tint: 0x000000,
alpha: 0.7
});
cardOverlay.addChild(bg);
// --- Define all available cards and their effects ---
var allAvailableCards = [{
key: "card1_ammo_firerate",
// Card1: More Ammo, Faster Fire Rate
asset: "Card1",
applyEffect: function applyEffect() {
if (typeof maxPlayerAmmo === 'undefined') maxPlayerAmmo = 10; // Defensive initialization
maxPlayerAmmo += 2; // Increase maximum ammo capacity
playerAmmo = Math.min(playerAmmo + 2, maxPlayerAmmo); // Add 2 to current ammo, capped by new maxPlayerAmmo
fireButtonCooldown = Math.max(1, Math.floor(fireButtonCooldown / 2)); // Halve cooldown, min 1
}
}, {
key: "card2_speed_spawn_heal",
// Card2: Speed Boost, Enemy Spawn Boost, Heal 1 HP
asset: "Csrd2",
applyEffect: function applyEffect() {
if (playerShip) {
if (typeof playerShip._originalMoveSpeed === 'undefined') {
playerShip._originalMoveSpeed = playerShip.moveSpeed;
}
playerShip.moveSpeed = playerShip._originalMoveSpeed * 2;
// Speed boost is now permanent, timeout removed.
// Heal 1 HP if not at max health
var currentMaxHealth = playerShip.maxHealth || 3;
if (playerShip.health < currentMaxHealth) {
playerShip.health = Math.min(playerShip.health + 1, currentMaxHealth);
if (typeof playerShip._fractionalHealth !== 'undefined') {
// Ensure fractional health is updated
playerShip._fractionalHealth = playerShip.health;
}
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < playerShip.health;
}
}
}
// Removed enemy spawn multiplier effect
}
}, {
key: "cardhp_maxhp_heal",
// Cardhp: Increase Max HP, Heal 1 HP
asset: "Cardhp",
applyEffect: function applyEffect() {
if (playerShip) {
if (typeof playerShip.maxHealth === 'undefined') {
playerShip.maxHealth = 3;
}
playerShip.maxHealth += 1;
playerShip.health = Math.min(playerShip.health + 1, playerShip.maxHealth); // Heal 1 HP up to new max
if (typeof playerShip._fractionalHealth !== 'undefined') {
// Ensure fractional health is updated
playerShip._fractionalHealth = playerShip.health;
}
// Add a new heart icon if needed (maxHealth might exceed initial hearts array length)
// Ensure heart icons match current maxHealth and health
while (hearts.length < playerShip.maxHealth) {
var newHeart = new Heart();
newHeart.x = hearts.length * 120 + 120;
newHeart.y = 60;
LK.gui.topLeft.addChild(newHeart);
hearts.push(newHeart);
}
for (var i = 0; i < hearts.length; i++) {
hearts[i].visible = i < playerShip.health;
}
}
}
}, {
key: "cardsttsck_doubledamage",
// Cardsttsck: Double Projectile Damage
asset: "Cardsttsck",
applyEffect: function applyEffect() {
if (typeof playerProjectileDamage === 'undefined') {
playerProjectileDamage = 1;
}
playerProjectileDamage *= 2;
}
}, {
// Adding new card Asplitmind
key: "asplitmind_split_shot",
asset: "Asplitmind",
applyEffect: function applyEffect() {
// playerSplitShotActive is globally defined and initialized to false.
// This effect makes the split shot permanently true.
playerSplitShotActive = true;
}
}];
// --- Shuffle and select 3 cards to display ---
// Fisher-Yates shuffle
for (var k = allAvailableCards.length - 1; k > 0; k--) {
var j_shuffle = Math.floor(Math.random() * (k + 1));
var tempCard = allAvailableCards[k];
allAvailableCards[k] = allAvailableCards[j_shuffle];
allAvailableCards[j_shuffle] = tempCard;
}
var cardsToDisplay = allAvailableCards.slice(0, 3); // Get the first 3 cards
// --- Position and display selected cards ---
var cardPositions = [{
x: 2048 / 2 - 450,
y: 2732 / 2
},
// Left card
{
x: 2048 / 2,
y: 2732 / 2
},
// Middle card
{
x: 2048 / 2 + 450,
y: 2732 / 2
} // Right card
];
for (var cardIdx = 0; cardIdx < cardsToDisplay.length; cardIdx++) {
var cardData = cardsToDisplay[cardIdx];
var cardContainer = new Container(); // Create a new container for each card
// The image itself is positioned globally based on cardPositions
var cardImg = LK.getAsset(cardData.asset, {
anchorX: 0.5,
anchorY: 0.5,
x: cardPositions[cardIdx].x,
y: cardPositions[cardIdx].y
});
cardContainer.addChild(cardImg);
// The container itself can be at 0,0 as its child (the image) is globally positioned.
// However, for consistency with how interactives are often handled,
// we can also set the container's position if we wanted the image local to it.
// For this setup, global positioning of the image is fine.
cardContainer.x = 0;
cardContainer.y = 0;
cardContainer.interactive = true;
// Use a closure to correctly capture cardData for each event handler
(function (currentCardData) {
cardContainer.down = function (x_down, y_down, obj_down) {
if (!game._cardChoiceActive || game._cardChoiceBlock) return;
currentCardData.applyEffect();
finishCardChoice();
};
})(cardData);
cardOverlay.addChild(cardContainer);
}
// Add overlay to game
game.addChild(cardOverlay);
// Block all input except card choice
oldGameDown = game.down;
oldGameMove = game.move;
oldGameUp = game.up;
game.down = function (x, y, obj) {
if (game._cardChoiceBlock) return;
// Defensive: Only forward to cards that exist in overlay
if (cardOverlay && cardOverlay.children) {
for (var i = 0; i < cardOverlay.children.length; i++) {
var child = cardOverlay.children[i];
if (child && child.interactive && typeof child.down === "function" && child.children && child.children.length > 0) {
var img = child.children[0];
if (img && typeof child.toLocal === "function") {
var local = child.toLocal({
x: x,
y: y
});
if (img.width && img.height && Math.abs(local.x) < img.width / 2 && Math.abs(local.y) < img.height / 2) {
child.down(x, y, obj);
return;
}
}
}
}
}
};
game.move = function () {};
game.up = function () {};
}
// }
// if (typeof scoreTxt !== 'undefined' && scoreTxt && scoreTxt.setText) {
// scoreTxt.setText(LK.getScore());
// }
return 1; // continue
// Move to the next enemy ship in the list
}
if (enemy.update) {
enemy.update(); // Call enemy ship's own update logic (movement, etc.)
}
// Then, check if the enemy ship flagged itself as off-screen (if not already destroyed)
if (enemy.isOffScreen) {
enemy.destroy(); // Destroy the enemy ship
enemyShips.splice(j, 1); // Remove from the tracking array
// No score for merely going off-screen, unless desired.
}
},
enemy,
boomImg,
healthRestore,
cardOverlay,
bg,
card1,
card1Bg,
card1Text,
card2,
card2Bg,
card2Text,
oldGameDown,
oldGameMove,
oldGameUp;
for (var j = enemyShips.length - 1; j >= 0; j--) {
if (_loop()) continue;
}
// Increase the number of enemies over time
// Spawn regular enemies only if NEITHER boss is currently active
var originalBossActive = bossEnemyInstance && !bossEnemyInstance.isDestroyed;
var giantBossActive = giantBossEnemyInstance && !giantBossEnemyInstance.isDestroyed;
if (!originalBossActive && !giantBossActive && LK.ticks % Math.max(30, 150 - Math.floor(LK.ticks / 300)) === 0) {
// Determine multiplier (default 1, or 3x if card2 effect active)
var multiplier = typeof game._enemySpawnMultiplier !== 'undefined' && game._enemySpawnMultiplier > 1 ? game._enemySpawnMultiplier : 1;
for (var spawnIdx = 0; spawnIdx < multiplier; spawnIdx++) {
// 25% chance to spawn armored enemy, else normal
var newEnemyShip;
if (Math.random() < 0.25 && typeof ArmoredEnemyShip !== 'undefined') {
newEnemyShip = new ArmoredEnemyShip();
} else {
newEnemyShip = new EnemyShip();
}
var enemyShipAssetHeight = newEnemyShip.shipSprite && newEnemyShip.shipSprite.height ? newEnemyShip.shipSprite.height : 146;
newEnemyShip.x = Math.random() * 2048;
newEnemyShip.y = -(enemyShipAssetHeight / 2) - 50;
newEnemyShip.lastX = newEnemyShip.x;
newEnemyShip.lastY = newEnemyShip.y;
game.addChild(newEnemyShip);
enemyShips.push(newEnemyShip);
}
}
// --- Boss Update and Defeat Logic ---
if (bossEnemyInstance) {
if (bossEnemyInstance.isDestroyed) {
// Duration each explosion frame is visible (ms)
// Helper function to manage the chained explosion sequence
var _showNextExplosion = function showNextExplosion(index, previousExplosionAsset) {
// Clean up the previous explosion asset from the screen
if (previousExplosionAsset && typeof previousExplosionAsset.destroy === 'function') {
previousExplosionAsset.destroy();
}
// Check if there are more explosion frames to show
if (index < explosionAssets.length) {
var assetId = explosionAssets[index];
var currentExplosion = LK.getAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
x: explosionX,
y: explosionY,
scaleX: 1.5,
scaleY: 1.5 // Make boss explosions bigger
});
game.addChild(currentExplosion);
LK.getSound('Boom').play(); // Play sound for each frame
LK.setTimeout(function () {
_showNextExplosion(index + 1, currentExplosion);
}, explosionAssetDuration);
} else {
// All explosion frames for the first boss are done
bossEnemyInstance = null; // Officially nullify the global reference after explosion
// Check if giant boss is NOT active (or not spawned / already destroyed)
if (!giantBossEnemyInstance || giantBossEnemyInstance.isDestroyed) {
LK.stopMusic(); // Stop current music first
LK.playMusic('Space'); // Resume normal gameplay music
}
// If giant boss IS active, its spawn logic would have (or will) set 'Boss' music.
}
}; // End of _showNextExplosion definition
// Boss has been defeated (main body destroyed)
LK.setScore(LK.getScore() + 250); // Big score for defeating the boss
money += 100;
storage.money = money; // Save money to storage
if (typeof moneyTxt !== 'undefined' && moneyTxt.setText) {
moneyTxt.setText(money + " $");
}
// Grand explosion for boss defeat
var explosionX = bossEnemyInstance.x + (bossEnemyInstance.mainBody ? bossEnemyInstance.mainBody.x : 0);
var explosionY = bossEnemyInstance.y + (bossEnemyInstance.mainBody ? bossEnemyInstance.mainBody.y : 0);
// Grand explosion for boss defeat is handled by a chained sequence
// Define explosion parameters (position is already defined above this block)
var explosionAssets = ['Boom', 'Boom2', 'Boom3', 'Boom4', 'Boom5', 'Boom6'];
var explosionAssetDuration = 150;
if (explosionAssets.length > 0) {
// Initial call to start the sequence with the first asset (index 0)
// There's no previousExplosionAsset for the first frame.
_showNextExplosion(0, null);
}
bossEnemyInstance.destroy(); // Remove boss from game (destroys the actual game object)
// Note: bossEnemyInstance = null; and LK.playMusic('Space'); have been moved into _showNextExplosion
// Optionally, trigger win condition or next phase
// LK.showYouWin();
} else {
bossEnemyInstance.update(); // Update active boss
}
}
};
// Music will be managed by game state - boss music during boss fights, space music otherwise
;
;
Звездолет вид сверху два д для 2d игры пиксельный. In-Game asset
Красный лазерный луч пиксельный вид сверху. In-Game asset. 2d. High contrast. No shadows
Пиксельная шестерёнка с гаечным ключом. In-Game asset. 2d. High contrast. No shadows
Карточка с изоброжение скорости атака пиксельная карточка улучшения пиксельная космическая. In-Game asset. 2d. High contrast. No shadows
Карта усиления пиксельная усиливает скорость игрока космическая 2д пиксели. In-Game asset. 2d. High contrast. No shadows
Бронированный летающий корабль звездолет пиксельный вид сверху 2д. In-Game asset. 2d. High contrast. No shadows
Start в космической пмксельном стиле. In-Game asset. 2d. High contrast. No shadows
pixel inscription battle of starships in the style of space pixel art. In-Game asset. 2d. High contrast. No shadows
Карта усиления дающие + хп пиксельная космическая. In-Game asset. 2d. High contrast. No shadows
Пиксельная карта усиления атаки космос битва. In-Game asset. 2d. High contrast. No shadows
Карточка улучшения раздватвает атаку пиксельная в стиле космоса. In-Game asset. 2d. High contrast. No shadows
Пиксельная круглая кнопка атаки. In-Game asset. 2d. High contrast. No shadows
Пиксельный корабль сверху с нарисованным огнем спереди вид сверху. In-Game asset. 2d. High contrast. No shadows
Звездолет оформление в стиле призрака пиксельный вид сверху. In-Game asset. 2d. High contrast. No shadows
Пиксельная черная дыра желто черного цвета. In-Game asset. 2d. High contrast. No shadows