/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // Arena border (visual only) var ArenaBorder = Container.expand(function () { var self = Container.call(this); var border = self.attachAsset('arenaBorderBox', { anchorX: 0.5, anchorY: 0.5, color: 0x181818 // dark color for the border fill }); border.alpha = 0.7; return self; }); // Arena outline (true outline, not filled) var ArenaOutline = Container.expand(function () { var self = Container.call(this); // Use 4 thin rectangles to form a rectangular outline var thickness = 10; var width = ARENA_W + thickness * 2; var height = ARENA_H + thickness * 2; // Top var top = LK.getAsset('arenaBorderBox', { width: width, height: thickness, color: 0xffffff, anchorX: 0.5, anchorY: 0 }); top.x = 0; top.y = -height / 2; self.addChild(top); // Bottom var bottom = LK.getAsset('arenaBorderBox', { width: width, height: thickness, color: 0xffffff, anchorX: 0.5, anchorY: 1 }); bottom.x = 0; bottom.y = height / 2; self.addChild(bottom); // Left var left = LK.getAsset('arenaBorderBox', { width: thickness, height: height, color: 0xffffff, anchorX: 0, anchorY: 0.5 }); left.x = -width / 2; left.y = 0; self.addChild(left); // Unique image asset for Attack button // Unique image asset for Heal button // Right var right = LK.getAsset('arenaBorderBox', { width: thickness, height: height, color: 0xffffff, anchorX: 1, anchorY: 0.5 }); right.x = width / 2; right.y = 0; self.addChild(right); self.alpha = 1; return self; }); var AttackButton = Container.expand(function () { var self = Container.call(this); // Button background (unique image) var bg = LK.getAsset('attackBtnImg', { anchorX: 0.5, anchorY: 0.5 }); bg.alpha = 0.95; self.addChild(bg); // Expose for hit detection self.bg = bg; return self; }); // BlastParticle class for soul death blast particles var BlastParticle = Container.expand(function () { var self = Container.call(this); var particleSprite = self.attachAsset('blastParticle', { anchorX: 0.5, anchorY: 0.5 }); self.vx = 0; self.vy = 0; self._gravity = 0.5; self.update = function () { self.vy += self._gravity; self.x += self.vx; self.y += self.vy; // Remove particle if it is fully faded out and not visible in the game area if (self.alpha <= 0.01 && (self.x < ARENA_X || self.x > ARENA_X + ARENA_W || self.y < ARENA_Y || self.y > ARENA_Y + ARENA_H)) { self.destroy(); } }; return self; }); // Bullet class var Bullet = Container.expand(function () { var self = Container.call(this); var bulletSprite = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.vx = 0; self.vy = 0; self.update = function () { self.x += self.vx; self.y += self.vy; }; return self; }); // Enemy (boss) class var Enemy = Container.expand(function () { var self = Container.call(this); var enemySprite = self.attachAsset('enemyBox', { anchorX: 0.5, anchorY: 0.5 }); // Health property self.maxHP = 20; self.hp = self.maxHP; // For hit flash self.flash = function () { tween(self, { alpha: 0.3 }, { duration: 80, onFinish: function onFinish() { tween(self, { alpha: 1 }, { duration: 120 }); } }); }; return self; }); // EnemyAsset: individual asset for enemy visual var EnemyAsset = Container.expand(function () { var self = Container.call(this); // Main body var body = LK.getAsset('enemyBox', { width: 220, height: 120, anchorX: 0.5, anchorY: 0.5 }); self.addChild(body); // Face/eyes (simple style) var eyeL = LK.getAsset('bullet', { width: 24, height: 24, color: 0xffffff, anchorX: 0.5, anchorY: 0.5 }); eyeL.x = -40; eyeL.y = -20; self.addChild(eyeL); var eyeR = LK.getAsset('bullet', { width: 24, height: 24, color: 0xffffff, anchorX: 0.5, anchorY: 0.5 }); eyeR.x = 40; eyeR.y = -20; self.addChild(eyeR); // Angry brow (simple rectangle) var browL = LK.getAsset('arenaBorderBox', { width: 32, height: 8, color: 0x222222, anchorX: 0.5, anchorY: 0.5 }); browL.x = -40; browL.y = -36; browL.rotation = -0.3; self.addChild(browL); var browR = LK.getAsset('arenaBorderBox', { width: 32, height: 8, color: 0x222222, anchorX: 0.5, anchorY: 0.5 }); browR.x = 40; browR.y = -36; browR.rotation = 0.3; self.addChild(browR); return self; }); // HealButton: individual asset for heal button var HealButton = Container.expand(function () { var self = Container.call(this); // Button background (unique image) var bg = LK.getAsset('healBtnImg', { anchorX: 0.5, anchorY: 0.5 }); bg.alpha = 0.95; self.addChild(bg); // Expose for hit detection self.bg = bg; return self; }); // SlashEffect: right-to-left slash over enemy var SlashEffect = Container.expand(function () { var self = Container.call(this); // Attach slash asset, anchor left-center (0,0.5) var slash = self.attachAsset('slashEffect', { anchorX: 0, anchorY: 0.5 }); // Start fully right of enemy, scale up for dramatic effect self.alpha = 1; self.scaleX = 1.2; self.scaleY = 1.2; // Will be positioned and sized when used self.play = function (enemy) { // Place at enemy's right edge, vertically centered var w = enemy.width || 220; var h = enemy.height || 120; self.x = enemy.x + w / 2 + 30; self.y = enemy.y; // Set initial scale and alpha self.scaleX = 1.2; self.scaleY = 1.2; self.alpha = 1; // Animate: move to left edge, fade out, slight scale tween(self, { x: enemy.x - w / 2 - 30, alpha: 0.1, scaleX: 1.5, scaleY: 1.5 }, { duration: 800, easing: tween.cubicOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); // Soul (player) class var Soul = Container.expand(function () { var self = Container.call(this); var soulSprite = self.attachAsset('soul', { anchorX: 0.5, anchorY: 0.5 }); // For hit flash self.flash = function () { tween(self, { alpha: 0.3 }, { duration: 80, onFinish: function onFinish() { tween(self, { alpha: 1 }, { duration: 120 }); } }); }; return self; }); // WarningBlinker: shows a blinking warning at bullet spawn position before bullet spawns var WarningBlinker = Container.expand(function () { var self = Container.call(this); var sprite = self.attachAsset('warningBlinkerImg', { anchorX: 0.5, anchorY: 0.5 }); self._blinkTime = 0; self._blinkDuration = 90; // 1.5s at 60fps self._blinkInterval = 12; // blink every 12 frames self._onFinish = null; self._destroyed = false; self.update = function () { self._blinkTime++; // Blink: toggle alpha every _blinkInterval frames if (Math.floor(self._blinkTime / self._blinkInterval) % 2 === 0) { self.alpha = 1; } else { self.alpha = 0.2; } if (self._blinkTime >= self._blinkDuration && !self._destroyed) { self._destroyed = true; if (typeof self._onFinish === "function") self._onFinish(); self.destroy(); } }; // Allow setting a callback for when blinking is done self.setFinishCallback = function (cb) { self._onFinish = cb; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Enemy healthbar bg // Enemy healthbar border // Player healthbar bg // Player healthbar border // Enemy only // Arena border only // Unique assets for each visual element // Enemy warning (yellow, for telegraphing) // Bullet (white circle) // Arena border (white rectangle, thin) // Heart-shaped soul (red) // Arena dimensions (centered) var ARENA_W = 1200; var ARENA_H = 900; var ARENA_X = 2048 / 2 - ARENA_W / 2; var ARENA_Y = 2732 / 2 - ARENA_H / 2; // Add arena outline (true outline asset, white) var arenaOutline = new ArenaOutline(); arenaOutline.x = 2048 / 2; arenaOutline.y = 2732 / 2; game.addChild(arenaOutline); // Add enemy (boss) above the arena var enemy = new Enemy(); enemy.x = 2048 / 2; enemy.y = ARENA_Y - 200; // Move enemy further up above arena top game.addChild(enemy); // Add hitmap asset (for skill-based attack) var hitmap = LK.getAsset('hitmapBg', { anchorX: 0.5, anchorY: 0.5, alpha: 1.0 }); hitmap.x = enemy.x; hitmap.y = enemy.y + 1400; // moved SIGNIFICANTLY more down below enemy game.addChild(hitmap); // --- Add attack and heal buttons under the hitmap --- // Button dimensions and spacing var buttonWidth = 340; var buttonHeight = 120; var buttonSpacing = 80; // Y position: just below hitmap var buttonsY = hitmap.y + hitmap.height / 2 + 60; // Attack Button (individual asset) var attackBtn = new AttackButton(); attackBtn.x = hitmap.x - buttonWidth / 2 - buttonSpacing / 2; attackBtn.y = buttonsY; game.addChild(attackBtn); // Heal Button (individual asset) var healBtn = new HealButton(); healBtn.x = hitmap.x + buttonWidth / 2 + buttonSpacing / 2; healBtn.y = buttonsY; game.addChild(healBtn); // Button interaction logic attackBtn._isButton = true; healBtn._isButton = true; // Helper: check if (x, y) is inside a button (using .bg for new button assets) function isInsideBtn(x, y, btn) { var btnBg = btn.bg; var globalX = btn.x; var globalY = btn.y; return x >= globalX - btnBg.width / 2 && x <= globalX + btnBg.width / 2 && y >= globalY - btnBg.height / 2 && y <= globalY + btnBg.height / 2; } // Save original game.down var origGameDown = game.down; game.down = function (x, y, obj) { // Check attack button if (isInsideBtn(x, y, attackBtn)) { // Only allow attack if skill-based attack is enabled if (canAttack && !attackRegistered) { // Simulate a tap in the center of the hitmap (auto-perfect) var centerX = hitmap.x; attackColumn.x = centerX; // Call original attack logic origGameDown.call(game, centerX, hitmap.y, obj); // After choosing attack, disable both attack and heal for this window canAttack = false; attackRegistered = true; } return; } // Check heal button if (isInsideBtn(x, y, healBtn)) { // Only allow heal if not at max HP and alive, and only during attack window if (canAttack && !attackRegistered && soulAlive && soulHP < soulMaxHP) { soulHP = Math.min(soulMaxHP, soulHP + 2); updatePlayerHealthbar(); // Flash green for feedback LK.effects.flashObject(playerHealthbarBox, 0x44ff44, 400); // After choosing heal, disable both attack and heal for this window canAttack = false; attackRegistered = true; // (Wave counter update removed) // Count this as an undamaged wave (healing is only allowed after undamaged wave) undamagedWaves++; LK.setTimeout(function () { if (!enemyDefeated) { if (currentWave + 1 < waves.length) { startWave(currentWave + 1); } else { waveSet += 1; buildWavesForSet(waveSet); startWave(0); } } }, 900); } return; } // Otherwise, normal game logic origGameDown.call(game, x, y, obj); }; // Enemy hover effect: slow up and down tween loop function enemyHoverUp() { tween(enemy, { y: ARENA_Y - 170 }, { duration: 1200, easing: tween.sineInOut, onFinish: enemyHoverDown }); } function enemyHoverDown() { tween(enemy, { y: ARENA_Y - 230 }, { duration: 1200, easing: tween.sineInOut, onFinish: enemyHoverUp }); } // Start at current position, tween down first enemyHoverDown(); // Add soul (player) var soul = new Soul(); soul.x = 2048 / 2; soul.y = 2732 / 2; game.addChild(soul); // GUI: Timer (shows wave time left) var timerTxt = new Text2('', { size: 80, fill: "#fff" }); timerTxt.anchor.set(0.5, 0); LK.gui.top.addChild(timerTxt); // (Wave counter removed) // Game state var bullets = []; var currentWave = 0; var waveTime = 0; var waveTimer = 0; var waveActive = false; var soulAlive = true; var dragSoul = false; var dragOffsetX = 0; var dragOffsetY = 0; // Skill-based attack: moving column state var attackColumn = LK.getAsset('arenaBorderBox', { width: 40, height: 100, color: 0xffe066, anchorX: 0.5, anchorY: 0.5, alpha: 0.85 }); attackColumn.x = hitmap.x - 120; // start left of hitmap attackColumn.y = hitmap.y; game.addChild(attackColumn); // Column movement state var columnDir = 1; // 1 = right, -1 = left var columnSpeed = 16; // px per frame (increased speed) attackColumn.lastX = attackColumn.x; // Skill attack enabled only at end of wave var canAttack = false; var attackRegistered = false; // Soul health var soulMaxHP = 5; var soulHP = soulMaxHP; // Player healthbar (box style) under the arena var playerHealthbarBg = LK.getAsset('playerHealthbarBg', { anchorX: 0.5, anchorY: 0.5 }); var playerHealthbarBox = LK.getAsset('playerHealthbarBox', { anchorX: 0, anchorY: 0.5 }); // Healthbar dimensions var healthbarWidth = 900; var healthbarHeight = 39.49; var healthbarX = 2048 / 2 - healthbarWidth / 2; var healthbarY = ARENA_Y + ARENA_H + 60; // 60px below arena playerHealthbarBg.x = 2048 / 2; playerHealthbarBg.y = healthbarY + healthbarHeight / 2; playerHealthbarBox.x = healthbarX; playerHealthbarBox.y = healthbarY + healthbarHeight / 2; // Add HP label to the left of the healthbar var hpLabelTxt = new Text2("HP", { size: 64, fill: "#fff" }); hpLabelTxt.anchor.set(1, 0.5); // right align, vertically centered // Place it 24px to the left of the healthbar hpLabelTxt.x = healthbarX - 24; hpLabelTxt.y = healthbarY + healthbarHeight / 2; // Add to game game.addChild(playerHealthbarBg); game.addChild(playerHealthbarBox); game.addChild(hpLabelTxt); // Update function for healthbar function updatePlayerHealthbar() { // Clamp HP var hp = Math.max(0, Math.min(soulHP, soulMaxHP)); // Set width proportional to HP playerHealthbarBox.width = healthbarWidth * (hp / soulMaxHP); } updatePlayerHealthbar(); // Arena bounds (for clamping soul) function clampSoul(x, y) { var hw = 50, hh = 50; // soul half size var minX = ARENA_X + hw; var maxX = ARENA_X + ARENA_W - hw; var minY = ARENA_Y + hh; var maxY = ARENA_Y + ARENA_H - hh; return { x: Math.max(minX, Math.min(maxX, x)), y: Math.max(minY, Math.min(maxY, y)) }; } // Touch/drag controls function handleSoulMove(x, y, obj) { if (!dragSoul || !soulAlive) return; var pos = clampSoul(x - dragOffsetX, y - dragOffsetY); soul.x = pos.x; soul.y = pos.y; } game.move = function (x, y, obj) { handleSoulMove(x, y, obj); }; game.down = function (x, y, obj) { // Prevent attack logic if heal button is pressed if (isInsideBtn(x, y, healBtn)) { // Only allow heal if not at max HP and alive, and only during attack window if (canAttack && !attackRegistered && soulAlive && soulHP < soulMaxHP) { soulHP = Math.min(soulMaxHP, soulHP + 2); updatePlayerHealthbar(); // Flash green for feedback LK.effects.flashObject(playerHealthbarBox, 0x44ff44, 400); // After choosing heal, disable both attack and heal for this window canAttack = false; attackRegistered = true; // Show a message and start next wave after a short delay waveTxt.setText("Healed! Next wave..."); // Count this as an undamaged wave (healing is only allowed after undamaged wave) undamagedWaves++; LK.setTimeout(function () { if (!enemyDefeated) { if (currentWave + 1 < waves.length) { startWave(currentWave + 1); } else { waveSet += 1; buildWavesForSet(waveSet); startWave(0); } } }, 900); } return; } // Skill-based attack: if attack window is open, check for hit if (canAttack && !attackRegistered) { // Check if column is in the center of hitmap (allow some margin) var centerX = hitmap.x; var margin = 32; if (Math.abs(attackColumn.x - centerX) <= margin) { // Successful attack! attackRegistered = true; canAttack = false; undamagedWaves++; // Enemy takes a hit if enough undamaged waves if (undamagedWaves >= requiredUndamagedWaves) { enemyDefeated = true; enemy.flash(); var slash = new SlashEffect(); game.addChild(slash); slash.play(enemy); LK.stopMusic(); LK.effects.flashScreen(0x00ff00, 1200); LK.setTimeout(function () { LK.showYouWin(); }, 1200); return; } else { // Flash enemy for each undamaged wave enemy.flash(); var slash = new SlashEffect(); game.addChild(slash); slash.play(enemy); } // Start next wave after short delay LK.setTimeout(function () { if (!enemyDefeated) { if (currentWave + 1 < waves.length) { startWave(currentWave + 1); } else { waveSet += 1; buildWavesForSet(waveSet); startWave(0); } } }, 700); return; } else { // Missed! (optional: flash hitmap red) tween(hitmap, { color: 0xff4444 }, { duration: 120, onFinish: function onFinish() { tween(hitmap, { color: 0x00ffcc }, { duration: 180 }); } }); // End the attack chance if player misses the center canAttack = false; attackRegistered = true; // (Wave counter update removed) // Optionally, start next wave after a short delay LK.setTimeout(function () { if (!enemyDefeated) { if (currentWave + 1 < waves.length) { startWave(currentWave + 1); } else { waveSet += 1; buildWavesForSet(waveSet); startWave(0); } } }, 900); return; } } // Only start drag if inside soul var dx = x - soul.x, dy = y - soul.y; if (dx * dx + dy * dy <= 60 * 60) { dragSoul = true; dragOffsetX = x - soul.x; dragOffsetY = y - soul.y; handleSoulMove(x, y, obj); } }; game.up = function (x, y, obj) { dragSoul = false; }; // Waves definition // Each wave: {duration, spawnFunc(tick, bulletsArr)} // Difficulty scaling: each time all waves are completed, the next set is harder var baseWaves = [ // Wave 1: Simple slow bullets from top { duration: 180, spawnFunc: function spawnFunc(tick, arr, difficulty) { // Unpredictable horizontal movement for the whole line if (typeof spawnFunc._targetX === "undefined") { spawnFunc._targetX = 0; spawnFunc._currentX = 0; spawnFunc._tween = null; spawnFunc._lastTick = 0; } if (tick - spawnFunc._lastTick > 40 || Math.abs(spawnFunc._targetX - spawnFunc._currentX) < 5) { // Pick a new random target X offset within [-180, 180] var newTarget = (Math.random() * 2 - 1) * 180; spawnFunc._lastTick = tick; spawnFunc._targetX = newTarget; // Tween smoothly to new target over 30-60 frames if (spawnFunc._tween) spawnFunc._tween.stop(); spawnFunc._tween = tween(spawnFunc, { _currentX: newTarget }, { duration: 30 + Math.floor(Math.random() * 30) }); } var lineXOffset = spawnFunc._currentX; var fireRate = Math.max(8, 24 - 2 * difficulty); // faster at higher difficulty if (tick % fireRate === 0) { var speed = 8 + 1.5 * difficulty; var count = 6 + Math.floor(difficulty / 2); for (var i = 0; i < count; ++i) { var b = new Bullet(); b.x = ARENA_X + 120 + i * ((ARENA_W - 240) / (count - 1)) + lineXOffset; b.y = ARENA_Y + 30; b.vx = 0; b.vy = speed; arr.push(b); game.addChild(b); } } } }, // Wave 2: Bullets from left/right { duration: 210, spawnFunc: function spawnFunc(tick, arr, difficulty) { var lineOscAmp = 120 + 10 * difficulty; var lineOscFreq = 0.025 + 0.002 * difficulty; var lineYOffset = Math.sin(tick * lineOscFreq) * lineOscAmp; var centerY1 = ARENA_Y + ARENA_H / 2 - 120; var centerY2 = ARENA_Y + ARENA_H / 2 + 120; var fireRate = Math.max(8, 24 - 2 * difficulty); if (tick % fireRate === 0) { var speed = 10 + 1.5 * difficulty; var amplitude = 60 + 6 * difficulty; var freq = 0.012 + 0.001 * difficulty; // Left to right var b1 = new Bullet(); b1.x = ARENA_X + 30; b1.y = (tick % 2 === 0 ? centerY1 : centerY2) + lineYOffset; b1.vx = speed; b1.vy = amplitude * Math.sin((tick + 0) * freq); b1._sPhase = (tick + 0) * freq; b1._sDir = 1; b1._baseY = b1.y; b1._baseX = b1.x; b1._t = 0; b1._lineOscAmp = lineOscAmp; b1._lineOscFreq = lineOscFreq; b1._spawnTick = tick; b1.update = function () { this.x += this.vx; this._t += 1; var lineYOffset = Math.sin((this._spawnTick + this._t) * this._lineOscFreq) * this._lineOscAmp; this.y = this._baseY + amplitude * Math.sin((this.x - this._baseX) * freq) + (lineYOffset - Math.sin(this._spawnTick * this._lineOscFreq) * this._lineOscAmp); }; arr.push(b1); game.addChild(b1); // Right to left var b2 = new Bullet(); b2.x = ARENA_X + ARENA_W - 30; b2.y = (tick % 2 === 0 ? centerY2 : centerY1) + lineYOffset; b2.vx = -speed; b2.vy = amplitude * Math.sin((tick + 60) * freq); b2._sPhase = (tick + 60) * freq; b2._sDir = -1; b2._baseY = b2.y; b2._baseX = b2.x; b2._t = 0; b2._lineOscAmp = lineOscAmp; b2._lineOscFreq = lineOscFreq; b2._spawnTick = tick; b2.update = function () { this.x += this.vx; this._t += 1; var lineYOffset = Math.sin((this._spawnTick + this._t) * this._lineOscFreq) * this._lineOscAmp; this.y = this._baseY + amplitude * Math.sin((this.x - this._baseX) * freq) + (lineYOffset - Math.sin(this._spawnTick * this._lineOscFreq) * this._lineOscAmp); }; arr.push(b2); game.addChild(b2); } } }, // Wave 3: Diagonal bullets from corners { duration: 240, spawnFunc: function spawnFunc(tick, arr, difficulty) { var fireRate = Math.max(8, 36 - 2 * difficulty); if (tick % fireRate === 0) { var speed = 7.5 + 1.2 * difficulty; var diag = speed / Math.sqrt(2); // Top-left var b1 = new Bullet(); b1.x = ARENA_X + 40; b1.y = ARENA_Y + 40; b1.vx = diag; b1.vy = diag; arr.push(b1); game.addChild(b1); // Top-right var b2 = new Bullet(); b2.x = ARENA_X + ARENA_W - 40; b2.y = ARENA_Y + 40; b2.vx = -diag; b2.vy = diag; arr.push(b2); game.addChild(b2); // Bottom-left var b3 = new Bullet(); b3.x = ARENA_X + 40; b3.y = ARENA_Y + ARENA_H - 40; b3.vx = diag; b3.vy = -diag; arr.push(b3); game.addChild(b3); // Bottom-right var b4 = new Bullet(); b4.x = ARENA_X + ARENA_W - 40; b4.y = ARENA_Y + ARENA_H - 40; b4.vx = -diag; b4.vy = -diag; arr.push(b4); game.addChild(b4); // At higher difficulty, add a random diagonal bullet if (difficulty >= 2) { var b5 = new Bullet(); var side = Math.floor(Math.random() * 4); if (side === 0) { b5.x = ARENA_X + 40; b5.y = ARENA_Y + 40; b5.vx = diag * 1.2; b5.vy = diag * 1.2; } if (side === 1) { b5.x = ARENA_X + ARENA_W - 40; b5.y = ARENA_Y + 40; b5.vx = -diag * 1.2; b5.vy = diag * 1.2; } if (side === 2) { b5.x = ARENA_X + 40; b5.y = ARENA_Y + ARENA_H - 40; b5.vx = diag * 1.2; b5.vy = -diag * 1.2; } if (side === 3) { b5.x = ARENA_X + ARENA_W - 40; b5.y = ARENA_Y + ARENA_H - 40; b5.vx = -diag * 1.2; b5.vy = -diag * 1.2; } arr.push(b5); game.addChild(b5); } } } }, // Wave 4: Random rain { duration: 210, spawnFunc: function spawnFunc(tick, arr, difficulty) { var fireRate = Math.max(2, 10 - Math.floor(difficulty / 2)); if (tick % fireRate === 0) { var minSpeed = 12 + 1.2 * difficulty, maxSpeed = 16 + 1.5 * difficulty; var count = 1 + Math.floor(difficulty / 2); for (var i = 0; i < count; ++i) { var b = new Bullet(); b.x = ARENA_X + 80 + Math.floor(Math.random() * (ARENA_W - 160)); b.y = ARENA_Y + 30; b.vx = 0; b.vy = minSpeed + Math.random() * (maxSpeed - minSpeed); arr.push(b); game.addChild(b); } } } }, // Wave 5: Spiral (center out) { duration: 270, spawnFunc: function spawnFunc(tick, arr, difficulty) { var fireRate = Math.max(3, 12 - Math.floor(difficulty / 2)); if (tick % fireRate === 0) { var speed = 9 + 1.2 * difficulty; var angle = tick / fireRate * (0.5 + 0.1 * difficulty); var count = 4 + Math.floor(difficulty / 2); for (var i = 0; i < count; ++i) { var a = angle + i * (2 * Math.PI / count); var b = new Bullet(); b.x = 2048 / 2; b.y = 2732 / 2; b.vx = Math.cos(a) * speed; b.vy = Math.sin(a) * speed; arr.push(b); game.addChild(b); } } } }, // Wave 6: New - Cross barrages (hard) { duration: 240, spawnFunc: function spawnFunc(tick, arr, difficulty) { var fireRate = Math.max(6, 24 - 2 * difficulty); if (tick % fireRate === 0) { var speed = 10 + 1.5 * difficulty; // Horizontal for (var i = 0; i < 3 + Math.floor(difficulty / 2); ++i) { var b = new Bullet(); b.x = ARENA_X + 80 + i * ((ARENA_W - 160) / (2 + Math.floor(difficulty / 2))); b.y = ARENA_Y + 30; b.vx = 0; b.vy = speed; arr.push(b); game.addChild(b); } // Vertical for (var i = 0; i < 3 + Math.floor(difficulty / 2); ++i) { var b = new Bullet(); b.x = ARENA_X + 30; b.y = ARENA_Y + 80 + i * ((ARENA_H - 160) / (2 + Math.floor(difficulty / 2))); b.vx = speed; b.vy = 0; arr.push(b); game.addChild(b); } for (var i = 0; i < 3 + Math.floor(difficulty / 2); ++i) { var b = new Bullet(); b.x = ARENA_X + ARENA_W - 30; b.y = ARENA_Y + 80 + i * ((ARENA_H - 160) / (2 + Math.floor(difficulty / 2))); b.vx = -speed; b.vy = 0; arr.push(b); game.addChild(b); } } } }, // Wave 7: New - Spiral rain (even easier) { duration: 300, spawnFunc: function spawnFunc(tick, arr, difficulty) { var fireRate = Math.max(5, 18 - Math.floor(difficulty / 2)); // much slower fire rate if (tick % fireRate === 0) { var speed = 5 + 0.7 * difficulty; // even slower speed var count = 3 + Math.floor(difficulty / 4); // even fewer bullets var baseAngle = tick / fireRate * 0.13; // even slower spiral for (var i = 0; i < count; ++i) { var a = baseAngle + i * (2 * Math.PI / count); var b = new Bullet(); b.x = 2048 / 2 + Math.cos(a) * 400; b.y = 2732 / 2 + Math.sin(a) * 400; b.vx = Math.cos(a) * -speed; b.vy = Math.sin(a) * -speed; arr.push(b); game.addChild(b); } } } }]; // The actual waves array is generated for each "cycle" of all waves, increasing difficulty each time var waves = []; var waveSet = 0; // how many times all waves have been completed // Track undamaged waves in a row var undamagedWaves = 0; var requiredUndamagedWaves = 5; var waveWasDamaged = false; var enemyDefeated = false; function buildWavesForSet(set) { // Create a shallow copy of baseWaves and shuffle it to randomize order var shuffled = []; for (var i = 0; i < baseWaves.length; ++i) shuffled.push(baseWaves[i]); // Fisher-Yates shuffle for (var i = shuffled.length - 1; i > 0; --i) { var j = Math.floor(Math.random() * (i + 1)); var temp = shuffled[i]; shuffled[i] = shuffled[j]; shuffled[j] = temp; } // Ensure Spiral Rain (wave 7, index 6 in baseWaves) is not first // Find the spiral rain wave in shuffled var spiralRainIdx = -1; for (var i = 0; i < shuffled.length; ++i) { if (shuffled[i] === baseWaves[6]) { spiralRainIdx = i; break; } } if (spiralRainIdx === 0 && shuffled.length > 1) { // Swap with a random other wave (not 0) var swapWith = 1 + Math.floor(Math.random() * (shuffled.length - 1)); var temp = shuffled[0]; shuffled[0] = shuffled[swapWith]; shuffled[swapWith] = temp; } waves = []; for (var i = 0; i < shuffled.length; ++i) { (function (baseWave, idx) { waves.push({ duration: Math.max(120, Math.floor(baseWave.duration * Math.max(0.7, 1 - set * 0.08))), // slightly shorter at higher sets spawnFunc: function spawnFunc(tick, arr) { baseWave.spawnFunc(tick, arr, set); } }); })(shuffled[i], i); } } buildWavesForSet(waveSet); // Start first wave function startWave(idx) { currentWave = idx; waveTime = 0; waveActive = true; waveTimer = 0; waveWasDamaged = false; // Remove all bullets for (var i = bullets.length - 1; i >= 0; --i) { bullets[i].destroy(); bullets.splice(i, 1); } // Remove all warning blinkers if (typeof warningBlinkers !== "undefined" && warningBlinkers.length) { for (var i = warningBlinkers.length - 1; i >= 0; --i) { warningBlinkers[i].destroy(); warningBlinkers.splice(i, 1); } } warningBlinkers = []; // (Wave counter update removed) // Reset HP and healthbar if starting first wave (new game) if (currentWave === 0) { soulHP = soulMaxHP; soulAlive = true; updatePlayerHealthbar(); // Restore soul sprite if needed if (soul.children && soul.children.length > 0) { var firstChild = soul.children[0]; if (firstChild.assetId === 'warning') { soul.removeChild(firstChild); var newSoulSprite = LK.getAsset('soul', { anchorX: 0.5, anchorY: 0.5 }); soul.addChildAt(newSoulSprite, 0); // Play sound on soul texture change (revive) LK.getSound('shoot').play(); } } } } startWave(0); // Play background music at game start LK.playMusic('music'); // Main update loop game.update = function () { if (!soulAlive) return; // Wave logic if (waveActive) { var wave = waves[currentWave]; // Only spawn bullets after 1.5 seconds (90 frames) if (waveTime === 1) { // On first frame of wave, create warning blinkers for all bullets that will spawn at t=0 if (typeof warningBlinkers === "undefined") warningBlinkers = []; var previewBullets = []; // Simulate spawnFunc at t=0, but don't add to game, just get positions var fakeArr = []; // For each wave, we need to know where bullets will spawn at t=0 // We'll use a proxy to collect spawn positions var addChildOrig = game.addChild; game.addChild = function (obj) { fakeArr.push(obj); return obj; }; wave.spawnFunc(0, []); game.addChild = addChildOrig; // Now, for each bullet in fakeArr, create a warning blinker at its x/y for (var i = 0; i < fakeArr.length; ++i) { var b = fakeArr[i]; if (typeof b.x === "number" && typeof b.y === "number") { var blinker = new WarningBlinker(); blinker.x = b.x; blinker.y = b.y; warningBlinkers.push(blinker); game.addChild(blinker); } } } if (waveTime === 90) { // Remove all warning blinkers at bullet spawn time if (typeof warningBlinkers !== "undefined" && warningBlinkers.length) { for (var i = warningBlinkers.length - 1; i >= 0; --i) { warningBlinkers[i].destroy(); warningBlinkers.splice(i, 1); } } } if (waveTime >= 90) { wave.spawnFunc(waveTime - 90, bullets); } waveTime++; // Update timer GUI var tleft = Math.max(0, Math.ceil((wave.duration - waveTime) / 60)); timerTxt.setText("Time: " + tleft + "s"); // End wave? if (waveTime >= wave.duration) { waveActive = false; // Remove all bullets after short delay LK.setTimeout(function () { for (var i = bullets.length - 1; i >= 0; --i) { bullets[i].destroy(); bullets.splice(i, 1); } // Only allow enemy defeat if not already defeated if (!enemyDefeated) { // Skill-based attack: enable attack window if player was not hit this wave if (!waveWasDamaged) { canAttack = true; attackRegistered = false; // Show a visual cue (flash hitmap) // No alpha tween for hitmap, keep at 1.0 // (Wave counter update removed) // Pause next wave until attack or heal is registered return; } else { // Do not reset undamagedWaves if hit; keep the counter unchanged // No healing is performed automatically here } } // Next wave or infinite loop if (!enemyDefeated) { if (currentWave + 1 < waves.length) { startWave(currentWave + 1); } else { // All waves in this set completed, increase difficulty and repeat with harder waves! waveSet += 1; buildWavesForSet(waveSet); startWave(0); } } }, 600); } } // Update moving column for skill-based attack if (canAttack && !attackRegistered) { // Move column left/right within hitmap bounds var leftEdge = hitmap.x - hitmap.width / 2 + attackColumn.width / 2 + 8; var rightEdge = hitmap.x + hitmap.width / 2 - attackColumn.width / 2 - 8; attackColumn.x += columnDir * columnSpeed; if (attackColumn.x <= leftEdge) { attackColumn.x = leftEdge; columnDir = 1; } if (attackColumn.x >= rightEdge) { attackColumn.x = rightEdge; columnDir = -1; } // Store lastX for event detection attackColumn.lastX = attackColumn.x; } // Update bullets for (var i = bullets.length - 1; i >= 0; --i) { var b = bullets[i]; b.update(); // Remove if out of arena (immediately on leaving the border) if (b.x < ARENA_X || b.x > ARENA_X + ARENA_W || b.y < ARENA_Y || b.y > ARENA_Y + ARENA_H) { b.destroy(); bullets.splice(i, 1); continue; } // Collision with soul var dx = b.x - soul.x, dy = b.y - soul.y; if (dx * dx + dy * dy < 45 * 45) { // Hit! soul.flash(); soulHP -= 1; updatePlayerHealthbar(); // Mark this wave as damaged waveWasDamaged = true; // Stop music only when soul dies (on 6th hit) if (soulHP <= 0 && soulAlive) { LK.stopMusic(); } // Remove bullet on hit b.destroy(); bullets.splice(i, 1); if (soulHP <= 0 && soulAlive) { soulAlive = false; // Delay soul's texture asset change to 2 seconds after dying LK.setTimeout(function () { if (soul.children && soul.children.length > 0) { var deadSprite = LK.getAsset('warning', { anchorX: 0.5, anchorY: 0.5 }); var oldSprite = soul.children[0]; deadSprite.x = oldSprite.x; deadSprite.y = oldSprite.y; soul.removeChild(oldSprite); soul.addChildAt(deadSprite, 0); // Play sound on soul texture change (death) LK.getSound('shoot').play(); // Blast into particles 1.35 sec after texture change LK.setTimeout(function () { // Remove soul sprite (if still present) if (soul.children && soul.children.length > 0) { soul.removeChild(soul.children[0]); } // Create 8 particles in a circle (reduced from 18) var numParticles = 8; var radius = 60; for (var i = 0; i < numParticles; ++i) { var angle = 2 * Math.PI * i / numParticles; var px = soul.x + Math.cos(angle) * 0; var py = soul.y + Math.sin(angle) * 0; var particle = new BlastParticle(); particle.x = px; particle.y = py; // Give each particle a random color (yellow/orange/red) var colors = [0xffe066, 0xffb347, 0xff4444, 0xffe0a0, 0xffd700]; var color = colors[Math.floor(Math.random() * colors.length)]; if (particle.children && particle.children.length > 0) { particle.children[0].tint = color; } // Set velocity outward (slow motion: reduce speed) var speed = (13 + Math.random() * 4) * 0.35; particle.vx = Math.cos(angle) * speed; particle.vy = Math.sin(angle) * speed; // Add gravity property for blast particles (slow motion: reduce gravity) particle._gravity = (0.5 + Math.random() * 0.15) * 0.25; // Fade out and destroy after 0.7s tween(particle, { alpha: 0 }, { duration: 700, onFinish: function () { // Only destroy if already off-screen, otherwise wait for off-screen if (this.x < ARENA_X - 100 || this.x > ARENA_X + ARENA_W + 100 || this.y < ARENA_Y - 100 || this.y > ARENA_Y + ARENA_H + 100) { this.destroy(); } else { // Wait until particle is far off-screen, then destroy var p = this; p._offscreenCheck = function () { if (p.x < ARENA_X - 100 || p.x > ARENA_X + ARENA_W + 100 || p.y < ARENA_Y - 100 || p.y > ARENA_Y + ARENA_H + 100) { p.destroy(); // Remove from update loop p.update = function () {}; } }; // Wrap the original update to check for offscreen var origUpdate = p.update; p.update = function () { origUpdate && origUpdate.call(p); p._offscreenCheck(); }; } }.bind(particle) }); game.addChild(particle); } }, 1350); } }, 2000); LK.effects.flashScreen(0xff0000, 800); LK.setTimeout(function () { LK.showGameOver(); }, 5000); break; } else { // Flash screen for hit, but not game over LK.effects.flashScreen(0xff0000, 300); } } } }; // Initial GUI text timerTxt.setText("Time: ");
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Arena border (visual only)
var ArenaBorder = Container.expand(function () {
var self = Container.call(this);
var border = self.attachAsset('arenaBorderBox', {
anchorX: 0.5,
anchorY: 0.5,
color: 0x181818 // dark color for the border fill
});
border.alpha = 0.7;
return self;
});
// Arena outline (true outline, not filled)
var ArenaOutline = Container.expand(function () {
var self = Container.call(this);
// Use 4 thin rectangles to form a rectangular outline
var thickness = 10;
var width = ARENA_W + thickness * 2;
var height = ARENA_H + thickness * 2;
// Top
var top = LK.getAsset('arenaBorderBox', {
width: width,
height: thickness,
color: 0xffffff,
anchorX: 0.5,
anchorY: 0
});
top.x = 0;
top.y = -height / 2;
self.addChild(top);
// Bottom
var bottom = LK.getAsset('arenaBorderBox', {
width: width,
height: thickness,
color: 0xffffff,
anchorX: 0.5,
anchorY: 1
});
bottom.x = 0;
bottom.y = height / 2;
self.addChild(bottom);
// Left
var left = LK.getAsset('arenaBorderBox', {
width: thickness,
height: height,
color: 0xffffff,
anchorX: 0,
anchorY: 0.5
});
left.x = -width / 2;
left.y = 0;
self.addChild(left);
// Unique image asset for Attack button
// Unique image asset for Heal button
// Right
var right = LK.getAsset('arenaBorderBox', {
width: thickness,
height: height,
color: 0xffffff,
anchorX: 1,
anchorY: 0.5
});
right.x = width / 2;
right.y = 0;
self.addChild(right);
self.alpha = 1;
return self;
});
var AttackButton = Container.expand(function () {
var self = Container.call(this);
// Button background (unique image)
var bg = LK.getAsset('attackBtnImg', {
anchorX: 0.5,
anchorY: 0.5
});
bg.alpha = 0.95;
self.addChild(bg);
// Expose for hit detection
self.bg = bg;
return self;
});
// BlastParticle class for soul death blast particles
var BlastParticle = Container.expand(function () {
var self = Container.call(this);
var particleSprite = self.attachAsset('blastParticle', {
anchorX: 0.5,
anchorY: 0.5
});
self.vx = 0;
self.vy = 0;
self._gravity = 0.5;
self.update = function () {
self.vy += self._gravity;
self.x += self.vx;
self.y += self.vy;
// Remove particle if it is fully faded out and not visible in the game area
if (self.alpha <= 0.01 && (self.x < ARENA_X || self.x > ARENA_X + ARENA_W || self.y < ARENA_Y || self.y > ARENA_Y + ARENA_H)) {
self.destroy();
}
};
return self;
});
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bulletSprite = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.vx = 0;
self.vy = 0;
self.update = function () {
self.x += self.vx;
self.y += self.vy;
};
return self;
});
// Enemy (boss) class
var Enemy = Container.expand(function () {
var self = Container.call(this);
var enemySprite = self.attachAsset('enemyBox', {
anchorX: 0.5,
anchorY: 0.5
});
// Health property
self.maxHP = 20;
self.hp = self.maxHP;
// For hit flash
self.flash = function () {
tween(self, {
alpha: 0.3
}, {
duration: 80,
onFinish: function onFinish() {
tween(self, {
alpha: 1
}, {
duration: 120
});
}
});
};
return self;
});
// EnemyAsset: individual asset for enemy visual
var EnemyAsset = Container.expand(function () {
var self = Container.call(this);
// Main body
var body = LK.getAsset('enemyBox', {
width: 220,
height: 120,
anchorX: 0.5,
anchorY: 0.5
});
self.addChild(body);
// Face/eyes (simple style)
var eyeL = LK.getAsset('bullet', {
width: 24,
height: 24,
color: 0xffffff,
anchorX: 0.5,
anchorY: 0.5
});
eyeL.x = -40;
eyeL.y = -20;
self.addChild(eyeL);
var eyeR = LK.getAsset('bullet', {
width: 24,
height: 24,
color: 0xffffff,
anchorX: 0.5,
anchorY: 0.5
});
eyeR.x = 40;
eyeR.y = -20;
self.addChild(eyeR);
// Angry brow (simple rectangle)
var browL = LK.getAsset('arenaBorderBox', {
width: 32,
height: 8,
color: 0x222222,
anchorX: 0.5,
anchorY: 0.5
});
browL.x = -40;
browL.y = -36;
browL.rotation = -0.3;
self.addChild(browL);
var browR = LK.getAsset('arenaBorderBox', {
width: 32,
height: 8,
color: 0x222222,
anchorX: 0.5,
anchorY: 0.5
});
browR.x = 40;
browR.y = -36;
browR.rotation = 0.3;
self.addChild(browR);
return self;
});
// HealButton: individual asset for heal button
var HealButton = Container.expand(function () {
var self = Container.call(this);
// Button background (unique image)
var bg = LK.getAsset('healBtnImg', {
anchorX: 0.5,
anchorY: 0.5
});
bg.alpha = 0.95;
self.addChild(bg);
// Expose for hit detection
self.bg = bg;
return self;
});
// SlashEffect: right-to-left slash over enemy
var SlashEffect = Container.expand(function () {
var self = Container.call(this);
// Attach slash asset, anchor left-center (0,0.5)
var slash = self.attachAsset('slashEffect', {
anchorX: 0,
anchorY: 0.5
});
// Start fully right of enemy, scale up for dramatic effect
self.alpha = 1;
self.scaleX = 1.2;
self.scaleY = 1.2;
// Will be positioned and sized when used
self.play = function (enemy) {
// Place at enemy's right edge, vertically centered
var w = enemy.width || 220;
var h = enemy.height || 120;
self.x = enemy.x + w / 2 + 30;
self.y = enemy.y;
// Set initial scale and alpha
self.scaleX = 1.2;
self.scaleY = 1.2;
self.alpha = 1;
// Animate: move to left edge, fade out, slight scale
tween(self, {
x: enemy.x - w / 2 - 30,
alpha: 0.1,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 800,
easing: tween.cubicOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Soul (player) class
var Soul = Container.expand(function () {
var self = Container.call(this);
var soulSprite = self.attachAsset('soul', {
anchorX: 0.5,
anchorY: 0.5
});
// For hit flash
self.flash = function () {
tween(self, {
alpha: 0.3
}, {
duration: 80,
onFinish: function onFinish() {
tween(self, {
alpha: 1
}, {
duration: 120
});
}
});
};
return self;
});
// WarningBlinker: shows a blinking warning at bullet spawn position before bullet spawns
var WarningBlinker = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('warningBlinkerImg', {
anchorX: 0.5,
anchorY: 0.5
});
self._blinkTime = 0;
self._blinkDuration = 90; // 1.5s at 60fps
self._blinkInterval = 12; // blink every 12 frames
self._onFinish = null;
self._destroyed = false;
self.update = function () {
self._blinkTime++;
// Blink: toggle alpha every _blinkInterval frames
if (Math.floor(self._blinkTime / self._blinkInterval) % 2 === 0) {
self.alpha = 1;
} else {
self.alpha = 0.2;
}
if (self._blinkTime >= self._blinkDuration && !self._destroyed) {
self._destroyed = true;
if (typeof self._onFinish === "function") self._onFinish();
self.destroy();
}
};
// Allow setting a callback for when blinking is done
self.setFinishCallback = function (cb) {
self._onFinish = cb;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Enemy healthbar bg
// Enemy healthbar border
// Player healthbar bg
// Player healthbar border
// Enemy only
// Arena border only
// Unique assets for each visual element
// Enemy warning (yellow, for telegraphing)
// Bullet (white circle)
// Arena border (white rectangle, thin)
// Heart-shaped soul (red)
// Arena dimensions (centered)
var ARENA_W = 1200;
var ARENA_H = 900;
var ARENA_X = 2048 / 2 - ARENA_W / 2;
var ARENA_Y = 2732 / 2 - ARENA_H / 2;
// Add arena outline (true outline asset, white)
var arenaOutline = new ArenaOutline();
arenaOutline.x = 2048 / 2;
arenaOutline.y = 2732 / 2;
game.addChild(arenaOutline);
// Add enemy (boss) above the arena
var enemy = new Enemy();
enemy.x = 2048 / 2;
enemy.y = ARENA_Y - 200; // Move enemy further up above arena top
game.addChild(enemy);
// Add hitmap asset (for skill-based attack)
var hitmap = LK.getAsset('hitmapBg', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 1.0
});
hitmap.x = enemy.x;
hitmap.y = enemy.y + 1400; // moved SIGNIFICANTLY more down below enemy
game.addChild(hitmap);
// --- Add attack and heal buttons under the hitmap ---
// Button dimensions and spacing
var buttonWidth = 340;
var buttonHeight = 120;
var buttonSpacing = 80;
// Y position: just below hitmap
var buttonsY = hitmap.y + hitmap.height / 2 + 60;
// Attack Button (individual asset)
var attackBtn = new AttackButton();
attackBtn.x = hitmap.x - buttonWidth / 2 - buttonSpacing / 2;
attackBtn.y = buttonsY;
game.addChild(attackBtn);
// Heal Button (individual asset)
var healBtn = new HealButton();
healBtn.x = hitmap.x + buttonWidth / 2 + buttonSpacing / 2;
healBtn.y = buttonsY;
game.addChild(healBtn);
// Button interaction logic
attackBtn._isButton = true;
healBtn._isButton = true;
// Helper: check if (x, y) is inside a button (using .bg for new button assets)
function isInsideBtn(x, y, btn) {
var btnBg = btn.bg;
var globalX = btn.x;
var globalY = btn.y;
return x >= globalX - btnBg.width / 2 && x <= globalX + btnBg.width / 2 && y >= globalY - btnBg.height / 2 && y <= globalY + btnBg.height / 2;
}
// Save original game.down
var origGameDown = game.down;
game.down = function (x, y, obj) {
// Check attack button
if (isInsideBtn(x, y, attackBtn)) {
// Only allow attack if skill-based attack is enabled
if (canAttack && !attackRegistered) {
// Simulate a tap in the center of the hitmap (auto-perfect)
var centerX = hitmap.x;
attackColumn.x = centerX;
// Call original attack logic
origGameDown.call(game, centerX, hitmap.y, obj);
// After choosing attack, disable both attack and heal for this window
canAttack = false;
attackRegistered = true;
}
return;
}
// Check heal button
if (isInsideBtn(x, y, healBtn)) {
// Only allow heal if not at max HP and alive, and only during attack window
if (canAttack && !attackRegistered && soulAlive && soulHP < soulMaxHP) {
soulHP = Math.min(soulMaxHP, soulHP + 2);
updatePlayerHealthbar();
// Flash green for feedback
LK.effects.flashObject(playerHealthbarBox, 0x44ff44, 400);
// After choosing heal, disable both attack and heal for this window
canAttack = false;
attackRegistered = true;
// (Wave counter update removed)
// Count this as an undamaged wave (healing is only allowed after undamaged wave)
undamagedWaves++;
LK.setTimeout(function () {
if (!enemyDefeated) {
if (currentWave + 1 < waves.length) {
startWave(currentWave + 1);
} else {
waveSet += 1;
buildWavesForSet(waveSet);
startWave(0);
}
}
}, 900);
}
return;
}
// Otherwise, normal game logic
origGameDown.call(game, x, y, obj);
};
// Enemy hover effect: slow up and down tween loop
function enemyHoverUp() {
tween(enemy, {
y: ARENA_Y - 170
}, {
duration: 1200,
easing: tween.sineInOut,
onFinish: enemyHoverDown
});
}
function enemyHoverDown() {
tween(enemy, {
y: ARENA_Y - 230
}, {
duration: 1200,
easing: tween.sineInOut,
onFinish: enemyHoverUp
});
}
// Start at current position, tween down first
enemyHoverDown();
// Add soul (player)
var soul = new Soul();
soul.x = 2048 / 2;
soul.y = 2732 / 2;
game.addChild(soul);
// GUI: Timer (shows wave time left)
var timerTxt = new Text2('', {
size: 80,
fill: "#fff"
});
timerTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(timerTxt);
// (Wave counter removed)
// Game state
var bullets = [];
var currentWave = 0;
var waveTime = 0;
var waveTimer = 0;
var waveActive = false;
var soulAlive = true;
var dragSoul = false;
var dragOffsetX = 0;
var dragOffsetY = 0;
// Skill-based attack: moving column state
var attackColumn = LK.getAsset('arenaBorderBox', {
width: 40,
height: 100,
color: 0xffe066,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.85
});
attackColumn.x = hitmap.x - 120; // start left of hitmap
attackColumn.y = hitmap.y;
game.addChild(attackColumn);
// Column movement state
var columnDir = 1; // 1 = right, -1 = left
var columnSpeed = 16; // px per frame (increased speed)
attackColumn.lastX = attackColumn.x;
// Skill attack enabled only at end of wave
var canAttack = false;
var attackRegistered = false;
// Soul health
var soulMaxHP = 5;
var soulHP = soulMaxHP;
// Player healthbar (box style) under the arena
var playerHealthbarBg = LK.getAsset('playerHealthbarBg', {
anchorX: 0.5,
anchorY: 0.5
});
var playerHealthbarBox = LK.getAsset('playerHealthbarBox', {
anchorX: 0,
anchorY: 0.5
});
// Healthbar dimensions
var healthbarWidth = 900;
var healthbarHeight = 39.49;
var healthbarX = 2048 / 2 - healthbarWidth / 2;
var healthbarY = ARENA_Y + ARENA_H + 60; // 60px below arena
playerHealthbarBg.x = 2048 / 2;
playerHealthbarBg.y = healthbarY + healthbarHeight / 2;
playerHealthbarBox.x = healthbarX;
playerHealthbarBox.y = healthbarY + healthbarHeight / 2;
// Add HP label to the left of the healthbar
var hpLabelTxt = new Text2("HP", {
size: 64,
fill: "#fff"
});
hpLabelTxt.anchor.set(1, 0.5); // right align, vertically centered
// Place it 24px to the left of the healthbar
hpLabelTxt.x = healthbarX - 24;
hpLabelTxt.y = healthbarY + healthbarHeight / 2;
// Add to game
game.addChild(playerHealthbarBg);
game.addChild(playerHealthbarBox);
game.addChild(hpLabelTxt);
// Update function for healthbar
function updatePlayerHealthbar() {
// Clamp HP
var hp = Math.max(0, Math.min(soulHP, soulMaxHP));
// Set width proportional to HP
playerHealthbarBox.width = healthbarWidth * (hp / soulMaxHP);
}
updatePlayerHealthbar();
// Arena bounds (for clamping soul)
function clampSoul(x, y) {
var hw = 50,
hh = 50; // soul half size
var minX = ARENA_X + hw;
var maxX = ARENA_X + ARENA_W - hw;
var minY = ARENA_Y + hh;
var maxY = ARENA_Y + ARENA_H - hh;
return {
x: Math.max(minX, Math.min(maxX, x)),
y: Math.max(minY, Math.min(maxY, y))
};
}
// Touch/drag controls
function handleSoulMove(x, y, obj) {
if (!dragSoul || !soulAlive) return;
var pos = clampSoul(x - dragOffsetX, y - dragOffsetY);
soul.x = pos.x;
soul.y = pos.y;
}
game.move = function (x, y, obj) {
handleSoulMove(x, y, obj);
};
game.down = function (x, y, obj) {
// Prevent attack logic if heal button is pressed
if (isInsideBtn(x, y, healBtn)) {
// Only allow heal if not at max HP and alive, and only during attack window
if (canAttack && !attackRegistered && soulAlive && soulHP < soulMaxHP) {
soulHP = Math.min(soulMaxHP, soulHP + 2);
updatePlayerHealthbar();
// Flash green for feedback
LK.effects.flashObject(playerHealthbarBox, 0x44ff44, 400);
// After choosing heal, disable both attack and heal for this window
canAttack = false;
attackRegistered = true;
// Show a message and start next wave after a short delay
waveTxt.setText("Healed! Next wave...");
// Count this as an undamaged wave (healing is only allowed after undamaged wave)
undamagedWaves++;
LK.setTimeout(function () {
if (!enemyDefeated) {
if (currentWave + 1 < waves.length) {
startWave(currentWave + 1);
} else {
waveSet += 1;
buildWavesForSet(waveSet);
startWave(0);
}
}
}, 900);
}
return;
}
// Skill-based attack: if attack window is open, check for hit
if (canAttack && !attackRegistered) {
// Check if column is in the center of hitmap (allow some margin)
var centerX = hitmap.x;
var margin = 32;
if (Math.abs(attackColumn.x - centerX) <= margin) {
// Successful attack!
attackRegistered = true;
canAttack = false;
undamagedWaves++;
// Enemy takes a hit if enough undamaged waves
if (undamagedWaves >= requiredUndamagedWaves) {
enemyDefeated = true;
enemy.flash();
var slash = new SlashEffect();
game.addChild(slash);
slash.play(enemy);
LK.stopMusic();
LK.effects.flashScreen(0x00ff00, 1200);
LK.setTimeout(function () {
LK.showYouWin();
}, 1200);
return;
} else {
// Flash enemy for each undamaged wave
enemy.flash();
var slash = new SlashEffect();
game.addChild(slash);
slash.play(enemy);
}
// Start next wave after short delay
LK.setTimeout(function () {
if (!enemyDefeated) {
if (currentWave + 1 < waves.length) {
startWave(currentWave + 1);
} else {
waveSet += 1;
buildWavesForSet(waveSet);
startWave(0);
}
}
}, 700);
return;
} else {
// Missed! (optional: flash hitmap red)
tween(hitmap, {
color: 0xff4444
}, {
duration: 120,
onFinish: function onFinish() {
tween(hitmap, {
color: 0x00ffcc
}, {
duration: 180
});
}
});
// End the attack chance if player misses the center
canAttack = false;
attackRegistered = true;
// (Wave counter update removed)
// Optionally, start next wave after a short delay
LK.setTimeout(function () {
if (!enemyDefeated) {
if (currentWave + 1 < waves.length) {
startWave(currentWave + 1);
} else {
waveSet += 1;
buildWavesForSet(waveSet);
startWave(0);
}
}
}, 900);
return;
}
}
// Only start drag if inside soul
var dx = x - soul.x,
dy = y - soul.y;
if (dx * dx + dy * dy <= 60 * 60) {
dragSoul = true;
dragOffsetX = x - soul.x;
dragOffsetY = y - soul.y;
handleSoulMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
dragSoul = false;
};
// Waves definition
// Each wave: {duration, spawnFunc(tick, bulletsArr)}
// Difficulty scaling: each time all waves are completed, the next set is harder
var baseWaves = [
// Wave 1: Simple slow bullets from top
{
duration: 180,
spawnFunc: function spawnFunc(tick, arr, difficulty) {
// Unpredictable horizontal movement for the whole line
if (typeof spawnFunc._targetX === "undefined") {
spawnFunc._targetX = 0;
spawnFunc._currentX = 0;
spawnFunc._tween = null;
spawnFunc._lastTick = 0;
}
if (tick - spawnFunc._lastTick > 40 || Math.abs(spawnFunc._targetX - spawnFunc._currentX) < 5) {
// Pick a new random target X offset within [-180, 180]
var newTarget = (Math.random() * 2 - 1) * 180;
spawnFunc._lastTick = tick;
spawnFunc._targetX = newTarget;
// Tween smoothly to new target over 30-60 frames
if (spawnFunc._tween) spawnFunc._tween.stop();
spawnFunc._tween = tween(spawnFunc, {
_currentX: newTarget
}, {
duration: 30 + Math.floor(Math.random() * 30)
});
}
var lineXOffset = spawnFunc._currentX;
var fireRate = Math.max(8, 24 - 2 * difficulty); // faster at higher difficulty
if (tick % fireRate === 0) {
var speed = 8 + 1.5 * difficulty;
var count = 6 + Math.floor(difficulty / 2);
for (var i = 0; i < count; ++i) {
var b = new Bullet();
b.x = ARENA_X + 120 + i * ((ARENA_W - 240) / (count - 1)) + lineXOffset;
b.y = ARENA_Y + 30;
b.vx = 0;
b.vy = speed;
arr.push(b);
game.addChild(b);
}
}
}
},
// Wave 2: Bullets from left/right
{
duration: 210,
spawnFunc: function spawnFunc(tick, arr, difficulty) {
var lineOscAmp = 120 + 10 * difficulty;
var lineOscFreq = 0.025 + 0.002 * difficulty;
var lineYOffset = Math.sin(tick * lineOscFreq) * lineOscAmp;
var centerY1 = ARENA_Y + ARENA_H / 2 - 120;
var centerY2 = ARENA_Y + ARENA_H / 2 + 120;
var fireRate = Math.max(8, 24 - 2 * difficulty);
if (tick % fireRate === 0) {
var speed = 10 + 1.5 * difficulty;
var amplitude = 60 + 6 * difficulty;
var freq = 0.012 + 0.001 * difficulty;
// Left to right
var b1 = new Bullet();
b1.x = ARENA_X + 30;
b1.y = (tick % 2 === 0 ? centerY1 : centerY2) + lineYOffset;
b1.vx = speed;
b1.vy = amplitude * Math.sin((tick + 0) * freq);
b1._sPhase = (tick + 0) * freq;
b1._sDir = 1;
b1._baseY = b1.y;
b1._baseX = b1.x;
b1._t = 0;
b1._lineOscAmp = lineOscAmp;
b1._lineOscFreq = lineOscFreq;
b1._spawnTick = tick;
b1.update = function () {
this.x += this.vx;
this._t += 1;
var lineYOffset = Math.sin((this._spawnTick + this._t) * this._lineOscFreq) * this._lineOscAmp;
this.y = this._baseY + amplitude * Math.sin((this.x - this._baseX) * freq) + (lineYOffset - Math.sin(this._spawnTick * this._lineOscFreq) * this._lineOscAmp);
};
arr.push(b1);
game.addChild(b1);
// Right to left
var b2 = new Bullet();
b2.x = ARENA_X + ARENA_W - 30;
b2.y = (tick % 2 === 0 ? centerY2 : centerY1) + lineYOffset;
b2.vx = -speed;
b2.vy = amplitude * Math.sin((tick + 60) * freq);
b2._sPhase = (tick + 60) * freq;
b2._sDir = -1;
b2._baseY = b2.y;
b2._baseX = b2.x;
b2._t = 0;
b2._lineOscAmp = lineOscAmp;
b2._lineOscFreq = lineOscFreq;
b2._spawnTick = tick;
b2.update = function () {
this.x += this.vx;
this._t += 1;
var lineYOffset = Math.sin((this._spawnTick + this._t) * this._lineOscFreq) * this._lineOscAmp;
this.y = this._baseY + amplitude * Math.sin((this.x - this._baseX) * freq) + (lineYOffset - Math.sin(this._spawnTick * this._lineOscFreq) * this._lineOscAmp);
};
arr.push(b2);
game.addChild(b2);
}
}
},
// Wave 3: Diagonal bullets from corners
{
duration: 240,
spawnFunc: function spawnFunc(tick, arr, difficulty) {
var fireRate = Math.max(8, 36 - 2 * difficulty);
if (tick % fireRate === 0) {
var speed = 7.5 + 1.2 * difficulty;
var diag = speed / Math.sqrt(2);
// Top-left
var b1 = new Bullet();
b1.x = ARENA_X + 40;
b1.y = ARENA_Y + 40;
b1.vx = diag;
b1.vy = diag;
arr.push(b1);
game.addChild(b1);
// Top-right
var b2 = new Bullet();
b2.x = ARENA_X + ARENA_W - 40;
b2.y = ARENA_Y + 40;
b2.vx = -diag;
b2.vy = diag;
arr.push(b2);
game.addChild(b2);
// Bottom-left
var b3 = new Bullet();
b3.x = ARENA_X + 40;
b3.y = ARENA_Y + ARENA_H - 40;
b3.vx = diag;
b3.vy = -diag;
arr.push(b3);
game.addChild(b3);
// Bottom-right
var b4 = new Bullet();
b4.x = ARENA_X + ARENA_W - 40;
b4.y = ARENA_Y + ARENA_H - 40;
b4.vx = -diag;
b4.vy = -diag;
arr.push(b4);
game.addChild(b4);
// At higher difficulty, add a random diagonal bullet
if (difficulty >= 2) {
var b5 = new Bullet();
var side = Math.floor(Math.random() * 4);
if (side === 0) {
b5.x = ARENA_X + 40;
b5.y = ARENA_Y + 40;
b5.vx = diag * 1.2;
b5.vy = diag * 1.2;
}
if (side === 1) {
b5.x = ARENA_X + ARENA_W - 40;
b5.y = ARENA_Y + 40;
b5.vx = -diag * 1.2;
b5.vy = diag * 1.2;
}
if (side === 2) {
b5.x = ARENA_X + 40;
b5.y = ARENA_Y + ARENA_H - 40;
b5.vx = diag * 1.2;
b5.vy = -diag * 1.2;
}
if (side === 3) {
b5.x = ARENA_X + ARENA_W - 40;
b5.y = ARENA_Y + ARENA_H - 40;
b5.vx = -diag * 1.2;
b5.vy = -diag * 1.2;
}
arr.push(b5);
game.addChild(b5);
}
}
}
},
// Wave 4: Random rain
{
duration: 210,
spawnFunc: function spawnFunc(tick, arr, difficulty) {
var fireRate = Math.max(2, 10 - Math.floor(difficulty / 2));
if (tick % fireRate === 0) {
var minSpeed = 12 + 1.2 * difficulty,
maxSpeed = 16 + 1.5 * difficulty;
var count = 1 + Math.floor(difficulty / 2);
for (var i = 0; i < count; ++i) {
var b = new Bullet();
b.x = ARENA_X + 80 + Math.floor(Math.random() * (ARENA_W - 160));
b.y = ARENA_Y + 30;
b.vx = 0;
b.vy = minSpeed + Math.random() * (maxSpeed - minSpeed);
arr.push(b);
game.addChild(b);
}
}
}
},
// Wave 5: Spiral (center out)
{
duration: 270,
spawnFunc: function spawnFunc(tick, arr, difficulty) {
var fireRate = Math.max(3, 12 - Math.floor(difficulty / 2));
if (tick % fireRate === 0) {
var speed = 9 + 1.2 * difficulty;
var angle = tick / fireRate * (0.5 + 0.1 * difficulty);
var count = 4 + Math.floor(difficulty / 2);
for (var i = 0; i < count; ++i) {
var a = angle + i * (2 * Math.PI / count);
var b = new Bullet();
b.x = 2048 / 2;
b.y = 2732 / 2;
b.vx = Math.cos(a) * speed;
b.vy = Math.sin(a) * speed;
arr.push(b);
game.addChild(b);
}
}
}
},
// Wave 6: New - Cross barrages (hard)
{
duration: 240,
spawnFunc: function spawnFunc(tick, arr, difficulty) {
var fireRate = Math.max(6, 24 - 2 * difficulty);
if (tick % fireRate === 0) {
var speed = 10 + 1.5 * difficulty;
// Horizontal
for (var i = 0; i < 3 + Math.floor(difficulty / 2); ++i) {
var b = new Bullet();
b.x = ARENA_X + 80 + i * ((ARENA_W - 160) / (2 + Math.floor(difficulty / 2)));
b.y = ARENA_Y + 30;
b.vx = 0;
b.vy = speed;
arr.push(b);
game.addChild(b);
}
// Vertical
for (var i = 0; i < 3 + Math.floor(difficulty / 2); ++i) {
var b = new Bullet();
b.x = ARENA_X + 30;
b.y = ARENA_Y + 80 + i * ((ARENA_H - 160) / (2 + Math.floor(difficulty / 2)));
b.vx = speed;
b.vy = 0;
arr.push(b);
game.addChild(b);
}
for (var i = 0; i < 3 + Math.floor(difficulty / 2); ++i) {
var b = new Bullet();
b.x = ARENA_X + ARENA_W - 30;
b.y = ARENA_Y + 80 + i * ((ARENA_H - 160) / (2 + Math.floor(difficulty / 2)));
b.vx = -speed;
b.vy = 0;
arr.push(b);
game.addChild(b);
}
}
}
},
// Wave 7: New - Spiral rain (even easier)
{
duration: 300,
spawnFunc: function spawnFunc(tick, arr, difficulty) {
var fireRate = Math.max(5, 18 - Math.floor(difficulty / 2)); // much slower fire rate
if (tick % fireRate === 0) {
var speed = 5 + 0.7 * difficulty; // even slower speed
var count = 3 + Math.floor(difficulty / 4); // even fewer bullets
var baseAngle = tick / fireRate * 0.13; // even slower spiral
for (var i = 0; i < count; ++i) {
var a = baseAngle + i * (2 * Math.PI / count);
var b = new Bullet();
b.x = 2048 / 2 + Math.cos(a) * 400;
b.y = 2732 / 2 + Math.sin(a) * 400;
b.vx = Math.cos(a) * -speed;
b.vy = Math.sin(a) * -speed;
arr.push(b);
game.addChild(b);
}
}
}
}];
// The actual waves array is generated for each "cycle" of all waves, increasing difficulty each time
var waves = [];
var waveSet = 0; // how many times all waves have been completed
// Track undamaged waves in a row
var undamagedWaves = 0;
var requiredUndamagedWaves = 5;
var waveWasDamaged = false;
var enemyDefeated = false;
function buildWavesForSet(set) {
// Create a shallow copy of baseWaves and shuffle it to randomize order
var shuffled = [];
for (var i = 0; i < baseWaves.length; ++i) shuffled.push(baseWaves[i]);
// Fisher-Yates shuffle
for (var i = shuffled.length - 1; i > 0; --i) {
var j = Math.floor(Math.random() * (i + 1));
var temp = shuffled[i];
shuffled[i] = shuffled[j];
shuffled[j] = temp;
}
// Ensure Spiral Rain (wave 7, index 6 in baseWaves) is not first
// Find the spiral rain wave in shuffled
var spiralRainIdx = -1;
for (var i = 0; i < shuffled.length; ++i) {
if (shuffled[i] === baseWaves[6]) {
spiralRainIdx = i;
break;
}
}
if (spiralRainIdx === 0 && shuffled.length > 1) {
// Swap with a random other wave (not 0)
var swapWith = 1 + Math.floor(Math.random() * (shuffled.length - 1));
var temp = shuffled[0];
shuffled[0] = shuffled[swapWith];
shuffled[swapWith] = temp;
}
waves = [];
for (var i = 0; i < shuffled.length; ++i) {
(function (baseWave, idx) {
waves.push({
duration: Math.max(120, Math.floor(baseWave.duration * Math.max(0.7, 1 - set * 0.08))),
// slightly shorter at higher sets
spawnFunc: function spawnFunc(tick, arr) {
baseWave.spawnFunc(tick, arr, set);
}
});
})(shuffled[i], i);
}
}
buildWavesForSet(waveSet);
// Start first wave
function startWave(idx) {
currentWave = idx;
waveTime = 0;
waveActive = true;
waveTimer = 0;
waveWasDamaged = false;
// Remove all bullets
for (var i = bullets.length - 1; i >= 0; --i) {
bullets[i].destroy();
bullets.splice(i, 1);
}
// Remove all warning blinkers
if (typeof warningBlinkers !== "undefined" && warningBlinkers.length) {
for (var i = warningBlinkers.length - 1; i >= 0; --i) {
warningBlinkers[i].destroy();
warningBlinkers.splice(i, 1);
}
}
warningBlinkers = [];
// (Wave counter update removed)
// Reset HP and healthbar if starting first wave (new game)
if (currentWave === 0) {
soulHP = soulMaxHP;
soulAlive = true;
updatePlayerHealthbar();
// Restore soul sprite if needed
if (soul.children && soul.children.length > 0) {
var firstChild = soul.children[0];
if (firstChild.assetId === 'warning') {
soul.removeChild(firstChild);
var newSoulSprite = LK.getAsset('soul', {
anchorX: 0.5,
anchorY: 0.5
});
soul.addChildAt(newSoulSprite, 0);
// Play sound on soul texture change (revive)
LK.getSound('shoot').play();
}
}
}
}
startWave(0);
// Play background music at game start
LK.playMusic('music');
// Main update loop
game.update = function () {
if (!soulAlive) return;
// Wave logic
if (waveActive) {
var wave = waves[currentWave];
// Only spawn bullets after 1.5 seconds (90 frames)
if (waveTime === 1) {
// On first frame of wave, create warning blinkers for all bullets that will spawn at t=0
if (typeof warningBlinkers === "undefined") warningBlinkers = [];
var previewBullets = [];
// Simulate spawnFunc at t=0, but don't add to game, just get positions
var fakeArr = [];
// For each wave, we need to know where bullets will spawn at t=0
// We'll use a proxy to collect spawn positions
var addChildOrig = game.addChild;
game.addChild = function (obj) {
fakeArr.push(obj);
return obj;
};
wave.spawnFunc(0, []);
game.addChild = addChildOrig;
// Now, for each bullet in fakeArr, create a warning blinker at its x/y
for (var i = 0; i < fakeArr.length; ++i) {
var b = fakeArr[i];
if (typeof b.x === "number" && typeof b.y === "number") {
var blinker = new WarningBlinker();
blinker.x = b.x;
blinker.y = b.y;
warningBlinkers.push(blinker);
game.addChild(blinker);
}
}
}
if (waveTime === 90) {
// Remove all warning blinkers at bullet spawn time
if (typeof warningBlinkers !== "undefined" && warningBlinkers.length) {
for (var i = warningBlinkers.length - 1; i >= 0; --i) {
warningBlinkers[i].destroy();
warningBlinkers.splice(i, 1);
}
}
}
if (waveTime >= 90) {
wave.spawnFunc(waveTime - 90, bullets);
}
waveTime++;
// Update timer GUI
var tleft = Math.max(0, Math.ceil((wave.duration - waveTime) / 60));
timerTxt.setText("Time: " + tleft + "s");
// End wave?
if (waveTime >= wave.duration) {
waveActive = false;
// Remove all bullets after short delay
LK.setTimeout(function () {
for (var i = bullets.length - 1; i >= 0; --i) {
bullets[i].destroy();
bullets.splice(i, 1);
}
// Only allow enemy defeat if not already defeated
if (!enemyDefeated) {
// Skill-based attack: enable attack window if player was not hit this wave
if (!waveWasDamaged) {
canAttack = true;
attackRegistered = false;
// Show a visual cue (flash hitmap)
// No alpha tween for hitmap, keep at 1.0
// (Wave counter update removed)
// Pause next wave until attack or heal is registered
return;
} else {
// Do not reset undamagedWaves if hit; keep the counter unchanged
// No healing is performed automatically here
}
}
// Next wave or infinite loop
if (!enemyDefeated) {
if (currentWave + 1 < waves.length) {
startWave(currentWave + 1);
} else {
// All waves in this set completed, increase difficulty and repeat with harder waves!
waveSet += 1;
buildWavesForSet(waveSet);
startWave(0);
}
}
}, 600);
}
}
// Update moving column for skill-based attack
if (canAttack && !attackRegistered) {
// Move column left/right within hitmap bounds
var leftEdge = hitmap.x - hitmap.width / 2 + attackColumn.width / 2 + 8;
var rightEdge = hitmap.x + hitmap.width / 2 - attackColumn.width / 2 - 8;
attackColumn.x += columnDir * columnSpeed;
if (attackColumn.x <= leftEdge) {
attackColumn.x = leftEdge;
columnDir = 1;
}
if (attackColumn.x >= rightEdge) {
attackColumn.x = rightEdge;
columnDir = -1;
}
// Store lastX for event detection
attackColumn.lastX = attackColumn.x;
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; --i) {
var b = bullets[i];
b.update();
// Remove if out of arena (immediately on leaving the border)
if (b.x < ARENA_X || b.x > ARENA_X + ARENA_W || b.y < ARENA_Y || b.y > ARENA_Y + ARENA_H) {
b.destroy();
bullets.splice(i, 1);
continue;
}
// Collision with soul
var dx = b.x - soul.x,
dy = b.y - soul.y;
if (dx * dx + dy * dy < 45 * 45) {
// Hit!
soul.flash();
soulHP -= 1;
updatePlayerHealthbar();
// Mark this wave as damaged
waveWasDamaged = true;
// Stop music only when soul dies (on 6th hit)
if (soulHP <= 0 && soulAlive) {
LK.stopMusic();
}
// Remove bullet on hit
b.destroy();
bullets.splice(i, 1);
if (soulHP <= 0 && soulAlive) {
soulAlive = false;
// Delay soul's texture asset change to 2 seconds after dying
LK.setTimeout(function () {
if (soul.children && soul.children.length > 0) {
var deadSprite = LK.getAsset('warning', {
anchorX: 0.5,
anchorY: 0.5
});
var oldSprite = soul.children[0];
deadSprite.x = oldSprite.x;
deadSprite.y = oldSprite.y;
soul.removeChild(oldSprite);
soul.addChildAt(deadSprite, 0);
// Play sound on soul texture change (death)
LK.getSound('shoot').play();
// Blast into particles 1.35 sec after texture change
LK.setTimeout(function () {
// Remove soul sprite (if still present)
if (soul.children && soul.children.length > 0) {
soul.removeChild(soul.children[0]);
}
// Create 8 particles in a circle (reduced from 18)
var numParticles = 8;
var radius = 60;
for (var i = 0; i < numParticles; ++i) {
var angle = 2 * Math.PI * i / numParticles;
var px = soul.x + Math.cos(angle) * 0;
var py = soul.y + Math.sin(angle) * 0;
var particle = new BlastParticle();
particle.x = px;
particle.y = py;
// Give each particle a random color (yellow/orange/red)
var colors = [0xffe066, 0xffb347, 0xff4444, 0xffe0a0, 0xffd700];
var color = colors[Math.floor(Math.random() * colors.length)];
if (particle.children && particle.children.length > 0) {
particle.children[0].tint = color;
}
// Set velocity outward (slow motion: reduce speed)
var speed = (13 + Math.random() * 4) * 0.35;
particle.vx = Math.cos(angle) * speed;
particle.vy = Math.sin(angle) * speed;
// Add gravity property for blast particles (slow motion: reduce gravity)
particle._gravity = (0.5 + Math.random() * 0.15) * 0.25;
// Fade out and destroy after 0.7s
tween(particle, {
alpha: 0
}, {
duration: 700,
onFinish: function () {
// Only destroy if already off-screen, otherwise wait for off-screen
if (this.x < ARENA_X - 100 || this.x > ARENA_X + ARENA_W + 100 || this.y < ARENA_Y - 100 || this.y > ARENA_Y + ARENA_H + 100) {
this.destroy();
} else {
// Wait until particle is far off-screen, then destroy
var p = this;
p._offscreenCheck = function () {
if (p.x < ARENA_X - 100 || p.x > ARENA_X + ARENA_W + 100 || p.y < ARENA_Y - 100 || p.y > ARENA_Y + ARENA_H + 100) {
p.destroy();
// Remove from update loop
p.update = function () {};
}
};
// Wrap the original update to check for offscreen
var origUpdate = p.update;
p.update = function () {
origUpdate && origUpdate.call(p);
p._offscreenCheck();
};
}
}.bind(particle)
});
game.addChild(particle);
}
}, 1350);
}
}, 2000);
LK.effects.flashScreen(0xff0000, 800);
LK.setTimeout(function () {
LK.showGameOver();
}, 5000);
break;
} else {
// Flash screen for hit, but not game over
LK.effects.flashScreen(0xff0000, 300);
}
}
}
};
// Initial GUI text
timerTxt.setText("Time: ");