/**** * 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: 1, upgradeCost: 0, // Starting depth songs: [{ name: "Gentle Waves", bpm: 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' }, { name: "Sunny Afternoon", bpm: 97, duration: 181800, // 3:01.8 pattern: "sunny_afternoon_custom", cost: 0, musicId: 'sunnyafternoon' }] }, { level: 2, name: "Mid Waters", fishSpeed: 7, fishValue: 2, 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" }] }, sunny_afternoon_custom: { beatsPerFish: 1.3, // Slightly faster than morning_tide_custom but still beginner-friendly doubleSpawnChance: 0.08, // Moderate chance for variety rareSpawnChance: 0.025, // Slightly better rewards than gentle_waves sections: [ // Gentle warm-up (0-20 seconds) { startTime: 0, endTime: 20000, spawnModifier: 0.8, description: "warm_sunny_start" }, // First activity burst (20-35 seconds) { startTime: 20000, endTime: 35000, spawnModifier: 1.4, description: "first_sunny_burst" }, // Breathing room (35-50 seconds) { startTime: 35000, endTime: 50000, spawnModifier: 0.7, description: "sunny_breather_1" }, // Second activity burst (50-70 seconds) { startTime: 50000, endTime: 70000, spawnModifier: 1.5, description: "second_sunny_burst" }, // Extended breathing room (70-90 seconds) { startTime: 70000, endTime: 90000, spawnModifier: 0.6, description: "sunny_breather_2" }, // Third activity burst (90-110 seconds) { startTime: 90000, endTime: 110000, spawnModifier: 1.3, description: "third_sunny_burst" }, // Breathing room (110-125 seconds) { startTime: 110000, endTime: 125000, spawnModifier: 0.8, description: "sunny_breather_3" }, // Final activity section (125-150 seconds) { startTime: 125000, endTime: 150000, spawnModifier: 1.2, description: "sunny_finale_buildup" }, // Wind down (150-181.8 seconds) { startTime: 150000, endTime: 181800, spawnModifier: 0.9, description: "sunny_afternoon_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 = 500; // Just 0.5 seconds buffer 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(); // 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); } }, 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 = Math.floor(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 = Math.floor(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: 450, // Increased height color: 0x000000, anchorX: 0.5, anchorY: 0.5, alpha: 0.75 })); tutorialTextDisplay = tutorialOverlayContainer.addChild(new Text2('', { size: 55, // Increased font size fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 1700, // Keep wordWrapWidth, adjust if lines are too cramped // Slightly less than background width align: 'center', lineHeight: 65 // Increased line height })); tutorialTextDisplay.anchor.set(0.5, 0.5); tutorialTextDisplay.x = tutorialTextBackground.x; tutorialTextDisplay.y = tutorialTextBackground.y - 50; // Adjusted Y for new background height and text size tutorialContinueButton = tutorialOverlayContainer.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: tutorialTextBackground.x, y: tutorialTextBackground.y + tutorialTextBackground.height / 2 - 55, // Adjusted Y for new background height // 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 = Math.floor(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" // Clear lane highlights from previous step if any if (tutorialLaneHighlights.length > 0) { tutorialLaneHighlights.forEach(function (overlay) { if (overlay && !overlay.destroyed) { overlay.destroy(); } }); tutorialLaneHighlights = []; } if (tutorialContinueButton) { tutorialContinueButton.visible = true; } // Default to visible if (tutorialContinueText) { tutorialContinueText.visible = true; } // Clear previous tutorial fish unless current step needs it (now steps 3 and 4) if (GameState.tutorialFish && GameState.tutorialStep !== 3 && GameState.tutorialStep !== 4) { 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 // Adjusted max step for animations to 8 (new end tutorial step is 7) if (fishingElements) { if (typeof fishingElements.startWaterSurfaceAnimation === 'function' && GameState.tutorialStep < 8) { fishingElements.startWaterSurfaceAnimation(); } if (typeof fishingElements.startBoatAndFishermanAnimation === 'function' && GameState.tutorialStep < 8) { fishingElements.startBoatAndFishermanAnimation(); } if (fishingElements.hook && GameState.tutorialStep < 8) { 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 Explanation setTutorialText("This is your hook. It automatically moves to the lane with the closest approaching 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: // Lanes & Tapping Explanation with Overlay setTutorialText("Fish swim in three lanes. When a fish is over the hook in its lane, TAP THE SCREEN in that lane to catch it. The lanes are highlighted. Tap 'CONTINUE'."); // Add highlights behind the text box elements for (var i = 0; i < GAME_CONFIG.LANES.length; i++) { var laneYPos = GAME_CONFIG.LANES[i].y; var highlight = LK.getAsset('laneHighlight', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: laneYPos, alpha: 0.25, // Semi-transparent width: 1800, height: 200 }); tutorialOverlayContainer.addChildAt(highlight, 0); // Add at the bottom of the container tutorialLaneHighlights.push(highlight); } GameState.tutorialPaused = true; break; case 3: // Was case 2 // Fish Approaching & First Catch Attempt setTutorialText("A fish will now approach. Tap in its lane when it's under the hook!"); 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 4: // Was 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 5: // Was case 4 // Combos setTutorialText("Nice one! Catch fish consecutively to build a COMBO for bonus points! Tap 'CONTINUE'."); GameState.tutorialPaused = true; break; case 6: // Was 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 7: // Was case 6 // End Tutorial setTutorialText("You're all set! Tap 'CONTINUE' to go to the fishing spots!"); GameState.tutorialPaused = true; break; default: // Effectively step 8 // Tutorial finished GameState.tutorialMode = false; tutorialOverlayContainer.visible = false; if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) { GameState.tutorialFish.destroy(); var idxDefault = fishArray.indexOf(GameState.tutorialFish); if (idxDefault > -1) { fishArray.splice(idxDefault, 1); } GameState.tutorialFish = null; } // Clear lane highlights if any persist if (tutorialLaneHighlights.length > 0) { tutorialLaneHighlights.forEach(function (overlay) { if (overlay && !overlay.destroyed) { overlay.destroy(); } }); tutorialLaneHighlights = []; } // 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; // Ensure fishing screen is visible and setup for tutorial showScreen('fishing'); fishingScreen.alpha = 1; // Clear any active game elements from a previous session fishArray.forEach(function (f) { if (f && !f.destroyed) { f.destroy(); } }); fishArray = []; ImprovedRhythmSpawner.reset(); // Clear existing UI text if (fishingElements.scoreText) { fishingElements.scoreText.setText(''); } if (fishingElements.fishText) { fishingElements.fishText.setText(''); } if (fishingElements.comboText) { fishingElements.comboText.setText(''); } if (fishingElements.progressText) { fishingElements.progressText.setText(''); } // CREATE LANE BRACKETS FOR TUTORIAL var bracketAssetHeight = 150; var bracketAssetWidth = 75; // Clear any existing brackets first 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 = []; if (fishingScreen && !fishingScreen.destroyed) { 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, y: laneY, height: bracketAssetHeight })); var rightBracket = fishingScreen.addChild(LK.getAsset('lanebracket', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5, scaleX: -1, x: 2048 - bracketAssetWidth / 2, y: laneY, height: bracketAssetHeight })); laneBrackets.push({ left: leftBracket, right: rightBracket }); } } // 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 (now steps 3 or 4) if (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) { 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 === 3 || GameState.tutorialStep === 4; // Adjusted step numbers 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.5; // Approx 1821 var tutorialButtonY = startButtonY + 600; // Maintain 150px gap var startButton = titleScreen.addChild(LK.getAsset('startbutton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: startButtonY, alpha: 0 })); var tutorialButton = titleScreen.addChild(LK.getAsset('tutorialbutton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: tutorialButtonY, alpha: 0 })); // No assignment here; assign tutorialButtonGfx after createTitleScreen returns return { startButton: startButton, tutorialButton: tutorialButton, // This is the graphics object 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 })); // Top section - Title, Money, and Back button var title = new Text2('SELECT FISHING SPOT', { size: 90, // Increased from 80 fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.x = GAME_CONFIG.SCREEN_CENTER_X; title.y = 180; // Moved up slightly levelSelectScreen.addChild(title); // Money display (keep at top) var moneyDisplay = new Text2('Money: $0', { size: 70, // Increased from 60 fill: 0xFFD700 }); moneyDisplay.anchor.set(1, 0); moneyDisplay.x = 1900; moneyDisplay.y = 80; // Moved up slightly levelSelectScreen.addChild(moneyDisplay); // Back button (moved to bottom center) var backButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 2732 - 300, // Positioned 100px from the bottom edge (center of button) tint: 0x757575 })); var backButtonText = new Text2('BACK', { size: 45, // Increased from 40 fill: 0xFFFFFF }); backButtonText.anchor.set(0.5, 0.5); backButtonText.x = backButton.x; backButtonText.y = backButton.y; levelSelectScreen.addChild(backButtonText); // Depth tabs (moved down and spaced out more) var depthTabs = []; // Song display area (moved to center of screen) var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1100, // Moved down significantly from 700 width: 900, // Made wider height: 300 // Made taller })); // Song navigation arrows (repositioned around the larger song card) var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 350, // Moved further left y: 1100, // Moved down with song card tint: 0x666666, width: 120, // Made bigger height: 120 })); var leftArrowText = new Text2('<', { size: 80, // Increased from 60 fill: 0xFFFFFF }); leftArrowText.anchor.set(0.5, 0.5); leftArrowText.x = 350; leftArrowText.y = 1100; levelSelectScreen.addChild(leftArrowText); var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 1698, // Moved further right y: 1100, // Moved down with song card tint: 0x666666, width: 120, // Made bigger height: 120 })); var rightArrowText = new Text2('>', { size: 80, // Increased from 60 fill: 0xFFFFFF }); rightArrowText.anchor.set(0.5, 0.5); rightArrowText.x = 1698; rightArrowText.y = 1100; levelSelectScreen.addChild(rightArrowText); // Song info (repositioned within the larger song card area) var songTitle = new Text2('Song Title', { size: 60, // Increased from 50 fill: 0xFFFFFF }); songTitle.anchor.set(0.5, 0.5); songTitle.x = GAME_CONFIG.SCREEN_CENTER_X; songTitle.y = 1020; // Positioned above song card center levelSelectScreen.addChild(songTitle); var songInfo = new Text2('BPM: 120 | Duration: 2:00', { size: 40, // Increased from 30 fill: 0xCCCCCC }); songInfo.anchor.set(0.5, 0.5); songInfo.x = GAME_CONFIG.SCREEN_CENTER_X; songInfo.y = 1100; // Centered in song card levelSelectScreen.addChild(songInfo); var songEarnings = new Text2('Potential Earnings: $50-100', { size: 40, // Increased from 30 fill: 0x4CAF50 }); songEarnings.anchor.set(0.5, 0.5); songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X; songEarnings.y = 1180; // Positioned below song card center levelSelectScreen.addChild(songEarnings); // Play/Buy button (moved down and made larger) var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1400, // Moved down significantly from 900 width: 500, // Made wider height: 130 // Made taller })); var playButtonText = new Text2('PLAY', { size: 60, // Increased from 50 fill: 0xFFFFFF }); playButtonText.anchor.set(0.5, 0.5); playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; playButtonText.y = 1400; levelSelectScreen.addChild(playButtonText); // Shop button (moved to bottom and made larger) var shopButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1650, // Moved down significantly from 1100 tint: 0x666666, // Grayed out since it's not available width: 450, // Made wider height: 120 // Made taller })); var shopButtonText = new Text2('UPGRADE ROD', { size: 50, // Increased from 40 fill: 0xFFFFFF }); shopButtonText.anchor.set(0.5, 0.5); shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; shopButtonText.y = 1650; levelSelectScreen.addChild(shopButtonText); // Add "Coming soon!" text underneath var comingSoonText = new Text2('Coming soon!', { size: 35, fill: 0xFFD700 // Gold color to make it stand out }); comingSoonText.anchor.set(0.5, 0.5); comingSoonText.x = GAME_CONFIG.SCREEN_CENTER_X; comingSoonText.y = 1730; // Position below the button levelSelectScreen.addChild(comingSoonText); 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; var tutorialLaneHighlights = []; // To store lane highlight graphics for the tutorial // 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 (now steps 3 or 4) if (GameState.tutorialMode && isDown && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !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.tutorialButton); // 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; } // Clear lane highlights if any exist when switching screen away from tutorial if (tutorialLaneHighlights.length > 0) { tutorialLaneHighlights.forEach(function (overlay) { if (overlay && !overlay.destroyed) { overlay.destroy(); } }); tutorialLaneHighlights = []; } 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 = 6000; // 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.tutorialButton.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, onFinish: function onFinish() { // Fade in tutorial button AFTER start button finishes tween(titleElements.tutorialButton, { 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; // Play reel sound effect with 300ms delay during hook animation LK.setTimeout(function () { LK.getSound('reel').play(); }, 600); 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 (positioned in the middle area) var tabStartY = 600; // Moved down from 400 var tabSpacing = 250; // Increased spacing between tabs 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: 200 + i * tabSpacing, // Increased spacing y: tabStartY, tint: isSelected ? 0x1976d2 : 0x455a64, width: 400, // Made wider height: 160 // Made taller })); var tabText = new Text2(depth.name.split(' ')[0], { size: 40, // Increased from 30 fill: 0xFFFFFF }); tabText.anchor.set(0.5, 0.5); tabText.x = 200 + i * tabSpacing; tabText.y = tabStartY; 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; // Always show as disabled - coming soon elements.shopButtonText.setText('UPGRADE ROD'); elements.shopButton.tint = 0x666666; // Always grayed out } 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 = Math.floor(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 = Math.floor(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) { // Check if it's a critical catch step (now steps 3 or 4) if (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) { setTutorialText("Oops! Make sure to tap when the fish is in the correct lane and over 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 === 3) { // Was step 2 setTutorialText("Great catch! That's how you do it. Tap 'CONTINUE'."); // GameState.tutorialStep = 4; // Advance to next conceptual phase -- Handled by game.down } else if (GameState.tutorialStep === 4) { // Was step 3 setTutorialText("Nice one! You're getting the hang of timing. Tap 'CONTINUE'."); // GameState.tutorialStep = 5; // Advance to Combo explanation -- Handled by game.down } } 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 // Score pop-up animation if (points > 0) { var scorePopupText = new Text2('+' + points, { size: 140, // Slightly larger for impact fill: 0xFFD700, // Gold color for score align: 'center', stroke: 0x000000, // Black stroke strokeThickness: 6 // Thickness of the stroke }); scorePopupText.anchor.set(0.5, 0.5); scorePopupText.x = GAME_CONFIG.SCREEN_CENTER_X; // Centered with the boat scorePopupText.y = GAME_CONFIG.BOAT_Y - 70; // Start slightly above the boat's deck line // Add to fishingScreen so it's part of the game view if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(scorePopupText); } tween(scorePopupText, { y: scorePopupText.y - 200, // Float up by 200 pixels alpha: 0 }, { duration: 1800, // Slightly longer duration for a nice float easing: tween.easeOut, onFinish: function onFinish() { if (scorePopupText && !scorePopupText.destroyed) { scorePopupText.destroy(); } } }); } } // 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); } }); } // Stop music immediately LK.stopMusic(); 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 is paused and it's a catch instruction step (3 or 4), // clicking "CONTINUE" implies a retry of that step. if (GameState.tutorialPaused && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4)) { // Logic for catch steps 3 or 4 when "CONTINUE" is pressed. // This covers scenarios where fish was caught, missed, passed hook, or swam off screen. var advanceAfterCatch = false; // First, check the state of the existing tutorial fish, if any. if (GameState.tutorialFish && GameState.tutorialFish.caught) { advanceAfterCatch = true; } // Regardless of success or failure, if there was a tutorial fish, clean it up. // This handles the fish that was just caught OR missed/swam_off. if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) { var idx = fishArray.indexOf(GameState.tutorialFish); if (idx > -1) { fishArray.splice(idx, 1); } GameState.tutorialFish.destroy(); } // Ensure GameState.tutorialFish is null before runTutorialStep, // as runTutorialStep might spawn a new one for the current or next step. GameState.tutorialFish = null; //{pq} // Ensure it's null before re-running step to spawn a new one. if (advanceAfterCatch) { // If the fish was caught, advance to the next tutorial step. GameState.tutorialStep++; runTutorialStep(); } else { // If the fish was missed, swam off, or there was no fish to catch (e.g. error state) // then retry the current step. runTutorialStep will handle re-spawning. runTutorialStep(); // Re-runs current step to spawn new fish } } else { // For any other tutorial step, or if not paused in a catch step, "CONTINUE" advances. GameState.tutorialStep++; runTutorialStep(); } } else if ((GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !GameState.tutorialPaused) { // Catch attempt steps are now 3 and 4 // 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 - disabled for "coming soon" 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) { // Do nothing - button is disabled } // 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: 1,
upgradeCost: 0,
// Starting depth
songs: [{
name: "Gentle Waves",
bpm: 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'
}, {
name: "Sunny Afternoon",
bpm: 97,
duration: 181800,
// 3:01.8
pattern: "sunny_afternoon_custom",
cost: 0,
musicId: 'sunnyafternoon'
}]
}, {
level: 2,
name: "Mid Waters",
fishSpeed: 7,
fishValue: 2,
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"
}]
},
sunny_afternoon_custom: {
beatsPerFish: 1.3,
// Slightly faster than morning_tide_custom but still beginner-friendly
doubleSpawnChance: 0.08,
// Moderate chance for variety
rareSpawnChance: 0.025,
// Slightly better rewards than gentle_waves
sections: [
// Gentle warm-up (0-20 seconds)
{
startTime: 0,
endTime: 20000,
spawnModifier: 0.8,
description: "warm_sunny_start"
},
// First activity burst (20-35 seconds)
{
startTime: 20000,
endTime: 35000,
spawnModifier: 1.4,
description: "first_sunny_burst"
},
// Breathing room (35-50 seconds)
{
startTime: 35000,
endTime: 50000,
spawnModifier: 0.7,
description: "sunny_breather_1"
},
// Second activity burst (50-70 seconds)
{
startTime: 50000,
endTime: 70000,
spawnModifier: 1.5,
description: "second_sunny_burst"
},
// Extended breathing room (70-90 seconds)
{
startTime: 70000,
endTime: 90000,
spawnModifier: 0.6,
description: "sunny_breather_2"
},
// Third activity burst (90-110 seconds)
{
startTime: 90000,
endTime: 110000,
spawnModifier: 1.3,
description: "third_sunny_burst"
},
// Breathing room (110-125 seconds)
{
startTime: 110000,
endTime: 125000,
spawnModifier: 0.8,
description: "sunny_breather_3"
},
// Final activity section (125-150 seconds)
{
startTime: 125000,
endTime: 150000,
spawnModifier: 1.2,
description: "sunny_finale_buildup"
},
// Wind down (150-181.8 seconds)
{
startTime: 150000,
endTime: 181800,
spawnModifier: 0.9,
description: "sunny_afternoon_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 = 500; // Just 0.5 seconds buffer
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();
// 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);
}
},
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 = Math.floor(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 = Math.floor(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: 450,
// Increased height
color: 0x000000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.75
}));
tutorialTextDisplay = tutorialOverlayContainer.addChild(new Text2('', {
size: 55,
// Increased font size
fill: 0xFFFFFF,
wordWrap: true,
wordWrapWidth: 1700,
// Keep wordWrapWidth, adjust if lines are too cramped
// Slightly less than background width
align: 'center',
lineHeight: 65 // Increased line height
}));
tutorialTextDisplay.anchor.set(0.5, 0.5);
tutorialTextDisplay.x = tutorialTextBackground.x;
tutorialTextDisplay.y = tutorialTextBackground.y - 50; // Adjusted Y for new background height and text size
tutorialContinueButton = tutorialOverlayContainer.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: tutorialTextBackground.x,
y: tutorialTextBackground.y + tutorialTextBackground.height / 2 - 55,
// Adjusted Y for new background height
// 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 = Math.floor(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"
// Clear lane highlights from previous step if any
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
if (tutorialContinueButton) {
tutorialContinueButton.visible = true;
} // Default to visible
if (tutorialContinueText) {
tutorialContinueText.visible = true;
}
// Clear previous tutorial fish unless current step needs it (now steps 3 and 4)
if (GameState.tutorialFish && GameState.tutorialStep !== 3 && GameState.tutorialStep !== 4) {
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
// Adjusted max step for animations to 8 (new end tutorial step is 7)
if (fishingElements) {
if (typeof fishingElements.startWaterSurfaceAnimation === 'function' && GameState.tutorialStep < 8) {
fishingElements.startWaterSurfaceAnimation();
}
if (typeof fishingElements.startBoatAndFishermanAnimation === 'function' && GameState.tutorialStep < 8) {
fishingElements.startBoatAndFishermanAnimation();
}
if (fishingElements.hook && GameState.tutorialStep < 8) {
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 Explanation
setTutorialText("This is your hook. It automatically moves to the lane with the closest approaching 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:
// Lanes & Tapping Explanation with Overlay
setTutorialText("Fish swim in three lanes. When a fish is over the hook in its lane, TAP THE SCREEN in that lane to catch it. The lanes are highlighted. Tap 'CONTINUE'.");
// Add highlights behind the text box elements
for (var i = 0; i < GAME_CONFIG.LANES.length; i++) {
var laneYPos = GAME_CONFIG.LANES[i].y;
var highlight = LK.getAsset('laneHighlight', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: laneYPos,
alpha: 0.25,
// Semi-transparent
width: 1800,
height: 200
});
tutorialOverlayContainer.addChildAt(highlight, 0); // Add at the bottom of the container
tutorialLaneHighlights.push(highlight);
}
GameState.tutorialPaused = true;
break;
case 3:
// Was case 2
// Fish Approaching & First Catch Attempt
setTutorialText("A fish will now approach. Tap in its lane when it's under the hook!");
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 4:
// Was 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 5:
// Was case 4
// Combos
setTutorialText("Nice one! Catch fish consecutively to build a COMBO for bonus points! Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 6:
// Was 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 7:
// Was case 6
// End Tutorial
setTutorialText("You're all set! Tap 'CONTINUE' to go to the fishing spots!");
GameState.tutorialPaused = true;
break;
default:
// Effectively step 8
// Tutorial finished
GameState.tutorialMode = false;
tutorialOverlayContainer.visible = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
var idxDefault = fishArray.indexOf(GameState.tutorialFish);
if (idxDefault > -1) {
fishArray.splice(idxDefault, 1);
}
GameState.tutorialFish = null;
}
// Clear lane highlights if any persist
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
// 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;
// Ensure fishing screen is visible and setup for tutorial
showScreen('fishing');
fishingScreen.alpha = 1;
// Clear any active game elements from a previous session
fishArray.forEach(function (f) {
if (f && !f.destroyed) {
f.destroy();
}
});
fishArray = [];
ImprovedRhythmSpawner.reset();
// Clear existing UI text
if (fishingElements.scoreText) {
fishingElements.scoreText.setText('');
}
if (fishingElements.fishText) {
fishingElements.fishText.setText('');
}
if (fishingElements.comboText) {
fishingElements.comboText.setText('');
}
if (fishingElements.progressText) {
fishingElements.progressText.setText('');
}
// CREATE LANE BRACKETS FOR TUTORIAL
var bracketAssetHeight = 150;
var bracketAssetWidth = 75;
// Clear any existing brackets first
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 = [];
if (fishingScreen && !fishingScreen.destroyed) {
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,
y: laneY,
height: bracketAssetHeight
}));
var rightBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
scaleX: -1,
x: 2048 - bracketAssetWidth / 2,
y: laneY,
height: bracketAssetHeight
}));
laneBrackets.push({
left: leftBracket,
right: rightBracket
});
}
}
// 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 (now steps 3 or 4)
if (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) {
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 === 3 || GameState.tutorialStep === 4; // Adjusted step numbers
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.5; // Approx 1821
var tutorialButtonY = startButtonY + 600; // Maintain 150px gap
var startButton = titleScreen.addChild(LK.getAsset('startbutton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: startButtonY,
alpha: 0
}));
var tutorialButton = titleScreen.addChild(LK.getAsset('tutorialbutton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: tutorialButtonY,
alpha: 0
}));
// No assignment here; assign tutorialButtonGfx after createTitleScreen returns
return {
startButton: startButton,
tutorialButton: tutorialButton,
// This is the graphics object
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
}));
// Top section - Title, Money, and Back button
var title = new Text2('SELECT FISHING SPOT', {
size: 90,
// Increased from 80
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.x = GAME_CONFIG.SCREEN_CENTER_X;
title.y = 180; // Moved up slightly
levelSelectScreen.addChild(title);
// Money display (keep at top)
var moneyDisplay = new Text2('Money: $0', {
size: 70,
// Increased from 60
fill: 0xFFD700
});
moneyDisplay.anchor.set(1, 0);
moneyDisplay.x = 1900;
moneyDisplay.y = 80; // Moved up slightly
levelSelectScreen.addChild(moneyDisplay);
// Back button (moved to bottom center)
var backButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 2732 - 300,
// Positioned 100px from the bottom edge (center of button)
tint: 0x757575
}));
var backButtonText = new Text2('BACK', {
size: 45,
// Increased from 40
fill: 0xFFFFFF
});
backButtonText.anchor.set(0.5, 0.5);
backButtonText.x = backButton.x;
backButtonText.y = backButton.y;
levelSelectScreen.addChild(backButtonText);
// Depth tabs (moved down and spaced out more)
var depthTabs = [];
// Song display area (moved to center of screen)
var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1100,
// Moved down significantly from 700
width: 900,
// Made wider
height: 300 // Made taller
}));
// Song navigation arrows (repositioned around the larger song card)
var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 350,
// Moved further left
y: 1100,
// Moved down with song card
tint: 0x666666,
width: 120,
// Made bigger
height: 120
}));
var leftArrowText = new Text2('<', {
size: 80,
// Increased from 60
fill: 0xFFFFFF
});
leftArrowText.anchor.set(0.5, 0.5);
leftArrowText.x = 350;
leftArrowText.y = 1100;
levelSelectScreen.addChild(leftArrowText);
var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 1698,
// Moved further right
y: 1100,
// Moved down with song card
tint: 0x666666,
width: 120,
// Made bigger
height: 120
}));
var rightArrowText = new Text2('>', {
size: 80,
// Increased from 60
fill: 0xFFFFFF
});
rightArrowText.anchor.set(0.5, 0.5);
rightArrowText.x = 1698;
rightArrowText.y = 1100;
levelSelectScreen.addChild(rightArrowText);
// Song info (repositioned within the larger song card area)
var songTitle = new Text2('Song Title', {
size: 60,
// Increased from 50
fill: 0xFFFFFF
});
songTitle.anchor.set(0.5, 0.5);
songTitle.x = GAME_CONFIG.SCREEN_CENTER_X;
songTitle.y = 1020; // Positioned above song card center
levelSelectScreen.addChild(songTitle);
var songInfo = new Text2('BPM: 120 | Duration: 2:00', {
size: 40,
// Increased from 30
fill: 0xCCCCCC
});
songInfo.anchor.set(0.5, 0.5);
songInfo.x = GAME_CONFIG.SCREEN_CENTER_X;
songInfo.y = 1100; // Centered in song card
levelSelectScreen.addChild(songInfo);
var songEarnings = new Text2('Potential Earnings: $50-100', {
size: 40,
// Increased from 30
fill: 0x4CAF50
});
songEarnings.anchor.set(0.5, 0.5);
songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X;
songEarnings.y = 1180; // Positioned below song card center
levelSelectScreen.addChild(songEarnings);
// Play/Buy button (moved down and made larger)
var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1400,
// Moved down significantly from 900
width: 500,
// Made wider
height: 130 // Made taller
}));
var playButtonText = new Text2('PLAY', {
size: 60,
// Increased from 50
fill: 0xFFFFFF
});
playButtonText.anchor.set(0.5, 0.5);
playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
playButtonText.y = 1400;
levelSelectScreen.addChild(playButtonText);
// Shop button (moved to bottom and made larger)
var shopButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1650,
// Moved down significantly from 1100
tint: 0x666666,
// Grayed out since it's not available
width: 450,
// Made wider
height: 120 // Made taller
}));
var shopButtonText = new Text2('UPGRADE ROD', {
size: 50,
// Increased from 40
fill: 0xFFFFFF
});
shopButtonText.anchor.set(0.5, 0.5);
shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
shopButtonText.y = 1650;
levelSelectScreen.addChild(shopButtonText);
// Add "Coming soon!" text underneath
var comingSoonText = new Text2('Coming soon!', {
size: 35,
fill: 0xFFD700 // Gold color to make it stand out
});
comingSoonText.anchor.set(0.5, 0.5);
comingSoonText.x = GAME_CONFIG.SCREEN_CENTER_X;
comingSoonText.y = 1730; // Position below the button
levelSelectScreen.addChild(comingSoonText);
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;
var tutorialLaneHighlights = []; // To store lane highlight graphics for the tutorial
// 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 (now steps 3 or 4)
if (GameState.tutorialMode && isDown && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !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.tutorialButton);
// 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;
}
// Clear lane highlights if any exist when switching screen away from tutorial
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
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 = 6000; // 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.tutorialButton.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,
onFinish: function onFinish() {
// Fade in tutorial button AFTER start button finishes
tween(titleElements.tutorialButton, {
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;
// Play reel sound effect with 300ms delay during hook animation
LK.setTimeout(function () {
LK.getSound('reel').play();
}, 600);
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 (positioned in the middle area)
var tabStartY = 600; // Moved down from 400
var tabSpacing = 250; // Increased spacing between tabs
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: 200 + i * tabSpacing,
// Increased spacing
y: tabStartY,
tint: isSelected ? 0x1976d2 : 0x455a64,
width: 400,
// Made wider
height: 160 // Made taller
}));
var tabText = new Text2(depth.name.split(' ')[0], {
size: 40,
// Increased from 30
fill: 0xFFFFFF
});
tabText.anchor.set(0.5, 0.5);
tabText.x = 200 + i * tabSpacing;
tabText.y = tabStartY;
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;
// Always show as disabled - coming soon
elements.shopButtonText.setText('UPGRADE ROD');
elements.shopButton.tint = 0x666666; // Always grayed out
}
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 = Math.floor(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 = Math.floor(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) {
// Check if it's a critical catch step (now steps 3 or 4)
if (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) {
setTutorialText("Oops! Make sure to tap when the fish is in the correct lane and over 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 === 3) {
// Was step 2
setTutorialText("Great catch! That's how you do it. Tap 'CONTINUE'.");
// GameState.tutorialStep = 4; // Advance to next conceptual phase -- Handled by game.down
} else if (GameState.tutorialStep === 4) {
// Was step 3
setTutorialText("Nice one! You're getting the hang of timing. Tap 'CONTINUE'.");
// GameState.tutorialStep = 5; // Advance to Combo explanation -- Handled by game.down
}
} 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
// Score pop-up animation
if (points > 0) {
var scorePopupText = new Text2('+' + points, {
size: 140,
// Slightly larger for impact
fill: 0xFFD700,
// Gold color for score
align: 'center',
stroke: 0x000000,
// Black stroke
strokeThickness: 6 // Thickness of the stroke
});
scorePopupText.anchor.set(0.5, 0.5);
scorePopupText.x = GAME_CONFIG.SCREEN_CENTER_X; // Centered with the boat
scorePopupText.y = GAME_CONFIG.BOAT_Y - 70; // Start slightly above the boat's deck line
// Add to fishingScreen so it's part of the game view
if (fishingScreen && !fishingScreen.destroyed) {
fishingScreen.addChild(scorePopupText);
}
tween(scorePopupText, {
y: scorePopupText.y - 200,
// Float up by 200 pixels
alpha: 0
}, {
duration: 1800,
// Slightly longer duration for a nice float
easing: tween.easeOut,
onFinish: function onFinish() {
if (scorePopupText && !scorePopupText.destroyed) {
scorePopupText.destroy();
}
}
});
}
}
// 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);
}
});
}
// Stop music immediately
LK.stopMusic();
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 is paused and it's a catch instruction step (3 or 4),
// clicking "CONTINUE" implies a retry of that step.
if (GameState.tutorialPaused && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4)) {
// Logic for catch steps 3 or 4 when "CONTINUE" is pressed.
// This covers scenarios where fish was caught, missed, passed hook, or swam off screen.
var advanceAfterCatch = false;
// First, check the state of the existing tutorial fish, if any.
if (GameState.tutorialFish && GameState.tutorialFish.caught) {
advanceAfterCatch = true;
}
// Regardless of success or failure, if there was a tutorial fish, clean it up.
// This handles the fish that was just caught OR missed/swam_off.
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish.destroy();
}
// Ensure GameState.tutorialFish is null before runTutorialStep,
// as runTutorialStep might spawn a new one for the current or next step.
GameState.tutorialFish = null; //{pq} // Ensure it's null before re-running step to spawn a new one.
if (advanceAfterCatch) {
// If the fish was caught, advance to the next tutorial step.
GameState.tutorialStep++;
runTutorialStep();
} else {
// If the fish was missed, swam off, or there was no fish to catch (e.g. error state)
// then retry the current step. runTutorialStep will handle re-spawning.
runTutorialStep(); // Re-runs current step to spawn new fish
}
} else {
// For any other tutorial step, or if not paused in a catch step, "CONTINUE" advances.
GameState.tutorialStep++;
runTutorialStep();
}
} else if ((GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !GameState.tutorialPaused) {
// Catch attempt steps are now 3 and 4
// 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 - disabled for "coming soon"
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) {
// Do nothing - button is disabled
}
// 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 sardine. 80s arcade machine graphics. Swimming Side profile. In-Game asset. 2d. High contrast. No shadows
An anchovy. 80s arcade machine graphics. Swimming Side profile. In-Game asset. 2d. High contrast. No shadows
A mackerel. 80s arcade machine graphics. Swimming Side profile. 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.