User prompt
Remove the all the combo UI
Code edit (2 edits merged)
Please save this source code
User prompt
make the start point on where the notes start appearing 200 above the center the screen, add a variable so i can adjust that as i want aswell
User prompt
Add perspective to the game, make the notes seem as if they come from distance towards the screen and the target boxes
User prompt
Please fix the bug: 'TypeError: tween.colorTo is not a function' in or related to this line: 'tween.colorTo(game, {' Line Number: 583
User prompt
Just animate the colors of the background
User prompt
Please fix the bug: 'particles is not defined' in or related to this line: 'self.emitter = new particles.Emitter(self);' Line Number: 46
User prompt
Please fix the bug: 'particles is not defined' in or related to this line: 'self.emitter = new particles.Emitter(self);' Line Number: 46
User prompt
Please fix the bug: 'particles is not defined' in or related to this line: 'self.emitter = new particles.Emitter(self);' Line Number: 46
User prompt
Please fix the bug: 'particles is not defined' in or related to this line: 'self.emitter = new particles.Emitter(self);' Line Number: 46
User prompt
Please fix the bug: 'particles is not defined' in or related to this line: 'self.emitter = new particles.Emitter(self);' Line Number: 46
User prompt
make the background animated and react to the miss and hit
User prompt
make the background animated like the target zone, and add a variable to choose the colors
User prompt
make the tiles increasingly faster ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Now, divide half of the screen for this mechanic, but the top half add two characters, the player character on the left and an enemy character on the right, add a ground, background
User prompt
Sometimes the buttons are giving a miss when they are inside the target zone, correct that
Code edit (1 edits merged)
Please save this source code
User prompt
Change the target box size variable to be able to adjust its size im x and y
User prompt
Make an variable so i can adjust the size of the target box
User prompt
Please fix the bug: 'Uncaught TypeError: Cannot set properties of undefined (setting 'fill')' in or related to this line: 'self.feedbackText.style.fill = color;' Line Number: 214
Code edit (1 edits merged)
Please save this source code
User prompt
Rhythm Reactor
Initial prompt
Make a simple rythm game mechanic, where buttons fall from top of the screen and you have to click in the right timing, in the bottom of the screen will have small semi transparent boxes where the buttons will fall on, and the player has to click when the button is inside the box for the right timing
/**** * 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
}
};