User prompt
Instead of using just shallowFish. Randomly pick from the three shallowFish assets.
User prompt
If a tap already missed a fish, don’t indicate it missed again as it passes the hook.
User prompt
Instead of a miss indicator when you tap on the wrong lane, still play the miss sound effect but tint the incorrect lane indicators red briefly. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make lane brackets almost the height of the lane with just a little bit of spacing between.
User prompt
Use lanebracket asset and mark each lane with a bracket the height of the lane on either edge of the screen, flipped on one side to maintain orientation. These should be intialized after the intro and have their alpha set to 0.5. When that lane is active and the hook is moving there, change the alpha on that set to 0.9.
User prompt
If a fish fully passes the hook without being caught call a miss.
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Timeout.tick error: spawnInterval is not defined' in or related to this line: 'this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime);' Line Number: 1067
Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'Timeout.tick error: Can't find variable: spawnInterval' in or related to this line: 'this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime);' Line Number: 1050
User prompt
Please fix the bug: 'Timeout.tick error: Can't find variable: spawnInterval' in or related to this line: 'this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime);' Line Number: 1050
User prompt
Please fix the bug: 'undefined is not a constructor (evaluating 'new Set()')' in or related to this line: 'var RhythmSpawner = {' Line Number: 933
User prompt
var RhythmSpawner = { nextBeatToSchedule: 1, scheduledBeats: new Set(), // Track which beats we've already scheduled // Main update function - call every frame update: function(currentTime) { if (!GameState.gameActive || GameState.songStartTime === 0) return; var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; // Calculate how far ahead we need to look var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var travelTimeMs = (distanceToHook / fishSpeed) * (1000/60); var beatsAhead = Math.ceil(travelTimeMs / spawnInterval) + 2; // +2 buffer // Check if we need to schedule more beats var songElapsed = currentTime - GameState.songStartTime; var currentSongBeat = songElapsed / spawnInterval; var maxBeatToSchedule = Math.floor(currentSongBeat) + beatsAhead; // Schedule any missing beats while (this.nextBeatToSchedule <= maxBeatToSchedule) { if (!this.scheduledBeats.has(this.nextBeatToSchedule)) { this.scheduleBeatFish(this.nextBeatToSchedule, spawnInterval, travelTimeMs); this.scheduledBeats.add(this.nextBeatToSchedule); } this.nextBeatToSchedule++; } }, // Schedule a fish to arrive on a specific beat scheduleBeatFish: function(beatNumber, spawnInterval, travelTimeMs) { var targetArrivalTime = GameState.songStartTime + (beatNumber * spawnInterval); var spawnTime = targetArrivalTime - travelTimeMs; var currentTime = LK.ticks * (1000/60); // Only schedule if spawn time is in the future (or very recent) if (spawnTime >= currentTime - 100) { // Use setTimeout to spawn at exact time var delay = Math.max(0, spawnTime - currentTime); LK.setTimeout(function() { RhythmSpawner.spawnRhythmFish(beatNumber, targetArrivalTime); }, delay); } }, // Spawn a fish for a specific beat arrival spawnRhythmFish: function(beatNumber, targetArrivalTime) { if (!GameState.gameActive) return; var currentTime = LK.ticks * (1000/60); var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Check if we should spawn (pattern checks, etc.) if (!PatternGenerator.canSpawnFishOnBeat(currentTime, 60000 / songConfig.bpm)) { return; } // Lane selection var laneIndex = PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; // Fish type selection var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; } else if (GameState.selectedDepth >= 2 && rand < 0.3) { fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; fishValue = depthConfig.fishValue; } // Calculate exact speed needed to arrive on time var timeRemaining = targetArrivalTime - currentTime; var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var spawnSide = Math.random() < 0.5 ? -1 : 1; // Calculate required speed to arrive exactly on target time var framesRemaining = timeRemaining / (1000/60); var requiredSpeed = (distanceToHook / framesRemaining) * spawnSide; // Create fish with calculated speed var newFish = new Fish(fishType, fishValue, requiredSpeed, laneIndex); newFish.spawnSide = spawnSide; newFish.targetArrivalTime = targetArrivalTime; // Store for debugging newFish.x = requiredSpeed > 0 ? -150 : 2048 + 150; newFish.y = targetLane.y; newFish.baseY = targetLane.y; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTime); // Handle multi-spawns this.handleMultiSpawns(beatNumber, pattern); return newFish; }, // Handle double/triple spawns handleMultiSpawns: function(beatNumber, pattern) { if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { // Schedule second fish for next beat var nextBeat = beatNumber + 1; if (!this.scheduledBeats.has(nextBeat)) { var songConfig = GameState.getCurrentSongConfig(); var spawnInterval = (60000 / songConfig.bpm) * pattern.beatsPerFish; var travelTimeMs = this.calculateTravelTime(); this.scheduleBeatFish(nextBeat, spawnInterval, travelTimeMs); this.scheduledBeats.add(nextBeat); } // Handle triplets if (pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance) { var thirdBeat = beatNumber + 2; if (!this.scheduledBeats.has(thirdBeat)) { this.scheduleBeatFish(thirdBeat, spawnInterval, travelTimeMs); this.scheduledBeats.add(thirdBeat); } } } }, // Helper to calculate travel time calculateTravelTime: function() { var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; return (distanceToHook / fishSpeed) * (1000/60); }, // Reset for new session reset: function() { this.nextBeatToSchedule = 1; this.scheduledBeats.clear(); }, // Debug info getDebugInfo: function() { return { nextBeat: this.nextBeatToSchedule, scheduledCount: this.scheduledBeats.size, scheduledBeats: Array.from(this.scheduledBeats).slice(-10) // Last 10 }; } }; ``` **Replace the entire spawn section in main game loop:** ```javascript // Remove all existing spawn logic and replace with: RhythmSpawner.update(currentTime); ``` **Add to startFishingSession():** ```javascript // Add after GameState.gameActive = true; RhythmSpawner.reset(); ``` **Add to endFishingSession():** ```javascript // Add with other cleanup RhythmSpawner.reset();
User prompt
// Add this to GameState GameState.lastSpawnedBeatNumber = 0; // Replace spawn section with: var songElapsed = currentTime - GameState.songStartTime; var beatInterval = 60000 / songConfig.bpm; var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var spawnInterval = beatInterval * pattern.beatsPerFish; // Calculate which beat number we should spawn for next var currentBeatNumber = Math.floor(songElapsed / spawnInterval); var nextBeatToSpawn = GameState.lastSpawnedBeatNumber + 1; // If we haven't spawned for this beat yet if (nextBeatToSpawn <= currentBeatNumber + 6) { // Allow spawning up to 6 beats ahead var nextBeatTime = nextBeatToSpawn * spawnInterval; // Relative to song start var absoluteArrivalTime = GameState.songStartTime + nextBeatTime; var targetArrivalFrame = Math.round(absoluteArrivalTime * 60 / 1000); // Calculate spawn timing var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var framesNeededToTravel = Math.round(distanceToHook / fishSpeed); var spawnFrame = targetArrivalFrame - framesNeededToTravel; // Spawn if we're at or past the spawn frame (with tolerance) if (LK.ticks >= spawnFrame && LK.ticks <= spawnFrame + 3) { if (PatternGenerator.canSpawnFishOnBeat(currentTime, spawnInterval)) { spawnFish(currentTime, {}); GameState.lastSpawnedBeatNumber = nextBeatToSpawn; } } } ``` Also add to `startFishingSession()`: ```javascript GameState.lastSpawnedBeatNumber = 0;
User prompt
// Replace entire spawn section with this: var songElapsed = currentTime - GameState.songStartTime; var beatInterval = 60000 / songConfig.bpm; var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var spawnInterval = beatInterval * pattern.beatsPerFish; // Calculate next beat arrival time var nextBeatNumber = Math.floor(songElapsed / spawnInterval) + 1; var nextBeatTime = nextBeatNumber * spawnInterval; var targetArrivalFrame = Math.round((GameState.songStartTime + nextBeatTime) * 60 / 1000); // Calculate spawn timing var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var framesNeededToTravel = Math.round(distanceToHook / fishSpeed); var spawnFrame = targetArrivalFrame - framesNeededToTravel; // Spawn on exact frame if (LK.ticks === spawnFrame) { spawnFish(currentTime, {}); }
User prompt
Update as needed with: var PredictiveSpawning = { // Calculate how long it takes a fish to reach the hook from spawn calculateTimeToHook: function(fishSpeed, spawnFromLeft) { var hookX = GAME_CONFIG.SCREEN_CENTER_X; var spawnX = spawnFromLeft ? -150 : 2048 + 150; var distanceToHook = Math.abs(hookX - spawnX); var speedPixelsPerFrame = Math.abs(fishSpeed); var framesToHook = distanceToHook / speedPixelsPerFrame; return framesToHook * (1000 / 60); // Convert to milliseconds }, // Schedule fish spawns to arrive on specific beats scheduledSpawns: [], // Array of {spawnTime, targetBeatTime, options} // Schedule a fish to arrive on a target beat scheduleSpawnForBeat: function(targetBeatTime, options) { options = options || {}; var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = depthConfig.fishSpeed; // Determine spawn side (left = -1, right = 1) var spawnSide = options.forcedSpawnSide !== undefined ? options.forcedSpawnSide : (Math.random() < 0.5 ? -1 : 1); var spawnFromLeft = (spawnSide === -1); var timeToHook = this.calculateTimeToHook(fishSpeed, spawnFromLeft); var spawnTime = targetBeatTime - timeToHook; // Only schedule if spawn time is in the future (or very recent) var currentTime = LK.ticks * (1000 / 60); if (spawnTime > currentTime - 100) { // Allow 100ms tolerance for recent spawns this.scheduledSpawns.push({ spawnTime: spawnTime, targetBeatTime: targetBeatTime, options: options, spawnSide: spawnSide }); } }, // Check and execute scheduled spawns updateScheduledSpawns: function(currentTime) { for (var i = this.scheduledSpawns.length - 1; i >= 0; i--) { var scheduledSpawn = this.scheduledSpawns[i]; // If it's time to spawn (with tolerance window) if (currentTime >= scheduledSpawn.spawnTime && currentTime < scheduledSpawn.spawnTime + 100) { // 100ms tolerance // Execute the spawn this.executeScheduledSpawn(scheduledSpawn, currentTime); // Remove from scheduled spawns this.scheduledSpawns.splice(i, 1); } // Remove old scheduled spawns that we missed else if (currentTime > scheduledSpawn.spawnTime + 500) { this.scheduledSpawns.splice(i, 1); } } }, // Execute a scheduled spawn executeScheduledSpawn: function(scheduledSpawn, currentTime) { var options = scheduledSpawn.options; options.forcedSpawnSide = scheduledSpawn.spawnSide; // Call the modified spawnFish function this.spawnPredictiveFish(currentTime, options); }, // Modified spawn fish function for predictive spawning spawnPredictiveFish: function(currentTimeForRegistration, options) { options = options || {}; var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Proximity check - skip spawn if too close to existing fish var isFirstFishOfBeat = !options.laneIndexToUse && !options.forcedSpawnSide; if (isFirstFishOfBeat) { for (var i = 0; i < fishArray.length; i++) { var existingFish = fishArray[i]; if (Math.abs(existingFish.x - GAME_CONFIG.SCREEN_CENTER_X) < PatternGenerator.minDistanceBetweenFish) { return null; // Skip this spawn } } } // Lane selection var laneIndex; if (options.laneIndexToUse !== undefined) { laneIndex = options.laneIndexToUse; PatternGenerator.lastLane = laneIndex; } else { laneIndex = PatternGenerator.getNextLane(); } var targetLane = GAME_CONFIG.LANES[laneIndex]; // Fish type selection var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; } else if (GameState.selectedDepth >= 2 && rand < 0.3) { fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; fishValue = depthConfig.fishValue; } // Determine spawn side and speed var spawnSide = options.forcedSpawnSide !== undefined ? options.forcedSpawnSide : (Math.random() < 0.5 ? -1 : 1); var fishSpeedValue = depthConfig.fishSpeed; var actualFishSpeed = Math.abs(fishSpeedValue) * spawnSide; // Create fish var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex); newFish.spawnSide = spawnSide; newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; newFish.y = targetLane.y; newFish.baseY = targetLane.y; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTimeForRegistration); return newFish; }, // Pre-populate schedule at session start prePopulateSchedule: function() { var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var beatInterval = 60000 / songConfig.bpm; var baseSpawnInterval = beatInterval * pattern.beatsPerFish; // Calculate how far ahead we need to schedule var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var timeToHook = (distanceToHook / fishSpeed) * (1000/60); var beatsAhead = Math.max(6, Math.ceil(timeToHook / beatInterval) + 1); // Pre-schedule fish for the first several beats var currentTime = LK.ticks * (1000 / 60); for (var i = 1; i <= beatsAhead + 3; i++) { // Start from beat 1 var targetBeatTime = currentTime + (i * baseSpawnInterval); this.scheduleSpawnForBeat(targetBeatTime, {}); // Add some variety with doubles (but not too early) if (i > 3 && pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { this.scheduleSpawnForBeat(targetBeatTime + baseSpawnInterval, {}); } } }, // Clear all scheduled spawns clearScheduledSpawns: function() { this.scheduledSpawns = []; } }; ``` **Modified main game loop section (replace the existing spawn section):** ```javascript // Check song end first if (currentTime - GameState.songStartTime >= songConfig.duration) { endFishingSession(); return; } // Handle beat timing and scheduling if (currentTime - GameState.lastBeatTime >= spawnInterval) { GameState.lastBeatTime = currentTime; GameState.beatCount++; if (PatternGenerator.canSpawnFishOnBeat(currentTime, spawnInterval)) { // Calculate timing for future fish var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var timeToHook = (distanceToHook / fishSpeed) * (1000/60); var beatsAhead = Math.max(6, Math.ceil(timeToHook / spawnInterval) + 1); // Schedule fish to arrive on future beat var targetBeatTime = currentTime + (beatsAhead * spawnInterval); PredictiveSpawning.scheduleSpawnForBeat(targetBeatTime, {}); // Handle multi-spawns for consecutive future beats if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { var isPotentiallyTriplet = pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance; // Schedule second fish for next beat PredictiveSpawning.scheduleSpawnForBeat(targetBeatTime + spawnInterval, {}); if (isPotentiallyTriplet) { // Schedule third fish for beat after that PredictiveSpawning.scheduleSpawnForBeat(targetBeatTime + (2 * spawnInterval), {}); } } } } // Process scheduled spawns PredictiveSpawning.updateScheduledSpawns(currentTime); ``` **Add to startFishingSession() function (after GameState.gameActive = true):** ```javascript // Add this after GameState.gameActive = true; PredictiveSpawning.clearScheduledSpawns(); // Add this after GameState.songStartTime = 0; is set // We'll call prePopulateSchedule() after the timer is initialized in the main loop ``` **Add to the main game loop (after GameState.songStartTime = currentTime;):** ```javascript // Initialize game timer if (GameState.songStartTime === 0) { GameState.songStartTime = currentTime; // Pre-populate the schedule so first fish appear immediately PredictiveSpawning.prePopulateSchedule(); } ``` **Add to endFishingSession():** ```javascript // Add this with the other cleanup PredictiveSpawning.clearScheduledSpawns();
User prompt
Update as needed with: if (currentTime - GameState.lastBeatTime >= spawnInterval) { var currentBeatTime = GameState.lastBeatTime + spawnInterval; GameState.lastBeatTime = currentBeatTime; GameState.beatCount++; if (PatternGenerator.canSpawnFishOnBeat(currentTime, spawnInterval)) { // Calculate how many beats in the future this fish should arrive var depthConfig = GameState.getCurrentDepthConfig(); var normalFishSpeed = Math.abs(depthConfig.fishSpeed); // Normal speed (6 pixels/frame) var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; // 1174 pixels var timeToHook = (distanceToHook / normalFishSpeed) * (1000/60); // ~3260ms var beatsToHook = Math.ceil(timeToHook / spawnInterval); // ~5 beats in future // Target arrival time is several beats in the future var targetArrivalTime = currentBeatTime + (beatsToHook * spawnInterval); // Spawn fish now for future arrival var firstFish = spawnFish(currentTime, {}); if (firstFish) { // Check for double beat if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { var isPotentiallyTriplet = pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance; LK.setTimeout(function() { var secondFish = spawnFish(LK.ticks * (1000/60), {}); if (secondFish && isPotentiallyTriplet) { LK.setTimeout(function() { spawnFish(LK.ticks * (1000/60), {}); }, TRIPLET_BEAT_SPAWN_DELAY_MS); } }, MULTI_BEAT_SPAWN_DELAY_MS); } } } }
User prompt
Update with: if (currentTime - GameState.lastBeatTime >= spawnInterval) { var currentBeatTime = GameState.lastBeatTime + spawnInterval; GameState.lastBeatTime = currentBeatTime; GameState.beatCount++; if (PatternGenerator.canSpawnFishOnBeat(currentTime, spawnInterval)) { // Calculate how many beats in the future this fish should arrive var depthConfig = GameState.getCurrentDepthConfig(); var normalFishSpeed = Math.abs(depthConfig.fishSpeed); // Normal speed (6 pixels/frame) var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; // 1174 pixels var timeToHook = (distanceToHook / normalFishSpeed) * (1000/60); // ~3260ms var beatsToHook = Math.ceil(timeToHook / spawnInterval); // ~5 beats in future // Target arrival time is several beats in the future var targetArrivalTime = currentBeatTime + (beatsToHook * spawnInterval); // Spawn fish now for future arrival var firstFish = spawnFish(currentTime, {}); if (firstFish) { // Check for double beat if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { var isPotentiallyTriplet = pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance; LK.setTimeout(function() { var secondFish = spawnFish(LK.ticks * (1000/60), {}); if (secondFish && isPotentiallyTriplet) { LK.setTimeout(function() { spawnFish(LK.ticks * (1000/60), {}); }, TRIPLET_BEAT_SPAWN_DELAY_MS); } }, MULTI_BEAT_SPAWN_DELAY_MS); } } } }
User prompt
Update as needed with: if (currentTime - GameState.lastBeatTime >= spawnInterval) { GameState.lastBeatTime = currentTime; // Keep this immediate GameState.beatCount++; // Calculate when fish should arrive (next beat) var targetArrivalTime = currentTime + spawnInterval; // Spawn fish immediately but calculate their speed to arrive on beat var depthConfig = GameState.getCurrentDepthConfig(); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var timeToHook = spawnInterval; // Fish should take exactly one beat interval to reach hook var requiredSpeed = distanceToHook / (timeToHook / (1000/60)); // Convert to pixels per frame // Use the calculated speed instead of config speed var firstFish = spawnFishWithSpeed(currentTime, {}, requiredSpeed); // Rest of double/triple spawn logic... }
User prompt
Update with: if (currentTime - GameState.lastBeatTime >= spawnInterval) { GameState.lastBeatTime = currentTime; // Keep this immediate GameState.beatCount++; // Calculate when fish should arrive (next beat) var targetArrivalTime = currentTime + spawnInterval; // Spawn fish immediately but calculate their speed to arrive on beat var depthConfig = GameState.getCurrentDepthConfig(); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var timeToHook = spawnInterval; // Fish should take exactly one beat interval to reach hook var requiredSpeed = distanceToHook / (timeToHook / (1000/60)); // Convert to pixels per frame // Use the calculated speed instead of config speed var firstFish = spawnFishWithSpeed(currentTime, {}, requiredSpeed); // Rest of double/triple spawn logic... }
User prompt
Update with: var PredictiveSpawning = { // Calculate how long it takes a fish to reach the hook from spawn calculateTimeToHook: function(fishSpeed, spawnFromLeft) { var hookX = GAME_CONFIG.SCREEN_CENTER_X; var spawnX = spawnFromLeft ? -150 : 2048 + 150; var distanceToHook = Math.abs(hookX - spawnX); var speedPixelsPerFrame = Math.abs(fishSpeed); var framesToHook = distanceToHook / speedPixelsPerFrame; return framesToHook * (1000 / 60); // Convert to milliseconds }, // Schedule fish spawns to arrive on specific beats scheduledSpawns: [], // Array of {spawnTime, targetBeatTime, options} // Schedule a fish to arrive on a target beat scheduleSpawnForBeat: function(targetBeatTime, options) { options = options || {}; var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = depthConfig.fishSpeed; // Determine spawn side (left = true, right = false) var spawnFromLeft = options.forcedSpawnSide !== undefined ? (options.forcedSpawnSide === -1) : (Math.random() < 0.5); var timeToHook = this.calculateTimeToHook(fishSpeed, spawnFromLeft); var spawnTime = targetBeatTime - timeToHook; // Store spawn side in options for spawnFish options.spawnFromLeft = spawnFromLeft; this.scheduledSpawns.push({ spawnTime: spawnTime, targetBeatTime: targetBeatTime, options: options }); }, // Check and execute scheduled spawns updateScheduledSpawns: function(currentTime) { for (var i = this.scheduledSpawns.length - 1; i >= 0; i--) { var scheduledSpawn = this.scheduledSpawns[i]; // If it's time to spawn (with small tolerance) if (currentTime >= scheduledSpawn.spawnTime && currentTime < scheduledSpawn.spawnTime + 100) { // 100ms tolerance // Execute the spawn this.executeScheduledSpawn(scheduledSpawn, currentTime); // Remove from scheduled spawns this.scheduledSpawns.splice(i, 1); } // Remove old scheduled spawns that we missed else if (currentTime > scheduledSpawn.spawnTime + 500) { this.scheduledSpawns.splice(i, 1); } } }, // Execute a scheduled spawn executeScheduledSpawn: function(scheduledSpawn, currentTime) { var options = scheduledSpawn.options; // Use the stored spawn side to determine fish speed direction var depthConfig = GameState.getCurrentDepthConfig(); var baseSpeed = Math.abs(depthConfig.fishSpeed); var fishSpeed = options.spawnFromLeft ? baseSpeed : -baseSpeed; // Call modified spawnFish function this.spawnPredictiveFish(currentTime, options, fishSpeed); }, // Modified spawn fish function for predictive spawning spawnPredictiveFish: function(currentTimeForRegistration, options, fishSpeed) { options = options || {}; var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Lane selection var laneIndex; if (options.laneIndexToUse !== undefined) { laneIndex = options.laneIndexToUse; PatternGenerator.lastLane = laneIndex; } else { laneIndex = PatternGenerator.getNextLane(); } var targetLane = GAME_CONFIG.LANES[laneIndex]; // Fish type selection (same as original) var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; } else if (GameState.selectedDepth >= 2 && rand < 0.3) { fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; fishValue = depthConfig.fishValue; } // Create fish var newFish = new Fish(fishType, fishValue, fishSpeed, laneIndex); newFish.spawnSide = Math.sign(fishSpeed); // Store spawn side (-1 or 1) newFish.x = fishSpeed > 0 ? -150 : 2048 + 150; // Position based on direction newFish.y = targetLane.y; newFish.baseY = targetLane.y; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTimeForRegistration); return newFish; }, // Clear all scheduled spawns (for session end/reset) clearScheduledSpawns: function() { this.scheduledSpawns = []; } }; ``` **Modified main game loop section:** ```javascript // Replace the existing spawn fish on beat section with this: if (currentTime - GameState.lastBeatTime >= spawnInterval) { var nextBeatTime = GameState.lastBeatTime + spawnInterval; GameState.lastBeatTime = nextBeatTime; GameState.beatCount++; if (PatternGenerator.canSpawnFishOnBeat(currentTime, spawnInterval)) { // Schedule first fish to arrive on beat PredictiveSpawning.scheduleSpawnForBeat(nextBeatTime, {}); // Check for double beat if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { var isPotentiallyTriplet = pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance; // Schedule second fish for next beat var secondBeatTime = nextBeatTime + beatInterval; PredictiveSpawning.scheduleSpawnForBeat(secondBeatTime, {}); if (isPotentiallyTriplet) { // Schedule third fish for beat after that var thirdBeatTime = secondBeatTime + beatInterval; PredictiveSpawning.scheduleSpawnForBeat(thirdBeatTime, {}); } } } } // Add this to the main update loop (after the spawn section): PredictiveSpawning.updateScheduledSpawns(currentTime); ``` **Modifications needed in other functions:** ```javascript // In startFishingSession(), add: PredictiveSpawning.clearScheduledSpawns(); // In endFishingSession(), add: PredictiveSpawning.clearScheduledSpawns();
User prompt
Update with: var PredictiveSpawning = { // Calculate how long it takes a fish to reach the hook from spawn calculateTimeToHook: function(fishSpeed, spawnFromLeft) { var hookX = GAME_CONFIG.SCREEN_CENTER_X; var spawnX = spawnFromLeft ? -150 : 2048 + 150; var distanceToHook = Math.abs(hookX - spawnX); var speedPixelsPerFrame = Math.abs(fishSpeed); var framesToHook = distanceToHook / speedPixelsPerFrame; return framesToHook * (1000 / 60); // Convert to milliseconds }, // Schedule fish spawns to arrive on specific beats scheduledSpawns: [], // Array of {spawnTime, targetBeatTime, options} // Schedule a fish to arrive on a target beat scheduleSpawnForBeat: function(targetBeatTime, options) { options = options || {}; var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = depthConfig.fishSpeed; // Determine spawn side (left = true, right = false) var spawnFromLeft = options.forcedSpawnSide !== undefined ? (options.forcedSpawnSide === -1) : (Math.random() < 0.5); var timeToHook = this.calculateTimeToHook(fishSpeed, spawnFromLeft); var spawnTime = targetBeatTime - timeToHook; // Store spawn side in options for spawnFish options.spawnFromLeft = spawnFromLeft; this.scheduledSpawns.push({ spawnTime: spawnTime, targetBeatTime: targetBeatTime, options: options }); }, // Check and execute scheduled spawns updateScheduledSpawns: function(currentTime) { for (var i = this.scheduledSpawns.length - 1; i >= 0; i--) { var scheduledSpawn = this.scheduledSpawns[i]; // If it's time to spawn (with small tolerance) if (currentTime >= scheduledSpawn.spawnTime && currentTime < scheduledSpawn.spawnTime + 100) { // 100ms tolerance // Execute the spawn this.executeScheduledSpawn(scheduledSpawn, currentTime); // Remove from scheduled spawns this.scheduledSpawns.splice(i, 1); } // Remove old scheduled spawns that we missed else if (currentTime > scheduledSpawn.spawnTime + 500) { this.scheduledSpawns.splice(i, 1); } } }, // Execute a scheduled spawn executeScheduledSpawn: function(scheduledSpawn, currentTime) { var options = scheduledSpawn.options; // Use the stored spawn side to determine fish speed direction var depthConfig = GameState.getCurrentDepthConfig(); var baseSpeed = Math.abs(depthConfig.fishSpeed); var fishSpeed = options.spawnFromLeft ? baseSpeed : -baseSpeed; // Call modified spawnFish function this.spawnPredictiveFish(currentTime, options, fishSpeed); }, // Modified spawn fish function for predictive spawning spawnPredictiveFish: function(currentTimeForRegistration, options, fishSpeed) { options = options || {}; var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Lane selection var laneIndex; if (options.laneIndexToUse !== undefined) { laneIndex = options.laneIndexToUse; PatternGenerator.lastLane = laneIndex; } else { laneIndex = PatternGenerator.getNextLane(); } var targetLane = GAME_CONFIG.LANES[laneIndex]; // Fish type selection (same as original) var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; } else if (GameState.selectedDepth >= 2 && rand < 0.3) { fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; fishValue = depthConfig.fishValue; } // Create fish var newFish = new Fish(fishType, fishValue, fishSpeed, laneIndex); newFish.spawnSide = Math.sign(fishSpeed); // Store spawn side (-1 or 1) newFish.x = fishSpeed > 0 ? -150 : 2048 + 150; // Position based on direction newFish.y = targetLane.y; newFish.baseY = targetLane.y; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTimeForRegistration); return newFish; }, // Clear all scheduled spawns (for session end/reset) clearScheduledSpawns: function() { this.scheduledSpawns = []; } }; ``` **Modified main game loop section:** ```javascript // Replace the existing spawn fish on beat section with this: if (currentTime - GameState.lastBeatTime >= spawnInterval) { var nextBeatTime = GameState.lastBeatTime + spawnInterval; GameState.lastBeatTime = nextBeatTime; GameState.beatCount++; if (PatternGenerator.canSpawnFishOnBeat(currentTime, spawnInterval)) { // Schedule first fish to arrive on beat PredictiveSpawning.scheduleSpawnForBeat(nextBeatTime, {}); // Check for double beat if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { var isPotentiallyTriplet = pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance; // Schedule second fish for next beat var secondBeatTime = nextBeatTime + beatInterval; PredictiveSpawning.scheduleSpawnForBeat(secondBeatTime, {}); if (isPotentiallyTriplet) { // Schedule third fish for beat after that var thirdBeatTime = secondBeatTime + beatInterval; PredictiveSpawning.scheduleSpawnForBeat(thirdBeatTime, {}); } } } } // Add this to the main update loop (after the spawn section): PredictiveSpawning.updateScheduledSpawns(currentTime); ``` **Modifications needed in other functions:** ```javascript // In startFishingSession(), add: PredictiveSpawning.clearScheduledSpawns(); // In endFishingSession(), add: PredictiveSpawning.clearScheduledSpawns();
User prompt
Morning tide song should use this pattern: morning_tide_custom: { beatsPerFish: 1.2, // More frequent than gentle_waves_custom (1.5) and much more than simple (2) doubleSpawnChance: 0.12, // Higher than simple (0.10) rareSpawnChance: 0.03, // Higher than simple (0.02) // Custom timing sections based on musical structure sections: [ // Gentle opening - ease into the song (0-25 seconds) { startTime: 0, endTime: 25000, spawnModifier: 0.9, // Slightly reduced for intro description: "calm_opening" }, // Building energy - first wave (25-50 seconds) { startTime: 25000, endTime: 50000, spawnModifier: 1.2, // More active description: "first_wave" }, // Peak intensity - morning rush (50-80 seconds) { startTime: 50000, endTime: 80000, spawnModifier: 1.5, // Most intense section description: "morning_rush" }, // Sustained energy - second wave (80-110 seconds) { startTime: 80000, endTime: 110000, spawnModifier: 1.3, // High but slightly less than peak description: "second_wave" }, // Climactic finish (110-140 seconds) { startTime: 110000, endTime: 140000, spawnModifier: 1.4, // Building back up description: "climactic_finish" }, // Gentle fade out (140-156.8 seconds) { startTime: 140000, endTime: 156827, spawnModifier: 0.8, // Calm ending description: "peaceful_fade" } ] }
Code edit (1 edits merged)
Please save this source code
User prompt
Change the levels to use the same single container animation methods for the boat and fisherman as the title screen.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var BubbleParticle = Container.expand(function (startX, startY) { var self = Container.call(this); self.gfx = self.attachAsset('bubbles', { anchorX: 0.5, anchorY: 0.5, alpha: 1 + Math.random() * 0.2, // Start with some variation scaleX: 1.2 + Math.random() * 0.25, // Larger bubbles // Bubbles are now 20% to 45% of original asset size scaleY: this.scaleX // Keep aspect ratio initially, can be changed in update if desired }); self.x = startX; self.y = startY; self.vx = (Math.random() - 0.5) * 0.4; // Even slower horizontal drift self.vy = -(0.4 + Math.random() * 0.3); // Slower upward movement self.life = 120 + Math.random() * 60; // Lifespan in frames (2 to 3 seconds) self.age = 0; self.isDone = false; var initialAlpha = self.gfx.alpha; var initialScale = self.gfx.scaleX; // Assuming scaleX and scaleY start the same self.update = function () { if (self.isDone) { return; } self.age++; self.x += self.vx; self.y += self.vy; // Fade out self.gfx.alpha = Math.max(0, initialAlpha * (1 - self.age / self.life)); // Optionally shrink a bit more, or grow slightly then shrink var scaleFactor = 1 - self.age / self.life; // Simple shrink self.gfx.scaleX = initialScale * scaleFactor; self.gfx.scaleY = initialScale * scaleFactor; if (self.age >= self.life || self.gfx.alpha <= 0 || self.gfx.scaleX <= 0.01) { self.isDone = true; } }; return self; }); var CloudParticle = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('cloud', { anchorX: 0.5, anchorY: 0.5 }); // Cloud spawn location - 30% chance to spawn on screen var spawnOnScreen = Math.random() < 0.3; if (spawnOnScreen) { // Spawn randomly across the screen width self.x = 200 + Math.random() * 1648; // Between 200 and 1848 to avoid edges // Random horizontal drift direction self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.1 + Math.random() * 0.2); } else { // Original off-screen spawn logic (70% of the time) var spawnFromLeft = Math.random() < 0.5; if (spawnFromLeft) { self.x = -100; // Start off-screen left self.vx = 0.1 + Math.random() * 0.2; // Slower drift right at 0.1-0.3 pixels/frame } else { self.x = 2048 + 100; // Start off-screen right self.vx = -(0.1 + Math.random() * 0.2); // Slower drift left } } // Spawn in sky area (above water surface) var skyTop = -500; // Where sky background starts var skyBottom = GAME_CONFIG.WATER_SURFACE_Y - 100; // Stay well above water self.y = skyTop + Math.random() * (skyBottom - skyTop); // Cloud properties var baseScale = 0.8 + Math.random() * 0.6; // Scale: 0.8x to 1.4x self.gfx.scale.set(baseScale); // Subtle vertical drift self.vy = (Math.random() - 0.5) * 0.02; // Even slower up/down drift // Opacity for atmospheric effect var targetAlpha = 0.4 + Math.random() * 0.3; // Alpha: 0.4 to 0.7 self.gfx.alpha = 0; // Start transparent self.isDone = false; self.fadingOut = false; self.hasFadedIn = false; // Track if initial fade in completed // Define screen boundaries for fade in/out var fadeInStartX = 400; // Start fading in when cloud reaches this X var fadeInEndX = 800; // Fully visible by this X var fadeOutStartX = 1248; // Start fading out at this X (2048 - 800) var fadeOutEndX = 1648; // Fully transparent by this X (2048 - 400) self.update = function () { if (self.isDone) { return; } // Apply movement self.x += self.vx; self.y += self.vy; // Handle mid-screen fade in/out based on position var currentAlpha = self.gfx.alpha; if (spawnFromLeft) { // Moving right: fade in then fade out if (!self.hasFadedIn && self.x >= fadeInStartX && self.x <= fadeInEndX) { // Calculate fade in progress var fadeInProgress = (self.x - fadeInStartX) / (fadeInEndX - fadeInStartX); self.gfx.alpha = targetAlpha * fadeInProgress; if (fadeInProgress >= 1) { self.hasFadedIn = true; } } else if (self.hasFadedIn && self.x >= fadeOutStartX && self.x <= fadeOutEndX) { // Calculate fade out progress var fadeOutProgress = (self.x - fadeOutStartX) / (fadeOutEndX - fadeOutStartX); self.gfx.alpha = targetAlpha * (1 - fadeOutProgress); } else if (self.hasFadedIn && self.x > fadeInEndX && self.x < fadeOutStartX) { // Maintain full opacity in middle section self.gfx.alpha = targetAlpha; } } else { // Moving left: fade in then fade out (reversed positions) if (!self.hasFadedIn && self.x <= fadeOutEndX && self.x >= fadeOutStartX) { // Calculate fade in progress (reversed) var fadeInProgress = (fadeOutEndX - self.x) / (fadeOutEndX - fadeOutStartX); self.gfx.alpha = targetAlpha * fadeInProgress; if (fadeInProgress >= 1) { self.hasFadedIn = true; } } else if (self.hasFadedIn && self.x <= fadeInEndX && self.x >= fadeInStartX) { // Calculate fade out progress (reversed) var fadeOutProgress = (fadeInEndX - self.x) / (fadeInEndX - fadeInStartX); self.gfx.alpha = targetAlpha * (1 - fadeOutProgress); } else if (self.hasFadedIn && self.x < fadeOutStartX && self.x > fadeInEndX) { // Maintain full opacity in middle section self.gfx.alpha = targetAlpha; } } // Check if completely off-screen var currentWidth = self.gfx.width * self.gfx.scale.x; if (self.x < -currentWidth || self.x > 2048 + currentWidth) { self.isDone = true; } }; return self; }); var FeedbackIndicator = Container.expand(function (type) { var self = Container.call(this); var indicator = self.attachAsset(type + 'Indicator', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); self.show = function () { indicator.alpha = 1; indicator.scaleX = 0.5; indicator.scaleY = 0.5; tween(indicator, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 600, easing: tween.easeOut }); }; return self; }); /**** * Title Screen ****/ var Fish = Container.expand(function (type, value, speed, lane) { var self = Container.call(this); var assetName = type + 'Fish'; self.fishGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); if (speed > 0) { // Moving right (coming from left), flip horizontally self.fishGraphics.scaleX = -1; } self.type = type; self.value = value; self.speed = speed; self.lane = lane; // Index of the lane (0, 1, or 2) self.caught = false; self.isSpecial = type === 'rare'; // For shimmer effect self.shimmerTime = 0; // Bubble properties self.lastBubbleSpawnTime = 0; self.bubbleSpawnInterval = 120 + Math.random() * 80; // 120-200ms interval (fewer bubbles) // Add properties for swimming animation self.swimTime = Math.random() * Math.PI * 2; // Random starting phase for variety self.baseY = self.y; // Store initial Y position self.scaleTime = 0; self.baseScale = 1; self.update = function () { if (!self.caught) { // Horizontal movement self.x += self.speed; // Sine wave vertical movement self.swimTime += 0.08; // Speed of sine wave oscillation var swimAmplitude = 15; // Pixels of vertical movement self.y = self.baseY + Math.sin(self.swimTime) * swimAmplitude; // Beat-synchronized scale pulsing if (GameState.gameActive && GameState.songStartTime > 0) { var currentTime = LK.ticks * (1000 / 60); var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var timeSinceLastBeat = (currentTime - GameState.songStartTime) % beatInterval; var beatProgress = timeSinceLastBeat / beatInterval; // Create a pulse effect that peaks at the beat var scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15; // 15% scale variation // Determine base scaleX considering direction var baseScaleXDirection = (self.speed > 0 ? -1 : 1) * self.baseScale; self.fishGraphics.scaleX = baseScaleXDirection * scalePulse; self.fishGraphics.scaleY = scalePulse * self.baseScale; } if (self.isSpecial) { // Shimmer effect for rare fish self.shimmerTime += 0.1; self.fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2; } else { // Reset alpha if not special self.fishGraphics.alpha = 1.0; } } }; self.catchFish = function () { self.caught = true; // Animation to boat tween(self, { y: GAME_CONFIG.BOAT_Y, // Target Y: boat position x: GAME_CONFIG.SCREEN_CENTER_X, //{r} // Target X: center of screen (boat position) scaleX: 0.5, scaleY: 0.5, alpha: 0 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); var MusicNoteParticle = Container.expand(function (startX, startY) { var self = Container.call(this); var FADE_IN_DURATION_MS = 600; // Duration for the note to fade in var TARGET_ALPHA = 0.6 + Math.random() * 0.4; // Target alpha between 0.6 and 1.0 for variety self.gfx = self.attachAsset('musicnote', { anchorX: 0.5, anchorY: 0.5, alpha: 0, // Start invisible for fade-in scaleX: 0.4 + Math.random() * 0.4 // Random initial scale (0.4x to 0.8x) }); self.gfx.scaleY = self.gfx.scaleX; // Maintain aspect ratio self.x = startX; self.y = startY; // Animation properties for lazy floating self.vx = (Math.random() - 0.5) * 0.8; // Slow horizontal drift speed self.vy = -(0.8 + Math.random() * 0.7); // Steady upward speed (0.8 to 1.5 pixels/frame) self.rotationSpeed = (Math.random() - 0.5) * 0.008; // Very slow rotation self.life = 240 + Math.random() * 120; // Lifespan in frames (4 to 6 seconds) self.age = 0; self.isDone = false; // Initial fade-in tween tween(self.gfx, { alpha: TARGET_ALPHA }, { duration: FADE_IN_DURATION_MS, easing: tween.easeOut }); self.update = function () { if (self.isDone) { return; } self.age++; self.x += self.vx; self.y += self.vy; self.gfx.rotation += self.rotationSpeed; var FADE_IN_TICKS = FADE_IN_DURATION_MS / (1000 / 60); // Fade-in duration in ticks // Only manage alpha manually after the fade-in tween is expected to be complete. if (self.age > FADE_IN_TICKS) { var lifePortionForFadeOut = 0.6; // Use last 60% of life for fade out var fadeOutStartTimeTicks = self.life * (1 - lifePortionForFadeOut); if (self.age >= fadeOutStartTimeTicks && self.life > fadeOutStartTimeTicks) { // ensure self.life > fadeOutStartTimeTicks to avoid division by zero var progressInFadeOut = (self.age - fadeOutStartTimeTicks) / (self.life * lifePortionForFadeOut); self.gfx.alpha = TARGET_ALPHA * (1 - progressInFadeOut); self.gfx.alpha = Math.max(0, self.gfx.alpha); // Clamp at 0 } else if (self.age <= fadeOutStartTimeTicks) { // At this point, the initial fade-in tween to TARGET_ALPHA should have completed. // The alpha value is expected to remain at TARGET_ALPHA (as set by the initial tween) // until the fade-out logic (in the 'if (self.age >= fadeOutStartTimeTicks)' block) begins. // The previous 'tween.isTweening' check was removed as the method does not exist in the plugin. } } // Check if particle's life is over or it has faded out if (self.age >= self.life || self.gfx.alpha !== undefined && self.gfx.alpha <= 0.01 && self.age > FADE_IN_TICKS) { self.isDone = true; } }; return self; }); var OceanBubbleParticle = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('oceanbubbles', { anchorX: 0.5, anchorY: 0.5 }); self.initialX = Math.random() * 2048; var waterTop = GAME_CONFIG.WATER_SURFACE_Y; var waterBottom = 2732; self.x = self.initialX; // Allow bubbles to spawn anywhere in the water, not just below the bottom self.y = waterTop + Math.random() * (waterBottom - waterTop); var baseScale = 0.1 + Math.random() * 0.4; // Scale: 0.1x to 0.5x self.gfx.scale.set(baseScale); self.vy = -(0.25 + Math.random() * 0.5); // Upward speed: 0.25 to 0.75 pixels/frame (slower, less variance) self.naturalVy = self.vy; // Store natural velocity for recovery self.driftAmplitude = 20 + Math.random() * 40; // Sideways drift: 20px to 60px amplitude self.naturalDriftAmplitude = self.driftAmplitude; // Store natural drift for recovery self.driftFrequency = (0.005 + Math.random() * 0.015) * (Math.random() < 0.5 ? 1 : -1); // Sideways drift speed/direction self.driftPhase = Math.random() * Math.PI * 2; // Initial phase for sine wave self.rotationSpeed = (Math.random() - 0.5) * 0.01; // Slow random rotation var targetAlpha = 0.2 + Math.random() * 0.3; // Max alpha: 0.2 to 0.5 (dimmer background bubbles) self.gfx.alpha = 0; // Start transparent self.isDone = false; self.fadingOut = false; tween(self.gfx, { alpha: targetAlpha }, { duration: 1000 + Math.random() * 1000, // Slow fade in: 1 to 2 seconds easing: tween.easeIn }); self.update = function () { if (self.isDone) { return; } self.y += self.vy; // Increment age self.age++; // Check if lifespan exceeded if (!self.fadingOut && self.age >= self.lifespan) { self.fadingOut = true; tween.stop(self.gfx); tween(self.gfx, { alpha: 0 }, { duration: 600 + Math.random() * 400, // 0.6-1 second fade easing: tween.easeOut, onFinish: function onFinish() { self.isDone = true; } }); } self.driftPhase += self.driftFrequency; self.x = self.initialX + Math.sin(self.driftPhase) * self.driftAmplitude; self.gfx.rotation += self.rotationSpeed; // Recovery mechanism: gradually return to natural upward movement var naturalVy = -(0.25 + Math.random() * 0.5); // Natural upward speed var recoveryRate = 0.02; // How quickly bubble recovers (2% per frame) // If bubble is moving slower than its natural speed or downward, recover if (self.vy > naturalVy) { self.vy = self.vy + (naturalVy - self.vy) * recoveryRate; } // Also gradually reduce excessive drift amplitude back to normal var normalDriftAmplitude = 20 + Math.random() * 40; if (self.driftAmplitude > normalDriftAmplitude) { self.driftAmplitude = self.driftAmplitude + (normalDriftAmplitude - self.driftAmplitude) * recoveryRate; } // Check if bubble reached surface or went off-screen // Use gfx.height * current scale for accurate boundary check var currentHeight = self.gfx.height * self.gfx.scale.y; var currentWidth = self.gfx.width * self.gfx.scale.x; if (!self.fadingOut && self.y <= GAME_CONFIG.WATER_SURFACE_Y - currentHeight * 0.5) { self.fadingOut = true; tween.stop(self.gfx); // Stop fade-in if ongoing tween(self.gfx, { alpha: 0 }, { duration: 300 + Math.random() * 200, // Quick fade out easing: tween.easeOut, onFinish: function onFinish() { self.isDone = true; // Mark as fully done for removal } }); } else if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth)) { // Off screen top/sides before reaching surface and starting fade self.isDone = true; self.gfx.alpha = 0; // Disappear immediately } }; return self; }); var SeaweedParticle = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('kelp', { anchorX: 0.5, anchorY: 0.5 }); // Determine spawn location var spawnType = Math.random(); var waterTop = GAME_CONFIG.WATER_SURFACE_Y; var waterBottom = 2732; if (spawnType < 0.4) { // 40% spawn from bottom self.x = Math.random() * 2048; self.y = waterBottom + 50; self.vx = (Math.random() - 0.5) * 0.3; // Slight horizontal drift self.vy = -(0.4 + Math.random() * 0.3); // Upward movement } else if (spawnType < 0.7) { // 30% spawn from left self.x = -50; self.y = waterTop + Math.random() * (waterBottom - waterTop); self.vx = 0.4 + Math.random() * 0.3; // Rightward movement self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift } else { // 30% spawn from right self.x = 2048 + 50; self.y = waterTop + Math.random() * (waterBottom - waterTop); self.vx = -(0.4 + Math.random() * 0.3); // Leftward movement self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift } self.initialX = self.x; self.naturalVx = self.vx; // Store natural velocity for recovery self.naturalVy = self.vy; // Seaweed properties var baseScale = 0.6 + Math.random() * 0.6; // Scale: 0.6x to 1.2x self.gfx.scale.set(baseScale); self.swayAmplitude = 15 + Math.random() * 25; // Sway: 15px to 40px self.swayFrequency = (0.003 + Math.random() * 0.007) * (Math.random() < 0.5 ? 1 : -1); self.swayPhase = Math.random() * Math.PI * 2; // Random initial rotation (full 360 degrees) self.gfx.rotation = Math.random() * Math.PI * 2; // Random continuous rotation speed (slower than ocean bubbles) self.continuousRotationSpeed = (Math.random() - 0.5) * 0.003; // -0.0015 to 0.0015 radians per frame var targetAlpha = 0.3 + Math.random() * 0.3; // Alpha: 0.3 to 0.6 self.gfx.alpha = 0; // Start transparent self.isDone = false; self.fadingOut = false; self.reachedSurface = false; // Add random lifespan (10-30 seconds) self.lifespan = 600 + Math.random() * 1200; // 600-1800 frames (10-30 seconds at 60fps) self.age = 0; tween(self.gfx, { alpha: targetAlpha }, { duration: 1500 + Math.random() * 1000, easing: tween.easeIn }); self.update = function () { if (self.isDone) { return; } // Apply movement self.x += self.vx; self.y += self.vy; // Add sway effect self.swayPhase += self.swayFrequency; var swayOffset = Math.sin(self.swayPhase) * self.swayAmplitude; // Apply continuous rotation plus sway-based rotation self.gfx.rotation += self.continuousRotationSpeed + swayOffset * 0.0001; // Reduced sway rotation influence // Recovery mechanism for velocity var recoveryRate = 0.015; if (self.vx !== self.naturalVx) { self.vx = self.vx + (self.naturalVx - self.vx) * recoveryRate; } if (self.vy !== self.naturalVy) { self.vy = self.vy + (self.naturalVy - self.vy) * recoveryRate; } // Check if reached surface var currentHeight = self.gfx.height * self.gfx.scale.y; var currentWidth = self.gfx.width * self.gfx.scale.x; if (!self.reachedSurface && self.y <= GAME_CONFIG.WATER_SURFACE_Y + currentHeight * 0.3) { self.reachedSurface = true; // Change to horizontal drift at surface self.vy = 0; self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.5 + Math.random() * 0.5); self.naturalVx = self.vx; self.naturalVy = 0; } // Check if off-screen if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth || self.y > waterBottom + currentHeight)) { self.fadingOut = true; tween.stop(self.gfx); tween(self.gfx, { alpha: 0 }, { duration: 400 + Math.random() * 200, easing: tween.easeOut, onFinish: function onFinish() { self.isDone = true; } }); } }; return self; }); /**** * Initialize Game ****/ /**** * Screen Containers ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ // Constants for Title Screen Animation var TITLE_ANIM_CONSTANTS = { INITIAL_GROUP_ALPHA: 0, FINAL_GROUP_ALPHA: 1, INITIAL_UI_ALPHA: 0, FINAL_UI_ALPHA: 1, INITIAL_GROUP_SCALE: 3.5, // Start with an extreme closeup FINAL_GROUP_SCALE: 2.8, // Zoom out slightly, staying closer GROUP_ANIM_DURATION: 4000, // Slower duration for group fade-in and zoom TEXT_FADE_DURATION: 1000, // Duration for title text fade-in BUTTON_FADE_DURATION: 800, // Duration for buttons fade-in // Positioning constants relative to titleAnimationGroup's origin (0,0) which is boat's center BOAT_ANCHOR_X: 0.5, BOAT_ANCHOR_Y: 0.5, FISHERMAN_ANCHOR_X: 0.5, FISHERMAN_ANCHOR_Y: 0.9, // Anchor at feet FISHERMAN_X_OFFSET: -20, // Relative to boat center FISHERMAN_Y_OFFSET: -100, // Relative to boat center, fisherman sits on boat LINE_ANCHOR_X: 0.5, LINE_ANCHOR_Y: 0, // Anchor at top of line LINE_X_OFFSET_FROM_FISHERMAN: 70, // Rod tip X from fisherman center LINE_Y_OFFSET_FROM_FISHERMAN: -130, // Rod tip Y from fisherman center (fisherman height ~200, anchorY 0.9) HOOK_ANCHOR_X: 0.5, HOOK_ANCHOR_Y: 0.5, HOOK_Y_DEPTH_FROM_LINE_START: 700, // Slightly longer line for the closeup // titleAnimationGroup positioning GROUP_PIVOT_X: 0, // Boat's X in the group GROUP_PIVOT_Y: 0, // Boat's Y in the group GROUP_INITIAL_Y_SCREEN_OFFSET: -450 // Adjusted for closer initial zoom }; // If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up. /**** * Pattern Generation System ****/ var PatternGenerator = { lastLane: -1, minDistanceBetweenFish: 300, // Used by spawnFish internal check // Minimum X distance between fish for visual clarity on hook lastActualSpawnTime: -100000, // Time of the last actual fish spawn getNextLane: function getNextLane() { if (this.lastLane === -1) { // First fish, start in middle lane this.lastLane = 1; return 1; } // Prefer staying in same lane or moving to adjacent lane var possibleLanes = [this.lastLane]; // Add adjacent lanes if (this.lastLane > 0) { possibleLanes.push(this.lastLane - 1); } if (this.lastLane < 2) { possibleLanes.push(this.lastLane + 1); } // 70% chance to stay in same/adjacent lane if (Math.random() < 0.7) { this.lastLane = possibleLanes[Math.floor(Math.random() * possibleLanes.length)]; } else { // 30% chance for any lane this.lastLane = Math.floor(Math.random() * 3); } return this.lastLane; }, // New method: Checks if enough time has passed since the last spawn. canSpawnFishOnBeat: function canSpawnFishOnBeat(currentTime, configuredSpawnInterval) { var timeSinceLast = currentTime - this.lastActualSpawnTime; var minRequiredGap = configuredSpawnInterval; // Default gap is the song's beat interval for fish return timeSinceLast >= minRequiredGap; }, // New method: Registers details of the fish that was just spawned. registerFishSpawn: function registerFishSpawn(spawnTime) { this.lastActualSpawnTime = spawnTime; }, reset: function reset() { this.lastLane = -1; this.lastActualSpawnTime = -100000; // Set far in the past to allow first spawn } }; /**** * Game Configuration ****/ game.up = function (x, y, obj) { // Note: We don't play buttonClick sound on 'up' typically, only on 'down'. switch (GameState.currentScreen) { case 'title': // title screen up actions (if any) break; case 'levelSelect': // level select screen up actions (if any, usually 'down' is enough for buttons) break; case 'fishing': handleFishingInput(x, y, false); // false for isUp break; case 'results': // results screen up actions (if any) break; } }; var GAME_CONFIG = { SCREEN_CENTER_X: 1024, SCREEN_CENTER_Y: 900, // Adjusted, though less critical with lanes BOAT_Y: 710, // Original 300 + 273 (10%) + 137 (5%) = 710 WATER_SURFACE_Y: 760, // Original 350 + 273 (10%) + 137 (5%) = 760 // 3 Lane System // Y-positions for each lane, adjusted downwards further LANES: [{ y: 1133, // Top lane Y: (723 + 273) + 137 = 996 + 137 = 1133 name: "shallow" }, { y: 1776, // Middle lane Y: (996 + 643) + 137 = 1639 + 137 = 1776 name: "medium" }, { y: 2419, // Bottom lane Y: (1639 + 643) + 137 = 2282 + 137 = 2419 name: "deep" }], // Timing windows PERFECT_WINDOW: 40, GOOD_WINDOW: 80, MISS_WINDOW: 120, // Depth levels - reduced money values! DEPTHS: [{ level: 1, name: "Shallow Waters", fishSpeed: 6, fishValue: 0.5, upgradeCost: 0, // Starting depth songs: [{ name: "Gentle Waves", bpm: 90, // Changed from 95 to 90 duration: 202000, // 3:22 pattern: "gentle_waves_custom", cost: 0 }, { name: "Morning Tide", bpm: 90, duration: 156827, pattern: "morning_tide_custom", cost: 50, musicId: 'morningtide' }] }, { level: 2, name: "Mid Waters", fishSpeed: 7, fishValue: 1.5, upgradeCost: 100, songs: [{ name: "Ocean Current", bpm: 120, duration: 90000, pattern: "medium", cost: 0 }, { name: "Deep Flow", bpm: 125, duration: 100000, pattern: "medium", cost: 150 }] }, { level: 3, name: "Deep Waters", fishSpeed: 8, fishValue: 3, upgradeCost: 400, songs: [{ name: "Storm Surge", bpm: 140, duration: 120000, pattern: "complex", cost: 0 }, { name: "Whirlpool", bpm: 150, duration: 135000, pattern: "complex", cost: 300 }] }, { level: 4, name: "Abyss", fishSpeed: 9, fishValue: 6, upgradeCost: 1000, songs: [{ name: "Leviathan", bpm: 160, duration: 150000, pattern: "expert", cost: 0 }, { name: "Deep Trench", bpm: 170, duration: 180000, pattern: "expert", cost: 600 }] }], // Updated patterns PATTERNS: { simple: { beatsPerFish: 2, // Increased from 1 - fish every 2 beats doubleSpawnChance: 0.10, // 10% chance of a double beat in simple rareSpawnChance: 0.02 }, medium: { beatsPerFish: 1.5, // Increased from 0.75 doubleSpawnChance: 0.15, //{1R} // 15% chance of a double beat rareSpawnChance: 0.05 }, complex: { beatsPerFish: 1, // Increased from 0.5 doubleSpawnChance: 0.25, //{1U} // 25% chance of a double beat rareSpawnChance: 0.08 }, expert: { beatsPerFish: 0.75, // Increased from 0.25 doubleSpawnChance: 0.35, //{1X} // 35% chance of a double beat tripletSpawnChance: 0.20, // 20% chance a double beat becomes a triplet rareSpawnChance: 0.12 }, gentle_waves_custom: { beatsPerFish: 1.5, // Slightly more frequent than default simple doubleSpawnChance: 0.05, // Very rare double beats for shallow rareSpawnChance: 0.01, // Almost no rare fish // Custom timing sections based on the musical structure sections: [ // Opening - Simple chord pattern (0-30 seconds) { startTime: 0, endTime: 30000, spawnModifier: 1.0, // Normal spawn rate description: "steady_chords" }, // Melody Introduction (30-60 seconds) { startTime: 30000, endTime: 60000, spawnModifier: 0.9, // Slightly fewer fish description: "simple_melody" }, // Development (60-120 seconds) - Gets a bit busier { startTime: 60000, endTime: 120000, spawnModifier: 1.1, // Slightly more fish description: "melody_development" }, // Climax (120-180 seconds) - Busiest section but still shallow { startTime: 120000, endTime: 180000, spawnModifier: 1.3, // More fish, but not overwhelming description: "gentle_climax" }, // Ending (180-202 seconds) - Calming down { startTime: 180000, endTime: 202000, spawnModifier: 0.8, // Fewer fish for gentle ending description: "peaceful_ending" }] }, morning_tide_custom: { beatsPerFish: 1.2, // More frequent than gentle_waves_custom (1.5) and much more than simple (2) doubleSpawnChance: 0.12, // Higher than simple (0.10) rareSpawnChance: 0.03, // Higher than simple (0.02) // Custom timing sections based on musical structure sections: [ // Gentle opening - ease into the song (0-25 seconds) { startTime: 0, endTime: 25000, spawnModifier: 0.9, // Slightly reduced for intro description: "calm_opening" }, // Building energy - first wave (25-50 seconds) { startTime: 25000, endTime: 50000, spawnModifier: 1.2, // More active description: "first_wave" }, // Peak intensity - morning rush (50-80 seconds) { startTime: 50000, endTime: 80000, spawnModifier: 1.5, // Most intense section description: "morning_rush" }, // Sustained energy - second wave (80-110 seconds) { startTime: 80000, endTime: 110000, spawnModifier: 1.3, // High but slightly less than peak description: "second_wave" }, // Climactic finish (110-140 seconds) { startTime: 110000, endTime: 140000, spawnModifier: 1.4, // Building back up description: "climactic_finish" }, // Gentle fade out (140-156.8 seconds) { startTime: 140000, endTime: 156827, spawnModifier: 0.8, // Calm ending description: "peaceful_fade" }] } } }; /**** * Game State Management ****/ var MULTI_BEAT_SPAWN_DELAY_MS = 250; // ms delay for sequential spawns in multi-beats (increased for more space) var TRIPLET_BEAT_SPAWN_DELAY_MS = 350; // ms delay for third fish in a triplet (even more space) var ImprovedRhythmSpawner = { nextBeatToSchedule: 1, scheduledBeats: [], lastFrameTime: 0, actualFrameTime: 1000 / 60, update: function update(currentTime) { if (!GameState.gameActive || GameState.songStartTime === 0) { return; } // Update actual frame time for accurate calculations if (this.lastFrameTime > 0) { this.actualFrameTime = currentTime - this.lastFrameTime; } this.lastFrameTime = currentTime; var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; // Calculate how far ahead to look (using actual frame time) var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var travelTimeMs = distanceToHook / fishSpeed * this.actualFrameTime; var beatsAhead = Math.ceil(travelTimeMs / spawnInterval) + 2; // Schedule beats ahead var songElapsed = currentTime - GameState.songStartTime; var currentSongBeat = songElapsed / spawnInterval; var maxBeatToSchedule = Math.floor(currentSongBeat) + beatsAhead; while (this.nextBeatToSchedule <= maxBeatToSchedule) { if (this.scheduledBeats.indexOf(this.nextBeatToSchedule) === -1) { this.scheduleBeatFish(this.nextBeatToSchedule, spawnInterval, travelTimeMs); this.scheduledBeats.push(this.nextBeatToSchedule); } this.nextBeatToSchedule++; } // Update existing fish positions for beat sync this.updateFishSync(currentTime); }, scheduleBeatFish: function scheduleBeatFish(beatNumber, spawnInterval, travelTimeMs) { var targetArrivalTime = GameState.songStartTime + beatNumber * spawnInterval; var spawnTime = targetArrivalTime - travelTimeMs; var currentTime = this.lastFrameTime; if (spawnTime >= currentTime - 100) { var delay = Math.max(0, spawnTime - currentTime); var self = this; LK.setTimeout(function () { if (GameState.gameActive && GameState.songStartTime !== 0) { self.spawnRhythmFish(beatNumber, targetArrivalTime); } }, delay); } }, spawnRhythmFish: function spawnRhythmFish(beatNumber, targetArrivalTime) { if (!GameState.gameActive) return; var currentTime = this.lastFrameTime; var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Calculate spawnInterval for this pattern - ADD THIS LINE var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; // Apply section modifiers if pattern has sections var spawnModifier = 1.0; if (pattern.sections) { var songElapsed = currentTime - GameState.songStartTime; for (var s = 0; s < pattern.sections.length; s++) { var section = pattern.sections[s]; if (songElapsed >= section.startTime && songElapsed <= section.endTime) { spawnModifier = section.spawnModifier; break; } } } // Check if we should spawn (with section modifier) if (!PatternGenerator.canSpawnFishOnBeat(currentTime, beatInterval)) { return; } // Apply spawn modifier chance if (Math.random() > spawnModifier) { return; } var laneIndex = PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; // Fish type selection (original logic) var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; } else if (GameState.selectedDepth >= 2 && rand < 0.3) { fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; fishValue = depthConfig.fishValue; } // Calculate exact speed for beat sync var timeRemainingMs = targetArrivalTime - currentTime; if (timeRemainingMs <= 0) return; var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var spawnSide = Math.random() < 0.5 ? -1 : 1; var framesRemaining = timeRemainingMs / this.actualFrameTime; if (framesRemaining <= 0) return; var requiredSpeed = distanceToHook / framesRemaining; if (spawnSide === 1) { requiredSpeed *= -1; } // Create fish with sync data var newFish = new Fish(fishType, fishValue, requiredSpeed, laneIndex); newFish.spawnSide = spawnSide; newFish.targetArrivalTime = targetArrivalTime; newFish.x = requiredSpeed > 0 ? -150 : 2048 + 150; newFish.y = targetLane.y; newFish.baseY = targetLane.y; // Add sync data for position correction newFish.syncData = { startX: newFish.x, targetX: GAME_CONFIG.SCREEN_CENTER_X, startTime: currentTime, arrivalTime: targetArrivalTime, originalSpeed: requiredSpeed }; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTime); // Handle multi-spawns - NOW spawnInterval IS DEFINED this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime); return newFish; }, handleMultiSpawns: function handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime) { if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { var travelTimeMs = this.calculateTravelTime(); var nextFishBeat = beatNumber + 1; if (this.scheduledBeats.indexOf(nextFishBeat) === -1) { this.scheduleBeatFish(nextFishBeat, spawnInterval, travelTimeMs); this.scheduledBeats.push(nextFishBeat); } // Handle triplets if (pattern.tripletSpawnChance && pattern.tripletSpawnChance > 0 && Math.random() < pattern.tripletSpawnChance) { var thirdFishBeat = beatNumber + 2; if (this.scheduledBeats.indexOf(thirdFishBeat) === -1) { this.scheduleBeatFish(thirdFishBeat, spawnInterval, travelTimeMs); this.scheduledBeats.push(thirdFishBeat); } } } }, calculateTravelTime: function calculateTravelTime() { var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); if (fishSpeed === 0) { return Infinity; } var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; return distanceToHook / fishSpeed * this.actualFrameTime; }, updateFishSync: function updateFishSync(currentTime) { // Correct fish positions to maintain beat synchronization for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (!fish.caught && fish.syncData) { var totalTime = fish.syncData.arrivalTime - fish.syncData.startTime; var elapsed = currentTime - fish.syncData.startTime; var progress = Math.max(0, Math.min(1, elapsed / totalTime)); // Interpolate to ensure exact arrival time var targetX = fish.syncData.startX + (fish.syncData.targetX - fish.syncData.startX) * progress; // Apply correction if fish has drifted var drift = Math.abs(fish.x - targetX); if (drift > 10) { // Only correct significant drift fish.x = targetX; } } } }, reset: function reset() { this.nextBeatToSchedule = 1; this.scheduledBeats = []; this.lastFrameTime = 0; this.actualFrameTime = 1000 / 60; }, getDebugInfo: function getDebugInfo() { return { nextBeat: this.nextBeatToSchedule, scheduledCount: this.scheduledBeats.length, scheduledBeatsPreview: this.scheduledBeats.slice(-10), frameTime: this.actualFrameTime }; } }; var GameState = { // Game flow currentScreen: 'title', // 'title', 'levelSelect', 'fishing', 'results' // Player progression currentDepth: 0, money: 0, totalFishCaught: 0, ownedSongs: [], // Array of {depth, songIndex} objects // Level selection selectedDepth: 0, selectedSong: 0, // Current session sessionScore: 0, sessionFishCaught: 0, sessionFishSpawned: 0, combo: 0, maxCombo: 0, // Game state gameActive: false, songStartTime: 0, lastBeatTime: 0, beatCount: 0, introPlaying: false, // Tracks if the intro animation is currently playing musicNotesActive: false, // Tracks if music note particle system is active currentPlayingMusicId: 'rhythmTrack', // ID of the music track currently playing in a session currentPlayingMusicInitialVolume: 0.8, // Initial volume of the current music track for fade reference hookTargetLaneIndex: 1, // Start with hook targeting the middle lane (index 1) // Initialize owned songs (first song of each unlocked depth is free) initOwnedSongs: function initOwnedSongs() { this.ownedSongs = []; for (var i = 0; i <= this.currentDepth; i++) { this.ownedSongs.push({ depth: i, songIndex: 0 }); } }, hasSong: function hasSong(depth, songIndex) { return this.ownedSongs.some(function (song) { return song.depth === depth && song.songIndex === songIndex; }); }, buySong: function buySong(depth, songIndex) { var song = GAME_CONFIG.DEPTHS[depth].songs[songIndex]; if (this.money >= song.cost && !this.hasSong(depth, songIndex)) { this.money -= song.cost; this.ownedSongs.push({ depth: depth, songIndex: songIndex }); return true; } return false; }, getCurrentDepthConfig: function getCurrentDepthConfig() { return GAME_CONFIG.DEPTHS[this.selectedDepth]; }, getCurrentSongConfig: function getCurrentSongConfig() { return GAME_CONFIG.DEPTHS[this.selectedDepth].songs[this.selectedSong]; }, canUpgrade: function canUpgrade() { var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1]; return nextDepth && this.money >= nextDepth.upgradeCost; }, upgrade: function upgrade() { if (this.canUpgrade()) { var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1]; this.money -= nextDepth.upgradeCost; this.currentDepth++; // Give free first song of new depth this.ownedSongs.push({ depth: this.currentDepth, songIndex: 0 }); return true; } return false; } }; var titleScreen = game.addChild(new Container()); var levelSelectScreen = game.addChild(new Container()); var fishingScreen = game.addChild(new Container()); var resultsScreen = game.addChild(new Container()); // Initialize GameState.initOwnedSongs(); /**** * Title Screen ****/ /**** * Title Screen - FIXED VERSION ****/ function createTitleScreen() { var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 1, height: 2732, color: 0x87CEEB })); // ANIMATED CONTAINER GROUP - This will contain ALL visual elements including backgrounds var titleAnimationGroup = titleScreen.addChild(new Container()); // Sky background image - NOW INSIDE animation group var titleSky = titleAnimationGroup.addChild(LK.getAsset('skybackground', { x: 0, y: -500 })); // Water background image - NOW INSIDE animation group var titleWater = titleAnimationGroup.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, width: 2048, height: 2732 - GAME_CONFIG.WATER_SURFACE_Y })); // Initialize containers for title screen ambient particles - INSIDE animation group titleScreenOceanBubbleContainer = titleAnimationGroup.addChild(new Container()); titleScreenSeaweedContainer = titleAnimationGroup.addChild(new Container()); titleScreenCloudContainer = titleAnimationGroup.addChild(new Container()); // Create a single container for boat, fisherman, and line that all move together var titleBoatGroup = titleAnimationGroup.addChild(new Container()); // Boat positioned at origin of the group var titleBoat = titleBoatGroup.addChild(LK.getAsset('boat', { anchorX: 0.5, anchorY: 0.74, x: 0, // Relative to group y: 0 // Relative to group })); // Fisherman positioned relative to boat within the same group var titleFisherman = titleBoatGroup.addChild(LK.getAsset('fisherman', { anchorX: 0.5, anchorY: 1, x: -100, // Relative to boat position y: -70 // Relative to boat position })); // Fishing line positioned relative to boat within the same group var rodTipX = -100 + 85; // fisherman offset + rod offset from fisherman center var rodTipY = -70 - 200; // fisherman y (feet) - fisherman height (to get to head area for rod) // initialHookY needs to be relative to the group's origin (which is boat's origin at WATER_SURFACE_Y) // GAME_CONFIG.LANES[1].y is an absolute world Y. // titleBoatGroup.y is GAME_CONFIG.WATER_SURFACE_Y. // So, hook's Y relative to group = absolute hook Y - group's absolute Y var initialHookYInGroup = GAME_CONFIG.LANES[1].y - GAME_CONFIG.WATER_SURFACE_Y; var titleLine = titleBoatGroup.addChild(LK.getAsset('fishingLine', { anchorX: 0.5, anchorY: 0, x: rodTipX, y: rodTipY, // Relative to group width: 6, height: initialHookYInGroup - rodTipY // Length from rod tip to hook, all relative to group })); var titleHook = titleBoatGroup.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: rodTipX, // Hook X matches line X initially y: initialHookYInGroup // Relative to group })); // Position the entire group in the world (within titleAnimationGroup) titleBoatGroup.x = GAME_CONFIG.SCREEN_CENTER_X; titleBoatGroup.y = GAME_CONFIG.WATER_SURFACE_Y; // Store base position for wave animation var boatGroupBaseY = titleBoatGroup.y; // This is the group's world Y var boatWaveAmplitude = 10; var boatWaveHalfCycleDuration = 2000; var boatRotationAmplitude = 0.03; var boatRotationDuration = 3000; // Wave animation variables for line (used in updateTitleFishingLineWave) var lineWaveAmplitude = 12; var lineWaveSpeed = 0.03; var linePhaseOffset = 0; // Animated Water Surface segments - INSIDE animation group var titleWaterSurfaceSegments = []; var NUM_WAVE_SEGMENTS_TITLE = 32; var SEGMENT_WIDTH_TITLE = 2048 / NUM_WAVE_SEGMENTS_TITLE; var SEGMENT_HEIGHT_TITLE = 24; var WAVE_AMPLITUDE_TITLE = 12; var WAVE_HALF_PERIOD_MS_TITLE = 2500; var PHASE_DELAY_MS_PER_SEGMENT_TITLE = WAVE_HALF_PERIOD_MS_TITLE * 2 / NUM_WAVE_SEGMENTS_TITLE; // Create water surface segments for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) { var segment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH_TITLE, y: GAME_CONFIG.WATER_SURFACE_Y, width: SEGMENT_WIDTH_TITLE + 1, height: SEGMENT_HEIGHT_TITLE, anchorX: 0, anchorY: 0.5, alpha: 0.8, tint: 0x4fc3f7 }); segment.baseY = GAME_CONFIG.WATER_SURFACE_Y; titleAnimationGroup.addChild(segment); // Water surface is part of main animation group, not boat group titleWaterSurfaceSegments.push(segment); } for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) { var whiteSegment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH_TITLE, y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2, width: SEGMENT_WIDTH_TITLE + 1, height: SEGMENT_HEIGHT_TITLE / 2, anchorX: 0, anchorY: 0.5, alpha: 0.6, tint: 0xffffff }); whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2; titleAnimationGroup.addChild(whiteSegment); // Water surface is part of main animation group titleWaterSurfaceSegments.push(whiteSegment); } // Animation group setup for zoom effect - PROPERLY CENTER THE BOAT ON SCREEN var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; // 1024 var targetBoatScreenY = GAME_CONFIG.SCREEN_CENTER_Y + 300; // Change +100 to move boat lower/higher var boatWorldY = GAME_CONFIG.WATER_SURFACE_Y; // 760 - where titleBoatGroup actually is var pivotY = boatWorldY - (targetBoatScreenY - boatWorldY); // Calculate proper pivot offset // Set pivot to calculated position and position group so boat ends up at screen center titleAnimationGroup.pivot.set(boatCenterX, pivotY); titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X; // 1024 titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // 1366 - this will center the boat at targetBoatScreenY // Initial zoom state var INITIAL_ZOOM_FACTOR = 3.0; var FINAL_ZOOM_FACTOR = 1.8; // This matches existing logic if it's used in showScreen titleAnimationGroup.scale.set(INITIAL_ZOOM_FACTOR); titleAnimationGroup.alpha = 1; // Main animation group is always visible // Single wave animation function - moves entire group together var targetUpY = boatGroupBaseY - boatWaveAmplitude; // boatGroupBaseY is absolute world Y var targetDownY = boatGroupBaseY + boatWaveAmplitude; // boatGroupBaseY is absolute world Y function moveTitleBoatGroupUp() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } // We tween the group's world Y position tween(titleBoatGroup, { y: targetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveTitleBoatGroupDown }); } function moveTitleBoatGroupDown() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } tween(titleBoatGroup, { y: targetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveTitleBoatGroupUp }); } // Single rotation animation - rotates entire group together function rockTitleBoatGroupLeft() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } tween(titleBoatGroup, { rotation: -boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut, onFinish: rockTitleBoatGroupRight }); } function rockTitleBoatGroupRight() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } tween(titleBoatGroup, { rotation: boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut, onFinish: rockTitleBoatGroupLeft }); } // Simplified line wave function - just the sway effect function updateTitleFishingLineWave() { // titleLine and titleHook are children of titleBoatGroup, their x/y are relative to it. // rodTipX, rodTipY, initialHookYInGroup are also relative to titleBoatGroup. if (!titleLine || titleLine.destroyed || !titleHook || titleHook.destroyed) { return; } linePhaseOffset += lineWaveSpeed; var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude; // Only apply the wave sway to X, positions stay relative to group's origin // rodTipX is the line's base X relative to the group. titleLine.x = rodTipX + waveOffset * 0.3; titleHook.x = rodTipX + waveOffset; // Y positions of line and hook (titleLine.y and titleHook.y) are static relative to the group. // Recalculate line rotation based on hook position relative to line's anchor // All coordinates here are relative to titleBoatGroup. var deltaX = titleHook.x - titleLine.x; // Difference in x relative to group var deltaY = titleHook.y - titleLine.y; // Difference in y relative to group var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY); titleLine.height = actualLineLength; if (actualLineLength > 0.001) { titleLine.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2; } else { titleLine.rotation = 0; } titleHook.rotation = titleLine.rotation; // Hook rotation matches line's } // Water surface animation function function startTitleWaterSurfaceAnimationFunc() { for (var k = 0; k < titleWaterSurfaceSegments.length; k++) { var segment = titleWaterSurfaceSegments[k]; if (!segment || segment.destroyed) { continue; } var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS_TITLE; (function (currentLocalSegment, currentLocalSegmentIndexForDelay) { var animUp, animDown; animDown = function animDown() { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE }, { duration: WAVE_HALF_PERIOD_MS_TITLE, easing: tween.easeInOut, onFinish: animUp }); }; animUp = function animUp() { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY - WAVE_AMPLITUDE_TITLE }, { duration: WAVE_HALF_PERIOD_MS_TITLE, easing: tween.easeInOut, onFinish: animDown }); }; LK.setTimeout(function () { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } // Start with DOWN movement first tween(currentLocalSegment, { y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE }, { duration: WAVE_HALF_PERIOD_MS_TITLE, easing: tween.easeInOut, onFinish: animUp // This should call animUp after going down }); }, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT_TITLE); })(segment, segmentIndexForDelay); } } // BLACK OVERLAY for reveal effect - this goes OVER everything var blackOverlay = titleScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, width: 2048, height: 2732, color: 0x000000, // Pure black alpha: 1 // Starts fully opaque })); // UI elements - OUTSIDE animation group so they don't zoom, ABOVE black overlay var titleImage = titleScreen.addChild(LK.getAsset('titleimage', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 700, // Positioned where the subtitle roughly was, adjust as needed alpha: 0, scaleX: 0.8, // Adjust scale as needed for optimal size scaleY: 0.8 // Adjust scale as needed for optimal size })); // Buttons - OUTSIDE animation group, ABOVE black overlay // New Y positions: 1/3 from bottom of screen (2732 / 3 = 910.66. Y = 2732 - 910.66 = 1821.33) var startButtonY = 2732 - 2732 / 3; // Approx 1821 var tutorialButtonY = startButtonY + 150; // Maintain 150px gap var startButton = titleScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: startButtonY, alpha: 0 })); var startText = titleScreen.addChild(new Text2('START', { size: 50, fill: 0xFFFFFF })); startText.anchor.set(0.5, 0.5); startText.x = GAME_CONFIG.SCREEN_CENTER_X; startText.y = startButtonY; startText.alpha = 0; var tutorialButton = titleScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: tutorialButtonY, tint: 0x757575, alpha: 0 })); var tutorialText = titleScreen.addChild(new Text2('TUTORIAL', { size: 40, fill: 0xFFFFFF })); tutorialText.anchor.set(0.5, 0.5); tutorialText.x = GAME_CONFIG.SCREEN_CENTER_X; tutorialText.y = tutorialButtonY; tutorialText.alpha = 0; return { startButton: startButton, startText: startText, tutorialButton: tutorialButton, tutorialText: tutorialText, titleImage: titleImage, titleAnimationGroup: titleAnimationGroup, blackOverlay: blackOverlay, // Return the group and its specific animation functions titleBoatGroup: titleBoatGroup, moveTitleBoatGroupUp: moveTitleBoatGroupUp, rockTitleBoatGroupLeft: rockTitleBoatGroupLeft, // Individual elements that might still be needed for direct access or other effects titleSky: titleSky, // Sky is not in titleBoatGroup titleWater: titleWater, // Water background is not in titleBoatGroup titleWaterSurfaceSegments: titleWaterSurfaceSegments, // Water surface segments are not in titleBoatGroup // Line and hook are part of titleBoatGroup, but if direct reference is needed for some reason, they could be returned. // However, following the goal's spirit of "Return the group instead of individual elements [that are part of it]" // titleLine and titleHook might be omitted here unless specifically needed by other parts of the code. // For now, including them if they were returned before and are still locally defined. // updateTitleFishingLineWave accesses titleLine and titleHook locally. titleLine: titleLine, // Still defined locally, might be useful for direct reference titleHook: titleHook, // Still defined locally startTitleWaterSurfaceAnimation: startTitleWaterSurfaceAnimationFunc, updateTitleFishingLineWave: updateTitleFishingLineWave // The new simplified version }; } /**** * Level Select Screen ****/ function createLevelSelectScreen() { var selectBg = levelSelectScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.8, height: 2732 })); // Title var title = new Text2('SELECT FISHING SPOT', { size: 80, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.x = GAME_CONFIG.SCREEN_CENTER_X; title.y = 200; levelSelectScreen.addChild(title); // Money display var moneyDisplay = new Text2('Money: $0', { size: 60, fill: 0xFFD700 }); moneyDisplay.anchor.set(1, 0); moneyDisplay.x = 1900; moneyDisplay.y = 100; levelSelectScreen.addChild(moneyDisplay); // Depth tabs (will be created dynamically) var depthTabs = []; // Song display area var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 700 })); // Song navigation arrows var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 400, y: 700, tint: 0x666666 })); var leftArrowText = new Text2('<', { size: 60, fill: 0xFFFFFF }); leftArrowText.anchor.set(0.5, 0.5); leftArrowText.x = 400; leftArrowText.y = 700; levelSelectScreen.addChild(leftArrowText); var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 1648, y: 700, tint: 0x666666 })); var rightArrowText = new Text2('>', { size: 60, fill: 0xFFFFFF }); rightArrowText.anchor.set(0.5, 0.5); rightArrowText.x = 1648; rightArrowText.y = 700; levelSelectScreen.addChild(rightArrowText); // Song info (will be updated dynamically) var songTitle = new Text2('Song Title', { size: 50, fill: 0xFFFFFF }); songTitle.anchor.set(0.5, 0.5); songTitle.x = GAME_CONFIG.SCREEN_CENTER_X; songTitle.y = 650; levelSelectScreen.addChild(songTitle); var songInfo = new Text2('BPM: 120 | Duration: 2:00', { size: 30, fill: 0xCCCCCC }); songInfo.anchor.set(0.5, 0.5); songInfo.x = GAME_CONFIG.SCREEN_CENTER_X; songInfo.y = 700; levelSelectScreen.addChild(songInfo); var songEarnings = new Text2('Potential Earnings: $50-100', { size: 30, fill: 0x4CAF50 }); songEarnings.anchor.set(0.5, 0.5); songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X; songEarnings.y = 730; levelSelectScreen.addChild(songEarnings); // Play/Buy button var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 900 })); var playButtonText = new Text2('PLAY', { size: 50, fill: 0xFFFFFF }); playButtonText.anchor.set(0.5, 0.5); playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; playButtonText.y = 900; levelSelectScreen.addChild(playButtonText); // Shop button var shopButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1100, tint: 0x2e7d32 })); var shopButtonText = new Text2('UPGRADE ROD', { size: 40, fill: 0xFFFFFF }); shopButtonText.anchor.set(0.5, 0.5); shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; shopButtonText.y = 1100; levelSelectScreen.addChild(shopButtonText); // Back button var backButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 200, y: 200, tint: 0x757575 })); var backButtonText = new Text2('BACK', { size: 40, fill: 0xFFFFFF }); backButtonText.anchor.set(0.5, 0.5); backButtonText.x = 200; backButtonText.y = 200; levelSelectScreen.addChild(backButtonText); return { moneyDisplay: moneyDisplay, depthTabs: depthTabs, leftArrow: leftArrow, rightArrow: rightArrow, songTitle: songTitle, songInfo: songInfo, songEarnings: songEarnings, playButton: playButton, playButtonText: playButtonText, shopButton: shopButton, shopButtonText: shopButtonText, backButton: backButton }; } /**** * Fishing Screen ****/ function createFishingScreen() { // Sky background - should be added first to be behind everything else var sky = fishingScreen.addChild(LK.getAsset('skybackground', { x: 0, y: -500 })); // Water background var water = fishingScreen.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, width: 2048, height: 2732 - GAME_CONFIG.WATER_SURFACE_Y })); // Create a container for ambient ocean bubbles (from bottom of screen) // This should be layered above the 'water' background, but below fish, boat, etc. globalOceanBubbleContainer = fishingScreen.addChild(new Container()); // Create a container for seaweed particles globalSeaweedContainer = fishingScreen.addChild(new Container()); // Create a container for cloud particles (added early so clouds appear behind UI) globalCloudContainer = fishingScreen.addChild(new Container()); // Create a container for bubbles to render them behind fish and other elements bubbleContainer = fishingScreen.addChild(new Container()); // Create a container for music notes musicNotesContainer = fishingScreen.addChild(new Container()); // Music notes should visually appear to come from the boat area, so their container // should ideally be layered accordingly. Adding it here means it's on top of water, // but if boat/fisherman are added later, notes might appear behind them if not managed. // For now, notes will be added to this container, which itself is added to fishingScreen. // Animated Water Surface segments code var waterSurfaceSegments = []; // This will be populated for returning and cleanup var waterSurfaceSegmentsBlueTemp = []; // Temporary array for blue segments var waterSurfaceSegmentsWhiteTemp = []; // Temporary array for white segments var NUM_WAVE_SEGMENTS = 32; var SEGMENT_WIDTH = 2048 / NUM_WAVE_SEGMENTS; var SEGMENT_HEIGHT = 24; var WAVE_AMPLITUDE = 12; var WAVE_HALF_PERIOD_MS = 2500; var PHASE_DELAY_MS_PER_SEGMENT = WAVE_HALF_PERIOD_MS * 2 / NUM_WAVE_SEGMENTS; // Create blue segments (assets only, not added to fishingScreen yet) for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) { var segment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH, y: GAME_CONFIG.WATER_SURFACE_Y, width: SEGMENT_WIDTH + 1, height: SEGMENT_HEIGHT, anchorX: 0, anchorY: 0.5, alpha: 0.8, tint: 0x4fc3f7 }); segment.baseY = GAME_CONFIG.WATER_SURFACE_Y; // Animation functions will be defined and started by startWaterSurfaceAnimationFunc waterSurfaceSegmentsBlueTemp.push(segment); } // Create white segments (assets only, not added to fishingScreen yet) for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) { var whiteSegment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH, y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2, width: SEGMENT_WIDTH + 1, height: SEGMENT_HEIGHT / 2, anchorX: 0, anchorY: 0.5, alpha: 0.6, tint: 0xffffff }); whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2; // Animation functions will be defined and started by startWaterSurfaceAnimationFunc waterSurfaceSegmentsWhiteTemp.push(whiteSegment); } // Boat - Add this to fishingScreen first var boat = fishingScreen.addChild(LK.getAsset('boat', { anchorX: 0.5, anchorY: 0.74, x: GAME_CONFIG.SCREEN_CENTER_X, y: GAME_CONFIG.WATER_SURFACE_Y })); // Now add the water segments to fishingScreen, so they render on top of the boat for (var i = 0; i < waterSurfaceSegmentsBlueTemp.length; i++) { fishingScreen.addChild(waterSurfaceSegmentsBlueTemp[i]); waterSurfaceSegments.push(waterSurfaceSegmentsBlueTemp[i]); // Also add to the main array for cleanup } for (var i = 0; i < waterSurfaceSegmentsWhiteTemp.length; i++) { fishingScreen.addChild(waterSurfaceSegmentsWhiteTemp[i]); waterSurfaceSegments.push(waterSurfaceSegmentsWhiteTemp[i]); // Also add to the main array for cleanup } // Create separate fisherman container that will sync with boat movement var fishermanContainer = fishingScreen.addChild(new Container()); // Fisherman (now in its own container, positioned to match boat) var fisherman = fishermanContainer.addChild(LK.getAsset('fisherman', { anchorX: 0.5, anchorY: 1, x: GAME_CONFIG.SCREEN_CENTER_X - 100, y: GAME_CONFIG.WATER_SURFACE_Y - 70 })); // Store references for wave animation sync var boatBaseY = boat.y; var fishermanBaseY = fishermanContainer.y; var boatWaveAmplitude = 10; var boatWaveHalfCycleDuration = 2000; // SINGLE ANIMATED FISHING LINE var initialHookY = GAME_CONFIG.LANES[1].y; var fishingLineStartY = -100; var line = fishingScreen.addChild(LK.getAsset('fishingLine', { anchorX: 0.5, anchorY: 0, x: GAME_CONFIG.SCREEN_CENTER_X, y: GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY, width: 6, height: initialHookY - (GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY) })); var hook = fishingScreen.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: initialHookY })); hook.originalY = initialHookY; var lineWaveAmplitude = 12; var lineWaveSpeed = 0.03; var linePhaseOffset = 0; function updateFishingLineWave() { linePhaseOffset += lineWaveSpeed; var rodTipX = fishermanContainer.x + fisherman.x + 85; var rodTipY = fishermanContainer.y + fisherman.y - fisherman.height; var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude; line.x = rodTipX + waveOffset * 0.3; line.y = rodTipY; hook.x = rodTipX + waveOffset; var hookAttachX = hook.x; var hookAttachY = hook.y - hook.height / 2; var deltaX = hookAttachX - line.x; var deltaY = hookAttachY - line.y; var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY); line.height = actualLineLength; if (actualLineLength > 0.001) { line.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2; } else { line.rotation = 0; } hook.rotation = line.rotation; } // Calculate target positions for boat wave animation var targetUpY = boatBaseY - boatWaveAmplitude; var targetDownY = boatBaseY + boatWaveAmplitude; var fishermanTargetUpY = fishermanBaseY - boatWaveAmplitude; var fishermanTargetDownY = fishermanBaseY + boatWaveAmplitude; // Synchronized wave animation functions (defined here to be closured) function moveBoatAndFishermanUp() { if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } function moveBoatAndFishermanDown() { if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } tween(boat, { y: targetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanUp }); tween(fishermanContainer, { y: fishermanTargetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } var boatRotationAmplitude = 0.03; var boatRotationDuration = 3000; function rockBoatLeft() { if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) { return; } tween(boat, { rotation: -boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut, onFinish: rockBoatRight }); tween(fisherman, { rotation: boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut }); } function rockBoatRight() { if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) { return; } tween(boat, { rotation: boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut, onFinish: rockBoatLeft }); tween(fisherman, { rotation: -boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut }); } // Function to start/restart water surface animations function startWaterSurfaceAnimationFunc() { var allSegments = waterSurfaceSegments; // Use the populated array from fishingElements via closure for (var k = 0; k < allSegments.length; k++) { var segment = allSegments[k]; if (!segment || segment.destroyed) { continue; } var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS; (function (currentLocalSegment, currentLocalSegmentIndexForDelay) { var animUp, animDown; animDown = function animDown() { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY + WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animUp }); }; animUp = function animUp() { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY - WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animDown }); }; LK.setTimeout(function () { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY - WAVE_AMPLITUDE }, { // Initial move up duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animDown }); }, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT); })(segment, segmentIndexForDelay); } } // Function to start/restart boat and fisherman animations function startBoatAndFishermanAnimationFunc() { if (boat && !boat.destroyed && fishermanContainer && !fishermanContainer.destroyed) { tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut }); rockBoatLeft(); } } // UI elements (from existing) var scoreText = new Text2('Score: 0', { size: 70, fill: 0xFFFFFF }); scoreText.anchor.set(1, 0); scoreText.x = 2048 - 50; scoreText.y = 50; fishingScreen.addChild(scoreText); var fishText = new Text2('Fish: 0/0', { size: 55, fill: 0xFFFFFF }); fishText.anchor.set(1, 0); fishText.x = 2048 - 50; fishText.y = 140; fishingScreen.addChild(fishText); var comboText = new Text2('Combo: 0', { size: 55, fill: 0xFF9800 }); comboText.anchor.set(1, 0); comboText.x = 2048 - 50; comboText.y = 210; fishingScreen.addChild(comboText); var progressText = new Text2('0:00 / 0:00', { size: 50, fill: 0x4FC3F7 }); progressText.anchor.set(1, 0); progressText.x = 2048 - 50; progressText.y = 280; fishingScreen.addChild(progressText); return { boat: boat, fishermanContainer: fishermanContainer, fisherman: fisherman, hook: hook, line: line, updateFishingLineWave: updateFishingLineWave, scoreText: scoreText, fishText: fishText, comboText: comboText, progressText: progressText, waterSurfaceSegments: waterSurfaceSegments, bubbleContainer: bubbleContainer, musicNotesContainer: musicNotesContainer, startWaterSurfaceAnimation: startWaterSurfaceAnimationFunc, startBoatAndFishermanAnimation: startBoatAndFishermanAnimationFunc }; } /**** * Initialize Screen Elements ****/ var titleElements = createTitleScreen(); var levelSelectElements = createLevelSelectScreen(); var fishingElements = createFishingScreen(); // Feedback indicators are now created on-demand by the showFeedback function. // The global feedbackIndicators object is no longer needed. // Game variables var fishArray = []; var bubblesArray = []; var bubbleContainer; // Container for bubbles, initialized in createFishingScreen var musicNotesArray = []; var musicNotesContainer; // Container for music notes var musicNoteSpawnCounter = 0; var MUSIC_NOTE_SPAWN_INTERVAL_TICKS = 45; // Spawn a note roughly every 0.75 seconds // Ocean Bubbles (ambient background) var globalOceanBubblesArray = []; var globalOceanBubbleContainer; var globalOceanBubbleSpawnCounter = 0; // Increase interval to reduce amount (higher = less frequent) var OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS = 40; // Spawn new ocean bubbles roughly every 2/3 second (was 20) // Seaweed particles (ambient background) var globalSeaweedArray = []; var globalSeaweedContainer; var globalSeaweedSpawnCounter = 0; var SEAWEED_SPAWN_INTERVAL_TICKS = 120; // Spawn seaweed less frequently than bubbles var MAX_SEAWEED_COUNT = 8; // Maximum number of seaweed particles at once // Cloud particles (ambient sky) var globalCloudArray = []; var globalCloudContainer; var globalCloudSpawnCounter = 0; var CLOUD_SPAWN_INTERVAL_TICKS = 180; // Spawn clouds less frequently than seaweed var MAX_CLOUD_COUNT = 5; // Maximum number of cloud particles at once // Title Screen Ambient Particle Systems var titleScreenOceanBubblesArray = []; var titleScreenOceanBubbleContainer; // Will be initialized in createTitleScreen var titleScreenOceanBubbleSpawnCounter = 0; var titleScreenSeaweedArray = []; var titleScreenSeaweedContainer; // Will be initialized in createTitleScreen var titleScreenSeaweedSpawnCounter = 0; var titleScreenCloudArray = []; var titleScreenCloudContainer; // Will be initialized in createTitleScreen var titleScreenCloudSpawnCounter = 0; // Timers for title screen ambient sounds var titleSeagullSoundTimer = null; var titleBoatSoundTimer = null; /**** * Input State and Helpers for Fishing ****/ var inputState = { touching: false, // Is the screen currently being touched? touchLane: -1, // Which lane was the touch initiated in? (0, 1, 2) touchStartTime: 0 // Timestamp of when the touch started (LK.ticks based) }; // Helper function to determine which lane a Y coordinate falls into function getTouchLane(y) { // Define boundaries based on the midpoints between lane Y coordinates // These are calculated from GAME_CONFIG.LANES[i].y values // Lane 0: y = 723 // Lane 1: y = 1366 // Lane 2: y = 2009 var boundary_lane0_lane1 = (GAME_CONFIG.LANES[0].y + GAME_CONFIG.LANES[1].y) / 2; // Approx 1044.5 var boundary_lane1_lane2 = (GAME_CONFIG.LANES[1].y + GAME_CONFIG.LANES[2].y) / 2; // Approx 1687.5 if (y < boundary_lane0_lane1) { return 0; // Top lane (e.g., shallow) } else if (y < boundary_lane1_lane2) { return 1; // Middle lane (e.g., medium) } else { return 2; // Bottom lane (e.g., deep) } } // Shows feedback (perfect, good, miss) at the specified lane // Shows feedback (perfect, good, miss) at the specified lane function showFeedback(type, laneIndex) { var feedbackY = GAME_CONFIG.LANES[laneIndex].y; var indicator = new FeedbackIndicator(type); // Creates a new indicator e.g. FeedbackIndicator('perfect') // Position feedback at the single hook's X coordinate and the fish's lane Y indicator.x = fishingElements.hook.x; // Use the single hook's X indicator.y = feedbackY; // Feedback appears at the fish's lane Y fishingScreen.addChild(indicator); indicator.show(); // Triggers the animation and self-destruction } // Animates the hook in a specific lane after a catch attempt // Animates the single hook after a catch attempt function animateHookCatch() { var hook = fishingElements.hook; // We need a stable originalY. The hook.originalY might change if we re-assign it during tweens. // Let's use the target Y of the current fish lane for the "resting" position after animation. var restingY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; // Quick bobbing animation for the single hook tween(hook, { y: restingY - 30 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(hook, { y: restingY }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { // Ensure originalY reflects the current target lane after animation. hook.originalY = restingY; } }); } }); } // Handles input specifically for the fishing screen (down and up events) function handleFishingInput(x, y, isDown) { if (!GameState.gameActive) { return; } var currentTime = LK.ticks * (1000 / 60); // Current time in ms if (isDown) { // Touch started inputState.touching = true; inputState.touchLane = getTouchLane(y); inputState.touchStartTime = currentTime; // A normal tap action will be processed on 'up'. } else { // Touch ended (isUp) if (inputState.touching) { // This was a normal tap. checkCatch(inputState.touchLane); } inputState.touching = false; } } /**** * Screen Management ****/ function showScreen(screenName) { titleScreen.visible = false; levelSelectScreen.visible = false; fishingScreen.visible = false; resultsScreen.visible = false; // Stop any ongoing tweens and clear particles if switching FROM title screen if (GameState.currentScreen === 'title' && titleElements) { tween.stop(titleElements.titleAnimationGroup); tween.stop(titleElements.blackOverlay); if (titleElements.titleImage) { tween.stop(titleElements.titleImage); } // Stop titleImage tween if (titleElements.logo) { tween.stop(titleElements.logo); } // Keep for safety if old code path if (titleElements.subtitle) { tween.stop(titleElements.subtitle); } // Keep for safety tween.stop(titleElements.startButton); tween.stop(titleElements.startText); tween.stop(titleElements.tutorialButton); tween.stop(titleElements.tutorialText); // Clear title screen sound timers if (titleSeagullSoundTimer) { LK.clearTimeout(titleSeagullSoundTimer); titleSeagullSoundTimer = null; } if (titleBoatSoundTimer) { LK.clearTimeout(titleBoatSoundTimer); titleBoatSoundTimer = null; } // Stop water surface animations for title screen if (titleElements.titleWaterSurfaceSegments) { titleElements.titleWaterSurfaceSegments.forEach(function (segment) { if (segment && !segment.destroyed) { tween.stop(segment); } }); } // Clear title screen particles if (titleScreenOceanBubbleContainer) { titleScreenOceanBubbleContainer.removeChildren(); } titleScreenOceanBubblesArray.forEach(function (p) { if (p && !p.destroyed) { p.destroy(); } }); titleScreenOceanBubblesArray = []; if (titleScreenSeaweedContainer) { titleScreenSeaweedContainer.removeChildren(); } titleScreenSeaweedArray.forEach(function (p) { if (p && !p.destroyed) { p.destroy(); } }); titleScreenSeaweedArray = []; if (titleScreenCloudContainer) { titleScreenCloudContainer.removeChildren(); } titleScreenCloudArray.forEach(function (p) { if (p && !p.destroyed) { p.destroy(); } }); titleScreenCloudArray = []; } GameState.currentScreen = screenName; switch (screenName) { case 'title': // Title Screen Sounds var _scheduleNextSeagullSound = function scheduleNextSeagullSound() { if (GameState.currentScreen !== 'title') { // If screen changed, ensure timer is stopped and not rescheduled if (titleSeagullSoundTimer) { LK.clearTimeout(titleSeagullSoundTimer); titleSeagullSoundTimer = null; } return; } var randomDelay = 5000 + Math.random() * 10000; // 5-15 seconds titleSeagullSoundTimer = LK.setTimeout(function () { if (GameState.currentScreen !== 'title') { return; // Don't play or reschedule if not on title screen } var seagullSounds = ['seagull1', 'seagull2', 'seagull3']; var randomSoundId = seagullSounds[Math.floor(Math.random() * seagullSounds.length)]; LK.getSound(randomSoundId).play(); _scheduleNextSeagullSound(); // Reschedule }, randomDelay); }; var _scheduleNextBoatSound = function scheduleNextBoatSound() { if (GameState.currentScreen !== 'title') { // If screen changed, ensure timer is stopped and not rescheduled if (titleBoatSoundTimer) { LK.clearTimeout(titleBoatSoundTimer); titleBoatSoundTimer = null; } return; } var fixedBoatSoundInterval = 8000; // Rhythmic interval: 8 seconds (reduced from 15) titleBoatSoundTimer = LK.setTimeout(function () { if (GameState.currentScreen !== 'title') { return; // Don't play or reschedule if not on title screen } LK.getSound('boatsounds').play(); _scheduleNextBoatSound(); // Reschedule }, fixedBoatSoundInterval); }; // Play initial random seagull sound titleScreen.visible = true; // Start all animations like in fishing screen if (titleElements.startTitleWaterSurfaceAnimation) { titleElements.startTitleWaterSurfaceAnimation(); } // Start boat group animations (single animations for the whole group) if (titleElements.moveTitleBoatGroupUp) { titleElements.moveTitleBoatGroupUp(); } if (titleElements.rockTitleBoatGroupLeft) { titleElements.rockTitleBoatGroupLeft(); } var initialSeagullSounds = ['seagull1', 'seagull2', 'seagull3']; var initialRandomSoundId = initialSeagullSounds[Math.floor(Math.random() * initialSeagullSounds.length)]; LK.getSound(initialRandomSoundId).play(); LK.getSound('boatsounds').play(); // Play boat sound immediately after initial seagull // Start the timed sounds (seagulls random, subsequent boats rhythmic) _scheduleNextSeagullSound(); _scheduleNextBoatSound(); // Schedules the *next* boat sound rhythmically // Reset particle spawn counters titleScreenOceanBubbleSpawnCounter = 0; titleScreenSeaweedSpawnCounter = 0; titleScreenCloudSpawnCounter = 0; // Animation timing var ZOOM_DURATION = 8000; // 8-second zoom var OVERLAY_FADE_DELAY = 1000; // Black overlay starts fading after 1 second (was 2) var OVERLAY_FADE_DURATION = 3000; // 3 seconds to fade out overlay var TEXT_DELAY = 4000; // Text appears as overlay finishes fading (was 5500) var BUTTON_DELAY = 5500; // Buttons appear sooner (was 7000) // Reset states titleElements.titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X; titleElements.titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // Centers boat vertically! titleElements.titleAnimationGroup.alpha = 1; // No alpha animation for main content titleElements.titleAnimationGroup.scale.set(3.0); // Reset black overlay to fully opaque titleElements.blackOverlay.alpha = 1; // Reset UI alphas if (titleElements.titleImage) { titleElements.titleImage.alpha = 0; } if (titleElements.logo) { titleElements.logo.alpha = 0; } if (titleElements.subtitle) { titleElements.subtitle.alpha = 0; } titleElements.startButton.alpha = 0; titleElements.startText.alpha = 0; titleElements.tutorialButton.alpha = 0; titleElements.tutorialText.alpha = 0; // Main zoom animation (no alpha change) tween(titleElements.titleAnimationGroup, { scaleX: 1.8, scaleY: 1.8 }, { duration: ZOOM_DURATION, easing: tween.easeInOut }); // Black overlay fade out (the reveal effect) LK.setTimeout(function () { tween(titleElements.blackOverlay, { alpha: 0 }, { duration: OVERLAY_FADE_DURATION, easing: tween.easeInOut }); }, OVERLAY_FADE_DELAY); // Fade in title image after overlay is mostly gone LK.setTimeout(function () { if (titleElements.titleImage) { tween(titleElements.titleImage, { alpha: 1 }, { duration: 1200, easing: tween.easeOut }); } else if (titleElements.logo && titleElements.subtitle) { // Fallback for old structure if needed tween(titleElements.logo, { alpha: 1 }, { duration: 1200, easing: tween.easeOut }); tween(titleElements.subtitle, { alpha: 1 }, { duration: 1200, easing: tween.easeOut }); } }, TEXT_DELAY); // Fade in buttons near the end LK.setTimeout(function () { tween(titleElements.startButton, { alpha: 1 }, { duration: 1000, easing: tween.easeOut }); tween(titleElements.startText, { alpha: 1 }, { duration: 1000, easing: tween.easeOut }); tween(titleElements.tutorialButton, { alpha: 1 }, { duration: 1000, easing: tween.easeOut }); tween(titleElements.tutorialText, { alpha: 1 }, { duration: 1000, easing: tween.easeOut }); }, BUTTON_DELAY); break; case 'levelSelect': levelSelectScreen.visible = true; updateLevelSelectScreen(); break; case 'fishing': fishingScreen.visible = true; playIntroAnimation(); // Play the intro sequence break; case 'results': resultsScreen.visible = true; break; } } /**** * Intro Animation ****/ function playIntroAnimation() { GameState.introPlaying = true; GameState.gameActive = false; // Start animations for water, boat, and fisherman at the beginning of the intro if (fishingElements) { if (typeof fishingElements.startWaterSurfaceAnimation === 'function') { fishingElements.startWaterSurfaceAnimation(); } if (typeof fishingElements.startBoatAndFishermanAnimation === 'function') { fishingElements.startBoatAndFishermanAnimation(); } } // Calculate rod tip position (relative to fishingScreen) var fc = fishingElements.fishermanContainer; var f = fishingElements.fisherman; var rodTipCalculatedX = fc.x + f.x + 85; var rodTipCalculatedY = fc.y + f.y - f.height; var initialHookDangleY = rodTipCalculatedY + 50; fishingElements.hook.y = initialHookDangleY; // Setup initial zoom and camera position for fishingScreen var INITIAL_ZOOM_FACTOR = 1.5; // Pivot around the boat's visual center var pivotX = fishingElements.boat.x; var pivotY = fishingElements.boat.y - fishingElements.boat.height * (fishingElements.boat.anchor.y - 0.5); fishingScreen.pivot.set(pivotX, pivotY); // Position screen so the pivot appears at screen center when zoomed var screenCenterX = 2048 / 2; var screenCenterY = 2732 / 2; fishingScreen.x = screenCenterX; fishingScreen.y = screenCenterY; fishingScreen.scale.set(INITIAL_ZOOM_FACTOR, INITIAL_ZOOM_FACTOR); var introDuration = 2000; // Tween for zoom out tween(fishingScreen.scale, { x: 1, y: 1 }, { duration: introDuration, easing: tween.easeInOut }); // Tween screen position to compensate for the zoom change // As we zoom out, we need to move the screen back toward the pivot tween(fishingScreen, { x: pivotX, y: pivotY }, { duration: introDuration, easing: tween.easeInOut }); // Hook drop animation var targetHookY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; tween(fishingElements.hook, { y: targetHookY }, { duration: introDuration * 0.8, delay: introDuration * 0.2, easing: tween.easeOut, onFinish: function onFinish() { GameState.introPlaying = false; // Reset to normal view fishingScreen.pivot.set(0, 0); fishingScreen.x = 0; fishingScreen.y = 0; startFishingSession(); } }); } /**** * Level Select Logic ****/ function updateLevelSelectScreen() { var elements = levelSelectElements; // Update money display elements.moneyDisplay.setText('Money: $' + GameState.money); // Create depth tabs createDepthTabs(); // Update song display updateSongDisplay(); // Update shop button updateShopButton(); } function createDepthTabs() { // Clear existing tabs levelSelectElements.depthTabs.forEach(function (tab) { if (tab.container) { tab.container.destroy(); } }); levelSelectElements.depthTabs = []; // Create tabs for unlocked depths for (var i = 0; i <= GameState.currentDepth; i++) { var depth = GAME_CONFIG.DEPTHS[i]; var isSelected = i === GameState.selectedDepth; var tabContainer = levelSelectScreen.addChild(new Container()); var tab = tabContainer.addChild(LK.getAsset('depthTab', { anchorX: 0.5, anchorY: 0.5, x: 300 + i * 220, y: 400, tint: isSelected ? 0x1976d2 : 0x455a64 })); var tabText = new Text2(depth.name.split(' ')[0], { size: 30, fill: 0xFFFFFF }); tabText.anchor.set(0.5, 0.5); tabText.x = 300 + i * 220; tabText.y = 400; tabContainer.addChild(tabText); levelSelectElements.depthTabs.push({ container: tabContainer, tab: tab, depthIndex: i }); } } function updateSongDisplay() { var elements = levelSelectElements; var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth]; var song = depth.songs[GameState.selectedSong]; var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong); // Update song info elements.songTitle.setText(song.name); elements.songInfo.setText('BPM: ' + song.bpm + ' | Duration: ' + formatTime(song.duration)); // Calculate potential earnings var minEarnings = Math.floor(depth.fishValue * 20); // Conservative estimate var maxEarnings = Math.floor(depth.fishValue * 60); // With combos and rare fish elements.songEarnings.setText('Potential Earnings: $' + minEarnings + '-$' + maxEarnings); // Update play/buy button if (owned) { elements.playButtonText.setText('PLAY'); elements.playButton.tint = 0x1976d2; } else { elements.playButtonText.setText('BUY ($' + song.cost + ')'); elements.playButton.tint = GameState.money >= song.cost ? 0x2e7d32 : 0x666666; } // Update arrow states elements.leftArrow.tint = GameState.selectedSong > 0 ? 0x1976d2 : 0x666666; elements.rightArrow.tint = GameState.selectedSong < depth.songs.length - 1 ? 0x1976d2 : 0x666666; } function updateShopButton() { var elements = levelSelectElements; var canUpgrade = GameState.canUpgrade(); var nextDepth = GAME_CONFIG.DEPTHS[GameState.currentDepth + 1]; if (nextDepth) { elements.shopButtonText.setText('UPGRADE ROD ($' + nextDepth.upgradeCost + ')'); elements.shopButton.tint = canUpgrade ? 0x2e7d32 : 0x666666; } else { elements.shopButtonText.setText('MAX DEPTH REACHED'); elements.shopButton.tint = 0x666666; } } function formatTime(ms) { var seconds = Math.floor(ms / 1000); var minutes = Math.floor(seconds / 60); seconds = seconds % 60; return minutes + ':' + (seconds < 10 ? '0' : '') + seconds; } /**** * Fishing Game Logic ****/ function startFishingSession() { // Reset session state GameState.sessionScore = 0; GameState.sessionFishCaught = 0; GameState.sessionFishSpawned = 0; GameState.combo = 0; GameState.maxCombo = 0; GameState.gameActive = true; GameState.songStartTime = 0; GameState.lastBeatTime = 0; GameState.beatCount = 0; GameState.musicNotesActive = true; ImprovedRhythmSpawner.reset(); musicNotesArray = []; if (fishingElements && fishingElements.musicNotesContainer) { fishingElements.musicNotesContainer.removeChildren(); } musicNoteSpawnCounter = 0; // Reset ocean bubbles globalOceanBubblesArray = []; if (globalOceanBubbleContainer) { globalOceanBubbleContainer.removeChildren(); } globalOceanBubbleSpawnCounter = 0; // Reset seaweed globalSeaweedArray = []; if (globalSeaweedContainer) { globalSeaweedContainer.removeChildren(); } globalSeaweedSpawnCounter = 0; // Reset clouds globalCloudArray = []; if (globalCloudContainer) { globalCloudContainer.removeChildren(); } globalCloudSpawnCounter = 0; // Animations for water, boat, and fisherman are now started in playIntroAnimation // Clear any existing fish fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; // Reset pattern generator for new session PatternGenerator.reset(); // Start music var songConfig = GameState.getCurrentSongConfig(); var musicIdToPlay = songConfig.musicId || 'rhythmTrack'; // Default to rhythmTrack if no specific id GameState.currentPlayingMusicId = musicIdToPlay; // Determine initial volume based on known assets for correct fade-out later if (musicIdToPlay === 'morningtide') { GameState.currentPlayingMusicInitialVolume = 1.0; // Volume defined in LK.init.music for 'morningtide' } else { // Default for 'rhythmTrack' or other unspecified tracks GameState.currentPlayingMusicInitialVolume = 0.8; // Volume defined in LK.init.music for 'rhythmTrack' } LK.playMusic(GameState.currentPlayingMusicId); // Play the selected music track } function spawnFish(currentTimeForRegistration, options) { options = options || {}; // Ensure options is an object var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Proximity check: Skip spawn if too close to existing fish. // This check is generally for the first fish of a beat or non-forced spawns. // For forced multi-beat spawns, this might prevent them if they are too close. // Consider if this rule should be relaxed for forced multi-beat spawns if visual overlap is acceptable for quick succession. // For now, keeping it as is. If a spawn is skipped, the multi-beat sequence might be shorter. var isFirstFishOfBeat = !options.laneIndexToUse && !options.forcedSpawnSide; if (isFirstFishOfBeat) { // Apply stricter proximity for non-forced spawns for (var i = 0; i < fishArray.length; i++) { var existingFish = fishArray[i]; if (Math.abs(existingFish.x - GAME_CONFIG.SCREEN_CENTER_X) < PatternGenerator.minDistanceBetweenFish) { return null; // Skip this spawn, do not register } } } var laneIndex; if (options.laneIndexToUse !== undefined) { laneIndex = options.laneIndexToUse; PatternGenerator.lastLane = laneIndex; // Update generator's state if lane is forced } else { laneIndex = PatternGenerator.getNextLane(); } var targetLane = GAME_CONFIG.LANES[laneIndex]; var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; } else if (GameState.selectedDepth >= 2 && rand < 0.3) { fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; fishValue = depthConfig.fishValue; } var fishSpeedValue = depthConfig.fishSpeed; var spawnSide; // -1 for left, 1 for right var actualFishSpeed; if (options.forcedSpawnSide !== undefined) { spawnSide = options.forcedSpawnSide; } else { spawnSide = Math.random() < 0.5 ? -1 : 1; } actualFishSpeed = Math.abs(fishSpeedValue) * spawnSide; var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex); newFish.spawnSide = spawnSide; // Store the side it spawned from newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen newFish.y = targetLane.y; newFish.baseY = targetLane.y; // Set baseY for swimming animation fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTimeForRegistration); return newFish; } function checkCatch(fishLane) { var hookX = fishingElements.hook.x; var closestFishInLane = null; var closestDistance = Infinity; // This is a tap action, find the closest fish in the tapped lane. for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (!fish.caught && fish.lane === fishLane) { var distance = Math.abs(fish.x - hookX); if (distance < closestDistance) { closestDistance = distance; closestFishInLane = fish; } } } if (!closestFishInLane) { // No fish found for tap showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // --- Normal Fish Catch Logic --- var points = 0; var multiplier = Math.max(1, Math.floor(GameState.combo / 10) + 1); if (closestDistance < GAME_CONFIG.PERFECT_WINDOW) { points = closestFishInLane.value * 2 * multiplier; showFeedback('perfect', fishLane); GameState.combo++; } else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) { points = closestFishInLane.value * multiplier; showFeedback('good', fishLane); GameState.combo++; } else if (closestDistance < GAME_CONFIG.MISS_WINDOW) { points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier)); showFeedback('good', fishLane); GameState.combo++; } else { showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // Successfully caught fish closestFishInLane.catchFish(); var fishIndex = fishArray.indexOf(closestFishInLane); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } GameState.sessionScore += points; GameState.money += points; GameState.sessionFishCaught++; GameState.totalFishCaught++; GameState.maxCombo = Math.max(GameState.maxCombo, GameState.combo); // Play a random catch sound effect var catchSounds = ['catch', 'catch2', 'catch3', 'catch4']; var randomCatchSound = catchSounds[Math.floor(Math.random() * catchSounds.length)]; LK.getSound(randomCatchSound).play(); animateHookCatch(); // Call parameterless animateHookCatch for the single hook } // Note: The old animateHookCatch function that was defined right after checkCatch // is now a global helper: animateHookCatch(laneIndex), defined earlier. // We remove the old local one if it existed here by not re-inserting it. function updateFishingUI() { var elements = fishingElements; elements.scoreText.setText('Score: ' + GameState.sessionScore); elements.fishText.setText('Fish: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned); elements.comboText.setText('Combo: ' + GameState.combo); // Update progress if (GameState.songStartTime > 0) { var currentTime = LK.ticks * (1000 / 60); var elapsed = currentTime - GameState.songStartTime; var songConfig = GameState.getCurrentSongConfig(); elements.progressText.setText(formatTime(elapsed) + ' / ' + formatTime(songConfig.duration)); } } function endFishingSession() { GameState.gameActive = false; // Stop the boat's wave animation to prevent it from running after the session if (fishingElements && fishingElements.boat) { tween.stop(fishingElements.boat); } // Stop fisherman container animation if (fishingElements && fishingElements.fishermanContainer) { tween.stop(fishingElements.fishermanContainer); } // Stop fisherman rotation animation if (fishingElements && fishingElements.fisherman) { tween.stop(fishingElements.fisherman); } // Stop water surface wave animations if (fishingElements && fishingElements.waterSurfaceSegments) { fishingElements.waterSurfaceSegments.forEach(function (segment) { if (segment && !segment.destroyed) { tween.stop(segment); } }); } // Fade out and stop music for the results screen var currentMusicId = GameState.currentPlayingMusicId; var currentMusicInitialVolume = GameState.currentPlayingMusicInitialVolume; var fadeOutDurationMs = 1000; // Duration of the fade out in milliseconds (e.g., 1 second) LK.playMusic(currentMusicId, { fade: { start: currentMusicInitialVolume, end: 0, duration: fadeOutDurationMs }, loop: false // Important to prevent restart after fade or if it's re-triggered }); // Ensure music is fully stopped after the fade duration has elapsed LK.setTimeout(function () { LK.stopMusic(); }, fadeOutDurationMs); ImprovedRhythmSpawner.reset(); // Clear fish fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; GameState.musicNotesActive = false; if (fishingElements && fishingElements.musicNotesContainer) { fishingElements.musicNotesContainer.removeChildren(); } // The MusicNoteParticle instances themselves will be garbage collected. // Clearing the array is important. musicNotesArray = []; // Clear ocean bubbles if (globalOceanBubbleContainer) { globalOceanBubbleContainer.removeChildren(); } globalOceanBubblesArray = []; // Clear seaweed if (globalSeaweedContainer) { globalSeaweedContainer.removeChildren(); } globalSeaweedArray = []; // Clear clouds if (globalCloudContainer) { globalCloudContainer.removeChildren(); } globalCloudArray = []; // Create results screen createResultsScreen(); showScreen('results'); } function createResultsScreen() { // Clear previous results resultsScreen.removeChildren(); var resultsBg = resultsScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.9, height: 2732 })); var title = new Text2('Fishing Complete!', { size: 100, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.x = GAME_CONFIG.SCREEN_CENTER_X; title.y = 400; resultsScreen.addChild(title); var scoreResult = new Text2('Score: ' + GameState.sessionScore, { size: 70, fill: 0xFFD700 }); scoreResult.anchor.set(0.5, 0.5); scoreResult.x = GAME_CONFIG.SCREEN_CENTER_X; scoreResult.y = 550; resultsScreen.addChild(scoreResult); var fishResult = new Text2('Fish Caught: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned, { size: 50, fill: 0xFFFFFF }); fishResult.anchor.set(0.5, 0.5); fishResult.x = GAME_CONFIG.SCREEN_CENTER_X; fishResult.y = 650; resultsScreen.addChild(fishResult); var comboResult = new Text2('Max Combo: ' + GameState.maxCombo, { size: 50, fill: 0xFF9800 }); comboResult.anchor.set(0.5, 0.5); comboResult.x = GAME_CONFIG.SCREEN_CENTER_X; comboResult.y = 750; resultsScreen.addChild(comboResult); var moneyEarned = new Text2('Money Earned: $' + GameState.sessionScore, { size: 50, fill: 0x4CAF50 }); moneyEarned.anchor.set(0.5, 0.5); moneyEarned.x = GAME_CONFIG.SCREEN_CENTER_X; moneyEarned.y = 850; resultsScreen.addChild(moneyEarned); // Accuracy var accuracy = GameState.sessionFishSpawned > 0 ? Math.round(GameState.sessionFishCaught / GameState.sessionFishSpawned * 100) : 0; var accuracyResult = new Text2('Accuracy: ' + accuracy + '%', { size: 50, fill: 0x2196F3 }); accuracyResult.anchor.set(0.5, 0.5); accuracyResult.x = GAME_CONFIG.SCREEN_CENTER_X; accuracyResult.y = 950; resultsScreen.addChild(accuracyResult); // Continue button var continueButton = resultsScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1200 })); var continueText = new Text2('CONTINUE', { size: 50, fill: 0xFFFFFF }); continueText.anchor.set(0.5, 0.5); continueText.x = GAME_CONFIG.SCREEN_CENTER_X; continueText.y = 1200; resultsScreen.addChild(continueText); // Fade in resultsScreen.alpha = 0; tween(resultsScreen, { alpha: 1 }, { duration: 500, easing: tween.easeOut }); } /**** * Input Handling ****/ game.down = function (x, y, obj) { LK.getSound('buttonClick').play(); switch (GameState.currentScreen) { case 'title': // Check if click is within start button bounds var startButton = titleElements.startButton; if (x >= startButton.x - startButton.width / 2 && x <= startButton.x + startButton.width / 2 && y >= startButton.y - startButton.height / 2 && y <= startButton.y + startButton.height / 2) { showScreen('levelSelect'); } // Check if click is within tutorial button bounds var tutorialButton = titleElements.tutorialButton; if (x >= tutorialButton.x - tutorialButton.width / 2 && x <= tutorialButton.x + tutorialButton.width / 2 && y >= tutorialButton.y - tutorialButton.height / 2 && y <= tutorialButton.y + tutorialButton.height / 2) { // TODO: Show tutorial } break; case 'levelSelect': handleLevelSelectInput(x, y); break; case 'fishing': handleFishingInput(x, y, true); // true for isDown break; case 'results': showScreen('levelSelect'); break; } }; function handleLevelSelectInput(x, y) { var elements = levelSelectElements; // Check depth tabs elements.depthTabs.forEach(function (tab) { var tabAsset = tab.tab; if (x >= tabAsset.x - tabAsset.width / 2 && x <= tabAsset.x + tabAsset.width / 2 && y >= tabAsset.y - tabAsset.height / 2 && y <= tabAsset.y + tabAsset.height / 2) { GameState.selectedDepth = tab.depthIndex; GameState.selectedSong = 0; // Reset to first song updateLevelSelectScreen(); } }); // Check song navigation var leftArrow = elements.leftArrow; if (x >= leftArrow.x - leftArrow.width / 2 && x <= leftArrow.x + leftArrow.width / 2 && y >= leftArrow.y - leftArrow.height / 2 && y <= leftArrow.y + leftArrow.height / 2 && GameState.selectedSong > 0) { GameState.selectedSong--; updateSongDisplay(); } var rightArrow = elements.rightArrow; if (x >= rightArrow.x - rightArrow.width / 2 && x <= rightArrow.x + rightArrow.width / 2 && y >= rightArrow.y - rightArrow.height / 2 && y <= rightArrow.y + rightArrow.height / 2) { var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth]; if (GameState.selectedSong < depth.songs.length - 1) { GameState.selectedSong++; updateSongDisplay(); } } // Check play/buy button var playButton = elements.playButton; if (x >= playButton.x - playButton.width / 2 && x <= playButton.x + playButton.width / 2 && y >= playButton.y - playButton.height / 2 && y <= playButton.y + playButton.height / 2) { var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong); if (owned) { showScreen('fishing'); } else { // Try to buy song if (GameState.buySong(GameState.selectedDepth, GameState.selectedSong)) { updateLevelSelectScreen(); } } } // Check shop button var shopButton = elements.shopButton; if (x >= shopButton.x - shopButton.width / 2 && x <= shopButton.x + shopButton.width / 2 && y >= shopButton.y - shopButton.height / 2 && y <= shopButton.y + shopButton.height / 2) { if (GameState.upgrade()) { LK.getSound('upgrade').play(); updateLevelSelectScreen(); } } // Check back button var backButton = elements.backButton; if (x >= backButton.x - backButton.width / 2 && x <= backButton.x + backButton.width / 2 && y >= backButton.y - backButton.height / 2 && y <= backButton.y + backButton.height / 2) { showScreen('title'); } } /**** * Main Game Loop ****/ game.update = function () { // Always update fishing line wave visuals if on fishing screen and elements are ready. // This needs to run even during the intro when gameActive might be false. if (GameState.currentScreen === 'fishing' && fishingElements && fishingElements.updateFishingLineWave) { fishingElements.updateFishingLineWave(); } // Update title screen ambient particles if title screen is active if (GameState.currentScreen === 'title') { // Add this to the game.update function, in the title screen section: if (GameState.currentScreen === 'title' && titleElements && titleElements.updateTitleFishingLineWave) { titleElements.updateTitleFishingLineWave(); } // Title Screen Ocean Bubbles if (titleScreenOceanBubbleContainer) { titleScreenOceanBubbleSpawnCounter++; if (titleScreenOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) { // Use same interval as fishing titleScreenOceanBubbleSpawnCounter = 0; var newOceanBubble = new OceanBubbleParticle(); // Assuming OceanBubbleParticle is general enough titleScreenOceanBubbleContainer.addChild(newOceanBubble); titleScreenOceanBubblesArray.push(newOceanBubble); } for (var obIdx = titleScreenOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) { var oceanBubble = titleScreenOceanBubblesArray[obIdx]; if (oceanBubble) { oceanBubble.update(); // No fish interaction on title screen if (oceanBubble.isDone) { oceanBubble.destroy(); titleScreenOceanBubblesArray.splice(obIdx, 1); } } else { titleScreenOceanBubblesArray.splice(obIdx, 1); } } } // Title Screen Seaweed if (titleScreenSeaweedContainer) { titleScreenSeaweedSpawnCounter++; if (titleScreenSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && titleScreenSeaweedArray.length < MAX_SEAWEED_COUNT) { titleScreenSeaweedSpawnCounter = 0; var newSeaweed = new SeaweedParticle(); titleScreenSeaweedContainer.addChild(newSeaweed); titleScreenSeaweedArray.push(newSeaweed); } for (var swIdx = titleScreenSeaweedArray.length - 1; swIdx >= 0; swIdx--) { var seaweed = titleScreenSeaweedArray[swIdx]; if (seaweed) { seaweed.update(); // No fish interaction on title screen if (seaweed.isDone) { seaweed.destroy(); titleScreenSeaweedArray.splice(swIdx, 1); } } else { titleScreenSeaweedArray.splice(swIdx, 1); } } } // Title Screen Clouds if (titleScreenCloudContainer) { titleScreenCloudSpawnCounter++; if (titleScreenCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && titleScreenCloudArray.length < MAX_CLOUD_COUNT) { titleScreenCloudSpawnCounter = 0; var newCloud = new CloudParticle(); titleScreenCloudContainer.addChild(newCloud); titleScreenCloudArray.push(newCloud); } for (var cldIdx = titleScreenCloudArray.length - 1; cldIdx >= 0; cldIdx--) { var cloud = titleScreenCloudArray[cldIdx]; if (cloud) { cloud.update(); if (cloud.isDone) { cloud.destroy(); titleScreenCloudArray.splice(cldIdx, 1); } } else { titleScreenCloudArray.splice(cldIdx, 1); } } } } // Spawn and update ambient ocean bubbles during intro and gameplay (fishing screen) if (GameState.currentScreen === 'fishing' && globalOceanBubbleContainer) { // Spawn bubbles during intro and gameplay globalOceanBubbleSpawnCounter++; if (globalOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) { globalOceanBubbleSpawnCounter = 0; var numToSpawn = 1; // Always spawn only 1 bubble per interval (was 1 or 2) for (var i = 0; i < numToSpawn; i++) { var newOceanBubble = new OceanBubbleParticle(); globalOceanBubbleContainer.addChild(newOceanBubble); globalOceanBubblesArray.push(newOceanBubble); } } // Update existing ocean bubbles for (var obIdx = globalOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) { var oceanBubble = globalOceanBubblesArray[obIdx]; if (oceanBubble) { // Apply fish physics to bubble for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) { var fish = fishArray[fishIdx]; if (fish && !fish.caught) { var dx = oceanBubble.x - fish.x; var dy = oceanBubble.y - fish.y; var distance = Math.sqrt(dx * dx + dy * dy); var influenceRadius = 150; // Radius of fish influence on bubbles var minDistance = 30; // Minimum distance to avoid division issues if (distance < influenceRadius && distance > minDistance) { // Calculate influence strength (stronger when closer) var influence = 1 - distance / influenceRadius; influence = influence * influence; // Square for more dramatic close-range effect // Calculate normalized direction away from fish var dirX = dx / distance; var dirY = dy / distance; // Apply force based on fish speed and direction var fishSpeedFactor = Math.abs(fish.speed) * 0.15; // Scale down fish speed influence var pushForce = fishSpeedFactor * influence; // Add horizontal push (stronger in direction of fish movement) oceanBubble.x += dirX * pushForce * 2; // Stronger horizontal push // Add vertical component (bubbles get pushed up/down) oceanBubble.vy += dirY * pushForce * 0.5; // Gentler vertical influence // Add some swirl/turbulence to drift oceanBubble.driftAmplitude = Math.min(80, oceanBubble.driftAmplitude + pushForce * 10); oceanBubble.driftFrequency *= 1 + influence * 0.1; // Slightly increase oscillation when disturbed } } } oceanBubble.update(); if (oceanBubble.isDone) { oceanBubble.destroy(); globalOceanBubblesArray.splice(obIdx, 1); } } else { globalOceanBubblesArray.splice(obIdx, 1); // Safeguard for null entries } } } // Spawn and update seaweed particles during intro and gameplay if (GameState.currentScreen === 'fishing' && globalSeaweedContainer) { // Spawn seaweed during intro and gameplay globalSeaweedSpawnCounter++; if (globalSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && globalSeaweedArray.length < MAX_SEAWEED_COUNT) { globalSeaweedSpawnCounter = 0; var newSeaweed = new SeaweedParticle(); globalSeaweedContainer.addChild(newSeaweed); globalSeaweedArray.push(newSeaweed); } // Update existing seaweed for (var swIdx = globalSeaweedArray.length - 1; swIdx >= 0; swIdx--) { var seaweed = globalSeaweedArray[swIdx]; if (seaweed) { // Apply fish physics to seaweed for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) { var fish = fishArray[fishIdx]; if (fish && !fish.caught) { var dx = seaweed.x - fish.x; var dy = seaweed.y - fish.y; var distance = Math.sqrt(dx * dx + dy * dy); var influenceRadius = 180; // Slightly larger influence for seaweed var minDistance = 40; if (distance < influenceRadius && distance > minDistance) { // Calculate influence strength var influence = 1 - distance / influenceRadius; influence = influence * influence; // Calculate normalized direction away from fish var dirX = dx / distance; var dirY = dy / distance; // Apply force based on fish speed var fishSpeedFactor = Math.abs(fish.speed) * 0.2; // Stronger influence on seaweed var pushForce = fishSpeedFactor * influence; // Add push forces seaweed.vx += dirX * pushForce * 1.5; // Seaweed is affected more horizontally seaweed.vy += dirY * pushForce * 0.8; // And moderately vertically // Increase sway when disturbed seaweed.swayAmplitude = Math.min(60, seaweed.swayAmplitude + pushForce * 15); } } } seaweed.update(); if (seaweed.isDone) { seaweed.destroy(); globalSeaweedArray.splice(swIdx, 1); } } else { globalSeaweedArray.splice(swIdx, 1); } } } // Spawn and update cloud particles during intro and gameplay if (GameState.currentScreen === 'fishing' && globalCloudContainer) { // Spawn clouds during intro and gameplay globalCloudSpawnCounter++; if (globalCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && globalCloudArray.length < MAX_CLOUD_COUNT) { globalCloudSpawnCounter = 0; var newCloud = new CloudParticle(); globalCloudContainer.addChild(newCloud); globalCloudArray.push(newCloud); } // Update existing clouds for (var cldIdx = globalCloudArray.length - 1; cldIdx >= 0; cldIdx--) { var cloud = globalCloudArray[cldIdx]; if (cloud) { cloud.update(); if (cloud.isDone) { cloud.destroy(); globalCloudArray.splice(cldIdx, 1); } } else { globalCloudArray.splice(cldIdx, 1); } } } // Standard game active check; if intro is playing, gameActive will be false. if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } // Note: The fishing line wave update was previously here, it's now moved up. var currentTime = LK.ticks * (1000 / 60); // Initialize game timer if (GameState.songStartTime === 0) { GameState.songStartTime = currentTime; } // Check song end var songConfig = GameState.getCurrentSongConfig(); if (currentTime - GameState.songStartTime >= songConfig.duration) { endFishingSession(); return; } // Use RhythmSpawner to handle fish spawning ImprovedRhythmSpawner.update(currentTime); // Dynamic Hook Movement Logic var approachingFish = null; var minDistanceToCenter = Infinity; for (var i = 0; i < fishArray.length; i++) { var f = fishArray[i]; if (!f.caught) { var distanceToHookX = Math.abs(f.x - fishingElements.hook.x); var isApproachingOrAtHook = f.speed > 0 && f.x < fishingElements.hook.x || f.speed < 0 && f.x > fishingElements.hook.x || distanceToHookX < GAME_CONFIG.MISS_WINDOW * 2; if (isApproachingOrAtHook && distanceToHookX < minDistanceToCenter) { minDistanceToCenter = distanceToHookX; approachingFish = f; } } } var targetLaneY; if (approachingFish) { if (GameState.hookTargetLaneIndex !== approachingFish.lane) { GameState.hookTargetLaneIndex = approachingFish.lane; } targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; } else { targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; } // Update hook Y position (X is handled by the wave animation) if (Math.abs(fishingElements.hook.y - targetLaneY) > 5) { // Only tween if significantly different tween(fishingElements.hook, { y: targetLaneY }, { duration: 150, easing: tween.easeOut }); fishingElements.hook.originalY = targetLaneY; } //{bO} // Re-using last relevant ID from replaced block for context if appropriate // Update fish for (var i = fishArray.length - 1; i >= 0; i--) { var fish = fishArray[i]; fish.update(); // Fish updates its own movement and appearance // Remove off-screen fish (if not caught) if (!fish.caught && (fish.x < -250 || fish.x > 2048 + 250)) { // Increased buffer slightly fish.destroy(); fishArray.splice(i, 1); } } // Update UI updateFishingUI(); // Spawn and update music notes if active if (GameState.musicNotesActive && fishingElements && fishingElements.boat && !fishingElements.boat.destroyed && musicNotesContainer) { musicNoteSpawnCounter++; if (musicNoteSpawnCounter >= MUSIC_NOTE_SPAWN_INTERVAL_TICKS) { musicNoteSpawnCounter = 0; // Spawn notes from the boat area, considering boat's current position var boatCenterX = fishingElements.boat.x; // boat.y is anchorY (0.74). Visual center is higher. var boatVisualCenterY = fishingElements.boat.y - fishingElements.boat.height * (fishingElements.boat.anchor.y - 0.5); var spawnX = boatCenterX + (Math.random() - 0.5) * (fishingElements.boat.width * 0.25); // Spread slightly around boat center var spawnY = boatVisualCenterY - 50 - Math.random() * 100; // Spawn above the boat's visual center var newNote = new MusicNoteParticle(spawnX, spawnY); musicNotesContainer.addChild(newNote); musicNotesArray.push(newNote); } } // Update existing music notes for (var mnIdx = musicNotesArray.length - 1; mnIdx >= 0; mnIdx--) { var note = musicNotesArray[mnIdx]; if (note) { note.update(); if (note.isDone) { note.destroy(); musicNotesArray.splice(mnIdx, 1); } } else { // Should not happen, but good to safeguard musicNotesArray.splice(mnIdx, 1); } } // Spawn bubbles for active fish if (bubbleContainer) { for (var f = 0; f < fishArray.length; f++) { var fish = fishArray[f]; if (fish && !fish.caught && !fish.isHeld && fish.fishGraphics) { if (currentTime - fish.lastBubbleSpawnTime > fish.bubbleSpawnInterval) { fish.lastBubbleSpawnTime = currentTime; // Calculate tail position based on fish direction and width // fish.fishGraphics.width is the original asset width. // fish.fishGraphics.scale.x might be negative, but width property itself is positive. // The anchor is 0.5, so width/2 is distance from center to edge. var tailOffsetDirection = Math.sign(fish.speed) * -1; // Bubbles appear opposite to movement direction var bubbleX = fish.x + tailOffsetDirection * (fish.fishGraphics.width * Math.abs(fish.fishGraphics.scaleX) / 2) * 0.8; // 80% towards tail var bubbleY = fish.y + (Math.random() - 0.5) * (fish.fishGraphics.height * Math.abs(fish.fishGraphics.scaleY) / 4); // Slight Y variance around fish center var newBubble = new BubbleParticle(bubbleX, bubbleY); bubbleContainer.addChild(newBubble); bubblesArray.push(newBubble); } } } } // Update and remove bubbles for (var b = bubblesArray.length - 1; b >= 0; b--) { var bubble = bubblesArray[b]; if (bubble) { // Extra safety check bubble.update(); if (bubble.isDone) { bubble.destroy(); bubblesArray.splice(b, 1); } } else { // If a null/undefined somehow got in bubblesArray.splice(b, 1); } } }; // Initialize game showScreen('title');
===================================================================
--- original.js
+++ change.js
@@ -945,8 +945,11 @@
var currentTime = this.lastFrameTime;
var depthConfig = GameState.getCurrentDepthConfig();
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
+ // Calculate spawnInterval for this pattern - ADD THIS LINE
+ var beatInterval = 60000 / songConfig.bpm;
+ var spawnInterval = beatInterval * pattern.beatsPerFish;
// Apply section modifiers if pattern has sections
var spawnModifier = 1.0;
if (pattern.sections) {
var songElapsed = currentTime - GameState.songStartTime;
@@ -958,9 +961,8 @@
}
}
}
// Check if we should spawn (with section modifier)
- var beatInterval = 60000 / songConfig.bpm;
if (!PatternGenerator.canSpawnFishOnBeat(currentTime, beatInterval)) {
return;
}
// Apply spawn modifier chance
@@ -1014,12 +1016,9 @@
fishArray.push(newFish);
fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTime);
- // Calculate spawnInterval for multi-spawns
- var beatInterval = 60000 / songConfig.bpm;
- var spawnInterval = beatInterval * pattern.beatsPerFish;
- // Handle multi-spawns
+ // Handle multi-spawns - NOW spawnInterval IS DEFINED
this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime);
return newFish;
},
handleMultiSpawns: function handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime) {
@@ -1042,9 +1041,11 @@
},
calculateTravelTime: function calculateTravelTime() {
var depthConfig = GameState.getCurrentDepthConfig();
var fishSpeed = Math.abs(depthConfig.fishSpeed);
- if (fishSpeed === 0) return Infinity;
+ if (fishSpeed === 0) {
+ return Infinity;
+ }
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
return distanceToHook / fishSpeed * this.actualFrameTime;
},
updateFishSync: function updateFishSync(currentTime) {
No background.
A music note. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A white bubble. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
Blue gradient background starting lighter blue at the top of the image and going to a darker blue at the bottom.. In-Game asset. 2d. High contrast. No shadows
A small single strand of loose floating seaweed. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A thin wispy white cloud. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
Game logo for the game ‘Beat Fisher’. High def 80’s color themed SVG of the word.. In-Game asset. 2d. High contrast. No shadows
A yellow star burst that says 'Perfect!' in the center. 80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
A red starburst with the word ‘Miss!’ In it. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A green starburst with the word ‘Good!’ In it. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
White stylized square bracket. Pixelated. In-Game asset. 2d. High contrast. No shadows
A golden fish. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A double sided fishing hook with a small speaker in the center. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
An SVG that says ‘Start’
Blue to green gradient on the word instead of pink.
Above water background image showing gradient of light to dark blue starting at the top. Looking straight down from high above.. In-Game asset. 2d. High contrast. No shadows
This boat, same perspective, boat facing down.
A small island centered with a large mountain taking up most of it with a waterfall on the south side and a fishing village just below it. Under the fishing village is a harbor with a single empty dock. The dock extends into a half open bay. 80s arcade machine inspire high definition graphics with 80s colored highlights. White background. Top down 3/4 view. In-Game asset. 2d. High contrast. No shadows
A small lock icon. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A seagull with wings spread straight out as if soaring. Top down view, looking down from above. 80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
A round button with an embossed edge with an anchor in the center. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A long fluffy white cloud seen from overhead. 80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
Much thinner border
The dark shadow silhouette of a fish. Top down view. In-Game asset. 2d. High contrast. No shadows
rhythmTrack
Music
morningtide
Music
miss
Sound effect
catch
Sound effect
catch2
Sound effect
catch3
Sound effect
catch4
Sound effect
seagull1
Sound effect
seagull2
Sound effect
seagull3
Sound effect
boatsounds
Sound effect
sunnyafternoon
Music
reel
Sound effect
sardineRiff
Sound effect
anchovyRiff
Sound effect