User prompt
Reduce speed and amount of ocean bubble particles. Start spawning in intro. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (6 edits merged)
Please save this source code
User prompt
Add bubble particles that start at the bottom of the screen and travel upwards to the surface of the water. Drifting side to side travel and random upwards velocity. Random scale. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (3 edits merged)
Please save this source code
User prompt
Set sky background as background in fishing session levels.
User prompt
Boat and water animations should be started as soon as the level starts.
User prompt
When I start a new level after finishing one, the water segments, boat and fisherman are not animated. Those animations need to start again at the beginning of each session.
User prompt
Please fix the bug: 'TypeError: tween.isTweening is not a function. (In 'tween.isTweening(self.gfx)', 'tween.isTweening' is undefined)' in or related to this line: 'if (!tween.isTweening(self.gfx)) {' Line Number: 236 ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Create an animation where music notes fade in and float up lazily from the boat. Start this animation when the music starts in a level.
Code edit (1 edits merged)
Please save this source code
User prompt
When the level starts, I’d like to start zoomed in on the fisherman and boat with the fishing line all the way retracted and the hook dangling near the top of the line. Then the hook starts to drop into the water and we zoom out to the orientation that we currently have. Then the music and the fish can start. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Double check that fish are arriving at the precise beat.
User prompt
Replace all Time.deltaTime movement with position-based calculations from audio beats 2. Implement the Conductor pattern for centralized timing authority
User prompt
Update with: // Simplified and Fixed Pattern System function checkAndSpawnFish() { if (!GameState.gameActive) { return; } var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var currentBeat = GameState.songPositionBeats; // Simple approach: calculate next spawn beat based on pattern var baseSpacing = pattern.beatsPerFish || 2; // Default to every 2 beats // Apply section modifier var spawnMultiplier = getSectionSpawnMultiplier(pattern, GameState.songPositionSeconds); var actualSpacing = baseSpacing / spawnMultiplier; // Calculate the next expected spawn beat var expectedNextBeat; if (GameState.lastSpawnBeat < 0) { // First spawn - start at beat 0 or slightly after expectedNextBeat = 0; } else { expectedNextBeat = GameState.lastSpawnBeat + actualSpacing; } // Check if we should spawn now if (currentBeat >= expectedNextBeat) { GameState.lastSpawnBeat = expectedNextBeat; var firstFish = spawnFishAtBeat(expectedNextBeat); if (firstFish) { // Handle multi-spawns if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { // Spawn second fish 0.5 beats later scheduleDelayedSpawn(expectedNextBeat + 0.5, firstFish); if (pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance) { // Spawn third fish 1 beat later scheduleDelayedSpawn(expectedNextBeat + 1.0, firstFish); } } } } } // Helper function to get section spawn multiplier function getSectionSpawnMultiplier(pattern, songPositionSeconds) { if (!pattern.sections) { return 1.0; } var elapsedTimeMs = songPositionSeconds * 1000; for (var i = 0; i < pattern.sections.length; i++) { var section = pattern.sections[i]; if (elapsedTimeMs >= section.startTime && elapsedTimeMs < section.endTime) { return section.spawnModifier; } } return 1.0; // Default if no section matches } // Updated pattern configurations - much simpler and more predictable GAME_CONFIG.PATTERNS = { simple: { beatsPerFish: 2.0, // Fish every 2 beats doubleSpawnChance: 0.10, // 10% chance of double beat rareSpawnChance: 0.02 }, medium: { beatsPerFish: 1.5, // Fish every 1.5 beats doubleSpawnChance: 0.15, // 15% chance of double beat rareSpawnChance: 0.05 }, complex: { beatsPerFish: 1.0, // Fish every beat doubleSpawnChance: 0.25, // 25% chance of double beat rareSpawnChance: 0.08 }, expert: { beatsPerFish: 0.75, // Fish every 3/4 beat doubleSpawnChance: 0.35, // 35% chance of double beat tripletSpawnChance: 0.20, // 20% chance triple becomes triplet rareSpawnChance: 0.12 }, // Simplified gentle waves pattern gentle_waves_custom: { beatsPerFish: 2.0, // Start with fish every 2 beats doubleSpawnChance: 0.05, // Very rare double beats rareSpawnChance: 0.01, // Almost no rare fish sections: [ // Opening - Sparse (0-30 seconds) { startTime: 0, endTime: 30000, spawnModifier: 0.8, // Fewer fish = 2.0/0.8 = 2.5 beats spacing description: "intro" }, // Melody (30-60 seconds) - Normal { startTime: 30000, endTime: 60000, spawnModifier: 1.0, // Normal = 2.0 beats spacing description: "melody" }, // Development (60-120 seconds) - Slightly busier { startTime: 60000, endTime: 120000, spawnModifier: 1.2, // More fish = 2.0/1.2 = 1.67 beats spacing description: "development" }, // Climax (120-180 seconds) - Busiest { startTime: 120000, endTime: 180000, spawnModifier: 1.5, // Most fish = 2.0/1.5 = 1.33 beats spacing description: "climax" }, // Ending (180-202 seconds) - Calm down { startTime: 180000, endTime: 202000, spawnModifier: 0.7, // Fewer fish = 2.0/0.7 = 2.86 beats spacing description: "outro" } ] } }; // Updated scheduleDelayedSpawn for fractional beat delays function scheduleDelayedSpawn(targetBeat, referenceFish) { var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; // Calculate delay in milliseconds from current song position var currentBeat = GameState.songPositionBeats; var beatDelay = targetBeat - currentBeat; var delayMs = beatDelay * secPerBeat * 1000; // Ensure minimum delay if (delayMs < 16) { delayMs = 16; } LK.setTimeout(function() { // Only spawn if game is still active if (!GameState.gameActive) { return; } var options = {}; if (referenceFish && Math.random() < 0.5) { // Same lane as reference fish options.laneIndexToUse = referenceFish.lane; options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs); } else if (referenceFish) { // Adjacent lane var possibleLanes = []; if (referenceFish.lane > 0) { possibleLanes.push(referenceFish.lane - 1); } if (referenceFish.lane < GAME_CONFIG.LANES.length - 1) { possibleLanes.push(referenceFish.lane + 1); } if (possibleLanes.length > 0) { options.laneIndexToUse = possibleLanes[Math.floor(Math.random() * possibleLanes.length)]; // 70% chance opposite side for visual variety if (Math.random() < 0.7) { options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs) * -1; } } else { options.laneIndexToUse = referenceFish.lane; options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs); } } else { // No reference fish options.laneIndexToUse = PatternGenerator.getNextLane(); } spawnFishAtBeat(targetBeat, options); }, delayMs); }
User prompt
Please fix the bug: 'ReferenceError: currentTime is not defined' in or related to this line: 'if (currentTime - fish.lastBubbleSpawnTime > fish.bubbleSpawnInterval) {' Line Number: 2132
User prompt
Please fix the bug: 'TypeError: performance is undefined' in or related to this line: 'GameState.audioStartTime = performance.now();' Line Number: 1652
User prompt
Please fix the bug: 'TypeError: LK.now is not a function' in or related to this line: 'GameState.audioStartTime = LK.now();' Line Number: 1652
User prompt
Please fix the bug: 'TypeError: performance is undefined' in or related to this line: 'GameState.audioStartTime = performance.now();' Line Number: 1652
User prompt
Please fix the bug: 'TypeError: LK.getMusic is not a function' in or related to this line: 'GameState.currentPlayingMusicInitialVolume = LK.getMusic(musicIdToPlay).volume || 0.7; // Use actual initialized volume' Line Number: 1639
User prompt
Update as needed using LK engine's code equivalents: /**** * Fixed Timing System - Replace existing timing code ****/ // Add these new properties to GameState var GameState = { // ... existing properties ... // New timing properties audioStartTime: 0, // Audio system start time (dspTime) songPositionSeconds: 0, // Current song position in seconds songPositionBeats: 0, // Current song position in beats lastSpawnBeat: -1, // Last beat where we spawned fish (integer) // ... rest of existing properties ... }; // Updated startFishingSession function function startFishingSession() { // ... existing reset code ... // Initialize audio-based timing GameState.audioStartTime = 0; GameState.songPositionSeconds = 0; GameState.songPositionBeats = 0; GameState.lastSpawnBeat = -1; // ... existing fish clearing and pattern reset ... // Start music and record precise start time var songConfig = GameState.getCurrentSongConfig(); var musicIdToPlay = songConfig.musicId || 'rhythmTrack'; GameState.currentPlayingMusicId = musicIdToPlay; // Record the audio system time when music starts // Note: You'll need to adapt this to your LK framework's audio timing // If LK doesn't provide dspTime equivalent, use performance.now() GameState.audioStartTime = performance.now(); // Fallback - adapt to LK's timing system LK.playMusic(GameState.currentPlayingMusicId); } // New function to update song position (call this first in game.update) function updateSongPosition() { if (!GameState.gameActive || GameState.audioStartTime === 0) { return; } // Use high-precision timing instead of frame-based timing var currentAudioTime = performance.now(); // Adapt to LK's high-precision timer GameState.songPositionSeconds = (currentAudioTime - GameState.audioStartTime) / 1000; var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; GameState.songPositionBeats = GameState.songPositionSeconds / secPerBeat; } // Fixed fish spawning logic function checkAndSpawnFish() { if (!GameState.gameActive) return; var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var currentBeatFloor = Math.floor(GameState.songPositionBeats); // Calculate which beats should have fish based on pattern var beatSpacing = pattern.beatsPerFish; var nextSpawnBeat = Math.floor((GameState.lastSpawnBeat + 1) / beatSpacing) * beatSpacing; // Apply section-based spawn modifier if pattern has sections var spawnMultiplier = 1.0; if (pattern.sections) { var elapsedTimeMs = GameState.songPositionSeconds * 1000; for (var s = 0; s < pattern.sections.length; s++) { var section = pattern.sections[s]; if (elapsedTimeMs >= section.startTime && elapsedTimeMs < section.endTime) { spawnMultiplier = section.spawnModifier; break; } } // Adjust spacing based on section modifier beatSpacing = pattern.beatsPerFish / spawnMultiplier; nextSpawnBeat = Math.floor((GameState.lastSpawnBeat + 1) / beatSpacing) * beatSpacing; } // Spawn fish if we've passed the next spawn beat if (currentBeatFloor >= nextSpawnBeat && nextSpawnBeat > GameState.lastSpawnBeat) { GameState.lastSpawnBeat = nextSpawnBeat; GameState.beatCount++; // Calculate exact spawn time for this beat var exactSpawnTime = GameState.audioStartTime + (nextSpawnBeat * 60000 / songConfig.bpm); if (PatternGenerator.canSpawnFishOnBeat(exactSpawnTime, 60000 / songConfig.bpm)) { var firstFish = spawnFishAtBeat(nextSpawnBeat); if (firstFish) { // Handle double/triple spawns with precise timing if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { scheduleDelayedSpawn(nextSpawnBeat + 1, firstFish); if (pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance) { scheduleDelayedSpawn(nextSpawnBeat + 2, firstFish); } } } } } } // New function for spawning fish at specific beats function spawnFishAtBeat(targetBeat, options) { options = options || {}; var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; // Calculate exact timing for when fish should reach hook var beatsToReachHook = depthConfig.beatsToReachHook || 3; var hookArrivalBeat = targetBeat + beatsToReachHook; var hookArrivalTime = GameState.audioStartTime + (hookArrivalBeat * secPerBeat * 1000); // Calculate fish speed for position-based movement (not frame-based) var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var travelTimeMs = beatsToReachHook * secPerBeat * 1000; var fishSpeedPxPerMs = distanceToHook / travelTimeMs; // pixels per millisecond // Rest of fish creation logic remains similar... var laneIndex = options.laneIndexToUse !== undefined ? options.laneIndexToUse : PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; var fishType, fishValue; // Fish type determination (unchanged) var rand = Math.random(); if (rand < GAME_CONFIG.PATTERNS[songConfig.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 spawnSide = options.forcedSpawnSide !== undefined ? options.forcedSpawnSide : (Math.random() < 0.5 ? -1 : 1); var newFish = new Fish(fishType, fishValue, fishSpeedPxPerMs * spawnSide, laneIndex); newFish.spawnBeat = targetBeat; newFish.arrivalBeat = hookArrivalBeat; newFish.arrivalTime = hookArrivalTime; newFish.speedPxPerMs = fishSpeedPxPerMs * spawnSide; newFish.x = fishSpeedPxPerMs > 0 ? -150 : 2048 + 150; newFish.y = targetLane.y; newFish.baseY = targetLane.y; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(hookArrivalTime); return newFish; } // Updated Fish.update method for position-based movement // Add this to your Fish class: var OriginalFishUpdate = Fish.prototype.update; Fish.prototype.update = function() { if (!this.caught && GameState.gameActive) { // Position-based movement instead of frame-based var currentTime = performance.now(); var timeSinceSpawn = currentTime - (GameState.audioStartTime + (this.spawnBeat * 60000 / GameState.getCurrentSongConfig().bpm)); // Calculate exact position based on time var targetX = this.speedPxPerMs > 0 ? -150 + (Math.abs(this.speedPxPerMs) * timeSinceSpawn) : 2048 + 150 - (Math.abs(this.speedPxPerMs) * timeSinceSpawn); this.x = targetX; // Rest of the update logic (swimming animation, etc.) remains the same this.swimTime += 0.08; var swimAmplitude = 15; this.y = this.baseY + Math.sin(this.swimTime) * swimAmplitude; // Beat-synchronized scale pulsing using song position if (GameState.songPositionBeats > 0) { var beatProgress = (GameState.songPositionBeats % 1); var scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15; var baseScaleXDirection = (this.speedPxPerMs > 0 ? -1 : 1) * this.baseScale; this.fishGraphics.scaleX = baseScaleXDirection * scalePulse; this.fishGraphics.scaleY = scalePulse * this.baseScale; } if (this.isSpecial) { this.shimmerTime += 0.1; this.fishGraphics.alpha = 0.8 + Math.sin(this.shimmerTime) * 0.2; } else { this.fishGraphics.alpha = 1.0; } } }; // Fixed input timing check function checkCatch(fishLane) { var currentTime = performance.now(); // Use high-precision timing var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; var beatInterval = secPerBeat * 1000; // Convert to milliseconds // Check if the tap is on beat using song position if (GameState.audioStartTime > 0 && beatInterval > 0) { var timeSinceSongStart = currentTime - GameState.audioStartTime; var timeIntoBeatCycle = timeSinceSongStart % beatInterval; var timeToNearestBeat = Math.min(timeIntoBeatCycle, beatInterval - timeIntoBeatCycle); if (timeToNearestBeat > GAME_CONFIG.GOOD_WINDOW) { // Tap was off-beat showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; animateHookCatch(); return; } } // Rest of catch logic remains the same... var hookX = fishingElements.hook.x; var closestFishInLane = null; var closestDistance = Infinity; 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; } } } // ... rest of existing catch logic } // Updated main game loop game.update = function() { if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } // CRITICAL: Update song position first, before everything else updateSongPosition(); // Check for song end using precise timing var songConfig = GameState.getCurrentSongConfig(); if (GameState.songPositionSeconds * 1000 >= songConfig.duration) { endFishingSession(); return; } // Use position-based fish spawning checkAndSpawnFish(); // Update fishing line wave animation if (fishingElements.updateFishingLineWave) { fishingElements.updateFishingLineWave(); } // ... rest of existing update logic (fish updates, UI, etc.) }; // Helper function for delayed spawns function scheduleDelayedSpawn(targetBeat, referenceFish) { var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; var delayMs = secPerBeat * 1000; // One beat delay LK.setTimeout(function() { var options = {}; if (Math.random() < 0.5) { // Same lane as reference fish options.laneIndexToUse = referenceFish.lane; options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs); } else { // Adjacent lane var possibleLanes = []; if (referenceFish.lane > 0) possibleLanes.push(referenceFish.lane - 1); if (referenceFish.lane < 2) possibleLanes.push(referenceFish.lane + 1); if (possibleLanes.length > 0) { options.laneIndexToUse = possibleLanes[Math.floor(Math.random() * possibleLanes.length)]; } else { options.laneIndexToUse = referenceFish.lane; options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs); } } spawnFishAtBeat(targetBeat, options); }, delayMs); }
User prompt
When checking if a fish is caught, the touch input has to be on the beat. Only after determining that do we check the fish/hook relationship. Also double check timing to make sure that fish ALWAYS arrive at the hook on the beat.
User prompt
I don’t want the game to have held fishes as an option any more. Remove them.
User prompt
Remove held fishes from being able to spawn at all. No more held fishes.
User prompt
Remove held fishes from the game.
User prompt
Double and triple fishes aren’t landing on the beat. Space them out so they properly do.
/**** * 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.8; // Slow horizontal drift self.vy = -(0.8 + Math.random() * 0.7); // Steady 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 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 = LK.ticks * (1000 / 60); // Initialize with current time self.bubbleSpawnInterval = 60 + Math.random() * 40; // 60-100ms interval (more 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 && GameState.gameActive) { // Position-based movement instead of frame-based, if speedPxPerMs is set if (typeof self.speedPxPerMs === 'number' && typeof self.spawnBeat === 'number' && GameState.audioStartTime > 0) { var currentTime = LK.ticks * (1000 / 60); var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; if (secPerBeat <= 0) secPerBeat = 1; // Avoid division by zero var spawnAudioTime = GameState.audioStartTime + self.spawnBeat * secPerBeat * 1000; var timeSinceSpawnMs = currentTime - spawnAudioTime; // Calculate exact position based on time // Initial X position was -150 (for positive speedPxPerMs) or 2048 + 150 (for negative) var initialX = self.speedPxPerMs > 0 ? -150 : 2048 + 150; var targetX = initialX + self.speedPxPerMs * timeSinceSpawnMs; self.x = targetX; } else if (typeof self.speed === 'number') { // Fallback to old speed if new properties not set self.x += self.speed; } // Rest of the update logic (swimming animation, etc.) remains the same self.swimTime += 0.08; var swimAmplitude = 15; self.y = self.baseY + Math.sin(self.swimTime) * swimAmplitude; // Beat-synchronized scale pulsing using song position if (GameState.songPositionBeats > 0) { var beatProgress = GameState.songPositionBeats % 1; // Progress within the current beat (0 to 1) var scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15; // Pulse peaks on the beat // Determine base scaleX considering direction based on speedPxPerMs or fallback to self.speed var currentSpeedDirection = 0; if (typeof self.speedPxPerMs === 'number') { currentSpeedDirection = Math.sign(self.speedPxPerMs); } else if (typeof self.speed === 'number') { currentSpeedDirection = Math.sign(self.speed); } var baseScaleXDirection = (currentSpeedDirection > 0 ? -1 : 1) * self.baseScale; if (currentSpeedDirection === 0) baseScaleXDirection = self.baseScale; // if no speed, don't flip self.fishGraphics.scaleX = baseScaleXDirection * scalePulse; self.fishGraphics.scaleY = scalePulse * self.baseScale; } if (self.isSpecial) { self.shimmerTime += 0.1; self.fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2; } else { 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; }); /**** * Initialize Game ****/ /**** * Screen Containers ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ // 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 - fish speed determined by beatsToReachHook and song BPM DEPTHS: [{ level: 1, name: "Shallow Waters", beatsToReachHook: 4, // Fish take 4 beats to reach the hook 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: 75000, pattern: "simple", cost: 50, musicId: 'morningtide' }] }, { level: 2, name: "Mid Waters", beatsToReachHook: 3, // Fish take 3 beats to reach the hook 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", beatsToReachHook: 3, // Fish take 3 beats to reach the hook 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", beatsToReachHook: 2, // Fish take 2 beats to reach the hook 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 // Updated pattern configurations - much simpler and more predictable PATTERNS: { simple: { beatsPerFish: 2.0, // Fish every 2 beats doubleSpawnChance: 0.10, // 10% chance of double beat rareSpawnChance: 0.02 }, medium: { beatsPerFish: 1.5, // Fish every 1.5 beats doubleSpawnChance: 0.15, // 15% chance of double beat rareSpawnChance: 0.05 }, complex: { beatsPerFish: 1.0, // Fish every beat doubleSpawnChance: 0.25, // 25% chance of double beat rareSpawnChance: 0.08 }, expert: { beatsPerFish: 0.75, // Fish every 3/4 beat doubleSpawnChance: 0.35, // 35% chance of double beat tripletSpawnChance: 0.20, // 20% chance triple becomes triplet rareSpawnChance: 0.12 }, // Simplified gentle waves pattern gentle_waves_custom: { beatsPerFish: 2.0, // Start with fish every 2 beats doubleSpawnChance: 0.05, // Very rare double beats rareSpawnChance: 0.01, // Almost no rare fish sections: [ // Opening - Sparse (0-30 seconds) { startTime: 0, endTime: 30000, spawnModifier: 0.8, // Fewer fish = 2.0/0.8 = 2.5 beats spacing description: "intro" }, // Melody (30-60 seconds) - Normal { startTime: 30000, endTime: 60000, spawnModifier: 1.0, // Normal = 2.0 beats spacing description: "melody" }, // Development (60-120 seconds) - Slightly busier { startTime: 60000, endTime: 120000, spawnModifier: 1.2, // More fish = 2.0/1.2 = 1.67 beats spacing description: "development" }, // Climax (120-180 seconds) - Busiest { startTime: 120000, endTime: 180000, spawnModifier: 1.5, // Most fish = 2.0/1.5 = 1.33 beats spacing description: "climax" }, // Ending (180-202 seconds) - Calm down { startTime: 180000, endTime: 202000, spawnModifier: 0.7, // Fewer fish = 2.0/0.7 = 2.86 beats spacing description: "outro" }] } } }; // Map to store initial volumes of music tracks, based on LK.init.music calls. // This is used because LK.getMusic() is not available to query asset properties at runtime. // Volumes here should match those defined in LK.init.music calls in the "Assets" section. var musicInitialVolumes = { 'morningtide': 0.7, 'rhythmTrack': 0.7 // If new music tracks are added with specific initial volumes, add them here. }; /**** * 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 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, // Original song start time (LK.ticks based), potentially for UI or non-critical timing lastBeatTime: 0, // Original last beat time (LK.ticks based) beatCount: 0, // Original beat count // New timing properties audioStartTime: 0, // Audio system start time (performance.now() based) songPositionSeconds: 0, // Current song position in seconds, from audioStartTime songPositionBeats: 0, // Current song position in beats, from audioStartTime lastSpawnBeat: -1, // Last beat where we spawned fish (integer, based on songPositionBeats) 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 ****/ function createTitleScreen() { var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.3 })); // Logo var logo = new Text2('BEAT FISHER', { size: 150, fill: 0xFFFFFF }); logo.anchor.set(0.5, 0.5); logo.x = GAME_CONFIG.SCREEN_CENTER_X; logo.y = 600; titleScreen.addChild(logo); var subtitle = new Text2('Rhythm Fishing Adventure', { size: 60, fill: 0x4FC3F7 }); subtitle.anchor.set(0.5, 0.5); subtitle.x = GAME_CONFIG.SCREEN_CENTER_X; subtitle.y = 700; titleScreen.addChild(subtitle); // Start button var startButton = titleScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1000 })); var startText = new Text2('START', { size: 50, fill: 0xFFFFFF }); startText.anchor.set(0.5, 0.5); startText.x = GAME_CONFIG.SCREEN_CENTER_X; startText.y = 1000; titleScreen.addChild(startText); // Tutorial button var tutorialButton = titleScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1150, tint: 0x757575 })); var tutorialText = new Text2('TUTORIAL', { size: 40, fill: 0xFFFFFF }); tutorialText.anchor.set(0.5, 0.5); tutorialText.x = GAME_CONFIG.SCREEN_CENTER_X; tutorialText.y = 1150; titleScreen.addChild(tutorialText); return { startButton: startButton, tutorialButton: tutorialButton }; } /**** * Level Select Screen ****/ function createLevelSelectScreen() { var selectBg = levelSelectScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.8 })); // 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() { // 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, alpha: 0.7 })); // Create a container for bubbles to render them behind fish and other elements bubbleContainer = fishingScreen.addChild(new Container()); // 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; (function (currentSegment, segmentIndex) { function animateSegmentUp() { if (!currentSegment || currentSegment.destroyed) { return; } tween(currentSegment, { y: currentSegment.baseY - WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animateSegmentDown }); } function animateSegmentDown() { if (!currentSegment || currentSegment.destroyed) { return; } tween(currentSegment, { y: currentSegment.baseY + WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animateSegmentUp }); } LK.setTimeout(function () { if (!currentSegment || currentSegment.destroyed) { return; } tween(currentSegment, { y: currentSegment.baseY - WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animateSegmentDown }); }, segmentIndex * PHASE_DELAY_MS_PER_SEGMENT); })(segment, i); 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; (function (currentSegment, segmentIndex, blueSegment) { // Pass the corresponding blue segment from the temp array function animateSegmentUp() { if (!currentSegment || currentSegment.destroyed) { return; } tween(currentSegment, { y: currentSegment.baseY - WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animateSegmentDown }); } function animateSegmentDown() { if (!currentSegment || currentSegment.destroyed) { return; } tween(currentSegment, { y: currentSegment.baseY + WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animateSegmentUp }); } LK.setTimeout(function () { if (!currentSegment || currentSegment.destroyed) { return; } tween(currentSegment, { y: currentSegment.baseY - WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animateSegmentDown }); }, segmentIndex * PHASE_DELAY_MS_PER_SEGMENT); })(whiteSegment, i, waterSurfaceSegmentsBlueTemp[i]); // Use blue segment from its temporary array 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; // Fisherman's rod tip position (where line originates) // fisherman.anchorX = 0.5, fisherman.anchorY = 1 (bottom-center of fisherman asset) // fisherman.x and fisherman.y are relative to fishermanContainer // fishermanContainer.x, fishermanContainer.y are absolute (or relative to fishingScreen) var rodTipX = fishermanContainer.x + fisherman.x + 85; // X pos on fisherman, offset to represent rod tip var rodTipY = fishermanContainer.y + fisherman.y - fisherman.height; // Y pos: top of fisherman asset // Wave offset for sway var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude; // Line's top anchor point (line.anchorX = 0.5, line.anchorY = 0) // The line's top has a reduced sway compared to the hook's attachment point line.x = rodTipX + waveOffset * 0.3; line.y = rodTipY; // Hook's anchor point (hook.anchorX = 0.5, hook.anchorY = 0.5 - its visual center) // Hook's X position sways fully, reflecting the end of the line hook.x = rodTipX + waveOffset; // hook.y is determined by gameplay (tweening to targetLaneY) and is NOT directly set here. // Attachment point for the line on the hook is the top-center of the hook graphic var hookAttachX = hook.x; // Hook's anchor is its center (anchorX = 0.5) var hookAttachY = hook.y - hook.height / 2; // hook.y is center, so top is y - height/2 // Vector from the line's top anchor to the hook's attachment point var deltaX = hookAttachX - line.x; var deltaY = hookAttachY - line.y; // Calculate actual length for the line sprite based on the vector var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY); line.height = actualLineLength; // Math.sqrt result is always >= 0 // Rotate the line sprite to visually connect its top anchor to the hook's attachment point. // The 'fishingLine' asset is assumed to be a vertical rectangle when rotation = 0, // with its length extending along its local +Y axis (downwards). // The angle of the vector (deltaX, deltaY) relative to the screen's +X axis is Math.atan2(deltaY, deltaX). // The +Y axis (downwards on screen) is at Math.PI / 2 from the +X axis. // So, the required rotation for the line sprite is atan2(deltaY, deltaX) - Math.PI / 2. if (actualLineLength > 0.001) { // Avoid issues if line length is zero or very close to zero line.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2; } else { line.rotation = 0; // Default rotation (straight down) if length is effectively zero } // Align the hook's rotation with the line's rotation for a consistent look. 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 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 }); } // Add rotation animation for boat rocking var boatRotationAmplitude = 0.03; // 0.03 radians = ~1.7 degrees var boatRotationDuration = 3000; // 3 seconds for a full rotation cycle function rockBoatLeft() { if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) { return; } tween(boat, { rotation: -boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut, onFinish: rockBoatRight }); // Counter-rotate fisherman to keep upright 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 }); // Counter-rotate fisherman to keep upright tween(fisherman, { rotation: -boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut }); } // Start the synchronized animation cycle 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 }); // Start boat rocking animation 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 // Expose if needed, though global access is fine }; } /**** * 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 /**** * 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; GameState.currentScreen = screenName; switch (screenName) { case 'title': titleScreen.visible = true; break; case 'levelSelect': levelSelectScreen.visible = true; updateLevelSelectScreen(); break; case 'fishing': fishingScreen.visible = true; startFishingSession(); break; case 'results': resultsScreen.visible = true; break; } } /**** * 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; } // Helper function to get section spawn multiplier function getSectionSpawnMultiplier(pattern, songPositionSeconds) { if (!pattern.sections) { return 1.0; } var elapsedTimeMs = songPositionSeconds * 1000; for (var i = 0; i < pattern.sections.length; i++) { var section = pattern.sections[i]; if (elapsedTimeMs >= section.startTime && elapsedTimeMs < section.endTime) { return section.spawnModifier; } } return 1.0; // Default if no section matches } // New function to update song position (call this first in game.update) function updateSongPosition() { if (!GameState.gameActive || GameState.audioStartTime === 0) { return; } // Use LK.ticks-based timing converted to milliseconds var currentAudioTime = LK.ticks * (1000 / 60); GameState.songPositionSeconds = (currentAudioTime - GameState.audioStartTime) / 1000; var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; if (secPerBeat > 0) { GameState.songPositionBeats = GameState.songPositionSeconds / secPerBeat; } else { GameState.songPositionBeats = 0; // Avoid division by zero if BPM is 0 } } // Simplified and Fixed Pattern System function checkAndSpawnFish() { if (!GameState.gameActive) { return; } var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var currentBeat = GameState.songPositionBeats; // Simple approach: calculate next spawn beat based on pattern var baseSpacing = pattern.beatsPerFish || 2; // Default to every 2 beats // Apply section modifier var spawnMultiplier = getSectionSpawnMultiplier(pattern, GameState.songPositionSeconds); var actualSpacing = baseSpacing / spawnMultiplier; if (actualSpacing <= 0) actualSpacing = 0.1; // Prevent zero or negative spacing // Calculate the next expected spawn beat var expectedNextBeat; if (GameState.lastSpawnBeat < 0) { // First spawn - start at beat 0 or slightly after expectedNextBeat = 0; } else { // Ensure expectedNextBeat is always ahead of lastSpawnBeat // This handles cases where actualSpacing might be very small or fluctuates var calculatedNext = GameState.lastSpawnBeat + actualSpacing; // If calculatedNext is too close or behind, advance by at least a small fraction of a beat or actualSpacing if (calculatedNext <= GameState.lastSpawnBeat + 0.01) { expectedNextBeat = GameState.lastSpawnBeat + Math.max(0.1, actualSpacing); } else { expectedNextBeat = calculatedNext; } } // Check if we should spawn now if (currentBeat >= expectedNextBeat) { GameState.lastSpawnBeat = expectedNextBeat; // Update with the beat we are spawning for var firstFish = spawnFishAtBeat(expectedNextBeat); if (firstFish) { // Handle multi-spawns if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { // Spawn second fish 0.5 beats later scheduleDelayedSpawn(expectedNextBeat + 0.5, firstFish); if (pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance) { // Spawn third fish 1 beat later (relative to first fish of multi-spawn) scheduleDelayedSpawn(expectedNextBeat + 1.0, firstFish); } } } } } // New function for spawning fish at specific beats function spawnFishAtBeat(targetBeat, options) { options = options || {}; var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; // Calculate exact timing for when fish should reach hook var beatsToReachHook = depthConfig.beatsToReachHook || 3; var hookArrivalBeat = targetBeat + beatsToReachHook; // hookArrivalTime is relative to audioStartTime var hookArrivalTime = GameState.audioStartTime + hookArrivalBeat * secPerBeat * 1000; // Calculate fish speed for position-based movement (not frame-based) var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; // Assumes spawn margin of 150px from center var travelTimeMs = beatsToReachHook * secPerBeat * 1000; var fishSpeedPxPerMs = 0; if (travelTimeMs > 0) { fishSpeedPxPerMs = distanceToHook / travelTimeMs; // pixels per millisecond } else { fishSpeedPxPerMs = 0.2; // Fallback speed if travelTimeMs is 0 (e.g. BPM very high or beatsToReachHook is 0) } var laneIndex = options.laneIndexToUse !== undefined ? options.laneIndexToUse : PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; var fishType, fishValue; // Fish type determination (unchanged from original logic) var rand = Math.random(); var currentPatternConfig = GAME_CONFIG.PATTERNS[songConfig.pattern] || GAME_CONFIG.PATTERNS.simple; // Fallback if (rand < currentPatternConfig.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 spawnSide = options.forcedSpawnSide !== undefined ? options.forcedSpawnSide : Math.random() < 0.5 ? -1 : 1; // -1 for left (moves right), 1 for right (moves left) // The third argument to Fish constructor (speed) is pixels per frame in original. // We are now using speedPxPerMs. The Fish instance will store this new property. // Pass the per-ms speed, though the old `this.speed` in Fish might not be directly used by new update. var newFish = new Fish(fishType, fishValue, fishSpeedPxPerMs * spawnSide, laneIndex); newFish.spawnBeat = targetBeat; // Beat number when spawned newFish.arrivalBeat = hookArrivalBeat; // Target beat number for arrival at hook newFish.arrivalTime = hookArrivalTime; // Target audio time for arrival at hook (performance.now() scale) newFish.speedPxPerMs = fishSpeedPxPerMs * spawnSide; // pixels per millisecond, signed for direction newFish.x = newFish.speedPxPerMs > 0 ? -150 : 2048 + 150; // Start off-screen newFish.y = targetLane.y; newFish.baseY = targetLane.y; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(GameState.audioStartTime + targetBeat * secPerBeat * 1000); // Register with audio time return newFish; } // Updated scheduleDelayedSpawn for fractional beat delays function scheduleDelayedSpawn(targetBeat, referenceFish) { var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; if (secPerBeat <= 0) secPerBeat = 0.5; // Fallback if BPM is 0 or invalid // Calculate delay in milliseconds from current song position var currentBeat = GameState.songPositionBeats; var beatDelay = targetBeat - currentBeat; var delayMs = beatDelay * secPerBeat * 1000; // Ensure minimum delay if targetBeat is very close or slightly in the past due to processing lag if (delayMs < 16) { // LK.setTimeout has a minimum practical resolution delayMs = 16; } LK.setTimeout(function () { // Only spawn if game is still active if (!GameState.gameActive) { return; } // Check if we've significantly overshot the targetBeat due to lag or game state changes // Allow a small margin (e.g., 0.25 beats) for timing leniency. if (GameState.songPositionBeats > targetBeat + 0.25) { // console.log("Delayed spawn for beat " + targetBeat + " missed (current: " + GameState.songPositionBeats + ")"); return; } var options = {}; if (referenceFish && typeof referenceFish.lane === 'number' && typeof referenceFish.speedPxPerMs === 'number') { if (Math.random() < 0.5) { // Same lane as reference fish options.laneIndexToUse = referenceFish.lane; options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs); } else { // Adjacent lane var possibleLanes = []; if (referenceFish.lane > 0) { possibleLanes.push(referenceFish.lane - 1); } if (referenceFish.lane < GAME_CONFIG.LANES.length - 1) { possibleLanes.push(referenceFish.lane + 1); } if (possibleLanes.length > 0) { options.laneIndexToUse = possibleLanes[Math.floor(Math.random() * possibleLanes.length)]; // 70% chance opposite side for visual variety if (Math.random() < 0.7) { options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs) * -1; } else { options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs); } } else { // Only one lane available (e.g., middle lane in a 1-lane system, or edge lane) options.laneIndexToUse = referenceFish.lane; // If only one lane, vary side more often if (Math.random() < 0.5) { options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs) * -1; } else { options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs); } } } } else { // No reference fish, or reference fish data is incomplete options.laneIndexToUse = PatternGenerator.getNextLane(); // spawnFishAtBeat will decide side if not forced } spawnFishAtBeat(targetBeat, options); }, delayMs); } /**** * Fishing Game Logic ****/ function startFishingSession() { // Reset session state GameState.sessionScore = 0; GameState.sessionFishCaught = 0; GameState.sessionFishSpawned = 0; GameState.combo = 0; GameState.maxCombo = 0; GameState.sessionScore = 0; GameState.sessionFishCaught = 0; GameState.sessionFishSpawned = 0; GameState.combo = 0; GameState.maxCombo = 0; GameState.gameActive = true; // Reset original timing properties (if still used for anything) GameState.songStartTime = 0; // LK.ticks based, might be obsolete or for coarse UI GameState.lastBeatTime = 0; // LK.ticks based GameState.beatCount = 0; // Initialize audio-based timing GameState.audioStartTime = 0; // Will be set precisely after this reset block GameState.songPositionSeconds = 0; GameState.songPositionBeats = 0; GameState.lastSpawnBeat = -1; // Initialize to -1 to allow spawning on beat 0 // Clear any existing fish fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; // Clear existing bubbles bubblesArray.forEach(function (bubble) { if (bubble && bubble.destroy) bubble.destroy(); }); bubblesArray = []; // Reset pattern generator for new session PatternGenerator.reset(); // Start music and record precise start time var songConfig = GameState.getCurrentSongConfig(); var musicIdToPlay = songConfig.musicId || 'rhythmTrack'; GameState.currentPlayingMusicId = musicIdToPlay; // Determine initial volume from our predefined map for correct fade-out later. // This is necessary because LK.getMusic() is not available to query asset properties at runtime. // The '|| 0.7' acts as a fallback if musicIdToPlay is not in musicInitialVolumes // or if its value in the map is falsy (e.g. 0, though volumes are typically > 0). // For current assets, this ensures GameState.currentPlayingMusicInitialVolume is set to 0.7. GameState.currentPlayingMusicInitialVolume = musicInitialVolumes[musicIdToPlay] || 0.7; // Record the audio system time when music starts // LK.playMusic is asynchronous in terms of when audio *actually* starts. // Use LK.ticks converted to milliseconds as our timing reference GameState.audioStartTime = LK.ticks * (1000 / 60); LK.playMusic(GameState.currentPlayingMusicId); // Initialize songStartTime (LK.ticks based) for any legacy UI or rough timing GameState.songStartTime = LK.ticks * (1000 / 60); GameState.lastBeatTime = GameState.songStartTime; } 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 (same as before) 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, do not register } } } var laneIndex; if (options.laneIndexToUse !== undefined) { laneIndex = options.laneIndexToUse; PatternGenerator.lastLane = laneIndex; } 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; } // Calculate fish speed to arrive on beat var beatInterval = 60000 / songConfig.bpm; var beatsToReachHook = depthConfig.beatsToReachHook || 3; // Default to 3 beats if not specified var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; // Assumes spawn margin of 150px var calculatedFishSpeedPxPerFrame = 0; if (beatInterval > 0 && beatsToReachHook > 0) { // targetTravelTimeMs = beatsToReachHook * beatInterval // targetTravelTimeFrames = targetTravelTimeMs / (1000 / 60) // speed = distance / targetTravelTimeFrames calculatedFishSpeedPxPerFrame = distanceToHook * 1000 / (beatsToReachHook * beatInterval * 60); } else { // Fallback if beatInterval or beatsToReachHook is invalid (e.g. BPM 0) // Use a default speed, though this scenario should be prevented by valid config calculatedFishSpeedPxPerFrame = 5; // A nominal default speed } var spawnSide; // -1 for left (positive speed), 1 for right (negative speed) var actualFishSpeed; if (options.forcedSpawnSide !== undefined) { spawnSide = options.forcedSpawnSide; } else { spawnSide = Math.random() < 0.5 ? -1 : 1; } actualFishSpeed = calculatedFishSpeedPxPerFrame * 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 currentTime = LK.ticks * (1000 / 60); // Use LK.ticks-based timing var songConfig = GameState.getCurrentSongConfig(); var secPerBeat = 60.0 / songConfig.bpm; var beatIntervalMs = 0; if (secPerBeat > 0) { beatIntervalMs = secPerBeat * 1000; // Convert to milliseconds } // Check if the tap is on beat using song position if (GameState.audioStartTime > 0 && beatIntervalMs > 0) { var timeSinceSongStartMs = currentTime - GameState.audioStartTime; var timeIntoBeatCycleMs = timeSinceSongStartMs % beatIntervalMs; var timeToNearestBeatMs = Math.min(timeIntoBeatCycleMs, beatIntervalMs - timeIntoBeatCycleMs); if (timeToNearestBeatMs > GAME_CONFIG.GOOD_WINDOW) { // GAME_CONFIG.GOOD_WINDOW is in ms // Tap was off-beat showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; animateHookCatch(); return; } } else if (beatIntervalMs <= 0) { // Fallback or error case: if beatIntervalMs is invalid (e.g., BPM 0), can't check beat timing. // Optionally, always miss or proceed with proximity only. // For now, proceeding with proximity but this should be an edge case. } //{9i} // Retaining original line ID for context 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, but tap was on beat (or beat check skipped) showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; animateHookCatch(); return; } // --- Normal Fish Catch Logic (tap was on beat and fish is present) --- 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) { // Even if fish is further, if tap was on beat and fish is within miss_window, still a catch (but maybe less points or just 'good') // For now, let's keep it as 'good' to be somewhat lenient if beat timing is met. points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier)); showFeedback('good', fishLane); GameState.combo++; } else { // Tap was on beat, but fish was too far (outside MISS_WINDOW) showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; animateHookCatch(); 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); // Clear fish fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; // 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 })); 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 () { if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } // CRITICAL: Update song position first, before everything else updateSongPosition(); // Update the animated fishing line wave if (fishingElements.updateFishingLineWave) { fishingElements.updateFishingLineWave(); } // Note: LK.ticks * (1000/60) is for frame-based timing. // GameState.songStartTime (LK.ticks based) might still be used for coarse UI or if audioStartTime isn't ready. // However, core logic should now use GameState.songPositionSeconds/Beats. var songConfig = GameState.getCurrentSongConfig(); // Check song end using precise timing // Ensure songConfig and duration are valid before checking if (songConfig && typeof songConfig.duration === 'number' && GameState.audioStartTime > 0) { if (GameState.songPositionSeconds * 1000 >= songConfig.duration) { endFishingSession(); return; } } else if (!songConfig || typeof songConfig.duration !== 'number') { // console.warn("Song config or duration is invalid. Cannot check song end."); // Potentially end session if config is broken to prevent endless loop } // Use position-based fish spawning if (GameState.audioStartTime > 0) { // Only spawn if audio timing is initialized checkAndSpawnFish(); } // 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 bubbles for active fish if (bubbleContainer) { var currentTime = LK.ticks * (1000 / 60); // Define currentTime for bubble spawning 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
@@ -178,12 +178,12 @@
/****
* Game Code
****/
+// If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up.
/****
* Pattern Generation System
****/
-// If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up.
var PatternGenerator = {
lastLane: -1,
minDistanceBetweenFish: 300,
// Used by spawnFish internal check
@@ -362,87 +362,88 @@
cost: 600
}]
}],
// Updated patterns
+ // Updated pattern configurations - much simpler and more predictable
PATTERNS: {
simple: {
- beatsPerFish: 2,
- // Increased from 1 - fish every 2 beats
+ beatsPerFish: 2.0,
+ // Fish every 2 beats
doubleSpawnChance: 0.10,
- // 10% chance of a double beat in simple
+ // 10% chance of double beat
rareSpawnChance: 0.02
},
medium: {
beatsPerFish: 1.5,
- // Increased from 0.75
+ // Fish every 1.5 beats
doubleSpawnChance: 0.15,
- //{1R} // 15% chance of a double beat
+ // 15% chance of double beat
rareSpawnChance: 0.05
},
complex: {
- beatsPerFish: 1,
- // Increased from 0.5
+ beatsPerFish: 1.0,
+ // Fish every beat
doubleSpawnChance: 0.25,
- //{1U} // 25% chance of a double beat
+ // 25% chance of double beat
rareSpawnChance: 0.08
},
expert: {
beatsPerFish: 0.75,
- // Increased from 0.25
+ // Fish every 3/4 beat
doubleSpawnChance: 0.35,
- //{1X} // 35% chance of a double beat
+ // 35% chance of double beat
tripletSpawnChance: 0.20,
- // 20% chance a double beat becomes a triplet
+ // 20% chance triple becomes triplet
rareSpawnChance: 0.12
},
+ // Simplified gentle waves pattern
gentle_waves_custom: {
- beatsPerFish: 1.5,
- // Slightly more frequent than default simple
+ beatsPerFish: 2.0,
+ // Start with fish every 2 beats
doubleSpawnChance: 0.05,
- // Very rare double beats for shallow
+ // Very rare double beats
rareSpawnChance: 0.01,
// Almost no rare fish
- // Custom timing sections based on the musical structure
sections: [
- // Opening - Simple chord pattern (0-30 seconds)
+ // Opening - Sparse (0-30 seconds)
{
startTime: 0,
endTime: 30000,
- spawnModifier: 1.0,
- // Normal spawn rate
- description: "steady_chords"
+ spawnModifier: 0.8,
+ // Fewer fish = 2.0/0.8 = 2.5 beats spacing
+ description: "intro"
},
- // Melody Introduction (30-60 seconds)
+ // Melody (30-60 seconds) - Normal
{
startTime: 30000,
endTime: 60000,
- spawnModifier: 0.9,
- // Slightly fewer fish
- description: "simple_melody"
+ spawnModifier: 1.0,
+ // Normal = 2.0 beats spacing
+ description: "melody"
},
- // Development (60-120 seconds) - Gets a bit busier
+ // Development (60-120 seconds) - Slightly busier
{
startTime: 60000,
endTime: 120000,
- spawnModifier: 1.1,
- // Slightly more fish
- description: "melody_development"
+ spawnModifier: 1.2,
+ // More fish = 2.0/1.2 = 1.67 beats spacing
+ description: "development"
},
- // Climax (120-180 seconds) - Busiest section but still shallow
+ // Climax (120-180 seconds) - Busiest
{
startTime: 120000,
endTime: 180000,
- spawnModifier: 1.3,
- // More fish, but not overwhelming
- description: "gentle_climax"
+ spawnModifier: 1.5,
+ // Most fish = 2.0/1.5 = 1.33 beats spacing
+ description: "climax"
},
- // Ending (180-202 seconds) - Calming down
+ // Ending (180-202 seconds) - Calm down
{
startTime: 180000,
endTime: 202000,
- spawnModifier: 0.8,
- // Fewer fish for gentle ending
- description: "peaceful_ending"
+ spawnModifier: 0.7,
+ // Fewer fish = 2.0/0.7 = 2.86 beats spacing
+ description: "outro"
}]
}
}
};
@@ -1362,8 +1363,22 @@
var minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
}
+// Helper function to get section spawn multiplier
+function getSectionSpawnMultiplier(pattern, songPositionSeconds) {
+ if (!pattern.sections) {
+ return 1.0;
+ }
+ var elapsedTimeMs = songPositionSeconds * 1000;
+ for (var i = 0; i < pattern.sections.length; i++) {
+ var section = pattern.sections[i];
+ if (elapsedTimeMs >= section.startTime && elapsedTimeMs < section.endTime) {
+ return section.spawnModifier;
+ }
+ }
+ return 1.0; // Default if no section matches
+}
// New function to update song position (call this first in game.update)
function updateSongPosition() {
if (!GameState.gameActive || GameState.audioStartTime === 0) {
return;
@@ -1378,61 +1393,50 @@
} else {
GameState.songPositionBeats = 0; // Avoid division by zero if BPM is 0
}
}
-// Fixed fish spawning logic
+// Simplified and Fixed Pattern System
function checkAndSpawnFish() {
- if (!GameState.gameActive) return;
+ if (!GameState.gameActive) {
+ return;
+ }
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
- var currentBeatFloor = Math.floor(GameState.songPositionBeats);
- // Calculate which beats should have fish based on pattern
- var beatSpacing = pattern.beatsPerFish;
- // Ensure beatSpacing is positive to avoid infinite loops or issues
- if (beatSpacing <= 0) beatSpacing = 1; // Fallback to 1 if pattern.beatsPerFish is invalid
- var nextSpawnBeat = GameState.lastSpawnBeat < 0 ? 0 : Math.floor((GameState.lastSpawnBeat + beatSpacing) / beatSpacing) * beatSpacing;
- if (GameState.lastSpawnBeat >= 0 && nextSpawnBeat <= GameState.lastSpawnBeat) {
- // Ensure progression
- nextSpawnBeat = Math.floor(GameState.lastSpawnBeat / beatSpacing + 1) * beatSpacing;
- }
- // Apply section-based spawn modifier if pattern has sections
- var spawnMultiplier = 1.0;
- if (pattern.sections) {
- var elapsedTimeSeconds = GameState.songPositionSeconds;
- for (var s = 0; s < pattern.sections.length; s++) {
- var section = pattern.sections[s];
- if (elapsedTimeSeconds >= section.startTime / 1000 && elapsedTimeSeconds < section.endTime / 1000) {
- spawnMultiplier = section.spawnModifier;
- break;
- }
+ var currentBeat = GameState.songPositionBeats;
+ // Simple approach: calculate next spawn beat based on pattern
+ var baseSpacing = pattern.beatsPerFish || 2; // Default to every 2 beats
+ // Apply section modifier
+ var spawnMultiplier = getSectionSpawnMultiplier(pattern, GameState.songPositionSeconds);
+ var actualSpacing = baseSpacing / spawnMultiplier;
+ if (actualSpacing <= 0) actualSpacing = 0.1; // Prevent zero or negative spacing
+ // Calculate the next expected spawn beat
+ var expectedNextBeat;
+ if (GameState.lastSpawnBeat < 0) {
+ // First spawn - start at beat 0 or slightly after
+ expectedNextBeat = 0;
+ } else {
+ // Ensure expectedNextBeat is always ahead of lastSpawnBeat
+ // This handles cases where actualSpacing might be very small or fluctuates
+ var calculatedNext = GameState.lastSpawnBeat + actualSpacing;
+ // If calculatedNext is too close or behind, advance by at least a small fraction of a beat or actualSpacing
+ if (calculatedNext <= GameState.lastSpawnBeat + 0.01) {
+ expectedNextBeat = GameState.lastSpawnBeat + Math.max(0.1, actualSpacing);
+ } else {
+ expectedNextBeat = calculatedNext;
}
- // Adjust spacing based on section modifier, ensuring it's positive
- var modifiedBeatSpacing = pattern.beatsPerFish / spawnMultiplier;
- if (modifiedBeatSpacing <= 0) modifiedBeatSpacing = 1; // Fallback
- nextSpawnBeat = GameState.lastSpawnBeat < 0 ? 0 : Math.floor((GameState.lastSpawnBeat + modifiedBeatSpacing) / modifiedBeatSpacing) * modifiedBeatSpacing;
- if (GameState.lastSpawnBeat >= 0 && nextSpawnBeat <= GameState.lastSpawnBeat) {
- // Ensure progression
- nextSpawnBeat = Math.floor(GameState.lastSpawnBeat / modifiedBeatSpacing + 1) * modifiedBeatSpacing;
- }
}
- // Spawn fish if we've passed the next spawn beat
- if (currentBeatFloor >= nextSpawnBeat && nextSpawnBeat > GameState.lastSpawnBeat) {
- GameState.lastSpawnBeat = nextSpawnBeat;
- // GameState.beatCount++; // This beatCount might be redundant or need re-evaluation for its purpose
- // Calculate exact spawn time for this beat (using performance.now() scale)
- var secPerBeat = 60.0 / songConfig.bpm;
- var exactSpawnAudioTime = GameState.audioStartTime + nextSpawnBeat * secPerBeat * 1000;
- // Using a simplified canSpawnFishOnBeat or relying on PatternGenerator internally
- // The provided canSpawnFishOnBeat used currentTime and configuredSpawnInterval.
- // Let's assume for now that the beat timing itself is the primary gate.
- // PatternGenerator.registerFishSpawn might still be useful for its internal state if it tracks density.
- var firstFish = spawnFishAtBeat(nextSpawnBeat); // Pass targetBeat
+ // Check if we should spawn now
+ if (currentBeat >= expectedNextBeat) {
+ GameState.lastSpawnBeat = expectedNextBeat; // Update with the beat we are spawning for
+ var firstFish = spawnFishAtBeat(expectedNextBeat);
if (firstFish) {
- // Handle double/triple spawns with precise timing
+ // Handle multi-spawns
if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) {
- scheduleDelayedSpawn(nextSpawnBeat + 1, firstFish); // Spawn on the next beat
+ // Spawn second fish 0.5 beats later
+ scheduleDelayedSpawn(expectedNextBeat + 0.5, firstFish);
if (pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance) {
- scheduleDelayedSpawn(nextSpawnBeat + 2, firstFish); // Spawn on the beat after next
+ // Spawn third fish 1 beat later (relative to first fish of multi-spawn)
+ scheduleDelayedSpawn(expectedNextBeat + 1.0, firstFish);
}
}
}
}
@@ -1493,57 +1497,71 @@
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(GameState.audioStartTime + targetBeat * secPerBeat * 1000); // Register with audio time
return newFish;
}
-// Helper function for delayed spawns
+// Updated scheduleDelayedSpawn for fractional beat delays
function scheduleDelayedSpawn(targetBeat, referenceFish) {
var songConfig = GameState.getCurrentSongConfig();
var secPerBeat = 60.0 / songConfig.bpm;
- // Delay is effectively one beat, but LK.setTimeout needs an absolute delay from "now"
- // The targetBeat is already the "future" beat. We just schedule the call.
- // The actual delay is implicitly handled by checkAndSpawnFish recognizing future beats.
- // This function is to trigger a spawn AT a future beat.
- // The goal's scheduleDelayedSpawn uses LK.setTimeout with a delay.
- // This implies the main spawn loop might not catch every single beat for multi-spawns if it's too fast.
- // Let's stick to the goal's LK.setTimeout approach.
- var delayMs = (targetBeat - GameState.songPositionBeats) * secPerBeat * 1000;
- // Ensure delay is not negative if we somehow missed the exact beat for the referenceFish.
- // Minimal delay if targetBeat is very close or slightly in past due to processing lag.
- if (delayMs < 0) delayMs = secPerBeat * 1000; // Delay by one beat interval if calculated is past
- if (delayMs < 16) delayMs = 16; // Minimum timeout delay
+ if (secPerBeat <= 0) secPerBeat = 0.5; // Fallback if BPM is 0 or invalid
+ // Calculate delay in milliseconds from current song position
+ var currentBeat = GameState.songPositionBeats;
+ var beatDelay = targetBeat - currentBeat;
+ var delayMs = beatDelay * secPerBeat * 1000;
+ // Ensure minimum delay if targetBeat is very close or slightly in the past due to processing lag
+ if (delayMs < 16) {
+ // LK.setTimeout has a minimum practical resolution
+ delayMs = 16;
+ }
LK.setTimeout(function () {
- // Only spawn if the game is still active and the targetBeat hasn't been grossly overshot
- if (!GameState.gameActive || Math.floor(GameState.songPositionBeats) > targetBeat + 1) {
+ // Only spawn if game is still active
+ if (!GameState.gameActive) {
return;
}
+ // Check if we've significantly overshot the targetBeat due to lag or game state changes
+ // Allow a small margin (e.g., 0.25 beats) for timing leniency.
+ if (GameState.songPositionBeats > targetBeat + 0.25) {
+ // console.log("Delayed spawn for beat " + targetBeat + " missed (current: " + GameState.songPositionBeats + ")");
+ return;
+ }
var options = {};
- var newSpawnSide;
- if (Math.random() < 0.5 && referenceFish) {
- // Same lane as reference fish
- options.laneIndexToUse = referenceFish.lane;
- newSpawnSide = Math.sign(referenceFish.speedPxPerMs); // comes from same side
- options.forcedSpawnSide = newSpawnSide;
- } else if (referenceFish) {
- // Adjacent lane
- var possibleLanes = [];
- if (referenceFish.lane > 0) possibleLanes.push(referenceFish.lane - 1);
- if (referenceFish.lane < GAME_CONFIG.LANES.length - 1) possibleLanes.push(referenceFish.lane + 1);
- if (possibleLanes.length > 0) {
- options.laneIndexToUse = possibleLanes[Math.floor(Math.random() * possibleLanes.length)];
- // For adjacent, let spawnFishAtBeat decide side, or force opposite for variety
- if (Math.random() < 0.7) {
- // 70% chance opposite side for visual spread
- options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs) * -1;
- }
- } else {
- // Fallback if no adjacent (e.g. referenceFish was null or bad lane)
+ if (referenceFish && typeof referenceFish.lane === 'number' && typeof referenceFish.speedPxPerMs === 'number') {
+ if (Math.random() < 0.5) {
+ // Same lane as reference fish
options.laneIndexToUse = referenceFish.lane;
- newSpawnSide = Math.sign(referenceFish.speedPxPerMs);
- options.forcedSpawnSide = newSpawnSide;
+ options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs);
+ } else {
+ // Adjacent lane
+ var possibleLanes = [];
+ if (referenceFish.lane > 0) {
+ possibleLanes.push(referenceFish.lane - 1);
+ }
+ if (referenceFish.lane < GAME_CONFIG.LANES.length - 1) {
+ possibleLanes.push(referenceFish.lane + 1);
+ }
+ if (possibleLanes.length > 0) {
+ options.laneIndexToUse = possibleLanes[Math.floor(Math.random() * possibleLanes.length)];
+ // 70% chance opposite side for visual variety
+ if (Math.random() < 0.7) {
+ options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs) * -1;
+ } else {
+ options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs);
+ }
+ } else {
+ // Only one lane available (e.g., middle lane in a 1-lane system, or edge lane)
+ options.laneIndexToUse = referenceFish.lane;
+ // If only one lane, vary side more often
+ if (Math.random() < 0.5) {
+ options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs) * -1;
+ } else {
+ options.forcedSpawnSide = Math.sign(referenceFish.speedPxPerMs);
+ }
+ }
}
} else {
- // Fallback if no referenceFish
+ // No reference fish, or reference fish data is incomplete
options.laneIndexToUse = PatternGenerator.getNextLane();
+ // spawnFishAtBeat will decide side if not forced
}
spawnFishAtBeat(targetBeat, options);
}, delayMs);
}
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