/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// --- Enemy Class ---
var Enemy = Container.expand(function () {
var self = Container.call(this);
// Attach enemy asset (image)
var enemyAsset = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = enemyAsset.width / 2;
self.speed = 3;
self.angle = 0;
self.hasSpawnedText = false; // Flag to track if this enemy already spawned text
self.update = function () {
// Store lastX, lastY for good practice (if needed for triggers)
if (typeof self.lastX === "undefined") self.lastX = self.x;
if (typeof self.lastY === "undefined") self.lastY = self.y;
// Move in direction of angle
self.x += Math.cos(self.angle) * self.speed * globalSpeedMultiplier;
self.y += Math.sin(self.angle) * self.speed * globalSpeedMultiplier;
// Update lastX, lastY
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// --- Enemy2 Class (Red, smaller, faster) ---
var Enemy2 = Container.expand(function () {
var self = Container.call(this);
// Attach enemy2 asset (image)
var enemyAsset = self.attachAsset('enemy2', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = enemyAsset.width / 2;
self.speed = 5; // Faster than regular enemy
self.angle = 0;
self.hasSpawnedText = false; // Flag to track if this enemy already spawned text
self.update = function () {
// Store lastX, lastY for good practice
if (typeof self.lastX === "undefined") self.lastX = self.x;
if (typeof self.lastY === "undefined") self.lastY = self.y;
// Move in direction of angle
self.x += Math.cos(self.angle) * self.speed * globalSpeedMultiplier;
self.y += Math.sin(self.angle) * self.speed * globalSpeedMultiplier;
// Update lastX, lastY
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// --- Enemy3 Class (Purple, wide, special movement) ---
var Enemy3 = Container.expand(function () {
var self = Container.call(this);
// Attach enemy3 asset (image)
var enemyAsset = self.attachAsset('enemy3', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = enemyAsset.width / 2;
self.speed = 2; // Slower but wider
self.angle = 0;
self.wobbleOffset = 0; // For special movement pattern
self.hasSpawnedText = false; // Flag to track if this enemy already spawned text
self.update = function () {
// Store lastX, lastY for good practice
if (typeof self.lastX === "undefined") self.lastX = self.x;
if (typeof self.lastY === "undefined") self.lastY = self.y;
// Special wobble movement
self.wobbleOffset += 0.1;
var wobbleX = Math.cos(self.wobbleOffset) * 50; // Side-to-side wobble
var wobbleY = Math.sin(self.wobbleOffset * 0.5) * 30; // Up-down wobble
// Move in direction of angle with wobble
self.x += Math.cos(self.angle) * self.speed * globalSpeedMultiplier + wobbleX * 0.02;
self.y += Math.sin(self.angle) * self.speed * globalSpeedMultiplier + wobbleY * 0.02;
// Update lastX, lastY
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// --- Player Class ---
var Player = Container.expand(function () {
var self = Container.call(this);
// Attach player asset
var playerAsset = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// Set radius for collision
self.radius = playerAsset.width / 2;
// Flash effect on hit
self.flash = function () {
LK.effects.flashObject(self, 0xff0000, 600);
};
return self;
});
// --- Powerup Class ---
var Powerup = Container.expand(function () {
var self = Container.call(this);
// Attach powerup asset
var powerupAsset = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
// Set radius for collision
self.radius = powerupAsset.width / 2;
return self;
});
// --- Trail Class ---
var Trail = Container.expand(function (assetId) {
var self = Container.call(this);
// Attach the same asset as the enemy
var trailAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Start with some transparency
self.alpha = 0.6;
// Fade out over time using tween
tween(self, {
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c24
});
/****
* Game Code
****/
// --- Game Variables ---
// --- Asset Initialization ---
// Red, smaller, faster
// Purple, wide, special
var player;
var enemies = [];
var powerups = [];
var trails = [];
var dragNode = null;
var lastPlayerPos = {
x: 0,
y: 0
};
var lastHit = false;
var score = 0;
var scoreTxt;
var survivalTime = 0;
var timeTxt;
var spawnTimer = 0;
var spawnInterval = 120; // frames (reduced from 150 to 120 for better balance)
var powerupTimer = 0;
var powerupInterval = 300; // frames (was 600, now twice as frequent)
var difficultyTimer = 0;
var minSpawnInterval = 60; // Increased from 30 to 60 to keep a reasonable minimum spawn rate
var gameStarted = false; // Control when gameplay begins
globalSpeedMultiplier = 1.0; // Reset speed multiplier
var globalSpeedMultiplier = 1.0; // Multiplier for all enemy speeds
var enemy2SpawnMultiplier = 1.0; // Multiplier for Enemy2 spawn chance
var enemy3SpawnMultiplier = 1.0; // Multiplier for Enemy3 spawn chance
var bestScore = storage.bestScore || 0; // Load best score from storage
var bestScoreTxt;
var dodgeSoundIndex = 0; // Track which dodge sound to play next
// --- UI ---
scoreTxt = new Text2('Score: 0', {
size: 100,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
timeTxt = new Text2('Time: 0.0', {
size: 70,
fill: 0xB0EAFF
});
timeTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(timeTxt);
timeTxt.y = 110;
bestScoreTxt = new Text2('Best: ' + bestScore, {
size: 60,
fill: 0x00FF00
});
bestScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestScoreTxt);
bestScoreTxt.y = 180;
// Warning text about shuriken increase
var warningTxt = new Text2('Shurikens Will Increase Every 10 Seconds Be Careful!', {
size: 60,
fill: 0xFFFF00
});
warningTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(warningTxt);
warningTxt.y = 300;
// Fade out warning text after 3 seconds
tween(warningTxt, {
alpha: 0
}, {
duration: 3000,
onFinish: function onFinish() {
warningTxt.visible = false;
gameStarted = true; // Start the game when text fades out
}
});
// --- Player ---
player = new Player();
player.x = 2048 / 2;
player.y = 2732 / 2;
game.addChild(player);
// --- Helper Functions ---
function resetGameVars() {
// Check if current score is a new record before resetting
if (score > bestScore) {
bestScore = score;
storage.bestScore = bestScore; // Save to storage
}
enemies = [];
powerups = [];
trails = [];
score = 0;
survivalTime = 0;
spawnTimer = 0;
spawnInterval = 90;
powerupTimer = 0;
difficultyTimer = 0;
lastHit = false;
gameStarted = false; // Reset game start state
globalSpeedMultiplier = 1.0; // Reset speed multiplier
enemy2SpawnMultiplier = 1.0; // Reset Enemy2 spawn multiplier
enemy3SpawnMultiplier = 1.0; // Reset Enemy3 spawn multiplier
scoreTxt.setText('Score: 0');
timeTxt.setText('Time: 0.0');
bestScoreTxt.setText('Best: ' + bestScore);
player.x = 2048 / 2;
player.y = 2732 / 2;
// Show warning text again on reset
warningTxt.visible = true;
warningTxt.alpha = 1;
// Fade out warning text after 3 seconds
tween(warningTxt, {
alpha: 0
}, {
duration: 3000,
onFinish: function onFinish() {
warningTxt.visible = false;
gameStarted = true; // Start the game when text fades out
}
});
}
// --- Spawning ---
function spawnEnemy() {
// Spawn at random edge, move toward player
var enemy;
// After 10 seconds, start mixing in Enemy2, after 20 seconds add Enemy3
if (survivalTime >= 20) {
var r = Math.random();
var enemy2Chance = Math.min(0.5, 0.3 * enemy2SpawnMultiplier);
var enemy3Chance = Math.min(0.4, 0.2 * enemy3SpawnMultiplier);
var enemyChance = Math.max(0.1, 0.4 - (enemy2Chance - 0.3) - (enemy3Chance - 0.2));
if (r < enemyChance) {
enemy = new Enemy();
} else if (r < enemyChance + enemy2Chance) {
enemy = new Enemy2();
} else {
enemy = new Enemy3();
}
} else if (survivalTime >= 10) {
var r = Math.random();
var enemy2Chance = Math.min(0.7, 0.4 * enemy2SpawnMultiplier);
var enemyChance = Math.max(0.3, 0.6 - (enemy2Chance - 0.4));
if (r < enemyChance) {
enemy = new Enemy();
} else {
enemy = new Enemy2();
}
} else {
enemy = new Enemy();
}
var edge = Math.floor(Math.random() * 4);
var x, y;
if (edge === 0) {
// Top
x = 200 + Math.random() * (2048 - 400);
y = -80;
} else if (edge === 1) {
// Bottom
x = 200 + Math.random() * (2048 - 400);
y = 2732 + 80;
} else if (edge === 2) {
// Left
x = -80;
y = 200 + Math.random() * (2732 - 400);
} else {
// Right
x = 2048 + 80;
y = 200 + Math.random() * (2732 - 400);
}
enemy.x = x;
enemy.y = y;
// Track spawn time for trail duration control
enemy.spawnTime = LK.ticks;
// Aim at player
var dx = player.x - x;
var dy = player.y - y;
enemy.angle = Math.atan2(dy, dx);
if (typeof baseEnemySpeed === "undefined") {
baseEnemySpeed = 3;
}
// Only set speed for Enemy (default), others use their own
if (enemy instanceof Enemy) {
enemy.speed = baseEnemySpeed + Math.random() * 1 + survivalTime / 40; // Reduced random from 2 to 1, survival bonus from /20 to /40
}
// Add more variety: after 30 seconds, occasionally spawn a random fast Enemy2 or a wobbly Enemy3
if (survivalTime >= 30 && Math.random() < 0.15) {
if (Math.random() < 0.5) {
enemy = new Enemy2();
enemy.speed += 2 + Math.random() * 2; // Super fast
} else {
enemy = new Enemy3();
enemy.speed += 1 + Math.random(); // Extra wobbly
}
enemy.x = x;
enemy.y = y;
enemy.spawnTime = LK.ticks;
enemy.angle = Math.atan2(dy, dx);
}
enemies.push(enemy);
game.addChild(enemy);
}
function spawnPowerup() {
var powerup = new Powerup();
powerup.x = 200 + Math.random() * (2048 - 400);
powerup.y = 200 + Math.random() * (2732 - 400);
powerups.push(powerup);
game.addChild(powerup);
}
// --- Collision ---
function circleIntersect(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < (a.radius + b.radius) * 0.85;
}
// --- Drag Controls ---
function handleMove(x, y, obj) {
if (dragNode) {
// Only move player if dragging (i.e., finger/mouse is moving after down)
// Calculate movement delta from lastPlayerPos
if (lastPlayerPos.active) {
var dx = x - lastPlayerPos.x;
var dy = y - lastPlayerPos.y;
// Only move if there is a delta (prevents teleport on first touch)
if (dx !== 0 || dy !== 0) {
var newX = dragNode.x + dx;
var newY = dragNode.y + dy;
// Clamp to game area (avoid top left 100x100)
var px = Math.max(100 + player.radius, Math.min(2048 - player.radius, newX));
var py = Math.max(100 + player.radius, Math.min(2732 - player.radius, newY));
dragNode.x = px;
dragNode.y = py;
}
}
// Update lastPlayerPos for next move
lastPlayerPos.x = x;
lastPlayerPos.y = y;
lastPlayerPos.active = true;
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
dragNode = player;
// Record initial drag position, but do not move player yet
lastPlayerPos.x = x;
lastPlayerPos.y = y;
lastPlayerPos.active = false; // Prevents teleport on first move
};
game.up = function (x, y, obj) {
dragNode = null;
lastPlayerPos.active = false;
};
// --- Main Update Loop ---
game.update = function () {
// Don't start gameplay until warning text has faded out
if (!gameStarted) {
return;
}
// Survival time
survivalTime += 1 / 60;
timeTxt.setText('Time: ' + survivalTime.toFixed(1));
// Difficulty scaling
difficultyTimer++;
if (difficultyTimer % 600 === 0) {
// Every 10 seconds, increase difficulty
// Decrease spawn interval (more frequent spawns)
if (spawnInterval > minSpawnInterval) {
spawnInterval -= 3; // Reduced from 8 to 3 for more gradual spawn rate increase
if (spawnInterval < minSpawnInterval) spawnInterval = minSpawnInterval;
}
// Increase number of enemies spawned per interval (only every other interval)
if (typeof enemiesPerSpawn === "undefined") {
enemiesPerSpawn = 1;
}
if (difficultyTimer % 1200 === 0) {
// Only increase every 20 seconds instead of 10
enemiesPerSpawn++;
}
// Increase base enemy speed
if (typeof baseEnemySpeed === "undefined") {
baseEnemySpeed = 3;
}
baseEnemySpeed += 0.3; // Reduced from 1.0 to 0.3 for more gradual speed increase
// Increase global speed multiplier for all enemies
globalSpeedMultiplier += 0.1; // Reduced from 0.3 to 0.1 for more gradual speed increase
// Increase Enemy2 and Enemy3 spawn rates
enemy2SpawnMultiplier += 0.2; // Reduced from 0.5 to 0.2
enemy3SpawnMultiplier += 0.15; // Reduced from 0.4 to 0.15
// Introduce new enemy types as time passes
// (handled in spawnEnemy, but you can add more logic here if needed)
}
// Enemy spawn
spawnTimer++;
if (spawnTimer >= spawnInterval) {
// Spawn multiple enemies per interval as difficulty increases
if (typeof enemiesPerSpawn === "undefined") {
enemiesPerSpawn = 1;
}
for (var s = 0; s < enemiesPerSpawn; s++) {
spawnEnemy();
}
spawnTimer = 0;
}
// Powerup spawn
powerupTimer++;
if (powerupTimer >= powerupInterval) {
// Spawn 2 powerups per interval for more frequent yellow balls
for (var i = 0; i < 2; i++) {
spawnPowerup();
}
powerupTimer = 0;
}
// Update enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var e = enemies[i];
e.update();
// Create trail only when enemy is moving faster than base speed AND within spawn time limits
var shouldCreateTrail = false;
var timeSinceSpawn = (LK.ticks - (e.spawnTime || 0)) / 60; // Convert to seconds
// Enemy has 2.25 seconds, Enemy2 and Enemy3 have 1 second
var trailTimeLimit = e instanceof Enemy ? 2.25 : 1.0;
if (timeSinceSpawn <= trailTimeLimit) {
if (e instanceof Enemy && e.speed > baseEnemySpeed + 1) {
shouldCreateTrail = true;
} else if (e instanceof Enemy2 && e.speed > 5) {
shouldCreateTrail = true;
} else if (e instanceof Enemy3 && e.speed > 2) {
shouldCreateTrail = true;
}
}
if (shouldCreateTrail && LK.ticks % 8 === 0) {
var assetId = 'enemy'; // Default
if (e instanceof Enemy2) assetId = 'enemy2';
if (e instanceof Enemy3) assetId = 'enemy3';
var trail = new Trail(assetId);
trail.x = e.x;
trail.y = e.y;
trail.rotation = e.rotation || 0;
trails.push(trail);
game.addChild(trail);
}
// Remove if off screen
if (e.x < -200 || e.x > 2248 || e.y < -200 || e.y > 2932) {
e.destroy();
enemies.splice(i, 1);
continue;
}
// Check for close calls (near misses)
var dx = e.x - player.x;
var dy = e.y - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var closeCallDistance = (e.radius + player.radius) * 1.5; // 1.5x collision distance
// Initialize lastWasClose if not set
if (typeof e.lastWasClose === "undefined") e.lastWasClose = false;
var isCloseNow = distance <= closeCallDistance;
// Trigger close call effect when transitioning from not close to close, but only once per enemy
if (!e.lastWasClose && isCloseNow && !circleIntersect(e, player) && !e.hasSpawnedText) {
// Create encouraging text
var messages = ['Good!', 'Wow!', 'Excellent!', 'Nice!', 'Amazing!'];
var message = messages[Math.floor(Math.random() * messages.length)];
var encourageText = new Text2(message, {
size: 120,
fill: 0x00FF00
});
encourageText.anchor.set(0.5, 0.5);
encourageText.x = player.x + (Math.random() - 0.5) * 200; // Random offset around player
encourageText.y = player.y + (Math.random() - 0.5) * 200;
// Ensure text stays within screen bounds
encourageText.x = Math.max(100, Math.min(2048 - 100, encourageText.x));
encourageText.y = Math.max(100, Math.min(2732 - 100, encourageText.y));
game.addChild(encourageText);
// Animate text: pop up, then fade out
encourageText.scaleX = 0.1;
encourageText.scaleY = 0.1;
encourageText.alpha = 1;
// --- Bonus Points and Bonus Text ---
score += 10;
scoreTxt.setText('Score: ' + score);
// Show "Bonus Point +10" text near the score table (top center)
var bonusText = new Text2("Bonus Point +10", {
size: 80,
fill: 0xFFD700
});
bonusText.anchor.set(0.5, 0);
bonusText.x = scoreTxt.x;
bonusText.y = scoreTxt.y + scoreTxt.height + 10;
LK.gui.top.addChild(bonusText);
bonusText.alpha = 1;
bonusText.scaleX = 0.7;
bonusText.scaleY = 0.7;
tween(bonusText, {
scaleX: 1.1,
scaleY: 1.1,
y: bonusText.y - 30
}, {
duration: 180,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(bonusText, {
alpha: 0,
y: bonusText.y - 30
}, {
duration: 600,
onFinish: function onFinish() {
if (bonusText.parent) bonusText.destroy();
}
});
}
});
// Mark this enemy as having spawned text
e.hasSpawnedText = true;
// Play dodge sound effect - alternate between two sounds
var soundName = dodgeSoundIndex === 0 ? 'dodge' : 'dodge2';
LK.getSound(soundName).play();
dodgeSoundIndex = (dodgeSoundIndex + 1) % 2; // Alternate between 0 and 1
// Player pop and flash effect for juicy feedback
tween(player, {
scaleX: 1.25,
scaleY: 1.25
}, {
duration: 80,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 120
});
}
});
LK.effects.flashObject(player, 0x00ffcc, 200);
// First tween: pop up
tween(encourageText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Second tween: fade out
tween(encourageText, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8,
y: encourageText.y - 50
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
if (encourageText.parent) {
encourageText.destroy();
}
}
});
}
});
}
// Update last close state
e.lastWasClose = isCloseNow;
// Collision with player
if (circleIntersect(e, player)) {
if (!lastHit) {
// Player hit: pop and flash effect
tween(player, {
scaleX: 1.4,
scaleY: 1.4
}, {
duration: 100,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
}
});
player.flash();
LK.effects.flashScreen(0xff0000, 600);
// Update best score before showing game over
if (score > bestScore) {
bestScore = score;
storage.bestScore = bestScore; // Save to storage
bestScoreTxt.setText('Best: ' + bestScore);
LK.showGameOver('New Record: ' + score);
} else {
LK.showGameOver();
}
lastHit = true;
return;
}
}
}
// Clean up destroyed trails
for (var t = trails.length - 1; t >= 0; t--) {
var trail = trails[t];
if (!trail.parent || trail.alpha <= 0) {
if (trail.parent) trail.destroy();
trails.splice(t, 1);
}
}
// Remove dead enemies if game over
if (lastHit) {
for (var j = 0; j < enemies.length; j++) {
enemies[j].destroy();
}
enemies = [];
for (var k = 0; k < powerups.length; k++) {
powerups[k].destroy();
}
powerups = [];
for (var l = 0; l < trails.length; l++) {
trails[l].destroy();
}
trails = [];
return;
}
// Update powerups
for (var p = powerups.length - 1; p >= 0; p--) {
var pu = powerups[p];
// Collision with player
if (circleIntersect(pu, player)) {
score += 10;
scoreTxt.setText('Score: ' + score);
// Create pop effect before destroying powerup
tween(pu, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
if (pu.parent) {
pu.destroy();
}
}
});
// Player pop and flash effect for collecting powerup
tween(player, {
scaleX: 1.18,
scaleY: 1.18
}, {
duration: 80,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 120
});
}
});
LK.effects.flashObject(player, 0xffff00, 180);
powerups.splice(p, 1);
continue;
}
}
// Score increases with time
if (Math.floor(survivalTime * 10) % 10 === 0) {
var newScore = Math.floor(survivalTime);
if (newScore > score) {
score = newScore;
scoreTxt.setText('Score: ' + score);
}
}
};
// --- Game Over Reset ---
LK.on('gameover', function () {
resetGameVars();
});
// --- You Win (not used in endless, but for completeness) ---
LK.on('youwin', function () {
resetGameVars();
});
// --- Start Game Music ---
LK.playMusic('Game-music'); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// --- Enemy Class ---
var Enemy = Container.expand(function () {
var self = Container.call(this);
// Attach enemy asset (image)
var enemyAsset = self.attachAsset('enemy', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = enemyAsset.width / 2;
self.speed = 3;
self.angle = 0;
self.hasSpawnedText = false; // Flag to track if this enemy already spawned text
self.update = function () {
// Store lastX, lastY for good practice (if needed for triggers)
if (typeof self.lastX === "undefined") self.lastX = self.x;
if (typeof self.lastY === "undefined") self.lastY = self.y;
// Move in direction of angle
self.x += Math.cos(self.angle) * self.speed * globalSpeedMultiplier;
self.y += Math.sin(self.angle) * self.speed * globalSpeedMultiplier;
// Update lastX, lastY
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// --- Enemy2 Class (Red, smaller, faster) ---
var Enemy2 = Container.expand(function () {
var self = Container.call(this);
// Attach enemy2 asset (image)
var enemyAsset = self.attachAsset('enemy2', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = enemyAsset.width / 2;
self.speed = 5; // Faster than regular enemy
self.angle = 0;
self.hasSpawnedText = false; // Flag to track if this enemy already spawned text
self.update = function () {
// Store lastX, lastY for good practice
if (typeof self.lastX === "undefined") self.lastX = self.x;
if (typeof self.lastY === "undefined") self.lastY = self.y;
// Move in direction of angle
self.x += Math.cos(self.angle) * self.speed * globalSpeedMultiplier;
self.y += Math.sin(self.angle) * self.speed * globalSpeedMultiplier;
// Update lastX, lastY
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// --- Enemy3 Class (Purple, wide, special movement) ---
var Enemy3 = Container.expand(function () {
var self = Container.call(this);
// Attach enemy3 asset (image)
var enemyAsset = self.attachAsset('enemy3', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = enemyAsset.width / 2;
self.speed = 2; // Slower but wider
self.angle = 0;
self.wobbleOffset = 0; // For special movement pattern
self.hasSpawnedText = false; // Flag to track if this enemy already spawned text
self.update = function () {
// Store lastX, lastY for good practice
if (typeof self.lastX === "undefined") self.lastX = self.x;
if (typeof self.lastY === "undefined") self.lastY = self.y;
// Special wobble movement
self.wobbleOffset += 0.1;
var wobbleX = Math.cos(self.wobbleOffset) * 50; // Side-to-side wobble
var wobbleY = Math.sin(self.wobbleOffset * 0.5) * 30; // Up-down wobble
// Move in direction of angle with wobble
self.x += Math.cos(self.angle) * self.speed * globalSpeedMultiplier + wobbleX * 0.02;
self.y += Math.sin(self.angle) * self.speed * globalSpeedMultiplier + wobbleY * 0.02;
// Update lastX, lastY
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
// --- Player Class ---
var Player = Container.expand(function () {
var self = Container.call(this);
// Attach player asset
var playerAsset = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// Set radius for collision
self.radius = playerAsset.width / 2;
// Flash effect on hit
self.flash = function () {
LK.effects.flashObject(self, 0xff0000, 600);
};
return self;
});
// --- Powerup Class ---
var Powerup = Container.expand(function () {
var self = Container.call(this);
// Attach powerup asset
var powerupAsset = self.attachAsset('powerup', {
anchorX: 0.5,
anchorY: 0.5
});
// Set radius for collision
self.radius = powerupAsset.width / 2;
return self;
});
// --- Trail Class ---
var Trail = Container.expand(function (assetId) {
var self = Container.call(this);
// Attach the same asset as the enemy
var trailAsset = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Start with some transparency
self.alpha = 0.6;
// Fade out over time using tween
tween(self, {
alpha: 0
}, {
duration: 700,
onFinish: function onFinish() {
if (self.parent) {
self.destroy();
}
}
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181c24
});
/****
* Game Code
****/
// --- Game Variables ---
// --- Asset Initialization ---
// Red, smaller, faster
// Purple, wide, special
var player;
var enemies = [];
var powerups = [];
var trails = [];
var dragNode = null;
var lastPlayerPos = {
x: 0,
y: 0
};
var lastHit = false;
var score = 0;
var scoreTxt;
var survivalTime = 0;
var timeTxt;
var spawnTimer = 0;
var spawnInterval = 120; // frames (reduced from 150 to 120 for better balance)
var powerupTimer = 0;
var powerupInterval = 300; // frames (was 600, now twice as frequent)
var difficultyTimer = 0;
var minSpawnInterval = 60; // Increased from 30 to 60 to keep a reasonable minimum spawn rate
var gameStarted = false; // Control when gameplay begins
globalSpeedMultiplier = 1.0; // Reset speed multiplier
var globalSpeedMultiplier = 1.0; // Multiplier for all enemy speeds
var enemy2SpawnMultiplier = 1.0; // Multiplier for Enemy2 spawn chance
var enemy3SpawnMultiplier = 1.0; // Multiplier for Enemy3 spawn chance
var bestScore = storage.bestScore || 0; // Load best score from storage
var bestScoreTxt;
var dodgeSoundIndex = 0; // Track which dodge sound to play next
// --- UI ---
scoreTxt = new Text2('Score: 0', {
size: 100,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
timeTxt = new Text2('Time: 0.0', {
size: 70,
fill: 0xB0EAFF
});
timeTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(timeTxt);
timeTxt.y = 110;
bestScoreTxt = new Text2('Best: ' + bestScore, {
size: 60,
fill: 0x00FF00
});
bestScoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(bestScoreTxt);
bestScoreTxt.y = 180;
// Warning text about shuriken increase
var warningTxt = new Text2('Shurikens Will Increase Every 10 Seconds Be Careful!', {
size: 60,
fill: 0xFFFF00
});
warningTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(warningTxt);
warningTxt.y = 300;
// Fade out warning text after 3 seconds
tween(warningTxt, {
alpha: 0
}, {
duration: 3000,
onFinish: function onFinish() {
warningTxt.visible = false;
gameStarted = true; // Start the game when text fades out
}
});
// --- Player ---
player = new Player();
player.x = 2048 / 2;
player.y = 2732 / 2;
game.addChild(player);
// --- Helper Functions ---
function resetGameVars() {
// Check if current score is a new record before resetting
if (score > bestScore) {
bestScore = score;
storage.bestScore = bestScore; // Save to storage
}
enemies = [];
powerups = [];
trails = [];
score = 0;
survivalTime = 0;
spawnTimer = 0;
spawnInterval = 90;
powerupTimer = 0;
difficultyTimer = 0;
lastHit = false;
gameStarted = false; // Reset game start state
globalSpeedMultiplier = 1.0; // Reset speed multiplier
enemy2SpawnMultiplier = 1.0; // Reset Enemy2 spawn multiplier
enemy3SpawnMultiplier = 1.0; // Reset Enemy3 spawn multiplier
scoreTxt.setText('Score: 0');
timeTxt.setText('Time: 0.0');
bestScoreTxt.setText('Best: ' + bestScore);
player.x = 2048 / 2;
player.y = 2732 / 2;
// Show warning text again on reset
warningTxt.visible = true;
warningTxt.alpha = 1;
// Fade out warning text after 3 seconds
tween(warningTxt, {
alpha: 0
}, {
duration: 3000,
onFinish: function onFinish() {
warningTxt.visible = false;
gameStarted = true; // Start the game when text fades out
}
});
}
// --- Spawning ---
function spawnEnemy() {
// Spawn at random edge, move toward player
var enemy;
// After 10 seconds, start mixing in Enemy2, after 20 seconds add Enemy3
if (survivalTime >= 20) {
var r = Math.random();
var enemy2Chance = Math.min(0.5, 0.3 * enemy2SpawnMultiplier);
var enemy3Chance = Math.min(0.4, 0.2 * enemy3SpawnMultiplier);
var enemyChance = Math.max(0.1, 0.4 - (enemy2Chance - 0.3) - (enemy3Chance - 0.2));
if (r < enemyChance) {
enemy = new Enemy();
} else if (r < enemyChance + enemy2Chance) {
enemy = new Enemy2();
} else {
enemy = new Enemy3();
}
} else if (survivalTime >= 10) {
var r = Math.random();
var enemy2Chance = Math.min(0.7, 0.4 * enemy2SpawnMultiplier);
var enemyChance = Math.max(0.3, 0.6 - (enemy2Chance - 0.4));
if (r < enemyChance) {
enemy = new Enemy();
} else {
enemy = new Enemy2();
}
} else {
enemy = new Enemy();
}
var edge = Math.floor(Math.random() * 4);
var x, y;
if (edge === 0) {
// Top
x = 200 + Math.random() * (2048 - 400);
y = -80;
} else if (edge === 1) {
// Bottom
x = 200 + Math.random() * (2048 - 400);
y = 2732 + 80;
} else if (edge === 2) {
// Left
x = -80;
y = 200 + Math.random() * (2732 - 400);
} else {
// Right
x = 2048 + 80;
y = 200 + Math.random() * (2732 - 400);
}
enemy.x = x;
enemy.y = y;
// Track spawn time for trail duration control
enemy.spawnTime = LK.ticks;
// Aim at player
var dx = player.x - x;
var dy = player.y - y;
enemy.angle = Math.atan2(dy, dx);
if (typeof baseEnemySpeed === "undefined") {
baseEnemySpeed = 3;
}
// Only set speed for Enemy (default), others use their own
if (enemy instanceof Enemy) {
enemy.speed = baseEnemySpeed + Math.random() * 1 + survivalTime / 40; // Reduced random from 2 to 1, survival bonus from /20 to /40
}
// Add more variety: after 30 seconds, occasionally spawn a random fast Enemy2 or a wobbly Enemy3
if (survivalTime >= 30 && Math.random() < 0.15) {
if (Math.random() < 0.5) {
enemy = new Enemy2();
enemy.speed += 2 + Math.random() * 2; // Super fast
} else {
enemy = new Enemy3();
enemy.speed += 1 + Math.random(); // Extra wobbly
}
enemy.x = x;
enemy.y = y;
enemy.spawnTime = LK.ticks;
enemy.angle = Math.atan2(dy, dx);
}
enemies.push(enemy);
game.addChild(enemy);
}
function spawnPowerup() {
var powerup = new Powerup();
powerup.x = 200 + Math.random() * (2048 - 400);
powerup.y = 200 + Math.random() * (2732 - 400);
powerups.push(powerup);
game.addChild(powerup);
}
// --- Collision ---
function circleIntersect(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
var dist = Math.sqrt(dx * dx + dy * dy);
return dist < (a.radius + b.radius) * 0.85;
}
// --- Drag Controls ---
function handleMove(x, y, obj) {
if (dragNode) {
// Only move player if dragging (i.e., finger/mouse is moving after down)
// Calculate movement delta from lastPlayerPos
if (lastPlayerPos.active) {
var dx = x - lastPlayerPos.x;
var dy = y - lastPlayerPos.y;
// Only move if there is a delta (prevents teleport on first touch)
if (dx !== 0 || dy !== 0) {
var newX = dragNode.x + dx;
var newY = dragNode.y + dy;
// Clamp to game area (avoid top left 100x100)
var px = Math.max(100 + player.radius, Math.min(2048 - player.radius, newX));
var py = Math.max(100 + player.radius, Math.min(2732 - player.radius, newY));
dragNode.x = px;
dragNode.y = py;
}
}
// Update lastPlayerPos for next move
lastPlayerPos.x = x;
lastPlayerPos.y = y;
lastPlayerPos.active = true;
}
}
game.move = handleMove;
game.down = function (x, y, obj) {
dragNode = player;
// Record initial drag position, but do not move player yet
lastPlayerPos.x = x;
lastPlayerPos.y = y;
lastPlayerPos.active = false; // Prevents teleport on first move
};
game.up = function (x, y, obj) {
dragNode = null;
lastPlayerPos.active = false;
};
// --- Main Update Loop ---
game.update = function () {
// Don't start gameplay until warning text has faded out
if (!gameStarted) {
return;
}
// Survival time
survivalTime += 1 / 60;
timeTxt.setText('Time: ' + survivalTime.toFixed(1));
// Difficulty scaling
difficultyTimer++;
if (difficultyTimer % 600 === 0) {
// Every 10 seconds, increase difficulty
// Decrease spawn interval (more frequent spawns)
if (spawnInterval > minSpawnInterval) {
spawnInterval -= 3; // Reduced from 8 to 3 for more gradual spawn rate increase
if (spawnInterval < minSpawnInterval) spawnInterval = minSpawnInterval;
}
// Increase number of enemies spawned per interval (only every other interval)
if (typeof enemiesPerSpawn === "undefined") {
enemiesPerSpawn = 1;
}
if (difficultyTimer % 1200 === 0) {
// Only increase every 20 seconds instead of 10
enemiesPerSpawn++;
}
// Increase base enemy speed
if (typeof baseEnemySpeed === "undefined") {
baseEnemySpeed = 3;
}
baseEnemySpeed += 0.3; // Reduced from 1.0 to 0.3 for more gradual speed increase
// Increase global speed multiplier for all enemies
globalSpeedMultiplier += 0.1; // Reduced from 0.3 to 0.1 for more gradual speed increase
// Increase Enemy2 and Enemy3 spawn rates
enemy2SpawnMultiplier += 0.2; // Reduced from 0.5 to 0.2
enemy3SpawnMultiplier += 0.15; // Reduced from 0.4 to 0.15
// Introduce new enemy types as time passes
// (handled in spawnEnemy, but you can add more logic here if needed)
}
// Enemy spawn
spawnTimer++;
if (spawnTimer >= spawnInterval) {
// Spawn multiple enemies per interval as difficulty increases
if (typeof enemiesPerSpawn === "undefined") {
enemiesPerSpawn = 1;
}
for (var s = 0; s < enemiesPerSpawn; s++) {
spawnEnemy();
}
spawnTimer = 0;
}
// Powerup spawn
powerupTimer++;
if (powerupTimer >= powerupInterval) {
// Spawn 2 powerups per interval for more frequent yellow balls
for (var i = 0; i < 2; i++) {
spawnPowerup();
}
powerupTimer = 0;
}
// Update enemies
for (var i = enemies.length - 1; i >= 0; i--) {
var e = enemies[i];
e.update();
// Create trail only when enemy is moving faster than base speed AND within spawn time limits
var shouldCreateTrail = false;
var timeSinceSpawn = (LK.ticks - (e.spawnTime || 0)) / 60; // Convert to seconds
// Enemy has 2.25 seconds, Enemy2 and Enemy3 have 1 second
var trailTimeLimit = e instanceof Enemy ? 2.25 : 1.0;
if (timeSinceSpawn <= trailTimeLimit) {
if (e instanceof Enemy && e.speed > baseEnemySpeed + 1) {
shouldCreateTrail = true;
} else if (e instanceof Enemy2 && e.speed > 5) {
shouldCreateTrail = true;
} else if (e instanceof Enemy3 && e.speed > 2) {
shouldCreateTrail = true;
}
}
if (shouldCreateTrail && LK.ticks % 8 === 0) {
var assetId = 'enemy'; // Default
if (e instanceof Enemy2) assetId = 'enemy2';
if (e instanceof Enemy3) assetId = 'enemy3';
var trail = new Trail(assetId);
trail.x = e.x;
trail.y = e.y;
trail.rotation = e.rotation || 0;
trails.push(trail);
game.addChild(trail);
}
// Remove if off screen
if (e.x < -200 || e.x > 2248 || e.y < -200 || e.y > 2932) {
e.destroy();
enemies.splice(i, 1);
continue;
}
// Check for close calls (near misses)
var dx = e.x - player.x;
var dy = e.y - player.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var closeCallDistance = (e.radius + player.radius) * 1.5; // 1.5x collision distance
// Initialize lastWasClose if not set
if (typeof e.lastWasClose === "undefined") e.lastWasClose = false;
var isCloseNow = distance <= closeCallDistance;
// Trigger close call effect when transitioning from not close to close, but only once per enemy
if (!e.lastWasClose && isCloseNow && !circleIntersect(e, player) && !e.hasSpawnedText) {
// Create encouraging text
var messages = ['Good!', 'Wow!', 'Excellent!', 'Nice!', 'Amazing!'];
var message = messages[Math.floor(Math.random() * messages.length)];
var encourageText = new Text2(message, {
size: 120,
fill: 0x00FF00
});
encourageText.anchor.set(0.5, 0.5);
encourageText.x = player.x + (Math.random() - 0.5) * 200; // Random offset around player
encourageText.y = player.y + (Math.random() - 0.5) * 200;
// Ensure text stays within screen bounds
encourageText.x = Math.max(100, Math.min(2048 - 100, encourageText.x));
encourageText.y = Math.max(100, Math.min(2732 - 100, encourageText.y));
game.addChild(encourageText);
// Animate text: pop up, then fade out
encourageText.scaleX = 0.1;
encourageText.scaleY = 0.1;
encourageText.alpha = 1;
// --- Bonus Points and Bonus Text ---
score += 10;
scoreTxt.setText('Score: ' + score);
// Show "Bonus Point +10" text near the score table (top center)
var bonusText = new Text2("Bonus Point +10", {
size: 80,
fill: 0xFFD700
});
bonusText.anchor.set(0.5, 0);
bonusText.x = scoreTxt.x;
bonusText.y = scoreTxt.y + scoreTxt.height + 10;
LK.gui.top.addChild(bonusText);
bonusText.alpha = 1;
bonusText.scaleX = 0.7;
bonusText.scaleY = 0.7;
tween(bonusText, {
scaleX: 1.1,
scaleY: 1.1,
y: bonusText.y - 30
}, {
duration: 180,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(bonusText, {
alpha: 0,
y: bonusText.y - 30
}, {
duration: 600,
onFinish: function onFinish() {
if (bonusText.parent) bonusText.destroy();
}
});
}
});
// Mark this enemy as having spawned text
e.hasSpawnedText = true;
// Play dodge sound effect - alternate between two sounds
var soundName = dodgeSoundIndex === 0 ? 'dodge' : 'dodge2';
LK.getSound(soundName).play();
dodgeSoundIndex = (dodgeSoundIndex + 1) % 2; // Alternate between 0 and 1
// Player pop and flash effect for juicy feedback
tween(player, {
scaleX: 1.25,
scaleY: 1.25
}, {
duration: 80,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 120
});
}
});
LK.effects.flashObject(player, 0x00ffcc, 200);
// First tween: pop up
tween(encourageText, {
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
// Second tween: fade out
tween(encourageText, {
alpha: 0,
scaleX: 0.8,
scaleY: 0.8,
y: encourageText.y - 50
}, {
duration: 800,
easing: tween.easeIn,
onFinish: function onFinish() {
if (encourageText.parent) {
encourageText.destroy();
}
}
});
}
});
}
// Update last close state
e.lastWasClose = isCloseNow;
// Collision with player
if (circleIntersect(e, player)) {
if (!lastHit) {
// Player hit: pop and flash effect
tween(player, {
scaleX: 1.4,
scaleY: 1.4
}, {
duration: 100,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 180
});
}
});
player.flash();
LK.effects.flashScreen(0xff0000, 600);
// Update best score before showing game over
if (score > bestScore) {
bestScore = score;
storage.bestScore = bestScore; // Save to storage
bestScoreTxt.setText('Best: ' + bestScore);
LK.showGameOver('New Record: ' + score);
} else {
LK.showGameOver();
}
lastHit = true;
return;
}
}
}
// Clean up destroyed trails
for (var t = trails.length - 1; t >= 0; t--) {
var trail = trails[t];
if (!trail.parent || trail.alpha <= 0) {
if (trail.parent) trail.destroy();
trails.splice(t, 1);
}
}
// Remove dead enemies if game over
if (lastHit) {
for (var j = 0; j < enemies.length; j++) {
enemies[j].destroy();
}
enemies = [];
for (var k = 0; k < powerups.length; k++) {
powerups[k].destroy();
}
powerups = [];
for (var l = 0; l < trails.length; l++) {
trails[l].destroy();
}
trails = [];
return;
}
// Update powerups
for (var p = powerups.length - 1; p >= 0; p--) {
var pu = powerups[p];
// Collision with player
if (circleIntersect(pu, player)) {
score += 10;
scoreTxt.setText('Score: ' + score);
// Create pop effect before destroying powerup
tween(pu, {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
if (pu.parent) {
pu.destroy();
}
}
});
// Player pop and flash effect for collecting powerup
tween(player, {
scaleX: 1.18,
scaleY: 1.18
}, {
duration: 80,
onFinish: function onFinish() {
tween(player, {
scaleX: 1,
scaleY: 1
}, {
duration: 120
});
}
});
LK.effects.flashObject(player, 0xffff00, 180);
powerups.splice(p, 1);
continue;
}
}
// Score increases with time
if (Math.floor(survivalTime * 10) % 10 === 0) {
var newScore = Math.floor(survivalTime);
if (newScore > score) {
score = newScore;
scoreTxt.setText('Score: ' + score);
}
}
};
// --- Game Over Reset ---
LK.on('gameover', function () {
resetGameVars();
});
// --- You Win (not used in endless, but for completeness) ---
LK.on('youwin', function () {
resetGameVars();
});
// --- Start Game Music ---
LK.playMusic('Game-music');