User prompt
Please fix the bug: 'Cannot read properties of undefined (reading '0')' in or related to this line: 'var lightBeam = LK.getAsset('laneDivider', {' Line Number: 197
User prompt
Gradient Backgrounds That React to Music Background colors shift smoothly based on the song’s energy and tempo. Glowing Lane Borders Each lane has soft neon edges that pulse in sync with the beat. Dynamic Lighting on the Ball The player ball has a glow that changes intensity and color with combos or perfect hits. Particle Trails for Movement As the ball moves, it leaves behind colored particles and sparkles that fade out smoothly. Color-Matched Impact Effects When the ball hits a correct color, a burst of light explodes briefly in matching hues. Transparent, Stylized UI Elements Score, combo, and pause buttons have minimalistic, semi-transparent designs that don't distract. Soft Camera Shake on Perfects Subtle screen shake adds excitement during high combo streaks or perfect matches. Animated Background Elements Floating stars, light beams, or moving shapes in the background add depth and motion. Sound-Reactive Lane Patterns The road itself flashes or waves slightly based on bass hits or melody lines. Custom Skins with Visual Flair Unlockable ball skins with trails, glitter, or effects (e.g. fire, electric, rainbow themes). ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Swipe or Tap to Switch Lanes Player can switch between 3 horizontal lanes using left/right swipes or A–D keys. Ball Rolls Automatically Forward The player controls only left/right movement; the ball moves forward at a steady rhythm. Match Colors to Score Points Hit objects that match the ball's current color to gain points. Hitting a different color ends the streak or causes a fail. Color Change Gates The ball changes color when passing through glowing gates, forcing the player to adapt. Music-Synced Obstacles Items, obstacles, and movement patterns sync with the beat of the background music. Progressive Speed Increase The game gradually becomes faster, increasing difficulty as the song progresses. Combo & Score System Consecutive correct hits increase combo and multiplier for high scores. Vibrant Visual Effects Trails, sparkles, and pulses follow the ball and obstacles in sync with music. Obstacle Avoidance Players must avoid falling off, hitting walls, or clashing with wrong-colored blocks. Unlockable Skins & Tracks Players can unlock new songs, backgrounds, and ball skins as they progress.
User prompt
chane the circles
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'createElement')' in or related to this line: 'var canvas = document.createElement('canvas');' Line Number: 116
User prompt
change the design
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'fill')' in or related to this line: 'textObj.style.fill = color;' Line Number: 131
User prompt
Performance & Optimization Batch canvas draws. Combine lane lines, static UI, and background into a cached layer that is redrawn only when the window resizes, cutting per-frame draw calls in half. Reuse objects. Keep hit-line and particle objects in a pool; recycle them instead of creating new ones every hit to reduce garbage collection spikes. Cap note queue. Spawn notes only a few seconds before they’re visible; free them as soon as they pass the hit window to keep memory usage flat. Throttle animation math. Use requestAnimationFrame for visuals and a fixed-timestep loop for game logic so physics stays stable on both 60 Hz and 144 Hz monitors. Lazy-load audio. Decode each song on first play and store in a weak map; unload when not used for 5 minutes to keep RAM footprint low.
User prompt
Performance & Optimization Batch canvas draws. Combine lane lines, static UI, and background into a cached layer that is redrawn only when the window resizes, cutting per-frame draw calls in half. Reuse objects. Keep hit-line and particle objects in a pool; recycle them instead of creating new ones every hit to reduce garbage collection spikes. Cap note queue. Spawn notes only a few seconds before they’re visible; free them as soon as they pass the hit window to keep memory usage flat. Throttle animation math. Use requestAnimationFrame for visuals and a fixed-timestep loop for game logic so physics stays stable on both 60 Hz and 144 Hz monitors. Lazy-load audio. Decode each song on first play and store in a weak map; unload when not used for 5 minutes to keep RAM footprint low.
User prompt
Key-Mapping Accuracy Lock each key to its lane. A → Top lane (leftmost letter on the staff). S → Middle lane. D → Bottom lane. Reject stray input. A press on A should never activate an S or D note even if the timing is perfect; filter by lane first, then judge timing. Visual confirmation. When the player hits the correct key on time, flash the matching lane letter and draw the beam only on that lane. Incorrect-lane presses should trigger a red shake on the letter and award no points. Key-remap safety. If the player remaps keys, prevent duplicates and instantly update the on-screen letters so muscle memory matches what they see. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
🎮 Patch Notes – Gameplay & Visual Update (v 1.1) (English, bullet-point format as requested) Target Zone Relocation The hit circles are no longer on the right edge of each lane; they now sit along the bottom edge of the screen, one beneath each lane. Notes travel downward toward these circles, aligning better with natural eye movement and freeing lateral space for future UI elements. Brighter Background Palette Replaced the dark gray/black backdrop with a light, low-saturation gradient (soft teal → mist blue). Increased overall contrast so lane colors (Cyan, Magenta, Yellow) remain vivid without straining the eyes. Score & Combo HUD Repositioning Moved the scoreboard to the upper-middle of the screen (center-top) for immediate visibility. Enlarged font size by 15 % and added a subtle drop shadow to keep text legible against the new lighter background. Key Mapping Verification A → Top lane, S → Middle lane, D → Bottom lane confirmed and locked; remap screen now prevents duplicate assignments. Each letter label glows only when its own note is pressed at the exact moment it reaches the circle—mis-timed presses no longer trigger adjacent lane feedback. Input Feedback Refinement Perfect hits generate a solid beam straight upward from the circle; Good hits use a dotted beam; Misses flash a short red spark. Beam length auto-scales to fit the new vertical lane layout. Background Music & Note Sync Recalculated note spawn timing to compensate for vertical travel distance so every note still arrives precisely on beat. Accessibility Tweaks Added a high-contrast “Day Mode” toggle: swaps the pastel gradient for a near-white backdrop and thickens lane outlines. All color cues are now paired with unique shapes (▲ ■ ●) for color-blind players. Minor Fixes Resolved a bug where combo counter reset visually but not internally after three consecutive misses. Optimized canvas redraw to maintain 60 FPS on lower-end devices. Summary: Targets are now at the bottom, the game’s look is brighter, the score sits centre-top, and key–lane behavior is airtight. Enjoy clearer visuals and more intuitive timing! ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
fix the code
User prompt
🔤 Letters A, S, D – Placement and Visual Design in the Game 📍 Where the Letters Appear The game screen has three horizontal lanes (top, middle, bottom). At the far right end of each lane is a target circle where players must hit notes. Directly above each target circle, place a large, glowing letter indicating the key to press: Lane Letter Keyboard Key Top lane A A key Middle lane S S key Bottom lane D D key ✨ Visual Style for the Letters Letters should be big and bold, so players can easily see them at a glance. Each letter is colored to match its lane’s theme color: A (top lane): Cyan / Turquoise S (middle lane): Magenta / Pink D (bottom lane): Yellow Example layout on screen: mathematica Kopyala Düzenle ───────────── ← Top lane O ● ← Target circle A ← Big glowing letter (Cyan) ───────────── ← Middle lane O ● S ← Big glowing letter (Magenta) ───────────── ← Bottom lane O ● D ← Big glowing letter (Yellow) 💡 Animations for the Letters When the player presses the correct key at the right time: The letter glows brighter (100% opacity). It grows slightly bigger (about 120% size) for 1 second, then gently returns to normal size. When the player presses incorrectly or misses timing: The letter briefly turns red. It shakes left and right (~3-4 pixels) for about 0.2 seconds to give negative feedback. 🎮 Why Use These Letters? The letters serve as clear reminders of which key controls each lane. They help new players instantly understand the controls without needing extra instructions. Because the letters are always visible near the targets, players develop muscle memory quickly. This improves gameplay flow and reduces confusion. 🔄 Optional Accessibility Feature For players with color vision difficulties, you can add a shape mode where each lane’s letter is replaced by a unique shape (e.g., ▲ ■ ●) along with color coding. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
🎮 Rhythm Game Full Redesign & Update Brief Title: Core Gameplay & Visual Update v1.0.0 Goal: Improve clarity, feedback, and user experience across all areas of the game — from layout to controls and visuals. 🔁 1. Gameplay Core Overhaul Redesign the gameplay to follow a 3-lane horizontal layout (Top, Middle, Bottom). Notes will travel from left to right along each lane and must be hit when they reach the target circle on the far right. 🎯 2. Note Hit System Replace all radial "hit" animations with a horizontal beam line that fires across the lane when the player hits the correct key: Perfect hit → solid glowing line, full lane color. Good hit → dotted line, 70% opacity. Miss → short red spark with small screen shake. Timing windows: Perfect = ±40ms Good = ±100ms Miss = anything beyond 100ms 🎮 3. Controls & Key Bindings Assign clear keyboard mappings: A → Top lane S → Middle lane D → Bottom lane Display large glowing letters (A, S, D) right above each lane’s target circle. Letters should animate slightly when their key is pressed correctly. Default layout should support remapping later (optional). 📊 4. Score, Combo, Accuracy HUD Create a single central HUD bar at the top-center of the screen showing: Total Score Current Combo Accuracy grade (Perfect, Good, Miss) HUD color and text glow increases as the combo rises: Normal: light gray 10 Combo: blue glow 50 Combo: golden glow 100 Combo: pulse + confetti effect 💥 5. Visual Feedback When a key is hit: The circle target glows briefly (white for Perfect, soft color for Good). A smooth, satisfying line beam shoots out to the left. Score pops visually with "+3", "+1", or "-". If the player misses: The entire lane flashes red for 0.2s. Combo resets to zero. Combo text shatters visually. 🧠 6. Difficulty Dynamics (Optional) Increase note speed by +3% every 10 Perfect hits (max +20%) Decrease speed by -3% after 3 Misses (minimum base speed) ⚙️ 7. Customization & Accessibility Allow key remapping in settings. Offer color-blind mode (shapes instead of color: ▲ ■ ●) Add volume sliders for: Background Music Hit Sound FX Miss Sound FX Optional metronome bar at the top of the screen to help new players learn timing. ✅ 8. General Polish Smooth UI animations (score rolls, combo pulses, hit spark fades) Smoother note movement synced to BPM Song and level selection screen (for future expansion) Tutorial mode: short guide explaining controls, timing, and scoring system 🎵 This update will make the game feel smooth, rewarding, responsive, and visually clean. It's a full rebuild of the core loop — focused on clarity, timing, fun, and satisfying feedback. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
FİX THE GAME ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'dropShadow')' in or related to this line: 'keyLabel.style.dropShadow = true;' Line Number: 171
User prompt
1. HUD & Performance Display Placement Pin a unified performance bar (Score | Combo | Accuracy) dead-centre along the very top edge of the screen. Width ≈ 40 % of viewport; height ≈ 8 % of viewport, so it dominates peripheral vision but never overlaps the falling notes. Typography Typeface: bold, rounded sans-serif for legibility. Dynamic colouring: Base state: soft white text on 40 % opaque charcoal panel. At every 10-hit combo the panel’s outline brightens; at 50-, 100-, 200-hit milestones the entire bar shifts to gold for 1 s. Count-up animation for Score (rolling odometer) and spring-scale animation (+10 % size) for Combo increases. 2. Definitive Key-to-Lane System (the “Letter Job”) 2.1 Key Allocation Lane Keyboard Key Rationale Left A Far-left key under resting index finger. Middle S Centred, natural pivot. Right D Far-right of three; easy reach. 2.2 On-Screen Glyph Anchors Oversized Glyphs Above Targets Directly above each bottom-lane circle, render an extra-large capital letter (A, S, D) at ~ 6 % of viewport height. Stroke weight: 4 px; fill uses the lane’s primary colour (Left = Cyan, Middle = Magenta, Right = Yellow). Add a subtle drop-shadow for readability on bright backgrounds. Glow & Motion Letter glyph glows gently at all times (opacity 50 %). When its lane registers a Perfect hit, the glyph briefly blazes to 100 % opacity and scales to 120 % before easing back. On a Miss, glyph desaturates and shakes horizontally 4 px for 0.15 s to reinforce “wrong timing.” Mobile / Touch Variant Replace the glyph with a translucent “tap pad” retaining the letter label in the centre. Pad diameter equals the target circle’s diameter × 1.3, ensuring easy thumb hits. 3. Lane Architecture & Note Behaviour 3.1 Lane Layout Three horizontal tracks stacked vertically (not vertical columns) to mimic a musical staff—top, middle, bottom—each separated by 8 % of viewport height. Notes slide left → right (or right → left, choose one; maintain consistency) along these tracks. 3.2 Note Entities Sprites: stylised musical notes matching lane colour. Each note carries a timing window: Perfect = within ±40 ms of beat arrival at centre of target circle. Good = ±100 ms. 4. “Line Hit” Feedback – replacing radial flashes When the player presses the key as a note overlaps its target circle: Impact Line (Signature Effect) Spawn a thin light-beam line that shoots horizontally from the circle’s centre across the full width of the lane. Perfect hit → line is solid, 2 px thick, full lane colour, travels at 900 px/s, fades in 0.4 s. Good hit → dashed line, 1 px thick, 70 % opacity, same fade. Miss → a short (25 %) red dash fires then fizzles in 0.2 s, indicating failure. Circle Pulse The bottom target circle shrinks to 80 % size on contact, then overshoots to 110 % and settles, giving a tactile “press-in” illusion. Sound Layer Perfect hit plays a crisp hi-hat click layered atop song. Good hit plays a softer rim-shot. Miss triggers a muted low-pass thud. This “impact line” reinforces rhythm flow visually, echoing the classic lane-strike aesthetic of arcade rhythm games while differentiating Perfect vs Good vs Miss states at a glance. 5. Combo & Difficulty Dynamics Combo Meter Numeric value sits beneath the main HUD bar, centred, sized at 4 % viewport height. Every 10 Perfects: combo text colour cycles through a hue wheel (cyan → lime → amber → pink). Adaptive Speed Rolling accuracy ≥ 85 % for 8 consecutive beats: escalate note velocity +3 % (cumulative to +20 %). Accuracy ≤ 60 % for 8 consecutive beats: reduce velocity –3 % (floors at base speed). Pattern Variation High combo segments unlock syncopated double-taps and sustained “hold” notes (lines the player must hold the key for across two beats). After 3 consecutive Misses, inject a one-measure rest to prevent overwhelm. 6. Accessibility & Customisation Full Key Remap Screen Left/Mid/Right lanes selectable; each listens for user keystroke to assign. Warn on duplicate assignment; allow gamepad or MIDI input. Colour-Blind Modes Presets: Deuteranopia, Protanopia, Tritanopia. Alternate mode swaps lane colours for unique glyph shapes (▲, ■, ●) while retaining original palette. Audio Assist Independent sliders: Master Music Hit Sounds Miss Sounds Visual Beat Guide toggle: faint metronome bar flashes at top of screen for players needing extra timing help. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
1. Center-Top Score, Combo & Accuracy HUD Place all performance feedback—current score, active combo, and real-time accuracy grade—together in a single HUD bar anchored to the exact horizontal midpoint of the screen and flush with the upper edge. Designing it this way keeps the player’s eyes on the action while making key statistics impossible to miss. Layout & Sizing Use a wide, translucent backing panel that spans roughly 40 % of screen width. Inside the panel, arrange information left-to-right: Score ➜ Combo ➜ Accuracy. Give each metric a large, semibold font (at least 6 % of viewport height) and reserve a generous 1 % gap between them so the text never feels cramped, even on narrow mobile devices. Styling & Readability Employ a neutral base color (soft white or light gray) and add a gentle outer glow that intensifies whenever the combo climbs by another 10 hits; this passive pulse advertises momentum without stealing focus from falling notes. When a player crosses major combo milestones—50, 100, 200—briefly shift the panel’s accent color to gold and show a short-lived radial burst behind the numbers to celebrate the achievement. Dynamic Updates Score increments should animate smoothly (rolling digits) to reinforce reward. Combo text should scale up 10 % and fade back whenever it increases, while a miss forces the combo value to snap to zero in red, then fade back to default color. 2. Explicit Key-to-Lane Mapping Tie each of the three vertical lanes definitively to a single keyboard button and reinforce that mapping visually from the very first frame of gameplay: Lane Key Visual Cue Left A Tiny “A” glyph sits just above the bottom-lane target circle, rendered in the same lane color. Middle S Center lane label “S” follows the same styling. Right D Right lane label “D”, consistent styling. Rationale: Clear, ever-present labels prevent onboarding friction. Even if a player walks away and returns days later, the mapping is instantly recalled because it was never hidden. Color Harmony: Pick a distinct hue for each lane (e.g., cyan-pink-yellow), and recolor the key glyph in that same hue so brain + muscle memory link them instinctively. Mobile Adaptation: For touch devices, replace glyphs with semitransparent tappable pads over each bottom target circle—still colored and labeled, just finger-friendly. 3. Feedback & Reward Loops Successful Hit Lane Flash: Ring surrounding the bottom circle emits a quick, 0.2-second radial flash whose brightness depends on timing quality (white = Perfect, pastel = Good). Sound Stinger: Overlay a short “ding” sample, pitched higher for Perfect, lower for Good, layered atop the existing song so it feels musical. Particle Spark: Emit 8-12 micro-particles that accelerate upward, reinforcing the notion of energy release. Missed Hit Lane Tint: Entire lane momentarily washes red, desaturating back to normal over 0.3 s. Thud FX: Play a low-passed, muffled thump—just loud enough to convey error without becoming intrusive. Combo Break: Combo counter instantly resets; a glass-shatter animation overlays the combo number to drive the lesson home. Combo Meter Flair Every 10 consecutive Perfects, give the HUD a subtle heartbeat pulse and flash a mini-banner “STREAK x10!” just beneath the combo value. Beyond 50-hit streaks, stack passive buffs (e.g., slight score multiplier) to motivate precision play. 4. Adaptive Challenge (Optional but Adds Depth) Speed Scaling Track player accuracy over a rolling 8-beat window. If ≥ 80 % are Perfect, gradually raise global note fall speed by 3 % per 8 beats until a 20 % cap is hit. If accuracy dips below 60 %, lower speed in similar increments to keep frustration low. Pattern Density On sustained high streaks (> 100), insert syncopated “double-note” bursts to surprise advanced players. Conversely, after three consecutive misses, insert a short rest measure to let newcomers reset. 5. Accessibility & Customisation Key Remapping Provide a simple settings toggle: Left Lane Key, Middle Lane Key, Right Lane Key. Defaults are A / S / D, but players can choose alternatives (e.g., J / K / L) or even map to gamepad buttons. Color-Blind Assist The lane/target circles gain unique shapes (triangle, square, circle) in addition to color. Allow a high-contrast mode that darkens the background and brightens note lanes. Volume & Beat Assist Separate sliders for music, hit sounds, and miss sounds so players with sensory sensitivity can tune experience. Summary By centralizing the score and combo indicators at the top, mapping lanes to A / S / D, and layering rich audiovisual feedback loops, this design maximizes clarity and satisfaction. Optional adaptive difficulty and robust accessibility settings ensure the game scales gracefully from casual newcomers to hardcore rhythm enthusiasts, all without relying on camera input. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Please fix the bug: 'TypeError: Cannot set properties of undefined (setting 'fill')' in or related to this line: 'comboText.style.fill = 0xFFAA00; // Reset combo color to default' Line Number: 362
User prompt
Center-Top Score & Combo Display Position the live score, combo counter, and accuracy grade centrally at the very top of the screen. Use a large, legible font with subtle glow so it’s always in the player’s eye-line without obscuring falling notes. Key-to-Lane Mapping Left lane → A key Middle lane → S key Right lane → D key Show faint key labels just above each bottom-lane circle so new players instantly know which key matches which lane. Visual & Audio Feedback Enhancements (optional but recommended) When a note is hit, flash the corresponding lane border and play a short success sound (different pitch for “Perfect” vs. “Good”). Missed notes briefly tint the lane red and play a muted thud to reinforce timing cues. Combo Meter Styling As the combo rises, gradually change the score text color (e.g., from white → gold) and add a gentle pulsing animation every 10-hit milestone. At major combo thresholds (50, 100, 200) trigger a screen-wide particle burst to reward sustained accuracy. Dynamic Difficulty (optional) If the player maintains a high combo, slightly speed up note fall rate to keep challenge engaging; slow it down after multiple misses. Accessibility Options Include a settings toggle to remap keys (e.g., J, K, L for right-hand play). Offer a color-blind mode that swaps note/lanes to distinct shapes rather than relying solely on color. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
i want change
User prompt
Beat Drop
Initial prompt
Create a 2D rhythm game with the following core structure: 🎵 Gameplay Mechanics: The game screen is divided vertically into 3 horizontal lines (lanes). Notes (musical symbols or dots) fall from the top of the screen down along each lane. At the bottom of each lane, there is a circle or target area where the player is supposed to hit the note. ⌨️ Player Input: Each lane is mapped to a specific key (for example: Left lane → A Middle lane → S Right lane → D) When a note reaches the circular zone at the bottom of a lane, the player must press the corresponding key exactly as the note enters the circle. If timed correctly, the player gains points. Perfect timing = +3 points Good timing = +1 point Miss = 0 / possible penalty 🌈 Visuals and Feedback: The falling notes are color-coded or shaped like musical notes. When the player hits the note correctly inside the circle: The circle briefly glows. A score indicator appears (like “Perfect!”, “Good!”). A sound feedback (e.g., chime or beat) is played. 🎶 Music Sync: The falling notes must be synchronized to the beat of a background music track. Each note corresponds to a drum kick, melody, or sound layer. The tempo of the falling notes changes based on the song speed. 📊 Scoring System: Combo counter increases with consecutive correct hits. Score is shown live on the screen. Optional: health bar that drains with missed notes and refills with accurate hits. ⚙️ Other Requirements: The game must not require a camera. Fully playable with keyboard only (or touch for mobile version). Must include an easy way to upload or use different songs to generate note patterns. Build a smooth, responsive rhythm game that rewards precise timing and syncs beautifully with music. Simple, fast-paced, and satisfying to play.
/**** * 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