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