User prompt
Update only as needed with: self.pushBack = function() { // ... lane selection code ... // Get the NEXT beat in the pattern (not current) self.currentPatternIndex++; // Move to next step first if (self.currentPatternIndex >= self.rhythmPattern.length) { // Pattern complete - shouldn't happen in pushBack return; } var nextBeat = self.rhythmPattern[self.currentPatternIndex]; // Now get the NEXT beat var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var timeToNextTap = nextBeat === 'beat' ? beatInterval : beatInterval / 2; // ... rest of distance/speed calculation using timeToNextTap ... }; ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Update with: self.pushBack = function() { self.isPushedBack = true; // Move to random different lane var newLane = self.lane; var availableLanes = []; for (var i = 0; i < GAME_CONFIG.LANES.length; i++) { if (i !== self.lane) availableLanes.push(i); } if (availableLanes.length > 0) { newLane = availableLanes[Math.floor(Math.random() * availableLanes.length)]; } self.lane = newLane; var targetY = GAME_CONFIG.LANES[newLane].y; self.baseY = targetY; // Calculate timing for next approach var currentBeat = self.rhythmPattern[self.currentPatternIndex]; var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var timeToNextTap = currentBeat === 'beat' ? beatInterval : beatInterval / 2; // Calculate pushback distance based on timing var framesForNextApproach = timeToNextTap / (1000/60); var distanceToHook = Math.abs(GAME_CONFIG.SCREEN_CENTER_X - self.x); var totalDistanceNeeded = distanceToHook + 200; // 200 = minimum pushback var maxPushback = framesForNextApproach * Math.abs(self.originalSpeed) - distanceToHook; var actualPushback = Math.min(Math.max(200, maxPushback), 400); // Between 200-400 pixels // Calculate pushback position var pushBackX = self.speed > 0 ? self.x - actualPushback : self.x + actualPushback; // Calculate new speed to reach hook exactly on time var totalDistance = Math.abs(GAME_CONFIG.SCREEN_CENTER_X - pushBackX); var newSpeed = totalDistance / framesForNextApproach; if (pushBackX > GAME_CONFIG.SCREEN_CENTER_X) { newSpeed = -newSpeed; } self.currentPatternIndex++; // One smooth motion: push back while moving to new lane, then continue to hook tween(self, { x: pushBackX, y: targetY }, { duration: 200, // Quick pushback easing: tween.easeOut, onFinish: function() { // Immediately start moving toward hook at calculated speed self.speed = newSpeed; self.isPushedBack = false; } }); }; // Remove the scheduleNextApproach method since we don't need it anymore
User prompt
Update with: ImprovedRhythmSpawner.spawnSingleFish = function(currentTime, beatInterval) { var depthConfig = GameState.getCurrentDepthConfig(); // Use the original fish speed from config var fishSpeed = depthConfig.fishSpeed; // This is the normal speed var spawnSide = Math.random() < 0.5 ? -1 : 1; var actualSpeed = Math.abs(fishSpeed) * spawnSide; // Determine fish type var laneIndex = PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; var fishType, fishValue; var rand = Math.random(); if (rand < 0.05) { 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 newFish = new Fish(fishType, fishValue, actualSpeed, laneIndex); newFish.x = actualSpeed > 0 ? -150 : 2048 + 150; newFish.y = targetLane.y; newFish.baseY = targetLane.y; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; }; ``` And simplify the main update to just spawn immediately when ready: ```javascript ImprovedRhythmSpawner.update = function(currentTime) { if (!GameState.gameActive || GameState.songStartTime === 0) { return; } // Only spawn if no battle is active and no fish are on screen if (GameState.battleState !== BATTLE_STATES.NONE || fishArray.length > 0) { return; } // If we're waiting for next spawn time if (GameState.nextFishSpawnTime > 0 && currentTime < GameState.nextFishSpawnTime) { return; } var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; // Spawn immediately with normal speed this.spawnSingleFish(currentTime, beatInterval); };
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'self.missedTap = function () { // Fish escapes due to miss __$(2624); self.missed = true; __$(2625); self.endBattle(false); // wasSuccessful is false // Remove fish from array __$(2626); var fishIndex = fishArray.indexOf(self); __$(2627); if (fishIndex > -1) { __$(2628); fishArray.splice(fishIndex, 1); } // Animate fish swimming away __$(2629); var escapeSpeed = Math.abs(self.originalSpeed) * 2; __$(2630); if (self.x > GAME_CONFIG.SCREEN_CENTER_X) { __$(2631); escapeSpeed = escapeSpeed; // swim right } else { __$(2632); escapeSpeed = -escapeSpeed; // swim left } __$(2633); self.speed = escapeSpeed; // Hide tap counter if it exists and fish is missed __$(2634); if (self.isMultiTapFish) { __$(2635); if (self.counterBg && !self.counterBg.destroyed) { __$(2637); self.counterBg.destroy(); } __$(2638); if (self.tapCounter && !self.tapCounter.destroyed) { __$(2640); self.tapCounter.destroy(); } } __$(2641); LK.setTimeout(function () { __$(2642); if (!self.destroyed) { __$(2643); self.destroy(); } }, 2000); // Give 2 seconds for fish to swim off screen }')' in or related to this line: 'self.missedTap = function () {' Line Number: 5128
User prompt
Please fix the bug: 'undefined is not an object (evaluating 'self.missedTap = function () { // Fish escapes due to miss __$(2624); self.missed = true; __$(2625); self.endBattle(false); // wasSuccessful is false // Remove fish from array __$(2626); var fishIndex = fishArray.indexOf(self); __$(2627); if (fishIndex > -1) { __$(2628); fishArray.splice(fishIndex, 1); } // Animate fish swimming away __$(2629); var escapeSpeed = Math.abs(self.originalSpeed) * 2; __$(2630); if (self.x > GAME_CONFIG.SCREEN_CENTER_X) { __$(2631); escapeSpeed = escapeSpeed; // swim right } else { __$(2632); escapeSpeed = -escapeSpeed; // swim left } __$(2633); self.speed = escapeSpeed; // Hide tap counter if it exists and fish is missed __$(2634); if (self.isMultiTapFish) { __$(2635); if (self.counterBg && !self.counterBg.destroyed) { __$(2637); self.counterBg.destroy(); } __$(2638); if (self.tapCounter && !self.tapCounter.destroyed) { __$(2640); self.tapCounter.destroy(); } } __$(2641); LK.setTimeout(function () { __$(2642); if (!self.destroyed) { __$(2643); self.destroy(); } }, 2000); // Give 2 seconds for fish to swim off screen }')' in or related to this line: 'self.missedTap = function () {' Line Number: 5128
User prompt
Update only as needed with: // Modified Fish methods // In the Fish class, modify these methods: self.handleTap = function() { if (!self.isMultiTapFish) { // Regular single-tap fish return true; } self.currentTaps++; var remainingTaps = self.maxTaps - self.currentTaps; if (self.tapCounter) { self.tapCounter.setText(remainingTaps.toString()); } if (remainingTaps <= 0) { // Fish is fully caught return true; } // Push fish back and continue battle self.pushBack(); return false; }; self.startBattle = function() { if (!self.isInBattle) { self.isInBattle = true; GameState.battleState = BATTLE_STATES.ACTIVE; GameState.currentBattleFish = self; } }; self.endBattle = function(wasSuccessful) { self.isInBattle = false; GameState.battleState = BATTLE_STATES.NONE; GameState.currentBattleFish = null; // Schedule next fish ImprovedRhythmSpawner.scheduleNextFish(); }; self.missedTap = function() { // Fish escapes due to miss self.missed = true; self.endBattle(false); // Remove fish from array var fishIndex = fishArray.indexOf(self); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } // Animate fish swimming away var escapeSpeed = Math.abs(self.originalSpeed) * 2; if (self.x > GAME_CONFIG.SCREEN_CENTER_X) { escapeSpeed = escapeSpeed; // swim right } else { escapeSpeed = -escapeSpeed; // swim left } self.speed = escapeSpeed; LK.setTimeout(function() { if (!self.destroyed) { self.destroy(); } }, 2000); }; // Modified catchFish self.catchFish = function () { self.caught = true; if (self.isInBattle) { self.endBattle(true); } else { // Single-tap fish - still schedule next ImprovedRhythmSpawner.scheduleNextFish(); } // Rest of existing catchFish animation code... var currentFishX = self.x; var currentFishY = self.y; var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; var boatLandingY = GAME_CONFIG.BOAT_Y; var peakArcY = boatLandingY - 150; var peakArcX = currentFishX + (boatCenterX - currentFishX) * 0.5; var durationPhase1 = 350; var durationPhase2 = 250; tween(self, { x: peakArcX, y: peakArcY, scaleX: 0.75, scaleY: 0.75, alpha: 0.8 }, { duration: durationPhase1, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { x: boatCenterX, y: boatLandingY, scaleX: 0.2, scaleY: 0.2, alpha: 0 }, { duration: durationPhase2, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); }; // Modified checkCatch function - add miss handling function checkCatch(fishLane) { var hookX = fishingElements.hook.x; // ... existing tutorial and fish finding logic ... if (!closestFishInLane) { // Miss - if we're in battle, end it LK.getSound('miss').play(); GameState.combo = 0; // Check if we missed during a battle if (GameState.battleState === BATTLE_STATES.ACTIVE && GameState.currentBattleFish) { GameState.currentBattleFish.missedTap(); } // ... existing bracket flashing code ... return; } // ... existing catch quality logic ... var isFullyCaught = closestFishInLane.handleTap(); if (isFullyCaught) { // Fish completely caught closestFishInLane.catchFish(); var fishIndex = fishArray.indexOf(closestFishInLane); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } // ... existing scoring logic ... } else { // Fish tapped but not fully caught - start/continue battle if (!closestFishInLane.isInBattle) { closestFishInLane.startBattle(); } } // ... existing sound and animation code ... } // Reset battle state when starting new session // Add to startFishingSession function: function startFishingSession() { // ... existing reset code ... // Reset battle state GameState.battleState = BATTLE_STATES.NONE; GameState.currentBattleFish = null; GameState.nextFishSpawnTime = 0; // ... rest of existing code ... }
User prompt
Update as needed with: // Simplified battle system - one fish at a time var BATTLE_STATES = { NONE: 'none', ACTIVE: 'active', WAITING_FOR_NEXT: 'waiting' }; // Add to GameState GameState.battleState = BATTLE_STATES.NONE; GameState.currentBattleFish = null; GameState.nextFishSpawnTime = 0; // Modified ImprovedRhythmSpawner - completely rewrite the update function ImprovedRhythmSpawner.update = function(currentTime) { if (!GameState.gameActive || GameState.songStartTime === 0) { return; } // Only spawn if no battle is active and no fish are on screen if (GameState.battleState !== BATTLE_STATES.NONE || fishArray.length > 0) { return; } var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; // If we're waiting for next spawn time if (GameState.nextFishSpawnTime > 0 && currentTime < GameState.nextFishSpawnTime) { return; } // Spawn one fish this.spawnSingleFish(currentTime, beatInterval); }; ImprovedRhythmSpawner.spawnSingleFish = function(currentTime, beatInterval) { var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); // Calculate when fish should arrive at hook (next beat) var songElapsed = currentTime - GameState.songStartTime; var beatsElapsed = songElapsed / beatInterval; var nextBeat = Math.ceil(beatsElapsed); var targetArrivalTime = GameState.songStartTime + (nextBeat * beatInterval); // Calculate spawn timing var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var travelTimeMs = distanceToHook / fishSpeed * (1000 / 60); var spawnTime = targetArrivalTime - travelTimeMs; // If spawn time is in the past, use next beat if (spawnTime <= currentTime) { nextBeat++; targetArrivalTime = GameState.songStartTime + (nextBeat * beatInterval); spawnTime = targetArrivalTime - travelTimeMs; } var delay = spawnTime - currentTime; LK.setTimeout(function() { if (GameState.gameActive && GameState.battleState === BATTLE_STATES.NONE && fishArray.length === 0) { // Determine fish type var laneIndex = PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; var fishType, fishValue; var rand = Math.random(); if (rand < 0.05) { // 5% rare 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 exact speed needed var timeRemaining = targetArrivalTime - (LK.ticks * (1000/60)); var framesRemaining = timeRemaining / (1000/60); var spawnSide = Math.random() < 0.5 ? -1 : 1; var requiredSpeed = distanceToHook / framesRemaining; if (spawnSide === 1) requiredSpeed *= -1; var newFish = new Fish(fishType, fishValue, requiredSpeed, laneIndex); newFish.x = requiredSpeed > 0 ? -150 : 2048 + 150; newFish.y = targetLane.y; newFish.baseY = targetLane.y; newFish.targetArrivalTime = targetArrivalTime; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; } }, Math.max(0, delay)); }; ImprovedRhythmSpawner.scheduleNextFish = function() { var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var currentTime = LK.ticks * (1000 / 60); // Schedule next fish spawn for 1-2 beats later var beatsDelay = 1 + Math.random(); // 1-2 beats GameState.nextFishSpawnTime = currentTime + (beatInterval * beatsDelay); GameState.battleState = BATTLE_STATES.WAITING_FOR_NEXT; // Clear waiting state after delay LK.setTimeout(function() { if (GameState.battleState === BATTLE_STATES.WAITING_FOR_NEXT) { GameState.battleState = BATTLE_STATES.NONE; GameState.nextFishSpawnTime = 0; } }, beatInterval * beatsDelay); };
User prompt
Implement this new fish catching system as needed: // Add to GAME_CONFIG var BATTLE_STATES = { NONE: 'none', ACTIVE: 'active' }; // Add to GameState GameState.battleState = BATTLE_STATES.NONE; GameState.currentBattleFish = null; // Fish rhythm patterns by type var FISH_RHYTHM_PATTERNS = { sardine: ['beat', 'offbeat', 'beat'], // 3 taps anchovy: ['beat', 'offbeat', 'offbeat', 'beat'], // 4 taps mackerel: ['beat', 'beat', 'offbeat'], // 3 taps rareFish: ['beat', 'offbeat', 'beat', 'offbeat', 'beat'] // 5 taps }; // Modified Fish class var Fish = Container.expand(function (type, value, speed, lane) { var self = Container.call(this); // Existing asset selection logic var assetName = type + 'Fish'; if (type === 'shallow') { var currentSongConfig = null; if (typeof GameState !== 'undefined' && GameState && typeof GameState.getCurrentSongConfig === 'function') { currentSongConfig = GameState.getCurrentSongConfig(); } if (currentSongConfig && currentSongConfig.name === "Gentle Waves") { var randGentle = Math.random(); if (randGentle < 0.60) { assetName = 'anchovy'; } else if (randGentle < 0.90) { assetName = 'sardine'; } else { assetName = 'mackerel'; } } else if (currentSongConfig && currentSongConfig.name === "Morning Tide") { var randMorning = Math.random(); if (randMorning < 0.40) { assetName = 'anchovy'; } else if (randMorning < 0.80) { assetName = 'sardine'; } else { assetName = 'mackerel'; } } else if (currentSongConfig && currentSongConfig.name === "Sunny Afternoon") { var randSunny = Math.random(); if (randSunny < 0.30) { assetName = 'anchovy'; } else if (randSunny < 0.60) { assetName = 'sardine'; } else { assetName = 'mackerel'; } } else { var shallowFishAssets = ['sardine', 'anchovy', 'mackerel']; assetName = shallowFishAssets[Math.floor(Math.random() * shallowFishAssets.length)]; } } self.fishGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); if (speed > 0) { self.fishGraphics.scaleX = -1; } // Existing properties self.type = type; self.value = value; self.speed = speed; self.lane = lane; self.caught = false; self.missed = false; self.lastX = 0; self.isSpecial = type === 'rare'; self.shimmerTime = 0; self.lastBubbleSpawnTime = 0; self.bubbleSpawnInterval = 120 + Math.random() * 80; self.swimTime = Math.random() * Math.PI * 2; self.baseY = self.y; self.scaleTime = 0; self.baseScale = 1; // NEW: Multi-tap properties var fishTypeName = assetName; self.rhythmPattern = FISH_RHYTHM_PATTERNS[fishTypeName] || ['beat']; // fallback to single tap self.maxTaps = self.rhythmPattern.length; self.currentTaps = 0; self.isMultiTapFish = self.maxTaps > 1; self.currentPatternIndex = 0; self.isPushedBack = false; self.originalSpeed = speed; self.isInBattle = false; // Create tap counter display for multi-tap fish if (self.isMultiTapFish) { self.counterBg = self.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 0, y: -80, width: 80, height: 80, tint: 0x1976d2, alpha: 0.8 })); self.tapCounter = self.addChild(new Text2(self.maxTaps.toString(), { size: 60, fill: 0xFFFFFF, stroke: 0x000000, strokeThickness: 3 })); self.tapCounter.anchor.set(0.5, 0.5); self.tapCounter.x = 0; self.tapCounter.y = -80; } // Existing update method with modifications self.update = function () { if (!self.caught && !self.isPushedBack) { self.x += self.speed; self.swimTime += 0.08; var swimAmplitude = 15; self.y = self.baseY + Math.sin(self.swimTime) * swimAmplitude; 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; var scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15; var baseScaleXDirection = (self.speed > 0 ? -1 : 1) * self.baseScale; self.fishGraphics.scaleX = baseScaleXDirection * scalePulse; self.fishGraphics.scaleY = scalePulse * self.baseScale; } if (self.isSpecial) { self.shimmerTime += 0.1; self.fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2; } else { self.fishGraphics.alpha = 1.0; } } }; // NEW: Handle multi-tap logic self.handleTap = function() { if (!self.isMultiTapFish) { // Regular single-tap fish - just catch it return true; } // Multi-tap fish logic self.currentTaps++; var remainingTaps = self.maxTaps - self.currentTaps; // Update counter display if (self.tapCounter) { self.tapCounter.setText(remainingTaps.toString()); } if (remainingTaps <= 0) { // Fish is fully caught return true; } // Push fish back and prepare for next approach self.pushBack(); return false; // Not fully caught yet }; self.pushBack = function() { self.isPushedBack = true; // Move to random different lane var newLane = self.lane; var availableLanes = []; for (var i = 0; i < GAME_CONFIG.LANES.length; i++) { if (i !== self.lane) availableLanes.push(i); } if (availableLanes.length > 0) { newLane = availableLanes[Math.floor(Math.random() * availableLanes.length)]; } self.lane = newLane; var targetY = GAME_CONFIG.LANES[newLane].y; self.baseY = targetY; // Push back animation var pushDistance = 300; var pushBackX = self.speed > 0 ? self.x - pushDistance : self.x + pushDistance; // Stop current movement self.speed = 0; tween(self, { x: pushBackX, y: targetY }, { duration: 200, easing: tween.easeOut, onFinish: function() { self.scheduleNextApproach(); } }); }; self.scheduleNextApproach = function() { if (self.currentPatternIndex >= self.rhythmPattern.length) { return; // Pattern complete } var currentBeat = self.rhythmPattern[self.currentPatternIndex]; var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var delay = currentBeat === 'beat' ? beatInterval : beatInterval / 2; self.currentPatternIndex++; LK.setTimeout(function() { if (!self.destroyed && !self.caught) { // Calculate new approach speed var distanceToHook = Math.abs(self.x - GAME_CONFIG.SCREEN_CENTER_X); var timeToReach = currentBeat === 'beat' ? beatInterval : beatInterval / 2; var framesNeeded = timeToReach / (1000/60); var newSpeed = distanceToHook / framesNeeded; if (self.x > GAME_CONFIG.SCREEN_CENTER_X) { newSpeed = -newSpeed; } self.speed = newSpeed; self.isPushedBack = false; } }, delay); }; self.startBattle = function() { self.isInBattle = true; GameState.battleState = BATTLE_STATES.ACTIVE; GameState.currentBattleFish = self; }; self.endBattle = function() { self.isInBattle = false; GameState.battleState = BATTLE_STATES.NONE; GameState.currentBattleFish = null; // Schedule next fish spawn on the next beat var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var currentTime = LK.ticks * (1000 / 60); var timeSinceLastBeat = (currentTime - GameState.songStartTime) % beatInterval; var timeToNextBeat = beatInterval - timeSinceLastBeat; LK.setTimeout(function() { if (GameState.gameActive && GameState.battleState === BATTLE_STATES.NONE) { // Resume normal spawning ImprovedRhythmSpawner.resumeAfterBattle(); } }, timeToNextBeat); }; // Modified catchFish for battle fish self.catchFish = function () { self.caught = true; if (self.isInBattle) { self.endBattle(); } var currentFishX = self.x; var currentFishY = self.y; var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; var boatLandingY = GAME_CONFIG.BOAT_Y; var peakArcY = boatLandingY - 150; var peakArcX = currentFishX + (boatCenterX - currentFishX) * 0.5; var durationPhase1 = 350; var durationPhase2 = 250; tween(self, { x: peakArcX, y: peakArcY, scaleX: 0.75, scaleY: 0.75, alpha: 0.8 }, { duration: durationPhase1, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { x: boatCenterX, y: boatLandingY, scaleX: 0.2, scaleY: 0.2, alpha: 0 }, { duration: durationPhase2, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); }; return self; }); // Modified checkCatch function function checkCatch(fishLane) { var hookX = fishingElements.hook.x; // Tutorial mode handling (unchanged) if (GameState.tutorialMode) { // ... existing tutorial logic ... return; } // Find closest fish in lane var closestFishInLane = null; var closestDistance = Infinity; for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (!fish.caught && !fish.missed && fish.lane === fishLane && !fish.isPushedBack) { var distance = Math.abs(fish.x - hookX); var maxCatchDistance = GAME_CONFIG.MISS_WINDOW * 2; if (distance < maxCatchDistance && distance < closestDistance) { closestDistance = distance; closestFishInLane = fish; } } } if (!closestFishInLane) { // Miss - play sound and break combo LK.getSound('miss').play(); GameState.combo = 0; // Flash lane brackets red (existing code) if (laneBrackets && laneBrackets[fishLane]) { var leftBracket = laneBrackets[fishLane].left; var rightBracket = laneBrackets[fishLane].right; var tintToRedDuration = 50; var holdRedDuration = 100; var tintToWhiteDuration = 150; if (leftBracket && !leftBracket.destroyed) { tween(leftBracket, { tint: 0xFF0000 }, { duration: tintToRedDuration, easing: tween.linear, onFinish: function onFinish() { LK.setTimeout(function () { if (leftBracket && !leftBracket.destroyed) { tween(leftBracket, { tint: 0xFFFFFF }, { duration: tintToWhiteDuration, easing: tween.linear }); } }, holdRedDuration); } }); } if (rightBracket && !rightBracket.destroyed) { tween(rightBracket, { tint: 0xFF0000 }, { duration: tintToRedDuration, easing: tween.linear, onFinish: function onFinish() { LK.setTimeout(function () { if (rightBracket && !rightBracket.destroyed) { tween(rightBracket, { tint: 0xFFFFFF }, { duration: tintToWhiteDuration, easing: tween.linear }); } }, holdRedDuration); } }); } } return; } // Determine catch quality based on timing var points = 0; var multiplier = Math.max(1, Math.floor(GameState.combo / 10) + 1); var catchType = ''; if (closestDistance < GAME_CONFIG.PERFECT_WINDOW) { points = closestFishInLane.value * 2 * multiplier; catchType = 'perfect'; GameState.combo++; } else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) { points = closestFishInLane.value * multiplier; catchType = 'good'; GameState.combo++; } else if (closestDistance < GAME_CONFIG.MISS_WINDOW) { points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier)); catchType = 'good'; GameState.combo++; } else { showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; if (closestFishInLane) { closestFishInLane.missed = true; } return; } showFeedback(catchType, fishLane); // Handle the tap - check if fish is fully caught var isFullyCaught = closestFishInLane.handleTap(); if (isFullyCaught) { // Fish is completely caught closestFishInLane.catchFish(); var fishIndex = fishArray.indexOf(closestFishInLane); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } // Award points and update stats GameState.sessionScore += points; GameState.money += points; GameState.sessionFishCaught++; GameState.totalFishCaught++; GameState.maxCombo = Math.max(GameState.maxCombo, GameState.combo); // Show score popup if (points > 0) { var scorePopupText = new Text2('+' + points, { size: 140, fill: 0xFFD700, align: 'center', stroke: 0x000000, strokeThickness: 6 }); scorePopupText.anchor.set(0.5, 0.5); scorePopupText.x = GAME_CONFIG.SCREEN_CENTER_X; scorePopupText.y = GAME_CONFIG.BOAT_Y - 70; if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(scorePopupText); } tween(scorePopupText, { y: scorePopupText.y - 200, alpha: 0 }, { duration: 1800, easing: tween.easeOut, onFinish: function onFinish() { if (scorePopupText && !scorePopupText.destroyed) { scorePopupText.destroy(); } } }); } } else { // Fish was tapped but not fully caught - start battle mode if (!closestFishInLane.isInBattle) { closestFishInLane.startBattle(); } } // Play catch sound var catchSounds = ['catch', 'catch2', 'catch3', 'catch4']; var randomCatchSound = catchSounds[Math.floor(Math.random() * catchSounds.length)]; LK.getSound(randomCatchSound).play(); animateHookCatch(); } // Modified ImprovedRhythmSpawner to respect battle state ImprovedRhythmSpawner.update = function(currentTime) { // Don't spawn new fish during battles if (GameState.battleState === BATTLE_STATES.ACTIVE) { return; } if (!GameState.gameActive || GameState.songStartTime === 0) { return; } // Rest of existing spawning logic... var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; 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; 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++; } }; // Add method to resume spawning after battle ImprovedRhythmSpawner.resumeAfterBattle = function() { // This gets called when a battle ends to resume normal spawning // The timing will be handled by the setTimeout in fish.endBattle() }; ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Update as needed with: self.createTriggerRing = function() { var currentTime = LK.ticks * (1000 / 60); var songElapsed = currentTime - GameState.songStartTime; var bpm = 90; var beatDurationMs = 60000 / bpm; // 666.67ms per beat // Calculate time to next beat var currentBeat = Math.floor(songElapsed / beatDurationMs); var nextBeatTime = (currentBeat + 1) * beatDurationMs; var timeToNextBeat = nextBeatTime - songElapsed; // If next beat is too soon, use the beat after that var minTriggerTime = 1000; // Minimum 1 second for trigger ring var targetBeatTime; if (timeToNextBeat < minTriggerTime) { // Use the beat after next targetBeatTime = (currentBeat + 2) * beatDurationMs; } else { // Use next beat targetBeatTime = nextBeatTime; } var timeToTargetBeat = targetBeatTime - songElapsed; var ringDuration = timeToTargetBeat * 0.8; // Ring shrinks for 80% of time // Store when melody should start (on the target beat) self.melodyStartBeat = GameState.songStartTime + targetBeatTime; var outerRadius = 80; var innerRadius = 25; self.currentRing = new ShrinkingRing(self.hook.x, self.hook.y, outerRadius, innerRadius, ringDuration); self.currentRing.onMiss = function() { self.onMissedBeat(); }; if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(self.currentRing); } self.currentRing.startShrinking(); self.isWaitingForTap = true; self.isTriggerRing = true; }; self.onSuccessfulBeat = function () { if (self.isTriggerRing) { self.isTriggerRing = false; var musicTrack = self.getMusicTrackForFish(); if (musicTrack) { // Calculate delay to start melody exactly on the planned beat var currentTime = LK.ticks * (1000 / 60); var delayToMelodyStart = self.melodyStartBeat - currentTime; if (delayToMelodyStart > 0) { // Wait for the beat LK.setTimeout(function() { LK.playMusic(musicTrack); }, delayToMelodyStart); } else { // Start immediately if we're already at/past the beat LK.playMusic(musicTrack); } self.melodyStartTime = self.melodyStartBeat; self.hasMelodyStarted = true; } self.currentSequenceIndex = 0; } else { self.currentSequenceIndex++; } // ... rest of function same as before };
Code edit (1 edits merged)
Please save this source code
User prompt
Update as needed with: self.start = function () { self.battleStartTime = LK.ticks * (1000 / 60); // Start with trigger ring, not the pattern self.createTriggerRing(); }; // NEW: Create the initial trigger ring that starts the melody self.createTriggerRing = function() { var currentTime = LK.ticks * (1000 / 60); var songElapsed = currentTime - GameState.songStartTime; var bpm = 90; var beatDurationMs = 60000 / bpm; // 666.67ms per beat // Calculate ring duration to collapse on next beat var currentBeat = Math.floor(songElapsed / beatDurationMs); var nextBeatTime = (currentBeat + 1) * beatDurationMs; var timeToNextBeat = nextBeatTime - songElapsed; // Create ring that collapses on the beat var ringDuration = timeToNextBeat * 0.8; // Ring shrinks for 80% of time to beat var outerRadius = 80; var innerRadius = 25; self.currentRing = new ShrinkingRing(self.hook.x, self.hook.y, outerRadius, innerRadius, ringDuration); self.currentRing.onMiss = function() { self.onMissedBeat(); }; if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(self.currentRing); } self.currentRing.startShrinking(); self.isWaitingForTap = true; self.isTriggerRing = true; // Flag to know this is the trigger }; self.onSuccessfulBeat = function () { if (self.isTriggerRing) { // This was the trigger ring - start melody and begin actual pattern self.isTriggerRing = false; var musicTrack = self.getMusicTrackForFish(); if (musicTrack) { LK.playMusic(musicTrack); // Start melody on beat self.melodyStartTime = LK.ticks * (1000 / 60); self.hasMelodyStarted = true; } // Reset sequence index to start the actual pattern self.currentSequenceIndex = 0; } else { // This is part of the actual pattern self.currentSequenceIndex++; } // Clean up current ring if (self.currentRing && !self.currentRing.destroyed) { self.currentRing.destroy(); } self.currentRing = null; self.isWaitingForTap = false; // Play success sound var catchSounds = ['catch', 'catch2', 'catch3', 'catch4']; var randomCatchSound = catchSounds[Math.floor(Math.random() * catchSounds.length)]; LK.getSound(randomCatchSound).play(); // Continue to next beat LK.setTimeout(function () { if (self.state === BATTLE_STATES.BATTLING) { self.moveToNextBeat(); } }, 200); }; // UPDATED: Pattern timings for 6 rings over 10551ms melody sardine: { sequence: ['right', 'left', 'right', 'left', 'right', 'left'], // 6 rings timings: [1759, 1759, 1759, 1759, 1759, 1756], // ~1.76s each, totaling 10551ms totalDuration: 10551 }
Code edit (1 edits merged)
Please save this source code
User prompt
Update with: self.onSuccessfulBeat = function () { // Start music on first successful tap, synchronized to backing track if (self.currentSequenceIndex === 0 && !self.hasMelodyStarted) { var musicTrack = self.getMusicTrackForFish(); if (musicTrack) { self.startSynchronizedMusic(musicTrack); // Don't call synchronizeToMusic() - use pattern timings } } // ... rest of function (same as before) }; // NEW: Start melody synchronized to backing track self.startSynchronizedMusic = function(musicTrack) { var currentTime = LK.ticks * (1000 / 60); var songElapsed = currentTime - GameState.songStartTime; var bpm = 90; var beatDurationMs = 60000 / bpm; // 666.67ms per beat // Find the next beat boundary var currentBeat = Math.floor(songElapsed / beatDurationMs); var nextBeatTime = (currentBeat + 1) * beatDurationMs; var delayUntilNextBeat = nextBeatTime - songElapsed; // Start the melody at the next beat LK.setTimeout(function() { LK.playMusic(musicTrack); }, delayUntilNextBeat); // Store when the melody will actually start self.melodyStartTime = GameState.songStartTime + nextBeatTime; self.hasMelodyStarted = true; }; // FIXED: Ring creation that respects pattern timings self.createShrinkingRing = function (duration) { var ringDuration; // ALWAYS use the pattern timing * 0.8 for ring duration // Don't override with beat synchronization ringDuration = duration * 0.8; var outerRadius = 80; var innerRadius = 25; self.currentRing = new ShrinkingRing(self.hook.x, self.hook.y, outerRadius, innerRadius, ringDuration); // Set up ring callbacks self.currentRing.onMiss = function() { self.onMissedBeat(); }; if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(self.currentRing); } self.currentRing.startShrinking(); self.isWaitingForTap = true; }; ``` ## **Remove synchronizeToMusic entirely** - it was causing the timing problems.
Code edit (1 edits merged)
Please save this source code
User prompt
Update with: sardine: { sequence: ['right', 'left', 'right', 'left', 'right', 'left'], // Extended sequence timings: [1000, 2000, 2000, 2000, 2000, 1551], // Distributed across 10551ms totalDuration: 10551 }
User prompt
Replace with: // Add these properties to the FishBattle constructor var FishBattle = Container.expand(function (fish, hook) { var self = Container.call(this); self.fish = fish; self.hook = hook; self.state = BATTLE_STATES.BATTLING; self.currentSequenceIndex = 0; self.battleStartTime = 0; self.nextBeatTime = 0; self.currentRing = null; self.isWaitingForTap = false; // NEW: Music synchronization properties self.melodyStartTime = null; self.hasMelodyStarted = false; // Get battle pattern based on fish type self.getBattlePattern = function () { var fishAssetName = fish.fishGraphics.asset || 'anchovy'; // Handle shallow fish types specifically if (fish.type === 'shallow') { if (FISH_BATTLE_PATTERNS.shallow[fishAssetName]) { return FISH_BATTLE_PATTERNS.shallow[fishAssetName]; } // Default to anchovy pattern if specific pattern not found return FISH_BATTLE_PATTERNS.shallow.anchovy; } return FISH_BATTLE_PATTERNS[fish.type] || FISH_BATTLE_PATTERNS.shallow.anchovy; }; // NEW: Get music track for fish type self.getMusicTrackForFish = function () { var fishAssetName = fish.fishGraphics.asset || 'anchovy'; if (fish.type === 'shallow' && fishAssetName === 'sardine') { return 'sardineRiff'; // 10551ms } // Add other fish -> music mappings here // if (fish.type === 'shallow' && fishAssetName === 'anchovy') { // return 'anchovyRiff'; // } return null; }; // NEW: Synchronize remaining rings to music duration self.synchronizeToMusic = function () { var musicDuration = 10551; // sardineRiff length - make this dynamic later var remainingBeats = self.pattern.sequence.length - 1; // Exclude first beat already completed if (remainingBeats > 0) { // Distribute remaining rings evenly across the melody duration var ringInterval = musicDuration / remainingBeats; // Update the pattern timings for remaining beats for (var i = 1; i < self.pattern.timings.length; i++) { self.pattern.timings[i] = ringInterval; } } }; self.pattern = self.getBattlePattern(); self.start = function () { self.battleStartTime = LK.ticks * (1000 / 60); self.nextBeatTime = self.battleStartTime + self.pattern.timings[0]; self.moveToNextBeat(); }; self.moveToNextBeat = function () { if (self.currentSequenceIndex >= self.pattern.sequence.length) { self.winBattle(); return; } var direction = self.pattern.sequence[self.currentSequenceIndex]; var beatDuration = self.pattern.timings[self.currentSequenceIndex]; // Animate fish struggle in the specified direction self.animateFishStruggle(direction); // Create shrinking ring self.createShrinkingRing(beatDuration); // Don't increment currentSequenceIndex here - do it in onSuccessfulBeat }; self.animateFishStruggle = function (direction) { var fish = self.fish; var pullDistance = 60; var flipDuration = 300; // Determine pull direction var targetX = direction === 'left' ? self.hook.x - pullDistance : self.hook.x + pullDistance; // Fish flip animation with Y-axis scaling tween(fish.fishGraphics.scale, { y: 0.1 // Collapse Y scale for flip effect }, { duration: flipDuration / 2, easing: tween.easeOut, onFinish: function onFinish() { // Change direction (flip horizontal scale if needed) if (direction === 'left' && fish.fishGraphics.scaleX > 0) { fish.fishGraphics.scaleX *= -1; } else if (direction === 'right' && fish.fishGraphics.scaleX < 0) { fish.fishGraphics.scaleX *= -1; } // Expand Y scale back tween(fish.fishGraphics.scale, { y: Math.abs(fish.fishGraphics.scaleX) }, { duration: flipDuration / 2, easing: tween.easeIn }); } }); // Move fish in struggle direction tween(fish, { x: targetX }, { duration: flipDuration, easing: tween.easeInOut, onFinish: function onFinish() { // Snap back toward hook tween(fish, { x: self.hook.x + (Math.random() - 0.5) * 20 }, { duration: 200, easing: tween.easeOut }); } }); }; // MODIFIED: Enhanced ring creation with beat synchronization self.createShrinkingRing = function (duration) { var ringDuration; if (self.currentSequenceIndex === 0 || !self.hasMelodyStarted) { // First ring uses original timing ringDuration = duration * 0.8; } else { // Subsequent rings: calculate duration to collapse on next beat var currentTime = LK.ticks * (1000 / 60); var beatDurationMs = 60000 / 90; // 666.67ms per beat at 90 BPM var timeSinceMelodyStart = currentTime - self.melodyStartTime; var currentBeat = Math.floor(timeSinceMelodyStart / beatDurationMs); var nextBeatTime = (currentBeat + 1) * beatDurationMs; ringDuration = nextBeatTime - timeSinceMelodyStart; // Ensure minimum ring duration if (ringDuration < 200) { ringDuration = nextBeatTime + beatDurationMs - timeSinceMelodyStart; } } var outerRadius = 80; var innerRadius = 25; self.currentRing = new ShrinkingRing(self.hook.x, self.hook.y, outerRadius, innerRadius, ringDuration); // Set up ring callbacks to communicate back to battle self.currentRing.onMiss = function() { self.onMissedBeat(); }; // Add ring to fishing screen if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(self.currentRing); } self.currentRing.startShrinking(); self.isWaitingForTap = true; }; self.handleTap = function () { if (!self.isWaitingForTap || !self.currentRing) { return false; } var success = self.currentRing.checkTap(); self.isWaitingForTap = false; if (success) { self.onSuccessfulBeat(); } else { self.onMissedBeat(); } return success; }; // MODIFIED: Enhanced successful beat handling with music synchronization self.onSuccessfulBeat = function () { // Start music immediately on first successful tap if (self.currentSequenceIndex === 0 && !self.hasMelodyStarted) { var musicTrack = self.getMusicTrackForFish(); if (musicTrack) { LK.playMusic(musicTrack); // Start immediately for responsiveness self.melodyStartTime = LK.ticks * (1000 / 60); // Record start time self.hasMelodyStarted = true; self.synchronizeToMusic(); // Recalculate remaining ring timings } } // Clean up current ring if (self.currentRing && !self.currentRing.destroyed) { self.currentRing.destroy(); } self.currentRing = null; self.isWaitingForTap = false; // Increment sequence index self.currentSequenceIndex++; // Play success sound var catchSounds = ['catch', 'catch2', 'catch3', 'catch4']; var randomCatchSound = catchSounds[Math.floor(Math.random() * catchSounds.length)]; LK.getSound(randomCatchSound).play(); // Continue to next beat LK.setTimeout(function () { if (self.state === BATTLE_STATES.BATTLING) { self.moveToNextBeat(); } }, 200); }; self.onMissedBeat = function () { // Play miss sound LK.getSound('miss').play(); // End battle in failure self.loseBattle(); }; self.winBattle = function () { self.state = BATTLE_STATES.WON; // Clean up ring if (self.currentRing && !self.currentRing.destroyed) { self.currentRing.destroy(); } // Animate fish being caught self.fish.catchFish(); // Award points and update stats var points = self.fish.value * 2; // Bonus for winning battle GameState.sessionScore += points; GameState.money += points; GameState.sessionFishCaught++; GameState.totalFishCaught++; GameState.combo++; GameState.maxCombo = Math.max(GameState.maxCombo, GameState.combo); // Show score popup self.showScorePopup(points); // Remove fish from array var fishIndex = fishArray.indexOf(self.fish); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } // End battle self.endBattle(); }; self.loseBattle = function () { self.state = BATTLE_STATES.LOST; // Clean up ring if (self.currentRing && !self.currentRing.destroyed) { self.currentRing.destroy(); } // Reset combo GameState.combo = 0; // Fish escapes - animate it swimming away var escapeDirection = Math.random() < 0.5 ? -1 : 1; var escapeDistance = 500; tween(self.fish, { x: self.fish.x + escapeDirection * escapeDistance, alpha: 0 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { if (self.fish && !self.fish.destroyed) { self.fish.destroy(); } } }); // Remove fish from array var fishIndex = fishArray.indexOf(self.fish); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } // End battle self.endBattle(); }; self.showScorePopup = function (points) { var scorePopupText = new Text2('+' + points, { size: 140, fill: 0xFFD700, align: 'center', stroke: 0x000000, strokeThickness: 6 }); scorePopupText.anchor.set(0.5, 0.5); scorePopupText.x = GAME_CONFIG.SCREEN_CENTER_X; scorePopupText.y = GAME_CONFIG.BOAT_Y - 70; if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(scorePopupText); } tween(scorePopupText, { y: scorePopupText.y - 200, alpha: 0 }, { duration: 1800, easing: tween.easeOut, onFinish: function onFinish() { if (scorePopupText && !scorePopupText.destroyed) { scorePopupText.destroy(); } } }); }; self.endBattle = function () { // Signal that battle is over GameState.currentBattle = null; // Allow new fish to be attracted after a delay LK.setTimeout(function () { if (GameState.gameActive) { GameState.battleState = BATTLE_STATES.NONE; } }, 2000); }; return self; }); ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Update with: 1. Remove the FishBattle timeout and let ShrinkingRing handle timing:** ```javascript self.createShrinkingRing = function (duration) { var ringDuration = duration * 0.8; var outerRadius = 80; var innerRadius = 25; self.currentRing = new ShrinkingRing(self.hook.x, self.hook.y, outerRadius, innerRadius, ringDuration); // Set up ring callbacks to communicate back to battle self.currentRing.onMiss = function() { self.onMissedBeat(); }; if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(self.currentRing); } self.currentRing.startShrinking(); self.isWaitingForTap = true; // NO setTimeout here - let the ring handle completion }; ``` **2. Restore the sequence increment to onSuccessfulBeat:** ```javascript self.moveToNextBeat = function () { if (self.currentSequenceIndex >= self.pattern.sequence.length) { self.winBattle(); return; } var direction = self.pattern.sequence[self.currentSequenceIndex]; var beatDuration = self.pattern.timings[self.currentSequenceIndex]; self.animateFishStruggle(direction); self.createShrinkingRing(beatDuration); // Don't increment here }; self.onSuccessfulBeat = function () { self.currentSequenceIndex++; // Increment on success // ... rest of success logic };
User prompt
Update with: Instead of just changing the timeout duration, restructure the flow: ```javascript self.moveToNextBeat = function () { if (self.currentSequenceIndex >= self.pattern.sequence.length) { self.winBattle(); return; } var direction = self.pattern.sequence[self.currentSequenceIndex]; var beatDuration = self.pattern.timings[self.currentSequenceIndex]; // Animate fish struggle self.animateFishStruggle(direction); // Create shrinking ring self.createShrinkingRing(beatDuration); // DON'T increment here - do it in onSuccessfulBeat }; self.onSuccessfulBeat = function () { // Clean up current ring if (self.currentRing && !self.currentRing.destroyed) { self.currentRing.destroy(); } self.currentRing = null; self.isWaitingForTap = false; // NOW increment the sequence self.currentSequenceIndex++; // Play success sound var catchSounds = ['catch', 'catch2', 'catch3', 'catch4']; var randomCatchSound = catchSounds[Math.floor(Math.random() * catchSounds.length)]; LK.getSound(randomCatchSound).play(); // Continue to next beat LK.setTimeout(function () { if (self.state === BATTLE_STATES.BATTLING) { self.moveToNextBeat(); } }, 200); }; ``` And in `createShrinkingRing`, remove the separate timeout entirely and let the ring’s completion handle the failure: ```javascript self.createShrinkingRing = function (duration) { var ringDuration = duration * 0.8; // ... create ring setup ... // Don't create a separate timeout - let the ring handle completion self.isWaitingForTap = true; };
User prompt
Update with: // Auto-advance if no tap received LK.setTimeout(function () { if (self.isWaitingForTap && self.currentRing) { self.onMissedBeat(); } }, ringDuration); // Use ringDuration, not duration
User prompt
Update with: // Spawn fish from left or right side only var spawnSide = Math.random() < 0.5 ? -1 : 1; // -1 = left, 1 = right var spawnX = spawnSide === -1 ? -150 : 2048 + 150; // Off-screen left or right var spawnY = GAME_CONFIG.WATER_SURFACE_Y + 200 + Math.random() * 800; // Random Y in water
Code edit (1 edits merged)
Please save this source code
User prompt
Update with: self.update = function () { if (self.isDone || !fishAsset || fishAsset.destroyed) { return; } var currentTicks = LK.ticks; var timeSinceFadeInStart = (currentTicks - swimStartTime) * (1000 / 60); if (timeSinceFadeInStart > fadeInDuration * 0.5 && timeSinceFadeInStart < fadeInDuration * 0.3 + swimDuration - fadeOutDuration * 0.5) { var swimProgress = (timeSinceFadeInStart - fadeInDuration * 0.3) / (swimDuration - fadeInDuration * 0.3); self.y = nodeY + verticalOffsetBase + Math.sin(swimProgress * Math.PI * 2) * verticalSwimAmplitude; } // Simplified rotation based on movement direction, ignoring small vertical movements var dx = self.x - self.lastX; var dy = self.y - self.lastY; // Only update rotation if horizontal movement is significant if (Math.abs(dx) > 0.5) { var angle = Math.atan2(dy, dx); // Simplify the scale-based rotation logic if (fishAsset.scale.x < 0) { // Fish is flipped horizontally fishAsset.rotation = angle + Math.PI; } else { // Fish is not flipped fishAsset.rotation = angle; } } self.lastX = self.x; self.lastY = self.y; };
User prompt
Update only as needed with: // In game.update, replace the title screen particle section with: if (GameState.currentScreen === 'title') { if (titleElements && titleElements.updateTitleFishingLineWave) { titleElements.updateTitleFishingLineWave(); } // Title Screen Ocean Bubbles if (titleScreenOceanBubbleContainer) { titleScreenOceanBubbleSpawnCounter++; if (titleScreenOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) { titleScreenOceanBubbleSpawnCounter = 0; var newOceanBubble = new OceanBubbleParticle(); titleScreenOceanBubbleContainer.addChild(newOceanBubble); titleScreenOceanBubblesArray.push(newOceanBubble); } updateParticleArray(titleScreenOceanBubblesArray); } // 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); } updateParticleArray(titleScreenSeaweedArray); } // 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); } updateParticleArray(titleScreenCloudArray); } } // And replace the fishing screen ocean bubble and seaweed sections with: if (GameState.currentScreen === 'fishing' && globalOceanBubbleContainer) { globalOceanBubbleSpawnCounter++; if (globalOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) { globalOceanBubbleSpawnCounter = 0; var numToSpawn = 1; for (var i = 0; i < numToSpawn; i++) { var newOceanBubble = new OceanBubbleParticle(); globalOceanBubbleContainer.addChild(newOceanBubble); globalOceanBubblesArray.push(newOceanBubble); } } // Keep the existing fish physics update logic here... } if (GameState.currentScreen === 'fishing' && globalSeaweedContainer) { globalSeaweedSpawnCounter++; if (globalSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && globalSeaweedArray.length < MAX_SEAWEED_COUNT) { globalSeaweedSpawnCounter = 0; var newSeaweed = new SeaweedParticle(); globalSeaweedContainer.addChild(newSeaweed); globalSeaweedArray.push(newSeaweed); } // Keep the existing fish physics update logic here... } // Keep the fishing screen clouds as they were: if (GameState.currentScreen === 'fishing' && globalCloudContainer) { globalCloudSpawnCounter++; if (globalCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && globalCloudArray.length < MAX_CLOUD_COUNT) { globalCloudSpawnCounter = 0; var newCloud = new CloudParticle(); globalCloudContainer.addChild(newCloud); globalCloudArray.push(newCloud); } updateParticleArray(globalCloudArray); }
Code edit (1 edits merged)
Please save this source code
User prompt
Update as needed with: var closestFishInLane = null; var closestDistance = Infinity; // Find the closest fish in the tapped lane that's actually near the hook 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); // Only consider fish that are reasonably close to the hook // Use a larger window than MISS_WINDOW to allow for early/late taps var maxCatchDistance = GAME_CONFIG.MISS_WINDOW * 2; // or some reasonable multiplier if (distance < maxCatchDistance && distance < closestDistance) { closestDistance = distance; closestFishInLane = fish; } } } if (!closestFishInLane) { // No fish found within catchable range LK.getSound('miss').play();
/**** * 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, scaleX: 1.2 + Math.random() * 0.25, scaleY: this.scaleX }); self.x = startX; self.y = startY; self.vx = (Math.random() - 0.5) * 0.4; self.vy = -(0.4 + Math.random() * 0.3); self.life = 120 + Math.random() * 60; self.age = 0; self.isDone = false; var initialAlpha = self.gfx.alpha; var initialScale = self.gfx.scaleX; self.update = function () { if (self.isDone) { return; } self.age++; self.x += self.vx; self.y += self.vy; self.gfx.alpha = Math.max(0, initialAlpha * (1 - self.age / self.life)); var scaleFactor = 1 - self.age / self.life; 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 }); var spawnOnScreen = Math.random() < 0.3; if (spawnOnScreen) { self.x = 200 + Math.random() * 1648; self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.1 + Math.random() * 0.2); } else { var spawnFromLeft = Math.random() < 0.5; if (spawnFromLeft) { self.x = -100; self.vx = 0.1 + Math.random() * 0.2; } else { self.x = 2048 + 100; self.vx = -(0.1 + Math.random() * 0.2); } } var skyTop = -500; var skyBottom = GAME_CONFIG.WATER_SURFACE_Y - 100; self.y = skyTop + Math.random() * (skyBottom - skyTop); var baseScale = 0.8 + Math.random() * 0.6; self.gfx.scale.set(baseScale); self.vy = (Math.random() - 0.5) * 0.02; var targetAlpha = 0.4 + Math.random() * 0.3; self.gfx.alpha = 0; self.isDone = false; self.fadingOut = false; self.hasFadedIn = false; var fadeInStartX = 400; var fadeInEndX = 800; var fadeOutStartX = 1248; var fadeOutEndX = 1648; self.update = function () { if (self.isDone) { return; } self.x += self.vx; self.y += self.vy; var currentAlpha = self.gfx.alpha; if (spawnFromLeft) { if (!self.hasFadedIn && self.x >= fadeInStartX && self.x <= fadeInEndX) { 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) { var fadeOutProgress = (self.x - fadeOutStartX) / (fadeOutEndX - fadeOutStartX); self.gfx.alpha = targetAlpha * (1 - fadeOutProgress); } else if (self.hasFadedIn && self.x > fadeInEndX && self.x < fadeOutStartX) { self.gfx.alpha = targetAlpha; } } else { if (!self.hasFadedIn && self.x <= fadeOutEndX && self.x >= fadeOutStartX) { 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) { var fadeOutProgress = (fadeInEndX - self.x) / (fadeInEndX - fadeInStartX); self.gfx.alpha = targetAlpha * (1 - fadeOutProgress); } else if (self.hasFadedIn && self.x < fadeOutStartX && self.x > fadeInEndX) { self.gfx.alpha = targetAlpha; } } 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; }); var Fish = Container.expand(function (type, value, speed, lane) { var self = Container.call(this); var assetName = type + 'Fish'; if (type === 'shallow') { var currentSongConfig = null; if (typeof GameState !== 'undefined' && GameState && typeof GameState.getCurrentSongConfig === 'function') { currentSongConfig = GameState.getCurrentSongConfig(); } if (currentSongConfig && currentSongConfig.name === "Gentle Waves") { var randGentle = Math.random(); if (randGentle < 0.60) { assetName = 'anchovy'; } else if (randGentle < 0.90) { assetName = 'sardine'; } else { assetName = 'mackerel'; } } else if (currentSongConfig && currentSongConfig.name === "Morning Tide") { var randMorning = Math.random(); if (randMorning < 0.40) { assetName = 'anchovy'; } else if (randMorning < 0.80) { assetName = 'sardine'; } else { assetName = 'mackerel'; } } else if (currentSongConfig && currentSongConfig.name === "Sunny Afternoon") { var randSunny = Math.random(); if (randSunny < 0.30) { assetName = 'anchovy'; } else if (randSunny < 0.60) { assetName = 'sardine'; } else { assetName = 'mackerel'; } } else { var shallowFishAssets = ['sardine', 'anchovy', 'mackerel']; assetName = shallowFishAssets[Math.floor(Math.random() * shallowFishAssets.length)]; } } self.fishGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); if (speed > 0) { self.fishGraphics.scaleX = -1; } self.type = type; self.value = value; self.speed = speed; self.lane = lane; self.caught = false; self.missed = false; self.lastX = 0; self.isSpecial = type === 'rare'; self.shimmerTime = 0; self.lastBubbleSpawnTime = 0; self.bubbleSpawnInterval = 120 + Math.random() * 80; self.swimTime = Math.random() * Math.PI * 2; self.baseY = self.y; self.scaleTime = 0; self.baseScale = 1; self.update = function () { if (!self.caught) { self.x += self.speed; self.swimTime += 0.08; var swimAmplitude = 15; self.y = self.baseY + Math.sin(self.swimTime) * swimAmplitude; 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; var scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15; var baseScaleXDirection = (self.speed > 0 ? -1 : 1) * self.baseScale; self.fishGraphics.scaleX = baseScaleXDirection * scalePulse; self.fishGraphics.scaleY = scalePulse * self.baseScale; } if (self.isSpecial) { self.shimmerTime += 0.1; self.fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2; } else { self.fishGraphics.alpha = 1.0; } } }; self.catchFish = function () { self.caught = true; var currentFishX = self.x; var currentFishY = self.y; var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; var boatLandingY = GAME_CONFIG.BOAT_Y; var peakArcY = boatLandingY - 150; var peakArcX = currentFishX + (boatCenterX - currentFishX) * 0.5; var durationPhase1 = 350; var durationPhase2 = 250; tween(self, { x: peakArcX, y: peakArcY, scaleX: 0.75, scaleY: 0.75, alpha: 0.8 }, { duration: durationPhase1, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { x: boatCenterX, y: boatLandingY, scaleX: 0.2, scaleY: 0.2, alpha: 0 }, { duration: durationPhase2, easing: tween.easeIn, onFinish: function onFinish() { self.destroy(); } }); } }); }; return self; }); var MapBubbleParticle = Container.expand(function (startX, startY) { var self = Container.call(this); var initialScale = 0.1 + Math.random() * 0.1; var targetScale = 0.4 + Math.random() * 0.3; self.gfx = self.attachAsset('bubbles', { anchorX: 0.5, anchorY: 0.5, alpha: 0, scaleX: initialScale, scaleY: initialScale }); self.x = startX + (Math.random() - 0.5) * 100; self.y = startY; var targetAlpha = 0.3 + Math.random() * 0.3; var riseDurationMs = 3000 + Math.random() * 2000; var riseDistance = 300 + Math.random() * 200; var driftDistance = (Math.random() - 0.5) * 100; var fadeInDurationMs = 600 + Math.random() * 400; var totalDurationMs = riseDurationMs; var fadeOutStartTimeMs = totalDurationMs * 0.6; var fadeOutDurationMs = totalDurationMs - fadeOutStartTimeMs; self.isDone = false; tween(self.gfx, { alpha: targetAlpha, scaleX: targetScale, scaleY: targetScale }, { duration: fadeInDurationMs, easing: tween.easeOut }); tween(self, { y: self.y - riseDistance, x: self.x + driftDistance }, { duration: totalDurationMs, easing: tween.linear }); LK.setTimeout(function () { if (self.isDone || !self.gfx || self.gfx.destroyed) { return; } tween(self.gfx, { alpha: 0, scaleX: initialScale * 0.5, scaleY: initialScale * 0.5 }, { duration: fadeOutDurationMs, easing: tween.easeIn, onFinish: function onFinish() { self.isDone = true; } }); }, fadeOutStartTimeMs); return self; }); var MapScreenCloudParticle = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('mapCloud', { anchorX: 0.5, anchorY: 0.5 }); self.gfx.alpha = 0.4 + Math.random() * 0.3; var baseScale = 0.9 + Math.random() * 0.4; self.gfx.scale.set(baseScale); var spawnFromLeft = Math.random() < 0.5; var offscreenBuffer = self.gfx.width * baseScale + 50; var speedMultiplier = 1.15; if (spawnFromLeft) { self.x = -offscreenBuffer; self.vx = (0.15 + Math.random() * 0.2) * speedMultiplier; } else { self.x = 2048 + offscreenBuffer; self.vx = -(0.15 + Math.random() * 0.2) * speedMultiplier; } var screenHeight = 2732; var topMargin = screenHeight * 0.1; var spawnableHeight = screenHeight * 0.8; self.y = topMargin + Math.random() * spawnableHeight; self.isDone = false; self.update = function () { if (self.isDone) { return; } self.x += self.vx; if (self.vx > 0 && self.x > 2048 + offscreenBuffer) { self.isDone = true; } else if (self.vx < 0 && self.x < -offscreenBuffer) { self.isDone = true; } }; return self; }); var MusicNoteParticle = Container.expand(function (startX, startY) { var self = Container.call(this); var FADE_IN_DURATION_MS = 600; var TARGET_ALPHA = 0.6 + Math.random() * 0.4; self.gfx = self.attachAsset('musicnote', { anchorX: 0.5, anchorY: 0.5, alpha: 0, scaleX: 0.4 + Math.random() * 0.4 }); self.gfx.scaleY = self.gfx.scaleX; self.x = startX; self.y = startY; self.vx = (Math.random() - 0.5) * 0.8; self.vy = -(0.8 + Math.random() * 0.7); self.rotationSpeed = (Math.random() - 0.5) * 0.008; self.life = 240 + Math.random() * 120; self.age = 0; self.isDone = false; 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); if (self.age > FADE_IN_TICKS) { var lifePortionForFadeOut = 0.6; var fadeOutStartTimeTicks = self.life * (1 - lifePortionForFadeOut); if (self.age >= fadeOutStartTimeTicks && self.life > fadeOutStartTimeTicks) { var progressInFadeOut = (self.age - fadeOutStartTimeTicks) / (self.life * lifePortionForFadeOut); self.gfx.alpha = TARGET_ALPHA * (1 - progressInFadeOut); self.gfx.alpha = Math.max(0, self.gfx.alpha); } } 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; self.y = waterTop + Math.random() * (waterBottom - waterTop); var baseScale = 0.1 + Math.random() * 0.4; self.gfx.scale.set(baseScale); self.vy = -(0.25 + Math.random() * 0.5); self.naturalVy = self.vy; self.driftAmplitude = 20 + Math.random() * 40; self.naturalDriftAmplitude = self.driftAmplitude; self.driftFrequency = (0.005 + Math.random() * 0.015) * (Math.random() < 0.5 ? 1 : -1); self.driftPhase = Math.random() * Math.PI * 2; self.rotationSpeed = (Math.random() - 0.5) * 0.01; var targetAlpha = 0.2 + Math.random() * 0.3; self.gfx.alpha = 0; self.isDone = false; self.fadingOut = false; tween(self.gfx, { alpha: targetAlpha }, { duration: 1000 + Math.random() * 1000, easing: tween.easeIn }); self.update = function () { if (self.isDone) { return; } self.y += self.vy; self.age++; if (!self.fadingOut && self.age >= self.lifespan) { self.fadingOut = true; tween.stop(self.gfx); tween(self.gfx, { alpha: 0 }, { duration: 600 + Math.random() * 400, 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; var naturalVy = -(0.25 + Math.random() * 0.5); var recoveryRate = 0.02; if (self.vy > naturalVy) { self.vy = self.vy + (naturalVy - self.vy) * recoveryRate; } var normalDriftAmplitude = 20 + Math.random() * 40; if (self.driftAmplitude > normalDriftAmplitude) { self.driftAmplitude = self.driftAmplitude + (normalDriftAmplitude - self.driftAmplitude) * recoveryRate; } 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); tween(self.gfx, { alpha: 0 }, { duration: 300 + Math.random() * 200, easing: tween.easeOut, onFinish: function onFinish() { self.isDone = true; } }); } else if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth)) { self.isDone = true; self.gfx.alpha = 0; } }; return self; }); var RippleParticle = Container.expand(function (spawnCenterX, spawnCenterY, spawnAngle, initialOffset, travelDistance, initialScale, finalScale, durationMs, speedFactor) { var self = Container.call(this); self.isDone = false; var effectiveDurationMs = durationMs; if (speedFactor !== undefined && speedFactor > 0 && speedFactor !== 1.0) { effectiveDurationMs = durationMs / speedFactor; } var rippleGfx = self.attachAsset('waveline', { anchorX: 0.5, anchorY: 0.5, scaleX: initialScale, scaleY: initialScale, alpha: 1.0, rotation: spawnAngle + Math.PI / 2 }); self.x = spawnCenterX + initialOffset * Math.cos(spawnAngle); self.y = spawnCenterY + initialOffset * Math.sin(spawnAngle); var targetX = spawnCenterX + (initialOffset + travelDistance) * Math.cos(spawnAngle); var targetY = spawnCenterY + (initialOffset + travelDistance) * Math.sin(spawnAngle); tween(self, { x: targetX, y: targetY, alpha: 0 }, { duration: effectiveDurationMs, easing: tween.linear, onFinish: function onFinish() { self.isDone = true; } }); tween(rippleGfx.scale, { x: finalScale, y: finalScale }, { duration: effectiveDurationMs, easing: tween.easeOut }); return self; }); var SeagullParticle = Container.expand(function () { var self = Container.call(this); self.isDone = false; var flyFromLeft = Math.random() < 0.5; var assetBaseScale = 0.6 + Math.random() * 0.4; self.gfx = self.attachAsset('seagull', { anchorX: 0.5, anchorY: 0.5, scaleX: flyFromLeft ? -assetBaseScale : assetBaseScale, scaleY: assetBaseScale }); var flightDuration = 7000 + Math.random() * 5000; var startX, endX; var startY = 250 + Math.random() * 750; var endY = 250 + Math.random() * 750; var offscreenBuffer = self.gfx.width * assetBaseScale + 50; if (flyFromLeft) { startX = -offscreenBuffer; endX = 2048 + offscreenBuffer; } else { startX = 2048 + offscreenBuffer; endX = -offscreenBuffer; } self.x = startX; self.y = startY; self.lastX = startX; self.lastY = startY; var arcHeight = 150 + Math.random() * 250; var arcUpwards = Math.random() < 0.5; var peakY; if (arcUpwards) { peakY = Math.min(startY, endY) - arcHeight; } else { peakY = Math.max(startY, endY) + arcHeight; } var targetScaleMagnitude = assetBaseScale + (Math.random() - 0.5) * 0.5; targetScaleMagnitude = Math.max(0.4, Math.min(1.2, targetScaleMagnitude)); var finalScaleX = flyFromLeft ? -targetScaleMagnitude : targetScaleMagnitude; var finalScaleY = targetScaleMagnitude; tween(self.gfx.scale, { x: finalScaleX, y: finalScaleY }, { duration: flightDuration, easing: tween.linear }); tween(self, { x: endX }, { duration: flightDuration, easing: tween.linear, onFinish: function onFinish() { self.isDone = true; } }); tween(self, { y: peakY }, { duration: flightDuration / 2, easing: tween.easeOut, onFinish: function onFinish() { if (self.isDone || !self.gfx || self.gfx.destroyed) { return; } tween(self, { y: endY }, { duration: flightDuration / 2, easing: tween.easeIn }); } }); self.update = function () { if (self.isDone || !self.gfx || self.gfx.destroyed) { return; } var dx = self.x - self.lastX; var dy = self.y - self.lastY; if (dx !== 0 || dy !== 0) { var angle = Math.atan2(dy, dx); if (self.gfx.scale.x > 0) { self.gfx.rotation = angle - Math.PI; } else { self.gfx.rotation = angle; } } self.lastX = self.x; self.lastY = self.y; }; return self; }); var SeaweedParticle = Container.expand(function () { var self = Container.call(this); self.gfx = self.attachAsset('kelp', { anchorX: 0.5, anchorY: 0.5 }); var spawnType = Math.random(); var waterTop = GAME_CONFIG.WATER_SURFACE_Y; var waterBottom = 2732; if (spawnType < 0.4) { self.x = Math.random() * 2048; self.y = waterBottom + 50; self.vx = (Math.random() - 0.5) * 0.3; self.vy = -(0.4 + Math.random() * 0.3); } else if (spawnType < 0.7) { self.x = -50; self.y = waterTop + Math.random() * (waterBottom - waterTop); self.vx = 0.4 + Math.random() * 0.3; self.vy = -(0.1 + Math.random() * 0.2); } else { self.x = 2048 + 50; self.y = waterTop + Math.random() * (waterBottom - waterTop); self.vx = -(0.4 + Math.random() * 0.3); self.vy = -(0.1 + Math.random() * 0.2); } self.initialX = self.x; self.naturalVx = self.vx; self.naturalVy = self.vy; var baseScale = 0.6 + Math.random() * 0.6; self.gfx.scale.set(baseScale); self.swayAmplitude = 15 + Math.random() * 25; self.swayFrequency = (0.003 + Math.random() * 0.007) * (Math.random() < 0.5 ? 1 : -1); self.swayPhase = Math.random() * Math.PI * 2; self.gfx.rotation = Math.random() * Math.PI * 2; self.continuousRotationSpeed = (Math.random() - 0.5) * 0.003; var targetAlpha = 0.3 + Math.random() * 0.3; self.gfx.alpha = 0; self.isDone = false; self.fadingOut = false; self.reachedSurface = false; self.lifespan = 600 + Math.random() * 1200; self.age = 0; tween(self.gfx, { alpha: targetAlpha }, { duration: 1500 + Math.random() * 1000, easing: tween.easeIn }); self.update = function () { if (self.isDone) { return; } self.x += self.vx; self.y += self.vy; self.swayPhase += self.swayFrequency; var swayOffset = Math.sin(self.swayPhase) * self.swayAmplitude; self.gfx.rotation += self.continuousRotationSpeed + swayOffset * 0.0001; 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; } 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; self.vy = 0; self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.5 + Math.random() * 0.5); self.naturalVx = self.vx; self.naturalVy = 0; } 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; }); var ShadowFishParticle = Container.expand(function (nodeX, nodeY) { var self = Container.call(this); self.isDone = false; var fishAsset = self.attachAsset('shadowfish', { anchorX: 0.5, anchorY: 0.5, alpha: 0, scaleX: 0.8, scaleY: 0.8 }); var startSide = Math.random() < 0.5 ? -1 : 1; var swimDistance = 300 + Math.random() * 150; var verticalOffsetBase = (Math.random() - 0.5) * 50; var verticalSwimAmplitude = 20 + Math.random() * 30; self.x = nodeX + startSide * (swimDistance + 150); self.y = nodeY + verticalOffsetBase; self.lastX = self.x; self.lastY = self.y; var targetX = nodeX - startSide * (swimDistance + 150); if (startSide > 0) { fishAsset.scale.x *= -1; } var fadeInDuration = 2000 + Math.random() * 1000; var swimDuration = 7000 + Math.random() * 4000; var fadeOutDuration = 700 + Math.random() * 300; var visibleAlpha = 0.5 + Math.random() * 0.2; tween(fishAsset, { alpha: visibleAlpha }, { duration: fadeInDuration, easing: tween.easeOut }); tween(self, { x: targetX }, { duration: swimDuration, easing: tween.linear, delay: fadeInDuration * 0.3, onFinish: function onFinish() { tween(fishAsset, { alpha: 0 }, { duration: fadeOutDuration, easing: tween.easeIn, onFinish: function onFinish() { self.isDone = true; } }); } }); var swimStartTime = LK.ticks; self.update = function () { if (self.isDone || !fishAsset || fishAsset.destroyed) { return; } var currentTicks = LK.ticks; var timeSinceFadeInStart = (currentTicks - swimStartTime) * (1000 / 60); if (timeSinceFadeInStart > fadeInDuration * 0.5 && timeSinceFadeInStart < fadeInDuration * 0.3 + swimDuration - fadeOutDuration * 0.5) { var swimProgress = (timeSinceFadeInStart - fadeInDuration * 0.3) / (swimDuration - fadeInDuration * 0.3); self.y = nodeY + verticalOffsetBase + Math.sin(swimProgress * Math.PI * 2) * verticalSwimAmplitude; } var dx = self.x - self.lastX; var dy = self.y - self.lastY; if (dx !== 0 || dy !== 0) { var angle = Math.atan2(dy, dx); if (fishAsset.scale.x > 0) { fishAsset.rotation = angle; } else { fishAsset.rotation = angle - Math.PI; } } self.lastX = self.x; self.lastY = self.y; }; return self; }); var WaterfallParticle = Container.expand(function (spawnX, spawnY) { var self = Container.call(this); self.gfx = self.attachAsset('bubbles', { anchorX: 0.5, anchorY: 0.5, alpha: 0.7 + Math.random() * 0.3, scaleX: 0.4 + Math.random() * 0.3 }); self.gfx.scaleY = self.gfx.scaleX; self.x = spawnX + (Math.random() - 0.5) * 50; self.y = spawnY; self.isDone = false; var fallDistance = 200; var sprayHeight = 50 + Math.random() * 50; var spraySpreadX = (Math.random() - 0.5) * 150; var fallDuration = 800 + Math.random() * 400; var sprayDuration = 700 + Math.random() * 500; tween(self, { y: self.y + fallDistance }, { duration: fallDuration, easing: tween.easeInSine, onFinish: function onFinish() { tween(self, { y: self.y - sprayHeight, x: self.x + spraySpreadX }, { duration: sprayDuration, easing: tween.easeOutSine, onFinish: function onFinish() { self.isDone = true; } }); tween(self.gfx, { alpha: 0, scaleX: self.gfx.scaleX * 0.4, scaleY: self.gfx.scaleY * 0.4 }, { duration: sprayDuration, easing: tween.easeOutSine }); } }); return self; }); var WaveParticle = Container.expand(function (movesRight) { var self = Container.call(this); self.isDone = false; var FINAL_SCALE_TARGET = 1.2 + Math.random() * 0.6; var assetScaleX = movesRight ? -FINAL_SCALE_TARGET : FINAL_SCALE_TARGET; var waveGfx = self.attachAsset('wave', { anchorX: 0.5, anchorY: 0.5, scaleX: assetScaleX, scaleY: FINAL_SCALE_TARGET, alpha: 0, rotation: 0 }); self.x = 100 + Math.random() * (2048 - 200); self.y = 100 + Math.random() * (2732 - 200); self.startX = self.x; self.startY = self.y; self.waveAmplitude = 10; self.waveFrequency = Math.PI * 2 / 150; var MOVE_DISTANCE_X = 200; var targetX; if (movesRight) { targetX = self.startX + MOVE_DISTANCE_X; } else { targetX = self.startX - MOVE_DISTANCE_X; } var SCALE_FADE_IN_DURATION_MS = 600; var MOVE_DURATION_MS = 5000; var SHRINK_FADE_OUT_DURATION_MS = MOVE_DURATION_MS - SCALE_FADE_IN_DURATION_MS; var VISIBLE_ALPHA_TARGET = 0.3 + Math.random() * 0.3; tween(waveGfx, { alpha: VISIBLE_ALPHA_TARGET }, { duration: SCALE_FADE_IN_DURATION_MS, easing: tween.easeOut }); tween(self, { x: targetX }, { duration: MOVE_DURATION_MS, easing: tween.linear }); var shrinkStartTimeDelay = MOVE_DURATION_MS - SHRINK_FADE_OUT_DURATION_MS; if (shrinkStartTimeDelay < 0) { shrinkStartTimeDelay = 0; } LK.setTimeout(function () { if (self.isDone || !waveGfx || waveGfx.destroyed) { return; } tween(waveGfx, { alpha: 0 }, { duration: SHRINK_FADE_OUT_DURATION_MS, easing: tween.easeIn, onFinish: function onFinish() { self.isDone = true; } }); }, shrinkStartTimeDelay); self.update = function () { if (self.isDone) { return; } var horizontalProgress; if (movesRight) { horizontalProgress = self.x - self.startX; } else { horizontalProgress = self.startX - self.x; } self.y = self.startY + self.waveAmplitude * Math.sin(horizontalProgress * self.waveFrequency); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ /**** * Utility Functions ****/ function updateParticleArray(particleArray) { for (var i = particleArray.length - 1; i >= 0; i--) { var particle = particleArray[i]; if (particle) { if (typeof particle.update === 'function') { particle.update(); } if (particle.isDone) { if (typeof particle.destroy === 'function') { particle.destroy(); } particleArray.splice(i, 1); } } else { particleArray.splice(i, 1); } } } function handleParticleSpawning(config) { config.counter++; if (config.counter >= config.interval && (!config.maxCount || config.array.length < config.maxCount)) { config.counter = 0; var newParticle = new config.constructor(); if (config.container && !config.container.destroyed) { config.container.addChild(newParticle); config.array.push(newParticle); } } } function clearTimer(timerId, isInterval) { if (timerId !== null) { if (isInterval) { LK.clearInterval(timerId); } else { LK.clearTimeout(timerId); } return null; } return timerId; } function cleanupParticleArray(array, container) { if (array) { array.forEach(function (item) { if (item && typeof item.destroy === 'function' && !item.destroyed) { item.destroy(); } }); array.length = 0; } if (container && typeof container.removeChildren === 'function' && !container.destroyed) { container.removeChildren(); } } function stopTween(object) { if (object && !object.destroyed) { tween.stop(object); } } function stopTweens(objects) { for (var i = 0; i < objects.length; i++) { stopTween(objects[i]); } } function createWaveAnimation(segment, amplitude, halfPeriod) { var animUp, animDown; animUp = function animUp() { if (!segment || segment.destroyed) return; tween(segment, { y: segment.baseY - amplitude }, { duration: halfPeriod, easing: tween.easeInOut, onFinish: animDown }); }; animDown = function animDown() { if (!segment || segment.destroyed) return; tween(segment, { y: segment.baseY + amplitude }, { duration: halfPeriod, easing: tween.easeInOut, onFinish: animUp }); }; return { up: animUp, down: animDown }; } function createAmbientSoundScheduler(config) { var timer = null; function scheduleNext() { if (GameState.currentScreen !== config.screenName) { timer = clearTimer(timer, false); return; } var delay = config.baseDelay + Math.random() * config.variance; timer = LK.setTimeout(function () { if (GameState.currentScreen !== config.screenName) return; var soundId = Array.isArray(config.sounds) ? config.sounds[Math.floor(Math.random() * config.sounds.length)] : config.sounds; LK.getSound(soundId).play(); scheduleNext(); }, delay); } return { start: scheduleNext, stop: function stop() { timer = clearTimer(timer, false); } }; } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) { return t; } var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) { return i; } throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } 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, FINAL_GROUP_SCALE: 2.8, GROUP_ANIM_DURATION: 4000, TEXT_FADE_DURATION: 1000, BUTTON_FADE_DURATION: 800, BOAT_ANCHOR_X: 0.5, BOAT_ANCHOR_Y: 0.5, FISHERMAN_ANCHOR_X: 0.5, FISHERMAN_ANCHOR_Y: 0.9, FISHERMAN_X_OFFSET: -20, FISHERMAN_Y_OFFSET: -100, LINE_ANCHOR_X: 0.5, LINE_ANCHOR_Y: 0, LINE_X_OFFSET_FROM_FISHERMAN: 70, LINE_Y_OFFSET_FROM_FISHERMAN: -130, HOOK_ANCHOR_X: 0.5, HOOK_ANCHOR_Y: 0.5, HOOK_Y_DEPTH_FROM_LINE_START: 700, GROUP_PIVOT_X: 0, GROUP_PIVOT_Y: 0, GROUP_INITIAL_Y_SCREEN_OFFSET: -450 }; game.up = function (x, y, obj) { switch (GameState.currentScreen) { case 'title': break; case 'levelSelect': break; case 'fishing': handleFishingInput(x, y, false); break; case 'results': break; } }; /**** * Pattern Generation System ****/ var PatternGenerator = { lastLane: -1, minDistanceBetweenFish: 300, lastActualSpawnTime: -100000, getNextLane: function getNextLane() { if (this.lastLane === -1) { this.lastLane = 1; return 1; } var possibleLanes = [this.lastLane]; if (this.lastLane > 0) { possibleLanes.push(this.lastLane - 1); } if (this.lastLane < 2) { possibleLanes.push(this.lastLane + 1); } if (Math.random() < 0.7) { this.lastLane = possibleLanes[Math.floor(Math.random() * possibleLanes.length)]; } else { this.lastLane = Math.floor(Math.random() * 3); } return this.lastLane; }, canSpawnFishOnBeat: function canSpawnFishOnBeat(currentTime, configuredSpawnInterval) { var timeSinceLast = currentTime - this.lastActualSpawnTime; var minRequiredGap = configuredSpawnInterval; return timeSinceLast >= minRequiredGap; }, registerFishSpawn: function registerFishSpawn(spawnTime) { this.lastActualSpawnTime = spawnTime; }, reset: function reset() { this.lastLane = -1; this.lastActualSpawnTime = -100000; } }; /**** * Game Configuration ****/ var GAME_CONFIG = { SCREEN_CENTER_X: 1024, SCREEN_CENTER_Y: 900, BOAT_Y: 710, WATER_SURFACE_Y: 760, LANES: [{ y: 1133, name: "shallow" }, { y: 1776, name: "medium" }, { y: 2419, name: "deep" }], PERFECT_WINDOW: 40, GOOD_WINDOW: 80, MISS_WINDOW: 120, DEPTHS: [{ level: 1, name: "Shallow Waters", fishSpeed: 6, fishValue: 1, upgradeCost: 0, songs: [{ name: "Gentle Waves", bpm: 90, duration: 202000, 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, 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 }] }], PATTERNS: { simple: { beatsPerFish: 2, doubleSpawnChance: 0.10, rareSpawnChance: 0.02 }, medium: { beatsPerFish: 1.5, doubleSpawnChance: 0.15, rareSpawnChance: 0.05 }, complex: { beatsPerFish: 1, doubleSpawnChance: 0.25, rareSpawnChance: 0.08 }, expert: { beatsPerFish: 0.75, doubleSpawnChance: 0.35, tripletSpawnChance: 0.20, rareSpawnChance: 0.12 }, gentle_waves_custom: { beatsPerFish: 1.5, doubleSpawnChance: 0.05, rareSpawnChance: 0.01, sections: [{ startTime: 0, endTime: 30000, spawnModifier: 1.0, description: "steady_chords" }, { startTime: 30000, endTime: 60000, spawnModifier: 0.9, description: "simple_melody" }, { startTime: 60000, endTime: 120000, spawnModifier: 1.1, description: "melody_development" }, { startTime: 120000, endTime: 180000, spawnModifier: 1.3, description: "gentle_climax" }, { startTime: 180000, endTime: 202000, spawnModifier: 0.8, description: "peaceful_ending" }] }, morning_tide_custom: { beatsPerFish: 1.2, doubleSpawnChance: 0.12, rareSpawnChance: 0.03, sections: [{ startTime: 0, endTime: 25000, spawnModifier: 0.9, description: "calm_opening" }, { startTime: 25000, endTime: 50000, spawnModifier: 1.2, description: "first_wave" }, { startTime: 50000, endTime: 80000, spawnModifier: 1.5, description: "morning_rush" }, { startTime: 80000, endTime: 110000, spawnModifier: 1.3, description: "second_wave" }, { startTime: 110000, endTime: 140000, spawnModifier: 1.4, description: "climactic_finish" }, { startTime: 140000, endTime: 156827, spawnModifier: 0.8, description: "peaceful_fade" }] }, sunny_afternoon_custom: { beatsPerFish: 1.3, doubleSpawnChance: 0.08, rareSpawnChance: 0.025, sections: [{ startTime: 0, endTime: 20000, spawnModifier: 0.8, description: "warm_sunny_start" }, { startTime: 20000, endTime: 35000, spawnModifier: 1.4, description: "first_sunny_burst" }, { startTime: 35000, endTime: 50000, spawnModifier: 0.7, description: "sunny_breather_1" }, { startTime: 50000, endTime: 70000, spawnModifier: 1.5, description: "second_sunny_burst" }, { startTime: 70000, endTime: 90000, spawnModifier: 0.6, description: "sunny_breather_2" }, { startTime: 90000, endTime: 110000, spawnModifier: 1.3, description: "third_sunny_burst" }, { startTime: 110000, endTime: 125000, spawnModifier: 0.8, description: "sunny_breather_3" }, { startTime: 125000, endTime: 150000, spawnModifier: 1.2, description: "sunny_finale_buildup" }, { startTime: 150000, endTime: 181800, spawnModifier: 0.9, description: "sunny_afternoon_fade" }] } } }; /**** * Game State Management ****/ var MULTI_BEAT_SPAWN_DELAY_MS = 250; var TRIPLET_BEAT_SPAWN_DELAY_MS = 350; var FISH_SPAWN_END_BUFFER_MS = 500; 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; 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; 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++; } }, scheduleBeatFish: function scheduleBeatFish(beatNumber, spawnInterval, travelTimeMs) { var targetArrivalTime = GameState.songStartTime + beatNumber * spawnInterval; var songConfig = GameState.getCurrentSongConfig(); var songEndTime = GameState.songStartTime + songConfig.duration; var lastValidArrivalTime = songEndTime - FISH_SPAWN_END_BUFFER_MS; if (songConfig && GameState.songStartTime > 0 && targetArrivalTime > lastValidArrivalTime) { return; } var spawnTime = targetArrivalTime - travelTimeMs; var currentTime = LK.ticks * (1000 / 60); if (spawnTime >= currentTime - 100) { var delay = Math.max(0, spawnTime - currentTime); var self = this; LK.setTimeout(function () { if (GameState.gameActive && GameState.songStartTime !== 0) { self.spawnRhythmFish(beatNumber, targetArrivalTime); } }, delay); } }, spawnRhythmFish: function spawnRhythmFish(beatNumber, targetArrivalTime) { if (!GameState.gameActive) { return; } var currentTime = LK.ticks * (1000 / 60); var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; 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; } } } if (!PatternGenerator.canSpawnFishOnBeat(currentTime, beatInterval)) { return; } if (Math.random() > spawnModifier) { return; } var 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 timeRemainingMs = targetArrivalTime - currentTime; if (timeRemainingMs <= 0) { return; } var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150; var spawnSide = Math.random() < 0.5 ? -1 : 1; var framesRemaining = timeRemainingMs / (1000 / 60); if (framesRemaining <= 0) { return; } var requiredSpeedPerFrame = distanceToHook / framesRemaining; if (spawnSide === 1) { requiredSpeedPerFrame *= -1; } 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; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTime); 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); } 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(); tutorialTextBackground = tutorialOverlayContainer.addChild(LK.getAsset('screenBackground', { x: GAME_CONFIG.SCREEN_CENTER_X, y: 2732 * 0.85, width: 1800, height: 450, color: 0x000000, anchorX: 0.5, anchorY: 0.5, alpha: 0.75 })); tutorialTextDisplay = tutorialOverlayContainer.addChild(new Text2('', { size: 55, fill: 0xFFFFFF, wordWrap: true, wordWrapWidth: 1700, align: 'center', lineHeight: 65 })); tutorialTextDisplay.anchor.set(0.5, 0.5); tutorialTextDisplay.x = tutorialTextBackground.x; tutorialTextDisplay.y = tutorialTextBackground.y - 50; tutorialContinueButton = tutorialOverlayContainer.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: tutorialTextBackground.x, y: tutorialTextBackground.y + tutorialTextBackground.height / 2 - 55, tint: 0x1976d2, width: 350, height: 70 })); tutorialContinueText = tutorialOverlayContainer.addChild(new Text2('CONTINUE', { size: 34, fill: 0xFFFFFF })); tutorialContinueText.anchor.set(0.5, 0.5); tutorialContinueText.x = tutorialContinueButton.x; tutorialContinueText.y = tutorialContinueButton.y; tutorialOverlayContainer.visible = false; } function setTutorialText(newText, showContinue) { if (showContinue === undefined) { showContinue = true; } if (!tutorialTextDisplay || !tutorialContinueButton || !tutorialContinueText) { createTutorialElements(); } tutorialTextDisplay.setText(newText); tutorialContinueButton.visible = showContinue; tutorialContinueText.visible = showContinue; tutorialOverlayContainer.visible = true; } function spawnTutorialFishHelper(config) { var fishType = config.type || 'shallow'; var depthConfig = GAME_CONFIG.DEPTHS[0]; var fishValue = Math.floor(depthConfig.fishValue / 2); var baseSpeed = depthConfig.fishSpeed; var speedMultiplier = config.speedMultiplier || 0.5; 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; newFish.y = GAME_CONFIG.LANES[laneIndex].y + (config.yOffset || 0); newFish.baseY = newFish.y; newFish.lastX = newFish.x; newFish.tutorialFish = true; fishArray.push(newFish); fishingScreen.addChild(newFish); return newFish; } function runTutorialStep() { GameState.tutorialPaused = false; GameState.tutorialAwaitingTap = false; if (tutorialLaneHighlights.length > 0) { tutorialLaneHighlights.forEach(function (overlay) { if (overlay && !overlay.destroyed) { overlay.destroy(); } }); tutorialLaneHighlights = []; } if (tutorialContinueButton) { tutorialContinueButton.visible = true; } if (tutorialContinueText) { tutorialContinueText.visible = true; } 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; } 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; GameState.hookTargetLaneIndex = 1; } } switch (GameState.tutorialStep) { case 0: setTutorialText("Welcome to Beat Fisher! Let's learn the basics. Tap 'CONTINUE'."); GameState.tutorialPaused = true; break; case 1: 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: 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'."); 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, width: 1800, height: 200 }); tutorialOverlayContainer.addChildAt(highlight, 0); tutorialLaneHighlights.push(highlight); } GameState.tutorialPaused = true; break; case 3: setTutorialText("A fish will now approach. Tap in its lane when it's under the hook!"); 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.35, lane: 1 }); break; case 4: 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: setTutorialText("Nice one! Catch fish consecutively to build a COMBO for bonus points! Tap 'CONTINUE'."); GameState.tutorialPaused = true; break; case 6: 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: setTutorialText("You're all set! Tap 'CONTINUE' to go to the fishing spots!"); GameState.tutorialPaused = true; break; default: 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; } if (tutorialLaneHighlights.length > 0) { tutorialLaneHighlights.forEach(function (overlay) { if (overlay && !overlay.destroyed) { overlay.destroy(); } }); tutorialLaneHighlights = []; } if (fishingElements && fishingElements.boat && !fishingElements.boat.destroyed) { stopTween(fishingElements.boat); } if (fishingElements && fishingElements.fishermanContainer && !fishingElements.fishermanContainer.destroyed) { stopTween(fishingElements.fishermanContainer); } if (fishingElements && fishingElements.waterSurfaceSegments) { stopTweens(fishingElements.waterSurfaceSegments); } showScreen('levelSelect'); break; } } function startTutorial() { GameState.tutorialMode = true; GameState.tutorialStep = 0; GameState.gameActive = false; showScreen('fishing'); fishingScreen.alpha = 1; fishArray.forEach(function (f) { if (f && !f.destroyed) { f.destroy(); } }); fishArray = []; ImprovedRhythmSpawner.reset(); if (fishingElements.scoreText) { fishingElements.scoreText.setText(''); } if (fishingElements.fishText) { fishingElements.fishText.setText(''); } if (fishingElements.comboText) { fishingElements.comboText.setText(''); } if (fishingElements.progressText) { fishingElements.progressText.setText(''); } var bracketAssetHeight = 150; var bracketAssetWidth = 75; 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 }); } } } function checkTutorialFishState() { var fish = GameState.tutorialFish; if (!fish || fish.destroyed || fish.caught || fish.missed) { return; } var hookX = fishingElements.hook.x; if (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) { var passedHook = fish.speed > 0 && fish.x > hookX + GAME_CONFIG.MISS_WINDOW + fish.fishGraphics.width / 2 || fish.speed < 0 && fish.x < hookX - GAME_CONFIG.MISS_WINDOW - fish.fishGraphics.width / 2; if (passedHook) { fish.missed = true; GameState.tutorialPaused = true; setTutorialText("It got away! Tap 'CONTINUE' to try that part again."); } } if (fish.x < -250 || fish.x > 2048 + 250) { var wasCriticalStep = GameState.tutorialStep === 3 || GameState.tutorialStep === 4; var fishIndex = fishArray.indexOf(fish); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } fish.destroy(); GameState.tutorialFish = null; if (wasCriticalStep && !fish.caught && !fish.missed) { GameState.tutorialPaused = true; setTutorialText("The fish swam off. Tap 'CONTINUE' to try that part again."); } } } var GameState = { currentScreen: 'title', currentDepth: 0, money: 0, totalFishCaught: 0, ownedSongs: [], selectedDepth: 0, selectedSong: 0, sessionScore: 0, sessionFishCaught: 0, sessionFishSpawned: 0, combo: 0, maxCombo: 0, gameActive: false, songStartTime: 0, lastBeatTime: 0, beatCount: 0, introPlaying: false, musicNotesActive: false, currentPlayingMusicId: 'rhythmTrack', currentPlayingMusicInitialVolume: 0.8, lastLevelSelectNodeKey: 'dock', hookTargetLaneIndex: 1, tutorialMode: false, tutorialStep: 0, tutorialPaused: false, tutorialAwaitingTap: false, tutorialFish: null, 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++; 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()); var globalFadeOverlay = game.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, width: 2048, height: 2732, color: 0x000000, alpha: 0 })); globalFadeOverlay.visible = false; if (game.children.indexOf(globalFadeOverlay) !== -1) { game.setChildIndex(globalFadeOverlay, game.children.length - 1); } GameState.initOwnedSongs(); /**** * Title Screen ****/ function createTitleScreen() { var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 1, height: 2732, color: 0x87CEEB })); var titleAnimationGroup = titleScreen.addChild(new Container()); var titleSky = titleAnimationGroup.addChild(LK.getAsset('skybackground', { x: 0, y: -500 })); var titleWater = titleAnimationGroup.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, width: 2048, height: 2732 - GAME_CONFIG.WATER_SURFACE_Y })); titleScreenOceanBubbleContainer = titleAnimationGroup.addChild(new Container()); titleScreenSeaweedContainer = titleAnimationGroup.addChild(new Container()); titleScreenCloudContainer = titleAnimationGroup.addChild(new Container()); var titleBoatGroup = titleAnimationGroup.addChild(new Container()); var titleBoat = titleBoatGroup.addChild(LK.getAsset('boat', { anchorX: 0.5, anchorY: 0.74, x: 0, y: 0 })); var titleFisherman = titleBoatGroup.addChild(LK.getAsset('fisherman', { anchorX: 0.5, anchorY: 1, x: -100, y: -70 })); var rodTipX = -100 + 85; var rodTipY = -70 - 200; 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, width: 6, height: initialHookYInGroup - rodTipY })); var titleHook = titleBoatGroup.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: rodTipX, y: initialHookYInGroup })); titleBoatGroup.x = GAME_CONFIG.SCREEN_CENTER_X; titleBoatGroup.y = GAME_CONFIG.WATER_SURFACE_Y; var boatGroupBaseY = titleBoatGroup.y; var boatWaveAmplitude = 10; var boatWaveHalfCycleDuration = 2000; var boatRotationAmplitude = 0.03; var boatRotationDuration = 3000; var lineWaveAmplitude = 12; var lineWaveSpeed = 0.03; var linePhaseOffset = 0; 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; for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) { var segment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH_TITLE, y: GAME_CONFIG.WATER_SURFACE_Y, width: SEGMENT_WIDTH_TITLE + 1, height: SEGMENT_HEIGHT_TITLE, anchorX: 0, anchorY: 0.5, alpha: 0.8, tint: 0x4fc3f7 }); segment.baseY = GAME_CONFIG.WATER_SURFACE_Y; titleAnimationGroup.addChild(segment); titleWaterSurfaceSegments.push(segment); } for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) { var whiteSegment = LK.getAsset('waterSurface', { x: i * SEGMENT_WIDTH_TITLE, y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2, width: SEGMENT_WIDTH_TITLE + 1, height: SEGMENT_HEIGHT_TITLE / 2, anchorX: 0, anchorY: 0.5, alpha: 0.6, tint: 0xffffff }); whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2; titleAnimationGroup.addChild(whiteSegment); titleWaterSurfaceSegments.push(whiteSegment); } var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; var targetBoatScreenY = GAME_CONFIG.SCREEN_CENTER_Y + 300; var boatWorldY = GAME_CONFIG.WATER_SURFACE_Y; var pivotY = boatWorldY - (targetBoatScreenY - boatWorldY); titleAnimationGroup.pivot.set(boatCenterX, pivotY); titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X; titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; var INITIAL_ZOOM_FACTOR = 3.0; var FINAL_ZOOM_FACTOR = 1.8; titleAnimationGroup.scale.set(INITIAL_ZOOM_FACTOR); titleAnimationGroup.alpha = 1; var targetUpY = boatGroupBaseY - boatWaveAmplitude; var targetDownY = boatGroupBaseY + boatWaveAmplitude; function moveTitleBoatGroupUp() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } tween(titleBoatGroup, { y: targetUpY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveTitleBoatGroupDown }); } function moveTitleBoatGroupDown() { if (!titleBoatGroup || titleBoatGroup.destroyed) { return; } tween(titleBoatGroup, { y: targetDownY }, { duration: boatWaveHalfCycleDuration, easing: tween.easeInOut, onFinish: moveTitleBoatGroupUp }); } 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 }); } function updateTitleFishingLineWave() { if (!titleLine || titleLine.destroyed || !titleHook || titleHook.destroyed) { return; } linePhaseOffset += lineWaveSpeed; var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude; titleLine.x = rodTipX + waveOffset * 0.3; titleHook.x = rodTipX + waveOffset; var deltaX = titleHook.x - titleLine.x; var deltaY = titleHook.y - titleLine.y; var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY); titleLine.height = actualLineLength; if (actualLineLength > 0.001) { titleLine.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2; } else { titleLine.rotation = 0; } titleHook.rotation = titleLine.rotation; } 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 waveAnim = createWaveAnimation(currentLocalSegment, WAVE_AMPLITUDE_TITLE, WAVE_HALF_PERIOD_MS_TITLE); LK.setTimeout(function () { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE }, { duration: WAVE_HALF_PERIOD_MS_TITLE, easing: tween.easeInOut, onFinish: waveAnim.up }); }, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT_TITLE); })(segment, segmentIndexForDelay); } } var blackOverlay = titleScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, width: 2048, height: 2732, color: 0x000000, alpha: 1 })); var titleImage = titleScreen.addChild(LK.getAsset('titleimage', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 700, alpha: 0, scaleX: 0.8, scaleY: 0.8 })); var startButtonY = 2732 - 2732 / 3.5; var tutorialButtonY = startButtonY + 600; 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 })); return { startButton: startButton, tutorialButton: tutorialButton, titleImage: titleImage, titleAnimationGroup: titleAnimationGroup, blackOverlay: blackOverlay, titleBoatGroup: titleBoatGroup, moveTitleBoatGroupUp: moveTitleBoatGroupUp, rockTitleBoatGroupLeft: rockTitleBoatGroupLeft, titleSky: titleSky, titleWater: titleWater, titleWaterSurfaceSegments: titleWaterSurfaceSegments, titleLine: titleLine, titleHook: titleHook, startTitleWaterSurfaceAnimation: startTitleWaterSurfaceAnimationFunc, updateTitleFishingLineWave: updateTitleFishingLineWave }; } /**** * Level Select Screen ****/ var MAP_CONFIG = { NODES: { dock: { x: 1200, y: 860, unlocked: true, type: 'dock' }, shallows: { x: 524, y: 1400, unlocked: true, type: 'fishing', depthIndex: 0 }, medium: { x: 1324, y: 1700, unlocked: false, type: 'fishing', depthIndex: 1 }, deep: { x: 524, y: 2000, unlocked: false, type: 'fishing', depthIndex: 2 }, abyss: { x: 1524, y: 2300, 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: [['dock', 'shallows'], ['shallows', 'medium'], ['medium', 'deep'], ['deep', 'abyss'], ['dock', 'shop'], ['dock', 'restaurant']], BOAT_BOB_AMPLITUDE: 8, BOAT_BOB_DURATION: 2000, BOAT_ROTATION_AMPLITUDE: 0.02, BOAT_ROTATION_DURATION: 3000, BOAT_TRAVEL_SPEED: 300 }; function createLevelSelectScreen() { levelSelectScreen.removeChildren(); var nodeNameTexts = {}; function getNodeDisplayName(nodeKey) { var nodeNames = { dock: 'Dock', shallows: 'Shallow Waters', medium: 'Mid Waters', deep: 'Deep Waters', abyss: 'The Abyss', shop: 'Fishing Shop', restaurant: 'Ocean Restaurant' }; return nodeNames[nodeKey] || 'Unknown'; } var mapBg = levelSelectScreen.addChild(LK.getAsset('mapBackground', { x: 0, y: 0 })); var moneyDisplayBackground = levelSelectScreen.addChild(LK.getAsset('songCard', { anchorX: 1, anchorY: 0, x: 1900 + 20, y: 80 - 10, width: 450, height: 100, color: 0x000000, alpha: 0.5 })); var ripplesContainer = levelSelectScreen.addChild(new Container()); var activeRipples = []; var rippleSpawnTimerId = null; var RIPPLE_SPAWN_INTERVAL_MS = 350; var RIPPLE_INITIAL_SCALE = 0.1; var RIPPLE_FINAL_SCALE = 1.8; var RIPPLE_INITIAL_OFFSET = 40; var RIPPLE_TRAVEL_DISTANCE = 140; var RIPPLE_DURATION_MS = 1800; var homeIslandRipplesContainer = levelSelectScreen.addChild(new Container()); var activeHomeIslandRipples = []; var homeIslandRippleSpawnTimerId = null; var HOME_ISLAND_RIPPLE_SPAWN_INTERVAL_MS = 225; var HOME_ISLAND_RIPPLE_INITIAL_SCALE = 0.1; var HOME_ISLAND_RIPPLE_FINAL_SCALE = 1.875; var HOME_ISLAND_RIPPLE_INITIAL_OFFSET_FROM_EDGE = 10; var HOME_ISLAND_RIPPLE_TRAVEL_DISTANCE = 220; var HOME_ISLAND_RIPPLE_DURATION_MS = 2000; var homeIsland = levelSelectScreen.addChild(LK.getAsset('homeisland', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 500 })); var shallowWatersNodeBubblesContainer = levelSelectScreen.addChild(new Container()); shadowFishContainer = levelSelectScreen.addChild(new Container()); var activeShallowWatersNodeBubbles = []; var shallowWatersNodeBubbleSpawnTimerId = null; var SHALLOW_NODE_BUBBLE_SPAWN_INTERVAL_MS = 350 + Math.random() * 250; var levelSelectCloudContainer; var activeLevelSelectClouds = []; var levelSelectCloudSpawnTimerId = null; var LEVEL_SELECT_CLOUD_SPAWN_INTERVAL_MS = 4000 + Math.random() * 3000; var MAX_LEVEL_SELECT_CLOUDS = 6; var shadowFishContainer; var activeShadowFish = []; var shadowFishSpawnTimerId = null; var SHADOW_FISH_SPAWN_INTERVAL_MS = 9000 + Math.random() * 5000; var moneyDisplay = new Text2('$0', { size: 70, fill: 0xFFD700, stroke: 0x000000, strokeThickness: 3 }); moneyDisplay.anchor.set(1, 0); moneyDisplay.x = 1900; moneyDisplay.y = 80; levelSelectScreen.addChild(moneyDisplay); var backButtonBg = levelSelectScreen.addChild(LK.getAsset('songCard', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 2600, width: 400, height: 130, alpha: 0.92 })); var backButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 1024, y: 2600, tint: 0x757575, width: 700, height: 170, alpha: 0 })); var backButtonText = new Text2('BACK TO TITLE', { size: 50, fill: 0xFFFFFF }); backButtonText.anchor.set(0.5, 0.5); backButtonText.x = backButton.x; backButtonText.y = backButton.y; levelSelectScreen.addChild(backButtonText); var dottedLinesContainer = levelSelectScreen.addChild(new Container()); var nodesContainer = levelSelectScreen.addChild(new Container()); var boatContainer = levelSelectScreen.addChild(new Container()); var songOverlayContainer = levelSelectScreen.addChild(new Container()); songOverlayContainer.visible = false; var overlayBg = songOverlayContainer.addChild(LK.getAsset('songCard', { x: 1024, y: 1366, width: 1700, height: 900, color: 0x424242, anchorX: 0.5, anchorY: 0.5, alpha: 0.95 })); var closeButton = songOverlayContainer.addChild(LK.getAsset('closeButton', { anchorX: 0.5, anchorY: 0.5, x: 1700, y: 1026, width: 100, height: 100, tint: 0xff4444 })); var closeButtonText = new Text2('X', { size: 60, fill: 0xFFFFFF }); closeButtonText.anchor.set(0.5, 0.5); closeButtonText.x = closeButton.x; closeButtonText.y = closeButton.y; songOverlayContainer.addChild(closeButtonText); var songElements = { leftArrow: null, rightArrow: null, songTitle: null, songInfo: null, songEarnings: null, playButton: null, playButtonText: null, leftArrowText: null, rightArrowText: null }; var currentNode = GameState.lastLevelSelectNodeKey; var initialBoatNodeConfig = MAP_CONFIG.NODES[currentNode]; var playerBoat = boatContainer.addChild(LK.getAsset('playerBoat', { anchorX: 0.5, anchorY: 0.5, x: initialBoatNodeConfig.x, y: initialBoatNodeConfig.y })); var boatMoving = false; var _songOverlayOpen = false; var selectedDepth = 0; var selectedSong = 0; var boatBaseY = playerBoat.y; var boatBobPhase = 0; var boatRotationPhase = 0; var levelSelectSeagullSoundTimer = null; var levelSelectBoatSoundTimer = null; var levelSelectScreenWavesArray = []; var levelSelectScreenWaveSpawnTimerId = null; var SCREEN_WAVE_SPAWN_INTERVAL_MS = (200 + Math.random() * 300) * 4 / 3; var seagullsContainer = levelSelectScreen.addChild(new Container()); var activeSeagulls = []; var seagullSpawnTimerId = null; var SEAGULL_SPAWN_INTERVAL_MS = 3500 + Math.random() * 2500; levelSelectCloudContainer = levelSelectScreen.addChild(new Container()); // Create ambient sound schedulers var seagullScheduler = createAmbientSoundScheduler({ screenName: 'levelSelect', sounds: ['seagull1', 'seagull2', 'seagull3'], baseDelay: 5000, variance: 10000 }); var boatScheduler = createAmbientSoundScheduler({ screenName: 'levelSelect', sounds: 'boatsounds', baseDelay: 6000, variance: 0 }); function startLevelSelectAmbientSounds() { var initialSeagullSounds = ['seagull1', 'seagull2', 'seagull3']; var initialRandomSoundId = initialSeagullSounds[Math.floor(Math.random() * initialSeagullSounds.length)]; LK.getSound(initialRandomSoundId).play(); LK.getSound('boatsounds').play(); seagullScheduler.start(); boatScheduler.start(); } function stopLevelSelectAmbientSounds() { seagullScheduler.stop(); boatScheduler.stop(); } // Initialize clouds at screen load for (var i = 0; i < 3; i++) { var initialCloud = new MapScreenCloudParticle(); initialCloud.x = 400 + Math.random() * 1248; initialCloud.y = 300 + Math.random() * 1700; initialCloud.vx = (Math.random() < 0.5 ? 1 : -1) * (0.15 + Math.random() * 0.2) * 1.15; if (initialCloud.gfx && typeof initialCloud.gfx.alpha === "number") { initialCloud.gfx.alpha = 0.4 + Math.random() * 0.3; } levelSelectCloudContainer.addChild(initialCloud); activeLevelSelectClouds.push(initialCloud); } function spawnRippleEffect() { if (!playerBoat || playerBoat.destroyed || GameState.currentScreen !== 'levelSelect') { return; } var boatX = playerBoat.x; var boatY = playerBoat.y; var spawnAngle = Math.random() * Math.PI * 2; var ripple = new RippleParticle(boatX, boatY, spawnAngle, RIPPLE_INITIAL_OFFSET, RIPPLE_TRAVEL_DISTANCE, RIPPLE_INITIAL_SCALE, RIPPLE_FINAL_SCALE, RIPPLE_DURATION_MS, 1.0); ripplesContainer.addChild(ripple); activeRipples.push(ripple); } function spawnHomeIslandRippleEffect() { if (!homeIsland || homeIsland.destroyed || GameState.currentScreen !== 'levelSelect') { return; } var islandCenterX = homeIsland.x; var islandCenterY = homeIsland.y; var islandRadius = homeIsland.width / 2 * 0.75; var spawnEdgeAngle = Math.random() * Math.PI; var edgeX = islandCenterX + islandRadius * Math.cos(spawnEdgeAngle); var edgeY = islandCenterY + islandRadius * Math.sin(spawnEdgeAngle); var ripple = new RippleParticle(edgeX, edgeY, spawnEdgeAngle, HOME_ISLAND_RIPPLE_INITIAL_OFFSET_FROM_EDGE, HOME_ISLAND_RIPPLE_TRAVEL_DISTANCE, HOME_ISLAND_RIPPLE_INITIAL_SCALE, HOME_ISLAND_RIPPLE_FINAL_SCALE, HOME_ISLAND_RIPPLE_DURATION_MS, 0.5); homeIslandRipplesContainer.addChild(ripple); activeHomeIslandRipples.push(ripple); } function spawnLevelSelectScreenWaveEffect() { if (GameState.currentScreen !== 'levelSelect' || !ripplesContainer || ripplesContainer.destroyed) { return; } var movesRight = Math.random() < 0.5; var wave = new WaveParticle(movesRight); ripplesContainer.addChild(wave); levelSelectScreenWavesArray.push(wave); } function spawnShallowWatersNodeBubbleEffect() { if (GameState.currentScreen !== 'levelSelect' || !shallowWatersNodeBubblesContainer || shallowWatersNodeBubblesContainer.destroyed) { return; } var shallowNodePos = MAP_CONFIG.NODES.shallows; if (!shallowNodePos || !shallowNodePos.unlocked) { return; } var spawnX = shallowNodePos.x; var spawnY = shallowNodePos.y + 250 + (Math.random() - 0.5) * 200; var bubble = new MapBubbleParticle(spawnX, spawnY); shallowWatersNodeBubblesContainer.addChild(bubble); activeShallowWatersNodeBubbles.push(bubble); } function spawnSeagullEffect() { if (GameState.currentScreen !== 'levelSelect' || !seagullsContainer || seagullsContainer.destroyed) { return; } var seagull = new SeagullParticle(); seagullsContainer.addChild(seagull); activeSeagulls.push(seagull); } function spawnLevelSelectCloudEffect() { if (GameState.currentScreen !== 'levelSelect' || !levelSelectCloudContainer || levelSelectCloudContainer.destroyed || activeLevelSelectClouds.length >= MAX_LEVEL_SELECT_CLOUDS) { return; } var cloud = new MapScreenCloudParticle(); levelSelectCloudContainer.addChild(cloud); activeLevelSelectClouds.push(cloud); } function spawnShadowFishEffect() { if (GameState.currentScreen !== 'levelSelect' || !shadowFishContainer || shadowFishContainer.destroyed) { return; } var shallowNodePos = MAP_CONFIG.NODES.shallows; if (!shallowNodePos || !shallowNodePos.unlocked) { return; } var fish = new ShadowFishParticle(shallowNodePos.x, shallowNodePos.y); shadowFishContainer.addChild(fish); activeShadowFish.push(fish); } var waterfallParticlesContainer = levelSelectScreen.addChild(new Container()); var activeWaterfallParticles = []; var waterfallSpawnTimerId = null; var WATERFALL_SPAWN_INTERVAL_MS = 80; var waterfallSpawnX = homeIsland.x + 20; var waterfallSpawnY = homeIsland.y - homeIsland.height / 2 + 350; function spawnWaterfallParticleEffect() { if (GameState.currentScreen !== 'levelSelect' || !waterfallParticlesContainer || waterfallParticlesContainer.destroyed) { return; } var particle = new WaterfallParticle(waterfallSpawnX, waterfallSpawnY); waterfallParticlesContainer.addChild(particle); activeWaterfallParticles.push(particle); } // Start timers rippleSpawnTimerId = LK.setInterval(spawnRippleEffect, RIPPLE_SPAWN_INTERVAL_MS); homeIslandRippleSpawnTimerId = LK.setInterval(spawnHomeIslandRippleEffect, HOME_ISLAND_RIPPLE_SPAWN_INTERVAL_MS); levelSelectScreenWaveSpawnTimerId = LK.setInterval(spawnLevelSelectScreenWaveEffect, SCREEN_WAVE_SPAWN_INTERVAL_MS); waterfallSpawnTimerId = LK.setInterval(spawnWaterfallParticleEffect, WATERFALL_SPAWN_INTERVAL_MS); shallowWatersNodeBubbleSpawnTimerId = LK.setInterval(spawnShallowWatersNodeBubbleEffect, SHALLOW_NODE_BUBBLE_SPAWN_INTERVAL_MS); seagullSpawnTimerId = LK.setInterval(spawnSeagullEffect, SEAGULL_SPAWN_INTERVAL_MS); levelSelectCloudSpawnTimerId = LK.setInterval(spawnLevelSelectCloudEffect, LEVEL_SELECT_CLOUD_SPAWN_INTERVAL_MS); shadowFishSpawnTimerId = LK.setInterval(spawnShadowFishEffect, SHADOW_FISH_SPAWN_INTERVAL_MS); function cleanupAllParticles() { rippleSpawnTimerId = clearTimer(rippleSpawnTimerId, true); homeIslandRippleSpawnTimerId = clearTimer(homeIslandRippleSpawnTimerId, true); levelSelectScreenWaveSpawnTimerId = clearTimer(levelSelectScreenWaveSpawnTimerId, true); waterfallSpawnTimerId = clearTimer(waterfallSpawnTimerId, true); shallowWatersNodeBubbleSpawnTimerId = clearTimer(shallowWatersNodeBubbleSpawnTimerId, true); seagullSpawnTimerId = clearTimer(seagullSpawnTimerId, true); levelSelectCloudSpawnTimerId = clearTimer(levelSelectCloudSpawnTimerId, true); shadowFishSpawnTimerId = clearTimer(shadowFishSpawnTimerId, true); cleanupParticleArray(activeRipples, ripplesContainer); cleanupParticleArray(activeHomeIslandRipples, homeIslandRipplesContainer); cleanupParticleArray(levelSelectScreenWavesArray, ripplesContainer); cleanupParticleArray(activeWaterfallParticles, waterfallParticlesContainer); cleanupParticleArray(activeShallowWatersNodeBubbles, shallowWatersNodeBubblesContainer); cleanupParticleArray(activeSeagulls, seagullsContainer); cleanupParticleArray(activeLevelSelectClouds, levelSelectCloudContainer); cleanupParticleArray(activeShadowFish, shadowFishContainer); } function updateNodeUnlocks() { var nodes = MAP_CONFIG.NODES; nodes.medium.unlocked = GameState.currentDepth >= 1; nodes.deep.unlocked = GameState.currentDepth >= 2; nodes.abyss.unlocked = GameState.currentDepth >= 3; nodes.shop.unlocked = GameState.currentDepth >= 1; nodes.restaurant.unlocked = GameState.currentDepth >= 2; } function createDottedLines() { dottedLinesContainer.removeChildren(); MAP_CONFIG.CONNECTIONS.forEach(function (connection) { var node1 = MAP_CONFIG.NODES[connection[0]]; var node2 = MAP_CONFIG.NODES[connection[1]]; 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 dotCount = Math.floor(distance / 20); for (var i = 0; i < dotCount; i++) { if (i % 2 === 0) { var dotX = node1.x + dx * i / dotCount; var dotY = node1.y + dy * i / dotCount; dottedLinesContainer.addChild(LK.getAsset('dottedLine', { anchorX: 0.5, anchorY: 0.5, x: dotX, y: dotY, width: 8, height: 8, tint: 0xFFFFFF, alpha: 0.7 })); } } } }); } function createNodes() { nodesContainer.removeChildren(); Object.keys(MAP_CONFIG.NODES).forEach(function (nodeKey) { var node = MAP_CONFIG.NODES[nodeKey]; var nodeContainer = nodesContainer.addChild(new Container()); 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 })); if (!node.unlocked) { nodeContainer.addChild(LK.getAsset('nodeLocked', { anchorX: 0.5, anchorY: 0.5, x: node.x, y: node.y - 5 })); } nodeGfx.nodeKey = nodeKey; var displayName = getNodeDisplayName(nodeKey); var nameTextFill = 0xFFFFFF; if (nodeKey === 'restaurant' || nodeKey === 'shop') { nameTextFill = 0xFFD700; } var labelY = node.y + nodeGfx.height / 2 + 15; var labelHeight = 80; var labelBg = null; var nameText = nodeContainer.addChild(new Text2(displayName, { size: 70, fill: nameTextFill, stroke: 0x000000, strokeThickness: 2, align: 'center' })); nameText.anchor.set(0.5, 0); nameText.x = node.x; nameText.y = labelY; nameText.visible = true; if (nodeKey === 'restaurant' || nodeKey === 'shop') { var labelWidth = nameText.width + 60; labelBg = nodeContainer.addChild(LK.getAsset('songCard', { anchorX: 0.5, anchorY: 0, x: node.x, y: labelY, width: labelWidth, height: labelHeight, color: 0x222222, alpha: 0.85 })); if (labelBg && nameText) { nodeContainer.setChildIndex(labelBg, nodeContainer.children.indexOf(nameText)); } } nodeNameTexts[nodeKey] = nameText; }); } 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; tween(playerBoat, { x: targetNode.x, y: targetNode.y }, { duration: travelTime, easing: tween.easeInOut, onFinish: function onFinish() { boatMoving = false; boatBaseY = playerBoat.y; tween(playerBoat, { rotation: 0 }, { duration: 500, easing: tween.easeOut }); if (MAP_CONFIG.NODES[targetNodeKey].type === 'fishing') { showSongSelection(MAP_CONFIG.NODES[targetNodeKey].depthIndex); } else if (MAP_CONFIG.NODES[targetNodeKey].type === 'shop') { console.log("Arrived at Shop"); } else if (MAP_CONFIG.NODES[targetNodeKey].type === 'restaurant') { console.log("Arrived at Restaurant"); } } }); } function showSongSelection(depthIndex) { selectedDepth = depthIndex; selectedSong = 0; _songOverlayOpen = true; songOverlayContainer.visible = true; createSongSelectionElements(); updateSongDisplay(); songOverlayContainer.alpha = 0; tween(songOverlayContainer, { alpha: 1 }, { duration: 300, easing: tween.easeOut }); } function hideSongSelection() { _songOverlayOpen = false; tween(songOverlayContainer, { alpha: 0 }, { duration: 200, easing: tween.easeIn, onFinish: function onFinish() { songOverlayContainer.visible = false; clearSongSelectionElements(); } }); } function createSongSelectionElements() { clearSongSelectionElements(); var overlayCenterX = 1024; var overlayCenterY = 1366; songElements.leftArrow = songOverlayContainer.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: overlayCenterX - 650, y: overlayCenterY, width: 120, height: 120, tint: 0x666666 })); songElements.leftArrowText = songOverlayContainer.addChild(new Text2('<', { size: 80, 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 + 650, y: overlayCenterY, width: 120, height: 120, tint: 0x666666 })); songElements.rightArrowText = songOverlayContainer.addChild(new Text2('>', { size: 80, fill: 0xFFFFFF })); songElements.rightArrowText.anchor.set(0.5, 0.5); songElements.rightArrowText.x = songElements.rightArrow.x; songElements.rightArrowText.y = songElements.rightArrow.y; songElements.songTitle = songOverlayContainer.addChild(new Text2('Song Title', { size: 110, fill: 0xFFFFFF })); songElements.songTitle.anchor.set(0.5, 0.5); songElements.songTitle.x = overlayCenterX; songElements.songTitle.y = overlayCenterY - 300; songElements.songInfo = songOverlayContainer.addChild(new Text2('BPM: 120 | Duration: 2:00', { size: 70, fill: 0xCCCCCC })); songElements.songInfo.anchor.set(0.5, 0.5); songElements.songInfo.x = overlayCenterX; songElements.songInfo.y = overlayCenterY - 200; songElements.fishDisplayContainer = songOverlayContainer.addChild(new Container()); songElements.fishDisplayContainer.x = overlayCenterX; songElements.fishDisplayContainer.y = songElements.songInfo.y + 35 + 30; var fishDisplayBottomY = songElements.fishDisplayContainer.y + 260; songElements.playButton = songOverlayContainer.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: overlayCenterX, y: fishDisplayBottomY + 25 + 50, width: 400, height: 100 })); songElements.playButtonText = songOverlayContainer.addChild(new Text2('PLAY', { size: 100, fill: 0xFFFFFF })); songElements.playButtonText.anchor.set(0.5, 0.5); songElements.playButtonText.x = songElements.playButton.x; songElements.playButtonText.y = songElements.playButton.y; } function clearSongSelectionElements() { Object.keys(songElements).forEach(function (key) { if (songElements[key] && songElements[key].destroy && !songElements[key].destroyed) { songElements[key].destroy(); } songElements[key] = null; }); } function updateSongDisplay() { if (!songElements.songTitle || !songElements.fishDisplayContainer) { return; } songElements.fishDisplayContainer.removeChildren(); var depthConfig = GAME_CONFIG.DEPTHS[selectedDepth]; if (!depthConfig || !depthConfig.songs || !depthConfig.songs[selectedSong]) { console.error("Invalid depth or song index:", selectedDepth, selectedSong); return; } var songConfig = depthConfig.songs[selectedSong]; var patternConfig = GAME_CONFIG.PATTERNS[songConfig.pattern]; var owned = GameState.hasSong(selectedDepth, selectedSong); var overlayCenterX = 1024; var overlayCenterY = 1366; if (songElements.songTitle) { songElements.songTitle.destroy(); } songElements.songTitle = songOverlayContainer.addChild(new Text2(songConfig.name, { size: 110, fill: 0xFFFFFF, wordWrap: false, align: 'center' })); songElements.songTitle.anchor.set(0.5, 0.5); songElements.songTitle.x = overlayCenterX; songElements.songTitle.y = overlayCenterY - 300; if (songElements.songInfo) { songElements.songInfo.destroy(); } songElements.songInfo = songOverlayContainer.addChild(new Text2('BPM: ' + songConfig.bpm + ' | Duration: ' + formatTime(songConfig.duration), { size: 70, fill: 0xCCCCCC, wordWrap: false, align: 'center' })); songElements.songInfo.anchor.set(0.5, 0.5); songElements.songInfo.x = overlayCenterX; songElements.songInfo.y = overlayCenterY - 200; function getFishDistributionForSong(songCfg, depthCfg, patternCfg, currentSelectedDepth) { var distribution = []; var fishNames = { anchovy: "Anchovy", sardine: "Sardine", mackerel: "Mackerel", mediumFish: "Mid-Size", deepFish: "Deep Lurker", rareFish: "Rare Catch" }; if (songCfg.name === "Gentle Waves") { distribution = [{ asset: 'anchovy', name: fishNames.anchovy, percentage: 60 }, { asset: 'sardine', name: fishNames.sardine, percentage: 30 }, { asset: 'mackerel', name: fishNames.mackerel, percentage: 10 }]; } else if (songCfg.name === "Morning Tide") { distribution = [{ asset: 'anchovy', name: fishNames.anchovy, percentage: 40 }, { asset: 'sardine', name: fishNames.sardine, percentage: 40 }, { asset: 'mackerel', name: fishNames.mackerel, percentage: 20 }]; } else if (songCfg.name === "Sunny Afternoon") { distribution = [{ asset: 'anchovy', name: fishNames.anchovy, percentage: 30 }, { asset: 'sardine', name: fishNames.sardine, percentage: 30 }, { asset: 'mackerel', name: fishNames.mackerel, percentage: 40 }]; } else { var p_rare = patternCfg.rareSpawnChance || 0; if (p_rare > 0) { distribution.push({ asset: 'rareFish', name: fishNames.rareFish, percentage: p_rare * 100 }); } var p_deep = 0; if (currentSelectedDepth >= 2) { p_deep = Math.max(0, 0.3 - p_rare); } if (p_deep > 0) { distribution.push({ asset: 'deepFish', name: fishNames.deepFish, percentage: p_deep * 100 }); } var p_medium = 0; if (currentSelectedDepth >= 1) { var lower_bound_for_medium = p_rare + p_deep; p_medium = Math.max(0, 0.6 - lower_bound_for_medium); } if (p_medium > 0) { distribution.push({ asset: 'mediumFish', name: fishNames.mediumFish, percentage: p_medium * 100 }); } var p_shallow_total = Math.max(0, 1.0 - (p_rare + p_deep + p_medium)); if (p_shallow_total > 0) { var shallowFishAssets = [{ asset: 'anchovy', name: fishNames.anchovy }, { asset: 'sardine', name: fishNames.sardine }, { asset: 'mackerel', name: fishNames.mackerel }]; if (currentSelectedDepth === 0 || p_rare + p_deep + p_medium < 0.8) { var per_shallow_percentage = p_shallow_total / shallowFishAssets.length * 100; if (per_shallow_percentage > 0.1) { shallowFishAssets.forEach(function (sf) { distribution.push(_objectSpread(_objectSpread({}, sf), { percentage: per_shallow_percentage })); }); } } else if (distribution.length === 0 && p_shallow_total > 0) { var per_shallow_percentage2 = p_shallow_total / shallowFishAssets.length * 100; if (per_shallow_percentage2 > 0.1) { shallowFishAssets.forEach(function (sf) { distribution.push(_objectSpread(_objectSpread({}, sf), { percentage: per_shallow_percentage2 })); }); } } } } return distribution.filter(function (f) { return f.percentage >= 1; }).sort(function (a, b) { return b.percentage - a.percentage; }); } var fishToDisplay = getFishDistributionForSong(songConfig, depthConfig, patternConfig, selectedDepth); var itemWidth = 180; var itemHeight = 130; var horizontalSpacing = 40; var verticalSpacing = 50; var itemsPerRow = 3; var startX = -((itemsPerRow - 1) * (itemWidth + horizontalSpacing)) / 2; for (var i = 0; i < fishToDisplay.length; i++) { var fishData = fishToDisplay[i]; var fishItemContainer = songElements.fishDisplayContainer.addChild(new Container()); var rowIndex = Math.floor(i / itemsPerRow); var colIndex = i % itemsPerRow; fishItemContainer.x = startX + colIndex * (itemWidth + horizontalSpacing) - 60; fishItemContainer.y = rowIndex * (itemHeight + verticalSpacing) + 60; var icon = fishItemContainer.attachAsset(fishData.asset, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7, scaleY: 0.7 }); icon.x = itemWidth / 2; icon.y = 5; var nameText = fishItemContainer.addChild(new Text2(fishData.name, { size: 48, fill: 0xFFFFFF, align: 'center', wordWrap: true, wordWrapWidth: itemWidth - 10 })); nameText.anchor.set(0.5, 0); nameText.x = itemWidth / 2; nameText.y = icon.y + icon.height * 0.35 + 5; var percentText = fishItemContainer.addChild(new Text2(fishData.percentage.toFixed(1) + '%', { size: 40, fill: 0xCCCCCC, align: 'center' })); percentText.anchor.set(0.5, 0); percentText.x = itemWidth / 2; percentText.y = nameText.y + nameText.height + 3; } if (owned) { songElements.playButtonText.setText('PLAY'); songElements.playButton.tint = 0x1976d2; } else { songElements.playButtonText.setText('BUY ($' + songConfig.cost + ')'); songElements.playButton.tint = GameState.money >= songConfig.cost ? 0x2e7d32 : 0x666666; } songElements.leftArrow.tint = selectedSong > 0 ? 0x1976d2 : 0x666666; songElements.rightArrow.tint = selectedSong < depthConfig.songs.length - 1 ? 0x1976d2 : 0x666666; } function updateBoatAnimation() { if (boatMoving) { return; } boatBobPhase += 0.03; var bobOffset = Math.sin(boatBobPhase) * MAP_CONFIG.BOAT_BOB_AMPLITUDE; playerBoat.y = boatBaseY + bobOffset; boatRotationPhase += 0.02; var rotationOffset = Math.sin(boatRotationPhase) * MAP_CONFIG.BOAT_ROTATION_AMPLITUDE; playerBoat.rotation = rotationOffset; } function handleMapInput(x, y) { if (_songOverlayOpen) { 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; } 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; } 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) { GameState.selectedDepth = selectedDepth; GameState.selectedSong = selectedSong; GameState.lastLevelSelectNodeKey = currentNode; showScreen('fishing'); } else { if (GameState.buySong(selectedDepth, selectedSong)) { updateSongDisplay(); updateMapDisplay(); } } return; } return; } 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; } if (nodesContainer && nodesContainer.children) { nodesContainer.children.forEach(function (nodeContainerInstance) { if (nodeContainerInstance && nodeContainerInstance.children && nodeContainerInstance.children.length > 0) { var nodeGfx = nodeContainerInstance.children[0]; 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) { moveBoatToNode(nodeGfx.nodeKey); } } } } }); } } function updateMapDisplay() { updateNodeUnlocks(); createDottedLines(); createNodes(); moneyDisplay.setText('$' + GameState.money); } updateMapDisplay(); if (songOverlayContainer && levelSelectScreen.children.indexOf(songOverlayContainer) !== -1) { levelSelectScreen.setChildIndex(songOverlayContainer, levelSelectScreen.children.length - 1); } return { updateMapDisplay: updateMapDisplay, handleMapInput: handleMapInput, updateBoatAnimation: updateBoatAnimation, moneyDisplay: moneyDisplay, songOverlayOpen: function songOverlayOpen() { return _songOverlayOpen; }, updateRipples: function updateRipples() { updateParticleArray(activeRipples); }, cleanupRipples: cleanupAllParticles, ripplesContainer: ripplesContainer, activeRipples: activeRipples, rippleSpawnTimerId: rippleSpawnTimerId, updateHomeIslandRipples: function updateHomeIslandRipples() { updateParticleArray(activeHomeIslandRipples); }, cleanupHomeIslandRipples: cleanupAllParticles, homeIslandRipplesContainer: homeIslandRipplesContainer, activeHomeIslandRipples: activeHomeIslandRipples, homeIslandRippleSpawnTimerId: homeIslandRippleSpawnTimerId, updateScreenWaves: function updateScreenWaves() { updateParticleArray(levelSelectScreenWavesArray); }, cleanupScreenWaves: cleanupAllParticles, screenWaveSpawnTimerId: levelSelectScreenWaveSpawnTimerId, updateShallowWatersNodeBubbles: function updateShallowWatersNodeBubbles() { updateParticleArray(activeShallowWatersNodeBubbles); }, cleanupShallowWatersNodeBubbles: cleanupAllParticles, shallowWatersNodeBubbleSpawnTimerId: shallowWatersNodeBubbleSpawnTimerId, updateSeagulls: function updateSeagulls() { updateParticleArray(activeSeagulls); }, cleanupSeagulls: cleanupAllParticles, seagullSpawnTimerId: seagullSpawnTimerId, levelSelectCloudContainer: levelSelectCloudContainer, updateLevelSelectClouds: function updateLevelSelectClouds() { updateParticleArray(activeLevelSelectClouds); }, cleanupLevelSelectClouds: cleanupAllParticles, levelSelectCloudSpawnTimerId: levelSelectCloudSpawnTimerId, updateWaterfallParticles: function updateWaterfallParticles() { updateParticleArray(activeWaterfallParticles); }, cleanupWaterfallParticles: cleanupAllParticles, waterfallSpawnTimerId: waterfallSpawnTimerId, updateShadowFish: function updateShadowFish() { updateParticleArray(activeShadowFish); }, cleanupLevelSelectShadowFish: cleanupAllParticles, shadowFishSpawnTimerId: shadowFishSpawnTimerId, startLevelSelectAmbientSounds: startLevelSelectAmbientSounds, stopLevelSelectAmbientSounds: stopLevelSelectAmbientSounds, restartParticleTimers: function restartParticleTimers() { cleanupAllParticles(); rippleSpawnTimerId = LK.setInterval(spawnRippleEffect, RIPPLE_SPAWN_INTERVAL_MS); homeIslandRippleSpawnTimerId = LK.setInterval(spawnHomeIslandRippleEffect, HOME_ISLAND_RIPPLE_SPAWN_INTERVAL_MS); levelSelectScreenWaveSpawnTimerId = LK.setInterval(spawnLevelSelectScreenWaveEffect, SCREEN_WAVE_SPAWN_INTERVAL_MS); waterfallSpawnTimerId = LK.setInterval(spawnWaterfallParticleEffect, WATERFALL_SPAWN_INTERVAL_MS); shallowWatersNodeBubbleSpawnTimerId = LK.setInterval(spawnShallowWatersNodeBubbleEffect, SHALLOW_NODE_BUBBLE_SPAWN_INTERVAL_MS); seagullSpawnTimerId = LK.setInterval(spawnSeagullEffect, SEAGULL_SPAWN_INTERVAL_MS); levelSelectCloudSpawnTimerId = LK.setInterval(spawnLevelSelectCloudEffect, LEVEL_SELECT_CLOUD_SPAWN_INTERVAL_MS); shadowFishSpawnTimerId = LK.setInterval(spawnShadowFishEffect, SHADOW_FISH_SPAWN_INTERVAL_MS); } }; } /**** * Fishing Screen ****/ function createFishingScreen() { var sky = fishingScreen.addChild(LK.getAsset('skybackground', { x: 0, y: -500 })); var water = fishingScreen.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, width: 2048, height: 2732 - GAME_CONFIG.WATER_SURFACE_Y })); globalOceanBubbleContainer = fishingScreen.addChild(new Container()); globalSeaweedContainer = fishingScreen.addChild(new Container()); globalCloudContainer = fishingScreen.addChild(new Container()); bubbleContainer = fishingScreen.addChild(new Container()); musicNotesContainer = fishingScreen.addChild(new Container()); var waterSurfaceSegments = []; var waterSurfaceSegmentsBlueTemp = []; var waterSurfaceSegmentsWhiteTemp = []; 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; 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; waterSurfaceSegmentsBlueTemp.push(segment); } 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; waterSurfaceSegmentsWhiteTemp.push(whiteSegment); } 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 })); for (var i = 0; i < waterSurfaceSegmentsBlueTemp.length; i++) { fishingScreen.addChild(waterSurfaceSegmentsBlueTemp[i]); waterSurfaceSegments.push(waterSurfaceSegmentsBlueTemp[i]); } for (var i = 0; i < waterSurfaceSegmentsWhiteTemp.length; i++) { fishingScreen.addChild(waterSurfaceSegmentsWhiteTemp[i]); waterSurfaceSegments.push(waterSurfaceSegmentsWhiteTemp[i]); } var fishermanContainer = fishingScreen.addChild(new Container()); 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 })); var boatBaseY = boat.y; var fishermanBaseY = fishermanContainer.y; var boatWaveAmplitude = 10; var boatWaveHalfCycleDuration = 2000; 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; } var targetUpY = boatBaseY - boatWaveAmplitude; var targetDownY = boatBaseY + boatWaveAmplitude; var fishermanTargetUpY = fishermanBaseY - boatWaveAmplitude; var fishermanTargetDownY = fishermanBaseY + boatWaveAmplitude; 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 startWaterSurfaceAnimationFunc() { var allSegments = waterSurfaceSegments; 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 waveAnim = createWaveAnimation(currentLocalSegment, WAVE_AMPLITUDE, WAVE_HALF_PERIOD_MS); LK.setTimeout(function () { if (!currentLocalSegment || currentLocalSegment.destroyed) { return; } tween(currentLocalSegment, { y: currentLocalSegment.baseY - WAVE_AMPLITUDE }, { duration: WAVE_HALF_PERIOD_MS, easing: tween.easeInOut, onFinish: waveAnim.down }); }, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT); })(segment, segmentIndexForDelay); } } 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(); } } 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; var levelSelectElements = createLevelSelectScreen(); var fishingElements = createFishingScreen(); var tutorialOverlayContainer = game.addChild(new Container()); tutorialOverlayContainer.visible = false; var tutorialTextBackground; var tutorialTextDisplay; var tutorialContinueButton; var tutorialContinueText; var tutorialLaneHighlights = []; var fishArray = []; var bubblesArray = []; var bubbleContainer; var musicNotesArray = []; var musicNotesContainer; var laneBrackets = []; var musicNoteSpawnCounter = 0; var MUSIC_NOTE_SPAWN_INTERVAL_TICKS = 45; var globalOceanBubblesArray = []; var globalOceanBubbleContainer; var globalOceanBubbleSpawnCounter = 0; var OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS = 40; var globalSeaweedArray = []; var globalSeaweedContainer; var globalSeaweedSpawnCounter = 0; var SEAWEED_SPAWN_INTERVAL_TICKS = 120; var MAX_SEAWEED_COUNT = 8; var globalCloudArray = []; var globalCloudContainer; var globalCloudSpawnCounter = 0; var CLOUD_SPAWN_INTERVAL_TICKS = 180; var MAX_CLOUD_COUNT = 5; var titleScreenOceanBubblesArray = []; var titleScreenOceanBubbleContainer; var titleScreenOceanBubbleSpawnCounter = 0; var titleScreenSeaweedArray = []; var titleScreenSeaweedContainer; var titleScreenSeaweedSpawnCounter = 0; var titleScreenCloudArray = []; var titleScreenCloudContainer; var titleScreenCloudSpawnCounter = 0; var titleSeagullSoundTimer = null; var titleBoatSoundTimer = null; /**** * Input State and Helpers for Fishing ****/ var inputState = { touching: false, touchLane: -1, touchStartTime: 0 }; function getTouchLane(y) { var boundary_lane0_lane1 = (GAME_CONFIG.LANES[0].y + GAME_CONFIG.LANES[1].y) / 2; var boundary_lane1_lane2 = (GAME_CONFIG.LANES[1].y + GAME_CONFIG.LANES[2].y) / 2; if (y < boundary_lane0_lane1) { return 0; } else if (y < boundary_lane1_lane2) { return 1; } else { return 2; } } function showFeedback(type, laneIndex) { var feedbackY = GAME_CONFIG.LANES[laneIndex].y; var indicator = new FeedbackIndicator(type); indicator.x = fishingElements.hook.x; indicator.y = feedbackY; fishingScreen.addChild(indicator); indicator.show(); } function animateHookCatch() { var hook = fishingElements.hook; var restingY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; 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() { hook.originalY = restingY; } }); } }); } function handleFishingInput(x, y, isDown) { if (GameState.tutorialMode && isDown && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !GameState.tutorialPaused) { if (GameState.tutorialFish && !GameState.tutorialFish.caught && !GameState.tutorialFish.missed) { checkCatch(getTouchLane(y)); } return; } if (!GameState.gameActive) { return; } var currentTime = LK.ticks * (1000 / 60); if (isDown) { inputState.touching = true; inputState.touchLane = getTouchLane(y); inputState.touchStartTime = currentTime; } else { if (inputState.touching) { checkCatch(inputState.touchLane); } inputState.touching = false; } } /**** * Screen Management ****/ function showScreen(screenName) { var previousScreen = GameState.currentScreen; if (previousScreen === 'title' && titleElements) { stopTweens([titleElements.titleAnimationGroup, titleElements.blackOverlay, titleElements.titleImage, titleElements.startButton, titleElements.tutorialButton]); if (titleElements.titleWaterSurfaceSegments) { stopTweens(titleElements.titleWaterSurfaceSegments); } titleSeagullSoundTimer = clearTimer(titleSeagullSoundTimer, false); titleBoatSoundTimer = clearTimer(titleBoatSoundTimer, false); cleanupParticleArray(titleScreenOceanBubblesArray, titleScreenOceanBubbleContainer); cleanupParticleArray(titleScreenSeaweedArray, titleScreenSeaweedContainer); cleanupParticleArray(titleScreenCloudArray, titleScreenCloudContainer); } else if (previousScreen === 'levelSelect' && screenName !== 'levelSelect') { if (levelSelectElements && typeof levelSelectElements.cleanupRipples === 'function') { levelSelectElements.cleanupRipples(); } if (levelSelectElements && typeof levelSelectElements.stopLevelSelectAmbientSounds === 'function') { levelSelectElements.stopLevelSelectAmbientSounds(); } } if (previousScreen === '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; } if (tutorialLaneHighlights.length > 0) { tutorialLaneHighlights.forEach(function (overlay) { if (overlay && !overlay.destroyed) { overlay.destroy(); } }); tutorialLaneHighlights = []; } GameState.tutorialMode = false; } if (previousScreen === 'title' && screenName === 'levelSelect') { globalFadeOverlay.alpha = 0; globalFadeOverlay.visible = true; if (game.children.indexOf(globalFadeOverlay) !== -1) { game.setChildIndex(globalFadeOverlay, game.children.length - 1); } tween(globalFadeOverlay, { alpha: 1 }, { duration: 500, easing: tween.easeOut, onFinish: function onFinish() { titleScreen.visible = false; if (levelSelectElements) { if (typeof levelSelectElements.cleanupRipples === 'function') { levelSelectElements.cleanupRipples(); } if (typeof levelSelectElements.stopLevelSelectAmbientSounds === 'function') { levelSelectElements.stopLevelSelectAmbientSounds(); } } levelSelectElements = createLevelSelectScreen(); levelSelectScreen.visible = true; GameState.currentScreen = 'levelSelect'; if (levelSelectElements && typeof levelSelectElements.updateMapDisplay === 'function') { levelSelectElements.updateMapDisplay(); } if (levelSelectElements && typeof levelSelectElements.startLevelSelectAmbientSounds === 'function') { levelSelectElements.startLevelSelectAmbientSounds(); } tween(globalFadeOverlay, { alpha: 0 }, { duration: 500, easing: tween.easeIn, onFinish: function onFinish() { globalFadeOverlay.visible = false; } }); } }); return; } titleScreen.visible = false; levelSelectScreen.visible = false; fishingScreen.visible = false; resultsScreen.visible = false; GameState.currentScreen = screenName; switch (screenName) { case 'title': titleScreen.visible = true; var seagullScheduler = createAmbientSoundScheduler({ screenName: 'title', sounds: ['seagull1', 'seagull2', 'seagull3'], baseDelay: 5000, variance: 10000 }); var boatScheduler = createAmbientSoundScheduler({ screenName: 'title', sounds: 'boatsounds', baseDelay: 6000, variance: 0 }); if (titleElements.startTitleWaterSurfaceAnimation) { titleElements.startTitleWaterSurfaceAnimation(); } 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(); seagullScheduler.start(); boatScheduler.start(); titleScreenOceanBubbleSpawnCounter = 0; titleScreenSeaweedSpawnCounter = 0; titleScreenCloudSpawnCounter = 0; var ZOOM_DURATION = 8000; var OVERLAY_FADE_DELAY = 1000; var OVERLAY_FADE_DURATION = 3000; var TEXT_DELAY = 4000; var BUTTON_DELAY = 5500; titleElements.titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X; titleElements.titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; titleElements.titleAnimationGroup.alpha = 1; titleElements.titleAnimationGroup.scale.set(3.0); titleElements.blackOverlay.alpha = 1; if (titleElements.titleImage) { titleElements.titleImage.alpha = 0; } titleElements.startButton.alpha = 0; titleElements.tutorialButton.alpha = 0; tween(titleElements.titleAnimationGroup, { scaleX: 1.8, scaleY: 1.8 }, { duration: ZOOM_DURATION, easing: tween.easeInOut }); LK.setTimeout(function () { tween(titleElements.blackOverlay, { alpha: 0 }, { duration: OVERLAY_FADE_DURATION, easing: tween.easeInOut }); }, OVERLAY_FADE_DELAY); LK.setTimeout(function () { if (titleElements.titleImage) { tween(titleElements.titleImage, { alpha: 1 }, { duration: 1200, easing: tween.easeOut }); } }, TEXT_DELAY); LK.setTimeout(function () { tween(titleElements.startButton, { alpha: 1 }, { duration: 1000, easing: tween.easeOut, onFinish: function onFinish() { tween(titleElements.tutorialButton, { alpha: 1 }, { duration: 1000, easing: tween.easeOut }); } }); }, BUTTON_DELAY); break; case 'levelSelect': if (previousScreen === 'results') { if (levelSelectElements) { if (typeof levelSelectElements.cleanupRipples === 'function') { levelSelectElements.cleanupRipples(); } if (typeof levelSelectElements.stopLevelSelectAmbientSounds === 'function') { levelSelectElements.stopLevelSelectAmbientSounds(); } if (typeof levelSelectElements.restartParticleTimers === 'function') { levelSelectElements.restartParticleTimers(); } if (typeof levelSelectElements.updateMapDisplay === 'function') { levelSelectElements.updateMapDisplay(); } if (typeof levelSelectElements.startLevelSelectAmbientSounds === 'function') { levelSelectElements.startLevelSelectAmbientSounds(); } } } else if (previousScreen !== 'title') { if (levelSelectElements && typeof levelSelectElements.updateMapDisplay === 'function') { levelSelectElements.updateMapDisplay(); } if (levelSelectElements && typeof levelSelectElements.startLevelSelectAmbientSounds === 'function') { levelSelectElements.startLevelSelectAmbientSounds(); } } levelSelectScreen.visible = true; break; case 'fishing': fishingScreen.visible = true; playIntroAnimation(); break; case 'results': resultsScreen.visible = true; createResultsScreen(); resultsScreen.alpha = 0; tween(resultsScreen, { alpha: 1 }, { duration: 500, easing: tween.easeOut }); break; } if (screenName !== 'fishing' && GameState.tutorialMode) { tutorialOverlayContainer.visible = false; } } /**** * Intro Animation ****/ function playIntroAnimation() { GameState.introPlaying = true; GameState.gameActive = false; if (fishingElements) { if (typeof fishingElements.startWaterSurfaceAnimation === 'function') { fishingElements.startWaterSurfaceAnimation(); } if (typeof fishingElements.startBoatAndFishermanAnimation === 'function') { fishingElements.startBoatAndFishermanAnimation(); } } 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; var INITIAL_ZOOM_FACTOR = 1.5; var pivotX = fishingElements.boat.x; var pivotY = fishingElements.boat.y - fishingElements.boat.height * (fishingElements.boat.anchor.y - 0.5); fishingScreen.pivot.set(pivotX, pivotY); 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(fishingScreen.scale, { x: 1, y: 1 }, { duration: introDuration, easing: tween.easeInOut }); tween(fishingScreen, { x: pivotX, y: pivotY }, { duration: introDuration, easing: tween.easeInOut }); var targetHookY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; 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; fishingScreen.pivot.set(0, 0); fishingScreen.x = 0; fishingScreen.y = 0; if (GameState.tutorialMode) { GameState.gameActive = false; createTutorialElements(); runTutorialStep(); } else { startFishingSession(); } } }); } /**** * Level Select Logic ****/ function updateLevelSelectScreen() { var elements = levelSelectElements; elements.moneyDisplay.setText('$' + GameState.money); createDepthTabs(); updateSongDisplay(); updateShopButton(); } function createDepthTabs() { levelSelectElements.depthTabs.forEach(function (tab) { if (tab.container) { tab.container.destroy(); } }); levelSelectElements.depthTabs = []; var tabStartY = 600; var tabSpacing = 250; 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, y: tabStartY, tint: isSelected ? 0x1976d2 : 0x455a64, width: 400, height: 160 })); var tabText = new Text2(depth.name.split(' ')[0], { size: 40, 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); elements.songTitle.setText(song.name); elements.songInfo.setText('BPM: ' + song.bpm + ' | Duration: ' + formatTime(song.duration)); var minEarnings = Math.floor(depth.fishValue * 20); var maxEarnings = Math.floor(depth.fishValue * 60); elements.songEarnings.setText('Potential Earnings: $' + minEarnings + '-$' + maxEarnings); 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; } elements.leftArrow.tint = GameState.selectedSong > 0 ? 0x1976d2 : 0x666666; elements.rightArrow.tint = GameState.selectedSong < depth.songs.length - 1 ? 0x1976d2 : 0x666666; } function updateShopButton() { var elements = levelSelectElements; elements.shopButtonText.setText('UPGRADE ROD'); elements.shopButton.tint = 0x666666; } function formatTime(ms) { var seconds = Math.floor(ms / 1000); var minutes = Math.floor(seconds / 60); seconds = seconds % 60; return minutes + ':' + (seconds < 10 ? '0' : '') + seconds; } /**** * Fishing Game Logic ****/ function startFishingSession() { GameState.tutorialMode = false; 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; globalOceanBubblesArray = []; if (globalOceanBubbleContainer) { globalOceanBubbleContainer.removeChildren(); } globalOceanBubbleSpawnCounter = 0; globalSeaweedArray = []; if (globalSeaweedContainer) { globalSeaweedContainer.removeChildren(); } globalSeaweedSpawnCounter = 0; globalCloudArray = []; if (globalCloudContainer) { globalCloudContainer.removeChildren(); } globalCloudSpawnCounter = 0; fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; PatternGenerator.reset(); 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; var bracketAssetWidth = 75; 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 }); } } var songConfig = GameState.getCurrentSongConfig(); var musicIdToPlay = songConfig.musicId || 'rhythmTrack'; GameState.currentPlayingMusicId = musicIdToPlay; if (musicIdToPlay === 'morningtide') { GameState.currentPlayingMusicInitialVolume = 1.0; } else { GameState.currentPlayingMusicInitialVolume = 0.8; } LK.playMusic(GameState.currentPlayingMusicId); } function spawnFish(currentTimeForRegistration, options) { options = options || {}; var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var isFirstFishOfBeat = !options.laneIndexToUse && !options.forcedSpawnSide; if (isFirstFishOfBeat) { 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; } } } var laneIndex; if (options.laneIndexToUse !== undefined) { laneIndex = options.laneIndexToUse; PatternGenerator.lastLane = laneIndex; } 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; 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; newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; newFish.y = targetLane.y; newFish.baseY = targetLane.y; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(currentTimeForRegistration); return newFish; } function checkCatch(fishLane) { var hookX = fishingElements.hook.x; if (GameState.tutorialMode) { var tutorialFish = GameState.tutorialFish; if (!tutorialFish || tutorialFish.lane !== fishLane || tutorialFish.caught || tutorialFish.missed) { if (GameState.tutorialStep === 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) { caughtType = 'good'; } else { caughtType = 'miss'; } showFeedback(caughtType, fishLane); if (caughtType === 'perfect' || caughtType === 'good') { tutorialFish.catchFish(); var fishIndex = fishArray.indexOf(tutorialFish); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } LK.getSound('catch').play(); animateHookCatch(); GameState.tutorialPaused = true; if (GameState.tutorialStep === 3) { setTutorialText("Great catch! That's how you do it. Tap 'CONTINUE'."); } else if (GameState.tutorialStep === 4) { setTutorialText("Nice one! You're getting the hang of timing. Tap 'CONTINUE'."); } } else { 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."); } return; } var closestFishInLane = null; var closestDistance = Infinity; for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (!fish.caught && !fish.missed && fish.lane === fishLane) { var distance = Math.abs(fish.x - hookX); var maxCatchDistance = GAME_CONFIG.MISS_WINDOW * 2; if (distance < maxCatchDistance && distance < closestDistance) { closestDistance = distance; closestFishInLane = fish; } } } if (!closestFishInLane) { LK.getSound('miss').play(); if (laneBrackets && laneBrackets[fishLane]) { var leftBracket = laneBrackets[fishLane].left; var rightBracket = laneBrackets[fishLane].right; var tintToRedDuration = 50; var holdRedDuration = 100; var tintToWhiteDuration = 150; if (leftBracket && !leftBracket.destroyed) { tween(leftBracket, { tint: 0xFF0000 }, { duration: tintToRedDuration, easing: tween.linear, onFinish: function onFinish() { LK.setTimeout(function () { if (leftBracket && !leftBracket.destroyed) { tween(leftBracket, { tint: 0xFFFFFF }, { duration: tintToWhiteDuration, easing: tween.linear }); } }, holdRedDuration); } }); } if (rightBracket && !rightBracket.destroyed) { tween(rightBracket, { tint: 0xFF0000 }, { duration: tintToRedDuration, easing: tween.linear, onFinish: function onFinish() { LK.setTimeout(function () { if (rightBracket && !rightBracket.destroyed) { tween(rightBracket, { tint: 0xFFFFFF }, { duration: tintToWhiteDuration, easing: tween.linear }); } }, holdRedDuration); } }); } } GameState.combo = 0; return; } 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; if (closestFishInLane) { closestFishInLane.missed = true; } return; } 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); var catchSounds = ['catch', 'catch2', 'catch3', 'catch4']; var randomCatchSound = catchSounds[Math.floor(Math.random() * catchSounds.length)]; LK.getSound(randomCatchSound).play(); animateHookCatch(); if (points > 0) { var scorePopupText = new Text2('+' + points, { size: 140, fill: 0xFFD700, align: 'center', stroke: 0x000000, strokeThickness: 6 }); scorePopupText.anchor.set(0.5, 0.5); scorePopupText.x = GAME_CONFIG.SCREEN_CENTER_X; scorePopupText.y = GAME_CONFIG.BOAT_Y - 70; if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(scorePopupText); } tween(scorePopupText, { y: scorePopupText.y - 200, alpha: 0 }, { duration: 1800, easing: tween.easeOut, onFinish: function onFinish() { if (scorePopupText && !scorePopupText.destroyed) { scorePopupText.destroy(); } } }); } } 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); 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; stopTweens([fishingElements.boat, fishingElements.fishermanContainer, fishingElements.fisherman]); if (fishingElements && fishingElements.waterSurfaceSegments) { stopTweens(fishingElements.waterSurfaceSegments); } LK.stopMusic(); ImprovedRhythmSpawner.reset(); 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 = []; } fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; GameState.musicNotesActive = false; if (fishingElements && fishingElements.musicNotesContainer) { fishingElements.musicNotesContainer.removeChildren(); } musicNotesArray = []; cleanupParticleArray(globalOceanBubblesArray, globalOceanBubbleContainer); cleanupParticleArray(globalSeaweedArray, globalSeaweedContainer); cleanupParticleArray(globalCloudArray, globalCloudContainer); createResultsScreen(); showScreen('results'); } function createResultsScreen() { 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); 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); 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); 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 (GameState.tutorialMode && (currentScreen === 'fishing' || currentScreen === 'tutorial')) { 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 (GameState.tutorialPaused && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4)) { var advanceAfterCatch = false; if (GameState.tutorialFish && GameState.tutorialFish.caught) { advanceAfterCatch = true; } if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) { var idx = fishArray.indexOf(GameState.tutorialFish); if (idx > -1) { fishArray.splice(idx, 1); } GameState.tutorialFish.destroy(); } GameState.tutorialFish = null; if (advanceAfterCatch) { GameState.tutorialStep++; runTutorialStep(); } else { runTutorialStep(); } } else { GameState.tutorialStep++; runTutorialStep(); } } else if ((GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !GameState.tutorialPaused) { handleFishingInput(x, y, true); } return; } switch (currentScreen) { case 'title': 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'); } var tutorialButtonGfx = titleElements.tutorialButtonGfx || titleElements.tutorialButton; 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) { if (typeof startTutorial === "function") { startTutorial(); } } break; case 'levelSelect': handleLevelSelectInput(x, y); break; case 'fishing': handleFishingInput(x, y, true); break; case 'results': showScreen('levelSelect'); break; } }; function handleLevelSelectInput(x, y) { if (levelSelectElements && levelSelectElements.handleMapInput) { levelSelectElements.handleMapInput(x, y); } } /**** * Main Game Loop ****/ game.update = function () { if (GameState.currentScreen === 'fishing' && fishingElements && fishingElements.updateFishingLineWave) { fishingElements.updateFishingLineWave(); } if (GameState.currentScreen === 'title') { if (titleElements && titleElements.updateTitleFishingLineWave) { titleElements.updateTitleFishingLineWave(); } // Title Screen Ambient Particles using consolidated functions if (titleScreenOceanBubbleContainer) { handleParticleSpawning({ counter: titleScreenOceanBubbleSpawnCounter, interval: OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS, array: titleScreenOceanBubblesArray, container: titleScreenOceanBubbleContainer, constructor: OceanBubbleParticle }); titleScreenOceanBubbleSpawnCounter++; updateParticleArray(titleScreenOceanBubblesArray); } if (titleScreenSeaweedContainer) { handleParticleSpawning({ counter: titleScreenSeaweedSpawnCounter, interval: SEAWEED_SPAWN_INTERVAL_TICKS, maxCount: MAX_SEAWEED_COUNT, array: titleScreenSeaweedArray, container: titleScreenSeaweedContainer, constructor: SeaweedParticle }); titleScreenSeaweedSpawnCounter++; updateParticleArray(titleScreenSeaweedArray); } if (titleScreenCloudContainer) { handleParticleSpawning({ counter: titleScreenCloudSpawnCounter, interval: CLOUD_SPAWN_INTERVAL_TICKS, maxCount: MAX_CLOUD_COUNT, array: titleScreenCloudArray, container: titleScreenCloudContainer, constructor: CloudParticle }); titleScreenCloudSpawnCounter++; updateParticleArray(titleScreenCloudArray); } } if (GameState.currentScreen === 'levelSelect' && levelSelectElements) { if (levelSelectElements.updateBoatAnimation && typeof levelSelectElements.updateBoatAnimation === 'function') { levelSelectElements.updateBoatAnimation(); } if (levelSelectElements.updateRipples && typeof levelSelectElements.updateRipples === 'function') { levelSelectElements.updateRipples(); } if (levelSelectElements.updateHomeIslandRipples && typeof levelSelectElements.updateHomeIslandRipples === 'function') { levelSelectElements.updateHomeIslandRipples(); } if (levelSelectElements.updateScreenWaves && typeof levelSelectElements.updateScreenWaves === 'function') { levelSelectElements.updateScreenWaves(); } if (levelSelectElements.updateShallowWatersNodeBubbles && typeof levelSelectElements.updateShallowWatersNodeBubbles === 'function') { levelSelectElements.updateShallowWatersNodeBubbles(); } if (levelSelectElements.updateSeagulls && typeof levelSelectElements.updateSeagulls === 'function') { levelSelectElements.updateSeagulls(); } if (levelSelectElements.updateLevelSelectClouds && typeof levelSelectElements.updateLevelSelectClouds === 'function') { levelSelectElements.updateLevelSelectClouds(); } if (levelSelectElements.updateWaterfallParticles && typeof levelSelectElements.updateWaterfallParticles === 'function') { levelSelectElements.updateWaterfallParticles(); } if (levelSelectElements.updateShadowFish && typeof levelSelectElements.updateShadowFish === 'function') { levelSelectElements.updateShadowFish(); } } // Spawn and update ambient particles during fishing screen if (GameState.currentScreen === 'fishing' && globalOceanBubbleContainer) { handleParticleSpawning({ counter: globalOceanBubbleSpawnCounter, interval: OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS, array: globalOceanBubblesArray, container: globalOceanBubbleContainer, constructor: OceanBubbleParticle }); globalOceanBubbleSpawnCounter++; // Update existing ocean bubbles with fish physics 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; var minDistance = 30; if (distance < influenceRadius && distance > minDistance) { var influence = 1 - distance / influenceRadius; influence = influence * influence; var dirX = dx / distance; var dirY = dy / distance; var fishSpeedFactor = Math.abs(fish.speed) * 0.15; var pushForce = fishSpeedFactor * influence; oceanBubble.x += dirX * pushForce * 2; oceanBubble.vy += dirY * pushForce * 0.5; oceanBubble.driftAmplitude = Math.min(80, oceanBubble.driftAmplitude + pushForce * 10); oceanBubble.driftFrequency *= 1 + influence * 0.1; } } } oceanBubble.update(); if (oceanBubble.isDone) { oceanBubble.destroy(); globalOceanBubblesArray.splice(obIdx, 1); } } else { globalOceanBubblesArray.splice(obIdx, 1); } } } // Spawn and update seaweed particles during fishing if (GameState.currentScreen === 'fishing' && globalSeaweedContainer) { handleParticleSpawning({ counter: globalSeaweedSpawnCounter, interval: SEAWEED_SPAWN_INTERVAL_TICKS, maxCount: MAX_SEAWEED_COUNT, array: globalSeaweedArray, container: globalSeaweedContainer, constructor: SeaweedParticle }); globalSeaweedSpawnCounter++; // Update existing seaweed with fish physics 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; var minDistance = 40; if (distance < influenceRadius && distance > minDistance) { var influence = 1 - distance / influenceRadius; influence = influence * influence; var dirX = dx / distance; var dirY = dy / distance; var fishSpeedFactor = Math.abs(fish.speed) * 0.2; var pushForce = fishSpeedFactor * influence; seaweed.vx += dirX * pushForce * 1.5; seaweed.vy += dirY * pushForce * 0.8; 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 fishing if (GameState.currentScreen === 'fishing' && globalCloudContainer) { handleParticleSpawning({ counter: globalCloudSpawnCounter, interval: CLOUD_SPAWN_INTERVAL_TICKS, maxCount: MAX_CLOUD_COUNT, array: globalCloudArray, container: globalCloudContainer, constructor: CloudParticle }); globalCloudSpawnCounter++; updateParticleArray(globalCloudArray); } // Tutorial mode logic if (GameState.currentScreen === 'fishing' && GameState.tutorialMode) { if (fishingElements && fishingElements.updateFishingLineWave) { fishingElements.updateFishingLineWave(); } if (!GameState.tutorialPaused) { if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) { GameState.tutorialFish.update(); checkTutorialFishState(); } // 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) { tween(fishingElements.hook, { y: targetLaneY }, { duration: 100, easing: tween.linear }); fishingElements.hook.originalY = targetLaneY; } } } updateLaneBracketsVisuals(); return; } if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } 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 if (Math.abs(fishingElements.hook.y - targetLaneY) > 5) { tween(fishingElements.hook, { y: targetLaneY }, { duration: 150, easing: tween.easeOut }); fishingElements.hook.originalY = targetLaneY; } updateLaneBracketsVisuals(); // Update fish for (var i = fishArray.length - 1; i >= 0; i--) { var fish = fishArray[i]; var previousFrameX = fish.lastX; fish.update(); var currentFrameX = fish.x; // Check for miss only if fish is active if (!fish.caught && !fish.missed) { var hookCenterX = fishingElements.hook.x; var missCheckBoundary = GAME_CONFIG.MISS_WINDOW; if (fish.speed > 0) { if (previousFrameX <= hookCenterX + missCheckBoundary && currentFrameX > hookCenterX + missCheckBoundary) { showFeedback('miss', fish.lane); LK.getSound('miss').play(); GameState.combo = 0; fish.missed = true; } } else if (fish.speed < 0) { if (previousFrameX >= hookCenterX - missCheckBoundary && currentFrameX < hookCenterX - missCheckBoundary) { showFeedback('miss', fish.lane); LK.getSound('miss').play(); GameState.combo = 0; fish.missed = true; } } } fish.lastX = currentFrameX; // Remove off-screen fish if (!fish.caught && (fish.x < -250 || fish.x > 2048 + 250)) { 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; var spawnX = fishingElements.hook.x; var spawnY = fishingElements.hook.y - 30; var newNote = new MusicNoteParticle(spawnX, spawnY); musicNotesContainer.addChild(newNote); musicNotesArray.push(newNote); // Add scale pulse to the hook if (fishingElements.hook && !fishingElements.hook.destroyed && fishingElements.hook.scale) { var currentSongConfig = GameState.getCurrentSongConfig(); var bpm = currentSongConfig && currentSongConfig.bpm ? currentSongConfig.bpm : 90; var beatDurationMs = 60000 / bpm; var pulsePhaseDuration = Math.max(50, beatDurationMs / 2); var pulseScaleFactor = 1.2; var originalScaleX = fishingElements.hook.scale.x !== undefined ? fishingElements.hook.scale.x : 1; var originalScaleY = fishingElements.hook.scale.y !== undefined ? fishingElements.hook.scale.y : 1; stopTween(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 }); } } }); } } } // Update existing music notes updateParticleArray(musicNotesArray); // 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; var tailOffsetDirection = Math.sign(fish.speed) * -1; var bubbleX = fish.x + tailOffsetDirection * (fish.fishGraphics.width * Math.abs(fish.fishGraphics.scaleX) / 2) * 0.8; var bubbleY = fish.y + (Math.random() - 0.5) * (fish.fishGraphics.height * Math.abs(fish.fishGraphics.scaleY) / 4); var newBubble = new BubbleParticle(bubbleX, bubbleY); bubbleContainer.addChild(newBubble); bubblesArray.push(newBubble); } } } } // Update and remove bubbles updateParticleArray(bubblesArray); }; /**** * Initialize game ****/ showScreen('title');
===================================================================
--- original.js
+++ change.js
@@ -11,34 +11,29 @@
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
+ scaleY: this.scaleX
});
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.vx = (Math.random() - 0.5) * 0.4;
+ self.vy = -(0.4 + Math.random() * 0.3);
+ self.life = 120 + Math.random() * 60;
self.age = 0;
self.isDone = false;
var initialAlpha = self.gfx.alpha;
- var initialScale = self.gfx.scaleX; // Assuming scaleX and scaleY start the same
+ var initialScale = self.gfx.scaleX;
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
+ var scaleFactor = 1 - self.age / self.life;
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;
@@ -51,91 +46,71 @@
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.x = 200 + Math.random() * 1648;
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
+ self.x = -100;
+ self.vx = 0.1 + Math.random() * 0.2;
} else {
- self.x = 2048 + 100; // Start off-screen right
- self.vx = -(0.1 + Math.random() * 0.2); // Slower drift left
+ self.x = 2048 + 100;
+ self.vx = -(0.1 + Math.random() * 0.2);
}
}
- // 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
+ var skyTop = -500;
+ var skyBottom = GAME_CONFIG.WATER_SURFACE_Y - 100;
self.y = skyTop + Math.random() * (skyBottom - skyTop);
- // Cloud properties
- var baseScale = 0.8 + Math.random() * 0.6; // Scale: 0.8x to 1.4x
+ var baseScale = 0.8 + Math.random() * 0.6;
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.vy = (Math.random() - 0.5) * 0.02;
+ var targetAlpha = 0.4 + Math.random() * 0.3;
+ self.gfx.alpha = 0;
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.hasFadedIn = false;
+ var fadeInStartX = 400;
+ var fadeInEndX = 800;
+ var fadeOutStartX = 1248;
+ var fadeOutEndX = 1648;
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;
}
@@ -163,64 +138,44 @@
});
};
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 sardine assets if type is shallow
if (type === 'shallow') {
var currentSongConfig = null;
- // Ensure GameState and its methods are available before calling
if (typeof GameState !== 'undefined' && GameState && typeof GameState.getCurrentSongConfig === 'function') {
currentSongConfig = GameState.getCurrentSongConfig();
}
- // Check if the current song is "Gentle Waves"
- // This song is part of the "Shallow Waters" depth.
if (currentSongConfig && currentSongConfig.name === "Gentle Waves") {
- // Apply specific percentages for Gentle Waves
var randGentle = Math.random();
if (randGentle < 0.60) {
- // 60% anchovy
assetName = 'anchovy';
} else if (randGentle < 0.90) {
- // 30% sardine (0.60 + 0.30 = 0.90)
assetName = 'sardine';
} else {
- // 10% mackerel
assetName = 'mackerel';
}
} else if (currentSongConfig && currentSongConfig.name === "Morning Tide") {
- // Apply specific percentages for Morning Tide
var randMorning = Math.random();
if (randMorning < 0.40) {
- // 40% anchovy
assetName = 'anchovy';
} else if (randMorning < 0.80) {
- // 40% sardine (0.40 + 0.40 = 0.80)
assetName = 'sardine';
} else {
- // 20% mackerel
assetName = 'mackerel';
}
} else if (currentSongConfig && currentSongConfig.name === "Sunny Afternoon") {
- // Apply specific percentages for Sunny Afternoon
var randSunny = Math.random();
if (randSunny < 0.30) {
- // 30% anchovy
assetName = 'anchovy';
} else if (randSunny < 0.60) {
- // 30% sardine (0.30 + 0.30 = 0.60)
assetName = 'sardine';
} else {
- // 40% mackerel
assetName = 'mackerel';
}
} else {
- // Original random selection for other shallow fish scenarios or if song info is unavailable
var shallowFishAssets = ['sardine', 'anchovy', 'mackerel'];
assetName = shallowFishAssets[Math.floor(Math.random() * shallowFishAssets.length)];
}
}
@@ -228,102 +183,81 @@
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.lane = lane;
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.missed = false;
+ self.lastX = 0;
+ self.isSpecial = type === 'rare';
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.bubbleSpawnInterval = 120 + Math.random() * 80;
+ self.swimTime = Math.random() * Math.PI * 2;
+ self.baseY = self.y;
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.swimTime += 0.08;
+ var swimAmplitude = 15;
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 scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15;
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 boatLandingY = GAME_CONFIG.BOAT_Y;
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.
+ var durationPhase1 = 350;
+ var durationPhase2 = 250;
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
+ alpha: 0.8
}, {
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
+ alpha: 0
}, {
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
+ self.destroy();
}
});
}
});
@@ -331,137 +265,117 @@
return self;
});
var MapBubbleParticle = Container.expand(function (startX, startY) {
var self = Container.call(this);
- var initialScale = 0.1 + Math.random() * 0.1; // Bubbles are 0.1x to 0.2x of original asset size
- var targetScale = 0.4 + Math.random() * 0.3; // Max scale during life: 0.4x to 0.7x
+ var initialScale = 0.1 + Math.random() * 0.1;
+ var targetScale = 0.4 + Math.random() * 0.3;
self.gfx = self.attachAsset('bubbles', {
- // Using 'bubbles' asset
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
- // Start transparent
scaleX: initialScale,
scaleY: initialScale
});
- self.x = startX + (Math.random() - 0.5) * 100; // Spread horizontally around startX by +/- 50px
- self.y = startY; // Start at the provided Y (below the shallow node)
- var targetAlpha = 0.3 + Math.random() * 0.3; // Max alpha during life: 0.3 to 0.6
- var riseDurationMs = 3000 + Math.random() * 2000; // 3 to 5 seconds to rise
- var riseDistance = 300 + Math.random() * 200; // How far up they go (e.g., 300-500 pixels)
- var driftDistance = (Math.random() - 0.5) * 100; // Horizontal drift over lifetime +/- 50px
- var fadeInDurationMs = 600 + Math.random() * 400; // 0.6 to 1 second to fade in
- var totalDurationMs = riseDurationMs; // Base lifetime on how long they take to rise
- var fadeOutStartTimeMs = totalDurationMs * 0.6; // Start fading out in the last 40% of life
- var fadeOutDurationMs = totalDurationMs - fadeOutStartTimeMs; // Duration of the fade out
+ self.x = startX + (Math.random() - 0.5) * 100;
+ self.y = startY;
+ var targetAlpha = 0.3 + Math.random() * 0.3;
+ var riseDurationMs = 3000 + Math.random() * 2000;
+ var riseDistance = 300 + Math.random() * 200;
+ var driftDistance = (Math.random() - 0.5) * 100;
+ var fadeInDurationMs = 600 + Math.random() * 400;
+ var totalDurationMs = riseDurationMs;
+ var fadeOutStartTimeMs = totalDurationMs * 0.6;
+ var fadeOutDurationMs = totalDurationMs - fadeOutStartTimeMs;
self.isDone = false;
- // Phase 1: Fade in and scale up to targetScale
tween(self.gfx, {
alpha: targetAlpha,
scaleX: targetScale,
scaleY: targetScale
}, {
duration: fadeInDurationMs,
easing: tween.easeOut
});
- // Phase 2: Move upwards and drift horizontally
tween(self, {
y: self.y - riseDistance,
x: self.x + driftDistance
}, {
duration: totalDurationMs,
- easing: tween.linear // Constant rise and drift
- // onFinish is not strictly needed here as fadeOut will mark as done
+ easing: tween.linear
});
- // Phase 3: Schedule fade out and shrink towards the end of life
LK.setTimeout(function () {
if (self.isDone || !self.gfx || self.gfx.destroyed) {
- // Check if already done or destroyed
return;
}
tween(self.gfx, {
alpha: 0,
scaleX: initialScale * 0.5,
scaleY: initialScale * 0.5
}, {
- // Shrink smaller than start
duration: fadeOutDurationMs,
easing: tween.easeIn,
onFinish: function onFinish() {
- self.isDone = true; // Mark as done for cleanup
+ self.isDone = true;
}
});
}, fadeOutStartTimeMs);
- // This particle is fully driven by tweens, so it doesn't need an update method.
return self;
});
var MapScreenCloudParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('mapCloud', {
- // Using 'mapCloud' asset
anchorX: 0.5,
anchorY: 0.5
});
- // Visual properties
- self.gfx.alpha = 0.4 + Math.random() * 0.3; // Alpha: 0.4 to 0.7
- var baseScale = 0.9 + Math.random() * 0.4; // Scale: 0.9x to 1.3x for variety
+ self.gfx.alpha = 0.4 + Math.random() * 0.3;
+ var baseScale = 0.9 + Math.random() * 0.4;
self.gfx.scale.set(baseScale);
- // Movement properties
var spawnFromLeft = Math.random() < 0.5;
- var offscreenBuffer = self.gfx.width * baseScale + 50; // Buffer to ensure fully offscreen
- var speedMultiplier = 1.15; // Increase speed by 15%
+ var offscreenBuffer = self.gfx.width * baseScale + 50;
+ var speedMultiplier = 1.15;
if (spawnFromLeft) {
self.x = -offscreenBuffer;
- self.vx = (0.15 + Math.random() * 0.2) * speedMultiplier; // Slow drift right: 0.15 to 0.35 pixels/frame, now 15% faster
+ self.vx = (0.15 + Math.random() * 0.2) * speedMultiplier;
} else {
self.x = 2048 + offscreenBuffer;
- self.vx = -(0.15 + Math.random() * 0.2) * speedMultiplier; // Slow drift left, now 15% faster
+ self.vx = -(0.15 + Math.random() * 0.2) * speedMultiplier;
}
- // Y position: In the upper part of the screen (sky area)
- // Spawn clouds anywhere along the Y axis in the middle 80% of the screen.
var screenHeight = 2732;
- var topMargin = screenHeight * 0.1; // Top 10%
- var spawnableHeight = screenHeight * 0.8; // Middle 80%
+ var topMargin = screenHeight * 0.1;
+ var spawnableHeight = screenHeight * 0.8;
self.y = topMargin + Math.random() * spawnableHeight;
self.isDone = false;
self.update = function () {
if (self.isDone) {
return;
}
self.x += self.vx;
- // Check if completely off-screen
if (self.vx > 0 && self.x > 2048 + offscreenBuffer) {
- // Moving right, went off right edge
self.isDone = true;
} else if (self.vx < 0 && self.x < -offscreenBuffer) {
- // Moving left, went off left edge
self.isDone = true;
}
};
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
+ var FADE_IN_DURATION_MS = 600;
+ var TARGET_ALPHA = 0.6 + Math.random() * 0.4;
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)
+ scaleX: 0.4 + Math.random() * 0.4
});
- self.gfx.scaleY = self.gfx.scaleX; // Maintain aspect ratio
+ self.gfx.scaleY = self.gfx.scaleX;
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.vx = (Math.random() - 0.5) * 0.8;
+ self.vy = -(0.8 + Math.random() * 0.7);
+ self.rotationSpeed = (Math.random() - 0.5) * 0.008;
+ self.life = 240 + Math.random() * 120;
self.age = 0;
self.isDone = false;
- // Initial fade-in tween
tween(self.gfx, {
alpha: TARGET_ALPHA
}, {
duration: FADE_IN_DURATION_MS,
@@ -474,26 +388,18 @@
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.
+ var FADE_IN_TICKS = FADE_IN_DURATION_MS / (1000 / 60);
if (self.age > FADE_IN_TICKS) {
- var lifePortionForFadeOut = 0.6; // Use last 60% of life for fade out
+ var lifePortionForFadeOut = 0.6;
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.
+ self.gfx.alpha = Math.max(0, self.gfx.alpha);
}
}
- // 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;
}
};
@@ -508,46 +414,41 @@
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
+ var baseScale = 0.1 + Math.random() * 0.4;
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.vy = -(0.25 + Math.random() * 0.5);
+ self.naturalVy = self.vy;
+ self.driftAmplitude = 20 + Math.random() * 40;
+ self.naturalDriftAmplitude = self.driftAmplitude;
+ self.driftFrequency = (0.005 + Math.random() * 0.015) * (Math.random() < 0.5 ? 1 : -1);
+ self.driftPhase = Math.random() * Math.PI * 2;
+ self.rotationSpeed = (Math.random() - 0.5) * 0.01;
+ var targetAlpha = 0.2 + Math.random() * 0.3;
+ self.gfx.alpha = 0;
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;
}
@@ -555,50 +456,41 @@
}
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
+ var naturalVy = -(0.25 + Math.random() * 0.5);
+ var recoveryRate = 0.02;
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.stop(self.gfx);
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
+ self.isDone = true;
}
});
} 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
+ self.gfx.alpha = 0;
}
};
return self;
});
var RippleParticle = Container.expand(function (spawnCenterX, spawnCenterY, spawnAngle, initialOffset, travelDistance, initialScale, finalScale, durationMs, speedFactor) {
var self = Container.call(this);
- // Added speedFactor
self.isDone = false;
- // Calculate effective duration based on speedFactor
var effectiveDurationMs = durationMs;
if (speedFactor !== undefined && speedFactor > 0 && speedFactor !== 1.0) {
effectiveDurationMs = durationMs / speedFactor;
}
@@ -607,25 +499,21 @@
anchorY: 0.5,
scaleX: initialScale,
scaleY: initialScale,
alpha: 1.0,
- rotation: spawnAngle + Math.PI / 2 // Orient concave side towards spawnCenter
+ rotation: spawnAngle + Math.PI / 2
});
- // Initial position
self.x = spawnCenterX + initialOffset * Math.cos(spawnAngle);
self.y = spawnCenterY + initialOffset * Math.sin(spawnAngle);
- // Target position
var targetX = spawnCenterX + (initialOffset + travelDistance) * Math.cos(spawnAngle);
var targetY = spawnCenterY + (initialOffset + travelDistance) * Math.sin(spawnAngle);
tween(self, {
x: targetX,
y: targetY,
alpha: 0
}, {
duration: effectiveDurationMs,
- // Use effectiveDurationMs
easing: tween.linear,
- // Position and alpha fade linearly
onFinish: function onFinish() {
self.isDone = true;
}
});
@@ -633,35 +521,28 @@
x: finalScale,
y: finalScale
}, {
duration: effectiveDurationMs,
- // Use effectiveDurationMs
- easing: tween.easeOut // Scale eases out
+ easing: tween.easeOut
});
- // LK engine calls .update if it exists. Since tweens handle everything,
- // an explicit update function is not strictly necessary for this particle's animation.
- // However, if we wanted to add more complex logic not handled by tweens,
- // we could add self.update = function() { ... }; here.
return self;
});
var SeagullParticle = Container.expand(function () {
var self = Container.call(this);
self.isDone = false;
var flyFromLeft = Math.random() < 0.5;
- var assetBaseScale = 0.6 + Math.random() * 0.4; // Initial visual scale 0.6x to 1.0x
+ var assetBaseScale = 0.6 + Math.random() * 0.4;
self.gfx = self.attachAsset('seagull', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: flyFromLeft ? -assetBaseScale : assetBaseScale,
- // Flip asset if flying left to right (assumes base asset faces left)
- // Flip asset if flying right to left
scaleY: assetBaseScale
});
- var flightDuration = 7000 + Math.random() * 5000; // 7 to 12 seconds flight time
+ var flightDuration = 7000 + Math.random() * 5000;
var startX, endX;
- var startY = 250 + Math.random() * 750; // Spawn vertically between y=150 and y=900
- var endY = 250 + Math.random() * 750; // End vertically between y=150 and y=900
- var offscreenBuffer = self.gfx.width * assetBaseScale + 50; // Buffer to ensure fully offscreen
+ var startY = 250 + Math.random() * 750;
+ var endY = 250 + Math.random() * 750;
+ var offscreenBuffer = self.gfx.width * assetBaseScale + 50;
if (flyFromLeft) {
startX = -offscreenBuffer;
endX = 2048 + offscreenBuffer;
} else {
@@ -671,54 +552,50 @@
self.x = startX;
self.y = startY;
self.lastX = startX;
self.lastY = startY;
- var arcHeight = 150 + Math.random() * 250; // How high or low the arc goes
- var arcUpwards = Math.random() < 0.5; // 50% chance to arc upwards
+ var arcHeight = 150 + Math.random() * 250;
+ var arcUpwards = Math.random() < 0.5;
var peakY;
if (arcUpwards) {
- peakY = Math.min(startY, endY) - arcHeight; // Calculate peak Y for an upward arc
+ peakY = Math.min(startY, endY) - arcHeight;
} else {
- peakY = Math.max(startY, endY) + arcHeight; // Calculate peak Y for a downward arc
+ peakY = Math.max(startY, endY) + arcHeight;
}
- // Scale shift animation
- var targetScaleMagnitude = assetBaseScale + (Math.random() - 0.5) * 0.5; // e.g. initial 0.8, can go 0.55 to 1.05
- targetScaleMagnitude = Math.max(0.4, Math.min(1.2, targetScaleMagnitude)); // Clamp scale (0.4x to 1.2x)
- var finalScaleX = flyFromLeft ? -targetScaleMagnitude : targetScaleMagnitude; // Maintains orientation for left-facing asset during scaling
+ var targetScaleMagnitude = assetBaseScale + (Math.random() - 0.5) * 0.5;
+ targetScaleMagnitude = Math.max(0.4, Math.min(1.2, targetScaleMagnitude));
+ var finalScaleX = flyFromLeft ? -targetScaleMagnitude : targetScaleMagnitude;
var finalScaleY = targetScaleMagnitude;
tween(self.gfx.scale, {
x: finalScaleX,
y: finalScaleY
}, {
duration: flightDuration,
- easing: tween.linear // Smooth scale change over the flight
+ easing: tween.linear
});
- // Horizontal movement
tween(self, {
x: endX
}, {
duration: flightDuration,
easing: tween.linear,
onFinish: function onFinish() {
- self.isDone = true; // Mark for cleanup when horizontal movement is done
+ self.isDone = true;
}
});
- // Vertical arcing movement (two parts)
tween(self, {
y: peakY
}, {
duration: flightDuration / 2,
easing: tween.easeOut,
- // Ease out to the peak
onFinish: function onFinish() {
if (self.isDone || !self.gfx || self.gfx.destroyed) {
return;
- } // Check if already marked done or destroyed
+ }
tween(self, {
y: endY
}, {
duration: flightDuration / 2,
- easing: tween.easeIn // Ease in from the peak to the end
+ easing: tween.easeIn
});
}
});
self.update = function () {
@@ -728,17 +605,11 @@
var dx = self.x - self.lastX;
var dy = self.y - self.lastY;
if (dx !== 0 || dy !== 0) {
var angle = Math.atan2(dy, dx);
- // If gfx.scale.x is positive, the seagull asset is visually facing left (effective angle PI).
- // We want its rotation to be angle - PI.
- // If gfx.scale.x is negative, the seagull asset is visually facing right (effective angle 0).
- // We want its rotation to be angle.
if (self.gfx.scale.x > 0) {
- // Visually facing left
self.gfx.rotation = angle - Math.PI;
} else {
- // Visually facing right
self.gfx.rotation = angle;
}
}
self.lastX = self.x;
@@ -751,51 +622,43 @@
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
+ self.vx = (Math.random() - 0.5) * 0.3;
+ self.vy = -(0.4 + Math.random() * 0.3);
} 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
+ self.vx = 0.4 + Math.random() * 0.3;
+ self.vy = -(0.1 + Math.random() * 0.2);
} 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.vx = -(0.4 + Math.random() * 0.3);
+ self.vy = -(0.1 + Math.random() * 0.2);
}
self.initialX = self.x;
- self.naturalVx = self.vx; // Store natural velocity for recovery
+ self.naturalVx = self.vx;
self.naturalVy = self.vy;
- // Seaweed properties
- var baseScale = 0.6 + Math.random() * 0.6; // Scale: 0.6x to 1.2x
+ var baseScale = 0.6 + Math.random() * 0.6;
self.gfx.scale.set(baseScale);
- self.swayAmplitude = 15 + Math.random() * 25; // Sway: 15px to 40px
+ self.swayAmplitude = 15 + Math.random() * 25;
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.continuousRotationSpeed = (Math.random() - 0.5) * 0.003;
+ var targetAlpha = 0.3 + Math.random() * 0.3;
+ self.gfx.alpha = 0;
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.lifespan = 600 + Math.random() * 1200;
self.age = 0;
tween(self.gfx, {
alpha: targetAlpha
}, {
@@ -805,36 +668,29 @@
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
+ self.gfx.rotation += self.continuousRotationSpeed + swayOffset * 0.0001;
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, {
@@ -857,47 +713,39 @@
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
scaleX: 0.8,
- // Slightly smaller than original asset
scaleY: 0.8
});
- var startSide = Math.random() < 0.5 ? -1 : 1; // -1 for left, 1 for right
- var swimDistance = 300 + Math.random() * 150; // How far it swims across relative to nodeX
- var verticalOffsetBase = (Math.random() - 0.5) * 50; // Base vertical variation around nodeY
- var verticalSwimAmplitude = 20 + Math.random() * 30; // Amplitude of vertical sine wave during swim
- // Initial position: starts further off-screen and swims into view near the node
- self.x = nodeX + startSide * (swimDistance + 150); // Start further out
+ var startSide = Math.random() < 0.5 ? -1 : 1;
+ var swimDistance = 300 + Math.random() * 150;
+ var verticalOffsetBase = (Math.random() - 0.5) * 50;
+ var verticalSwimAmplitude = 20 + Math.random() * 30;
+ self.x = nodeX + startSide * (swimDistance + 150);
self.y = nodeY + verticalOffsetBase;
- // Initialize last positions for rotation calculation
self.lastX = self.x;
self.lastY = self.y;
- var targetX = nodeX - startSide * (swimDistance + 150); // Swim to the other side, further out
+ var targetX = nodeX - startSide * (swimDistance + 150);
if (startSide > 0) {
- // Starts on right, swims left, so flip asset
fishAsset.scale.x *= -1;
}
var fadeInDuration = 2000 + Math.random() * 1000;
- var swimDuration = 7000 + Math.random() * 4000; // Longer, slower swim
- var fadeOutDuration = 700 + Math.random() * 300; // Reduced fade out time
- var visibleAlpha = 0.5 + Math.random() * 0.2; // Max alpha during visibility
- // Phase 1: Fade In
+ var swimDuration = 7000 + Math.random() * 4000;
+ var fadeOutDuration = 700 + Math.random() * 300;
+ var visibleAlpha = 0.5 + Math.random() * 0.2;
tween(fishAsset, {
alpha: visibleAlpha
}, {
duration: fadeInDuration,
easing: tween.easeOut
});
- // Phase 2: Swim Horizontally
tween(self, {
x: targetX
}, {
duration: swimDuration,
easing: tween.linear,
delay: fadeInDuration * 0.3,
- // Start swimming a bit after fade-in begins
onFinish: function onFinish() {
- // Phase 3: Fade Out (starts when swim finishes)
tween(fishAsset, {
alpha: 0
}, {
duration: fadeOutDuration,
@@ -907,160 +755,124 @@
}
});
}
});
- // Vertical sine wave movement for self.y during swim
var swimStartTime = LK.ticks;
self.update = function () {
if (self.isDone || !fishAsset || fishAsset.destroyed) {
return;
}
- // Sine wave vertical movement (self.x is updated by its tween)
- // Only apply sine wave after fade-in is somewhat complete and before fade-out starts
var currentTicks = LK.ticks;
var timeSinceFadeInStart = (currentTicks - swimStartTime) * (1000 / 60);
if (timeSinceFadeInStart > fadeInDuration * 0.5 && timeSinceFadeInStart < fadeInDuration * 0.3 + swimDuration - fadeOutDuration * 0.5) {
var swimProgress = (timeSinceFadeInStart - fadeInDuration * 0.3) / (swimDuration - fadeInDuration * 0.3);
- self.y = nodeY + verticalOffsetBase + Math.sin(swimProgress * Math.PI * 2) * verticalSwimAmplitude; // Full sine wave cycle over swim duration
+ self.y = nodeY + verticalOffsetBase + Math.sin(swimProgress * Math.PI * 2) * verticalSwimAmplitude;
}
- // Rotation logic: make the fish face the direction it's moving.
var dx = self.x - self.lastX;
var dy = self.y - self.lastY;
if (dx !== 0 || dy !== 0) {
- // Only rotate if there was movement
var angle = Math.atan2(dy, dx);
- // Assuming the base 'shadowfish' asset faces right (positive X).
- // If fishAsset.scale.x is positive, it's visually facing right.
- // If fishAsset.scale.x is negative, it's flipped and visually facing left.
if (fishAsset.scale.x > 0) {
fishAsset.rotation = angle;
} else {
- // Compensate for the flip caused by negative scale.x
fishAsset.rotation = angle - Math.PI;
}
}
- // Update last positions for the next frame's calculation.
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
var WaterfallParticle = Container.expand(function (spawnX, spawnY) {
var self = Container.call(this);
self.gfx = self.attachAsset('bubbles', {
- // Using 'bubbles' asset
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.7 + Math.random() * 0.3,
- // Start with a good alpha
- scaleX: 0.4 + Math.random() * 0.3 // Random initial scale (reduced size)
+ scaleX: 0.4 + Math.random() * 0.3
});
- self.gfx.scaleY = self.gfx.scaleX; // Maintain aspect ratio
- self.x = spawnX + (Math.random() - 0.5) * 50; // Increased horizontal spread at spawn
+ self.gfx.scaleY = self.gfx.scaleX;
+ self.x = spawnX + (Math.random() - 0.5) * 50;
self.y = spawnY;
self.isDone = false;
var fallDistance = 200;
- var sprayHeight = 50 + Math.random() * 50; // How high it bounces
- var spraySpreadX = (Math.random() - 0.5) * 150; // Increased horizontal spread during spray for a wider angle
- var fallDuration = 800 + Math.random() * 400; // 0.8s to 1.2s to fall
- var sprayDuration = 700 + Math.random() * 500; // 0.7s to 1.2s for spray/fade
- // Phase 1: Fall
+ var sprayHeight = 50 + Math.random() * 50;
+ var spraySpreadX = (Math.random() - 0.5) * 150;
+ var fallDuration = 800 + Math.random() * 400;
+ var sprayDuration = 700 + Math.random() * 500;
tween(self, {
y: self.y + fallDistance
}, {
duration: fallDuration,
easing: tween.easeInSine,
- // Gentle acceleration downwards
onFinish: function onFinish() {
- // Phase 2: Bounce (Spray) and Fade
- // Animate position for the bounce
tween(self, {
y: self.y - sprayHeight,
- // Move up
- x: self.x + spraySpreadX // Spread out
+ x: self.x + spraySpreadX
}, {
duration: sprayDuration,
easing: tween.easeOutSine,
- // Gentle deceleration upwards
onFinish: function onFinish() {
- self.isDone = true; // Mark for cleanup after bounce animation completes
+ self.isDone = true;
}
});
- // Animate visual properties (fade and shrink) during the bounce
tween(self.gfx, {
alpha: 0,
scaleX: self.gfx.scaleX * 0.4,
- // Shrink to 40% of its current size
scaleY: self.gfx.scaleY * 0.4
}, {
duration: sprayDuration,
- // Match position animation duration
- easing: tween.easeOutSine // Corresponds with bounce easing
+ easing: tween.easeOutSine
});
}
});
return self;
});
var WaveParticle = Container.expand(function (movesRight) {
var self = Container.call(this);
- // Added movesRight parameter
self.isDone = false;
- // Define FINAL_SCALE_TARGET early for initial setup
- var FINAL_SCALE_TARGET = 1.2 + Math.random() * 0.6; // Constant scale between 1.2x to 1.8x
- // Determine asset scale based on direction. Asset 'wave' is flipX:1 (points left by default)
+ var FINAL_SCALE_TARGET = 1.2 + Math.random() * 0.6;
var assetScaleX = movesRight ? -FINAL_SCALE_TARGET : FINAL_SCALE_TARGET;
var waveGfx = self.attachAsset('wave', {
- // 'wave' asset is 100x60.16
anchorX: 0.5,
anchorY: 0.5,
scaleX: assetScaleX,
- // Adjusted for direction
scaleY: FINAL_SCALE_TARGET,
- // Set initial fixed scale
alpha: 0,
- // Start transparent
- rotation: 0 // No initial rotation
+ rotation: 0
});
- // Random spawn position, avoiding extreme edges for better initial visibility
self.x = 100 + Math.random() * (2048 - 200);
self.y = 100 + Math.random() * (2732 - 200);
- self.startX = self.x; // Store initial X for sine wave calculation
- self.startY = self.y; // Store initial Y for sine wave calculation
- self.waveAmplitude = 10; // Amplitude of the sine wave in pixels (Reduced from 40)
- self.waveFrequency = Math.PI * 2 / 150; // Frequency for a wavelength of 150px
- // MODIFIED: Standardized travel distance
+ self.startX = self.x;
+ self.startY = self.y;
+ self.waveAmplitude = 10;
+ self.waveFrequency = Math.PI * 2 / 150;
var MOVE_DISTANCE_X = 200;
var targetX;
if (movesRight) {
- targetX = self.startX + MOVE_DISTANCE_X; // Move right
+ targetX = self.startX + MOVE_DISTANCE_X;
} else {
- targetX = self.startX - MOVE_DISTANCE_X; // Move left (original)
+ targetX = self.startX - MOVE_DISTANCE_X;
}
- var SCALE_FADE_IN_DURATION_MS = 600; // Time to fade in
- var MOVE_DURATION_MS = 5000; // Extended to 5 seconds total time spent moving
+ var SCALE_FADE_IN_DURATION_MS = 600;
+ var MOVE_DURATION_MS = 5000;
var SHRINK_FADE_OUT_DURATION_MS = MOVE_DURATION_MS - SCALE_FADE_IN_DURATION_MS;
- var VISIBLE_ALPHA_TARGET = 0.3 + Math.random() * 0.3; // Waves are somewhat transparent (0.3 to 0.6 alpha)
- // Phase 1: Fade In
+ var VISIBLE_ALPHA_TARGET = 0.3 + Math.random() * 0.3;
tween(waveGfx, {
alpha: VISIBLE_ALPHA_TARGET
}, {
duration: SCALE_FADE_IN_DURATION_MS,
easing: tween.easeOut
});
- // Phase 2: Move horizontally.
tween(self, {
x: targetX
}, {
duration: MOVE_DURATION_MS,
- easing: tween.linear,
- onFinish: function onFinish() {
- // self.isDone will be set by the delayed shrink/fade tweens.
- } //{4N} // Closing brace for onFinish function
+ easing: tween.linear
});
- // Phase 3 (Shrink and Fade Out) scheduled
var shrinkStartTimeDelay = MOVE_DURATION_MS - SHRINK_FADE_OUT_DURATION_MS;
if (shrinkStartTimeDelay < 0) {
- shrinkStartTimeDelay = 0; // Ensure delay is not negative
+ shrinkStartTimeDelay = 0;
}
LK.setTimeout(function () {
if (self.isDone || !waveGfx || waveGfx.destroyed) {
return;
@@ -1070,45 +882,152 @@
}, {
duration: SHRINK_FADE_OUT_DURATION_MS,
easing: tween.easeIn,
onFinish: function onFinish() {
- self.isDone = true; // Mark as done for cleanup
+ self.isDone = true;
}
});
}, shrinkStartTimeDelay);
- // Update method for sine wave movement
self.update = function () {
if (self.isDone) {
return;
}
- // Calculate sine wave movement for Y based on X
- // self.x is updated by the horizontal tween
var horizontalProgress;
if (movesRight) {
- horizontalProgress = self.x - self.startX; // Amount moved from startX (increases as x increases)
+ horizontalProgress = self.x - self.startX;
} else {
- horizontalProgress = self.startX - self.x; // Amount moved from startX (increases as x decreases)
+ horizontalProgress = self.startX - self.x;
}
self.y = self.startY + self.waveAmplitude * Math.sin(horizontalProgress * self.waveFrequency);
};
- // This particle uses tweens for horizontal movement and alpha, and an update method for Y position.
return self;
});
/****
* Initialize Game
****/
-/****
-* Screen Containers
-****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
-// Constants for Title Screen Animation
+/****
+* Utility Functions
+****/
+function updateParticleArray(particleArray) {
+ for (var i = particleArray.length - 1; i >= 0; i--) {
+ var particle = particleArray[i];
+ if (particle) {
+ if (typeof particle.update === 'function') {
+ particle.update();
+ }
+ if (particle.isDone) {
+ if (typeof particle.destroy === 'function') {
+ particle.destroy();
+ }
+ particleArray.splice(i, 1);
+ }
+ } else {
+ particleArray.splice(i, 1);
+ }
+ }
+}
+function handleParticleSpawning(config) {
+ config.counter++;
+ if (config.counter >= config.interval && (!config.maxCount || config.array.length < config.maxCount)) {
+ config.counter = 0;
+ var newParticle = new config.constructor();
+ if (config.container && !config.container.destroyed) {
+ config.container.addChild(newParticle);
+ config.array.push(newParticle);
+ }
+ }
+}
+function clearTimer(timerId, isInterval) {
+ if (timerId !== null) {
+ if (isInterval) {
+ LK.clearInterval(timerId);
+ } else {
+ LK.clearTimeout(timerId);
+ }
+ return null;
+ }
+ return timerId;
+}
+function cleanupParticleArray(array, container) {
+ if (array) {
+ array.forEach(function (item) {
+ if (item && typeof item.destroy === 'function' && !item.destroyed) {
+ item.destroy();
+ }
+ });
+ array.length = 0;
+ }
+ if (container && typeof container.removeChildren === 'function' && !container.destroyed) {
+ container.removeChildren();
+ }
+}
+function stopTween(object) {
+ if (object && !object.destroyed) {
+ tween.stop(object);
+ }
+}
+function stopTweens(objects) {
+ for (var i = 0; i < objects.length; i++) {
+ stopTween(objects[i]);
+ }
+}
+function createWaveAnimation(segment, amplitude, halfPeriod) {
+ var animUp, animDown;
+ animUp = function animUp() {
+ if (!segment || segment.destroyed) return;
+ tween(segment, {
+ y: segment.baseY - amplitude
+ }, {
+ duration: halfPeriod,
+ easing: tween.easeInOut,
+ onFinish: animDown
+ });
+ };
+ animDown = function animDown() {
+ if (!segment || segment.destroyed) return;
+ tween(segment, {
+ y: segment.baseY + amplitude
+ }, {
+ duration: halfPeriod,
+ easing: tween.easeInOut,
+ onFinish: animUp
+ });
+ };
+ return {
+ up: animUp,
+ down: animDown
+ };
+}
+function createAmbientSoundScheduler(config) {
+ var timer = null;
+ function scheduleNext() {
+ if (GameState.currentScreen !== config.screenName) {
+ timer = clearTimer(timer, false);
+ return;
+ }
+ var delay = config.baseDelay + Math.random() * config.variance;
+ timer = LK.setTimeout(function () {
+ if (GameState.currentScreen !== config.screenName) return;
+ var soundId = Array.isArray(config.sounds) ? config.sounds[Math.floor(Math.random() * config.sounds.length)] : config.sounds;
+ LK.getSound(soundId).play();
+ scheduleNext();
+ }, delay);
+ }
+ return {
+ start: scheduleNext,
+ stop: function stop() {
+ timer = clearTimer(timer, false);
+ }
+ };
+}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
@@ -1168,155 +1087,112 @@
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
+ GROUP_INITIAL_Y_SCREEN_OFFSET: -450
};
-// If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up.
+game.up = function (x, y, obj) {
+ switch (GameState.currentScreen) {
+ case 'title':
+ break;
+ case 'levelSelect':
+ break;
+ case 'fishing':
+ handleFishingInput(x, y, false);
+ break;
+ case 'results':
+ break;
+ }
+};
/****
* 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
+ var minRequiredGap = configuredSpawnInterval;
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
+ this.lastActualSpawnTime = -100000;
}
};
/****
* 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",
@@ -1328,9 +1204,8 @@
}, {
name: "Sunny Afternoon",
bpm: 97,
duration: 181800,
- // 3:01.8
pattern: "sunny_afternoon_custom",
cost: 0,
musicId: 'sunnyafternoon'
}]
@@ -1391,214 +1266,142 @@
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)
- {
+ sections: [{
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)
- {
+ sections: [{
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)
- {
+ sections: [{
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"
@@ -1608,11 +1411,11 @@
};
/****
* 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 MULTI_BEAT_SPAWN_DELAY_MS = 250;
+var TRIPLET_BEAT_SPAWN_DELAY_MS = 350;
+var FISH_SPAWN_END_BUFFER_MS = 500;
var ImprovedRhythmSpawner = {
nextBeatToSchedule: 1,
scheduledBeats: [],
update: function update(currentTime) {
@@ -1622,15 +1425,13 @@
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) {
@@ -1639,23 +1440,19 @@
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
+ return;
}
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 () {
@@ -1672,12 +1469,10 @@
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++) {
@@ -1687,19 +1482,16 @@
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';
@@ -1713,39 +1505,33 @@
} 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
+ requiredSpeedPerFrame *= -1;
}
- // 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!
+ newFish.lastX = newFish.x;
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) {
@@ -1755,9 +1541,8 @@
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);
@@ -1808,94 +1593,82 @@
}
}
}
function createTutorialElements() {
- tutorialOverlayContainer.removeChildren(); // Clear previous elements
+ tutorialOverlayContainer.removeChildren();
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
+ lineHeight: 65
}));
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
+ tutorialTextDisplay.y = tutorialTextBackground.y - 50;
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
+ tutorialOverlayContainer.visible = false;
}
function setTutorialText(newText, showContinue) {
if (showContinue === undefined) {
showContinue = true;
}
if (!tutorialTextDisplay || !tutorialContinueButton || !tutorialContinueText) {
- createTutorialElements(); // Ensure elements exist
+ createTutorialElements();
}
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 fishValue = Math.floor(depthConfig.fishValue / 2);
var baseSpeed = depthConfig.fishSpeed;
- var speedMultiplier = config.speedMultiplier || 0.5; // Slower for tutorial
+ var speedMultiplier = config.speedMultiplier || 0.5;
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.x = actualFishSpeed > 0 ? -150 : 2048 + 150;
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
+ newFish.tutorialFish = true;
+ fishArray.push(newFish);
+ fishingScreen.addChild(newFish);
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
+ GameState.tutorialAwaitingTap = false;
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
@@ -1904,13 +1677,12 @@
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();
}
@@ -1919,30 +1691,26 @@
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
+ fishingElements.hook.y = GAME_CONFIG.LANES[1].y;
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,
@@ -1963,35 +1731,30 @@
}
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
+ tutorialOverlayContainer.addChildAt(highlight, 0);
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
+ tutorialContinueButton.visible = false;
tutorialContinueText.visible = false;
- GameState.tutorialPaused = false; // Fish moves
+ GameState.tutorialPaused = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
GameState.tutorialFish = spawnTutorialFishHelper({
@@ -2000,10 +1763,8 @@
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;
@@ -2016,28 +1777,20 @@
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();
@@ -2046,31 +1799,24 @@
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);
+ stopTween(fishingElements.boat);
}
if (fishingElements && fishingElements.fishermanContainer && !fishingElements.fishermanContainer.destroyed) {
- tween.stop(fishingElements.fishermanContainer);
+ stopTween(fishingElements.fishermanContainer);
}
- // Stop water surface animations
if (fishingElements && fishingElements.waterSurfaceSegments) {
- fishingElements.waterSurfaceSegments.forEach(function (segment) {
- if (segment && !segment.destroyed) {
- tween.stop(segment);
- }
- });
+ stopTweens(fishingElements.waterSurfaceSegments);
}
showScreen('levelSelect');
break;
}
@@ -2078,20 +1824,17 @@
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) {
@@ -2102,12 +1845,10 @@
}
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();
@@ -2143,94 +1884,65 @@
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
+ var passedHook = fish.speed > 0 && fish.x > hookX + GAME_CONFIG.MISS_WINDOW + fish.fishGraphics.width / 2 || fish.speed < 0 && fish.x < hookX - GAME_CONFIG.MISS_WINDOW - fish.fishGraphics.width / 2;
if (passedHook) {
- fish.missed = true; // Mark to prevent further attempts on this specific fish instance
- GameState.tutorialPaused = true; // Pause to show message
+ fish.missed = true;
+ GameState.tutorialPaused = true;
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 wasCriticalStep = GameState.tutorialStep === 3 || GameState.tutorialStep === 4;
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
lastLevelSelectNodeKey: 'dock',
- // Store the key of the node the boat was at on level select
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({
@@ -2270,9 +1982,8 @@
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
});
@@ -2284,120 +1995,93 @@
var titleScreen = game.addChild(new Container());
var levelSelectScreen = game.addChild(new Container());
var fishingScreen = game.addChild(new Container());
var resultsScreen = game.addChild(new Container());
-// Global Fade Overlay for screen transitions
var globalFadeOverlay = game.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
width: 2048,
height: 2732,
color: 0x000000,
alpha: 0
}));
-globalFadeOverlay.visible = false; // Start invisible
-// Ensure it's on top initially, will be re-asserted during use
+globalFadeOverlay.visible = false;
if (game.children.indexOf(globalFadeOverlay) !== -1) {
game.setChildIndex(globalFadeOverlay, game.children.length - 1);
}
-// 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
+ y: 0
}));
- // 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
+ y: -70
}));
- // 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 rodTipX = -100 + 85;
+ var rodTipY = -70 - 200;
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
+ height: initialHookYInGroup - rodTipY
}));
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
+ y: initialHookYInGroup
}));
- // 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 boatGroupBaseY = titleBoatGroup.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,
@@ -2408,9 +2092,9 @@
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
+ titleAnimationGroup.addChild(segment);
titleWaterSurfaceSegments.push(segment);
}
for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) {
var whiteSegment = LK.getAsset('waterSurface', {
@@ -2423,33 +2107,28 @@
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
+ titleAnimationGroup.addChild(whiteSegment);
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
+ var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X;
+ var targetBoatScreenY = GAME_CONFIG.SCREEN_CENTER_Y + 300;
+ var boatWorldY = GAME_CONFIG.WATER_SURFACE_Y;
+ var pivotY = boatWorldY - (targetBoatScreenY - boatWorldY);
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
+ titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X;
+ titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y;
var INITIAL_ZOOM_FACTOR = 3.0;
- var FINAL_ZOOM_FACTOR = 1.8; // This matches existing logic if it's used in showScreen
+ var FINAL_ZOOM_FACTOR = 1.8;
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
+ titleAnimationGroup.alpha = 1;
+ var targetUpY = boatGroupBaseY - boatWaveAmplitude;
+ var targetDownY = boatGroupBaseY + boatWaveAmplitude;
function moveTitleBoatGroupUp() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
- // We tween the group's world Y position
tween(titleBoatGroup, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration,
@@ -2468,9 +2147,8 @@
easing: tween.easeInOut,
onFinish: moveTitleBoatGroupUp
});
}
- // Single rotation animation - rotates entire group together
function rockTitleBoatGroupLeft() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
@@ -2493,111 +2171,70 @@
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 deltaX = titleHook.x - titleLine.x;
+ var deltaY = titleHook.y - titleLine.y;
var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
titleLine.height = actualLineLength;
if (actualLineLength > 0.001) {
titleLine.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2;
} else {
titleLine.rotation = 0;
}
- titleHook.rotation = titleLine.rotation; // Hook rotation matches line's
+ titleHook.rotation = titleLine.rotation;
}
- // 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
- });
- };
+ var waveAnim = createWaveAnimation(currentLocalSegment, WAVE_AMPLITUDE_TITLE, WAVE_HALF_PERIOD_MS_TITLE);
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
+ onFinish: waveAnim.up
});
}, 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
+ alpha: 1
}));
- // 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
+ scaleY: 0.8
}));
- // 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 startButtonY = 2732 - 2732 / 3.5;
+ var tutorialButtonY = startButtonY + 600;
var startButton = titleScreen.addChild(LK.getAsset('startbutton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
@@ -2610,49 +2247,30 @@
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
+ updateTitleFishingLineWave: updateTitleFishingLineWave
};
}
/****
* Level Select Screen
****/
-/****
-* Map Level Select Screen - Complete Replacement
-****/
-// Map configuration
var MAP_CONFIG = {
- // Node positions on the map
NODES: {
dock: {
x: 1200,
y: 860,
@@ -2699,21 +2317,18 @@
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
+ BOAT_TRAVEL_SPEED: 300
};
function createLevelSelectScreen() {
- // Clear existing level select screen
levelSelectScreen.removeChildren();
- var nodeNameTexts = {}; // To store references to the text objects for node names
+ var nodeNameTexts = {};
function getNodeDisplayName(nodeKey) {
var nodeNames = {
dock: 'Dock',
shallows: 'Shallow Waters',
@@ -2724,77 +2339,60 @@
restaurant: 'Ocean Restaurant'
};
return nodeNames[nodeKey] || 'Unknown';
}
- // Map background
var mapBg = levelSelectScreen.addChild(LK.getAsset('mapBackground', {
x: 0,
y: 0
}));
- // Background for money display
var moneyDisplayBackground = levelSelectScreen.addChild(LK.getAsset('songCard', {
anchorX: 1,
anchorY: 0,
x: 1900 + 20,
- // Offset slightly to give padding from the right edge
y: 80 - 10,
- // Offset slightly to give padding from the top edge
width: 450,
- // Adjusted width to fit typical money amounts
height: 100,
- // Adjusted height
color: 0x000000,
- // Black background
- alpha: 0.5 // Semi-transparent
+ alpha: 0.5
}));
- // Money display (keep at top right)
- // Ripple Effect Variables for Boat
var ripplesContainer = levelSelectScreen.addChild(new Container());
var activeRipples = [];
- var rippleSpawnTimerId = null; // For boat ripples
+ var rippleSpawnTimerId = null;
var RIPPLE_SPAWN_INTERVAL_MS = 350;
var RIPPLE_INITIAL_SCALE = 0.1;
var RIPPLE_FINAL_SCALE = 1.8;
- var RIPPLE_INITIAL_OFFSET = 40; // Start near boat edge
- var RIPPLE_TRAVEL_DISTANCE = 140; // Reduced from 180
- var RIPPLE_DURATION_MS = 1800; // Shortened from 2200
- // NEW: Home Island Ripple Effect Variables
- // Add container for home island ripples BEFORE homeIsland asset for correct layering
+ var RIPPLE_INITIAL_OFFSET = 40;
+ var RIPPLE_TRAVEL_DISTANCE = 140;
+ var RIPPLE_DURATION_MS = 1800;
var homeIslandRipplesContainer = levelSelectScreen.addChild(new Container());
var activeHomeIslandRipples = [];
- var homeIslandRippleSpawnTimerId = null; // For home island ripples
- // Constants for Home Island Ripples
- var HOME_ISLAND_RIPPLE_SPAWN_INTERVAL_MS = 225; // Increased frequency (was 450)
+ var homeIslandRippleSpawnTimerId = null;
+ var HOME_ISLAND_RIPPLE_SPAWN_INTERVAL_MS = 225;
var HOME_ISLAND_RIPPLE_INITIAL_SCALE = 0.1;
var HOME_ISLAND_RIPPLE_FINAL_SCALE = 1.875;
- var HOME_ISLAND_RIPPLE_INITIAL_OFFSET_FROM_EDGE = 10; // How far from edge it starts expanding
- var HOME_ISLAND_RIPPLE_TRAVEL_DISTANCE = 220; // Travel distance increased from 180
- var HOME_ISLAND_RIPPLE_DURATION_MS = 2000; // Lifetime
- // Home island at top center
+ var HOME_ISLAND_RIPPLE_INITIAL_OFFSET_FROM_EDGE = 10;
+ var HOME_ISLAND_RIPPLE_TRAVEL_DISTANCE = 220;
+ var HOME_ISLAND_RIPPLE_DURATION_MS = 2000;
var homeIsland = levelSelectScreen.addChild(LK.getAsset('homeisland', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 500
}));
- // Container for bubbles rising from shallow waters node
var shallowWatersNodeBubblesContainer = levelSelectScreen.addChild(new Container());
- // NEW: Container for shadow fish, added after bubbles to be on a similar or slightly above layer
shadowFishContainer = levelSelectScreen.addChild(new Container());
var activeShallowWatersNodeBubbles = [];
var shallowWatersNodeBubbleSpawnTimerId = null;
- var SHALLOW_NODE_BUBBLE_SPAWN_INTERVAL_MS = 350 + Math.random() * 250; // Spawn every 0.35-0.6s
- // NEW: Level Select Screen Cloud properties
- var levelSelectCloudContainer; // Declare container, will be added later for correct layering
+ var SHALLOW_NODE_BUBBLE_SPAWN_INTERVAL_MS = 350 + Math.random() * 250;
+ var levelSelectCloudContainer;
var activeLevelSelectClouds = [];
var levelSelectCloudSpawnTimerId = null;
- var LEVEL_SELECT_CLOUD_SPAWN_INTERVAL_MS = 4000 + Math.random() * 3000; // Spawn a cloud every 4-7 seconds
- var MAX_LEVEL_SELECT_CLOUDS = 6; // Max clouds on screen at once
- // NEW: Shadow Fish Particle System variables
+ var LEVEL_SELECT_CLOUD_SPAWN_INTERVAL_MS = 4000 + Math.random() * 3000;
+ var MAX_LEVEL_SELECT_CLOUDS = 6;
var shadowFishContainer;
var activeShadowFish = [];
var shadowFishSpawnTimerId = null;
- var SHADOW_FISH_SPAWN_INTERVAL_MS = 9000 + Math.random() * 5000; // Spawn a shadow fish every 9-14 seconds
+ var SHADOW_FISH_SPAWN_INTERVAL_MS = 9000 + Math.random() * 5000;
var moneyDisplay = new Text2('$0', {
size: 70,
fill: 0xFFD700,
stroke: 0x000000,
@@ -2803,10 +2401,8 @@
moneyDisplay.anchor.set(1, 0);
moneyDisplay.x = 1900;
moneyDisplay.y = 80;
levelSelectScreen.addChild(moneyDisplay);
- // Back button (bottom center)
- // Add a songCard background for the back to title button text
var backButtonBg = levelSelectScreen.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
@@ -2822,53 +2418,38 @@
y: 2600,
tint: 0x757575,
width: 700,
height: 170,
- alpha: 0 // Make the button itself invisible, only for input
+ alpha: 0
}));
- // Larger text for back to title
var backButtonText = new Text2('BACK TO TITLE', {
size: 50,
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('songCard', {
x: 1024,
- // Centered on screen (horizontally)
y: 1366,
- // Centered on screen (vertically, 2732 / 2)
width: 1700,
- // Increased width of the overlay
height: 900,
- // Height of the overlay (remains the same)
color: 0x424242,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.95
}));
- // Close button for overlay
- // New overlay width = 1800, new overlay Y = 1366
- // overlayCenterX = 1024, overlayHeight = 800
- // closeButton.x = overlayCenterX + new_overlay_width / 2 - 60 = 1024 + 1800 / 2 - 60 = 1024 + 900 - 60 = 1864
- // closeButton.y = new_overlayCenterY - overlay_height / 2 + 60 = 1366 - 800 / 2 + 60 = 1366 - 400 + 60 = 1026
var closeButton = songOverlayContainer.addChild(LK.getAsset('closeButton', {
anchorX: 0.5,
anchorY: 0.5,
x: 1700,
- // Adjusted for new overlay width and position (top right)
y: 1026,
- // Adjusted for new overlay Y and position (top right)
width: 100,
height: 100,
tint: 0xff4444
}));
@@ -2879,9 +2460,8 @@
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,
@@ -2889,18 +2469,11 @@
songEarnings: null,
playButton: null,
playButtonText: null,
leftArrowText: null,
- // Added for text cleanup
- rightArrowText: null // Added for text cleanup
+ rightArrowText: null
};
- // Player boat
- // State variables
- // Determine the node the boat should start at.
- // If returning from fishing, GameState.lastLevelSelectNodeKey will have the stored node.
- // Otherwise (e.g. from title), it defaults to 'dock' as set in GameState.
var currentNode = GameState.lastLevelSelectNodeKey;
- // Player boat
var initialBoatNodeConfig = MAP_CONFIG.NODES[currentNode];
var playerBoat = boatContainer.addChild(LK.getAsset('playerBoat', {
anchorX: 0.5,
anchorY: 0.5,
@@ -2910,151 +2483,69 @@
var boatMoving = false;
var _songOverlayOpen = false;
var selectedDepth = 0;
var selectedSong = 0;
- // Boat animation variables
- var boatBaseY = playerBoat.y; // Set based on the actual starting Y of the boat
+ var boatBaseY = playerBoat.y;
var boatBobPhase = 0;
var boatRotationPhase = 0;
- // Ambient sound timers for level select screen
var levelSelectSeagullSoundTimer = null;
var levelSelectBoatSoundTimer = null;
- // NEW: Ambient Screen Wave Variables
var levelSelectScreenWavesArray = [];
var levelSelectScreenWaveSpawnTimerId = null;
- // New interval is old_interval * 4/3 to reduce spawn rate to 3/4
- var SCREEN_WAVE_SPAWN_INTERVAL_MS = (200 + Math.random() * 300) * 4 / 3; // Spawn a new wave every ~0.27 to ~0.67 seconds
- // NEW: Seagull Particle System
- var seagullsContainer = levelSelectScreen.addChild(new Container()); // Added after boatContainer for layering
+ var SCREEN_WAVE_SPAWN_INTERVAL_MS = (200 + Math.random() * 300) * 4 / 3;
+ var seagullsContainer = levelSelectScreen.addChild(new Container());
var activeSeagulls = [];
var seagullSpawnTimerId = null;
- var SEAGULL_SPAWN_INTERVAL_MS = 3500 + Math.random() * 2500; // Spawn a seagull every 3.5-6 seconds
- levelSelectCloudContainer = levelSelectScreen.addChild(new Container()); // Clouds added as a top layer for environmental effects
- // --- Ambient Sound Functions for Level Select Screen ---
- function _scheduleNextLevelSelectSeagullSound() {
- if (GameState.currentScreen !== 'levelSelect') {
- if (levelSelectSeagullSoundTimer) {
- LK.clearTimeout(levelSelectSeagullSoundTimer);
- levelSelectSeagullSoundTimer = null;
- }
- return;
- }
- var randomDelay = 5000 + Math.random() * 10000; // 5-15 seconds
- levelSelectSeagullSoundTimer = LK.setTimeout(function () {
- if (GameState.currentScreen !== 'levelSelect') {
- return;
- }
- var seagullSounds = ['seagull1', 'seagull2', 'seagull3'];
- var randomSoundId = seagullSounds[Math.floor(Math.random() * seagullSounds.length)];
- LK.getSound(randomSoundId).play();
- _scheduleNextLevelSelectSeagullSound(); // Reschedule
- }, randomDelay);
- }
- function _scheduleNextLevelSelectBoatSound() {
- if (GameState.currentScreen !== 'levelSelect') {
- if (levelSelectBoatSoundTimer) {
- LK.clearTimeout(levelSelectBoatSoundTimer);
- levelSelectBoatSoundTimer = null;
- }
- return;
- }
- var fixedBoatSoundInterval = 6000; // Rhythmic interval: 6 seconds
- levelSelectBoatSoundTimer = LK.setTimeout(function () {
- if (GameState.currentScreen !== 'levelSelect') {
- return;
- }
- LK.getSound('boatsounds').play();
- _scheduleNextLevelSelectBoatSound(); // Reschedule
- }, fixedBoatSoundInterval);
- }
+ var SEAGULL_SPAWN_INTERVAL_MS = 3500 + Math.random() * 2500;
+ levelSelectCloudContainer = levelSelectScreen.addChild(new Container());
+ // Create ambient sound schedulers
+ var seagullScheduler = createAmbientSoundScheduler({
+ screenName: 'levelSelect',
+ sounds: ['seagull1', 'seagull2', 'seagull3'],
+ baseDelay: 5000,
+ variance: 10000
+ });
+ var boatScheduler = createAmbientSoundScheduler({
+ screenName: 'levelSelect',
+ sounds: 'boatsounds',
+ baseDelay: 6000,
+ variance: 0
+ });
function startLevelSelectAmbientSounds() {
- // Play initial sounds immediately
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
- _scheduleNextLevelSelectSeagullSound();
- _scheduleNextLevelSelectBoatSound();
+ LK.getSound('boatsounds').play();
+ seagullScheduler.start();
+ boatScheduler.start();
}
function stopLevelSelectAmbientSounds() {
- if (levelSelectSeagullSoundTimer) {
- LK.clearTimeout(levelSelectSeagullSoundTimer);
- levelSelectSeagullSoundTimer = null;
- }
- if (levelSelectBoatSoundTimer) {
- LK.clearTimeout(levelSelectBoatSoundTimer);
- levelSelectBoatSoundTimer = null;
- }
+ seagullScheduler.stop();
+ boatScheduler.stop();
}
- // --- End Ambient Sound Functions ---
- // Spawn a few clouds immediately at screen load
+ // Initialize clouds at screen load
for (var i = 0; i < 3; i++) {
var initialCloud = new MapScreenCloudParticle();
- // Place them at random X in the middle 60% of the screen, and random Y in the upper 80%
- initialCloud.x = 400 + Math.random() * 1248; // 400 to 1648
- initialCloud.y = 300 + Math.random() * 1700; // 300 to 2000
- // Give them a random drift direction and speed
+ initialCloud.x = 400 + Math.random() * 1248;
+ initialCloud.y = 300 + Math.random() * 1700;
initialCloud.vx = (Math.random() < 0.5 ? 1 : -1) * (0.15 + Math.random() * 0.2) * 1.15;
- // Set alpha to targetAlpha immediately (skip fade-in)
if (initialCloud.gfx && typeof initialCloud.gfx.alpha === "number") {
initialCloud.gfx.alpha = 0.4 + Math.random() * 0.3;
}
levelSelectCloudContainer.addChild(initialCloud);
activeLevelSelectClouds.push(initialCloud);
}
- // Note: Ripple Effect Variables and its addChild to levelSelectScreen were moved up.
- // The addChildAt logic that was here to position it relative to boatContainer is now removed.
function spawnRippleEffect() {
if (!playerBoat || playerBoat.destroyed || GameState.currentScreen !== 'levelSelect') {
return;
}
var boatX = playerBoat.x;
var boatY = playerBoat.y;
var spawnAngle = Math.random() * Math.PI * 2;
- var ripple = new RippleParticle(boatX, boatY, spawnAngle, RIPPLE_INITIAL_OFFSET, RIPPLE_TRAVEL_DISTANCE, RIPPLE_INITIAL_SCALE, RIPPLE_FINAL_SCALE, RIPPLE_DURATION_MS);
- ripplesContainer.addChild(ripple);
- activeRipples.push(ripple);
- }
- // Functions for Boat Ripples (existing)
- function spawnRippleEffect() {
- if (!playerBoat || playerBoat.destroyed || GameState.currentScreen !== 'levelSelect') {
- return;
- }
- var boatX = playerBoat.x;
- var boatY = playerBoat.y;
- var spawnAngle = Math.random() * Math.PI * 2;
var ripple = new RippleParticle(boatX, boatY, spawnAngle, RIPPLE_INITIAL_OFFSET, RIPPLE_TRAVEL_DISTANCE, RIPPLE_INITIAL_SCALE, RIPPLE_FINAL_SCALE, RIPPLE_DURATION_MS, 1.0);
ripplesContainer.addChild(ripple);
activeRipples.push(ripple);
}
- function updateRipplesCleanup() {
- for (var i = activeRipples.length - 1; i >= 0; i--) {
- var ripple = activeRipples[i];
- if (ripple.isDone) {
- if (!ripple.destroyed) {
- ripple.destroy();
- }
- activeRipples.splice(i, 1);
- }
- }
- }
- function cleanupLevelSelectRipples() {
- // This is for boat ripples
- if (rippleSpawnTimerId !== null) {
- LK.clearInterval(rippleSpawnTimerId);
- rippleSpawnTimerId = null;
- }
- // Do not removeChildren from ripplesContainer here as it's shared.
- // Instead, iterate and destroy specific ripples.
- activeRipples.forEach(function (rippleInstance) {
- if (rippleInstance && !rippleInstance.destroyed) {
- rippleInstance.destroy();
- }
- });
- activeRipples = [];
- }
- // NEW: Functions for Home Island Ripples
function spawnHomeIslandRippleEffect() {
if (!homeIsland || homeIsland.destroyed || GameState.currentScreen !== 'levelSelect') {
return;
}
@@ -3067,394 +2558,163 @@
var ripple = new RippleParticle(edgeX, edgeY, spawnEdgeAngle, HOME_ISLAND_RIPPLE_INITIAL_OFFSET_FROM_EDGE, HOME_ISLAND_RIPPLE_TRAVEL_DISTANCE, HOME_ISLAND_RIPPLE_INITIAL_SCALE, HOME_ISLAND_RIPPLE_FINAL_SCALE, HOME_ISLAND_RIPPLE_DURATION_MS, 0.5);
homeIslandRipplesContainer.addChild(ripple);
activeHomeIslandRipples.push(ripple);
}
- function updateHomeIslandRipplesCleanup() {
- for (var i = activeHomeIslandRipples.length - 1; i >= 0; i--) {
- var ripple = activeHomeIslandRipples[i];
- if (ripple.isDone) {
- if (!ripple.destroyed) {
- ripple.destroy();
- }
- activeHomeIslandRipples.splice(i, 1);
- }
- }
- }
- function cleanupLevelSelectHomeIslandRipples() {
- if (homeIslandRippleSpawnTimerId !== null) {
- LK.clearInterval(homeIslandRippleSpawnTimerId);
- homeIslandRippleSpawnTimerId = null;
- }
- if (homeIslandRipplesContainer) {
- // Iterate and destroy only home island ripples
- activeHomeIslandRipples.forEach(function (rippleInstance) {
- if (rippleInstance && !rippleInstance.destroyed) {
- rippleInstance.destroy();
- }
- });
- // If homeIslandRipplesContainer ONLY contains these, then removeChildren is fine.
- // Assuming it's dedicated:
- // homeIslandRipplesContainer.removeChildren();
- }
- activeHomeIslandRipples = [];
- }
- // NEW: Functions for Ambient Screen Waves
function spawnLevelSelectScreenWaveEffect() {
if (GameState.currentScreen !== 'levelSelect' || !ripplesContainer || ripplesContainer.destroyed) {
return;
}
- var movesRight = Math.random() < 0.5; // 50% chance to move right
- var wave = new WaveParticle(movesRight); // Pass direction to constructor
- ripplesContainer.addChild(wave); // Add to the shared ripplesContainer
+ var movesRight = Math.random() < 0.5;
+ var wave = new WaveParticle(movesRight);
+ ripplesContainer.addChild(wave);
levelSelectScreenWavesArray.push(wave);
}
- function updateLevelSelectScreenWavesCleanup() {
- for (var i = levelSelectScreenWavesArray.length - 1; i >= 0; i--) {
- var wave = levelSelectScreenWavesArray[i];
- if (wave.isDone) {
- if (wave && !wave.destroyed) {
- wave.destroy();
- }
- levelSelectScreenWavesArray.splice(i, 1);
- }
- }
- }
- function cleanupLevelSelectScreenWaves() {
- if (levelSelectScreenWaveSpawnTimerId !== null) {
- LK.clearInterval(levelSelectScreenWaveSpawnTimerId);
- levelSelectScreenWaveSpawnTimerId = null;
- }
- levelSelectScreenWavesArray.forEach(function (wave) {
- if (wave && !wave.destroyed) {
- wave.destroy();
- }
- });
- levelSelectScreenWavesArray = [];
- }
- // Timer starting logic for boat ripples
- if (levelSelectElements && levelSelectElements.rippleSpawnTimerId) {
- LK.clearInterval(levelSelectElements.rippleSpawnTimerId);
- }
- rippleSpawnTimerId = LK.setInterval(spawnRippleEffect, RIPPLE_SPAWN_INTERVAL_MS);
- // Timer starting logic for home island ripples
- if (levelSelectElements && levelSelectElements.homeIslandRippleSpawnTimerId) {
- LK.clearInterval(levelSelectElements.homeIslandRippleSpawnTimerId);
- }
- homeIslandRippleSpawnTimerId = LK.setInterval(spawnHomeIslandRippleEffect, HOME_ISLAND_RIPPLE_SPAWN_INTERVAL_MS);
- // NEW: Timer starting logic for ambient screen waves
- if (levelSelectElements && levelSelectElements.screenWaveSpawnTimerId) {
- // Check against the name used in returned object
- LK.clearInterval(levelSelectElements.screenWaveSpawnTimerId);
- }
- levelSelectScreenWaveSpawnTimerId = LK.setInterval(spawnLevelSelectScreenWaveEffect, SCREEN_WAVE_SPAWN_INTERVAL_MS);
- // NEW: Waterfall Particle System for Home Island
- var waterfallParticlesContainer = levelSelectScreen.addChild(new Container());
- // Ensure waterfallParticlesContainer is added *after* homeIsland for correct layering.
- // The homeIsland is added earlier. This line implicitly adds it after.
- var activeWaterfallParticles = [];
- var waterfallSpawnTimerId = null;
- var WATERFALL_SPAWN_INTERVAL_MS = 80; // Reduced spawn interval for more particles
- var waterfallSpawnX = homeIsland.x + 20; // Horizontally centered on home island
- // homeIsland Y is 500, height 1000, anchor 0.5. Top edge is Y=0.
- // Start waterfall 150px down from the top edge of the island graphic.
- var waterfallSpawnY = homeIsland.y - homeIsland.height / 2 + 350;
- function spawnWaterfallParticleEffect() {
- if (GameState.currentScreen !== 'levelSelect' || !waterfallParticlesContainer || waterfallParticlesContainer.destroyed) {
- return;
- }
- var particle = new WaterfallParticle(waterfallSpawnX, waterfallSpawnY);
- waterfallParticlesContainer.addChild(particle);
- activeWaterfallParticles.push(particle);
- }
- if (levelSelectElements && levelSelectElements.waterfallSpawnTimerId) {
- LK.clearInterval(levelSelectElements.waterfallSpawnTimerId);
- }
- waterfallSpawnTimerId = LK.setInterval(spawnWaterfallParticleEffect, WATERFALL_SPAWN_INTERVAL_MS);
- function updateWaterfallParticlesCleanup() {
- for (var i = activeWaterfallParticles.length - 1; i >= 0; i--) {
- var p = activeWaterfallParticles[i];
- if (p.isDone) {
- if (p && !p.destroyed) {
- p.destroy();
- }
- activeWaterfallParticles.splice(i, 1);
- }
- }
- }
- function cleanupLevelSelectWaterfallParticles() {
- if (waterfallSpawnTimerId !== null) {
- LK.clearInterval(waterfallSpawnTimerId);
- waterfallSpawnTimerId = null;
- }
- activeWaterfallParticles.forEach(function (p) {
- if (p && !p.destroyed) {
- p.destroy();
- }
- });
- activeWaterfallParticles = [];
- if (waterfallParticlesContainer && !waterfallParticlesContainer.destroyed) {
- // waterfallParticlesContainer.removeChildren(); // Optionally clear if only for these
- }
- }
- // Functions for Shallow Waters Node Bubbles
function spawnShallowWatersNodeBubbleEffect() {
if (GameState.currentScreen !== 'levelSelect' || !shallowWatersNodeBubblesContainer || shallowWatersNodeBubblesContainer.destroyed) {
return;
}
var shallowNodePos = MAP_CONFIG.NODES.shallows;
if (!shallowNodePos || !shallowNodePos.unlocked) {
- // Only spawn if node exists and is unlocked
return;
}
- // Spawn bubbles appearing to rise from "deep" below the shallow node
var spawnX = shallowNodePos.x;
- var spawnY = shallowNodePos.y + 250 + (Math.random() - 0.5) * 200; // Start 250px below the node's y-coordinate, with an additional +/-100px random Y spread
+ var spawnY = shallowNodePos.y + 250 + (Math.random() - 0.5) * 200;
var bubble = new MapBubbleParticle(spawnX, spawnY);
shallowWatersNodeBubblesContainer.addChild(bubble);
activeShallowWatersNodeBubbles.push(bubble);
}
- function updateShallowWatersNodeBubblesCleanup() {
- for (var i = activeShallowWatersNodeBubbles.length - 1; i >= 0; i--) {
- var bubble = activeShallowWatersNodeBubbles[i];
- // MapBubbleParticle is tween-driven and sets its own isDone flag.
- // No explicit bubble.update() call needed here.
- if (bubble.isDone) {
- if (bubble && !bubble.destroyed) {
- bubble.destroy();
- }
- activeShallowWatersNodeBubbles.splice(i, 1);
- }
- }
- }
- function cleanupLevelSelectShallowWatersNodeBubbles() {
- if (shallowWatersNodeBubbleSpawnTimerId !== null) {
- LK.clearInterval(shallowWatersNodeBubbleSpawnTimerId);
- shallowWatersNodeBubbleSpawnTimerId = null;
- }
- activeShallowWatersNodeBubbles.forEach(function (bubble) {
- if (bubble && !bubble.destroyed) {
- bubble.destroy();
- }
- });
- activeShallowWatersNodeBubbles = [];
- if (shallowWatersNodeBubblesContainer && !shallowWatersNodeBubblesContainer.destroyed) {
- // shallowWatersNodeBubblesContainer.removeChildren(); // If exclusively for these bubbles
- }
- }
- // Start timer for shallow waters node bubbles
- if (levelSelectElements && levelSelectElements.shallowWatersNodeBubbleSpawnTimerId) {
- LK.clearInterval(levelSelectElements.shallowWatersNodeBubbleSpawnTimerId);
- }
- shallowWatersNodeBubbleSpawnTimerId = LK.setInterval(spawnShallowWatersNodeBubbleEffect, SHALLOW_NODE_BUBBLE_SPAWN_INTERVAL_MS);
- // NEW: Functions for Seagull Particles
function spawnSeagullEffect() {
if (GameState.currentScreen !== 'levelSelect' || !seagullsContainer || seagullsContainer.destroyed) {
return;
}
var seagull = new SeagullParticle();
seagullsContainer.addChild(seagull);
activeSeagulls.push(seagull);
}
- function updateSeagullsCleanup() {
- for (var i = activeSeagulls.length - 1; i >= 0; i--) {
- var seagull = activeSeagulls[i];
- // SeagullParticle is fully tween-driven and sets its own isDone flag.
- // No explicit seagull.update() call needed here unless it's added later.
- if (seagull.isDone) {
- if (seagull && !seagull.destroyed) {
- seagull.destroy();
- }
- activeSeagulls.splice(i, 1);
- }
- }
- }
- function cleanupLevelSelectSeagulls() {
- if (seagullSpawnTimerId !== null) {
- LK.clearInterval(seagullSpawnTimerId);
- seagullSpawnTimerId = null;
- }
- activeSeagulls.forEach(function (seagull) {
- if (seagull && !seagull.destroyed) {
- seagull.destroy();
- }
- });
- activeSeagulls = [];
- if (seagullsContainer && !seagullsContainer.destroyed) {
- // seagullsContainer.removeChildren(); // If exclusively for these seagulls
- }
- }
- // Start timer for seagulls
- if (levelSelectElements && levelSelectElements.seagullSpawnTimerId) {
- // Check based on returned object key
- LK.clearInterval(levelSelectElements.seagullSpawnTimerId);
- }
- seagullSpawnTimerId = LK.setInterval(spawnSeagullEffect, SEAGULL_SPAWN_INTERVAL_MS);
- // NEW: Functions for Level Select Screen Clouds
function spawnLevelSelectCloudEffect() {
if (GameState.currentScreen !== 'levelSelect' || !levelSelectCloudContainer || levelSelectCloudContainer.destroyed || activeLevelSelectClouds.length >= MAX_LEVEL_SELECT_CLOUDS) {
return;
}
var cloud = new MapScreenCloudParticle();
levelSelectCloudContainer.addChild(cloud);
activeLevelSelectClouds.push(cloud);
}
- function updateLevelSelectCloudsCleanup() {
- for (var i = activeLevelSelectClouds.length - 1; i >= 0; i--) {
- var cloud = activeLevelSelectClouds[i];
- if (cloud.isDone) {
- if (cloud && !cloud.destroyed) {
- cloud.destroy();
- }
- activeLevelSelectClouds.splice(i, 1);
- } else if (cloud && typeof cloud.update === 'function') {
- cloud.update();
- }
- }
- }
- function cleanupLevelSelectClouds() {
- if (levelSelectCloudSpawnTimerId !== null) {
- LK.clearInterval(levelSelectCloudSpawnTimerId);
- levelSelectCloudSpawnTimerId = null;
- }
- activeLevelSelectClouds.forEach(function (cloud) {
- if (cloud && !cloud.destroyed) {
- cloud.destroy();
- }
- });
- activeLevelSelectClouds = [];
- // No need to removeChildren from levelSelectCloudContainer if it might be reused or if particles are its only children
- }
- // Start timer for level select clouds
- if (levelSelectElements && levelSelectElements.levelSelectCloudSpawnTimerId) {
- // Check against the name used in returned object
- LK.clearInterval(levelSelectElements.levelSelectCloudSpawnTimerId);
- }
- levelSelectCloudSpawnTimerId = LK.setInterval(spawnLevelSelectCloudEffect, LEVEL_SELECT_CLOUD_SPAWN_INTERVAL_MS);
- // NEW: Functions for Shadow Fish Particle System
function spawnShadowFishEffect() {
if (GameState.currentScreen !== 'levelSelect' || !shadowFishContainer || shadowFishContainer.destroyed) {
return;
}
var shallowNodePos = MAP_CONFIG.NODES.shallows;
if (!shallowNodePos || !shallowNodePos.unlocked) {
- return; // Only spawn if shallows node is available
+ return;
}
var fish = new ShadowFishParticle(shallowNodePos.x, shallowNodePos.y);
shadowFishContainer.addChild(fish);
activeShadowFish.push(fish);
}
- function updateShadowFishCleanup() {
- for (var i = activeShadowFish.length - 1; i >= 0; i--) {
- var fish = activeShadowFish[i];
- if (fish.isDone) {
- if (fish && !fish.destroyed) {
- fish.destroy();
- }
- activeShadowFish.splice(i, 1);
- } else if (fish && typeof fish.update === 'function') {
- fish.update(); // Call update for animations like sine wave
- }
+ var waterfallParticlesContainer = levelSelectScreen.addChild(new Container());
+ var activeWaterfallParticles = [];
+ var waterfallSpawnTimerId = null;
+ var WATERFALL_SPAWN_INTERVAL_MS = 80;
+ var waterfallSpawnX = homeIsland.x + 20;
+ var waterfallSpawnY = homeIsland.y - homeIsland.height / 2 + 350;
+ function spawnWaterfallParticleEffect() {
+ if (GameState.currentScreen !== 'levelSelect' || !waterfallParticlesContainer || waterfallParticlesContainer.destroyed) {
+ return;
}
+ var particle = new WaterfallParticle(waterfallSpawnX, waterfallSpawnY);
+ waterfallParticlesContainer.addChild(particle);
+ activeWaterfallParticles.push(particle);
}
- function cleanupLevelSelectShadowFish() {
- if (shadowFishSpawnTimerId !== null) {
- LK.clearInterval(shadowFishSpawnTimerId);
- shadowFishSpawnTimerId = null;
- }
- activeShadowFish.forEach(function (fish) {
- if (fish && !fish.destroyed) {
- fish.destroy();
- }
- });
- activeShadowFish = [];
- if (shadowFishContainer && !shadowFishContainer.destroyed) {
- // shadowFishContainer.removeChildren(); // Optionally clear if only for these
- }
- }
- // Start timer for shadow fish
- if (levelSelectElements && levelSelectElements.shadowFishSpawnTimerId) {
- LK.clearInterval(levelSelectElements.shadowFishSpawnTimerId);
- }
+ // Start timers
+ rippleSpawnTimerId = LK.setInterval(spawnRippleEffect, RIPPLE_SPAWN_INTERVAL_MS);
+ homeIslandRippleSpawnTimerId = LK.setInterval(spawnHomeIslandRippleEffect, HOME_ISLAND_RIPPLE_SPAWN_INTERVAL_MS);
+ levelSelectScreenWaveSpawnTimerId = LK.setInterval(spawnLevelSelectScreenWaveEffect, SCREEN_WAVE_SPAWN_INTERVAL_MS);
+ waterfallSpawnTimerId = LK.setInterval(spawnWaterfallParticleEffect, WATERFALL_SPAWN_INTERVAL_MS);
+ shallowWatersNodeBubbleSpawnTimerId = LK.setInterval(spawnShallowWatersNodeBubbleEffect, SHALLOW_NODE_BUBBLE_SPAWN_INTERVAL_MS);
+ seagullSpawnTimerId = LK.setInterval(spawnSeagullEffect, SEAGULL_SPAWN_INTERVAL_MS);
+ levelSelectCloudSpawnTimerId = LK.setInterval(spawnLevelSelectCloudEffect, LEVEL_SELECT_CLOUD_SPAWN_INTERVAL_MS);
shadowFishSpawnTimerId = LK.setInterval(spawnShadowFishEffect, SHADOW_FISH_SPAWN_INTERVAL_MS);
- // Update node unlock status based on game state
+ function cleanupAllParticles() {
+ rippleSpawnTimerId = clearTimer(rippleSpawnTimerId, true);
+ homeIslandRippleSpawnTimerId = clearTimer(homeIslandRippleSpawnTimerId, true);
+ levelSelectScreenWaveSpawnTimerId = clearTimer(levelSelectScreenWaveSpawnTimerId, true);
+ waterfallSpawnTimerId = clearTimer(waterfallSpawnTimerId, true);
+ shallowWatersNodeBubbleSpawnTimerId = clearTimer(shallowWatersNodeBubbleSpawnTimerId, true);
+ seagullSpawnTimerId = clearTimer(seagullSpawnTimerId, true);
+ levelSelectCloudSpawnTimerId = clearTimer(levelSelectCloudSpawnTimerId, true);
+ shadowFishSpawnTimerId = clearTimer(shadowFishSpawnTimerId, true);
+ cleanupParticleArray(activeRipples, ripplesContainer);
+ cleanupParticleArray(activeHomeIslandRipples, homeIslandRipplesContainer);
+ cleanupParticleArray(levelSelectScreenWavesArray, ripplesContainer);
+ cleanupParticleArray(activeWaterfallParticles, waterfallParticlesContainer);
+ cleanupParticleArray(activeShallowWatersNodeBubbles, shallowWatersNodeBubblesContainer);
+ cleanupParticleArray(activeSeagulls, seagullsContainer);
+ cleanupParticleArray(activeLevelSelectClouds, levelSelectCloudContainer);
+ cleanupParticleArray(activeShadowFish, shadowFishContainer);
+ }
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
+ nodes.shop.unlocked = GameState.currentDepth >= 1;
+ nodes.restaurant.unlocked = GameState.currentDepth >= 2;
}
- // 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
+ var dotCount = Math.floor(distance / 20);
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
}));
- // 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
}));
}
- // Store reference for input handling
- nodeGfx.nodeKey = nodeKey; // Attach nodeKey to the clickable graphic
+ nodeGfx.nodeKey = nodeKey;
var displayName = getNodeDisplayName(nodeKey);
var nameTextFill = 0xFFFFFF;
if (nodeKey === 'restaurant' || nodeKey === 'shop') {
- nameTextFill = 0xFFD700; // Yellow
+ nameTextFill = 0xFFD700;
}
- // Add a background behind the Ocean Restaurant and Fishing Shop node labels
var labelY = node.y + nodeGfx.height / 2 + 15;
var labelHeight = 80;
var labelBg = null;
var nameText = nodeContainer.addChild(new Text2(displayName, {
@@ -3463,35 +2723,31 @@
stroke: 0x000000,
strokeThickness: 2,
align: 'center'
}));
- nameText.anchor.set(0.5, 0); // Anchor top-middle
- nameText.x = node.x; // Position using node's global X
- nameText.y = labelY; // Position 15px below the node icon's bottom edge
- nameText.visible = true; // Always visible
+ nameText.anchor.set(0.5, 0);
+ nameText.x = node.x;
+ nameText.y = labelY;
+ nameText.visible = true;
if (nodeKey === 'restaurant' || nodeKey === 'shop') {
- // Calculate width based on text width, add padding
var labelWidth = nameText.width + 60;
labelBg = nodeContainer.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 0,
x: node.x,
y: labelY,
- // Slightly above text for padding
width: labelWidth,
height: labelHeight,
color: 0x222222,
alpha: 0.85
}));
- // Move background behind text in display order
if (labelBg && nameText) {
nodeContainer.setChildIndex(labelBg, nodeContainer.children.indexOf(nameText));
}
}
nodeNameTexts[nodeKey] = nameText;
});
}
- // Move boat to new node
function moveBoatToNode(targetNodeKey) {
if (boatMoving || !MAP_CONFIG.NODES[targetNodeKey].unlocked) {
return;
}
@@ -3503,59 +2759,48 @@
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)
+ boatBaseY = playerBoat.y;
tween(playerBoat, {
rotation: 0
}, {
duration: 500,
easing: tween.easeOut
});
- // 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
+ } else if (MAP_CONFIG.NODES[targetNodeKey].type === 'shop') {
console.log("Arrived at Shop");
} else if (MAP_CONFIG.NODES[targetNodeKey].type === 'restaurant') {
- // Placeholder for restaurant logic
console.log("Arrived at Restaurant");
}
}
});
}
- // 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
@@ -3567,27 +2812,19 @@
clearSongSelectionElements();
}
});
}
- // Create song selection UI elements
function createSongSelectionElements() {
- // Clear existing elements
clearSongSelectionElements();
var overlayCenterX = 1024;
- var overlayCenterY = 1366; // Updated to match new overlayBg y (2732 / 2)
- // Song navigation arrows
+ var overlayCenterY = 1366;
songElements.leftArrow = songOverlayContainer.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: overlayCenterX - 650,
- // Moved further left
- // Relative to overlay center
y: overlayCenterY,
- // Vertically centered with overlay
width: 120,
- // Increased size
height: 120,
- // Increased size
tint: 0x666666
}));
songElements.leftArrowText = songOverlayContainer.addChild(new Text2('<', {
size: 80,
@@ -3599,16 +2836,11 @@
songElements.rightArrow = songOverlayContainer.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: overlayCenterX + 650,
- // Moved further right
- // Relative to overlay center
y: overlayCenterY,
- // Vertically centered with overlay
width: 120,
- // Increased size
height: 120,
- // Increased size
tint: 0x666666
}));
songElements.rightArrowText = songOverlayContainer.addChild(new Text2('>', {
size: 80,
@@ -3616,40 +2848,31 @@
}));
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: 110,
fill: 0xFFFFFF
}));
songElements.songTitle.anchor.set(0.5, 0.5);
songElements.songTitle.x = overlayCenterX;
- songElements.songTitle.y = overlayCenterY - 300; // Adjusted Y for new layout
+ songElements.songTitle.y = overlayCenterY - 300;
songElements.songInfo = songOverlayContainer.addChild(new Text2('BPM: 120 | Duration: 2:00', {
size: 70,
fill: 0xCCCCCC
}));
songElements.songInfo.anchor.set(0.5, 0.5);
songElements.songInfo.x = overlayCenterX;
- songElements.songInfo.y = overlayCenterY - 200; // Adjusted Y for new layout
- // Container for fish display
+ songElements.songInfo.y = overlayCenterY - 200;
songElements.fishDisplayContainer = songOverlayContainer.addChild(new Container());
songElements.fishDisplayContainer.x = overlayCenterX;
- // Position its top edge relative to songInfo
- // Assuming songInfo text has an approximate height of 70 (due to font size)
- // songInfo.y is its center, so its bottom edge is roughly songInfo.y + 35
- songElements.fishDisplayContainer.y = songElements.songInfo.y + 35 + 30; // songInfo bottom + spacing
- // Play/Buy button
- // Position play button relative to where the fish display will roughly end
- // Assuming fishDisplayContainer will be about 250px tall. Its top is at songElements.fishDisplayContainer.y
- var fishDisplayBottomY = songElements.fishDisplayContainer.y + 260; // Estimated bottom
+ songElements.fishDisplayContainer.y = songElements.songInfo.y + 35 + 30;
+ var fishDisplayBottomY = songElements.fishDisplayContainer.y + 260;
songElements.playButton = songOverlayContainer.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: overlayCenterX,
y: fishDisplayBottomY + 25 + 50,
- // fish_display_bottom + spacing + half_button_height
width: 400,
height: 100
}));
songElements.playButtonText = songOverlayContainer.addChild(new Text2('PLAY', {
@@ -3659,23 +2882,20 @@
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 || !songElements.fishDisplayContainer) {
return;
- } // Elements not created yet
- // Clear previous fish display
+ }
songElements.fishDisplayContainer.removeChildren();
var depthConfig = GAME_CONFIG.DEPTHS[selectedDepth];
if (!depthConfig || !depthConfig.songs || !depthConfig.songs[selectedSong]) {
console.error("Invalid depth or song index:", selectedDepth, selectedSong);
@@ -3685,9 +2905,8 @@
var patternConfig = GAME_CONFIG.PATTERNS[songConfig.pattern];
var owned = GameState.hasSong(selectedDepth, selectedSong);
var overlayCenterX = 1024;
var overlayCenterY = 1366;
- // Recreate song title
if (songElements.songTitle) {
songElements.songTitle.destroy();
}
songElements.songTitle = songOverlayContainer.addChild(new Text2(songConfig.name, {
@@ -3698,9 +2917,8 @@
}));
songElements.songTitle.anchor.set(0.5, 0.5);
songElements.songTitle.x = overlayCenterX;
songElements.songTitle.y = overlayCenterY - 300;
- // Recreate song info
if (songElements.songInfo) {
songElements.songInfo.destroy();
}
songElements.songInfo = songOverlayContainer.addChild(new Text2('BPM: ' + songConfig.bpm + ' | Duration: ' + formatTime(songConfig.duration), {
@@ -3711,9 +2929,8 @@
}));
songElements.songInfo.anchor.set(0.5, 0.5);
songElements.songInfo.x = overlayCenterX;
songElements.songInfo.y = overlayCenterY - 200;
- // --- Helper to get fish distribution ---
function getFishDistributionForSong(songCfg, depthCfg, patternCfg, currentSelectedDepth) {
var distribution = [];
var fishNames = {
anchovy: "Anchovy",
@@ -3835,9 +3052,8 @@
}).sort(function (a, b) {
return b.percentage - a.percentage;
});
}
- // --- End Helper ---
var fishToDisplay = getFishDistributionForSong(songConfig, depthConfig, patternConfig, selectedDepth);
var itemWidth = 180;
var itemHeight = 130;
var horizontalSpacing = 40;
@@ -3877,43 +3093,35 @@
percentText.anchor.set(0.5, 0);
percentText.x = itemWidth / 2;
percentText.y = nameText.y + nameText.height + 3;
}
- // Update play/buy button text and tint
if (owned) {
songElements.playButtonText.setText('PLAY');
- songElements.playButton.tint = 0x1976d2; // Blue for play
+ songElements.playButton.tint = 0x1976d2;
} else {
songElements.playButtonText.setText('BUY ($' + songConfig.cost + ')');
- songElements.playButton.tint = GameState.money >= songConfig.cost ? 0x2e7d32 : 0x666666; // Green if affordable, gray if not
+ songElements.playButton.tint = GameState.money >= songConfig.cost ? 0x2e7d32 : 0x666666;
}
- // Update arrow states
songElements.leftArrow.tint = selectedSong > 0 ? 0x1976d2 : 0x666666;
songElements.rightArrow.tint = selectedSong < depthConfig.songs.length - 1 ? 0x1976d2 : 0x666666;
}
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;
@@ -3925,205 +3133,152 @@
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;
- // Store the boat's current node key before going to fishing
GameState.lastLevelSelectNodeKey = currentNode;
showScreen('fishing');
} else {
- // Try to buy song
if (GameState.buySong(selectedDepth, selectedSong)) {
- updateSongDisplay(); // Reflect purchase
- updateMapDisplay(); // Update money on map screen
+ updateSongDisplay();
+ updateMapDisplay();
}
}
return;
}
- return; // Don't process map clicks while overlay is open
+ return;
}
- // 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
+ var nodeGfx = nodeContainerInstance.children[0];
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('$' + GameState.money);
}
- // Initialize map view
updateMapDisplay();
- // Ensure song overlay is on the highest layer
if (songOverlayContainer && levelSelectScreen.children.indexOf(songOverlayContainer) !== -1) {
levelSelectScreen.setChildIndex(songOverlayContainer, levelSelectScreen.children.length - 1);
}
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
- updateRipples: updateRipplesCleanup,
- // Boat ripples
- cleanupRipples: cleanupLevelSelectRipples,
- // Boat ripples
+ updateRipples: function updateRipples() {
+ updateParticleArray(activeRipples);
+ },
+ cleanupRipples: cleanupAllParticles,
ripplesContainer: ripplesContainer,
- // Boat ripples container
activeRipples: activeRipples,
- // Boat ripples array
rippleSpawnTimerId: rippleSpawnTimerId,
- // Boat ripple timer ID
- // NEW: Home Island Ripple properties
- updateHomeIslandRipples: updateHomeIslandRipplesCleanup,
- cleanupHomeIslandRipples: cleanupLevelSelectHomeIslandRipples,
+ updateHomeIslandRipples: function updateHomeIslandRipples() {
+ updateParticleArray(activeHomeIslandRipples);
+ },
+ cleanupHomeIslandRipples: cleanupAllParticles,
homeIslandRipplesContainer: homeIslandRipplesContainer,
- // Optional, if needed externally
activeHomeIslandRipples: activeHomeIslandRipples,
- // Optional, if needed externally
homeIslandRippleSpawnTimerId: homeIslandRippleSpawnTimerId,
- // Store timer ID for cleanup
- // NEW: Ambient Screen Wave properties
- updateScreenWaves: updateLevelSelectScreenWavesCleanup,
- cleanupScreenWaves: cleanupLevelSelectScreenWaves,
- // activeScreenWaves: levelSelectScreenWavesArray, // Not typically needed externally if update/cleanup handles them
+ updateScreenWaves: function updateScreenWaves() {
+ updateParticleArray(levelSelectScreenWavesArray);
+ },
+ cleanupScreenWaves: cleanupAllParticles,
screenWaveSpawnTimerId: levelSelectScreenWaveSpawnTimerId,
- // For potential external re-initialization checks
- // Shallow Waters Node Bubbles properties
- updateShallowWatersNodeBubbles: updateShallowWatersNodeBubblesCleanup,
- cleanupShallowWatersNodeBubbles: cleanupLevelSelectShallowWatersNodeBubbles,
+ updateShallowWatersNodeBubbles: function updateShallowWatersNodeBubbles() {
+ updateParticleArray(activeShallowWatersNodeBubbles);
+ },
+ cleanupShallowWatersNodeBubbles: cleanupAllParticles,
shallowWatersNodeBubbleSpawnTimerId: shallowWatersNodeBubbleSpawnTimerId,
- // NEW: Seagull Particle properties
- updateSeagulls: updateSeagullsCleanup,
- cleanupSeagulls: cleanupLevelSelectSeagulls,
+ updateSeagulls: function updateSeagulls() {
+ updateParticleArray(activeSeagulls);
+ },
+ cleanupSeagulls: cleanupAllParticles,
seagullSpawnTimerId: seagullSpawnTimerId,
- // NEW: Level Select Cloud properties
levelSelectCloudContainer: levelSelectCloudContainer,
- updateLevelSelectClouds: updateLevelSelectCloudsCleanup,
- cleanupLevelSelectClouds: cleanupLevelSelectClouds,
+ updateLevelSelectClouds: function updateLevelSelectClouds() {
+ updateParticleArray(activeLevelSelectClouds);
+ },
+ cleanupLevelSelectClouds: cleanupAllParticles,
levelSelectCloudSpawnTimerId: levelSelectCloudSpawnTimerId,
- // NEW: Waterfall Particle properties for Home Island
- updateWaterfallParticles: updateWaterfallParticlesCleanup,
- cleanupWaterfallParticles: cleanupLevelSelectWaterfallParticles,
+ updateWaterfallParticles: function updateWaterfallParticles() {
+ updateParticleArray(activeWaterfallParticles);
+ },
+ cleanupWaterfallParticles: cleanupAllParticles,
waterfallSpawnTimerId: waterfallSpawnTimerId,
- // NEW: Shadow Fish Particle properties
- updateShadowFish: updateShadowFishCleanup,
- cleanupLevelSelectShadowFish: cleanupLevelSelectShadowFish,
+ updateShadowFish: function updateShadowFish() {
+ updateParticleArray(activeShadowFish);
+ },
+ cleanupLevelSelectShadowFish: cleanupAllParticles,
shadowFishSpawnTimerId: shadowFishSpawnTimerId,
- // Ambient Sounds Control
startLevelSelectAmbientSounds: startLevelSelectAmbientSounds,
stopLevelSelectAmbientSounds: stopLevelSelectAmbientSounds,
- // Method to restart particle timers
restartParticleTimers: function restartParticleTimers() {
- // Clear existing timers by calling their respective cleanup functions
- // which handle clearing the LK.clearInterval and the active particle arrays.
- // The cleanup functions use the local timerId variables from createLevelSelectScreen's scope.
- cleanupLevelSelectRipples();
- cleanupLevelSelectHomeIslandRipples();
- cleanupLevelSelectScreenWaves();
- cleanupLevelSelectWaterfallParticles();
- cleanupLevelSelectShallowWatersNodeBubbles();
- cleanupLevelSelectSeagulls();
- cleanupLevelSelectClouds();
- cleanupLevelSelectShadowFish();
- // Restart timers, assign to local vars (which cleanup functions use)
- // AND update properties on `this` (the levelSelectElements object)
- // so the returned object also has the latest timer IDs.
+ cleanupAllParticles();
rippleSpawnTimerId = LK.setInterval(spawnRippleEffect, RIPPLE_SPAWN_INTERVAL_MS);
- this.rippleSpawnTimerId = rippleSpawnTimerId;
homeIslandRippleSpawnTimerId = LK.setInterval(spawnHomeIslandRippleEffect, HOME_ISLAND_RIPPLE_SPAWN_INTERVAL_MS);
- this.homeIslandRippleSpawnTimerId = homeIslandRippleSpawnTimerId;
levelSelectScreenWaveSpawnTimerId = LK.setInterval(spawnLevelSelectScreenWaveEffect, SCREEN_WAVE_SPAWN_INTERVAL_MS);
- this.levelSelectScreenWaveSpawnTimerId = levelSelectScreenWaveSpawnTimerId;
waterfallSpawnTimerId = LK.setInterval(spawnWaterfallParticleEffect, WATERFALL_SPAWN_INTERVAL_MS);
- this.waterfallSpawnTimerId = waterfallSpawnTimerId;
shallowWatersNodeBubbleSpawnTimerId = LK.setInterval(spawnShallowWatersNodeBubbleEffect, SHALLOW_NODE_BUBBLE_SPAWN_INTERVAL_MS);
- this.shallowWatersNodeBubbleSpawnTimerId = shallowWatersNodeBubbleSpawnTimerId;
seagullSpawnTimerId = LK.setInterval(spawnSeagullEffect, SEAGULL_SPAWN_INTERVAL_MS);
- this.seagullSpawnTimerId = seagullSpawnTimerId;
levelSelectCloudSpawnTimerId = LK.setInterval(spawnLevelSelectCloudEffect, LEVEL_SELECT_CLOUD_SPAWN_INTERVAL_MS);
- this.levelSelectCloudSpawnTimerId = levelSelectCloudSpawnTimerId;
shadowFishSpawnTimerId = LK.setInterval(spawnShadowFishEffect, SHADOW_FISH_SPAWN_INTERVAL_MS);
- this.shadowFishSpawnTimerId = shadowFishSpawnTimerId;
}
};
}
/****
* 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 waterSurfaceSegments = [];
+ var waterSurfaceSegmentsBlueTemp = [];
+ var waterSurfaceSegmentsWhiteTemp = [];
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,
@@ -4134,12 +3289,10 @@
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,
@@ -4150,42 +3303,35 @@
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
+ waterSurfaceSegments.push(waterSurfaceSegmentsBlueTemp[i]);
}
for (var i = 0; i < waterSurfaceSegmentsWhiteTemp.length; i++) {
fishingScreen.addChild(waterSurfaceSegmentsWhiteTemp[i]);
- waterSurfaceSegments.push(waterSurfaceSegmentsWhiteTemp[i]); // Also add to the main array for cleanup
+ waterSurfaceSegments.push(waterSurfaceSegmentsWhiteTemp[i]);
}
- // 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,
@@ -4225,14 +3371,12 @@
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;
}
@@ -4305,60 +3449,33 @@
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
+ var allSegments = waterSurfaceSegments;
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
- });
- };
+ var waveAnim = createWaveAnimation(currentLocalSegment, WAVE_AMPLITUDE, WAVE_HALF_PERIOD_MS);
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
+ onFinish: waveAnim.down
});
}, 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
@@ -4375,9 +3492,8 @@
});
rockBoatLeft();
}
}
- // UI elements (from existing)
var scoreText = new Text2('Score: 0', {
size: 70,
fill: 0xFFFFFF
});
@@ -4430,107 +3546,81 @@
/****
* Initialize Screen Elements
****/
var titleElements = createTitleScreen();
-titleElements.tutorialButtonGfx = titleElements.tutorialButton; // Store the graphical button for compatibility
+titleElements.tutorialButtonGfx = titleElements.tutorialButton;
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 tutorialLaneHighlights = [];
var fishArray = [];
var bubblesArray = [];
-var bubbleContainer; // Container for bubbles, initialized in createFishingScreen
+var bubbleContainer;
var musicNotesArray = [];
-var musicNotesContainer; // Container for music notes
-var laneBrackets = []; // Stores the visual bracket pairs for each lane
+var musicNotesContainer;
+var laneBrackets = [];
var musicNoteSpawnCounter = 0;
-var MUSIC_NOTE_SPAWN_INTERVAL_TICKS = 45; // Spawn a note roughly every 0.75 seconds
-// Ocean Bubbles (ambient background)
+var MUSIC_NOTE_SPAWN_INTERVAL_TICKS = 45;
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 OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS = 40;
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 SEAWEED_SPAWN_INTERVAL_TICKS = 120;
+var MAX_SEAWEED_COUNT = 8;
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 CLOUD_SPAWN_INTERVAL_TICKS = 180;
+var MAX_CLOUD_COUNT = 5;
var titleScreenOceanBubblesArray = [];
-var titleScreenOceanBubbleContainer; // Will be initialized in createTitleScreen
+var titleScreenOceanBubbleContainer;
var titleScreenOceanBubbleSpawnCounter = 0;
var titleScreenSeaweedArray = [];
-var titleScreenSeaweedContainer; // Will be initialized in createTitleScreen
+var titleScreenSeaweedContainer;
var titleScreenSeaweedSpawnCounter = 0;
var titleScreenCloudArray = [];
-var titleScreenCloudContainer; // Will be initialized in createTitleScreen
+var titleScreenCloudContainer;
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)
+ touchStartTime: 0
};
-// 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
+ var boundary_lane0_lane1 = (GAME_CONFIG.LANES[0].y + GAME_CONFIG.LANES[1].y) / 2;
+ var boundary_lane1_lane2 = (GAME_CONFIG.LANES[1].y + GAME_CONFIG.LANES[2].y) / 2;
if (y < boundary_lane0_lane1) {
- return 0; // Top lane (e.g., shallow)
+ return 0;
} else if (y < boundary_lane1_lane2) {
- return 1; // Middle lane (e.g., medium)
+ return 1;
} else {
- return 2; // Bottom lane (e.g., deep)
+ return 2;
}
}
-// 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
+ var indicator = new FeedbackIndicator(type);
+ indicator.x = fishingElements.hook.x;
+ indicator.y = feedbackY;
fishingScreen.addChild(indicator);
- indicator.show(); // Triggers the animation and self-destruction
+ indicator.show();
}
-// 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,
@@ -4541,39 +3631,31 @@
}, {
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
+ checkCatch(getTouchLane(y));
}
- return; // Tutorial catch handled
+ return;
}
- // Existing game active check
if (!GameState.gameActive) {
return;
}
- var currentTime = LK.ticks * (1000 / 60); // Current time in ms
+ var currentTime = LK.ticks * (1000 / 60);
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;
}
@@ -4582,91 +3664,22 @@
* Screen Management
****/
function showScreen(screenName) {
var previousScreen = GameState.currentScreen;
- // --- Cleanup logic for the 'previousScreen' ---
if (previousScreen === 'title' && titleElements) {
- tween.stop(titleElements.titleAnimationGroup);
- tween.stop(titleElements.blackOverlay);
- if (titleElements.titleImage) {
- tween.stop(titleElements.titleImage);
- }
- if (titleElements.logo) {
- tween.stop(titleElements.logo);
- }
- if (titleElements.subtitle) {
- tween.stop(titleElements.subtitle);
- }
- tween.stop(titleElements.startButton);
- tween.stop(titleElements.tutorialButton);
- if (titleSeagullSoundTimer) {
- LK.clearTimeout(titleSeagullSoundTimer);
- titleSeagullSoundTimer = null;
- }
- if (titleBoatSoundTimer) {
- LK.clearTimeout(titleBoatSoundTimer);
- titleBoatSoundTimer = null;
- }
+ stopTweens([titleElements.titleAnimationGroup, titleElements.blackOverlay, titleElements.titleImage, titleElements.startButton, titleElements.tutorialButton]);
if (titleElements.titleWaterSurfaceSegments) {
- titleElements.titleWaterSurfaceSegments.forEach(function (segment) {
- if (segment && !segment.destroyed) {
- tween.stop(segment);
- }
- });
+ stopTweens(titleElements.titleWaterSurfaceSegments);
}
- 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 = [];
+ titleSeagullSoundTimer = clearTimer(titleSeagullSoundTimer, false);
+ titleBoatSoundTimer = clearTimer(titleBoatSoundTimer, false);
+ cleanupParticleArray(titleScreenOceanBubblesArray, titleScreenOceanBubbleContainer);
+ cleanupParticleArray(titleScreenSeaweedArray, titleScreenSeaweedContainer);
+ cleanupParticleArray(titleScreenCloudArray, titleScreenCloudContainer);
} else if (previousScreen === 'levelSelect' && screenName !== 'levelSelect') {
if (levelSelectElements && typeof levelSelectElements.cleanupRipples === 'function') {
levelSelectElements.cleanupRipples();
}
- if (levelSelectElements && typeof levelSelectElements.cleanupHomeIslandRipples === 'function') {
- levelSelectElements.cleanupHomeIslandRipples();
- }
- if (levelSelectElements && typeof levelSelectElements.cleanupScreenWaves === 'function') {
- levelSelectElements.cleanupScreenWaves();
- }
- if (levelSelectElements && typeof levelSelectElements.cleanupShallowWatersNodeBubbles === 'function') {
- levelSelectElements.cleanupShallowWatersNodeBubbles();
- }
- if (levelSelectElements && typeof levelSelectElements.cleanupSeagulls === 'function') {
- levelSelectElements.cleanupSeagulls();
- }
- if (levelSelectElements && typeof levelSelectElements.cleanupLevelSelectClouds === 'function') {
- levelSelectElements.cleanupLevelSelectClouds();
- }
- if (levelSelectElements && typeof levelSelectElements.cleanupWaterfallParticles === 'function') {
- levelSelectElements.cleanupWaterfallParticles();
- }
- // NEW: Cleanup shadow fish
- if (levelSelectElements && typeof levelSelectElements.cleanupLevelSelectShadowFish === 'function') {
- levelSelectElements.cleanupLevelSelectShadowFish();
- }
if (levelSelectElements && typeof levelSelectElements.stopLevelSelectAmbientSounds === 'function') {
levelSelectElements.stopLevelSelectAmbientSounds();
}
}
@@ -4689,114 +3702,71 @@
tutorialLaneHighlights = [];
}
GameState.tutorialMode = false;
}
- // --- Transition Logic: Title to LevelSelect with Fade ---
if (previousScreen === 'title' && screenName === 'levelSelect') {
globalFadeOverlay.alpha = 0;
globalFadeOverlay.visible = true;
if (game.children.indexOf(globalFadeOverlay) !== -1) {
- // Ensure overlay is on top
game.setChildIndex(globalFadeOverlay, game.children.length - 1);
}
tween(globalFadeOverlay, {
alpha: 1
}, {
- // Fade TO black
duration: 500,
- // Duration of fade-out
easing: tween.easeOut,
onFinish: function onFinish() {
- // Old screen is now blacked out
titleScreen.visible = false;
- // --- START NEW RESET LOGIC ---
- // Cleanup existing level select elements and timers if they exist
if (levelSelectElements) {
- if (typeof levelSelectElements.cleanupRipples === 'function') levelSelectElements.cleanupRipples();
- if (typeof levelSelectElements.cleanupHomeIslandRipples === 'function') levelSelectElements.cleanupHomeIslandRipples();
- if (typeof levelSelectElements.cleanupScreenWaves === 'function') levelSelectElements.cleanupScreenWaves();
- if (typeof levelSelectElements.cleanupShallowWatersNodeBubbles === 'function') levelSelectElements.cleanupShallowWatersNodeBubbles();
- if (typeof levelSelectElements.cleanupSeagulls === 'function') levelSelectElements.cleanupSeagulls();
- if (typeof levelSelectElements.cleanupLevelSelectClouds === 'function') levelSelectElements.cleanupLevelSelectClouds();
- if (typeof levelSelectElements.cleanupWaterfallParticles === 'function') levelSelectElements.cleanupWaterfallParticles();
- if (typeof levelSelectElements.cleanupLevelSelectShadowFish === 'function') levelSelectElements.cleanupLevelSelectShadowFish();
- if (typeof levelSelectElements.stopLevelSelectAmbientSounds === 'function') levelSelectElements.stopLevelSelectAmbientSounds();
+ if (typeof levelSelectElements.cleanupRipples === 'function') {
+ levelSelectElements.cleanupRipples();
+ }
+ if (typeof levelSelectElements.stopLevelSelectAmbientSounds === 'function') {
+ levelSelectElements.stopLevelSelectAmbientSounds();
+ }
}
- // Re-create the level select screen elements
- // This will reset boat position and restart particle timers internally
levelSelectElements = createLevelSelectScreen();
- // --- END NEW RESET LOGIC ---
- // Set up and show new screen (still under black overlay)
levelSelectScreen.visible = true;
- GameState.currentScreen = 'levelSelect'; // Update state
- // Initialize the newly created elements
+ GameState.currentScreen = 'levelSelect';
if (levelSelectElements && typeof levelSelectElements.updateMapDisplay === 'function') {
levelSelectElements.updateMapDisplay();
}
if (levelSelectElements && typeof levelSelectElements.startLevelSelectAmbientSounds === 'function') {
levelSelectElements.startLevelSelectAmbientSounds();
}
- // Fade FROM black
tween(globalFadeOverlay, {
alpha: 0
}, {
duration: 500,
- // Duration of fade-in
easing: tween.easeIn,
onFinish: function onFinish() {
globalFadeOverlay.visible = false;
}
});
}
});
- return; // Handled by fade transition
+ return;
}
- // --- Default Instant Screen Switching ---
titleScreen.visible = false;
levelSelectScreen.visible = false;
fishingScreen.visible = false;
resultsScreen.visible = false;
GameState.currentScreen = screenName;
switch (screenName) {
case 'title':
titleScreen.visible = true;
- // Copied from original:
- var _scheduleNextSeagullSound = function scheduleNextSeagullSound() {
- if (GameState.currentScreen !== 'title') {
- if (titleSeagullSoundTimer) {
- LK.clearTimeout(titleSeagullSoundTimer);
- titleSeagullSoundTimer = null;
- }
- return;
- }
- var randomDelay = 5000 + Math.random() * 10000;
- titleSeagullSoundTimer = LK.setTimeout(function () {
- if (GameState.currentScreen !== 'title') {
- return;
- }
- var seagullSounds = ['seagull1', 'seagull2', 'seagull3'];
- var randomSoundId = seagullSounds[Math.floor(Math.random() * seagullSounds.length)];
- LK.getSound(randomSoundId).play();
- _scheduleNextSeagullSound();
- }, randomDelay);
- };
- var _scheduleNextBoatSound = function scheduleNextBoatSound() {
- if (GameState.currentScreen !== 'title') {
- if (titleBoatSoundTimer) {
- LK.clearTimeout(titleBoatSoundTimer);
- titleBoatSoundTimer = null;
- }
- return;
- }
- var fixedBoatSoundInterval = 6000;
- titleBoatSoundTimer = LK.setTimeout(function () {
- if (GameState.currentScreen !== 'title') {
- return;
- }
- LK.getSound('boatsounds').play();
- _scheduleNextBoatSound();
- }, fixedBoatSoundInterval);
- };
+ var seagullScheduler = createAmbientSoundScheduler({
+ screenName: 'title',
+ sounds: ['seagull1', 'seagull2', 'seagull3'],
+ baseDelay: 5000,
+ variance: 10000
+ });
+ var boatScheduler = createAmbientSoundScheduler({
+ screenName: 'title',
+ sounds: 'boatsounds',
+ baseDelay: 6000,
+ variance: 0
+ });
if (titleElements.startTitleWaterSurfaceAnimation) {
titleElements.startTitleWaterSurfaceAnimation();
}
if (titleElements.moveTitleBoatGroupUp) {
@@ -4808,10 +3778,10 @@
var initialSeagullSounds = ['seagull1', 'seagull2', 'seagull3'];
var initialRandomSoundId = initialSeagullSounds[Math.floor(Math.random() * initialSeagullSounds.length)];
LK.getSound(initialRandomSoundId).play();
LK.getSound('boatsounds').play();
- _scheduleNextSeagullSound();
- _scheduleNextBoatSound();
+ seagullScheduler.start();
+ boatScheduler.start();
titleScreenOceanBubbleSpawnCounter = 0;
titleScreenSeaweedSpawnCounter = 0;
titleScreenCloudSpawnCounter = 0;
var ZOOM_DURATION = 8000;
@@ -4826,14 +3796,8 @@
titleElements.blackOverlay.alpha = 1;
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;
tween(titleElements.titleAnimationGroup, {
scaleX: 1.8,
@@ -4857,21 +3821,8 @@
}, {
duration: 1200,
easing: tween.easeOut
});
- } else if (titleElements.logo && titleElements.subtitle) {
- tween(titleElements.logo, {
- alpha: 1
- }, {
- duration: 1200,
- easing: tween.easeOut
- });
- tween(titleElements.subtitle, {
- alpha: 1
- }, {
- duration: 1200,
- easing: tween.easeOut
- });
}
}, TEXT_DELAY);
LK.setTimeout(function () {
tween(titleElements.startButton, {
@@ -4891,58 +3842,43 @@
}, BUTTON_DELAY);
break;
case 'levelSelect':
if (previousScreen === 'results') {
- // Cleanup existing level select elements and timers
if (levelSelectElements) {
- if (typeof levelSelectElements.cleanupRipples === 'function') levelSelectElements.cleanupRipples();
- if (typeof levelSelectElements.cleanupHomeIslandRipples === 'function') levelSelectElements.cleanupHomeIslandRipples();
- if (typeof levelSelectElements.cleanupScreenWaves === 'function') levelSelectElements.cleanupScreenWaves();
- if (typeof levelSelectElements.cleanupShallowWatersNodeBubbles === 'function') levelSelectElements.cleanupShallowWatersNodeBubbles();
- if (typeof levelSelectElements.cleanupSeagulls === 'function') levelSelectElements.cleanupSeagulls();
- if (typeof levelSelectElements.cleanupLevelSelectClouds === 'function') levelSelectElements.cleanupLevelSelectClouds();
- if (typeof levelSelectElements.cleanupWaterfallParticles === 'function') levelSelectElements.cleanupWaterfallParticles();
- if (typeof levelSelectElements.cleanupLevelSelectShadowFish === 'function') levelSelectElements.cleanupLevelSelectShadowFish();
+ if (typeof levelSelectElements.cleanupRipples === 'function') {
+ levelSelectElements.cleanupRipples();
+ }
if (typeof levelSelectElements.stopLevelSelectAmbientSounds === 'function') {
levelSelectElements.stopLevelSelectAmbientSounds();
}
- // Restart particle timers using the new method
if (typeof levelSelectElements.restartParticleTimers === 'function') {
levelSelectElements.restartParticleTimers();
}
- // Update map display (money, nodes, etc.)
if (typeof levelSelectElements.updateMapDisplay === 'function') {
levelSelectElements.updateMapDisplay();
}
- // Restart ambient sounds
if (typeof levelSelectElements.startLevelSelectAmbientSounds === 'function') {
levelSelectElements.startLevelSelectAmbientSounds();
}
}
- // DO NOT RECREATE: levelSelectElements = createLevelSelectScreen();
- // This preserves boat position. Particles and sounds are handled above.
} else if (previousScreen !== 'title') {
- // This 'else if' covers transitions from other screens (not 'title', which has its own handler, and not 'results')
- // to 'levelSelect' where we might not want a full recreate.
- // For these cases, we just update the display and ensure sounds are running.
if (levelSelectElements && typeof levelSelectElements.updateMapDisplay === 'function') {
levelSelectElements.updateMapDisplay();
}
if (levelSelectElements && typeof levelSelectElements.startLevelSelectAmbientSounds === 'function') {
- levelSelectElements.startLevelSelectAmbientSounds(); // Ensure sounds are running
+ levelSelectElements.startLevelSelectAmbientSounds();
}
}
- // If previousScreen was 'title', its transition to 'levelSelect' is handled by a separate block higher up which includes a 'return'.
levelSelectScreen.visible = true;
break;
case 'fishing':
fishingScreen.visible = true;
playIntroAnimation();
break;
case 'results':
resultsScreen.visible = true;
- createResultsScreen(); // Content is created here now
- resultsScreen.alpha = 0; // Prepare for fade-in
+ createResultsScreen();
+ resultsScreen.alpha = 0;
tween(resultsScreen, {
alpha: 1
}, {
duration: 500,
@@ -4952,64 +3888,54 @@
}
if (screenName !== 'fishing' && GameState.tutorialMode) {
tutorialOverlayContainer.visible = false;
}
- // Tutorial System functions moved to global scope. (Comment is fine)
}
/****
* 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, {
@@ -5019,20 +3945,16 @@
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
+ GameState.gameActive = false;
+ createTutorialElements();
+ runTutorialStep();
} else {
- // Normal fishing session
startFishingSession();
}
}
});
@@ -5041,46 +3963,37 @@
* Level Select Logic
****/
function updateLevelSelectScreen() {
var elements = levelSelectElements;
- // Update money display
elements.moneyDisplay.setText('$' + 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
+ var tabStartY = 600;
+ var tabSpacing = 250;
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
+ height: 160
}));
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;
@@ -5097,32 +4010,27 @@
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
+ var minEarnings = Math.floor(depth.fishValue * 20);
+ var maxEarnings = Math.floor(depth.fishValue * 60);
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
+ elements.shopButton.tint = 0x666666;
}
function formatTime(ms) {
var seconds = Math.floor(ms / 1000);
var minutes = Math.floor(seconds / 60);
@@ -5132,10 +4040,9 @@
/****
* Fishing Game Logic
****/
function startFishingSession() {
- // Reset session state
- GameState.tutorialMode = false; // Ensure tutorial mode is off
+ GameState.tutorialMode = false;
GameState.sessionScore = 0;
GameState.sessionFishCaught = 0;
GameState.sessionFishSpawned = 0;
GameState.combo = 0;
@@ -5150,35 +4057,28 @@
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();
@@ -5188,31 +4088,27 @@
}
});
}
laneBrackets = [];
- var bracketAssetHeight = 150; // Height of the lanebracket asset
- var bracketAssetWidth = 75; // Width of the lanebracket asset
+ var bracketAssetHeight = 150;
+ var bracketAssetWidth = 75;
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({
@@ -5220,45 +4116,36 @@
right: rightBracket
});
}
}
- // Start music
var songConfig = GameState.getCurrentSongConfig();
- var musicIdToPlay = songConfig.musicId || 'rhythmTrack'; // Default to rhythmTrack if no specific id
+ var musicIdToPlay = songConfig.musicId || 'rhythmTrack';
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'
+ GameState.currentPlayingMusicInitialVolume = 1.0;
} else {
- // Default for 'rhythmTrack' or other unspecified tracks
- GameState.currentPlayingMusicInitialVolume = 0.8; // Volume defined in LK.init.music for 'rhythmTrack'
+ GameState.currentPlayingMusicInitialVolume = 0.8;
}
- LK.playMusic(GameState.currentPlayingMusicId); // Play the selected music track
+ LK.playMusic(GameState.currentPlayingMusicId);
}
function spawnFish(currentTimeForRegistration, options) {
- options = options || {}; // Ensure options is an object
+ options = options || {};
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
+ return null;
}
}
}
var laneIndex;
if (options.laneIndexToUse !== undefined) {
laneIndex = options.laneIndexToUse;
- PatternGenerator.lastLane = laneIndex; // Update generator's state if lane is forced
+ PatternGenerator.lastLane = laneIndex;
} else {
laneIndex = PatternGenerator.getNextLane();
}
var targetLane = GAME_CONFIG.LANES[laneIndex];
@@ -5277,21 +4164,21 @@
fishType = 'shallow';
fishValue = Math.floor(depthConfig.fishValue);
}
var fishSpeedValue = depthConfig.fishSpeed;
- var spawnSide; // -1 for left, 1 for right
+ var spawnSide;
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.spawnSide = spawnSide;
+ newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150;
newFish.y = targetLane.y;
- newFish.baseY = targetLane.y; // Set baseY for swimming animation
+ newFish.baseY = targetLane.y;
fishArray.push(newFish);
fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTimeForRegistration);
@@ -5301,9 +4188,8 @@
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;
}
@@ -5316,80 +4202,63 @@
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
+ showFeedback(caughtType, fishLane);
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
+ GameState.tutorialPaused = true;
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
+ return;
}
var closestFishInLane = null;
var closestDistance = Infinity;
- // Find the closest fish in the tapped lane that's actually near the hook
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);
- // Only consider fish that are reasonably close to the hook
- // Use a larger window than MISS_WINDOW to allow for early/late taps
- var maxCatchDistance = GAME_CONFIG.MISS_WINDOW * 2; // or some reasonable multiplier
+ var maxCatchDistance = GAME_CONFIG.MISS_WINDOW * 2;
if (distance < maxCatchDistance && distance < closestDistance) {
closestDistance = distance;
closestFishInLane = fish;
}
}
}
if (!closestFishInLane) {
- // No fish found within catchable range
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)
+ var tintToRedDuration = 50;
+ var holdRedDuration = 100;
+ var tintToWhiteDuration = 150;
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
@@ -5402,16 +4271,14 @@
}
});
}
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
@@ -5427,9 +4294,8 @@
}
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;
@@ -5446,15 +4312,13 @@
} 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);
@@ -5463,39 +4327,31 @@
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
+ animateHookCatch();
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
+ strokeThickness: 6
});
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
+ scorePopupText.x = GAME_CONFIG.SCREEN_CENTER_X;
+ scorePopupText.y = GAME_CONFIG.BOAT_Y - 70;
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();
@@ -5503,17 +4359,13 @@
}
});
}
}
-// 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();
@@ -5521,33 +4373,15 @@
}
}
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
+ GameState.tutorialMode = false;
+ stopTweens([fishingElements.boat, fishingElements.fishermanContainer, fishingElements.fisherman]);
if (fishingElements && fishingElements.waterSurfaceSegments) {
- fishingElements.waterSurfaceSegments.forEach(function (segment) {
- if (segment && !segment.destroyed) {
- tween.stop(segment);
- }
- });
+ stopTweens(fishingElements.waterSurfaceSegments);
}
- // 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();
@@ -5557,41 +4391,24 @@
}
});
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
+ cleanupParticleArray(globalOceanBubblesArray, globalOceanBubbleContainer);
+ cleanupParticleArray(globalSeaweedArray, globalSeaweedContainer);
+ cleanupParticleArray(globalCloudArray, globalCloudContainer);
createResultsScreen();
showScreen('results');
}
function createResultsScreen() {
- // Clear previous results
resultsScreen.removeChildren();
var resultsBg = resultsScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
@@ -5637,9 +4454,8 @@
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
@@ -5647,9 +4463,8 @@
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,
@@ -5662,9 +4477,8 @@
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
}, {
@@ -5677,67 +4491,47 @@
****/
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.
+ GameState.tutorialFish = null;
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
+ runTutorialStep();
}
} 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
+ handleFishingInput(x, y, true);
}
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
+ var tutorialButtonGfx = titleElements.tutorialButtonGfx || titleElements.tutorialButton;
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();
}
}
@@ -5745,16 +4539,15 @@
case 'levelSelect':
handleLevelSelectInput(x, y);
break;
case 'fishing':
- handleFishingInput(x, y, true); // true for isDown
+ handleFishingInput(x, y, true);
break;
case 'results':
showScreen('levelSelect');
break;
}
};
-// Update the main input handler
function handleLevelSelectInput(x, y) {
if (levelSelectElements && levelSelectElements.handleMapInput) {
levelSelectElements.handleMapInput(x, y);
}
@@ -5762,138 +4555,92 @@
/****
* 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
+ // Title Screen Ambient Particles using consolidated functions
if (titleScreenOceanBubbleContainer) {
+ handleParticleSpawning({
+ counter: titleScreenOceanBubbleSpawnCounter,
+ interval: OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS,
+ array: titleScreenOceanBubblesArray,
+ container: titleScreenOceanBubbleContainer,
+ constructor: OceanBubbleParticle
+ });
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);
- }
- }
+ updateParticleArray(titleScreenOceanBubblesArray);
}
- // Title Screen Seaweed
if (titleScreenSeaweedContainer) {
+ handleParticleSpawning({
+ counter: titleScreenSeaweedSpawnCounter,
+ interval: SEAWEED_SPAWN_INTERVAL_TICKS,
+ maxCount: MAX_SEAWEED_COUNT,
+ array: titleScreenSeaweedArray,
+ container: titleScreenSeaweedContainer,
+ constructor: SeaweedParticle
+ });
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);
- }
- }
+ updateParticleArray(titleScreenSeaweedArray);
}
- // Title Screen Clouds
if (titleScreenCloudContainer) {
+ handleParticleSpawning({
+ counter: titleScreenCloudSpawnCounter,
+ interval: CLOUD_SPAWN_INTERVAL_TICKS,
+ maxCount: MAX_CLOUD_COUNT,
+ array: titleScreenCloudArray,
+ container: titleScreenCloudContainer,
+ constructor: CloudParticle
+ });
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);
- }
- }
+ updateParticleArray(titleScreenCloudArray);
}
}
- // Update level select screen boat animation if active
if (GameState.currentScreen === 'levelSelect' && levelSelectElements) {
if (levelSelectElements.updateBoatAnimation && typeof levelSelectElements.updateBoatAnimation === 'function') {
levelSelectElements.updateBoatAnimation();
}
if (levelSelectElements.updateRipples && typeof levelSelectElements.updateRipples === 'function') {
- levelSelectElements.updateRipples(); // For boat
+ levelSelectElements.updateRipples();
}
- // NEW: Update home island ripples
if (levelSelectElements.updateHomeIslandRipples && typeof levelSelectElements.updateHomeIslandRipples === 'function') {
levelSelectElements.updateHomeIslandRipples();
}
- // NEW: Update ambient screen waves
if (levelSelectElements.updateScreenWaves && typeof levelSelectElements.updateScreenWaves === 'function') {
levelSelectElements.updateScreenWaves();
}
- // Update shallow waters node bubbles
if (levelSelectElements.updateShallowWatersNodeBubbles && typeof levelSelectElements.updateShallowWatersNodeBubbles === 'function') {
levelSelectElements.updateShallowWatersNodeBubbles();
}
- // NEW: Update seagulls
if (levelSelectElements.updateSeagulls && typeof levelSelectElements.updateSeagulls === 'function') {
levelSelectElements.updateSeagulls();
}
- // NEW: Update level select clouds
if (levelSelectElements.updateLevelSelectClouds && typeof levelSelectElements.updateLevelSelectClouds === 'function') {
levelSelectElements.updateLevelSelectClouds();
}
- // NEW: Update waterfall particles on home island
if (levelSelectElements.updateWaterfallParticles && typeof levelSelectElements.updateWaterfallParticles === 'function') {
levelSelectElements.updateWaterfallParticles();
}
- // NEW: Update shadow fish
if (levelSelectElements.updateShadowFish && typeof levelSelectElements.updateShadowFish === 'function') {
levelSelectElements.updateShadowFish();
}
}
- // Spawn and update ambient ocean bubbles during intro and gameplay (fishing screen)
+ // Spawn and update ambient particles during fishing screen
if (GameState.currentScreen === 'fishing' && globalOceanBubbleContainer) {
- // Spawn bubbles during intro and gameplay
+ handleParticleSpawning({
+ counter: globalOceanBubbleSpawnCounter,
+ interval: OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS,
+ array: globalOceanBubblesArray,
+ container: globalOceanBubbleContainer,
+ constructor: OceanBubbleParticle
+ });
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
+ // Update existing ocean bubbles with fish physics
for (var obIdx = globalOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) {
var oceanBubble = globalOceanBubblesArray[obIdx];
if (oceanBubble) {
// Apply fish physics to bubble
@@ -5902,27 +4649,21 @@
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
+ var influenceRadius = 150;
+ var minDistance = 30;
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
+ influence = influence * influence;
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 fishSpeedFactor = Math.abs(fish.speed) * 0.15;
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.x += dirX * pushForce * 2;
+ oceanBubble.vy += dirY * pushForce * 0.5;
oceanBubble.driftAmplitude = Math.min(80, oceanBubble.driftAmplitude + pushForce * 10);
- oceanBubble.driftFrequency *= 1 + influence * 0.1; // Slightly increase oscillation when disturbed
+ oceanBubble.driftFrequency *= 1 + influence * 0.1;
}
}
}
oceanBubble.update();
@@ -5930,23 +4671,24 @@
oceanBubble.destroy();
globalOceanBubblesArray.splice(obIdx, 1);
}
} else {
- globalOceanBubblesArray.splice(obIdx, 1); // Safeguard for null entries
+ globalOceanBubblesArray.splice(obIdx, 1);
}
}
}
- // Spawn and update seaweed particles during intro and gameplay
+ // Spawn and update seaweed particles during fishing
if (GameState.currentScreen === 'fishing' && globalSeaweedContainer) {
- // Spawn seaweed during intro and gameplay
+ handleParticleSpawning({
+ counter: globalSeaweedSpawnCounter,
+ interval: SEAWEED_SPAWN_INTERVAL_TICKS,
+ maxCount: MAX_SEAWEED_COUNT,
+ array: globalSeaweedArray,
+ container: globalSeaweedContainer,
+ constructor: SeaweedParticle
+ });
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
+ // Update existing seaweed with fish physics
for (var swIdx = globalSeaweedArray.length - 1; swIdx >= 0; swIdx--) {
var seaweed = globalSeaweedArray[swIdx];
if (seaweed) {
// Apply fish physics to seaweed
@@ -5955,24 +4697,19 @@
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 influenceRadius = 180;
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 fishSpeedFactor = Math.abs(fish.speed) * 0.2;
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.vx += dirX * pushForce * 1.5;
+ seaweed.vy += dirY * pushForce * 0.8;
seaweed.swayAmplitude = Math.min(60, seaweed.swayAmplitude + pushForce * 15);
}
}
}
@@ -5985,80 +4722,54 @@
globalSeaweedArray.splice(swIdx, 1);
}
}
}
- // Spawn and update cloud particles during intro and gameplay
+ // Spawn and update cloud particles during fishing
if (GameState.currentScreen === 'fishing' && globalCloudContainer) {
- // Spawn clouds during intro and gameplay
+ handleParticleSpawning({
+ counter: globalCloudSpawnCounter,
+ interval: CLOUD_SPAWN_INTERVAL_TICKS,
+ maxCount: MAX_CLOUD_COUNT,
+ array: globalCloudArray,
+ container: globalCloudContainer,
+ constructor: CloudParticle
+ });
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);
- }
- }
+ updateParticleArray(globalCloudArray);
}
- // Standard game active check; if intro is playing, gameActive will be false.
- // Tutorial mode has its own logic path
+ // Tutorial mode logic
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)
+ checkTutorialFishState();
}
// 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
+ return;
}
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;
@@ -6093,60 +4804,48 @@
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)
+ // Update hook Y position
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)
+ var previousFrameX = fish.lastX;
+ fish.update();
+ var currentFrameX = fish.x;
+ // Check for miss only if fish is active
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
+ fish.missed = true;
}
} 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
+ fish.missed = true;
}
}
}
- // 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.
+ // Remove off-screen fish
if (!fish.caught && (fish.x < -250 || fish.x > 2048 + 250)) {
- // Increased buffer slightly
fish.destroy();
fishArray.splice(i, 1);
}
}
@@ -6156,26 +4855,23 @@
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 spawnY = fishingElements.hook.y - 30;
var newNote = new MusicNoteParticle(spawnX, spawnY);
musicNotesContainer.addChild(newNote);
musicNotesArray.push(newNote);
- // Add scale pulse to the hook, synced with BPM
+ // Add scale pulse to the hook
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 bpm = currentSongConfig && currentSongConfig.bpm ? currentSongConfig.bpm : 90;
var beatDurationMs = 60000 / bpm;
- var pulsePhaseDuration = Math.max(50, beatDurationMs / 2); // Each phase (up/down) is half a beat, min 50ms
+ var pulsePhaseDuration = Math.max(50, beatDurationMs / 2);
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);
+ stopTween(fishingElements.hook.scale);
tween(fishingElements.hook.scale, {
x: originalScaleX * pulseScaleFactor,
y: originalScaleY * pulseScaleFactor
}, {
@@ -6187,65 +4883,38 @@
x: originalScaleX,
y: originalScaleY
}, {
duration: pulsePhaseDuration,
- easing: tween.easeIn // Or tween.easeOut for a softer return
+ easing: tween.easeIn
});
}
}
});
}
}
}
// 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);
- }
- }
+ updateParticleArray(musicNotesArray);
// 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 tailOffsetDirection = Math.sign(fish.speed) * -1;
+ var bubbleX = fish.x + tailOffsetDirection * (fish.fishGraphics.width * Math.abs(fish.fishGraphics.scaleX) / 2) * 0.8;
+ var bubbleY = fish.y + (Math.random() - 0.5) * (fish.fishGraphics.height * Math.abs(fish.fishGraphics.scaleY) / 4);
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);
- }
- }
+ updateParticleArray(bubblesArray);
};
-// Initialize game
+/****
+* Initialize game
+****/
showScreen('title');
\ No newline at end of file
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