/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // CircleLaserEnemy: A circle that appears and fires a thick laser once var CircleLaserEnemy = Container.expand(function () { var self = Container.call(this); // Attach a green circle asset var circle = self.attachAsset('centerCircle', { width: 120, height: 120, color: 0x00ff99, shape: 'ellipse', anchorX: 0.5, anchorY: 0.5 }); // Laser graphics (thick rectangle, hidden at first) var laser = self.attachAsset('boxBorder', { width: 40, height: boxHeight + 200, color: 0xff2222, shape: 'box', anchorX: 0.5, anchorY: 0 }); laser.visible = false; laser.y = 60; // Start just below the circle // State self.laserFired = false; self.laserDuration = 60; // 1 second self.laserTimer = 0; // For collision detection self.laserActive = false; // Called every tick self.update = function () { // Animate circle (optional: pulse) circle.scaleX = 1.0 + 0.08 * Math.sin(LK.ticks / 8); circle.scaleY = 1.0 + 0.08 * Math.sin(LK.ticks / 8); // Fire laser after short delay if (!self.laserFired) { if (!self._waitTicks) self._waitTicks = 0; self._waitTicks++; if (self._waitTicks === 40) { // Fire! laser.visible = true; self.laserActive = true; self.laserFired = true; self.laserTimer = 0; // Optional: flash effect LK.effects.flashObject(laser, 0xffffff, 120); } } // Laser active if (self.laserActive) { self.laserTimer++; // Check collision with soul // Laser is a vertical thick bar, check if soul overlaps horizontally var laserGlobalX = self.x; var laserLeft = laserGlobalX - laser.width / 2; var laserRight = laserGlobalX + laser.width / 2; var soulLeft = soul.x - soul.radius; var soulRight = soul.x + soul.radius; var soulTop = soul.y - soul.radius; var soulBottom = soul.y + soul.radius; var laserTop = self.y + laser.y; var laserBottom = laserTop + laser.height; // Only check if soul is inside the box if (invincibleTicks === 0 && soulRight > laserLeft && soulLeft < laserRight && soulBottom > laserTop && soulTop < laserBottom) { // Damage player hp--; soul.flash(); invincibleTicks = 60; hpText.setText('HP: ' + hp); LK.effects.flashObject(soul, 0xffffff, 300); if (hp <= 0) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); return; } } // Laser lasts for laserDuration ticks if (self.laserTimer > self.laserDuration) { self.laserActive = false; laser.visible = false; } } }; return self; }); // Enemy (for future expansion, currently just a box at the top) var Enemy = Container.expand(function () { var self = Container.call(this); var enemyBox = self.attachAsset('enemyBox', { width: 400, height: 260, // Increased height for a longer (taller) enemy box, width unchanged color: 0x2222ff, shape: 'box', anchorX: 0.5, anchorY: 0.5 }); return self; }); // Enemy Bullet var EnemyBullet = Container.expand(function () { var self = Container.call(this); // Attach bullet asset (white circle) var bullet = self.attachAsset('enemyBullet', { width: 60, height: 60, color: 0xffffff, shape: 'ellipse', anchorX: 0.5, anchorY: 0.5 }); // Movement properties self.vx = 0; self.vy = 0; // For pattern logic self.pattern = null; // Always initialize patternTick to 0 for each bullet self.patternTick = 0; // Called every tick self.update = function () { if (self.pattern) { self.pattern(self, self.patternTick); // Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") { self.patternTick = 0; } else { // Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") { self.patternTick = 0; } else { // Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") { self.patternTick = 1; } else { // Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") { self.patternTick = 1; } else { // Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") { self.patternTick = 1; } else { // Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") { self.patternTick = 1; } else { if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") { self.patternTick = 1; } else { // Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") { self.patternTick = 1; } else { // Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") { self.patternTick = 1; } else { // Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) at line 91 try { var n = Number(self.patternTick); if (typeof n === "number" && !isNaN(n) && !(n && typeof n.then === "function")) { // Defensive: ensure n is a number and not a thenable before incrementing if (typeof n === "number" && !isNaN(n) && !(n && typeof n.then === "function")) { // Defensive: ensure n is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) at line 97 if (typeof n === "number" && !isNaN(n) && !(n && typeof n.then === "function")) { self.patternTick = n + 1; } else { self.patternTick = 1; } } else { self.patternTick = 1; } } else { self.patternTick = 1; } } catch (e) { self.patternTick = 1; } } } } } } } } } } } else { // Defensive: ensure self.vx is a number before using if (typeof self.vx === "number") { self.x += self.vx; } // Defensive: ensure self.vy is a number and not a thenable before using if (typeof self.vy === "number" && !(_typeof6(self.vy) === "object" && self.vy !== null && typeof self.vy.then === "function")) { self.y += self.vy; } } }; return self; }); // Freeze Bullet (special: only damages if player is not moving) var FreezeBullet = Container.expand(function () { var self = Container.call(this); // Attach freeze bullet asset (blue ellipse) var bullet = self.attachAsset('freezeBullet', { width: 60, height: 60, color: 0x3399ff, shape: 'ellipse', anchorX: 0.5, anchorY: 0.5 }); // Movement properties self.vx = 0; self.vy = 0; // For pattern logic self.pattern = null; self.patternTick = 0; self.update = function () { if (self.pattern) { self.pattern(self, self.patternTick); if (typeof self.patternTick !== "number" || _typeof5(self.patternTick) === "object" && self.patternTick !== null && typeof self.patternTick.then === "function") { self.patternTick = 0; } if (typeof self.patternTick === "number" && !(_typeof5(self.patternTick) === "object" && self.patternTick !== null && typeof self.patternTick.then === "function")) { self.patternTick += 1; } else { self.patternTick = 1; } } else { if (typeof self.vx === "number") { self.x += self.vx; } if (typeof self.vy === "number" && !(_typeof2(self.vy) === "object" && self.vy !== null && typeof self.vy.then === "function")) { self.y += self.vy; } } }; return self; }); // Heart-shaped Soul (Player) var Soul = Container.expand(function () { var self = Container.call(this); // Attach heart asset (ellipse, red, scaled to look like a heart) // Since we can't draw a heart, use an ellipse and scale it to look heart-like var heart = self.attachAsset('soulHeart', { width: 60, height: 50, color: 0xff2d55, shape: 'ellipse', anchorX: 0.5, anchorY: 0.5 }); // Squash to make it more heart-like heart.scaleY = 1.1; heart.scaleX = 1.2; // For hit feedback self.flash = function () { tween(heart, { tint: 0xffffff }, { duration: 80, onFinish: function onFinish() { tween(heart, { tint: 0xff2d55 }, { duration: 120 }); } }); }; // For movement bounds self.radius = 30; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Game area (the "box" where the soul can move) // Tween plugin for movement and effects function _typeof12(o) { "@babel/helpers - typeof"; return _typeof12 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof12(o); } function _typeof11(o) { "@babel/helpers - typeof"; return _typeof11 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof11(o); } function _typeof10(o) { "@babel/helpers - typeof"; return _typeof10 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof10(o); } function _typeof1(o) { "@babel/helpers - typeof"; return _typeof1 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof1(o); } function _typeof0(o) { "@babel/helpers - typeof"; return _typeof0 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof0(o); } function _typeof9(o) { "@babel/helpers - typeof"; return _typeof9 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof9(o); } function _typeof8(o) { "@babel/helpers - typeof"; return _typeof8 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof8(o); } function _typeof7(o) { "@babel/helpers - typeof"; return _typeof7 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof7(o); } function _typeof6(o) { "@babel/helpers - typeof"; return _typeof6 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof6(o); } function _typeof5(o) { "@babel/helpers - typeof"; return _typeof5 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof5(o); } function _typeof4(o) { "@babel/helpers - typeof"; return _typeof4 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof4(o); } function _typeof3(o) { "@babel/helpers - typeof"; return _typeof3 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof3(o); } function _typeof2(o) { "@babel/helpers - typeof"; return _typeof2 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof2(o); } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } var boxWidth = 900; var boxHeight = 900; var boxX = (2048 - boxWidth) / 2; var boxY = 2732 - boxHeight - 200; // Draw the box (white border, transparent fill) var boxBorder = LK.getAsset('boxBorder', { width: boxWidth, height: boxHeight, color: 0xffffff, shape: 'box', anchorX: 0, anchorY: 0 }); boxBorder.alpha = 0.15; boxBorder.x = boxX; boxBorder.y = boxY; game.addChild(boxBorder); // Add a big enemy (bi) at the top of the game area var enemy = new Enemy(); enemy.x = 2048 / 2; enemy.y = 2732 / 2 - 120; // Move enemy a little up from center enemy.scaleX = 2.2; enemy.scaleY = 2.2; game.addChild(enemy); // Enemy health bar var enemyMaxHP = 20; var enemyHP = enemyMaxHP; var enemyHealthBarWidth = 700; var enemyHealthBarHeight = 60; var enemyHealthBarBg = LK.getAsset('boxBorder', { width: enemyHealthBarWidth, height: enemyHealthBarHeight, color: 0x333333, shape: 'box', anchorX: 0.5, anchorY: 0.5 }); // Place health bar background further below the enemy enemyHealthBarBg.x = enemy.x; enemyHealthBarBg.y = enemy.y + 260; // Move health bar further down to match new enemy position game.addChild(enemyHealthBarBg); var enemyHealthBar = LK.getAsset('boxBorder', { width: enemyHealthBarWidth - 8, height: enemyHealthBarHeight - 2, color: 0xFFFF00, // yellow shape: 'box', anchorX: 0.5, anchorY: 0.5 }); enemyHealthBar.x = enemy.x; enemyHealthBar.y = enemy.y + 260; game.addChild(enemyHealthBar); // Add soul (player) at center of box var soul = new Soul(); soul.x = boxX + boxWidth / 2; soul.y = boxY + boxHeight / 2; game.addChild(soul); // Health system var maxHP = 5; var hp = maxHP; // Score (number of waves survived) var score = 0; // Bullets array var enemyBullets = []; // GUI: HP display (top right, avoid top left) var hpText = new Text2('HP: ' + hp, { size: 90, fill: 0xFF2D55 }); hpText.anchor.set(1, 0); LK.gui.topRight.addChild(hpText); // GUI: Score display (top center) var scoreText = new Text2('WAVES: 0', { size: 90, fill: 0xFF2D55 }); scoreText.anchor.set(0.5, 0); LK.gui.top.addChild(scoreText); // Touch controls for soul movement var draggingSoul = false; var dragOffsetX = 0; var dragOffsetY = 0; // Helper: clamp soul inside box function clampSoulPosition(x, y) { var r = soul.radius; var minX = boxX + r; var maxX = boxX + boxWidth - r; var minY = boxY + r; var maxY = boxY + boxHeight - r; if (x < minX) x = minX; if (x > maxX) x = maxX; if (y < minY) y = minY; if (y > maxY) y = maxY; return { x: x, y: y }; } // Touch down: start dragging if inside soul game.down = function (x, y, obj) { // Only allow drag if inside soul var dx = x - soul.x; var dy = y - soul.y; if (dx * dx + dy * dy <= soul.radius * soul.radius) { draggingSoul = true; dragOffsetX = soul.x - x; dragOffsetY = soul.y - y; } }; // Touch up: stop dragging game.up = function (x, y, obj) { draggingSoul = false; }; // Touch move: move soul game.move = function (x, y, obj) { // Always follow the mouse/touch, clamped to the box var pos = clampSoulPosition(x, y); soul.x = pos.x; soul.y = pos.y; }; // Bullet patterns // Multi-attack per wave support var waveAttacks = [ // Each entry is an array of attack functions for that wave // Wave 0: 2 attacks: radial burst, then spiral [function () { // Radial burst var count = 8; var speed = 3.5; for (var i = 0; i < count; i++) { var angle = Math.PI * 2 * i / count; var b = new EnemyBullet(); b.x = 2048 / 2; b.y = boxY + 40; b.vx = Math.cos(angle) * speed; b.vy = Math.sin(angle) * speed; enemyBullets.push(b); game.addChild(b); } }, function () { // Spiral var spiralCount = 12; var spiralSpawnY = boxY + 180; var spiralSpeed = 3.2; for (var i = 0; i < spiralCount; i++) { var b = new EnemyBullet(); b.x = 2048 / 2; b.y = spiralSpawnY; var baseAngle = Math.PI * 2 * i / spiralCount; b.pattern = function (angleOffset) { return function (self, t) { var speed = spiralSpeed; var angle = angleOffset + t * 0.07; self.x = 2048 / 2 + Math.cos(angle) * speed * t; self.y = spiralSpawnY + Math.sin(angle) * speed * t; }; }(baseAngle); enemyBullets.push(b); game.addChild(b); } }], // Wave 1: Sine wave, then zigzag [function () { // Sine wave bullets from both sides var sineCount = 4; var sineSpeed = 4.2; for (var i = 0; i < sineCount; i++) { var b = new EnemyBullet(); b.x = boxX + 40; b.y = boxY + 120 + i * 120; b.vx = sineSpeed; b.vy = 0; b.pattern = function (self, t) { self.x += self.vx; self.y += Math.sin((self.x + t * 2) / 80) * 5; }; enemyBullets.push(b); game.addChild(b); } for (var i = 0; i < sineCount; i++) { var b = new EnemyBullet(); b.x = boxX + boxWidth - 40; b.y = boxY + 120 + i * 120; b.vx = -sineSpeed; b.vy = 0; b.pattern = function (self, t) { self.x += self.vx; self.y += Math.sin((self.x + t * 2) / 80) * 5; }; enemyBullets.push(b); game.addChild(b); } }, function () { // Zigzag bullets from above var zigzagCount = 8; var zigzagSpeed = 4.5; for (var i = 0; i < zigzagCount; i++) { var b = new EnemyBullet(); b.x = boxX + 80 + (boxWidth - 160) * (i / zigzagCount); b.y = boxY + 20; b.vx = 0; b.vy = zigzagSpeed; b.pattern = function (self, t) { self.y += self.vy; self.x += Math.sin(self.y / 60) * 3; }; enemyBullets.push(b); game.addChild(b); } }], // Wave 2: Spiral, then aimed shots [function () { // Spiral pattern from center var spiralCount = 18; var spiralSpawnY = boxY + 180; var spiralSpeed = 4.2; for (var i = 0; i < spiralCount; i++) { var b = new EnemyBullet(); b.x = 2048 / 2; b.y = spiralSpawnY; var baseAngle = Math.PI * 2 * i / spiralCount; b.pattern = function (angleOffset) { return function (self, t) { var speed = spiralSpeed; var angle = angleOffset + t * 0.07; self.x = 2048 / 2 + Math.cos(angle) * speed * t; self.y = spiralSpawnY + Math.sin(angle) * speed * t; }; }(baseAngle); enemyBullets.push(b); game.addChild(b); } }, function () { // Aimed shots at the player (soul), with some spread var shots = 7; var aimedSpeed = 5.2; for (var i = 0; i < shots; i++) { var b = new EnemyBullet(); b.x = 2048 / 2; b.y = boxY + 40; var dx = soul.x - b.x; var dy = soul.y - b.y; var angle = Math.atan2(dy, dx) + (i - (shots - 1) / 2) * 0.12; b.vx = Math.cos(angle) * aimedSpeed; b.vy = Math.sin(angle) * aimedSpeed; enemyBullets.push(b); game.addChild(b); } }], // Wave 3: Freeze bullets, then CircleLaserEnemy [function () { // Freeze bullets (special blue bullets) var freezeCount = 7; var freezeSpeed = 4.5; for (var i = 0; i < freezeCount; i++) { var b = new FreezeBullet(); b.x = boxX + 80 + (boxWidth - 160) * (i / (freezeCount - 1)); b.y = boxY + 20; b.vx = 0; b.vy = freezeSpeed; enemyBullets.push(b); game.addChild(b); } // Add freeze bullets from the left side var sideFreezeCount = 3; var sideFreezeSpeed = 4.2; for (var i = 0; i < sideFreezeCount; i++) { var fbLeft = new FreezeBullet(); fbLeft.x = boxX + 20; fbLeft.y = boxY + 120 + (boxHeight - 240) * (i / (sideFreezeCount - 1)); fbLeft.vx = sideFreezeSpeed; fbLeft.vy = 0; enemyBullets.push(fbLeft); game.addChild(fbLeft); } // Add freeze bullets from the right side for (var i = 0; i < sideFreezeCount; i++) { var fbRight = new FreezeBullet(); fbRight.x = boxX + boxWidth - 20; fbRight.y = boxY + 120 + (boxHeight - 240) * (i / (sideFreezeCount - 1)); fbRight.vx = -sideFreezeSpeed; fbRight.vy = 0; enemyBullets.push(fbRight); game.addChild(fbRight); } }, function () { // Multiple CircleLaserEnemy with different angles var numLasers = 3; var baseY = boxY + 100; var centerX = 2048 / 2; var radius = 220; for (var i = 0; i < numLasers; i++) { var angle = Math.PI * 2 * i / numLasers; var cle = new CircleLaserEnemy(); cle.x = centerX + Math.cos(angle) * radius; cle.y = baseY + Math.sin(angle) * radius * 0.5; cle._laserAngle = angle; (function (cle, angle) { var origUpdate = cle.update; cle.update = function () { origUpdate.call(cle); if (cle.children && cle.children.length > 1) { var laser = cle.children[1]; laser.rotation = angle; laser.x = Math.cos(angle) * 60; laser.y = Math.sin(angle) * 60; } }; })(cle, angle); game.addChild(cle); enemyBullets.push(cle); } }]]; // For higher waves, repeat and increase difficulty function getWaveAttacks(waveNum) { if (waveNum < waveAttacks.length) { return waveAttacks[waveNum]; } // For higher waves, repeat and add a third attack var base = waveAttacks[waveNum % waveAttacks.length]; // For higher waves, repeat and add a third attack var extraAttack = function extraAttack() { // Harder radial burst var count = 12 + Math.floor(waveNum / 2); var speed = 4.5 + waveNum * 0.12; for (var i = 0; i < count; i++) { var angle = Math.PI * 2 * i / count; var b = new EnemyBullet(); b.x = 2048 / 2; b.y = boxY + 40; b.vx = Math.cos(angle) * speed; b.vy = Math.sin(angle) * speed; enemyBullets.push(b); game.addChild(b); } // Add more freeze attacks for higher waves var freezeCount = 6 + Math.floor(waveNum / 2); var freezeSpeed = 4.5 + waveNum * 0.08; for (var i = 0; i < freezeCount; i++) { var fb = new FreezeBullet(); fb.x = boxX + 80 + (boxWidth - 160) * (i / (freezeCount - 1)); fb.y = boxY + 20; fb.vx = 0; fb.vy = freezeSpeed; enemyBullets.push(fb); game.addChild(fb); } // Add freeze bullets from the left side var sideFreezeCount = 2 + Math.floor(waveNum / 3); var sideFreezeSpeed = 4.2 + waveNum * 0.08; for (var i = 0; i < sideFreezeCount; i++) { var fbLeft = new FreezeBullet(); fbLeft.x = boxX + 20; fbLeft.y = boxY + 120 + (boxHeight - 240) * (i / (sideFreezeCount - 1)); fbLeft.vx = sideFreezeSpeed; fbLeft.vy = 0; enemyBullets.push(fbLeft); game.addChild(fbLeft); } // Add freeze bullets from the right side for (var i = 0; i < sideFreezeCount; i++) { var fbRight = new FreezeBullet(); fbRight.x = boxX + boxWidth - 20; fbRight.y = boxY + 120 + (boxHeight - 240) * (i / (sideFreezeCount - 1)); fbRight.vx = -sideFreezeSpeed; fbRight.vy = 0; enemyBullets.push(fbRight); game.addChild(fbRight); } }; return base.concat([extraAttack]); } var currentAttackIndex = 0; var currentWaveAttacks = getWaveAttacks(0); var attackInterval = 80; // ticks between attacks in a wave function spawnWave(waveNum) { // Clear bullets for (var i = 0; i < enemyBullets.length; i++) { enemyBullets[i].destroy(); } enemyBullets.length = 0; // Setup attacks for this wave currentAttackIndex = 0; currentWaveAttacks = getWaveAttacks(waveNum); // Immediately launch the first attack if (currentWaveAttacks.length > 0) { currentWaveAttacks[0](); } } // Game state var waveNum = 0; var waveTimer = 0; var waveDuration = 240; // 4 seconds per wave // Invincibility after hit var invincibleTicks = 0; // Main update loop game.update = function () { // Track last soul position for freeze bullet logic if (typeof lastSoulX !== "number") lastSoulX = soul.x; if (typeof lastSoulY !== "number") lastSoulY = soul.y; var prevSoulX = soul.x; var prevSoulY = soul.y; // Update bullets for (var i = enemyBullets.length - 1; i >= 0; i--) { var b = enemyBullets[i]; b.update && b.update(); // Remove if out of box (for normal bullets) if (b.constructor && b.constructor === CircleLaserEnemy) { // Remove CircleLaserEnemy after laser is done if (b.laserFired && !b.laserActive) { b.destroy && b.destroy(); enemyBullets.splice(i, 1); continue; } } else { if (b.x < boxX - 80 || b.x > boxX + boxWidth + 80 || b.y < boxY - 80 || b.y > boxY + boxHeight + 80) { b.destroy(); enemyBullets.splice(i, 1); continue; } } // Collision with soul if (invincibleTicks === 0 && b.intersects(soul)) { // If it's a FreezeBullet, only damage if player is not moving if (b.constructor && b.constructor === FreezeBullet) { // Check if player is moving: compare soul.x/y to lastSoulX/lastSoulY if (typeof lastSoulX === "number" && typeof lastSoulY === "number" && soul.x === lastSoulX && soul.y === lastSoulY) { // Player is NOT moving, do NOT damage // Optionally, flash blue to indicate safe LK.effects.flashObject(soul, 0x3399ff, 200); } else { // Player is moving, take damage hp--; soul.flash(); invincibleTicks = 60; // 1 second invincibility hpText.setText('HP: ' + hp); LK.effects.flashObject(soul, 0xffffff, 300); if (hp <= 0) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); return; } } } else { // Normal bullet: always damage hp--; soul.flash(); invincibleTicks = 60; // 1 second invincibility hpText.setText('HP: ' + hp); LK.effects.flashObject(soul, 0xffffff, 300); if (hp <= 0) { LK.effects.flashScreen(0xff0000, 1000); LK.showGameOver(); return; } } } // Example: Decrease enemyHP for demonstration (remove this in real game, or connect to your attack logic) // if (someCondition) { enemyHP--; } enemyHealthBar.width = (enemyHealthBarWidth - 8) * Math.max(0, enemyHP) / enemyMaxHP; } // Invincibility timer if (invincibleTicks > 0) { invincibleTicks--; } // Wave logic waveTimer++; if (currentWaveAttacks && currentAttackIndex < currentWaveAttacks.length - 1) { // Time for next attack in this wave? if (waveTimer % attackInterval === 0) { currentAttackIndex++; if (currentWaveAttacks[currentAttackIndex]) { currentWaveAttacks[currentAttackIndex](); } } } // If all attacks in this wave have been launched, advance to next wave after waveDuration if (waveTimer >= waveDuration) { waveNum++; score++; scoreText.setText('WAVES: ' + score); waveTimer = 0; spawnWave(waveNum); } // After wave 15, allow player to attack sans (enemy) if (waveNum >= 15) { // Enable player attack mode if (!game.playerAttackEnabled) { game.playerAttackEnabled = true; // Show a visual cue or instruction (optional) LK.effects.flashObject(enemy, 0xffff00, 600); // Add "Attack" button to GUI if not already present if (!game.attackButton) { game.attackButton = new Text2('ATTACK', { size: 120, fill: 0xff2222, font: "Impact, Arial Black, Tahoma" }); game.attackButton.anchor.set(0.5, 0.5); // Place button at bottom center, above the box LK.gui.bottom.addChild(game.attackButton); // Button state game.attackButton.enabled = true; // Button handler game.attackButton.down = function (x, y, obj) { if (!game.attackButton.enabled) return; game.attackButton.enabled = false; // 99999 crit attack! enemyHP = 0; enemyHealthBar.width = 0; // Show crit text var critText = new Text2('99999 CRIT!', { size: 160, fill: 0xff2222, font: "Impact, Arial Black, Tahoma" }); critText.anchor.set(0.5, 0.5); critText.x = enemy.x; critText.y = enemy.y - 120; game.addChild(critText); LK.effects.flashObject(enemy, 0xffffff, 200); LK.effects.flashScreen(0xffff00, 600); // Remove all bullets for (var i = 0; i < enemyBullets.length; i++) { enemyBullets[i].destroy && enemyBullets[i].destroy(); } enemyBullets.length = 0; // Remove button after attack LK.setTimeout(function () { if (game.attackButton && game.attackButton.parent) { game.attackButton.parent.removeChild(game.attackButton); } if (critText && critText.parent) { critText.parent.removeChild(critText); } }, 1200); // Win after short delay LK.setTimeout(function () { LK.showYouWin(); }, 900); }; } } // Listen for tap on enemy to attack (legacy, still allow) if (!enemy._attackHandlerAttached) { enemy._attackHandlerAttached = true; enemy.down = function (x, y, obj) { // Only allow attack if player attack is enabled and enemy is alive if (game.playerAttackEnabled && enemyHP > 0) { enemyHP--; LK.getSound('sans').play(); LK.effects.flashObject(enemy, 0xffffff, 120); enemyHealthBar.width = (enemyHealthBarWidth - 8) * Math.max(0, enemyHP) / enemyMaxHP; if (enemyHP <= 0) { LK.effects.flashScreen(0x00ff00, 1000); LK.showYouWin(); } } }; } } // Update lastSoulX and lastSoulY for next frame lastSoulX = prevSoulX; lastSoulY = prevSoulY; }; // Start first wave spawnWave(waveNum);
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// CircleLaserEnemy: A circle that appears and fires a thick laser once
var CircleLaserEnemy = Container.expand(function () {
var self = Container.call(this);
// Attach a green circle asset
var circle = self.attachAsset('centerCircle', {
width: 120,
height: 120,
color: 0x00ff99,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
// Laser graphics (thick rectangle, hidden at first)
var laser = self.attachAsset('boxBorder', {
width: 40,
height: boxHeight + 200,
color: 0xff2222,
shape: 'box',
anchorX: 0.5,
anchorY: 0
});
laser.visible = false;
laser.y = 60; // Start just below the circle
// State
self.laserFired = false;
self.laserDuration = 60; // 1 second
self.laserTimer = 0;
// For collision detection
self.laserActive = false;
// Called every tick
self.update = function () {
// Animate circle (optional: pulse)
circle.scaleX = 1.0 + 0.08 * Math.sin(LK.ticks / 8);
circle.scaleY = 1.0 + 0.08 * Math.sin(LK.ticks / 8);
// Fire laser after short delay
if (!self.laserFired) {
if (!self._waitTicks) self._waitTicks = 0;
self._waitTicks++;
if (self._waitTicks === 40) {
// Fire!
laser.visible = true;
self.laserActive = true;
self.laserFired = true;
self.laserTimer = 0;
// Optional: flash effect
LK.effects.flashObject(laser, 0xffffff, 120);
}
}
// Laser active
if (self.laserActive) {
self.laserTimer++;
// Check collision with soul
// Laser is a vertical thick bar, check if soul overlaps horizontally
var laserGlobalX = self.x;
var laserLeft = laserGlobalX - laser.width / 2;
var laserRight = laserGlobalX + laser.width / 2;
var soulLeft = soul.x - soul.radius;
var soulRight = soul.x + soul.radius;
var soulTop = soul.y - soul.radius;
var soulBottom = soul.y + soul.radius;
var laserTop = self.y + laser.y;
var laserBottom = laserTop + laser.height;
// Only check if soul is inside the box
if (invincibleTicks === 0 && soulRight > laserLeft && soulLeft < laserRight && soulBottom > laserTop && soulTop < laserBottom) {
// Damage player
hp--;
soul.flash();
invincibleTicks = 60;
hpText.setText('HP: ' + hp);
LK.effects.flashObject(soul, 0xffffff, 300);
if (hp <= 0) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
}
// Laser lasts for laserDuration ticks
if (self.laserTimer > self.laserDuration) {
self.laserActive = false;
laser.visible = false;
}
}
};
return self;
});
// Enemy (for future expansion, currently just a box at the top)
var Enemy = Container.expand(function () {
var self = Container.call(this);
var enemyBox = self.attachAsset('enemyBox', {
width: 400,
height: 260,
// Increased height for a longer (taller) enemy box, width unchanged
color: 0x2222ff,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
// Enemy Bullet
var EnemyBullet = Container.expand(function () {
var self = Container.call(this);
// Attach bullet asset (white circle)
var bullet = self.attachAsset('enemyBullet', {
width: 60,
height: 60,
color: 0xffffff,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
// Movement properties
self.vx = 0;
self.vy = 0;
// For pattern logic
self.pattern = null;
// Always initialize patternTick to 0 for each bullet
self.patternTick = 0;
// Called every tick
self.update = function () {
if (self.pattern) {
self.pattern(self, self.patternTick);
// Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function)
if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") {
self.patternTick = 0;
} else {
// Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function)
if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") {
self.patternTick = 0;
} else {
// Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function)
if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") {
self.patternTick = 1;
} else {
// Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function)
if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") {
self.patternTick = 1;
} else {
// Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function)
if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") {
self.patternTick = 1;
} else {
// Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function)
if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") {
self.patternTick = 1;
} else {
if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") {
self.patternTick = 1;
} else {
// Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function)
if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") {
self.patternTick = 1;
} else {
// Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function)
if (typeof self.patternTick !== "number" || self.patternTick && typeof self.patternTick.then === "function") {
self.patternTick = 1;
} else {
// Defensive: ensure self.patternTick is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) at line 91
try {
var n = Number(self.patternTick);
if (typeof n === "number" && !isNaN(n) && !(n && typeof n.then === "function")) {
// Defensive: ensure n is a number and not a thenable before incrementing
if (typeof n === "number" && !isNaN(n) && !(n && typeof n.then === "function")) {
// Defensive: ensure n is a number and not a thenable before incrementing (fix for s.apply(...).then is not a function) at line 97
if (typeof n === "number" && !isNaN(n) && !(n && typeof n.then === "function")) {
self.patternTick = n + 1;
} else {
self.patternTick = 1;
}
} else {
self.patternTick = 1;
}
} else {
self.patternTick = 1;
}
} catch (e) {
self.patternTick = 1;
}
}
}
}
}
}
}
}
}
}
} else {
// Defensive: ensure self.vx is a number before using
if (typeof self.vx === "number") {
self.x += self.vx;
}
// Defensive: ensure self.vy is a number and not a thenable before using
if (typeof self.vy === "number" && !(_typeof6(self.vy) === "object" && self.vy !== null && typeof self.vy.then === "function")) {
self.y += self.vy;
}
}
};
return self;
});
// Freeze Bullet (special: only damages if player is not moving)
var FreezeBullet = Container.expand(function () {
var self = Container.call(this);
// Attach freeze bullet asset (blue ellipse)
var bullet = self.attachAsset('freezeBullet', {
width: 60,
height: 60,
color: 0x3399ff,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
// Movement properties
self.vx = 0;
self.vy = 0;
// For pattern logic
self.pattern = null;
self.patternTick = 0;
self.update = function () {
if (self.pattern) {
self.pattern(self, self.patternTick);
if (typeof self.patternTick !== "number" || _typeof5(self.patternTick) === "object" && self.patternTick !== null && typeof self.patternTick.then === "function") {
self.patternTick = 0;
}
if (typeof self.patternTick === "number" && !(_typeof5(self.patternTick) === "object" && self.patternTick !== null && typeof self.patternTick.then === "function")) {
self.patternTick += 1;
} else {
self.patternTick = 1;
}
} else {
if (typeof self.vx === "number") {
self.x += self.vx;
}
if (typeof self.vy === "number" && !(_typeof2(self.vy) === "object" && self.vy !== null && typeof self.vy.then === "function")) {
self.y += self.vy;
}
}
};
return self;
});
// Heart-shaped Soul (Player)
var Soul = Container.expand(function () {
var self = Container.call(this);
// Attach heart asset (ellipse, red, scaled to look like a heart)
// Since we can't draw a heart, use an ellipse and scale it to look heart-like
var heart = self.attachAsset('soulHeart', {
width: 60,
height: 50,
color: 0xff2d55,
shape: 'ellipse',
anchorX: 0.5,
anchorY: 0.5
});
// Squash to make it more heart-like
heart.scaleY = 1.1;
heart.scaleX = 1.2;
// For hit feedback
self.flash = function () {
tween(heart, {
tint: 0xffffff
}, {
duration: 80,
onFinish: function onFinish() {
tween(heart, {
tint: 0xff2d55
}, {
duration: 120
});
}
});
};
// For movement bounds
self.radius = 30;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game area (the "box" where the soul can move)
// Tween plugin for movement and effects
function _typeof12(o) {
"@babel/helpers - typeof";
return _typeof12 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof12(o);
}
function _typeof11(o) {
"@babel/helpers - typeof";
return _typeof11 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof11(o);
}
function _typeof10(o) {
"@babel/helpers - typeof";
return _typeof10 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof10(o);
}
function _typeof1(o) {
"@babel/helpers - typeof";
return _typeof1 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof1(o);
}
function _typeof0(o) {
"@babel/helpers - typeof";
return _typeof0 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof0(o);
}
function _typeof9(o) {
"@babel/helpers - typeof";
return _typeof9 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof9(o);
}
function _typeof8(o) {
"@babel/helpers - typeof";
return _typeof8 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof8(o);
}
function _typeof7(o) {
"@babel/helpers - typeof";
return _typeof7 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof7(o);
}
function _typeof6(o) {
"@babel/helpers - typeof";
return _typeof6 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof6(o);
}
function _typeof5(o) {
"@babel/helpers - typeof";
return _typeof5 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof5(o);
}
function _typeof4(o) {
"@babel/helpers - typeof";
return _typeof4 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof4(o);
}
function _typeof3(o) {
"@babel/helpers - typeof";
return _typeof3 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof3(o);
}
function _typeof2(o) {
"@babel/helpers - typeof";
return _typeof2 = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof2(o);
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
var boxWidth = 900;
var boxHeight = 900;
var boxX = (2048 - boxWidth) / 2;
var boxY = 2732 - boxHeight - 200;
// Draw the box (white border, transparent fill)
var boxBorder = LK.getAsset('boxBorder', {
width: boxWidth,
height: boxHeight,
color: 0xffffff,
shape: 'box',
anchorX: 0,
anchorY: 0
});
boxBorder.alpha = 0.15;
boxBorder.x = boxX;
boxBorder.y = boxY;
game.addChild(boxBorder);
// Add a big enemy (bi) at the top of the game area
var enemy = new Enemy();
enemy.x = 2048 / 2;
enemy.y = 2732 / 2 - 120; // Move enemy a little up from center
enemy.scaleX = 2.2;
enemy.scaleY = 2.2;
game.addChild(enemy);
// Enemy health bar
var enemyMaxHP = 20;
var enemyHP = enemyMaxHP;
var enemyHealthBarWidth = 700;
var enemyHealthBarHeight = 60;
var enemyHealthBarBg = LK.getAsset('boxBorder', {
width: enemyHealthBarWidth,
height: enemyHealthBarHeight,
color: 0x333333,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5
});
// Place health bar background further below the enemy
enemyHealthBarBg.x = enemy.x;
enemyHealthBarBg.y = enemy.y + 260; // Move health bar further down to match new enemy position
game.addChild(enemyHealthBarBg);
var enemyHealthBar = LK.getAsset('boxBorder', {
width: enemyHealthBarWidth - 8,
height: enemyHealthBarHeight - 2,
color: 0xFFFF00,
// yellow
shape: 'box',
anchorX: 0.5,
anchorY: 0.5
});
enemyHealthBar.x = enemy.x;
enemyHealthBar.y = enemy.y + 260;
game.addChild(enemyHealthBar);
// Add soul (player) at center of box
var soul = new Soul();
soul.x = boxX + boxWidth / 2;
soul.y = boxY + boxHeight / 2;
game.addChild(soul);
// Health system
var maxHP = 5;
var hp = maxHP;
// Score (number of waves survived)
var score = 0;
// Bullets array
var enemyBullets = [];
// GUI: HP display (top right, avoid top left)
var hpText = new Text2('HP: ' + hp, {
size: 90,
fill: 0xFF2D55
});
hpText.anchor.set(1, 0);
LK.gui.topRight.addChild(hpText);
// GUI: Score display (top center)
var scoreText = new Text2('WAVES: 0', {
size: 90,
fill: 0xFF2D55
});
scoreText.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreText);
// Touch controls for soul movement
var draggingSoul = false;
var dragOffsetX = 0;
var dragOffsetY = 0;
// Helper: clamp soul inside box
function clampSoulPosition(x, y) {
var r = soul.radius;
var minX = boxX + r;
var maxX = boxX + boxWidth - r;
var minY = boxY + r;
var maxY = boxY + boxHeight - r;
if (x < minX) x = minX;
if (x > maxX) x = maxX;
if (y < minY) y = minY;
if (y > maxY) y = maxY;
return {
x: x,
y: y
};
}
// Touch down: start dragging if inside soul
game.down = function (x, y, obj) {
// Only allow drag if inside soul
var dx = x - soul.x;
var dy = y - soul.y;
if (dx * dx + dy * dy <= soul.radius * soul.radius) {
draggingSoul = true;
dragOffsetX = soul.x - x;
dragOffsetY = soul.y - y;
}
};
// Touch up: stop dragging
game.up = function (x, y, obj) {
draggingSoul = false;
};
// Touch move: move soul
game.move = function (x, y, obj) {
// Always follow the mouse/touch, clamped to the box
var pos = clampSoulPosition(x, y);
soul.x = pos.x;
soul.y = pos.y;
};
// Bullet patterns
// Multi-attack per wave support
var waveAttacks = [
// Each entry is an array of attack functions for that wave
// Wave 0: 2 attacks: radial burst, then spiral
[function () {
// Radial burst
var count = 8;
var speed = 3.5;
for (var i = 0; i < count; i++) {
var angle = Math.PI * 2 * i / count;
var b = new EnemyBullet();
b.x = 2048 / 2;
b.y = boxY + 40;
b.vx = Math.cos(angle) * speed;
b.vy = Math.sin(angle) * speed;
enemyBullets.push(b);
game.addChild(b);
}
}, function () {
// Spiral
var spiralCount = 12;
var spiralSpawnY = boxY + 180;
var spiralSpeed = 3.2;
for (var i = 0; i < spiralCount; i++) {
var b = new EnemyBullet();
b.x = 2048 / 2;
b.y = spiralSpawnY;
var baseAngle = Math.PI * 2 * i / spiralCount;
b.pattern = function (angleOffset) {
return function (self, t) {
var speed = spiralSpeed;
var angle = angleOffset + t * 0.07;
self.x = 2048 / 2 + Math.cos(angle) * speed * t;
self.y = spiralSpawnY + Math.sin(angle) * speed * t;
};
}(baseAngle);
enemyBullets.push(b);
game.addChild(b);
}
}],
// Wave 1: Sine wave, then zigzag
[function () {
// Sine wave bullets from both sides
var sineCount = 4;
var sineSpeed = 4.2;
for (var i = 0; i < sineCount; i++) {
var b = new EnemyBullet();
b.x = boxX + 40;
b.y = boxY + 120 + i * 120;
b.vx = sineSpeed;
b.vy = 0;
b.pattern = function (self, t) {
self.x += self.vx;
self.y += Math.sin((self.x + t * 2) / 80) * 5;
};
enemyBullets.push(b);
game.addChild(b);
}
for (var i = 0; i < sineCount; i++) {
var b = new EnemyBullet();
b.x = boxX + boxWidth - 40;
b.y = boxY + 120 + i * 120;
b.vx = -sineSpeed;
b.vy = 0;
b.pattern = function (self, t) {
self.x += self.vx;
self.y += Math.sin((self.x + t * 2) / 80) * 5;
};
enemyBullets.push(b);
game.addChild(b);
}
}, function () {
// Zigzag bullets from above
var zigzagCount = 8;
var zigzagSpeed = 4.5;
for (var i = 0; i < zigzagCount; i++) {
var b = new EnemyBullet();
b.x = boxX + 80 + (boxWidth - 160) * (i / zigzagCount);
b.y = boxY + 20;
b.vx = 0;
b.vy = zigzagSpeed;
b.pattern = function (self, t) {
self.y += self.vy;
self.x += Math.sin(self.y / 60) * 3;
};
enemyBullets.push(b);
game.addChild(b);
}
}],
// Wave 2: Spiral, then aimed shots
[function () {
// Spiral pattern from center
var spiralCount = 18;
var spiralSpawnY = boxY + 180;
var spiralSpeed = 4.2;
for (var i = 0; i < spiralCount; i++) {
var b = new EnemyBullet();
b.x = 2048 / 2;
b.y = spiralSpawnY;
var baseAngle = Math.PI * 2 * i / spiralCount;
b.pattern = function (angleOffset) {
return function (self, t) {
var speed = spiralSpeed;
var angle = angleOffset + t * 0.07;
self.x = 2048 / 2 + Math.cos(angle) * speed * t;
self.y = spiralSpawnY + Math.sin(angle) * speed * t;
};
}(baseAngle);
enemyBullets.push(b);
game.addChild(b);
}
}, function () {
// Aimed shots at the player (soul), with some spread
var shots = 7;
var aimedSpeed = 5.2;
for (var i = 0; i < shots; i++) {
var b = new EnemyBullet();
b.x = 2048 / 2;
b.y = boxY + 40;
var dx = soul.x - b.x;
var dy = soul.y - b.y;
var angle = Math.atan2(dy, dx) + (i - (shots - 1) / 2) * 0.12;
b.vx = Math.cos(angle) * aimedSpeed;
b.vy = Math.sin(angle) * aimedSpeed;
enemyBullets.push(b);
game.addChild(b);
}
}],
// Wave 3: Freeze bullets, then CircleLaserEnemy
[function () {
// Freeze bullets (special blue bullets)
var freezeCount = 7;
var freezeSpeed = 4.5;
for (var i = 0; i < freezeCount; i++) {
var b = new FreezeBullet();
b.x = boxX + 80 + (boxWidth - 160) * (i / (freezeCount - 1));
b.y = boxY + 20;
b.vx = 0;
b.vy = freezeSpeed;
enemyBullets.push(b);
game.addChild(b);
}
// Add freeze bullets from the left side
var sideFreezeCount = 3;
var sideFreezeSpeed = 4.2;
for (var i = 0; i < sideFreezeCount; i++) {
var fbLeft = new FreezeBullet();
fbLeft.x = boxX + 20;
fbLeft.y = boxY + 120 + (boxHeight - 240) * (i / (sideFreezeCount - 1));
fbLeft.vx = sideFreezeSpeed;
fbLeft.vy = 0;
enemyBullets.push(fbLeft);
game.addChild(fbLeft);
}
// Add freeze bullets from the right side
for (var i = 0; i < sideFreezeCount; i++) {
var fbRight = new FreezeBullet();
fbRight.x = boxX + boxWidth - 20;
fbRight.y = boxY + 120 + (boxHeight - 240) * (i / (sideFreezeCount - 1));
fbRight.vx = -sideFreezeSpeed;
fbRight.vy = 0;
enemyBullets.push(fbRight);
game.addChild(fbRight);
}
}, function () {
// Multiple CircleLaserEnemy with different angles
var numLasers = 3;
var baseY = boxY + 100;
var centerX = 2048 / 2;
var radius = 220;
for (var i = 0; i < numLasers; i++) {
var angle = Math.PI * 2 * i / numLasers;
var cle = new CircleLaserEnemy();
cle.x = centerX + Math.cos(angle) * radius;
cle.y = baseY + Math.sin(angle) * radius * 0.5;
cle._laserAngle = angle;
(function (cle, angle) {
var origUpdate = cle.update;
cle.update = function () {
origUpdate.call(cle);
if (cle.children && cle.children.length > 1) {
var laser = cle.children[1];
laser.rotation = angle;
laser.x = Math.cos(angle) * 60;
laser.y = Math.sin(angle) * 60;
}
};
})(cle, angle);
game.addChild(cle);
enemyBullets.push(cle);
}
}]];
// For higher waves, repeat and increase difficulty
function getWaveAttacks(waveNum) {
if (waveNum < waveAttacks.length) {
return waveAttacks[waveNum];
}
// For higher waves, repeat and add a third attack
var base = waveAttacks[waveNum % waveAttacks.length];
// For higher waves, repeat and add a third attack
var extraAttack = function extraAttack() {
// Harder radial burst
var count = 12 + Math.floor(waveNum / 2);
var speed = 4.5 + waveNum * 0.12;
for (var i = 0; i < count; i++) {
var angle = Math.PI * 2 * i / count;
var b = new EnemyBullet();
b.x = 2048 / 2;
b.y = boxY + 40;
b.vx = Math.cos(angle) * speed;
b.vy = Math.sin(angle) * speed;
enemyBullets.push(b);
game.addChild(b);
}
// Add more freeze attacks for higher waves
var freezeCount = 6 + Math.floor(waveNum / 2);
var freezeSpeed = 4.5 + waveNum * 0.08;
for (var i = 0; i < freezeCount; i++) {
var fb = new FreezeBullet();
fb.x = boxX + 80 + (boxWidth - 160) * (i / (freezeCount - 1));
fb.y = boxY + 20;
fb.vx = 0;
fb.vy = freezeSpeed;
enemyBullets.push(fb);
game.addChild(fb);
}
// Add freeze bullets from the left side
var sideFreezeCount = 2 + Math.floor(waveNum / 3);
var sideFreezeSpeed = 4.2 + waveNum * 0.08;
for (var i = 0; i < sideFreezeCount; i++) {
var fbLeft = new FreezeBullet();
fbLeft.x = boxX + 20;
fbLeft.y = boxY + 120 + (boxHeight - 240) * (i / (sideFreezeCount - 1));
fbLeft.vx = sideFreezeSpeed;
fbLeft.vy = 0;
enemyBullets.push(fbLeft);
game.addChild(fbLeft);
}
// Add freeze bullets from the right side
for (var i = 0; i < sideFreezeCount; i++) {
var fbRight = new FreezeBullet();
fbRight.x = boxX + boxWidth - 20;
fbRight.y = boxY + 120 + (boxHeight - 240) * (i / (sideFreezeCount - 1));
fbRight.vx = -sideFreezeSpeed;
fbRight.vy = 0;
enemyBullets.push(fbRight);
game.addChild(fbRight);
}
};
return base.concat([extraAttack]);
}
var currentAttackIndex = 0;
var currentWaveAttacks = getWaveAttacks(0);
var attackInterval = 80; // ticks between attacks in a wave
function spawnWave(waveNum) {
// Clear bullets
for (var i = 0; i < enemyBullets.length; i++) {
enemyBullets[i].destroy();
}
enemyBullets.length = 0;
// Setup attacks for this wave
currentAttackIndex = 0;
currentWaveAttacks = getWaveAttacks(waveNum);
// Immediately launch the first attack
if (currentWaveAttacks.length > 0) {
currentWaveAttacks[0]();
}
}
// Game state
var waveNum = 0;
var waveTimer = 0;
var waveDuration = 240; // 4 seconds per wave
// Invincibility after hit
var invincibleTicks = 0;
// Main update loop
game.update = function () {
// Track last soul position for freeze bullet logic
if (typeof lastSoulX !== "number") lastSoulX = soul.x;
if (typeof lastSoulY !== "number") lastSoulY = soul.y;
var prevSoulX = soul.x;
var prevSoulY = soul.y;
// Update bullets
for (var i = enemyBullets.length - 1; i >= 0; i--) {
var b = enemyBullets[i];
b.update && b.update();
// Remove if out of box (for normal bullets)
if (b.constructor && b.constructor === CircleLaserEnemy) {
// Remove CircleLaserEnemy after laser is done
if (b.laserFired && !b.laserActive) {
b.destroy && b.destroy();
enemyBullets.splice(i, 1);
continue;
}
} else {
if (b.x < boxX - 80 || b.x > boxX + boxWidth + 80 || b.y < boxY - 80 || b.y > boxY + boxHeight + 80) {
b.destroy();
enemyBullets.splice(i, 1);
continue;
}
}
// Collision with soul
if (invincibleTicks === 0 && b.intersects(soul)) {
// If it's a FreezeBullet, only damage if player is not moving
if (b.constructor && b.constructor === FreezeBullet) {
// Check if player is moving: compare soul.x/y to lastSoulX/lastSoulY
if (typeof lastSoulX === "number" && typeof lastSoulY === "number" && soul.x === lastSoulX && soul.y === lastSoulY) {
// Player is NOT moving, do NOT damage
// Optionally, flash blue to indicate safe
LK.effects.flashObject(soul, 0x3399ff, 200);
} else {
// Player is moving, take damage
hp--;
soul.flash();
invincibleTicks = 60; // 1 second invincibility
hpText.setText('HP: ' + hp);
LK.effects.flashObject(soul, 0xffffff, 300);
if (hp <= 0) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
}
} else {
// Normal bullet: always damage
hp--;
soul.flash();
invincibleTicks = 60; // 1 second invincibility
hpText.setText('HP: ' + hp);
LK.effects.flashObject(soul, 0xffffff, 300);
if (hp <= 0) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
return;
}
}
}
// Example: Decrease enemyHP for demonstration (remove this in real game, or connect to your attack logic)
// if (someCondition) { enemyHP--; }
enemyHealthBar.width = (enemyHealthBarWidth - 8) * Math.max(0, enemyHP) / enemyMaxHP;
}
// Invincibility timer
if (invincibleTicks > 0) {
invincibleTicks--;
}
// Wave logic
waveTimer++;
if (currentWaveAttacks && currentAttackIndex < currentWaveAttacks.length - 1) {
// Time for next attack in this wave?
if (waveTimer % attackInterval === 0) {
currentAttackIndex++;
if (currentWaveAttacks[currentAttackIndex]) {
currentWaveAttacks[currentAttackIndex]();
}
}
}
// If all attacks in this wave have been launched, advance to next wave after waveDuration
if (waveTimer >= waveDuration) {
waveNum++;
score++;
scoreText.setText('WAVES: ' + score);
waveTimer = 0;
spawnWave(waveNum);
}
// After wave 15, allow player to attack sans (enemy)
if (waveNum >= 15) {
// Enable player attack mode
if (!game.playerAttackEnabled) {
game.playerAttackEnabled = true;
// Show a visual cue or instruction (optional)
LK.effects.flashObject(enemy, 0xffff00, 600);
// Add "Attack" button to GUI if not already present
if (!game.attackButton) {
game.attackButton = new Text2('ATTACK', {
size: 120,
fill: 0xff2222,
font: "Impact, Arial Black, Tahoma"
});
game.attackButton.anchor.set(0.5, 0.5);
// Place button at bottom center, above the box
LK.gui.bottom.addChild(game.attackButton);
// Button state
game.attackButton.enabled = true;
// Button handler
game.attackButton.down = function (x, y, obj) {
if (!game.attackButton.enabled) return;
game.attackButton.enabled = false;
// 99999 crit attack!
enemyHP = 0;
enemyHealthBar.width = 0;
// Show crit text
var critText = new Text2('99999 CRIT!', {
size: 160,
fill: 0xff2222,
font: "Impact, Arial Black, Tahoma"
});
critText.anchor.set(0.5, 0.5);
critText.x = enemy.x;
critText.y = enemy.y - 120;
game.addChild(critText);
LK.effects.flashObject(enemy, 0xffffff, 200);
LK.effects.flashScreen(0xffff00, 600);
// Remove all bullets
for (var i = 0; i < enemyBullets.length; i++) {
enemyBullets[i].destroy && enemyBullets[i].destroy();
}
enemyBullets.length = 0;
// Remove button after attack
LK.setTimeout(function () {
if (game.attackButton && game.attackButton.parent) {
game.attackButton.parent.removeChild(game.attackButton);
}
if (critText && critText.parent) {
critText.parent.removeChild(critText);
}
}, 1200);
// Win after short delay
LK.setTimeout(function () {
LK.showYouWin();
}, 900);
};
}
}
// Listen for tap on enemy to attack (legacy, still allow)
if (!enemy._attackHandlerAttached) {
enemy._attackHandlerAttached = true;
enemy.down = function (x, y, obj) {
// Only allow attack if player attack is enabled and enemy is alive
if (game.playerAttackEnabled && enemyHP > 0) {
enemyHP--;
LK.getSound('sans').play();
LK.effects.flashObject(enemy, 0xffffff, 120);
enemyHealthBar.width = (enemyHealthBarWidth - 8) * Math.max(0, enemyHP) / enemyMaxHP;
if (enemyHP <= 0) {
LK.effects.flashScreen(0x00ff00, 1000);
LK.showYouWin();
}
}
};
}
}
// Update lastSoulX and lastSoulY for next frame
lastSoulX = prevSoulX;
lastSoulY = prevSoulY;
};
// Start first wave
spawnWave(waveNum);