User prompt
Change gentle waves duration to 135250 and 93 BPM ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Update with: self.pushBack = function() { self.isPushedBack = true; // Calculate pushback timing based on difficulty var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var pushbackBeats = GAME_DIFFICULTY.pushbackMultiplier[GAME_DIFFICULTY.current] || 2; // Work with PatternGenerator to find appropriate lane var preferredLane = self.calculateSmartNextLane(); var targetLane = PatternGenerator.reserveLaneForMultiTap(preferredLane, pushbackBeats); var laneFishWasInBeforeThisMove = self.lane; self.lane = targetLane; var targetY = GAME_CONFIG.LANES[self.lane].y; self.baseY = targetY; // Update planned moves var tapsRemainingIncludingThisOne = self.maxTaps - self.currentTaps; if (tapsRemainingIncludingThisOne > 1) { self.nextPlannedMoveLane = self.calculateSmartNextLane(targetLane); } else { self.nextPlannedMoveLane = -1; } // Update counter display if (self.isMultiTapFish && self.tapCounter && !self.tapCounter.destroyed) { var tapsStillNeeded = self.maxTaps - self.currentTaps; var counterText = tapsStillNeeded.toString(); if (tapsStillNeeded > 1 && self.nextPlannedMoveLane !== -1 && self.nextPlannedMoveLane !== self.lane) { if (self.nextPlannedMoveLane < self.lane) { counterText += "↑"; } else if (self.nextPlannedMoveLane > self.lane) { counterText += "↓"; } } self.tapCounter.setText(counterText); } // Ensure minimum 1 beat for lane changes if (targetLane !== laneFishWasInBeforeThisMove) { pushbackBeats = Math.max(pushbackBeats, 1); } var timeToNextTap = beatInterval * pushbackBeats; var fishSpeed = Math.abs(self.originalSpeed); var framesForNextApproach = timeToNextTap / (1000 / 60); var distanceNeededToReachHook = fishSpeed * framesForNextApproach; var pushBackX; if (self.originalSpeed > 0) { pushBackX = GAME_CONFIG.SCREEN_CENTER_X - distanceNeededToReachHook; } else { pushBackX = GAME_CONFIG.SCREEN_CENTER_X + distanceNeededToReachHook; } // Register the return arrival time to prevent conflicts var returnArrivalTime = LK.ticks * (1000 / 60) + timeToNextTap; if (ImprovedRhythmSpawner.scheduledArrivals) { ImprovedRhythmSpawner.scheduledArrivals.push(returnArrivalTime); } tween(self, { x: pushBackX, y: targetY }, { duration: 200, easing: tween.easeOut, onFinish: function() { self.speed = self.originalSpeed; self.isPushedBack = false; } }); }; ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Update with: var ImprovedRhythmSpawner = { nextFishSpawnTime: 0, scheduledArrivals: [], // Track when fish will arrive at hook update: function(currentTime) { if (!GameState.gameActive || GameState.songStartTime === 0) { return; } var songConfig = GameState.getCurrentSongConfig(); if (!songConfig || !songConfig.bpm) return; var beatInterval = 60000 / songConfig.bpm; // Clean up old arrival times this.scheduledArrivals = this.scheduledArrivals.filter(arrivalTime => arrivalTime > currentTime); // Check if it's time to spawn the next fish if (currentTime >= this.nextFishSpawnTime) { this.scheduleNextFish(currentTime, beatInterval, songConfig); } }, scheduleNextFish: function(currentTime, beatInterval, songConfig) { var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; if (!pattern) { this.spawnSingleFish(currentTime, beatInterval); this.nextFishSpawnTime = currentTime + beatInterval * 1.5; return; } // Calculate spawn timing var spawnInterval = pattern.beatsPerFish || 1.5; var sectionModifier = 1.0; if (pattern.sections) { var elapsed = currentTime - GameState.songStartTime; var section = pattern.sections.find(s => elapsed >= s.startTime && elapsed < s.endTime); if (section) sectionModifier = section.spawnModifier; } var adjustedInterval = spawnInterval / sectionModifier; var difficultyMultiplier = { easy: 1.3, medium: 1.0, hard: 0.7 }[GAME_DIFFICULTY.current] || 1.0; adjustedInterval *= difficultyMultiplier; // Find next available beat that doesn't conflict with existing arrivals var currentBeat = (currentTime - GameState.songStartTime) / beatInterval; var targetBeat = Math.ceil(currentBeat + adjustedInterval); var targetArrivalTime = GameState.songStartTime + targetBeat * beatInterval; // Check for conflicts and adjust if necessary targetArrivalTime = this.findNonConflictingArrivalTime(targetArrivalTime, beatInterval); // Calculate when fish should spawn to arrive at target time var travelTime = this.calculateTravelTime(); var fishSpawnTime = targetArrivalTime - travelTime; if (fishSpawnTime <= currentTime) { this.spawnSingleFish(currentTime, beatInterval); this.nextFishSpawnTime = currentTime + beatInterval * Math.max(0.5, adjustedInterval); } else { LK.setTimeout(() => { if (GameState.gameActive) { this.spawnSingleFish(fishSpawnTime, beatInterval); } }, fishSpawnTime - currentTime); // Register this arrival time this.scheduledArrivals.push(targetArrivalTime); this.nextFishSpawnTime = targetArrivalTime + beatInterval * adjustedInterval; } // Handle double spawns with offset timing if (Math.random() < (pattern.doubleSpawnChance || 0)) { var secondArrivalTime = this.findNonConflictingArrivalTime( targetArrivalTime + beatInterval * 0.5, beatInterval ); var secondTravelTime = this.calculateTravelTime(); var secondSpawnTime = secondArrivalTime - secondTravelTime; LK.setTimeout(() => { if (GameState.gameActive) { this.spawnSingleFish(secondSpawnTime, beatInterval); } }, Math.max(0, secondSpawnTime - currentTime)); this.scheduledArrivals.push(secondArrivalTime); } }, findNonConflictingArrivalTime: function(preferredTime, beatInterval) { var minGap = 200; // Minimum 200ms between fish arrivals var currentTime = preferredTime; // Check if this time conflicts with existing arrivals while (this.hasArrivalConflict(currentTime, minGap)) { currentTime += beatInterval * 0.25; // Move by quarter beat } return currentTime; }, hasArrivalConflict: function(arrivalTime, minGap) { return this.scheduledArrivals.some(scheduledTime => { return Math.abs(scheduledTime - arrivalTime) < minGap; }); }, calculateTravelTime: function() { var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = 1174; return (distanceToHook / fishSpeed) * (1000 / 60); }, // ... rest of the spawner methods remain the same spawnSingleFish: function(currentTime, beatInterval) { // Same as before, just call PatternGenerator.getNextLane() for lane var depthConfig = GameState.getCurrentDepthConfig(); if (!depthConfig) return; var activeBattles = fishArray.filter(f => f.isMultiTapFish && f.isInBattle).length; var maxBattles = GAME_DIFFICULTY.current === 'hard' ? 2 : 1; var fishSpeed = depthConfig.fishSpeed; var spawnSide = Math.random() < 0.5 ? -1 : 1; var actualSpeed = Math.abs(fishSpeed) * spawnSide; var laneIndex = PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; // Fish type determination (same as before) var fishType, fishValue; var rand = Math.random(); var pattern = GAME_CONFIG.PATTERNS[GameState.getCurrentSongConfig().pattern] || {}; var shallowProbability = 0.75; if (pattern.rareSpawnChance && rand < pattern.rareSpawnChance && activeBattles < maxBattles) { fishType = 'rare'; fishValue = Math.floor(depthConfig.fishValue * 4); } else if (GameState.selectedDepth >= 1 && rand < (pattern.rareSpawnChance || 0) + 0.1 && activeBattles < maxBattles) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else if (rand < shallowProbability) { var shallowRand = Math.random(); var songConfig = GameState.getCurrentSongConfig(); if (songConfig.name === "Gentle Waves") { if (shallowRand < 0.5) fishType = 'sardine'; else if (shallowRand < 0.85) fishType = 'anchovy'; else fishType = 'mackerel'; } else if (songConfig.name === "Morning Tide") { if (shallowRand < 0.4) fishType = 'sardine'; else if (shallowRand < 0.7) fishType = 'anchovy'; else fishType = 'mackerel'; } else if (songConfig.name === "Sunny Afternoon") { if (shallowRand < 0.3) fishType = 'sardine'; else if (shallowRand < 0.6) fishType = 'anchovy'; else fishType = 'mackerel'; } else { if (shallowRand < 0.45) fishType = 'sardine'; else if (shallowRand < 0.8) fishType = 'anchovy'; else fishType = 'mackerel'; } fishValue = Math.floor(depthConfig.fishValue); } 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; newFish.lastX = newFish.x; fishArray.push(newFish); if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(newFish); } GameState.sessionFishSpawned++; }, scheduleNextFish: function() { var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var currentTime = LK.ticks * (1000 / 60); var beatsDelay = 0.5; this.nextFishSpawnTime = currentTime + beatInterval * beatsDelay; }, reset: function() { this.nextFishSpawnTime = 0; this.scheduledArrivals = []; PatternGenerator.reset(); } };
User prompt
Update with: var PatternGenerator = { lastLane: -1, minDistanceBetweenFish: 300, lastActualSpawnTime: -100000, currentLaneSequence: [], sequenceIndex: 0, sameLaneCount: 0, maxSameLane: 3, minSameLane: 2, targetSameLaneCount: 2, getNextLane: function() { // If we don't have a sequence or finished current sequence, generate new one if (this.currentLaneSequence.length === 0 || this.sequenceIndex >= this.currentLaneSequence.length) { this.generateNewLaneSequence(); } var lane = this.currentLaneSequence[this.sequenceIndex]; this.sequenceIndex++; this.lastLane = lane; return lane; }, generateNewLaneSequence: function() { this.currentLaneSequence = []; this.sequenceIndex = 0; // Choose a starting lane (avoid same as last sequence end if possible) var availableLanes = [0, 1, 2]; if (this.lastLane !== -1) { availableLanes = availableLanes.filter(lane => lane !== this.lastLane); } var currentLane = availableLanes[Math.floor(Math.random() * availableLanes.length)]; // Generate sequence with 2-4 fish in same lane, then switch var totalFish = 8 + Math.floor(Math.random() * 4); // 8-12 fish per sequence var fishAdded = 0; while (fishAdded < totalFish) { // Determine how many fish in this lane (2-4) this.targetSameLaneCount = this.minSameLane + Math.floor(Math.random() * (this.maxSameLane - this.minSameLane + 1)); // Add fish to current lane for (var i = 0; i < this.targetSameLaneCount && fishAdded < totalFish; i++) { this.currentLaneSequence.push(currentLane); fishAdded++; } // Switch to different lane if (fishAdded < totalFish) { var newLaneOptions = [0, 1, 2].filter(lane => lane !== currentLane); currentLane = newLaneOptions[Math.floor(Math.random() * newLaneOptions.length)]; } } console.log("Generated lane sequence:", this.currentLaneSequence); }, // Method to reserve a lane for multi-tap fish pushback reserveLaneForMultiTap: function(preferredLane, beatsFromNow) { // Calculate which position in sequence this would be var estimatedSpawnsFromNow = Math.max(1, Math.floor(beatsFromNow / 1.5)); // Rough estimate var targetIndex = this.sequenceIndex + estimatedSpawnsFromNow; // If within current sequence, try to modify it if (targetIndex < this.currentLaneSequence.length) { // Check if we can place the multi-tap fish in preferred lane without breaking pattern too much var originalLane = this.currentLaneSequence[targetIndex]; // If preferred lane is available and not creating a conflict, use it if (preferredLane !== undefined && preferredLane >= 0 && preferredLane <= 2) { this.currentLaneSequence[targetIndex] = preferredLane; return preferredLane; } } return preferredLane !== undefined ? preferredLane : this.getNextLane(); }, canSpawnFishOnBeat: function(currentTime, configuredSpawnInterval) { var timeSinceLast = currentTime - this.lastActualSpawnTime; var minRequiredGap = configuredSpawnInterval; return timeSinceLast >= minRequiredGap; }, registerFishSpawn: function(spawnTime) { this.lastActualSpawnTime = spawnTime; }, reset: function() { this.lastLane = -1; this.lastActualSpawnTime = -100000; this.currentLaneSequence = []; this.sequenceIndex = 0; this.sameLaneCount = 0; } };
User prompt
Update with: var ImprovedRhythmSpawner = { nextFishSpawnTime: 0, update: function(currentTime) { if (!GameState.gameActive || GameState.songStartTime === 0) { return; } var songConfig = GameState.getCurrentSongConfig(); if (!songConfig || !songConfig.bpm) return; var beatInterval = 60000 / songConfig.bpm; // Check if it's time to spawn the next fish if (currentTime >= this.nextFishSpawnTime) { this.scheduleNextFish(currentTime, beatInterval, songConfig); } }, scheduleNextFish: function(currentTime, beatInterval, songConfig) { var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; if (!pattern) { // Fallback to simple spawning this.spawnSingleFish(currentTime, beatInterval); this.nextFishSpawnTime = currentTime + beatInterval * 1.5; return; } // Determine how many beats until next spawn var spawnInterval = pattern.beatsPerFish || 1.5; // Get section modifier if using custom patterns var sectionModifier = 1.0; if (pattern.sections) { var elapsed = currentTime - GameState.songStartTime; var section = pattern.sections.find(s => elapsed >= s.startTime && elapsed < s.endTime); if (section) sectionModifier = section.spawnModifier; } var adjustedInterval = spawnInterval / sectionModifier; // Difficulty affects spawn rate var difficultyMultiplier = { easy: 1.3, medium: 1.0, hard: 0.7 }[GAME_DIFFICULTY.current] || 1.0; adjustedInterval *= difficultyMultiplier; // Calculate next spawn time aligned to beats var currentBeat = (currentTime - GameState.songStartTime) / beatInterval; var nextSpawnBeat = Math.ceil(currentBeat + adjustedInterval); var nextSpawnTime = GameState.songStartTime + nextSpawnBeat * beatInterval; // Calculate when fish should spawn to arrive at that beat var travelTime = this.calculateTravelTime(); var fishSpawnTime = nextSpawnTime - travelTime; if (fishSpawnTime <= currentTime) { // If spawn time is in the past, spawn now this.spawnSingleFish(currentTime, beatInterval); this.nextFishSpawnTime = currentTime + beatInterval * Math.max(0.5, adjustedInterval); } else { // Schedule spawn for the calculated time LK.setTimeout(() => { if (GameState.gameActive) { this.spawnSingleFish(fishSpawnTime, beatInterval); } }, fishSpawnTime - currentTime); this.nextFishSpawnTime = nextSpawnTime + beatInterval * adjustedInterval; } // Check for double spawns if (Math.random() < (pattern.doubleSpawnChance || 0)) { var secondFishDelay = beatInterval * 0.5; // Half beat later LK.setTimeout(() => { if (GameState.gameActive) { this.spawnSingleFish(currentTime + secondFishDelay, beatInterval); } }, Math.max(0, fishSpawnTime - currentTime + secondFishDelay)); } }, calculateTravelTime: function() { var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = 1174; // Distance from spawn edge to center return (distanceToHook / fishSpeed) * (1000 / 60); // Convert to milliseconds }, spawnSingleFish: function(currentTime, beatInterval) { var depthConfig = GameState.getCurrentDepthConfig(); if (!depthConfig) return; // Check if too many multi-tap battles are active var activeBattles = fishArray.filter(f => f.isMultiTapFish && f.isInBattle).length; var maxBattles = GAME_DIFFICULTY.current === 'hard' ? 2 : 1; var fishSpeed = depthConfig.fishSpeed; var spawnSide = Math.random() < 0.5 ? -1 : 1; var actualSpeed = Math.abs(fishSpeed) * spawnSide; var laneIndex = PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; // Determine fish type with increased shallow fish probability var fishType, fishValue; var rand = Math.random(); var pattern = GAME_CONFIG.PATTERNS[GameState.getCurrentSongConfig().pattern] || {}; // Significantly increase sardine/anchovy spawn rate var shallowProbability = 0.75; // 75% chance for shallow fish if (pattern.rareSpawnChance && rand < pattern.rareSpawnChance && activeBattles < maxBattles) { fishType = 'rare'; fishValue = Math.floor(depthConfig.fishValue * 4); } else if (GameState.selectedDepth >= 1 && rand < (pattern.rareSpawnChance || 0) + 0.1 && activeBattles < maxBattles) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else if (rand < shallowProbability) { // Weighted selection for shallow fish types var shallowRand = Math.random(); var songConfig = GameState.getCurrentSongConfig(); if (songConfig.name === "Gentle Waves") { if (shallowRand < 0.5) fishType = 'sardine'; else if (shallowRand < 0.85) fishType = 'anchovy'; else fishType = 'mackerel'; } else if (songConfig.name === "Morning Tide") { if (shallowRand < 0.4) fishType = 'sardine'; else if (shallowRand < 0.7) fishType = 'anchovy'; else fishType = 'mackerel'; } else if (songConfig.name === "Sunny Afternoon") { if (shallowRand < 0.3) fishType = 'sardine'; else if (shallowRand < 0.6) fishType = 'anchovy'; else fishType = 'mackerel'; } else { if (shallowRand < 0.45) fishType = 'sardine'; else if (shallowRand < 0.8) fishType = 'anchovy'; else fishType = 'mackerel'; } fishValue = Math.floor(depthConfig.fishValue); } else { // Fallback 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; newFish.lastX = newFish.x; fishArray.push(newFish); if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(newFish); } GameState.sessionFishSpawned++; }, scheduleNextFish: function() { // This is called when a battle ends - simplified for compatibility var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var currentTime = LK.ticks * (1000 / 60); var beatsDelay = 0.5; this.nextFishSpawnTime = currentTime + beatInterval * beatsDelay; }, reset: function() { this.nextFishSpawnTime = 0; PatternGenerator.reset(); } };
User prompt
Update with: var PatternGenerator = { lastLane: -1, minDistanceBetweenFish: 200, // Reduced for more fish lastActualSpawnTime: -100000, laneSequence: [], sequenceIndex: 0, getNextLane: function() { // Generate a sequence that ensures good lane distribution if (this.laneSequence.length === 0) { this.generateLaneSequence(); } var lane = this.laneSequence[this.sequenceIndex]; this.sequenceIndex = (this.sequenceIndex + 1) % this.laneSequence.length; this.lastLane = lane; return lane; }, generateLaneSequence: function() { // Create a pattern that ensures all lanes are used and avoids too much repetition var sequences = [ [1, 0, 2, 1, 2, 0, 1, 0], // Balanced sequence [0, 1, 2, 1, 0, 2, 1, 2], // Different balanced sequence [2, 1, 0, 1, 2, 0, 1, 0], // Another variation ]; this.laneSequence = sequences[Math.floor(Math.random() * sequences.length)]; this.sequenceIndex = 0; }, canSpawnFishOnBeat: function(currentTime, configuredSpawnInterval) { var timeSinceLast = currentTime - this.lastActualSpawnTime; var minRequiredGap = Math.min(configuredSpawnInterval, 500); // Allow closer spawns return timeSinceLast >= minRequiredGap; }, registerFishSpawn: function(spawnTime) { this.lastActualSpawnTime = spawnTime; }, reset: function() { this.lastLane = -1; this.lastActualSpawnTime = -100000; this.laneSequence = []; this.sequenceIndex = 0; } };
User prompt
Update with: // Add to Fish class - modify the pushBack function self.pushBack = function() { self.isPushedBack = true; // Calculate next lane with proper timing consideration var targetLane = self.calculateSmartNextLane(); var laneFishWasInBeforeThisMove = self.lane; self.lane = targetLane; var targetY = GAME_CONFIG.LANES[self.lane].y; self.baseY = targetY; // Determine the next planned move for subsequent pushback var tapsRemainingIncludingThisOne = self.maxTaps - self.currentTaps; if (tapsRemainingIncludingThisOne > 1) { self.nextPlannedMoveLane = self.calculateSmartNextLane(targetLane); } else { self.nextPlannedMoveLane = -1; } // Update counter display if (self.isMultiTapFish && self.tapCounter && !self.tapCounter.destroyed) { var tapsStillNeeded = self.maxTaps - self.currentTaps; var counterText = tapsStillNeeded.toString(); if (tapsStillNeeded > 1 && self.nextPlannedMoveLane !== -1 && self.nextPlannedMoveLane !== self.lane) { if (self.nextPlannedMoveLane < self.lane) { counterText += "↑"; } else if (self.nextPlannedMoveLane > self.lane) { counterText += "↓"; } } self.tapCounter.setText(counterText); } // Calculate pushback timing based on difficulty var songConfig = GameState.getCurrentSongConfig(); var beatInterval = 60000 / songConfig.bpm; var pushbackBeats = GAME_DIFFICULTY.pushbackMultiplier[GAME_DIFFICULTY.current] || 2; // Ensure minimum 1 beat for lane changes if (targetLane !== laneFishWasInBeforeThisMove) { pushbackBeats = Math.max(pushbackBeats, 1); } var timeToNextTap = beatInterval * pushbackBeats; var fishSpeed = Math.abs(self.originalSpeed); var framesForNextApproach = timeToNextTap / (1000 / 60); var distanceNeededToReachHook = fishSpeed * framesForNextApproach; var pushBackX; if (self.originalSpeed > 0) { pushBackX = GAME_CONFIG.SCREEN_CENTER_X - distanceNeededToReachHook; } else { pushBackX = GAME_CONFIG.SCREEN_CENTER_X + distanceNeededToReachHook; } // Smooth movement to new position tween(self, { x: pushBackX, y: targetY }, { duration: 200, easing: tween.easeOut, onFinish: function() { self.speed = self.originalSpeed; self.isPushedBack = false; } }); }; // Add smart lane calculation self.calculateSmartNextLane = function(currentLane) { if (currentLane === undefined) currentLane = self.lane; // For fish with 2 taps (mackerel), prefer adjacent lanes if (self.maxTaps === 2) { if (currentLane === 0) return 1; if (currentLane === 2) return 1; return Math.random() < 0.5 ? 0 : 2; // From middle, go to top or bottom } // For fish with 3 taps (rare), cycle through lanes if (self.maxTaps === 3) { return (currentLane + 1) % 3; } return currentLane; // Fallback }; ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Update with: var ImprovedRhythmSpawner = { scheduledBeats: [], nextBeatToSchedule: 0, update: function(currentTime) { if (!GameState.gameActive || GameState.songStartTime === 0) { return; } var songConfig = GameState.getCurrentSongConfig(); if (!songConfig || !songConfig.bpm) return; var beatInterval = 60000 / songConfig.bpm; var currentBeat = Math.floor((currentTime - GameState.songStartTime) / beatInterval); // Schedule upcoming beats this.scheduleUpcomingBeats(currentTime, beatInterval, songConfig); // Execute scheduled spawns this.executeScheduledSpawns(currentTime, beatInterval); }, scheduleUpcomingBeats: function(currentTime, beatInterval, songConfig) { var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; if (!pattern) return; var beatsAhead = 8; // Schedule 8 beats in advance var currentBeat = Math.floor((currentTime - GameState.songStartTime) / beatInterval); for (var i = 0; i < beatsAhead; i++) { var targetBeat = currentBeat + i; if (targetBeat <= this.nextBeatToSchedule) continue; var beatTime = GameState.songStartTime + targetBeat * beatInterval; // Check if we should spawn on this beat based on pattern if (this.shouldSpawnOnBeat(targetBeat, pattern, songConfig)) { this.scheduleSpawnGroup(beatTime, targetBeat, pattern); } this.nextBeatToSchedule = Math.max(this.nextBeatToSchedule, targetBeat + 1); } }, shouldSpawnOnBeat: function(beatNumber, pattern, songConfig) { // Use beatsPerFish to determine spawn frequency var spawnInterval = pattern.beatsPerFish || 1.5; // Get section modifier if using custom patterns var sectionModifier = 1.0; if (pattern.sections) { var currentTime = beatNumber * (60000 / songConfig.bpm); var section = pattern.sections.find(s => currentTime >= s.startTime && currentTime < s.endTime); if (section) sectionModifier = section.spawnModifier; } var adjustedInterval = spawnInterval / sectionModifier; // Difficulty affects spawn rate var difficultyMultiplier = { easy: 1.2, medium: 1.0, hard: 0.8 }[GAME_DIFFICULTY.current] || 1.0; adjustedInterval *= difficultyMultiplier; return (beatNumber % Math.round(adjustedInterval)) === 0; }, scheduleSpawnGroup: function(beatTime, beatNumber, pattern) { var spawnsThisBeat = [{ delay: 0, lane: PatternGenerator.getNextLane() }]; // Check for double/triple spawns if (Math.random() < (pattern.doubleSpawnChance || 0)) { spawnsThisBeat.push({ delay: 0, lane: PatternGenerator.getNextLane() }); // Triple spawn chance (for hard difficulty) if (GAME_DIFFICULTY.current === 'hard' && Math.random() < 0.3) { spawnsThisBeat.push({ delay: 0, lane: PatternGenerator.getNextLane() }); } } // Add half-beat spawns for increased density if (GAME_DIFFICULTY.current !== 'easy' && Math.random() < 0.4) { spawnsThisBeat.push({ delay: 30000 / GameState.getCurrentSongConfig().bpm, // Half beat lane: PatternGenerator.getNextLane() }); } spawnsThisBeat.forEach(spawn => { this.scheduledBeats.push({ spawnTime: beatTime + spawn.delay, lane: spawn.lane, executed: false }); }); }, executeScheduledSpawns: function(currentTime, beatInterval) { for (var i = this.scheduledBeats.length - 1; i >= 0; i--) { var scheduled = this.scheduledBeats[i]; if (scheduled.executed) { this.scheduledBeats.splice(i, 1); continue; } // Calculate when fish should spawn to arrive exactly on beat var arrivalTime = scheduled.spawnTime; var travelTime = this.calculateTravelTime(scheduled.lane); var spawnTime = arrivalTime - travelTime; if (currentTime >= spawnTime && !scheduled.executed) { this.spawnFishForBeat(scheduled.lane, arrivalTime); scheduled.executed = true; } } }, calculateTravelTime: function(laneIndex) { var depthConfig = GameState.getCurrentDepthConfig(); var fishSpeed = Math.abs(depthConfig.fishSpeed); var distanceToHook = 1174; // Distance from spawn edge to center return (distanceToHook / fishSpeed) * (1000 / 60); // Convert to milliseconds }, spawnFishForBeat: function(laneIndex, arrivalTime) { // Don't spawn if too many multi-tap fish battles active var activeBattles = fishArray.filter(f => f.isMultiTapFish && f.isInBattle).length; var maxBattles = GAME_DIFFICULTY.current === 'hard' ? 2 : 1; var depthConfig = GameState.getCurrentDepthConfig(); var pattern = GAME_CONFIG.PATTERNS[GameState.getCurrentSongConfig().pattern]; // Determine fish type with increased shallow fish probability var fishType, fishValue; var rand = Math.random(); // Increase sardine/anchovy spawn rate significantly var shallowProbability = 0.75; // 75% chance for shallow fish if (pattern.rareSpawnChance && rand < pattern.rareSpawnChance && activeBattles < maxBattles) { fishType = 'rare'; fishValue = Math.floor(depthConfig.fishValue * 4); } else if (GameState.selectedDepth >= 1 && rand < pattern.rareSpawnChance + 0.15 && activeBattles < maxBattles) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else if (rand < shallowProbability) { // Weighted selection for shallow fish var shallowRand = Math.random(); var songConfig = GameState.getCurrentSongConfig(); if (songConfig.name === "Gentle Waves") { if (shallowRand < 0.5) fishType = 'sardine'; else if (shallowRand < 0.85) fishType = 'anchovy'; else fishType = 'mackerel'; } else if (songConfig.name === "Morning Tide") { if (shallowRand < 0.4) fishType = 'sardine'; else if (shallowRand < 0.7) fishType = 'anchovy'; else fishType = 'mackerel'; } else { if (shallowRand < 0.45) fishType = 'sardine'; else if (shallowRand < 0.8) fishType = 'anchovy'; else fishType = 'mackerel'; } fishValue = Math.floor(depthConfig.fishValue); } else { // Fallback to mackerel if no other type selected fishType = 'mackerel'; fishValue = Math.floor(depthConfig.fishValue); } var fishSpeed = depthConfig.fishSpeed; var spawnSide = Math.random() < 0.5 ? -1 : 1; var actualSpeed = Math.abs(fishSpeed) * spawnSide; var newFish = new Fish(fishType, fishValue, actualSpeed, laneIndex); newFish.x = actualSpeed > 0 ? -150 : 2048 + 150; newFish.y = GAME_CONFIG.LANES[laneIndex].y; newFish.baseY = newFish.y; newFish.lastX = newFish.x; newFish.scheduledArrivalTime = arrivalTime; fishArray.push(newFish); if (fishingScreen && !fishingScreen.destroyed) { fishingScreen.addChild(newFish); } GameState.sessionFishSpawned++; PatternGenerator.registerFishSpawn(arrivalTime); }, reset: function() { this.scheduledBeats = []; this.nextBeatToSchedule = 0; PatternGenerator.reset(); } };
User prompt
Update with: var FISH_RHYTHM_PATTERNS = { sardine: ['beat'], // Reduced from 2 to 1 tap anchovy: ['beat'], // Reduced from 3 to 1 tap mackerel: ['beat', 'beat'], // Reduced from 4 to 2 taps rareFish: ['beat', 'beat', 'beat'] // Reduced from 5 to 3 taps };
User prompt
Replace songCard asset with restaurantFloor asset for restaurant floor background
User prompt
Remove the numbered fishes and pushback and instead generate a pattern of fishes that spawn to the beat of the song and are catchable in one tap. Do each step one a time and wait for me to type continue after each is saved.
User prompt
Remove the numbered fishes and pushback and instead generate a pattern of fishes that spawn to the beat of the song and are catchable in one tap. Take your time and plan it out.
User prompt
Slow down the rate of new customers.
Code edit (1 edits merged)
Please save this source code
User prompt
I got 2400 points and still only got 1 star in the cooking mini game. Confirm how many points you need for each category and if it’s possible to get that many points.
User prompt
Make sure the last chop line of each cooking phase is cleaned up properly and doesn’t just freeze in the center of the screen.
User prompt
Increase hit thresholds slightly for cooking game beat indicators to make it a little easier.
User prompt
Okay now change code to match that please.
User prompt
Increase 3 star rating requirement for cooking mini game. 3 stars requires a perfect score.
User prompt
Please fix the bug: 'Script error.' in or related to this line: 'var plateTapX = currentPhaseContainer.contentArea.beatTarget.x + contentArea.x + overlay.phaseContainer.x; // Global X of tap zone' Line Number: 3551
User prompt
Modify all three restaurant phases, prep, cook and plate, to use the same chop lines and beat indicators as the prep phase. All three will operate on a random pattern of beat indicators as a metric of success. Keep animations in the window the same.
User prompt
case 'cook': var linesCook = overlay.cookPhaseLines || []; var timeInPhase = LK.ticks * (1000 / 60) - CookingState.phaseStartTime; var currentBeatInPhaseExact = timeInPhase / BeatDetector.beatInterval; if (eventType === 'down') { for (var i = linesCook.length - 1; i >= 0; i--) { var line = linesCook[i]; if (line.isLongNote && line.canTap && !line.isBeingHeld) { // Check if pressed on beat for the long note's start var noteStartProgress = currentBeatInPhaseExact - line.beatNumber; if (Math.abs(noteStartProgress * BeatDetector.beatInterval) < BeatDetector.tolerance * 1.5) { line.isBeingHeld = true; line.holdStartTime = LK.ticks * (1000 / 60); CookingState.activeHeldLine = line; if (line.beatIndicator) { line.beatIndicator.tint = 0x0088FF; } success = true; showCookingFeedback('good', line.targetX, line.y - 100); break; } } } } else if (eventType === 'up') { if (CookingState.activeHeldLine) { var line = CookingState.activeHeldLine; var holdDurationMs = LK.ticks * (1000 / 60) - line.holdStartTime; var expectedHoldDurationMs = line.durationBeats * BeatDetector.beatInterval; var noteEndBeatExact = line.beatNumber + line.durationBeats; var releaseProgress = currentBeatInPhaseExact - noteEndBeatExact; if (Math.abs(releaseProgress * BeatDetector.beatInterval) < BeatDetector.tolerance * 1.5) { points = 200; feedback = 'perfect'; } else if (Math.abs(releaseProgress * BeatDetector.beatInterval) < BeatDetector.tolerance * 2) { points = 100; feedback = 'good'; } else { points = 25; feedback = 'miss'; } success = true; line.wasTappedSuccessfully = true; line.isBeingHeld = false; CookingState.activeHeldLine = null; if (line.beatIndicator) { line.beatIndicator.tint = COOKING_PHASES.cook.color; } line.destroy(); var lineIndex = linesCook.indexOf(line); if (lineIndex > -1) { linesCook.splice(lineIndex, 1); } } } else { // 'tap' event - only for short notes for (var i = linesCook.length - 1; i >= 0; i--) { var line = linesCook[i]; if (!line.isLongNote && line.canTap) { var noteProgress = currentBeatInPhaseExact - line.beatNumber; if (Math.abs(noteProgress * BeatDetector.beatInterval) < BeatDetector.tolerance) { points = 150; feedback = 'perfect'; } else if (Math.abs(noteProgress * BeatDetector.beatInterval) < BeatDetector.tolerance * 2) { points = 75; feedback = 'good'; } else { points = 10; feedback = 'miss'; } success = true; line.wasTappedSuccessfully = true; line.destroy(); var lineIndexTap = linesCook.indexOf(line); if (lineIndexTap > -1) { linesCook.splice(lineIndexTap, 1); } break; } } } // REMOVE ALL the old positioning-based detection code that was here // Only keep the fish animation update: if (success && points > 0) { if (currentPhaseContainer.contentArea && currentPhaseContainer.contentArea.fishPieces) { currentPhaseContainer.contentArea.fishPieces.forEach(function (piece) { var currentTint = piece.tint || 0xF4A460; var r = (currentTint >> 16) & 0xFF, g = (currentTint >> 8) & 0xFF, b = currentTint & 0xFF; r = Math.min(0xCD, r + 0x11); g = Math.min(0x85, g + 0x11); b = Math.min(0x3F, b + 0x11); piece.tint = (r << 16) | (g << 8) | b; }); } } break;
User prompt
Update with: // In the ChopLine class, modify setupVisuals method: self.setupVisuals = function (isLong, beats, indicatorTint) { self.isLongNote = isLong; if (self.isLongNote) { self.durationBeats = beats > 0 ? beats : 1; if (self.beatIndicator) { // Make hold indicators MUCH longer - multiply by more than just beats var baseWidth = 60; var holdMultiplier = 80; // Increased from 50 to 80 self.beatIndicator.width = baseWidth + (self.durationBeats * holdMultiplier); self.beatIndicator.tint = indicatorTint || 0x00BFFF; // Deep sky blue for long notes } if (self.line) { self.line.tint = indicatorTint || 0x00BFFF; self.line.alpha = 0.8; } } else if (self.beatIndicator) { self.beatIndicator.tint = indicatorTint || 0x00FF00; // Default green for short tap indicators } };
User prompt
Update with: } else if (phaseName === 'cook' && contentArea && overlay) { var beatInfo = BeatDetector.update(); var timeSincePhaseStart = currentTime - CookingState.phaseStartTime; var beatInterval = BeatDetector.beatInterval; var currentBeatNumber = Math.floor(timeSincePhaseStart / beatInterval); var spawnThisBeat = false; if (!overlay.cookPhaseLines) { overlay.cookPhaseLines = []; } if (currentBeatNumber > CookingState.lastCookLineBeat) { if (!CookingState.cookPhasePatternComplete) { spawnThisBeat = true; CookingState.lastCookLineBeat = currentBeatNumber; } } if (spawnThisBeat) { // Check if this beat should have an interactive element var shouldShowBeatIndicator = false; var isLongNote = false; var noteDuration = 1; // Find if any pattern item matches this beat for (var p = 0; p < CookingState.cookPattern.length; p++) { var patternItem = CookingState.cookPattern[p]; if (patternItem.beat === currentBeatNumber + 1) { // +1 because we spawn one beat ahead shouldShowBeatIndicator = patternItem.beatIndicator; isLongNote = patternItem.type === 'hold'; noteDuration = patternItem.durationBeats || 1; break; } } // Use EXACT same positioning as prep phase var startX = 1800; var targetX = 1024; var lineY = 2000; // Same as prep phase var lineDuration = beatInterval * 1.2; var newLine = new ChopLine(startX, targetX, lineY, lineDuration, shouldShowBeatIndicator); newLine.beatNumber = currentBeatNumber + 1; if (shouldShowBeatIndicator) { newLine.setupVisuals(isLongNote, noteDuration, COOKING_PHASES.cook.color); newLine.isIndicator = true; newLine.wasTapped = false; } if (overlay && !overlay.destroyed) { overlay.addChild(newLine); overlay.cookPhaseLines.push(newLine); } } // Check if pattern is complete var maxBeatInPattern = 0; for (var p = 0; p < CookingState.cookPattern.length; p++) { if (CookingState.cookPattern[p].beat > maxBeatInPattern) { maxBeatInPattern = CookingState.cookPattern[p].beat; } } if (currentBeatNumber > maxBeatInPattern + 2 && CookingState.cookPhaseEventsResolved >= CookingState.cookPhaseTotalEvents) { if (!CookingState.cookPhasePatternComplete) { CookingState.cookPhasePatternComplete = true; completePhase(); } } // Clean up finished/missed lines for cook phase if (overlay.cookPhaseLines) { for (var i = overlay.cookPhaseLines.length - 1; i >= 0; i--) { var line = overlay.cookPhaseLines[i]; if (line.isDone) { var missed = true; if (line.wasTappedSuccessfully) { missed = false; } if (missed && line.isIndicator) { CookingState.cookPhaseEventsResolved++; if (CookingState.currentPhaseContainer && CookingState.currentPhaseContainer.beatCounter) { CookingState.currentPhaseContainer.beatCounter.setText(CookingState.cookPhaseEventsResolved + '/' + CookingState.cookPhaseTotalEvents); } showCookingFeedback('miss', line.targetX, line.y - 150); } line.destroy(); overlay.cookPhaseLines.splice(i, 1); } } }
User prompt
Update as needed with: } else if (phaseName === 'cook' && contentArea && overlay) { var beatInfo = BeatDetector.update(); var timeSincePhaseStart = currentTime - CookingState.phaseStartTime; var beatInterval = BeatDetector.beatInterval; var currentBeatNumber = Math.floor(timeSincePhaseStart / beatInterval); if (!overlay.cookPhaseLines) { overlay.cookPhaseLines = []; } // Spawn a line every beat (like prep phase) var spawnThisBeat = false; if (currentBeatNumber > CookingState.lastCookLineBeat) { if (!CookingState.cookPhasePatternComplete) { spawnThisBeat = true; CookingState.lastCookLineBeat = currentBeatNumber; } } if (spawnThisBeat) { // Check if this beat should have an interactive element var shouldShowBeatIndicator = false; var isLongNote = false; var noteDuration = 1; // Find if any pattern item matches this beat for (var p = 0; p < CookingState.cookPattern.length; p++) { var patternItem = CookingState.cookPattern[p]; if (patternItem.beat === currentBeatNumber + 1) { // +1 because we spawn one beat ahead shouldShowBeatIndicator = patternItem.beatIndicator; isLongNote = patternItem.type === 'hold'; noteDuration = patternItem.durationBeats || 1; break; } } // Spawn line (similar to prep phase) var startX = 1800; var targetX = 1024; var lineYRelativeToOverlay = 1366 - 450 + contentArea.y + 180; var lineDuration = beatInterval * 1.2; var newLine = new ChopLine(startX, targetX, lineYRelativeToOverlay, lineDuration, shouldShowBeatIndicator); newLine.beatNumber = currentBeatNumber + 1; if (shouldShowBeatIndicator) { newLine.setupVisuals(isLongNote, noteDuration, COOKING_PHASES.cook.color); } if (overlay && !overlay.destroyed) { overlay.addChild(newLine); overlay.cookPhaseLines.push(newLine); } } // Check if pattern is complete (all pattern items have been processed) var allPatternItemsProcessed = true; for (var p = 0; p < CookingState.cookPattern.length; p++) { var patternItem = CookingState.cookPattern[p]; if (patternItem.beat <= currentBeatNumber + 2) { // Allow some buffer // This pattern item should have been encountered by now continue; } else { allPatternItemsProcessed = false; break; } } if (allPatternItemsProcessed && CookingState.cookPhaseEventsResolved >= CookingState.cookPhaseTotalEvents) { if (!CookingState.cookPhasePatternComplete) { CookingState.cookPhasePatternComplete = true; completePhase(); } } // Clean up finished/missed lines // ... rest of cleanup code remains the same }
/**** * 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; } // 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; }; 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: 93, duration: 135250, 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 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); } } 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) { 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... } // Spawn and update seaweed particles during fishing 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... } // 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
@@ -140,58 +140,8 @@
return self;
});
var Fish = Container.expand(function (type, value, speed, lane) {
var self = Container.call(this);
- self.determineFirstPlannedLane = function () {
- // Based on initial self.lane (the lane the fish spawns in)
- if (self.lane === 1) {
- return 0;
- } // From middle, prefer to go to top lane (0)
- if (self.lane === 0) {
- return 1;
- } // From top lane (0), must go to middle lane (1)
- if (self.lane === 2) {
- return 1;
- } // From bottom lane (2), must go to middle lane (1)
- return self.lane; // Fallback, should not be reached with 3 lanes
- };
- self.determineSubsequentPlannedLane = function (currentLane, laneJustMovedFrom) {
- // currentLane is where the fish IS after the current pushback
- // laneJustMovedFrom is where it WAS before the current pushback
- if (currentLane === 0) {
- return 1;
- } // Is at top (0), next must be middle (1)
- if (currentLane === 2) {
- return 1;
- } // Is at bottom (2), next must be middle (1)
- if (currentLane === 1) {
- // Is in middle lane (1)
- if (laneJustMovedFrom === 0) {
- return 2;
- } // Came from top (0), next must be bottom (2)
- if (laneJustMovedFrom === 2) {
- return 0;
- } // Came from bottom (2), next must be top (0)
- }
- // This fallback should ideally not be reached if fish always changes lane
- // and the game has 3 lanes.
- return currentLane;
- };
- self.calculateSmartNextLane = function (currentLane) {
- if (currentLane === undefined) currentLane = self.lane;
- // For fish with 2 taps (mackerel), prefer adjacent lanes
- if (self.maxTaps === 2) {
- if (currentLane === 0) return 1;
- if (currentLane === 2) return 1;
- return Math.random() < 0.5 ? 0 : 2; // From middle, go to top or bottom
- }
- // For fish with 3 taps (rare), cycle through lanes
- if (self.maxTaps === 3) {
- return (currentLane + 1) % 3;
- }
- return currentLane; // Fallback
- };
- // Existing asset selection logic
var assetName = type + 'Fish';
if (type === 'shallow') {
var currentSongConfig = null;
if (typeof GameState !== 'undefined' && GameState && typeof GameState.getCurrentSongConfig === 'function') {
@@ -235,13 +185,12 @@
});
if (speed > 0) {
self.fishGraphics.scaleX = -1;
}
- // Existing properties
self.type = type;
self.value = value;
self.speed = speed;
- self.lane = lane; // This is the initial lane
+ self.lane = lane;
self.caught = false;
self.missed = false;
self.lastX = 0;
self.isSpecial = type === 'rare';
@@ -251,78 +200,24 @@
self.swimTime = Math.random() * Math.PI * 2;
self.baseY = self.y;
self.scaleTime = 0;
self.baseScale = 1;
- // Tutorial interaction flags
- self.wasCaughtThisInteraction = false;
- self.wasMissedThisInteraction = false;
- self.tutorialFish = false; // Will be set true by spawnTutorialFishHelper if applicable
- // NEW: Multi-tap properties
- var fishTypeName = assetName; // Use the determined assetName for rhythm patterns
- 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;
- self.nextPlannedMoveLane = -1; // Initialize planned move lane
- // Create tap counter display for multi-tap fish
- if (self.isMultiTapFish) {
- self.nextPlannedMoveLane = self.determineFirstPlannedLane(); // Determine the very first planned move
- self.counterBg = self.addChild(LK.getAsset('songCard', {
- anchorX: 0.5,
- anchorY: 0.5,
- x: 0,
- y: -80,
- // Position above the fish
- width: 120,
- height: 80,
- alpha: 0.8
- }));
- var initialCounterText = self.maxTaps.toString();
- // Arrow for the FIRST planned move (if more than one tap total)
- if (self.maxTaps > 1 && self.nextPlannedMoveLane !== -1 && self.nextPlannedMoveLane !== self.lane) {
- if (self.nextPlannedMoveLane < self.lane) {
- initialCounterText += "↑"; // Arrow indicates upward movement
- } else if (self.nextPlannedMoveLane > self.lane) {
- initialCounterText += "↓"; // Arrow indicates downward movement
- }
- }
- self.tapCounter = self.addChild(new Text2(initialCounterText, {
- size: 70,
- fill: 0xFFFFFF,
- // White text
- stroke: 0x000000,
- // Black stroke for visibility
- strokeThickness: 3
- }));
- self.tapCounter.anchor.set(0.5, 0.5);
- self.tapCounter.x = 0;
- self.tapCounter.y = -80; // Align with background
- // Arrow graphics removed, will use text emojis in tapCounter
- }
self.update = function () {
- if (!self.caught && !self.isPushedBack) {
- // Check isPushedBack
+ 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();
- if (songConfig && songConfig.bpm) {
- // Ensure songConfig and bpm are valid
- 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;
- }
+ 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;
@@ -330,161 +225,10 @@
self.fishGraphics.alpha = 1.0;
}
}
};
- 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
- if (self.counterBg && !self.counterBg.destroyed) {
- self.counterBg.destroy();
- }
- if (self.tapCounter && !self.tapCounter.destroyed) {
- self.tapCounter.destroy();
- }
- return true;
- }
- // Push fish back and continue battle
- self.pushBack();
- return false;
- };
- self.pushBack = function () {
- self.isPushedBack = true;
- // Calculate pushback timing based on difficulty
- var songConfig = GameState.getCurrentSongConfig();
- var beatInterval = 60000 / songConfig.bpm;
- var pushbackBeats = GAME_DIFFICULTY.pushbackMultiplier[GAME_DIFFICULTY.current] || 2;
- // Work with PatternGenerator to find appropriate lane
- var preferredLane = self.calculateSmartNextLane();
- var targetLane = PatternGenerator.reserveLaneForMultiTap(preferredLane, pushbackBeats);
- var laneFishWasInBeforeThisMove = self.lane;
- self.lane = targetLane;
- var targetY = GAME_CONFIG.LANES[self.lane].y;
- self.baseY = targetY;
- // Update planned moves
- var tapsRemainingIncludingThisOne = self.maxTaps - self.currentTaps;
- if (tapsRemainingIncludingThisOne > 1) {
- self.nextPlannedMoveLane = self.calculateSmartNextLane(targetLane);
- } else {
- self.nextPlannedMoveLane = -1;
- }
- // Update counter display
- if (self.isMultiTapFish && self.tapCounter && !self.tapCounter.destroyed) {
- var tapsStillNeeded = self.maxTaps - self.currentTaps;
- var counterText = tapsStillNeeded.toString();
- if (tapsStillNeeded > 1 && self.nextPlannedMoveLane !== -1 && self.nextPlannedMoveLane !== self.lane) {
- if (self.nextPlannedMoveLane < self.lane) {
- counterText += "↑";
- } else if (self.nextPlannedMoveLane > self.lane) {
- counterText += "↓";
- }
- }
- self.tapCounter.setText(counterText);
- }
- // Ensure minimum 1 beat for lane changes
- if (targetLane !== laneFishWasInBeforeThisMove) {
- pushbackBeats = Math.max(pushbackBeats, 1);
- }
- var timeToNextTap = beatInterval * pushbackBeats;
- var fishSpeed = Math.abs(self.originalSpeed);
- var framesForNextApproach = timeToNextTap / (1000 / 60);
- var distanceNeededToReachHook = fishSpeed * framesForNextApproach;
- var pushBackX;
- if (self.originalSpeed > 0) {
- pushBackX = GAME_CONFIG.SCREEN_CENTER_X - distanceNeededToReachHook;
- } else {
- pushBackX = GAME_CONFIG.SCREEN_CENTER_X + distanceNeededToReachHook;
- }
- // Register the return arrival time to prevent conflicts
- var returnArrivalTime = LK.ticks * (1000 / 60) + timeToNextTap;
- if (ImprovedRhythmSpawner.scheduledArrivals) {
- ImprovedRhythmSpawner.scheduledArrivals.push(returnArrivalTime);
- }
- tween(self, {
- x: pushBackX,
- y: targetY
- }, {
- duration: 200,
- easing: tween.easeOut,
- onFinish: function onFinish() {
- self.speed = self.originalSpeed;
- self.isPushedBack = false;
- }
- });
- };
- self.startBattle = function () {
- if (!self.isInBattle) {
- 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
- ImprovedRhythmSpawner.scheduleNextFishAfterBattle();
- };
- self.missedTap = function () {
- // Fish escapes due to miss
- self.missed = true;
- self.endBattle(false); // wasSuccessful is 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;
- // Hide tap counter if it exists and fish is missed
- if (self.isMultiTapFish) {
- if (self.counterBg && !self.counterBg.destroyed) {
- self.counterBg.destroy();
- }
- if (self.tapCounter && !self.tapCounter.destroyed) {
- self.tapCounter.destroy();
- }
- // Arrow graphics destruction removed
- }
- LK.setTimeout(function () {
- if (!self.destroyed) {
- self.destroy();
- }
- }, 2000); // Give 2 seconds for fish to swim off screen
- };
self.catchFish = function () {
self.caught = true;
- if (self.isMultiTapFish && self.counterBg && !self.counterBg.destroyed) {
- self.counterBg.destroy();
- }
- if (self.isMultiTapFish && self.tapCounter && !self.tapCounter.destroyed) {
- self.tapCounter.destroy();
- }
- // Arrow graphics destruction removed
- if (self.isMultiTapFish) {
- // No graphics to destroy here anymore
- }
- if (self.isInBattle) {
- self.endBattle(true); // wasSuccessful is true
- } else {
- // Single-tap fish - still schedule next
- ImprovedRhythmSpawner.scheduleNextFishAfterBattle();
- }
var currentFishX = self.x;
var currentFishY = self.y;
var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X;
var boatLandingY = GAME_CONFIG.BOAT_Y;
@@ -511,11 +255,9 @@
}, {
duration: durationPhase2,
easing: tween.easeIn,
onFinish: function onFinish() {
- if (self && !self.destroyed) {
- self.destroy();
- }
+ self.destroy();
}
});
}
});
@@ -1243,11 +985,9 @@
}
function createWaveAnimation(segment, amplitude, halfPeriod) {
var animUp, animDown;
animUp = function animUp() {
- if (!segment || segment.destroyed) {
- return;
- }
+ if (!segment || segment.destroyed) return;
tween(segment, {
y: segment.baseY - amplitude
}, {
duration: halfPeriod,
@@ -1255,11 +995,9 @@
onFinish: animDown
});
};
animDown = function animDown() {
- if (!segment || segment.destroyed) {
- return;
- }
+ if (!segment || segment.destroyed) return;
tween(segment, {
y: segment.baseY + amplitude
}, {
duration: halfPeriod,
@@ -1280,11 +1018,9 @@
return;
}
var delay = config.baseDelay + Math.random() * config.variance;
timer = LK.setTimeout(function () {
- if (GameState.currentScreen !== config.screenName) {
- return;
- }
+ 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);
@@ -1377,99 +1113,46 @@
GROUP_PIVOT_X: 0,
GROUP_PIVOT_Y: 0,
GROUP_INITIAL_Y_SCREEN_OFFSET: -450
};
-var GAME_DIFFICULTY = {
- current: 'easy',
- // Default difficulty
- pushbackMultiplier: {
- easy: 3,
- medium: 2,
- hard: 1
- }
-};
game.up = function (x, y, obj) {
- // This function now primarily handles the release phase of a touch/click.
- // For the fishing screen, it will always delegate to handleFishingInput.
- // UI button 'up' events are typically not needed if 'down' triggers action.
- if (GameState.currentScreen === 'fishing') {
- handleFishingInput(x, y, false); // false for isDown (release/up)
+ switch (GameState.currentScreen) {
+ case 'title':
+ break;
+ case 'levelSelect':
+ break;
+ case 'fishing':
+ handleFishingInput(x, y, false);
+ break;
+ case 'results':
+ break;
}
- // Other screens can have specific 'up' logic if needed for things like drag-release.
- // For the current scope of tutorial interaction, this is sufficient.
};
/****
* Pattern Generation System
****/
var PatternGenerator = {
lastLane: -1,
minDistanceBetweenFish: 300,
lastActualSpawnTime: -100000,
- currentLaneSequence: [],
- sequenceIndex: 0,
- sameLaneCount: 0,
- maxSameLane: 3,
- minSameLane: 2,
- targetSameLaneCount: 2,
getNextLane: function getNextLane() {
- // If we don't have a sequence or finished current sequence, generate new one
- if (this.currentLaneSequence.length === 0 || this.sequenceIndex >= this.currentLaneSequence.length) {
- this.generateNewLaneSequence();
+ if (this.lastLane === -1) {
+ this.lastLane = 1;
+ return 1;
}
- var lane = this.currentLaneSequence[this.sequenceIndex];
- this.sequenceIndex++;
- this.lastLane = lane;
- return lane;
- },
- generateNewLaneSequence: function generateNewLaneSequence() {
- var _this = this;
- this.currentLaneSequence = [];
- this.sequenceIndex = 0;
- // Choose a starting lane (avoid same as last sequence end if possible)
- var availableLanes = [0, 1, 2];
- if (this.lastLane !== -1) {
- availableLanes = availableLanes.filter(function (lane) {
- return lane !== _this.lastLane;
- });
+ var possibleLanes = [this.lastLane];
+ if (this.lastLane > 0) {
+ possibleLanes.push(this.lastLane - 1);
}
- var currentLane = availableLanes.length > 0 ? availableLanes[Math.floor(Math.random() * availableLanes.length)] : Math.floor(Math.random() * 3);
- // Generate sequence with 2-4 fish in same lane, then switch
- var totalFish = 8 + Math.floor(Math.random() * 4); // 8-12 fish per sequence
- var fishAdded = 0;
- while (fishAdded < totalFish) {
- // Determine how many fish in this lane (2-4)
- this.targetSameLaneCount = this.minSameLane + Math.floor(Math.random() * (this.maxSameLane - this.minSameLane + 1));
- // Add fish to current lane
- for (var i = 0; i < this.targetSameLaneCount && fishAdded < totalFish; i++) {
- this.currentLaneSequence.push(currentLane);
- fishAdded++;
- }
- // Switch to different lane
- if (fishAdded < totalFish) {
- var newLaneOptions = [0, 1, 2].filter(function (lane) {
- return lane !== currentLane;
- });
- currentLane = newLaneOptions.length > 0 ? newLaneOptions[Math.floor(Math.random() * newLaneOptions.length)] : (currentLane + 1) % 3;
- }
+ if (this.lastLane < 2) {
+ possibleLanes.push(this.lastLane + 1);
}
- // console.log("Generated lane sequence:", this.currentLaneSequence); // LK Sandbox does not support console.log with multiple arguments like this
- },
- // Method to reserve a lane for multi-tap fish pushback
- reserveLaneForMultiTap: function reserveLaneForMultiTap(preferredLane, beatsFromNow) {
- // Calculate which position in sequence this would be
- var estimatedSpawnsFromNow = Math.max(1, Math.floor(beatsFromNow / 1.5)); // Rough estimate
- var targetIndex = this.sequenceIndex + estimatedSpawnsFromNow;
- // If within current sequence, try to modify it
- if (targetIndex < this.currentLaneSequence.length) {
- // Check if we can place the multi-tap fish in preferred lane without breaking pattern too much
- var originalLane = this.currentLaneSequence[targetIndex];
- // If preferred lane is available and not creating a conflict, use it
- if (preferredLane !== undefined && preferredLane >= 0 && preferredLane <= 2) {
- this.currentLaneSequence[targetIndex] = preferredLane;
- return preferredLane;
- }
+ if (Math.random() < 0.7) {
+ this.lastLane = possibleLanes[Math.floor(Math.random() * possibleLanes.length)];
+ } else {
+ this.lastLane = Math.floor(Math.random() * 3);
}
- return preferredLane !== undefined ? preferredLane : this.getNextLane();
+ return this.lastLane;
},
canSpawnFishOnBeat: function canSpawnFishOnBeat(currentTime, configuredSpawnInterval) {
var timeSinceLast = currentTime - this.lastActualSpawnTime;
var minRequiredGap = configuredSpawnInterval;
@@ -1480,35 +1163,13 @@
},
reset: function reset() {
this.lastLane = -1;
this.lastActualSpawnTime = -100000;
- this.currentLaneSequence = [];
- this.sequenceIndex = 0;
- this.sameLaneCount = 0;
}
};
/****
* Game Configuration
****/
-// Simplified battle system - one fish at a time
-var BATTLE_STATES = {
- NONE: 'none',
- ACTIVE: 'active',
- WAITING_FOR_NEXT: 'waiting'
-};
-var FISH_RHYTHM_PATTERNS = {
- sardine: ['beat'],
- // Reduced from 2 to 1 tap
- anchovy: ['beat'],
- // Reduced from 3 to 1 tap
- mackerel: ['beat', 'beat'],
- // Reduced from 4 to 2 taps
- rareFish: ['beat', 'beat', 'beat'] // Reduced from 5 to 3 taps
-};
-var BATTLE_STATES = {
- NONE: 'none',
- ACTIVE: 'active'
-};
var GAME_CONFIG = {
SCREEN_CENTER_X: 1024,
SCREEN_CENTER_Y: 900,
BOAT_Y: 710,
@@ -1522,9 +1183,8 @@
}, {
y: 2419,
name: "deep"
}],
- //[FN#9T]
PERFECT_WINDOW: 40,
GOOD_WINDOW: 80,
MISS_WINDOW: 120,
DEPTHS: [{
@@ -1760,177 +1420,162 @@
var MULTI_BEAT_SPAWN_DELAY_MS = 250;
var TRIPLET_BEAT_SPAWN_DELAY_MS = 350;
var FISH_SPAWN_END_BUFFER_MS = 500;
var ImprovedRhythmSpawner = {
- nextFishSpawnTime: 0,
- scheduledArrivals: [],
- // Track when fish will arrive at hook
+ nextBeatToSchedule: 1,
+ scheduledBeats: [],
update: function update(currentTime) {
if (!GameState.gameActive || GameState.songStartTime === 0) {
return;
}
var songConfig = GameState.getCurrentSongConfig();
- if (!songConfig || !songConfig.bpm) return;
+ var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
var beatInterval = 60000 / songConfig.bpm;
- // Clean up old arrival times
- this.scheduledArrivals = this.scheduledArrivals.filter(function (arrivalTime) {
- return arrivalTime > currentTime;
- });
- // Check if it's time to spawn the next fish
- if (currentTime >= this.nextFishSpawnTime) {
- this.scheduleNextFishArrival(currentTime, beatInterval, songConfig);
+ 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++;
}
},
- scheduleNextFishArrival: function scheduleNextFishArrival(currentTime, beatInterval, songConfig) {
- var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
- if (!pattern) {
- this.spawnSingleFish(currentTime, beatInterval);
- this.nextFishSpawnTime = currentTime + beatInterval * 1.5;
+ 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;
}
- // Calculate spawn timing
- var spawnInterval = pattern.beatsPerFish || 1.5;
- var sectionModifier = 1.0;
- if (pattern.sections) {
- var elapsed = currentTime - GameState.songStartTime;
- var section = pattern.sections.find(function (s) {
- return elapsed >= s.startTime && elapsed < s.endTime;
- });
- if (section) sectionModifier = section.spawnModifier;
- }
- var adjustedInterval = spawnInterval / sectionModifier;
- var difficultyMultiplier = {
- easy: 1.3,
- medium: 1.0,
- hard: 0.7
- }[GAME_DIFFICULTY.current] || 1.0;
- adjustedInterval *= difficultyMultiplier;
- // Find next available beat that doesn't conflict with existing arrivals
- var currentBeat = (currentTime - GameState.songStartTime) / beatInterval;
- var targetBeat = Math.ceil(currentBeat + adjustedInterval);
- var targetArrivalTime = GameState.songStartTime + targetBeat * beatInterval;
- // Check for conflicts and adjust if necessary
- targetArrivalTime = this.findNonConflictingArrivalTime(targetArrivalTime, beatInterval);
- // Calculate when fish should spawn to arrive at target time
- var travelTime = this.calculateTravelTime();
- var fishSpawnTime = targetArrivalTime - travelTime;
- var self = this;
- if (fishSpawnTime <= currentTime) {
- this.spawnSingleFish(currentTime, beatInterval); // Pass current time as it's spawning now
- this.nextFishSpawnTime = currentTime + beatInterval * Math.max(0.5, adjustedInterval);
- } else {
+ 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) {
- self.spawnSingleFish(fishSpawnTime, beatInterval); // Pass scheduled fishSpawnTime
+ if (GameState.gameActive && GameState.songStartTime !== 0) {
+ self.spawnRhythmFish(beatNumber, targetArrivalTime);
}
- }, fishSpawnTime - currentTime);
- // Register this arrival time
- this.scheduledArrivals.push(targetArrivalTime);
- // Set nextFishSpawnTime relative to the *arrival* of the fish just scheduled, plus interval
- this.nextFishSpawnTime = targetArrivalTime + beatInterval * Math.max(0.25, adjustedInterval); // Ensure some delay
+ }, delay);
}
- // Handle double spawns with offset timing
- if (Math.random() < (pattern.doubleSpawnChance || 0)) {
- var secondArrivalTime = this.findNonConflictingArrivalTime(targetArrivalTime + beatInterval * 0.5,
- // Try half beat later
- beatInterval);
- var secondTravelTime = this.calculateTravelTime(); // Could be different if fish speed varies by type, but currently doesn't
- var secondSpawnTime = secondArrivalTime - secondTravelTime;
- LK.setTimeout(function () {
- if (GameState.gameActive) {
- self.spawnSingleFish(secondSpawnTime, beatInterval); // Pass its own scheduled spawn time
- }
- }, Math.max(0, secondSpawnTime - currentTime)); // Ensure delay is not negative
- this.scheduledArrivals.push(secondArrivalTime);
- }
},
- findNonConflictingArrivalTime: function findNonConflictingArrivalTime(preferredTime, beatInterval) {
- var minGap = 200; // Minimum 200ms between fish arrivals
- var currentTimeToTest = preferredTime;
- // Check if this time conflicts with existing arrivals
- while (this.hasArrivalConflict(currentTimeToTest, minGap)) {
- currentTimeToTest += beatInterval * 0.25; // Move by quarter beat
+ spawnRhythmFish: function spawnRhythmFish(beatNumber, targetArrivalTime) {
+ if (!GameState.gameActive) {
+ return;
}
- return currentTimeToTest;
- },
- hasArrivalConflict: function hasArrivalConflict(arrivalTime, minGap) {
- return this.scheduledArrivals.some(function (scheduledTime) {
- return Math.abs(scheduledTime - arrivalTime) < minGap;
- });
- },
- calculateTravelTime: function calculateTravelTime() {
+ var currentTime = LK.ticks * (1000 / 60);
var depthConfig = GameState.getCurrentDepthConfig();
- var fishSpeed = Math.abs(depthConfig.fishSpeed);
- var distanceToHook = 1174; // Distance from spawn edge to center
- return distanceToHook / fishSpeed * (1000 / 60); // Convert to milliseconds
- },
- spawnSingleFish: function spawnSingleFish(actualSpawnTime, beatInterval) {
- // Renamed currentTime to actualSpawnTime for clarity
- var depthConfig = GameState.getCurrentDepthConfig();
- if (!depthConfig) return;
- var activeBattles = fishArray.filter(function (f) {
- return f.isMultiTapFish && f.isInBattle;
- }).length;
- var maxBattles = GAME_DIFFICULTY.current === 'hard' ? 2 : 1;
- var fishSpeed = depthConfig.fishSpeed;
- var spawnSide = Math.random() < 0.5 ? -1 : 1;
- var actualSpeed = Math.abs(fishSpeed) * spawnSide;
+ 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();
- var pattern = GAME_CONFIG.PATTERNS[GameState.getCurrentSongConfig().pattern] || {};
- var shallowProbability = 0.75;
- if (pattern.rareSpawnChance && rand < pattern.rareSpawnChance && activeBattles < maxBattles) {
+ if (rand < pattern.rareSpawnChance) {
fishType = 'rare';
fishValue = Math.floor(depthConfig.fishValue * 4);
- } else if (GameState.selectedDepth >= 1 && rand < (pattern.rareSpawnChance || 0) + 0.1 && activeBattles < maxBattles) {
+ } 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 if (rand < shallowProbability) {
- var shallowRand = Math.random();
- var songConfig = GameState.getCurrentSongConfig();
- if (songConfig.name === "Gentle Waves") {
- if (shallowRand < 0.50) fishType = 'sardine';else if (shallowRand < 0.85) fishType = 'anchovy';else fishType = 'mackerel';
- } else if (songConfig.name === "Morning Tide") {
- if (shallowRand < 0.40) fishType = 'sardine';else if (shallowRand < 0.70) fishType = 'anchovy';else fishType = 'mackerel';
- } else if (songConfig.name === "Sunny Afternoon") {
- if (shallowRand < 0.30) fishType = 'sardine';else if (shallowRand < 0.60) fishType = 'anchovy';else fishType = 'mackerel';
- } else {
- if (shallowRand < 0.45) fishType = 'sardine';else if (shallowRand < 0.80) fishType = 'anchovy';else fishType = 'mackerel';
- }
- fishValue = Math.floor(depthConfig.fishValue);
} else {
- fishType = 'shallow'; // Default to a shallow type
+ fishType = 'shallow';
fishValue = Math.floor(depthConfig.fishValue);
}
- var newFish = new Fish(fishType, fishValue, actualSpeed, laneIndex);
- newFish.x = actualSpeed > 0 ? -150 : 2048 + 150;
+ 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);
- if (fishingScreen && !fishingScreen.destroyed) {
- fishingScreen.addChild(newFish);
- }
+ fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
- PatternGenerator.registerFishSpawn(actualSpawnTime); // Register with the actual spawn time
+ PatternGenerator.registerFishSpawn(currentTime);
+ this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime);
+ return newFish;
},
- // This function is intended to be called after a battle or a single-tap catch
- // to schedule the *next* fish spawn without complex arrival time calculation for that specific call.
- // It sets a simple delay for the next fish consideration.
- scheduleNextFishAfterBattle: function scheduleNextFishAfterBattle() {
- var songConfig = GameState.getCurrentSongConfig();
- if (!songConfig || !songConfig.bpm) return;
- var beatInterval = 60000 / songConfig.bpm;
- var currentTime = LK.ticks * (1000 / 60);
- var beatsDelay = 0.5; // How many beats to wait before considering the next regular spawn
- this.nextFishSpawnTime = currentTime + beatInterval * beatsDelay;
+ 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.nextFishSpawnTime = 0;
- this.scheduledArrivals = [];
- PatternGenerator.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
@@ -2007,155 +1652,71 @@
tutorialOverlayContainer.visible = true;
}
function spawnTutorialFishHelper(config) {
var fishType = config.type || 'shallow';
- var depthConfig = GAME_CONFIG.DEPTHS[0]; // Tutorial always uses shallow depth config for simplicity
+ 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);
- // Override fish asset if specified in config
- if (config.fishAsset) {
- if (newFish.fishGraphics && !newFish.fishGraphics.destroyed) {
- newFish.fishGraphics.destroy();
- }
- newFish.fishGraphics = newFish.attachAsset(config.fishAsset, {
- anchorX: 0.5,
- anchorY: 0.5
- });
- if (actualFishSpeed > 0) {
- // Moving right
- newFish.fishGraphics.scaleX = -1; // Flip if moving L to R
- } else {
- newFish.fishGraphics.scaleX = 1; // Normal if moving R to L
- }
- // Override rhythm pattern and multi-tap properties for the forced fish type
- newFish.rhythmPattern = FISH_RHYTHM_PATTERNS[config.fishAsset] || ['beat']; // Default to single beat
- newFish.maxTaps = newFish.rhythmPattern.length;
- newFish.currentTaps = 0;
- newFish.isMultiTapFish = newFish.maxTaps > 1;
- newFish.currentPatternIndex = 0;
- newFish.isPushedBack = false;
- newFish.isInBattle = false;
- newFish.nextPlannedMoveLane = -1;
- // Recreate multi-tap display if it's a multi-tap fish
- if (newFish.isMultiTapFish) {
- // Destroy existing counter if any (from original Fish constructor)
- if (newFish.counterBg && !newFish.counterBg.destroyed) newFish.counterBg.destroy();
- if (newFish.tapCounter && !newFish.tapCounter.destroyed) newFish.tapCounter.destroy();
- newFish.nextPlannedMoveLane = newFish.determineFirstPlannedLane();
- newFish.counterBg = newFish.addChild(LK.getAsset('songCard', {
- anchorX: 0.5,
- anchorY: 0.5,
- x: 0,
- y: -80,
- // Position above the fish
- width: 120,
- height: 80,
- alpha: 0.8
- }));
- var initialCounterText = newFish.maxTaps.toString();
- if (newFish.maxTaps > 1 && newFish.nextPlannedMoveLane !== -1 && newFish.nextPlannedMoveLane !== newFish.lane) {
- if (newFish.nextPlannedMoveLane < newFish.lane) {
- initialCounterText += "↑";
- } else if (newFish.nextPlannedMoveLane > newFish.lane) {
- initialCounterText += "↓";
- }
- }
- newFish.tapCounter = newFish.addChild(new Text2(initialCounterText, {
- size: 70,
- fill: 0xFFFFFF,
- // White text
- stroke: 0x000000,
- // Black stroke for visibility
- strokeThickness: 3
- }));
- newFish.tapCounter.anchor.set(0.5, 0.5);
- newFish.tapCounter.x = 0;
- newFish.tapCounter.y = -80; // Align with background
- }
- }
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;
- newFish.wasCaughtThisInteraction = false; // Initialize tutorial interaction flag
- newFish.wasMissedThisInteraction = false; // Initialize tutorial interaction flag
fishArray.push(newFish);
fishingScreen.addChild(newFish);
return newFish;
}
function runTutorialStep() {
- // GameState.tutorialPaused is set by each case.
- // GameState.tutorialAwaitingTap is not used in this refactor.
- // Clear previous lane highlights if any
+ GameState.tutorialPaused = false;
+ GameState.tutorialAwaitingTap = false;
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
- // Default to showing continue button, specific cases will hide it.
if (tutorialContinueButton) {
tutorialContinueButton.visible = true;
}
if (tutorialContinueText) {
tutorialContinueText.visible = true;
}
- // Clean up previous tutorial fish if it exists and we are not in a fish-centric step (3, 4, 5)
- // For steps 3, 4, 5, fish cleanup is handled by game.down or the step logic itself.
- if (GameState.tutorialFish && GameState.tutorialStep !== 3 && GameState.tutorialStep !== 4 && GameState.tutorialStep !== 5) {
+ if (GameState.tutorialFish && GameState.tutorialStep !== 3 && GameState.tutorialStep !== 4) {
if (!GameState.tutorialFish.destroyed) {
- var idx = fishArray.indexOf(GameState.tutorialFish);
- if (idx > -1) {
- fishArray.splice(idx, 1);
- }
GameState.tutorialFish.destroy();
}
+ var idx = fishArray.indexOf(GameState.tutorialFish);
+ if (idx > -1) {
+ fishArray.splice(idx, 1);
+ }
GameState.tutorialFish = null;
}
- // Reset interaction flags on the current tutorial fish if it exists (for retries)
- if (GameState.tutorialFish) {
- GameState.tutorialFish.wasCaughtThisInteraction = false;
- GameState.tutorialFish.wasMissedThisInteraction = false;
- }
if (fishingElements) {
- // Ensure fishing animations are running for tutorial steps before the end
- if (typeof fishingElements.startWaterSurfaceAnimation === 'function' && GameState.tutorialStep < 9) {
+ if (typeof fishingElements.startWaterSurfaceAnimation === 'function' && GameState.tutorialStep < 8) {
fishingElements.startWaterSurfaceAnimation();
}
- if (typeof fishingElements.startBoatAndFishermanAnimation === 'function' && GameState.tutorialStep < 9) {
+ if (typeof fishingElements.startBoatAndFishermanAnimation === 'function' && GameState.tutorialStep < 8) {
fishingElements.startBoatAndFishermanAnimation();
}
- if (fishingElements.hook) {
- tween.stop(fishingElements.hook);
- // For early steps, or if explicitly needed, force hook to a position.
- if (GameState.tutorialStep <= 1) {
- // Steps 0, 1: Hook starts in middle
- swipeState.currentLane = 1;
- fishingElements.hook.y = GAME_CONFIG.LANES[1].y;
- fishingElements.hook.originalY = GAME_CONFIG.LANES[1].y;
- GameState.hookTargetLaneIndex = 1;
- updateLaneBracketsVisuals();
- }
- // For step 2, hook is where player left it from step 1, or middle if step 1 was skipped.
- // For step 3, hook is where player left it from step 2.
- // For step 4, hook is where player left it from step 3.
+ 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. SWIPE UP or DOWN to move it between lanes. Try it now, then tap 'CONTINUE'.");
+ 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
@@ -2172,12 +1733,12 @@
}
}
});
}
- GameState.tutorialPaused = true; // Paused for CONTINUE, but swipe is enabled by handleFishingInput
+ GameState.tutorialPaused = true;
break;
case 2:
- setTutorialText("Great! Fish swim in three lanes. Try SWIPING your hook to different lanes. When ready, tap 'CONTINUE'.");
+ 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,
@@ -2190,91 +1751,78 @@
});
tutorialOverlayContainer.addChildAt(highlight, 0);
tutorialLaneHighlights.push(highlight);
}
- GameState.tutorialPaused = true; // Paused for CONTINUE, swipe enabled by handleFishingInput
+ GameState.tutorialPaused = true;
break;
case 3:
- // Ensure hook is visually and logically in the lane the fish will spawn.
- // swipeState.currentLane should hold the player's chosen lane from step 2 (or default to 1 if step 2 was instant).
- // The hook's Y position is determined by the swipe tween in handleFishingInput.
- // The tween.stop(fishingElements.hook) at the start of runTutorialStep will have halted any ongoing motion.
- // The hook will be at the Y position where its tween was stopped.
- GameState.hookTargetLaneIndex = swipeState.currentLane;
- updateLaneBracketsVisuals();
- setTutorialText("Nice! A sardine will approach in your current lane (" + (swipeState.currentLane === 0 ? "TOP" : swipeState.currentLane === 1 ? "MIDDLE" : "BOTTOM") + "). It needs TWO TAPS. Tap to catch it!");
+ 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; // Player needs to act (tap fish)
- if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) GameState.tutorialFish.destroy();
+ GameState.tutorialPaused = false;
+ if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
+ GameState.tutorialFish.destroy();
+ }
GameState.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.35,
- lane: swipeState.currentLane,
- // Fish in the hook's current lane
- fishAsset: 'sardine' // Force sardine for 2 taps
+ lane: 1
});
- if (GameState.tutorialFish) {
- GameState.tutorialFish.wasCaughtThisInteraction = false;
- GameState.tutorialFish.wasMissedThisInteraction = false;
- }
break;
case 4:
- // Player needs to swipe to the top lane. Hook is wherever they left it.
- // The fish will spawn in the top lane.
- setTutorialText("Great! Now, SWIPE to the TOP lane. A sardine will appear there. Catch it with TWO TAPS!");
+ 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; // Player needs to swipe and tap
- if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) GameState.tutorialFish.destroy();
+ GameState.tutorialPaused = false;
+ if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
+ GameState.tutorialFish.destroy();
+ }
GameState.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.45,
- lane: 0,
- // Fish in top lane (index 0)
- fishAsset: 'sardine'
+ lane: 1
});
- if (GameState.tutorialFish) {
- GameState.tutorialFish.wasCaughtThisInteraction = false;
- GameState.tutorialFish.wasMissedThisInteraction = false;
- }
break;
case 5:
- setTutorialText("Excellent! Notice how the fish moved to different lanes after each tap? The arrow showed you where it would go next. This is the multi-tap battle system! Tap 'CONTINUE'.");
+ setTutorialText("Nice one! Catch fish consecutively to build a COMBO for bonus points! Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 6:
- setTutorialText("Catch fish consecutively to build a COMBO for bonus points! Tap 'CONTINUE'.");
- GameState.tutorialPaused = true;
- break;
- case 7:
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 8:
+ case 7:
setTutorialText("You're all set! Tap 'CONTINUE' to go to the fishing spots!");
GameState.tutorialPaused = true;
break;
default:
- // End of tutorial
GameState.tutorialMode = false;
tutorialOverlayContainer.visible = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
- var idxDefault = fishArray.indexOf(GameState.tutorialFish);
- if (idxDefault > -1) fishArray.splice(idxDefault, 1);
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();
+ if (overlay && !overlay.destroyed) {
+ overlay.destroy();
+ }
});
tutorialLaneHighlights = [];
}
- // Restore normal game animations if they were stopped/altered
- 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);
+ 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;
}
}
@@ -2385,8 +1933,10 @@
combo: 0,
maxCombo: 0,
gameActive: false,
songStartTime: 0,
+ lastBeatTime: 0,
+ beatCount: 0,
introPlaying: false,
musicNotesActive: false,
currentPlayingMusicId: 'rhythmTrack',
currentPlayingMusicInitialVolume: 0.8,
@@ -2396,11 +1946,8 @@
tutorialStep: 0,
tutorialPaused: false,
tutorialAwaitingTap: false,
tutorialFish: null,
- battleState: BATTLE_STATES.NONE,
- currentBattleFish: null,
- nextFishSpawnTime: 0,
initOwnedSongs: function initOwnedSongs() {
this.ownedSongs = [];
for (var i = 0; i <= this.currentDepth; i++) {
this.ownedSongs.push({
@@ -2927,15 +2474,9 @@
songEarnings: null,
playButton: null,
playButtonText: null,
leftArrowText: null,
- rightArrowText: null,
- difficultyEasyButton: null,
- difficultyEasyButtonText: null,
- difficultyMediumButton: null,
- difficultyMediumButtonText: null,
- difficultyHardButton: null,
- difficultyHardButtonText: null
+ rightArrowText: null
};
var currentNode = GameState.lastLevelSelectNodeKey;
var initialBoatNodeConfig = MAP_CONFIG.NODES[currentNode];
var playerBoat = boatContainer.addChild(LK.getAsset('playerBoat', {
@@ -3345,61 +2886,8 @@
}));
songElements.playButtonText.anchor.set(0.5, 0.5);
songElements.playButtonText.x = songElements.playButton.x;
songElements.playButtonText.y = songElements.playButton.y;
- // Add Difficulty Buttons
- var difficultyButtonY = songElements.playButton.y + songElements.playButton.height / 2 + 30 + 40; // playButton bottom + 30px spacing + half height of new button (80/2=40)
- var difficultyButtonWidth = 250;
- var difficultyButtonHeight = 80;
- var difficultyButtonSpacing = 30;
- // Easy Button
- songElements.difficultyEasyButton = songOverlayContainer.addChild(LK.getAsset('button', {
- anchorX: 0.5,
- anchorY: 0.5,
- x: overlayCenterX - difficultyButtonWidth - difficultyButtonSpacing,
- y: difficultyButtonY,
- width: difficultyButtonWidth,
- height: difficultyButtonHeight
- }));
- songElements.difficultyEasyButtonText = songOverlayContainer.addChild(new Text2('EASY', {
- size: 40,
- fill: 0xFFFFFF
- }));
- songElements.difficultyEasyButtonText.anchor.set(0.5, 0.5);
- songElements.difficultyEasyButtonText.x = songElements.difficultyEasyButton.x;
- songElements.difficultyEasyButtonText.y = songElements.difficultyEasyButton.y;
- // Medium Button
- songElements.difficultyMediumButton = songOverlayContainer.addChild(LK.getAsset('button', {
- anchorX: 0.5,
- anchorY: 0.5,
- x: overlayCenterX,
- y: difficultyButtonY,
- width: difficultyButtonWidth,
- height: difficultyButtonHeight
- }));
- songElements.difficultyMediumButtonText = songOverlayContainer.addChild(new Text2('MEDIUM', {
- size: 40,
- fill: 0xFFFFFF
- }));
- songElements.difficultyMediumButtonText.anchor.set(0.5, 0.5);
- songElements.difficultyMediumButtonText.x = songElements.difficultyMediumButton.x;
- songElements.difficultyMediumButtonText.y = songElements.difficultyMediumButton.y;
- // Hard Button
- songElements.difficultyHardButton = songOverlayContainer.addChild(LK.getAsset('button', {
- anchorX: 0.5,
- anchorY: 0.5,
- x: overlayCenterX + difficultyButtonWidth + difficultyButtonSpacing,
- y: difficultyButtonY,
- width: difficultyButtonWidth,
- height: difficultyButtonHeight
- }));
- songElements.difficultyHardButtonText = songOverlayContainer.addChild(new Text2('HARD', {
- size: 40,
- fill: 0xFFFFFF
- }));
- songElements.difficultyHardButtonText.anchor.set(0.5, 0.5);
- songElements.difficultyHardButtonText.x = songElements.difficultyHardButton.x;
- songElements.difficultyHardButtonText.y = songElements.difficultyHardButton.y;
}
function clearSongSelectionElements() {
Object.keys(songElements).forEach(function (key) {
if (songElements[key] && songElements[key].destroy && !songElements[key].destroyed) {
@@ -3619,21 +3107,8 @@
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;
- // Update difficulty button tints
- var easyColor = GAME_DIFFICULTY.current === 'easy' ? 0x2e7d32 : 0x666666; // Green if selected, grey otherwise
- var mediumColor = GAME_DIFFICULTY.current === 'medium' ? 0x2e7d32 : 0x666666;
- var hardColor = GAME_DIFFICULTY.current === 'hard' ? 0x2e7d32 : 0x666666;
- if (songElements.difficultyEasyButton && !songElements.difficultyEasyButton.destroyed) {
- songElements.difficultyEasyButton.tint = easyColor;
- }
- if (songElements.difficultyMediumButton && !songElements.difficultyMediumButton.destroyed) {
- songElements.difficultyMediumButton.tint = mediumColor;
- }
- if (songElements.difficultyHardButton && !songElements.difficultyHardButton.destroyed) {
- songElements.difficultyHardButton.tint = hardColor;
- }
}
function updateBoatAnimation() {
if (boatMoving) {
return;
@@ -3655,27 +3130,8 @@
selectedSong--;
updateSongDisplay();
return;
}
- // Handle Difficulty Button Clicks
- if (songElements.difficultyEasyButton && x >= songElements.difficultyEasyButton.x - songElements.difficultyEasyButton.width / 2 && x <= songElements.difficultyEasyButton.x + songElements.difficultyEasyButton.width / 2 && y >= songElements.difficultyEasyButton.y - songElements.difficultyEasyButton.height / 2 && y <= songElements.difficultyEasyButton.y + songElements.difficultyEasyButton.height / 2) {
- GAME_DIFFICULTY.current = 'easy';
- updateSongDisplay();
- LK.getSound('buttonClick').play();
- return;
- }
- if (songElements.difficultyMediumButton && x >= songElements.difficultyMediumButton.x - songElements.difficultyMediumButton.width / 2 && x <= songElements.difficultyMediumButton.x + songElements.difficultyMediumButton.width / 2 && y >= songElements.difficultyMediumButton.y - songElements.difficultyMediumButton.height / 2 && y <= songElements.difficultyMediumButton.y + songElements.difficultyMediumButton.height / 2) {
- GAME_DIFFICULTY.current = 'medium';
- updateSongDisplay();
- LK.getSound('buttonClick').play();
- return;
- }
- if (songElements.difficultyHardButton && x >= songElements.difficultyHardButton.x - songElements.difficultyHardButton.width / 2 && x <= songElements.difficultyHardButton.x + songElements.difficultyHardButton.width / 2 && y >= songElements.difficultyHardButton.y - songElements.difficultyHardButton.height / 2 && y <= songElements.difficultyHardButton.y + songElements.difficultyHardButton.height / 2) {
- GAME_DIFFICULTY.current = 'hard';
- updateSongDisplay();
- LK.getSound('buttonClick').play();
- 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++;
@@ -4187,78 +3643,27 @@
}
});
}
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) {
- swipeState.startX = x;
- swipeState.startY = y;
- // No action on 'down' other than recording start position for swipe/tap detection.
+ inputState.touching = true;
+ inputState.touchLane = getTouchLane(y);
+ inputState.touchStartTime = currentTime;
} else {
- // This is a release (tap or swipe end)
- var deltaX = x - swipeState.startX;
- var deltaY = y - swipeState.startY;
- var distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
- var isTap = distance < swipeState.tapThreshold;
- var isVerticalSwipe = !isTap && Math.abs(deltaY) > swipeState.swipeThreshold && Math.abs(deltaY) > Math.abs(deltaX);
- if (GameState.tutorialMode) {
- // --- TUTORIAL MODE INPUT HANDLING ---
- if (isTap) {
- // Process taps for catching fish ONLY if tutorial is NOT paused AND it's a fishing-action step.
- if (!GameState.tutorialPaused && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4 || GameState.tutorialStep === 5)) {
- if (GameState.tutorialFish && !GameState.tutorialFish.caught && !GameState.tutorialFish.missed) {
- checkCatch(swipeState.currentLane); // Check against the hook's current lane
- }
- }
- // Other tutorial taps (like "CONTINUE" button) are handled by game.down.
- } else if (isVerticalSwipe) {
- // Determine if swiping is allowed in the current tutorial context.
- var canSwipeInTutorial = GameState.tutorialStep === 1 || GameState.tutorialStep === 2 ||
- // Steps 1 & 2: Practice swiping (can be paused for "CONTINUE").
- !GameState.tutorialPaused && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4 || GameState.tutorialStep === 5); // Steps 3, 4 & 5: Active fishing/battle (must be unpaused).
- if (canSwipeInTutorial) {
- if (deltaY < 0) {
- // Swipe up
- swipeState.currentLane = Math.max(0, swipeState.currentLane - 1);
- } else {
- // Swipe down
- swipeState.currentLane = Math.min(GAME_CONFIG.LANES.length - 1, swipeState.currentLane + 1);
- }
- var targetY = GAME_CONFIG.LANES[swipeState.currentLane].y;
- tween(fishingElements.hook, {
- y: targetY
- }, {
- duration: 200,
- easing: tween.easeOut
- });
- fishingElements.hook.originalY = targetY;
- GameState.hookTargetLaneIndex = swipeState.currentLane;
- updateLaneBracketsVisuals();
- }
- }
- } else if (GameState.gameActive) {
- // --- REGULAR GAMEPLAY INPUT HANDLING ---
- if (isTap) {
- checkCatch(swipeState.currentLane);
- } else if (isVerticalSwipe) {
- if (deltaY < 0) {
- // Swipe up
- swipeState.currentLane = Math.max(0, swipeState.currentLane - 1);
- } else {
- // Swipe down
- swipeState.currentLane = Math.min(GAME_CONFIG.LANES.length - 1, swipeState.currentLane + 1);
- }
- var targetY = GAME_CONFIG.LANES[swipeState.currentLane].y;
- tween(fishingElements.hook, {
- y: targetY
- }, {
- duration: 200,
- easing: tween.easeOut
- });
- fishingElements.hook.originalY = targetY;
- GameState.hookTargetLaneIndex = swipeState.currentLane;
- updateLaneBracketsVisuals();
- }
+ if (inputState.touching) {
+ checkCatch(inputState.touchLane);
}
+ inputState.touching = false;
}
}
/****
* Screen Management
@@ -4533,11 +3938,8 @@
}, {
duration: introDuration,
easing: tween.easeInOut
});
- // CRITICAL FIX: Set both visual and logical positions
- swipeState.currentLane = 1; // Ensure logical state is center
- GameState.hookTargetLaneIndex = 1; // Ensure game state is center
var targetHookY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
LK.setTimeout(function () {
LK.getSound('reel').play();
}, 600);
@@ -4551,15 +3953,9 @@
GameState.introPlaying = false;
fishingScreen.pivot.set(0, 0);
fishingScreen.x = 0;
fishingScreen.y = 0;
- fishingElements.hook.originalY = targetHookY;
if (GameState.tutorialMode) {
- // CRITICAL FIX: Initialize tutorial with same state as regular gameplay
- swipeState.currentLane = 1;
- GameState.hookTargetLaneIndex = 1;
- fishingElements.hook.y = GAME_CONFIG.LANES[1].y;
- fishingElements.hook.originalY = GAME_CONFIG.LANES[1].y;
GameState.gameActive = false;
createTutorialElements();
runTutorialStep();
} else {
@@ -4657,13 +4053,11 @@
GameState.combo = 0;
GameState.maxCombo = 0;
GameState.gameActive = true;
GameState.songStartTime = 0;
+ GameState.lastBeatTime = 0;
+ GameState.beatCount = 0;
GameState.musicNotesActive = true;
- // Reset battle state
- GameState.battleState = BATTLE_STATES.NONE;
- GameState.currentBattleFish = null;
- GameState.nextFishSpawnTime = 0;
ImprovedRhythmSpawner.reset();
musicNotesArray = [];
if (fishingElements && fishingElements.musicNotesContainer) {
fishingElements.musicNotesContainer.removeChildren();
@@ -4794,77 +4188,59 @@
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTimeForRegistration);
return newFish;
}
-// Modify the checkCatch function to use the current player lane instead of detecting from touch position
-function checkCatch(playerLane) {
+function checkCatch(fishLane) {
var hookX = fishingElements.hook.x;
if (GameState.tutorialMode) {
var tutorialFish = GameState.tutorialFish;
- if (!tutorialFish || tutorialFish.caught || tutorialFish.missed) {
- // No active tutorial fish to interact with, or it's already been handled.
+ 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;
}
- // For tutorial steps involving active fishing, ensure the hook is in the fish's lane for a valid attempt.
- // This is a soft check; the main check is distance.
- if ((GameState.tutorialStep === 3 || GameState.tutorialStep === 4 || GameState.tutorialStep === 5) && tutorialFish.lane !== playerLane) {
- // If player taps while hook is in the wrong lane, it's a miss conceptually.
- // However, a player might tap just as they finish a swipe. Distance check is more robust.
- // For tutorial clarity, if the tap is clearly in the wrong lane and far from the fish, it's a miss.
- }
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) {
- // Looser window for tutorial's "good"
caughtType = 'good';
} else {
caughtType = 'miss';
}
- showFeedback(caughtType, playerLane); // Show visual feedback (Perfect, Good, 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();
- var isFullyCaughtByThisTap = tutorialFish.handleTap(); // This updates tap count and pushes back if multi-tap
- if (isFullyCaughtByThisTap) {
- tutorialFish.catchFish(); // Handles animation, sets internal 'caught' flag.
- // Actual removal from fishArray and destruction will happen in game.down
- // after "CONTINUE" is pressed, to prevent issues if player exits early.
- GameState.tutorialPaused = true;
- tutorialFish.wasCaughtThisInteraction = true; // Signal to game.down
- if (GameState.tutorialStep === 3) {
- setTutorialText("Great catch! You tapped correctly. Tap 'CONTINUE'.");
- } else if (GameState.tutorialStep === 4) {
- setTutorialText("Perfect! You're mastering the swipe-and-tap. Tap 'CONTINUE'.");
- }
- // Step 5 is usually an informational message after step 4's catch.
- } else {
- // Fish was tapped (multi-tap) but not fully caught. It's pushed back.
- // Tutorial remains unpaused. Player needs to tap again. No text change yet.
- }
- } else {
- // Miss
- LK.getSound('miss').play();
- // Don't set tutorialFish.missed = true permanently, as the player will retry the step.
GameState.tutorialPaused = true;
- tutorialFish.wasMissedThisInteraction = true; // Signal to game.down
if (GameState.tutorialStep === 3) {
- setTutorialText("Almost! Make sure you're in the right lane and tap closer to the fish. Tap 'CONTINUE' to try again.");
+ setTutorialText("Great catch! That's how you do it. Tap 'CONTINUE'.");
} else if (GameState.tutorialStep === 4) {
- setTutorialText("Oops! Try SWIPING to the TOP lane first, then TAP the fish when it's close. Tap 'CONTINUE' to try again.");
+ 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;
}
- // Rest of the function for non-tutorial mode
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 === playerLane && !fish.isPushedBack) {
+ 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;
@@ -4872,19 +4248,12 @@
}
}
}
if (!closestFishInLane) {
- // Miss - play sound and break combo
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();
- }
- // Flash lane brackets red
- if (laneBrackets && laneBrackets[playerLane]) {
- var leftBracket = laneBrackets[playerLane].left;
- var rightBracket = laneBrackets[playerLane].right;
+ 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) {
@@ -4927,85 +4296,75 @@
}
});
}
}
+ GameState.combo = 0;
return;
}
- // Rest of catch logic remains the same...
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';
+ showFeedback('perfect', fishLane);
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) {
points = closestFishInLane.value * multiplier;
- catchType = 'good';
+ showFeedback('good', fishLane);
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.MISS_WINDOW) {
points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier));
- catchType = 'good';
+ showFeedback('good', fishLane);
GameState.combo++;
} else {
- showFeedback('miss', playerLane);
+ showFeedback('miss', fishLane);
LK.getSound('miss').play();
GameState.combo = 0;
if (closestFishInLane) {
closestFishInLane.missed = true;
}
return;
}
- showFeedback(catchType, playerLane);
- var isFullyCaught = closestFishInLane.handleTap();
- if (isFullyCaught) {
- 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);
- // 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 {
- if (!closestFishInLane.isInBattle) {
- closestFishInLane.startBattle();
- }
+ 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);
@@ -5137,49 +4496,39 @@
****/
game.down = function (x, y, obj) {
LK.getSound('buttonClick').play();
var currentScreen = GameState.currentScreen;
- if (GameState.tutorialMode && currentScreen === 'fishing') {
- // 1. Check for "CONTINUE" button tap
+ 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();
- // Special handling for steps 3 & 4 after fish interaction (catch/miss leads to a pause)
- if ((GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && GameState.tutorialPaused) {
- var advanceToNextLogicalStep = false; // Determine if we should advance to the next logical tutorial step (e.g., from 3 to 4)
- if (GameState.tutorialFish && GameState.tutorialFish.wasCaughtThisInteraction) {
- advanceToNextLogicalStep = true;
+ if (GameState.tutorialPaused && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4)) {
+ var advanceAfterCatch = false;
+ if (GameState.tutorialFish && GameState.tutorialFish.caught) {
+ advanceAfterCatch = true;
}
- // Cleanup tutorial fish from previous attempt if it was interacted with
- if (GameState.tutorialFish && (GameState.tutorialFish.wasCaughtThisInteraction || GameState.tutorialFish.wasMissedThisInteraction)) {
- if (!GameState.tutorialFish.destroyed) {
- var idx = fishArray.indexOf(GameState.tutorialFish);
- if (idx > -1) {
- fishArray.splice(idx, 1);
- }
- GameState.tutorialFish.destroy();
+ if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
+ var idx = fishArray.indexOf(GameState.tutorialFish);
+ if (idx > -1) {
+ fishArray.splice(idx, 1);
}
- GameState.tutorialFish = null; // Clear the reference
+ GameState.tutorialFish.destroy();
}
- if (advanceToNextLogicalStep) {
- GameState.tutorialStep++; // Advance to the next tutorial segment (e.g., 3 becomes 4)
+ GameState.tutorialFish = null;
+ if (advanceAfterCatch) {
+ GameState.tutorialStep++;
+ runTutorialStep();
+ } else {
+ runTutorialStep();
}
- // Always call runTutorialStep:
- // - If !advanceToNextLogicalStep, it retries the current step (e.g., step 3 again after a miss).
- // - If advanceToNextLogicalStep, it sets up the new, incremented step.
- runTutorialStep();
} else {
- // Standard "CONTINUE" button behavior for other tutorial steps
GameState.tutorialStep++;
runTutorialStep();
}
- return; // Processed "CONTINUE" tap, exit early
+ } else if ((GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !GameState.tutorialPaused) {
+ handleFishingInput(x, y, true);
}
- // 2. For other taps/presses on the fishing screen during tutorial,
- // pass to handleFishingInput to record swipe start. Actual tap/swipe processing on release (game.up).
- handleFishingInput(x, y, true); // true for isDown
- return; // Processed tutorial screen press, exit early
+ return;
}
- // Original non-tutorial input handling
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) {
@@ -5195,9 +4544,8 @@
case 'levelSelect':
handleLevelSelectInput(x, y);
break;
case 'fishing':
- //{EO} // This case now only handles non-tutorial fishing presses
handleFishingInput(x, y, true);
break;
case 'results':
showScreen('levelSelect');
@@ -5219,8 +4567,9 @@
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;
@@ -5329,10 +4678,24 @@
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) {
GameState.tutorialFish.update();
checkTutorialFishState();
}
- // Automatic hook movement removed; player swipe input (via handleFishingInput)
- // and initial setup in runTutorialStep now control the hook position.
+ // 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;
}
@@ -5351,10 +4714,42 @@
return;
}
// Use RhythmSpawner to handle fish spawning
ImprovedRhythmSpawner.update(currentTime);
- // (Automatic hook following logic removed as player now controls lane position via swipe)
- // updateLaneBracketsVisuals() is now called within handleFishingInput after a swipe.
+ // 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;
@@ -5382,14 +4777,8 @@
}
fish.lastX = currentFrameX;
// Remove off-screen fish
if (!fish.caught && (fish.x < -250 || fish.x > 2048 + 250)) {
- // If this was the battle fish, reset battle state
- if (GameState.currentBattleFish === fish) {
- GameState.battleState = BATTLE_STATES.NONE;
- GameState.currentBattleFish = null;
- GameState.nextFishSpawnTime = 0;
- }
fish.destroy();
fishArray.splice(i, 1);
}
}
@@ -5435,14 +4824,8 @@
});
}
}
}
- // In game.update(), add this after updating fish:
- if (GameState.battleState === BATTLE_STATES.ACTIVE && !GameState.currentBattleFish) {
- // Reset battle state if no active battle fish exists
- GameState.battleState = BATTLE_STATES.NONE;
- GameState.nextFishSpawnTime = 0;
- }
// Update existing music notes
updateParticleArray(musicNotesArray);
// Spawn bubbles for active fish
if (bubbleContainer) {
@@ -5466,16 +4849,5 @@
};
/****
* Initialize game
****/
-showScreen('title');
-// Add to the top of the game code, after the existing variables
-var swipeState = {
- startX: 0,
- startY: 0,
- currentLane: 1,
- // Start in middle lane
- isSwipe: false,
- swipeThreshold: 50,
- // Minimum distance for swipe
- tapThreshold: 30 // Maximum distance for tap
-};
\ No newline at end of file
+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