/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 20;
self.dx = 0;
self.dy = 0;
self.speed = 38;
self.isPlayer = false;
self.lifetime = 60; // frames
self.update = function () {
self.x += self.dx * self.speed;
self.y += self.dy * self.speed;
self.lifetime--;
};
return self;
});
// Enemy class
var Enemy = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 55;
self.speed = 4 + Math.random() * 2; // slower enemies
self.shootCooldown = 0;
self.shootDelay = 60 + Math.floor(Math.random() * 30);
self.targetX = 0;
self.targetY = 0;
self.isAlive = true;
self.aiTimer = 0;
self.update = function () {
if (countdownActive) return;
if (!self.isAlive) return;
// AI: Move toward player, avoid obstacles
if (self.aiTimer <= 0) {
// Pick a new target near player, with some randomness
var px = player.x + (Math.random() - 0.5) * 300;
var py = player.y + (Math.random() - 0.5) * 300;
// Clamp to arena
px = Math.max(arena.x + 100, Math.min(arena.x + arena.w - 100, px));
py = Math.max(arena.y + 100, Math.min(arena.y + arena.h - 100, py));
self.targetX = px;
self.targetY = py;
self.aiTimer = 30 + Math.floor(Math.random() * 30);
} else {
self.aiTimer--;
}
// Move toward target
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var mag = Math.sqrt(dx * dx + dy * dy);
if (mag > 10) {
var mx = dx / mag * self.speed;
var my = dy / mag * self.speed;
// Try to move, but avoid obstacles
var nx = self.x + mx;
var ny = self.y + my;
var blocked = false;
for (var i = 0; i < obstacles.length; ++i) {
if (rectCircleCollide(obstacles[i], nx, ny, self.radius)) {
blocked = true;
break;
}
}
if (!blocked) {
self.x = nx;
self.y = ny;
}
}
// Shoot at player if in line of sight
if (self.shootCooldown > 0) self.shootCooldown--;else {
if (canSee(self.x, self.y, player.x, player.y)) {
var ddx = player.x - self.x;
var ddy = player.y - self.y;
var dmag = Math.sqrt(ddx * ddx + ddy * ddy);
if (dmag > 0) {
var bullet = new Bullet();
bullet.x = self.x + ddx / dmag * (self.radius + 30);
bullet.y = self.y + ddy / dmag * (self.radius + 30);
bullet.dx = ddx / dmag;
bullet.dy = ddy / dmag;
bullet.isPlayer = false;
bullet.speed = 13; // much slower enemy bullets
bullet.lifetime = 90; // slightly shorter range
bullets.push(bullet);
game.addChild(bullet);
self.shootCooldown = self.shootDelay + 60 + Math.floor(Math.random() * 40); // much slower fire rate
}
}
}
};
// Take damage
self.hit = function () {
if (!self.isAlive) return;
self.isAlive = false;
tween(sprite, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Obstacle class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = sprite.width;
self.height = sprite.height;
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 60;
self.speed = 28; // faster player
self.shootCooldown = 0;
self.shootDelay = 18; // frames
self.isAlive = true;
self.invincible = 0; // frames left of invincibility
// For touch drag
self.down = function (x, y, obj) {};
self.up = function (x, y, obj) {};
// For powerup
self.powered = false;
self.powerTimer = 0;
self.update = function () {
if (!self.isAlive) return;
if (self.invincible > 0) self.invincible--;
if (self.powered) {
self.powerTimer--;
if (self.powerTimer <= 0) {
self.powered = false;
sprite.tint = 0x2a6cff;
}
}
if (self.shootCooldown > 0) self.shootCooldown--;
};
// Shoot a bullet in direction (dx, dy)
self.shoot = function (dx, dy) {
if (!self.isAlive) return;
if (self.shootCooldown > 0) return;
var mag = Math.sqrt(dx * dx + dy * dy);
if (mag < 0.1) return;
var bullet = new Bullet();
bullet.x = self.x + dx / mag * (self.radius + 30);
bullet.y = self.y + dy / mag * (self.radius + 30);
bullet.dx = dx / mag;
bullet.dy = dy / mag;
bullet.isPlayer = true;
bullets.push(bullet);
game.addChild(bullet);
self.shootCooldown = self.powered ? 5 : self.shootDelay;
};
// Take damage
self.hit = function () {
if (self.invincible > 0 || !self.isAlive) return;
self.invincible = 60; // 1 second of invincibility (60 frames)
if (typeof playerHealth !== "undefined") {
playerHealth--;
// Update health display
if (healthTxt) {
var hearts = '';
for (var i = 0; i < playerHealth; ++i) hearts += '♥';
healthTxt.setText(hearts);
}
// Flash player sprite
tween(sprite, {
alpha: 0.3
}, {
duration: 80,
yoyo: true,
repeat: 5,
onFinish: function onFinish() {
sprite.alpha = 1;
}
});
if (playerHealth <= 0) {
self.isAlive = false;
tween(sprite, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
}
});
}
} else {
// fallback: kill player
self.isAlive = false;
tween(sprite, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
}
});
}
};
// Powerup
self.powerup = function () {
self.powered = true;
self.powerTimer = 360; // 6 seconds
sprite.tint = 0x3be14a;
};
return self;
});
// Powerup class
var Powerup = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 30;
self.type = 'rapid';
self.lifetime = 600; // 10 seconds
self.update = function () {
self.lifetime--;
};
self.apply = function () {
if (self.type === 'rapid') {
player.powerup();
powerupTxt.setText('Rapid Fire!');
} else if (self.type === 'health') {
if (typeof playerHealth !== "undefined" && playerHealth < playerMaxHealth) {
playerHealth++;
// Update health display
if (healthTxt) {
var hearts = '';
for (var i = 0; i < playerHealth; ++i) hearts += '♥';
healthTxt.setText(hearts);
}
powerupTxt.setText('Health Up!');
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Arena bounds
// Player character: blue box
// Enemy: red box
// Bullet: yellow ellipse
// Obstacle: gray box
// Powerup: green ellipse
var arena = {
x: 180,
y: 300,
w: 2048 - 360,
h: 2732 - 600
};
// Game state
var player;
var enemies = [];
var bullets = [];
var obstacles = [];
var powerups = [];
var round = 1;
var roundActive = false;
var roundTimer = 0;
var spawnTimer = 0;
var dragNode = null;
var lastTouch = {
x: 0,
y: 0
};
var scoreTxt;
var roundTxt;
var powerupTxt;
var gameOver = false;
// Health system
var playerMaxHealth = 3;
var playerHealth = playerMaxHealth;
var healthTxt;
// Countdown state
var countdownActive = false;
var countdownValue = 0;
var countdownTxt = null;
var countdownTimeout = null;
// GUI
scoreTxt = new Text2('Score: 0', {
size: 90,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
roundTxt = new Text2('Round 1', {
size: 70,
fill: 0xFFCC00
});
roundTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(roundTxt);
powerupTxt = new Text2('', {
size: 60,
fill: 0x3BE14A
});
powerupTxt.anchor.set(0.5, 0);
LK.gui.bottom.addChild(powerupTxt);
// Health display
healthTxt = new Text2('♥♥♥', {
size: 90,
fill: 0xFF4444
});
healthTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(healthTxt);
// Place obstacles
function setupObstacles() {
for (var i = 0; i < obstacles.length; ++i) obstacles[i].destroy();
obstacles = [];
// Place 4-6 obstacles randomly
var obsCount = 4 + Math.floor(Math.random() * 3);
for (var i = 0; i < obsCount; ++i) {
var obs = new Obstacle();
obs.x = arena.x + 200 + Math.random() * (arena.w - 400);
obs.y = arena.y + 200 + Math.random() * (arena.h - 400);
if (i % 2 == 0) obs.rotation = Math.PI / 2;
obstacles.push(obs);
game.addChild(obs);
}
}
// Place player
function setupPlayer() {
if (player) player.destroy();
player = new Player();
player.x = arena.x + arena.w / 2;
player.y = arena.y + arena.h - 200;
game.addChild(player);
}
// Place enemies
function setupEnemies(n) {
for (var i = 0; i < enemies.length; ++i) enemies[i].destroy();
enemies = [];
for (var i = 0; i < n; ++i) {
var e = new Enemy();
// Spawn at top or sides
var edge = Math.floor(Math.random() * 4);
if (edge == 0) {
// top
e.x = arena.x + 200 + Math.random() * (arena.w - 400);
e.y = arena.y + 120;
} else if (edge == 1) {
// left
e.x = arena.x + 120;
e.y = arena.y + 200 + Math.random() * (arena.h - 400);
} else if (edge == 2) {
// right
e.x = arena.x + arena.w - 120;
e.y = arena.y + 200 + Math.random() * (arena.h - 400);
} else {
// bottom
e.x = arena.x + 200 + Math.random() * (arena.w - 400);
e.y = arena.y + arena.h - 120;
}
enemies.push(e);
game.addChild(e);
}
}
// Place powerup
function spawnPowerup() {
var p = new Powerup();
p.x = arena.x + 150 + Math.random() * (arena.w - 300);
p.y = arena.y + 150 + Math.random() * (arena.h - 300);
// 50% chance for health, 50% for rapid
if (Math.random() < 0.5) {
p.type = 'health';
// visually distinguish health powerup
p.children[0].tint = 0xff4444;
} else {
p.type = 'rapid';
p.children[0].tint = 0x3be14a;
}
powerups.push(p);
game.addChild(p);
}
// Start round
function startRound() {
// Start countdown before round actually starts
roundActive = false;
countdownActive = true;
countdownValue = 3;
// Remove previous countdown text if exists
if (countdownTxt) {
countdownTxt.destroy();
countdownTxt = null;
}
countdownTxt = new Text2('3', {
size: 300,
fill: 0xFFCC00
});
countdownTxt.anchor.set(0.5, 0.5);
LK.gui.center.addChild(countdownTxt);
// Setup everything for the round, but don't activate yet
roundTimer = 0;
spawnTimer = 0;
setupObstacles();
setupPlayer();
setupEnemies(2 + round);
for (var i = 0; i < bullets.length; ++i) bullets[i].destroy();
bullets = [];
for (var i = 0; i < powerups.length; ++i) powerups[i].destroy();
powerups = [];
roundTxt.setText('Round ' + round);
powerupTxt.setText('');
LK.setScore(LK.getScore());
scoreTxt.setText('Score: ' + LK.getScore());
// Reset player health at start of round
playerHealth = playerMaxHealth;
if (healthTxt) {
var hearts = '';
for (var i = 0; i < playerHealth; ++i) hearts += '♥';
healthTxt.setText(hearts);
}
// Start countdown timer
function doCountdown() {
if (!countdownActive) return;
countdownValue--;
if (countdownValue > 0) {
countdownTxt.setText(countdownValue + '');
countdownTimeout = LK.setTimeout(doCountdown, 800);
} else {
// Go!
countdownTxt.setText('GO!');
countdownTimeout = LK.setTimeout(function () {
if (countdownTxt) {
countdownTxt.destroy();
countdownTxt = null;
}
countdownActive = false;
roundActive = true;
}, 600);
}
}
countdownTimeout = LK.setTimeout(doCountdown, 800);
}
// Utility: circle-rectangle collision
function rectCircleCollide(obs, cx, cy, cr) {
var rx = obs.x - obs.width / 2;
var ry = obs.y - obs.height / 2;
var rw = obs.width;
var rh = obs.height;
var closestX = Math.max(rx, Math.min(cx, rx + rw));
var closestY = Math.max(ry, Math.min(cy, ry + rh));
var dx = cx - closestX;
var dy = cy - closestY;
return dx * dx + dy * dy < cr * cr;
}
// Utility: circle-circle collision
function circleCollide(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
var r = (a.radius || 0) + (b.radius || 0);
return dx * dx + dy * dy < r * r;
}
// Utility: line of sight (returns true if no obstacle blocks)
function canSee(x1, y1, x2, y2) {
for (var i = 0; i < obstacles.length; ++i) {
if (lineRectIntersect(x1, y1, x2, y2, obstacles[i])) return false;
}
return true;
}
// Utility: line-rectangle intersection
function lineRectIntersect(x1, y1, x2, y2, obs) {
var rx = obs.x - obs.width / 2;
var ry = obs.y - obs.height / 2;
var rw = obs.width;
var rh = obs.height;
// Check each side
if (lineLineIntersect(x1, y1, x2, y2, rx, ry, rx + rw, ry)) return true;
if (lineLineIntersect(x1, y1, x2, y2, rx + rw, ry, rx + rw, ry + rh)) return true;
if (lineLineIntersect(x1, y1, x2, y2, rx + rw, ry + rh, rx, ry + rh)) return true;
if (lineLineIntersect(x1, y1, x2, y2, rx, ry + rh, rx, ry)) return true;
return false;
}
// Utility: line-line intersection
function lineLineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
var denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
if (denom == 0) return false;
var ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
var ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
}
// Clamp player inside arena and avoid obstacles
function clampPlayer(px, py) {
// Clamp to arena
px = Math.max(arena.x + player.radius, Math.min(arena.x + arena.w - player.radius, px));
py = Math.max(arena.y + player.radius, Math.min(arena.y + arena.h - player.radius, py));
// Avoid obstacles
for (var i = 0; i < obstacles.length; ++i) {
if (rectCircleCollide(obstacles[i], px, py, player.radius)) {
// Push out
var ox = obstacles[i].x;
var oy = obstacles[i].y;
var dx = px - ox;
var dy = py - oy;
var mag = Math.sqrt(dx * dx + dy * dy);
if (mag > 0) {
px = ox + dx / mag * (obstacles[i].width / 2 + player.radius + 2);
py = oy + dy / mag * (obstacles[i].height / 2 + player.radius + 2);
}
}
}
return {
x: px,
y: py
};
}
// Touch controls: drag to move, tap to shoot
game.down = function (x, y, obj) {
if (countdownActive) return;
if (!player.isAlive) return;
// If touch is near player, start drag
var dx = x - player.x;
var dy = y - player.y;
if (dx * dx + dy * dy < player.radius * player.radius * 2) {
dragNode = player;
} else {
// Else, shoot toward touch
var dirx = x - player.x;
var diry = y - player.y;
player.shoot(dirx, diry);
}
lastTouch.x = x;
lastTouch.y = y;
};
game.move = function (x, y, obj) {
if (countdownActive) return;
if (!player.isAlive) return;
if (dragNode === player) {
// Move player toward touch, but clamp
var dx = x - player.x;
var dy = y - player.y;
var mag = Math.sqrt(dx * dx + dy * dy);
if (mag > 0) {
var step = Math.min(player.speed, mag);
var nx = player.x + dx / mag * step;
var ny = player.y + dy / mag * step;
var clamped = clampPlayer(nx, ny);
player.x = clamped.x;
player.y = clamped.y;
}
lastTouch.x = x;
lastTouch.y = y;
}
};
game.up = function (x, y, obj) {
dragNode = null;
};
// Main update loop
game.update = function () {
// If countdown is active, pause gameplay updates
if (countdownActive) {
return;
}
if (!roundActive) {
startRound();
return;
}
if (!player.isAlive) return;
// Update player
player.update();
// Update enemies
for (var i = enemies.length - 1; i >= 0; --i) {
var e = enemies[i];
e.update();
// If dead, remove
if (!e.isAlive && e.alpha <= 0) {
e.destroy();
enemies.splice(i, 1);
LK.setScore(LK.getScore() + 1);
scoreTxt.setText('Score: ' + LK.getScore());
}
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; --i) {
var b = bullets[i];
b.update();
// Remove if out of bounds or expired
if (b.x < arena.x - 100 || b.x > arena.x + arena.w + 100 || b.y < arena.y - 100 || b.y > arena.y + arena.h + 100 || b.lifetime <= 0) {
b.destroy();
bullets.splice(i, 1);
continue;
}
// Collide with obstacles
var hitObs = false;
for (var j = 0; j < obstacles.length; ++j) {
if (rectCircleCollide(obstacles[j], b.x, b.y, b.radius)) {
hitObs = true;
break;
}
}
if (hitObs) {
b.destroy();
bullets.splice(i, 1);
continue;
}
// Collide with enemies (if player bullet)
if (b.isPlayer) {
for (var j = 0; j < enemies.length; ++j) {
if (enemies[j].isAlive && circleCollide(b, enemies[j])) {
enemies[j].hit();
b.destroy();
bullets.splice(i, 1);
break;
}
}
} else {
// Collide with player
if (player.isAlive && circleCollide(b, player)) {
if (player.invincible <= 0) {
player.hit();
}
b.destroy();
bullets.splice(i, 1);
}
}
}
// Update powerups
for (var i = powerups.length - 1; i >= 0; --i) {
var p = powerups[i];
p.update();
if (p.lifetime <= 0) {
p.destroy();
powerups.splice(i, 1);
continue;
}
// Collide with player
if (player.isAlive && circleCollide(p, player)) {
if (typeof p.apply === "function") {
p.apply();
}
p.destroy();
powerups.splice(i, 1);
}
}
// Spawn powerup occasionally
if (LK.ticks % 600 == 0 && powerups.length == 0) {
spawnPowerup();
}
// End round if all enemies dead
if (enemies.length == 0) {
roundActive = false;
round++;
LK.setTimeout(function () {
startRound();
}, 1200);
}
};
// Initial round
startRound(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Bullet class
var Bullet = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 20;
self.dx = 0;
self.dy = 0;
self.speed = 38;
self.isPlayer = false;
self.lifetime = 60; // frames
self.update = function () {
self.x += self.dx * self.speed;
self.y += self.dy * self.speed;
self.lifetime--;
};
return self;
});
// Enemy class
var Enemy = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 55;
self.speed = 4 + Math.random() * 2; // slower enemies
self.shootCooldown = 0;
self.shootDelay = 60 + Math.floor(Math.random() * 30);
self.targetX = 0;
self.targetY = 0;
self.isAlive = true;
self.aiTimer = 0;
self.update = function () {
if (countdownActive) return;
if (!self.isAlive) return;
// AI: Move toward player, avoid obstacles
if (self.aiTimer <= 0) {
// Pick a new target near player, with some randomness
var px = player.x + (Math.random() - 0.5) * 300;
var py = player.y + (Math.random() - 0.5) * 300;
// Clamp to arena
px = Math.max(arena.x + 100, Math.min(arena.x + arena.w - 100, px));
py = Math.max(arena.y + 100, Math.min(arena.y + arena.h - 100, py));
self.targetX = px;
self.targetY = py;
self.aiTimer = 30 + Math.floor(Math.random() * 30);
} else {
self.aiTimer--;
}
// Move toward target
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var mag = Math.sqrt(dx * dx + dy * dy);
if (mag > 10) {
var mx = dx / mag * self.speed;
var my = dy / mag * self.speed;
// Try to move, but avoid obstacles
var nx = self.x + mx;
var ny = self.y + my;
var blocked = false;
for (var i = 0; i < obstacles.length; ++i) {
if (rectCircleCollide(obstacles[i], nx, ny, self.radius)) {
blocked = true;
break;
}
}
if (!blocked) {
self.x = nx;
self.y = ny;
}
}
// Shoot at player if in line of sight
if (self.shootCooldown > 0) self.shootCooldown--;else {
if (canSee(self.x, self.y, player.x, player.y)) {
var ddx = player.x - self.x;
var ddy = player.y - self.y;
var dmag = Math.sqrt(ddx * ddx + ddy * ddy);
if (dmag > 0) {
var bullet = new Bullet();
bullet.x = self.x + ddx / dmag * (self.radius + 30);
bullet.y = self.y + ddy / dmag * (self.radius + 30);
bullet.dx = ddx / dmag;
bullet.dy = ddy / dmag;
bullet.isPlayer = false;
bullet.speed = 13; // much slower enemy bullets
bullet.lifetime = 90; // slightly shorter range
bullets.push(bullet);
game.addChild(bullet);
self.shootCooldown = self.shootDelay + 60 + Math.floor(Math.random() * 40); // much slower fire rate
}
}
}
};
// Take damage
self.hit = function () {
if (!self.isAlive) return;
self.isAlive = false;
tween(sprite, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
// Obstacle class
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('obstacle', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = sprite.width;
self.height = sprite.height;
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 60;
self.speed = 28; // faster player
self.shootCooldown = 0;
self.shootDelay = 18; // frames
self.isAlive = true;
self.invincible = 0; // frames left of invincibility
// For touch drag
self.down = function (x, y, obj) {};
self.up = function (x, y, obj) {};
// For powerup
self.powered = false;
self.powerTimer = 0;
self.update = function () {
if (!self.isAlive) return;
if (self.invincible > 0) self.invincible--;
if (self.powered) {
self.powerTimer--;
if (self.powerTimer <= 0) {
self.powered = false;
sprite.tint = 0x2a6cff;
}
}
if (self.shootCooldown > 0) self.shootCooldown--;
};
// Shoot a bullet in direction (dx, dy)
self.shoot = function (dx, dy) {
if (!self.isAlive) return;
if (self.shootCooldown > 0) return;
var mag = Math.sqrt(dx * dx + dy * dy);
if (mag < 0.1) return;
var bullet = new Bullet();
bullet.x = self.x + dx / mag * (self.radius + 30);
bullet.y = self.y + dy / mag * (self.radius + 30);
bullet.dx = dx / mag;
bullet.dy = dy / mag;
bullet.isPlayer = true;
bullets.push(bullet);
game.addChild(bullet);
self.shootCooldown = self.powered ? 5 : self.shootDelay;
};
// Take damage
self.hit = function () {
if (self.invincible > 0 || !self.isAlive) return;
self.invincible = 60; // 1 second of invincibility (60 frames)
if (typeof playerHealth !== "undefined") {
playerHealth--;
// Update health display
if (healthTxt) {
var hearts = '';
for (var i = 0; i < playerHealth; ++i) hearts += '♥';
healthTxt.setText(hearts);
}
// Flash player sprite
tween(sprite, {
alpha: 0.3
}, {
duration: 80,
yoyo: true,
repeat: 5,
onFinish: function onFinish() {
sprite.alpha = 1;
}
});
if (playerHealth <= 0) {
self.isAlive = false;
tween(sprite, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
}
});
}
} else {
// fallback: kill player
self.isAlive = false;
tween(sprite, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
}
});
}
};
// Powerup
self.powerup = function () {
self.powered = true;
self.powerTimer = 360; // 6 seconds
sprite.tint = 0x3be14a;
};
return self;
});
// Powerup class
var Powerup = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 30;
self.type = 'rapid';
self.lifetime = 600; // 10 seconds
self.update = function () {
self.lifetime--;
};
self.apply = function () {
if (self.type === 'rapid') {
player.powerup();
powerupTxt.setText('Rapid Fire!');
} else if (self.type === 'health') {
if (typeof playerHealth !== "undefined" && playerHealth < playerMaxHealth) {
playerHealth++;
// Update health display
if (healthTxt) {
var hearts = '';
for (var i = 0; i < playerHealth; ++i) hearts += '♥';
healthTxt.setText(hearts);
}
powerupTxt.setText('Health Up!');
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Arena bounds
// Player character: blue box
// Enemy: red box
// Bullet: yellow ellipse
// Obstacle: gray box
// Powerup: green ellipse
var arena = {
x: 180,
y: 300,
w: 2048 - 360,
h: 2732 - 600
};
// Game state
var player;
var enemies = [];
var bullets = [];
var obstacles = [];
var powerups = [];
var round = 1;
var roundActive = false;
var roundTimer = 0;
var spawnTimer = 0;
var dragNode = null;
var lastTouch = {
x: 0,
y: 0
};
var scoreTxt;
var roundTxt;
var powerupTxt;
var gameOver = false;
// Health system
var playerMaxHealth = 3;
var playerHealth = playerMaxHealth;
var healthTxt;
// Countdown state
var countdownActive = false;
var countdownValue = 0;
var countdownTxt = null;
var countdownTimeout = null;
// GUI
scoreTxt = new Text2('Score: 0', {
size: 90,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
roundTxt = new Text2('Round 1', {
size: 70,
fill: 0xFFCC00
});
roundTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(roundTxt);
powerupTxt = new Text2('', {
size: 60,
fill: 0x3BE14A
});
powerupTxt.anchor.set(0.5, 0);
LK.gui.bottom.addChild(powerupTxt);
// Health display
healthTxt = new Text2('♥♥♥', {
size: 90,
fill: 0xFF4444
});
healthTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(healthTxt);
// Place obstacles
function setupObstacles() {
for (var i = 0; i < obstacles.length; ++i) obstacles[i].destroy();
obstacles = [];
// Place 4-6 obstacles randomly
var obsCount = 4 + Math.floor(Math.random() * 3);
for (var i = 0; i < obsCount; ++i) {
var obs = new Obstacle();
obs.x = arena.x + 200 + Math.random() * (arena.w - 400);
obs.y = arena.y + 200 + Math.random() * (arena.h - 400);
if (i % 2 == 0) obs.rotation = Math.PI / 2;
obstacles.push(obs);
game.addChild(obs);
}
}
// Place player
function setupPlayer() {
if (player) player.destroy();
player = new Player();
player.x = arena.x + arena.w / 2;
player.y = arena.y + arena.h - 200;
game.addChild(player);
}
// Place enemies
function setupEnemies(n) {
for (var i = 0; i < enemies.length; ++i) enemies[i].destroy();
enemies = [];
for (var i = 0; i < n; ++i) {
var e = new Enemy();
// Spawn at top or sides
var edge = Math.floor(Math.random() * 4);
if (edge == 0) {
// top
e.x = arena.x + 200 + Math.random() * (arena.w - 400);
e.y = arena.y + 120;
} else if (edge == 1) {
// left
e.x = arena.x + 120;
e.y = arena.y + 200 + Math.random() * (arena.h - 400);
} else if (edge == 2) {
// right
e.x = arena.x + arena.w - 120;
e.y = arena.y + 200 + Math.random() * (arena.h - 400);
} else {
// bottom
e.x = arena.x + 200 + Math.random() * (arena.w - 400);
e.y = arena.y + arena.h - 120;
}
enemies.push(e);
game.addChild(e);
}
}
// Place powerup
function spawnPowerup() {
var p = new Powerup();
p.x = arena.x + 150 + Math.random() * (arena.w - 300);
p.y = arena.y + 150 + Math.random() * (arena.h - 300);
// 50% chance for health, 50% for rapid
if (Math.random() < 0.5) {
p.type = 'health';
// visually distinguish health powerup
p.children[0].tint = 0xff4444;
} else {
p.type = 'rapid';
p.children[0].tint = 0x3be14a;
}
powerups.push(p);
game.addChild(p);
}
// Start round
function startRound() {
// Start countdown before round actually starts
roundActive = false;
countdownActive = true;
countdownValue = 3;
// Remove previous countdown text if exists
if (countdownTxt) {
countdownTxt.destroy();
countdownTxt = null;
}
countdownTxt = new Text2('3', {
size: 300,
fill: 0xFFCC00
});
countdownTxt.anchor.set(0.5, 0.5);
LK.gui.center.addChild(countdownTxt);
// Setup everything for the round, but don't activate yet
roundTimer = 0;
spawnTimer = 0;
setupObstacles();
setupPlayer();
setupEnemies(2 + round);
for (var i = 0; i < bullets.length; ++i) bullets[i].destroy();
bullets = [];
for (var i = 0; i < powerups.length; ++i) powerups[i].destroy();
powerups = [];
roundTxt.setText('Round ' + round);
powerupTxt.setText('');
LK.setScore(LK.getScore());
scoreTxt.setText('Score: ' + LK.getScore());
// Reset player health at start of round
playerHealth = playerMaxHealth;
if (healthTxt) {
var hearts = '';
for (var i = 0; i < playerHealth; ++i) hearts += '♥';
healthTxt.setText(hearts);
}
// Start countdown timer
function doCountdown() {
if (!countdownActive) return;
countdownValue--;
if (countdownValue > 0) {
countdownTxt.setText(countdownValue + '');
countdownTimeout = LK.setTimeout(doCountdown, 800);
} else {
// Go!
countdownTxt.setText('GO!');
countdownTimeout = LK.setTimeout(function () {
if (countdownTxt) {
countdownTxt.destroy();
countdownTxt = null;
}
countdownActive = false;
roundActive = true;
}, 600);
}
}
countdownTimeout = LK.setTimeout(doCountdown, 800);
}
// Utility: circle-rectangle collision
function rectCircleCollide(obs, cx, cy, cr) {
var rx = obs.x - obs.width / 2;
var ry = obs.y - obs.height / 2;
var rw = obs.width;
var rh = obs.height;
var closestX = Math.max(rx, Math.min(cx, rx + rw));
var closestY = Math.max(ry, Math.min(cy, ry + rh));
var dx = cx - closestX;
var dy = cy - closestY;
return dx * dx + dy * dy < cr * cr;
}
// Utility: circle-circle collision
function circleCollide(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
var r = (a.radius || 0) + (b.radius || 0);
return dx * dx + dy * dy < r * r;
}
// Utility: line of sight (returns true if no obstacle blocks)
function canSee(x1, y1, x2, y2) {
for (var i = 0; i < obstacles.length; ++i) {
if (lineRectIntersect(x1, y1, x2, y2, obstacles[i])) return false;
}
return true;
}
// Utility: line-rectangle intersection
function lineRectIntersect(x1, y1, x2, y2, obs) {
var rx = obs.x - obs.width / 2;
var ry = obs.y - obs.height / 2;
var rw = obs.width;
var rh = obs.height;
// Check each side
if (lineLineIntersect(x1, y1, x2, y2, rx, ry, rx + rw, ry)) return true;
if (lineLineIntersect(x1, y1, x2, y2, rx + rw, ry, rx + rw, ry + rh)) return true;
if (lineLineIntersect(x1, y1, x2, y2, rx + rw, ry + rh, rx, ry + rh)) return true;
if (lineLineIntersect(x1, y1, x2, y2, rx, ry + rh, rx, ry)) return true;
return false;
}
// Utility: line-line intersection
function lineLineIntersect(x1, y1, x2, y2, x3, y3, x4, y4) {
var denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
if (denom == 0) return false;
var ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom;
var ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom;
return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
}
// Clamp player inside arena and avoid obstacles
function clampPlayer(px, py) {
// Clamp to arena
px = Math.max(arena.x + player.radius, Math.min(arena.x + arena.w - player.radius, px));
py = Math.max(arena.y + player.radius, Math.min(arena.y + arena.h - player.radius, py));
// Avoid obstacles
for (var i = 0; i < obstacles.length; ++i) {
if (rectCircleCollide(obstacles[i], px, py, player.radius)) {
// Push out
var ox = obstacles[i].x;
var oy = obstacles[i].y;
var dx = px - ox;
var dy = py - oy;
var mag = Math.sqrt(dx * dx + dy * dy);
if (mag > 0) {
px = ox + dx / mag * (obstacles[i].width / 2 + player.radius + 2);
py = oy + dy / mag * (obstacles[i].height / 2 + player.radius + 2);
}
}
}
return {
x: px,
y: py
};
}
// Touch controls: drag to move, tap to shoot
game.down = function (x, y, obj) {
if (countdownActive) return;
if (!player.isAlive) return;
// If touch is near player, start drag
var dx = x - player.x;
var dy = y - player.y;
if (dx * dx + dy * dy < player.radius * player.radius * 2) {
dragNode = player;
} else {
// Else, shoot toward touch
var dirx = x - player.x;
var diry = y - player.y;
player.shoot(dirx, diry);
}
lastTouch.x = x;
lastTouch.y = y;
};
game.move = function (x, y, obj) {
if (countdownActive) return;
if (!player.isAlive) return;
if (dragNode === player) {
// Move player toward touch, but clamp
var dx = x - player.x;
var dy = y - player.y;
var mag = Math.sqrt(dx * dx + dy * dy);
if (mag > 0) {
var step = Math.min(player.speed, mag);
var nx = player.x + dx / mag * step;
var ny = player.y + dy / mag * step;
var clamped = clampPlayer(nx, ny);
player.x = clamped.x;
player.y = clamped.y;
}
lastTouch.x = x;
lastTouch.y = y;
}
};
game.up = function (x, y, obj) {
dragNode = null;
};
// Main update loop
game.update = function () {
// If countdown is active, pause gameplay updates
if (countdownActive) {
return;
}
if (!roundActive) {
startRound();
return;
}
if (!player.isAlive) return;
// Update player
player.update();
// Update enemies
for (var i = enemies.length - 1; i >= 0; --i) {
var e = enemies[i];
e.update();
// If dead, remove
if (!e.isAlive && e.alpha <= 0) {
e.destroy();
enemies.splice(i, 1);
LK.setScore(LK.getScore() + 1);
scoreTxt.setText('Score: ' + LK.getScore());
}
}
// Update bullets
for (var i = bullets.length - 1; i >= 0; --i) {
var b = bullets[i];
b.update();
// Remove if out of bounds or expired
if (b.x < arena.x - 100 || b.x > arena.x + arena.w + 100 || b.y < arena.y - 100 || b.y > arena.y + arena.h + 100 || b.lifetime <= 0) {
b.destroy();
bullets.splice(i, 1);
continue;
}
// Collide with obstacles
var hitObs = false;
for (var j = 0; j < obstacles.length; ++j) {
if (rectCircleCollide(obstacles[j], b.x, b.y, b.radius)) {
hitObs = true;
break;
}
}
if (hitObs) {
b.destroy();
bullets.splice(i, 1);
continue;
}
// Collide with enemies (if player bullet)
if (b.isPlayer) {
for (var j = 0; j < enemies.length; ++j) {
if (enemies[j].isAlive && circleCollide(b, enemies[j])) {
enemies[j].hit();
b.destroy();
bullets.splice(i, 1);
break;
}
}
} else {
// Collide with player
if (player.isAlive && circleCollide(b, player)) {
if (player.invincible <= 0) {
player.hit();
}
b.destroy();
bullets.splice(i, 1);
}
}
}
// Update powerups
for (var i = powerups.length - 1; i >= 0; --i) {
var p = powerups[i];
p.update();
if (p.lifetime <= 0) {
p.destroy();
powerups.splice(i, 1);
continue;
}
// Collide with player
if (player.isAlive && circleCollide(p, player)) {
if (typeof p.apply === "function") {
p.apply();
}
p.destroy();
powerups.splice(i, 1);
}
}
// Spawn powerup occasionally
if (LK.ticks % 600 == 0 && powerups.length == 0) {
spawnPowerup();
}
// End round if all enemies dead
if (enemies.length == 0) {
roundActive = false;
round++;
LK.setTimeout(function () {
startRound();
}, 1200);
}
};
// Initial round
startRound();