User prompt
increase the volume of the s shape
User prompt
make the bullets go in S shpae in wave 2
User prompt
the speeds of buullets vary for each wave
User prompt
make the bullets disappear immiedaitly after going outside of the arena
User prompt
add an ouutline to arenaBorderf
Code edit (1 edits merged)
Please save this source code
User prompt
Soul Dodge: Heart of Battle
Initial prompt
make a game like UNDERTALE
/**** * 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: ");