/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var BeatOrb = Container.expand(function (orbType) {
var self = Container.call(this);
self.orbType = orbType;
self.speed = 4;
self.hit = false;
var orbGraphics = self.attachAsset(orbType, {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
var speedMultiplier = 1;
if (selectedCharacter && selectedCharacter.freezeActive) {
speedMultiplier = 0.3;
}
// Simple fall down movement
self.y += self.speed * speedMultiplier;
};
return self;
});
var Character = Container.expand(function (characterType) {
var self = Container.call(this);
self.type = characterType;
self.abilityReady = true;
self.abilityCooldown = 0;
self.freezeActive = false;
self.freezeTime = 0;
self.phaseMode = false;
self.phaseModeTime = 0;
self.baseY = 0;
self.floatOffset = 0;
self.animSpeed = 0.05 + Math.random() * 0.03;
var characterGraphics = self.attachAsset(characterType, {
anchorX: 0.5,
anchorY: 0.5
});
self.useAbility = function () {
if (!self.abilityReady) {
return false;
}
LK.getSound('ability').play();
if (self.type === 'ninja') {
// Phase Mode: Pass through black orbs for 3 seconds
self.phaseMode = true;
self.phaseModeTime = 3000;
// Add glowing effect
tween(self, {
tint: 0x9932cc
}, {
duration: 500
});
LK.effects.flashObject(self, 0x9932cc, 3000);
} else if (self.type === 'wizard') {
// Time Freeze: Slow orbs for 3 seconds
self.freezeActive = true;
self.freezeTime = 3000;
LK.effects.flashObject(self, 0x00ffff, 3000);
} else if (self.type === 'swordmaster') {
// Clone Ability: Spawn temporary clone that auto-hits orbs
spawnSwordmasterClone();
LK.effects.flashObject(self, 0xffd700, 1000);
}
self.abilityReady = false;
self.abilityCooldown = 10000; // 10 second cooldown
return true;
};
self.update = function () {
if (!self.abilityReady) {
self.abilityCooldown -= 16; // ~60fps
if (self.abilityCooldown <= 0) {
self.abilityReady = true;
}
}
if (self.freezeActive) {
self.freezeTime -= 16;
if (self.freezeTime <= 0) {
self.freezeActive = false;
}
}
if (self.phaseMode) {
self.phaseModeTime -= 16;
if (self.phaseModeTime <= 0) {
self.phaseMode = false;
// Remove glow effect
tween(self, {
tint: 0xffffff
}, {
duration: 500
});
}
}
// Floating animation for character selection
if (gameState === 'classSelection') {
self.floatOffset += self.animSpeed;
self.y = self.baseY + Math.sin(self.floatOffset) * 20;
// Add mystical particle effect for wizard
if (self.type === 'wizard' && LK.ticks % 20 === 0) {
LK.effects.flashObject(self, 0x4169e1, 400);
}
// Add shadow dash effect for ninja
if (self.type === 'ninja' && LK.ticks % 30 === 0) {
LK.effects.flashObject(self, 0x9932cc, 300);
}
// Add sword gleam for swordmaster
if (self.type === 'swordmaster' && LK.ticks % 25 === 0) {
LK.effects.flashObject(self, 0xffd700, 350);
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a2e
});
/****
* Game Code
****/
// Game state
var gameState = 'classSelection'; // 'classSelection', 'playing', 'gameOver'
var selectedCharacter = null;
var score = 0;
var combo = 0;
var maxCombo = 0;
var lives = 3;
var comboMode = 0; // Ninja ability counter
var dualStrikeActive = false; // Swordmaster ability
var swordmasterClone = null;
var cloneActiveTime = 0;
var songProgress = 0;
var songDuration = 120000; // 2 minutes
// Orb speed mechanics
var baseOrbSpeed = 4; // Base fall speed for orbs
// BPM configuration for music synchronization
var baseBPM = 120; // Base BPM for the background music
var currentBPM = baseBPM; // Current BPM (can be modified dynamically)
// Function to calculate spawn interval based on current BPM
function calculateSpawnInterval() {
// Convert BPM to milliseconds per beat
// 60 seconds = 60000ms, so interval = 60000 / BPM
return Math.max(200, 60000 / currentBPM); // Minimum 200ms to prevent overwhelming
}
// Function to increase BPM when orb is hit
function increaseBPMOnHit() {
currentBPM = Math.min(200, currentBPM + 2); // Increase BPM by 2, cap at 200
spawnInterval = calculateSpawnInterval();
}
// Drag mechanics
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
var blackOrbHits = 0; // Track black orb hits for game over
var missedCount = 0; // Track missed red/green orbs
var blackTouchCount = 0; // Track black orb touches
// Game objects
var orbs = [];
var lanes = [];
var characters = {};
var targetZone = null;
// UI elements
var scoreText = null;
var comboText = null;
var livesText = null;
var abilityText = null;
var instructionText = null;
var ninjaDescText = null;
var wizardDescText = null;
var swordmasterDescText = null;
var titleText = null;
// Target position for Beat Saber style
var targetY = 2200;
var lastSpawnTime = 0;
var spawnInterval = calculateSpawnInterval(); // BPM-based spawn interval
// Start ambient music for character selection
LK.playMusic('ambient', {
volume: 0.6
});
// Target zone
targetZone = game.addChild(LK.getAsset('targetZone', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2400,
alpha: 0.5,
width: 2048,
height: 300,
tint: 0x000080
}));
// Create character selection
characters.ninja = game.addChild(new Character('ninja'));
characters.ninja.x = 1500 * 0.25;
characters.ninja.y = 1000;
characters.ninja.baseY = 1000;
characters.wizard = game.addChild(new Character('wizard'));
characters.wizard.x = 2048 * 0.5;
characters.wizard.y = 1200;
characters.wizard.baseY = 1200;
characters.swordmaster = game.addChild(new Character('swordmaster'));
characters.swordmaster.x = 2200 * 0.75;
characters.swordmaster.y = 1400;
characters.swordmaster.baseY = 1400;
// UI Setup
scoreText = new Text2('Score: 0', {
size: 60,
fill: 0xFFFFFF
});
scoreText.anchor.set(0, 0);
LK.gui.topLeft.addChild(scoreText);
scoreText.x = 120;
scoreText.y = 20;
comboText = new Text2('Combo: 0', {
size: 50,
fill: 0xFFFF00
});
comboText.anchor.set(0.5, 0);
LK.gui.top.addChild(comboText);
comboText.y = 80;
livesText = new Text2('Lives: 3', {
size: 50,
fill: 0xFF4444
});
livesText.anchor.set(1, 0);
LK.gui.topRight.addChild(livesText);
livesText.x = -20;
livesText.y = 20;
abilityText = new Text2('Ability Ready!', {
size: 40,
fill: 0x00FF00
});
abilityText.anchor.set(0.5, 1);
LK.gui.bottom.addChild(abilityText);
abilityText.y = -100;
// Title text
titleText = new Text2('RHYTHM HEROES\nBeat Battle Arena', {
size: 80,
fill: 0xFFD700
});
titleText.anchor.set(0.5, 0);
LK.gui.top.addChild(titleText);
titleText.y = 200;
// Character descriptions
ninjaDescText = new Text2('⚫ SHADOW NINJA\n"Strike with silence. Dance in shadow."\nAbility: Pass through black bomb \nmonsters for 3 seconds', {
size: 45,
fill: 0x9932cc
});
ninjaDescText.anchor.set(0.5, 0);
game.addChild(ninjaDescText);
ninjaDescText.x = 1800 * 0.25;
ninjaDescText.y = 1200;
wizardDescText = new Text2('🔵 BLUE MAGE\n"Slow the rhythm, control the chaos."\nAbility: Ice Freeze slows monsters', {
size: 50,
fill: 0x4169e1
});
wizardDescText.anchor.set(0.5, 0);
game.addChild(wizardDescText);
wizardDescText.x = 2048 * 0.5;
wizardDescText.y = 1400;
swordmasterDescText = new Text2('⚔️ DUAL BLADE SWORDMASTER\n"Two blades. One rhythm."\nAbility: Creat Ai clon ', {
size: 50,
fill: 0x8b4513
});
swordmasterDescText.anchor.set(0.5, 0);
game.addChild(swordmasterDescText);
swordmasterDescText.x = 2150 * 0.75;
swordmasterDescText.y = 1600;
instructionText = new Text2('TAP A CHARACTER TO BEGIN', {
size: 50,
fill: 0xFFFFFF
});
instructionText.anchor.set(0.5, 1);
LK.gui.bottom.addChild(instructionText);
instructionText.y = -50;
// Add move handler for dragging
game.move = function (x, y, obj) {
if (gameState === 'playing' && isDragging && selectedCharacter) {
// Constrain character movement to targetZone bounds
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
// Keep character within targetZone
selectedCharacter.x = Math.max(targetLeft + 60, Math.min(targetRight - 60, x));
selectedCharacter.y = Math.max(targetTop + 60, Math.min(targetBottom - 60, y));
}
};
// Input handling
game.down = function (x, y, obj) {
if (gameState === 'classSelection') {
// Check character selection using bounds checking
var ninjaLeft = characters.ninja.x - 60;
var ninjaRight = characters.ninja.x + 60;
var ninjaTop = characters.ninja.y - 60;
var ninjaBottom = characters.ninja.y + 60;
if (x >= ninjaLeft && x <= ninjaRight && y >= ninjaTop && y <= ninjaBottom) {
selectCharacter('ninja');
} else {
var wizardLeft = characters.wizard.x - 60;
var wizardRight = characters.wizard.x + 60;
var wizardTop = characters.wizard.y - 60;
var wizardBottom = characters.wizard.y + 60;
if (x >= wizardLeft && x <= wizardRight && y >= wizardTop && y <= wizardBottom) {
selectCharacter('wizard');
} else {
var swordmasterLeft = characters.swordmaster.x - 60;
var swordmasterRight = characters.swordmaster.x + 60;
var swordmasterTop = characters.swordmaster.y - 60;
var swordmasterBottom = characters.swordmaster.y + 60;
if (x >= swordmasterLeft && x <= swordmasterRight && y >= swordmasterTop && y <= swordmasterBottom) {
selectCharacter('swordmaster');
}
}
}
} else if (gameState === 'playing') {
// Check if tapping on character
var charLeft = selectedCharacter.x - 60;
var charRight = selectedCharacter.x + 60;
var charTop = selectedCharacter.y - 60;
var charBottom = selectedCharacter.y + 60;
if (x >= charLeft && x <= charRight && y >= charTop && y <= charBottom) {
// Start dragging
isDragging = true;
dragStartX = x;
dragStartY = y;
} else {
// Check if tap is within targetZone bounds for orb hits (fallback for non-drag gameplay)
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
if (x >= targetLeft && x <= targetRight && y >= targetTop && y <= targetBottom) {
// Determine hit type based on tap position (Beat Saber style)
var hitType = x < 2048 * 0.5 ? 'red' : 'green';
handleOrbHit(x, y, hitType);
}
}
}
};
// Handle touch/mouse release
game.up = function (x, y, obj) {
if (gameState === 'playing' && isDragging) {
isDragging = false;
// Check if this was a tap (minimal movement) for ability activation
var moveDistance = Math.sqrt(Math.pow(x - dragStartX, 2) + Math.pow(y - dragStartY, 2));
if (moveDistance < 30) {
// Minimal movement threshold
// This was a tap, not a drag - trigger ability
if (selectedCharacter && selectedCharacter.abilityReady) {
selectedCharacter.useAbility();
updateAbilityText();
}
}
}
};
function selectCharacter(type) {
selectedCharacter = characters[type];
gameState = 'playing';
// Hide character descriptions and selection UI
titleText.visible = false;
ninjaDescText.visible = false;
wizardDescText.visible = false;
swordmasterDescText.visible = false;
instructionText.visible = false;
// Hide unselected characters
for (var charType in characters) {
if (charType !== type) {
characters[charType].visible = false;
}
}
// Position selected character in targetZone
selectedCharacter.x = 2048 / 2;
selectedCharacter.y = targetZone.y;
// Character-specific selection effects
if (type === 'ninja') {
LK.effects.flashScreen(0x9932cc, 800);
} else if (type === 'wizard') {
LK.effects.flashScreen(0x4169e1, 800);
} else if (type === 'swordmaster') {
LK.effects.flashScreen(0xffd700, 800);
}
// Start bgtrack music for gameplay
LK.stopMusic();
LK.playMusic('bgtrack', {
volume: 0.9,
loop: true
});
}
function checkCharacterOrbCollisions() {
if (!selectedCharacter || gameState !== 'playing') {
return;
}
// Check collision with each orb
for (var i = orbs.length - 1; i >= 0; i--) {
var orb = orbs[i];
if (orb.hit) {
continue;
}
// Check if orb is within targetZone
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
var orbInZone = orb.x >= targetLeft && orb.x <= targetRight && orb.y >= targetTop && orb.y <= targetBottom;
if (orbInZone) {
// Check collision with character (simple distance check)
var dx = orb.x - selectedCharacter.x;
var dy = orb.y - selectedCharacter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 80) {
// Collision threshold
if (orb.orbType === 'blackBomb') {
// Check if ninja is in phase mode
if (selectedCharacter.type === 'ninja' && selectedCharacter.phaseMode) {
// Phase through black orb without penalty
orb.hit = true;
LK.effects.flashObject(selectedCharacter, 0x9932cc, 200);
} else {
// Normal black orb collision
blackTouchCount++;
combo = 0; // Reset combo on black orb hit
if (blackTouchCount >= 5) {
// Fifth black orb touch - game over
LK.getSound('bomb').play();
LK.effects.flashScreen(0xff0000, 500);
endGame();
return;
} else {
// Black orb touch - visual feedback
LK.getSound('bomb').play();
LK.effects.flashScreen(0xff0000, 200);
orb.hit = true;
}
}
} else {
// Hit red or green orb - always successful when touching character
var points = 100;
if (comboMode > 0) {
comboMode--;
points *= 2; // Ninja bonus
}
if (dualStrikeActive) {
points *= 2; // Swordmaster bonus
dualStrikeActive = false;
}
combo++;
if (combo > maxCombo) {
maxCombo = combo;
}
// Increase BPM on orb hit
increaseBPMOnHit();
points *= Math.min(combo, 10);
score += points;
if (orb.orbType === 'redOrb') {
LK.getSound('hitr').play();
} else {
LK.getSound('hit').play();
}
// Visual feedback
var hitColor = orb.orbType === 'redOrb' ? 0xff4444 : 0x44ff44;
LK.effects.flashScreen(hitColor, 150);
tween(orb, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300
});
orb.hit = true;
updateUI();
}
}
}
}
}
function handleOrbHit(tapX, tapY, expectedType) {
var hitOrb = null;
var bestDistance = Infinity;
// Find closest orb near tap position within targetZone (Beat Saber style)
for (var i = 0; i < orbs.length; i++) {
var orb = orbs[i];
if (!orb.hit) {
// Check if orb is within targetZone bounds
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
var orbInZone = orb.x >= targetLeft && orb.x <= targetRight && orb.y >= targetTop && orb.y <= targetBottom;
if (orbInZone) {
var dx = orb.x - tapX;
var dy = orb.y - tapY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 120 && distance < bestDistance) {
hitOrb = orb;
bestDistance = distance;
}
}
}
}
if (hitOrb) {
if (hitOrb.orbType === 'blackBomb') {
// Hit a bomb - lose life
lives--;
combo = 0;
LK.getSound('bomb').play();
LK.effects.flashScreen(0xff0000, 300);
hitOrb.hit = true;
if (lives <= 0) {
endGame();
return;
}
} else if (hitOrb.orbType === 'redOrb' && expectedType === 'red' || hitOrb.orbType === 'greenOrb' && expectedType === 'green' || comboMode > 0) {
// Successful hit
var points = 100;
if (comboMode > 0) {
comboMode--;
points *= 2; // Ninja bonus
}
if (dualStrikeActive) {
points *= 2; // Swordmaster bonus
dualStrikeActive = false;
}
combo++;
if (combo > maxCombo) {
maxCombo = combo;
}
// Increase BPM on orb hit
increaseBPMOnHit();
// Combo multiplier
points *= Math.min(combo, 10);
score += points;
if (hitOrb.orbType === 'redOrb') {
LK.getSound('hitr').play();
} else {
LK.getSound('hit').play();
}
// Enhanced visual feedback for Beat Saber style
var hitColor = hitOrb.orbType === 'redOrb' ? 0xff4444 : 0x44ff44;
LK.effects.flashScreen(hitColor, 150);
tween(hitOrb, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300
});
hitOrb.hit = true;
} else {
// Wrong color hit
combo = 0;
LK.getSound('miss').play();
}
} else {
// Missed - no orb in range
combo = 0;
LK.getSound('miss').play();
}
updateUI();
}
function spawnSwordmasterClone() {
if (swordmasterClone) {
return;
} // Only one clone at a time
swordmasterClone = game.addChild(LK.getAsset('swordmaster', {
anchorX: 0.5,
anchorY: 0.5,
x: selectedCharacter.x + 150,
y: selectedCharacter.y,
alpha: 0.7
}));
cloneActiveTime = 2000; // 2 seconds
// Add AI properties to clone
swordmasterClone.speed = 8;
swordmasterClone.targetOrb = null;
swordmasterClone.lastAutoHitTime = 0;
// Flash effect for clone spawn
LK.effects.flashObject(swordmasterClone, 0xffd700, 300);
}
function spawnOrb() {
var orbTypes = ['redOrb', 'greenOrb', 'blackBomb'];
var weights = [0.4, 0.4, 0.2]; // 40% red, 40% green, 20% bomb
var rand = Math.random();
var orbType = 'redOrb';
if (rand < weights[0]) {
orbType = 'redOrb';
} else if (rand < weights[0] + weights[1]) {
orbType = 'greenOrb';
} else {
orbType = 'blackBomb';
}
var orb = new BeatOrb(orbType);
// Spawn from random position at top of screen
orb.x = 400 + Math.random() * 1248; // Random X within safe bounds
orb.y = -100; // Start above screen
orbs.push(orb);
game.addChild(orb);
// Add spawn effect
LK.effects.flashObject(orb, 0xffffff, 200);
}
function updateCloneAI() {
if (!swordmasterClone) {
return;
}
// Find nearest red or green orb within targetZone
var nearestOrb = null;
var nearestDistance = Infinity;
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
for (var i = 0; i < orbs.length; i++) {
var orb = orbs[i];
if (!orb.hit && (orb.orbType === 'redOrb' || orb.orbType === 'greenOrb')) {
// Check if orb is within targetZone
var orbInZone = orb.x >= targetLeft && orb.x <= targetRight && orb.y >= targetTop && orb.y <= targetBottom;
if (orbInZone) {
var dx = orb.x - swordmasterClone.x;
var dy = orb.y - swordmasterClone.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < nearestDistance) {
nearestOrb = orb;
nearestDistance = distance;
}
}
}
}
// Move toward nearest orb
if (nearestOrb) {
var dx = nearestOrb.x - swordmasterClone.x;
var dy = nearestOrb.y - swordmasterClone.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction and apply speed
var moveX = dx / distance * swordmasterClone.speed;
var moveY = dy / distance * swordmasterClone.speed;
// Update clone position
var newX = swordmasterClone.x + moveX;
var newY = swordmasterClone.y + moveY;
// Constrain to targetZone bounds
swordmasterClone.x = Math.max(targetLeft + 60, Math.min(targetRight - 60, newX));
swordmasterClone.y = Math.max(targetTop + 60, Math.min(targetBottom - 60, newY));
}
}
}
function autoHitOrbWithClone() {
if (!swordmasterClone) {
return;
}
// Find closest non-bomb orb within range to auto-hit
var closestOrb = null;
var closestDistance = Infinity;
for (var i = 0; i < orbs.length; i++) {
var orb = orbs[i];
if (!orb.hit && orb.orbType !== 'blackBomb') {
// Check if orb is within targetZone bounds
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
var orbInZone = orb.x >= targetLeft && orb.x <= targetRight && orb.y >= targetTop && orb.y <= targetBottom;
if (orbInZone) {
var dx = orb.x - swordmasterClone.x;
var dy = orb.y - swordmasterClone.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Only target orbs within range (distance < 100)
if (distance < 100 && distance < closestDistance) {
closestOrb = orb;
closestDistance = distance;
}
}
}
}
// Auto-hit the closest orb in range
if (closestOrb) {
var points = 150; // Clone hits give bonus points
combo++;
if (combo > maxCombo) {
maxCombo = combo;
}
// Increase BPM on orb hit
increaseBPMOnHit();
points *= Math.min(combo, 10);
score += points;
if (closestOrb.orbType === 'redOrb') {
LK.getSound('hitr').play();
} else {
LK.getSound('hit').play();
}
// Visual feedback
var hitColor = closestOrb.orbType === 'redOrb' ? 0xff4444 : 0x44ff44;
LK.effects.flashScreen(hitColor, 100);
tween(closestOrb, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300
});
closestOrb.hit = true;
updateUI();
}
}
function updateUI() {
scoreText.setText('Score: ' + score);
comboText.setText('Combo: ' + combo);
livesText.setText('Missed: ' + missedCount + '/5 | Black: ' + blackTouchCount + '/5');
LK.setScore(score);
}
function updateAbilityText() {
if (!selectedCharacter) {
return;
}
if (selectedCharacter.abilityReady) {
abilityText.setText('Ability Ready!');
abilityText.tint = 0x00ff00;
} else {
var cooldownSeconds = Math.ceil(selectedCharacter.abilityCooldown / 1000);
abilityText.setText('Ability: ' + cooldownSeconds + 's');
abilityText.tint = 0xff4444;
}
}
function gameOver() {
gameState = 'gameOver';
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
}
function endGame() {
gameState = 'gameOver';
// Save high score
var highScore = storage.highScore || 0;
if (score > highScore) {
storage.highScore = score;
}
// Save stats
storage.gamesPlayed = (storage.gamesPlayed || 0) + 1;
if (maxCombo > (storage.maxCombo || 0)) {
storage.maxCombo = maxCombo;
}
LK.showGameOver();
}
function checkWinCondition() {
if (songProgress >= songDuration) {
// Survived the full song!
var bonusScore = lives * 1000 + maxCombo * 100;
score += bonusScore;
LK.setScore(score);
// Save completion
storage.songsCompleted = (storage.songsCompleted || 0) + 1;
LK.showYouWin();
}
}
// Main game update loop
game.update = function () {
if (gameState !== 'playing') {
return;
}
songProgress += 16; // ~60fps
// Update character abilities
if (selectedCharacter) {
selectedCharacter.update();
updateAbilityText();
}
// Check character-orb collisions
checkCharacterOrbCollisions();
// Update swordmaster clone
if (swordmasterClone && cloneActiveTime > 0) {
cloneActiveTime -= 16; // ~60fps
// Update clone AI movement
updateCloneAI();
// Clone auto-hits orbs every 100ms
var currentTime = Date.now();
if (currentTime - swordmasterClone.lastAutoHitTime > 100) {
autoHitOrbWithClone();
swordmasterClone.lastAutoHitTime = currentTime;
}
// Remove clone when time expires
if (cloneActiveTime <= 0) {
tween(swordmasterClone, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 300,
onFinish: function onFinish() {
if (swordmasterClone) {
swordmasterClone.destroy();
swordmasterClone = null;
}
}
});
}
}
// Spawn orbs based on BPM timing (Beat Saber style)
var currentTime = Date.now();
if (currentTime - lastSpawnTime > spawnInterval) {
spawnOrb();
lastSpawnTime = currentTime;
}
// Update orbs
for (var i = orbs.length - 1; i >= 0; i--) {
var orb = orbs[i];
// Remove hit orbs after animation
if (orb.hit) {
orb.alpha -= 0.1;
if (orb.alpha <= 0) {
orb.destroy();
orbs.splice(i, 1);
}
continue;
}
// Check if orb passed the targetZone (missed)
var targetBottom = targetZone.y + targetZone.height / 2;
if (orb.y > targetBottom + 50) {
if (orb.orbType === 'blackBomb') {
// Bomb passed through - no penalty for missing bombs
} else {
// Missed a good orb
missedCount++;
combo = 0;
LK.getSound('miss').play();
// Check if missed 5 orbs
if (missedCount >= 5) {
endGame();
return;
}
}
orb.destroy();
orbs.splice(i, 1);
updateUI();
continue;
}
}
// Check win condition
checkWinCondition();
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var BeatOrb = Container.expand(function (orbType) {
var self = Container.call(this);
self.orbType = orbType;
self.speed = 4;
self.hit = false;
var orbGraphics = self.attachAsset(orbType, {
anchorX: 0.5,
anchorY: 0.5
});
self.update = function () {
var speedMultiplier = 1;
if (selectedCharacter && selectedCharacter.freezeActive) {
speedMultiplier = 0.3;
}
// Simple fall down movement
self.y += self.speed * speedMultiplier;
};
return self;
});
var Character = Container.expand(function (characterType) {
var self = Container.call(this);
self.type = characterType;
self.abilityReady = true;
self.abilityCooldown = 0;
self.freezeActive = false;
self.freezeTime = 0;
self.phaseMode = false;
self.phaseModeTime = 0;
self.baseY = 0;
self.floatOffset = 0;
self.animSpeed = 0.05 + Math.random() * 0.03;
var characterGraphics = self.attachAsset(characterType, {
anchorX: 0.5,
anchorY: 0.5
});
self.useAbility = function () {
if (!self.abilityReady) {
return false;
}
LK.getSound('ability').play();
if (self.type === 'ninja') {
// Phase Mode: Pass through black orbs for 3 seconds
self.phaseMode = true;
self.phaseModeTime = 3000;
// Add glowing effect
tween(self, {
tint: 0x9932cc
}, {
duration: 500
});
LK.effects.flashObject(self, 0x9932cc, 3000);
} else if (self.type === 'wizard') {
// Time Freeze: Slow orbs for 3 seconds
self.freezeActive = true;
self.freezeTime = 3000;
LK.effects.flashObject(self, 0x00ffff, 3000);
} else if (self.type === 'swordmaster') {
// Clone Ability: Spawn temporary clone that auto-hits orbs
spawnSwordmasterClone();
LK.effects.flashObject(self, 0xffd700, 1000);
}
self.abilityReady = false;
self.abilityCooldown = 10000; // 10 second cooldown
return true;
};
self.update = function () {
if (!self.abilityReady) {
self.abilityCooldown -= 16; // ~60fps
if (self.abilityCooldown <= 0) {
self.abilityReady = true;
}
}
if (self.freezeActive) {
self.freezeTime -= 16;
if (self.freezeTime <= 0) {
self.freezeActive = false;
}
}
if (self.phaseMode) {
self.phaseModeTime -= 16;
if (self.phaseModeTime <= 0) {
self.phaseMode = false;
// Remove glow effect
tween(self, {
tint: 0xffffff
}, {
duration: 500
});
}
}
// Floating animation for character selection
if (gameState === 'classSelection') {
self.floatOffset += self.animSpeed;
self.y = self.baseY + Math.sin(self.floatOffset) * 20;
// Add mystical particle effect for wizard
if (self.type === 'wizard' && LK.ticks % 20 === 0) {
LK.effects.flashObject(self, 0x4169e1, 400);
}
// Add shadow dash effect for ninja
if (self.type === 'ninja' && LK.ticks % 30 === 0) {
LK.effects.flashObject(self, 0x9932cc, 300);
}
// Add sword gleam for swordmaster
if (self.type === 'swordmaster' && LK.ticks % 25 === 0) {
LK.effects.flashObject(self, 0xffd700, 350);
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a2e
});
/****
* Game Code
****/
// Game state
var gameState = 'classSelection'; // 'classSelection', 'playing', 'gameOver'
var selectedCharacter = null;
var score = 0;
var combo = 0;
var maxCombo = 0;
var lives = 3;
var comboMode = 0; // Ninja ability counter
var dualStrikeActive = false; // Swordmaster ability
var swordmasterClone = null;
var cloneActiveTime = 0;
var songProgress = 0;
var songDuration = 120000; // 2 minutes
// Orb speed mechanics
var baseOrbSpeed = 4; // Base fall speed for orbs
// BPM configuration for music synchronization
var baseBPM = 120; // Base BPM for the background music
var currentBPM = baseBPM; // Current BPM (can be modified dynamically)
// Function to calculate spawn interval based on current BPM
function calculateSpawnInterval() {
// Convert BPM to milliseconds per beat
// 60 seconds = 60000ms, so interval = 60000 / BPM
return Math.max(200, 60000 / currentBPM); // Minimum 200ms to prevent overwhelming
}
// Function to increase BPM when orb is hit
function increaseBPMOnHit() {
currentBPM = Math.min(200, currentBPM + 2); // Increase BPM by 2, cap at 200
spawnInterval = calculateSpawnInterval();
}
// Drag mechanics
var isDragging = false;
var dragStartX = 0;
var dragStartY = 0;
var blackOrbHits = 0; // Track black orb hits for game over
var missedCount = 0; // Track missed red/green orbs
var blackTouchCount = 0; // Track black orb touches
// Game objects
var orbs = [];
var lanes = [];
var characters = {};
var targetZone = null;
// UI elements
var scoreText = null;
var comboText = null;
var livesText = null;
var abilityText = null;
var instructionText = null;
var ninjaDescText = null;
var wizardDescText = null;
var swordmasterDescText = null;
var titleText = null;
// Target position for Beat Saber style
var targetY = 2200;
var lastSpawnTime = 0;
var spawnInterval = calculateSpawnInterval(); // BPM-based spawn interval
// Start ambient music for character selection
LK.playMusic('ambient', {
volume: 0.6
});
// Target zone
targetZone = game.addChild(LK.getAsset('targetZone', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2400,
alpha: 0.5,
width: 2048,
height: 300,
tint: 0x000080
}));
// Create character selection
characters.ninja = game.addChild(new Character('ninja'));
characters.ninja.x = 1500 * 0.25;
characters.ninja.y = 1000;
characters.ninja.baseY = 1000;
characters.wizard = game.addChild(new Character('wizard'));
characters.wizard.x = 2048 * 0.5;
characters.wizard.y = 1200;
characters.wizard.baseY = 1200;
characters.swordmaster = game.addChild(new Character('swordmaster'));
characters.swordmaster.x = 2200 * 0.75;
characters.swordmaster.y = 1400;
characters.swordmaster.baseY = 1400;
// UI Setup
scoreText = new Text2('Score: 0', {
size: 60,
fill: 0xFFFFFF
});
scoreText.anchor.set(0, 0);
LK.gui.topLeft.addChild(scoreText);
scoreText.x = 120;
scoreText.y = 20;
comboText = new Text2('Combo: 0', {
size: 50,
fill: 0xFFFF00
});
comboText.anchor.set(0.5, 0);
LK.gui.top.addChild(comboText);
comboText.y = 80;
livesText = new Text2('Lives: 3', {
size: 50,
fill: 0xFF4444
});
livesText.anchor.set(1, 0);
LK.gui.topRight.addChild(livesText);
livesText.x = -20;
livesText.y = 20;
abilityText = new Text2('Ability Ready!', {
size: 40,
fill: 0x00FF00
});
abilityText.anchor.set(0.5, 1);
LK.gui.bottom.addChild(abilityText);
abilityText.y = -100;
// Title text
titleText = new Text2('RHYTHM HEROES\nBeat Battle Arena', {
size: 80,
fill: 0xFFD700
});
titleText.anchor.set(0.5, 0);
LK.gui.top.addChild(titleText);
titleText.y = 200;
// Character descriptions
ninjaDescText = new Text2('⚫ SHADOW NINJA\n"Strike with silence. Dance in shadow."\nAbility: Pass through black bomb \nmonsters for 3 seconds', {
size: 45,
fill: 0x9932cc
});
ninjaDescText.anchor.set(0.5, 0);
game.addChild(ninjaDescText);
ninjaDescText.x = 1800 * 0.25;
ninjaDescText.y = 1200;
wizardDescText = new Text2('🔵 BLUE MAGE\n"Slow the rhythm, control the chaos."\nAbility: Ice Freeze slows monsters', {
size: 50,
fill: 0x4169e1
});
wizardDescText.anchor.set(0.5, 0);
game.addChild(wizardDescText);
wizardDescText.x = 2048 * 0.5;
wizardDescText.y = 1400;
swordmasterDescText = new Text2('⚔️ DUAL BLADE SWORDMASTER\n"Two blades. One rhythm."\nAbility: Creat Ai clon ', {
size: 50,
fill: 0x8b4513
});
swordmasterDescText.anchor.set(0.5, 0);
game.addChild(swordmasterDescText);
swordmasterDescText.x = 2150 * 0.75;
swordmasterDescText.y = 1600;
instructionText = new Text2('TAP A CHARACTER TO BEGIN', {
size: 50,
fill: 0xFFFFFF
});
instructionText.anchor.set(0.5, 1);
LK.gui.bottom.addChild(instructionText);
instructionText.y = -50;
// Add move handler for dragging
game.move = function (x, y, obj) {
if (gameState === 'playing' && isDragging && selectedCharacter) {
// Constrain character movement to targetZone bounds
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
// Keep character within targetZone
selectedCharacter.x = Math.max(targetLeft + 60, Math.min(targetRight - 60, x));
selectedCharacter.y = Math.max(targetTop + 60, Math.min(targetBottom - 60, y));
}
};
// Input handling
game.down = function (x, y, obj) {
if (gameState === 'classSelection') {
// Check character selection using bounds checking
var ninjaLeft = characters.ninja.x - 60;
var ninjaRight = characters.ninja.x + 60;
var ninjaTop = characters.ninja.y - 60;
var ninjaBottom = characters.ninja.y + 60;
if (x >= ninjaLeft && x <= ninjaRight && y >= ninjaTop && y <= ninjaBottom) {
selectCharacter('ninja');
} else {
var wizardLeft = characters.wizard.x - 60;
var wizardRight = characters.wizard.x + 60;
var wizardTop = characters.wizard.y - 60;
var wizardBottom = characters.wizard.y + 60;
if (x >= wizardLeft && x <= wizardRight && y >= wizardTop && y <= wizardBottom) {
selectCharacter('wizard');
} else {
var swordmasterLeft = characters.swordmaster.x - 60;
var swordmasterRight = characters.swordmaster.x + 60;
var swordmasterTop = characters.swordmaster.y - 60;
var swordmasterBottom = characters.swordmaster.y + 60;
if (x >= swordmasterLeft && x <= swordmasterRight && y >= swordmasterTop && y <= swordmasterBottom) {
selectCharacter('swordmaster');
}
}
}
} else if (gameState === 'playing') {
// Check if tapping on character
var charLeft = selectedCharacter.x - 60;
var charRight = selectedCharacter.x + 60;
var charTop = selectedCharacter.y - 60;
var charBottom = selectedCharacter.y + 60;
if (x >= charLeft && x <= charRight && y >= charTop && y <= charBottom) {
// Start dragging
isDragging = true;
dragStartX = x;
dragStartY = y;
} else {
// Check if tap is within targetZone bounds for orb hits (fallback for non-drag gameplay)
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
if (x >= targetLeft && x <= targetRight && y >= targetTop && y <= targetBottom) {
// Determine hit type based on tap position (Beat Saber style)
var hitType = x < 2048 * 0.5 ? 'red' : 'green';
handleOrbHit(x, y, hitType);
}
}
}
};
// Handle touch/mouse release
game.up = function (x, y, obj) {
if (gameState === 'playing' && isDragging) {
isDragging = false;
// Check if this was a tap (minimal movement) for ability activation
var moveDistance = Math.sqrt(Math.pow(x - dragStartX, 2) + Math.pow(y - dragStartY, 2));
if (moveDistance < 30) {
// Minimal movement threshold
// This was a tap, not a drag - trigger ability
if (selectedCharacter && selectedCharacter.abilityReady) {
selectedCharacter.useAbility();
updateAbilityText();
}
}
}
};
function selectCharacter(type) {
selectedCharacter = characters[type];
gameState = 'playing';
// Hide character descriptions and selection UI
titleText.visible = false;
ninjaDescText.visible = false;
wizardDescText.visible = false;
swordmasterDescText.visible = false;
instructionText.visible = false;
// Hide unselected characters
for (var charType in characters) {
if (charType !== type) {
characters[charType].visible = false;
}
}
// Position selected character in targetZone
selectedCharacter.x = 2048 / 2;
selectedCharacter.y = targetZone.y;
// Character-specific selection effects
if (type === 'ninja') {
LK.effects.flashScreen(0x9932cc, 800);
} else if (type === 'wizard') {
LK.effects.flashScreen(0x4169e1, 800);
} else if (type === 'swordmaster') {
LK.effects.flashScreen(0xffd700, 800);
}
// Start bgtrack music for gameplay
LK.stopMusic();
LK.playMusic('bgtrack', {
volume: 0.9,
loop: true
});
}
function checkCharacterOrbCollisions() {
if (!selectedCharacter || gameState !== 'playing') {
return;
}
// Check collision with each orb
for (var i = orbs.length - 1; i >= 0; i--) {
var orb = orbs[i];
if (orb.hit) {
continue;
}
// Check if orb is within targetZone
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
var orbInZone = orb.x >= targetLeft && orb.x <= targetRight && orb.y >= targetTop && orb.y <= targetBottom;
if (orbInZone) {
// Check collision with character (simple distance check)
var dx = orb.x - selectedCharacter.x;
var dy = orb.y - selectedCharacter.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 80) {
// Collision threshold
if (orb.orbType === 'blackBomb') {
// Check if ninja is in phase mode
if (selectedCharacter.type === 'ninja' && selectedCharacter.phaseMode) {
// Phase through black orb without penalty
orb.hit = true;
LK.effects.flashObject(selectedCharacter, 0x9932cc, 200);
} else {
// Normal black orb collision
blackTouchCount++;
combo = 0; // Reset combo on black orb hit
if (blackTouchCount >= 5) {
// Fifth black orb touch - game over
LK.getSound('bomb').play();
LK.effects.flashScreen(0xff0000, 500);
endGame();
return;
} else {
// Black orb touch - visual feedback
LK.getSound('bomb').play();
LK.effects.flashScreen(0xff0000, 200);
orb.hit = true;
}
}
} else {
// Hit red or green orb - always successful when touching character
var points = 100;
if (comboMode > 0) {
comboMode--;
points *= 2; // Ninja bonus
}
if (dualStrikeActive) {
points *= 2; // Swordmaster bonus
dualStrikeActive = false;
}
combo++;
if (combo > maxCombo) {
maxCombo = combo;
}
// Increase BPM on orb hit
increaseBPMOnHit();
points *= Math.min(combo, 10);
score += points;
if (orb.orbType === 'redOrb') {
LK.getSound('hitr').play();
} else {
LK.getSound('hit').play();
}
// Visual feedback
var hitColor = orb.orbType === 'redOrb' ? 0xff4444 : 0x44ff44;
LK.effects.flashScreen(hitColor, 150);
tween(orb, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300
});
orb.hit = true;
updateUI();
}
}
}
}
}
function handleOrbHit(tapX, tapY, expectedType) {
var hitOrb = null;
var bestDistance = Infinity;
// Find closest orb near tap position within targetZone (Beat Saber style)
for (var i = 0; i < orbs.length; i++) {
var orb = orbs[i];
if (!orb.hit) {
// Check if orb is within targetZone bounds
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
var orbInZone = orb.x >= targetLeft && orb.x <= targetRight && orb.y >= targetTop && orb.y <= targetBottom;
if (orbInZone) {
var dx = orb.x - tapX;
var dy = orb.y - tapY;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 120 && distance < bestDistance) {
hitOrb = orb;
bestDistance = distance;
}
}
}
}
if (hitOrb) {
if (hitOrb.orbType === 'blackBomb') {
// Hit a bomb - lose life
lives--;
combo = 0;
LK.getSound('bomb').play();
LK.effects.flashScreen(0xff0000, 300);
hitOrb.hit = true;
if (lives <= 0) {
endGame();
return;
}
} else if (hitOrb.orbType === 'redOrb' && expectedType === 'red' || hitOrb.orbType === 'greenOrb' && expectedType === 'green' || comboMode > 0) {
// Successful hit
var points = 100;
if (comboMode > 0) {
comboMode--;
points *= 2; // Ninja bonus
}
if (dualStrikeActive) {
points *= 2; // Swordmaster bonus
dualStrikeActive = false;
}
combo++;
if (combo > maxCombo) {
maxCombo = combo;
}
// Increase BPM on orb hit
increaseBPMOnHit();
// Combo multiplier
points *= Math.min(combo, 10);
score += points;
if (hitOrb.orbType === 'redOrb') {
LK.getSound('hitr').play();
} else {
LK.getSound('hit').play();
}
// Enhanced visual feedback for Beat Saber style
var hitColor = hitOrb.orbType === 'redOrb' ? 0xff4444 : 0x44ff44;
LK.effects.flashScreen(hitColor, 150);
tween(hitOrb, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300
});
hitOrb.hit = true;
} else {
// Wrong color hit
combo = 0;
LK.getSound('miss').play();
}
} else {
// Missed - no orb in range
combo = 0;
LK.getSound('miss').play();
}
updateUI();
}
function spawnSwordmasterClone() {
if (swordmasterClone) {
return;
} // Only one clone at a time
swordmasterClone = game.addChild(LK.getAsset('swordmaster', {
anchorX: 0.5,
anchorY: 0.5,
x: selectedCharacter.x + 150,
y: selectedCharacter.y,
alpha: 0.7
}));
cloneActiveTime = 2000; // 2 seconds
// Add AI properties to clone
swordmasterClone.speed = 8;
swordmasterClone.targetOrb = null;
swordmasterClone.lastAutoHitTime = 0;
// Flash effect for clone spawn
LK.effects.flashObject(swordmasterClone, 0xffd700, 300);
}
function spawnOrb() {
var orbTypes = ['redOrb', 'greenOrb', 'blackBomb'];
var weights = [0.4, 0.4, 0.2]; // 40% red, 40% green, 20% bomb
var rand = Math.random();
var orbType = 'redOrb';
if (rand < weights[0]) {
orbType = 'redOrb';
} else if (rand < weights[0] + weights[1]) {
orbType = 'greenOrb';
} else {
orbType = 'blackBomb';
}
var orb = new BeatOrb(orbType);
// Spawn from random position at top of screen
orb.x = 400 + Math.random() * 1248; // Random X within safe bounds
orb.y = -100; // Start above screen
orbs.push(orb);
game.addChild(orb);
// Add spawn effect
LK.effects.flashObject(orb, 0xffffff, 200);
}
function updateCloneAI() {
if (!swordmasterClone) {
return;
}
// Find nearest red or green orb within targetZone
var nearestOrb = null;
var nearestDistance = Infinity;
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
for (var i = 0; i < orbs.length; i++) {
var orb = orbs[i];
if (!orb.hit && (orb.orbType === 'redOrb' || orb.orbType === 'greenOrb')) {
// Check if orb is within targetZone
var orbInZone = orb.x >= targetLeft && orb.x <= targetRight && orb.y >= targetTop && orb.y <= targetBottom;
if (orbInZone) {
var dx = orb.x - swordmasterClone.x;
var dy = orb.y - swordmasterClone.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < nearestDistance) {
nearestOrb = orb;
nearestDistance = distance;
}
}
}
}
// Move toward nearest orb
if (nearestOrb) {
var dx = nearestOrb.x - swordmasterClone.x;
var dy = nearestOrb.y - swordmasterClone.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
// Normalize direction and apply speed
var moveX = dx / distance * swordmasterClone.speed;
var moveY = dy / distance * swordmasterClone.speed;
// Update clone position
var newX = swordmasterClone.x + moveX;
var newY = swordmasterClone.y + moveY;
// Constrain to targetZone bounds
swordmasterClone.x = Math.max(targetLeft + 60, Math.min(targetRight - 60, newX));
swordmasterClone.y = Math.max(targetTop + 60, Math.min(targetBottom - 60, newY));
}
}
}
function autoHitOrbWithClone() {
if (!swordmasterClone) {
return;
}
// Find closest non-bomb orb within range to auto-hit
var closestOrb = null;
var closestDistance = Infinity;
for (var i = 0; i < orbs.length; i++) {
var orb = orbs[i];
if (!orb.hit && orb.orbType !== 'blackBomb') {
// Check if orb is within targetZone bounds
var targetLeft = targetZone.x - targetZone.width / 2;
var targetRight = targetZone.x + targetZone.width / 2;
var targetTop = targetZone.y - targetZone.height / 2;
var targetBottom = targetZone.y + targetZone.height / 2;
var orbInZone = orb.x >= targetLeft && orb.x <= targetRight && orb.y >= targetTop && orb.y <= targetBottom;
if (orbInZone) {
var dx = orb.x - swordmasterClone.x;
var dy = orb.y - swordmasterClone.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Only target orbs within range (distance < 100)
if (distance < 100 && distance < closestDistance) {
closestOrb = orb;
closestDistance = distance;
}
}
}
}
// Auto-hit the closest orb in range
if (closestOrb) {
var points = 150; // Clone hits give bonus points
combo++;
if (combo > maxCombo) {
maxCombo = combo;
}
// Increase BPM on orb hit
increaseBPMOnHit();
points *= Math.min(combo, 10);
score += points;
if (closestOrb.orbType === 'redOrb') {
LK.getSound('hitr').play();
} else {
LK.getSound('hit').play();
}
// Visual feedback
var hitColor = closestOrb.orbType === 'redOrb' ? 0xff4444 : 0x44ff44;
LK.effects.flashScreen(hitColor, 100);
tween(closestOrb, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 300
});
closestOrb.hit = true;
updateUI();
}
}
function updateUI() {
scoreText.setText('Score: ' + score);
comboText.setText('Combo: ' + combo);
livesText.setText('Missed: ' + missedCount + '/5 | Black: ' + blackTouchCount + '/5');
LK.setScore(score);
}
function updateAbilityText() {
if (!selectedCharacter) {
return;
}
if (selectedCharacter.abilityReady) {
abilityText.setText('Ability Ready!');
abilityText.tint = 0x00ff00;
} else {
var cooldownSeconds = Math.ceil(selectedCharacter.abilityCooldown / 1000);
abilityText.setText('Ability: ' + cooldownSeconds + 's');
abilityText.tint = 0xff4444;
}
}
function gameOver() {
gameState = 'gameOver';
LK.effects.flashScreen(0xff0000, 1000);
LK.showGameOver();
}
function endGame() {
gameState = 'gameOver';
// Save high score
var highScore = storage.highScore || 0;
if (score > highScore) {
storage.highScore = score;
}
// Save stats
storage.gamesPlayed = (storage.gamesPlayed || 0) + 1;
if (maxCombo > (storage.maxCombo || 0)) {
storage.maxCombo = maxCombo;
}
LK.showGameOver();
}
function checkWinCondition() {
if (songProgress >= songDuration) {
// Survived the full song!
var bonusScore = lives * 1000 + maxCombo * 100;
score += bonusScore;
LK.setScore(score);
// Save completion
storage.songsCompleted = (storage.songsCompleted || 0) + 1;
LK.showYouWin();
}
}
// Main game update loop
game.update = function () {
if (gameState !== 'playing') {
return;
}
songProgress += 16; // ~60fps
// Update character abilities
if (selectedCharacter) {
selectedCharacter.update();
updateAbilityText();
}
// Check character-orb collisions
checkCharacterOrbCollisions();
// Update swordmaster clone
if (swordmasterClone && cloneActiveTime > 0) {
cloneActiveTime -= 16; // ~60fps
// Update clone AI movement
updateCloneAI();
// Clone auto-hits orbs every 100ms
var currentTime = Date.now();
if (currentTime - swordmasterClone.lastAutoHitTime > 100) {
autoHitOrbWithClone();
swordmasterClone.lastAutoHitTime = currentTime;
}
// Remove clone when time expires
if (cloneActiveTime <= 0) {
tween(swordmasterClone, {
alpha: 0,
scaleX: 0.5,
scaleY: 0.5
}, {
duration: 300,
onFinish: function onFinish() {
if (swordmasterClone) {
swordmasterClone.destroy();
swordmasterClone = null;
}
}
});
}
}
// Spawn orbs based on BPM timing (Beat Saber style)
var currentTime = Date.now();
if (currentTime - lastSpawnTime > spawnInterval) {
spawnOrb();
lastSpawnTime = currentTime;
}
// Update orbs
for (var i = orbs.length - 1; i >= 0; i--) {
var orb = orbs[i];
// Remove hit orbs after animation
if (orb.hit) {
orb.alpha -= 0.1;
if (orb.alpha <= 0) {
orb.destroy();
orbs.splice(i, 1);
}
continue;
}
// Check if orb passed the targetZone (missed)
var targetBottom = targetZone.y + targetZone.height / 2;
if (orb.y > targetBottom + 50) {
if (orb.orbType === 'blackBomb') {
// Bomb passed through - no penalty for missing bombs
} else {
// Missed a good orb
missedCount++;
combo = 0;
LK.getSound('miss').play();
// Check if missed 5 orbs
if (missedCount >= 5) {
endGame();
return;
}
}
orb.destroy();
orbs.splice(i, 1);
updateUI();
continue;
}
}
// Check win condition
checkWinCondition();
};
A ninja wearing tight black clothes, purple scarf, masked face, white skin, glowing purple eyes, slim and agile body, simple background, front-facing character with no background, standing pose, 2D game character. In-Game asset. 2d. High contrast. No shadows
A white-clothed male samurai with torn clothes, long gray hair tied back, pale skin, red belt on his waist, holding two swords, standing confidently, simple pose, no background, front-facing, 2D game character. In-Game asset. 2d. High contrast. No shadows
A female mage with pale skin, long white hair, wearing a long blue robe and a pointed blue hat, holding a glowing icy staff with a blue crystal, snowflake patterns on robe, cold expression, simple background, front-facing, no background, 2D game character. In-Game asset. 2d. High contrast. No shadows
Green goblin without backround. In-Game asset. 2d. High contrast. No shadows
Red wild monster. In-Game asset. 2d. High contrast. No shadows
Black monster , white eyes looks like bomb. In-Game asset. 2d. High contrast. No shadows