/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { highScore: 0 }); /**** * Classes ****/ var HitFeedback = Container.expand(function () { var self = Container.call(this); self.feedbackText = null; self.messages = ['WOW!', 'AMAZING!!', 'INCREDIBLE!', 'SPECTACULAR!', 'SUPER!!', 'UNSTOPPABLE!!!']; self.init = function () { // Create feedback text with golden color self.feedbackText = new Text2('', { size: 480, fill: 0xFFD700, font: "'GillSans-Bold',Impact,'Arial Black',Tahoma" }); self.feedbackText.anchor.set(0.5, 0.5); self.addChild(self.feedbackText); // Initially hide the text self.feedbackText.alpha = 0; return self; }; self.showMessage = function (hitCount) { // Determine which message to show based on hit count var index = Math.min(Math.floor(hitCount / 5) - 1, self.messages.length - 1); if (index < 0) { return; } // Don't show anything for less than 5 hits var message = self.messages[index]; // Add combo count to the message var comboMessage = "Combo: " + hitCount + "\n" + message; // Play one of the three streak sounds randomly when milestone hit var streakSounds = ['streakSound', 'hitSound2', 'hitSound3']; var randomSoundIndex = Math.floor(Math.random() * streakSounds.length); LK.getSound(streakSounds[randomSoundIndex]).play(); // Stop any ongoing animations tween.stop(self.feedbackText, { alpha: true, scaleX: true, scaleY: true, rotation: true }); // Set the text self.feedbackText.setText(comboMessage); // Reset properties for animation self.feedbackText.alpha = 0; self.feedbackText.scale.set(0.5, 0.5); self.feedbackText.rotation = -0.1; // Animate in tween(self.feedbackText, { alpha: 1, scaleX: 1.2, scaleY: 1.2, rotation: 0.1 }, { duration: 300, easing: tween.elasticOut, onFinish: function onFinish() { // Hold for a moment LK.setTimeout(function () { // Animate out tween(self.feedbackText, { alpha: 0, scaleX: 1.5, scaleY: 1.5, rotation: 0 }, { duration: 400, easing: tween.easeOut }); }, 600); } }); }; return self; }); var Note = Container.expand(function () { var self = Container.call(this); self.type = 'normal'; // normal, perfect, good self.lane = 0; // 0-3 for different lanes self.speed = 10; // Base speed self.active = true; // Whether note is still active self.scored = false; // Whether note has been scored self.noteGraphics = null; self.perspective = 0; // 0 = distance, 1 = close to screen self.baseWidth = 180; // The base width for perspective scaling self.baseHeight = 30; // The base height for perspective scaling self.distanceZ = 0; // Z-coordinate for 3D perspective (0 = far, 1 = close) self.init = function (noteType, laneNum, noteSpeed) { self.type = noteType || 'normal'; self.lane = laneNum || 0; self.speed = 10; // Reduced speed for lower difficulty self.distanceZ = 0; // Start far away self.targetX = null; // Target x position for the lane self.xSpeed = null; // How fast to move horizontally per frame // Create note graphic based on type var assetId = 'note'; if (self.type === 'perfect') { assetId = 'perfectNote'; } else if (self.type === 'good') { assetId = 'goodNote'; } self.noteGraphics = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, alpha: 0.9 }); // Apply initial perspective (start small) self.updatePerspective(0); return self; }; // Update visual perspective based on distance value (0=far, 1=close) self.updatePerspective = function (distanceValue) { self.distanceZ = distanceValue; // Calculate scale based on distance (smaller when far, normal size when close) var scale = 0.3 + distanceValue * 0.7; // Scale from 30% to 100% // Apply scale to note self.noteGraphics.scale.x = scale; self.noteGraphics.scale.y = scale; // Adjust alpha to fade in as it gets closer self.noteGraphics.alpha = 0.5 + distanceValue * 0.5; }; self.update = function () { if (!self.active) { return; } // Move note down self.y += self.speed; // Store last speed for smooth changes self.lastSpeed = self.speed; // Move note horizontally towards target lane if xSpeed is defined if (self.xSpeed) { // Calculate remaining distance to target var remainingX = self.targetX - self.x; // Move towards target, but don't overshoot if (Math.abs(remainingX) < Math.abs(self.xSpeed)) { self.x = self.targetX; // Arrived at exact position } else { self.x += self.xSpeed; // Move towards target } } // Calculate perspective based on y position // Map y position from starting position to target (2500) to 0-1 perspective value var startPos = 2732 / 2 + noteStartY; var targetPos = 2500; var totalDistance = targetPos - startPos; var perspectiveValue = Math.max(0, Math.min(1, (self.y - startPos) / totalDistance)); self.updatePerspective(perspectiveValue); // Check if note passed through target zone and wasn't scored var hitY = targetZones[self.lane].y; var targetBottom = hitY + targetBoxSize.height / 2; // Track when note passes completely through the target zone if (self.y > targetBottom + 50 && !self.scored) { // Note passed through target zone without being hit self.miss(); } // Check if note is completely out of bounds and hasn't been scored else if (self.y > 2732 + 100 && !self.scored) { self.miss(); } }; self.hit = function (accuracy) { if (!self.active || self.scored) { return; } self.scored = true; self.active = false; // Create hit effect LK.effects.flashObject(self, 0xFFFFFF, 300); if (accuracy === 'perfect') { LK.getSound('perfectSound').play(); tween(self.noteGraphics, { alpha: 0, scaleX: 1.5, scaleY: 1.5 }, { duration: 300, onFinish: function onFinish() { self.destroy(); } }); } else if (accuracy === 'good') { LK.getSound('hitSound').play(); tween(self.noteGraphics, { alpha: 0, scaleX: 1.3, scaleY: 1.3 }, { duration: 300, onFinish: function onFinish() { self.destroy(); } }); } else { LK.getSound('hitSound').play(); tween(self.noteGraphics, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); } }; self.miss = function () { if (!self.active || self.scored) { return; } self.scored = true; self.active = false; // Increment missedNotes counter only if the note passed through target zone var hitY = targetZones[self.lane].y; var targetTop = hitY - targetBoxSize.height / 2; var targetBottom = hitY + targetBoxSize.height / 2; var wasInTargetZone = self.y >= targetTop && self.y <= targetBottom; if (wasInTargetZone || self.y > targetBottom) { // Only count as missed if it was in or passed the target zone missedNotes++; // Increment consecutive misses consecutiveMisses++; // Reset consecutive hits and feedback threshold consecutiveHits = 0; lastFeedbackThreshold = 0; } // Update accuracy display when a note is missed scoreDisplay.updateAccuracy(calculateAccuracy()); scoreDisplay.showFeedback('miss'); LK.getSound('missSound').play(); // Change to missed note appearance if (self.noteGraphics) { self.removeChild(self.noteGraphics); } self.noteGraphics = self.attachAsset('missedNote', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); tween(self.noteGraphics, { alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); var ScoreDisplay = Container.expand(function () { var self = Container.call(this); self.scoreText = null; self.comboText = null; self.accuracyText = null; self.feedbackText = null; self.init = function () { // Create score text self.scoreText = new Text2('Score: 0', { size: 80, fill: 0xFFFFFF }); self.scoreText.anchor.set(0.5, 0); self.addChild(self.scoreText); // Create high score text self.highScoreText = new Text2('High Score: ' + highScore, { size: 40, fill: 0xFFAA00 }); self.highScoreText.anchor.set(0.5, 0); self.highScoreText.y = 100; self.addChild(self.highScoreText); // Create accuracy text self.accuracyText = new Text2('Accuracy: 0%', { size: 40, fill: 0xDDDDDD }); self.accuracyText.anchor.set(0.5, 0); self.accuracyText.y = 160; self.addChild(self.accuracyText); // Create feedback text for "Perfect!", "Good!", "Miss!" self.feedbackText = new Text2('', { size: 90, fill: 0xFFFFFF }); self.feedbackText.anchor.set(0.5, 0.5); self.feedbackText.y = 300; self.addChild(self.feedbackText); return self; }; self.updateScore = function (score) { self.scoreText.setText('Score: ' + score); }; // Combo update method removed self.updateAccuracy = function (accuracy) { // Color code the accuracy based on performance var color = "#DDDDDD"; // Default color if (accuracy >= 90) { color = "#00FF00"; // Excellent (green) } else if (accuracy >= 75) { color = "#FFFF00"; // Good (yellow) } else if (accuracy >= 50) { color = "#FFA500"; // Fair (orange) } else { color = "#FF0000"; // Poor (red) } self.accuracyText.setText('Accuracy: ' + accuracy + '%', { fill: color }); }; self.updateHighScore = function (newHighScore) { self.highScoreText.setText('High Score: ' + newHighScore); }; self.showFeedback = function (type) { // Clear any existing animations tween.stop(self.feedbackText, { alpha: true, scaleX: true, scaleY: true }); var color = "#FFFFFF"; var text = ""; if (type === 'perfect') { text = "Perfect!"; color = "#FF4500"; } else if (type === 'good') { text = "Good!"; color = "#32CD32"; } else if (type === 'miss') { text = "Miss!"; color = "#888888"; } self.feedbackText.setText(text); // Set the text with the new color in the style self.feedbackText.setText(text, { fill: color }); self.feedbackText.alpha = 1; self.feedbackText.scale.set(1.3, 1.3); tween(self.feedbackText, { alpha: 0, scaleX: 1, scaleY: 1 }, { duration: 600, easing: tween.easeOut }); }; return self; }); var TargetZone = Container.expand(function () { var self = Container.call(this); self.lane = 0; self.zoneGraphics = null; self.active = false; self.pulseTimer = 0; self.perspectiveLines = null; self.init = function (laneNum) { self.lane = laneNum || 0; // Create target zone with 3D perspective appearance self.zoneGraphics = self.attachAsset('targetZone', { anchorX: 0.5, anchorY: 0.5, alpha: 0.4, width: targetBoxSize.width, // Use the target box size variable here height: targetBoxSize.height }); // Create perspective guide elements to enhance 3D effect self.createPerspectiveGuides(); return self; }; // Add perspective guides to enhance 3D effect self.createPerspectiveGuides = function () { self.perspectiveLines = new Container(); self.addChild(self.perspectiveLines); // Create 3D perspective indicator lines in the lane for (var i = 1; i <= 3; i++) { var depth = i * 0.25; // 0.25, 0.5, 0.75 positions var lineSize = targetBoxSize.width * (0.4 + depth * 0.6); // Gets larger as it gets closer var opacity = 0.15 + (1 - depth) * 0.1; // Fades as it gets closer var line = LK.getAsset('targetZone', { anchorX: 0.5, anchorY: 0.5, width: lineSize, height: 5, alpha: opacity }); // Position lines above the target zone line.y = -(400 * depth); self.perspectiveLines.addChild(line); } }; self.update = function () { // Subtle pulsing effect when not active if (!self.active) { self.pulseTimer += 0.05; self.zoneGraphics.alpha = 0.3 + Math.sin(self.pulseTimer) * 0.1; } }; self.activate = function () { if (self.active) { return; } self.active = true; tween(self.zoneGraphics, { alpha: 0.8, scaleX: 1.1, scaleY: 1.1 }, { duration: 100, onFinish: function onFinish() { self.deactivate(); } }); }; self.deactivate = function () { self.active = false; tween(self.zoneGraphics, { alpha: 0.4, scaleX: 1.0, scaleY: 1.0 }, { duration: 200 }); }; self.down = function (x, y, obj) { self.activate(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x111133 }); /**** * Game Code ****/ // Default size, will be overridden when initialized // Game configuration var laneCount = 4; var laneWidth = 2048 / laneCount; var targetBoxSize = { width: 500, height: 400 }; // Adjustable target zone dimensions var noteStartY = -800; // Adjustable starting position for notes, default is 200 above screen center var minNoteSpeed = 5; // Minimum speed of notes at game start var maxNoteSpeed = 40; // Maximum speed notes can reach var speedRampUpTime = 60 * 240; // Time in frames to reach max speed (30 seconds at 60fps) var gameSpeed = minNoteSpeed; // Starting game speed uses minNoteSpeed var currentLevel = 1; var difficultyMultiplier = 1.0; var speedIncreaseInterval = 300; // Frames between speed increases (5 seconds at 60fps) var speedIncreaseAmount = 0.05; // How much to increase speed each interval var speedIncreaseMax = maxNoteSpeed / minNoteSpeed; // Maximum speed multiplier derived from min/max speed var lastSpeedIncreaseTime = 0; // Track last time speed was increased var beatFrequency = 60; // Frames between beat patterns var beatVariance = 10; // Random variation in beat timing var nextBeatFrame = 20; // When to spawn the next beat var perspectiveEnabled = true; // Enable the perspective effect var perspectiveHeight = 2000; // Visual height of the 3D perspective lane // Note spawning configuration var minNotesPerSecond = 0.05; // Minimum notes per second at game start (at 60fps) var maxNotesPerSecond = 2.5; // Maximum notes per second the game can reach var noteRampUpTime = 60 * 480; // Time in frames to reach max notes per second (4 minutes at 60fps) var currentNotesPerSecond = minNotesPerSecond; // Current notes per second rate var noteSpawnCounter = 0; // Counter for note spawning logic // Game state var score = 0; var combo = 0; var maxCombo = 0; var totalNotes = 0; var hitNotes = 0; var missedNotes = 0; // Track notes that passed through the target zone var consecutiveMisses = 0; // Track consecutive misses var consecutiveHits = 0; // Track consecutive hits for feedback messages var hitStreakThreshold = 12; // Number of consecutive hits needed to show feedback var lastFeedbackThreshold = 0; // Last threshold where feedback was shown var maxConsecutiveMissesAllowed = 10; // Maximum consecutive misses allowed before game over - can be adjusted for difficulty var perfectHits = 0; var activeNotes = []; var targetZones = []; var gameStarted = false; var gameOver = false; var highScore = storage.highScore || 0; var hitFeedback; // Container for hit streak feedback messages // UI elements var scoreDisplay; var startText; var backgroundShape; // Add background layers // First add the back background layer var backgroundBackShape = game.addChild(LK.getAsset('backgroundBack', { anchorX: 0, anchorY: 0, width: 2048, height: 2732 })); // Background image removed // Make sure backgroundBackShape is at the bottom of the display stack game.setChildIndex(backgroundBackShape, 0); // Create perspective lanes and target zones function createPerspectiveLane(laneNum) { var container = new Container(); var laneX = laneWidth * (laneNum + 0.5); // Add the lane image var laneImage = LK.getAsset('noteLane', { anchorX: 0.5, anchorY: 0, alpha: 0.6, width: targetBoxSize.width * 0.9 // Make it slightly narrower than target box }); // Position the lane image laneImage.y = 500; // Start position laneImage.height = perspectiveHeight; // Set lane height // Add some subtle perspective effect with gradual fade var gradient = []; for (var i = 1; i <= 5; i++) { var distance = i / 5; // 0.2 to 1.0 var y = 500 + distance * 1800; // Map to y positions var lineOpacity = 0.05 + (1 - distance) * 0.1; // Fade with distance // Create lane markers as subtle horizontal lines var marker = LK.getAsset('targetZone', { anchorX: 0.5, anchorY: 0.5, width: targetBoxSize.width * (0.4 + distance * 0.6), height: 2, alpha: lineOpacity }); marker.y = y; gradient.push(marker); container.addChild(marker); } // Add lane image last so it appears on top of markers container.addChild(laneImage); container.x = laneX; return container; } // Create perspective lanes - use a single lane image background instead of multiple lanes if (perspectiveEnabled) { // Add a single noteLane image spanning the entire width of the game var fullLaneImage = LK.getAsset('noteLane', { anchorX: 0.5, anchorY: 0, alpha: 0.6, width: 2048, // Full game width height: perspectiveHeight }); // Position the lane image fullLaneImage.x = 2048 / 2; // Center horizontally fullLaneImage.y = 500; // Start position game.addChild(fullLaneImage); // Add subtle lane markers to help with visual guidance for (var i = 1; i <= laneCount - 1; i++) { // Create divider lines between lanes var divider = LK.getAsset('targetZone', { anchorX: 0.5, anchorY: 0, width: 2, height: perspectiveHeight, alpha: 0.3 }); divider.x = laneWidth * i; // Position at lane dividers divider.y = 500; game.addChild(divider); } } // Create target zones for (var i = 0; i < laneCount; i++) { var zone = new TargetZone().init(i); zone.x = laneWidth * (i + 0.5); zone.y = 2500; // Bottom of screen targetZones.push(zone); game.addChild(zone); } // Create and position score display scoreDisplay = new ScoreDisplay().init(); scoreDisplay.x = 2048 / 2; scoreDisplay.y = 150; game.addChild(scoreDisplay); // Create hit streak feedback display hitFeedback = new HitFeedback().init(); hitFeedback.x = 2048 / 2; hitFeedback.y = 2732 / 3; // Positioned in the top third of the screen game.addChild(hitFeedback); // Create start text startText = new Text2('Tap to Start', { size: 120, fill: 0xFFFFFF }); startText.anchor.set(0.5, 0.5); startText.x = 2048 / 2; startText.y = 2732 / 2; game.addChild(startText); // Calculate accuracy as a percentage function calculateAccuracy() { // Base case: if no notes have reached the target zone yet if (hitNotes + missedNotes === 0) { return 100; } // Calculate the actual percentage of notes hit versus total notes that passed through the target zone return Math.floor(hitNotes / (hitNotes + missedNotes) * 100); } // Spawn a new note function spawnNote() { var laneNum = Math.floor(Math.random() * laneCount); var noteType = 'normal'; // 20% chance for a "perfect" note, 30% chance for a "good" note var rand = Math.random(); if (rand < 0.2) { noteType = 'perfect'; } else if (rand < 0.5) { noteType = 'good'; } // Use constant speed for all notes, regardless of game progress var noteSpeed = 10; // Reduced constant speed value for lower difficulty var note = new Note().init(noteType, laneNum, noteSpeed); note.x = 2048 / 2; // Start at the center of the screen on x axis note.y = 2732 / 2 + noteStartY; // Start above center of the screen based on noteStartY variable note.targetX = laneWidth * (laneNum + 0.5); // Store target x position // Calculate distance to travel in x axis var xDistance = note.targetX - note.x; // Calculate time to reach target zone based on y distance and speed var timeToTarget = (2500 - (2732 / 2 + noteStartY)) / noteSpeed; // Calculate how much to move per frame to reach target at the same time as reaching target zone note.xSpeed = xDistance / timeToTarget; // No immediate tween - will move gradually in update function // Apply perspective effect if (perspectiveEnabled) { // Start with small scale to create distance effect note.updatePerspective(0); // 0 = far away // Apply entrance animation for perspective effect note.noteGraphics.alpha = 0.4; // Start more transparent // Apply a grow animation to the note as it appears tween(note.noteGraphics, { alpha: 0.9 }, { duration: 300, easing: tween.easeOut }); } activeNotes.push(note); game.addChild(note); totalNotes++; return note; } // Generate beat pattern function generateBeatPattern() { var patternLength = Math.min(4, 1 + Math.floor(currentLevel / 2)); for (var i = 0; i < patternLength; i++) { // Stagger notes based on pattern position LK.setTimeout(function () { if (!gameOver && gameStarted) { spawnNote(); } }, i * (200 / difficultyMultiplier)); } } // Check if a note is in the hit zone function checkNoteHit(laneNum) { var hitZone = targetZones[laneNum]; var hitY = hitZone.y; var hitFound = false; // Find notes in this lane for (var i = 0; i < activeNotes.length; i++) { var note = activeNotes[i]; if (note.lane === laneNum && note.active && !note.scored) { // Check if note is inside target zone bounds var targetTop = hitY - targetBoxSize.height / 2; var targetBottom = hitY + targetBoxSize.height / 2; var isInsideTargetZone = note.y >= targetTop && note.y <= targetBottom; if (isInsideTargetZone) { // Calculate distance from perfect hit position as a percentage of target box height var distance = Math.abs(note.y - hitY); var distancePercent = distance / (targetBoxSize.height / 2); // Different hit zones based on percentage of target box height if (distancePercent < 0.3) { // Perfect hit - center 30% of target zone score += 100; hitNotes++; perfectHits++; note.hit('perfect'); scoreDisplay.showFeedback('perfect'); hitFound = true; // Reset consecutive misses on successful hit consecutiveMisses = 0; // Increment consecutive hits consecutiveHits++; // Show feedback message at each hit streak milestone if (Math.floor(consecutiveHits / hitStreakThreshold) > lastFeedbackThreshold) { lastFeedbackThreshold = Math.floor(consecutiveHits / hitStreakThreshold); hitFeedback.showMessage(consecutiveHits); } break; } else if (distancePercent < 0.7) { // Good hit - middle 40% of target zone score += 50; hitNotes++; note.hit('good'); scoreDisplay.showFeedback('good'); hitFound = true; // Reset consecutive misses on successful hit consecutiveMisses = 0; // Increment consecutive hits consecutiveHits++; // Show feedback message at each hit streak milestone if (Math.floor(consecutiveHits / hitStreakThreshold) > lastFeedbackThreshold) { lastFeedbackThreshold = Math.floor(consecutiveHits / hitStreakThreshold); hitFeedback.showMessage(consecutiveHits); } break; } else { // Within bounds but not centered - early/late hit at edges of target zone score += 10; hitNotes++; note.hit('early'); scoreDisplay.showFeedback('good'); hitFound = true; // Reset consecutive misses on successful hit consecutiveMisses = 0; // Increment consecutive hits consecutiveHits++; // Show feedback message at each hit streak milestone if (Math.floor(consecutiveHits / hitStreakThreshold) > lastFeedbackThreshold) { lastFeedbackThreshold = Math.floor(consecutiveHits / hitStreakThreshold); hitFeedback.showMessage(consecutiveHits); } break; } } } } // If we didn't hit any notes, it's a miss if (!hitFound) { scoreDisplay.showFeedback('miss'); // Note: We don't increment consecutiveMisses here because this is a tap miss, // not a note pass-through miss which is handled in Note.miss() } // Update score display var roundedScore = Math.floor(score); scoreDisplay.updateScore(roundedScore); maxCombo = Math.max(maxCombo, combo); // Check and update high score if (roundedScore > highScore) { highScore = roundedScore; storage.highScore = highScore; scoreDisplay.updateHighScore(highScore); // Flash high score when new record is set if (roundedScore > 0) { scoreDisplay.showFeedback("New High Score!"); LK.effects.flashObject(scoreDisplay.highScoreText, 0xFFFF00, 500); } } // Update accuracy after every note hit var accuracy = calculateAccuracy(); scoreDisplay.updateAccuracy(accuracy); return hitFound; } // Handle tap on a lane function handleLaneTap(laneNum) { if (!gameStarted || gameOver) { return; } targetZones[laneNum].activate(); return checkNoteHit(laneNum); } // Start the game function startGame() { if (gameStarted) { return; } gameStarted = true; game.removeChild(startText); // Reset game state score = 0; combo = 0; maxCombo = 0; totalNotes = 0; hitNotes = 0; missedNotes = 0; // Reset missed notes counter consecutiveMisses = 0; // Reset consecutive misses counter consecutiveHits = 0; // Reset consecutive hits counter lastFeedbackThreshold = 0; // Reset feedback threshold perfectHits = 0; currentLevel = 1; difficultyMultiplier = 1.0; gameSpeed = minNoteSpeed; // Reset base speed to minimum lastSpeedIncreaseTime = 0; // Reset note spawning variables currentNotesPerSecond = minNotesPerSecond; noteSpawnCounter = 0; gameOver = false; // Update UI scoreDisplay.updateScore(score); // Initialize accuracy with default 100% at game start scoreDisplay.updateAccuracy(100); scoreDisplay.updateHighScore(highScore); // Initial speed up message scoreDisplay.showFeedback("Get Ready!"); // Start music LK.playMusic('gameMusic', { fade: { start: 0, end: 1, duration: 1000 } }); // Set the first beat to spawn soon nextBeatFrame = 60; } // Check if game should end (based on time or score) function checkGameOver() { // For demo purposes, end game after reaching level 10 if (currentLevel >= 10 && score >= 10000) { endGame(true); // Win } // Player loses if accuracy drops too low if (totalNotes > 20 && calculateAccuracy() < 40) { endGame(false); // Lose due to low accuracy } // Player loses if they miss too many notes in a row if (consecutiveMisses >= maxConsecutiveMissesAllowed) { endGame(false); // Lose due to too many consecutive misses } } // End the game function endGame(win) { gameOver = true; // Fade out music LK.playMusic('gameMusic', { fade: { start: 1, end: 0, duration: 800 } }); // Save high score to storage if (score > highScore) { highScore = Math.floor(score); storage.highScore = highScore; } // Show game over or win screen based on performance if (win) { LK.showYouWin(); } else { LK.showGameOver(); } } // Handle lane tap detection - we'll divide the screen into lanes function getLaneFromPosition(x) { return Math.floor(x / laneWidth); } // Handle touch events game.down = function (x, y, obj) { if (!gameStarted) { startGame(); } else { var lane = getLaneFromPosition(x); if (lane >= 0 && lane < laneCount) { handleLaneTap(lane); } } }; game.update = function () { if (!gameStarted) { return; } // Update all active notes for (var i = activeNotes.length - 1; i >= 0; i--) { var note = activeNotes[i]; // Remove notes that have been scored and faded out if (!note.active && note.scored) { activeNotes.splice(i, 1); } } // Update target zones for (var j = 0; j < targetZones.length; j++) { targetZones[j].update(); } // Calculate note spawning rate based on progress if (gameStarted && !gameOver) { // Calculate progress for notes per second (separate from speed progress) var progressToMaxNoteRate = Math.min(1.0, LK.ticks / noteRampUpTime); // Calculate new notes per second rate based on progress between min and max currentNotesPerSecond = minNotesPerSecond + (maxNotesPerSecond - minNotesPerSecond) * progressToMaxNoteRate; // Calculate frames between notes based on notes per second var framesBetweenNotes = Math.floor(60 / currentNotesPerSecond); // Spawn notes based on the current rate noteSpawnCounter++; if (noteSpawnCounter >= framesBetweenNotes) { spawnNote(); noteSpawnCounter = 0; // Visual feedback when note rate increases significantly if (LK.ticks % 600 === 0 && progressToMaxNoteRate > 0.2) { scoreDisplay.showFeedback("More Notes!"); } } } // Generate new beat patterns at intervals - this is now supplementary to the base note spawning if (gameStarted && !gameOver) { if (LK.ticks >= nextBeatFrame) { generateBeatPattern(); // Calculate next beat timing with some variance var interval = beatFrequency - currentLevel * 5; interval = Math.max(interval, 30); // Don't go too fast var variance = Math.floor(Math.random() * beatVariance) - beatVariance / 2; nextBeatFrame = LK.ticks + interval + variance; // Increase difficulty over time if (LK.ticks % 600 === 0) { // Every 10 seconds currentLevel++; difficultyMultiplier = 1.0 + currentLevel * 0.2; // Increased from 0.1 to 0.2 for faster speed growth // No longer dynamically increasing speed of existing notes for (var i = 0; i < activeNotes.length; i++) { var note = activeNotes[i]; // We no longer change speed of active notes } } } } // No longer applying speed increases to notes during gameplay if (gameStarted && !gameOver) { // We still calculate this for other game mechanics, but don't apply to note speeds var progressToMaxSpeed = Math.min(1.0, LK.ticks / speedRampUpTime); var newGameSpeed = minNoteSpeed + (maxNoteSpeed - minNoteSpeed) * progressToMaxSpeed; // Only update game speed variable but don't change note speeds if (Math.abs(gameSpeed - newGameSpeed) > 0.1) { // Update game speed for new notes gameSpeed = newGameSpeed; // Visual feedback when speed increases significantly if (LK.ticks % 300 === 0) { scoreDisplay.showFeedback("Speed Up!"); // Flash effect on target zones to indicate increased speed for (var i = 0; i < targetZones.length; i++) { LK.effects.flashObject(targetZones[i], 0xFF4500, 300); } } } } // Check for game over conditions checkGameOver(); // Check if missedNotes exceeds limit for automatic game over if (missedNotes >= 10) { endGame(false); // Lose due to too many missed notes } };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
highScore: 0
});
/****
* Classes
****/
var HitFeedback = Container.expand(function () {
var self = Container.call(this);
self.feedbackText = null;
self.messages = ['WOW!', 'AMAZING!!', 'INCREDIBLE!', 'SPECTACULAR!', 'SUPER!!', 'UNSTOPPABLE!!!'];
self.init = function () {
// Create feedback text with golden color
self.feedbackText = new Text2('', {
size: 480,
fill: 0xFFD700,
font: "'GillSans-Bold',Impact,'Arial Black',Tahoma"
});
self.feedbackText.anchor.set(0.5, 0.5);
self.addChild(self.feedbackText);
// Initially hide the text
self.feedbackText.alpha = 0;
return self;
};
self.showMessage = function (hitCount) {
// Determine which message to show based on hit count
var index = Math.min(Math.floor(hitCount / 5) - 1, self.messages.length - 1);
if (index < 0) {
return;
} // Don't show anything for less than 5 hits
var message = self.messages[index];
// Add combo count to the message
var comboMessage = "Combo: " + hitCount + "\n" + message;
// Play one of the three streak sounds randomly when milestone hit
var streakSounds = ['streakSound', 'hitSound2', 'hitSound3'];
var randomSoundIndex = Math.floor(Math.random() * streakSounds.length);
LK.getSound(streakSounds[randomSoundIndex]).play();
// Stop any ongoing animations
tween.stop(self.feedbackText, {
alpha: true,
scaleX: true,
scaleY: true,
rotation: true
});
// Set the text
self.feedbackText.setText(comboMessage);
// Reset properties for animation
self.feedbackText.alpha = 0;
self.feedbackText.scale.set(0.5, 0.5);
self.feedbackText.rotation = -0.1;
// Animate in
tween(self.feedbackText, {
alpha: 1,
scaleX: 1.2,
scaleY: 1.2,
rotation: 0.1
}, {
duration: 300,
easing: tween.elasticOut,
onFinish: function onFinish() {
// Hold for a moment
LK.setTimeout(function () {
// Animate out
tween(self.feedbackText, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5,
rotation: 0
}, {
duration: 400,
easing: tween.easeOut
});
}, 600);
}
});
};
return self;
});
var Note = Container.expand(function () {
var self = Container.call(this);
self.type = 'normal'; // normal, perfect, good
self.lane = 0; // 0-3 for different lanes
self.speed = 10; // Base speed
self.active = true; // Whether note is still active
self.scored = false; // Whether note has been scored
self.noteGraphics = null;
self.perspective = 0; // 0 = distance, 1 = close to screen
self.baseWidth = 180; // The base width for perspective scaling
self.baseHeight = 30; // The base height for perspective scaling
self.distanceZ = 0; // Z-coordinate for 3D perspective (0 = far, 1 = close)
self.init = function (noteType, laneNum, noteSpeed) {
self.type = noteType || 'normal';
self.lane = laneNum || 0;
self.speed = 10; // Reduced speed for lower difficulty
self.distanceZ = 0; // Start far away
self.targetX = null; // Target x position for the lane
self.xSpeed = null; // How fast to move horizontally per frame
// Create note graphic based on type
var assetId = 'note';
if (self.type === 'perfect') {
assetId = 'perfectNote';
} else if (self.type === 'good') {
assetId = 'goodNote';
}
self.noteGraphics = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.9
});
// Apply initial perspective (start small)
self.updatePerspective(0);
return self;
};
// Update visual perspective based on distance value (0=far, 1=close)
self.updatePerspective = function (distanceValue) {
self.distanceZ = distanceValue;
// Calculate scale based on distance (smaller when far, normal size when close)
var scale = 0.3 + distanceValue * 0.7; // Scale from 30% to 100%
// Apply scale to note
self.noteGraphics.scale.x = scale;
self.noteGraphics.scale.y = scale;
// Adjust alpha to fade in as it gets closer
self.noteGraphics.alpha = 0.5 + distanceValue * 0.5;
};
self.update = function () {
if (!self.active) {
return;
}
// Move note down
self.y += self.speed;
// Store last speed for smooth changes
self.lastSpeed = self.speed;
// Move note horizontally towards target lane if xSpeed is defined
if (self.xSpeed) {
// Calculate remaining distance to target
var remainingX = self.targetX - self.x;
// Move towards target, but don't overshoot
if (Math.abs(remainingX) < Math.abs(self.xSpeed)) {
self.x = self.targetX; // Arrived at exact position
} else {
self.x += self.xSpeed; // Move towards target
}
}
// Calculate perspective based on y position
// Map y position from starting position to target (2500) to 0-1 perspective value
var startPos = 2732 / 2 + noteStartY;
var targetPos = 2500;
var totalDistance = targetPos - startPos;
var perspectiveValue = Math.max(0, Math.min(1, (self.y - startPos) / totalDistance));
self.updatePerspective(perspectiveValue);
// Check if note passed through target zone and wasn't scored
var hitY = targetZones[self.lane].y;
var targetBottom = hitY + targetBoxSize.height / 2;
// Track when note passes completely through the target zone
if (self.y > targetBottom + 50 && !self.scored) {
// Note passed through target zone without being hit
self.miss();
}
// Check if note is completely out of bounds and hasn't been scored
else if (self.y > 2732 + 100 && !self.scored) {
self.miss();
}
};
self.hit = function (accuracy) {
if (!self.active || self.scored) {
return;
}
self.scored = true;
self.active = false;
// Create hit effect
LK.effects.flashObject(self, 0xFFFFFF, 300);
if (accuracy === 'perfect') {
LK.getSound('perfectSound').play();
tween(self.noteGraphics, {
alpha: 0,
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
onFinish: function onFinish() {
self.destroy();
}
});
} else if (accuracy === 'good') {
LK.getSound('hitSound').play();
tween(self.noteGraphics, {
alpha: 0,
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 300,
onFinish: function onFinish() {
self.destroy();
}
});
} else {
LK.getSound('hitSound').play();
tween(self.noteGraphics, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
}
};
self.miss = function () {
if (!self.active || self.scored) {
return;
}
self.scored = true;
self.active = false;
// Increment missedNotes counter only if the note passed through target zone
var hitY = targetZones[self.lane].y;
var targetTop = hitY - targetBoxSize.height / 2;
var targetBottom = hitY + targetBoxSize.height / 2;
var wasInTargetZone = self.y >= targetTop && self.y <= targetBottom;
if (wasInTargetZone || self.y > targetBottom) {
// Only count as missed if it was in or passed the target zone
missedNotes++;
// Increment consecutive misses
consecutiveMisses++;
// Reset consecutive hits and feedback threshold
consecutiveHits = 0;
lastFeedbackThreshold = 0;
}
// Update accuracy display when a note is missed
scoreDisplay.updateAccuracy(calculateAccuracy());
scoreDisplay.showFeedback('miss');
LK.getSound('missSound').play();
// Change to missed note appearance
if (self.noteGraphics) {
self.removeChild(self.noteGraphics);
}
self.noteGraphics = self.attachAsset('missedNote', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5
});
tween(self.noteGraphics, {
alpha: 0
}, {
duration: 200,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
var ScoreDisplay = Container.expand(function () {
var self = Container.call(this);
self.scoreText = null;
self.comboText = null;
self.accuracyText = null;
self.feedbackText = null;
self.init = function () {
// Create score text
self.scoreText = new Text2('Score: 0', {
size: 80,
fill: 0xFFFFFF
});
self.scoreText.anchor.set(0.5, 0);
self.addChild(self.scoreText);
// Create high score text
self.highScoreText = new Text2('High Score: ' + highScore, {
size: 40,
fill: 0xFFAA00
});
self.highScoreText.anchor.set(0.5, 0);
self.highScoreText.y = 100;
self.addChild(self.highScoreText);
// Create accuracy text
self.accuracyText = new Text2('Accuracy: 0%', {
size: 40,
fill: 0xDDDDDD
});
self.accuracyText.anchor.set(0.5, 0);
self.accuracyText.y = 160;
self.addChild(self.accuracyText);
// Create feedback text for "Perfect!", "Good!", "Miss!"
self.feedbackText = new Text2('', {
size: 90,
fill: 0xFFFFFF
});
self.feedbackText.anchor.set(0.5, 0.5);
self.feedbackText.y = 300;
self.addChild(self.feedbackText);
return self;
};
self.updateScore = function (score) {
self.scoreText.setText('Score: ' + score);
};
// Combo update method removed
self.updateAccuracy = function (accuracy) {
// Color code the accuracy based on performance
var color = "#DDDDDD"; // Default color
if (accuracy >= 90) {
color = "#00FF00"; // Excellent (green)
} else if (accuracy >= 75) {
color = "#FFFF00"; // Good (yellow)
} else if (accuracy >= 50) {
color = "#FFA500"; // Fair (orange)
} else {
color = "#FF0000"; // Poor (red)
}
self.accuracyText.setText('Accuracy: ' + accuracy + '%', {
fill: color
});
};
self.updateHighScore = function (newHighScore) {
self.highScoreText.setText('High Score: ' + newHighScore);
};
self.showFeedback = function (type) {
// Clear any existing animations
tween.stop(self.feedbackText, {
alpha: true,
scaleX: true,
scaleY: true
});
var color = "#FFFFFF";
var text = "";
if (type === 'perfect') {
text = "Perfect!";
color = "#FF4500";
} else if (type === 'good') {
text = "Good!";
color = "#32CD32";
} else if (type === 'miss') {
text = "Miss!";
color = "#888888";
}
self.feedbackText.setText(text);
// Set the text with the new color in the style
self.feedbackText.setText(text, {
fill: color
});
self.feedbackText.alpha = 1;
self.feedbackText.scale.set(1.3, 1.3);
tween(self.feedbackText, {
alpha: 0,
scaleX: 1,
scaleY: 1
}, {
duration: 600,
easing: tween.easeOut
});
};
return self;
});
var TargetZone = Container.expand(function () {
var self = Container.call(this);
self.lane = 0;
self.zoneGraphics = null;
self.active = false;
self.pulseTimer = 0;
self.perspectiveLines = null;
self.init = function (laneNum) {
self.lane = laneNum || 0;
// Create target zone with 3D perspective appearance
self.zoneGraphics = self.attachAsset('targetZone', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.4,
width: targetBoxSize.width,
// Use the target box size variable here
height: targetBoxSize.height
});
// Create perspective guide elements to enhance 3D effect
self.createPerspectiveGuides();
return self;
};
// Add perspective guides to enhance 3D effect
self.createPerspectiveGuides = function () {
self.perspectiveLines = new Container();
self.addChild(self.perspectiveLines);
// Create 3D perspective indicator lines in the lane
for (var i = 1; i <= 3; i++) {
var depth = i * 0.25; // 0.25, 0.5, 0.75 positions
var lineSize = targetBoxSize.width * (0.4 + depth * 0.6); // Gets larger as it gets closer
var opacity = 0.15 + (1 - depth) * 0.1; // Fades as it gets closer
var line = LK.getAsset('targetZone', {
anchorX: 0.5,
anchorY: 0.5,
width: lineSize,
height: 5,
alpha: opacity
});
// Position lines above the target zone
line.y = -(400 * depth);
self.perspectiveLines.addChild(line);
}
};
self.update = function () {
// Subtle pulsing effect when not active
if (!self.active) {
self.pulseTimer += 0.05;
self.zoneGraphics.alpha = 0.3 + Math.sin(self.pulseTimer) * 0.1;
}
};
self.activate = function () {
if (self.active) {
return;
}
self.active = true;
tween(self.zoneGraphics, {
alpha: 0.8,
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 100,
onFinish: function onFinish() {
self.deactivate();
}
});
};
self.deactivate = function () {
self.active = false;
tween(self.zoneGraphics, {
alpha: 0.4,
scaleX: 1.0,
scaleY: 1.0
}, {
duration: 200
});
};
self.down = function (x, y, obj) {
self.activate();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111133
});
/****
* Game Code
****/
// Default size, will be overridden when initialized
// Game configuration
var laneCount = 4;
var laneWidth = 2048 / laneCount;
var targetBoxSize = {
width: 500,
height: 400
}; // Adjustable target zone dimensions
var noteStartY = -800; // Adjustable starting position for notes, default is 200 above screen center
var minNoteSpeed = 5; // Minimum speed of notes at game start
var maxNoteSpeed = 40; // Maximum speed notes can reach
var speedRampUpTime = 60 * 240; // Time in frames to reach max speed (30 seconds at 60fps)
var gameSpeed = minNoteSpeed; // Starting game speed uses minNoteSpeed
var currentLevel = 1;
var difficultyMultiplier = 1.0;
var speedIncreaseInterval = 300; // Frames between speed increases (5 seconds at 60fps)
var speedIncreaseAmount = 0.05; // How much to increase speed each interval
var speedIncreaseMax = maxNoteSpeed / minNoteSpeed; // Maximum speed multiplier derived from min/max speed
var lastSpeedIncreaseTime = 0; // Track last time speed was increased
var beatFrequency = 60; // Frames between beat patterns
var beatVariance = 10; // Random variation in beat timing
var nextBeatFrame = 20; // When to spawn the next beat
var perspectiveEnabled = true; // Enable the perspective effect
var perspectiveHeight = 2000; // Visual height of the 3D perspective lane
// Note spawning configuration
var minNotesPerSecond = 0.05; // Minimum notes per second at game start (at 60fps)
var maxNotesPerSecond = 2.5; // Maximum notes per second the game can reach
var noteRampUpTime = 60 * 480; // Time in frames to reach max notes per second (4 minutes at 60fps)
var currentNotesPerSecond = minNotesPerSecond; // Current notes per second rate
var noteSpawnCounter = 0; // Counter for note spawning logic
// Game state
var score = 0;
var combo = 0;
var maxCombo = 0;
var totalNotes = 0;
var hitNotes = 0;
var missedNotes = 0; // Track notes that passed through the target zone
var consecutiveMisses = 0; // Track consecutive misses
var consecutiveHits = 0; // Track consecutive hits for feedback messages
var hitStreakThreshold = 12; // Number of consecutive hits needed to show feedback
var lastFeedbackThreshold = 0; // Last threshold where feedback was shown
var maxConsecutiveMissesAllowed = 10; // Maximum consecutive misses allowed before game over - can be adjusted for difficulty
var perfectHits = 0;
var activeNotes = [];
var targetZones = [];
var gameStarted = false;
var gameOver = false;
var highScore = storage.highScore || 0;
var hitFeedback; // Container for hit streak feedback messages
// UI elements
var scoreDisplay;
var startText;
var backgroundShape;
// Add background layers
// First add the back background layer
var backgroundBackShape = game.addChild(LK.getAsset('backgroundBack', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732
}));
// Background image removed
// Make sure backgroundBackShape is at the bottom of the display stack
game.setChildIndex(backgroundBackShape, 0);
// Create perspective lanes and target zones
function createPerspectiveLane(laneNum) {
var container = new Container();
var laneX = laneWidth * (laneNum + 0.5);
// Add the lane image
var laneImage = LK.getAsset('noteLane', {
anchorX: 0.5,
anchorY: 0,
alpha: 0.6,
width: targetBoxSize.width * 0.9 // Make it slightly narrower than target box
});
// Position the lane image
laneImage.y = 500; // Start position
laneImage.height = perspectiveHeight; // Set lane height
// Add some subtle perspective effect with gradual fade
var gradient = [];
for (var i = 1; i <= 5; i++) {
var distance = i / 5; // 0.2 to 1.0
var y = 500 + distance * 1800; // Map to y positions
var lineOpacity = 0.05 + (1 - distance) * 0.1; // Fade with distance
// Create lane markers as subtle horizontal lines
var marker = LK.getAsset('targetZone', {
anchorX: 0.5,
anchorY: 0.5,
width: targetBoxSize.width * (0.4 + distance * 0.6),
height: 2,
alpha: lineOpacity
});
marker.y = y;
gradient.push(marker);
container.addChild(marker);
}
// Add lane image last so it appears on top of markers
container.addChild(laneImage);
container.x = laneX;
return container;
}
// Create perspective lanes - use a single lane image background instead of multiple lanes
if (perspectiveEnabled) {
// Add a single noteLane image spanning the entire width of the game
var fullLaneImage = LK.getAsset('noteLane', {
anchorX: 0.5,
anchorY: 0,
alpha: 0.6,
width: 2048,
// Full game width
height: perspectiveHeight
});
// Position the lane image
fullLaneImage.x = 2048 / 2; // Center horizontally
fullLaneImage.y = 500; // Start position
game.addChild(fullLaneImage);
// Add subtle lane markers to help with visual guidance
for (var i = 1; i <= laneCount - 1; i++) {
// Create divider lines between lanes
var divider = LK.getAsset('targetZone', {
anchorX: 0.5,
anchorY: 0,
width: 2,
height: perspectiveHeight,
alpha: 0.3
});
divider.x = laneWidth * i; // Position at lane dividers
divider.y = 500;
game.addChild(divider);
}
}
// Create target zones
for (var i = 0; i < laneCount; i++) {
var zone = new TargetZone().init(i);
zone.x = laneWidth * (i + 0.5);
zone.y = 2500; // Bottom of screen
targetZones.push(zone);
game.addChild(zone);
}
// Create and position score display
scoreDisplay = new ScoreDisplay().init();
scoreDisplay.x = 2048 / 2;
scoreDisplay.y = 150;
game.addChild(scoreDisplay);
// Create hit streak feedback display
hitFeedback = new HitFeedback().init();
hitFeedback.x = 2048 / 2;
hitFeedback.y = 2732 / 3; // Positioned in the top third of the screen
game.addChild(hitFeedback);
// Create start text
startText = new Text2('Tap to Start', {
size: 120,
fill: 0xFFFFFF
});
startText.anchor.set(0.5, 0.5);
startText.x = 2048 / 2;
startText.y = 2732 / 2;
game.addChild(startText);
// Calculate accuracy as a percentage
function calculateAccuracy() {
// Base case: if no notes have reached the target zone yet
if (hitNotes + missedNotes === 0) {
return 100;
}
// Calculate the actual percentage of notes hit versus total notes that passed through the target zone
return Math.floor(hitNotes / (hitNotes + missedNotes) * 100);
}
// Spawn a new note
function spawnNote() {
var laneNum = Math.floor(Math.random() * laneCount);
var noteType = 'normal';
// 20% chance for a "perfect" note, 30% chance for a "good" note
var rand = Math.random();
if (rand < 0.2) {
noteType = 'perfect';
} else if (rand < 0.5) {
noteType = 'good';
}
// Use constant speed for all notes, regardless of game progress
var noteSpeed = 10; // Reduced constant speed value for lower difficulty
var note = new Note().init(noteType, laneNum, noteSpeed);
note.x = 2048 / 2; // Start at the center of the screen on x axis
note.y = 2732 / 2 + noteStartY; // Start above center of the screen based on noteStartY variable
note.targetX = laneWidth * (laneNum + 0.5); // Store target x position
// Calculate distance to travel in x axis
var xDistance = note.targetX - note.x;
// Calculate time to reach target zone based on y distance and speed
var timeToTarget = (2500 - (2732 / 2 + noteStartY)) / noteSpeed;
// Calculate how much to move per frame to reach target at the same time as reaching target zone
note.xSpeed = xDistance / timeToTarget;
// No immediate tween - will move gradually in update function
// Apply perspective effect
if (perspectiveEnabled) {
// Start with small scale to create distance effect
note.updatePerspective(0); // 0 = far away
// Apply entrance animation for perspective effect
note.noteGraphics.alpha = 0.4; // Start more transparent
// Apply a grow animation to the note as it appears
tween(note.noteGraphics, {
alpha: 0.9
}, {
duration: 300,
easing: tween.easeOut
});
}
activeNotes.push(note);
game.addChild(note);
totalNotes++;
return note;
}
// Generate beat pattern
function generateBeatPattern() {
var patternLength = Math.min(4, 1 + Math.floor(currentLevel / 2));
for (var i = 0; i < patternLength; i++) {
// Stagger notes based on pattern position
LK.setTimeout(function () {
if (!gameOver && gameStarted) {
spawnNote();
}
}, i * (200 / difficultyMultiplier));
}
}
// Check if a note is in the hit zone
function checkNoteHit(laneNum) {
var hitZone = targetZones[laneNum];
var hitY = hitZone.y;
var hitFound = false;
// Find notes in this lane
for (var i = 0; i < activeNotes.length; i++) {
var note = activeNotes[i];
if (note.lane === laneNum && note.active && !note.scored) {
// Check if note is inside target zone bounds
var targetTop = hitY - targetBoxSize.height / 2;
var targetBottom = hitY + targetBoxSize.height / 2;
var isInsideTargetZone = note.y >= targetTop && note.y <= targetBottom;
if (isInsideTargetZone) {
// Calculate distance from perfect hit position as a percentage of target box height
var distance = Math.abs(note.y - hitY);
var distancePercent = distance / (targetBoxSize.height / 2);
// Different hit zones based on percentage of target box height
if (distancePercent < 0.3) {
// Perfect hit - center 30% of target zone
score += 100;
hitNotes++;
perfectHits++;
note.hit('perfect');
scoreDisplay.showFeedback('perfect');
hitFound = true;
// Reset consecutive misses on successful hit
consecutiveMisses = 0;
// Increment consecutive hits
consecutiveHits++;
// Show feedback message at each hit streak milestone
if (Math.floor(consecutiveHits / hitStreakThreshold) > lastFeedbackThreshold) {
lastFeedbackThreshold = Math.floor(consecutiveHits / hitStreakThreshold);
hitFeedback.showMessage(consecutiveHits);
}
break;
} else if (distancePercent < 0.7) {
// Good hit - middle 40% of target zone
score += 50;
hitNotes++;
note.hit('good');
scoreDisplay.showFeedback('good');
hitFound = true;
// Reset consecutive misses on successful hit
consecutiveMisses = 0;
// Increment consecutive hits
consecutiveHits++;
// Show feedback message at each hit streak milestone
if (Math.floor(consecutiveHits / hitStreakThreshold) > lastFeedbackThreshold) {
lastFeedbackThreshold = Math.floor(consecutiveHits / hitStreakThreshold);
hitFeedback.showMessage(consecutiveHits);
}
break;
} else {
// Within bounds but not centered - early/late hit at edges of target zone
score += 10;
hitNotes++;
note.hit('early');
scoreDisplay.showFeedback('good');
hitFound = true;
// Reset consecutive misses on successful hit
consecutiveMisses = 0;
// Increment consecutive hits
consecutiveHits++;
// Show feedback message at each hit streak milestone
if (Math.floor(consecutiveHits / hitStreakThreshold) > lastFeedbackThreshold) {
lastFeedbackThreshold = Math.floor(consecutiveHits / hitStreakThreshold);
hitFeedback.showMessage(consecutiveHits);
}
break;
}
}
}
}
// If we didn't hit any notes, it's a miss
if (!hitFound) {
scoreDisplay.showFeedback('miss');
// Note: We don't increment consecutiveMisses here because this is a tap miss,
// not a note pass-through miss which is handled in Note.miss()
}
// Update score display
var roundedScore = Math.floor(score);
scoreDisplay.updateScore(roundedScore);
maxCombo = Math.max(maxCombo, combo);
// Check and update high score
if (roundedScore > highScore) {
highScore = roundedScore;
storage.highScore = highScore;
scoreDisplay.updateHighScore(highScore);
// Flash high score when new record is set
if (roundedScore > 0) {
scoreDisplay.showFeedback("New High Score!");
LK.effects.flashObject(scoreDisplay.highScoreText, 0xFFFF00, 500);
}
}
// Update accuracy after every note hit
var accuracy = calculateAccuracy();
scoreDisplay.updateAccuracy(accuracy);
return hitFound;
}
// Handle tap on a lane
function handleLaneTap(laneNum) {
if (!gameStarted || gameOver) {
return;
}
targetZones[laneNum].activate();
return checkNoteHit(laneNum);
}
// Start the game
function startGame() {
if (gameStarted) {
return;
}
gameStarted = true;
game.removeChild(startText);
// Reset game state
score = 0;
combo = 0;
maxCombo = 0;
totalNotes = 0;
hitNotes = 0;
missedNotes = 0; // Reset missed notes counter
consecutiveMisses = 0; // Reset consecutive misses counter
consecutiveHits = 0; // Reset consecutive hits counter
lastFeedbackThreshold = 0; // Reset feedback threshold
perfectHits = 0;
currentLevel = 1;
difficultyMultiplier = 1.0;
gameSpeed = minNoteSpeed; // Reset base speed to minimum
lastSpeedIncreaseTime = 0;
// Reset note spawning variables
currentNotesPerSecond = minNotesPerSecond;
noteSpawnCounter = 0;
gameOver = false;
// Update UI
scoreDisplay.updateScore(score);
// Initialize accuracy with default 100% at game start
scoreDisplay.updateAccuracy(100);
scoreDisplay.updateHighScore(highScore);
// Initial speed up message
scoreDisplay.showFeedback("Get Ready!");
// Start music
LK.playMusic('gameMusic', {
fade: {
start: 0,
end: 1,
duration: 1000
}
});
// Set the first beat to spawn soon
nextBeatFrame = 60;
}
// Check if game should end (based on time or score)
function checkGameOver() {
// For demo purposes, end game after reaching level 10
if (currentLevel >= 10 && score >= 10000) {
endGame(true); // Win
}
// Player loses if accuracy drops too low
if (totalNotes > 20 && calculateAccuracy() < 40) {
endGame(false); // Lose due to low accuracy
}
// Player loses if they miss too many notes in a row
if (consecutiveMisses >= maxConsecutiveMissesAllowed) {
endGame(false); // Lose due to too many consecutive misses
}
}
// End the game
function endGame(win) {
gameOver = true;
// Fade out music
LK.playMusic('gameMusic', {
fade: {
start: 1,
end: 0,
duration: 800
}
});
// Save high score to storage
if (score > highScore) {
highScore = Math.floor(score);
storage.highScore = highScore;
}
// Show game over or win screen based on performance
if (win) {
LK.showYouWin();
} else {
LK.showGameOver();
}
}
// Handle lane tap detection - we'll divide the screen into lanes
function getLaneFromPosition(x) {
return Math.floor(x / laneWidth);
}
// Handle touch events
game.down = function (x, y, obj) {
if (!gameStarted) {
startGame();
} else {
var lane = getLaneFromPosition(x);
if (lane >= 0 && lane < laneCount) {
handleLaneTap(lane);
}
}
};
game.update = function () {
if (!gameStarted) {
return;
}
// Update all active notes
for (var i = activeNotes.length - 1; i >= 0; i--) {
var note = activeNotes[i];
// Remove notes that have been scored and faded out
if (!note.active && note.scored) {
activeNotes.splice(i, 1);
}
}
// Update target zones
for (var j = 0; j < targetZones.length; j++) {
targetZones[j].update();
}
// Calculate note spawning rate based on progress
if (gameStarted && !gameOver) {
// Calculate progress for notes per second (separate from speed progress)
var progressToMaxNoteRate = Math.min(1.0, LK.ticks / noteRampUpTime);
// Calculate new notes per second rate based on progress between min and max
currentNotesPerSecond = minNotesPerSecond + (maxNotesPerSecond - minNotesPerSecond) * progressToMaxNoteRate;
// Calculate frames between notes based on notes per second
var framesBetweenNotes = Math.floor(60 / currentNotesPerSecond);
// Spawn notes based on the current rate
noteSpawnCounter++;
if (noteSpawnCounter >= framesBetweenNotes) {
spawnNote();
noteSpawnCounter = 0;
// Visual feedback when note rate increases significantly
if (LK.ticks % 600 === 0 && progressToMaxNoteRate > 0.2) {
scoreDisplay.showFeedback("More Notes!");
}
}
}
// Generate new beat patterns at intervals - this is now supplementary to the base note spawning
if (gameStarted && !gameOver) {
if (LK.ticks >= nextBeatFrame) {
generateBeatPattern();
// Calculate next beat timing with some variance
var interval = beatFrequency - currentLevel * 5;
interval = Math.max(interval, 30); // Don't go too fast
var variance = Math.floor(Math.random() * beatVariance) - beatVariance / 2;
nextBeatFrame = LK.ticks + interval + variance;
// Increase difficulty over time
if (LK.ticks % 600 === 0) {
// Every 10 seconds
currentLevel++;
difficultyMultiplier = 1.0 + currentLevel * 0.2; // Increased from 0.1 to 0.2 for faster speed growth
// No longer dynamically increasing speed of existing notes
for (var i = 0; i < activeNotes.length; i++) {
var note = activeNotes[i];
// We no longer change speed of active notes
}
}
}
}
// No longer applying speed increases to notes during gameplay
if (gameStarted && !gameOver) {
// We still calculate this for other game mechanics, but don't apply to note speeds
var progressToMaxSpeed = Math.min(1.0, LK.ticks / speedRampUpTime);
var newGameSpeed = minNoteSpeed + (maxNoteSpeed - minNoteSpeed) * progressToMaxSpeed;
// Only update game speed variable but don't change note speeds
if (Math.abs(gameSpeed - newGameSpeed) > 0.1) {
// Update game speed for new notes
gameSpeed = newGameSpeed;
// Visual feedback when speed increases significantly
if (LK.ticks % 300 === 0) {
scoreDisplay.showFeedback("Speed Up!");
// Flash effect on target zones to indicate increased speed
for (var i = 0; i < targetZones.length; i++) {
LK.effects.flashObject(targetZones[i], 0xFF4500, 300);
}
}
}
}
// Check for game over conditions
checkGameOver();
// Check if missedNotes exceeds limit for automatic game over
if (missedNotes >= 10) {
endGame(false); // Lose due to too many missed notes
}
};