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