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