/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Creature projectile class with unique behaviors
var CreatureProjectile = Container.expand(function () {
var self = Container.call(this);
var asset = self.attachAsset('bulletModel', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 32;
self.speed = 32;
self.angle = 0;
self.damage = 1;
// Assign a unique behavior type to each projectile
var behaviorTypes = ['homing', 'zigzag', 'boomerang', 'spiral', 'dash', 'orbit', 'bounce'];
self.behavior = behaviorTypes[Math.floor(Math.random() * behaviorTypes.length)];
self.behaviorSeed = Math.random() * 10000;
self.behaviorTick = 0;
self.originX = self.x;
self.originY = self.y;
self.maxDistance = 1200 + Math.random() * 400;
self.orbitAngle = Math.random() * Math.PI * 2;
self.bounceCount = 0;
self.update = function () {
self.behaviorTick++;
// Always fly straight toward the nearest enemy (no random behaviors)
var nearest = null;
var minDist = 999999;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
var dx = e.x - self.x;
var dy = e.y - self.y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) {
minDist = d;
nearest = e;
}
}
if (nearest && minDist > 1) {
// Always point directly at the nearest enemy
self.angle = Math.atan2(nearest.y - self.y, nearest.x - self.x);
}
// Move forward in a straight line
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
// Save last position for AI
self.lastX = self.x;
self.lastY = self.y;
// Remove projectile if it has traveled too far from its origin
var dx0 = self.x - self.originX;
var dy0 = self.y - self.originY;
if (dx0 * dx0 + dy0 * dy0 > self.maxDistance * self.maxDistance) {
self.destroy();
}
};
return self;
});
// Creature weapon class (auto-attacks for player)
var CreatureWeapon = Container.expand(function () {
var self = Container.call(this);
// Attach asset (box, yellow)
var asset = self.attachAsset('creature', {
anchorX: 0.5,
anchorY: 0.5
});
// Attach weapon model (sword) as a visual
var weaponModel = self.attachAsset('weaponModel', {
anchorX: 0.5,
anchorY: 0.8
});
weaponModel.y = 60; // Offset so it appears in front of the player
weaponModel.rotation = Math.PI / 8 * (Math.random() > 0.5 ? 1 : -1); // Slight random angle for variety
weaponModel.alpha = 0.85;
// Weapon stats
self.damage = 1;
self.speed = 32;
self.cooldown = 60; // frames between shots
self.timer = 0;
self.projectileCount = 1;
self.spread = 0.2; // radians
// Update method: fires projectiles if timer is up
self.update = function () {
if (self.timer > 0) {
self.timer--;
} else {
self.fire();
self.timer = self.cooldown;
}
};
// Fire projectiles in a spread
self.fire = function () {
// Projectiles orbit and launch from around the player
var now = Date.now();
for (var i = 0; i < self.projectileCount; i++) {
// Orbit offset for each projectile
var orbitAngle = 2 * Math.PI / self.projectileCount * i + now % 1000 / 1000 * Math.PI * 2;
var launchDist = 120 + Math.random() * 30;
var px = player.x + Math.cos(orbitAngle) * launchDist;
var py = player.y + Math.sin(orbitAngle) * launchDist;
// Clamp projectile spawn to play area
if (px < 0) px = 0;
if (px > GAME_WIDTH) px = GAME_WIDTH;
if (py < 0) py = 0;
if (py > GAME_HEIGHT) py = GAME_HEIGHT;
// Aim at nearest enemy if possible
var target = null;
var minDist = 999999;
for (var j = 0; j < enemies.length; j++) {
var e = enemies[j];
var dx = e.x - px;
var dy = e.y - py;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) {
minDist = d;
target = e;
}
}
var angle;
if (target) {
angle = Math.atan2(target.y - py, target.x - px);
} else {
angle = orbitAngle;
}
var proj = new CreatureProjectile();
proj.x = px;
proj.y = py;
proj.angle = angle;
proj.speed = self.speed;
proj.damage = self.damage;
projectiles.push(proj);
game.addChild(proj);
}
};
// Upgrade weapon
self.upgrade = function (type) {
if (type === 'damage') self.damage++;
if (type === 'speed') self.speed += 6;
if (type === 'cooldown' && self.cooldown > 20) self.cooldown -= 8;
if (type === 'projectile') self.projectileCount++;
if (type === 'spread') self.spread += 0.1;
if (type === 'area') {
// Add area damage property
if (!self.areaDamage) self.areaDamage = 1;else self.areaDamage += 1;
}
if (type === 'pierce') {
// Add piercing property
if (!self.pierce) self.pierce = 1;else self.pierce += 1;
}
if (type === 'regen') {
// Add regen to player
if (!player.regen) player.regen = 1;else player.regen += 1;
}
if (type === 'expBoost') {
// Add exp boost to player
if (!player.expBoost) player.expBoost = 0.2;else player.expBoost += 0.2;
}
};
return self;
});
// Enemy class
var Enemy = Container.expand(function () {
var self = Container.call(this);
var asset = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 48;
// Slow down very fast speeds of living beings (enemies)
self.speed = 6 + Math.random() * 2.2; // reduced max speed from 9 to 8.2
if (self.speed > 8) self.speed = 8; // clamp to max 8
self.hp = 2 + Math.floor(wave * 0.5);
self.maxHp = self.hp;
// For movement
self.update = function () {
// Save last position for AI
self.lastX = self.x;
self.lastY = self.y;
// Move towards player
var dx = player.x - self.x;
var dy = player.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
};
// Take damage
self.takeDamage = function (amount) {
self.hp -= amount;
LK.effects.flashObject(self, 0xffff00, 200);
if (self.hp <= 0) {
spawnExp(self.x, self.y, 1 + Math.floor(wave * 0.2));
self.destroy();
return true;
}
return false;
};
return self;
});
// Experience gem class
var ExpGem = Container.expand(function () {
var self = Container.call(this);
var asset = self.attachAsset('expGem', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 28;
self.value = 1;
self.update = function () {
// Move towards player if close
var dx = player.x - self.x;
var dy = player.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var moveTarget = null;
var minDist = dist;
if (dist < 300) {
moveTarget = player;
minDist = dist;
}
// Also move towards living beings (enemies) that have weapons in their hands
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
// Assume enemies with weapons have a property hasWeapon === true
if (e.hasWeapon) {
var edx = e.x - self.x;
var edy = e.y - self.y;
var edist = Math.sqrt(edx * edx + edy * edy);
if (edist < 300 && edist < minDist) {
moveTarget = e;
minDist = edist;
}
}
}
if (moveTarget && minDist < 300) {
var mdx = moveTarget.x - self.x;
var mdy = moveTarget.y - self.y;
var mlen = Math.sqrt(mdx * mdx + mdy * mdy);
if (mlen > 0) {
self.x += mdx / mlen * 18;
self.y += mdy / mlen * 18;
}
}
};
return self;
});
// Player character class
var Player = Container.expand(function () {
var self = Container.call(this);
// Attach player asset (ellipse, green)
var playerAsset = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// Attach weapon model (gun) to player's hand
var gunModel = self.attachAsset('weaponModel', {
anchorX: 0.2,
// grip point
anchorY: 0.7
});
gunModel.x = 38; // offset to player's right hand
gunModel.y = 38;
gunModel.rotation = Math.PI / 8;
gunModel.alpha = 0.95;
self.gunModel = gunModel;
// Set up player stats
self.radius = 60; // for collision
self.speed = 18; // movement speed per frame
self.level = 1;
self.exp = 0;
self.expToLevel = 10;
self.hp = 3;
self.maxHp = 3;
// For movement smoothing
self.targetX = GAME_WIDTH / 2;
self.targetY = GAME_HEIGHT / 2;
// For invulnerability after hit
self.invulnTicks = 0;
// Update method
self.update = function () {
// Smoothly move towards target
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > self.speed) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
} else {
self.x = self.targetX;
self.y = self.targetY;
}
// Clamp to game area
if (self.x < self.radius) self.x = self.radius;
if (self.x > GAME_WIDTH - self.radius) self.x = GAME_WIDTH - self.radius;
if (self.y < self.radius) self.y = self.radius;
if (self.y > GAME_HEIGHT - self.radius) self.y = GAME_HEIGHT - self.radius;
// Invulnerability timer
if (self.invulnTicks > 0) {
self.invulnTicks--;
playerAsset.alpha = self.invulnTicks % 10 < 5 ? 0.5 : 1;
} else {
playerAsset.alpha = 1;
}
// Update gun model to follow hand (right side of player)
if (self.gunModel) {
// Gun follows player's facing direction (right side)
self.gunModel.x = self.radius * 0.55;
self.gunModel.y = self.radius * 0.25;
// Find nearest living creature (enemy)
var nearest = null;
var minDist = 999999;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
var dx = e.x - (self.x + self.gunModel.x);
var dy = e.y - (self.y + self.gunModel.y);
var d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) {
minDist = d;
nearest = e;
}
}
if (nearest) {
// Point gun barrel at nearest enemy
var dx = nearest.x - (self.x + self.gunModel.x);
var dy = nearest.y - (self.y + self.gunModel.y);
self.gunModel.rotation = Math.atan2(dy, dx);
} else {
// Optionally, add a little bobbing for effect if no enemy
self.gunModel.rotation = Math.PI / 8 + Math.sin(LK.ticks * 0.08) * 0.08;
}
}
};
// Level up
self.gainExp = function (amount) {
var gain = amount;
if (self.expBoost) gain = Math.floor(amount * (1 + self.expBoost));
self.exp += gain;
while (self.exp >= self.expToLevel) {
self.exp -= self.expToLevel;
self.level++;
self.expToLevel = Math.floor(self.expToLevel * 1.3 + 5);
showLevelUp();
}
updateExpBar();
};
// Take damage
self.takeDamage = function () {
if (self.invulnTicks > 0) return;
self.hp--;
updateHpBar();
self.invulnTicks = 60;
LK.effects.flashObject(self, 0xff0000, 500);
if (self.hp <= 0) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// Level up tab model (for future customization)
// Weapon model asset (simple sword shape)
// --- GUI Elements ---
var player;
var creatureWeapon;
var projectiles = [];
var enemies = [];
var expGems = [];
var wave = 1; // still used for scaling difficulty
var dragNode = null;
// --- GUI Elements ---
var scoreTxt = new Text2('0', {
size: 100,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var expBar = new Text2('', {
size: 60,
fill: 0xAAFF00
});
expBar.anchor.set(0.5, 0);
LK.gui.top.addChild(expBar);
var hpBar = new Text2('', {
size: 60,
fill: 0xFF4444
});
hpBar.anchor.set(0.5, 0);
LK.gui.top.addChild(hpBar);
// --- Level Progress Bar ---
var levelProgressBar = new Text2('', {
size: 48,
fill: 0x00EAFF
});
levelProgressBar.anchor.set(0.5, 0);
LK.gui.top.addChild(levelProgressBar);
// --- Asset Screen Button ---
var assetScreenBtn = new Text2('Assets', {
size: 60,
fill: "#fff"
});
assetScreenBtn.anchor.set(0.5, 0.5);
assetScreenBtn.x = 1800;
assetScreenBtn.y = 80;
LK.gui.top.addChild(assetScreenBtn);
var assetScreenPopup = null;
assetScreenBtn.down = function (x, y, obj) {
if (assetScreenPopup) return;
assetScreenPopup = new Container();
var bg = LK.getAsset('levelUpBg', {
width: 1200,
height: 1200,
color: 0x222244,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5
});
assetScreenPopup.addChild(bg);
bg.x = 0;
bg.y = 0;
var title = new Text2('Assets', {
size: 120,
fill: "#fff"
});
title.anchor.set(0.5, 0);
title.x = 0;
title.y = -520;
assetScreenPopup.addChild(title);
// Show player, gun, and police-compatible asset (enemy)
var yStart = -320;
var yStep = 320;
var assetList = [{
id: 'player',
label: 'Player'
}, {
id: 'weaponModel',
label: 'Gun'
}, {
id: 'enemy',
label: 'Police'
}];
for (var i = 0; i < assetList.length; i++) {
var assetInfo = assetList[i];
var assetIcon = LK.getAsset(assetInfo.id, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
assetIcon.x = -300;
assetIcon.y = yStart + i * yStep;
assetScreenPopup.addChild(assetIcon);
var label = new Text2(assetInfo.label, {
size: 80,
fill: "#fff"
});
label.anchor.set(0, 0.5);
label.x = -180;
label.y = assetIcon.y;
assetScreenPopup.addChild(label);
}
// Close button
var closeBtn = LK.getAsset('levelUpBtnClose', {
width: 300,
height: 100,
color: 0x4444aa,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5
});
closeBtn.x = 0;
closeBtn.y = 520;
assetScreenPopup.addChild(closeBtn);
var closeTxt = new Text2('Close', {
size: 60,
fill: "#fff"
});
closeTxt.anchor.set(0.5, 0.5);
closeTxt.x = 0;
closeTxt.y = closeBtn.y;
assetScreenPopup.addChild(closeTxt);
closeBtn.down = function (x, y, obj) {
LK.gui.center.removeChild(assetScreenPopup);
assetScreenPopup = null;
};
assetScreenPopup.x = 0;
assetScreenPopup.y = 0;
LK.gui.center.addChild(assetScreenPopup);
};
function updateExpBar() {
expBar.setText('LV ' + player.level + ' EXP: ' + player.exp + '/' + player.expToLevel);
// Level progress bar
var percent = Math.min(1, player.exp / player.expToLevel);
var barLen = 600;
var filled = Math.floor(barLen * percent);
var empty = barLen - filled;
var bar = '';
for (var i = 0; i < filled / 20; i++) bar += '█';
for (var i = 0; i < empty / 20; i++) bar += '░';
levelProgressBar.setText('Level Progress: ' + bar + ' ' + Math.floor(percent * 100) + '%');
}
function updateHpBar() {
hpBar.setText('HP: ' + player.hp + '/' + player.maxHp);
}
function updateScore() {
scoreTxt.setText('Wave: ' + wave);
}
// --- Asset initialization (shapes) ---
// --- Game setup ---
// Add background model (for future customization)
var backgroundModel = LK.getAsset('levelUpBg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: GAME_WIDTH / 100,
scaleY: GAME_HEIGHT / 100,
x: GAME_WIDTH / 2,
y: GAME_HEIGHT / 2
});
game.addChild(backgroundModel);
player = new Player();
// Expanded play area
var GAME_WIDTH = 3072;
var GAME_HEIGHT = 4096;
player.x = GAME_WIDTH / 2;
player.y = GAME_HEIGHT / 2;
game.addChild(player);
creatureWeapon = new CreatureWeapon();
game.addChild(creatureWeapon);
updateExpBar();
updateHpBar();
updateScore();
// --- Level up popup ---
var levelUpPopup = null;
function showLevelUp() {
if (levelUpPopup) return; // Already showing
// Randomly pick 3 upgrades
var upgrades = [{
type: 'damage',
label: 'Creature Damage +'
}, {
type: 'speed',
label: 'Creature Speed +'
}, {
type: 'cooldown',
label: 'Faster Attacks'
}, {
type: 'projectile',
label: 'More Projectiles'
}, {
type: 'spread',
label: 'Wider Spread'
}, {
type: 'area',
label: 'Area Damage (AOE)'
}, {
type: 'pierce',
label: 'Piercing Projectiles'
}, {
type: 'regen',
label: 'Regenerate HP'
}, {
type: 'expBoost',
label: 'EXP Gain +'
}];
// Shuffle and pick 3
for (var i = upgrades.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = upgrades[i];
upgrades[i] = upgrades[j];
upgrades[j] = t;
}
var options = upgrades.slice(0, 3);
// Popup container
levelUpPopup = new Container();
var bg = LK.getAsset('levelUpTab', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.2,
scaleY: 6
});
levelUpPopup.addChild(bg);
bg.x = 0;
bg.y = 0;
var title = new Text2('LEVEL UP!', {
size: 120,
fill: "#fff"
});
title.anchor.set(0.5, 0);
title.x = 0;
title.y = -300;
levelUpPopup.addChild(title);
// Option buttons
for (var i = 0; i < 3; i++) {
(function (idx) {
var opt = options[idx];
var btn = LK.getAsset('levelUpBtn' + idx, {
width: 700,
height: 140,
color: 0x4444aa,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5
});
btn.x = 0;
btn.y = -80 + idx * 180;
levelUpPopup.addChild(btn);
var txt = new Text2(opt.label, {
size: 70,
fill: "#fff"
});
txt.anchor.set(0.5, 0.5);
txt.x = 0;
txt.y = btn.y;
levelUpPopup.addChild(txt);
btn.down = function (x, y, obj) {
creatureWeapon.upgrade(opt.type);
// Remove popup
LK.gui.center.removeChild(levelUpPopup);
levelUpPopup = null;
};
})(i);
}
levelUpPopup.x = 0;
levelUpPopup.y = 0;
LK.gui.center.addChild(levelUpPopup);
}
// --- Spawn experience gem ---
function spawnExp(x, y, value) {
var gem = new ExpGem();
gem.x = x;
gem.y = y;
gem.value = value;
expGems.push(gem);
game.addChild(gem);
}
// --- Continuous enemy spawn system ---
var spawnDelay = 32; // frames between spawns, will decrease as wave increases
var spawnDelayTimer = 0;
// Add a delay before the first enemy spawns
var initialSpawnDelay = 120; // 2 seconds at 60fps
var initialSpawnDelayTimer = 0;
// Limit for max living enemies, starts low and increases over time
var maxLivingEnemies = 3;
var maxLivingEnemiesIncreaseInterval = 60 * 60 * 3; // 3 minutes at 60fps
var lastMaxLivingEnemiesIncreaseTick = 0;
function updateSpawnRate() {
// Increase spawn rate over time, but clamp to a minimum
spawnDelay = Math.max(4, 32 - Math.floor(wave * 1.2));
}
function spawnEnemy() {
if (enemies.length >= maxLivingEnemies) return; // Don't spawn if at max
var enemy = new Enemy();
// Give some enemies weapons in their hands (e.g. 30% chance)
enemy.hasWeapon = Math.random() < 0.3;
// Spawn at random edge
var edge = Math.floor(Math.random() * 4);
if (edge === 0) {
// top
enemy.x = Math.random() * GAME_WIDTH;
enemy.y = -100;
} else if (edge === 1) {
// bottom
enemy.x = Math.random() * GAME_WIDTH;
enemy.y = GAME_HEIGHT + 100;
} else if (edge === 2) {
// left
enemy.x = -100;
enemy.y = Math.random() * GAME_HEIGHT;
} else {
// right
enemy.x = GAME_WIDTH + 100;
enemy.y = Math.random() * GAME_HEIGHT;
}
enemies.push(enemy);
game.addChild(enemy);
}
// --- Game move handler (touch/mouse drag to move player) ---
function handleMove(x, y, obj) {
// Don't allow movement if level up popup is open
if (levelUpPopup) return;
// Clamp to avoid top left menu
if (x < 100) x = 100;
if (y < 0) y = 0;
player.targetX = x;
player.targetY = y;
}
game.move = handleMove;
game.down = function (x, y, obj) {
handleMove(x, y, obj);
};
game.up = function (x, y, obj) {
// No dragNode needed, movement is always allowed
};
// --- Main game update loop ---
game.update = function () {
// Pause all game logic except GUI and popup when level up popup is open
if (levelUpPopup) {
// Only update GUI, skip all gameplay logic
return;
}
// Player regen
if (player.regen && LK.ticks % 90 === 0 && player.hp < player.maxHp) {
player.hp += player.regen;
if (player.hp > player.maxHp) player.hp = player.maxHp;
updateHpBar();
}
// Update player
player.update();
// Update weapon
creatureWeapon.update();
// Update projectiles
for (var i = projectiles.length - 1; i >= 0; i--) {
var proj = projectiles[i];
proj.update();
// Remove if out of bounds
if (proj.x < -100 || proj.x > GAME_WIDTH + 100 || proj.y < -100 || proj.y > GAME_HEIGHT + 100) {
proj.destroy();
projectiles.splice(i, 1);
continue;
}
// Check collision with enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
var dx = proj.x - enemy.x;
var dy = proj.y - enemy.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < proj.radius + enemy.radius) {
var killed = enemy.takeDamage(proj.damage);
// Area damage
if (creatureWeapon.areaDamage) {
for (var k = 0; k < enemies.length; k++) {
if (k !== j) {
var e2 = enemies[k];
var d2 = Math.sqrt((proj.x - e2.x) * (proj.x - e2.x) + (proj.y - e2.y) * (proj.y - e2.y));
if (d2 < 180) {
e2.takeDamage(creatureWeapon.areaDamage);
}
}
}
}
// Piercing
if (creatureWeapon.pierce && proj.pierced === undefined) {
proj.pierced = 1;
}
if (creatureWeapon.pierce && proj.pierced < creatureWeapon.pierce) {
proj.pierced++;
// Do not destroy, allow to hit more enemies
} else {
proj.destroy();
projectiles.splice(i, 1);
}
if (killed) {
enemies.splice(j, 1);
}
break;
}
}
}
// Update enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemy.update();
// Check collision with player
var dx = enemy.x - player.x;
var dy = enemy.y - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// If enemy is still alive and close enough to player, but projectiles should kill before touching
if (dist < enemy.radius + player.radius) {
// Check if any projectile is close enough to kill this enemy before it touches the player
var killedByProjectile = false;
for (var p = 0; p < projectiles.length; p++) {
var proj = projectiles[p];
var pdx = proj.x - enemy.x;
var pdy = proj.y - enemy.y;
var pdist = Math.sqrt(pdx * pdx + pdy * pdy);
if (pdist < proj.radius + enemy.radius) {
var killed = enemy.takeDamage(proj.damage);
proj.destroy();
projectiles.splice(p, 1);
if (killed) {
enemies.splice(i, 1);
killedByProjectile = true;
}
break;
}
}
// Only damage player if enemy was not killed by projectile and not during level up
if (!killedByProjectile && !levelUpPopup) {
player.takeDamage();
// Knockback enemy
var angle = Math.atan2(dy, dx);
enemy.x += Math.cos(angle) * 120;
enemy.y += Math.sin(angle) * 120;
}
// Prevent creatures from causing damage while level up popup is open
if (!killedByProjectile && levelUpPopup) {
// Do not damage player or apply knockback
}
}
}
// Update exp gems
for (var i = expGems.length - 1; i >= 0; i--) {
var gem = expGems[i];
gem.update();
// Collect if close to player
var dx = gem.x - player.x;
var dy = gem.y - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < gem.radius + player.radius) {
player.gainExp(gem.value);
gem.destroy();
expGems.splice(i, 1);
}
}
// Continuous enemy spawn (no waves)
if (!levelUpPopup) {
// Wait for initial spawn delay before starting enemy spawns
if (initialSpawnDelayTimer < initialSpawnDelay) {
initialSpawnDelayTimer++;
} else {
spawnDelayTimer++;
if (spawnDelayTimer >= spawnDelay) {
spawnEnemy();
spawnDelayTimer = 0;
}
// Increase difficulty over time, every 10 seconds
if (LK.ticks % (60 * 10) === 0) {
wave++;
updateScore();
updateSpawnRate();
}
// Increase maxLivingEnemies by 1 every 3 minutes
if (LK.ticks - lastMaxLivingEnemiesIncreaseTick >= maxLivingEnemiesIncreaseInterval) {
maxLivingEnemies++;
lastMaxLivingEnemiesIncreaseTick = LK.ticks;
}
}
}
};
// --- GUI positioning ---
scoreTxt.x = GAME_WIDTH / 2;
scoreTxt.y = 20;
expBar.x = GAME_WIDTH / 2;
expBar.y = 140;
levelProgressBar.x = GAME_WIDTH / 2;
levelProgressBar.y = 200;
hpBar.x = GAME_WIDTH / 2;
hpBar.y = 280;
assetScreenBtn.x = GAME_WIDTH - 200;
assetScreenBtn.y = 80;
// --- Start first wave ---
// No prepareWave needed; enemy spawning is now continuous. /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Creature projectile class with unique behaviors
var CreatureProjectile = Container.expand(function () {
var self = Container.call(this);
var asset = self.attachAsset('bulletModel', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 32;
self.speed = 32;
self.angle = 0;
self.damage = 1;
// Assign a unique behavior type to each projectile
var behaviorTypes = ['homing', 'zigzag', 'boomerang', 'spiral', 'dash', 'orbit', 'bounce'];
self.behavior = behaviorTypes[Math.floor(Math.random() * behaviorTypes.length)];
self.behaviorSeed = Math.random() * 10000;
self.behaviorTick = 0;
self.originX = self.x;
self.originY = self.y;
self.maxDistance = 1200 + Math.random() * 400;
self.orbitAngle = Math.random() * Math.PI * 2;
self.bounceCount = 0;
self.update = function () {
self.behaviorTick++;
// Always fly straight toward the nearest enemy (no random behaviors)
var nearest = null;
var minDist = 999999;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
var dx = e.x - self.x;
var dy = e.y - self.y;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) {
minDist = d;
nearest = e;
}
}
if (nearest && minDist > 1) {
// Always point directly at the nearest enemy
self.angle = Math.atan2(nearest.y - self.y, nearest.x - self.x);
}
// Move forward in a straight line
self.x += Math.cos(self.angle) * self.speed;
self.y += Math.sin(self.angle) * self.speed;
// Save last position for AI
self.lastX = self.x;
self.lastY = self.y;
// Remove projectile if it has traveled too far from its origin
var dx0 = self.x - self.originX;
var dy0 = self.y - self.originY;
if (dx0 * dx0 + dy0 * dy0 > self.maxDistance * self.maxDistance) {
self.destroy();
}
};
return self;
});
// Creature weapon class (auto-attacks for player)
var CreatureWeapon = Container.expand(function () {
var self = Container.call(this);
// Attach asset (box, yellow)
var asset = self.attachAsset('creature', {
anchorX: 0.5,
anchorY: 0.5
});
// Attach weapon model (sword) as a visual
var weaponModel = self.attachAsset('weaponModel', {
anchorX: 0.5,
anchorY: 0.8
});
weaponModel.y = 60; // Offset so it appears in front of the player
weaponModel.rotation = Math.PI / 8 * (Math.random() > 0.5 ? 1 : -1); // Slight random angle for variety
weaponModel.alpha = 0.85;
// Weapon stats
self.damage = 1;
self.speed = 32;
self.cooldown = 60; // frames between shots
self.timer = 0;
self.projectileCount = 1;
self.spread = 0.2; // radians
// Update method: fires projectiles if timer is up
self.update = function () {
if (self.timer > 0) {
self.timer--;
} else {
self.fire();
self.timer = self.cooldown;
}
};
// Fire projectiles in a spread
self.fire = function () {
// Projectiles orbit and launch from around the player
var now = Date.now();
for (var i = 0; i < self.projectileCount; i++) {
// Orbit offset for each projectile
var orbitAngle = 2 * Math.PI / self.projectileCount * i + now % 1000 / 1000 * Math.PI * 2;
var launchDist = 120 + Math.random() * 30;
var px = player.x + Math.cos(orbitAngle) * launchDist;
var py = player.y + Math.sin(orbitAngle) * launchDist;
// Clamp projectile spawn to play area
if (px < 0) px = 0;
if (px > GAME_WIDTH) px = GAME_WIDTH;
if (py < 0) py = 0;
if (py > GAME_HEIGHT) py = GAME_HEIGHT;
// Aim at nearest enemy if possible
var target = null;
var minDist = 999999;
for (var j = 0; j < enemies.length; j++) {
var e = enemies[j];
var dx = e.x - px;
var dy = e.y - py;
var d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) {
minDist = d;
target = e;
}
}
var angle;
if (target) {
angle = Math.atan2(target.y - py, target.x - px);
} else {
angle = orbitAngle;
}
var proj = new CreatureProjectile();
proj.x = px;
proj.y = py;
proj.angle = angle;
proj.speed = self.speed;
proj.damage = self.damage;
projectiles.push(proj);
game.addChild(proj);
}
};
// Upgrade weapon
self.upgrade = function (type) {
if (type === 'damage') self.damage++;
if (type === 'speed') self.speed += 6;
if (type === 'cooldown' && self.cooldown > 20) self.cooldown -= 8;
if (type === 'projectile') self.projectileCount++;
if (type === 'spread') self.spread += 0.1;
if (type === 'area') {
// Add area damage property
if (!self.areaDamage) self.areaDamage = 1;else self.areaDamage += 1;
}
if (type === 'pierce') {
// Add piercing property
if (!self.pierce) self.pierce = 1;else self.pierce += 1;
}
if (type === 'regen') {
// Add regen to player
if (!player.regen) player.regen = 1;else player.regen += 1;
}
if (type === 'expBoost') {
// Add exp boost to player
if (!player.expBoost) player.expBoost = 0.2;else player.expBoost += 0.2;
}
};
return self;
});
// Enemy class
var Enemy = Container.expand(function () {
var self = Container.call(this);
var asset = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 48;
// Slow down very fast speeds of living beings (enemies)
self.speed = 6 + Math.random() * 2.2; // reduced max speed from 9 to 8.2
if (self.speed > 8) self.speed = 8; // clamp to max 8
self.hp = 2 + Math.floor(wave * 0.5);
self.maxHp = self.hp;
// For movement
self.update = function () {
// Save last position for AI
self.lastX = self.x;
self.lastY = self.y;
// Move towards player
var dx = player.x - self.x;
var dy = player.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
}
};
// Take damage
self.takeDamage = function (amount) {
self.hp -= amount;
LK.effects.flashObject(self, 0xffff00, 200);
if (self.hp <= 0) {
spawnExp(self.x, self.y, 1 + Math.floor(wave * 0.2));
self.destroy();
return true;
}
return false;
};
return self;
});
// Experience gem class
var ExpGem = Container.expand(function () {
var self = Container.call(this);
var asset = self.attachAsset('expGem', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = 28;
self.value = 1;
self.update = function () {
// Move towards player if close
var dx = player.x - self.x;
var dy = player.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var moveTarget = null;
var minDist = dist;
if (dist < 300) {
moveTarget = player;
minDist = dist;
}
// Also move towards living beings (enemies) that have weapons in their hands
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
// Assume enemies with weapons have a property hasWeapon === true
if (e.hasWeapon) {
var edx = e.x - self.x;
var edy = e.y - self.y;
var edist = Math.sqrt(edx * edx + edy * edy);
if (edist < 300 && edist < minDist) {
moveTarget = e;
minDist = edist;
}
}
}
if (moveTarget && minDist < 300) {
var mdx = moveTarget.x - self.x;
var mdy = moveTarget.y - self.y;
var mlen = Math.sqrt(mdx * mdx + mdy * mdy);
if (mlen > 0) {
self.x += mdx / mlen * 18;
self.y += mdy / mlen * 18;
}
}
};
return self;
});
// Player character class
var Player = Container.expand(function () {
var self = Container.call(this);
// Attach player asset (ellipse, green)
var playerAsset = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// Attach weapon model (gun) to player's hand
var gunModel = self.attachAsset('weaponModel', {
anchorX: 0.2,
// grip point
anchorY: 0.7
});
gunModel.x = 38; // offset to player's right hand
gunModel.y = 38;
gunModel.rotation = Math.PI / 8;
gunModel.alpha = 0.95;
self.gunModel = gunModel;
// Set up player stats
self.radius = 60; // for collision
self.speed = 18; // movement speed per frame
self.level = 1;
self.exp = 0;
self.expToLevel = 10;
self.hp = 3;
self.maxHp = 3;
// For movement smoothing
self.targetX = GAME_WIDTH / 2;
self.targetY = GAME_HEIGHT / 2;
// For invulnerability after hit
self.invulnTicks = 0;
// Update method
self.update = function () {
// Smoothly move towards target
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > self.speed) {
self.x += dx / dist * self.speed;
self.y += dy / dist * self.speed;
} else {
self.x = self.targetX;
self.y = self.targetY;
}
// Clamp to game area
if (self.x < self.radius) self.x = self.radius;
if (self.x > GAME_WIDTH - self.radius) self.x = GAME_WIDTH - self.radius;
if (self.y < self.radius) self.y = self.radius;
if (self.y > GAME_HEIGHT - self.radius) self.y = GAME_HEIGHT - self.radius;
// Invulnerability timer
if (self.invulnTicks > 0) {
self.invulnTicks--;
playerAsset.alpha = self.invulnTicks % 10 < 5 ? 0.5 : 1;
} else {
playerAsset.alpha = 1;
}
// Update gun model to follow hand (right side of player)
if (self.gunModel) {
// Gun follows player's facing direction (right side)
self.gunModel.x = self.radius * 0.55;
self.gunModel.y = self.radius * 0.25;
// Find nearest living creature (enemy)
var nearest = null;
var minDist = 999999;
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
var dx = e.x - (self.x + self.gunModel.x);
var dy = e.y - (self.y + self.gunModel.y);
var d = Math.sqrt(dx * dx + dy * dy);
if (d < minDist) {
minDist = d;
nearest = e;
}
}
if (nearest) {
// Point gun barrel at nearest enemy
var dx = nearest.x - (self.x + self.gunModel.x);
var dy = nearest.y - (self.y + self.gunModel.y);
self.gunModel.rotation = Math.atan2(dy, dx);
} else {
// Optionally, add a little bobbing for effect if no enemy
self.gunModel.rotation = Math.PI / 8 + Math.sin(LK.ticks * 0.08) * 0.08;
}
}
};
// Level up
self.gainExp = function (amount) {
var gain = amount;
if (self.expBoost) gain = Math.floor(amount * (1 + self.expBoost));
self.exp += gain;
while (self.exp >= self.expToLevel) {
self.exp -= self.expToLevel;
self.level++;
self.expToLevel = Math.floor(self.expToLevel * 1.3 + 5);
showLevelUp();
}
updateExpBar();
};
// Take damage
self.takeDamage = function () {
if (self.invulnTicks > 0) return;
self.hp--;
updateHpBar();
self.invulnTicks = 60;
LK.effects.flashObject(self, 0xff0000, 500);
if (self.hp <= 0) {
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// Level up tab model (for future customization)
// Weapon model asset (simple sword shape)
// --- GUI Elements ---
var player;
var creatureWeapon;
var projectiles = [];
var enemies = [];
var expGems = [];
var wave = 1; // still used for scaling difficulty
var dragNode = null;
// --- GUI Elements ---
var scoreTxt = new Text2('0', {
size: 100,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
var expBar = new Text2('', {
size: 60,
fill: 0xAAFF00
});
expBar.anchor.set(0.5, 0);
LK.gui.top.addChild(expBar);
var hpBar = new Text2('', {
size: 60,
fill: 0xFF4444
});
hpBar.anchor.set(0.5, 0);
LK.gui.top.addChild(hpBar);
// --- Level Progress Bar ---
var levelProgressBar = new Text2('', {
size: 48,
fill: 0x00EAFF
});
levelProgressBar.anchor.set(0.5, 0);
LK.gui.top.addChild(levelProgressBar);
// --- Asset Screen Button ---
var assetScreenBtn = new Text2('Assets', {
size: 60,
fill: "#fff"
});
assetScreenBtn.anchor.set(0.5, 0.5);
assetScreenBtn.x = 1800;
assetScreenBtn.y = 80;
LK.gui.top.addChild(assetScreenBtn);
var assetScreenPopup = null;
assetScreenBtn.down = function (x, y, obj) {
if (assetScreenPopup) return;
assetScreenPopup = new Container();
var bg = LK.getAsset('levelUpBg', {
width: 1200,
height: 1200,
color: 0x222244,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5
});
assetScreenPopup.addChild(bg);
bg.x = 0;
bg.y = 0;
var title = new Text2('Assets', {
size: 120,
fill: "#fff"
});
title.anchor.set(0.5, 0);
title.x = 0;
title.y = -520;
assetScreenPopup.addChild(title);
// Show player, gun, and police-compatible asset (enemy)
var yStart = -320;
var yStep = 320;
var assetList = [{
id: 'player',
label: 'Player'
}, {
id: 'weaponModel',
label: 'Gun'
}, {
id: 'enemy',
label: 'Police'
}];
for (var i = 0; i < assetList.length; i++) {
var assetInfo = assetList[i];
var assetIcon = LK.getAsset(assetInfo.id, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2
});
assetIcon.x = -300;
assetIcon.y = yStart + i * yStep;
assetScreenPopup.addChild(assetIcon);
var label = new Text2(assetInfo.label, {
size: 80,
fill: "#fff"
});
label.anchor.set(0, 0.5);
label.x = -180;
label.y = assetIcon.y;
assetScreenPopup.addChild(label);
}
// Close button
var closeBtn = LK.getAsset('levelUpBtnClose', {
width: 300,
height: 100,
color: 0x4444aa,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5
});
closeBtn.x = 0;
closeBtn.y = 520;
assetScreenPopup.addChild(closeBtn);
var closeTxt = new Text2('Close', {
size: 60,
fill: "#fff"
});
closeTxt.anchor.set(0.5, 0.5);
closeTxt.x = 0;
closeTxt.y = closeBtn.y;
assetScreenPopup.addChild(closeTxt);
closeBtn.down = function (x, y, obj) {
LK.gui.center.removeChild(assetScreenPopup);
assetScreenPopup = null;
};
assetScreenPopup.x = 0;
assetScreenPopup.y = 0;
LK.gui.center.addChild(assetScreenPopup);
};
function updateExpBar() {
expBar.setText('LV ' + player.level + ' EXP: ' + player.exp + '/' + player.expToLevel);
// Level progress bar
var percent = Math.min(1, player.exp / player.expToLevel);
var barLen = 600;
var filled = Math.floor(barLen * percent);
var empty = barLen - filled;
var bar = '';
for (var i = 0; i < filled / 20; i++) bar += '█';
for (var i = 0; i < empty / 20; i++) bar += '░';
levelProgressBar.setText('Level Progress: ' + bar + ' ' + Math.floor(percent * 100) + '%');
}
function updateHpBar() {
hpBar.setText('HP: ' + player.hp + '/' + player.maxHp);
}
function updateScore() {
scoreTxt.setText('Wave: ' + wave);
}
// --- Asset initialization (shapes) ---
// --- Game setup ---
// Add background model (for future customization)
var backgroundModel = LK.getAsset('levelUpBg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: GAME_WIDTH / 100,
scaleY: GAME_HEIGHT / 100,
x: GAME_WIDTH / 2,
y: GAME_HEIGHT / 2
});
game.addChild(backgroundModel);
player = new Player();
// Expanded play area
var GAME_WIDTH = 3072;
var GAME_HEIGHT = 4096;
player.x = GAME_WIDTH / 2;
player.y = GAME_HEIGHT / 2;
game.addChild(player);
creatureWeapon = new CreatureWeapon();
game.addChild(creatureWeapon);
updateExpBar();
updateHpBar();
updateScore();
// --- Level up popup ---
var levelUpPopup = null;
function showLevelUp() {
if (levelUpPopup) return; // Already showing
// Randomly pick 3 upgrades
var upgrades = [{
type: 'damage',
label: 'Creature Damage +'
}, {
type: 'speed',
label: 'Creature Speed +'
}, {
type: 'cooldown',
label: 'Faster Attacks'
}, {
type: 'projectile',
label: 'More Projectiles'
}, {
type: 'spread',
label: 'Wider Spread'
}, {
type: 'area',
label: 'Area Damage (AOE)'
}, {
type: 'pierce',
label: 'Piercing Projectiles'
}, {
type: 'regen',
label: 'Regenerate HP'
}, {
type: 'expBoost',
label: 'EXP Gain +'
}];
// Shuffle and pick 3
for (var i = upgrades.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = upgrades[i];
upgrades[i] = upgrades[j];
upgrades[j] = t;
}
var options = upgrades.slice(0, 3);
// Popup container
levelUpPopup = new Container();
var bg = LK.getAsset('levelUpTab', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.2,
scaleY: 6
});
levelUpPopup.addChild(bg);
bg.x = 0;
bg.y = 0;
var title = new Text2('LEVEL UP!', {
size: 120,
fill: "#fff"
});
title.anchor.set(0.5, 0);
title.x = 0;
title.y = -300;
levelUpPopup.addChild(title);
// Option buttons
for (var i = 0; i < 3; i++) {
(function (idx) {
var opt = options[idx];
var btn = LK.getAsset('levelUpBtn' + idx, {
width: 700,
height: 140,
color: 0x4444aa,
shape: 'box',
anchorX: 0.5,
anchorY: 0.5
});
btn.x = 0;
btn.y = -80 + idx * 180;
levelUpPopup.addChild(btn);
var txt = new Text2(opt.label, {
size: 70,
fill: "#fff"
});
txt.anchor.set(0.5, 0.5);
txt.x = 0;
txt.y = btn.y;
levelUpPopup.addChild(txt);
btn.down = function (x, y, obj) {
creatureWeapon.upgrade(opt.type);
// Remove popup
LK.gui.center.removeChild(levelUpPopup);
levelUpPopup = null;
};
})(i);
}
levelUpPopup.x = 0;
levelUpPopup.y = 0;
LK.gui.center.addChild(levelUpPopup);
}
// --- Spawn experience gem ---
function spawnExp(x, y, value) {
var gem = new ExpGem();
gem.x = x;
gem.y = y;
gem.value = value;
expGems.push(gem);
game.addChild(gem);
}
// --- Continuous enemy spawn system ---
var spawnDelay = 32; // frames between spawns, will decrease as wave increases
var spawnDelayTimer = 0;
// Add a delay before the first enemy spawns
var initialSpawnDelay = 120; // 2 seconds at 60fps
var initialSpawnDelayTimer = 0;
// Limit for max living enemies, starts low and increases over time
var maxLivingEnemies = 3;
var maxLivingEnemiesIncreaseInterval = 60 * 60 * 3; // 3 minutes at 60fps
var lastMaxLivingEnemiesIncreaseTick = 0;
function updateSpawnRate() {
// Increase spawn rate over time, but clamp to a minimum
spawnDelay = Math.max(4, 32 - Math.floor(wave * 1.2));
}
function spawnEnemy() {
if (enemies.length >= maxLivingEnemies) return; // Don't spawn if at max
var enemy = new Enemy();
// Give some enemies weapons in their hands (e.g. 30% chance)
enemy.hasWeapon = Math.random() < 0.3;
// Spawn at random edge
var edge = Math.floor(Math.random() * 4);
if (edge === 0) {
// top
enemy.x = Math.random() * GAME_WIDTH;
enemy.y = -100;
} else if (edge === 1) {
// bottom
enemy.x = Math.random() * GAME_WIDTH;
enemy.y = GAME_HEIGHT + 100;
} else if (edge === 2) {
// left
enemy.x = -100;
enemy.y = Math.random() * GAME_HEIGHT;
} else {
// right
enemy.x = GAME_WIDTH + 100;
enemy.y = Math.random() * GAME_HEIGHT;
}
enemies.push(enemy);
game.addChild(enemy);
}
// --- Game move handler (touch/mouse drag to move player) ---
function handleMove(x, y, obj) {
// Don't allow movement if level up popup is open
if (levelUpPopup) return;
// Clamp to avoid top left menu
if (x < 100) x = 100;
if (y < 0) y = 0;
player.targetX = x;
player.targetY = y;
}
game.move = handleMove;
game.down = function (x, y, obj) {
handleMove(x, y, obj);
};
game.up = function (x, y, obj) {
// No dragNode needed, movement is always allowed
};
// --- Main game update loop ---
game.update = function () {
// Pause all game logic except GUI and popup when level up popup is open
if (levelUpPopup) {
// Only update GUI, skip all gameplay logic
return;
}
// Player regen
if (player.regen && LK.ticks % 90 === 0 && player.hp < player.maxHp) {
player.hp += player.regen;
if (player.hp > player.maxHp) player.hp = player.maxHp;
updateHpBar();
}
// Update player
player.update();
// Update weapon
creatureWeapon.update();
// Update projectiles
for (var i = projectiles.length - 1; i >= 0; i--) {
var proj = projectiles[i];
proj.update();
// Remove if out of bounds
if (proj.x < -100 || proj.x > GAME_WIDTH + 100 || proj.y < -100 || proj.y > GAME_HEIGHT + 100) {
proj.destroy();
projectiles.splice(i, 1);
continue;
}
// Check collision with enemies
for (var j = enemies.length - 1; j >= 0; j--) {
var enemy = enemies[j];
var dx = proj.x - enemy.x;
var dy = proj.y - enemy.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < proj.radius + enemy.radius) {
var killed = enemy.takeDamage(proj.damage);
// Area damage
if (creatureWeapon.areaDamage) {
for (var k = 0; k < enemies.length; k++) {
if (k !== j) {
var e2 = enemies[k];
var d2 = Math.sqrt((proj.x - e2.x) * (proj.x - e2.x) + (proj.y - e2.y) * (proj.y - e2.y));
if (d2 < 180) {
e2.takeDamage(creatureWeapon.areaDamage);
}
}
}
}
// Piercing
if (creatureWeapon.pierce && proj.pierced === undefined) {
proj.pierced = 1;
}
if (creatureWeapon.pierce && proj.pierced < creatureWeapon.pierce) {
proj.pierced++;
// Do not destroy, allow to hit more enemies
} else {
proj.destroy();
projectiles.splice(i, 1);
}
if (killed) {
enemies.splice(j, 1);
}
break;
}
}
}
// Update enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var enemy = enemies[i];
enemy.update();
// Check collision with player
var dx = enemy.x - player.x;
var dy = enemy.y - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
// If enemy is still alive and close enough to player, but projectiles should kill before touching
if (dist < enemy.radius + player.radius) {
// Check if any projectile is close enough to kill this enemy before it touches the player
var killedByProjectile = false;
for (var p = 0; p < projectiles.length; p++) {
var proj = projectiles[p];
var pdx = proj.x - enemy.x;
var pdy = proj.y - enemy.y;
var pdist = Math.sqrt(pdx * pdx + pdy * pdy);
if (pdist < proj.radius + enemy.radius) {
var killed = enemy.takeDamage(proj.damage);
proj.destroy();
projectiles.splice(p, 1);
if (killed) {
enemies.splice(i, 1);
killedByProjectile = true;
}
break;
}
}
// Only damage player if enemy was not killed by projectile and not during level up
if (!killedByProjectile && !levelUpPopup) {
player.takeDamage();
// Knockback enemy
var angle = Math.atan2(dy, dx);
enemy.x += Math.cos(angle) * 120;
enemy.y += Math.sin(angle) * 120;
}
// Prevent creatures from causing damage while level up popup is open
if (!killedByProjectile && levelUpPopup) {
// Do not damage player or apply knockback
}
}
}
// Update exp gems
for (var i = expGems.length - 1; i >= 0; i--) {
var gem = expGems[i];
gem.update();
// Collect if close to player
var dx = gem.x - player.x;
var dy = gem.y - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < gem.radius + player.radius) {
player.gainExp(gem.value);
gem.destroy();
expGems.splice(i, 1);
}
}
// Continuous enemy spawn (no waves)
if (!levelUpPopup) {
// Wait for initial spawn delay before starting enemy spawns
if (initialSpawnDelayTimer < initialSpawnDelay) {
initialSpawnDelayTimer++;
} else {
spawnDelayTimer++;
if (spawnDelayTimer >= spawnDelay) {
spawnEnemy();
spawnDelayTimer = 0;
}
// Increase difficulty over time, every 10 seconds
if (LK.ticks % (60 * 10) === 0) {
wave++;
updateScore();
updateSpawnRate();
}
// Increase maxLivingEnemies by 1 every 3 minutes
if (LK.ticks - lastMaxLivingEnemiesIncreaseTick >= maxLivingEnemiesIncreaseInterval) {
maxLivingEnemies++;
lastMaxLivingEnemiesIncreaseTick = LK.ticks;
}
}
}
};
// --- GUI positioning ---
scoreTxt.x = GAME_WIDTH / 2;
scoreTxt.y = 20;
expBar.x = GAME_WIDTH / 2;
expBar.y = 140;
levelProgressBar.x = GAME_WIDTH / 2;
levelProgressBar.y = 200;
hpBar.x = GAME_WIDTH / 2;
hpBar.y = 280;
assetScreenBtn.x = GAME_WIDTH - 200;
assetScreenBtn.y = 80;
// --- Start first wave ---
// No prepareWave needed; enemy spawning is now continuous.
police pixel art. In-Game asset. 2d. High contrast. No shadows
black person pixel art. In-Game asset. High contrast. No shadows
glock pixel art. In-Game asset. High contrast. No shadows
pixel art bullet. In-Game asset. High contrast. No shadows
money pixel art. In-Game asset. High contrast. No shadows
a police themed level up screen pixel art. In-Game asset. High contrast. No shadows