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