Code edit (1 edits merged)
Please save this source code
User prompt
double the size of the score popup on the fishing screen
Code edit (2 edits merged)
Please save this source code
User prompt
move the back button on the level select screen, centered on the bottom of the screen
Code edit (1 edits merged)
Please save this source code
User prompt
update with: // Change FISH_SPAWN_END_BUFFER_MS to be much smaller var FISH_SPAWN_END_BUFFER_MS = 500; // Just 0.5 seconds buffer // In ImprovedRhythmSpawner.scheduleBeatFish, replace the entire buffer logic: scheduleBeatFish: function scheduleBeatFish(beatNumber, spawnInterval, travelTimeMs) { var targetArrivalTime = GameState.songStartTime + beatNumber * spawnInterval; var songConfig = GameState.getCurrentSongConfig(); // FIXED: The buffer should be applied to when the song ACTUALLY ends, // not when the fish arrives at the hook var songEndTime = GameState.songStartTime + songConfig.duration; var lastValidArrivalTime = songEndTime - FISH_SPAWN_END_BUFFER_MS; if (songConfig && GameState.songStartTime > 0 && targetArrivalTime > lastValidArrivalTime) { return; // Don't schedule this fish, it would arrive too close to song end } var spawnTime = targetArrivalTime - travelTimeMs; var currentTime = LK.ticks * (1000 / 60); // Rest of function stays the same... if (spawnTime >= currentTime - 100) { var delay = Math.max(0, spawnTime - currentTime); var self = this; LK.setTimeout(function () { if (GameState.gameActive && GameState.songStartTime !== 0) { self.spawnRhythmFish(beatNumber, targetArrivalTime); } }, delay); } },
Code edit (1 edits merged)
Please save this source code
User prompt
Fish spawning is stopping way too early in a song. Take a the maximum time it takes a fish to get to the hook and double it. Subtract that from the song length. That’s when the last fish should spawn. Don’t stop spawning too early?
User prompt
After a fish lands in the boat, add a score pop up that floats up from the boat. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Increase the size of the tutorial text and adjust container size and formatting to adjust.
Code edit (1 edits merged)
Please save this source code
User prompt
If case 3 or 4 in the tutorial are successful, move to the next step. Only reset if it’s not successful.
Code edit (1 edits merged)
Please save this source code
User prompt
If you miss a fish in the tutorial, make sure to reset that step.
User prompt
Do not progress the tutorial if a fish is missed, reset that step correctly.
User prompt
Add a section to the tutorial that explains about the different lanes and show where you have to tap I with a transparent lane overlay
User prompt
Some of the tutorial phases are being skipped. Analyze and fix.
User prompt
Fade in the tutorialbutton AFTER the start button. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (3 edits merged)
Please save this source code
User prompt
No half points for fishes. All points are full numbers.
User prompt
Remove the text from the title screen and just use the button assets.
Code edit (1 edits merged)
Please save this source code
Code edit (1 edits merged)
Please save this source code
User prompt
Replace the start and tutorial buttons on the title screen with their appropriately named assets.
Code edit (1 edits merged)
Please save this source code
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var BubbleParticle = Container.expand(function (startX, startY) { var self = Container.call(this); self.gfx = self.attachAsset('bubbles', { anchorX: 0.5, anchorY: 0.5, alpha: 1 + Math.random() * 0.2, // Start with some variation scaleX: 1.2 + Math.random() * 0.25, // Larger bubbles // Bubbles are now 20% to 45% of original asset size scaleY: this.scaleX // Keep aspect ratio initially, can be changed in update if desired }); self.x = startX; self.y = startY; self.vx = (Math.random() - 0.5) * 0.4; // Even slower horizontal drift self.vy = -(0.4 + Math.random() * 0.3); // Slower upward movement self.life = 120 + Math.random() * 60; // Lifespan in frames (2 to 3 seconds) self.age = 0; self.isDone = false; var initialAlpha = self.gfx.alpha; var initialScale = self.gfx.scaleX; // Assuming scaleX and scaleY start the same self.update = function () { if (self.isDone) { return; } self.age++; self.x += self.vx; self.y += self.vy; // Fade out self.gfx.alpha = Math.max(0, initialAlpha * (1 - self.age / self.life)); // Optionally shrink a bit more, or grow slightly then shrink var scaleFactor = 1 - self.age / self.life; // Simple shrink self.gfx.scaleX = initialScale * scaleFactor; self.gfx.scaleY = initialScale * scaleFactor; if (self.age >= self.life || self.gfx.alpha <= 0 || self.gfx.scaleX <= 0.01) { self.isDone = true; } }; return self; }); var CloudParticle = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('cloud', { anchorX: 0.5, anchorY: 0.5 }); // Cloud spawn location - 30% chance to spawn on screen var spawnOnScreen = Math.random() < 0.3; if (spawnOnScreen) { // Spawn randomly across the screen width self.x = 200 + Math.random() * 1648; // Between 200 and 1848 to avoid edges // Random horizontal drift direction self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.1 + Math.random() * 0.2); } else { // Original off-screen spawn logic (70% of the time) var spawnFromLeft = Math.random() < 0.5; if (spawnFromLeft) { self.x = -100; // Start off-screen left self.vx = 0.1 + Math.random() * 0.2; // Slower drift right at 0.1-0.3 pixels/frame } else { self.x = 2048 + 100; // Start off-screen right self.vx = -(0.1 + Math.random() * 0.2); // Slower drift left } } // Spawn in sky area (above water surface) var skyTop = -500; // Where sky background starts var skyBottom = GAME_CONFIG.WATER_SURFACE_Y - 100; // Stay well above water self.y = skyTop + Math.random() * (skyBottom - skyTop); // Cloud properties var baseScale = 0.8 + Math.random() * 0.6; // Scale: 0.8x to 1.4x self.gfx.scale.set(baseScale); // Subtle vertical drift self.vy = (Math.random() - 0.5) * 0.02; // Even slower up/down drift // Opacity for atmospheric effect var targetAlpha = 0.4 + Math.random() * 0.3; // Alpha: 0.4 to 0.7 self.gfx.alpha = 0; // Start transparent self.isDone = false; self.fadingOut = false; self.hasFadedIn = false; // Track if initial fade in completed // Define screen boundaries for fade in/out var fadeInStartX = 400; // Start fading in when cloud reaches this X var fadeInEndX = 800; // Fully visible by this X var fadeOutStartX = 1248; // Start fading out at this X (2048 - 800) var fadeOutEndX = 1648; // Fully transparent by this X (2048 - 400) self.update = function () { if (self.isDone) { return; } // Apply movement self.x += self.vx; self.y += self.vy; // Handle mid-screen fade in/out based on position var currentAlpha = self.gfx.alpha; if (spawnFromLeft) { // Moving right: fade in then fade out if (!self.hasFadedIn && self.x >= fadeInStartX && self.x <= fadeInEndX) { // Calculate fade in progress var fadeInProgress = (self.x - fadeInStartX) / (fadeInEndX - fadeInStartX); self.gfx.alpha = targetAlpha * fadeInProgress; if (fadeInProgress >= 1) { self.hasFadedIn = true; } } else if (self.hasFadedIn && self.x >= fadeOutStartX && self.x <= fadeOutEndX) { // Calculate fade out progress var fadeOutProgress = (self.x - fadeOutStartX) / (fadeOutEndX - fadeOutStartX); self.gfx.alpha = targetAlpha * (1 - fadeOutProgress); } else if (self.hasFadedIn && self.x > fadeInEndX && self.x < fadeOutStartX) { // Maintain full opacity in middle section self.gfx.alpha = targetAlpha; } } else { // Moving left: fade in then fade out (reversed positions) if (!self.hasFadedIn && self.x <= fadeOutEndX && self.x >= fadeOutStartX) { // Calculate fade in progress (reversed) var fadeInProgress = (fadeOutEndX - self.x) / (fadeOutEndX - fadeOutStartX); self.gfx.alpha = targetAlpha * fadeInProgress; if (fadeInProgress >= 1) { self.hasFadedIn = true; } } else if (self.hasFadedIn && self.x <= fadeInEndX && self.x >= fadeInStartX) { // Calculate fade out progress (reversed) var fadeOutProgress = (fadeInEndX - self.x) / (fadeInEndX - fadeInStartX); self.gfx.alpha = targetAlpha * (1 - fadeOutProgress); } else if (self.hasFadedIn && self.x < fadeOutStartX && self.x > fadeInEndX) { // Maintain full opacity in middle section self.gfx.alpha = targetAlpha; } } // Check if completely off-screen var currentWidth = self.gfx.width * self.gfx.scale.x; if (self.x < -currentWidth || self.x > 2048 + currentWidth) { self.isDone = true; } }; return self; }); var FeedbackIndicator = Container.expand(function (type) { var self = Container.call(this); var indicator = self.attachAsset(type + 'Indicator', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); self.show = function () { indicator.alpha = 1; indicator.scaleX = 0.5; indicator.scaleY = 0.5; tween(indicator, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 1400, 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'; // Randomly pick from the three shallowFish assets if type is shallow if (type === 'shallow') { var shallowFishAssets = ['shallowFish', 'shallowFish2', 'shallowFish3']; assetName = shallowFishAssets[Math.floor(Math.random() * shallowFishAssets.length)]; } 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.missed = false; // True if the fish passed the hook without being caught self.lastX = 0; // Stores the x position from the previous frame for miss detection self.isSpecial = type === 'rare'; // For shimmer effect self.shimmerTime = 0; // Bubble properties self.lastBubbleSpawnTime = 0; self.bubbleSpawnInterval = 120 + Math.random() * 80; // 120-200ms interval (fewer bubbles) // Add properties for swimming animation self.swimTime = Math.random() * Math.PI * 2; // Random starting phase for variety self.baseY = self.y; // Store initial Y position self.scaleTime = 0; self.baseScale = 1; self.update = function () { if (!self.caught) { // Horizontal movement self.x += self.speed; // Sine wave vertical movement self.swimTime += 0.08; // Speed of sine wave oscillation var swimAmplitude = 15; // Pixels of vertical movement self.y = self.baseY + Math.sin(self.swimTime) * swimAmplitude; // Beat-synchronized scale pulsing if (GameState.gameActive && GameState.songStartTime > 0) { var currentTime = LK.ticks * (1000 / 60); var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var timeSinceLastBeat = (currentTime - GameState.songStartTime) % beatInterval; var beatProgress = timeSinceLastBeat / beatInterval; // Create a pulse effect that peaks at the beat var scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15; // 15% scale variation // Determine base scaleX considering direction var baseScaleXDirection = (self.speed > 0 ? -1 : 1) * self.baseScale; self.fishGraphics.scaleX = baseScaleXDirection * scalePulse; self.fishGraphics.scaleY = scalePulse * self.baseScale; } if (self.isSpecial) { // Shimmer effect for rare fish self.shimmerTime += 0.1; self.fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2; } else { // Reset alpha if not special self.fishGraphics.alpha = 1.0; } } }; self.catchFish = function () { self.caught = true; // Animation: Fish arcs up over the boat, then down into it. var currentFishX = self.x; var currentFishY = self.y; var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; var boatLandingY = GAME_CONFIG.BOAT_Y; // Y-coordinate where the fish "lands" in the boat // Define the peak of the arc - e.g., 150 pixels above the boat's landing spot var peakArcY = boatLandingY - 150; // Define the X coordinate at the peak of the arc - halfway between current X and boat center X var peakArcX = currentFishX + (boatCenterX - currentFishX) * 0.5; var durationPhase1 = 350; // Duration for the first part of the arc (upwards) var durationPhase2 = 250; // Duration for the second part of the arc (downwards), total 600ms // Phase 1: Arc upwards and towards the boat's horizontal center. // The container 'self' is tweened for position, scale, and alpha. // Initial self.scale.x and self.scale.y are expected to be 1 for the container. tween(self, { x: peakArcX, y: peakArcY, scaleX: 0.75, // Scale container to 75% of its original size scaleY: 0.75, alpha: 0.8 // Partially fade }, { duration: durationPhase1, easing: tween.easeOut, // Ease out for the upward motion to the peak onFinish: function onFinish() { // Phase 2: Arc downwards into the boat and disappear. tween(self, { x: boatCenterX, y: boatLandingY, scaleX: 0.2, // Shrink container further to 20% of its original size scaleY: 0.2, alpha: 0 // Fade out completely }, { duration: durationPhase2, easing: tween.easeIn, // Ease in for the downward motion into the boat onFinish: function onFinish() { self.destroy(); // Remove fish once animation is complete } }); } }); }; return self; }); var MusicNoteParticle = Container.expand(function (startX, startY) { var self = Container.call(this); var FADE_IN_DURATION_MS = 600; // Duration for the note to fade in var TARGET_ALPHA = 0.6 + Math.random() * 0.4; // Target alpha between 0.6 and 1.0 for variety self.gfx = self.attachAsset('musicnote', { anchorX: 0.5, anchorY: 0.5, alpha: 0, // Start invisible for fade-in scaleX: 0.4 + Math.random() * 0.4 // Random initial scale (0.4x to 0.8x) }); self.gfx.scaleY = self.gfx.scaleX; // Maintain aspect ratio self.x = startX; self.y = startY; // Animation properties for lazy floating self.vx = (Math.random() - 0.5) * 0.8; // Slow horizontal drift speed self.vy = -(0.8 + Math.random() * 0.7); // Steady upward speed (0.8 to 1.5 pixels/frame) self.rotationSpeed = (Math.random() - 0.5) * 0.008; // Very slow rotation self.life = 240 + Math.random() * 120; // Lifespan in frames (4 to 6 seconds) self.age = 0; self.isDone = false; // Initial fade-in tween tween(self.gfx, { alpha: TARGET_ALPHA }, { duration: FADE_IN_DURATION_MS, easing: tween.easeOut }); self.update = function () { if (self.isDone) { return; } self.age++; self.x += self.vx; self.y += self.vy; self.gfx.rotation += self.rotationSpeed; var FADE_IN_TICKS = FADE_IN_DURATION_MS / (1000 / 60); // Fade-in duration in ticks // Only manage alpha manually after the fade-in tween is expected to be complete. if (self.age > FADE_IN_TICKS) { var lifePortionForFadeOut = 0.6; // Use last 60% of life for fade out var fadeOutStartTimeTicks = self.life * (1 - lifePortionForFadeOut); if (self.age >= fadeOutStartTimeTicks && self.life > fadeOutStartTimeTicks) { // ensure self.life > fadeOutStartTimeTicks to avoid division by zero var progressInFadeOut = (self.age - fadeOutStartTimeTicks) / (self.life * lifePortionForFadeOut); self.gfx.alpha = TARGET_ALPHA * (1 - progressInFadeOut); self.gfx.alpha = Math.max(0, self.gfx.alpha); // Clamp at 0 } else if (self.age <= fadeOutStartTimeTicks) { // At this point, the initial fade-in tween to TARGET_ALPHA should have completed. // The alpha value is expected to remain at TARGET_ALPHA (as set by the initial tween) // until the fade-out logic (in the 'if (self.age >= fadeOutStartTimeTicks)' block) begins. // The previous 'tween.isTweening' check was removed as the method does not exist in the plugin. } } // Check if particle's life is over or it has faded out if (self.age >= self.life || self.gfx.alpha !== undefined && self.gfx.alpha <= 0.01 && self.age > FADE_IN_TICKS) { self.isDone = true; } }; return self; }); var OceanBubbleParticle = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('oceanbubbles', { anchorX: 0.5, anchorY: 0.5 }); self.initialX = Math.random() * 2048; var waterTop = GAME_CONFIG.WATER_SURFACE_Y; var waterBottom = 2732; self.x = self.initialX; // Allow bubbles to spawn anywhere in the water, not just below the bottom self.y = waterTop + Math.random() * (waterBottom - waterTop); var baseScale = 0.1 + Math.random() * 0.4; // Scale: 0.1x to 0.5x self.gfx.scale.set(baseScale); self.vy = -(0.25 + Math.random() * 0.5); // Upward speed: 0.25 to 0.75 pixels/frame (slower, less variance) self.naturalVy = self.vy; // Store natural velocity for recovery self.driftAmplitude = 20 + Math.random() * 40; // Sideways drift: 20px to 60px amplitude self.naturalDriftAmplitude = self.driftAmplitude; // Store natural drift for recovery self.driftFrequency = (0.005 + Math.random() * 0.015) * (Math.random() < 0.5 ? 1 : -1); // Sideways drift speed/direction self.driftPhase = Math.random() * Math.PI * 2; // Initial phase for sine wave self.rotationSpeed = (Math.random() - 0.5) * 0.01; // Slow random rotation var targetAlpha = 0.2 + Math.random() * 0.3; // Max alpha: 0.2 to 0.5 (dimmer background bubbles) self.gfx.alpha = 0; // Start transparent self.isDone = false; self.fadingOut = false; tween(self.gfx, { alpha: targetAlpha }, { duration: 1000 + Math.random() * 1000, // Slow fade in: 1 to 2 seconds easing: tween.easeIn }); self.update = function () { if (self.isDone) { return; } self.y += self.vy; // Increment age self.age++; // Check if lifespan exceeded if (!self.fadingOut && self.age >= self.lifespan) { self.fadingOut = true; tween.stop(self.gfx); tween(self.gfx, { alpha: 0 }, { duration: 600 + Math.random() * 400, // 0.6-1 second fade easing: tween.easeOut, onFinish: function onFinish() { self.isDone = true; } }); } self.driftPhase += self.driftFrequency; self.x = self.initialX + Math.sin(self.driftPhase) * self.driftAmplitude; self.gfx.rotation += self.rotationSpeed; // Recovery mechanism: gradually return to natural upward movement var naturalVy = -(0.25 + Math.random() * 0.5); // Natural upward speed var recoveryRate = 0.02; // How quickly bubble recovers (2% per frame) // If bubble is moving slower than its natural speed or downward, recover if (self.vy > naturalVy) { self.vy = self.vy + (naturalVy - self.vy) * recoveryRate; } // Also gradually reduce excessive drift amplitude back to normal var normalDriftAmplitude = 20 + Math.random() * 40; if (self.driftAmplitude > normalDriftAmplitude) { self.driftAmplitude = self.driftAmplitude + (normalDriftAmplitude - self.driftAmplitude) * recoveryRate; } // Check if bubble reached surface or went off-screen // Use gfx.height * current scale for accurate boundary check var currentHeight = self.gfx.height * self.gfx.scale.y; var currentWidth = self.gfx.width * self.gfx.scale.x; if (!self.fadingOut && self.y <= GAME_CONFIG.WATER_SURFACE_Y - currentHeight * 0.5) { self.fadingOut = true; tween.stop(self.gfx); // Stop fade-in if ongoing tween(self.gfx, { alpha: 0 }, { duration: 300 + Math.random() * 200, // Quick fade out easing: tween.easeOut, onFinish: function onFinish() { self.isDone = true; // Mark as fully done for removal } }); } else if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth)) { // Off screen top/sides before reaching surface and starting fade self.isDone = true; self.gfx.alpha = 0; // Disappear immediately } }; return self; }); var SeaweedParticle = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('kelp', { anchorX: 0.5, anchorY: 0.5 }); // Determine spawn location var spawnType = Math.random(); var waterTop = GAME_CONFIG.WATER_SURFACE_Y; var waterBottom = 2732; if (spawnType < 0.4) { // 40% spawn from bottom self.x = Math.random() * 2048; self.y = waterBottom + 50; self.vx = (Math.random() - 0.5) * 0.3; // Slight horizontal drift self.vy = -(0.4 + Math.random() * 0.3); // Upward movement } else if (spawnType < 0.7) { // 30% spawn from left self.x = -50; self.y = waterTop + Math.random() * (waterBottom - waterTop); self.vx = 0.4 + Math.random() * 0.3; // Rightward movement self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift } else { // 30% spawn from right self.x = 2048 + 50; self.y = waterTop + Math.random() * (waterBottom - waterTop); self.vx = -(0.4 + Math.random() * 0.3); // Leftward movement self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift } self.initialX = self.x; self.naturalVx = self.vx; // Store natural velocity for recovery self.naturalVy = self.vy; // Seaweed properties var baseScale = 0.6 + Math.random() * 0.6; // Scale: 0.6x to 1.2x self.gfx.scale.set(baseScale); self.swayAmplitude = 15 + Math.random() * 25; // Sway: 15px to 40px self.swayFrequency = (0.003 + Math.random() * 0.007) * (Math.random() < 0.5 ? 1 : -1); self.swayPhase = Math.random() * Math.PI * 2; // Random initial rotation (full 360 degrees) self.gfx.rotation = Math.random() * Math.PI * 2; // Random continuous rotation speed (slower than ocean bubbles) self.continuousRotationSpeed = (Math.random() - 0.5) * 0.003; // -0.0015 to 0.0015 radians per frame var targetAlpha = 0.3 + Math.random() * 0.3; // Alpha: 0.3 to 0.6 self.gfx.alpha = 0; // Start transparent self.isDone = false; self.fadingOut = false; self.reachedSurface = false; // Add random lifespan (10-30 seconds) self.lifespan = 600 + Math.random() * 1200; // 600-1800 frames (10-30 seconds at 60fps) self.age = 0; tween(self.gfx, { alpha: targetAlpha }, { duration: 1500 + Math.random() * 1000, easing: tween.easeIn }); self.update = function () { if (self.isDone) { return; } // Apply movement self.x += self.vx; self.y += self.vy; // Add sway effect self.swayPhase += self.swayFrequency; var swayOffset = Math.sin(self.swayPhase) * self.swayAmplitude; // Apply continuous rotation plus sway-based rotation self.gfx.rotation += self.continuousRotationSpeed + swayOffset * 0.0001; // Reduced sway rotation influence // Recovery mechanism for velocity var recoveryRate = 0.015; if (self.vx !== self.naturalVx) { self.vx = self.vx + (self.naturalVx - self.vx) * recoveryRate; } if (self.vy !== self.naturalVy) { self.vy = self.vy + (self.naturalVy - self.vy) * recoveryRate; } // Check if reached surface var currentHeight = self.gfx.height * self.gfx.scale.y; var currentWidth = self.gfx.width * self.gfx.scale.x; if (!self.reachedSurface && self.y <= GAME_CONFIG.WATER_SURFACE_Y + currentHeight * 0.3) { self.reachedSurface = true; // Change to horizontal drift at surface self.vy = 0; self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.5 + Math.random() * 0.5); self.naturalVx = self.vx; self.naturalVy = 0; } // Check if off-screen if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth || self.y > waterBottom + currentHeight)) { self.fadingOut = true; tween.stop(self.gfx); tween(self.gfx, { alpha: 0 }, { duration: 400 + Math.random() * 200, easing: tween.easeOut, onFinish: function onFinish() { self.isDone = true; } }); } }; return self; }); /**** * Initialize Game ****/ /**** * Screen Containers ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ // Constants for Title Screen Animation var TITLE_ANIM_CONSTANTS = { INITIAL_GROUP_ALPHA: 0, FINAL_GROUP_ALPHA: 1, INITIAL_UI_ALPHA: 0, FINAL_UI_ALPHA: 1, INITIAL_GROUP_SCALE: 3.5, // Start with an extreme closeup FINAL_GROUP_SCALE: 2.8, // Zoom out slightly, staying closer GROUP_ANIM_DURATION: 4000, // Slower duration for group fade-in and zoom TEXT_FADE_DURATION: 1000, // Duration for title text fade-in BUTTON_FADE_DURATION: 800, // Duration for buttons fade-in // Positioning constants relative to titleAnimationGroup's origin (0,0) which is boat's center BOAT_ANCHOR_X: 0.5, BOAT_ANCHOR_Y: 0.5, FISHERMAN_ANCHOR_X: 0.5, FISHERMAN_ANCHOR_Y: 0.9, // Anchor at feet FISHERMAN_X_OFFSET: -20, // Relative to boat center FISHERMAN_Y_OFFSET: -100, // Relative to boat center, fisherman sits on boat LINE_ANCHOR_X: 0.5, LINE_ANCHOR_Y: 0, // Anchor at top of line LINE_X_OFFSET_FROM_FISHERMAN: 70, // Rod tip X from fisherman center LINE_Y_OFFSET_FROM_FISHERMAN: -130, // Rod tip Y from fisherman center (fisherman height ~200, anchorY 0.9) HOOK_ANCHOR_X: 0.5, HOOK_ANCHOR_Y: 0.5, HOOK_Y_DEPTH_FROM_LINE_START: 700, // Slightly longer line for the closeup // titleAnimationGroup positioning GROUP_PIVOT_X: 0, // Boat's X in the group GROUP_PIVOT_Y: 0, // Boat's Y in the group GROUP_INITIAL_Y_SCREEN_OFFSET: -450 // Adjusted for closer initial zoom }; // If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up. /**** * Pattern Generation System ****/ var PatternGenerator = { lastLane: -1, minDistanceBetweenFish: 300, // Used by spawnFish internal check // Minimum X distance between fish for visual clarity on hook lastActualSpawnTime: -100000, // Time of the last actual fish spawn getNextLane: function getNextLane() { if (this.lastLane === -1) { // First fish, start in middle lane this.lastLane = 1; return 1; } // Prefer staying in same lane or moving to adjacent lane var possibleLanes = [this.lastLane]; // Add adjacent lanes if (this.lastLane > 0) { possibleLanes.push(this.lastLane - 1); } if (this.lastLane < 2) { possibleLanes.push(this.lastLane + 1); } // 70% chance to stay in same/adjacent lane if (Math.random() < 0.7) { this.lastLane = possibleLanes[Math.floor(Math.random() * possibleLanes.length)]; } else { // 30% chance for any lane this.lastLane = Math.floor(Math.random() * 3); } return this.lastLane; }, // New method: Checks if enough time has passed since the last spawn. canSpawnFishOnBeat: function canSpawnFishOnBeat(currentTime, configuredSpawnInterval) { var timeSinceLast = currentTime - this.lastActualSpawnTime; var minRequiredGap = configuredSpawnInterval; // Default gap is the song's beat interval for fish return timeSinceLast >= minRequiredGap; }, // New method: Registers details of the fish that was just spawned. registerFishSpawn: function registerFishSpawn(spawnTime) { this.lastActualSpawnTime = spawnTime; }, reset: function reset() { this.lastLane = -1; this.lastActualSpawnTime = -100000; // Set far in the past to allow first spawn } }; /**** * Game Configuration ****/ game.up = function (x, y, obj) { // Note: We don't play buttonClick sound on 'up' typically, only on 'down'. switch (GameState.currentScreen) { case 'title': // title screen up actions (if any) break; case 'levelSelect': // level select screen up actions (if any, usually 'down' is enough for buttons) break; case 'fishing': handleFishingInput(x, y, false); // false for isUp break; case 'results': // results screen up actions (if any) break; } }; var GAME_CONFIG = { SCREEN_CENTER_X: 1024, SCREEN_CENTER_Y: 900, // Adjusted, though less critical with lanes BOAT_Y: 710, // Original 300 + 273 (10%) + 137 (5%) = 710 WATER_SURFACE_Y: 760, // Original 350 + 273 (10%) + 137 (5%) = 760 // 3 Lane System // Y-positions for each lane, adjusted downwards further LANES: [{ y: 1133, // Top lane Y: (723 + 273) + 137 = 996 + 137 = 1133 name: "shallow" }, { y: 1776, // Middle lane Y: (996 + 643) + 137 = 1639 + 137 = 1776 name: "medium" }, { y: 2419, // Bottom lane Y: (1639 + 643) + 137 = 2282 + 137 = 2419 name: "deep" }], // Timing windows PERFECT_WINDOW: 40, GOOD_WINDOW: 80, MISS_WINDOW: 120, // Depth levels - reduced money values! DEPTHS: [{ level: 1, name: "Shallow Waters", fishSpeed: 6, fishValue: 0.5, upgradeCost: 0, // Starting depth songs: [{ name: "Gentle Waves", bpm: 90, // Changed from 95 to 90 duration: 202000, // 3:22 pattern: "gentle_waves_custom", cost: 0 }, { name: "Morning Tide", bpm: 90, duration: 156827, pattern: "morning_tide_custom", cost: 0, musicId: 'morningtide' }] }, { level: 2, name: "Mid Waters", fishSpeed: 7, fishValue: 1.5, upgradeCost: 100, songs: [{ name: "Ocean Current", bpm: 120, duration: 90000, pattern: "medium", cost: 0 }, { name: "Deep Flow", bpm: 125, duration: 100000, pattern: "medium", cost: 150 }] }, { level: 3, name: "Deep Waters", fishSpeed: 8, fishValue: 3, upgradeCost: 400, songs: [{ name: "Storm Surge", bpm: 140, duration: 120000, pattern: "complex", cost: 0 }, { name: "Whirlpool", bpm: 150, duration: 135000, pattern: "complex", cost: 300 }] }, { level: 4, name: "Abyss", fishSpeed: 9, fishValue: 6, upgradeCost: 1000, songs: [{ name: "Leviathan", bpm: 160, duration: 150000, pattern: "expert", cost: 0 }, { name: "Deep Trench", bpm: 170, duration: 180000, pattern: "expert", cost: 600 }] }], // Updated patterns PATTERNS: { simple: { beatsPerFish: 2, // Increased from 1 - fish every 2 beats doubleSpawnChance: 0.10, // 10% chance of a double beat in simple rareSpawnChance: 0.02 }, medium: { beatsPerFish: 1.5, // Increased from 0.75 doubleSpawnChance: 0.15, //{1R} // 15% chance of a double beat rareSpawnChance: 0.05 }, complex: { beatsPerFish: 1, // Increased from 0.5 doubleSpawnChance: 0.25, //{1U} // 25% chance of a double beat rareSpawnChance: 0.08 }, expert: { beatsPerFish: 0.75, // Increased from 0.25 doubleSpawnChance: 0.35, //{1X} // 35% chance of a double beat tripletSpawnChance: 0.20, // 20% chance a double beat becomes a triplet rareSpawnChance: 0.12 }, gentle_waves_custom: { beatsPerFish: 1.5, // Slightly more frequent than default simple doubleSpawnChance: 0.05, // Very rare double beats for shallow rareSpawnChance: 0.01, // Almost no rare fish // Custom timing sections based on the musical structure sections: [ // Opening - Simple chord pattern (0-30 seconds) { startTime: 0, endTime: 30000, spawnModifier: 1.0, // Normal spawn rate description: "steady_chords" }, // Melody Introduction (30-60 seconds) { startTime: 30000, endTime: 60000, spawnModifier: 0.9, // Slightly fewer fish description: "simple_melody" }, // Development (60-120 seconds) - Gets a bit busier { startTime: 60000, endTime: 120000, spawnModifier: 1.1, // Slightly more fish description: "melody_development" }, // Climax (120-180 seconds) - Busiest section but still shallow { startTime: 120000, endTime: 180000, spawnModifier: 1.3, // More fish, but not overwhelming description: "gentle_climax" }, // Ending (180-202 seconds) - Calming down { startTime: 180000, endTime: 202000, spawnModifier: 0.8, // Fewer fish for gentle ending description: "peaceful_ending" }] }, morning_tide_custom: { beatsPerFish: 1.2, // More frequent than gentle_waves_custom (1.5) and much more than simple (2) doubleSpawnChance: 0.12, // Higher than simple (0.10) rareSpawnChance: 0.03, // Higher than simple (0.02) // Custom timing sections based on musical structure sections: [ // Gentle opening - ease into the song (0-25 seconds) { startTime: 0, endTime: 25000, spawnModifier: 0.9, // Slightly reduced for intro description: "calm_opening" }, // Building energy - first wave (25-50 seconds) { startTime: 25000, endTime: 50000, spawnModifier: 1.2, // More active description: "first_wave" }, // Peak intensity - morning rush (50-80 seconds) { startTime: 50000, endTime: 80000, spawnModifier: 1.5, // Most intense section description: "morning_rush" }, // Sustained energy - second wave (80-110 seconds) { startTime: 80000, endTime: 110000, spawnModifier: 1.3, // High but slightly less than peak description: "second_wave" }, // Climactic finish (110-140 seconds) { startTime: 110000, endTime: 140000, spawnModifier: 1.4, // Building back up description: "climactic_finish" }, // Gentle fade out (140-156.8 seconds) { startTime: 140000, endTime: 156827, spawnModifier: 0.8, // Calm ending description: "peaceful_fade" }] } } }; /**** * Game State Management ****/ var MULTI_BEAT_SPAWN_DELAY_MS = 250; // ms delay for sequential spawns in multi-beats (increased for more space) var TRIPLET_BEAT_SPAWN_DELAY_MS = 350; // ms delay for third fish in a triplet (even more space) var FISH_SPAWN_END_BUFFER_MS = 2500; // Adjusted: This buffer ensures the last fish is scheduled to arrive this many milliseconds before the song's actual end, giving the player time to catch it while reducing empty time at the level's conclusion. var ImprovedRhythmSpawner = { nextBeatToSchedule: 1, scheduledBeats: [], update: function update(currentTime) { if (!GameState.gameActive || GameState.songStartTime === 0) { return; } var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; // Calculate how far ahead to look (use original logic) var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var travelTimeMs = distanceToHook / fishSpeed * (1000 / 60); var beatsAhead = Math.ceil(travelTimeMs / spawnInterval) + 2; // Schedule beats ahead var songElapsed = currentTime - GameState.songStartTime; var currentSongBeat = songElapsed / spawnInterval; var maxBeatToSchedule = Math.floor(currentSongBeat) + beatsAhead; while (this.nextBeatToSchedule <= maxBeatToSchedule) { if (this.scheduledBeats.indexOf(this.nextBeatToSchedule) === -1) { this.scheduleBeatFish(this.nextBeatToSchedule, spawnInterval, travelTimeMs); this.scheduledBeats.push(this.nextBeatToSchedule); } this.nextBeatToSchedule++; } // NO FISH SYNCING - let them move naturally! }, scheduleBeatFish: function scheduleBeatFish(beatNumber, spawnInterval, travelTimeMs) { var targetArrivalTime = GameState.songStartTime + beatNumber * spawnInterval; var songConfig = GameState.getCurrentSongConfig(); // Get current song config // Ensure this fish doesn't arrive too close to the actual end of the song if (songConfig && GameState.songStartTime > 0 && targetArrivalTime > songConfig.duration - FISH_SPAWN_END_BUFFER_MS) { return; // Don't schedule this fish, it's too late } var spawnTime = targetArrivalTime - travelTimeMs; var currentTime = LK.ticks * (1000 / 60); if (spawnTime >= currentTime - 100) { var delay = Math.max(0, spawnTime - currentTime); var self = this; LK.setTimeout(function () { if (GameState.gameActive && GameState.songStartTime !== 0) { self.spawnRhythmFish(beatNumber, targetArrivalTime); } }, delay); } }, spawnRhythmFish: function spawnRhythmFish(beatNumber, targetArrivalTime) { if (!GameState.gameActive) { return; } var currentTime = LK.ticks * (1000 / 60); var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Calculate spawnInterval for this pattern var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; // Apply section modifiers if pattern has sections var spawnModifier = 1.0; if (pattern.sections) { var songElapsed = currentTime - GameState.songStartTime; for (var s = 0; s < pattern.sections.length; s++) { var section = pattern.sections[s]; if (songElapsed >= section.startTime && songElapsed <= section.endTime) { spawnModifier = section.spawnModifier; break; } } } // Check if we should spawn if (!PatternGenerator.canSpawnFishOnBeat(currentTime, beatInterval)) { return; } // Apply spawn modifier chance if (Math.random() > spawnModifier) { return; } var laneIndex = PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; // Fish type selection (original logic) var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; } else if (GameState.selectedDepth >= 2 && rand < 0.3) { fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; fishValue = depthConfig.fishValue; } // Calculate precise speed to arrive exactly on beat var timeRemainingMs = targetArrivalTime - currentTime; if (timeRemainingMs <= 0) { return; } var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var spawnSide = Math.random() < 0.5 ? -1 : 1; // Calculate frames remaining and required speed per frame var framesRemaining = timeRemainingMs / (1000 / 60); if (framesRemaining <= 0) { return; } var requiredSpeedPerFrame = distanceToHook / framesRemaining; if (spawnSide === 1) { requiredSpeedPerFrame *= -1; // Moving left } // Create fish with exact calculated speed - NO SYNC DATA var newFish = new Fish(fishType, fishValue, requiredSpeedPerFrame, laneIndex); newFish.spawnSide = spawnSide; newFish.targetArrivalTime = targetArrivalTime; newFish.x = requiredSpeedPerFrame > 0 ? -150 : 2048 + 150; newFish.y = targetLane.y; newFish.baseY = targetLane.y; newFish.lastX = newFish.x; // Initialize lastX for miss detection // newFish.missed is false by default from constructor // Don't add any syncData - let fish move naturally! fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTime); // Handle multi-spawns this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime); return newFish; }, handleMultiSpawns: function handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime) { if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) { var travelTimeMs = this.calculateTravelTime(); var nextFishBeat = beatNumber + 1; if (this.scheduledBeats.indexOf(nextFishBeat) === -1) { this.scheduleBeatFish(nextFishBeat, spawnInterval, travelTimeMs); this.scheduledBeats.push(nextFishBeat); } // Handle triplets if (pattern.tripletSpawnChance && pattern.tripletSpawnChance > 0 && Math.random() < pattern.tripletSpawnChance) { var thirdFishBeat = beatNumber + 2; if (this.scheduledBeats.indexOf(thirdFishBeat) === -1) { this.scheduleBeatFish(thirdFishBeat, spawnInterval, travelTimeMs); this.scheduledBeats.push(thirdFishBeat); } } } }, calculateTravelTime: function calculateTravelTime() { var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); if (fishSpeed === 0) { return Infinity; } var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; return distanceToHook / fishSpeed * (1000 / 60); }, reset: function reset() { this.nextBeatToSchedule = 1; this.scheduledBeats = []; }, getDebugInfo: function getDebugInfo() { return { nextBeat: this.nextBeatToSchedule, scheduledCount: this.scheduledBeats.length, scheduledBeatsPreview: this.scheduledBeats.slice(-10) }; } }; /**** * Tutorial System - Global Scope ****/ function updateLaneBracketsVisuals() { if (laneBrackets && laneBrackets.length === GAME_CONFIG.LANES.length) { for (var i = 0; i < laneBrackets.length; i++) { var isActiveLane = i === GameState.hookTargetLaneIndex; var targetAlpha = isActiveLane ? 0.9 : 0.5; if (laneBrackets[i] && laneBrackets[i].left && !laneBrackets[i].left.destroyed) { if (laneBrackets[i].left.alpha !== targetAlpha) { laneBrackets[i].left.alpha = targetAlpha; } } if (laneBrackets[i] && laneBrackets[i].right && !laneBrackets[i].right.destroyed) { if (laneBrackets[i].right.alpha !== targetAlpha) { laneBrackets[i].right.alpha = targetAlpha; } } } } } function createTutorialElements() { tutorialOverlayContainer.removeChildren(); // Clear previous elements tutorialTextBackground = tutorialOverlayContainer.addChild(LK.getAsset('screenBackground', { x: GAME_CONFIG.SCREEN_CENTER_X, y: 2732 * 0.85, // Position towards the bottom width: 1800, height: 350, color: 0x000000, anchorX: 0.5, anchorY: 0.5, alpha: 0.75 })); tutorialTextDisplay = tutorialOverlayContainer.addChild(new Text2('', { size: 45, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 1700, // Slightly less than background width align: 'center', lineHeight: 55 })); tutorialTextDisplay.anchor.set(0.5, 0.5); tutorialTextDisplay.x = tutorialTextBackground.x; tutorialTextDisplay.y = tutorialTextBackground.y - 40; // Adjust Y to be within the new background tutorialContinueButton = tutorialOverlayContainer.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: tutorialTextBackground.x, y: tutorialTextBackground.y + tutorialTextBackground.height / 2 - 65, // Positioned at the bottom of the text background tint: 0x1976d2, // Blue color width: 350, // Standard button size height: 70 })); tutorialContinueText = tutorialOverlayContainer.addChild(new Text2('CONTINUE', { // Changed text size: 34, fill: 0xFFFFFF })); tutorialContinueText.anchor.set(0.5, 0.5); tutorialContinueText.x = tutorialContinueButton.x; tutorialContinueText.y = tutorialContinueButton.y; tutorialOverlayContainer.visible = false; // Initially hidden } function setTutorialText(newText, showContinue) { if (showContinue === undefined) { showContinue = true; } if (!tutorialTextDisplay || !tutorialContinueButton || !tutorialContinueText) { createTutorialElements(); // Ensure elements exist } tutorialTextDisplay.setText(newText); tutorialContinueButton.visible = showContinue; tutorialContinueText.visible = showContinue; tutorialOverlayContainer.visible = true; } function spawnTutorialFishHelper(config) { var fishType = config.type || 'shallow'; // Use a consistent depth config for tutorial fish, e.g., shallowest var depthConfig = GAME_CONFIG.DEPTHS[0]; var fishValue = depthConfig.fishValue / 2; // Tutorial fish might be worth less or nothing var baseSpeed = depthConfig.fishSpeed; var speedMultiplier = config.speedMultiplier || 0.5; // Slower for tutorial var laneIndex = config.lane !== undefined ? config.lane : 1; var spawnSide = config.spawnSide !== undefined ? config.spawnSide : Math.random() < 0.5 ? -1 : 1; var actualFishSpeed = Math.abs(baseSpeed) * speedMultiplier * spawnSide; var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex); newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen newFish.y = GAME_CONFIG.LANES[laneIndex].y + (config.yOffset || 0); newFish.baseY = newFish.y; newFish.lastX = newFish.x; newFish.tutorialFish = true; // Custom flag fishArray.push(newFish); // Add to global fishArray for rendering by main loop fishingScreen.addChild(newFish); // Add to fishingScreen for visibility return newFish; } function runTutorialStep() { GameState.tutorialPaused = false; GameState.tutorialAwaitingTap = false; // Will be set by steps if needed for "tap screen" if (tutorialContinueButton) { tutorialContinueButton.visible = true; } // Default to visible if (tutorialContinueText) { tutorialContinueText.visible = true; } // Clear previous tutorial fish unless current step needs it if (GameState.tutorialFish && GameState.tutorialStep !== 2 && GameState.tutorialStep !== 3) { if (!GameState.tutorialFish.destroyed) { GameState.tutorialFish.destroy(); } var idx = fishArray.indexOf(GameState.tutorialFish); if (idx > -1) { fishArray.splice(idx, 1); } GameState.tutorialFish = null; } // Ensure fishing screen elements are visually active (boat, water animations) // but game logic (spawner, score) is controlled by tutorial state. if (fishingElements) { if (typeof fishingElements.startWaterSurfaceAnimation === 'function' && GameState.tutorialStep < 7) { fishingElements.startWaterSurfaceAnimation(); } if (typeof fishingElements.startBoatAndFishermanAnimation === 'function' && GameState.tutorialStep < 7) { fishingElements.startBoatAndFishermanAnimation(); } if (fishingElements.hook && GameState.tutorialStep < 7) { fishingElements.hook.y = GAME_CONFIG.LANES[1].y; // Reset hook GameState.hookTargetLaneIndex = 1; } } switch (GameState.tutorialStep) { case 0: // Welcome setTutorialText("Welcome to Beat Fisher! Let's learn the basics. Tap 'CONTINUE'."); GameState.tutorialPaused = true; break; case 1: // The Hook & Lanes setTutorialText("This is your hook. Fish swim in three lanes. The hook automatically follows the closest fish. Tap 'CONTINUE'."); if (fishingElements.hook && !fishingElements.hook.destroyed) { tween(fishingElements.hook.scale, { x: 1.2, y: 1.2 }, { duration: 250, onFinish: function onFinish() { if (fishingElements.hook && !fishingElements.hook.destroyed) { tween(fishingElements.hook.scale, { x: 1, y: 1 }, { duration: 250 }); } } }); } GameState.tutorialPaused = true; break; case 2: // Fish Approaching & First Catch Attempt setTutorialText("A fish will now approach. When it's under the hook, TAP THE SCREEN to catch it!"); tutorialContinueButton.visible = false; // No continue button, action is tapping screen tutorialContinueText.visible = false; GameState.tutorialPaused = false; // Fish moves if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) { GameState.tutorialFish.destroy(); } GameState.tutorialFish = spawnTutorialFishHelper({ type: 'shallow', speedMultiplier: 0.35, lane: 1 }); break; case 3: // Perfect/Good Timing (after first successful catch) setTutorialText("Great! Timing is key. 'Perfect' or 'Good' catches earn more. Try to catch this next fish!"); tutorialContinueButton.visible = false; tutorialContinueText.visible = false; GameState.tutorialPaused = false; if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) { GameState.tutorialFish.destroy(); } GameState.tutorialFish = spawnTutorialFishHelper({ type: 'shallow', speedMultiplier: 0.45, lane: 1 }); break; case 4: // Combos setTutorialText("Nice one! Catch fish consecutively to build a COMBO for bonus points! Tap 'CONTINUE'."); GameState.tutorialPaused = true; break; case 5: // Music & Rhythm setTutorialText("Fish will approach the hook on the beat with the music's rhythm. Listen to the beat! Tap 'CONTINUE'."); GameState.tutorialPaused = true; break; case 6: // End Tutorial setTutorialText("You're all set! Tap 'CONTINUE' to go to the fishing spots!"); GameState.tutorialPaused = true; break; default: // Tutorial finished GameState.tutorialMode = false; tutorialOverlayContainer.visible = false; if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) { GameState.tutorialFish.destroy(); var idx = fishArray.indexOf(GameState.tutorialFish); if (idx > -1) { fishArray.splice(idx, 1); } GameState.tutorialFish = null; } // Clean up fishing screen elements explicitly if they were started by tutorial if (fishingElements && fishingElements.boat && !fishingElements.boat.destroyed) { tween.stop(fishingElements.boat); } if (fishingElements && fishingElements.fishermanContainer && !fishingElements.fishermanContainer.destroyed) { tween.stop(fishingElements.fishermanContainer); } // Stop water surface animations if (fishingElements && fishingElements.waterSurfaceSegments) { fishingElements.waterSurfaceSegments.forEach(function (segment) { if (segment && !segment.destroyed) { tween.stop(segment); } }); } showScreen('levelSelect'); break; } } function startTutorial() { GameState.tutorialMode = true; GameState.tutorialStep = 0; GameState.gameActive = false; // Gameplay logic like score, song progression is off // Ensure fishing screen is visible and setup for tutorial showScreen('fishing'); // This will make fishingScreen visible fishingScreen.alpha = 1; // Ensure it's fully visible // Clear any active game elements from a previous session fishArray.forEach(function (f) { if (f && !f.destroyed) { f.destroy(); } }); fishArray = []; ImprovedRhythmSpawner.reset(); if (fishingElements.scoreText) { fishingElements.scoreText.setText(''); } if (fishingElements.fishText) { fishingElements.fishText.setText(''); } if (fishingElements.comboText) { fishingElements.comboText.setText(''); } if (fishingElements.progressText) { fishingElements.progressText.setText(''); } // Hide regular fishing UI if necessary or leave it for context // For now, let's assume it's okay to see score/combo at 0. // Don't create tutorial elements here - wait for intro to finish // createTutorialElements() and runTutorialStep() will be called after intro } // This function is called from game.update when tutorialFish exists function checkTutorialFishState() { var fish = GameState.tutorialFish; if (!fish || fish.destroyed || fish.caught || fish.missed) { return; } var hookX = fishingElements.hook.x; // Fish passes hook without interaction during catch steps if (GameState.tutorialStep === 2 || GameState.tutorialStep === 3) { var passedHook = fish.speed > 0 && fish.x > hookX + GAME_CONFIG.MISS_WINDOW + fish.fishGraphics.width / 2 || // fish right edge passed hook right boundary fish.speed < 0 && fish.x < hookX - GAME_CONFIG.MISS_WINDOW - fish.fishGraphics.width / 2; // fish left edge passed hook left boundary if (passedHook) { fish.missed = true; // Mark to prevent further attempts on this specific fish instance GameState.tutorialPaused = true; // Pause to show message setTutorialText("It got away! Tap 'CONTINUE' to try that part again."); // Next tap on "CONTINUE" will call runTutorialStep(), which will re-run current step. } } // Generic off-screen removal for tutorial fish if (fish.x < -250 || fish.x > 2048 + 250) { var wasCriticalStep = GameState.tutorialStep === 2 || GameState.tutorialStep === 3; var fishIndex = fishArray.indexOf(fish); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } fish.destroy(); GameState.tutorialFish = null; if (wasCriticalStep && !fish.caught && !fish.missed) { // If it just went off screen without resolution GameState.tutorialPaused = true; setTutorialText("The fish swam off. Tap 'CONTINUE' to try that part again."); } } } var GameState = { // Game flow currentScreen: 'title', // 'title', 'levelSelect', 'fishing', 'results' // Player progression currentDepth: 0, money: 0, totalFishCaught: 0, ownedSongs: [], // Array of {depth, songIndex} objects // Level selection selectedDepth: 0, selectedSong: 0, // Current session sessionScore: 0, sessionFishCaught: 0, sessionFishSpawned: 0, combo: 0, maxCombo: 0, // Game state gameActive: false, songStartTime: 0, lastBeatTime: 0, beatCount: 0, introPlaying: false, // Tracks if the intro animation is currently playing musicNotesActive: false, // Tracks if music note particle system is active currentPlayingMusicId: 'rhythmTrack', // ID of the music track currently playing in a session currentPlayingMusicInitialVolume: 0.8, // Initial volume of the current music track for fade reference hookTargetLaneIndex: 1, // Start with hook targeting the middle lane (index 1) // Tutorial State tutorialMode: false, // Is the tutorial currently active? tutorialStep: 0, // Current step in the tutorial sequence tutorialPaused: false, // Is the tutorial paused (e.g., for text display)? tutorialAwaitingTap: false, // Is the tutorial waiting for a generic tap to continue? tutorialFish: null, // Stores the fish object used in a tutorial step // Initialize owned songs (first song of each unlocked depth is free) initOwnedSongs: function initOwnedSongs() { this.ownedSongs = []; for (var i = 0; i <= this.currentDepth; i++) { this.ownedSongs.push({ depth: i, songIndex: 0 }); } }, hasSong: function hasSong(depth, songIndex) { return this.ownedSongs.some(function (song) { return song.depth === depth && song.songIndex === songIndex; }); }, buySong: function buySong(depth, songIndex) { var song = GAME_CONFIG.DEPTHS[depth].songs[songIndex]; if (this.money >= song.cost && !this.hasSong(depth, songIndex)) { this.money -= song.cost; this.ownedSongs.push({ depth: depth, songIndex: songIndex }); return true; } return false; }, getCurrentDepthConfig: function getCurrentDepthConfig() { return GAME_CONFIG.DEPTHS[this.selectedDepth]; }, getCurrentSongConfig: function getCurrentSongConfig() { return GAME_CONFIG.DEPTHS[this.selectedDepth].songs[this.selectedSong]; }, canUpgrade: function canUpgrade() { var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1]; return nextDepth && this.money >= nextDepth.upgradeCost; }, upgrade: function upgrade() { if (this.canUpgrade()) { var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1]; this.money -= nextDepth.upgradeCost; this.currentDepth++; // Give free first song of new depth this.ownedSongs.push({ depth: this.currentDepth, songIndex: 0 }); return true; } return false; } }; var titleScreen = game.addChild(new Container()); var levelSelectScreen = game.addChild(new Container()); var fishingScreen = game.addChild(new Container()); var resultsScreen = game.addChild(new Container()); // Initialize GameState.initOwnedSongs(); /**** * Title Screen ****/ /**** * Title Screen - FIXED VERSION ****/ function createTitleScreen() { var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 1, height: 2732, color: 0x87CEEB })); // ANIMATED CONTAINER GROUP - This will contain ALL visual elements including backgrounds var titleAnimationGroup = titleScreen.addChild(new Container()); // Sky background image - NOW INSIDE animation group var titleSky = titleAnimationGroup.addChild(LK.getAsset('skybackground', { x: 0, y: -500 })); // Water background image - NOW INSIDE animation group var titleWater = titleAnimationGroup.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, width: 2048, height: 2732 - GAME_CONFIG.WATER_SURFACE_Y })); // Initialize containers for title screen ambient particles - INSIDE animation group titleScreenOceanBubbleContainer = titleAnimationGroup.addChild(new Container()); titleScreenSeaweedContainer = titleAnimationGroup.addChild(new Container()); titleScreenCloudContainer = titleAnimationGroup.addChild(new Container()); // Create a single container for boat, fisherman, and line that all move together var titleBoatGroup = titleAnimationGroup.addChild(new Container()); // Boat positioned at origin of the group var titleBoat = titleBoatGroup.addChild(LK.getAsset('boat', { anchorX: 0.5, anchorY: 0.74, x: 0, // Relative to group y: 0 // Relative to group })); // Fisherman positioned relative to boat within the same group var titleFisherman = titleBoatGroup.addChild(LK.getAsset('fisherman', { anchorX: 0.5, anchorY: 1, x: -100, // Relative to boat position y: -70 // Relative to boat position })); // Fishing line positioned relative to boat within the same group var rodTipX = -100 + 85; // fisherman offset + rod offset from fisherman center var rodTipY = -70 - 200; // fisherman y (feet) - fisherman height (to get to head area for rod) // initialHookY needs to be relative to the group's origin (which is boat's origin at WATER_SURFACE_Y) // GAME_CONFIG.LANES[1].y is an absolute world Y. // titleBoatGroup.y is GAME_CONFIG.WATER_SURFACE_Y. // So, hook's Y relative to group = absolute hook Y - group's absolute Y var initialHookYInGroup = GAME_CONFIG.LANES[1].y - GAME_CONFIG.WATER_SURFACE_Y; var titleLine = titleBoatGroup.addChild(LK.getAsset('fishingLine', { anchorX: 0.5, anchorY: 0, x: rodTipX, y: rodTipY, // Relative to group width: 6, height: initialHookYInGroup - rodTipY // Length from rod tip to hook, all relative to group })); var titleHook = titleBoatGroup.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: rodTipX, // Hook X matches line X initially y: initialHookYInGroup // Relative to group })); // Position the entire group in the world (within titleAnimationGroup) titleBoatGroup.x = GAME_CONFIG.SCREEN_CENTER_X; titleBoatGroup.y = GAME_CONFIG.WATER_SURFACE_Y; // Store base position for wave animation var boatGroupBaseY = titleBoatGroup.y; // This is the group's world Y var boatWaveAmplitude = 10; var boatWaveHalfCycleDuration = 2000; var boatRotationAmplitude = 0.03; var boatRotationDuration = 3000; // Wave animation variables for line (used in updateTitleFishingLineWave) var lineWaveAmplitude = 12; var lineWaveSpeed = 0.03; var linePhaseOffset = 0; // Animated Water Surface segments - INSIDE animation group var titleWaterSurfaceSegments = []; var NUM_WAVE_SEGMENTS_TITLE = 32; var SEGMENT_WIDTH_TITLE = 2048 / NUM_WAVE_SEGMENTS_TITLE; var SEGMENT_HEIGHT_TITLE = 24; var WAVE_AMPLITUDE_TITLE = 12; var WAVE_HALF_PERIOD_MS_TITLE = 2500; var PHASE_DELAY_MS_PER_SEGMENT_TITLE = WAVE_HALF_PERIOD_MS_TITLE * 2 / NUM_WAVE_SEGMENTS_TITLE; // Create water surface segments for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) { var segment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH_TITLE, y: GAME_CONFIG.WATER_SURFACE_Y, width: SEGMENT_WIDTH_TITLE + 1, height: SEGMENT_HEIGHT_TITLE, anchorX: 0, anchorY: 0.5, alpha: 0.8, tint: 0x4fc3f7 }); segment.baseY = GAME_CONFIG.WATER_SURFACE_Y; titleAnimationGroup.addChild(segment); // Water surface is part of main animation group, not boat group titleWaterSurfaceSegments.push(segment); } for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) { var whiteSegment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH_TITLE, y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2, width: SEGMENT_WIDTH_TITLE + 1, height: SEGMENT_HEIGHT_TITLE / 2, anchorX: 0, anchorY: 0.5, alpha: 0.6, tint: 0xffffff }); whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2; titleAnimationGroup.addChild(whiteSegment); // Water surface is part of main animation group titleWaterSurfaceSegments.push(whiteSegment); } // Animation group setup for zoom effect - PROPERLY CENTER THE BOAT ON SCREEN var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; // 1024 var targetBoatScreenY = GAME_CONFIG.SCREEN_CENTER_Y + 300; // Change +100 to move boat lower/higher var boatWorldY = GAME_CONFIG.WATER_SURFACE_Y; // 760 - where titleBoatGroup actually is var pivotY = boatWorldY - (targetBoatScreenY - boatWorldY); // Calculate proper pivot offset // Set pivot to calculated position and position group so boat ends up at screen center titleAnimationGroup.pivot.set(boatCenterX, pivotY); titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X; // 1024 titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // 1366 - this will center the boat at targetBoatScreenY // Initial zoom state var INITIAL_ZOOM_FACTOR = 3.0; var FINAL_ZOOM_FACTOR = 1.8; // This matches existing logic if it's used in showScreen titleAnimationGroup.scale.set(INITIAL_ZOOM_FACTOR); titleAnimationGroup.alpha = 1; // Main animation group is always visible // Single wave animation function - moves entire group together var targetUpY = boatGroupBaseY - boatWaveAmplitude; // boatGroupBaseY is absolute world Y var targetDownY = boatGroupBaseY + boatWaveAmplitude; // boatGroupBaseY is absolute world Y function moveTitleBoatGroupUp() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } // We tween the group's world Y position tween(titleBoatGroup, { y: targetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveTitleBoatGroupDown }); } function moveTitleBoatGroupDown() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } tween(titleBoatGroup, { y: targetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveTitleBoatGroupUp }); } // Single rotation animation - rotates entire group together function rockTitleBoatGroupLeft() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } tween(titleBoatGroup, { rotation: -boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut, onFinish: rockTitleBoatGroupRight }); } function rockTitleBoatGroupRight() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } tween(titleBoatGroup, { rotation: boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut, onFinish: rockTitleBoatGroupLeft }); } // Simplified line wave function - just the sway effect function updateTitleFishingLineWave() { // titleLine and titleHook are children of titleBoatGroup, their x/y are relative to it. // rodTipX, rodTipY, initialHookYInGroup are also relative to titleBoatGroup. if (!titleLine || titleLine.destroyed || !titleHook || titleHook.destroyed) { return; } linePhaseOffset += lineWaveSpeed; var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude; // Only apply the wave sway to X, positions stay relative to group's origin // rodTipX is the line's base X relative to the group. titleLine.x = rodTipX + waveOffset * 0.3; titleHook.x = rodTipX + waveOffset; // Y positions of line and hook (titleLine.y and titleHook.y) are static relative to the group. // Recalculate line rotation based on hook position relative to line's anchor // All coordinates here are relative to titleBoatGroup. var deltaX = titleHook.x - titleLine.x; // Difference in x relative to group var deltaY = titleHook.y - titleLine.y; // Difference in y relative to group var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY); titleLine.height = actualLineLength; if (actualLineLength > 0.001) { titleLine.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2; } else { titleLine.rotation = 0; } titleHook.rotation = titleLine.rotation; // Hook rotation matches line's } // Water surface animation function function startTitleWaterSurfaceAnimationFunc() { for (var k = 0; k < titleWaterSurfaceSegments.length; k++) { var segment = titleWaterSurfaceSegments[k]; if (!segment || segment.destroyed) { continue; } var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS_TITLE; (function (currentLocalSegment, currentLocalSegmentIndexForDelay) { var animUp, animDown; animDown = function animDown() { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE }, { duration: WAVE_HALF_PERIOD_MS_TITLE, easing: tween.easeInOut, onFinish: animUp }); }; animUp = function animUp() { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY - WAVE_AMPLITUDE_TITLE }, { duration: WAVE_HALF_PERIOD_MS_TITLE, easing: tween.easeInOut, onFinish: animDown }); }; LK.setTimeout(function () { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } // Start with DOWN movement first tween(currentLocalSegment, { y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE }, { duration: WAVE_HALF_PERIOD_MS_TITLE, easing: tween.easeInOut, onFinish: animUp // This should call animUp after going down }); }, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT_TITLE); })(segment, segmentIndexForDelay); } } // BLACK OVERLAY for reveal effect - this goes OVER everything var blackOverlay = titleScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, width: 2048, height: 2732, color: 0x000000, // Pure black alpha: 1 // Starts fully opaque })); // UI elements - OUTSIDE animation group so they don't zoom, ABOVE black overlay var titleImage = titleScreen.addChild(LK.getAsset('titleimage', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 700, // Positioned where the subtitle roughly was, adjust as needed alpha: 0, scaleX: 0.8, // Adjust scale as needed for optimal size scaleY: 0.8 // Adjust scale as needed for optimal size })); // Buttons - OUTSIDE animation group, ABOVE black overlay // New Y positions: 1/3 from bottom of screen (2732 / 3 = 910.66. Y = 2732 - 910.66 = 1821.33) var startButtonY = 2732 - 2732 / 3; // Approx 1821 var tutorialButtonY = startButtonY + 150; // Maintain 150px gap var startButton = titleScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: startButtonY, alpha: 0 })); var startText = titleScreen.addChild(new Text2('START', { size: 50, fill: 0xFFFFFF })); startText.anchor.set(0.5, 0.5); startText.x = GAME_CONFIG.SCREEN_CENTER_X; startText.y = startButtonY; startText.alpha = 0; var tutorialButton = titleScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: tutorialButtonY, tint: 0x757575, alpha: 0 })); var tutorialText = titleScreen.addChild(new Text2('TUTORIAL', { size: 40, fill: 0xFFFFFF })); tutorialText.anchor.set(0.5, 0.5); tutorialText.x = GAME_CONFIG.SCREEN_CENTER_X; tutorialText.y = tutorialButtonY; tutorialText.alpha = 0; // No assignment here; assign tutorialButtonGfx after createTitleScreen returns return { startButton: startButton, startText: startText, tutorialButton: tutorialButton, // This is the graphics object tutorialText: tutorialText, titleImage: titleImage, titleAnimationGroup: titleAnimationGroup, blackOverlay: blackOverlay, // Return the group and its specific animation functions titleBoatGroup: titleBoatGroup, moveTitleBoatGroupUp: moveTitleBoatGroupUp, rockTitleBoatGroupLeft: rockTitleBoatGroupLeft, // Individual elements that might still be needed for direct access or other effects titleSky: titleSky, // Sky is not in titleBoatGroup titleWater: titleWater, // Water background is not in titleBoatGroup titleWaterSurfaceSegments: titleWaterSurfaceSegments, // Water surface segments are not in titleBoatGroup // Line and hook are part of titleBoatGroup, but if direct reference is needed for some reason, they could be returned. // However, following the goal's spirit of "Return the group instead of individual elements [that are part of it]" // titleLine and titleHook might be omitted here unless specifically needed by other parts of the code. // For now, including them if they were returned before and are still locally defined. // updateTitleFishingLineWave accesses titleLine and titleHook locally. titleLine: titleLine, // Still defined locally, might be useful for direct reference titleHook: titleHook, // Still defined locally startTitleWaterSurfaceAnimation: startTitleWaterSurfaceAnimationFunc, updateTitleFishingLineWave: updateTitleFishingLineWave // The new simplified version }; } /**** * Level Select Screen ****/ function createLevelSelectScreen() { var selectBg = levelSelectScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.8, height: 2732 })); // Title var title = new Text2('SELECT FISHING SPOT', { size: 80, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.x = GAME_CONFIG.SCREEN_CENTER_X; title.y = 200; levelSelectScreen.addChild(title); // Money display var moneyDisplay = new Text2('Money: $0', { size: 60, fill: 0xFFD700 }); moneyDisplay.anchor.set(1, 0); moneyDisplay.x = 1900; moneyDisplay.y = 100; levelSelectScreen.addChild(moneyDisplay); // Depth tabs (will be created dynamically) var depthTabs = []; // Song display area var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 700 })); // Song navigation arrows var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 400, y: 700, tint: 0x666666 })); var leftArrowText = new Text2('<', { size: 60, fill: 0xFFFFFF }); leftArrowText.anchor.set(0.5, 0.5); leftArrowText.x = 400; leftArrowText.y = 700; levelSelectScreen.addChild(leftArrowText); var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 1648, y: 700, tint: 0x666666 })); var rightArrowText = new Text2('>', { size: 60, fill: 0xFFFFFF }); rightArrowText.anchor.set(0.5, 0.5); rightArrowText.x = 1648; rightArrowText.y = 700; levelSelectScreen.addChild(rightArrowText); // Song info (will be updated dynamically) var songTitle = new Text2('Song Title', { size: 50, fill: 0xFFFFFF }); songTitle.anchor.set(0.5, 0.5); songTitle.x = GAME_CONFIG.SCREEN_CENTER_X; songTitle.y = 650; levelSelectScreen.addChild(songTitle); var songInfo = new Text2('BPM: 120 | Duration: 2:00', { size: 30, fill: 0xCCCCCC }); songInfo.anchor.set(0.5, 0.5); songInfo.x = GAME_CONFIG.SCREEN_CENTER_X; songInfo.y = 700; levelSelectScreen.addChild(songInfo); var songEarnings = new Text2('Potential Earnings: $50-100', { size: 30, fill: 0x4CAF50 }); songEarnings.anchor.set(0.5, 0.5); songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X; songEarnings.y = 730; levelSelectScreen.addChild(songEarnings); // Play/Buy button var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 900 })); var playButtonText = new Text2('PLAY', { size: 50, fill: 0xFFFFFF }); playButtonText.anchor.set(0.5, 0.5); playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; playButtonText.y = 900; levelSelectScreen.addChild(playButtonText); // Shop button var shopButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1100, tint: 0x2e7d32 })); var shopButtonText = new Text2('UPGRADE ROD', { size: 40, fill: 0xFFFFFF }); shopButtonText.anchor.set(0.5, 0.5); shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; shopButtonText.y = 1100; levelSelectScreen.addChild(shopButtonText); // Back button var backButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 200, y: 200, tint: 0x757575 })); var backButtonText = new Text2('BACK', { size: 40, fill: 0xFFFFFF }); backButtonText.anchor.set(0.5, 0.5); backButtonText.x = 200; backButtonText.y = 200; levelSelectScreen.addChild(backButtonText); return { moneyDisplay: moneyDisplay, depthTabs: depthTabs, leftArrow: leftArrow, rightArrow: rightArrow, songTitle: songTitle, songInfo: songInfo, songEarnings: songEarnings, playButton: playButton, playButtonText: playButtonText, shopButton: shopButton, shopButtonText: shopButtonText, backButton: backButton }; } /**** * Fishing Screen ****/ function createFishingScreen() { // Sky background - should be added first to be behind everything else var sky = fishingScreen.addChild(LK.getAsset('skybackground', { x: 0, y: -500 })); // Water background var water = fishingScreen.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, width: 2048, height: 2732 - GAME_CONFIG.WATER_SURFACE_Y })); // Create a container for ambient ocean bubbles (from bottom of screen) // This should be layered above the 'water' background, but below fish, boat, etc. globalOceanBubbleContainer = fishingScreen.addChild(new Container()); // Create a container for seaweed particles globalSeaweedContainer = fishingScreen.addChild(new Container()); // Create a container for cloud particles (added early so clouds appear behind UI) globalCloudContainer = fishingScreen.addChild(new Container()); // Create a container for bubbles to render them behind fish and other elements bubbleContainer = fishingScreen.addChild(new Container()); // Create a container for music notes musicNotesContainer = fishingScreen.addChild(new Container()); // Music notes should visually appear to come from the boat area, so their container // should ideally be layered accordingly. Adding it here means it's on top of water, // but if boat/fisherman are added later, notes might appear behind them if not managed. // For now, notes will be added to this container, which itself is added to fishingScreen. // Animated Water Surface segments code var waterSurfaceSegments = []; // This will be populated for returning and cleanup var waterSurfaceSegmentsBlueTemp = []; // Temporary array for blue segments var waterSurfaceSegmentsWhiteTemp = []; // Temporary array for white segments var NUM_WAVE_SEGMENTS = 32; var SEGMENT_WIDTH = 2048 / NUM_WAVE_SEGMENTS; var SEGMENT_HEIGHT = 24; var WAVE_AMPLITUDE = 12; var WAVE_HALF_PERIOD_MS = 2500; var PHASE_DELAY_MS_PER_SEGMENT = WAVE_HALF_PERIOD_MS * 2 / NUM_WAVE_SEGMENTS; // Create blue segments (assets only, not added to fishingScreen yet) for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) { var segment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH, y: GAME_CONFIG.WATER_SURFACE_Y, width: SEGMENT_WIDTH + 1, height: SEGMENT_HEIGHT, anchorX: 0, anchorY: 0.5, alpha: 0.8, tint: 0x4fc3f7 }); segment.baseY = GAME_CONFIG.WATER_SURFACE_Y; // Animation functions will be defined and started by startWaterSurfaceAnimationFunc waterSurfaceSegmentsBlueTemp.push(segment); } // Create white segments (assets only, not added to fishingScreen yet) for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) { var whiteSegment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH, y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2, width: SEGMENT_WIDTH + 1, height: SEGMENT_HEIGHT / 2, anchorX: 0, anchorY: 0.5, alpha: 0.6, tint: 0xffffff }); whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2; // Animation functions will be defined and started by startWaterSurfaceAnimationFunc waterSurfaceSegmentsWhiteTemp.push(whiteSegment); } // Boat - Add this to fishingScreen first var boat = fishingScreen.addChild(LK.getAsset('boat', { anchorX: 0.5, anchorY: 0.74, x: GAME_CONFIG.SCREEN_CENTER_X, y: GAME_CONFIG.WATER_SURFACE_Y })); // Now add the water segments to fishingScreen, so they render on top of the boat for (var i = 0; i < waterSurfaceSegmentsBlueTemp.length; i++) { fishingScreen.addChild(waterSurfaceSegmentsBlueTemp[i]); waterSurfaceSegments.push(waterSurfaceSegmentsBlueTemp[i]); // Also add to the main array for cleanup } for (var i = 0; i < waterSurfaceSegmentsWhiteTemp.length; i++) { fishingScreen.addChild(waterSurfaceSegmentsWhiteTemp[i]); waterSurfaceSegments.push(waterSurfaceSegmentsWhiteTemp[i]); // Also add to the main array for cleanup } // Create separate fisherman container that will sync with boat movement var fishermanContainer = fishingScreen.addChild(new Container()); // Fisherman (now in its own container, positioned to match boat) var fisherman = fishermanContainer.addChild(LK.getAsset('fisherman', { anchorX: 0.5, anchorY: 1, x: GAME_CONFIG.SCREEN_CENTER_X - 100, y: GAME_CONFIG.WATER_SURFACE_Y - 70 })); // Store references for wave animation sync var boatBaseY = boat.y; var fishermanBaseY = fishermanContainer.y; var boatWaveAmplitude = 10; var boatWaveHalfCycleDuration = 2000; // SINGLE ANIMATED FISHING LINE var initialHookY = GAME_CONFIG.LANES[1].y; var fishingLineStartY = -100; var line = fishingScreen.addChild(LK.getAsset('fishingLine', { anchorX: 0.5, anchorY: 0, x: GAME_CONFIG.SCREEN_CENTER_X, y: GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY, width: 6, height: initialHookY - (GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY) })); var hook = fishingScreen.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: initialHookY })); hook.originalY = initialHookY; var lineWaveAmplitude = 12; var lineWaveSpeed = 0.03; var linePhaseOffset = 0; function updateFishingLineWave() { linePhaseOffset += lineWaveSpeed; var rodTipX = fishermanContainer.x + fisherman.x + 85; var rodTipY = fishermanContainer.y + fisherman.y - fisherman.height; var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude; line.x = rodTipX + waveOffset * 0.3; line.y = rodTipY; hook.x = rodTipX + waveOffset; var hookAttachX = hook.x; var hookAttachY = hook.y - hook.height / 2; var deltaX = hookAttachX - line.x; var deltaY = hookAttachY - line.y; var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY); line.height = actualLineLength; if (actualLineLength > 0.001) { line.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2; } else { line.rotation = 0; } hook.rotation = line.rotation; } // Calculate target positions for boat wave animation var targetUpY = boatBaseY - boatWaveAmplitude; var targetDownY = boatBaseY + boatWaveAmplitude; var fishermanTargetUpY = fishermanBaseY - boatWaveAmplitude; var fishermanTargetDownY = fishermanBaseY + boatWaveAmplitude; // Synchronized wave animation functions (defined here to be closured) function moveBoatAndFishermanUp() { if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } function moveBoatAndFishermanDown() { if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } tween(boat, { y: targetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanUp }); tween(fishermanContainer, { y: fishermanTargetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } var boatRotationAmplitude = 0.03; var boatRotationDuration = 3000; function rockBoatLeft() { if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) { return; } tween(boat, { rotation: -boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut, onFinish: rockBoatRight }); tween(fisherman, { rotation: boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut }); } function rockBoatRight() { if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) { return; } tween(boat, { rotation: boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut, onFinish: rockBoatLeft }); tween(fisherman, { rotation: -boatRotationAmplitude }, { duration: boatRotationDuration, easing: tween.easeInOut }); } // Function to start/restart water surface animations function startWaterSurfaceAnimationFunc() { var allSegments = waterSurfaceSegments; // Use the populated array from fishingElements via closure for (var k = 0; k < allSegments.length; k++) { var segment = allSegments[k]; if (!segment || segment.destroyed) { continue; } var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS; (function (currentLocalSegment, currentLocalSegmentIndexForDelay) { var animUp, animDown; animDown = function animDown() { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY + WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animUp }); }; animUp = function animUp() { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY - WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animDown }); }; LK.setTimeout(function () { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY - WAVE_AMPLITUDE }, { // Initial move up duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animDown }); }, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT); })(segment, segmentIndexForDelay); } } // Function to start/restart boat and fisherman animations function startBoatAndFishermanAnimationFunc() { if (boat && !boat.destroyed && fishermanContainer && !fishermanContainer.destroyed) { tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut }); rockBoatLeft(); } } // UI elements (from existing) var scoreText = new Text2('Score: 0', { size: 70, fill: 0xFFFFFF }); scoreText.anchor.set(1, 0); scoreText.x = 2048 - 50; scoreText.y = 50; fishingScreen.addChild(scoreText); var fishText = new Text2('Fish: 0/0', { size: 55, fill: 0xFFFFFF }); fishText.anchor.set(1, 0); fishText.x = 2048 - 50; fishText.y = 140; fishingScreen.addChild(fishText); var comboText = new Text2('Combo: 0', { size: 55, fill: 0xFF9800 }); comboText.anchor.set(1, 0); comboText.x = 2048 - 50; comboText.y = 210; fishingScreen.addChild(comboText); var progressText = new Text2('0:00 / 0:00', { size: 50, fill: 0x4FC3F7 }); progressText.anchor.set(1, 0); progressText.x = 2048 - 50; progressText.y = 280; fishingScreen.addChild(progressText); return { boat: boat, fishermanContainer: fishermanContainer, fisherman: fisherman, hook: hook, line: line, updateFishingLineWave: updateFishingLineWave, scoreText: scoreText, fishText: fishText, comboText: comboText, progressText: progressText, waterSurfaceSegments: waterSurfaceSegments, bubbleContainer: bubbleContainer, musicNotesContainer: musicNotesContainer, startWaterSurfaceAnimation: startWaterSurfaceAnimationFunc, startBoatAndFishermanAnimation: startBoatAndFishermanAnimationFunc }; } /**** * Initialize Screen Elements ****/ var titleElements = createTitleScreen(); titleElements.tutorialButtonGfx = titleElements.tutorialButton; // Store the graphical button for compatibility var levelSelectElements = createLevelSelectScreen(); var fishingElements = createFishingScreen(); // Tutorial UI Elements - MUST be at global scope var tutorialOverlayContainer = game.addChild(new Container()); tutorialOverlayContainer.visible = false; var tutorialTextBackground; var tutorialTextDisplay; var tutorialContinueButton; var tutorialContinueText; // Feedback indicators are now created on-demand by the showFeedback function. // The global feedbackIndicators object is no longer needed. // Game variables var fishArray = []; var bubblesArray = []; var bubbleContainer; // Container for bubbles, initialized in createFishingScreen var musicNotesArray = []; var musicNotesContainer; // Container for music notes var laneBrackets = []; // Stores the visual bracket pairs for each lane var musicNoteSpawnCounter = 0; var MUSIC_NOTE_SPAWN_INTERVAL_TICKS = 45; // Spawn a note roughly every 0.75 seconds // Ocean Bubbles (ambient background) var globalOceanBubblesArray = []; var globalOceanBubbleContainer; var globalOceanBubbleSpawnCounter = 0; // Increase interval to reduce amount (higher = less frequent) var OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS = 40; // Spawn new ocean bubbles roughly every 2/3 second (was 20) // Seaweed particles (ambient background) var globalSeaweedArray = []; var globalSeaweedContainer; var globalSeaweedSpawnCounter = 0; var SEAWEED_SPAWN_INTERVAL_TICKS = 120; // Spawn seaweed less frequently than bubbles var MAX_SEAWEED_COUNT = 8; // Maximum number of seaweed particles at once // Cloud particles (ambient sky) var globalCloudArray = []; var globalCloudContainer; var globalCloudSpawnCounter = 0; var CLOUD_SPAWN_INTERVAL_TICKS = 180; // Spawn clouds less frequently than seaweed var MAX_CLOUD_COUNT = 5; // Maximum number of cloud particles at once // Title Screen Ambient Particle Systems var titleScreenOceanBubblesArray = []; var titleScreenOceanBubbleContainer; // Will be initialized in createTitleScreen var titleScreenOceanBubbleSpawnCounter = 0; var titleScreenSeaweedArray = []; var titleScreenSeaweedContainer; // Will be initialized in createTitleScreen var titleScreenSeaweedSpawnCounter = 0; var titleScreenCloudArray = []; var titleScreenCloudContainer; // Will be initialized in createTitleScreen var titleScreenCloudSpawnCounter = 0; // Timers for title screen ambient sounds var titleSeagullSoundTimer = null; var titleBoatSoundTimer = null; /**** * Input State and Helpers for Fishing ****/ var inputState = { touching: false, // Is the screen currently being touched? touchLane: -1, // Which lane was the touch initiated in? (0, 1, 2) touchStartTime: 0 // Timestamp of when the touch started (LK.ticks based) }; // Helper function to determine which lane a Y coordinate falls into function getTouchLane(y) { // Define boundaries based on the midpoints between lane Y coordinates // These are calculated from GAME_CONFIG.LANES[i].y values // Lane 0: y = 723 // Lane 1: y = 1366 // Lane 2: y = 2009 var boundary_lane0_lane1 = (GAME_CONFIG.LANES[0].y + GAME_CONFIG.LANES[1].y) / 2; // Approx 1044.5 var boundary_lane1_lane2 = (GAME_CONFIG.LANES[1].y + GAME_CONFIG.LANES[2].y) / 2; // Approx 1687.5 if (y < boundary_lane0_lane1) { return 0; // Top lane (e.g., shallow) } else if (y < boundary_lane1_lane2) { return 1; // Middle lane (e.g., medium) } else { return 2; // Bottom lane (e.g., deep) } } // Shows feedback (perfect, good, miss) at the specified lane // Shows feedback (perfect, good, miss) at the specified lane function showFeedback(type, laneIndex) { var feedbackY = GAME_CONFIG.LANES[laneIndex].y; var indicator = new FeedbackIndicator(type); // Creates a new indicator e.g. FeedbackIndicator('perfect') // Position feedback at the single hook's X coordinate and the fish's lane Y indicator.x = fishingElements.hook.x; // Use the single hook's X indicator.y = feedbackY; // Feedback appears at the fish's lane Y fishingScreen.addChild(indicator); indicator.show(); // Triggers the animation and self-destruction } // Animates the hook in a specific lane after a catch attempt // Animates the single hook after a catch attempt function animateHookCatch() { var hook = fishingElements.hook; // We need a stable originalY. The hook.originalY might change if we re-assign it during tweens. // Let's use the target Y of the current fish lane for the "resting" position after animation. var restingY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; // Quick bobbing animation for the single hook tween(hook, { y: restingY - 30 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(hook, { y: restingY }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { // Ensure originalY reflects the current target lane after animation. hook.originalY = restingY; } }); } }); } // Handles input specifically for the fishing screen (down and up events) function handleFishingInput(x, y, isDown) { // If in tutorial mode, and it's a 'down' event during an active catch step if (GameState.tutorialMode && isDown && (GameState.tutorialStep === 2 || GameState.tutorialStep === 3) && !GameState.tutorialPaused) { if (GameState.tutorialFish && !GameState.tutorialFish.caught && !GameState.tutorialFish.missed) { checkCatch(getTouchLane(y)); // Attempt catch } return; // Tutorial catch handled } // Existing game active check if (!GameState.gameActive) { return; } var currentTime = LK.ticks * (1000 / 60); // Current time in ms if (isDown) { // Touch started inputState.touching = true; inputState.touchLane = getTouchLane(y); inputState.touchStartTime = currentTime; // A normal tap action will be processed on 'up'. } else { // Touch ended (isUp) if (inputState.touching) { // This was a normal tap. checkCatch(inputState.touchLane); } inputState.touching = false; } } /**** * Screen Management ****/ function showScreen(screenName) { titleScreen.visible = false; levelSelectScreen.visible = false; fishingScreen.visible = false; resultsScreen.visible = false; // Stop any ongoing tweens and clear particles if switching FROM title screen if (GameState.currentScreen === 'title' && titleElements) { tween.stop(titleElements.titleAnimationGroup); tween.stop(titleElements.blackOverlay); if (titleElements.titleImage) { tween.stop(titleElements.titleImage); } // Stop titleImage tween if (titleElements.logo) { tween.stop(titleElements.logo); } // Keep for safety if old code path if (titleElements.subtitle) { tween.stop(titleElements.subtitle); } // Keep for safety tween.stop(titleElements.startButton); tween.stop(titleElements.startText); tween.stop(titleElements.tutorialButton); tween.stop(titleElements.tutorialText); // Clear title screen sound timers if (titleSeagullSoundTimer) { LK.clearTimeout(titleSeagullSoundTimer); titleSeagullSoundTimer = null; } if (titleBoatSoundTimer) { LK.clearTimeout(titleBoatSoundTimer); titleBoatSoundTimer = null; } // Stop water surface animations for title screen if (titleElements.titleWaterSurfaceSegments) { titleElements.titleWaterSurfaceSegments.forEach(function (segment) { if (segment && !segment.destroyed) { tween.stop(segment); } }); } // Clear title screen particles if (titleScreenOceanBubbleContainer) { titleScreenOceanBubbleContainer.removeChildren(); } titleScreenOceanBubblesArray.forEach(function (p) { if (p && !p.destroyed) { p.destroy(); } }); titleScreenOceanBubblesArray = []; if (titleScreenSeaweedContainer) { titleScreenSeaweedContainer.removeChildren(); } titleScreenSeaweedArray.forEach(function (p) { if (p && !p.destroyed) { p.destroy(); } }); titleScreenSeaweedArray = []; if (titleScreenCloudContainer) { titleScreenCloudContainer.removeChildren(); } titleScreenCloudArray.forEach(function (p) { if (p && !p.destroyed) { p.destroy(); } }); titleScreenCloudArray = []; } // Cleanup tutorial elements if switching away from fishing/tutorial if (GameState.currentScreen === 'fishing' && GameState.tutorialMode && screenName !== 'fishing') { tutorialOverlayContainer.visible = false; if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) { GameState.tutorialFish.destroy(); var idx = fishArray.indexOf(GameState.tutorialFish); if (idx > -1) { fishArray.splice(idx, 1); } GameState.tutorialFish = null; } GameState.tutorialMode = false; // Ensure tutorial mode is exited } GameState.currentScreen = screenName; switch (screenName) { case 'title': // Title Screen Sounds var _scheduleNextSeagullSound = function scheduleNextSeagullSound() { if (GameState.currentScreen !== 'title') { // If screen changed, ensure timer is stopped and not rescheduled if (titleSeagullSoundTimer) { LK.clearTimeout(titleSeagullSoundTimer); titleSeagullSoundTimer = null; } return; } var randomDelay = 5000 + Math.random() * 10000; // 5-15 seconds titleSeagullSoundTimer = LK.setTimeout(function () { if (GameState.currentScreen !== 'title') { return; // Don't play or reschedule if not on title screen } var seagullSounds = ['seagull1', 'seagull2', 'seagull3']; var randomSoundId = seagullSounds[Math.floor(Math.random() * seagullSounds.length)]; LK.getSound(randomSoundId).play(); _scheduleNextSeagullSound(); // Reschedule }, randomDelay); }; var _scheduleNextBoatSound = function scheduleNextBoatSound() { if (GameState.currentScreen !== 'title') { // If screen changed, ensure timer is stopped and not rescheduled if (titleBoatSoundTimer) { LK.clearTimeout(titleBoatSoundTimer); titleBoatSoundTimer = null; } return; } var fixedBoatSoundInterval = 8000; // Rhythmic interval: 8 seconds (reduced from 15) titleBoatSoundTimer = LK.setTimeout(function () { if (GameState.currentScreen !== 'title') { return; // Don't play or reschedule if not on title screen } LK.getSound('boatsounds').play(); _scheduleNextBoatSound(); // Reschedule }, fixedBoatSoundInterval); }; // Play initial random seagull sound titleScreen.visible = true; // Start all animations like in fishing screen if (titleElements.startTitleWaterSurfaceAnimation) { titleElements.startTitleWaterSurfaceAnimation(); } // Start boat group animations (single animations for the whole group) if (titleElements.moveTitleBoatGroupUp) { titleElements.moveTitleBoatGroupUp(); } if (titleElements.rockTitleBoatGroupLeft) { titleElements.rockTitleBoatGroupLeft(); } var initialSeagullSounds = ['seagull1', 'seagull2', 'seagull3']; var initialRandomSoundId = initialSeagullSounds[Math.floor(Math.random() * initialSeagullSounds.length)]; LK.getSound(initialRandomSoundId).play(); LK.getSound('boatsounds').play(); // Play boat sound immediately after initial seagull // Start the timed sounds (seagulls random, subsequent boats rhythmic) _scheduleNextSeagullSound(); _scheduleNextBoatSound(); // Schedules the *next* boat sound rhythmically // Reset particle spawn counters titleScreenOceanBubbleSpawnCounter = 0; titleScreenSeaweedSpawnCounter = 0; titleScreenCloudSpawnCounter = 0; // Animation timing var ZOOM_DURATION = 8000; // 8-second zoom var OVERLAY_FADE_DELAY = 1000; // Black overlay starts fading after 1 second (was 2) var OVERLAY_FADE_DURATION = 3000; // 3 seconds to fade out overlay var TEXT_DELAY = 4000; // Text appears as overlay finishes fading (was 5500) var BUTTON_DELAY = 5500; // Buttons appear sooner (was 7000) // Reset states titleElements.titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X; titleElements.titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // Centers boat vertically! titleElements.titleAnimationGroup.alpha = 1; // No alpha animation for main content titleElements.titleAnimationGroup.scale.set(3.0); // Reset black overlay to fully opaque titleElements.blackOverlay.alpha = 1; // Reset UI alphas if (titleElements.titleImage) { titleElements.titleImage.alpha = 0; } if (titleElements.logo) { titleElements.logo.alpha = 0; } if (titleElements.subtitle) { titleElements.subtitle.alpha = 0; } titleElements.startButton.alpha = 0; titleElements.startText.alpha = 0; titleElements.tutorialButton.alpha = 0; titleElements.tutorialText.alpha = 0; // Main zoom animation (no alpha change) tween(titleElements.titleAnimationGroup, { scaleX: 1.8, scaleY: 1.8 }, { duration: ZOOM_DURATION, easing: tween.easeInOut }); // Black overlay fade out (the reveal effect) LK.setTimeout(function () { tween(titleElements.blackOverlay, { alpha: 0 }, { duration: OVERLAY_FADE_DURATION, easing: tween.easeInOut }); }, OVERLAY_FADE_DELAY); // Fade in title image after overlay is mostly gone LK.setTimeout(function () { if (titleElements.titleImage) { tween(titleElements.titleImage, { alpha: 1 }, { duration: 1200, easing: tween.easeOut }); } else if (titleElements.logo && titleElements.subtitle) { // Fallback for old structure if needed tween(titleElements.logo, { alpha: 1 }, { duration: 1200, easing: tween.easeOut }); tween(titleElements.subtitle, { alpha: 1 }, { duration: 1200, easing: tween.easeOut }); } }, TEXT_DELAY); // Fade in buttons near the end LK.setTimeout(function () { tween(titleElements.startButton, { alpha: 1 }, { duration: 1000, easing: tween.easeOut }); tween(titleElements.startText, { alpha: 1 }, { duration: 1000, easing: tween.easeOut }); tween(titleElements.tutorialButton, { alpha: 1 }, { duration: 1000, easing: tween.easeOut }); tween(titleElements.tutorialText, { alpha: 1 }, { duration: 1000, easing: tween.easeOut }); }, BUTTON_DELAY); break; case 'levelSelect': levelSelectScreen.visible = true; updateLevelSelectScreen(); break; case 'fishing': fishingScreen.visible = true; playIntroAnimation(); // Play the intro sequence break; case 'results': resultsScreen.visible = true; break; } // Tutorial System functions moved to global scope. } /**** * Intro Animation ****/ function playIntroAnimation() { GameState.introPlaying = true; GameState.gameActive = false; // Start animations for water, boat, and fisherman at the beginning of the intro if (fishingElements) { if (typeof fishingElements.startWaterSurfaceAnimation === 'function') { fishingElements.startWaterSurfaceAnimation(); } if (typeof fishingElements.startBoatAndFishermanAnimation === 'function') { fishingElements.startBoatAndFishermanAnimation(); } } // Calculate rod tip position (relative to fishingScreen) var fc = fishingElements.fishermanContainer; var f = fishingElements.fisherman; var rodTipCalculatedX = fc.x + f.x + 85; var rodTipCalculatedY = fc.y + f.y - f.height; var initialHookDangleY = rodTipCalculatedY + 50; fishingElements.hook.y = initialHookDangleY; // Setup initial zoom and camera position for fishingScreen var INITIAL_ZOOM_FACTOR = 1.5; // Pivot around the boat's visual center var pivotX = fishingElements.boat.x; var pivotY = fishingElements.boat.y - fishingElements.boat.height * (fishingElements.boat.anchor.y - 0.5); fishingScreen.pivot.set(pivotX, pivotY); // Position screen so the pivot appears at screen center when zoomed var screenCenterX = 2048 / 2; var screenCenterY = 2732 / 2; fishingScreen.x = screenCenterX; fishingScreen.y = screenCenterY; fishingScreen.scale.set(INITIAL_ZOOM_FACTOR, INITIAL_ZOOM_FACTOR); var introDuration = 2000; // Tween for zoom out tween(fishingScreen.scale, { x: 1, y: 1 }, { duration: introDuration, easing: tween.easeInOut }); // Tween screen position to compensate for the zoom change tween(fishingScreen, { x: pivotX, y: pivotY }, { duration: introDuration, easing: tween.easeInOut }); // Hook drop animation var targetHookY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; tween(fishingElements.hook, { y: targetHookY }, { duration: introDuration * 0.8, delay: introDuration * 0.2, easing: tween.easeOut, onFinish: function onFinish() { GameState.introPlaying = false; // Reset to normal view fishingScreen.pivot.set(0, 0); fishingScreen.x = 0; fishingScreen.y = 0; // Check if we're in tutorial mode if (GameState.tutorialMode) { // Start the tutorial properly after intro GameState.gameActive = false; // Keep game inactive for tutorial createTutorialElements(); // Create tutorial UI runTutorialStep(); // Start with step 0 } else { // Normal fishing session startFishingSession(); } } }); } /**** * Level Select Logic ****/ function updateLevelSelectScreen() { var elements = levelSelectElements; // Update money display elements.moneyDisplay.setText('Money: $' + GameState.money); // Create depth tabs createDepthTabs(); // Update song display updateSongDisplay(); // Update shop button updateShopButton(); } function createDepthTabs() { // Clear existing tabs levelSelectElements.depthTabs.forEach(function (tab) { if (tab.container) { tab.container.destroy(); } }); levelSelectElements.depthTabs = []; // Create tabs for unlocked depths for (var i = 0; i <= GameState.currentDepth; i++) { var depth = GAME_CONFIG.DEPTHS[i]; var isSelected = i === GameState.selectedDepth; var tabContainer = levelSelectScreen.addChild(new Container()); var tab = tabContainer.addChild(LK.getAsset('depthTab', { anchorX: 0.5, anchorY: 0.5, x: 300 + i * 220, y: 400, tint: isSelected ? 0x1976d2 : 0x455a64 })); var tabText = new Text2(depth.name.split(' ')[0], { size: 30, fill: 0xFFFFFF }); tabText.anchor.set(0.5, 0.5); tabText.x = 300 + i * 220; tabText.y = 400; tabContainer.addChild(tabText); levelSelectElements.depthTabs.push({ container: tabContainer, tab: tab, depthIndex: i }); } } function updateSongDisplay() { var elements = levelSelectElements; var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth]; var song = depth.songs[GameState.selectedSong]; var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong); // Update song info elements.songTitle.setText(song.name); elements.songInfo.setText('BPM: ' + song.bpm + ' | Duration: ' + formatTime(song.duration)); // Calculate potential earnings var minEarnings = Math.floor(depth.fishValue * 20); // Conservative estimate var maxEarnings = Math.floor(depth.fishValue * 60); // With combos and rare fish elements.songEarnings.setText('Potential Earnings: $' + minEarnings + '-$' + maxEarnings); // Update play/buy button if (owned) { elements.playButtonText.setText('PLAY'); elements.playButton.tint = 0x1976d2; } else { elements.playButtonText.setText('BUY ($' + song.cost + ')'); elements.playButton.tint = GameState.money >= song.cost ? 0x2e7d32 : 0x666666; } // Update arrow states elements.leftArrow.tint = GameState.selectedSong > 0 ? 0x1976d2 : 0x666666; elements.rightArrow.tint = GameState.selectedSong < depth.songs.length - 1 ? 0x1976d2 : 0x666666; } function updateShopButton() { var elements = levelSelectElements; var canUpgrade = GameState.canUpgrade(); var nextDepth = GAME_CONFIG.DEPTHS[GameState.currentDepth + 1]; if (nextDepth) { elements.shopButtonText.setText('UPGRADE ROD ($' + nextDepth.upgradeCost + ')'); elements.shopButton.tint = canUpgrade ? 0x2e7d32 : 0x666666; } else { elements.shopButtonText.setText('MAX DEPTH REACHED'); elements.shopButton.tint = 0x666666; } } function formatTime(ms) { var seconds = Math.floor(ms / 1000); var minutes = Math.floor(seconds / 60); seconds = seconds % 60; return minutes + ':' + (seconds < 10 ? '0' : '') + seconds; } /**** * Fishing Game Logic ****/ function startFishingSession() { // Reset session state GameState.tutorialMode = false; // Ensure tutorial mode is off GameState.sessionScore = 0; GameState.sessionFishCaught = 0; GameState.sessionFishSpawned = 0; GameState.combo = 0; GameState.maxCombo = 0; GameState.gameActive = true; GameState.songStartTime = 0; GameState.lastBeatTime = 0; GameState.beatCount = 0; GameState.musicNotesActive = true; ImprovedRhythmSpawner.reset(); musicNotesArray = []; if (fishingElements && fishingElements.musicNotesContainer) { fishingElements.musicNotesContainer.removeChildren(); } musicNoteSpawnCounter = 0; // Reset ocean bubbles globalOceanBubblesArray = []; if (globalOceanBubbleContainer) { globalOceanBubbleContainer.removeChildren(); } globalOceanBubbleSpawnCounter = 0; // Reset seaweed globalSeaweedArray = []; if (globalSeaweedContainer) { globalSeaweedContainer.removeChildren(); } globalSeaweedSpawnCounter = 0; // Reset clouds globalCloudArray = []; if (globalCloudContainer) { globalCloudContainer.removeChildren(); } globalCloudSpawnCounter = 0; // Animations for water, boat, and fisherman are now started in playIntroAnimation // Clear any existing fish fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; // Reset pattern generator for new session PatternGenerator.reset(); // Clear and create lane brackets if (laneBrackets && laneBrackets.length > 0) { laneBrackets.forEach(function (bracketPair) { if (bracketPair.left && !bracketPair.left.destroyed) { bracketPair.left.destroy(); } if (bracketPair.right && !bracketPair.right.destroyed) { bracketPair.right.destroy(); } }); } laneBrackets = []; var bracketAssetHeight = 150; // Height of the lanebracket asset var bracketAssetWidth = 75; // Width of the lanebracket asset if (fishingScreen && !fishingScreen.destroyed) { // Ensure fishingScreen is available for (var i = 0; i < GAME_CONFIG.LANES.length; i++) { var laneY = GAME_CONFIG.LANES[i].y; var leftBracket = fishingScreen.addChild(LK.getAsset('lanebracket', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5, x: bracketAssetWidth / 2, // Position its center so left edge is at 0 y: laneY, height: bracketAssetHeight })); var rightBracket = fishingScreen.addChild(LK.getAsset('lanebracket', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5, scaleX: -1, // Flipped horizontally x: 2048 - bracketAssetWidth / 2, // Position its center so right edge is at 2048 y: laneY, height: bracketAssetHeight })); laneBrackets.push({ left: leftBracket, right: rightBracket }); } } // Start music var songConfig = GameState.getCurrentSongConfig(); var musicIdToPlay = songConfig.musicId || 'rhythmTrack'; // Default to rhythmTrack if no specific id GameState.currentPlayingMusicId = musicIdToPlay; // Determine initial volume based on known assets for correct fade-out later if (musicIdToPlay === 'morningtide') { GameState.currentPlayingMusicInitialVolume = 1.0; // Volume defined in LK.init.music for 'morningtide' } else { // Default for 'rhythmTrack' or other unspecified tracks GameState.currentPlayingMusicInitialVolume = 0.8; // Volume defined in LK.init.music for 'rhythmTrack' } LK.playMusic(GameState.currentPlayingMusicId); // Play the selected music track } function spawnFish(currentTimeForRegistration, options) { options = options || {}; // Ensure options is an object var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Proximity check: Skip spawn if too close to existing fish. // This check is generally for the first fish of a beat or non-forced spawns. // For forced multi-beat spawns, this might prevent them if they are too close. // Consider if this rule should be relaxed for forced multi-beat spawns if visual overlap is acceptable for quick succession. // For now, keeping it as is. If a spawn is skipped, the multi-beat sequence might be shorter. var isFirstFishOfBeat = !options.laneIndexToUse && !options.forcedSpawnSide; if (isFirstFishOfBeat) { // Apply stricter proximity for non-forced spawns for (var i = 0; i < fishArray.length; i++) { var existingFish = fishArray[i]; if (Math.abs(existingFish.x - GAME_CONFIG.SCREEN_CENTER_X) < PatternGenerator.minDistanceBetweenFish) { return null; // Skip this spawn, do not register } } } var laneIndex; if (options.laneIndexToUse !== undefined) { laneIndex = options.laneIndexToUse; PatternGenerator.lastLane = laneIndex; // Update generator's state if lane is forced } else { laneIndex = PatternGenerator.getNextLane(); } var targetLane = GAME_CONFIG.LANES[laneIndex]; var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; } else if (GameState.selectedDepth >= 2 && rand < 0.3) { fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; fishValue = depthConfig.fishValue; } var fishSpeedValue = depthConfig.fishSpeed; var spawnSide; // -1 for left, 1 for right var actualFishSpeed; if (options.forcedSpawnSide !== undefined) { spawnSide = options.forcedSpawnSide; } else { spawnSide = Math.random() < 0.5 ? -1 : 1; } actualFishSpeed = Math.abs(fishSpeedValue) * spawnSide; var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex); newFish.spawnSide = spawnSide; // Store the side it spawned from newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen newFish.y = targetLane.y; newFish.baseY = targetLane.y; // Set baseY for swimming animation fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTimeForRegistration); return newFish; } function checkCatch(fishLane) { var hookX = fishingElements.hook.x; if (GameState.tutorialMode) { var tutorialFish = GameState.tutorialFish; if (!tutorialFish || tutorialFish.lane !== fishLane || tutorialFish.caught || tutorialFish.missed) { if (GameState.tutorialStep === 2 || GameState.tutorialStep === 3) { setTutorialText("Oops! Make sure to tap when the fish is in the correct lane and under the hook. Tap 'CONTINUE' to try again."); GameState.tutorialPaused = true; } LK.getSound('miss').play(); return; } var distance = Math.abs(tutorialFish.x - hookX); var caughtType = null; if (distance < GAME_CONFIG.PERFECT_WINDOW) { caughtType = 'perfect'; } else if (distance < GAME_CONFIG.GOOD_WINDOW) { caughtType = 'good'; } else if (distance < GAME_CONFIG.MISS_WINDOW) { // For tutorial, make 'miss window' taps still count as 'good' to be more forgiving caughtType = 'good'; } else { caughtType = 'miss'; } showFeedback(caughtType, fishLane); // Show visual feedback based on derived type if (caughtType === 'perfect' || caughtType === 'good') { tutorialFish.catchFish(); var fishIndex = fishArray.indexOf(tutorialFish); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } // No points/money in tutorial normally, but can add if desired. LK.getSound('catch').play(); animateHookCatch(); GameState.tutorialPaused = true; // Pause to show message if (GameState.tutorialStep === 2) { setTutorialText("Great catch! That's how you do it. Tap 'CONTINUE'."); GameState.tutorialStep = 3; // Advance to next conceptual phase (Perfect/Good explanation) } else if (GameState.tutorialStep === 3) { setTutorialText("Nice one! You're getting the hang of timing. Tap 'CONTINUE'."); GameState.tutorialStep = 4; // Advance to Combo explanation } } else { // Miss // Feedback 'miss' was already shown LK.getSound('miss').play(); tutorialFish.missed = true; GameState.tutorialPaused = true; setTutorialText("Almost! Try to tap when the fish is closer. Tap 'CONTINUE' to try this part again."); // The runTutorialStep logic on "CONTINUE" will handle respawning for this step. } return; // Tutorial catch logic finished } 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]; // Ensure fish is not caught, not already missed, and in the correct lane if (!fish.caught && !fish.missed && fish.lane === fishLane) { var distance = Math.abs(fish.x - hookX); if (distance < closestDistance) { closestDistance = distance; closestFishInLane = fish; } } } if (!closestFishInLane) { // No fish found for tap // Play miss sound LK.getSound('miss').play(); // Tint incorrect lane indicators red briefly using tween if (laneBrackets && laneBrackets[fishLane]) { var leftBracket = laneBrackets[fishLane].left; var rightBracket = laneBrackets[fishLane].right; var tintToRedDuration = 50; // Duration to tween to red (ms) var holdRedDuration = 100; // How long it stays fully red (ms) var tintToWhiteDuration = 150; // Duration to tween back to white (ms) if (leftBracket && !leftBracket.destroyed) { // Tween to red tween(leftBracket, { tint: 0xFF0000 }, { duration: tintToRedDuration, easing: tween.linear, onFinish: function onFinish() { // After tinting to red, wait, then tween back to white LK.setTimeout(function () { if (leftBracket && !leftBracket.destroyed) { tween(leftBracket, { tint: 0xFFFFFF }, { duration: tintToWhiteDuration, easing: tween.linear }); } }, holdRedDuration); } }); } if (rightBracket && !rightBracket.destroyed) { // Tween to red tween(rightBracket, { tint: 0xFF0000 }, { duration: tintToRedDuration, easing: tween.linear, onFinish: function onFinish() { // After tinting to red, wait, then tween back to white LK.setTimeout(function () { if (rightBracket && !rightBracket.destroyed) { tween(rightBracket, { tint: 0xFFFFFF }, { duration: tintToWhiteDuration, easing: tween.linear }); } }, holdRedDuration); } }); } } GameState.combo = 0; return; } // --- Normal Fish Catch Logic --- var points = 0; var multiplier = Math.max(1, Math.floor(GameState.combo / 10) + 1); if (closestDistance < GAME_CONFIG.PERFECT_WINDOW) { points = closestFishInLane.value * 2 * multiplier; showFeedback('perfect', fishLane); GameState.combo++; } else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) { points = closestFishInLane.value * multiplier; showFeedback('good', fishLane); GameState.combo++; } else if (closestDistance < GAME_CONFIG.MISS_WINDOW) { points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier)); showFeedback('good', fishLane); GameState.combo++; } else { showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; // Mark the specific fish that was tapped but missed if (closestFishInLane) { closestFishInLane.missed = true; } 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; GameState.tutorialMode = false; // Ensure tutorial mode is off // Stop the boat's wave animation to prevent it from running after the session if (fishingElements && fishingElements.boat) { tween.stop(fishingElements.boat); } // Stop fisherman container animation if (fishingElements && fishingElements.fishermanContainer) { tween.stop(fishingElements.fishermanContainer); } // Stop fisherman rotation animation if (fishingElements && fishingElements.fisherman) { tween.stop(fishingElements.fisherman); } // Stop water surface wave animations if (fishingElements && fishingElements.waterSurfaceSegments) { fishingElements.waterSurfaceSegments.forEach(function (segment) { if (segment && !segment.destroyed) { tween.stop(segment); } }); } // Fade out and stop music for the results screen var currentMusicId = GameState.currentPlayingMusicId; var currentMusicInitialVolume = GameState.currentPlayingMusicInitialVolume; var fadeOutDurationMs = 1000; // Duration of the fade out in milliseconds (e.g., 1 second) LK.playMusic(currentMusicId, { fade: { start: currentMusicInitialVolume, end: 0, duration: fadeOutDurationMs }, loop: false // Important to prevent restart after fade or if it's re-triggered }); // Ensure music is fully stopped after the fade duration has elapsed LK.setTimeout(function () { LK.stopMusic(); }, fadeOutDurationMs); ImprovedRhythmSpawner.reset(); // Clear lane brackets if (laneBrackets && laneBrackets.length > 0) { laneBrackets.forEach(function (bracketPair) { if (bracketPair.left && !bracketPair.left.destroyed) { bracketPair.left.destroy(); } if (bracketPair.right && !bracketPair.right.destroyed) { bracketPair.right.destroy(); } }); laneBrackets = []; } // Clear fish fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; GameState.musicNotesActive = false; if (fishingElements && fishingElements.musicNotesContainer) { fishingElements.musicNotesContainer.removeChildren(); } // The MusicNoteParticle instances themselves will be garbage collected. // Clearing the array is important. musicNotesArray = []; // Clear ocean bubbles if (globalOceanBubbleContainer) { globalOceanBubbleContainer.removeChildren(); } globalOceanBubblesArray = []; // Clear seaweed if (globalSeaweedContainer) { globalSeaweedContainer.removeChildren(); } globalSeaweedArray = []; // Clear clouds if (globalCloudContainer) { globalCloudContainer.removeChildren(); } globalCloudArray = []; // Create results screen createResultsScreen(); showScreen('results'); } function createResultsScreen() { // Clear previous results resultsScreen.removeChildren(); var resultsBg = resultsScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.9, height: 2732 })); var title = new Text2('Fishing Complete!', { size: 100, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.x = GAME_CONFIG.SCREEN_CENTER_X; title.y = 400; resultsScreen.addChild(title); var scoreResult = new Text2('Score: ' + GameState.sessionScore, { size: 70, fill: 0xFFD700 }); scoreResult.anchor.set(0.5, 0.5); scoreResult.x = GAME_CONFIG.SCREEN_CENTER_X; scoreResult.y = 550; resultsScreen.addChild(scoreResult); var fishResult = new Text2('Fish Caught: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned, { size: 50, fill: 0xFFFFFF }); fishResult.anchor.set(0.5, 0.5); fishResult.x = GAME_CONFIG.SCREEN_CENTER_X; fishResult.y = 650; resultsScreen.addChild(fishResult); var comboResult = new Text2('Max Combo: ' + GameState.maxCombo, { size: 50, fill: 0xFF9800 }); comboResult.anchor.set(0.5, 0.5); comboResult.x = GAME_CONFIG.SCREEN_CENTER_X; comboResult.y = 750; resultsScreen.addChild(comboResult); var moneyEarned = new Text2('Money Earned: $' + GameState.sessionScore, { size: 50, fill: 0x4CAF50 }); moneyEarned.anchor.set(0.5, 0.5); moneyEarned.x = GAME_CONFIG.SCREEN_CENTER_X; moneyEarned.y = 850; resultsScreen.addChild(moneyEarned); // Accuracy var accuracy = GameState.sessionFishSpawned > 0 ? Math.round(GameState.sessionFishCaught / GameState.sessionFishSpawned * 100) : 0; var accuracyResult = new Text2('Accuracy: ' + accuracy + '%', { size: 50, fill: 0x2196F3 }); accuracyResult.anchor.set(0.5, 0.5); accuracyResult.x = GAME_CONFIG.SCREEN_CENTER_X; accuracyResult.y = 950; resultsScreen.addChild(accuracyResult); // Continue button var continueButton = resultsScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1200 })); var continueText = new Text2('CONTINUE', { size: 50, fill: 0xFFFFFF }); continueText.anchor.set(0.5, 0.5); continueText.x = GAME_CONFIG.SCREEN_CENTER_X; continueText.y = 1200; resultsScreen.addChild(continueText); // Fade in resultsScreen.alpha = 0; tween(resultsScreen, { alpha: 1 }, { duration: 500, easing: tween.easeOut }); } /**** * Input Handling ****/ game.down = function (x, y, obj) { LK.getSound('buttonClick').play(); var currentScreen = GameState.currentScreen; // If tutorial mode is active, treat as 'tutorial' screen for input if (GameState.tutorialMode && (currentScreen === 'fishing' || currentScreen === 'tutorial')) { // New case for tutorial input if (tutorialOverlayContainer.visible && tutorialContinueButton && tutorialContinueButton.visible && x >= tutorialContinueButton.x - tutorialContinueButton.width / 2 && x <= tutorialContinueButton.x + tutorialContinueButton.width / 2 && y >= tutorialContinueButton.y - tutorialContinueButton.height / 2 && y <= tutorialContinueButton.y + tutorialContinueButton.height / 2) { LK.getSound('buttonClick').play(); // If tutorial was paused for a message after a miss in a catch step, re-run current step. if (GameState.tutorialPaused && (GameState.tutorialStep === 2 || GameState.tutorialStep === 3) && GameState.tutorialFish && GameState.tutorialFish.missed) { // Fish was missed, button click should retry the step if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) { GameState.tutorialFish.destroy(); } var idx = fishArray.indexOf(GameState.tutorialFish); if (idx > -1) { fishArray.splice(idx, 1); } GameState.tutorialFish = null; runTutorialStep(); // Re-runs current step to spawn new fish } else { GameState.tutorialStep++; runTutorialStep(); } } else if ((GameState.tutorialStep === 2 || GameState.tutorialStep === 3) && !GameState.tutorialPaused) { // Catch attempt by tapping screen, not the continue button handleFishingInput(x, y, true); // True for isDown } return; } switch (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 tutorialButtonGfx = titleElements.tutorialButtonGfx || titleElements.tutorialButton; // Compatibility if (x >= tutorialButtonGfx.x - tutorialButtonGfx.width / 2 && x <= tutorialButtonGfx.x + tutorialButtonGfx.width / 2 && y >= tutorialButtonGfx.y - tutorialButtonGfx.height / 2 && y <= tutorialButtonGfx.y + tutorialButtonGfx.height / 2) { // Defensive: check if startTutorial is defined before calling if (typeof startTutorial === "function") { startTutorial(); } } break; case 'levelSelect': handleLevelSelectInput(x, y); break; case 'fishing': handleFishingInput(x, y, true); // true for isDown break; case 'results': showScreen('levelSelect'); break; } }; function handleLevelSelectInput(x, y) { var elements = levelSelectElements; // Check depth tabs elements.depthTabs.forEach(function (tab) { var tabAsset = tab.tab; if (x >= tabAsset.x - tabAsset.width / 2 && x <= tabAsset.x + tabAsset.width / 2 && y >= tabAsset.y - tabAsset.height / 2 && y <= tabAsset.y + tabAsset.height / 2) { GameState.selectedDepth = tab.depthIndex; GameState.selectedSong = 0; // Reset to first song updateLevelSelectScreen(); } }); // Check song navigation var leftArrow = elements.leftArrow; if (x >= leftArrow.x - leftArrow.width / 2 && x <= leftArrow.x + leftArrow.width / 2 && y >= leftArrow.y - leftArrow.height / 2 && y <= leftArrow.y + leftArrow.height / 2 && GameState.selectedSong > 0) { GameState.selectedSong--; updateSongDisplay(); } var rightArrow = elements.rightArrow; if (x >= rightArrow.x - rightArrow.width / 2 && x <= rightArrow.x + rightArrow.width / 2 && y >= rightArrow.y - rightArrow.height / 2 && y <= rightArrow.y + rightArrow.height / 2) { var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth]; if (GameState.selectedSong < depth.songs.length - 1) { GameState.selectedSong++; updateSongDisplay(); } } // Check play/buy button var playButton = elements.playButton; if (x >= playButton.x - playButton.width / 2 && x <= playButton.x + playButton.width / 2 && y >= playButton.y - playButton.height / 2 && y <= playButton.y + playButton.height / 2) { var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong); if (owned) { showScreen('fishing'); } else { // Try to buy song if (GameState.buySong(GameState.selectedDepth, GameState.selectedSong)) { updateLevelSelectScreen(); } } } // Check shop button var shopButton = elements.shopButton; if (x >= shopButton.x - shopButton.width / 2 && x <= shopButton.x + shopButton.width / 2 && y >= shopButton.y - shopButton.height / 2 && y <= shopButton.y + shopButton.height / 2) { if (GameState.upgrade()) { LK.getSound('upgrade').play(); updateLevelSelectScreen(); } } // Check back button var backButton = elements.backButton; if (x >= backButton.x - backButton.width / 2 && x <= backButton.x + backButton.width / 2 && y >= backButton.y - backButton.height / 2 && y <= backButton.y + backButton.height / 2) { showScreen('title'); } } /**** * Main Game Loop ****/ game.update = function () { // Always update fishing line wave visuals if on fishing screen and elements are ready. // This needs to run even during the intro when gameActive might be false. if (GameState.currentScreen === 'fishing' && fishingElements && fishingElements.updateFishingLineWave) { fishingElements.updateFishingLineWave(); } // Update title screen ambient particles if title screen is active if (GameState.currentScreen === 'title') { // Add this to the game.update function, in the title screen section: if (GameState.currentScreen === 'title' && titleElements && titleElements.updateTitleFishingLineWave) { titleElements.updateTitleFishingLineWave(); } // Title Screen Ocean Bubbles if (titleScreenOceanBubbleContainer) { titleScreenOceanBubbleSpawnCounter++; if (titleScreenOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) { // Use same interval as fishing titleScreenOceanBubbleSpawnCounter = 0; var newOceanBubble = new OceanBubbleParticle(); // Assuming OceanBubbleParticle is general enough titleScreenOceanBubbleContainer.addChild(newOceanBubble); titleScreenOceanBubblesArray.push(newOceanBubble); } for (var obIdx = titleScreenOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) { var oceanBubble = titleScreenOceanBubblesArray[obIdx]; if (oceanBubble) { oceanBubble.update(); // No fish interaction on title screen if (oceanBubble.isDone) { oceanBubble.destroy(); titleScreenOceanBubblesArray.splice(obIdx, 1); } } else { titleScreenOceanBubblesArray.splice(obIdx, 1); } } } // Title Screen Seaweed if (titleScreenSeaweedContainer) { titleScreenSeaweedSpawnCounter++; if (titleScreenSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && titleScreenSeaweedArray.length < MAX_SEAWEED_COUNT) { titleScreenSeaweedSpawnCounter = 0; var newSeaweed = new SeaweedParticle(); titleScreenSeaweedContainer.addChild(newSeaweed); titleScreenSeaweedArray.push(newSeaweed); } for (var swIdx = titleScreenSeaweedArray.length - 1; swIdx >= 0; swIdx--) { var seaweed = titleScreenSeaweedArray[swIdx]; if (seaweed) { seaweed.update(); // No fish interaction on title screen if (seaweed.isDone) { seaweed.destroy(); titleScreenSeaweedArray.splice(swIdx, 1); } } else { titleScreenSeaweedArray.splice(swIdx, 1); } } } // Title Screen Clouds if (titleScreenCloudContainer) { titleScreenCloudSpawnCounter++; if (titleScreenCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && titleScreenCloudArray.length < MAX_CLOUD_COUNT) { titleScreenCloudSpawnCounter = 0; var newCloud = new CloudParticle(); titleScreenCloudContainer.addChild(newCloud); titleScreenCloudArray.push(newCloud); } for (var cldIdx = titleScreenCloudArray.length - 1; cldIdx >= 0; cldIdx--) { var cloud = titleScreenCloudArray[cldIdx]; if (cloud) { cloud.update(); if (cloud.isDone) { cloud.destroy(); titleScreenCloudArray.splice(cldIdx, 1); } } else { titleScreenCloudArray.splice(cldIdx, 1); } } } } // Spawn and update ambient ocean bubbles during intro and gameplay (fishing screen) if (GameState.currentScreen === 'fishing' && globalOceanBubbleContainer) { // Spawn bubbles during intro and gameplay globalOceanBubbleSpawnCounter++; if (globalOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) { globalOceanBubbleSpawnCounter = 0; var numToSpawn = 1; // Always spawn only 1 bubble per interval (was 1 or 2) for (var i = 0; i < numToSpawn; i++) { var newOceanBubble = new OceanBubbleParticle(); globalOceanBubbleContainer.addChild(newOceanBubble); globalOceanBubblesArray.push(newOceanBubble); } } // Update existing ocean bubbles for (var obIdx = globalOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) { var oceanBubble = globalOceanBubblesArray[obIdx]; if (oceanBubble) { // Apply fish physics to bubble for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) { var fish = fishArray[fishIdx]; if (fish && !fish.caught) { var dx = oceanBubble.x - fish.x; var dy = oceanBubble.y - fish.y; var distance = Math.sqrt(dx * dx + dy * dy); var influenceRadius = 150; // Radius of fish influence on bubbles var minDistance = 30; // Minimum distance to avoid division issues if (distance < influenceRadius && distance > minDistance) { // Calculate influence strength (stronger when closer) var influence = 1 - distance / influenceRadius; influence = influence * influence; // Square for more dramatic close-range effect // Calculate normalized direction away from fish var dirX = dx / distance; var dirY = dy / distance; // Apply force based on fish speed and direction var fishSpeedFactor = Math.abs(fish.speed) * 0.15; // Scale down fish speed influence var pushForce = fishSpeedFactor * influence; // Add horizontal push (stronger in direction of fish movement) oceanBubble.x += dirX * pushForce * 2; // Stronger horizontal push // Add vertical component (bubbles get pushed up/down) oceanBubble.vy += dirY * pushForce * 0.5; // Gentler vertical influence // Add some swirl/turbulence to drift oceanBubble.driftAmplitude = Math.min(80, oceanBubble.driftAmplitude + pushForce * 10); oceanBubble.driftFrequency *= 1 + influence * 0.1; // Slightly increase oscillation when disturbed } } } oceanBubble.update(); if (oceanBubble.isDone) { oceanBubble.destroy(); globalOceanBubblesArray.splice(obIdx, 1); } } else { globalOceanBubblesArray.splice(obIdx, 1); // Safeguard for null entries } } } // Spawn and update seaweed particles during intro and gameplay if (GameState.currentScreen === 'fishing' && globalSeaweedContainer) { // Spawn seaweed during intro and gameplay globalSeaweedSpawnCounter++; if (globalSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && globalSeaweedArray.length < MAX_SEAWEED_COUNT) { globalSeaweedSpawnCounter = 0; var newSeaweed = new SeaweedParticle(); globalSeaweedContainer.addChild(newSeaweed); globalSeaweedArray.push(newSeaweed); } // Update existing seaweed for (var swIdx = globalSeaweedArray.length - 1; swIdx >= 0; swIdx--) { var seaweed = globalSeaweedArray[swIdx]; if (seaweed) { // Apply fish physics to seaweed for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) { var fish = fishArray[fishIdx]; if (fish && !fish.caught) { var dx = seaweed.x - fish.x; var dy = seaweed.y - fish.y; var distance = Math.sqrt(dx * dx + dy * dy); var influenceRadius = 180; // Slightly larger influence for seaweed var minDistance = 40; if (distance < influenceRadius && distance > minDistance) { // Calculate influence strength var influence = 1 - distance / influenceRadius; influence = influence * influence; // Calculate normalized direction away from fish var dirX = dx / distance; var dirY = dy / distance; // Apply force based on fish speed var fishSpeedFactor = Math.abs(fish.speed) * 0.2; // Stronger influence on seaweed var pushForce = fishSpeedFactor * influence; // Add push forces seaweed.vx += dirX * pushForce * 1.5; // Seaweed is affected more horizontally seaweed.vy += dirY * pushForce * 0.8; // And moderately vertically // Increase sway when disturbed seaweed.swayAmplitude = Math.min(60, seaweed.swayAmplitude + pushForce * 15); } } } seaweed.update(); if (seaweed.isDone) { seaweed.destroy(); globalSeaweedArray.splice(swIdx, 1); } } else { globalSeaweedArray.splice(swIdx, 1); } } } // Spawn and update cloud particles during intro and gameplay if (GameState.currentScreen === 'fishing' && globalCloudContainer) { // Spawn clouds during intro and gameplay globalCloudSpawnCounter++; if (globalCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && globalCloudArray.length < MAX_CLOUD_COUNT) { globalCloudSpawnCounter = 0; var newCloud = new CloudParticle(); globalCloudContainer.addChild(newCloud); globalCloudArray.push(newCloud); } // Update existing clouds for (var cldIdx = globalCloudArray.length - 1; cldIdx >= 0; cldIdx--) { var cloud = globalCloudArray[cldIdx]; if (cloud) { cloud.update(); if (cloud.isDone) { cloud.destroy(); globalCloudArray.splice(cldIdx, 1); } } else { globalCloudArray.splice(cldIdx, 1); } } } // Standard game active check; if intro is playing, gameActive will be false. // Tutorial mode has its own logic path if (GameState.currentScreen === 'fishing' && GameState.tutorialMode) { // Update essential non-paused elements like fishing line wave if (fishingElements && fishingElements.updateFishingLineWave) { fishingElements.updateFishingLineWave(); } // Update ambient particles (clouds, background bubbles, seaweed can run if desired) // ... (Can copy particle update logic here if they should be active in tutorial) if (!GameState.tutorialPaused) { // Update tutorial fish if one exists and is active if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) { GameState.tutorialFish.update(); checkTutorialFishState(); // Check its state (missed, off-screen) } // Hook follows tutorial fish if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) { if (GameState.hookTargetLaneIndex !== GameState.tutorialFish.lane) { GameState.hookTargetLaneIndex = GameState.tutorialFish.lane; } var targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; if (fishingElements.hook && Math.abs(fishingElements.hook.y - targetLaneY) > 1) { // Smoother threshold // fishingElements.hook.y = targetLaneY; // Instant for tutorial responsiveness tween(fishingElements.hook, { y: targetLaneY }, { duration: 100, easing: tween.linear }); fishingElements.hook.originalY = targetLaneY; } } else if (fishingElements.hook) { // If no tutorial fish, hook stays in middle or last targeted lane var middleLaneY = GAME_CONFIG.LANES[1].y; if (fishingElements.hook.y !== middleLaneY) { // tween(fishingElements.hook, { y: middleLaneY }, { duration: 150, easing: tween.easeOut }); // fishingElements.hook.originalY = middleLaneY; } } } updateLaneBracketsVisuals(); return; // End tutorial update logic } if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } // Note: The fishing line wave update was previously here, it's now moved up. var currentTime = LK.ticks * (1000 / 60); // Initialize game timer if (GameState.songStartTime === 0) { GameState.songStartTime = currentTime; } // Check song end var songConfig = GameState.getCurrentSongConfig(); if (currentTime - GameState.songStartTime >= songConfig.duration) { endFishingSession(); return; } // Use RhythmSpawner to handle fish spawning ImprovedRhythmSpawner.update(currentTime); // Dynamic Hook Movement Logic var approachingFish = null; var minDistanceToCenter = Infinity; for (var i = 0; i < fishArray.length; i++) { var f = fishArray[i]; if (!f.caught) { var distanceToHookX = Math.abs(f.x - fishingElements.hook.x); var isApproachingOrAtHook = f.speed > 0 && f.x < fishingElements.hook.x || f.speed < 0 && f.x > fishingElements.hook.x || distanceToHookX < GAME_CONFIG.MISS_WINDOW * 2; if (isApproachingOrAtHook && distanceToHookX < minDistanceToCenter) { minDistanceToCenter = distanceToHookX; approachingFish = f; } } } var targetLaneY; if (approachingFish) { if (GameState.hookTargetLaneIndex !== approachingFish.lane) { GameState.hookTargetLaneIndex = approachingFish.lane; } targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; } else { targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; } // Update hook Y position (X is handled by the wave animation) if (Math.abs(fishingElements.hook.y - targetLaneY) > 5) { // Only tween if significantly different tween(fishingElements.hook, { y: targetLaneY }, { duration: 150, easing: tween.easeOut }); fishingElements.hook.originalY = targetLaneY; } //{bO} // Re-using last relevant ID from replaced block for context if appropriate updateLaneBracketsVisuals(); // Update fish for (var i = fishArray.length - 1; i >= 0; i--) { var fish = fishArray[i]; var previousFrameX = fish.lastX; // X position from the end of the previous game tick fish.update(); // Fish updates its own movement (fish.x) and appearance var currentFrameX = fish.x; // X position after this tick's update // Check for miss only if fish is active (not caught, not already missed) if (!fish.caught && !fish.missed) { var hookCenterX = fishingElements.hook.x; // GAME_CONFIG.MISS_WINDOW is the distance from hook center that still counts as a "miss" tap. // If fish center passes this boundary, it's considered a full pass. var missCheckBoundary = GAME_CONFIG.MISS_WINDOW; if (fish.speed > 0) { // Moving Left to Right // Fish's center (previousFrameX) was to the left of/at the hook's right miss boundary, // and its center (currentFrameX) is now to the right of it. if (previousFrameX <= hookCenterX + missCheckBoundary && currentFrameX > hookCenterX + missCheckBoundary) { showFeedback('miss', fish.lane); LK.getSound('miss').play(); GameState.combo = 0; fish.missed = true; // Mark as missed } } else if (fish.speed < 0) { // Moving Right to Left // Fish's center (previousFrameX) was to the right of/at the hook's left miss boundary, // and its center (currentFrameX) is now to the left of it. if (previousFrameX >= hookCenterX - missCheckBoundary && currentFrameX < hookCenterX - missCheckBoundary) { showFeedback('miss', fish.lane); LK.getSound('miss').play(); GameState.combo = 0; fish.missed = true; // Mark as missed } } } // Update lastX for the next frame, using the fish's position *after* its update this frame fish.lastX = currentFrameX; // Remove off-screen fish (if not caught). // If a fish is `missed`, it's also `!caught`, so it will be removed by this logic. if (!fish.caught && (fish.x < -250 || fish.x > 2048 + 250)) { // Increased buffer slightly fish.destroy(); fishArray.splice(i, 1); } } // Update UI updateFishingUI(); // Spawn and update music notes if active if (GameState.musicNotesActive && fishingElements && fishingElements.hook && !fishingElements.hook.destroyed && musicNotesContainer) { musicNoteSpawnCounter++; if (musicNoteSpawnCounter >= MUSIC_NOTE_SPAWN_INTERVAL_TICKS) { musicNoteSpawnCounter = 0; // Spawn notes from the fishing hook's position var spawnX = fishingElements.hook.x; var spawnY = fishingElements.hook.y - 30; // Spawn slightly above the hook's center for better visual origin var newNote = new MusicNoteParticle(spawnX, spawnY); musicNotesContainer.addChild(newNote); musicNotesArray.push(newNote); // Add scale pulse to the hook, synced with BPM if (fishingElements.hook && !fishingElements.hook.destroyed && fishingElements.hook.scale) { var currentSongConfig = GameState.getCurrentSongConfig(); var bpm = currentSongConfig && currentSongConfig.bpm ? currentSongConfig.bpm : 90; // Default to 90 BPM var beatDurationMs = 60000 / bpm; var pulsePhaseDuration = Math.max(50, beatDurationMs / 2); // Each phase (up/down) is half a beat, min 50ms var pulseScaleFactor = 1.2; // Ensure we have valid original scales, defaulting to 1 if undefined var originalScaleX = fishingElements.hook.scale.x !== undefined ? fishingElements.hook.scale.x : 1; var originalScaleY = fishingElements.hook.scale.y !== undefined ? fishingElements.hook.scale.y : 1; // Stop any previous scale tweens on the hook to prevent conflicts tween.stop(fishingElements.hook.scale); tween(fishingElements.hook.scale, { x: originalScaleX * pulseScaleFactor, y: originalScaleY * pulseScaleFactor }, { duration: pulsePhaseDuration, easing: tween.easeOut, onFinish: function onFinish() { if (fishingElements.hook && !fishingElements.hook.destroyed && fishingElements.hook.scale) { tween(fishingElements.hook.scale, { x: originalScaleX, y: originalScaleY }, { duration: pulsePhaseDuration, easing: tween.easeIn // Or tween.easeOut for a softer return }); } } }); } } } // Update existing music notes for (var mnIdx = musicNotesArray.length - 1; mnIdx >= 0; mnIdx--) { var note = musicNotesArray[mnIdx]; if (note) { note.update(); if (note.isDone) { note.destroy(); musicNotesArray.splice(mnIdx, 1); } } else { // Should not happen, but good to safeguard musicNotesArray.splice(mnIdx, 1); } } // Spawn bubbles for active fish if (bubbleContainer) { for (var f = 0; f < fishArray.length; f++) { var fish = fishArray[f]; if (fish && !fish.caught && !fish.isHeld && fish.fishGraphics) { if (currentTime - fish.lastBubbleSpawnTime > fish.bubbleSpawnInterval) { fish.lastBubbleSpawnTime = currentTime; // Calculate tail position based on fish direction and width // fish.fishGraphics.width is the original asset width. // fish.fishGraphics.scale.x might be negative, but width property itself is positive. // The anchor is 0.5, so width/2 is distance from center to edge. var tailOffsetDirection = Math.sign(fish.speed) * -1; // Bubbles appear opposite to movement direction var bubbleX = fish.x + tailOffsetDirection * (fish.fishGraphics.width * Math.abs(fish.fishGraphics.scaleX) / 2) * 0.8; // 80% towards tail var bubbleY = fish.y + (Math.random() - 0.5) * (fish.fishGraphics.height * Math.abs(fish.fishGraphics.scaleY) / 4); // Slight Y variance around fish center var newBubble = new BubbleParticle(bubbleX, bubbleY); bubbleContainer.addChild(newBubble); bubblesArray.push(newBubble); } } } } // Update and remove bubbles for (var b = bubblesArray.length - 1; b >= 0; b--) { var bubble = bubblesArray[b]; if (bubble) { // Extra safety check bubble.update(); if (bubble.isDone) { bubble.destroy(); bubblesArray.splice(b, 1); } } else { // If a null/undefined somehow got in bubblesArray.splice(b, 1); } } }; // Initialize game showScreen('title');
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var BubbleParticle = Container.expand(function (startX, startY) {
var self = Container.call(this);
self.gfx = self.attachAsset('bubbles', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 1 + Math.random() * 0.2,
// Start with some variation
scaleX: 1.2 + Math.random() * 0.25,
// Larger bubbles
// Bubbles are now 20% to 45% of original asset size
scaleY: this.scaleX // Keep aspect ratio initially, can be changed in update if desired
});
self.x = startX;
self.y = startY;
self.vx = (Math.random() - 0.5) * 0.4; // Even slower horizontal drift
self.vy = -(0.4 + Math.random() * 0.3); // Slower upward movement
self.life = 120 + Math.random() * 60; // Lifespan in frames (2 to 3 seconds)
self.age = 0;
self.isDone = false;
var initialAlpha = self.gfx.alpha;
var initialScale = self.gfx.scaleX; // Assuming scaleX and scaleY start the same
self.update = function () {
if (self.isDone) {
return;
}
self.age++;
self.x += self.vx;
self.y += self.vy;
// Fade out
self.gfx.alpha = Math.max(0, initialAlpha * (1 - self.age / self.life));
// Optionally shrink a bit more, or grow slightly then shrink
var scaleFactor = 1 - self.age / self.life; // Simple shrink
self.gfx.scaleX = initialScale * scaleFactor;
self.gfx.scaleY = initialScale * scaleFactor;
if (self.age >= self.life || self.gfx.alpha <= 0 || self.gfx.scaleX <= 0.01) {
self.isDone = true;
}
};
return self;
});
var CloudParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('cloud', {
anchorX: 0.5,
anchorY: 0.5
});
// Cloud spawn location - 30% chance to spawn on screen
var spawnOnScreen = Math.random() < 0.3;
if (spawnOnScreen) {
// Spawn randomly across the screen width
self.x = 200 + Math.random() * 1648; // Between 200 and 1848 to avoid edges
// Random horizontal drift direction
self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.1 + Math.random() * 0.2);
} else {
// Original off-screen spawn logic (70% of the time)
var spawnFromLeft = Math.random() < 0.5;
if (spawnFromLeft) {
self.x = -100; // Start off-screen left
self.vx = 0.1 + Math.random() * 0.2; // Slower drift right at 0.1-0.3 pixels/frame
} else {
self.x = 2048 + 100; // Start off-screen right
self.vx = -(0.1 + Math.random() * 0.2); // Slower drift left
}
}
// Spawn in sky area (above water surface)
var skyTop = -500; // Where sky background starts
var skyBottom = GAME_CONFIG.WATER_SURFACE_Y - 100; // Stay well above water
self.y = skyTop + Math.random() * (skyBottom - skyTop);
// Cloud properties
var baseScale = 0.8 + Math.random() * 0.6; // Scale: 0.8x to 1.4x
self.gfx.scale.set(baseScale);
// Subtle vertical drift
self.vy = (Math.random() - 0.5) * 0.02; // Even slower up/down drift
// Opacity for atmospheric effect
var targetAlpha = 0.4 + Math.random() * 0.3; // Alpha: 0.4 to 0.7
self.gfx.alpha = 0; // Start transparent
self.isDone = false;
self.fadingOut = false;
self.hasFadedIn = false; // Track if initial fade in completed
// Define screen boundaries for fade in/out
var fadeInStartX = 400; // Start fading in when cloud reaches this X
var fadeInEndX = 800; // Fully visible by this X
var fadeOutStartX = 1248; // Start fading out at this X (2048 - 800)
var fadeOutEndX = 1648; // Fully transparent by this X (2048 - 400)
self.update = function () {
if (self.isDone) {
return;
}
// Apply movement
self.x += self.vx;
self.y += self.vy;
// Handle mid-screen fade in/out based on position
var currentAlpha = self.gfx.alpha;
if (spawnFromLeft) {
// Moving right: fade in then fade out
if (!self.hasFadedIn && self.x >= fadeInStartX && self.x <= fadeInEndX) {
// Calculate fade in progress
var fadeInProgress = (self.x - fadeInStartX) / (fadeInEndX - fadeInStartX);
self.gfx.alpha = targetAlpha * fadeInProgress;
if (fadeInProgress >= 1) {
self.hasFadedIn = true;
}
} else if (self.hasFadedIn && self.x >= fadeOutStartX && self.x <= fadeOutEndX) {
// Calculate fade out progress
var fadeOutProgress = (self.x - fadeOutStartX) / (fadeOutEndX - fadeOutStartX);
self.gfx.alpha = targetAlpha * (1 - fadeOutProgress);
} else if (self.hasFadedIn && self.x > fadeInEndX && self.x < fadeOutStartX) {
// Maintain full opacity in middle section
self.gfx.alpha = targetAlpha;
}
} else {
// Moving left: fade in then fade out (reversed positions)
if (!self.hasFadedIn && self.x <= fadeOutEndX && self.x >= fadeOutStartX) {
// Calculate fade in progress (reversed)
var fadeInProgress = (fadeOutEndX - self.x) / (fadeOutEndX - fadeOutStartX);
self.gfx.alpha = targetAlpha * fadeInProgress;
if (fadeInProgress >= 1) {
self.hasFadedIn = true;
}
} else if (self.hasFadedIn && self.x <= fadeInEndX && self.x >= fadeInStartX) {
// Calculate fade out progress (reversed)
var fadeOutProgress = (fadeInEndX - self.x) / (fadeInEndX - fadeInStartX);
self.gfx.alpha = targetAlpha * (1 - fadeOutProgress);
} else if (self.hasFadedIn && self.x < fadeOutStartX && self.x > fadeInEndX) {
// Maintain full opacity in middle section
self.gfx.alpha = targetAlpha;
}
}
// Check if completely off-screen
var currentWidth = self.gfx.width * self.gfx.scale.x;
if (self.x < -currentWidth || self.x > 2048 + currentWidth) {
self.isDone = true;
}
};
return self;
});
var FeedbackIndicator = Container.expand(function (type) {
var self = Container.call(this);
var indicator = self.attachAsset(type + 'Indicator', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
self.show = function () {
indicator.alpha = 1;
indicator.scaleX = 0.5;
indicator.scaleY = 0.5;
tween(indicator, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 1400,
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';
// Randomly pick from the three shallowFish assets if type is shallow
if (type === 'shallow') {
var shallowFishAssets = ['shallowFish', 'shallowFish2', 'shallowFish3'];
assetName = shallowFishAssets[Math.floor(Math.random() * shallowFishAssets.length)];
}
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.missed = false; // True if the fish passed the hook without being caught
self.lastX = 0; // Stores the x position from the previous frame for miss detection
self.isSpecial = type === 'rare'; // For shimmer effect
self.shimmerTime = 0;
// Bubble properties
self.lastBubbleSpawnTime = 0;
self.bubbleSpawnInterval = 120 + Math.random() * 80; // 120-200ms interval (fewer bubbles)
// Add properties for swimming animation
self.swimTime = Math.random() * Math.PI * 2; // Random starting phase for variety
self.baseY = self.y; // Store initial Y position
self.scaleTime = 0;
self.baseScale = 1;
self.update = function () {
if (!self.caught) {
// Horizontal movement
self.x += self.speed;
// Sine wave vertical movement
self.swimTime += 0.08; // Speed of sine wave oscillation
var swimAmplitude = 15; // Pixels of vertical movement
self.y = self.baseY + Math.sin(self.swimTime) * swimAmplitude;
// Beat-synchronized scale pulsing
if (GameState.gameActive && GameState.songStartTime > 0) {
var currentTime = LK.ticks * (1000 / 60);
var songConfig = GameState.getCurrentSongConfig();
var beatInterval = 60000 / songConfig.bpm;
var timeSinceLastBeat = (currentTime - GameState.songStartTime) % beatInterval;
var beatProgress = timeSinceLastBeat / beatInterval;
// Create a pulse effect that peaks at the beat
var scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15; // 15% scale variation
// Determine base scaleX considering direction
var baseScaleXDirection = (self.speed > 0 ? -1 : 1) * self.baseScale;
self.fishGraphics.scaleX = baseScaleXDirection * scalePulse;
self.fishGraphics.scaleY = scalePulse * self.baseScale;
}
if (self.isSpecial) {
// Shimmer effect for rare fish
self.shimmerTime += 0.1;
self.fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2;
} else {
// Reset alpha if not special
self.fishGraphics.alpha = 1.0;
}
}
};
self.catchFish = function () {
self.caught = true;
// Animation: Fish arcs up over the boat, then down into it.
var currentFishX = self.x;
var currentFishY = self.y;
var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X;
var boatLandingY = GAME_CONFIG.BOAT_Y; // Y-coordinate where the fish "lands" in the boat
// Define the peak of the arc - e.g., 150 pixels above the boat's landing spot
var peakArcY = boatLandingY - 150;
// Define the X coordinate at the peak of the arc - halfway between current X and boat center X
var peakArcX = currentFishX + (boatCenterX - currentFishX) * 0.5;
var durationPhase1 = 350; // Duration for the first part of the arc (upwards)
var durationPhase2 = 250; // Duration for the second part of the arc (downwards), total 600ms
// Phase 1: Arc upwards and towards the boat's horizontal center.
// The container 'self' is tweened for position, scale, and alpha.
// Initial self.scale.x and self.scale.y are expected to be 1 for the container.
tween(self, {
x: peakArcX,
y: peakArcY,
scaleX: 0.75,
// Scale container to 75% of its original size
scaleY: 0.75,
alpha: 0.8 // Partially fade
}, {
duration: durationPhase1,
easing: tween.easeOut,
// Ease out for the upward motion to the peak
onFinish: function onFinish() {
// Phase 2: Arc downwards into the boat and disappear.
tween(self, {
x: boatCenterX,
y: boatLandingY,
scaleX: 0.2,
// Shrink container further to 20% of its original size
scaleY: 0.2,
alpha: 0 // Fade out completely
}, {
duration: durationPhase2,
easing: tween.easeIn,
// Ease in for the downward motion into the boat
onFinish: function onFinish() {
self.destroy(); // Remove fish once animation is complete
}
});
}
});
};
return self;
});
var MusicNoteParticle = Container.expand(function (startX, startY) {
var self = Container.call(this);
var FADE_IN_DURATION_MS = 600; // Duration for the note to fade in
var TARGET_ALPHA = 0.6 + Math.random() * 0.4; // Target alpha between 0.6 and 1.0 for variety
self.gfx = self.attachAsset('musicnote', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
// Start invisible for fade-in
scaleX: 0.4 + Math.random() * 0.4 // Random initial scale (0.4x to 0.8x)
});
self.gfx.scaleY = self.gfx.scaleX; // Maintain aspect ratio
self.x = startX;
self.y = startY;
// Animation properties for lazy floating
self.vx = (Math.random() - 0.5) * 0.8; // Slow horizontal drift speed
self.vy = -(0.8 + Math.random() * 0.7); // Steady upward speed (0.8 to 1.5 pixels/frame)
self.rotationSpeed = (Math.random() - 0.5) * 0.008; // Very slow rotation
self.life = 240 + Math.random() * 120; // Lifespan in frames (4 to 6 seconds)
self.age = 0;
self.isDone = false;
// Initial fade-in tween
tween(self.gfx, {
alpha: TARGET_ALPHA
}, {
duration: FADE_IN_DURATION_MS,
easing: tween.easeOut
});
self.update = function () {
if (self.isDone) {
return;
}
self.age++;
self.x += self.vx;
self.y += self.vy;
self.gfx.rotation += self.rotationSpeed;
var FADE_IN_TICKS = FADE_IN_DURATION_MS / (1000 / 60); // Fade-in duration in ticks
// Only manage alpha manually after the fade-in tween is expected to be complete.
if (self.age > FADE_IN_TICKS) {
var lifePortionForFadeOut = 0.6; // Use last 60% of life for fade out
var fadeOutStartTimeTicks = self.life * (1 - lifePortionForFadeOut);
if (self.age >= fadeOutStartTimeTicks && self.life > fadeOutStartTimeTicks) {
// ensure self.life > fadeOutStartTimeTicks to avoid division by zero
var progressInFadeOut = (self.age - fadeOutStartTimeTicks) / (self.life * lifePortionForFadeOut);
self.gfx.alpha = TARGET_ALPHA * (1 - progressInFadeOut);
self.gfx.alpha = Math.max(0, self.gfx.alpha); // Clamp at 0
} else if (self.age <= fadeOutStartTimeTicks) {
// At this point, the initial fade-in tween to TARGET_ALPHA should have completed.
// The alpha value is expected to remain at TARGET_ALPHA (as set by the initial tween)
// until the fade-out logic (in the 'if (self.age >= fadeOutStartTimeTicks)' block) begins.
// The previous 'tween.isTweening' check was removed as the method does not exist in the plugin.
}
}
// Check if particle's life is over or it has faded out
if (self.age >= self.life || self.gfx.alpha !== undefined && self.gfx.alpha <= 0.01 && self.age > FADE_IN_TICKS) {
self.isDone = true;
}
};
return self;
});
var OceanBubbleParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('oceanbubbles', {
anchorX: 0.5,
anchorY: 0.5
});
self.initialX = Math.random() * 2048;
var waterTop = GAME_CONFIG.WATER_SURFACE_Y;
var waterBottom = 2732;
self.x = self.initialX;
// Allow bubbles to spawn anywhere in the water, not just below the bottom
self.y = waterTop + Math.random() * (waterBottom - waterTop);
var baseScale = 0.1 + Math.random() * 0.4; // Scale: 0.1x to 0.5x
self.gfx.scale.set(baseScale);
self.vy = -(0.25 + Math.random() * 0.5); // Upward speed: 0.25 to 0.75 pixels/frame (slower, less variance)
self.naturalVy = self.vy; // Store natural velocity for recovery
self.driftAmplitude = 20 + Math.random() * 40; // Sideways drift: 20px to 60px amplitude
self.naturalDriftAmplitude = self.driftAmplitude; // Store natural drift for recovery
self.driftFrequency = (0.005 + Math.random() * 0.015) * (Math.random() < 0.5 ? 1 : -1); // Sideways drift speed/direction
self.driftPhase = Math.random() * Math.PI * 2; // Initial phase for sine wave
self.rotationSpeed = (Math.random() - 0.5) * 0.01; // Slow random rotation
var targetAlpha = 0.2 + Math.random() * 0.3; // Max alpha: 0.2 to 0.5 (dimmer background bubbles)
self.gfx.alpha = 0; // Start transparent
self.isDone = false;
self.fadingOut = false;
tween(self.gfx, {
alpha: targetAlpha
}, {
duration: 1000 + Math.random() * 1000,
// Slow fade in: 1 to 2 seconds
easing: tween.easeIn
});
self.update = function () {
if (self.isDone) {
return;
}
self.y += self.vy;
// Increment age
self.age++;
// Check if lifespan exceeded
if (!self.fadingOut && self.age >= self.lifespan) {
self.fadingOut = true;
tween.stop(self.gfx);
tween(self.gfx, {
alpha: 0
}, {
duration: 600 + Math.random() * 400,
// 0.6-1 second fade
easing: tween.easeOut,
onFinish: function onFinish() {
self.isDone = true;
}
});
}
self.driftPhase += self.driftFrequency;
self.x = self.initialX + Math.sin(self.driftPhase) * self.driftAmplitude;
self.gfx.rotation += self.rotationSpeed;
// Recovery mechanism: gradually return to natural upward movement
var naturalVy = -(0.25 + Math.random() * 0.5); // Natural upward speed
var recoveryRate = 0.02; // How quickly bubble recovers (2% per frame)
// If bubble is moving slower than its natural speed or downward, recover
if (self.vy > naturalVy) {
self.vy = self.vy + (naturalVy - self.vy) * recoveryRate;
}
// Also gradually reduce excessive drift amplitude back to normal
var normalDriftAmplitude = 20 + Math.random() * 40;
if (self.driftAmplitude > normalDriftAmplitude) {
self.driftAmplitude = self.driftAmplitude + (normalDriftAmplitude - self.driftAmplitude) * recoveryRate;
}
// Check if bubble reached surface or went off-screen
// Use gfx.height * current scale for accurate boundary check
var currentHeight = self.gfx.height * self.gfx.scale.y;
var currentWidth = self.gfx.width * self.gfx.scale.x;
if (!self.fadingOut && self.y <= GAME_CONFIG.WATER_SURFACE_Y - currentHeight * 0.5) {
self.fadingOut = true;
tween.stop(self.gfx); // Stop fade-in if ongoing
tween(self.gfx, {
alpha: 0
}, {
duration: 300 + Math.random() * 200,
// Quick fade out
easing: tween.easeOut,
onFinish: function onFinish() {
self.isDone = true; // Mark as fully done for removal
}
});
} else if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth)) {
// Off screen top/sides before reaching surface and starting fade
self.isDone = true;
self.gfx.alpha = 0; // Disappear immediately
}
};
return self;
});
var SeaweedParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('kelp', {
anchorX: 0.5,
anchorY: 0.5
});
// Determine spawn location
var spawnType = Math.random();
var waterTop = GAME_CONFIG.WATER_SURFACE_Y;
var waterBottom = 2732;
if (spawnType < 0.4) {
// 40% spawn from bottom
self.x = Math.random() * 2048;
self.y = waterBottom + 50;
self.vx = (Math.random() - 0.5) * 0.3; // Slight horizontal drift
self.vy = -(0.4 + Math.random() * 0.3); // Upward movement
} else if (spawnType < 0.7) {
// 30% spawn from left
self.x = -50;
self.y = waterTop + Math.random() * (waterBottom - waterTop);
self.vx = 0.4 + Math.random() * 0.3; // Rightward movement
self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift
} else {
// 30% spawn from right
self.x = 2048 + 50;
self.y = waterTop + Math.random() * (waterBottom - waterTop);
self.vx = -(0.4 + Math.random() * 0.3); // Leftward movement
self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift
}
self.initialX = self.x;
self.naturalVx = self.vx; // Store natural velocity for recovery
self.naturalVy = self.vy;
// Seaweed properties
var baseScale = 0.6 + Math.random() * 0.6; // Scale: 0.6x to 1.2x
self.gfx.scale.set(baseScale);
self.swayAmplitude = 15 + Math.random() * 25; // Sway: 15px to 40px
self.swayFrequency = (0.003 + Math.random() * 0.007) * (Math.random() < 0.5 ? 1 : -1);
self.swayPhase = Math.random() * Math.PI * 2;
// Random initial rotation (full 360 degrees)
self.gfx.rotation = Math.random() * Math.PI * 2;
// Random continuous rotation speed (slower than ocean bubbles)
self.continuousRotationSpeed = (Math.random() - 0.5) * 0.003; // -0.0015 to 0.0015 radians per frame
var targetAlpha = 0.3 + Math.random() * 0.3; // Alpha: 0.3 to 0.6
self.gfx.alpha = 0; // Start transparent
self.isDone = false;
self.fadingOut = false;
self.reachedSurface = false;
// Add random lifespan (10-30 seconds)
self.lifespan = 600 + Math.random() * 1200; // 600-1800 frames (10-30 seconds at 60fps)
self.age = 0;
tween(self.gfx, {
alpha: targetAlpha
}, {
duration: 1500 + Math.random() * 1000,
easing: tween.easeIn
});
self.update = function () {
if (self.isDone) {
return;
}
// Apply movement
self.x += self.vx;
self.y += self.vy;
// Add sway effect
self.swayPhase += self.swayFrequency;
var swayOffset = Math.sin(self.swayPhase) * self.swayAmplitude;
// Apply continuous rotation plus sway-based rotation
self.gfx.rotation += self.continuousRotationSpeed + swayOffset * 0.0001; // Reduced sway rotation influence
// Recovery mechanism for velocity
var recoveryRate = 0.015;
if (self.vx !== self.naturalVx) {
self.vx = self.vx + (self.naturalVx - self.vx) * recoveryRate;
}
if (self.vy !== self.naturalVy) {
self.vy = self.vy + (self.naturalVy - self.vy) * recoveryRate;
}
// Check if reached surface
var currentHeight = self.gfx.height * self.gfx.scale.y;
var currentWidth = self.gfx.width * self.gfx.scale.x;
if (!self.reachedSurface && self.y <= GAME_CONFIG.WATER_SURFACE_Y + currentHeight * 0.3) {
self.reachedSurface = true;
// Change to horizontal drift at surface
self.vy = 0;
self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.5 + Math.random() * 0.5);
self.naturalVx = self.vx;
self.naturalVy = 0;
}
// Check if off-screen
if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth || self.y > waterBottom + currentHeight)) {
self.fadingOut = true;
tween.stop(self.gfx);
tween(self.gfx, {
alpha: 0
}, {
duration: 400 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isDone = true;
}
});
}
};
return self;
});
/****
* Initialize Game
****/
/****
* Screen Containers
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
// Constants for Title Screen Animation
var TITLE_ANIM_CONSTANTS = {
INITIAL_GROUP_ALPHA: 0,
FINAL_GROUP_ALPHA: 1,
INITIAL_UI_ALPHA: 0,
FINAL_UI_ALPHA: 1,
INITIAL_GROUP_SCALE: 3.5,
// Start with an extreme closeup
FINAL_GROUP_SCALE: 2.8,
// Zoom out slightly, staying closer
GROUP_ANIM_DURATION: 4000,
// Slower duration for group fade-in and zoom
TEXT_FADE_DURATION: 1000,
// Duration for title text fade-in
BUTTON_FADE_DURATION: 800,
// Duration for buttons fade-in
// Positioning constants relative to titleAnimationGroup's origin (0,0) which is boat's center
BOAT_ANCHOR_X: 0.5,
BOAT_ANCHOR_Y: 0.5,
FISHERMAN_ANCHOR_X: 0.5,
FISHERMAN_ANCHOR_Y: 0.9,
// Anchor at feet
FISHERMAN_X_OFFSET: -20,
// Relative to boat center
FISHERMAN_Y_OFFSET: -100,
// Relative to boat center, fisherman sits on boat
LINE_ANCHOR_X: 0.5,
LINE_ANCHOR_Y: 0,
// Anchor at top of line
LINE_X_OFFSET_FROM_FISHERMAN: 70,
// Rod tip X from fisherman center
LINE_Y_OFFSET_FROM_FISHERMAN: -130,
// Rod tip Y from fisherman center (fisherman height ~200, anchorY 0.9)
HOOK_ANCHOR_X: 0.5,
HOOK_ANCHOR_Y: 0.5,
HOOK_Y_DEPTH_FROM_LINE_START: 700,
// Slightly longer line for the closeup
// titleAnimationGroup positioning
GROUP_PIVOT_X: 0,
// Boat's X in the group
GROUP_PIVOT_Y: 0,
// Boat's Y in the group
GROUP_INITIAL_Y_SCREEN_OFFSET: -450 // Adjusted for closer initial zoom
};
// If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up.
/****
* Pattern Generation System
****/
var PatternGenerator = {
lastLane: -1,
minDistanceBetweenFish: 300,
// Used by spawnFish internal check
// Minimum X distance between fish for visual clarity on hook
lastActualSpawnTime: -100000,
// Time of the last actual fish spawn
getNextLane: function getNextLane() {
if (this.lastLane === -1) {
// First fish, start in middle lane
this.lastLane = 1;
return 1;
}
// Prefer staying in same lane or moving to adjacent lane
var possibleLanes = [this.lastLane];
// Add adjacent lanes
if (this.lastLane > 0) {
possibleLanes.push(this.lastLane - 1);
}
if (this.lastLane < 2) {
possibleLanes.push(this.lastLane + 1);
}
// 70% chance to stay in same/adjacent lane
if (Math.random() < 0.7) {
this.lastLane = possibleLanes[Math.floor(Math.random() * possibleLanes.length)];
} else {
// 30% chance for any lane
this.lastLane = Math.floor(Math.random() * 3);
}
return this.lastLane;
},
// New method: Checks if enough time has passed since the last spawn.
canSpawnFishOnBeat: function canSpawnFishOnBeat(currentTime, configuredSpawnInterval) {
var timeSinceLast = currentTime - this.lastActualSpawnTime;
var minRequiredGap = configuredSpawnInterval; // Default gap is the song's beat interval for fish
return timeSinceLast >= minRequiredGap;
},
// New method: Registers details of the fish that was just spawned.
registerFishSpawn: function registerFishSpawn(spawnTime) {
this.lastActualSpawnTime = spawnTime;
},
reset: function reset() {
this.lastLane = -1;
this.lastActualSpawnTime = -100000; // Set far in the past to allow first spawn
}
};
/****
* Game Configuration
****/
game.up = function (x, y, obj) {
// Note: We don't play buttonClick sound on 'up' typically, only on 'down'.
switch (GameState.currentScreen) {
case 'title':
// title screen up actions (if any)
break;
case 'levelSelect':
// level select screen up actions (if any, usually 'down' is enough for buttons)
break;
case 'fishing':
handleFishingInput(x, y, false); // false for isUp
break;
case 'results':
// results screen up actions (if any)
break;
}
};
var GAME_CONFIG = {
SCREEN_CENTER_X: 1024,
SCREEN_CENTER_Y: 900,
// Adjusted, though less critical with lanes
BOAT_Y: 710,
// Original 300 + 273 (10%) + 137 (5%) = 710
WATER_SURFACE_Y: 760,
// Original 350 + 273 (10%) + 137 (5%) = 760
// 3 Lane System
// Y-positions for each lane, adjusted downwards further
LANES: [{
y: 1133,
// Top lane Y: (723 + 273) + 137 = 996 + 137 = 1133
name: "shallow"
}, {
y: 1776,
// Middle lane Y: (996 + 643) + 137 = 1639 + 137 = 1776
name: "medium"
}, {
y: 2419,
// Bottom lane Y: (1639 + 643) + 137 = 2282 + 137 = 2419
name: "deep"
}],
// Timing windows
PERFECT_WINDOW: 40,
GOOD_WINDOW: 80,
MISS_WINDOW: 120,
// Depth levels - reduced money values!
DEPTHS: [{
level: 1,
name: "Shallow Waters",
fishSpeed: 6,
fishValue: 0.5,
upgradeCost: 0,
// Starting depth
songs: [{
name: "Gentle Waves",
bpm: 90,
// Changed from 95 to 90
duration: 202000,
// 3:22
pattern: "gentle_waves_custom",
cost: 0
}, {
name: "Morning Tide",
bpm: 90,
duration: 156827,
pattern: "morning_tide_custom",
cost: 0,
musicId: 'morningtide'
}]
}, {
level: 2,
name: "Mid Waters",
fishSpeed: 7,
fishValue: 1.5,
upgradeCost: 100,
songs: [{
name: "Ocean Current",
bpm: 120,
duration: 90000,
pattern: "medium",
cost: 0
}, {
name: "Deep Flow",
bpm: 125,
duration: 100000,
pattern: "medium",
cost: 150
}]
}, {
level: 3,
name: "Deep Waters",
fishSpeed: 8,
fishValue: 3,
upgradeCost: 400,
songs: [{
name: "Storm Surge",
bpm: 140,
duration: 120000,
pattern: "complex",
cost: 0
}, {
name: "Whirlpool",
bpm: 150,
duration: 135000,
pattern: "complex",
cost: 300
}]
}, {
level: 4,
name: "Abyss",
fishSpeed: 9,
fishValue: 6,
upgradeCost: 1000,
songs: [{
name: "Leviathan",
bpm: 160,
duration: 150000,
pattern: "expert",
cost: 0
}, {
name: "Deep Trench",
bpm: 170,
duration: 180000,
pattern: "expert",
cost: 600
}]
}],
// Updated patterns
PATTERNS: {
simple: {
beatsPerFish: 2,
// Increased from 1 - fish every 2 beats
doubleSpawnChance: 0.10,
// 10% chance of a double beat in simple
rareSpawnChance: 0.02
},
medium: {
beatsPerFish: 1.5,
// Increased from 0.75
doubleSpawnChance: 0.15,
//{1R} // 15% chance of a double beat
rareSpawnChance: 0.05
},
complex: {
beatsPerFish: 1,
// Increased from 0.5
doubleSpawnChance: 0.25,
//{1U} // 25% chance of a double beat
rareSpawnChance: 0.08
},
expert: {
beatsPerFish: 0.75,
// Increased from 0.25
doubleSpawnChance: 0.35,
//{1X} // 35% chance of a double beat
tripletSpawnChance: 0.20,
// 20% chance a double beat becomes a triplet
rareSpawnChance: 0.12
},
gentle_waves_custom: {
beatsPerFish: 1.5,
// Slightly more frequent than default simple
doubleSpawnChance: 0.05,
// Very rare double beats for shallow
rareSpawnChance: 0.01,
// Almost no rare fish
// Custom timing sections based on the musical structure
sections: [
// Opening - Simple chord pattern (0-30 seconds)
{
startTime: 0,
endTime: 30000,
spawnModifier: 1.0,
// Normal spawn rate
description: "steady_chords"
},
// Melody Introduction (30-60 seconds)
{
startTime: 30000,
endTime: 60000,
spawnModifier: 0.9,
// Slightly fewer fish
description: "simple_melody"
},
// Development (60-120 seconds) - Gets a bit busier
{
startTime: 60000,
endTime: 120000,
spawnModifier: 1.1,
// Slightly more fish
description: "melody_development"
},
// Climax (120-180 seconds) - Busiest section but still shallow
{
startTime: 120000,
endTime: 180000,
spawnModifier: 1.3,
// More fish, but not overwhelming
description: "gentle_climax"
},
// Ending (180-202 seconds) - Calming down
{
startTime: 180000,
endTime: 202000,
spawnModifier: 0.8,
// Fewer fish for gentle ending
description: "peaceful_ending"
}]
},
morning_tide_custom: {
beatsPerFish: 1.2,
// More frequent than gentle_waves_custom (1.5) and much more than simple (2)
doubleSpawnChance: 0.12,
// Higher than simple (0.10)
rareSpawnChance: 0.03,
// Higher than simple (0.02)
// Custom timing sections based on musical structure
sections: [
// Gentle opening - ease into the song (0-25 seconds)
{
startTime: 0,
endTime: 25000,
spawnModifier: 0.9,
// Slightly reduced for intro
description: "calm_opening"
},
// Building energy - first wave (25-50 seconds)
{
startTime: 25000,
endTime: 50000,
spawnModifier: 1.2,
// More active
description: "first_wave"
},
// Peak intensity - morning rush (50-80 seconds)
{
startTime: 50000,
endTime: 80000,
spawnModifier: 1.5,
// Most intense section
description: "morning_rush"
},
// Sustained energy - second wave (80-110 seconds)
{
startTime: 80000,
endTime: 110000,
spawnModifier: 1.3,
// High but slightly less than peak
description: "second_wave"
},
// Climactic finish (110-140 seconds)
{
startTime: 110000,
endTime: 140000,
spawnModifier: 1.4,
// Building back up
description: "climactic_finish"
},
// Gentle fade out (140-156.8 seconds)
{
startTime: 140000,
endTime: 156827,
spawnModifier: 0.8,
// Calm ending
description: "peaceful_fade"
}]
}
}
};
/****
* Game State Management
****/
var MULTI_BEAT_SPAWN_DELAY_MS = 250; // ms delay for sequential spawns in multi-beats (increased for more space)
var TRIPLET_BEAT_SPAWN_DELAY_MS = 350; // ms delay for third fish in a triplet (even more space)
var FISH_SPAWN_END_BUFFER_MS = 2500; // Adjusted: This buffer ensures the last fish is scheduled to arrive this many milliseconds before the song's actual end, giving the player time to catch it while reducing empty time at the level's conclusion.
var ImprovedRhythmSpawner = {
nextBeatToSchedule: 1,
scheduledBeats: [],
update: function update(currentTime) {
if (!GameState.gameActive || GameState.songStartTime === 0) {
return;
}
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
var beatInterval = 60000 / songConfig.bpm;
var spawnInterval = beatInterval * pattern.beatsPerFish;
// Calculate how far ahead to look (use original logic)
var depthConfig = GameState.getCurrentDepthConfig();
var fishSpeed = Math.abs(depthConfig.fishSpeed);
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
var travelTimeMs = distanceToHook / fishSpeed * (1000 / 60);
var beatsAhead = Math.ceil(travelTimeMs / spawnInterval) + 2;
// Schedule beats ahead
var songElapsed = currentTime - GameState.songStartTime;
var currentSongBeat = songElapsed / spawnInterval;
var maxBeatToSchedule = Math.floor(currentSongBeat) + beatsAhead;
while (this.nextBeatToSchedule <= maxBeatToSchedule) {
if (this.scheduledBeats.indexOf(this.nextBeatToSchedule) === -1) {
this.scheduleBeatFish(this.nextBeatToSchedule, spawnInterval, travelTimeMs);
this.scheduledBeats.push(this.nextBeatToSchedule);
}
this.nextBeatToSchedule++;
}
// NO FISH SYNCING - let them move naturally!
},
scheduleBeatFish: function scheduleBeatFish(beatNumber, spawnInterval, travelTimeMs) {
var targetArrivalTime = GameState.songStartTime + beatNumber * spawnInterval;
var songConfig = GameState.getCurrentSongConfig(); // Get current song config
// Ensure this fish doesn't arrive too close to the actual end of the song
if (songConfig && GameState.songStartTime > 0 && targetArrivalTime > songConfig.duration - FISH_SPAWN_END_BUFFER_MS) {
return; // Don't schedule this fish, it's too late
}
var spawnTime = targetArrivalTime - travelTimeMs;
var currentTime = LK.ticks * (1000 / 60);
if (spawnTime >= currentTime - 100) {
var delay = Math.max(0, spawnTime - currentTime);
var self = this;
LK.setTimeout(function () {
if (GameState.gameActive && GameState.songStartTime !== 0) {
self.spawnRhythmFish(beatNumber, targetArrivalTime);
}
}, delay);
}
},
spawnRhythmFish: function spawnRhythmFish(beatNumber, targetArrivalTime) {
if (!GameState.gameActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60);
var depthConfig = GameState.getCurrentDepthConfig();
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
// Calculate spawnInterval for this pattern
var beatInterval = 60000 / songConfig.bpm;
var spawnInterval = beatInterval * pattern.beatsPerFish;
// Apply section modifiers if pattern has sections
var spawnModifier = 1.0;
if (pattern.sections) {
var songElapsed = currentTime - GameState.songStartTime;
for (var s = 0; s < pattern.sections.length; s++) {
var section = pattern.sections[s];
if (songElapsed >= section.startTime && songElapsed <= section.endTime) {
spawnModifier = section.spawnModifier;
break;
}
}
}
// Check if we should spawn
if (!PatternGenerator.canSpawnFishOnBeat(currentTime, beatInterval)) {
return;
}
// Apply spawn modifier chance
if (Math.random() > spawnModifier) {
return;
}
var laneIndex = PatternGenerator.getNextLane();
var targetLane = GAME_CONFIG.LANES[laneIndex];
// Fish type selection (original logic)
var fishType, fishValue;
var rand = Math.random();
if (rand < pattern.rareSpawnChance) {
fishType = 'rare';
fishValue = depthConfig.fishValue * 4;
} else if (GameState.selectedDepth >= 2 && rand < 0.3) {
fishType = 'deep';
fishValue = Math.floor(depthConfig.fishValue * 2);
} else if (GameState.selectedDepth >= 1 && rand < 0.6) {
fishType = 'medium';
fishValue = Math.floor(depthConfig.fishValue * 1.5);
} else {
fishType = 'shallow';
fishValue = depthConfig.fishValue;
}
// Calculate precise speed to arrive exactly on beat
var timeRemainingMs = targetArrivalTime - currentTime;
if (timeRemainingMs <= 0) {
return;
}
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
var spawnSide = Math.random() < 0.5 ? -1 : 1;
// Calculate frames remaining and required speed per frame
var framesRemaining = timeRemainingMs / (1000 / 60);
if (framesRemaining <= 0) {
return;
}
var requiredSpeedPerFrame = distanceToHook / framesRemaining;
if (spawnSide === 1) {
requiredSpeedPerFrame *= -1; // Moving left
}
// Create fish with exact calculated speed - NO SYNC DATA
var newFish = new Fish(fishType, fishValue, requiredSpeedPerFrame, laneIndex);
newFish.spawnSide = spawnSide;
newFish.targetArrivalTime = targetArrivalTime;
newFish.x = requiredSpeedPerFrame > 0 ? -150 : 2048 + 150;
newFish.y = targetLane.y;
newFish.baseY = targetLane.y;
newFish.lastX = newFish.x; // Initialize lastX for miss detection
// newFish.missed is false by default from constructor
// Don't add any syncData - let fish move naturally!
fishArray.push(newFish);
fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTime);
// Handle multi-spawns
this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime);
return newFish;
},
handleMultiSpawns: function handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime) {
if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) {
var travelTimeMs = this.calculateTravelTime();
var nextFishBeat = beatNumber + 1;
if (this.scheduledBeats.indexOf(nextFishBeat) === -1) {
this.scheduleBeatFish(nextFishBeat, spawnInterval, travelTimeMs);
this.scheduledBeats.push(nextFishBeat);
}
// Handle triplets
if (pattern.tripletSpawnChance && pattern.tripletSpawnChance > 0 && Math.random() < pattern.tripletSpawnChance) {
var thirdFishBeat = beatNumber + 2;
if (this.scheduledBeats.indexOf(thirdFishBeat) === -1) {
this.scheduleBeatFish(thirdFishBeat, spawnInterval, travelTimeMs);
this.scheduledBeats.push(thirdFishBeat);
}
}
}
},
calculateTravelTime: function calculateTravelTime() {
var depthConfig = GameState.getCurrentDepthConfig();
var fishSpeed = Math.abs(depthConfig.fishSpeed);
if (fishSpeed === 0) {
return Infinity;
}
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
return distanceToHook / fishSpeed * (1000 / 60);
},
reset: function reset() {
this.nextBeatToSchedule = 1;
this.scheduledBeats = [];
},
getDebugInfo: function getDebugInfo() {
return {
nextBeat: this.nextBeatToSchedule,
scheduledCount: this.scheduledBeats.length,
scheduledBeatsPreview: this.scheduledBeats.slice(-10)
};
}
};
/****
* Tutorial System - Global Scope
****/
function updateLaneBracketsVisuals() {
if (laneBrackets && laneBrackets.length === GAME_CONFIG.LANES.length) {
for (var i = 0; i < laneBrackets.length; i++) {
var isActiveLane = i === GameState.hookTargetLaneIndex;
var targetAlpha = isActiveLane ? 0.9 : 0.5;
if (laneBrackets[i] && laneBrackets[i].left && !laneBrackets[i].left.destroyed) {
if (laneBrackets[i].left.alpha !== targetAlpha) {
laneBrackets[i].left.alpha = targetAlpha;
}
}
if (laneBrackets[i] && laneBrackets[i].right && !laneBrackets[i].right.destroyed) {
if (laneBrackets[i].right.alpha !== targetAlpha) {
laneBrackets[i].right.alpha = targetAlpha;
}
}
}
}
}
function createTutorialElements() {
tutorialOverlayContainer.removeChildren(); // Clear previous elements
tutorialTextBackground = tutorialOverlayContainer.addChild(LK.getAsset('screenBackground', {
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 2732 * 0.85,
// Position towards the bottom
width: 1800,
height: 350,
color: 0x000000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.75
}));
tutorialTextDisplay = tutorialOverlayContainer.addChild(new Text2('', {
size: 45,
fill: 0xFFFFFF,
wordWrap: true,
wordWrapWidth: 1700,
// Slightly less than background width
align: 'center',
lineHeight: 55
}));
tutorialTextDisplay.anchor.set(0.5, 0.5);
tutorialTextDisplay.x = tutorialTextBackground.x;
tutorialTextDisplay.y = tutorialTextBackground.y - 40; // Adjust Y to be within the new background
tutorialContinueButton = tutorialOverlayContainer.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: tutorialTextBackground.x,
y: tutorialTextBackground.y + tutorialTextBackground.height / 2 - 65,
// Positioned at the bottom of the text background
tint: 0x1976d2,
// Blue color
width: 350,
// Standard button size
height: 70
}));
tutorialContinueText = tutorialOverlayContainer.addChild(new Text2('CONTINUE', {
// Changed text
size: 34,
fill: 0xFFFFFF
}));
tutorialContinueText.anchor.set(0.5, 0.5);
tutorialContinueText.x = tutorialContinueButton.x;
tutorialContinueText.y = tutorialContinueButton.y;
tutorialOverlayContainer.visible = false; // Initially hidden
}
function setTutorialText(newText, showContinue) {
if (showContinue === undefined) {
showContinue = true;
}
if (!tutorialTextDisplay || !tutorialContinueButton || !tutorialContinueText) {
createTutorialElements(); // Ensure elements exist
}
tutorialTextDisplay.setText(newText);
tutorialContinueButton.visible = showContinue;
tutorialContinueText.visible = showContinue;
tutorialOverlayContainer.visible = true;
}
function spawnTutorialFishHelper(config) {
var fishType = config.type || 'shallow';
// Use a consistent depth config for tutorial fish, e.g., shallowest
var depthConfig = GAME_CONFIG.DEPTHS[0];
var fishValue = depthConfig.fishValue / 2; // Tutorial fish might be worth less or nothing
var baseSpeed = depthConfig.fishSpeed;
var speedMultiplier = config.speedMultiplier || 0.5; // Slower for tutorial
var laneIndex = config.lane !== undefined ? config.lane : 1;
var spawnSide = config.spawnSide !== undefined ? config.spawnSide : Math.random() < 0.5 ? -1 : 1;
var actualFishSpeed = Math.abs(baseSpeed) * speedMultiplier * spawnSide;
var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex);
newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen
newFish.y = GAME_CONFIG.LANES[laneIndex].y + (config.yOffset || 0);
newFish.baseY = newFish.y;
newFish.lastX = newFish.x;
newFish.tutorialFish = true; // Custom flag
fishArray.push(newFish); // Add to global fishArray for rendering by main loop
fishingScreen.addChild(newFish); // Add to fishingScreen for visibility
return newFish;
}
function runTutorialStep() {
GameState.tutorialPaused = false;
GameState.tutorialAwaitingTap = false; // Will be set by steps if needed for "tap screen"
if (tutorialContinueButton) {
tutorialContinueButton.visible = true;
} // Default to visible
if (tutorialContinueText) {
tutorialContinueText.visible = true;
}
// Clear previous tutorial fish unless current step needs it
if (GameState.tutorialFish && GameState.tutorialStep !== 2 && GameState.tutorialStep !== 3) {
if (!GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish = null;
}
// Ensure fishing screen elements are visually active (boat, water animations)
// but game logic (spawner, score) is controlled by tutorial state.
if (fishingElements) {
if (typeof fishingElements.startWaterSurfaceAnimation === 'function' && GameState.tutorialStep < 7) {
fishingElements.startWaterSurfaceAnimation();
}
if (typeof fishingElements.startBoatAndFishermanAnimation === 'function' && GameState.tutorialStep < 7) {
fishingElements.startBoatAndFishermanAnimation();
}
if (fishingElements.hook && GameState.tutorialStep < 7) {
fishingElements.hook.y = GAME_CONFIG.LANES[1].y; // Reset hook
GameState.hookTargetLaneIndex = 1;
}
}
switch (GameState.tutorialStep) {
case 0:
// Welcome
setTutorialText("Welcome to Beat Fisher! Let's learn the basics. Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 1:
// The Hook & Lanes
setTutorialText("This is your hook. Fish swim in three lanes. The hook automatically follows the closest fish. Tap 'CONTINUE'.");
if (fishingElements.hook && !fishingElements.hook.destroyed) {
tween(fishingElements.hook.scale, {
x: 1.2,
y: 1.2
}, {
duration: 250,
onFinish: function onFinish() {
if (fishingElements.hook && !fishingElements.hook.destroyed) {
tween(fishingElements.hook.scale, {
x: 1,
y: 1
}, {
duration: 250
});
}
}
});
}
GameState.tutorialPaused = true;
break;
case 2:
// Fish Approaching & First Catch Attempt
setTutorialText("A fish will now approach. When it's under the hook, TAP THE SCREEN to catch it!");
tutorialContinueButton.visible = false; // No continue button, action is tapping screen
tutorialContinueText.visible = false;
GameState.tutorialPaused = false; // Fish moves
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
GameState.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.35,
lane: 1
});
break;
case 3:
// Perfect/Good Timing (after first successful catch)
setTutorialText("Great! Timing is key. 'Perfect' or 'Good' catches earn more. Try to catch this next fish!");
tutorialContinueButton.visible = false;
tutorialContinueText.visible = false;
GameState.tutorialPaused = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
GameState.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.45,
lane: 1
});
break;
case 4:
// Combos
setTutorialText("Nice one! Catch fish consecutively to build a COMBO for bonus points! Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 5:
// Music & Rhythm
setTutorialText("Fish will approach the hook on the beat with the music's rhythm. Listen to the beat! Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 6:
// End Tutorial
setTutorialText("You're all set! Tap 'CONTINUE' to go to the fishing spots!");
GameState.tutorialPaused = true;
break;
default:
// Tutorial finished
GameState.tutorialMode = false;
tutorialOverlayContainer.visible = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish = null;
}
// Clean up fishing screen elements explicitly if they were started by tutorial
if (fishingElements && fishingElements.boat && !fishingElements.boat.destroyed) {
tween.stop(fishingElements.boat);
}
if (fishingElements && fishingElements.fishermanContainer && !fishingElements.fishermanContainer.destroyed) {
tween.stop(fishingElements.fishermanContainer);
}
// Stop water surface animations
if (fishingElements && fishingElements.waterSurfaceSegments) {
fishingElements.waterSurfaceSegments.forEach(function (segment) {
if (segment && !segment.destroyed) {
tween.stop(segment);
}
});
}
showScreen('levelSelect');
break;
}
}
function startTutorial() {
GameState.tutorialMode = true;
GameState.tutorialStep = 0;
GameState.gameActive = false; // Gameplay logic like score, song progression is off
// Ensure fishing screen is visible and setup for tutorial
showScreen('fishing'); // This will make fishingScreen visible
fishingScreen.alpha = 1; // Ensure it's fully visible
// Clear any active game elements from a previous session
fishArray.forEach(function (f) {
if (f && !f.destroyed) {
f.destroy();
}
});
fishArray = [];
ImprovedRhythmSpawner.reset();
if (fishingElements.scoreText) {
fishingElements.scoreText.setText('');
}
if (fishingElements.fishText) {
fishingElements.fishText.setText('');
}
if (fishingElements.comboText) {
fishingElements.comboText.setText('');
}
if (fishingElements.progressText) {
fishingElements.progressText.setText('');
}
// Hide regular fishing UI if necessary or leave it for context
// For now, let's assume it's okay to see score/combo at 0.
// Don't create tutorial elements here - wait for intro to finish
// createTutorialElements() and runTutorialStep() will be called after intro
}
// This function is called from game.update when tutorialFish exists
function checkTutorialFishState() {
var fish = GameState.tutorialFish;
if (!fish || fish.destroyed || fish.caught || fish.missed) {
return;
}
var hookX = fishingElements.hook.x;
// Fish passes hook without interaction during catch steps
if (GameState.tutorialStep === 2 || GameState.tutorialStep === 3) {
var passedHook = fish.speed > 0 && fish.x > hookX + GAME_CONFIG.MISS_WINDOW + fish.fishGraphics.width / 2 ||
// fish right edge passed hook right boundary
fish.speed < 0 && fish.x < hookX - GAME_CONFIG.MISS_WINDOW - fish.fishGraphics.width / 2; // fish left edge passed hook left boundary
if (passedHook) {
fish.missed = true; // Mark to prevent further attempts on this specific fish instance
GameState.tutorialPaused = true; // Pause to show message
setTutorialText("It got away! Tap 'CONTINUE' to try that part again.");
// Next tap on "CONTINUE" will call runTutorialStep(), which will re-run current step.
}
}
// Generic off-screen removal for tutorial fish
if (fish.x < -250 || fish.x > 2048 + 250) {
var wasCriticalStep = GameState.tutorialStep === 2 || GameState.tutorialStep === 3;
var fishIndex = fishArray.indexOf(fish);
if (fishIndex > -1) {
fishArray.splice(fishIndex, 1);
}
fish.destroy();
GameState.tutorialFish = null;
if (wasCriticalStep && !fish.caught && !fish.missed) {
// If it just went off screen without resolution
GameState.tutorialPaused = true;
setTutorialText("The fish swam off. Tap 'CONTINUE' to try that part again.");
}
}
}
var GameState = {
// Game flow
currentScreen: 'title',
// 'title', 'levelSelect', 'fishing', 'results'
// Player progression
currentDepth: 0,
money: 0,
totalFishCaught: 0,
ownedSongs: [],
// Array of {depth, songIndex} objects
// Level selection
selectedDepth: 0,
selectedSong: 0,
// Current session
sessionScore: 0,
sessionFishCaught: 0,
sessionFishSpawned: 0,
combo: 0,
maxCombo: 0,
// Game state
gameActive: false,
songStartTime: 0,
lastBeatTime: 0,
beatCount: 0,
introPlaying: false,
// Tracks if the intro animation is currently playing
musicNotesActive: false,
// Tracks if music note particle system is active
currentPlayingMusicId: 'rhythmTrack',
// ID of the music track currently playing in a session
currentPlayingMusicInitialVolume: 0.8,
// Initial volume of the current music track for fade reference
hookTargetLaneIndex: 1,
// Start with hook targeting the middle lane (index 1)
// Tutorial State
tutorialMode: false,
// Is the tutorial currently active?
tutorialStep: 0,
// Current step in the tutorial sequence
tutorialPaused: false,
// Is the tutorial paused (e.g., for text display)?
tutorialAwaitingTap: false,
// Is the tutorial waiting for a generic tap to continue?
tutorialFish: null,
// Stores the fish object used in a tutorial step
// Initialize owned songs (first song of each unlocked depth is free)
initOwnedSongs: function initOwnedSongs() {
this.ownedSongs = [];
for (var i = 0; i <= this.currentDepth; i++) {
this.ownedSongs.push({
depth: i,
songIndex: 0
});
}
},
hasSong: function hasSong(depth, songIndex) {
return this.ownedSongs.some(function (song) {
return song.depth === depth && song.songIndex === songIndex;
});
},
buySong: function buySong(depth, songIndex) {
var song = GAME_CONFIG.DEPTHS[depth].songs[songIndex];
if (this.money >= song.cost && !this.hasSong(depth, songIndex)) {
this.money -= song.cost;
this.ownedSongs.push({
depth: depth,
songIndex: songIndex
});
return true;
}
return false;
},
getCurrentDepthConfig: function getCurrentDepthConfig() {
return GAME_CONFIG.DEPTHS[this.selectedDepth];
},
getCurrentSongConfig: function getCurrentSongConfig() {
return GAME_CONFIG.DEPTHS[this.selectedDepth].songs[this.selectedSong];
},
canUpgrade: function canUpgrade() {
var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1];
return nextDepth && this.money >= nextDepth.upgradeCost;
},
upgrade: function upgrade() {
if (this.canUpgrade()) {
var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1];
this.money -= nextDepth.upgradeCost;
this.currentDepth++;
// Give free first song of new depth
this.ownedSongs.push({
depth: this.currentDepth,
songIndex: 0
});
return true;
}
return false;
}
};
var titleScreen = game.addChild(new Container());
var levelSelectScreen = game.addChild(new Container());
var fishingScreen = game.addChild(new Container());
var resultsScreen = game.addChild(new Container());
// Initialize
GameState.initOwnedSongs();
/****
* Title Screen
****/
/****
* Title Screen - FIXED VERSION
****/
function createTitleScreen() {
var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 1,
height: 2732,
color: 0x87CEEB
}));
// ANIMATED CONTAINER GROUP - This will contain ALL visual elements including backgrounds
var titleAnimationGroup = titleScreen.addChild(new Container());
// Sky background image - NOW INSIDE animation group
var titleSky = titleAnimationGroup.addChild(LK.getAsset('skybackground', {
x: 0,
y: -500
}));
// Water background image - NOW INSIDE animation group
var titleWater = titleAnimationGroup.addChild(LK.getAsset('water', {
x: 0,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: 2048,
height: 2732 - GAME_CONFIG.WATER_SURFACE_Y
}));
// Initialize containers for title screen ambient particles - INSIDE animation group
titleScreenOceanBubbleContainer = titleAnimationGroup.addChild(new Container());
titleScreenSeaweedContainer = titleAnimationGroup.addChild(new Container());
titleScreenCloudContainer = titleAnimationGroup.addChild(new Container());
// Create a single container for boat, fisherman, and line that all move together
var titleBoatGroup = titleAnimationGroup.addChild(new Container());
// Boat positioned at origin of the group
var titleBoat = titleBoatGroup.addChild(LK.getAsset('boat', {
anchorX: 0.5,
anchorY: 0.74,
x: 0,
// Relative to group
y: 0 // Relative to group
}));
// Fisherman positioned relative to boat within the same group
var titleFisherman = titleBoatGroup.addChild(LK.getAsset('fisherman', {
anchorX: 0.5,
anchorY: 1,
x: -100,
// Relative to boat position
y: -70 // Relative to boat position
}));
// Fishing line positioned relative to boat within the same group
var rodTipX = -100 + 85; // fisherman offset + rod offset from fisherman center
var rodTipY = -70 - 200; // fisherman y (feet) - fisherman height (to get to head area for rod)
// initialHookY needs to be relative to the group's origin (which is boat's origin at WATER_SURFACE_Y)
// GAME_CONFIG.LANES[1].y is an absolute world Y.
// titleBoatGroup.y is GAME_CONFIG.WATER_SURFACE_Y.
// So, hook's Y relative to group = absolute hook Y - group's absolute Y
var initialHookYInGroup = GAME_CONFIG.LANES[1].y - GAME_CONFIG.WATER_SURFACE_Y;
var titleLine = titleBoatGroup.addChild(LK.getAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0,
x: rodTipX,
y: rodTipY,
// Relative to group
width: 6,
height: initialHookYInGroup - rodTipY // Length from rod tip to hook, all relative to group
}));
var titleHook = titleBoatGroup.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
x: rodTipX,
// Hook X matches line X initially
y: initialHookYInGroup // Relative to group
}));
// Position the entire group in the world (within titleAnimationGroup)
titleBoatGroup.x = GAME_CONFIG.SCREEN_CENTER_X;
titleBoatGroup.y = GAME_CONFIG.WATER_SURFACE_Y;
// Store base position for wave animation
var boatGroupBaseY = titleBoatGroup.y; // This is the group's world Y
var boatWaveAmplitude = 10;
var boatWaveHalfCycleDuration = 2000;
var boatRotationAmplitude = 0.03;
var boatRotationDuration = 3000;
// Wave animation variables for line (used in updateTitleFishingLineWave)
var lineWaveAmplitude = 12;
var lineWaveSpeed = 0.03;
var linePhaseOffset = 0;
// Animated Water Surface segments - INSIDE animation group
var titleWaterSurfaceSegments = [];
var NUM_WAVE_SEGMENTS_TITLE = 32;
var SEGMENT_WIDTH_TITLE = 2048 / NUM_WAVE_SEGMENTS_TITLE;
var SEGMENT_HEIGHT_TITLE = 24;
var WAVE_AMPLITUDE_TITLE = 12;
var WAVE_HALF_PERIOD_MS_TITLE = 2500;
var PHASE_DELAY_MS_PER_SEGMENT_TITLE = WAVE_HALF_PERIOD_MS_TITLE * 2 / NUM_WAVE_SEGMENTS_TITLE;
// Create water surface segments
for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) {
var segment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH_TITLE,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: SEGMENT_WIDTH_TITLE + 1,
height: SEGMENT_HEIGHT_TITLE,
anchorX: 0,
anchorY: 0.5,
alpha: 0.8,
tint: 0x4fc3f7
});
segment.baseY = GAME_CONFIG.WATER_SURFACE_Y;
titleAnimationGroup.addChild(segment); // Water surface is part of main animation group, not boat group
titleWaterSurfaceSegments.push(segment);
}
for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) {
var whiteSegment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH_TITLE,
y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2,
width: SEGMENT_WIDTH_TITLE + 1,
height: SEGMENT_HEIGHT_TITLE / 2,
anchorX: 0,
anchorY: 0.5,
alpha: 0.6,
tint: 0xffffff
});
whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2;
titleAnimationGroup.addChild(whiteSegment); // Water surface is part of main animation group
titleWaterSurfaceSegments.push(whiteSegment);
}
// Animation group setup for zoom effect - PROPERLY CENTER THE BOAT ON SCREEN
var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; // 1024
var targetBoatScreenY = GAME_CONFIG.SCREEN_CENTER_Y + 300; // Change +100 to move boat lower/higher
var boatWorldY = GAME_CONFIG.WATER_SURFACE_Y; // 760 - where titleBoatGroup actually is
var pivotY = boatWorldY - (targetBoatScreenY - boatWorldY); // Calculate proper pivot offset
// Set pivot to calculated position and position group so boat ends up at screen center
titleAnimationGroup.pivot.set(boatCenterX, pivotY);
titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X; // 1024
titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // 1366 - this will center the boat at targetBoatScreenY
// Initial zoom state
var INITIAL_ZOOM_FACTOR = 3.0;
var FINAL_ZOOM_FACTOR = 1.8; // This matches existing logic if it's used in showScreen
titleAnimationGroup.scale.set(INITIAL_ZOOM_FACTOR);
titleAnimationGroup.alpha = 1; // Main animation group is always visible
// Single wave animation function - moves entire group together
var targetUpY = boatGroupBaseY - boatWaveAmplitude; // boatGroupBaseY is absolute world Y
var targetDownY = boatGroupBaseY + boatWaveAmplitude; // boatGroupBaseY is absolute world Y
function moveTitleBoatGroupUp() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
// We tween the group's world Y position
tween(titleBoatGroup, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveTitleBoatGroupDown
});
}
function moveTitleBoatGroupDown() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
tween(titleBoatGroup, {
y: targetDownY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveTitleBoatGroupUp
});
}
// Single rotation animation - rotates entire group together
function rockTitleBoatGroupLeft() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
tween(titleBoatGroup, {
rotation: -boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockTitleBoatGroupRight
});
}
function rockTitleBoatGroupRight() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
tween(titleBoatGroup, {
rotation: boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockTitleBoatGroupLeft
});
}
// Simplified line wave function - just the sway effect
function updateTitleFishingLineWave() {
// titleLine and titleHook are children of titleBoatGroup, their x/y are relative to it.
// rodTipX, rodTipY, initialHookYInGroup are also relative to titleBoatGroup.
if (!titleLine || titleLine.destroyed || !titleHook || titleHook.destroyed) {
return;
}
linePhaseOffset += lineWaveSpeed;
var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude;
// Only apply the wave sway to X, positions stay relative to group's origin
// rodTipX is the line's base X relative to the group.
titleLine.x = rodTipX + waveOffset * 0.3;
titleHook.x = rodTipX + waveOffset;
// Y positions of line and hook (titleLine.y and titleHook.y) are static relative to the group.
// Recalculate line rotation based on hook position relative to line's anchor
// All coordinates here are relative to titleBoatGroup.
var deltaX = titleHook.x - titleLine.x; // Difference in x relative to group
var deltaY = titleHook.y - titleLine.y; // Difference in y relative to group
var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
titleLine.height = actualLineLength;
if (actualLineLength > 0.001) {
titleLine.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2;
} else {
titleLine.rotation = 0;
}
titleHook.rotation = titleLine.rotation; // Hook rotation matches line's
}
// Water surface animation function
function startTitleWaterSurfaceAnimationFunc() {
for (var k = 0; k < titleWaterSurfaceSegments.length; k++) {
var segment = titleWaterSurfaceSegments[k];
if (!segment || segment.destroyed) {
continue;
}
var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS_TITLE;
(function (currentLocalSegment, currentLocalSegmentIndexForDelay) {
var animUp, animDown;
animDown = function animDown() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE
}, {
duration: WAVE_HALF_PERIOD_MS_TITLE,
easing: tween.easeInOut,
onFinish: animUp
});
};
animUp = function animUp() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY - WAVE_AMPLITUDE_TITLE
}, {
duration: WAVE_HALF_PERIOD_MS_TITLE,
easing: tween.easeInOut,
onFinish: animDown
});
};
LK.setTimeout(function () {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
// Start with DOWN movement first
tween(currentLocalSegment, {
y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE
}, {
duration: WAVE_HALF_PERIOD_MS_TITLE,
easing: tween.easeInOut,
onFinish: animUp // This should call animUp after going down
});
}, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT_TITLE);
})(segment, segmentIndexForDelay);
}
}
// BLACK OVERLAY for reveal effect - this goes OVER everything
var blackOverlay = titleScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
width: 2048,
height: 2732,
color: 0x000000,
// Pure black
alpha: 1 // Starts fully opaque
}));
// UI elements - OUTSIDE animation group so they don't zoom, ABOVE black overlay
var titleImage = titleScreen.addChild(LK.getAsset('titleimage', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 700,
// Positioned where the subtitle roughly was, adjust as needed
alpha: 0,
scaleX: 0.8,
// Adjust scale as needed for optimal size
scaleY: 0.8 // Adjust scale as needed for optimal size
}));
// Buttons - OUTSIDE animation group, ABOVE black overlay
// New Y positions: 1/3 from bottom of screen (2732 / 3 = 910.66. Y = 2732 - 910.66 = 1821.33)
var startButtonY = 2732 - 2732 / 3; // Approx 1821
var tutorialButtonY = startButtonY + 150; // Maintain 150px gap
var startButton = titleScreen.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: startButtonY,
alpha: 0
}));
var startText = titleScreen.addChild(new Text2('START', {
size: 50,
fill: 0xFFFFFF
}));
startText.anchor.set(0.5, 0.5);
startText.x = GAME_CONFIG.SCREEN_CENTER_X;
startText.y = startButtonY;
startText.alpha = 0;
var tutorialButton = titleScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: tutorialButtonY,
tint: 0x757575,
alpha: 0
}));
var tutorialText = titleScreen.addChild(new Text2('TUTORIAL', {
size: 40,
fill: 0xFFFFFF
}));
tutorialText.anchor.set(0.5, 0.5);
tutorialText.x = GAME_CONFIG.SCREEN_CENTER_X;
tutorialText.y = tutorialButtonY;
tutorialText.alpha = 0;
// No assignment here; assign tutorialButtonGfx after createTitleScreen returns
return {
startButton: startButton,
startText: startText,
tutorialButton: tutorialButton,
// This is the graphics object
tutorialText: tutorialText,
titleImage: titleImage,
titleAnimationGroup: titleAnimationGroup,
blackOverlay: blackOverlay,
// Return the group and its specific animation functions
titleBoatGroup: titleBoatGroup,
moveTitleBoatGroupUp: moveTitleBoatGroupUp,
rockTitleBoatGroupLeft: rockTitleBoatGroupLeft,
// Individual elements that might still be needed for direct access or other effects
titleSky: titleSky,
// Sky is not in titleBoatGroup
titleWater: titleWater,
// Water background is not in titleBoatGroup
titleWaterSurfaceSegments: titleWaterSurfaceSegments,
// Water surface segments are not in titleBoatGroup
// Line and hook are part of titleBoatGroup, but if direct reference is needed for some reason, they could be returned.
// However, following the goal's spirit of "Return the group instead of individual elements [that are part of it]"
// titleLine and titleHook might be omitted here unless specifically needed by other parts of the code.
// For now, including them if they were returned before and are still locally defined.
// updateTitleFishingLineWave accesses titleLine and titleHook locally.
titleLine: titleLine,
// Still defined locally, might be useful for direct reference
titleHook: titleHook,
// Still defined locally
startTitleWaterSurfaceAnimation: startTitleWaterSurfaceAnimationFunc,
updateTitleFishingLineWave: updateTitleFishingLineWave // The new simplified version
};
}
/****
* Level Select Screen
****/
function createLevelSelectScreen() {
var selectBg = levelSelectScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 0.8,
height: 2732
}));
// Title
var title = new Text2('SELECT FISHING SPOT', {
size: 80,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.x = GAME_CONFIG.SCREEN_CENTER_X;
title.y = 200;
levelSelectScreen.addChild(title);
// Money display
var moneyDisplay = new Text2('Money: $0', {
size: 60,
fill: 0xFFD700
});
moneyDisplay.anchor.set(1, 0);
moneyDisplay.x = 1900;
moneyDisplay.y = 100;
levelSelectScreen.addChild(moneyDisplay);
// Depth tabs (will be created dynamically)
var depthTabs = [];
// Song display area
var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 700
}));
// Song navigation arrows
var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 400,
y: 700,
tint: 0x666666
}));
var leftArrowText = new Text2('<', {
size: 60,
fill: 0xFFFFFF
});
leftArrowText.anchor.set(0.5, 0.5);
leftArrowText.x = 400;
leftArrowText.y = 700;
levelSelectScreen.addChild(leftArrowText);
var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 1648,
y: 700,
tint: 0x666666
}));
var rightArrowText = new Text2('>', {
size: 60,
fill: 0xFFFFFF
});
rightArrowText.anchor.set(0.5, 0.5);
rightArrowText.x = 1648;
rightArrowText.y = 700;
levelSelectScreen.addChild(rightArrowText);
// Song info (will be updated dynamically)
var songTitle = new Text2('Song Title', {
size: 50,
fill: 0xFFFFFF
});
songTitle.anchor.set(0.5, 0.5);
songTitle.x = GAME_CONFIG.SCREEN_CENTER_X;
songTitle.y = 650;
levelSelectScreen.addChild(songTitle);
var songInfo = new Text2('BPM: 120 | Duration: 2:00', {
size: 30,
fill: 0xCCCCCC
});
songInfo.anchor.set(0.5, 0.5);
songInfo.x = GAME_CONFIG.SCREEN_CENTER_X;
songInfo.y = 700;
levelSelectScreen.addChild(songInfo);
var songEarnings = new Text2('Potential Earnings: $50-100', {
size: 30,
fill: 0x4CAF50
});
songEarnings.anchor.set(0.5, 0.5);
songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X;
songEarnings.y = 730;
levelSelectScreen.addChild(songEarnings);
// Play/Buy button
var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 900
}));
var playButtonText = new Text2('PLAY', {
size: 50,
fill: 0xFFFFFF
});
playButtonText.anchor.set(0.5, 0.5);
playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
playButtonText.y = 900;
levelSelectScreen.addChild(playButtonText);
// Shop button
var shopButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1100,
tint: 0x2e7d32
}));
var shopButtonText = new Text2('UPGRADE ROD', {
size: 40,
fill: 0xFFFFFF
});
shopButtonText.anchor.set(0.5, 0.5);
shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
shopButtonText.y = 1100;
levelSelectScreen.addChild(shopButtonText);
// Back button
var backButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 200,
y: 200,
tint: 0x757575
}));
var backButtonText = new Text2('BACK', {
size: 40,
fill: 0xFFFFFF
});
backButtonText.anchor.set(0.5, 0.5);
backButtonText.x = 200;
backButtonText.y = 200;
levelSelectScreen.addChild(backButtonText);
return {
moneyDisplay: moneyDisplay,
depthTabs: depthTabs,
leftArrow: leftArrow,
rightArrow: rightArrow,
songTitle: songTitle,
songInfo: songInfo,
songEarnings: songEarnings,
playButton: playButton,
playButtonText: playButtonText,
shopButton: shopButton,
shopButtonText: shopButtonText,
backButton: backButton
};
}
/****
* Fishing Screen
****/
function createFishingScreen() {
// Sky background - should be added first to be behind everything else
var sky = fishingScreen.addChild(LK.getAsset('skybackground', {
x: 0,
y: -500
}));
// Water background
var water = fishingScreen.addChild(LK.getAsset('water', {
x: 0,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: 2048,
height: 2732 - GAME_CONFIG.WATER_SURFACE_Y
}));
// Create a container for ambient ocean bubbles (from bottom of screen)
// This should be layered above the 'water' background, but below fish, boat, etc.
globalOceanBubbleContainer = fishingScreen.addChild(new Container());
// Create a container for seaweed particles
globalSeaweedContainer = fishingScreen.addChild(new Container());
// Create a container for cloud particles (added early so clouds appear behind UI)
globalCloudContainer = fishingScreen.addChild(new Container());
// Create a container for bubbles to render them behind fish and other elements
bubbleContainer = fishingScreen.addChild(new Container());
// Create a container for music notes
musicNotesContainer = fishingScreen.addChild(new Container());
// Music notes should visually appear to come from the boat area, so their container
// should ideally be layered accordingly. Adding it here means it's on top of water,
// but if boat/fisherman are added later, notes might appear behind them if not managed.
// For now, notes will be added to this container, which itself is added to fishingScreen.
// Animated Water Surface segments code
var waterSurfaceSegments = []; // This will be populated for returning and cleanup
var waterSurfaceSegmentsBlueTemp = []; // Temporary array for blue segments
var waterSurfaceSegmentsWhiteTemp = []; // Temporary array for white segments
var NUM_WAVE_SEGMENTS = 32;
var SEGMENT_WIDTH = 2048 / NUM_WAVE_SEGMENTS;
var SEGMENT_HEIGHT = 24;
var WAVE_AMPLITUDE = 12;
var WAVE_HALF_PERIOD_MS = 2500;
var PHASE_DELAY_MS_PER_SEGMENT = WAVE_HALF_PERIOD_MS * 2 / NUM_WAVE_SEGMENTS;
// Create blue segments (assets only, not added to fishingScreen yet)
for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) {
var segment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: SEGMENT_WIDTH + 1,
height: SEGMENT_HEIGHT,
anchorX: 0,
anchorY: 0.5,
alpha: 0.8,
tint: 0x4fc3f7
});
segment.baseY = GAME_CONFIG.WATER_SURFACE_Y;
// Animation functions will be defined and started by startWaterSurfaceAnimationFunc
waterSurfaceSegmentsBlueTemp.push(segment);
}
// Create white segments (assets only, not added to fishingScreen yet)
for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) {
var whiteSegment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH,
y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2,
width: SEGMENT_WIDTH + 1,
height: SEGMENT_HEIGHT / 2,
anchorX: 0,
anchorY: 0.5,
alpha: 0.6,
tint: 0xffffff
});
whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2;
// Animation functions will be defined and started by startWaterSurfaceAnimationFunc
waterSurfaceSegmentsWhiteTemp.push(whiteSegment);
}
// Boat - Add this to fishingScreen first
var boat = fishingScreen.addChild(LK.getAsset('boat', {
anchorX: 0.5,
anchorY: 0.74,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: GAME_CONFIG.WATER_SURFACE_Y
}));
// Now add the water segments to fishingScreen, so they render on top of the boat
for (var i = 0; i < waterSurfaceSegmentsBlueTemp.length; i++) {
fishingScreen.addChild(waterSurfaceSegmentsBlueTemp[i]);
waterSurfaceSegments.push(waterSurfaceSegmentsBlueTemp[i]); // Also add to the main array for cleanup
}
for (var i = 0; i < waterSurfaceSegmentsWhiteTemp.length; i++) {
fishingScreen.addChild(waterSurfaceSegmentsWhiteTemp[i]);
waterSurfaceSegments.push(waterSurfaceSegmentsWhiteTemp[i]); // Also add to the main array for cleanup
}
// Create separate fisherman container that will sync with boat movement
var fishermanContainer = fishingScreen.addChild(new Container());
// Fisherman (now in its own container, positioned to match boat)
var fisherman = fishermanContainer.addChild(LK.getAsset('fisherman', {
anchorX: 0.5,
anchorY: 1,
x: GAME_CONFIG.SCREEN_CENTER_X - 100,
y: GAME_CONFIG.WATER_SURFACE_Y - 70
}));
// Store references for wave animation sync
var boatBaseY = boat.y;
var fishermanBaseY = fishermanContainer.y;
var boatWaveAmplitude = 10;
var boatWaveHalfCycleDuration = 2000;
// SINGLE ANIMATED FISHING LINE
var initialHookY = GAME_CONFIG.LANES[1].y;
var fishingLineStartY = -100;
var line = fishingScreen.addChild(LK.getAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY,
width: 6,
height: initialHookY - (GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY)
}));
var hook = fishingScreen.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: initialHookY
}));
hook.originalY = initialHookY;
var lineWaveAmplitude = 12;
var lineWaveSpeed = 0.03;
var linePhaseOffset = 0;
function updateFishingLineWave() {
linePhaseOffset += lineWaveSpeed;
var rodTipX = fishermanContainer.x + fisherman.x + 85;
var rodTipY = fishermanContainer.y + fisherman.y - fisherman.height;
var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude;
line.x = rodTipX + waveOffset * 0.3;
line.y = rodTipY;
hook.x = rodTipX + waveOffset;
var hookAttachX = hook.x;
var hookAttachY = hook.y - hook.height / 2;
var deltaX = hookAttachX - line.x;
var deltaY = hookAttachY - line.y;
var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
line.height = actualLineLength;
if (actualLineLength > 0.001) {
line.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2;
} else {
line.rotation = 0;
}
hook.rotation = line.rotation;
}
// Calculate target positions for boat wave animation
var targetUpY = boatBaseY - boatWaveAmplitude;
var targetDownY = boatBaseY + boatWaveAmplitude;
var fishermanTargetUpY = fishermanBaseY - boatWaveAmplitude;
var fishermanTargetDownY = fishermanBaseY + boatWaveAmplitude;
// Synchronized wave animation functions (defined here to be closured)
function moveBoatAndFishermanUp() {
if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) {
return;
}
tween(boat, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveBoatAndFishermanDown
});
tween(fishermanContainer, {
y: fishermanTargetUpY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut
});
}
function moveBoatAndFishermanDown() {
if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) {
return;
}
tween(boat, {
y: targetDownY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveBoatAndFishermanUp
});
tween(fishermanContainer, {
y: fishermanTargetDownY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut
});
}
var boatRotationAmplitude = 0.03;
var boatRotationDuration = 3000;
function rockBoatLeft() {
if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) {
return;
}
tween(boat, {
rotation: -boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockBoatRight
});
tween(fisherman, {
rotation: boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut
});
}
function rockBoatRight() {
if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) {
return;
}
tween(boat, {
rotation: boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockBoatLeft
});
tween(fisherman, {
rotation: -boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut
});
}
// Function to start/restart water surface animations
function startWaterSurfaceAnimationFunc() {
var allSegments = waterSurfaceSegments; // Use the populated array from fishingElements via closure
for (var k = 0; k < allSegments.length; k++) {
var segment = allSegments[k];
if (!segment || segment.destroyed) {
continue;
}
var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS;
(function (currentLocalSegment, currentLocalSegmentIndexForDelay) {
var animUp, animDown;
animDown = function animDown() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY + WAVE_AMPLITUDE
}, {
duration: WAVE_HALF_PERIOD_MS,
easing: tween.easeInOut,
onFinish: animUp
});
};
animUp = function animUp() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY - WAVE_AMPLITUDE
}, {
duration: WAVE_HALF_PERIOD_MS,
easing: tween.easeInOut,
onFinish: animDown
});
};
LK.setTimeout(function () {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY - WAVE_AMPLITUDE
}, {
// Initial move up
duration: WAVE_HALF_PERIOD_MS,
easing: tween.easeInOut,
onFinish: animDown
});
}, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT);
})(segment, segmentIndexForDelay);
}
}
// Function to start/restart boat and fisherman animations
function startBoatAndFishermanAnimationFunc() {
if (boat && !boat.destroyed && fishermanContainer && !fishermanContainer.destroyed) {
tween(boat, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration / 2,
easing: tween.easeOut,
onFinish: moveBoatAndFishermanDown
});
tween(fishermanContainer, {
y: fishermanTargetUpY
}, {
duration: boatWaveHalfCycleDuration / 2,
easing: tween.easeOut
});
rockBoatLeft();
}
}
// UI elements (from existing)
var scoreText = new Text2('Score: 0', {
size: 70,
fill: 0xFFFFFF
});
scoreText.anchor.set(1, 0);
scoreText.x = 2048 - 50;
scoreText.y = 50;
fishingScreen.addChild(scoreText);
var fishText = new Text2('Fish: 0/0', {
size: 55,
fill: 0xFFFFFF
});
fishText.anchor.set(1, 0);
fishText.x = 2048 - 50;
fishText.y = 140;
fishingScreen.addChild(fishText);
var comboText = new Text2('Combo: 0', {
size: 55,
fill: 0xFF9800
});
comboText.anchor.set(1, 0);
comboText.x = 2048 - 50;
comboText.y = 210;
fishingScreen.addChild(comboText);
var progressText = new Text2('0:00 / 0:00', {
size: 50,
fill: 0x4FC3F7
});
progressText.anchor.set(1, 0);
progressText.x = 2048 - 50;
progressText.y = 280;
fishingScreen.addChild(progressText);
return {
boat: boat,
fishermanContainer: fishermanContainer,
fisherman: fisherman,
hook: hook,
line: line,
updateFishingLineWave: updateFishingLineWave,
scoreText: scoreText,
fishText: fishText,
comboText: comboText,
progressText: progressText,
waterSurfaceSegments: waterSurfaceSegments,
bubbleContainer: bubbleContainer,
musicNotesContainer: musicNotesContainer,
startWaterSurfaceAnimation: startWaterSurfaceAnimationFunc,
startBoatAndFishermanAnimation: startBoatAndFishermanAnimationFunc
};
}
/****
* Initialize Screen Elements
****/
var titleElements = createTitleScreen();
titleElements.tutorialButtonGfx = titleElements.tutorialButton; // Store the graphical button for compatibility
var levelSelectElements = createLevelSelectScreen();
var fishingElements = createFishingScreen();
// Tutorial UI Elements - MUST be at global scope
var tutorialOverlayContainer = game.addChild(new Container());
tutorialOverlayContainer.visible = false;
var tutorialTextBackground;
var tutorialTextDisplay;
var tutorialContinueButton;
var tutorialContinueText;
// Feedback indicators are now created on-demand by the showFeedback function.
// The global feedbackIndicators object is no longer needed.
// Game variables
var fishArray = [];
var bubblesArray = [];
var bubbleContainer; // Container for bubbles, initialized in createFishingScreen
var musicNotesArray = [];
var musicNotesContainer; // Container for music notes
var laneBrackets = []; // Stores the visual bracket pairs for each lane
var musicNoteSpawnCounter = 0;
var MUSIC_NOTE_SPAWN_INTERVAL_TICKS = 45; // Spawn a note roughly every 0.75 seconds
// Ocean Bubbles (ambient background)
var globalOceanBubblesArray = [];
var globalOceanBubbleContainer;
var globalOceanBubbleSpawnCounter = 0;
// Increase interval to reduce amount (higher = less frequent)
var OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS = 40; // Spawn new ocean bubbles roughly every 2/3 second (was 20)
// Seaweed particles (ambient background)
var globalSeaweedArray = [];
var globalSeaweedContainer;
var globalSeaweedSpawnCounter = 0;
var SEAWEED_SPAWN_INTERVAL_TICKS = 120; // Spawn seaweed less frequently than bubbles
var MAX_SEAWEED_COUNT = 8; // Maximum number of seaweed particles at once
// Cloud particles (ambient sky)
var globalCloudArray = [];
var globalCloudContainer;
var globalCloudSpawnCounter = 0;
var CLOUD_SPAWN_INTERVAL_TICKS = 180; // Spawn clouds less frequently than seaweed
var MAX_CLOUD_COUNT = 5; // Maximum number of cloud particles at once
// Title Screen Ambient Particle Systems
var titleScreenOceanBubblesArray = [];
var titleScreenOceanBubbleContainer; // Will be initialized in createTitleScreen
var titleScreenOceanBubbleSpawnCounter = 0;
var titleScreenSeaweedArray = [];
var titleScreenSeaweedContainer; // Will be initialized in createTitleScreen
var titleScreenSeaweedSpawnCounter = 0;
var titleScreenCloudArray = [];
var titleScreenCloudContainer; // Will be initialized in createTitleScreen
var titleScreenCloudSpawnCounter = 0;
// Timers for title screen ambient sounds
var titleSeagullSoundTimer = null;
var titleBoatSoundTimer = null;
/****
* Input State and Helpers for Fishing
****/
var inputState = {
touching: false,
// Is the screen currently being touched?
touchLane: -1,
// Which lane was the touch initiated in? (0, 1, 2)
touchStartTime: 0 // Timestamp of when the touch started (LK.ticks based)
};
// Helper function to determine which lane a Y coordinate falls into
function getTouchLane(y) {
// Define boundaries based on the midpoints between lane Y coordinates
// These are calculated from GAME_CONFIG.LANES[i].y values
// Lane 0: y = 723
// Lane 1: y = 1366
// Lane 2: y = 2009
var boundary_lane0_lane1 = (GAME_CONFIG.LANES[0].y + GAME_CONFIG.LANES[1].y) / 2; // Approx 1044.5
var boundary_lane1_lane2 = (GAME_CONFIG.LANES[1].y + GAME_CONFIG.LANES[2].y) / 2; // Approx 1687.5
if (y < boundary_lane0_lane1) {
return 0; // Top lane (e.g., shallow)
} else if (y < boundary_lane1_lane2) {
return 1; // Middle lane (e.g., medium)
} else {
return 2; // Bottom lane (e.g., deep)
}
}
// Shows feedback (perfect, good, miss) at the specified lane
// Shows feedback (perfect, good, miss) at the specified lane
function showFeedback(type, laneIndex) {
var feedbackY = GAME_CONFIG.LANES[laneIndex].y;
var indicator = new FeedbackIndicator(type); // Creates a new indicator e.g. FeedbackIndicator('perfect')
// Position feedback at the single hook's X coordinate and the fish's lane Y
indicator.x = fishingElements.hook.x; // Use the single hook's X
indicator.y = feedbackY; // Feedback appears at the fish's lane Y
fishingScreen.addChild(indicator);
indicator.show(); // Triggers the animation and self-destruction
}
// Animates the hook in a specific lane after a catch attempt
// Animates the single hook after a catch attempt
function animateHookCatch() {
var hook = fishingElements.hook;
// We need a stable originalY. The hook.originalY might change if we re-assign it during tweens.
// Let's use the target Y of the current fish lane for the "resting" position after animation.
var restingY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
// Quick bobbing animation for the single hook
tween(hook, {
y: restingY - 30
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(hook, {
y: restingY
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
// Ensure originalY reflects the current target lane after animation.
hook.originalY = restingY;
}
});
}
});
}
// Handles input specifically for the fishing screen (down and up events)
function handleFishingInput(x, y, isDown) {
// If in tutorial mode, and it's a 'down' event during an active catch step
if (GameState.tutorialMode && isDown && (GameState.tutorialStep === 2 || GameState.tutorialStep === 3) && !GameState.tutorialPaused) {
if (GameState.tutorialFish && !GameState.tutorialFish.caught && !GameState.tutorialFish.missed) {
checkCatch(getTouchLane(y)); // Attempt catch
}
return; // Tutorial catch handled
}
// Existing game active check
if (!GameState.gameActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60); // Current time in ms
if (isDown) {
// Touch started
inputState.touching = true;
inputState.touchLane = getTouchLane(y);
inputState.touchStartTime = currentTime;
// A normal tap action will be processed on 'up'.
} else {
// Touch ended (isUp)
if (inputState.touching) {
// This was a normal tap.
checkCatch(inputState.touchLane);
}
inputState.touching = false;
}
}
/****
* Screen Management
****/
function showScreen(screenName) {
titleScreen.visible = false;
levelSelectScreen.visible = false;
fishingScreen.visible = false;
resultsScreen.visible = false;
// Stop any ongoing tweens and clear particles if switching FROM title screen
if (GameState.currentScreen === 'title' && titleElements) {
tween.stop(titleElements.titleAnimationGroup);
tween.stop(titleElements.blackOverlay);
if (titleElements.titleImage) {
tween.stop(titleElements.titleImage);
} // Stop titleImage tween
if (titleElements.logo) {
tween.stop(titleElements.logo);
} // Keep for safety if old code path
if (titleElements.subtitle) {
tween.stop(titleElements.subtitle);
} // Keep for safety
tween.stop(titleElements.startButton);
tween.stop(titleElements.startText);
tween.stop(titleElements.tutorialButton);
tween.stop(titleElements.tutorialText);
// Clear title screen sound timers
if (titleSeagullSoundTimer) {
LK.clearTimeout(titleSeagullSoundTimer);
titleSeagullSoundTimer = null;
}
if (titleBoatSoundTimer) {
LK.clearTimeout(titleBoatSoundTimer);
titleBoatSoundTimer = null;
}
// Stop water surface animations for title screen
if (titleElements.titleWaterSurfaceSegments) {
titleElements.titleWaterSurfaceSegments.forEach(function (segment) {
if (segment && !segment.destroyed) {
tween.stop(segment);
}
});
}
// Clear title screen particles
if (titleScreenOceanBubbleContainer) {
titleScreenOceanBubbleContainer.removeChildren();
}
titleScreenOceanBubblesArray.forEach(function (p) {
if (p && !p.destroyed) {
p.destroy();
}
});
titleScreenOceanBubblesArray = [];
if (titleScreenSeaweedContainer) {
titleScreenSeaweedContainer.removeChildren();
}
titleScreenSeaweedArray.forEach(function (p) {
if (p && !p.destroyed) {
p.destroy();
}
});
titleScreenSeaweedArray = [];
if (titleScreenCloudContainer) {
titleScreenCloudContainer.removeChildren();
}
titleScreenCloudArray.forEach(function (p) {
if (p && !p.destroyed) {
p.destroy();
}
});
titleScreenCloudArray = [];
}
// Cleanup tutorial elements if switching away from fishing/tutorial
if (GameState.currentScreen === 'fishing' && GameState.tutorialMode && screenName !== 'fishing') {
tutorialOverlayContainer.visible = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish = null;
}
GameState.tutorialMode = false; // Ensure tutorial mode is exited
}
GameState.currentScreen = screenName;
switch (screenName) {
case 'title':
// Title Screen Sounds
var _scheduleNextSeagullSound = function scheduleNextSeagullSound() {
if (GameState.currentScreen !== 'title') {
// If screen changed, ensure timer is stopped and not rescheduled
if (titleSeagullSoundTimer) {
LK.clearTimeout(titleSeagullSoundTimer);
titleSeagullSoundTimer = null;
}
return;
}
var randomDelay = 5000 + Math.random() * 10000; // 5-15 seconds
titleSeagullSoundTimer = LK.setTimeout(function () {
if (GameState.currentScreen !== 'title') {
return; // Don't play or reschedule if not on title screen
}
var seagullSounds = ['seagull1', 'seagull2', 'seagull3'];
var randomSoundId = seagullSounds[Math.floor(Math.random() * seagullSounds.length)];
LK.getSound(randomSoundId).play();
_scheduleNextSeagullSound(); // Reschedule
}, randomDelay);
};
var _scheduleNextBoatSound = function scheduleNextBoatSound() {
if (GameState.currentScreen !== 'title') {
// If screen changed, ensure timer is stopped and not rescheduled
if (titleBoatSoundTimer) {
LK.clearTimeout(titleBoatSoundTimer);
titleBoatSoundTimer = null;
}
return;
}
var fixedBoatSoundInterval = 8000; // Rhythmic interval: 8 seconds (reduced from 15)
titleBoatSoundTimer = LK.setTimeout(function () {
if (GameState.currentScreen !== 'title') {
return; // Don't play or reschedule if not on title screen
}
LK.getSound('boatsounds').play();
_scheduleNextBoatSound(); // Reschedule
}, fixedBoatSoundInterval);
}; // Play initial random seagull sound
titleScreen.visible = true;
// Start all animations like in fishing screen
if (titleElements.startTitleWaterSurfaceAnimation) {
titleElements.startTitleWaterSurfaceAnimation();
}
// Start boat group animations (single animations for the whole group)
if (titleElements.moveTitleBoatGroupUp) {
titleElements.moveTitleBoatGroupUp();
}
if (titleElements.rockTitleBoatGroupLeft) {
titleElements.rockTitleBoatGroupLeft();
}
var initialSeagullSounds = ['seagull1', 'seagull2', 'seagull3'];
var initialRandomSoundId = initialSeagullSounds[Math.floor(Math.random() * initialSeagullSounds.length)];
LK.getSound(initialRandomSoundId).play();
LK.getSound('boatsounds').play(); // Play boat sound immediately after initial seagull
// Start the timed sounds (seagulls random, subsequent boats rhythmic)
_scheduleNextSeagullSound();
_scheduleNextBoatSound(); // Schedules the *next* boat sound rhythmically
// Reset particle spawn counters
titleScreenOceanBubbleSpawnCounter = 0;
titleScreenSeaweedSpawnCounter = 0;
titleScreenCloudSpawnCounter = 0;
// Animation timing
var ZOOM_DURATION = 8000; // 8-second zoom
var OVERLAY_FADE_DELAY = 1000; // Black overlay starts fading after 1 second (was 2)
var OVERLAY_FADE_DURATION = 3000; // 3 seconds to fade out overlay
var TEXT_DELAY = 4000; // Text appears as overlay finishes fading (was 5500)
var BUTTON_DELAY = 5500; // Buttons appear sooner (was 7000)
// Reset states
titleElements.titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X;
titleElements.titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // Centers boat vertically!
titleElements.titleAnimationGroup.alpha = 1; // No alpha animation for main content
titleElements.titleAnimationGroup.scale.set(3.0);
// Reset black overlay to fully opaque
titleElements.blackOverlay.alpha = 1;
// Reset UI alphas
if (titleElements.titleImage) {
titleElements.titleImage.alpha = 0;
}
if (titleElements.logo) {
titleElements.logo.alpha = 0;
}
if (titleElements.subtitle) {
titleElements.subtitle.alpha = 0;
}
titleElements.startButton.alpha = 0;
titleElements.startText.alpha = 0;
titleElements.tutorialButton.alpha = 0;
titleElements.tutorialText.alpha = 0;
// Main zoom animation (no alpha change)
tween(titleElements.titleAnimationGroup, {
scaleX: 1.8,
scaleY: 1.8
}, {
duration: ZOOM_DURATION,
easing: tween.easeInOut
});
// Black overlay fade out (the reveal effect)
LK.setTimeout(function () {
tween(titleElements.blackOverlay, {
alpha: 0
}, {
duration: OVERLAY_FADE_DURATION,
easing: tween.easeInOut
});
}, OVERLAY_FADE_DELAY);
// Fade in title image after overlay is mostly gone
LK.setTimeout(function () {
if (titleElements.titleImage) {
tween(titleElements.titleImage, {
alpha: 1
}, {
duration: 1200,
easing: tween.easeOut
});
} else if (titleElements.logo && titleElements.subtitle) {
// Fallback for old structure if needed
tween(titleElements.logo, {
alpha: 1
}, {
duration: 1200,
easing: tween.easeOut
});
tween(titleElements.subtitle, {
alpha: 1
}, {
duration: 1200,
easing: tween.easeOut
});
}
}, TEXT_DELAY);
// Fade in buttons near the end
LK.setTimeout(function () {
tween(titleElements.startButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut
});
tween(titleElements.startText, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut
});
tween(titleElements.tutorialButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut
});
tween(titleElements.tutorialText, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut
});
}, BUTTON_DELAY);
break;
case 'levelSelect':
levelSelectScreen.visible = true;
updateLevelSelectScreen();
break;
case 'fishing':
fishingScreen.visible = true;
playIntroAnimation(); // Play the intro sequence
break;
case 'results':
resultsScreen.visible = true;
break;
}
// Tutorial System functions moved to global scope.
}
/****
* Intro Animation
****/
function playIntroAnimation() {
GameState.introPlaying = true;
GameState.gameActive = false;
// Start animations for water, boat, and fisherman at the beginning of the intro
if (fishingElements) {
if (typeof fishingElements.startWaterSurfaceAnimation === 'function') {
fishingElements.startWaterSurfaceAnimation();
}
if (typeof fishingElements.startBoatAndFishermanAnimation === 'function') {
fishingElements.startBoatAndFishermanAnimation();
}
}
// Calculate rod tip position (relative to fishingScreen)
var fc = fishingElements.fishermanContainer;
var f = fishingElements.fisherman;
var rodTipCalculatedX = fc.x + f.x + 85;
var rodTipCalculatedY = fc.y + f.y - f.height;
var initialHookDangleY = rodTipCalculatedY + 50;
fishingElements.hook.y = initialHookDangleY;
// Setup initial zoom and camera position for fishingScreen
var INITIAL_ZOOM_FACTOR = 1.5;
// Pivot around the boat's visual center
var pivotX = fishingElements.boat.x;
var pivotY = fishingElements.boat.y - fishingElements.boat.height * (fishingElements.boat.anchor.y - 0.5);
fishingScreen.pivot.set(pivotX, pivotY);
// Position screen so the pivot appears at screen center when zoomed
var screenCenterX = 2048 / 2;
var screenCenterY = 2732 / 2;
fishingScreen.x = screenCenterX;
fishingScreen.y = screenCenterY;
fishingScreen.scale.set(INITIAL_ZOOM_FACTOR, INITIAL_ZOOM_FACTOR);
var introDuration = 2000;
// Tween for zoom out
tween(fishingScreen.scale, {
x: 1,
y: 1
}, {
duration: introDuration,
easing: tween.easeInOut
});
// Tween screen position to compensate for the zoom change
tween(fishingScreen, {
x: pivotX,
y: pivotY
}, {
duration: introDuration,
easing: tween.easeInOut
});
// Hook drop animation
var targetHookY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
tween(fishingElements.hook, {
y: targetHookY
}, {
duration: introDuration * 0.8,
delay: introDuration * 0.2,
easing: tween.easeOut,
onFinish: function onFinish() {
GameState.introPlaying = false;
// Reset to normal view
fishingScreen.pivot.set(0, 0);
fishingScreen.x = 0;
fishingScreen.y = 0;
// Check if we're in tutorial mode
if (GameState.tutorialMode) {
// Start the tutorial properly after intro
GameState.gameActive = false; // Keep game inactive for tutorial
createTutorialElements(); // Create tutorial UI
runTutorialStep(); // Start with step 0
} else {
// Normal fishing session
startFishingSession();
}
}
});
}
/****
* Level Select Logic
****/
function updateLevelSelectScreen() {
var elements = levelSelectElements;
// Update money display
elements.moneyDisplay.setText('Money: $' + GameState.money);
// Create depth tabs
createDepthTabs();
// Update song display
updateSongDisplay();
// Update shop button
updateShopButton();
}
function createDepthTabs() {
// Clear existing tabs
levelSelectElements.depthTabs.forEach(function (tab) {
if (tab.container) {
tab.container.destroy();
}
});
levelSelectElements.depthTabs = [];
// Create tabs for unlocked depths
for (var i = 0; i <= GameState.currentDepth; i++) {
var depth = GAME_CONFIG.DEPTHS[i];
var isSelected = i === GameState.selectedDepth;
var tabContainer = levelSelectScreen.addChild(new Container());
var tab = tabContainer.addChild(LK.getAsset('depthTab', {
anchorX: 0.5,
anchorY: 0.5,
x: 300 + i * 220,
y: 400,
tint: isSelected ? 0x1976d2 : 0x455a64
}));
var tabText = new Text2(depth.name.split(' ')[0], {
size: 30,
fill: 0xFFFFFF
});
tabText.anchor.set(0.5, 0.5);
tabText.x = 300 + i * 220;
tabText.y = 400;
tabContainer.addChild(tabText);
levelSelectElements.depthTabs.push({
container: tabContainer,
tab: tab,
depthIndex: i
});
}
}
function updateSongDisplay() {
var elements = levelSelectElements;
var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth];
var song = depth.songs[GameState.selectedSong];
var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong);
// Update song info
elements.songTitle.setText(song.name);
elements.songInfo.setText('BPM: ' + song.bpm + ' | Duration: ' + formatTime(song.duration));
// Calculate potential earnings
var minEarnings = Math.floor(depth.fishValue * 20); // Conservative estimate
var maxEarnings = Math.floor(depth.fishValue * 60); // With combos and rare fish
elements.songEarnings.setText('Potential Earnings: $' + minEarnings + '-$' + maxEarnings);
// Update play/buy button
if (owned) {
elements.playButtonText.setText('PLAY');
elements.playButton.tint = 0x1976d2;
} else {
elements.playButtonText.setText('BUY ($' + song.cost + ')');
elements.playButton.tint = GameState.money >= song.cost ? 0x2e7d32 : 0x666666;
}
// Update arrow states
elements.leftArrow.tint = GameState.selectedSong > 0 ? 0x1976d2 : 0x666666;
elements.rightArrow.tint = GameState.selectedSong < depth.songs.length - 1 ? 0x1976d2 : 0x666666;
}
function updateShopButton() {
var elements = levelSelectElements;
var canUpgrade = GameState.canUpgrade();
var nextDepth = GAME_CONFIG.DEPTHS[GameState.currentDepth + 1];
if (nextDepth) {
elements.shopButtonText.setText('UPGRADE ROD ($' + nextDepth.upgradeCost + ')');
elements.shopButton.tint = canUpgrade ? 0x2e7d32 : 0x666666;
} else {
elements.shopButtonText.setText('MAX DEPTH REACHED');
elements.shopButton.tint = 0x666666;
}
}
function formatTime(ms) {
var seconds = Math.floor(ms / 1000);
var minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
}
/****
* Fishing Game Logic
****/
function startFishingSession() {
// Reset session state
GameState.tutorialMode = false; // Ensure tutorial mode is off
GameState.sessionScore = 0;
GameState.sessionFishCaught = 0;
GameState.sessionFishSpawned = 0;
GameState.combo = 0;
GameState.maxCombo = 0;
GameState.gameActive = true;
GameState.songStartTime = 0;
GameState.lastBeatTime = 0;
GameState.beatCount = 0;
GameState.musicNotesActive = true;
ImprovedRhythmSpawner.reset();
musicNotesArray = [];
if (fishingElements && fishingElements.musicNotesContainer) {
fishingElements.musicNotesContainer.removeChildren();
}
musicNoteSpawnCounter = 0;
// Reset ocean bubbles
globalOceanBubblesArray = [];
if (globalOceanBubbleContainer) {
globalOceanBubbleContainer.removeChildren();
}
globalOceanBubbleSpawnCounter = 0;
// Reset seaweed
globalSeaweedArray = [];
if (globalSeaweedContainer) {
globalSeaweedContainer.removeChildren();
}
globalSeaweedSpawnCounter = 0;
// Reset clouds
globalCloudArray = [];
if (globalCloudContainer) {
globalCloudContainer.removeChildren();
}
globalCloudSpawnCounter = 0;
// Animations for water, boat, and fisherman are now started in playIntroAnimation
// Clear any existing fish
fishArray.forEach(function (fish) {
fish.destroy();
});
fishArray = [];
// Reset pattern generator for new session
PatternGenerator.reset();
// Clear and create lane brackets
if (laneBrackets && laneBrackets.length > 0) {
laneBrackets.forEach(function (bracketPair) {
if (bracketPair.left && !bracketPair.left.destroyed) {
bracketPair.left.destroy();
}
if (bracketPair.right && !bracketPair.right.destroyed) {
bracketPair.right.destroy();
}
});
}
laneBrackets = [];
var bracketAssetHeight = 150; // Height of the lanebracket asset
var bracketAssetWidth = 75; // Width of the lanebracket asset
if (fishingScreen && !fishingScreen.destroyed) {
// Ensure fishingScreen is available
for (var i = 0; i < GAME_CONFIG.LANES.length; i++) {
var laneY = GAME_CONFIG.LANES[i].y;
var leftBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
x: bracketAssetWidth / 2,
// Position its center so left edge is at 0
y: laneY,
height: bracketAssetHeight
}));
var rightBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
scaleX: -1,
// Flipped horizontally
x: 2048 - bracketAssetWidth / 2,
// Position its center so right edge is at 2048
y: laneY,
height: bracketAssetHeight
}));
laneBrackets.push({
left: leftBracket,
right: rightBracket
});
}
}
// Start music
var songConfig = GameState.getCurrentSongConfig();
var musicIdToPlay = songConfig.musicId || 'rhythmTrack'; // Default to rhythmTrack if no specific id
GameState.currentPlayingMusicId = musicIdToPlay;
// Determine initial volume based on known assets for correct fade-out later
if (musicIdToPlay === 'morningtide') {
GameState.currentPlayingMusicInitialVolume = 1.0; // Volume defined in LK.init.music for 'morningtide'
} else {
// Default for 'rhythmTrack' or other unspecified tracks
GameState.currentPlayingMusicInitialVolume = 0.8; // Volume defined in LK.init.music for 'rhythmTrack'
}
LK.playMusic(GameState.currentPlayingMusicId); // Play the selected music track
}
function spawnFish(currentTimeForRegistration, options) {
options = options || {}; // Ensure options is an object
var depthConfig = GameState.getCurrentDepthConfig();
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
// Proximity check: Skip spawn if too close to existing fish.
// This check is generally for the first fish of a beat or non-forced spawns.
// For forced multi-beat spawns, this might prevent them if they are too close.
// Consider if this rule should be relaxed for forced multi-beat spawns if visual overlap is acceptable for quick succession.
// For now, keeping it as is. If a spawn is skipped, the multi-beat sequence might be shorter.
var isFirstFishOfBeat = !options.laneIndexToUse && !options.forcedSpawnSide;
if (isFirstFishOfBeat) {
// Apply stricter proximity for non-forced spawns
for (var i = 0; i < fishArray.length; i++) {
var existingFish = fishArray[i];
if (Math.abs(existingFish.x - GAME_CONFIG.SCREEN_CENTER_X) < PatternGenerator.minDistanceBetweenFish) {
return null; // Skip this spawn, do not register
}
}
}
var laneIndex;
if (options.laneIndexToUse !== undefined) {
laneIndex = options.laneIndexToUse;
PatternGenerator.lastLane = laneIndex; // Update generator's state if lane is forced
} else {
laneIndex = PatternGenerator.getNextLane();
}
var targetLane = GAME_CONFIG.LANES[laneIndex];
var fishType, fishValue;
var rand = Math.random();
if (rand < pattern.rareSpawnChance) {
fishType = 'rare';
fishValue = depthConfig.fishValue * 4;
} else if (GameState.selectedDepth >= 2 && rand < 0.3) {
fishType = 'deep';
fishValue = Math.floor(depthConfig.fishValue * 2);
} else if (GameState.selectedDepth >= 1 && rand < 0.6) {
fishType = 'medium';
fishValue = Math.floor(depthConfig.fishValue * 1.5);
} else {
fishType = 'shallow';
fishValue = depthConfig.fishValue;
}
var fishSpeedValue = depthConfig.fishSpeed;
var spawnSide; // -1 for left, 1 for right
var actualFishSpeed;
if (options.forcedSpawnSide !== undefined) {
spawnSide = options.forcedSpawnSide;
} else {
spawnSide = Math.random() < 0.5 ? -1 : 1;
}
actualFishSpeed = Math.abs(fishSpeedValue) * spawnSide;
var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex);
newFish.spawnSide = spawnSide; // Store the side it spawned from
newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen
newFish.y = targetLane.y;
newFish.baseY = targetLane.y; // Set baseY for swimming animation
fishArray.push(newFish);
fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTimeForRegistration);
return newFish;
}
function checkCatch(fishLane) {
var hookX = fishingElements.hook.x;
if (GameState.tutorialMode) {
var tutorialFish = GameState.tutorialFish;
if (!tutorialFish || tutorialFish.lane !== fishLane || tutorialFish.caught || tutorialFish.missed) {
if (GameState.tutorialStep === 2 || GameState.tutorialStep === 3) {
setTutorialText("Oops! Make sure to tap when the fish is in the correct lane and under the hook. Tap 'CONTINUE' to try again.");
GameState.tutorialPaused = true;
}
LK.getSound('miss').play();
return;
}
var distance = Math.abs(tutorialFish.x - hookX);
var caughtType = null;
if (distance < GAME_CONFIG.PERFECT_WINDOW) {
caughtType = 'perfect';
} else if (distance < GAME_CONFIG.GOOD_WINDOW) {
caughtType = 'good';
} else if (distance < GAME_CONFIG.MISS_WINDOW) {
// For tutorial, make 'miss window' taps still count as 'good' to be more forgiving
caughtType = 'good';
} else {
caughtType = 'miss';
}
showFeedback(caughtType, fishLane); // Show visual feedback based on derived type
if (caughtType === 'perfect' || caughtType === 'good') {
tutorialFish.catchFish();
var fishIndex = fishArray.indexOf(tutorialFish);
if (fishIndex > -1) {
fishArray.splice(fishIndex, 1);
}
// No points/money in tutorial normally, but can add if desired.
LK.getSound('catch').play();
animateHookCatch();
GameState.tutorialPaused = true; // Pause to show message
if (GameState.tutorialStep === 2) {
setTutorialText("Great catch! That's how you do it. Tap 'CONTINUE'.");
GameState.tutorialStep = 3; // Advance to next conceptual phase (Perfect/Good explanation)
} else if (GameState.tutorialStep === 3) {
setTutorialText("Nice one! You're getting the hang of timing. Tap 'CONTINUE'.");
GameState.tutorialStep = 4; // Advance to Combo explanation
}
} else {
// Miss
// Feedback 'miss' was already shown
LK.getSound('miss').play();
tutorialFish.missed = true;
GameState.tutorialPaused = true;
setTutorialText("Almost! Try to tap when the fish is closer. Tap 'CONTINUE' to try this part again.");
// The runTutorialStep logic on "CONTINUE" will handle respawning for this step.
}
return; // Tutorial catch logic finished
}
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];
// Ensure fish is not caught, not already missed, and in the correct lane
if (!fish.caught && !fish.missed && fish.lane === fishLane) {
var distance = Math.abs(fish.x - hookX);
if (distance < closestDistance) {
closestDistance = distance;
closestFishInLane = fish;
}
}
}
if (!closestFishInLane) {
// No fish found for tap
// Play miss sound
LK.getSound('miss').play();
// Tint incorrect lane indicators red briefly using tween
if (laneBrackets && laneBrackets[fishLane]) {
var leftBracket = laneBrackets[fishLane].left;
var rightBracket = laneBrackets[fishLane].right;
var tintToRedDuration = 50; // Duration to tween to red (ms)
var holdRedDuration = 100; // How long it stays fully red (ms)
var tintToWhiteDuration = 150; // Duration to tween back to white (ms)
if (leftBracket && !leftBracket.destroyed) {
// Tween to red
tween(leftBracket, {
tint: 0xFF0000
}, {
duration: tintToRedDuration,
easing: tween.linear,
onFinish: function onFinish() {
// After tinting to red, wait, then tween back to white
LK.setTimeout(function () {
if (leftBracket && !leftBracket.destroyed) {
tween(leftBracket, {
tint: 0xFFFFFF
}, {
duration: tintToWhiteDuration,
easing: tween.linear
});
}
}, holdRedDuration);
}
});
}
if (rightBracket && !rightBracket.destroyed) {
// Tween to red
tween(rightBracket, {
tint: 0xFF0000
}, {
duration: tintToRedDuration,
easing: tween.linear,
onFinish: function onFinish() {
// After tinting to red, wait, then tween back to white
LK.setTimeout(function () {
if (rightBracket && !rightBracket.destroyed) {
tween(rightBracket, {
tint: 0xFFFFFF
}, {
duration: tintToWhiteDuration,
easing: tween.linear
});
}
}, holdRedDuration);
}
});
}
}
GameState.combo = 0;
return;
}
// --- Normal Fish Catch Logic ---
var points = 0;
var multiplier = Math.max(1, Math.floor(GameState.combo / 10) + 1);
if (closestDistance < GAME_CONFIG.PERFECT_WINDOW) {
points = closestFishInLane.value * 2 * multiplier;
showFeedback('perfect', fishLane);
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) {
points = closestFishInLane.value * multiplier;
showFeedback('good', fishLane);
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.MISS_WINDOW) {
points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier));
showFeedback('good', fishLane);
GameState.combo++;
} else {
showFeedback('miss', fishLane);
LK.getSound('miss').play();
GameState.combo = 0;
// Mark the specific fish that was tapped but missed
if (closestFishInLane) {
closestFishInLane.missed = true;
}
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;
GameState.tutorialMode = false; // Ensure tutorial mode is off
// Stop the boat's wave animation to prevent it from running after the session
if (fishingElements && fishingElements.boat) {
tween.stop(fishingElements.boat);
}
// Stop fisherman container animation
if (fishingElements && fishingElements.fishermanContainer) {
tween.stop(fishingElements.fishermanContainer);
}
// Stop fisherman rotation animation
if (fishingElements && fishingElements.fisherman) {
tween.stop(fishingElements.fisherman);
}
// Stop water surface wave animations
if (fishingElements && fishingElements.waterSurfaceSegments) {
fishingElements.waterSurfaceSegments.forEach(function (segment) {
if (segment && !segment.destroyed) {
tween.stop(segment);
}
});
}
// Fade out and stop music for the results screen
var currentMusicId = GameState.currentPlayingMusicId;
var currentMusicInitialVolume = GameState.currentPlayingMusicInitialVolume;
var fadeOutDurationMs = 1000; // Duration of the fade out in milliseconds (e.g., 1 second)
LK.playMusic(currentMusicId, {
fade: {
start: currentMusicInitialVolume,
end: 0,
duration: fadeOutDurationMs
},
loop: false // Important to prevent restart after fade or if it's re-triggered
});
// Ensure music is fully stopped after the fade duration has elapsed
LK.setTimeout(function () {
LK.stopMusic();
}, fadeOutDurationMs);
ImprovedRhythmSpawner.reset();
// Clear lane brackets
if (laneBrackets && laneBrackets.length > 0) {
laneBrackets.forEach(function (bracketPair) {
if (bracketPair.left && !bracketPair.left.destroyed) {
bracketPair.left.destroy();
}
if (bracketPair.right && !bracketPair.right.destroyed) {
bracketPair.right.destroy();
}
});
laneBrackets = [];
}
// Clear fish
fishArray.forEach(function (fish) {
fish.destroy();
});
fishArray = [];
GameState.musicNotesActive = false;
if (fishingElements && fishingElements.musicNotesContainer) {
fishingElements.musicNotesContainer.removeChildren();
}
// The MusicNoteParticle instances themselves will be garbage collected.
// Clearing the array is important.
musicNotesArray = [];
// Clear ocean bubbles
if (globalOceanBubbleContainer) {
globalOceanBubbleContainer.removeChildren();
}
globalOceanBubblesArray = [];
// Clear seaweed
if (globalSeaweedContainer) {
globalSeaweedContainer.removeChildren();
}
globalSeaweedArray = [];
// Clear clouds
if (globalCloudContainer) {
globalCloudContainer.removeChildren();
}
globalCloudArray = [];
// Create results screen
createResultsScreen();
showScreen('results');
}
function createResultsScreen() {
// Clear previous results
resultsScreen.removeChildren();
var resultsBg = resultsScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 0.9,
height: 2732
}));
var title = new Text2('Fishing Complete!', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.x = GAME_CONFIG.SCREEN_CENTER_X;
title.y = 400;
resultsScreen.addChild(title);
var scoreResult = new Text2('Score: ' + GameState.sessionScore, {
size: 70,
fill: 0xFFD700
});
scoreResult.anchor.set(0.5, 0.5);
scoreResult.x = GAME_CONFIG.SCREEN_CENTER_X;
scoreResult.y = 550;
resultsScreen.addChild(scoreResult);
var fishResult = new Text2('Fish Caught: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned, {
size: 50,
fill: 0xFFFFFF
});
fishResult.anchor.set(0.5, 0.5);
fishResult.x = GAME_CONFIG.SCREEN_CENTER_X;
fishResult.y = 650;
resultsScreen.addChild(fishResult);
var comboResult = new Text2('Max Combo: ' + GameState.maxCombo, {
size: 50,
fill: 0xFF9800
});
comboResult.anchor.set(0.5, 0.5);
comboResult.x = GAME_CONFIG.SCREEN_CENTER_X;
comboResult.y = 750;
resultsScreen.addChild(comboResult);
var moneyEarned = new Text2('Money Earned: $' + GameState.sessionScore, {
size: 50,
fill: 0x4CAF50
});
moneyEarned.anchor.set(0.5, 0.5);
moneyEarned.x = GAME_CONFIG.SCREEN_CENTER_X;
moneyEarned.y = 850;
resultsScreen.addChild(moneyEarned);
// Accuracy
var accuracy = GameState.sessionFishSpawned > 0 ? Math.round(GameState.sessionFishCaught / GameState.sessionFishSpawned * 100) : 0;
var accuracyResult = new Text2('Accuracy: ' + accuracy + '%', {
size: 50,
fill: 0x2196F3
});
accuracyResult.anchor.set(0.5, 0.5);
accuracyResult.x = GAME_CONFIG.SCREEN_CENTER_X;
accuracyResult.y = 950;
resultsScreen.addChild(accuracyResult);
// Continue button
var continueButton = resultsScreen.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1200
}));
var continueText = new Text2('CONTINUE', {
size: 50,
fill: 0xFFFFFF
});
continueText.anchor.set(0.5, 0.5);
continueText.x = GAME_CONFIG.SCREEN_CENTER_X;
continueText.y = 1200;
resultsScreen.addChild(continueText);
// Fade in
resultsScreen.alpha = 0;
tween(resultsScreen, {
alpha: 1
}, {
duration: 500,
easing: tween.easeOut
});
}
/****
* Input Handling
****/
game.down = function (x, y, obj) {
LK.getSound('buttonClick').play();
var currentScreen = GameState.currentScreen;
// If tutorial mode is active, treat as 'tutorial' screen for input
if (GameState.tutorialMode && (currentScreen === 'fishing' || currentScreen === 'tutorial')) {
// New case for tutorial input
if (tutorialOverlayContainer.visible && tutorialContinueButton && tutorialContinueButton.visible && x >= tutorialContinueButton.x - tutorialContinueButton.width / 2 && x <= tutorialContinueButton.x + tutorialContinueButton.width / 2 && y >= tutorialContinueButton.y - tutorialContinueButton.height / 2 && y <= tutorialContinueButton.y + tutorialContinueButton.height / 2) {
LK.getSound('buttonClick').play();
// If tutorial was paused for a message after a miss in a catch step, re-run current step.
if (GameState.tutorialPaused && (GameState.tutorialStep === 2 || GameState.tutorialStep === 3) && GameState.tutorialFish && GameState.tutorialFish.missed) {
// Fish was missed, button click should retry the step
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish = null;
runTutorialStep(); // Re-runs current step to spawn new fish
} else {
GameState.tutorialStep++;
runTutorialStep();
}
} else if ((GameState.tutorialStep === 2 || GameState.tutorialStep === 3) && !GameState.tutorialPaused) {
// Catch attempt by tapping screen, not the continue button
handleFishingInput(x, y, true); // True for isDown
}
return;
}
switch (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 tutorialButtonGfx = titleElements.tutorialButtonGfx || titleElements.tutorialButton; // Compatibility
if (x >= tutorialButtonGfx.x - tutorialButtonGfx.width / 2 && x <= tutorialButtonGfx.x + tutorialButtonGfx.width / 2 && y >= tutorialButtonGfx.y - tutorialButtonGfx.height / 2 && y <= tutorialButtonGfx.y + tutorialButtonGfx.height / 2) {
// Defensive: check if startTutorial is defined before calling
if (typeof startTutorial === "function") {
startTutorial();
}
}
break;
case 'levelSelect':
handleLevelSelectInput(x, y);
break;
case 'fishing':
handleFishingInput(x, y, true); // true for isDown
break;
case 'results':
showScreen('levelSelect');
break;
}
};
function handleLevelSelectInput(x, y) {
var elements = levelSelectElements;
// Check depth tabs
elements.depthTabs.forEach(function (tab) {
var tabAsset = tab.tab;
if (x >= tabAsset.x - tabAsset.width / 2 && x <= tabAsset.x + tabAsset.width / 2 && y >= tabAsset.y - tabAsset.height / 2 && y <= tabAsset.y + tabAsset.height / 2) {
GameState.selectedDepth = tab.depthIndex;
GameState.selectedSong = 0; // Reset to first song
updateLevelSelectScreen();
}
});
// Check song navigation
var leftArrow = elements.leftArrow;
if (x >= leftArrow.x - leftArrow.width / 2 && x <= leftArrow.x + leftArrow.width / 2 && y >= leftArrow.y - leftArrow.height / 2 && y <= leftArrow.y + leftArrow.height / 2 && GameState.selectedSong > 0) {
GameState.selectedSong--;
updateSongDisplay();
}
var rightArrow = elements.rightArrow;
if (x >= rightArrow.x - rightArrow.width / 2 && x <= rightArrow.x + rightArrow.width / 2 && y >= rightArrow.y - rightArrow.height / 2 && y <= rightArrow.y + rightArrow.height / 2) {
var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth];
if (GameState.selectedSong < depth.songs.length - 1) {
GameState.selectedSong++;
updateSongDisplay();
}
}
// Check play/buy button
var playButton = elements.playButton;
if (x >= playButton.x - playButton.width / 2 && x <= playButton.x + playButton.width / 2 && y >= playButton.y - playButton.height / 2 && y <= playButton.y + playButton.height / 2) {
var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong);
if (owned) {
showScreen('fishing');
} else {
// Try to buy song
if (GameState.buySong(GameState.selectedDepth, GameState.selectedSong)) {
updateLevelSelectScreen();
}
}
}
// Check shop button
var shopButton = elements.shopButton;
if (x >= shopButton.x - shopButton.width / 2 && x <= shopButton.x + shopButton.width / 2 && y >= shopButton.y - shopButton.height / 2 && y <= shopButton.y + shopButton.height / 2) {
if (GameState.upgrade()) {
LK.getSound('upgrade').play();
updateLevelSelectScreen();
}
}
// Check back button
var backButton = elements.backButton;
if (x >= backButton.x - backButton.width / 2 && x <= backButton.x + backButton.width / 2 && y >= backButton.y - backButton.height / 2 && y <= backButton.y + backButton.height / 2) {
showScreen('title');
}
}
/****
* Main Game Loop
****/
game.update = function () {
// Always update fishing line wave visuals if on fishing screen and elements are ready.
// This needs to run even during the intro when gameActive might be false.
if (GameState.currentScreen === 'fishing' && fishingElements && fishingElements.updateFishingLineWave) {
fishingElements.updateFishingLineWave();
}
// Update title screen ambient particles if title screen is active
if (GameState.currentScreen === 'title') {
// Add this to the game.update function, in the title screen section:
if (GameState.currentScreen === 'title' && titleElements && titleElements.updateTitleFishingLineWave) {
titleElements.updateTitleFishingLineWave();
}
// Title Screen Ocean Bubbles
if (titleScreenOceanBubbleContainer) {
titleScreenOceanBubbleSpawnCounter++;
if (titleScreenOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) {
// Use same interval as fishing
titleScreenOceanBubbleSpawnCounter = 0;
var newOceanBubble = new OceanBubbleParticle(); // Assuming OceanBubbleParticle is general enough
titleScreenOceanBubbleContainer.addChild(newOceanBubble);
titleScreenOceanBubblesArray.push(newOceanBubble);
}
for (var obIdx = titleScreenOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) {
var oceanBubble = titleScreenOceanBubblesArray[obIdx];
if (oceanBubble) {
oceanBubble.update(); // No fish interaction on title screen
if (oceanBubble.isDone) {
oceanBubble.destroy();
titleScreenOceanBubblesArray.splice(obIdx, 1);
}
} else {
titleScreenOceanBubblesArray.splice(obIdx, 1);
}
}
}
// Title Screen Seaweed
if (titleScreenSeaweedContainer) {
titleScreenSeaweedSpawnCounter++;
if (titleScreenSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && titleScreenSeaweedArray.length < MAX_SEAWEED_COUNT) {
titleScreenSeaweedSpawnCounter = 0;
var newSeaweed = new SeaweedParticle();
titleScreenSeaweedContainer.addChild(newSeaweed);
titleScreenSeaweedArray.push(newSeaweed);
}
for (var swIdx = titleScreenSeaweedArray.length - 1; swIdx >= 0; swIdx--) {
var seaweed = titleScreenSeaweedArray[swIdx];
if (seaweed) {
seaweed.update(); // No fish interaction on title screen
if (seaweed.isDone) {
seaweed.destroy();
titleScreenSeaweedArray.splice(swIdx, 1);
}
} else {
titleScreenSeaweedArray.splice(swIdx, 1);
}
}
}
// Title Screen Clouds
if (titleScreenCloudContainer) {
titleScreenCloudSpawnCounter++;
if (titleScreenCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && titleScreenCloudArray.length < MAX_CLOUD_COUNT) {
titleScreenCloudSpawnCounter = 0;
var newCloud = new CloudParticle();
titleScreenCloudContainer.addChild(newCloud);
titleScreenCloudArray.push(newCloud);
}
for (var cldIdx = titleScreenCloudArray.length - 1; cldIdx >= 0; cldIdx--) {
var cloud = titleScreenCloudArray[cldIdx];
if (cloud) {
cloud.update();
if (cloud.isDone) {
cloud.destroy();
titleScreenCloudArray.splice(cldIdx, 1);
}
} else {
titleScreenCloudArray.splice(cldIdx, 1);
}
}
}
}
// Spawn and update ambient ocean bubbles during intro and gameplay (fishing screen)
if (GameState.currentScreen === 'fishing' && globalOceanBubbleContainer) {
// Spawn bubbles during intro and gameplay
globalOceanBubbleSpawnCounter++;
if (globalOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) {
globalOceanBubbleSpawnCounter = 0;
var numToSpawn = 1; // Always spawn only 1 bubble per interval (was 1 or 2)
for (var i = 0; i < numToSpawn; i++) {
var newOceanBubble = new OceanBubbleParticle();
globalOceanBubbleContainer.addChild(newOceanBubble);
globalOceanBubblesArray.push(newOceanBubble);
}
}
// Update existing ocean bubbles
for (var obIdx = globalOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) {
var oceanBubble = globalOceanBubblesArray[obIdx];
if (oceanBubble) {
// Apply fish physics to bubble
for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) {
var fish = fishArray[fishIdx];
if (fish && !fish.caught) {
var dx = oceanBubble.x - fish.x;
var dy = oceanBubble.y - fish.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var influenceRadius = 150; // Radius of fish influence on bubbles
var minDistance = 30; // Minimum distance to avoid division issues
if (distance < influenceRadius && distance > minDistance) {
// Calculate influence strength (stronger when closer)
var influence = 1 - distance / influenceRadius;
influence = influence * influence; // Square for more dramatic close-range effect
// Calculate normalized direction away from fish
var dirX = dx / distance;
var dirY = dy / distance;
// Apply force based on fish speed and direction
var fishSpeedFactor = Math.abs(fish.speed) * 0.15; // Scale down fish speed influence
var pushForce = fishSpeedFactor * influence;
// Add horizontal push (stronger in direction of fish movement)
oceanBubble.x += dirX * pushForce * 2; // Stronger horizontal push
// Add vertical component (bubbles get pushed up/down)
oceanBubble.vy += dirY * pushForce * 0.5; // Gentler vertical influence
// Add some swirl/turbulence to drift
oceanBubble.driftAmplitude = Math.min(80, oceanBubble.driftAmplitude + pushForce * 10);
oceanBubble.driftFrequency *= 1 + influence * 0.1; // Slightly increase oscillation when disturbed
}
}
}
oceanBubble.update();
if (oceanBubble.isDone) {
oceanBubble.destroy();
globalOceanBubblesArray.splice(obIdx, 1);
}
} else {
globalOceanBubblesArray.splice(obIdx, 1); // Safeguard for null entries
}
}
}
// Spawn and update seaweed particles during intro and gameplay
if (GameState.currentScreen === 'fishing' && globalSeaweedContainer) {
// Spawn seaweed during intro and gameplay
globalSeaweedSpawnCounter++;
if (globalSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && globalSeaweedArray.length < MAX_SEAWEED_COUNT) {
globalSeaweedSpawnCounter = 0;
var newSeaweed = new SeaweedParticle();
globalSeaweedContainer.addChild(newSeaweed);
globalSeaweedArray.push(newSeaweed);
}
// Update existing seaweed
for (var swIdx = globalSeaweedArray.length - 1; swIdx >= 0; swIdx--) {
var seaweed = globalSeaweedArray[swIdx];
if (seaweed) {
// Apply fish physics to seaweed
for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) {
var fish = fishArray[fishIdx];
if (fish && !fish.caught) {
var dx = seaweed.x - fish.x;
var dy = seaweed.y - fish.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var influenceRadius = 180; // Slightly larger influence for seaweed
var minDistance = 40;
if (distance < influenceRadius && distance > minDistance) {
// Calculate influence strength
var influence = 1 - distance / influenceRadius;
influence = influence * influence;
// Calculate normalized direction away from fish
var dirX = dx / distance;
var dirY = dy / distance;
// Apply force based on fish speed
var fishSpeedFactor = Math.abs(fish.speed) * 0.2; // Stronger influence on seaweed
var pushForce = fishSpeedFactor * influence;
// Add push forces
seaweed.vx += dirX * pushForce * 1.5; // Seaweed is affected more horizontally
seaweed.vy += dirY * pushForce * 0.8; // And moderately vertically
// Increase sway when disturbed
seaweed.swayAmplitude = Math.min(60, seaweed.swayAmplitude + pushForce * 15);
}
}
}
seaweed.update();
if (seaweed.isDone) {
seaweed.destroy();
globalSeaweedArray.splice(swIdx, 1);
}
} else {
globalSeaweedArray.splice(swIdx, 1);
}
}
}
// Spawn and update cloud particles during intro and gameplay
if (GameState.currentScreen === 'fishing' && globalCloudContainer) {
// Spawn clouds during intro and gameplay
globalCloudSpawnCounter++;
if (globalCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && globalCloudArray.length < MAX_CLOUD_COUNT) {
globalCloudSpawnCounter = 0;
var newCloud = new CloudParticle();
globalCloudContainer.addChild(newCloud);
globalCloudArray.push(newCloud);
}
// Update existing clouds
for (var cldIdx = globalCloudArray.length - 1; cldIdx >= 0; cldIdx--) {
var cloud = globalCloudArray[cldIdx];
if (cloud) {
cloud.update();
if (cloud.isDone) {
cloud.destroy();
globalCloudArray.splice(cldIdx, 1);
}
} else {
globalCloudArray.splice(cldIdx, 1);
}
}
}
// Standard game active check; if intro is playing, gameActive will be false.
// Tutorial mode has its own logic path
if (GameState.currentScreen === 'fishing' && GameState.tutorialMode) {
// Update essential non-paused elements like fishing line wave
if (fishingElements && fishingElements.updateFishingLineWave) {
fishingElements.updateFishingLineWave();
}
// Update ambient particles (clouds, background bubbles, seaweed can run if desired)
// ... (Can copy particle update logic here if they should be active in tutorial)
if (!GameState.tutorialPaused) {
// Update tutorial fish if one exists and is active
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) {
GameState.tutorialFish.update();
checkTutorialFishState(); // Check its state (missed, off-screen)
}
// Hook follows tutorial fish
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) {
if (GameState.hookTargetLaneIndex !== GameState.tutorialFish.lane) {
GameState.hookTargetLaneIndex = GameState.tutorialFish.lane;
}
var targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
if (fishingElements.hook && Math.abs(fishingElements.hook.y - targetLaneY) > 1) {
// Smoother threshold
// fishingElements.hook.y = targetLaneY; // Instant for tutorial responsiveness
tween(fishingElements.hook, {
y: targetLaneY
}, {
duration: 100,
easing: tween.linear
});
fishingElements.hook.originalY = targetLaneY;
}
} else if (fishingElements.hook) {
// If no tutorial fish, hook stays in middle or last targeted lane
var middleLaneY = GAME_CONFIG.LANES[1].y;
if (fishingElements.hook.y !== middleLaneY) {
// tween(fishingElements.hook, { y: middleLaneY }, { duration: 150, easing: tween.easeOut });
// fishingElements.hook.originalY = middleLaneY;
}
}
}
updateLaneBracketsVisuals();
return; // End tutorial update logic
}
if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) {
return;
}
// Note: The fishing line wave update was previously here, it's now moved up.
var currentTime = LK.ticks * (1000 / 60);
// Initialize game timer
if (GameState.songStartTime === 0) {
GameState.songStartTime = currentTime;
}
// Check song end
var songConfig = GameState.getCurrentSongConfig();
if (currentTime - GameState.songStartTime >= songConfig.duration) {
endFishingSession();
return;
}
// Use RhythmSpawner to handle fish spawning
ImprovedRhythmSpawner.update(currentTime);
// Dynamic Hook Movement Logic
var approachingFish = null;
var minDistanceToCenter = Infinity;
for (var i = 0; i < fishArray.length; i++) {
var f = fishArray[i];
if (!f.caught) {
var distanceToHookX = Math.abs(f.x - fishingElements.hook.x);
var isApproachingOrAtHook = f.speed > 0 && f.x < fishingElements.hook.x || f.speed < 0 && f.x > fishingElements.hook.x || distanceToHookX < GAME_CONFIG.MISS_WINDOW * 2;
if (isApproachingOrAtHook && distanceToHookX < minDistanceToCenter) {
minDistanceToCenter = distanceToHookX;
approachingFish = f;
}
}
}
var targetLaneY;
if (approachingFish) {
if (GameState.hookTargetLaneIndex !== approachingFish.lane) {
GameState.hookTargetLaneIndex = approachingFish.lane;
}
targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
} else {
targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
}
// Update hook Y position (X is handled by the wave animation)
if (Math.abs(fishingElements.hook.y - targetLaneY) > 5) {
// Only tween if significantly different
tween(fishingElements.hook, {
y: targetLaneY
}, {
duration: 150,
easing: tween.easeOut
});
fishingElements.hook.originalY = targetLaneY;
} //{bO} // Re-using last relevant ID from replaced block for context if appropriate
updateLaneBracketsVisuals();
// Update fish
for (var i = fishArray.length - 1; i >= 0; i--) {
var fish = fishArray[i];
var previousFrameX = fish.lastX; // X position from the end of the previous game tick
fish.update(); // Fish updates its own movement (fish.x) and appearance
var currentFrameX = fish.x; // X position after this tick's update
// Check for miss only if fish is active (not caught, not already missed)
if (!fish.caught && !fish.missed) {
var hookCenterX = fishingElements.hook.x;
// GAME_CONFIG.MISS_WINDOW is the distance from hook center that still counts as a "miss" tap.
// If fish center passes this boundary, it's considered a full pass.
var missCheckBoundary = GAME_CONFIG.MISS_WINDOW;
if (fish.speed > 0) {
// Moving Left to Right
// Fish's center (previousFrameX) was to the left of/at the hook's right miss boundary,
// and its center (currentFrameX) is now to the right of it.
if (previousFrameX <= hookCenterX + missCheckBoundary && currentFrameX > hookCenterX + missCheckBoundary) {
showFeedback('miss', fish.lane);
LK.getSound('miss').play();
GameState.combo = 0;
fish.missed = true; // Mark as missed
}
} else if (fish.speed < 0) {
// Moving Right to Left
// Fish's center (previousFrameX) was to the right of/at the hook's left miss boundary,
// and its center (currentFrameX) is now to the left of it.
if (previousFrameX >= hookCenterX - missCheckBoundary && currentFrameX < hookCenterX - missCheckBoundary) {
showFeedback('miss', fish.lane);
LK.getSound('miss').play();
GameState.combo = 0;
fish.missed = true; // Mark as missed
}
}
}
// Update lastX for the next frame, using the fish's position *after* its update this frame
fish.lastX = currentFrameX;
// Remove off-screen fish (if not caught).
// If a fish is `missed`, it's also `!caught`, so it will be removed by this logic.
if (!fish.caught && (fish.x < -250 || fish.x > 2048 + 250)) {
// Increased buffer slightly
fish.destroy();
fishArray.splice(i, 1);
}
}
// Update UI
updateFishingUI();
// Spawn and update music notes if active
if (GameState.musicNotesActive && fishingElements && fishingElements.hook && !fishingElements.hook.destroyed && musicNotesContainer) {
musicNoteSpawnCounter++;
if (musicNoteSpawnCounter >= MUSIC_NOTE_SPAWN_INTERVAL_TICKS) {
musicNoteSpawnCounter = 0;
// Spawn notes from the fishing hook's position
var spawnX = fishingElements.hook.x;
var spawnY = fishingElements.hook.y - 30; // Spawn slightly above the hook's center for better visual origin
var newNote = new MusicNoteParticle(spawnX, spawnY);
musicNotesContainer.addChild(newNote);
musicNotesArray.push(newNote);
// Add scale pulse to the hook, synced with BPM
if (fishingElements.hook && !fishingElements.hook.destroyed && fishingElements.hook.scale) {
var currentSongConfig = GameState.getCurrentSongConfig();
var bpm = currentSongConfig && currentSongConfig.bpm ? currentSongConfig.bpm : 90; // Default to 90 BPM
var beatDurationMs = 60000 / bpm;
var pulsePhaseDuration = Math.max(50, beatDurationMs / 2); // Each phase (up/down) is half a beat, min 50ms
var pulseScaleFactor = 1.2;
// Ensure we have valid original scales, defaulting to 1 if undefined
var originalScaleX = fishingElements.hook.scale.x !== undefined ? fishingElements.hook.scale.x : 1;
var originalScaleY = fishingElements.hook.scale.y !== undefined ? fishingElements.hook.scale.y : 1;
// Stop any previous scale tweens on the hook to prevent conflicts
tween.stop(fishingElements.hook.scale);
tween(fishingElements.hook.scale, {
x: originalScaleX * pulseScaleFactor,
y: originalScaleY * pulseScaleFactor
}, {
duration: pulsePhaseDuration,
easing: tween.easeOut,
onFinish: function onFinish() {
if (fishingElements.hook && !fishingElements.hook.destroyed && fishingElements.hook.scale) {
tween(fishingElements.hook.scale, {
x: originalScaleX,
y: originalScaleY
}, {
duration: pulsePhaseDuration,
easing: tween.easeIn // Or tween.easeOut for a softer return
});
}
}
});
}
}
}
// Update existing music notes
for (var mnIdx = musicNotesArray.length - 1; mnIdx >= 0; mnIdx--) {
var note = musicNotesArray[mnIdx];
if (note) {
note.update();
if (note.isDone) {
note.destroy();
musicNotesArray.splice(mnIdx, 1);
}
} else {
// Should not happen, but good to safeguard
musicNotesArray.splice(mnIdx, 1);
}
}
// Spawn bubbles for active fish
if (bubbleContainer) {
for (var f = 0; f < fishArray.length; f++) {
var fish = fishArray[f];
if (fish && !fish.caught && !fish.isHeld && fish.fishGraphics) {
if (currentTime - fish.lastBubbleSpawnTime > fish.bubbleSpawnInterval) {
fish.lastBubbleSpawnTime = currentTime;
// Calculate tail position based on fish direction and width
// fish.fishGraphics.width is the original asset width.
// fish.fishGraphics.scale.x might be negative, but width property itself is positive.
// The anchor is 0.5, so width/2 is distance from center to edge.
var tailOffsetDirection = Math.sign(fish.speed) * -1; // Bubbles appear opposite to movement direction
var bubbleX = fish.x + tailOffsetDirection * (fish.fishGraphics.width * Math.abs(fish.fishGraphics.scaleX) / 2) * 0.8; // 80% towards tail
var bubbleY = fish.y + (Math.random() - 0.5) * (fish.fishGraphics.height * Math.abs(fish.fishGraphics.scaleY) / 4); // Slight Y variance around fish center
var newBubble = new BubbleParticle(bubbleX, bubbleY);
bubbleContainer.addChild(newBubble);
bubblesArray.push(newBubble);
}
}
}
}
// Update and remove bubbles
for (var b = bubblesArray.length - 1; b >= 0; b--) {
var bubble = bubblesArray[b];
if (bubble) {
// Extra safety check
bubble.update();
if (bubble.isDone) {
bubble.destroy();
bubblesArray.splice(b, 1);
}
} else {
// If a null/undefined somehow got in
bubblesArray.splice(b, 1);
}
}
};
// Initialize game
showScreen('title');
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