/**** * 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