User prompt
Move pinned fishing line top 15 pixels left.
User prompt
Update with: // Calculate fisherman's current position - pinned to top right var fishermanCurrentX = fishermanContainer.x + fisherman.x + 100; // Right side var fishermanCurrentY = fishermanContainer.y + fisherman.y - fisherman.height; // Top of fisherman
User prompt
Update with: // Function to update fishing line animation function updateFishingLineWave() { linePhaseOffset += lineWaveSpeed; // Calculate fisherman's current position - pinned to top right var fishermanCurrentX = fishermanContainer.x + fisherman.x + 100; // Added +100 to move to right side var fishermanCurrentY = fishermanContainer.y + fisherman.y + fishingLineStartY; // Calculate wave offset for the line var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude; // Update line position - pinned at top to fisherman, sways in middle line.x = fishermanCurrentX + (waveOffset * 0.3); // Less sway at top line.y = fishermanCurrentY; // Update line height to reach the hook var lineLength = hook.y - fishermanCurrentY; line.height = Math.max(0, lineLength); // Update hook position with more sway (it's at the end of the line) hook.x = fishermanCurrentX + waveOffset; }
User prompt
Update as needed with: function createFishingScreen() { // Water background var water = fishingScreen.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, width: 2048, height: 2732 - GAME_CONFIG.WATER_SURFACE_Y, alpha: 0.7 })); // [Keep existing water surface animation code...] // Animated Water Surface segments code remains the same... // Boat 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 })); // 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; // Start hook in the middle lane var fishingLineStartY = -100; // Relative to fisherman (top of fishing rod) // Create single fishing line 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) })); // Hook var hook = fishingScreen.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: initialHookY })); hook.originalY = initialHookY; // Line animation variables var lineWaveAmplitude = 12; // How much the line sways var lineWaveSpeed = 0.03; // Speed of the wave animation var linePhaseOffset = 0; // Global phase for the wave animation // Function to update fishing line animation function updateFishingLineWave() { linePhaseOffset += lineWaveSpeed; // Calculate fisherman's current position var fishermanCurrentX = fishermanContainer.x + fisherman.x; var fishermanCurrentY = fishermanContainer.y + fisherman.y + fishingLineStartY; // Calculate wave offset for the line var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude; // Update line position - pinned at top to fisherman, sways in middle line.x = fishermanCurrentX + (waveOffset * 0.3); // Less sway at top line.y = fishermanCurrentY; // Update line height to reach the hook var lineLength = hook.y - fishermanCurrentY; line.height = Math.max(0, lineLength); // Update hook position with more sway (it's at the end of the line) hook.x = fishermanCurrentX + waveOffset; } // Calculate target positions for boat wave animation var targetUpY = boatBaseY - boatWaveAmplitude; var targetDownY = boatBaseY + boatWaveAmplitude; var fishermanTargetUpY = fishermanBaseY - boatWaveAmplitude; var fishermanTargetDownY = fishermanBaseY + boatWaveAmplitude; // Synchronized wave animation functions function moveBoatAndFishermanUp() { if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } function moveBoatAndFishermanDown() { if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } tween(boat, { y: targetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanUp }); tween(fishermanContainer, { y: fishermanTargetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } // Start the synchronized animation cycle if (boat && !boat.destroyed && fishermanContainer && !fishermanContainer.destroyed) { tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut }); } // [UI elements remain the same...] 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); var holdText = new Text2('TAP: Normal Fish | HOLD: Golden Fish', { size: 35, fill: 0xFFD700, alpha: 0.8 }); holdText.anchor.set(0.5, 0); holdText.x = GAME_CONFIG.SCREEN_CENTER_X; holdText.y = GAME_CONFIG.LANES[GAME_CONFIG.LANES.length - 1].y + 100; fishingScreen.addChild(holdText); return { boat: boat, fishermanContainer: fishermanContainer, fisherman: fisherman, hook: hook, line: line, updateFishingLineWave: updateFishingLineWave, scoreText: scoreText, fishText: fishText, comboText: comboText, progressText: progressText, waterSurfaceSegments: waterSurfaceSegments }; } ``` And update the main game loop to include the line animation and proper hook movement: ```javascript game.update = function () { if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } // Update the animated fishing line wave if (fishingElements.updateFishingLineWave) { fishingElements.updateFishingLineWave(); } var currentTime = LK.ticks * (1000 / 60); // Initialize game timer if (GameState.songStartTime === 0) { GameState.songStartTime = currentTime; } // [Keep existing song/spawn logic...] 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; } // [Keep existing spawn logic...] if (currentTime - GameState.lastBeatTime >= spawnInterval) { GameState.lastBeatTime = currentTime; GameState.beatCount++; if (PatternGenerator.canSpawnFishOnBeat(currentTime, spawnInterval)) { var firstFish = spawnFish(currentTime, {}); // [Keep existing multi-beat spawn logic...] } } // 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; } // Update fish for (var i = fishArray.length - 1; i >= 0; i--) { var fish = fishArray[i]; fish.update(); // Remove off-screen fish if (!fish.caught && !fish.isHeld && (fish.x < -250 || fish.x > 2048 + 250)) { fish.destroy(); fishArray.splice(i, 1); } } // [Keep existing hold fish logic and UI updates...] updateFishingUI(); }; ``` And update the `endFishingSession` function: ```javascript function endFishingSession() { GameState.gameActive = false; // Stop the boat's wave animation if (fishingElements && fishingElements.boat) { tween.stop(fishingElements.boat); } // Stop fisherman container animation if (fishingElements && fishingElements.fishermanContainer) { tween.stop(fishingElements.fishermanContainer); } // Stop water surface wave animations if (fishingElements && fishingElements.waterSurfaceSegments) { fishingElements.waterSurfaceSegments.forEach(function(segment) { if (segment && !segment.destroyed) { tween.stop(segment); } }); } // [Rest of existing endFishingSession code...] }
User prompt
Update as needed with: function createFishingScreen() { // Water background var water = fishingScreen.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, width: 2048, height: 2732 - GAME_CONFIG.WATER_SURFACE_Y, alpha: 0.7 })); // [Keep existing water surface animation code...] // Animated Water Surface segments code remains the same... // Boat 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 })); // 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; // SEGMENTED FISHING LINE SYSTEM var NUM_LINE_SEGMENTS = 25; // Number of segments for the fishing line var fishingLineSegments = []; var fishingLineStartX = GAME_CONFIG.SCREEN_CENTER_X - 100; // Fisherman's X position var fishingLineStartY = GAME_CONFIG.WATER_SURFACE_Y - 70 - 100; // Top of fisherman's head/rod var initialHookY = GAME_CONFIG.LANES[1].y; // Start hook in the middle lane var totalLineLength = initialHookY - fishingLineStartY; var segmentHeight = totalLineLength / NUM_LINE_SEGMENTS; // Create fishing line segments for (var i = 0; i < NUM_LINE_SEGMENTS; i++) { var segment = fishingScreen.addChild(LK.getAsset('fishingLine', { anchorX: 0.5, anchorY: 0, x: fishingLineStartX, y: fishingLineStartY + (i * segmentHeight), width: 6, height: segmentHeight + 2, // Overlap slightly to avoid gaps alpha: 0.9 })); segment.baseX = fishingLineStartX; segment.baseY = fishingLineStartY + (i * segmentHeight); segment.segmentIndex = i; segment.waveTime = 0; fishingLineSegments.push(segment); } // Hook (will be animated with the line) var hook = fishingScreen.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: initialHookY })); hook.originalY = initialHookY; hook.baseX = GAME_CONFIG.SCREEN_CENTER_X; hook.baseY = initialHookY; hook.waveTime = 0; // Line animation variables var lineWaveAmplitude = 8; // How much the line sways var lineWaveSpeed = 0.04; // Speed of the wave animation var lineWaveFrequency = 0.15; // How many waves along the line length var linePhaseOffset = 0; // Global phase for the wave animation // Function to update fishing line animation function updateFishingLineWave() { linePhaseOffset += lineWaveSpeed; // Update fisherman position (this affects the line start point) var fishermanCurrentX = fishermanContainer.x + fisherman.x; var fishermanCurrentY = fishermanContainer.y + fisherman.y - 100; // Top of fisherman // Update each line segment for (var i = 0; i < fishingLineSegments.length; i++) { var segment = fishingLineSegments[i]; var segmentProgress = i / NUM_LINE_SEGMENTS; // 0 to 1 from top to bottom // Calculate wave offset for this segment var wavePhase = linePhaseOffset + (segmentProgress * lineWaveFrequency * Math.PI * 2); var waveOffset = Math.sin(wavePhase) * lineWaveAmplitude * segmentProgress; // More sway at bottom // Update segment position segment.x = fishermanCurrentX + waveOffset; segment.y = fishermanCurrentY + (i * segmentHeight); } // Update hook position (it follows the last segment + wave motion) var hookProgress = 1.0; // Hook is at the end var hookWavePhase = linePhaseOffset + (hookProgress * lineWaveFrequency * Math.PI * 2); var hookWaveOffset = Math.sin(hookWavePhase) * lineWaveAmplitude; hook.x = fishermanCurrentX + hookWaveOffset; // Hook Y will be managed by the dynamic hook movement system } // Calculate target positions for boat wave animation var targetUpY = boatBaseY - boatWaveAmplitude; var targetDownY = boatBaseY + boatWaveAmplitude; var fishermanTargetUpY = fishermanBaseY - boatWaveAmplitude; var fishermanTargetDownY = fishermanBaseY + boatWaveAmplitude; // Synchronized wave animation functions function moveBoatAndFishermanUp() { if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } function moveBoatAndFishermanDown() { if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } tween(boat, { y: targetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanUp }); tween(fishermanContainer, { y: fishermanTargetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } // Start the synchronized animation cycle if (boat && !boat.destroyed && fishermanContainer && !fishermanContainer.destroyed) { tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut }); } // [Rest of the UI elements remain the same...] 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); var holdText = new Text2('TAP: Normal Fish | HOLD: Golden Fish', { size: 35, fill: 0xFFD700, alpha: 0.8 }); holdText.anchor.set(0.5, 0); holdText.x = GAME_CONFIG.SCREEN_CENTER_X; holdText.y = GAME_CONFIG.LANES[GAME_CONFIG.LANES.length - 1].y + 100; fishingScreen.addChild(holdText); return { boat: boat, fishermanContainer: fishermanContainer, fisherman: fisherman, hook: hook, fishingLineSegments: fishingLineSegments, // Return segments for access updateFishingLineWave: updateFishingLineWave, // Return update function scoreText: scoreText, fishText: fishText, comboText: comboText, progressText: progressText, waterSurfaceSegments: waterSurfaceSegments }; } ``` And update the main game loop to include the line animation: ```javascript game.update = function () { if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } // Update the animated fishing line wave if (fishingElements.updateFishingLineWave) { fishingElements.updateFishingLineWave(); } var currentTime = LK.ticks * (1000 / 60); // [Rest of the existing game update logic remains the same...] // Initialize game timer if (GameState.songStartTime === 0) { GameState.songStartTime = currentTime; } // [Continue with existing spawn logic, hook movement, fish updates, etc.] // Dynamic Hook Movement Logic (updated to work with segmented line) 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 and line length if (fishingElements.hook.y !== targetLaneY) { tween(fishingElements.hook, { y: targetLaneY }, { duration: 150, easing: tween.easeOut }); // Update the fishing line segments to match new hook depth if (fishingElements.fishingLineSegments) { var newTotalLength = targetLaneY - (GAME_CONFIG.WATER_SURFACE_Y - 70 - 100); var newSegmentHeight = newTotalLength / fishingElements.fishingLineSegments.length; for (var i = 0; i < fishingElements.fishingLineSegments.length; i++) { var segment = fishingElements.fishingLineSegments[i]; tween(segment, { height: newSegmentHeight + 2 }, { duration: 150, easing: tween.easeOut }); } } fishingElements.hook.originalY = targetLaneY; } // [Continue with existing fish update logic, UI updates, etc.] }; ``` Also update the `endFishingSession` function to stop the line animation: ```javascript function endFishingSession() { GameState.gameActive = false; // Stop the boat's wave animation if (fishingElements && fishingElements.boat) { tween.stop(fishingElements.boat); } // Stop fishing line segment animations if (fishingElements && fishingElements.fishingLineSegments) { fishingElements.fishingLineSegments.forEach(function(segment) { if (segment && !segment.destroyed) { tween.stop(segment); } }); } // [Rest of existing endFishingSession code...] }
User prompt
Bring the top the fishing line up to the top of the fisherman while not changing the bottom heights.
User prompt
update as needed with: function createFishingScreen() { // ... (water and wave setup code remains the same until boat creation) ... // Boat 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 })); // 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, // Same relative X as before y: GAME_CONFIG.WATER_SURFACE_Y - 70 // Same relative Y as before })); // Store references for wave animation sync var boatBaseY = boat.y; var fishermanBaseY = fishermanContainer.y; var boatWaveAmplitude = 10; var boatWaveHalfCycleDuration = 2000; // Calculate target positions var targetUpY = boatBaseY - boatWaveAmplitude; var targetDownY = boatBaseY + boatWaveAmplitude; var fishermanTargetUpY = fishermanBaseY - boatWaveAmplitude; var fishermanTargetDownY = fishermanBaseY + boatWaveAmplitude; // Synchronized wave animation functions function moveBoatAndFishermanUp() { // Defensive checks if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } // Animate both boat and fisherman container together tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } function moveBoatAndFishermanDown() { // Defensive checks if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) { return; } // Animate both boat and fisherman container together tween(boat, { y: targetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatAndFishermanUp }); tween(fishermanContainer, { y: fishermanTargetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut }); } // Start the synchronized animation cycle if (boat && !boat.destroyed && fishermanContainer && !fishermanContainer.destroyed) { tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut, onFinish: moveBoatAndFishermanDown }); tween(fishermanContainer, { y: fishermanTargetUpY }, { duration: boatWaveHalfCycleDuration / 2, easing: tween.easeOut }); } // ... (rest of the fishing screen setup remains the same) ... return { boat: boat, fishermanContainer: fishermanContainer, // Return container for access fisherman: fisherman, // Return fisherman for direct access hook: hook, line: line, scoreText: scoreText, fishText: fishText, comboText: comboText, progressText: progressText, waterSurfaceSegments: waterSurfaceSegments }; } ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make the UI in the top left corner of the fishing screen bigger and move to the right top corner.
User prompt
Make white water segments half as thick.
User prompt
White segments should be synced with blue ones so they move as one.
User prompt
Add another layer of water segments above the existing ones and make them white.
User prompt
Instead of using tint to differentiate and show readiness to be caught for held fish, have them have a trail of “shadow images” of themselves that lag behind. When the fish is held, these images begin to catch up with the fish, being removed as they reach the fishes location. When the last one is removed, which should align with its catch time, they are ready to be caught.
User prompt
Instead of using a tint for held fish, make a trail of a ghost image that has to catch up with the fish as an indicator that it is caught. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Make water segments thicker
User prompt
Give fish a bit of slight sine wave up and down motion as they swim and have them scale pulse to the beat. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Currently, fish all face left. So the ones coming from the right side of the screen are correct. Flip only the left side of the screen fishes.
User prompt
Fish coming from the left side of the screen should flip to face right.
User prompt
Flip fish coming from the left side of the screen along the Y axis.
Code edit (1 edits merged)
Please save this source code
User prompt
Move the boat down the child index in the container to be below the fisherman
User prompt
Initialize boat before fisherman so that the boat will appear below the fisherman or move the fisherman up the child index
User prompt
The fisherman is not animating with the boat. It needs to move in sync
User prompt
Move the fisherman on top of boat layer wise.
User prompt
Add the fisherman asset on top the boat and pin to center.
User prompt
Alternating water segments are out of sync. They need to transition one after another all the way along the surface.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ 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, isHoldFish) { var self = Container.call(this); var assetName = type + 'Fish'; var fishGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); // Flip fish based on movement direction if (speed > 0) { // Moving right (coming from left), flip horizontally fishGraphics.scaleX = -1; } self.type = type; self.value = value; self.speed = speed; self.lane = lane; // Index of the lane (0, 1, or 2) self.isHoldFish = isHoldFish || false; self.caught = false; self.isSpecial = type === 'rare'; // For shimmer effect self.shimmerTime = 0; self.isHeld = false; // True if player is currently pressing down on this fish // Hold fish specific properties if (self.isHoldFish) { fishGraphics.tint = 0xFFD700; // Golden tint for hold fish self.holdStarted = false; // True if current hold duration meets requiredHoldTime self.requiredHoldTime = 800; // 800ms hold required to "prime" the catch for a hold fish } self.update = function () { if (self.isHeld) { // If being actively held by player input if (self.isHoldFish) { // Only hold fish show metered feedback via tint if (self.holdStarted) { // Primed (duration met) fishGraphics.tint = 0x00FF00; // Green tint for "primed" state } else { // Holding, but not yet primed fishGraphics.tint = 0xFFD700; // Original hold fish golden tint } } // Fish does not move while held return; } else { // Not being held by player input (e.g., released or never held) if (self.isHoldFish) { fishGraphics.tint = 0xFFD700; // Revert to default golden tint for hold fish } // For non-hold fish, tint is not managed here. // If fish was holdStarted and then isHeld becomes false, tint reverts here. } if (!self.caught) { self.x += self.speed; if (self.isSpecial) { // Shimmer effect for rare fish self.shimmerTime += 0.1; fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2; } else if (!self.isHoldFish) { // Reset alpha if not special and not a hold fish (hold fish manage their appearance) fishGraphics.alpha = 1.0; } // Ensure hold fish alpha is managed if needed, e.g. if they can also be special // For now, hold fish alpha is implicitly 1.0 unless also special. } }; self.catchFish = function () { self.caught = true; // Animation to boat tween(self, { y: GAME_CONFIG.BOAT_Y, // Target Y: boat position x: GAME_CONFIG.SCREEN_CENTER_X, //{r} // Target X: center of screen (boat position) scaleX: 0.5, scaleY: 0.5, alpha: 0 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); /**** * Initialize Game ****/ /**** * Screen Containers ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ /**** * Pattern Generation System ****/ // If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up. var PatternGenerator = { lastLane: -1, minDistanceBetweenFish: 300, // Used by spawnFish internal check // Minimum X distance between fish for visual clarity on hook lastActualSpawnTime: -100000, // Time of the last actual fish spawn lastSpawnWasHold: false, // Was the last spawned fish a hold fish? 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, // considering if it was a hold fish. canSpawnFishOnBeat: function canSpawnFishOnBeat(currentTime, configuredSpawnInterval) { var timeSinceLast = currentTime - this.lastActualSpawnTime; var minRequiredGap = configuredSpawnInterval; // Default gap is the song's beat interval for fish if (this.lastSpawnWasHold) { // If the last fish was a hold fish, ensure its hold time + buffer has passed. // Fish.requiredHoldTime is 800ms. Reduced buffer to 250ms. // 250ms = 100ms (hook travel) + 100ms (player reaction) + 50ms (padding) minRequiredGap = Math.max(minRequiredGap, 800 + 250); } return timeSinceLast >= minRequiredGap; }, // New method: Registers details of the fish that was just spawned. registerFishSpawn: function registerFishSpawn(spawnTime, wasHoldFish) { this.lastActualSpawnTime = spawnTime; this.lastSpawnWasHold = wasHoldFish; }, reset: function reset() { this.lastLane = -1; this.lastActualSpawnTime = -100000; // Set far in the past to allow first spawn this.lastSpawnWasHold = false; } }; /**** * 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 with hold fish - reduced frequency for playability 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, holdFishChance: 0.05 // Reduced from 0.1 }, medium: { beatsPerFish: 1.5, // Increased from 0.75 doubleSpawnChance: 0.15, //{1R} // 15% chance of a double beat rareSpawnChance: 0.05, holdFishChance: 0.1 // Reduced from 0.2 }, complex: { beatsPerFish: 1, // Increased from 0.5 doubleSpawnChance: 0.25, //{1U} // 25% chance of a double beat rareSpawnChance: 0.08, holdFishChance: 0.15 // Reduced from 0.3 }, 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, holdFishChance: 0.2 // Reduced from 0.4 }, 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 holdFishChance: 0.02, // Very few hold fish for beginners // 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, currentPlayingMusicId: 'rhythmTrack', // ID of the music track currently playing in a session currentPlayingMusicInitialVolume: 0.8, // Initial volume of the current music track for fade reference hookTargetLaneIndex: 1, // Start with hook targeting the middle lane (index 1) // Initialize owned songs (first song of each unlocked depth is free) initOwnedSongs: function initOwnedSongs() { this.ownedSongs = []; for (var i = 0; i <= this.currentDepth; i++) { this.ownedSongs.push({ depth: i, songIndex: 0 }); } }, hasSong: function hasSong(depth, songIndex) { return this.ownedSongs.some(function (song) { return song.depth === depth && song.songIndex === songIndex; }); }, buySong: function buySong(depth, songIndex) { var song = GAME_CONFIG.DEPTHS[depth].songs[songIndex]; if (this.money >= song.cost && !this.hasSong(depth, songIndex)) { this.money -= song.cost; this.ownedSongs.push({ depth: depth, songIndex: songIndex }); return true; } return false; }, getCurrentDepthConfig: function getCurrentDepthConfig() { return GAME_CONFIG.DEPTHS[this.selectedDepth]; }, getCurrentSongConfig: function getCurrentSongConfig() { return GAME_CONFIG.DEPTHS[this.selectedDepth].songs[this.selectedSong]; }, canUpgrade: function canUpgrade() { var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1]; return nextDepth && this.money >= nextDepth.upgradeCost; }, upgrade: function upgrade() { if (this.canUpgrade()) { var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1]; this.money -= nextDepth.upgradeCost; this.currentDepth++; // Give free first song of new depth this.ownedSongs.push({ depth: this.currentDepth, songIndex: 0 }); return true; } return false; } }; var titleScreen = game.addChild(new Container()); var levelSelectScreen = game.addChild(new Container()); var fishingScreen = game.addChild(new Container()); var resultsScreen = game.addChild(new Container()); // Initialize GameState.initOwnedSongs(); /**** * Title Screen ****/ function createTitleScreen() { var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.3 })); // Logo var logo = new Text2('BEAT FISHER', { size: 150, fill: 0xFFFFFF }); logo.anchor.set(0.5, 0.5); logo.x = GAME_CONFIG.SCREEN_CENTER_X; logo.y = 600; titleScreen.addChild(logo); var subtitle = new Text2('Rhythm Fishing Adventure', { size: 60, fill: 0x4FC3F7 }); subtitle.anchor.set(0.5, 0.5); subtitle.x = GAME_CONFIG.SCREEN_CENTER_X; subtitle.y = 700; titleScreen.addChild(subtitle); // Start button var startButton = titleScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1000 })); var startText = new Text2('START', { size: 50, fill: 0xFFFFFF }); startText.anchor.set(0.5, 0.5); startText.x = GAME_CONFIG.SCREEN_CENTER_X; startText.y = 1000; titleScreen.addChild(startText); // Tutorial button var tutorialButton = titleScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1150, tint: 0x757575 })); var tutorialText = new Text2('TUTORIAL', { size: 40, fill: 0xFFFFFF }); tutorialText.anchor.set(0.5, 0.5); tutorialText.x = GAME_CONFIG.SCREEN_CENTER_X; tutorialText.y = 1150; titleScreen.addChild(tutorialText); return { startButton: startButton, tutorialButton: tutorialButton }; } /**** * Level Select Screen ****/ function createLevelSelectScreen() { var selectBg = levelSelectScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.8 })); // Title var title = new Text2('SELECT FISHING SPOT', { size: 80, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.x = GAME_CONFIG.SCREEN_CENTER_X; title.y = 200; levelSelectScreen.addChild(title); // Money display var moneyDisplay = new Text2('Money: $0', { size: 60, fill: 0xFFD700 }); moneyDisplay.anchor.set(1, 0); moneyDisplay.x = 1900; moneyDisplay.y = 100; levelSelectScreen.addChild(moneyDisplay); // Depth tabs (will be created dynamically) var depthTabs = []; // Song display area var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 700 })); // Song navigation arrows var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 400, y: 700, tint: 0x666666 })); var leftArrowText = new Text2('<', { size: 60, fill: 0xFFFFFF }); leftArrowText.anchor.set(0.5, 0.5); leftArrowText.x = 400; leftArrowText.y = 700; levelSelectScreen.addChild(leftArrowText); var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 1648, y: 700, tint: 0x666666 })); var rightArrowText = new Text2('>', { size: 60, fill: 0xFFFFFF }); rightArrowText.anchor.set(0.5, 0.5); rightArrowText.x = 1648; rightArrowText.y = 700; levelSelectScreen.addChild(rightArrowText); // Song info (will be updated dynamically) var songTitle = new Text2('Song Title', { size: 50, fill: 0xFFFFFF }); songTitle.anchor.set(0.5, 0.5); songTitle.x = GAME_CONFIG.SCREEN_CENTER_X; songTitle.y = 650; levelSelectScreen.addChild(songTitle); var songInfo = new Text2('BPM: 120 | Duration: 2:00', { size: 30, fill: 0xCCCCCC }); songInfo.anchor.set(0.5, 0.5); songInfo.x = GAME_CONFIG.SCREEN_CENTER_X; songInfo.y = 700; levelSelectScreen.addChild(songInfo); var songEarnings = new Text2('Potential Earnings: $50-100', { size: 30, fill: 0x4CAF50 }); songEarnings.anchor.set(0.5, 0.5); songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X; songEarnings.y = 730; levelSelectScreen.addChild(songEarnings); // Play/Buy button var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 900 })); var playButtonText = new Text2('PLAY', { size: 50, fill: 0xFFFFFF }); playButtonText.anchor.set(0.5, 0.5); playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; playButtonText.y = 900; levelSelectScreen.addChild(playButtonText); // Shop button var shopButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1100, tint: 0x2e7d32 })); var shopButtonText = new Text2('UPGRADE ROD', { size: 40, fill: 0xFFFFFF }); shopButtonText.anchor.set(0.5, 0.5); shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; shopButtonText.y = 1100; levelSelectScreen.addChild(shopButtonText); // Back button var backButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 200, y: 200, tint: 0x757575 })); var backButtonText = new Text2('BACK', { size: 40, fill: 0xFFFFFF }); backButtonText.anchor.set(0.5, 0.5); backButtonText.x = 200; backButtonText.y = 200; levelSelectScreen.addChild(backButtonText); return { moneyDisplay: moneyDisplay, depthTabs: depthTabs, leftArrow: leftArrow, rightArrow: rightArrow, songTitle: songTitle, songInfo: songInfo, songEarnings: songEarnings, playButton: playButton, playButtonText: playButtonText, shopButton: shopButton, shopButtonText: shopButtonText, backButton: backButton }; } /**** * Fishing Screen ****/ function createFishingScreen() { // Water background var water = fishingScreen.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, //{3l} // Adjusted to new config if different width: 2048, // Ensure it covers screen width height: 2732 - GAME_CONFIG.WATER_SURFACE_Y, // Ensure it covers below surface alpha: 0.7 })); // Animated Water Surface var waterSurfaceSegments = []; var NUM_WAVE_SEGMENTS = 32; // Number of segments for the wave effect var SEGMENT_WIDTH = 2048 / NUM_WAVE_SEGMENTS; var SEGMENT_HEIGHT = 10; // Height of each wave segment var WAVE_AMPLITUDE = 12; // How high/low the waves go from the base Y var WAVE_HALF_PERIOD_MS = 2500; // Duration for one half of the wave cycle (e.g., moving up) var PHASE_DELAY_MS_PER_SEGMENT = WAVE_HALF_PERIOD_MS * 2 / NUM_WAVE_SEGMENTS; // Delay to create phase offset for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) { var segment = fishingScreen.addChild(LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH, y: GAME_CONFIG.WATER_SURFACE_Y, width: SEGMENT_WIDTH + 1, // Add 1 to ensure no gaps due to rounding/exact positioning height: SEGMENT_HEIGHT, anchorX: 0, anchorY: 0.5, // Anchor in the middle vertically for y-oscillation alpha: 0.8, tint: 0x4fc3f7 // Ensure color matches original waterSurface if asset default is different })); segment.baseY = GAME_CONFIG.WATER_SURFACE_Y; // IIFE to correctly scope segment and segmentIndex for timeouts and callbacks (function (currentSegment, segmentIndex) { function animateSegmentUp() { if (!currentSegment || currentSegment.destroyed) { return; } tween(currentSegment, { y: currentSegment.baseY - WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, // Smooth easing for wave motion onFinish: animateSegmentDown }); } function animateSegmentDown() { if (!currentSegment || currentSegment.destroyed) { return; } tween(currentSegment, { y: currentSegment.baseY + WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animateSegmentUp }); } // Start the animation for this segment with a phase delay LK.setTimeout(function () { if (!currentSegment || currentSegment.destroyed) { return; } // Start animation for this segment. All segments start with the same initial motion (e.g., moving to the top peak). // The phase delay applied by LK.setTimeout ensures they start sequentially, creating a flowing wave. tween(currentSegment, { y: currentSegment.baseY - WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: animateSegmentDown }); }, segmentIndex * PHASE_DELAY_MS_PER_SEGMENT); })(segment, i); waterSurfaceSegments.push(segment); } // Store segments in fishingElements for access later (e.g., cleanup) // This assumes fishingElements is being populated and returned by createFishingScreen, // and that this new property will be part of that returned object. // The existing structure returns an object from createFishingScreen, so we just add to it. // This line will be added at the end of the `createFishingScreen` function, before the return statement. // For now, let's ensure the `fishingElements` object will contain it if modified directly. // We will add `waterSurfaceSegments: waterSurfaceSegments` to the return object of createFishingScreen. // Boat var boat = fishingScreen.addChild(LK.getAsset('boat', { anchorX: 0.5, anchorY: 0.74, // Anchor at bottom-middle of boat asset x: GAME_CONFIG.SCREEN_CENTER_X, y: GAME_CONFIG.WATER_SURFACE_Y // Boat sits on water surface })); // Fisherman var fisherman = LK.getAsset('fisherman', { anchorX: 0.5, // Center horizontally anchorY: 1 // Anchor at the fisherman's feet }); // Position the fisherman relative to the boat's anchor point. // boat's anchor is (0.5, 0.74). fisherman's anchor is (0.5, 1). fisherman.x = -100; // This will center the fisherman on the boat horizontally. fisherman.y = -70; // Place fisherman's feet 50px above the boat's anchor point. // Adjust this value if needed for better visual placement on the boat's deck. boat.addChild(fisherman); // Make fisherman a child of boat, so it moves with the boat. // Boat wave animation var boatBaseY = boat.y; var boatWaveAmplitude = 10; // Pixels for up/down movement var boatWaveHalfCycleDuration = 2000; // Milliseconds for one direction (e.g., moving up) var targetUpY = boatBaseY - boatWaveAmplitude; var targetDownY = boatBaseY + boatWaveAmplitude; // Helper functions for the looping tween, defined within createFishingScreen // to capture 'boat' and other necessary variables in their scope. function moveBoatUp() { // Defensive check: ensure boat exists and hasn't been destroyed if (!boat || boat.destroyed) { return; } tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, // Smooth easing for gentle motion onFinish: moveBoatDown // When up-tween finishes, start down-tween }); } function moveBoatDown() { // Defensive check if (!boat || boat.destroyed) { return; } tween(boat, { y: targetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveBoatUp // When down-tween finishes, start up-tween }); } // Start the animation cycle. // The first movement is from the boat's initial Y (center of wave) to a peak. // This should take half the duration of a full up/down swing to start smoothly. if (boat && !boat.destroyed) { tween(boat, { y: targetUpY }, { duration: boatWaveHalfCycleDuration / 2, // Shorter duration for the initial move to peak easing: tween.easeOut, // Ease out to the first peak onFinish: moveBoatDown // Kick off the continuous up/down cycle }); } // Single, centered hook and line var initialHookY = GAME_CONFIG.LANES[1].y; // Start hook in the middle lane // Fishing line var line = fishingScreen.addChild(LK.getAsset('fishingLine', { anchorX: 0.5, anchorY: 0, x: GAME_CONFIG.SCREEN_CENTER_X, y: GAME_CONFIG.WATER_SURFACE_Y, height: initialHookY - GAME_CONFIG.WATER_SURFACE_Y })); // Hook var hook = fishingScreen.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: initialHookY })); hook.originalY = initialHookY; // Store original Y for animation and reference // Lane labels are removed as the hook is dynamic. // UI elements var scoreText = new Text2('Score: 0', { size: 50, fill: 0xFFFFFF }); scoreText.anchor.set(0, 0); scoreText.x = 50; scoreText.y = 50; fishingScreen.addChild(scoreText); var fishText = new Text2('Fish: 0/0', { size: 40, fill: 0xFFFFFF }); fishText.anchor.set(0, 0); fishText.x = 50; fishText.y = 120; fishingScreen.addChild(fishText); var comboText = new Text2('Combo: 0', { size: 40, fill: 0xFF9800 }); comboText.anchor.set(0, 0); comboText.x = 50; comboText.y = 180; // Corrected typo from y: 180 fishingScreen.addChild(comboText); // Song progress - Re-added as it's used by updateFishingUI var progressText = new Text2('0:00 / 0:00', { size: 40, fill: 0x4FC3F7 }); progressText.anchor.set(1, 0); // Anchor top-right progressText.x = 2048 - 50; // Position on the top-right side progressText.y = 50; fishingScreen.addChild(progressText); // Hold instruction Text var holdText = new Text2('TAP: Normal Fish | HOLD: Golden Fish', { size: 35, fill: 0xFFD700, // Golden color for emphasis alpha: 0.8 }); holdText.anchor.set(0.5, 0); // Anchor top-center holdText.x = GAME_CONFIG.SCREEN_CENTER_X; holdText.y = GAME_CONFIG.LANES[GAME_CONFIG.LANES.length - 1].y + 100; // Below the last lane (now matches new vertical spread) fishingScreen.addChild(holdText); return { boat: boat, hook: hook, // Single hook line: line, // Single line scoreText: scoreText, fishText: fishText, comboText: comboText, progressText: progressText, waterSurfaceSegments: waterSurfaceSegments, // Add this line fisherman: fisherman }; } /**** * 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 = []; /**** * 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) holdFish: null // Reference to a fish if a hold action is being performed on it }; // 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; inputState.holdFish = null; // Reset any previous hold target var hookX = fishingElements.hook.x; for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (fish.lane === inputState.touchLane && fish.isHoldFish && !fish.caught && !fish.isHeld) { var distanceToHook = Math.abs(fish.x - hookX); if (distanceToHook < GAME_CONFIG.MISS_WINDOW + 20) { // Proximity check inputState.holdFish = fish; fish.isHeld = true; // Fish stops moving via its update() fish.holdStarted = false; // Reset primed state, game.update will set it break; } } } // If not a hold fish, or no hold fish in range, inputState.holdFish remains null. // A normal tap action will be processed on 'up' if inputState.holdFish is null. } else { // Touch ended (isUp) if (inputState.touching) { var holdDuration = currentTime - inputState.touchStartTime; if (inputState.holdFish) { // A hold was active on a specific fish var fishBeingHeld = inputState.holdFish; fishBeingHeld.isHeld = false; // Allow fish to move again if not caught // checkCatch will determine if hold was successful (duration, proximity) checkCatch(fishBeingHeld.lane, true, holdDuration, fishBeingHeld); } else { // This was a normal tap (inputState.holdFish is null) // Or a tap that didn't successfully initiate a hold on a hold fish checkCatch(inputState.touchLane, false, holdDuration, null); } } inputState.touching = false; inputState.holdFish = null; // Reset hold target } } /**** * Screen Management ****/ function showScreen(screenName) { titleScreen.visible = false; levelSelectScreen.visible = false; fishingScreen.visible = false; resultsScreen.visible = false; GameState.currentScreen = screenName; switch (screenName) { case 'title': titleScreen.visible = true; break; case 'levelSelect': levelSelectScreen.visible = true; updateLevelSelectScreen(); break; case 'fishing': fishingScreen.visible = true; startFishingSession(); break; case 'results': resultsScreen.visible = true; break; } } /**** * Level Select Logic ****/ function updateLevelSelectScreen() { var elements = levelSelectElements; // Update money display elements.moneyDisplay.setText('Money: $' + GameState.money); // Create depth tabs createDepthTabs(); // Update song display updateSongDisplay(); // Update shop button updateShopButton(); } function createDepthTabs() { // Clear existing tabs levelSelectElements.depthTabs.forEach(function (tab) { if (tab.container) { tab.container.destroy(); } }); levelSelectElements.depthTabs = []; // Create tabs for unlocked depths for (var i = 0; i <= GameState.currentDepth; i++) { var depth = GAME_CONFIG.DEPTHS[i]; var isSelected = i === GameState.selectedDepth; var tabContainer = levelSelectScreen.addChild(new Container()); var tab = tabContainer.addChild(LK.getAsset('depthTab', { anchorX: 0.5, anchorY: 0.5, x: 300 + i * 220, y: 400, tint: isSelected ? 0x1976d2 : 0x455a64 })); var tabText = new Text2(depth.name.split(' ')[0], { size: 30, fill: 0xFFFFFF }); tabText.anchor.set(0.5, 0.5); tabText.x = 300 + i * 220; tabText.y = 400; tabContainer.addChild(tabText); levelSelectElements.depthTabs.push({ container: tabContainer, tab: tab, depthIndex: i }); } } function updateSongDisplay() { var elements = levelSelectElements; var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth]; var song = depth.songs[GameState.selectedSong]; var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong); // Update song info elements.songTitle.setText(song.name); elements.songInfo.setText('BPM: ' + song.bpm + ' | Duration: ' + formatTime(song.duration)); // Calculate potential earnings var minEarnings = Math.floor(depth.fishValue * 20); // Conservative estimate var maxEarnings = Math.floor(depth.fishValue * 60); // With combos and rare fish elements.songEarnings.setText('Potential Earnings: $' + minEarnings + '-$' + maxEarnings); // Update play/buy button if (owned) { elements.playButtonText.setText('PLAY'); elements.playButton.tint = 0x1976d2; } else { elements.playButtonText.setText('BUY ($' + song.cost + ')'); elements.playButton.tint = GameState.money >= song.cost ? 0x2e7d32 : 0x666666; } // Update arrow states elements.leftArrow.tint = GameState.selectedSong > 0 ? 0x1976d2 : 0x666666; elements.rightArrow.tint = GameState.selectedSong < depth.songs.length - 1 ? 0x1976d2 : 0x666666; } function updateShopButton() { var elements = levelSelectElements; var canUpgrade = GameState.canUpgrade(); var nextDepth = GAME_CONFIG.DEPTHS[GameState.currentDepth + 1]; if (nextDepth) { elements.shopButtonText.setText('UPGRADE ROD ($' + nextDepth.upgradeCost + ')'); elements.shopButton.tint = canUpgrade ? 0x2e7d32 : 0x666666; } else { elements.shopButtonText.setText('MAX DEPTH REACHED'); elements.shopButton.tint = 0x666666; } } function formatTime(ms) { var seconds = Math.floor(ms / 1000); var minutes = Math.floor(seconds / 60); seconds = seconds % 60; return minutes + ':' + (seconds < 10 ? '0' : '') + seconds; } /**** * 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; // 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]; // If options.forceNonHold is true, this fish cannot be a hold fish. // Otherwise, determine based on normal chance. var isHoldFish = options.forceNonHold ? false : Math.random() < (pattern.holdFishChance || 0); 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; } if (isHoldFish) { fishValue = Math.floor(fishValue * 1.5); } 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, isHoldFish); newFish.spawnSide = spawnSide; // Store the side it spawned from newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen newFish.y = targetLane.y; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTimeForRegistration, newFish.isHoldFish); return newFish; } function checkCatch(fishLane, isHoldAction, holdDurationMs, specificHeldFish) { var hookX = fishingElements.hook.x; var closestFishInLane = null; var closestDistance = Infinity; if (specificHeldFish) { // This is the release of a hold action on a specific fish. closestFishInLane = specificHeldFish; // Distance check is still important for scoring. // The fish should be at hookX because it was held, but check anyway. closestDistance = Math.abs(closestFishInLane.x - hookX); if (closestFishInLane.lane !== fishLane) { // Should not happen if isHeld worked showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } } else { // This is a tap action, find the closest fish in the tapped lane. // Do not consider fish that are currently being held by an active input. for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (!fish.caught && fish.lane === fishLane && !fish.isHeld) { var distance = Math.abs(fish.x - hookX); if (distance < closestDistance) { closestDistance = distance; closestFishInLane = fish; } } } } if (!closestFishInLane) { // No fish found for tap, or specificHeldFish was somehow null (shouldn't be if called correctly) showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // --- Hold Fish Logic --- if (closestFishInLane.isHoldFish) { if (!isHoldAction) { // Tapped a hold fish (expected a hold, not a tap). showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // It is a hold action, check if duration was sufficient. // fish.holdStarted (primed state) is a visual cue; actual duration check is here. if (holdDurationMs < closestFishInLane.requiredHoldTime) { // Held, but not long enough. showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // If we reach here, hold duration was sufficient. Proximity check below will determine score. } else { // Normal Fish Logic if (isHoldAction) { // Held a normal fish (counts as a miss/mistake). showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } } // --- End Hold Fish 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); LK.getSound('catch').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 water surface wave animations if (fishingElements && fishingElements.waterSurfaceSegments) { fishingElements.waterSurfaceSegments.forEach(function (segment) { if (segment && !segment.destroyed) { tween.stop(segment); } }); } // Fade out and stop music for the results screen var currentMusicId = GameState.currentPlayingMusicId; var currentMusicInitialVolume = GameState.currentPlayingMusicInitialVolume; var fadeOutDurationMs = 1000; // Duration of the fade out in milliseconds (e.g., 1 second) LK.playMusic(currentMusicId, { fade: { start: currentMusicInitialVolume, end: 0, duration: fadeOutDurationMs }, loop: false // Important to prevent restart after fade or if it's re-triggered }); // Ensure music is fully stopped after the fade duration has elapsed LK.setTimeout(function () { LK.stopMusic(); }, fadeOutDurationMs); // Clear fish fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; // Create results screen createResultsScreen(); showScreen('results'); } function createResultsScreen() { // Clear previous results resultsScreen.removeChildren(); var resultsBg = resultsScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.9 })); var title = new Text2('Fishing Complete!', { size: 100, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.x = GAME_CONFIG.SCREEN_CENTER_X; title.y = 400; resultsScreen.addChild(title); var scoreResult = new Text2('Score: ' + GameState.sessionScore, { size: 70, fill: 0xFFD700 }); scoreResult.anchor.set(0.5, 0.5); scoreResult.x = GAME_CONFIG.SCREEN_CENTER_X; scoreResult.y = 550; resultsScreen.addChild(scoreResult); var fishResult = new Text2('Fish Caught: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned, { size: 50, fill: 0xFFFFFF }); fishResult.anchor.set(0.5, 0.5); fishResult.x = GAME_CONFIG.SCREEN_CENTER_X; fishResult.y = 650; resultsScreen.addChild(fishResult); var comboResult = new Text2('Max Combo: ' + GameState.maxCombo, { size: 50, fill: 0xFF9800 }); comboResult.anchor.set(0.5, 0.5); comboResult.x = GAME_CONFIG.SCREEN_CENTER_X; comboResult.y = 750; resultsScreen.addChild(comboResult); var moneyEarned = new Text2('Money Earned: $' + GameState.sessionScore, { size: 50, fill: 0x4CAF50 }); moneyEarned.anchor.set(0.5, 0.5); moneyEarned.x = GAME_CONFIG.SCREEN_CENTER_X; moneyEarned.y = 850; resultsScreen.addChild(moneyEarned); // Accuracy var accuracy = GameState.sessionFishSpawned > 0 ? Math.round(GameState.sessionFishCaught / GameState.sessionFishSpawned * 100) : 0; var accuracyResult = new Text2('Accuracy: ' + accuracy + '%', { size: 50, fill: 0x2196F3 }); accuracyResult.anchor.set(0.5, 0.5); accuracyResult.x = GAME_CONFIG.SCREEN_CENTER_X; accuracyResult.y = 950; resultsScreen.addChild(accuracyResult); // Continue button var continueButton = resultsScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1200 })); var continueText = new Text2('CONTINUE', { size: 50, fill: 0xFFFFFF }); continueText.anchor.set(0.5, 0.5); continueText.x = GAME_CONFIG.SCREEN_CENTER_X; continueText.y = 1200; resultsScreen.addChild(continueText); // Fade in resultsScreen.alpha = 0; tween(resultsScreen, { alpha: 1 }, { duration: 500, easing: tween.easeOut }); } /**** * Input Handling ****/ game.down = function (x, y, obj) { LK.getSound('buttonClick').play(); switch (GameState.currentScreen) { case 'title': // Check if click is within start button bounds var startButton = titleElements.startButton; if (x >= startButton.x - startButton.width / 2 && x <= startButton.x + startButton.width / 2 && y >= startButton.y - startButton.height / 2 && y <= startButton.y + startButton.height / 2) { showScreen('levelSelect'); } // Check if click is within tutorial button bounds var tutorialButton = titleElements.tutorialButton; if (x >= tutorialButton.x - tutorialButton.width / 2 && x <= tutorialButton.x + tutorialButton.width / 2 && y >= tutorialButton.y - tutorialButton.height / 2 && y <= tutorialButton.y + tutorialButton.height / 2) { // TODO: Show tutorial } break; case 'levelSelect': handleLevelSelectInput(x, y); break; case 'fishing': handleFishingInput(x, y, true); // true for isDown break; case 'results': showScreen('levelSelect'); break; } }; function handleLevelSelectInput(x, y) { var elements = levelSelectElements; // Check depth tabs elements.depthTabs.forEach(function (tab) { var tabAsset = tab.tab; if (x >= tabAsset.x - tabAsset.width / 2 && x <= tabAsset.x + tabAsset.width / 2 && y >= tabAsset.y - tabAsset.height / 2 && y <= tabAsset.y + tabAsset.height / 2) { GameState.selectedDepth = tab.depthIndex; GameState.selectedSong = 0; // Reset to first song updateLevelSelectScreen(); } }); // Check song navigation var leftArrow = elements.leftArrow; if (x >= leftArrow.x - leftArrow.width / 2 && x <= leftArrow.x + leftArrow.width / 2 && y >= leftArrow.y - leftArrow.height / 2 && y <= leftArrow.y + leftArrow.height / 2 && GameState.selectedSong > 0) { GameState.selectedSong--; updateSongDisplay(); } var rightArrow = elements.rightArrow; if (x >= rightArrow.x - rightArrow.width / 2 && x <= rightArrow.x + rightArrow.width / 2 && y >= rightArrow.y - rightArrow.height / 2 && y <= rightArrow.y + rightArrow.height / 2) { var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth]; if (GameState.selectedSong < depth.songs.length - 1) { GameState.selectedSong++; updateSongDisplay(); } } // Check play/buy button var playButton = elements.playButton; if (x >= playButton.x - playButton.width / 2 && x <= playButton.x + playButton.width / 2 && y >= playButton.y - playButton.height / 2 && y <= playButton.y + playButton.height / 2) { var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong); if (owned) { showScreen('fishing'); } else { // Try to buy song if (GameState.buySong(GameState.selectedDepth, GameState.selectedSong)) { updateLevelSelectScreen(); } } } // Check shop button var shopButton = elements.shopButton; if (x >= shopButton.x - shopButton.width / 2 && x <= shopButton.x + shopButton.width / 2 && y >= shopButton.y - shopButton.height / 2 && y <= shopButton.y + shopButton.height / 2) { if (GameState.upgrade()) { LK.getSound('upgrade').play(); updateLevelSelectScreen(); } } // Check back button var backButton = elements.backButton; if (x >= backButton.x - backButton.width / 2 && x <= backButton.x + backButton.width / 2 && y >= backButton.y - backButton.height / 2 && y <= backButton.y + backButton.height / 2) { showScreen('title'); } } /**** * Main Game Loop ****/ game.update = function () { if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } 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) { // NEW: Only proceed with multi-beat if the first fish is NOT a hold fish. if (!firstFish.isHoldFish) { var isPotentiallyTriplet = pattern.tripletSpawnChance && Math.random() < pattern.tripletSpawnChance; 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; secondFishOptions.forceNonHold = true; // Second fish in a multi-beat pattern cannot be a hold fish 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; thirdFishOptions.forceNonHold = true; // Third fish in a multi-beat pattern cannot be a hold fish spawnFish(LK.ticks * (1000 / 60), thirdFishOptions); }, TRIPLET_BEAT_SPAWN_DELAY_MS); // Third fish spawns TRIPLET_BEAT_SPAWN_DELAY_MS after the second } }, MULTI_BEAT_SPAWN_DELAY_MS); // Second fish spawns MULTI_BEAT_SPAWN_DELAY_MS after the first } } } } } // 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); // Consider fish that are moving towards the hook OR are very close to it already 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 { // No fish approaching, hook returns/stays at middle lane (or last known target) // For simplicity, let's use the last known/default target index. targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; } // Tween hook and line to the target Y if not already there or already tweening // A more robust solution would check if a tween is active on the hook. // For now, we'll just update if the current Y is different. if (fishingElements.hook.y !== targetLaneY) { tween(fishingElements.hook, { y: targetLaneY }, { duration: 150, easing: tween.easeOut }); tween(fishingElements.line, { height: targetLaneY - GAME_CONFIG.WATER_SURFACE_Y }, { duration: 150, easing: tween.easeOut }); fishingElements.hook.originalY = targetLaneY; // Update reference for animations } // Update fish for (var i = fishArray.length - 1; i >= 0; i--) { var fish = fishArray[i]; fish.update(); // Fish updates its own movement and appearance based on isHeld, holdStarted // Remove off-screen fish (if not caught and not currently held) // A held fish should not be removed for being off-screen as its X is static. if (!fish.caught && !fish.isHeld && (fish.x < -250 || fish.x > 2048 + 250)) { // Increased buffer slightly fish.destroy(); fishArray.splice(i, 1); } } // Update holdStarted state for the currently active hold fish if (inputState.holdFish && inputState.holdFish.isHeld && inputState.touching) { var currentActiveHoldFish = inputState.holdFish; var currentHoldDuration = currentTime - inputState.touchStartTime; if (currentHoldDuration >= currentActiveHoldFish.requiredHoldTime) { if (!currentActiveHoldFish.holdStarted) { currentActiveHoldFish.holdStarted = true; // Fish is now "primed" } } else { // If, for some reason, duration is no longer met (e.g. timer issue, though unlikely) // or to ensure it's false before being primed. if (currentActiveHoldFish.holdStarted) { currentActiveHoldFish.holdStarted = false; } } } // Update UI updateFishingUI(); }; // Initialize game showScreen('title');
===================================================================
--- original.js
+++ change.js
@@ -37,8 +37,13 @@
var fishGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
+ // Flip fish based on movement direction
+ if (speed > 0) {
+ // Moving right (coming from left), flip horizontally
+ fishGraphics.scaleX = -1;
+ }
self.type = type;
self.value = value;
self.speed = speed;
self.lane = lane; // Index of the lane (0, 1, or 2)
@@ -1288,12 +1293,8 @@
var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex, isHoldFish);
newFish.spawnSide = spawnSide; // Store the side it spawned from
newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen
newFish.y = targetLane.y;
- // Flip fish horizontally if spawning from left (moving right)
- if (spawnSide === -1) {
- newFish.scaleX = -1; // Flip horizontally to face right
- }
fishArray.push(newFish);
fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTimeForRegistration, newFish.isHoldFish);
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