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