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