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