/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // --- Bullet Class --- var Bullet = Container.expand(function () { var self = Container.call(this); // Attach bullet asset (white ellipse) var bulletAsset = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); // Bullet speed (pixels per frame) self.speed = 160; // Direction in radians (set on creation) self.direction = 0; // Damage dealt by this bullet self.damage = 1; // Defensive: track last position for possible future use self.lastX = self.x; self.lastY = self.y; // Update method called every tick self.update = function () { self.lastX = self.x; self.lastY = self.y; self.x += Math.cos(self.direction) * self.speed; self.y += Math.sin(self.direction) * self.speed; }; return self; }); // --- Enemy Class --- var Enemy = Container.expand(function () { var self = Container.call(this); // Attach enemy asset (pink ellipse), scaled up 2x var enemyAsset = self.attachAsset('enemyCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); // Enemy properties self.radius = 60 + Math.random() * 40; // for spawn distance self.speed = 1.15 + Math.random() * 0.55; // even faster approach self.maxHp = 3; // always 3 HP self.hp = self.maxHp; // Direction towards center (set on spawn) self.direction = 0; // Health bar (white background, green foreground), both rectangles, above the enemy self.hpBarBg = self.addChild(LK.getAsset('hpbar_bg', { anchorX: 0.5, anchorY: 1.0, y: -140 // higher above the enemy })); self.hpBar = self.addChild(LK.getAsset('hpbar_fg', { anchorX: 0.5, anchorY: 1.0, y: -140 // match background bar position })); // Defensive: track last position for possible future use self.lastX = self.x; self.lastY = self.y; // Update health bar width self.updateHpBar = function () { // Use asset width for scaling, so health bar always matches background self.hpBar.width = self.hpBarBg.width * (self.hp / self.maxHp); self.hpBar.x = self.hpBarBg.x - (self.hpBarBg.width - self.hpBar.width) / 2; }; // Update method called every tick self.update = function () { self.lastX = self.x; self.lastY = self.y; self.x += Math.cos(self.direction) * self.speed; self.y += Math.sin(self.direction) * self.speed; }; // Take damage self.takeDamage = function (amount) { self.hp -= amount; if (self.hp < 0) self.hp = 0; self.updateHpBar(); }; // On death self.die = function () { // Flash effect LK.effects.flashObject(self, 0xffffff, 200); // Particle explosion (intense red, gravity-free) // More and bigger particles, even more red! for (var i = 0; i < 38; i++) { var p = new Container(); var asset = p.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, // Make particles much bigger overall scaleX: 1.1 + Math.random() * 0.8, scaleY: 1.1 + Math.random() * 0.8, // Use a deeper, more saturated red color: 0xff0000, alpha: 0.82 + Math.random() * 0.18 }); p.x = self.x; p.y = self.y; var angle = Math.random() * Math.PI * 2; var speed = 22 + Math.random() * 20; var life = 26 + Math.floor(Math.random() * 18); p.update = function (a, s, l) { var vx = Math.cos(a) * s; var vy = Math.sin(a) * s; var ticks = 0; return function () { this.x += vx; this.y += vy; vx *= 0.88; vy *= 0.88; this.alpha *= 0.93; ticks++; if (ticks > l) { this.destroy(); } }; }(angle, speed, life); game.addChild(p); // Add to update loop if (!game._enemyParticles) game._enemyParticles = []; game._enemyParticles.push(p); } }; // Initialize health bar self.updateHpBar(); return self; }); // --- HealthBox Class --- var HealthBox = Container.expand(function () { var self = Container.call(this); // Attach health asset (red box), scaled up 1.5x var asset = self.attachAsset('healthBox', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); self.radius = 60 + Math.random() * 40; self.speed = 0.32 + Math.random() * 0.16; // even faster movement self.maxHp = 1; self.hp = self.maxHp; self.direction = 0; // Defensive: track last position self.lastX = self.x; self.lastY = self.y; // Update method self.update = function () { self.lastX = self.x; self.lastY = self.y; self.x += Math.cos(self.direction) * self.speed; self.y += Math.sin(self.direction) * self.speed; }; // Take damage self.takeDamage = function (amount) { self.hp -= amount; if (self.hp < 0) self.hp = 0; }; // On death self.die = function () { LK.effects.flashObject(self, 0xffffff, 200); // Particle effect (white explosion) for (var i = 0; i < 18; i++) { var p = new Container(); var asset = p.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5, scaleX: 0.9 + Math.random() * 0.7, scaleY: 0.9 + Math.random() * 0.7, color: 0xffffff, alpha: 0.8 + Math.random() * 0.15 }); p.x = self.x; p.y = self.y; var angle = Math.random() * Math.PI * 2; var speed = 12 + Math.random() * 10; var life = 16 + Math.floor(Math.random() * 10); p.update = function (a, s, l) { var vx = Math.cos(a) * s; var vy = Math.sin(a) * s; var ticks = 0; return function () { this.x += vx; this.y += vy; vx *= 0.88; vy *= 0.88; this.alpha *= 0.91; ticks++; if (ticks > l) { this.destroy(); } }; }(angle, speed, life); game.addChild(p); if (!game._enemyParticles) game._enemyParticles = []; game._enemyParticles.push(p); } }; return self; }); // --- Player Class --- var Player = Container.expand(function () { var self = Container.call(this); // Attach player asset (blue ellipse), scaled up 2x var playerAsset = self.attachAsset('character', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2 }); // Player properties self.radius = 0; // always at center self.rotation = 0; // radians self.maxHp = 5; self.hp = self.maxHp; // Health bar (red background, green foreground) - always on top, not rotating self.hpBarBg = LK.getAsset('hpbar_bg', { anchorX: 0.5, anchorY: 0.5, y: -140 }); self.hpBar = LK.getAsset('hpbar_fg', { anchorX: 0.5, anchorY: 0.5, y: -140 }); // Defensive: track last position for possible future use self.lastX = self.x; self.lastY = self.y; // Update health bar width self.updateHpBar = function () { // Clamp hp to [0, maxHp] if (self.hp < 0) self.hp = 0; if (self.hp > self.maxHp) self.hp = self.maxHp; // Use asset width for scaling, so health bar always matches background // Health bar shrinks from left to right (left edge fixed) var targetWidth = self.hpBarBg.width * (self.hp / self.maxHp); if (targetWidth < 0) targetWidth = 0; // Smoothly animate width decrease (if needed) if (typeof self.hpBar.width === "undefined") self.hpBar.width = self.hpBarBg.width; if (self.hpBar.width > targetWidth) { self.hpBar.width -= Math.max(6, (self.hpBar.width - targetWidth) * 0.18); if (self.hpBar.width < targetWidth) self.hpBar.width = targetWidth; } else { self.hpBar.width = targetWidth; } // Left edge fixed: set x so left edge stays at bg left self.hpBar.x = self.hpBarBg.x - self.hpBarBg.width / 2 + self.hpBar.width / 2; // Add a white border to the health bar background for better visibility if (!self.hpBarBg._borderAdded) { var border = LK.getAsset('hpbar_bg', { anchorX: 0.5, anchorY: 0.5, y: self.hpBarBg.y, width: self.hpBarBg.width + 8, height: self.hpBarBg.height + 8, color: 0xffffff, alpha: 0.25 }); border.x = self.hpBarBg.x; border.y = self.hpBarBg.y; if (self.hpBarBg.parent) self.hpBarBg.parent.addChild(border); self.hpBarBg._borderAdded = true; } }; // Take damage self.takeDamage = function (amount) { self.hp -= amount; if (self.hp < 0) self.hp = 0; if (self.hp > self.maxHp) self.hp = self.maxHp; self.updateHpBar(); }; // On death self.die = function () { LK.effects.flashObject(self, 0xff0000, 500); }; // Initialize health bar self.updateHpBar(); return self; }); // --- Shield Layer Class --- var ShieldLayer = Container.expand(function () { var self = Container.call(this); // Properties self.radius = 0; // distance from player center self.maxHp = 3; self.hp = self.maxHp; self.index = 0; // 0=innermost, 1=outer self.active = true; // Visual: support shield1, shield2, and shield3 assets var shieldAssetNames = ['shield1', 'shield2', 'shield3']; var assetName = shieldAssetNames[self.index] || 'shield1'; self.shieldAsset = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5, scaleX: 4.2, scaleY: 4.2, alpha: 0.25 }); // Health bar above shield self.hpBarBg = self.addChild(LK.getAsset('hpbar_bg', { anchorX: 0.5, anchorY: 0.5, y: -self.radius - 120 })); self.hpBar = self.addChild(LK.getAsset('hpbar_fg', { anchorX: 0.5, anchorY: 0.5, y: -self.radius - 120 })); // Defensive: track last position for possible future use self.lastX = self.x; self.lastY = self.y; // Update health bar width self.updateHpBar = function () { // Use asset width for scaling, so health bar always matches background self.hpBar.width = self.hpBarBg.width * (self.hp / self.maxHp); self.hpBar.x = self.hpBarBg.x - (self.hpBarBg.width - self.hpBar.width) / 2; self.hpBar.visible = self.active; self.hpBarBg.visible = self.active; }; // Take damage self.takeDamage = function (amount) { if (!self.active) return; self.hp -= amount; if (self.hp < 0) self.hp = 0; self.updateHpBar(); if (self.hp <= 0) { // Play shield explosion sound LK.getSound('shieldExplode').play(); self.active = false; self.visible = false; self.hpBar.visible = false; self.hpBarBg.visible = false; } }; // Reset shield self.reset = function () { self.hp = self.maxHp; self.active = true; self.visible = true; self.updateHpBar(); }; // Initialize self.updateHpBar(); return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // Health bar assets for bg/fg // --- Background Image --- var backgroundImg = LK.getAsset('background', { anchorX: 0.5, anchorY: 0.5, x: 2048 / 2, y: 2732 / 2, scaleX: 2048 / 4096, scaleY: 2732 / 4096 }); game.addChild(backgroundImg); // --- Global Variables --- var player; var enemies = []; var bullets = []; var spawnTimer = 0; var fireCooldown = 0; var dragActive = false; var lastTouchAngle = 0; // --- Score Display --- var scoreTxt = new Text2('0', { size: 120, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // --- Helper Line for Fire Direction (dotted, in fire direction, aligned with bullet) --- var helperLineDots = []; var helperLineAlpha = 0.7; var helperLineWidth = 18; var helperLineDotSpacing = 48; // px between dots // Calculate max possible length from player to farthest screen edge function getHelperLineLength(angle) { // Player is always at center var cx = 2048 / 2; var cy = 2732 / 2; // Find intersection with screen edge in direction of angle var dx = Math.cos(angle); var dy = Math.sin(angle); // Calculate t for each edge, pick the smallest positive t var tVals = []; if (dx !== 0) { var t1 = (0 - cx) / dx; var t2 = (2048 - cx) / dx; tVals.push(t1, t2); } if (dy !== 0) { var t3 = (0 - cy) / dy; var t4 = (2732 - cy) / dy; tVals.push(t3, t4); } // Only consider positive t (forward direction) var maxLen = 0; for (var i = 0; i < tVals.length; i++) { if (tVals[i] > 0) { var px = cx + dx * tVals[i]; var py = cy + dy * tVals[i]; // Check if point is on screen if (px >= 0 && px <= 2048 && py >= 0 && py <= 2732) { var dist = Math.sqrt((px - cx) * (px - cx) + (py - cy) * (py - cy)); if (dist > maxLen) maxLen = dist; } } } // Clamp to a minimum length if (maxLen < 200) maxLen = 200; return maxLen; } // Pre-create enough dots for the longest possible line (diagonal) var maxPossibleLength = Math.sqrt(2048 * 2048 + 2732 * 2732); var helperLineDotCount = Math.ceil(maxPossibleLength / helperLineDotSpacing); for (var i = 1; i <= helperLineDotCount; i++) { var dot = new Container(); var asset = dot.attachAsset('helperDot', { anchorX: 0.5, anchorY: 0.5, scaleX: helperLineWidth / 32, scaleY: helperLineWidth / 32, alpha: helperLineAlpha }); dot.visible = false; game.addChild(dot); helperLineDots.push(dot); } // --- Player Initialization --- player = new Player(); player.x = 2048 / 2; player.y = 2732 / 2; // Always keep player sprite visually fixed (no rotation) player.rotation = 0; game.addChild(player); // Add player health bar to game (not as child of player, so it doesn't rotate) game.addChild(player.hpBarBg); game.addChild(player.hpBar); // Position health bar above player player.hpBarBg.x = player.x; player.hpBarBg.y = player.y - 170; player.hpBar.x = player.x; player.hpBar.y = player.y - 170; // --- Shield Layers Initialization --- // Add three shield layers: shield1 (innermost), shield2 (2x), shield3 (3x) var shieldLayers = []; var shieldRadii = [110, 220, 330]; // shield1, shield2, shield3 (further out) var shieldScales = [4.2, 8.4, 12.6]; // shield2 is 2x, shield3 is 3x larger than shield1 var shieldAssetNames = ['shield1', 'shield2', 'shield3']; for (var i = 0; i < 3; i++) { var shield = new ShieldLayer(); shield.index = i; shield.radius = shieldRadii[i]; // Place shield at player center shield.x = 2048 / 2; shield.y = 2732 / 2; // Set shield visual size shield.shieldAsset.destroy(); // Remove default asset shield.shieldAsset = shield.attachAsset(shieldAssetNames[i], { anchorX: 0.5, anchorY: 0.5, scaleX: shieldScales[i], scaleY: shieldScales[i], alpha: 0.25 + 0.15 * i }); // Position health bar above shield if (i === 1) { // shield2: place its health bar above shield2 visual, and above shield1/player health bar // Calculate shield2 visual height (scaled) var shield2VisualHeight = LK.getAsset('shield2', { anchorX: 0.5, anchorY: 0.5 }).height * shieldScales[1]; // Place health bar just above shield2 visual shield.hpBarBg.y = -shield2VisualHeight / 2 - 60; shield.hpBar.y = -shield2VisualHeight / 2 - 60; } else if (i === 2) { // shield3: place its health bar above shield3 visual, and above shield2 var shield3VisualHeight = LK.getAsset('shield3', { anchorX: 0.5, anchorY: 0.5 }).height * shieldScales[2]; shield.hpBarBg.y = -shield3VisualHeight / 2 - 60; shield.hpBar.y = -shield3VisualHeight / 2 - 60; } else { // shield1: default position shield.hpBarBg.y = -shield.radius - 120; shield.hpBar.y = -shield.radius - 120; } // Add to game and to shieldLayers array game.addChild(shield); shieldLayers.push(shield); } // --- Helper: Get angle from center to (x, y) --- function getAngleToCenter(x, y) { var cx = 2048 / 2; var cy = 2732 / 2; return Math.atan2(y - cy, x - cx); } // --- Helper: Fire Bullet --- function fireBullet(angle) { if (fireCooldown > 0) return; var bullet = new Bullet(); bullet.x = player.x; bullet.y = player.y; bullet.direction = angle; // Defensive: initialize lastX/lastY bullet.lastX = bullet.x; bullet.lastY = bullet.y; bullets.push(bullet); game.addChild(bullet); fireCooldown = 4; // frames (was 10, now fires much more frequently) // Play shoot sound LK.getSound('shoot').play(); } // --- Handle Touch/Drag to Set Fire Angle (Player sprite does NOT rotate) --- game.down = function (x, y, obj) { dragActive = true; // Store angle from player center to touch point var angle = Math.atan2(y - player.y, x - player.x); lastTouchAngle = angle; }; game.move = function (x, y, obj) { if (dragActive) { // Update angle from player center to current touch point var angle = Math.atan2(y - player.y, x - player.x); lastTouchAngle = angle; } }; game.up = function (x, y, obj) { dragActive = false; // Fire bullet in the direction of last drag/touch (from center to release point) fireBullet(lastTouchAngle); }; // --- Enemy Spawning --- function spawnEnemy() { var angle = Math.random() * Math.PI * 2; var distance = 1100 + Math.random() * 400; var ex = 2048 / 2 + Math.cos(angle) * distance; var ey = 2732 / 2 + Math.sin(angle) * distance; // 1 in 5 chance to spawn a health box instead of enemy (increased frequency) if (Math.random() < 0.2) { var healthBox = new HealthBox(); healthBox.x = ex; healthBox.y = ey; healthBox.direction = Math.atan2(2732 / 2 - ey, 2048 / 2 - ex); healthBox.lastX = healthBox.x; healthBox.lastY = healthBox.y; enemies.push(healthBox); game.addChild(healthBox); } else { var enemy = new Enemy(); enemy.x = ex; enemy.y = ey; // Set direction towards player center enemy.direction = Math.atan2(2732 / 2 - ey, 2048 / 2 - ex); // Scale enemy speed with score: base speed + (score * 0.08), capped at 7x base var score = LK.getScore(); var speedScale = 1 + Math.min(score * 0.08, 6); // up to 7x base speed enemy.speed *= speedScale; // Defensive: initialize lastX/lastY enemy.lastX = enemy.x; enemy.lastY = enemy.y; enemies.push(enemy); game.addChild(enemy); } } // --- Game Update Loop --- game.update = function () { // --- Fire cooldown --- if (fireCooldown > 0) fireCooldown--; // --- Update Helper Line (dotted, in fire direction, aligned with bullet) --- var cx = player.x; var cy = player.y; var lineLen = getHelperLineLength(lastTouchAngle); var dotCount = Math.floor(lineLen / helperLineDotSpacing); for (var i = 0; i < helperLineDots.length; i++) { var dot = helperLineDots[i]; if (i < dotCount) { var dist = (i + 1) * helperLineDotSpacing; dot.x = cx + Math.cos(lastTouchAngle) * dist; dot.y = cy + Math.sin(lastTouchAngle) * dist; dot.rotation = lastTouchAngle; dot.visible = true; } else { dot.visible = false; } } // Keep player health bar above player (not rotating) player.hpBarBg.x = player.x; player.hpBarBg.y = player.y - 170; player.hpBar.x = player.x; player.hpBar.y = player.y - 170; player.updateHpBar(); // --- Enemy spawn timer --- spawnTimer--; if (spawnTimer <= 0) { spawnEnemy(); // Make enemies and health boxes spawn less frequently (every 2.5-3.5s) spawnTimer = 150 + Math.floor(Math.random() * 60); // spawn every 2.5-3.5s } // --- Update Bullets --- for (var i = bullets.length - 1; i >= 0; i--) { var bullet = bullets[i]; // Defensive: initialize lastX/lastY if not set if (typeof bullet.lastX === "undefined") bullet.lastX = bullet.x; if (typeof bullet.lastY === "undefined") bullet.lastY = bullet.y; bullet.update(); // Remove if out of bounds or destroyed if (bullet.x < -200 || bullet.x > 2048 + 200 || bullet.y < -200 || bullet.y > 2732 + 200 || bullet.destroyed) { if (!bullet.destroyed) bullet.destroy(); bullets.splice(i, 1); continue; } // Check collision with enemies var hit = false; for (var j = enemies.length - 1; j >= 0; j--) { var enemy = enemies[j]; // Defensive: initialize lastX/lastY if not set if (typeof enemy.lastX === "undefined") enemy.lastX = enemy.x; if (typeof enemy.lastY === "undefined") enemy.lastY = enemy.y; if (enemy.destroyed) continue; if (bullet.intersects(enemy)) { // HealthBox dies in 1 hit, Enemy dies at 0 hp var isHealthBox = typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1; if (isHealthBox) { // HealthBox: always die in one hit, regardless of hp enemy.hp = 0; } else { enemy.takeDamage(bullet.damage); } if (enemy.hp <= 0) { var isHealthBox = typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1; if (isHealthBox) { // HealthBox: fill player health to max player.hp = player.maxHp; player.updateHpBar(); // Find the outermost active shield (highest index, active) var outermostActive = -1; for (var si = shieldLayers.length - 1; si >= 0; si--) { if (shieldLayers[si].active) { outermostActive = si; break; } } // If all shields are inactive, add the next shield (lowest inactive) if (outermostActive === -1) { for (var si = 0; si < shieldLayers.length; si++) { if (!shieldLayers[si].active) { shieldLayers[si].reset(); LK.effects.flashObject(shieldLayers[si], 0x44ff44, 400); LK.getSound('shieldHit').play(); break; } } } else { // If outermost shield is not full, restore its HP var shield = shieldLayers[outermostActive]; if (shield.hp < shield.maxHp) { shield.hp = shield.maxHp; shield.updateHpBar(); LK.effects.flashObject(shield, 0x44ff44, 400); LK.getSound('shieldHit').play(); } else { // If outermost shield is full, try to add the next shield (if exists and inactive) if (outermostActive + 1 < shieldLayers.length && !shieldLayers[outermostActive + 1].active) { shieldLayers[outermostActive + 1].reset(); LK.effects.flashObject(shieldLayers[outermostActive + 1], 0x44ff44, 400); LK.getSound('shieldHit').play(); } else { // All shields are active and full, just play effect on outermost LK.effects.flashObject(shield, 0x44ff44, 400); LK.getSound('shieldHit').play(); } } } } else { LK.getSound('enemyDie').play(); } enemy.die(); if (!enemy.destroyed) enemy.destroy(); enemies.splice(j, 1); if (!(typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1)) { LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); } } if (!bullet.destroyed) bullet.destroy(); bullets.splice(i, 1); hit = true; break; } } if (hit) continue; } // Remove destroyed bullets (defensive, in case any remain) for (var i = bullets.length - 1; i >= 0; i--) { if (bullets[i].destroyed) bullets.splice(i, 1); } // --- Update Enemies --- for (var i = enemies.length - 1; i >= 0; i--) { var enemy = enemies[i]; // Defensive: initialize lastX/lastY if not set if (typeof enemy.lastX === "undefined") enemy.lastX = enemy.x; if (typeof enemy.lastY === "undefined") enemy.lastY = enemy.y; if (enemy.destroyed) { enemies.splice(i, 1); continue; } enemy.update(); // Check collision with shields (outermost to innermost) var blocked = false; for (var s = shieldLayers.length - 1; s >= 0; s--) { var shield = shieldLayers[s]; if (shield.active) { // Defensive: initialize lastX/lastY if not set if (typeof shield.lastX === "undefined") shield.lastX = shield.x; if (typeof shield.lastY === "undefined") shield.lastY = shield.y; // Check distance from player center to enemy center var dx = enemy.x - player.x; var dy = enemy.y - player.y; var dist = Math.sqrt(dx * dx + dy * dy); // Use actual shield visual size for collision (shieldAsset is ellipse, scaleX/scaleY applied) // Defensive: recalculate radius using current scale for each shield asset var shieldAssetNames = ['shield1', 'shield2', 'shield3']; var assetName = shieldAssetNames[shield.index] || 'shield1'; var baseRadius = LK.getAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }).width / 2; var shieldVisualRadius = baseRadius * shield.shieldAsset.scaleX; var enemyVisualRadius = (enemy.width > enemy.height ? enemy.width : enemy.height) / 2; if (dist < shieldVisualRadius + enemyVisualRadius) { // Collided with shield var isHealthBox = typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1; if (isHealthBox) { // HealthBox: just disappears, does not affect shield, no revive or restore enemy.die(); if (!enemy.destroyed) enemy.destroy(); enemies.splice(i, 1); blocked = true; break; } else { // Regular enemy: damage shield shield.takeDamage(1); LK.getSound('shieldHit').play(); enemy.die(); if (!enemy.destroyed) enemy.destroy(); enemies.splice(i, 1); // Increase score when enemy dies by hitting shield LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); // Flash shield LK.effects.flashObject(shield, 0x44aaff, 200); blocked = true; break; } } } } if (blocked) continue; // If not blocked, check collision with player if (enemy.intersects(player)) { var isHealthBox = typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1; if (isHealthBox) { // HealthBox: just disappears, does not affect player health enemy.die(); if (!enemy.destroyed) enemy.destroy(); enemies.splice(i, 1); // Do not change player health or flash screen } else { player.takeDamage(1); if (player.hp < 0) player.hp = 0; if (player.hp > player.maxHp) player.hp = player.maxHp; LK.getSound('enemyDie').play(); enemy.die(); if (!enemy.destroyed) enemy.destroy(); enemies.splice(i, 1); // Flash screen LK.effects.flashScreen(0xff0000, 400); // Game over if player dead if (player.hp <= 0) { player.die(); LK.showGameOver(); return; } } } } // Remove destroyed enemies (defensive, in case any remain) for (var i = enemies.length - 1; i >= 0; i--) { if (enemies[i].destroyed) enemies.splice(i, 1); } ; // --- Update and cleanup enemy death particles --- if (game._enemyParticles) { for (var i = game._enemyParticles.length - 1; i >= 0; i--) { var p = game._enemyParticles[i]; // Defensive: initialize lastX/lastY if not set if (typeof p.lastX === "undefined") p.lastX = p.x; if (typeof p.lastY === "undefined") p.lastY = p.y; if (p.destroyed) { game._enemyParticles.splice(i, 1); continue; } if (typeof p.update === "function") p.update(); } // Defensive: remove any remaining destroyed particles for (var i = game._enemyParticles.length - 1; i >= 0; i--) { if (game._enemyParticles[i].destroyed) game._enemyParticles.splice(i, 1); } } }; // --- Score Initialization --- LK.setScore(0); scoreTxt.setText(LK.getScore());
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// --- Bullet Class ---
var Bullet = Container.expand(function () {
var self = Container.call(this);
// Attach bullet asset (white ellipse)
var bulletAsset = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
// Bullet speed (pixels per frame)
self.speed = 160;
// Direction in radians (set on creation)
self.direction = 0;
// Damage dealt by this bullet
self.damage = 1;
// Defensive: track last position for possible future use
self.lastX = self.x;
self.lastY = self.y;
// Update method called every tick
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
self.x += Math.cos(self.direction) * self.speed;
self.y += Math.sin(self.direction) * self.speed;
};
return self;
});
// --- Enemy Class ---
var Enemy = Container.expand(function () {
var self = Container.call(this);
// Attach enemy asset (pink ellipse), scaled up 2x
var enemyAsset = self.attachAsset('enemyCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
// Enemy properties
self.radius = 60 + Math.random() * 40; // for spawn distance
self.speed = 1.15 + Math.random() * 0.55; // even faster approach
self.maxHp = 3; // always 3 HP
self.hp = self.maxHp;
// Direction towards center (set on spawn)
self.direction = 0;
// Health bar (white background, green foreground), both rectangles, above the enemy
self.hpBarBg = self.addChild(LK.getAsset('hpbar_bg', {
anchorX: 0.5,
anchorY: 1.0,
y: -140 // higher above the enemy
}));
self.hpBar = self.addChild(LK.getAsset('hpbar_fg', {
anchorX: 0.5,
anchorY: 1.0,
y: -140 // match background bar position
}));
// Defensive: track last position for possible future use
self.lastX = self.x;
self.lastY = self.y;
// Update health bar width
self.updateHpBar = function () {
// Use asset width for scaling, so health bar always matches background
self.hpBar.width = self.hpBarBg.width * (self.hp / self.maxHp);
self.hpBar.x = self.hpBarBg.x - (self.hpBarBg.width - self.hpBar.width) / 2;
};
// Update method called every tick
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
self.x += Math.cos(self.direction) * self.speed;
self.y += Math.sin(self.direction) * self.speed;
};
// Take damage
self.takeDamage = function (amount) {
self.hp -= amount;
if (self.hp < 0) self.hp = 0;
self.updateHpBar();
};
// On death
self.die = function () {
// Flash effect
LK.effects.flashObject(self, 0xffffff, 200);
// Particle explosion (intense red, gravity-free)
// More and bigger particles, even more red!
for (var i = 0; i < 38; i++) {
var p = new Container();
var asset = p.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
// Make particles much bigger overall
scaleX: 1.1 + Math.random() * 0.8,
scaleY: 1.1 + Math.random() * 0.8,
// Use a deeper, more saturated red
color: 0xff0000,
alpha: 0.82 + Math.random() * 0.18
});
p.x = self.x;
p.y = self.y;
var angle = Math.random() * Math.PI * 2;
var speed = 22 + Math.random() * 20;
var life = 26 + Math.floor(Math.random() * 18);
p.update = function (a, s, l) {
var vx = Math.cos(a) * s;
var vy = Math.sin(a) * s;
var ticks = 0;
return function () {
this.x += vx;
this.y += vy;
vx *= 0.88;
vy *= 0.88;
this.alpha *= 0.93;
ticks++;
if (ticks > l) {
this.destroy();
}
};
}(angle, speed, life);
game.addChild(p);
// Add to update loop
if (!game._enemyParticles) game._enemyParticles = [];
game._enemyParticles.push(p);
}
};
// Initialize health bar
self.updateHpBar();
return self;
});
// --- HealthBox Class ---
var HealthBox = Container.expand(function () {
var self = Container.call(this);
// Attach health asset (red box), scaled up 1.5x
var asset = self.attachAsset('healthBox', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.radius = 60 + Math.random() * 40;
self.speed = 0.32 + Math.random() * 0.16; // even faster movement
self.maxHp = 1;
self.hp = self.maxHp;
self.direction = 0;
// Defensive: track last position
self.lastX = self.x;
self.lastY = self.y;
// Update method
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
self.x += Math.cos(self.direction) * self.speed;
self.y += Math.sin(self.direction) * self.speed;
};
// Take damage
self.takeDamage = function (amount) {
self.hp -= amount;
if (self.hp < 0) self.hp = 0;
};
// On death
self.die = function () {
LK.effects.flashObject(self, 0xffffff, 200);
// Particle effect (white explosion)
for (var i = 0; i < 18; i++) {
var p = new Container();
var asset = p.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.9 + Math.random() * 0.7,
scaleY: 0.9 + Math.random() * 0.7,
color: 0xffffff,
alpha: 0.8 + Math.random() * 0.15
});
p.x = self.x;
p.y = self.y;
var angle = Math.random() * Math.PI * 2;
var speed = 12 + Math.random() * 10;
var life = 16 + Math.floor(Math.random() * 10);
p.update = function (a, s, l) {
var vx = Math.cos(a) * s;
var vy = Math.sin(a) * s;
var ticks = 0;
return function () {
this.x += vx;
this.y += vy;
vx *= 0.88;
vy *= 0.88;
this.alpha *= 0.91;
ticks++;
if (ticks > l) {
this.destroy();
}
};
}(angle, speed, life);
game.addChild(p);
if (!game._enemyParticles) game._enemyParticles = [];
game._enemyParticles.push(p);
}
};
return self;
});
// --- Player Class ---
var Player = Container.expand(function () {
var self = Container.call(this);
// Attach player asset (blue ellipse), scaled up 2x
var playerAsset = self.attachAsset('character', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
// Player properties
self.radius = 0; // always at center
self.rotation = 0; // radians
self.maxHp = 5;
self.hp = self.maxHp;
// Health bar (red background, green foreground) - always on top, not rotating
self.hpBarBg = LK.getAsset('hpbar_bg', {
anchorX: 0.5,
anchorY: 0.5,
y: -140
});
self.hpBar = LK.getAsset('hpbar_fg', {
anchorX: 0.5,
anchorY: 0.5,
y: -140
});
// Defensive: track last position for possible future use
self.lastX = self.x;
self.lastY = self.y;
// Update health bar width
self.updateHpBar = function () {
// Clamp hp to [0, maxHp]
if (self.hp < 0) self.hp = 0;
if (self.hp > self.maxHp) self.hp = self.maxHp;
// Use asset width for scaling, so health bar always matches background
// Health bar shrinks from left to right (left edge fixed)
var targetWidth = self.hpBarBg.width * (self.hp / self.maxHp);
if (targetWidth < 0) targetWidth = 0;
// Smoothly animate width decrease (if needed)
if (typeof self.hpBar.width === "undefined") self.hpBar.width = self.hpBarBg.width;
if (self.hpBar.width > targetWidth) {
self.hpBar.width -= Math.max(6, (self.hpBar.width - targetWidth) * 0.18);
if (self.hpBar.width < targetWidth) self.hpBar.width = targetWidth;
} else {
self.hpBar.width = targetWidth;
}
// Left edge fixed: set x so left edge stays at bg left
self.hpBar.x = self.hpBarBg.x - self.hpBarBg.width / 2 + self.hpBar.width / 2;
// Add a white border to the health bar background for better visibility
if (!self.hpBarBg._borderAdded) {
var border = LK.getAsset('hpbar_bg', {
anchorX: 0.5,
anchorY: 0.5,
y: self.hpBarBg.y,
width: self.hpBarBg.width + 8,
height: self.hpBarBg.height + 8,
color: 0xffffff,
alpha: 0.25
});
border.x = self.hpBarBg.x;
border.y = self.hpBarBg.y;
if (self.hpBarBg.parent) self.hpBarBg.parent.addChild(border);
self.hpBarBg._borderAdded = true;
}
};
// Take damage
self.takeDamage = function (amount) {
self.hp -= amount;
if (self.hp < 0) self.hp = 0;
if (self.hp > self.maxHp) self.hp = self.maxHp;
self.updateHpBar();
};
// On death
self.die = function () {
LK.effects.flashObject(self, 0xff0000, 500);
};
// Initialize health bar
self.updateHpBar();
return self;
});
// --- Shield Layer Class ---
var ShieldLayer = Container.expand(function () {
var self = Container.call(this);
// Properties
self.radius = 0; // distance from player center
self.maxHp = 3;
self.hp = self.maxHp;
self.index = 0; // 0=innermost, 1=outer
self.active = true;
// Visual: support shield1, shield2, and shield3 assets
var shieldAssetNames = ['shield1', 'shield2', 'shield3'];
var assetName = shieldAssetNames[self.index] || 'shield1';
self.shieldAsset = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 4.2,
scaleY: 4.2,
alpha: 0.25
});
// Health bar above shield
self.hpBarBg = self.addChild(LK.getAsset('hpbar_bg', {
anchorX: 0.5,
anchorY: 0.5,
y: -self.radius - 120
}));
self.hpBar = self.addChild(LK.getAsset('hpbar_fg', {
anchorX: 0.5,
anchorY: 0.5,
y: -self.radius - 120
}));
// Defensive: track last position for possible future use
self.lastX = self.x;
self.lastY = self.y;
// Update health bar width
self.updateHpBar = function () {
// Use asset width for scaling, so health bar always matches background
self.hpBar.width = self.hpBarBg.width * (self.hp / self.maxHp);
self.hpBar.x = self.hpBarBg.x - (self.hpBarBg.width - self.hpBar.width) / 2;
self.hpBar.visible = self.active;
self.hpBarBg.visible = self.active;
};
// Take damage
self.takeDamage = function (amount) {
if (!self.active) return;
self.hp -= amount;
if (self.hp < 0) self.hp = 0;
self.updateHpBar();
if (self.hp <= 0) {
// Play shield explosion sound
LK.getSound('shieldExplode').play();
self.active = false;
self.visible = false;
self.hpBar.visible = false;
self.hpBarBg.visible = false;
}
};
// Reset shield
self.reset = function () {
self.hp = self.maxHp;
self.active = true;
self.visible = true;
self.updateHpBar();
};
// Initialize
self.updateHpBar();
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Health bar assets for bg/fg
// --- Background Image ---
var backgroundImg = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 2048 / 4096,
scaleY: 2732 / 4096
});
game.addChild(backgroundImg);
// --- Global Variables ---
var player;
var enemies = [];
var bullets = [];
var spawnTimer = 0;
var fireCooldown = 0;
var dragActive = false;
var lastTouchAngle = 0;
// --- Score Display ---
var scoreTxt = new Text2('0', {
size: 120,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- Helper Line for Fire Direction (dotted, in fire direction, aligned with bullet) ---
var helperLineDots = [];
var helperLineAlpha = 0.7;
var helperLineWidth = 18;
var helperLineDotSpacing = 48; // px between dots
// Calculate max possible length from player to farthest screen edge
function getHelperLineLength(angle) {
// Player is always at center
var cx = 2048 / 2;
var cy = 2732 / 2;
// Find intersection with screen edge in direction of angle
var dx = Math.cos(angle);
var dy = Math.sin(angle);
// Calculate t for each edge, pick the smallest positive t
var tVals = [];
if (dx !== 0) {
var t1 = (0 - cx) / dx;
var t2 = (2048 - cx) / dx;
tVals.push(t1, t2);
}
if (dy !== 0) {
var t3 = (0 - cy) / dy;
var t4 = (2732 - cy) / dy;
tVals.push(t3, t4);
}
// Only consider positive t (forward direction)
var maxLen = 0;
for (var i = 0; i < tVals.length; i++) {
if (tVals[i] > 0) {
var px = cx + dx * tVals[i];
var py = cy + dy * tVals[i];
// Check if point is on screen
if (px >= 0 && px <= 2048 && py >= 0 && py <= 2732) {
var dist = Math.sqrt((px - cx) * (px - cx) + (py - cy) * (py - cy));
if (dist > maxLen) maxLen = dist;
}
}
}
// Clamp to a minimum length
if (maxLen < 200) maxLen = 200;
return maxLen;
}
// Pre-create enough dots for the longest possible line (diagonal)
var maxPossibleLength = Math.sqrt(2048 * 2048 + 2732 * 2732);
var helperLineDotCount = Math.ceil(maxPossibleLength / helperLineDotSpacing);
for (var i = 1; i <= helperLineDotCount; i++) {
var dot = new Container();
var asset = dot.attachAsset('helperDot', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: helperLineWidth / 32,
scaleY: helperLineWidth / 32,
alpha: helperLineAlpha
});
dot.visible = false;
game.addChild(dot);
helperLineDots.push(dot);
}
// --- Player Initialization ---
player = new Player();
player.x = 2048 / 2;
player.y = 2732 / 2;
// Always keep player sprite visually fixed (no rotation)
player.rotation = 0;
game.addChild(player);
// Add player health bar to game (not as child of player, so it doesn't rotate)
game.addChild(player.hpBarBg);
game.addChild(player.hpBar);
// Position health bar above player
player.hpBarBg.x = player.x;
player.hpBarBg.y = player.y - 170;
player.hpBar.x = player.x;
player.hpBar.y = player.y - 170;
// --- Shield Layers Initialization ---
// Add three shield layers: shield1 (innermost), shield2 (2x), shield3 (3x)
var shieldLayers = [];
var shieldRadii = [110, 220, 330]; // shield1, shield2, shield3 (further out)
var shieldScales = [4.2, 8.4, 12.6]; // shield2 is 2x, shield3 is 3x larger than shield1
var shieldAssetNames = ['shield1', 'shield2', 'shield3'];
for (var i = 0; i < 3; i++) {
var shield = new ShieldLayer();
shield.index = i;
shield.radius = shieldRadii[i];
// Place shield at player center
shield.x = 2048 / 2;
shield.y = 2732 / 2;
// Set shield visual size
shield.shieldAsset.destroy(); // Remove default asset
shield.shieldAsset = shield.attachAsset(shieldAssetNames[i], {
anchorX: 0.5,
anchorY: 0.5,
scaleX: shieldScales[i],
scaleY: shieldScales[i],
alpha: 0.25 + 0.15 * i
});
// Position health bar above shield
if (i === 1) {
// shield2: place its health bar above shield2 visual, and above shield1/player health bar
// Calculate shield2 visual height (scaled)
var shield2VisualHeight = LK.getAsset('shield2', {
anchorX: 0.5,
anchorY: 0.5
}).height * shieldScales[1];
// Place health bar just above shield2 visual
shield.hpBarBg.y = -shield2VisualHeight / 2 - 60;
shield.hpBar.y = -shield2VisualHeight / 2 - 60;
} else if (i === 2) {
// shield3: place its health bar above shield3 visual, and above shield2
var shield3VisualHeight = LK.getAsset('shield3', {
anchorX: 0.5,
anchorY: 0.5
}).height * shieldScales[2];
shield.hpBarBg.y = -shield3VisualHeight / 2 - 60;
shield.hpBar.y = -shield3VisualHeight / 2 - 60;
} else {
// shield1: default position
shield.hpBarBg.y = -shield.radius - 120;
shield.hpBar.y = -shield.radius - 120;
}
// Add to game and to shieldLayers array
game.addChild(shield);
shieldLayers.push(shield);
}
// --- Helper: Get angle from center to (x, y) ---
function getAngleToCenter(x, y) {
var cx = 2048 / 2;
var cy = 2732 / 2;
return Math.atan2(y - cy, x - cx);
}
// --- Helper: Fire Bullet ---
function fireBullet(angle) {
if (fireCooldown > 0) return;
var bullet = new Bullet();
bullet.x = player.x;
bullet.y = player.y;
bullet.direction = angle;
// Defensive: initialize lastX/lastY
bullet.lastX = bullet.x;
bullet.lastY = bullet.y;
bullets.push(bullet);
game.addChild(bullet);
fireCooldown = 4; // frames (was 10, now fires much more frequently)
// Play shoot sound
LK.getSound('shoot').play();
}
// --- Handle Touch/Drag to Set Fire Angle (Player sprite does NOT rotate) ---
game.down = function (x, y, obj) {
dragActive = true;
// Store angle from player center to touch point
var angle = Math.atan2(y - player.y, x - player.x);
lastTouchAngle = angle;
};
game.move = function (x, y, obj) {
if (dragActive) {
// Update angle from player center to current touch point
var angle = Math.atan2(y - player.y, x - player.x);
lastTouchAngle = angle;
}
};
game.up = function (x, y, obj) {
dragActive = false;
// Fire bullet in the direction of last drag/touch (from center to release point)
fireBullet(lastTouchAngle);
};
// --- Enemy Spawning ---
function spawnEnemy() {
var angle = Math.random() * Math.PI * 2;
var distance = 1100 + Math.random() * 400;
var ex = 2048 / 2 + Math.cos(angle) * distance;
var ey = 2732 / 2 + Math.sin(angle) * distance;
// 1 in 5 chance to spawn a health box instead of enemy (increased frequency)
if (Math.random() < 0.2) {
var healthBox = new HealthBox();
healthBox.x = ex;
healthBox.y = ey;
healthBox.direction = Math.atan2(2732 / 2 - ey, 2048 / 2 - ex);
healthBox.lastX = healthBox.x;
healthBox.lastY = healthBox.y;
enemies.push(healthBox);
game.addChild(healthBox);
} else {
var enemy = new Enemy();
enemy.x = ex;
enemy.y = ey;
// Set direction towards player center
enemy.direction = Math.atan2(2732 / 2 - ey, 2048 / 2 - ex);
// Scale enemy speed with score: base speed + (score * 0.08), capped at 7x base
var score = LK.getScore();
var speedScale = 1 + Math.min(score * 0.08, 6); // up to 7x base speed
enemy.speed *= speedScale;
// Defensive: initialize lastX/lastY
enemy.lastX = enemy.x;
enemy.lastY = enemy.y;
enemies.push(enemy);
game.addChild(enemy);
}
}
// --- Game Update Loop ---
game.update = function () {
// --- Fire cooldown ---
if (fireCooldown > 0) fireCooldown--;
// --- Update Helper Line (dotted, in fire direction, aligned with bullet) ---
var cx = player.x;
var cy = player.y;
var lineLen = getHelperLineLength(lastTouchAngle);
var dotCount = Math.floor(lineLen / helperLineDotSpacing);
for (var i = 0; i < helperLineDots.length; i++) {
var dot = helperLineDots[i];
if (i < dotCount) {
var dist = (i + 1) * helperLineDotSpacing;
dot.x = cx + Math.cos(lastTouchAngle) * dist;
dot.y = cy + Math.sin(lastTouchAngle) * dist;
dot.rotation = lastTouchAngle;
dot.visible = true;
} else {
dot.visible = false;
}
}
// Keep player health bar above player (not rotating)
player.hpBarBg.x = player.x;
player.hpBarBg.y = player.y - 170;
player.hpBar.x = player.x;
player.hpBar.y = player.y - 170;
player.updateHpBar();
// --- Enemy spawn timer ---
spawnTimer--;
if (spawnTimer <= 0) {
spawnEnemy();
// Make enemies and health boxes spawn less frequently (every 2.5-3.5s)
spawnTimer = 150 + Math.floor(Math.random() * 60); // spawn every 2.5-3.5s
}
// --- Update Bullets ---
for (var i = bullets.length - 1; i >= 0; i--) {
var bullet = bullets[i];
// Defensive: initialize lastX/lastY if not set
if (typeof bullet.lastX === "undefined") bullet.lastX = bullet.x;
if (typeof bullet.lastY === "undefined") bullet.lastY = bullet.y;
bullet.update();
// Remove if out of bounds or destroyed
if (bullet.x < -200 || bullet.x > 2048 + 200 || bullet.y < -200 || bullet.y > 2732 + 200 || bullet.destroyed) {
if (!bullet.destroyed) bullet.destroy();
bullets.splice(i, 1);
continue;
}
// Check collision with enemies
var hit = false;
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
// Defensive: initialize lastX/lastY if not set
if (typeof enemy.lastX === "undefined") enemy.lastX = enemy.x;
if (typeof enemy.lastY === "undefined") enemy.lastY = enemy.y;
if (enemy.destroyed) continue;
if (bullet.intersects(enemy)) {
// HealthBox dies in 1 hit, Enemy dies at 0 hp
var isHealthBox = typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1;
if (isHealthBox) {
// HealthBox: always die in one hit, regardless of hp
enemy.hp = 0;
} else {
enemy.takeDamage(bullet.damage);
}
if (enemy.hp <= 0) {
var isHealthBox = typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1;
if (isHealthBox) {
// HealthBox: fill player health to max
player.hp = player.maxHp;
player.updateHpBar();
// Find the outermost active shield (highest index, active)
var outermostActive = -1;
for (var si = shieldLayers.length - 1; si >= 0; si--) {
if (shieldLayers[si].active) {
outermostActive = si;
break;
}
}
// If all shields are inactive, add the next shield (lowest inactive)
if (outermostActive === -1) {
for (var si = 0; si < shieldLayers.length; si++) {
if (!shieldLayers[si].active) {
shieldLayers[si].reset();
LK.effects.flashObject(shieldLayers[si], 0x44ff44, 400);
LK.getSound('shieldHit').play();
break;
}
}
} else {
// If outermost shield is not full, restore its HP
var shield = shieldLayers[outermostActive];
if (shield.hp < shield.maxHp) {
shield.hp = shield.maxHp;
shield.updateHpBar();
LK.effects.flashObject(shield, 0x44ff44, 400);
LK.getSound('shieldHit').play();
} else {
// If outermost shield is full, try to add the next shield (if exists and inactive)
if (outermostActive + 1 < shieldLayers.length && !shieldLayers[outermostActive + 1].active) {
shieldLayers[outermostActive + 1].reset();
LK.effects.flashObject(shieldLayers[outermostActive + 1], 0x44ff44, 400);
LK.getSound('shieldHit').play();
} else {
// All shields are active and full, just play effect on outermost
LK.effects.flashObject(shield, 0x44ff44, 400);
LK.getSound('shieldHit').play();
}
}
}
} else {
LK.getSound('enemyDie').play();
}
enemy.die();
if (!enemy.destroyed) enemy.destroy();
enemies.splice(j, 1);
if (!(typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1)) {
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
}
}
if (!bullet.destroyed) bullet.destroy();
bullets.splice(i, 1);
hit = true;
break;
}
}
if (hit) continue;
}
// Remove destroyed bullets (defensive, in case any remain)
for (var i = bullets.length - 1; i >= 0; i--) {
if (bullets[i].destroyed) bullets.splice(i, 1);
}
// --- Update Enemies ---
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
// Defensive: initialize lastX/lastY if not set
if (typeof enemy.lastX === "undefined") enemy.lastX = enemy.x;
if (typeof enemy.lastY === "undefined") enemy.lastY = enemy.y;
if (enemy.destroyed) {
enemies.splice(i, 1);
continue;
}
enemy.update();
// Check collision with shields (outermost to innermost)
var blocked = false;
for (var s = shieldLayers.length - 1; s >= 0; s--) {
var shield = shieldLayers[s];
if (shield.active) {
// Defensive: initialize lastX/lastY if not set
if (typeof shield.lastX === "undefined") shield.lastX = shield.x;
if (typeof shield.lastY === "undefined") shield.lastY = shield.y;
// Check distance from player center to enemy center
var dx = enemy.x - player.x;
var dy = enemy.y - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// Use actual shield visual size for collision (shieldAsset is ellipse, scaleX/scaleY applied)
// Defensive: recalculate radius using current scale for each shield asset
var shieldAssetNames = ['shield1', 'shield2', 'shield3'];
var assetName = shieldAssetNames[shield.index] || 'shield1';
var baseRadius = LK.getAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
}).width / 2;
var shieldVisualRadius = baseRadius * shield.shieldAsset.scaleX;
var enemyVisualRadius = (enemy.width > enemy.height ? enemy.width : enemy.height) / 2;
if (dist < shieldVisualRadius + enemyVisualRadius) {
// Collided with shield
var isHealthBox = typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1;
if (isHealthBox) {
// HealthBox: just disappears, does not affect shield, no revive or restore
enemy.die();
if (!enemy.destroyed) enemy.destroy();
enemies.splice(i, 1);
blocked = true;
break;
} else {
// Regular enemy: damage shield
shield.takeDamage(1);
LK.getSound('shieldHit').play();
enemy.die();
if (!enemy.destroyed) enemy.destroy();
enemies.splice(i, 1);
// Increase score when enemy dies by hitting shield
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
// Flash shield
LK.effects.flashObject(shield, 0x44aaff, 200);
blocked = true;
break;
}
}
}
}
if (blocked) continue;
// If not blocked, check collision with player
if (enemy.intersects(player)) {
var isHealthBox = typeof enemy.maxHp !== "undefined" && enemy.maxHp === 1;
if (isHealthBox) {
// HealthBox: just disappears, does not affect player health
enemy.die();
if (!enemy.destroyed) enemy.destroy();
enemies.splice(i, 1);
// Do not change player health or flash screen
} else {
player.takeDamage(1);
if (player.hp < 0) player.hp = 0;
if (player.hp > player.maxHp) player.hp = player.maxHp;
LK.getSound('enemyDie').play();
enemy.die();
if (!enemy.destroyed) enemy.destroy();
enemies.splice(i, 1);
// Flash screen
LK.effects.flashScreen(0xff0000, 400);
// Game over if player dead
if (player.hp <= 0) {
player.die();
LK.showGameOver();
return;
}
}
}
}
// Remove destroyed enemies (defensive, in case any remain)
for (var i = enemies.length - 1; i >= 0; i--) {
if (enemies[i].destroyed) enemies.splice(i, 1);
}
;
// --- Update and cleanup enemy death particles ---
if (game._enemyParticles) {
for (var i = game._enemyParticles.length - 1; i >= 0; i--) {
var p = game._enemyParticles[i];
// Defensive: initialize lastX/lastY if not set
if (typeof p.lastX === "undefined") p.lastX = p.x;
if (typeof p.lastY === "undefined") p.lastY = p.y;
if (p.destroyed) {
game._enemyParticles.splice(i, 1);
continue;
}
if (typeof p.update === "function") p.update();
}
// Defensive: remove any remaining destroyed particles
for (var i = game._enemyParticles.length - 1; i >= 0; i--) {
if (game._enemyParticles[i].destroyed) game._enemyParticles.splice(i, 1);
}
}
};
// --- Score Initialization ---
LK.setScore(0);
scoreTxt.setText(LK.getScore());