/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var FallingNote = Container.expand(function (lane, noteType) {
var self = Container.call(this);
self.lane = lane;
self.noteType = noteType;
self.speed = 8; // Vertical speed (moving down)
self.hasBeenHit = false;
self.lastY = 0; // Track Y position for vertical movement
// Attach the appropriate note asset based on lane with matching colors
var assetName = 'noteLeft';
if (lane === 1) assetName = 'noteMiddle';
if (lane === 2) assetName = 'noteRight';
var noteGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Apply lane colors to match the new color scheme
var laneBorderColors = [0x00bfff, 0xff33cc, 0xffe066]; // Brighter pastel blue, magenta, yellow
// Ensure note starts with white base color before tinting
noteGraphics.tint = 0xffffff; // Reset to white first
noteGraphics.tint = laneBorderColors[lane];
// Add glow effect
var noteGlow = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
// Ensure glow starts with white base color before tinting
noteGlow.tint = 0xffffff; // Reset to white first
noteGlow.tint = laneBorderColors[lane];
noteGlow.alpha = 0.3;
self.setChildIndex(noteGlow, 0);
// Add simplified light trail effect
self.trail = [];
self.update = function () {
self.lastY = self.y; // Track last Y position
self.y += self.speed; // Move vertically downward
// Update trail
if (LK.ticks % 5 === 0) {
var trail = LK.getAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
// Ensure trail starts with white base color before tinting
trail.tint = 0xffffff; // Reset to white first
trail.tint = laneBorderColors[self.lane];
trail.alpha = 0.1;
trail.x = self.x;
trail.y = self.y - 10;
if (self.parent) self.parent.addChild(trail);
self.trail.push(trail);
// Fade and remove old trail
tween(trail, {
alpha: 0,
scaleX: 0.2,
scaleY: 0.2
}, {
duration: 200,
onFinish: function onFinish() {
if (trail.parent) trail.parent.removeChild(trail);
}
});
}
// Clean up old trail references
self.trail = self.trail.filter(function (t) {
return t.parent;
});
};
return self;
});
var ScoreText = Container.expand(function (text, color) {
var self = Container.call(this);
self.life = 60; // 1 second at 60fps
self.speed = -2;
var textObj = new Text2(text, {
size: 40,
fill: color || "#ffffff"
});
textObj.anchor.set(0.5, 0.5);
self.addChild(textObj);
self.update = function () {
self.y += self.speed;
self.life--;
var textObj = self.getChildAt(0);
textObj.alpha = self.life / 60;
// Life cycle is managed by the main game loop for pooling
};
return self;
});
// Game constants
var TargetCircle = Container.expand(function (lane) {
var self = Container.call(this);
self.lane = lane;
self.isGlowing = false;
self.glowTime = 0;
// Main circle (filled)
var circleGraphics = self.attachAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5
});
var laneBorderColors = [0x00bfff, 0xff33cc, 0xffe066]; // Brighter pastel blue, magenta, yellow
var laneFillColors = [0xe6f7ff, 0xffe6fa, 0xfffbe6]; // Very light pastel fill
// Set fill color for the main circle
circleGraphics.tint = laneFillColors[lane];
circleGraphics.alpha = 0.7;
// Add a border effect as a second, slightly larger circle
var borderCircle = self.attachAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.18,
scaleY: 1.18
});
borderCircle.tint = laneBorderColors[lane];
borderCircle.alpha = 0.9;
// Make sure border is behind the main circle
self.setChildIndex(borderCircle, 0);
// Glow effect
self.glow = function () {
self.isGlowing = true;
self.glowTime = 20; // Glow for 20 frames
circleGraphics.alpha = 1;
borderCircle.alpha = 1;
};
self.update = function () {
if (self.isGlowing) {
self.glowTime--;
if (self.glowTime <= 0) {
self.isGlowing = false;
circleGraphics.alpha = 0.7;
borderCircle.alpha = 0.9;
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xB2E6E6 // Soft teal as fallback for gradient
});
/****
* Game Code
****/
// Gradient background cache for performance
var gradientBg = null;
var bgTintOverlay = null;
var musicBeatCounter = 0;
var musicIntensity = 0;
function drawGradientBg() {
if (gradientBg && gradientBg.parent) {
gradientBg.parent.removeChild(gradientBg);
}
if (bgTintOverlay && bgTintOverlay.parent) {
bgTintOverlay.parent.removeChild(bgTintOverlay);
}
// Use a vertical gradient image asset for background
// Assume 'bgGradient' is a vertical gradient image asset (2048x2732) from soft teal to mist blue
gradientBg = LK.getAsset('bgGradient', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChildAt(gradientBg, 0); // Add as background
// Add tint overlay for music reactive colors
bgTintOverlay = LK.getAsset('bgGradient', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
bgTintOverlay.alpha = 0.3;
bgTintOverlay.tint = 0x00ffff;
game.addChildAt(bgTintOverlay, 1);
}
// Draw once at start
drawGradientBg();
// Redraw on resize for dynamic fit
LK.on('resize', function () {
drawGradientBg();
});
// Background music
// Sound effects
// Lane dividers
// Target circles at bottom of lanes
// Note shapes for each lane with different colors
// Game constants
// Define lane positions early as they're used in multiple places
var laneXPositions = [1024 - 300, 1024, 1024 + 300]; // Three vertical lanes
// Animated background elements
var bgElements = [];
var bgStars = [];
// Create floating background elements
for (var be = 0; be < 12; be++) {
var bgShape = LK.getAsset('noteLeft', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: Math.random() * 0.3 + 0.1,
scaleY: Math.random() * 0.3 + 0.1
});
bgShape.x = Math.random() * 2048;
bgShape.y = Math.random() * 2732;
bgShape.alpha = 0.1;
bgShape.tint = [0x00bfff, 0xff33cc, 0xffe066][Math.floor(Math.random() * 3)];
bgShape.speed = Math.random() * 0.5 + 0.2;
bgShape.rotation = Math.random() * Math.PI * 2;
game.addChildAt(bgShape, 2);
bgElements.push(bgShape);
}
// Create light beams
for (var lb = 0; lb < 3; lb++) {
var lightBeam = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0,
x: laneXPositions[lb],
y: 0,
scaleX: 0.2,
scaleY: 2732 / 4
});
lightBeam.alpha = 0.05;
lightBeam.tint = [0x00bfff, 0xff33cc, 0xffe066][lb];
game.addChildAt(lightBeam, 3);
bgStars.push(lightBeam);
}
// Object Pools for Performance Optimization
var floatingTextPool = [];
var impactLinePool = [];
var missFlashPool = [];
var missLinePool = [];
function getPooledFloatingText(text, color) {
var floatingText;
if (floatingTextPool.length > 0) {
floatingText = floatingTextPool.pop();
floatingText.visible = true;
} else {
floatingText = new ScoreText('', '#ffffff');
}
// Re-initialize
floatingText.life = 60;
var textObj = floatingText.getChildAt(0);
textObj.setText(text);
if (color && textObj.style) textObj.style.fill = color;
textObj.alpha = 1;
floatingText.y = 0;
floatingText.alpha = 1;
floatingText.scale.set(1);
floatingTexts.push(floatingText);
game.addChild(floatingText);
return floatingText;
}
function releaseFloatingText(text) {
for (var i = floatingTexts.length - 1; i >= 0; i--) {
if (floatingTexts[i] === text) {
floatingTexts.splice(i, 1);
break;
}
}
if (text.parent) text.parent.removeChild(text);
text.visible = false;
// Cap pool size to prevent unlimited growth
if (floatingTextPool.length < 20) {
floatingTextPool.push(text);
} else {
text.destroy(); // Destroy excess objects to free memory
}
}
function getPooledObject(pool, createFn) {
var obj;
if (pool.length > 0) {
obj = pool.pop();
obj.visible = true;
} else {
obj = createFn();
}
game.addChild(obj);
return obj;
}
function releaseObject(obj, pool) {
if (obj && obj.parent) {
obj.parent.removeChild(obj);
obj.visible = false;
// Cap pool size to prevent unlimited growth
if (pool.length < 10) {
pool.push(obj);
} else {
obj.destroy(); // Destroy excess objects to free memory
}
}
}
function getPooledImpactLine() {
return getPooledObject(impactLinePool, function () {
return LK.getAsset('laneDivider', {});
});
}
function releaseImpactLine(line) {
releaseObject(line, impactLinePool);
}
function getPooledMissFlash() {
return getPooledObject(missFlashPool, function () {
return LK.getAsset('laneDivider', {});
});
}
function releaseMissFlash(flash) {
releaseObject(flash, missFlashPool);
}
function getPooledMissLine() {
return getPooledObject(missLinePool, function () {
return LK.getAsset('laneDivider', {});
});
}
function releaseMissLine(line) {
releaseObject(line, missLinePool);
}
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
var TARGET_Y = 2400; // Target circle Y position near bottom
var HIT_ZONE = 120; // Pixels around target for hitting
var PERFECT_ZONE = 50; // Pixels for perfect hit
// Game variables
var fallingNotes = [];
var targetCircles = [];
var score = 0;
var combo = 0;
var noteSpawnTimer = 0;
var noteSpawnInterval = 60; // Spawn note every 60 frames initially
var gameSpeed = 1;
var floatingTexts = [];
var totalNotes = 0;
var hitNotes = 0;
var accuracy = 100;
var missedNotes = 0; // Track missed notes for game over condition
var MAX_MISSES = 5; // Game over after 5 misses
// Track consecutive notes per lane
var consecutiveNotesPerLane = [0, 0, 0]; // Track how many consecutive notes spawned in each lane
var lastSpawnedLane = -1; // Track the last lane that spawned a note
// Performance optimization constants
var MAX_NOTES_ON_SCREEN = 15; // Cap notes to prevent memory bloat
var NOTE_VISIBILITY_BUFFER = 200; // Extra pixels before note becomes visible
var CLEANUP_THRESHOLD = 2900; // Y position to cleanup notes that missed target
// Vertical lane dividers with glowing borders
var laneBorders = [];
// Add vertical lane dividers between lanes
for (var ld = 0; ld < 2; ld++) {
var dividerX = laneXPositions[ld] + (laneXPositions[ld + 1] - laneXPositions[ld]) / 2;
var laneDivider = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: dividerX,
y: 1366,
scaleX: 0.5,
scaleY: 2732 / 4,
tint: 0x444444
});
laneDivider.alpha = 0.2;
game.addChild(laneDivider);
}
// Add glowing lane borders
for (var lb = 0; lb < 3; lb++) {
var leftBorder = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[lb] - 150,
y: 1366,
scaleX: 0.3,
scaleY: 2732 / 4,
tint: [0x00bfff, 0xff33cc, 0xffe066][lb]
});
leftBorder.alpha = 0.4;
game.addChild(leftBorder);
laneBorders.push(leftBorder);
var rightBorder = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[lb] + 150,
y: 1366,
scaleX: 0.3,
scaleY: 2732 / 4,
tint: [0x00bfff, 0xff33cc, 0xffe066][lb]
});
rightBorder.alpha = 0.4;
game.addChild(rightBorder);
laneBorders.push(rightBorder);
}
// Key remapping support
var defaultKeyLabels = ['A', 'S', 'D'];
var keyLabels = ['A', 'S', 'D'];
var keyLabelObjects = []; // Store key label references for animation
var keyGlowObjects = []; // Store glow background references
var laneColors = [0xAEEFFF, 0xF7B3E6, 0xFFF9B2]; // Pastel cyan, magenta, yellow for improved contrast
function updateKeyLabels() {
for (var i = 0; i < 3; i++) {
if (keyLabelObjects[i]) {
keyLabelObjects[i].setText(keyLabels[i]);
}
}
}
// Prevent duplicate key assignments
function remapKey(lane, newKey) {
// Only allow remap if newKey is not already assigned
var upperKey = newKey.toUpperCase();
for (var i = 0; i < 3; i++) {
if (i !== lane && keyLabels[i].toUpperCase() === upperKey) {
// Duplicate found, reject remap
return false;
}
}
keyLabels[lane] = upperKey;
updateKeyLabels();
return true;
}
// Initial label creation
for (var i = 0; i < 3; i++) {
var target = new TargetCircle(i);
target.x = laneXPositions[i];
target.y = TARGET_Y; // Fixed target position
targetCircles.push(target);
game.addChild(target);
// Add glow background first (behind the letter)
var keyGlow = LK.getAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[i],
y: TARGET_Y + 150,
// Position below target circle
scaleX: 2.5,
scaleY: 2.5,
alpha: 0.2
});
var laneBorderColors = [0x00bfff, 0xff33cc, 0xffe066]; // Brighter pastel blue, magenta, yellow
keyGlow.tint = laneBorderColors[i];
keyGlowObjects.push(keyGlow);
game.addChild(keyGlow);
}
// HUD panel background removed - no longer needed
var hudPanel = null; // Keep reference for compatibility with existing code
// Score display - left side of HUD
var scoreText = new Text2('0', {
size: 115,
// Same size as combo text
fill: 0xFFFFFF,
// White text for maximum contrast
dropShadow: true,
dropShadowColor: 0x000000,
//{2P} // Black shadow for contrast
dropShadowDistance: 6,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 8,
dropShadowAlpha: 0.9
});
scoreText.anchor.set(0.5, 0.5);
var scoreLabel = new Text2('SKOR', {
size: 50,
// Slightly larger label
fill: 0xFFFFFF,
// White for consistency
dropShadow: true,
dropShadowColor: 0x000000,
// Black shadow
dropShadowDistance: 3,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 5,
dropShadowAlpha: 0.8
});
// Add semi-transparent background panel for score
var scorePanel = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024 - 400,
y: 180,
// Moved down from 140 to match new score position
scaleX: 80,
scaleY: 50,
tint: 0x000000,
alpha: 0.5
});
LK.gui.top.addChild(scorePanel);
scoreText.x = 1024 - 400;
scoreLabel.x = 1024 - 400;
scoreText.y = 240; // Moved down from 200
LK.gui.top.addChild(scoreText);
// Score label
scoreLabel.anchor.set(0.5, 1);
scoreLabel.y = 160; // Moved down from 120
LK.gui.top.addChild(scoreLabel);
// Add COMBO text below score
var comboLabelBelow = new Text2('KOMBO', {
size: 40,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 4,
dropShadowAlpha: 0.8
});
comboLabelBelow.anchor.set(0.5, 0);
comboLabelBelow.x = 1024 - 400;
comboLabelBelow.y = 320; // Moved down from 280
LK.gui.top.addChild(comboLabelBelow);
// Add combo counter below combo label
var comboCounterText = new Text2('0', {
size: 90,
fill: 0xFFFFFF,
// White text
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 4,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 6,
dropShadowAlpha: 0.9
});
comboCounterText.anchor.set(0.5, 0);
comboCounterText.x = 1024 - 400;
comboCounterText.y = 360; // Below combo label
LK.gui.top.addChild(comboCounterText);
// Combo display - center of HUD
var comboText = new Text2('0', {
size: 115,
fill: 0xFFAA00,
dropShadow: true,
dropShadowColor: 0xFFFFFF,
dropShadowDistance: 4,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 6,
dropShadowAlpha: 0.7
});
comboText.anchor.set(0.5, 0.5);
comboText.x = 1024;
comboText.y = 240;
LK.gui.top.addChild(comboText);
// Combo label
var comboLabel = new Text2('KOMBO', {
size: 46,
fill: 0x888888,
dropShadow: true,
dropShadowColor: 0xFFFFFF,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 4,
dropShadowAlpha: 0.7
});
comboLabel.anchor.set(0.5, 1);
comboLabel.x = 1024;
comboLabel.y = 200;
LK.gui.top.addChild(comboLabel);
// Accuracy display - right side of HUD
var accuracyText = new Text2('100%', {
size: 115,
fill: 0x00CC66,
dropShadow: true,
dropShadowColor: 0xFFFFFF,
dropShadowDistance: 4,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 6,
dropShadowAlpha: 0.7
});
accuracyText.anchor.set(0.5, 0.5);
accuracyText.x = 1024 + 300;
accuracyText.y = 180;
LK.gui.top.addChild(accuracyText);
// Accuracy label
var accuracyLabel = new Text2('İSABET', {
size: 46,
fill: 0x888888,
dropShadow: true,
dropShadowColor: 0xFFFFFF,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 4,
dropShadowAlpha: 0.7
});
accuracyLabel.anchor.set(0.5, 1);
accuracyLabel.x = 1024 + 300;
accuracyLabel.y = 140;
LK.gui.top.addChild(accuracyLabel);
// Tutorial overlay
var tutorialActive = true;
var tutorialContainer = new Container();
game.addChild(tutorialContainer);
// Tutorial background
var tutorialBg = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
scaleX: 512,
scaleY: 683,
tint: 0x000000,
alpha: 0.85
});
tutorialContainer.addChild(tutorialBg);
// Language selection container at top center
var langContainer = new Container();
langContainer.x = 1024;
langContainer.y = 400;
tutorialContainer.addChild(langContainer);
// Language selection background
var langBg = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
scaleX: 100,
scaleY: 20,
tint: 0x333333,
alpha: 0.7
});
langContainer.addChild(langBg);
// Language options
var languages = ['EN', 'MT', 'ES', 'DE', 'IT', 'FR', 'TR'];
var selectedLang = 0; // 0 for English, 1 for Maltese, 2 for Spanish, 3 for German, 4 for Italian, 5 for French, 6 for Turkish
var langButtons = [];
// Create language buttons
for (var li = 0; li < languages.length; li++) {
var langButton = new Container();
langButton.x = (li - 0.5) * 120 - 300;
langButton.y = 0;
// Button background
var btnBg = LK.getAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8,
tint: li === selectedLang ? 0x00FF00 : 0x666666,
alpha: li === selectedLang ? 0.9 : 0.6
});
langButton.addChild(btnBg);
// Button text
var btnText = new Text2(languages[li], {
size: 40,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 3,
dropShadowAlpha: 0.8
});
btnText.anchor.set(0.5, 0.5);
langButton.addChild(btnText);
// Store button references
langButton.btnBg = btnBg;
langButton.langIndex = li;
langButtons.push(langButton);
// Button interaction
langButton.down = function () {
var index = this.langIndex;
if (selectedLang !== index) {
// Update selected language
selectedLang = index;
// Update button appearances
for (var j = 0; j < langButtons.length; j++) {
var btn = langButtons[j];
var isSelected = j === selectedLang;
btn.btnBg.tint = isSelected ? 0x00FF00 : 0x666666;
btn.btnBg.alpha = isSelected ? 0.9 : 0.6;
}
// Update tutorial texts based on language
updateTutorialLanguage();
}
};
langContainer.addChild(langButton);
}
// Tutorial texts in multiple languages
var tutorialTexts = {
TR: {
title: 'NASIL OYNANIR',
instructions: ['Notalar aşağı düşer', 'Notalar daireye ulaştığında', 'Daireye bas!', '', 'PUANLAMA:', 'PERFECT = 3 puan', 'GOOD = 1 puan', 'Kombo bonusu ekler!', '', 'Yüksek kombo = Yüksek skor!', '', 'DİKKAT:', '5 nota kaçırırsan kaybedersin!'],
start: 'BAŞLA',
score: 'SKOR',
combo: 'KOMBO',
accuracy: 'İSABET',
miss: 'KAÇIRDIN!'
},
EN: {
title: 'HOW TO PLAY',
instructions: ['Notes fall down', 'When notes reach the circle', 'Tap the circle!', '', 'SCORING:', 'PERFECT = 3 points', 'GOOD = 1 point', 'Adds combo bonus!', '', 'High combo = High score!', '', 'WARNING:', 'Miss 5 notes and you lose!'],
start: 'START',
score: 'SCORE',
combo: 'COMBO',
accuracy: 'ACCURACY',
miss: 'MISS!'
},
ES: {
title: 'CÓMO JUGAR',
instructions: ['Las notas caen', 'Cuando las notas llegan al círculo', '¡Toca el círculo!', '', 'PUNTUACIÓN:', 'PERFECTO = 3 puntos', 'BUENO = 1 punto', '¡Añade bonus de combo!', '', '¡Combo alto = Puntuación alta!', '', '¡CUIDADO:', 'Falla 5 notas y pierdes!'],
start: 'INICIAR',
score: 'PUNTOS',
combo: 'COMBO',
accuracy: 'PRECISIÓN',
miss: '¡FALLO!'
},
DE: {
title: 'ANLEITUNG',
instructions: ['Noten fallen herab', 'Wenn Noten den Kreis erreichen', 'Tippe den Kreis!', '', 'BEWERTUNG:', 'PERFEKT = 3 Punkte', 'GUT = 1 Punkt', 'Fügt Combo-Bonus hinzu!', '', 'Hohe Combo = Hohe Punktzahl!', '', 'ACHTUNG:', 'Verfehle 5 Noten und du verlierst!'],
start: 'START',
score: 'PUNKTE',
combo: 'COMBO',
accuracy: 'GENAUIGKEIT',
miss: 'VERFEHLT!'
},
IT: {
title: 'COME GIOCARE',
instructions: ['Le note cadono', 'Quando le note raggiungono il cerchio', 'Tocca il cerchio!', '', 'PUNTEGGIO:', 'PERFETTO = 3 punti', 'BUONO = 1 punto', 'Aggiunge bonus combo!', '', 'Combo alta = Punteggio alto!', '', 'ATTENZIONE:', 'Mancare 5 note e perdi!'],
start: 'INIZIA',
score: 'PUNTEGGIO',
combo: 'COMBO',
accuracy: 'PRECISIONE',
miss: 'MANCATO!'
},
FR: {
title: 'COMMENT JOUER',
instructions: ['Les notes tombent', 'Quand les notes atteignent le cercle', 'Touchez le cercle!', '', 'NOTATION:', 'PARFAIT = 3 points', 'BON = 1 point', 'Ajoute bonus combo!', '', 'Combo élevé = Score élevé!', '', 'ATTENTION:', 'Ratez 5 notes et vous perdez!'],
start: 'COMMENCER',
score: 'SCORE',
combo: 'COMBO',
accuracy: 'PRÉCISION',
miss: 'RATÉ!'
},
MT: {
title: 'KIEL TILGĦAB',
instructions: ['In-noti jaqgħu', 'Meta n-noti jaslu għaċ-ċirku', 'Aqra ċ-ċirku!', '', 'PUNTI:', 'PERFETT = 3 punti', 'TAJJEB = 1 punt', 'Iżżid bonus combo!', '', 'Combo għoli = Punteġġ għoli!', '', 'ATTENZJONI:', 'Tilef 5 noti u titlef!'],
start: 'IBDA',
score: 'PUNTEĠĠ',
combo: 'COMBO',
accuracy: 'PREĊIŻJONI',
miss: 'TILEF!'
}
};
// Tutorial title
var tutorialTitle = new Text2(tutorialTexts.TR.title, {
size: 120,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 4,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 6,
dropShadowAlpha: 0.9
});
tutorialTitle.anchor.set(0.5, 0.5);
tutorialTitle.x = 1024;
tutorialTitle.y = 800;
tutorialContainer.addChild(tutorialTitle);
// Tutorial instructions - will be created by updateTutorialLanguage function
var instructionTexts = [];
var startText; // Store reference to start button text
// Function to update tutorial language
function updateTutorialLanguage() {
var langKeys = ['EN', 'MT', 'ES', 'DE', 'IT', 'FR', 'TR'];
var lang = langKeys[selectedLang] || 'EN';
var texts = tutorialTexts[lang];
// Update title
tutorialTitle.setText(texts.title);
// Clear existing instruction texts
for (var i = 0; i < instructionTexts.length; i++) {
if (instructionTexts[i].parent) {
instructionTexts[i].parent.removeChild(instructionTexts[i]);
}
}
instructionTexts = [];
// Create new instruction texts
var instructionY = 950;
for (var ti = 0; ti < texts.instructions.length; ti++) {
var instructLine = new Text2(texts.instructions[ti], {
size: 60,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 4,
dropShadowAlpha: 0.8
});
instructLine.anchor.set(0.5, 0);
instructLine.x = 1024;
instructLine.y = instructionY;
tutorialContainer.addChild(instructLine);
instructionTexts.push(instructLine);
instructionY += 95;
}
// Update start button text if it exists
if (startText) {
startText.setText(texts.start);
}
// Update score board labels
if (scoreLabel) {
scoreLabel.setText(texts.score);
}
if (comboLabelBelow) {
comboLabelBelow.setText(texts.combo);
}
if (comboLabel) {
comboLabel.setText(texts.combo);
}
if (accuracyLabel) {
accuracyLabel.setText(texts.accuracy);
}
}
// Initial language setup
selectedLang = 0; // Default to English (first in new order)
updateTutorialLanguage();
// Start button
var startButton = new Container();
var startBg = LK.getAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 1,
tint: 0x00FF00,
alpha: 0.8
});
startButton.addChild(startBg);
startText = new Text2('BAŞLA', {
size: 80,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 3,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 5,
dropShadowAlpha: 0.9
});
startText.anchor.set(0.5, 0.5);
startButton.addChild(startText);
startButton.x = 1024;
startButton.y = 2000;
tutorialContainer.addChild(startButton);
// Button hover effect
startButton.down = function () {
tween(startBg, {
scaleX: 3.2,
scaleY: 1.1,
alpha: 1
}, {
duration: 100,
easing: tween.easeOut
});
};
startButton.up = function () {
tween(startBg, {
scaleX: 3,
scaleY: 1,
alpha: 0.8
}, {
duration: 100,
easing: tween.easeOut
});
// Start game
tutorialActive = false;
tween(tutorialContainer, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tutorialContainer.destroy();
// Start playing music after tutorial
LK.playMusic('bgmusic');
}
});
};
// Visual examples
var exampleNote = LK.getAsset('noteMiddle', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2150,
scaleX: 1.5,
scaleY: 1.5
});
exampleNote.tint = 0xff33cc;
tutorialContainer.addChild(exampleNote);
// Animate example note
tween(exampleNote, {
y: 2250,
scaleX: 2,
scaleY: 2
}, {
duration: 1000,
loop: true,
yoyo: true,
easing: tween.easeInOut
});
// Instructions removed - no longer needed
function spawnNote() {
// Cap note queue - only spawn if we're under the limit
if (fallingNotes.length >= MAX_NOTES_ON_SCREEN) {
return; // Skip spawning to prevent memory bloat
}
// Create array of available lanes (lanes that haven't reached 2 consecutive notes)
var availableLanes = [];
for (var i = 0; i < 3; i++) {
// A lane is available if it hasn't spawned 2 consecutive notes
if (i !== lastSpawnedLane || consecutiveNotesPerLane[i] < 2) {
availableLanes.push(i);
}
}
// If no lanes are available (shouldn't happen with 3 lanes), reset and use all lanes
if (availableLanes.length === 0) {
availableLanes = [0, 1, 2];
}
// Choose a random lane from available lanes
var randomIndex = Math.floor(Math.random() * availableLanes.length);
var selectedLane = availableLanes[randomIndex];
// Update consecutive note tracking
if (selectedLane === lastSpawnedLane) {
consecutiveNotesPerLane[selectedLane]++;
} else {
// Reset all lanes' consecutive counts
for (var j = 0; j < 3; j++) {
consecutiveNotesPerLane[j] = 0;
}
consecutiveNotesPerLane[selectedLane] = 1;
}
lastSpawnedLane = selectedLane;
var note = new FallingNote(selectedLane);
note.x = laneXPositions[selectedLane]; // Position in correct lane
note.y = -NOTE_VISIBILITY_BUFFER; // Start above screen
note.lastY = note.y; // Initialize lastY tracking
fallingNotes.push(note);
game.addChild(note);
}
function hitNote(lane) {
var hitNote = null;
var closestDistance = Infinity;
// Find the closest note in the hit zone for this lane
for (var i = 0; i < fallingNotes.length; i++) {
var note = fallingNotes[i];
if (note.lane === lane && !note.hasBeenHit) {
var distance = Math.abs(note.y - TARGET_Y);
if (distance < HIT_ZONE && distance < closestDistance) {
hitNote = note;
closestDistance = distance;
}
}
}
if (hitNote) {
hitNote.hasBeenHit = true;
var points = 0;
var feedback = "";
var color = "#ffffff";
if (closestDistance <= PERFECT_ZONE) {
points = 3;
feedback = "PERFECT!";
color = "#00ff00";
LK.getSound('perfect').play();
} else {
points = 1;
feedback = "GOOD!";
color = "#ffff00";
LK.getSound('hit').play();
}
// Increment combo for consecutive hits
combo++;
score += points * (1 + Math.floor(combo / 10) * 0.5); // Combo bonus
hitNotes++;
totalNotes++;
accuracy = Math.round(hitNotes / totalNotes * 100);
// Smooth score animation
var targetScore = Math.floor(score);
tween(scoreText, {
text: targetScore
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
scoreText.setText(targetScore.toString());
}
});
// Update displays
comboText.setText(combo.toString());
comboCounterText.setText(combo.toString());
accuracyText.setText(accuracy + '%');
// Update accuracy color based on performance
if (accuracy >= 95) accuracyText.tint = 0x00FF00; // Green
else if (accuracy >= 80) accuracyText.tint = 0xFFFF00; // Yellow
else if (accuracy >= 60) accuracyText.tint = 0xFF8800; // Orange
else accuracyText.tint = 0xFF0000; // Red
// Change combo text color based on combo level
var comboColor = 0xFFAA00; // Default orange
if (combo >= 100) comboColor = 0xFF0000; // Red for high combo
else if (combo >= 50) comboColor = 0xFF00FF; // Magenta for medium combo
else if (combo >= 20) comboColor = 0x00FFFF; // Cyan for building combo
else if (combo >= 10) comboColor = 0xFFFF00; // Yellow for decent combo
comboText.tint = comboColor;
// HUD glow pulse every 10 hits
if (combo > 0 && combo % 10 === 0) {
// Pulse combo text
tween(comboText, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(comboText, {
scaleX: 1,
scaleY: 1
}, {
duration: 150,
easing: tween.easeOut
});
}
});
// HUD panel glow pulse removed - no hudPanel to animate
// Show streak banner
var streakText = getPooledFloatingText('STREAK x' + combo + '!', '#FFD700');
streakText.x = 1024;
streakText.y = 300;
}
// Screen-wide particle burst at major milestones
if (combo === 50 || combo === 100 || combo === 200) {
LK.effects.flashScreen(0xFFD700, 800); // Gold flash
// HUD panel effect removed - no hudPanel to animate
// Show milestone banner
var milestoneText = getPooledFloatingText('★ ' + combo + ' COMBO! ★', '#FFD700');
milestoneText.x = 1024;
milestoneText.y = 400;
tween(milestoneText, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut
});
// Add camera shake for major milestones
var shakeIntensity = 15;
var originalGameX = game.x;
var originalGameY = game.y;
tween(game, {
x: originalGameX + shakeIntensity
}, {
duration: 50,
onFinish: function onFinish() {
tween(game, {
x: originalGameX - shakeIntensity
}, {
duration: 50,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: 50
});
}
});
}
});
}
// Add subtle camera shake on perfect hits
if (closestDistance <= PERFECT_ZONE && combo > 10) {
var shakeAmount = 5;
var origX = game.x;
var origY = game.y;
tween(game, {
x: origX + shakeAmount,
y: origY + shakeAmount
}, {
duration: 40,
onFinish: function onFinish() {
tween(game, {
x: origX,
y: origY
}, {
duration: 40
});
}
});
}
// Change background tint color on note hit
if (bgTintOverlay) {
var hitColors = [0x00ffff, 0xff00ff, 0xffff00];
bgTintOverlay.tint = hitColors[Math.floor(Math.random() * hitColors.length)];
tween(bgTintOverlay, {
alpha: 0.5
}, {
duration: 200,
onFinish: function onFinish() {
tween(bgTintOverlay, {
alpha: 0.3
}, {
duration: 400
});
}
});
}
// Visual feedback
targetCircles[lane].glow();
// Animate key label when pressed - blaze to 100% opacity and scale to 120%
if (keyLabelObjects[lane]) {
// Animate the letter
tween(keyLabelObjects[lane], {
scaleX: 1.2,
scaleY: 1.2,
alpha: 1 // Blaze to 100% opacity for Perfect hits
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
scaleX: 1,
scaleY: 1,
alpha: 0.8 // Return to gentle glow
}, {
duration: 300,
easing: tween.easeOut
});
}
});
// Animate the glow background
if (keyGlowObjects[lane]) {
tween(keyGlowObjects[lane], {
scaleX: 3.0,
scaleY: 3.0,
alpha: 0.6
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyGlowObjects[lane], {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0.2
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
}
// Create vertical impact beam effect that shoots upward from the target
var impactLine = getPooledImpactLine();
impactLine.anchor.set(0.5, 1);
impactLine.x = laneXPositions[lane];
impactLine.y = 2600;
impactLine.scale.set(closestDistance <= PERFECT_ZONE ? 3 : 2, closestDistance <= PERFECT_ZONE ? 2732 / 4 : 2000 / 4);
impactLine.alpha = 0.8;
var laneColors = [0x00FFFF, 0xFF00FF, 0xFFFF00]; // Cyan, Magenta, Yellow
impactLine.tint = closestDistance <= PERFECT_ZONE ? 0xFFFFFF : laneColors[lane]; // White for perfect, lane color for good
// Add color-matched burst effect
for (var b = 0; b < 8; b++) {
var burst = LK.getAsset('noteLeft', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.3
});
burst.tint = laneColors[lane];
burst.x = laneXPositions[lane];
burst.y = 2600;
burst.alpha = 0.9;
game.addChild(burst);
var angle = Math.PI * 2 / 8 * b;
var distance = closestDistance <= PERFECT_ZONE ? 200 : 120;
tween(burst, {
x: burst.x + Math.cos(angle) * distance,
y: burst.y + Math.sin(angle) * distance,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
if (burst.parent) burst.parent.removeChild(burst);
}
});
}
// Animate vertical beam shooting upward
tween(impactLine, {
scaleY: 0,
// Collapse upward
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
releaseImpactLine(impactLine);
}
});
// Target circle pulse effect
var targetCircle = targetCircles[lane];
tween(targetCircle, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(targetCircle, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(targetCircle, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeOut
});
}
});
}
});
// Floating score text
var floatingText = getPooledFloatingText('+' + points, color);
floatingText.x = laneXPositions[lane];
floatingText.y = TARGET_Y - 100;
// Remove note
hitNote.destroy();
for (var j = 0; j < fallingNotes.length; j++) {
if (fallingNotes[j] === hitNote) {
fallingNotes.splice(j, 1);
break;
}
}
} else {
// Miss - reset combo and update accuracy
if (combo > 0) {
combo = 0;
totalNotes++;
missedNotes++; // Increment miss counter
accuracy = Math.round(hitNotes / totalNotes * 100);
comboText.setText('0');
comboCounterText.setText('0');
comboText.tint = 0xFFAA00; // Reset combo color to default
accuracyText.setText(accuracy + '%');
// Update accuracy color
if (accuracy >= 95) accuracyText.tint = 0x00FF00;else if (accuracy >= 80) accuracyText.tint = 0xFFFF00;else if (accuracy >= 60) accuracyText.tint = 0xFF8800;else accuracyText.tint = 0xFF0000;
LK.getSound('miss').play();
var langKeys = ['EN', 'MT', 'ES', 'DE', 'IT', 'FR', 'TR'];
var currentLang = langKeys[selectedLang] || 'EN';
var missText = getPooledFloatingText(tutorialTexts[currentLang].miss, '#ff0000');
missText.x = laneXPositions[lane];
missText.y = TARGET_Y - 100;
// Check for game over condition
if (missedNotes >= MAX_MISSES) {
LK.showGameOver();
return;
}
// Animate key label miss effect - desaturate and shake
if (keyLabelObjects[lane]) {
// Desaturate by changing tint to gray
var originalTint = keyLabelObjects[lane].tint;
keyLabelObjects[lane].tint = 0x888888; // Gray for desaturation
var originalX = keyLabelObjects[lane].x;
// Shake effect - move left and right
var shakeDistance = 8;
tween(keyLabelObjects[lane], {
x: originalX - shakeDistance
}, {
duration: 75,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
x: originalX + shakeDistance
}, {
duration: 75,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
x: originalX,
tint: originalTint // Restore original color
}, {
duration: 75,
easing: tween.easeOut
});
}
});
}
});
}
// Flash lane red for miss feedback
var laneFlash = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[lane],
y: 1366,
scaleX: 0.8,
scaleY: 2732 / 4,
tint: 0xFF0000,
alpha: 0.3
});
game.addChild(laneFlash);
tween(laneFlash, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
if (laneFlash.parent) laneFlash.parent.removeChild(laneFlash);
}
});
}
}
}
// Stars and particle effects storage
var backgroundStars = [];
var comboParticles = [];
// Create animated background stars
for (var s = 0; s < 30; s++) {
var star = LK.getAsset('noteLeft', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: Math.random() * 0.2 + 0.05,
scaleY: Math.random() * 0.2 + 0.05
});
star.x = Math.random() * 2048;
star.y = Math.random() * 2732;
star.alpha = Math.random() * 0.3 + 0.1;
star.tint = 0xffffff;
star.speed = Math.random() * 0.5 + 0.1;
star.twinkleSpeed = Math.random() * 0.02 + 0.01;
game.addChildAt(star, 2);
backgroundStars.push(star);
}
// Tap detection for lanes
game.down = function (x, y, obj) {
// Don't process taps during tutorial
if (tutorialActive) {
return;
}
// Determine which lane was tapped
var tappedLane = -1;
for (var i = 0; i < 3; i++) {
if (Math.abs(x - laneXPositions[i]) < 150) {
tappedLane = i;
break;
}
}
if (tappedLane >= 0) {
hitNote(tappedLane);
}
};
// Keyboard controls for mapped keys, strictly mapped to lanes and supporting remap
LK.on('keyDown', function (event) {
// Don't process keyboard during tutorial
if (tutorialActive) {
return;
}
var pressedKey = event.key ? event.key.toUpperCase() : '';
var lane = -1;
for (var i = 0; i < 3; i++) {
if (keyLabels[i].toUpperCase() === pressedKey) {
lane = i;
break;
}
}
if (lane >= 0 && lane < 3) {
// Only allow hit if there is a note in this lane in the hit zone
var found = false;
for (var j = 0; j < fallingNotes.length; j++) {
var note = fallingNotes[j];
if (note.lane === lane && !note.hasBeenHit) {
var distance = Math.abs(note.y - TARGET_Y);
if (distance < HIT_ZONE) {
found = true;
break;
}
}
}
if (found) {
hitNote(lane);
// Visual feedback for key press
if (targetCircles[lane]) {
targetCircles[lane].glow();
}
} else {
// Incorrect timing or no note in this lane: shake and flash red
if (keyLabelObjects[lane]) {
var originalTint = keyLabelObjects[lane].tint;
keyLabelObjects[lane].tint = 0xFF0000; // Red for error
var originalX = keyLabelObjects[lane].x;
var shakeDistance = 8;
tween(keyLabelObjects[lane], {
x: originalX - shakeDistance
}, {
duration: 60,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
x: originalX + shakeDistance
}, {
duration: 60,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
x: originalX,
tint: originalTint
}, {
duration: 60,
easing: tween.easeOut
});
}
});
}
});
}
}
}
});
game.update = function () {
// Don't update game while tutorial is active
if (tutorialActive) {
return;
}
// Music-synced spawn: use music time if available, else fallback to timer
noteSpawnTimer++;
if (noteSpawnTimer >= noteSpawnInterval) {
spawnNote();
noteSpawnTimer = 0;
// Progressive speed increase: decrease interval as score/combo rises
if (accuracy >= 85 && combo >= 10) {
if (noteSpawnInterval > 12) {
noteSpawnInterval -= 0.25;
}
} else if (accuracy < 60 && combo === 0) {
if (noteSpawnInterval < 80) {
noteSpawnInterval += 0.5;
}
} else {
if (noteSpawnInterval > 18) {
noteSpawnInterval -= 0.07;
}
}
// Ball speed logic removed - this is now a rhythm game without a player ball
}
// Update animated background stars
for (var s = 0; s < backgroundStars.length; s++) {
var star = backgroundStars[s];
star.y += star.speed;
star.alpha = 0.1 + Math.sin(LK.ticks * star.twinkleSpeed) * 0.2;
if (star.y > 2832) {
star.y = -100;
star.x = Math.random() * 2048;
}
}
// Update falling notes (now moving horizontally)
for (var i = fallingNotes.length - 1; i >= 0; i--) {
var note = fallingNotes[i];
// Check if note missed (passed target vertically)
if (note.lastY <= TARGET_Y + HIT_ZONE && note.y > TARGET_Y + HIT_ZONE && !note.hasBeenHit) {
// Note was missed
combo = 0;
totalNotes++;
missedNotes++; // Increment miss counter
accuracy = Math.round(hitNotes / totalNotes * 100);
comboText.setText('0');
comboCounterText.setText('0');
comboText.tint = 0xFFAA00; // Reset combo color to default
accuracyText.setText(accuracy + '%');
// Update accuracy color
if (accuracy >= 95) accuracyText.tint = 0x00FF00;else if (accuracy >= 80) accuracyText.tint = 0xFFFF00;else if (accuracy >= 60) accuracyText.tint = 0xFF8800;else accuracyText.tint = 0xFF0000;
LK.getSound('miss').play();
var langKeys = ['EN', 'MT', 'ES', 'DE', 'IT', 'FR', 'TR'];
var currentLang = langKeys[selectedLang] || 'EN';
var missText = getPooledFloatingText(tutorialTexts[currentLang].miss, '#ff0000');
missText.x = laneXPositions[note.lane];
missText.y = TARGET_Y - 100;
// Check for game over condition
if (missedNotes >= MAX_MISSES) {
LK.showGameOver();
return;
}
// Animate key label miss effect for passed notes
var missedLane = note.lane;
if (keyLabelObjects[missedLane]) {
// Desaturate by changing tint to gray
var originalTint = keyLabelObjects[missedLane].tint;
keyLabelObjects[missedLane].tint = 0x888888; // Gray for desaturation
var originalX = keyLabelObjects[missedLane].x;
// Shake effect - move left and right
var shakeDistance = 8;
tween(keyLabelObjects[missedLane], {
x: originalX - shakeDistance
}, {
duration: 75,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[missedLane], {
x: originalX + shakeDistance
}, {
duration: 75,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[missedLane], {
x: originalX,
tint: originalTint // Restore original color
}, {
duration: 75,
easing: tween.easeOut
});
}
});
}
});
}
// Flash lane red for missed note
var laneFlash = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[note.lane],
y: 1366,
scaleX: 0.8,
scaleY: 2732 / 4,
tint: 0xFF0000,
alpha: 0.3
});
game.addChild(laneFlash);
// Animate miss flash fade out
tween(laneFlash, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
if (laneFlash.parent) laneFlash.parent.removeChild(laneFlash);
}
});
}
// Remove notes that are off screen (bottom now) - optimized cleanup threshold
if (note.y > CLEANUP_THRESHOLD) {
note.destroy();
fallingNotes.splice(i, 1);
}
}
// Music-synced pulse: every 60 frames, pulse the ball and target
if (LK.ticks % 60 === 0) {
musicBeatCounter++;
musicIntensity = 0.5 + Math.sin(musicBeatCounter * 0.1) * 0.5;
// Pulse lane borders with beat
for (var lb = 0; lb < laneBorders.length; lb++) {
tween(laneBorders[lb], {
alpha: 0.6 * musicIntensity,
scaleX: 0.8
}, {
duration: 100,
onFinish: function (border) {
return function () {
tween(border, {
alpha: 0.3,
scaleX: 0.5
}, {
duration: 300
});
};
}(laneBorders[lb])
});
}
// Pulse hit circles with music beat
for (var i = 0; i < targetCircles.length; i++) {
tween(targetCircles[i], {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 80,
onFinish: function (tc) {
return function () {
tween(tc, {
scaleX: 1,
scaleY: 1
}, {
duration: 80
});
};
}(targetCircles[i])
});
}
}
// Update animated background elements
for (var e = 0; e < bgElements.length; e++) {
bgElements[e].y -= bgElements[e].speed;
bgElements[e].rotation += 0.01;
bgElements[e].alpha = 0.1 + musicIntensity * 0.1;
if (bgElements[e].y < -100) {
bgElements[e].y = 2832;
bgElements[e].x = Math.random() * 2048;
}
}
// Pulse light beams with music
if (LK.ticks % 30 === 0 && bgStars.length > 0) {
var beamIndex = Math.floor(Math.random() * bgStars.length);
tween(bgStars[beamIndex], {
alpha: 0.2 * musicIntensity,
scaleX: 0.5
}, {
duration: 200,
onFinish: function onFinish() {
tween(bgStars[beamIndex], {
alpha: 0.05,
scaleX: 0.2
}, {
duration: 400
});
}
});
}
// Add combo streak particles
if (combo > 0 && combo % 5 === 0) {
for (var cp = 0; cp < 3; cp++) {
var particle = LK.getAsset('noteLeft', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.3
});
particle.tint = [0x00bfff, 0xff33cc, 0xffe066][cp];
particle.x = laneXPositions[cp];
particle.y = TARGET_Y;
particle.alpha = 0.8;
game.addChild(particle);
tween(particle, {
y: particle.y - 200,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 800,
onFinish: function onFinish() {
if (particle.parent) particle.parent.removeChild(particle);
}
});
}
}
// Update floating texts with batch cleanup
var textsToRemove = [];
for (var i = floatingTexts.length - 1; i >= 0; i--) {
var text = floatingTexts[i];
if (text.life <= 0) {
textsToRemove.push(text);
}
}
// Batch remove expired floating texts to reduce array operations
for (var j = 0; j < textsToRemove.length; j++) {
releaseFloatingText(textsToRemove[j]);
}
// Instructions removed - no longer shown after tutorial
// Achievement milestones
if (!window.achievements) window.achievements = {};
if (score >= 100 && !window.achievements['century']) {
window.achievements['century'] = true;
var achievementText = getPooledFloatingText('★ CENTURY SCORE! ★', '#FFD700');
achievementText.x = 1024;
achievementText.y = 600;
tween(achievementText, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut
});
}
if (combo >= 50 && !window.achievements['combo50']) {
window.achievements['combo50'] = true;
var achievementText2 = getPooledFloatingText('★ 50 COMBO MASTER! ★', '#FF00FF');
achievementText2.x = 1024;
achievementText2.y = 700;
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var FallingNote = Container.expand(function (lane, noteType) {
var self = Container.call(this);
self.lane = lane;
self.noteType = noteType;
self.speed = 8; // Vertical speed (moving down)
self.hasBeenHit = false;
self.lastY = 0; // Track Y position for vertical movement
// Attach the appropriate note asset based on lane with matching colors
var assetName = 'noteLeft';
if (lane === 1) assetName = 'noteMiddle';
if (lane === 2) assetName = 'noteRight';
var noteGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Apply lane colors to match the new color scheme
var laneBorderColors = [0x00bfff, 0xff33cc, 0xffe066]; // Brighter pastel blue, magenta, yellow
// Ensure note starts with white base color before tinting
noteGraphics.tint = 0xffffff; // Reset to white first
noteGraphics.tint = laneBorderColors[lane];
// Add glow effect
var noteGlow = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
// Ensure glow starts with white base color before tinting
noteGlow.tint = 0xffffff; // Reset to white first
noteGlow.tint = laneBorderColors[lane];
noteGlow.alpha = 0.3;
self.setChildIndex(noteGlow, 0);
// Add simplified light trail effect
self.trail = [];
self.update = function () {
self.lastY = self.y; // Track last Y position
self.y += self.speed; // Move vertically downward
// Update trail
if (LK.ticks % 5 === 0) {
var trail = LK.getAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.5,
scaleY: 0.5
});
// Ensure trail starts with white base color before tinting
trail.tint = 0xffffff; // Reset to white first
trail.tint = laneBorderColors[self.lane];
trail.alpha = 0.1;
trail.x = self.x;
trail.y = self.y - 10;
if (self.parent) self.parent.addChild(trail);
self.trail.push(trail);
// Fade and remove old trail
tween(trail, {
alpha: 0,
scaleX: 0.2,
scaleY: 0.2
}, {
duration: 200,
onFinish: function onFinish() {
if (trail.parent) trail.parent.removeChild(trail);
}
});
}
// Clean up old trail references
self.trail = self.trail.filter(function (t) {
return t.parent;
});
};
return self;
});
var ScoreText = Container.expand(function (text, color) {
var self = Container.call(this);
self.life = 60; // 1 second at 60fps
self.speed = -2;
var textObj = new Text2(text, {
size: 40,
fill: color || "#ffffff"
});
textObj.anchor.set(0.5, 0.5);
self.addChild(textObj);
self.update = function () {
self.y += self.speed;
self.life--;
var textObj = self.getChildAt(0);
textObj.alpha = self.life / 60;
// Life cycle is managed by the main game loop for pooling
};
return self;
});
// Game constants
var TargetCircle = Container.expand(function (lane) {
var self = Container.call(this);
self.lane = lane;
self.isGlowing = false;
self.glowTime = 0;
// Main circle (filled)
var circleGraphics = self.attachAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5
});
var laneBorderColors = [0x00bfff, 0xff33cc, 0xffe066]; // Brighter pastel blue, magenta, yellow
var laneFillColors = [0xe6f7ff, 0xffe6fa, 0xfffbe6]; // Very light pastel fill
// Set fill color for the main circle
circleGraphics.tint = laneFillColors[lane];
circleGraphics.alpha = 0.7;
// Add a border effect as a second, slightly larger circle
var borderCircle = self.attachAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.18,
scaleY: 1.18
});
borderCircle.tint = laneBorderColors[lane];
borderCircle.alpha = 0.9;
// Make sure border is behind the main circle
self.setChildIndex(borderCircle, 0);
// Glow effect
self.glow = function () {
self.isGlowing = true;
self.glowTime = 20; // Glow for 20 frames
circleGraphics.alpha = 1;
borderCircle.alpha = 1;
};
self.update = function () {
if (self.isGlowing) {
self.glowTime--;
if (self.glowTime <= 0) {
self.isGlowing = false;
circleGraphics.alpha = 0.7;
borderCircle.alpha = 0.9;
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xB2E6E6 // Soft teal as fallback for gradient
});
/****
* Game Code
****/
// Gradient background cache for performance
var gradientBg = null;
var bgTintOverlay = null;
var musicBeatCounter = 0;
var musicIntensity = 0;
function drawGradientBg() {
if (gradientBg && gradientBg.parent) {
gradientBg.parent.removeChild(gradientBg);
}
if (bgTintOverlay && bgTintOverlay.parent) {
bgTintOverlay.parent.removeChild(bgTintOverlay);
}
// Use a vertical gradient image asset for background
// Assume 'bgGradient' is a vertical gradient image asset (2048x2732) from soft teal to mist blue
gradientBg = LK.getAsset('bgGradient', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
game.addChildAt(gradientBg, 0); // Add as background
// Add tint overlay for music reactive colors
bgTintOverlay = LK.getAsset('bgGradient', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 2048,
height: 2732
});
bgTintOverlay.alpha = 0.3;
bgTintOverlay.tint = 0x00ffff;
game.addChildAt(bgTintOverlay, 1);
}
// Draw once at start
drawGradientBg();
// Redraw on resize for dynamic fit
LK.on('resize', function () {
drawGradientBg();
});
// Background music
// Sound effects
// Lane dividers
// Target circles at bottom of lanes
// Note shapes for each lane with different colors
// Game constants
// Define lane positions early as they're used in multiple places
var laneXPositions = [1024 - 300, 1024, 1024 + 300]; // Three vertical lanes
// Animated background elements
var bgElements = [];
var bgStars = [];
// Create floating background elements
for (var be = 0; be < 12; be++) {
var bgShape = LK.getAsset('noteLeft', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: Math.random() * 0.3 + 0.1,
scaleY: Math.random() * 0.3 + 0.1
});
bgShape.x = Math.random() * 2048;
bgShape.y = Math.random() * 2732;
bgShape.alpha = 0.1;
bgShape.tint = [0x00bfff, 0xff33cc, 0xffe066][Math.floor(Math.random() * 3)];
bgShape.speed = Math.random() * 0.5 + 0.2;
bgShape.rotation = Math.random() * Math.PI * 2;
game.addChildAt(bgShape, 2);
bgElements.push(bgShape);
}
// Create light beams
for (var lb = 0; lb < 3; lb++) {
var lightBeam = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0,
x: laneXPositions[lb],
y: 0,
scaleX: 0.2,
scaleY: 2732 / 4
});
lightBeam.alpha = 0.05;
lightBeam.tint = [0x00bfff, 0xff33cc, 0xffe066][lb];
game.addChildAt(lightBeam, 3);
bgStars.push(lightBeam);
}
// Object Pools for Performance Optimization
var floatingTextPool = [];
var impactLinePool = [];
var missFlashPool = [];
var missLinePool = [];
function getPooledFloatingText(text, color) {
var floatingText;
if (floatingTextPool.length > 0) {
floatingText = floatingTextPool.pop();
floatingText.visible = true;
} else {
floatingText = new ScoreText('', '#ffffff');
}
// Re-initialize
floatingText.life = 60;
var textObj = floatingText.getChildAt(0);
textObj.setText(text);
if (color && textObj.style) textObj.style.fill = color;
textObj.alpha = 1;
floatingText.y = 0;
floatingText.alpha = 1;
floatingText.scale.set(1);
floatingTexts.push(floatingText);
game.addChild(floatingText);
return floatingText;
}
function releaseFloatingText(text) {
for (var i = floatingTexts.length - 1; i >= 0; i--) {
if (floatingTexts[i] === text) {
floatingTexts.splice(i, 1);
break;
}
}
if (text.parent) text.parent.removeChild(text);
text.visible = false;
// Cap pool size to prevent unlimited growth
if (floatingTextPool.length < 20) {
floatingTextPool.push(text);
} else {
text.destroy(); // Destroy excess objects to free memory
}
}
function getPooledObject(pool, createFn) {
var obj;
if (pool.length > 0) {
obj = pool.pop();
obj.visible = true;
} else {
obj = createFn();
}
game.addChild(obj);
return obj;
}
function releaseObject(obj, pool) {
if (obj && obj.parent) {
obj.parent.removeChild(obj);
obj.visible = false;
// Cap pool size to prevent unlimited growth
if (pool.length < 10) {
pool.push(obj);
} else {
obj.destroy(); // Destroy excess objects to free memory
}
}
}
function getPooledImpactLine() {
return getPooledObject(impactLinePool, function () {
return LK.getAsset('laneDivider', {});
});
}
function releaseImpactLine(line) {
releaseObject(line, impactLinePool);
}
function getPooledMissFlash() {
return getPooledObject(missFlashPool, function () {
return LK.getAsset('laneDivider', {});
});
}
function releaseMissFlash(flash) {
releaseObject(flash, missFlashPool);
}
function getPooledMissLine() {
return getPooledObject(missLinePool, function () {
return LK.getAsset('laneDivider', {});
});
}
function releaseMissLine(line) {
releaseObject(line, missLinePool);
}
//Minimalistic tween library which should be used for animations over time, including tinting / colouring an object, scaling, rotating, or changing any game object property.
var TARGET_Y = 2400; // Target circle Y position near bottom
var HIT_ZONE = 120; // Pixels around target for hitting
var PERFECT_ZONE = 50; // Pixels for perfect hit
// Game variables
var fallingNotes = [];
var targetCircles = [];
var score = 0;
var combo = 0;
var noteSpawnTimer = 0;
var noteSpawnInterval = 60; // Spawn note every 60 frames initially
var gameSpeed = 1;
var floatingTexts = [];
var totalNotes = 0;
var hitNotes = 0;
var accuracy = 100;
var missedNotes = 0; // Track missed notes for game over condition
var MAX_MISSES = 5; // Game over after 5 misses
// Track consecutive notes per lane
var consecutiveNotesPerLane = [0, 0, 0]; // Track how many consecutive notes spawned in each lane
var lastSpawnedLane = -1; // Track the last lane that spawned a note
// Performance optimization constants
var MAX_NOTES_ON_SCREEN = 15; // Cap notes to prevent memory bloat
var NOTE_VISIBILITY_BUFFER = 200; // Extra pixels before note becomes visible
var CLEANUP_THRESHOLD = 2900; // Y position to cleanup notes that missed target
// Vertical lane dividers with glowing borders
var laneBorders = [];
// Add vertical lane dividers between lanes
for (var ld = 0; ld < 2; ld++) {
var dividerX = laneXPositions[ld] + (laneXPositions[ld + 1] - laneXPositions[ld]) / 2;
var laneDivider = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: dividerX,
y: 1366,
scaleX: 0.5,
scaleY: 2732 / 4,
tint: 0x444444
});
laneDivider.alpha = 0.2;
game.addChild(laneDivider);
}
// Add glowing lane borders
for (var lb = 0; lb < 3; lb++) {
var leftBorder = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[lb] - 150,
y: 1366,
scaleX: 0.3,
scaleY: 2732 / 4,
tint: [0x00bfff, 0xff33cc, 0xffe066][lb]
});
leftBorder.alpha = 0.4;
game.addChild(leftBorder);
laneBorders.push(leftBorder);
var rightBorder = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[lb] + 150,
y: 1366,
scaleX: 0.3,
scaleY: 2732 / 4,
tint: [0x00bfff, 0xff33cc, 0xffe066][lb]
});
rightBorder.alpha = 0.4;
game.addChild(rightBorder);
laneBorders.push(rightBorder);
}
// Key remapping support
var defaultKeyLabels = ['A', 'S', 'D'];
var keyLabels = ['A', 'S', 'D'];
var keyLabelObjects = []; // Store key label references for animation
var keyGlowObjects = []; // Store glow background references
var laneColors = [0xAEEFFF, 0xF7B3E6, 0xFFF9B2]; // Pastel cyan, magenta, yellow for improved contrast
function updateKeyLabels() {
for (var i = 0; i < 3; i++) {
if (keyLabelObjects[i]) {
keyLabelObjects[i].setText(keyLabels[i]);
}
}
}
// Prevent duplicate key assignments
function remapKey(lane, newKey) {
// Only allow remap if newKey is not already assigned
var upperKey = newKey.toUpperCase();
for (var i = 0; i < 3; i++) {
if (i !== lane && keyLabels[i].toUpperCase() === upperKey) {
// Duplicate found, reject remap
return false;
}
}
keyLabels[lane] = upperKey;
updateKeyLabels();
return true;
}
// Initial label creation
for (var i = 0; i < 3; i++) {
var target = new TargetCircle(i);
target.x = laneXPositions[i];
target.y = TARGET_Y; // Fixed target position
targetCircles.push(target);
game.addChild(target);
// Add glow background first (behind the letter)
var keyGlow = LK.getAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[i],
y: TARGET_Y + 150,
// Position below target circle
scaleX: 2.5,
scaleY: 2.5,
alpha: 0.2
});
var laneBorderColors = [0x00bfff, 0xff33cc, 0xffe066]; // Brighter pastel blue, magenta, yellow
keyGlow.tint = laneBorderColors[i];
keyGlowObjects.push(keyGlow);
game.addChild(keyGlow);
}
// HUD panel background removed - no longer needed
var hudPanel = null; // Keep reference for compatibility with existing code
// Score display - left side of HUD
var scoreText = new Text2('0', {
size: 115,
// Same size as combo text
fill: 0xFFFFFF,
// White text for maximum contrast
dropShadow: true,
dropShadowColor: 0x000000,
//{2P} // Black shadow for contrast
dropShadowDistance: 6,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 8,
dropShadowAlpha: 0.9
});
scoreText.anchor.set(0.5, 0.5);
var scoreLabel = new Text2('SKOR', {
size: 50,
// Slightly larger label
fill: 0xFFFFFF,
// White for consistency
dropShadow: true,
dropShadowColor: 0x000000,
// Black shadow
dropShadowDistance: 3,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 5,
dropShadowAlpha: 0.8
});
// Add semi-transparent background panel for score
var scorePanel = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024 - 400,
y: 180,
// Moved down from 140 to match new score position
scaleX: 80,
scaleY: 50,
tint: 0x000000,
alpha: 0.5
});
LK.gui.top.addChild(scorePanel);
scoreText.x = 1024 - 400;
scoreLabel.x = 1024 - 400;
scoreText.y = 240; // Moved down from 200
LK.gui.top.addChild(scoreText);
// Score label
scoreLabel.anchor.set(0.5, 1);
scoreLabel.y = 160; // Moved down from 120
LK.gui.top.addChild(scoreLabel);
// Add COMBO text below score
var comboLabelBelow = new Text2('KOMBO', {
size: 40,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 4,
dropShadowAlpha: 0.8
});
comboLabelBelow.anchor.set(0.5, 0);
comboLabelBelow.x = 1024 - 400;
comboLabelBelow.y = 320; // Moved down from 280
LK.gui.top.addChild(comboLabelBelow);
// Add combo counter below combo label
var comboCounterText = new Text2('0', {
size: 90,
fill: 0xFFFFFF,
// White text
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 4,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 6,
dropShadowAlpha: 0.9
});
comboCounterText.anchor.set(0.5, 0);
comboCounterText.x = 1024 - 400;
comboCounterText.y = 360; // Below combo label
LK.gui.top.addChild(comboCounterText);
// Combo display - center of HUD
var comboText = new Text2('0', {
size: 115,
fill: 0xFFAA00,
dropShadow: true,
dropShadowColor: 0xFFFFFF,
dropShadowDistance: 4,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 6,
dropShadowAlpha: 0.7
});
comboText.anchor.set(0.5, 0.5);
comboText.x = 1024;
comboText.y = 240;
LK.gui.top.addChild(comboText);
// Combo label
var comboLabel = new Text2('KOMBO', {
size: 46,
fill: 0x888888,
dropShadow: true,
dropShadowColor: 0xFFFFFF,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 4,
dropShadowAlpha: 0.7
});
comboLabel.anchor.set(0.5, 1);
comboLabel.x = 1024;
comboLabel.y = 200;
LK.gui.top.addChild(comboLabel);
// Accuracy display - right side of HUD
var accuracyText = new Text2('100%', {
size: 115,
fill: 0x00CC66,
dropShadow: true,
dropShadowColor: 0xFFFFFF,
dropShadowDistance: 4,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 6,
dropShadowAlpha: 0.7
});
accuracyText.anchor.set(0.5, 0.5);
accuracyText.x = 1024 + 300;
accuracyText.y = 180;
LK.gui.top.addChild(accuracyText);
// Accuracy label
var accuracyLabel = new Text2('İSABET', {
size: 46,
fill: 0x888888,
dropShadow: true,
dropShadowColor: 0xFFFFFF,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 4,
dropShadowAlpha: 0.7
});
accuracyLabel.anchor.set(0.5, 1);
accuracyLabel.x = 1024 + 300;
accuracyLabel.y = 140;
LK.gui.top.addChild(accuracyLabel);
// Tutorial overlay
var tutorialActive = true;
var tutorialContainer = new Container();
game.addChild(tutorialContainer);
// Tutorial background
var tutorialBg = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
scaleX: 512,
scaleY: 683,
tint: 0x000000,
alpha: 0.85
});
tutorialContainer.addChild(tutorialBg);
// Language selection container at top center
var langContainer = new Container();
langContainer.x = 1024;
langContainer.y = 400;
tutorialContainer.addChild(langContainer);
// Language selection background
var langBg = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
scaleX: 100,
scaleY: 20,
tint: 0x333333,
alpha: 0.7
});
langContainer.addChild(langBg);
// Language options
var languages = ['EN', 'MT', 'ES', 'DE', 'IT', 'FR', 'TR'];
var selectedLang = 0; // 0 for English, 1 for Maltese, 2 for Spanish, 3 for German, 4 for Italian, 5 for French, 6 for Turkish
var langButtons = [];
// Create language buttons
for (var li = 0; li < languages.length; li++) {
var langButton = new Container();
langButton.x = (li - 0.5) * 120 - 300;
langButton.y = 0;
// Button background
var btnBg = LK.getAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.8,
scaleY: 0.8,
tint: li === selectedLang ? 0x00FF00 : 0x666666,
alpha: li === selectedLang ? 0.9 : 0.6
});
langButton.addChild(btnBg);
// Button text
var btnText = new Text2(languages[li], {
size: 40,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 3,
dropShadowAlpha: 0.8
});
btnText.anchor.set(0.5, 0.5);
langButton.addChild(btnText);
// Store button references
langButton.btnBg = btnBg;
langButton.langIndex = li;
langButtons.push(langButton);
// Button interaction
langButton.down = function () {
var index = this.langIndex;
if (selectedLang !== index) {
// Update selected language
selectedLang = index;
// Update button appearances
for (var j = 0; j < langButtons.length; j++) {
var btn = langButtons[j];
var isSelected = j === selectedLang;
btn.btnBg.tint = isSelected ? 0x00FF00 : 0x666666;
btn.btnBg.alpha = isSelected ? 0.9 : 0.6;
}
// Update tutorial texts based on language
updateTutorialLanguage();
}
};
langContainer.addChild(langButton);
}
// Tutorial texts in multiple languages
var tutorialTexts = {
TR: {
title: 'NASIL OYNANIR',
instructions: ['Notalar aşağı düşer', 'Notalar daireye ulaştığında', 'Daireye bas!', '', 'PUANLAMA:', 'PERFECT = 3 puan', 'GOOD = 1 puan', 'Kombo bonusu ekler!', '', 'Yüksek kombo = Yüksek skor!', '', 'DİKKAT:', '5 nota kaçırırsan kaybedersin!'],
start: 'BAŞLA',
score: 'SKOR',
combo: 'KOMBO',
accuracy: 'İSABET',
miss: 'KAÇIRDIN!'
},
EN: {
title: 'HOW TO PLAY',
instructions: ['Notes fall down', 'When notes reach the circle', 'Tap the circle!', '', 'SCORING:', 'PERFECT = 3 points', 'GOOD = 1 point', 'Adds combo bonus!', '', 'High combo = High score!', '', 'WARNING:', 'Miss 5 notes and you lose!'],
start: 'START',
score: 'SCORE',
combo: 'COMBO',
accuracy: 'ACCURACY',
miss: 'MISS!'
},
ES: {
title: 'CÓMO JUGAR',
instructions: ['Las notas caen', 'Cuando las notas llegan al círculo', '¡Toca el círculo!', '', 'PUNTUACIÓN:', 'PERFECTO = 3 puntos', 'BUENO = 1 punto', '¡Añade bonus de combo!', '', '¡Combo alto = Puntuación alta!', '', '¡CUIDADO:', 'Falla 5 notas y pierdes!'],
start: 'INICIAR',
score: 'PUNTOS',
combo: 'COMBO',
accuracy: 'PRECISIÓN',
miss: '¡FALLO!'
},
DE: {
title: 'ANLEITUNG',
instructions: ['Noten fallen herab', 'Wenn Noten den Kreis erreichen', 'Tippe den Kreis!', '', 'BEWERTUNG:', 'PERFEKT = 3 Punkte', 'GUT = 1 Punkt', 'Fügt Combo-Bonus hinzu!', '', 'Hohe Combo = Hohe Punktzahl!', '', 'ACHTUNG:', 'Verfehle 5 Noten und du verlierst!'],
start: 'START',
score: 'PUNKTE',
combo: 'COMBO',
accuracy: 'GENAUIGKEIT',
miss: 'VERFEHLT!'
},
IT: {
title: 'COME GIOCARE',
instructions: ['Le note cadono', 'Quando le note raggiungono il cerchio', 'Tocca il cerchio!', '', 'PUNTEGGIO:', 'PERFETTO = 3 punti', 'BUONO = 1 punto', 'Aggiunge bonus combo!', '', 'Combo alta = Punteggio alto!', '', 'ATTENZIONE:', 'Mancare 5 note e perdi!'],
start: 'INIZIA',
score: 'PUNTEGGIO',
combo: 'COMBO',
accuracy: 'PRECISIONE',
miss: 'MANCATO!'
},
FR: {
title: 'COMMENT JOUER',
instructions: ['Les notes tombent', 'Quand les notes atteignent le cercle', 'Touchez le cercle!', '', 'NOTATION:', 'PARFAIT = 3 points', 'BON = 1 point', 'Ajoute bonus combo!', '', 'Combo élevé = Score élevé!', '', 'ATTENTION:', 'Ratez 5 notes et vous perdez!'],
start: 'COMMENCER',
score: 'SCORE',
combo: 'COMBO',
accuracy: 'PRÉCISION',
miss: 'RATÉ!'
},
MT: {
title: 'KIEL TILGĦAB',
instructions: ['In-noti jaqgħu', 'Meta n-noti jaslu għaċ-ċirku', 'Aqra ċ-ċirku!', '', 'PUNTI:', 'PERFETT = 3 punti', 'TAJJEB = 1 punt', 'Iżżid bonus combo!', '', 'Combo għoli = Punteġġ għoli!', '', 'ATTENZJONI:', 'Tilef 5 noti u titlef!'],
start: 'IBDA',
score: 'PUNTEĠĠ',
combo: 'COMBO',
accuracy: 'PREĊIŻJONI',
miss: 'TILEF!'
}
};
// Tutorial title
var tutorialTitle = new Text2(tutorialTexts.TR.title, {
size: 120,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 4,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 6,
dropShadowAlpha: 0.9
});
tutorialTitle.anchor.set(0.5, 0.5);
tutorialTitle.x = 1024;
tutorialTitle.y = 800;
tutorialContainer.addChild(tutorialTitle);
// Tutorial instructions - will be created by updateTutorialLanguage function
var instructionTexts = [];
var startText; // Store reference to start button text
// Function to update tutorial language
function updateTutorialLanguage() {
var langKeys = ['EN', 'MT', 'ES', 'DE', 'IT', 'FR', 'TR'];
var lang = langKeys[selectedLang] || 'EN';
var texts = tutorialTexts[lang];
// Update title
tutorialTitle.setText(texts.title);
// Clear existing instruction texts
for (var i = 0; i < instructionTexts.length; i++) {
if (instructionTexts[i].parent) {
instructionTexts[i].parent.removeChild(instructionTexts[i]);
}
}
instructionTexts = [];
// Create new instruction texts
var instructionY = 950;
for (var ti = 0; ti < texts.instructions.length; ti++) {
var instructLine = new Text2(texts.instructions[ti], {
size: 60,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 2,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 4,
dropShadowAlpha: 0.8
});
instructLine.anchor.set(0.5, 0);
instructLine.x = 1024;
instructLine.y = instructionY;
tutorialContainer.addChild(instructLine);
instructionTexts.push(instructLine);
instructionY += 95;
}
// Update start button text if it exists
if (startText) {
startText.setText(texts.start);
}
// Update score board labels
if (scoreLabel) {
scoreLabel.setText(texts.score);
}
if (comboLabelBelow) {
comboLabelBelow.setText(texts.combo);
}
if (comboLabel) {
comboLabel.setText(texts.combo);
}
if (accuracyLabel) {
accuracyLabel.setText(texts.accuracy);
}
}
// Initial language setup
selectedLang = 0; // Default to English (first in new order)
updateTutorialLanguage();
// Start button
var startButton = new Container();
var startBg = LK.getAsset('targetCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 1,
tint: 0x00FF00,
alpha: 0.8
});
startButton.addChild(startBg);
startText = new Text2('BAŞLA', {
size: 80,
fill: 0xFFFFFF,
dropShadow: true,
dropShadowColor: 0x000000,
dropShadowDistance: 3,
dropShadowAngle: Math.PI / 2,
dropShadowBlur: 5,
dropShadowAlpha: 0.9
});
startText.anchor.set(0.5, 0.5);
startButton.addChild(startText);
startButton.x = 1024;
startButton.y = 2000;
tutorialContainer.addChild(startButton);
// Button hover effect
startButton.down = function () {
tween(startBg, {
scaleX: 3.2,
scaleY: 1.1,
alpha: 1
}, {
duration: 100,
easing: tween.easeOut
});
};
startButton.up = function () {
tween(startBg, {
scaleX: 3,
scaleY: 1,
alpha: 0.8
}, {
duration: 100,
easing: tween.easeOut
});
// Start game
tutorialActive = false;
tween(tutorialContainer, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
tutorialContainer.destroy();
// Start playing music after tutorial
LK.playMusic('bgmusic');
}
});
};
// Visual examples
var exampleNote = LK.getAsset('noteMiddle', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2150,
scaleX: 1.5,
scaleY: 1.5
});
exampleNote.tint = 0xff33cc;
tutorialContainer.addChild(exampleNote);
// Animate example note
tween(exampleNote, {
y: 2250,
scaleX: 2,
scaleY: 2
}, {
duration: 1000,
loop: true,
yoyo: true,
easing: tween.easeInOut
});
// Instructions removed - no longer needed
function spawnNote() {
// Cap note queue - only spawn if we're under the limit
if (fallingNotes.length >= MAX_NOTES_ON_SCREEN) {
return; // Skip spawning to prevent memory bloat
}
// Create array of available lanes (lanes that haven't reached 2 consecutive notes)
var availableLanes = [];
for (var i = 0; i < 3; i++) {
// A lane is available if it hasn't spawned 2 consecutive notes
if (i !== lastSpawnedLane || consecutiveNotesPerLane[i] < 2) {
availableLanes.push(i);
}
}
// If no lanes are available (shouldn't happen with 3 lanes), reset and use all lanes
if (availableLanes.length === 0) {
availableLanes = [0, 1, 2];
}
// Choose a random lane from available lanes
var randomIndex = Math.floor(Math.random() * availableLanes.length);
var selectedLane = availableLanes[randomIndex];
// Update consecutive note tracking
if (selectedLane === lastSpawnedLane) {
consecutiveNotesPerLane[selectedLane]++;
} else {
// Reset all lanes' consecutive counts
for (var j = 0; j < 3; j++) {
consecutiveNotesPerLane[j] = 0;
}
consecutiveNotesPerLane[selectedLane] = 1;
}
lastSpawnedLane = selectedLane;
var note = new FallingNote(selectedLane);
note.x = laneXPositions[selectedLane]; // Position in correct lane
note.y = -NOTE_VISIBILITY_BUFFER; // Start above screen
note.lastY = note.y; // Initialize lastY tracking
fallingNotes.push(note);
game.addChild(note);
}
function hitNote(lane) {
var hitNote = null;
var closestDistance = Infinity;
// Find the closest note in the hit zone for this lane
for (var i = 0; i < fallingNotes.length; i++) {
var note = fallingNotes[i];
if (note.lane === lane && !note.hasBeenHit) {
var distance = Math.abs(note.y - TARGET_Y);
if (distance < HIT_ZONE && distance < closestDistance) {
hitNote = note;
closestDistance = distance;
}
}
}
if (hitNote) {
hitNote.hasBeenHit = true;
var points = 0;
var feedback = "";
var color = "#ffffff";
if (closestDistance <= PERFECT_ZONE) {
points = 3;
feedback = "PERFECT!";
color = "#00ff00";
LK.getSound('perfect').play();
} else {
points = 1;
feedback = "GOOD!";
color = "#ffff00";
LK.getSound('hit').play();
}
// Increment combo for consecutive hits
combo++;
score += points * (1 + Math.floor(combo / 10) * 0.5); // Combo bonus
hitNotes++;
totalNotes++;
accuracy = Math.round(hitNotes / totalNotes * 100);
// Smooth score animation
var targetScore = Math.floor(score);
tween(scoreText, {
text: targetScore
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
scoreText.setText(targetScore.toString());
}
});
// Update displays
comboText.setText(combo.toString());
comboCounterText.setText(combo.toString());
accuracyText.setText(accuracy + '%');
// Update accuracy color based on performance
if (accuracy >= 95) accuracyText.tint = 0x00FF00; // Green
else if (accuracy >= 80) accuracyText.tint = 0xFFFF00; // Yellow
else if (accuracy >= 60) accuracyText.tint = 0xFF8800; // Orange
else accuracyText.tint = 0xFF0000; // Red
// Change combo text color based on combo level
var comboColor = 0xFFAA00; // Default orange
if (combo >= 100) comboColor = 0xFF0000; // Red for high combo
else if (combo >= 50) comboColor = 0xFF00FF; // Magenta for medium combo
else if (combo >= 20) comboColor = 0x00FFFF; // Cyan for building combo
else if (combo >= 10) comboColor = 0xFFFF00; // Yellow for decent combo
comboText.tint = comboColor;
// HUD glow pulse every 10 hits
if (combo > 0 && combo % 10 === 0) {
// Pulse combo text
tween(comboText, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(comboText, {
scaleX: 1,
scaleY: 1
}, {
duration: 150,
easing: tween.easeOut
});
}
});
// HUD panel glow pulse removed - no hudPanel to animate
// Show streak banner
var streakText = getPooledFloatingText('STREAK x' + combo + '!', '#FFD700');
streakText.x = 1024;
streakText.y = 300;
}
// Screen-wide particle burst at major milestones
if (combo === 50 || combo === 100 || combo === 200) {
LK.effects.flashScreen(0xFFD700, 800); // Gold flash
// HUD panel effect removed - no hudPanel to animate
// Show milestone banner
var milestoneText = getPooledFloatingText('★ ' + combo + ' COMBO! ★', '#FFD700');
milestoneText.x = 1024;
milestoneText.y = 400;
tween(milestoneText, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut
});
// Add camera shake for major milestones
var shakeIntensity = 15;
var originalGameX = game.x;
var originalGameY = game.y;
tween(game, {
x: originalGameX + shakeIntensity
}, {
duration: 50,
onFinish: function onFinish() {
tween(game, {
x: originalGameX - shakeIntensity
}, {
duration: 50,
onFinish: function onFinish() {
tween(game, {
x: originalGameX,
y: originalGameY
}, {
duration: 50
});
}
});
}
});
}
// Add subtle camera shake on perfect hits
if (closestDistance <= PERFECT_ZONE && combo > 10) {
var shakeAmount = 5;
var origX = game.x;
var origY = game.y;
tween(game, {
x: origX + shakeAmount,
y: origY + shakeAmount
}, {
duration: 40,
onFinish: function onFinish() {
tween(game, {
x: origX,
y: origY
}, {
duration: 40
});
}
});
}
// Change background tint color on note hit
if (bgTintOverlay) {
var hitColors = [0x00ffff, 0xff00ff, 0xffff00];
bgTintOverlay.tint = hitColors[Math.floor(Math.random() * hitColors.length)];
tween(bgTintOverlay, {
alpha: 0.5
}, {
duration: 200,
onFinish: function onFinish() {
tween(bgTintOverlay, {
alpha: 0.3
}, {
duration: 400
});
}
});
}
// Visual feedback
targetCircles[lane].glow();
// Animate key label when pressed - blaze to 100% opacity and scale to 120%
if (keyLabelObjects[lane]) {
// Animate the letter
tween(keyLabelObjects[lane], {
scaleX: 1.2,
scaleY: 1.2,
alpha: 1 // Blaze to 100% opacity for Perfect hits
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
scaleX: 1,
scaleY: 1,
alpha: 0.8 // Return to gentle glow
}, {
duration: 300,
easing: tween.easeOut
});
}
});
// Animate the glow background
if (keyGlowObjects[lane]) {
tween(keyGlowObjects[lane], {
scaleX: 3.0,
scaleY: 3.0,
alpha: 0.6
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyGlowObjects[lane], {
scaleX: 2.5,
scaleY: 2.5,
alpha: 0.2
}, {
duration: 200,
easing: tween.easeOut
});
}
});
}
}
// Create vertical impact beam effect that shoots upward from the target
var impactLine = getPooledImpactLine();
impactLine.anchor.set(0.5, 1);
impactLine.x = laneXPositions[lane];
impactLine.y = 2600;
impactLine.scale.set(closestDistance <= PERFECT_ZONE ? 3 : 2, closestDistance <= PERFECT_ZONE ? 2732 / 4 : 2000 / 4);
impactLine.alpha = 0.8;
var laneColors = [0x00FFFF, 0xFF00FF, 0xFFFF00]; // Cyan, Magenta, Yellow
impactLine.tint = closestDistance <= PERFECT_ZONE ? 0xFFFFFF : laneColors[lane]; // White for perfect, lane color for good
// Add color-matched burst effect
for (var b = 0; b < 8; b++) {
var burst = LK.getAsset('noteLeft', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.3
});
burst.tint = laneColors[lane];
burst.x = laneXPositions[lane];
burst.y = 2600;
burst.alpha = 0.9;
game.addChild(burst);
var angle = Math.PI * 2 / 8 * b;
var distance = closestDistance <= PERFECT_ZONE ? 200 : 120;
tween(burst, {
x: burst.x + Math.cos(angle) * distance,
y: burst.y + Math.sin(angle) * distance,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
if (burst.parent) burst.parent.removeChild(burst);
}
});
}
// Animate vertical beam shooting upward
tween(impactLine, {
scaleY: 0,
// Collapse upward
alpha: 0
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
releaseImpactLine(impactLine);
}
});
// Target circle pulse effect
var targetCircle = targetCircles[lane];
tween(targetCircle, {
scaleX: 0.8,
scaleY: 0.8
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(targetCircle, {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(targetCircle, {
scaleX: 1,
scaleY: 1
}, {
duration: 100,
easing: tween.easeOut
});
}
});
}
});
// Floating score text
var floatingText = getPooledFloatingText('+' + points, color);
floatingText.x = laneXPositions[lane];
floatingText.y = TARGET_Y - 100;
// Remove note
hitNote.destroy();
for (var j = 0; j < fallingNotes.length; j++) {
if (fallingNotes[j] === hitNote) {
fallingNotes.splice(j, 1);
break;
}
}
} else {
// Miss - reset combo and update accuracy
if (combo > 0) {
combo = 0;
totalNotes++;
missedNotes++; // Increment miss counter
accuracy = Math.round(hitNotes / totalNotes * 100);
comboText.setText('0');
comboCounterText.setText('0');
comboText.tint = 0xFFAA00; // Reset combo color to default
accuracyText.setText(accuracy + '%');
// Update accuracy color
if (accuracy >= 95) accuracyText.tint = 0x00FF00;else if (accuracy >= 80) accuracyText.tint = 0xFFFF00;else if (accuracy >= 60) accuracyText.tint = 0xFF8800;else accuracyText.tint = 0xFF0000;
LK.getSound('miss').play();
var langKeys = ['EN', 'MT', 'ES', 'DE', 'IT', 'FR', 'TR'];
var currentLang = langKeys[selectedLang] || 'EN';
var missText = getPooledFloatingText(tutorialTexts[currentLang].miss, '#ff0000');
missText.x = laneXPositions[lane];
missText.y = TARGET_Y - 100;
// Check for game over condition
if (missedNotes >= MAX_MISSES) {
LK.showGameOver();
return;
}
// Animate key label miss effect - desaturate and shake
if (keyLabelObjects[lane]) {
// Desaturate by changing tint to gray
var originalTint = keyLabelObjects[lane].tint;
keyLabelObjects[lane].tint = 0x888888; // Gray for desaturation
var originalX = keyLabelObjects[lane].x;
// Shake effect - move left and right
var shakeDistance = 8;
tween(keyLabelObjects[lane], {
x: originalX - shakeDistance
}, {
duration: 75,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
x: originalX + shakeDistance
}, {
duration: 75,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
x: originalX,
tint: originalTint // Restore original color
}, {
duration: 75,
easing: tween.easeOut
});
}
});
}
});
}
// Flash lane red for miss feedback
var laneFlash = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[lane],
y: 1366,
scaleX: 0.8,
scaleY: 2732 / 4,
tint: 0xFF0000,
alpha: 0.3
});
game.addChild(laneFlash);
tween(laneFlash, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
if (laneFlash.parent) laneFlash.parent.removeChild(laneFlash);
}
});
}
}
}
// Stars and particle effects storage
var backgroundStars = [];
var comboParticles = [];
// Create animated background stars
for (var s = 0; s < 30; s++) {
var star = LK.getAsset('noteLeft', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: Math.random() * 0.2 + 0.05,
scaleY: Math.random() * 0.2 + 0.05
});
star.x = Math.random() * 2048;
star.y = Math.random() * 2732;
star.alpha = Math.random() * 0.3 + 0.1;
star.tint = 0xffffff;
star.speed = Math.random() * 0.5 + 0.1;
star.twinkleSpeed = Math.random() * 0.02 + 0.01;
game.addChildAt(star, 2);
backgroundStars.push(star);
}
// Tap detection for lanes
game.down = function (x, y, obj) {
// Don't process taps during tutorial
if (tutorialActive) {
return;
}
// Determine which lane was tapped
var tappedLane = -1;
for (var i = 0; i < 3; i++) {
if (Math.abs(x - laneXPositions[i]) < 150) {
tappedLane = i;
break;
}
}
if (tappedLane >= 0) {
hitNote(tappedLane);
}
};
// Keyboard controls for mapped keys, strictly mapped to lanes and supporting remap
LK.on('keyDown', function (event) {
// Don't process keyboard during tutorial
if (tutorialActive) {
return;
}
var pressedKey = event.key ? event.key.toUpperCase() : '';
var lane = -1;
for (var i = 0; i < 3; i++) {
if (keyLabels[i].toUpperCase() === pressedKey) {
lane = i;
break;
}
}
if (lane >= 0 && lane < 3) {
// Only allow hit if there is a note in this lane in the hit zone
var found = false;
for (var j = 0; j < fallingNotes.length; j++) {
var note = fallingNotes[j];
if (note.lane === lane && !note.hasBeenHit) {
var distance = Math.abs(note.y - TARGET_Y);
if (distance < HIT_ZONE) {
found = true;
break;
}
}
}
if (found) {
hitNote(lane);
// Visual feedback for key press
if (targetCircles[lane]) {
targetCircles[lane].glow();
}
} else {
// Incorrect timing or no note in this lane: shake and flash red
if (keyLabelObjects[lane]) {
var originalTint = keyLabelObjects[lane].tint;
keyLabelObjects[lane].tint = 0xFF0000; // Red for error
var originalX = keyLabelObjects[lane].x;
var shakeDistance = 8;
tween(keyLabelObjects[lane], {
x: originalX - shakeDistance
}, {
duration: 60,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
x: originalX + shakeDistance
}, {
duration: 60,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[lane], {
x: originalX,
tint: originalTint
}, {
duration: 60,
easing: tween.easeOut
});
}
});
}
});
}
}
}
});
game.update = function () {
// Don't update game while tutorial is active
if (tutorialActive) {
return;
}
// Music-synced spawn: use music time if available, else fallback to timer
noteSpawnTimer++;
if (noteSpawnTimer >= noteSpawnInterval) {
spawnNote();
noteSpawnTimer = 0;
// Progressive speed increase: decrease interval as score/combo rises
if (accuracy >= 85 && combo >= 10) {
if (noteSpawnInterval > 12) {
noteSpawnInterval -= 0.25;
}
} else if (accuracy < 60 && combo === 0) {
if (noteSpawnInterval < 80) {
noteSpawnInterval += 0.5;
}
} else {
if (noteSpawnInterval > 18) {
noteSpawnInterval -= 0.07;
}
}
// Ball speed logic removed - this is now a rhythm game without a player ball
}
// Update animated background stars
for (var s = 0; s < backgroundStars.length; s++) {
var star = backgroundStars[s];
star.y += star.speed;
star.alpha = 0.1 + Math.sin(LK.ticks * star.twinkleSpeed) * 0.2;
if (star.y > 2832) {
star.y = -100;
star.x = Math.random() * 2048;
}
}
// Update falling notes (now moving horizontally)
for (var i = fallingNotes.length - 1; i >= 0; i--) {
var note = fallingNotes[i];
// Check if note missed (passed target vertically)
if (note.lastY <= TARGET_Y + HIT_ZONE && note.y > TARGET_Y + HIT_ZONE && !note.hasBeenHit) {
// Note was missed
combo = 0;
totalNotes++;
missedNotes++; // Increment miss counter
accuracy = Math.round(hitNotes / totalNotes * 100);
comboText.setText('0');
comboCounterText.setText('0');
comboText.tint = 0xFFAA00; // Reset combo color to default
accuracyText.setText(accuracy + '%');
// Update accuracy color
if (accuracy >= 95) accuracyText.tint = 0x00FF00;else if (accuracy >= 80) accuracyText.tint = 0xFFFF00;else if (accuracy >= 60) accuracyText.tint = 0xFF8800;else accuracyText.tint = 0xFF0000;
LK.getSound('miss').play();
var langKeys = ['EN', 'MT', 'ES', 'DE', 'IT', 'FR', 'TR'];
var currentLang = langKeys[selectedLang] || 'EN';
var missText = getPooledFloatingText(tutorialTexts[currentLang].miss, '#ff0000');
missText.x = laneXPositions[note.lane];
missText.y = TARGET_Y - 100;
// Check for game over condition
if (missedNotes >= MAX_MISSES) {
LK.showGameOver();
return;
}
// Animate key label miss effect for passed notes
var missedLane = note.lane;
if (keyLabelObjects[missedLane]) {
// Desaturate by changing tint to gray
var originalTint = keyLabelObjects[missedLane].tint;
keyLabelObjects[missedLane].tint = 0x888888; // Gray for desaturation
var originalX = keyLabelObjects[missedLane].x;
// Shake effect - move left and right
var shakeDistance = 8;
tween(keyLabelObjects[missedLane], {
x: originalX - shakeDistance
}, {
duration: 75,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[missedLane], {
x: originalX + shakeDistance
}, {
duration: 75,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(keyLabelObjects[missedLane], {
x: originalX,
tint: originalTint // Restore original color
}, {
duration: 75,
easing: tween.easeOut
});
}
});
}
});
}
// Flash lane red for missed note
var laneFlash = LK.getAsset('laneDivider', {
anchorX: 0.5,
anchorY: 0.5,
x: laneXPositions[note.lane],
y: 1366,
scaleX: 0.8,
scaleY: 2732 / 4,
tint: 0xFF0000,
alpha: 0.3
});
game.addChild(laneFlash);
// Animate miss flash fade out
tween(laneFlash, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
if (laneFlash.parent) laneFlash.parent.removeChild(laneFlash);
}
});
}
// Remove notes that are off screen (bottom now) - optimized cleanup threshold
if (note.y > CLEANUP_THRESHOLD) {
note.destroy();
fallingNotes.splice(i, 1);
}
}
// Music-synced pulse: every 60 frames, pulse the ball and target
if (LK.ticks % 60 === 0) {
musicBeatCounter++;
musicIntensity = 0.5 + Math.sin(musicBeatCounter * 0.1) * 0.5;
// Pulse lane borders with beat
for (var lb = 0; lb < laneBorders.length; lb++) {
tween(laneBorders[lb], {
alpha: 0.6 * musicIntensity,
scaleX: 0.8
}, {
duration: 100,
onFinish: function (border) {
return function () {
tween(border, {
alpha: 0.3,
scaleX: 0.5
}, {
duration: 300
});
};
}(laneBorders[lb])
});
}
// Pulse hit circles with music beat
for (var i = 0; i < targetCircles.length; i++) {
tween(targetCircles[i], {
scaleX: 1.1,
scaleY: 1.1
}, {
duration: 80,
onFinish: function (tc) {
return function () {
tween(tc, {
scaleX: 1,
scaleY: 1
}, {
duration: 80
});
};
}(targetCircles[i])
});
}
}
// Update animated background elements
for (var e = 0; e < bgElements.length; e++) {
bgElements[e].y -= bgElements[e].speed;
bgElements[e].rotation += 0.01;
bgElements[e].alpha = 0.1 + musicIntensity * 0.1;
if (bgElements[e].y < -100) {
bgElements[e].y = 2832;
bgElements[e].x = Math.random() * 2048;
}
}
// Pulse light beams with music
if (LK.ticks % 30 === 0 && bgStars.length > 0) {
var beamIndex = Math.floor(Math.random() * bgStars.length);
tween(bgStars[beamIndex], {
alpha: 0.2 * musicIntensity,
scaleX: 0.5
}, {
duration: 200,
onFinish: function onFinish() {
tween(bgStars[beamIndex], {
alpha: 0.05,
scaleX: 0.2
}, {
duration: 400
});
}
});
}
// Add combo streak particles
if (combo > 0 && combo % 5 === 0) {
for (var cp = 0; cp < 3; cp++) {
var particle = LK.getAsset('noteLeft', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.3,
scaleY: 0.3
});
particle.tint = [0x00bfff, 0xff33cc, 0xffe066][cp];
particle.x = laneXPositions[cp];
particle.y = TARGET_Y;
particle.alpha = 0.8;
game.addChild(particle);
tween(particle, {
y: particle.y - 200,
alpha: 0,
scaleX: 0.05,
scaleY: 0.05
}, {
duration: 800,
onFinish: function onFinish() {
if (particle.parent) particle.parent.removeChild(particle);
}
});
}
}
// Update floating texts with batch cleanup
var textsToRemove = [];
for (var i = floatingTexts.length - 1; i >= 0; i--) {
var text = floatingTexts[i];
if (text.life <= 0) {
textsToRemove.push(text);
}
}
// Batch remove expired floating texts to reduce array operations
for (var j = 0; j < textsToRemove.length; j++) {
releaseFloatingText(textsToRemove[j]);
}
// Instructions removed - no longer shown after tutorial
// Achievement milestones
if (!window.achievements) window.achievements = {};
if (score >= 100 && !window.achievements['century']) {
window.achievements['century'] = true;
var achievementText = getPooledFloatingText('★ CENTURY SCORE! ★', '#FFD700');
achievementText.x = 1024;
achievementText.y = 600;
tween(achievementText, {
scaleX: 1.5,
scaleY: 1.5
}, {
duration: 300,
easing: tween.elasticOut
});
}
if (combo >= 50 && !window.achievements['combo50']) {
window.achievements['combo50'] = true;
var achievementText2 = getPooledFloatingText('★ 50 COMBO MASTER! ★', '#FF00FF');
achievementText2.x = 1024;
achievementText2.y = 700;
}
};
sarı 1 sekizlik nota arka planı olmasın sade olsun 2d. In-Game asset. 2d. High contrast. No shadows
beyaz 1 nota arka planı olmasın sade olsun 2d.. In-Game asset. High contrast. No shadows BEYAZ OLCAK SİYAH DEĞİL
sarı 1 üçlük nota arka planı olmasın sade olsun 2d.. In-Game asset. 2d. High contrast. No shadows