User prompt
Increase hold particle size
User prompt
Give notes a scale pulse to the beat after they reach full scale from scaling in. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add a slight sine wave to stars upwards travel. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make it faster. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make the star particle alpha pulse faster ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Add a slight alpha pulse to the stars to simulate twinkling. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Star background should be moving already when the game starts.
User prompt
Increase their size by another 30%
User prompt
Increase background particle average size by 20%
User prompt
Background particles should be different sizes and speeds and travel upwards to make a moving star field effect.
User prompt
Make background particles that create a multicolored neon star field effect with different sized parallax. Traveling from bottom to top.
Code edit (1 edits merged)
Please save this source code
User prompt
Currently all tap notes are showing up as red, make sure left zone notes are blue.
User prompt
Left zone notes should be blue, not red.
User prompt
Zone notes will be color coded now. Left zone Blue, right zone red. Synced tap notes will be pink and synced hold notes will be purple.
User prompt
Remove tap and hold pattern type. Notes should not spawn beside hold notes other than sync hold notes.
User prompt
Add more space between tap notes in complex song patterns.
User prompt
Reduce amount of hold notes.
User prompt
Change note hit detection to use the entire size of the note core (good) and to award more points (perfect) for the center portion. Add large pop up text to inform the player of their accuracy.
User prompt
Add a 5% safety zone on the top and sides and a 10% safety zone on the bottom of the screen where notes do not spawn. Also take this into consideration for hold note ends.
User prompt
Synced and synced hold notes should be the same color
User prompt
Remove the win scenario.
User prompt
Reduce note density in dense patterns and lean more towards syncing notes and holds between sides overall
User prompt
Make the length between the start and end of hold notes shorter overall and make sure that the placement of the starting note does not place the end off screen.
User prompt
Make the space between hold notes shorter overall and make sure the end does not go off the screen. Smarter placement of hold note starts.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var Note = Container.expand(function (noteData, spawnTime) { var self = Container.call(this); // Note properties self.noteData = noteData; self.type = noteData.type; // 'tap', 'hold', 'flick' self.targetX = noteData.x; self.targetY = noteData.y; self.hitTime = noteData.time; self.duration = noteData.duration || 0; // For hold notes // self.flickTarget = noteData.flickTarget || null; // Removed self.spawnTime = spawnTime; self.zone = noteData.x < GAME_WIDTH / 2 ? 'left' : 'right'; // Determine zone // Determine note color based on type and zone, and if it's a sync note if (noteData.color && noteData.color === SYNC_NOTE_COLOR) { // This note was flagged as a sync note by SongGenerator if (self.type === 'tap') { self.color = COLOR_SYNC_TAP; } else if (self.type === 'hold') { self.color = COLOR_SYNC_HOLD; } else { // Fallback for unexpected sync note types (should not occur with current patterns) self.color = self.zone === 'left' ? COLOR_LEFT_ZONE : COLOR_RIGHT_ZONE; } } else { // Standard note, color by zone self.color = self.zone === 'left' ? COLOR_LEFT_ZONE : COLOR_RIGHT_ZONE; } // self.requiredFlickDirection = noteData.flickDirection || null; // Removed // State self.active = true; self.isHit = false; self.isMissed = false; self.isSpawning = true; self.isHolding = false; // True if player is currently holding this note self.holdSuccessfullyCompleted = false; // True if hold was completed successfully self.holdParticles = []; // Stores active connection particles for this hold note self.lastParticleEmissionTime = 0; // For throttling particle emission self.PARTICLE_EMISSION_INTERVAL = 50; // Emit particles every 50ms during hold // self.isHolding = false; // For hold notes // Duplicate line, already declared above self.holdStarted = false; // Visual components var assetName = self.type === 'hold' ? 'holdNoteCore' : 'noteCore'; // Removed flickNoteCore self.noteGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); self.noteGraphics.tint = self.color; // self.flickDirectionIndicator logic removed self.glowRing = self.attachAsset('glowRing', { anchorX: 0.5, anchorY: 0.5 }); self.glowRing.tint = self.color; self.glowRing.alpha = 0.3; // Position self.x = self.targetX; self.y = self.targetY; // Start invisible and scale up self.alpha = 0; self.scaleX = 0.1; self.scaleY = 0.1; // Define createHoldTrail method first self.createHoldTrail = function () { // Determine scanner direction at hit time var cycleTime = self.hitTime % SCANNER_CYCLE_DURATION; var scannerMovingUp = cycleTime < SCANNER_HALF_CYCLE; var direction = scannerMovingUp ? -1 : 1; // -1 for up, 1 for down // Calculate initial trail length based on original duration var originalRequestedDuration = self.duration; var maxPossibleTrailPixelLength = originalRequestedDuration / SCANNER_HALF_CYCLE * PLAY_AREA_HEIGHT; var actualTrailPixelLength = maxPossibleTrailPixelLength; // Cap trail length to stay within SCANNER_Y_MIN and SCANNER_Y_MAX if (direction === -1) { // Trail goes upwards from the note // End note Y would be self.y - actualTrailPixelLength. It must be >= SCANNER_Y_MIN. if (self.y - actualTrailPixelLength < SCANNER_Y_MIN) { actualTrailPixelLength = self.y - SCANNER_Y_MIN; } } else { // Trail goes downwards from the note // End note Y would be self.y + actualTrailPixelLength. It must be <= SCANNER_Y_MAX. if (self.y + actualTrailPixelLength > SCANNER_Y_MAX) { actualTrailPixelLength = SCANNER_Y_MAX - self.y; } } actualTrailPixelLength = Math.max(0, actualTrailPixelLength); // Ensure length is not negative // If the trail length was capped, adjust self.duration to match the visual representation. // Avoid division by zero if critical constants are invalid. if (actualTrailPixelLength < maxPossibleTrailPixelLength) { if (PLAY_AREA_HEIGHT > 0 && SCANNER_HALF_CYCLE > 0) { self.duration = actualTrailPixelLength / PLAY_AREA_HEIGHT * SCANNER_HALF_CYCLE; } else if (actualTrailPixelLength === 0) { // If trail length is zero due to invalid constants or extreme capping, set duration to 0. self.duration = 0; } // else, retain original duration if recalculation is not possible but length wasn't zeroed. } self.duration = Math.max(0, self.duration); // Final check to ensure duration is non-negative. // Create the main connecting trail line var trailLine = self.attachAsset('holdTrail', { anchorX: 0.5, anchorY: 0 // Anchor at the top of the trail asset }); trailLine.tint = self.color; trailLine.alpha = 0.6; trailLine.width = 20; // Set a visible width for the trail line trailLine.height = actualTrailPixelLength; if (direction === -1) { // Trail goes upwards from the note trailLine.y = -actualTrailPixelLength; } else { // Trail goes downwards from the note trailLine.y = 0; // AnchorY is 0, so top of trailLine is at note's y } self.holdTrails.push(trailLine); // Add end indicator note, positioned relative to the start note self.endNote = self.attachAsset('noteCore', { anchorX: 0.5, anchorY: 0.5 }); self.endNote.tint = self.color; self.endNote.alpha = 0.8; self.endNote.scaleX = 0.7; self.endNote.scaleY = 0.7; self.endNote.y = actualTrailPixelLength * direction; // Add a subtle glow to the end note, positioned with the end note self.endGlow = self.attachAsset('glowRing', { anchorX: 0.5, anchorY: 0.5 }); self.endGlow.tint = self.color; self.endGlow.alpha = 0.2; self.endGlow.scaleX = 0.5; self.endGlow.scaleY = 0.5; self.endGlow.y = actualTrailPixelLength * direction; }; // Hold trail for hold notes self.holdTrails = []; if (self.type === 'hold' && self.duration > 0) { self.createHoldTrail(); } self.spawnIn = function () { self.isSpawning = true; tween(self, { alpha: 1, scaleX: 1, scaleY: 1 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { self.isSpawning = false; } }); }; self.showHitEffect = function () { // Primarily for TAP notes now LK.getSound('hitSound').play(); self.explodeIntoParticles(); // For tap notes, there's no endNote or endGlow to clean typically. // This method makes the note inactive. tween(self, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 200, onFinish: function onFinish() { self.active = false; } }); }; self.startHoldEffect = function () { // Called for HOLD note initial interaction LK.getSound('hitSound').play(); // Visual cue for hold start, e.g., a slight pulse. Note remains active. tween(self.noteGraphics, { scaleX: 1.2, scaleY: 1.2 }, { duration: 100, onFinish: function onFinish() { tween(self.noteGraphics, { scaleX: 1, scaleY: 1 }, { duration: 100 }); } }); // EndNote and EndGlow MUST remain visible. Note itself is not deactivated here. }; // self.showFlickEffect method removed. // self.createFlickTrail method is now obsolete and removed. self.showMissEffect = function () { LK.getSound('missSound').play(); // Flick direction indicator cleanup removed tween(self.noteGraphics, { tint: 0xff0000 }, { duration: 150, onFinish: function onFinish() { tween(self, { alpha: 0.3 }, { duration: 300, onFinish: function onFinish() { self.active = false; } }); } }); }; self.explodeIntoParticles = function () { // Flick direction indicator cleanup removed var particleCount = 12; for (var i = 0; i < particleCount; i++) { var particle = game.attachAsset('particle', { anchorX: 0.5, anchorY: 0.5 }); particle.tint = self.color; particle.x = self.x; particle.y = self.y; var angle = i / particleCount * Math.PI * 2; var speed = 150 + Math.random() * 100; var targetX = self.x + Math.cos(angle) * speed; var targetY = self.y + Math.sin(angle) * speed; tween(particle, { x: targetX, y: targetY, alpha: 0, scaleX: 0.3, scaleY: 0.3 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { if (particle && particle.destroy) { particle.destroy(); } } }); } }; self.updateHoldEffects = function (scannerCurrentY) { if (self.type === 'hold' && self.isHolding && self.active && !self.holdSuccessfullyCompleted && !self.isMissed) { var currentTime = Date.now(); if (currentTime - self.lastParticleEmissionTime > self.PARTICLE_EMISSION_INTERVAL) { self.lastParticleEmissionTime = currentTime; var trailStartY = self.y; var trailEndY = self.y + (self.endNote ? self.endNote.y : 0); // endNote.y is relative var scannerIsOverTrail = self.endNote && scannerCurrentY >= Math.min(trailStartY, trailEndY) && scannerCurrentY <= Math.max(trailStartY, trailEndY); if (scannerIsOverTrail) { var particle = game.attachAsset('holdConnectionParticle', { anchorX: 0.5, anchorY: 0.5 }); particle.x = self.x; particle.y = scannerCurrentY; particle.tint = self.color; particle.alpha = 0.8; self.holdParticles.push(particle); tween(particle, { alpha: 0, scaleX: 0.2, scaleY: 0.2 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { if (particle && particle.destroy) particle.destroy(); var index = self.holdParticles.indexOf(particle); if (index > -1) self.holdParticles.splice(index, 1); } }); } } } }; self.cleanupHoldVisuals = function () { self.holdParticles.forEach(function (p) { if (p && p.destroy) p.destroy(); }); self.holdParticles = []; if (self.endNote && self.endNote.destroy) self.endNote.destroy(); if (self.endGlow && self.endGlow.destroy) self.endGlow.destroy(); self.endNote = null; self.endGlow = null; self.holdTrails.forEach(function (trail) { if (trail && trail.destroy) trail.destroy(); }); self.holdTrails = []; }; self.completeHold = function () { if (self.holdSuccessfullyCompleted || self.isMissed) return; self.holdSuccessfullyCompleted = true; self.isHolding = false; self.explodeIntoParticles(); // Main note explosion self.cleanupHoldVisuals(); // Clean up trail, end note, particles // The main note (self) will become inactive due to explodeIntoParticles tween or explicitly here tween(self, { alpha: 0 }, { duration: 100, delay: 100, onFinish: function onFinish() { self.active = false; } }); }; self.failHold = function () { if (self.isMissed || self.holdSuccessfullyCompleted) return; self.isMissed = true; self.isHolding = false; self.showMissEffect(); // Main note shows miss effect self.cleanupHoldVisuals(); // Clean up trail, end note, particles // self.active will be set to false by showMissEffect's tween's onFinish }; return self; }); var Scanner = Container.expand(function () { var self = Container.call(this); self.glow = self.attachAsset('scannerGlow', { anchorX: 0, anchorY: 0.5 }); self.glow.alpha = 0.3; self.line = self.attachAsset('scannerLine', { anchorX: 0, anchorY: 0.5 }); self.x = 0; self.isMovingUp = true; // Start moving up return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x000000 }); /**** * Game Code ****/ // Game Constants var GAME_WIDTH = 2048; var GAME_HEIGHT = 2732; // Safety Zone Percentages var SAFETY_MARGIN_TOP_PERCENT = 0.05; // 5% from the top var SAFETY_MARGIN_BOTTOM_PERCENT = 0.10; // 10% from the bottom var SAFETY_MARGIN_SIDES_PERCENT = 0.05; // 5% from the sides var BPM = 120; var BEAT_DURATION_MS = 60 / BPM * 1000; var SCANNER_Y_MIN = GAME_HEIGHT * SAFETY_MARGIN_TOP_PERCENT; var SCANNER_Y_MAX = GAME_HEIGHT * (1 - SAFETY_MARGIN_BOTTOM_PERCENT); var PLAY_AREA_HEIGHT = SCANNER_Y_MAX - SCANNER_Y_MIN; // var HIT_TOLERANCE_PX = 80; // Replaced by note-size based detection var NOTE_SPAWN_AHEAD_MS = 800; // Reduced to 800ms to prevent mass spawning // New Hit Accuracy and Popup Constants var PERFECT_HIT_RATIO = 0.4; // 40% of the note core height (center portion) for "perfect" var POINTS_PERFECT_TAP = 30; var POINTS_GOOD_TAP = 15; var POINTS_PERFECT_HOLD_START = 10; // Initial points for starting a hold perfectly var POINTS_GOOD_HOLD_START = 5; // Initial points for starting a hold well var ACCURACY_POPUP_DURATION = 750; // ms for the popup to be visible var ACCURACY_POPUP_Y_OFFSET = -100; // Initial Y offset from note center for the popup var ACCURACY_POPUP_FONT_SIZE = 80; var PERFECT_POPUP_COLOR = 0x00FF00; // Green for "Perfect" var GOOD_POPUP_COLOR = 0xFFFF00; // Yellow for "Good" var SCANNER_CYCLE_DURATION = 4000; var SCANNER_HALF_CYCLE = SCANNER_CYCLE_DURATION / 2; var HIT_TOLERANCE_PX_FOR_HOLD_END = 100; // Tolerance for releasing hold note at the end var HOLD_END_GRACE_PERIOD_MS = 250; // Grace period for hold note release time vs scanner position // var MIN_FLICK_DISTANCE = 75; // Removed // Game State Variables var activeHoldNotes = {}; // Stores the currently active hold note, keyed by 'left' or 'right' zone // var activeFlickPrimer = null; // Removed var scoreTxt; var countdownTxt; var scanner; var scannerIsMovingUp = true; var gameStartTime; var gameStarted = false; var notes = []; var currentSongData; var spawnedNotes = []; // Using array instead of Set var leftZone, rightZone; var isDragging = false; var dragStartX, dragStartY; var currentTrail = []; // Note Color Definitions var COLOR_LEFT_ZONE = 0x0077FF; // Blue for left zone notes var COLOR_RIGHT_ZONE = 0xFF3333; // Red for right zone notes var COLOR_SYNC_TAP = 0xFF69B4; // Pink for synced tap notes (HotPink) var COLOR_SYNC_HOLD = 0x9370DB; // Purple for synced hold notes (MediumPurple) // SYNC_NOTE_COLOR is used by SongGenerator as a FLAG to indicate a note is part of a sync pattern. // The Note class will then apply the final COLOR_SYNC_TAP or COLOR_SYNC_HOLD. var SYNC_NOTE_COLOR = 0xFFA500; // This specific value signals a sync note to the Note class. // Song Pattern System // Pattern Templates - reusable note patterns var PatternTemplates = { // Basic patterns tapLeft: function tapLeft(startTime) { return [{ time: startTime, type: 'tap', zone: 'left' }]; }, tapRight: function tapRight(startTime) { return [{ time: startTime, type: 'tap', zone: 'right' }]; }, syncTap: function syncTap(startTime) { return [{ time: startTime, type: 'tap', zone: 'left' }, { time: startTime, type: 'tap', zone: 'right' }]; }, // Alternating patterns leftRightTaps: function leftRightTaps(startTime, spacing) { spacing = spacing || 500; // Default to beat spacing return [{ time: startTime, type: 'tap', zone: 'left' }, { time: startTime + spacing, type: 'tap', zone: 'right' }]; }, rightLeftTaps: function rightLeftTaps(startTime, spacing) { spacing = spacing || 500; return [{ time: startTime, type: 'tap', zone: 'right' }, { time: startTime + spacing, type: 'tap', zone: 'left' }]; }, // Hold patterns holdLeft: function holdLeft(startTime, duration) { duration = duration || 1000; return [{ time: startTime, type: 'hold', zone: 'left', duration: duration }]; }, holdRight: function holdRight(startTime, duration) { duration = duration || 1000; return [{ time: startTime, type: 'hold', zone: 'right', duration: duration }]; }, syncHold: function syncHold(startTime, duration) { duration = duration || 1000; return [{ time: startTime, type: 'hold', zone: 'left', duration: duration }, { time: startTime, type: 'hold', zone: 'right', duration: duration }]; }, // Complex patterns tripletTaps: function tripletTaps(startTime, zone, spacing) { spacing = spacing || 167; // Triplet timing return [{ time: startTime, type: 'tap', zone: zone }, { time: startTime + spacing, type: 'tap', zone: zone }, { time: startTime + spacing * 2, type: 'tap', zone: zone }]; }, alternatingTriplets: function alternatingTriplets(startTime, spacing) { spacing = spacing || 167; return [{ time: startTime, type: 'tap', zone: 'left' }, { time: startTime + spacing, type: 'tap', zone: 'right' }, { time: startTime + spacing * 2, type: 'tap', zone: 'left' }]; }, buildUp: function buildUp(startTime) { return [{ time: startTime, type: 'tap', zone: 'left' }, { time: startTime + 250, type: 'tap', zone: 'right' }, { time: startTime + 500, type: 'tap', zone: 'left' }, { time: startTime + 625, type: 'tap', zone: 'right' }, { time: startTime + 750, type: 'tap', zone: 'left' }, { time: startTime + 875, type: 'tap', zone: 'right' }]; } }; var SongGenerator = { generateSong: function generateSong(config) { var notes = []; var totalLength = config.totalLength || 202136; var startTime = config.startDelay || 2000; notes = this.generateZoneAwareSong(startTime, totalLength); notes.sort(function (a, b) { return a.time - b.time; }); // Ensure notes are sorted return notes; }, generateZoneAwareSong: function generateZoneAwareSong(startTime, songTotalLength) { var notes = []; var currentTime = startTime; // Track when each zone will be free var zoneFreeTime = { left: startTime, right: startTime }; // Minimum spacing between notes (human reaction time) var MIN_SAME_ZONE_SPACING = 400; // 400ms minimum in same zone var MIN_DIFFERENT_ZONE_SPACING = 200; // 200ms minimum between different zones var MIN_SYNC_SPACING = 1000; // Increased to 1000ms for more spacing between sync events var lastSyncTime = startTime - MIN_SYNC_SPACING; // Adjusted initialization // Define sections with different complexities var sections = [{ start: 0, end: 16000, complexity: 'simple' }, { start: 16000, end: 48000, complexity: 'medium' }, { start: 48000, end: 72000, complexity: 'medium' }, // Original: chorus1, intensity 0.8 { start: 72000, end: 104000, complexity: 'medium' }, { start: 104000, end: 128000, complexity: 'complex' }, // Original: chorus2, intensity 0.9 { start: 128000, end: 160000, complexity: 'medium' }, { start: 160000, end: 184000, complexity: 'complex' }, // Original: finalchorus, intensity 1.0 { start: 184000, end: songTotalLength, complexity: 'simple' } // Ensure it goes to the end ]; for (var s = 0; s < sections.length; s++) { var section = sections[s]; var sectionStartTime = startTime + section.start; var sectionEndTime = startTime + section.end; if (sectionEndTime > startTime + songTotalLength) { sectionEndTime = startTime + songTotalLength; } if (sectionStartTime >= sectionEndTime) continue; currentTime = Math.max(currentTime, sectionStartTime); while (currentTime < sectionEndTime - 1000) { // Leave 1s buffer at end of section var pattern = this.selectPattern(section.complexity, zoneFreeTime, currentTime, lastSyncTime); var patternResult = this.placePattern(pattern, currentTime, zoneFreeTime, MIN_SAME_ZONE_SPACING, MIN_DIFFERENT_ZONE_SPACING, sectionEndTime); if (patternResult.notes.length > 0) { var allNotesFit = true; for (var ni = 0; ni < patternResult.notes.length; ni++) { if (patternResult.notes[ni].time + (patternResult.notes[ni].duration || 0) > sectionEndTime - 200) { // Check end time allNotesFit = false; break; } } if (allNotesFit) { notes = notes.concat(patternResult.notes); currentTime = patternResult.nextTime; if (pattern.type === 'sync' || pattern.type === 'syncHold') { // Also update for syncHold lastSyncTime = patternResult.notes[0].time; // Time of the sync notes themselves } } else { currentTime += 300; // Advance time if pattern didn't fit } } else { currentTime += 300; // If pattern couldn't be placed, advance time } if (currentTime >= sectionEndTime - 1000) break; } } return notes; }, selectPattern: function selectPattern(complexity, zoneFreeTime, currentTime, lastSyncTime) { var patterns = []; var MIN_SYNC_SPACING_FOR_SELECTION = 1000; // Consistent with SongGenerator.generateZoneAwareSong if (complexity === 'simple') { patterns = [{ type: 'single', zone: 'left', duration: 800 }, // Longer footprint { type: 'single', zone: 'right', duration: 800 }, // Longer footprint { type: 'rest', duration: 1200 }, { type: 'rest', duration: 1200 } // More rests ]; if (currentTime - lastSyncTime >= MIN_SYNC_SPACING_FOR_SELECTION + 200) { // Sync less frequent patterns.push({ type: 'sync', duration: 1000 }); } } else if (complexity === 'medium') { patterns = [ // { type: 'hold', zone: 'left', holdDuration: 1000, duration: 700 }, // Reduced //{5M} //{5N} //{5O} { type: 'hold', zone: 'right', holdDuration: 1000, duration: 700 }, { type: 'single', zone: 'left', duration: 600 }, { type: 'single', zone: 'right', duration: 600 }, { type: 'single', zone: 'left', duration: 600 }, // Added more single taps { type: 'single', zone: 'right', duration: 600 }, // Added more single taps { type: 'alternating', duration: 750 }, // Footprint for 2 taps ~300ms apart { type: 'rest', duration: 1000 }]; if (currentTime - lastSyncTime >= MIN_SYNC_SPACING_FOR_SELECTION) { patterns.push({ type: 'sync', duration: 800 }); patterns.push({ type: 'sync', duration: 800 }); // Increase likelihood // patterns.push({ type: 'syncHold', holdDuration: 1000, duration: 800 }); // Removed syncHold //{6i} //{6j} //{6k} } } else { // complex patterns = [ // { type: 'hold', zone: 'left', holdDuration: 1200, duration: 800 }, // Reduced //{6p} //{6q} //{6r} { type: 'hold', zone: 'right', holdDuration: 1200, duration: 800 }, // { type: 'holdWithTaps', zone: 'left', holdDuration: 1200, duration: 1000 }, // Reduced //{6y} //{6z} //{6A} { type: 'alternating', duration: 800 // Increased from 600 }, { type: 'alternating', duration: 800 // Increased from 600 }, // Added more alternating // Slightly larger footprint for alternating taps { type: 'single', zone: 'left', duration: 700 // Increased from 500 }, // Slightly larger footprint for single taps { type: 'single', zone: 'right', duration: 700 // Increased from 500 }, { type: 'single', zone: 'left', duration: 700 // Increased from 500 }, // Added more single { type: 'single', zone: 'right', duration: 700 // Increased from 500 } // Added more single ]; if (currentTime - lastSyncTime >= MIN_SYNC_SPACING_FOR_SELECTION - 200) { // Allow syncs a bit closer patterns.push({ type: 'sync', duration: 600 }); patterns.push({ type: 'sync', duration: 600 }); // Increase likelihood // patterns.push({ type: 'syncHold', holdDuration: 1200, duration: 900 }); // Reduced one syncHold //{74} //{75} //{76} //{77} patterns.push({ type: 'syncHold', holdDuration: 1200, duration: 900 }); // Increase likelihood } } return patterns[Math.floor(Math.random() * patterns.length)]; }, placePattern: function placePattern(pattern, requestedTime, zoneFreeTime, minSameZoneSpacing, minDiffZoneSpacing, sectionEndTime) { var notes = []; var earliestTime = requestedTime; var nextTimeAdvance = pattern.duration || 500; // Default advancement if (pattern.type === 'single') { earliestTime = Math.max(requestedTime, zoneFreeTime[pattern.zone]); if (earliestTime + (pattern.duration || 0) < sectionEndTime) { notes.push({ time: earliestTime, type: 'tap', zone: pattern.zone }); zoneFreeTime[pattern.zone] = earliestTime + minSameZoneSpacing; return { notes: notes, nextTime: earliestTime + nextTimeAdvance }; } } else if (pattern.type === 'sync') { earliestTime = Math.max(requestedTime, zoneFreeTime.left, zoneFreeTime.right); if (earliestTime + (pattern.duration || 0) < sectionEndTime) { notes.push({ time: earliestTime, type: 'tap', zone: 'left', color: SYNC_NOTE_COLOR }); notes.push({ time: earliestTime, type: 'tap', zone: 'right', color: SYNC_NOTE_COLOR }); zoneFreeTime.left = earliestTime + minSameZoneSpacing; zoneFreeTime.right = earliestTime + minSameZoneSpacing; return { notes: notes, nextTime: earliestTime + nextTimeAdvance }; } } else if (pattern.type === 'alternating') { // Attempt Left then Right var firstZone = Math.random() < 0.5 ? 'left' : 'right'; var secondZone = firstZone === 'left' ? 'right' : 'left'; var t1 = Math.max(requestedTime, zoneFreeTime[firstZone]); var t2 = Math.max(t1 + minDiffZoneSpacing, zoneFreeTime[secondZone]); if (t2 + (pattern.duration || 0) - (t2 - t1) < sectionEndTime) { // check if second note fits notes.push({ time: t1, type: 'tap', zone: firstZone }); notes.push({ time: t2, type: 'tap', zone: secondZone }); zoneFreeTime[firstZone] = t1 + minSameZoneSpacing; zoneFreeTime[secondZone] = t2 + minSameZoneSpacing; return { notes: notes, nextTime: t1 + nextTimeAdvance // Use pattern's duration for next time, (nextTimeAdvance is pattern.duration) }; } } else if (pattern.type === 'syncHold') { earliestTime = Math.max(requestedTime, zoneFreeTime.left, zoneFreeTime.right); var holdDuration = pattern.holdDuration || 1200; // Default if not specified if (earliestTime + holdDuration < sectionEndTime) { notes.push({ time: earliestTime, type: 'hold', zone: 'left', duration: holdDuration, color: SYNC_NOTE_COLOR }); notes.push({ time: earliestTime, type: 'hold', zone: 'right', duration: holdDuration, color: SYNC_NOTE_COLOR }); zoneFreeTime.left = earliestTime + holdDuration + 200; // Buffer after hold zoneFreeTime.right = earliestTime + holdDuration + 200; // Buffer after hold return { notes: notes, nextTime: earliestTime + (pattern.duration || 800) // Use pattern's defined footprint }; } } else if (pattern.type === 'hold') { earliestTime = Math.max(requestedTime, zoneFreeTime[pattern.zone]); var holdDuration = pattern.holdDuration || 1500; if (earliestTime + holdDuration < sectionEndTime) { notes.push({ time: earliestTime, type: 'hold', zone: pattern.zone, duration: holdDuration }); var otherZone = pattern.zone === 'left' ? 'right' : 'left'; zoneFreeTime[pattern.zone] = earliestTime + holdDuration + 200; // Buffer after hold in its own zone // Block the other zone for the duration of this hold note, plus minDiffZoneSpacing buffer. // This prevents other notes from spawning in the adjacent lane during the hold. zoneFreeTime[otherZone] = Math.max(zoneFreeTime[otherZone], earliestTime + holdDuration + minDiffZoneSpacing); return { notes: notes, nextTime: earliestTime + nextTimeAdvance }; // Pattern's own duration logic } } else if (pattern.type === 'rest') { return { notes: [], nextTime: requestedTime + (pattern.duration || 500) }; } return { notes: [], nextTime: requestedTime + 100 }; // Failed to place, small advance } }; // Simpler song database var SongDatabase = { 'gameMusic': { bpm: 120, scannerCycleDuration: 4000, totalLength: 202136, notes: null } }; // Generate with the new system SongDatabase['gameMusic'].notes = SongGenerator.generateSong({ startDelay: 2000, totalLength: SongDatabase['gameMusic'].totalLength }); var defaultSongData = SongDatabase['gameMusic']; // Helper function to add new songs easily function addSong(songId, config) { SongDatabase[songId] = { bpm: config.bpm || 120, scannerCycleDuration: config.scannerCycleDuration || 4000, totalLength: config.totalLength, notes: config.customNotes || SongGenerator.generateSong(config) }; } // Example of adding a new song: /* addSong('newSong', { bpm: 140, totalLength: 180000, // 3 minutes startDelay: 1500, verseCount: 2, verseLength: 30000 }); */ function calculateNotePosition(noteTime) { // Calculate where scanner will be when note should be hit var cycleTime = noteTime % SCANNER_CYCLE_DURATION; var y; if (cycleTime < SCANNER_HALF_CYCLE) { // First half: moving up from bottom to top var progress = cycleTime / SCANNER_HALF_CYCLE; y = SCANNER_Y_MAX - progress * PLAY_AREA_HEIGHT; } else { // Second half: moving down from top to bottom var progress = (cycleTime - SCANNER_HALF_CYCLE) / SCANNER_HALF_CYCLE; y = SCANNER_Y_MIN + progress * PLAY_AREA_HEIGHT; } return y; } function calculateZoneX(zone) { // Calculate random X position within the specified zone var sideMargin = GAME_WIDTH * SAFETY_MARGIN_SIDES_PERCENT; // Calculate the available width for random placement within a zone, considering margins from both the screen edge and the center line. var randomSpan = GAME_WIDTH / 2 - 2 * sideMargin; // Ensure randomSpan is not negative, which could happen if margins are very large. // This would effectively mean notes spawn exactly on the margin line if randomSpan is 0. if (randomSpan < 0) { randomSpan = 0; } if (zone === 'left') { // Spawn between left_edge_margin and (center_line - center_margin) return sideMargin + Math.random() * randomSpan; } else { // zone === 'right' // Spawn between (center_line + center_margin) and (right_edge - right_edge_margin) return GAME_WIDTH / 2 + sideMargin + Math.random() * randomSpan; } } function createZoneIndicators() { // Left zone indicator leftZone = game.attachAsset('zoneIndicator', { anchorX: 0, anchorY: 0 }); leftZone.x = 0; leftZone.y = 0; leftZone.width = GAME_WIDTH / 2; leftZone.alpha = 0.1; leftZone.tint = 0x00ffff; // Right zone indicator rightZone = game.attachAsset('zoneIndicator', { anchorX: 0, anchorY: 0 }); rightZone.x = GAME_WIDTH / 2; rightZone.y = 0; rightZone.width = GAME_WIDTH / 2; rightZone.alpha = 0.1; rightZone.tint = 0xff00ff; } function showAccuracyPopup(text, x, y, color) { var popup = new Text2(text, { size: ACCURACY_POPUP_FONT_SIZE, fill: color, stroke: 0x000000, // Black stroke for better visibility strokeThickness: 4 }); popup.anchor.set(0.5, 0.5); popup.x = x; popup.y = y + ACCURACY_POPUP_Y_OFFSET; popup.alpha = 0; // Start invisible game.addChild(popup); // Animation: Fade in and move up slightly, then continue moving up and fade out. var initialY = popup.y; tween(popup, { alpha: 1, y: initialY - 30 }, { // Fade in and initial move duration: ACCURACY_POPUP_DURATION * 0.25, easing: tween.easeOut, onFinish: function onFinish() { // Second part of animation: fade out while continuing to move up tween(popup, { alpha: 0, y: initialY - 130 }, { // Target Y is further up from original initialY duration: ACCURACY_POPUP_DURATION * 0.75, easing: tween.easeIn, onFinish: function onFinish() { if (popup && popup.destroy) { popup.destroy(); } } }); } }); } // function createFlickResultNote is removed as it's no longer used. function startCountdown() { countdownTxt = new Text2('3', { size: 200, fill: 0xFFFFFF }); countdownTxt.anchor.set(0.5, 0.5); countdownTxt.x = GAME_WIDTH / 2; countdownTxt.y = GAME_HEIGHT / 2; game.addChild(countdownTxt); var count = 3; var countdownInterval = LK.setInterval(function () { count--; if (count > 0) { countdownTxt.setText(count.toString()); } else if (count === 0) { countdownTxt.setText('GO!'); } else { LK.clearInterval(countdownInterval); if (countdownTxt && countdownTxt.destroy) { countdownTxt.destroy(); } startGame(); } }, 1000); } function startGame() { gameStarted = true; gameStartTime = Date.now(); moveScanner(); LK.playMusic('gameMusic', { loop: true }); // updateHealthDisplay function is removed. } function moveScanner() { if (!scanner || !gameStarted) return; tween.stop(scanner); var targetY = scannerIsMovingUp ? SCANNER_Y_MIN : SCANNER_Y_MAX; tween(scanner, { y: targetY }, { duration: SCANNER_CYCLE_DURATION / 2, easing: tween.linear, onFinish: function onFinish() { scannerIsMovingUp = !scannerIsMovingUp; moveScanner(); } }); } function spawnNotesForCurrentTime(currentTime) { if (!currentSongData) return; currentSongData.notes.forEach(function (noteData, index) { var noteKey = index + '_' + noteData.time; // Check if already spawned var alreadySpawned = false; for (var i = 0; i < spawnedNotes.length; i++) { if (spawnedNotes[i] === noteKey) { alreadySpawned = true; break; } } // Only spawn if note time is approaching and not yet spawned if (!alreadySpawned && currentTime >= noteData.time - NOTE_SPAWN_AHEAD_MS && currentTime <= noteData.time - NOTE_SPAWN_AHEAD_MS + 100) { // Narrow spawn window // Calculate proper position var calculatedY = calculateNotePosition(noteData.time); var calculatedX = calculateZoneX(noteData.zone); // Create note data with calculated positions var processedNoteData = { time: noteData.time, type: noteData.type, x: calculatedX, y: calculatedY, color: noteData.color, // Pass the color from song data (e.g. SYNC_NOTE_COLOR flag or undefined) // The Note class constructor will determine the final actual color. duration: noteData.duration // flickDirection: noteData.flickDirection // Removed }; // Flick target logic is removed var note = new Note(processedNoteData, currentTime); notes.push(note); game.addChild(note); note.spawnIn(); spawnedNotes.push(noteKey); } }); } function setupGame() { LK.setScore(0); // playerHealth = INITIAL_PLAYER_HEALTH; // playerHealth assignment removed gameStarted = false; // Score Text if (scoreTxt && scoreTxt.destroy) { scoreTxt.destroy(); } scoreTxt = new Text2('0', { size: 100, fill: 0xFFFFFF }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); scoreTxt.setText(LK.getScore()); // Health Display removed // Zone indicators createZoneIndicators(); // Scanner - start at bottom if (scanner && scanner.destroy) { scanner.destroy(); } scanner = new Scanner(); scanner.y = SCANNER_Y_MAX; // Start at bottom game.addChild(scanner); // Clear previous notes notes.forEach(function (note) { if (note && note.destroy) { note.destroy(); } }); notes = []; spawnedNotes = []; // Load song data currentSongData = defaultSongData; BPM = currentSongData.bpm; BEAT_DURATION_MS = 60 / BPM * 1000; scannerIsMovingUp = true; // Will move up first // Start countdown instead of immediate game start startCountdown(); } function checkZoneHit(x, y) { var hitZone = x < GAME_WIDTH / 2 ? 'left' : 'right'; var currentTime = Date.now() - gameStartTime; for (var i = notes.length - 1; i >= 0; i--) { var note = notes[i]; if (!note || !note.active || note.isHit || note.isSpawning || !note.noteGraphics) continue; // Check if note is in the same zone if (note.zone === hitZone) { var noteCoreHeight = note.noteGraphics.height; // Assumes scale is 1 for active notes var noteCenterY = note.y; var scannerY = scanner.y; // Define hit windows based on note core size var perfectHitRadius = noteCoreHeight * PERFECT_HIT_RATIO / 2; var goodHitRadius = noteCoreHeight / 2; var perfectHitTop = noteCenterY - perfectHitRadius; var perfectHitBottom = noteCenterY + perfectHitRadius; var goodHitTop = noteCenterY - goodHitRadius; var goodHitBottom = noteCenterY + goodHitRadius; var accuracyText = null; var hitPoints = 0; var popupColor = 0xFFFFFF; if (scannerY >= perfectHitTop && scannerY <= perfectHitBottom) { accuracyText = "PERFECT"; popupColor = PERFECT_POPUP_COLOR; if (note.type === 'tap') { hitPoints = POINTS_PERFECT_TAP; } else if (note.type === 'hold') { hitPoints = POINTS_PERFECT_HOLD_START; } } else if (scannerY >= goodHitTop && scannerY <= goodHitBottom) { accuracyText = "GOOD"; popupColor = GOOD_POPUP_COLOR; if (note.type === 'tap') { hitPoints = POINTS_GOOD_TAP; } else if (note.type === 'hold') { hitPoints = POINTS_GOOD_HOLD_START; } } if (accuracyText) { note.isHit = true; // Mark as judged for timing. showAccuracyPopup(accuracyText, note.x, note.y, popupColor); if (note.type === 'tap') { note.showHitEffect(); // Makes note inactive } else if (note.type === 'hold') { note.startHoldEffect(); // Prepares note for holding } if (hitPoints > 0) { LK.setScore(LK.getScore() + hitPoints); scoreTxt.setText(LK.getScore()); } break; //{4u} // Found and processed a note for this hit } } } } // Initial setup call setupGame(); game.down = function (x, y, obj) { if (!gameStarted) return; //{4L} // playerHealth check removed checkZoneHit(x, y); // This will set note.isHit for taps and call startHoldEffect for holds. var hitZoneForDown = x < GAME_WIDTH / 2 ? 'left' : 'right'; // Logic for starting a hold // activeFlickPrimer = null; // Removed for (var i = 0; i < notes.length; i++) { var note = notes[i]; if (!note.active || note.isSpawning || note.holdSuccessfullyCompleted || note.isMissed) continue; if (note.zone === hitZoneForDown && note.isHit) { // note.isHit is now set by checkZoneHit if it was a "Good" or "Perfect" hit. // checkZoneHit also calls startHoldEffect and awards initial points. if (note.type === 'hold' && !note.isHolding) { note.isHolding = true; activeHoldNotes[note.zone] = note; // A flick note should not also be a hold note. // Comment remains for context, but flick logic is gone. } //{4I} // This closing brace now corresponds to 'if (note.type === 'hold' ...)' } } isDragging = true; // Still useful for game.up to know a drag sequence occurred dragStartX = x; // General drag start, flick primer uses its own copy dragStartY = y; // currentTrail related lines removed as trails are gone. }; game.move = function (x, y, obj) { if (!isDragging || !gameStarted) return; // Particle trail logic removed. This function can be empty or used for other drag-move effects if needed later. }; game.up = function (x, y, obj) { if (!gameStarted) return; // Allow processing even if not dragging for safety, but check gameStarted var releasedZone = x < GAME_WIDTH / 2 ? 'left' : 'right'; // Flick logic (activeFlickPrimer related) removed entirely. // Now, directly handle hold note release. var heldNote = activeHoldNotes[releasedZone]; if (heldNote && heldNote.isHolding) { heldNote.isHolding = false; // Stop particle effects etc. delete activeHoldNotes[releasedZone]; var currentTimeMs = Date.now() - gameStartTime; var noteExpectedEndTimeMs = heldNote.hitTime + heldNote.duration; var absoluteEndNoteY = heldNote.y + (heldNote.endNote ? heldNote.endNote.y : 0); // endNote.y is relative // Determine scanner direction at hit time for this note (needed for correct end check if trail goes up) var hitTimeCycle = heldNote.hitTime % SCANNER_CYCLE_DURATION; var noteScannerMovingUp = hitTimeCycle < SCANNER_HALF_CYCLE; var trailDirection = noteScannerMovingUp ? -1 : 1; var scannerAtEnd; if (trailDirection === -1) { // Trail goes upwards scannerAtEnd = scanner.y <= absoluteEndNoteY + HIT_TOLERANCE_PX_FOR_HOLD_END && scanner.y >= absoluteEndNoteY - HIT_TOLERANCE_PX_FOR_HOLD_END / 2; // Scanner near or slightly past the end upwardly } else { // Trail goes downwards scannerAtEnd = scanner.y >= absoluteEndNoteY - HIT_TOLERANCE_PX_FOR_HOLD_END && scanner.y <= absoluteEndNoteY + HIT_TOLERANCE_PX_FOR_HOLD_END / 2; // Scanner near or slightly past the end downwardly } var timeReachedOrPassedEnd = currentTimeMs >= noteExpectedEndTimeMs - HOLD_END_GRACE_PERIOD_MS; var timeNotTooLate = currentTimeMs <= noteExpectedEndTimeMs + HOLD_END_GRACE_PERIOD_MS + 200; // Not held excessively long if (scannerAtEnd && timeReachedOrPassedEnd && timeNotTooLate) { heldNote.completeHold(); LK.setScore(LK.getScore() + 50); // More points for a successful hold scoreTxt.setText(LK.getScore()); } else { heldNote.failHold(); // playerHealth--;//{5x} // Health decrement removed // updateHealthDisplay();//{5y} // Health display update removed } } // This bracket no longer closes an "else" block, but the "if (heldNote && heldNote.isHolding)" block. isDragging = false; // Reset general dragging flag. // currentTrail cleanup is removed as it's no longer created. }; game.update = function () { if (!gameStarted) return; //{5C} // playerHealth check removed var currentTime = Date.now() - gameStartTime; spawnNotesForCurrentTime(currentTime); for (var i = notes.length - 1; i >= 0; i--) { var note = notes[i]; if (!note) { notes.splice(i, 1); continue; } // Update hold effects if note is being held if (note.type === 'hold' && note.isHolding && note.active && !note.holdSuccessfullyCompleted && !note.isMissed) { note.updateHoldEffects(scanner.y); // Fail-safe for holds that are held too long past their end time var currentTimeMsForUpdate = Date.now() - gameStartTime; var noteExpectedEndTimeMsForUpdate = note.hitTime + note.duration; if (currentTimeMsForUpdate > noteExpectedEndTimeMsForUpdate + HOLD_END_GRACE_PERIOD_MS + 500) { // 500ms buffer if (activeHoldNotes[note.zone] === note) { // Still registered as actively held note.failHold(); // playerHealth--;//{5I} // Health decrement removed // updateHealthDisplay();//{5J} // Health display update removed delete activeHoldNotes[note.zone]; } } } // Check for missed notes if (note.active && !note.isHit && !note.isMissed && !note.isSpawning) { var timeDiff = currentTime - note.hitTime; var scannerPassed = false; if (scannerIsMovingUp && scanner.y < note.y - 50) { scannerPassed = true; } else if (!scannerIsMovingUp && scanner.y > note.y + 50) { scannerPassed = true; } if (scannerPassed && timeDiff > 300) { note.isMissed = true; note.showMissEffect(); // playerHealth--;//{5Q} // Health decrement removed // updateHealthDisplay();//{5R} // Health display update removed // if (playerHealth <= 0) {//{5S} // Game over check removed // return; // } } } // Cleanup inactive notes if (!note.active) { notes.splice(i, 1); if (game.children.indexOf(note) > -1) { game.removeChild(note); } if (note.destroy) { note.destroy(); } } } };
===================================================================
--- original.js
+++ change.js
@@ -12,8 +12,13 @@
self.noteData = noteData;
self.type = noteData.type; // 'tap', 'hold', 'flick'
self.targetX = noteData.x;
self.targetY = noteData.y;
+ self.hitTime = noteData.time;
+ self.duration = noteData.duration || 0; // For hold notes
+ // self.flickTarget = noteData.flickTarget || null; // Removed
+ self.spawnTime = spawnTime;
+ self.zone = noteData.x < GAME_WIDTH / 2 ? 'left' : 'right'; // Determine zone
// Determine note color based on type and zone, and if it's a sync note
if (noteData.color && noteData.color === SYNC_NOTE_COLOR) {
// This note was flagged as a sync note by SongGenerator
if (self.type === 'tap') {
@@ -27,13 +32,8 @@
} else {
// Standard note, color by zone
self.color = self.zone === 'left' ? COLOR_LEFT_ZONE : COLOR_RIGHT_ZONE;
}
- self.hitTime = noteData.time;
- self.duration = noteData.duration || 0; // For hold notes
- // self.flickTarget = noteData.flickTarget || null; // Removed
- self.spawnTime = spawnTime;
- self.zone = noteData.x < GAME_WIDTH / 2 ? 'left' : 'right'; // Determine zone
// self.requiredFlickDirection = noteData.flickDirection || null; // Removed
// State
self.active = true;
self.isHit = false;
The word 'Pulsar' in a glowing neon SVG in futuristic font. The word is half blue on the left and half red on the right. In-Game asset. 2d. High contrast. No shadows
Remove the background.
A thin expanding ring with energy distortion ``` - Outer ring: 4-6 pixels thick, bright cyan (#00FFFF) - Inner ring: 2-3 pixels thick, white (#FFFFFF) - Ring thickness: Tapers from thick to thin as it expands - Transparency: Ring itself at 80% opacity - Background: Completely transparent - Edge treatment: Soft anti-aliased edges, slight glow effect - Optional: Subtle "energy crackle" texture within the ring. In-Game asset. 2d. High contrast. No shadows
Soft, lingering light effect ``` - Center: Warm orange (#FF6600) at 40% opacity - Middle: Yellow (#FFAA00) at 25% opacity - Edge: Transparent - Shape: Perfect circle with very soft, wide falloff. In-Game asset. 2d. High contrast. No shadows