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 }
User prompt
We should keep the chop lines going even after the prep phase is complete and use them for the cook phase as well. Instead of the current cooking tap detection. We should add a pattern of extended beat indicators that are long notes that last over several beats that have to be held and released at the right time. Keep the pan graphics.
User prompt
When the plating phase is done there is a bug where a whole bunch of assets slide in front the left repeatedly. Please analyze and provide fix.
User prompt
The prep phase does not correctly move to the cooking phase when complete.
User prompt
Make sure that the taps are properly detected for the beat indicators. Right now it always misses. βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
Only one beat indicator spawns on the prep screen, it should spawn a pattern. βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
When the cooking prep mini game starts, the chop lines should start running on tempo with the beat continuously, not just the few we have now. Then the tap indicators can be spaced out in a pattern. Once the pattern has been completed or all indicators have passed, we give the player feedback for how well they did, wrapped in a short timeout, and then move on to the cooking phase. We donβt have to wait a full 15 seconds before moving on. βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
The chop lines should travel horizontally, but be oriented vertically. βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
function createPrepContent(contentArea) { var WINDOW_WIDTH = 1700; var WINDOW_HEIGHT = 900; // Cutting board (centered) var cuttingBoard = contentArea.addChild(LK.getAsset('songCard', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, width: 500, height: 300, color: 0xD2691E, alpha: 0.9 })); // Fish to be cut var fish = contentArea.addChild(LK.getAsset('sardine', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, scaleX: 1.2, scaleY: 1.2 })); contentArea.fish = fish; // Knife (shows when player taps) - STAYS IN THE PREP WINDOW var knife = contentArea.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: 0, // Centered in prep window y: 0, // Centered in prep window with fish scaleX: 0.8, scaleY: 0.8, rotation: Math.PI / 4, alpha: 0 })); contentArea.knife = knife; } ``` ## 2. Create the lines in the cooking overlay itself (below the window): ```javascript // Add this to updateCookingPhases for the prep phase: if (phaseName === 'prep' && contentArea) { var beatInfo = BeatDetector.update(); var timeSincePhaseStart = currentTime - CookingState.phaseStartTime; var beatInterval = BeatDetector.beatInterval; var beatsElapsed = Math.floor(timeSincePhaseStart / beatInterval); // Spawn a line every beat for the required number of beats if (beatsElapsed < COOKING_PHASES.prep.beatsRequired) { var expectedLines = beatsElapsed + 1; if (!overlay.rhythmLines) { overlay.rhythmLines = []; } if (overlay.rhythmLines.length < expectedLines) { var startX = 1800; // Start from right edge of screen var targetX = 1024; // Travel to center (where beat indicator is) var lineY = 2000; // Same Y as beat indicator var lineDuration = beatInterval * 1.2; // Every second line should be a beat indicator var shouldShowBeatIndicator = (overlay.rhythmLines.length % 2) === 1; // Create line directly in the overlay (below the cooking window) var newLine = new ChopLine(startX, targetX, lineY, lineDuration, shouldShowBeatIndicator); overlay.addChild(newLine); // Add to overlay, not contentArea overlay.rhythmLines.push(newLine); } } // Clean up finished lines if (overlay.rhythmLines) { for (var i = overlay.rhythmLines.length - 1; i >= 0; i--) { var line = overlay.rhythmLines[i]; if (line.isDone) { line.destroy(); overlay.rhythmLines.splice(i, 1); } } } } ``` ## 3. Update ChopLine to work with absolute positioning: ```javascript var ChopLine = Container.expand(function(startX, targetX, y, duration, shouldShowBeatIndicator) { var self = Container.call(this); // Create horizontal line that travels horizontally self.line = self.attachAsset('fishingLine', { anchorX: 0.5, anchorY: 0.5, width: 200, // Horizontal line width height: 8, // Thin horizontal line rotation: 0, tint: shouldShowBeatIndicator ? 0x00FF00 : 0xFFFF00, alpha: shouldShowBeatIndicator ? 1.0 : 0.6 }); // Add beat indicator for special lines if (shouldShowBeatIndicator) { self.beatIndicator = self.attachAsset('button', { anchorX: 0.5, anchorY: 0.5, width: 60, height: 60, tint: 0x00FF00, alpha: 0.8, x: 0, y: 0 }); } self.x = startX; self.y = y; self.targetX = targetX; self.duration = duration; self.isDone = false; self.canTap = false; self.shouldShowBeatIndicator = shouldShowBeatIndicator; // Move horizontally towards beat indicator tween(self, { x: targetX }, { duration: duration, easing: tween.linear, onFinish: function() { self.isDone = true; } }); // Enable tapping when close to beat indicator var tapWindow = 80; var checkTapWindow = function() { if (!self.isDone && Math.abs(self.x - targetX) <= tapWindow) { self.canTap = true; if (self.shouldShowBeatIndicator) { self.line.tint = 0x00FF00; } } }; self.tapTimer = LK.setInterval(checkTapWindow, 16); self.destroy = function() { if (self.tapTimer) { LK.clearInterval(self.tapTimer); } Container.prototype.destroy.call(self); }; return self; }); βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
case 'prep': // Check if player tapped when a line is in the tap window if (currentPhaseContainer.contentArea && currentPhaseContainer.contentArea.activeLines) { var lines = currentPhaseContainer.contentArea.activeLines; for (var i = lines.length - 1; i >= 0; i--) { var line = lines[i]; // Check if line is in tap window and tappable if (line.canTap && line.shouldShowBeatIndicator) { // Only allow tapping on green beat indicator lines points = 150; feedback = 'perfect'; success = true; // Remove the line (it gets "absorbed") line.destroy(); lines.splice(i, 1); // Show knife animation if (currentPhaseContainer.contentArea.knife) { var knife = currentPhaseContainer.contentArea.knife; knife.alpha = 1; tween(knife, { alpha: 0 }, { duration: 200 }); } // Damage fish visual if (currentPhaseContainer.contentArea.fish) { var fish = currentPhaseContainer.contentArea.fish; fish.alpha = Math.max(0.3, fish.alpha - 0.25); } // Make beat button flash when hit if (currentPhaseContainer.contentArea.beatTarget) { var beatBtn = currentPhaseContainer.contentArea.beatTarget; beatBtn.tint = 0xFFFFFF; // Flash white LK.setTimeout(function() { if (beatBtn && !beatBtn.destroyed) { beatBtn.tint = 0x4CAF50; // Back to green } }, 100); } break; } } } break; βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
if (phaseName === 'prep' && contentArea) { // Spawn chop lines on beat var beatInfo = BeatDetector.update(); var timeSincePhaseStart = currentTime - CookingState.phaseStartTime; var beatInterval = BeatDetector.beatInterval; var beatsElapsed = Math.floor(timeSincePhaseStart / beatInterval); // Spawn a line every beat for the required number of beats if (beatsElapsed < COOKING_PHASES.prep.beatsRequired) { var expectedLines = beatsElapsed + 1; if (contentArea.activeLines && contentArea.activeLines.length < expectedLines) { var startX = 600; // Start from right side of prep area var targetX = -600; // Travel to beat button on left var lineDuration = beatInterval * 1.2; // Travel time // Every second line should be a beat indicator (when player should tap) var shouldShowBeatIndicator = (contentArea.activeLines.length % 2) === 1; var newLine = new ChopLine(startX, targetX, lineDuration, shouldShowBeatIndicator); if (contentArea.linesContainer && !contentArea.linesContainer.destroyed) { contentArea.linesContainer.addChild(newLine); contentArea.activeLines.push(newLine); } } } // Clean up finished lines if (contentArea.activeLines) { for (var i = contentArea.activeLines.length - 1; i >= 0; i--) { var line = contentArea.activeLines[i]; if (line.isDone) { line.destroy(); contentArea.activeLines.splice(i, 1); } } } } βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
Update with: var ChopLine = Container.expand(function(startX, targetX, duration, shouldShowBeatIndicator) { var self = Container.call(this); // Create vertical line that travels horizontally self.line = self.attachAsset('fishingLine', { anchorX: 0.5, anchorY: 0.5, width: 8, height: 300, // Tall vertical line rotation: 0, // Keep it vertical tint: shouldShowBeatIndicator ? 0x00FF00 : 0xFFFF00, alpha: shouldShowBeatIndicator ? 1.0 : 0.6 }); // Add beat indicator circle for special lines if (shouldShowBeatIndicator) { self.beatIndicator = self.attachAsset('button', { anchorX: 0.5, anchorY: 0.5, width: 60, height: 60, tint: 0x00FF00, alpha: 0.8, x: 0, y: 0 }); // Pulsing animation tween(self.beatIndicator.scale, { x: 1.3, y: 1.3 }, { duration: 200, easing: tween.easeInOut, onFinish: function() { if (self.beatIndicator && !self.beatIndicator.destroyed) { tween(self.beatIndicator.scale, { x: 1.0, y: 1.0 }, { duration: 200, easing: tween.easeInOut }); } } }); } self.x = startX; self.y = 0; // Centered vertically in prep area self.targetX = targetX; self.duration = duration; self.isDone = false; self.canTap = false; self.shouldShowBeatIndicator = shouldShowBeatIndicator; // Move horizontally towards target (right to left) tween(self, { x: targetX }, { duration: duration, easing: tween.linear, onFinish: function() { self.isDone = true; } }); // Enable tapping when close to target var tapWindow = 80; // pixels from target var checkTapWindow = function() { if (!self.isDone && Math.abs(self.x - targetX) <= tapWindow) { self.canTap = true; if (self.shouldShowBeatIndicator) { self.line.tint = 0x00FF00; // Bright green when in tap window } else { self.line.tint = 0xFFFF00; // Bright yellow when in tap window } } }; self.tapTimer = LK.setInterval(checkTapWindow, 16); self.destroy = function() { if (self.tapTimer) { LK.clearInterval(self.tapTimer); } Container.prototype.destroy.call(self); }; return self; });
User prompt
Please fix the bug: 'TypeError: null is not an object (evaluating 'CookingState.currentRecipe.name')' in or related to this line: 'var recipeName = resultsContainer.addChild(new Text2(CookingState.currentRecipe.name, {' Line Number: 3069
User prompt
Please fix the bug: 'TypeError: null is not an object (evaluating 'recipe.baseValue')' in or related to this line: 'var earnings = Math.floor(recipe.baseValue * recipe.multiplier * stars);' Line Number: 3012
User prompt
function checkPhaseTimeout() { if (!CookingState.active || CookingState.transitionActive) { return; } var currentTime = LK.ticks * (1000 / 60); var timeSincePhaseStart = currentTime - CookingState.phaseStartTime; var phases = ['prep', 'cook', 'plate']; var phaseName = phases[CookingState.currentPhase]; var phaseConfig = COOKING_PHASES[phaseName]; if (!phaseConfig) { console.error("Invalid phase config in checkPhaseTimeout for phase:", CookingState.currentPhase); return; } // Force phase completion after reasonable time (15 seconds) var forceCompleteTime = 15000; if (timeSincePhaseStart > forceCompleteTime) { console.log("Force completing phase due to timeout:", phaseName); completePhase(); return; } // Also complete if all required beats have been hit if (CookingState.beatsHit >= phaseConfig.beatsRequired) { console.log("Completing phase - all beats hit:", phaseName); completePhase(); } }
User prompt
Update as needed with: function setupPhase(phaseIndex) { // Prevent multiple simultaneous phase setups if (CookingState.transitionActive) { console.log("Phase transition already in progress, ignoring setupPhase call"); return; } var phases = ['prep', 'cook', 'plate']; var phaseName = phases[phaseIndex]; var phaseConfig = COOKING_PHASES[phaseName]; // Add safety check for phaseConfig if (!phaseConfig) { console.error("Invalid phase configuration for:", phaseName, "at index:", phaseIndex); return; } console.log("Setting up phase:", phaseName, "at index:", phaseIndex); CookingState.currentPhase = phaseIndex; CookingState.phaseStartTime = LK.ticks * (1000 / 60); CookingState.totalBeats = phaseConfig.beatsRequired; CookingState.beatsHit = 0; CookingState.transitionActive = true; // Set this FIRST to prevent duplicate calls var overlay = restaurantScreen.cookingOverlay; if (!overlay) { CookingState.transitionActive = false; // Reset if overlay doesn't exist return; } // Update fixed UI elements if (overlay.progressText) { overlay.progressText.setText('Phase ' + (phaseIndex + 1) + '/3'); } // Create new phase container var newPhaseContainer = createPhaseContainer(phaseName, phaseConfig); var WINDOW_WIDTH = 1700; // Position new container off-screen based on slide direction if (phaseConfig.slideDirection === 'left') { newPhaseContainer.x = -WINDOW_WIDTH; } else { newPhaseContainer.x = WINDOW_WIDTH; } newPhaseContainer.y = 0; overlay.phaseContainer.addChild(newPhaseContainer); CookingState.nextPhaseContainer = newPhaseContainer; // Slide out old container (if exists) and slide in new container var slideDuration = 800; if (CookingState.currentPhaseContainer) { var slideOutTarget = phaseConfig.slideDirection === 'left' ? WINDOW_WIDTH : -WINDOW_WIDTH; tween(CookingState.currentPhaseContainer, { x: slideOutTarget }, { duration: slideDuration, easing: tween.easeInOut, onFinish: function() { if (CookingState.currentPhaseContainer && !CookingState.currentPhaseContainer.destroyed) { CookingState.currentPhaseContainer.destroy(); } } }); } // Slide in new container tween(newPhaseContainer, { x: 0 }, { duration: slideDuration, easing: tween.easeInOut, onFinish: function() { CookingState.currentPhaseContainer = newPhaseContainer; CookingState.nextPhaseContainer = null; CookingState.transitionActive = false; // Only reset after transition is complete console.log("Phase transition completed for:", phaseName); } }); // Play transition sound LK.getSound('reel').play(); } βͺπ‘ Consider importing and using the following plugins: @upit/tween.v1
User prompt
Update with: function updateCookingPhases() { if (!CookingState.active || !CookingState.currentPhaseContainer) { return; } // Check for phase timeout/completion checkPhaseTimeout(); var currentTime = LK.ticks * (1000 / 60); var phaseName = ['prep', 'cook', 'plate'][CookingState.currentPhase]; var contentArea = CookingState.currentPhaseContainer.contentArea; if (phaseName === 'prep' && contentArea) { // Spawn chop lines on beat var beatInfo = BeatDetector.update(); var timeSincePhaseStart = currentTime - CookingState.phaseStartTime; var beatInterval = BeatDetector.beatInterval; var beatsElapsed = Math.floor(timeSincePhaseStart / beatInterval); // Spawn a line every beat for the required number of beats if (beatsElapsed < COOKING_PHASES.prep.beatsRequired) { var expectedLines = beatsElapsed + 1; if (contentArea.activeLines && contentArea.activeLines.length < expectedLines) { var startY = -150; // Start above the prep area var targetY = 180; // Target zone Y position var lineDuration = beatInterval * 1.2; // Slightly longer travel time // Every second line should be a beat indicator (when player should tap) var shouldShowBeatIndicator = (contentArea.activeLines.length % 2) === 1; var newLine = new ChopLine(startY, targetY, lineDuration, shouldShowBeatIndicator); if (contentArea.linesContainer && !contentArea.linesContainer.destroyed) { contentArea.linesContainer.addChild(newLine); contentArea.activeLines.push(newLine); } } } // Clean up finished lines if (contentArea.activeLines) { for (var i = contentArea.activeLines.length - 1; i >= 0; i--) { var line = contentArea.activeLines[i]; if (line.isDone) { line.destroy(); contentArea.activeLines.splice(i, 1); } } } } else if (phaseName === 'plate' && contentArea) { // Add safety checks for plate phase if (!contentArea.garnishPattern) { console.error("garnishPattern not initialized for plate phase"); return; } if (!contentArea.activeRings) { contentArea.activeRings = []; } if (contentArea.nextPatternIndex === undefined) { contentArea.nextPatternIndex = 0; } // Spawn garnish rings according to pattern var beatInfo = BeatDetector.update(); var timeSincePhaseStart = currentTime - CookingState.phaseStartTime; var beatInterval = BeatDetector.beatInterval; var beatsElapsed = Math.floor(timeSincePhaseStart / beatInterval); // Check if we should spawn a ring for this beat if (contentArea.nextPatternIndex < contentArea.garnishPattern.length) { var nextPattern = contentArea.garnishPattern[contentArea.nextPatternIndex]; if (beatsElapsed >= nextPattern.beat - 1) { // Spawn one beat before target var spot = contentArea.garnishSpots[nextPattern.spotIndex]; var initialRadius = 80; var shrinkDuration = beatInterval * 1.5; // Time to shrink to center var newRing = new GarnishRing(spot.x, spot.y, initialRadius, shrinkDuration); if (contentArea.ringsContainer && !contentArea.ringsContainer.destroyed) { contentArea.ringsContainer.addChild(newRing); contentArea.activeRings.push(newRing); } contentArea.nextPatternIndex++; } } // Clean up finished rings for (var i = contentArea.activeRings.length - 1; i >= 0; i--) { var ring = contentArea.activeRings[i]; if (ring.isDone) { ring.destroy(); contentArea.activeRings.splice(i, 1); } } } }
User prompt
Update only as needed with: function createPlateContent(contentArea) { // Plate var plate = contentArea.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, width: 350, height: 350, tint: 0xFFFFFF, alpha: 0.9 })); // Cooked fish (main dish) var cookedFish = contentArea.addChild(LK.getAsset('sardine', { anchorX: 0.5, anchorY: 0.5, x: 0, y: 0, scaleX: 0.8, scaleY: 0.8, tint: 0xCD853F })); contentArea.cookedFish = cookedFish; // Garnish spots (where rings will collapse to) var garnishSpots = []; var spotPositions = [ { x: -120, y: -80 }, // Top left { x: 120, y: -80 }, // Top right { x: -120, y: 80 }, // Bottom left { x: 120, y: 80 } // Bottom right ]; for (var i = 0; i < spotPositions.length; i++) { var pos = spotPositions[i]; var spot = contentArea.addChild(LK.getAsset('dottedLine', { anchorX: 0.5, anchorY: 0.5, x: pos.x, y: pos.y, width: 30, height: 30, tint: 0x888888, alpha: 0.5 })); garnishSpots.push(spot); } contentArea.garnishSpots = garnishSpots; contentArea.garnishes = []; contentArea.activeRings = []; contentArea.ringsContainer = contentArea.addChild(new Container()); // IMPORTANT: Always initialize garnishPattern contentArea.garnishPattern = [ { spotIndex: 0, beat: 1 }, { spotIndex: 2, beat: 3 }, { spotIndex: 1, beat: 5 }, { spotIndex: 3, beat: 7 } ]; contentArea.nextPatternIndex = 0; }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.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 ChopLine = Container.expand(function (startX, targetX, y, duration, shouldShowBeatIndicator) {
var self = Container.call(this);
// Create vertical line that travels horizontally
self.line = self.attachAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0.5,
width: 8,
// Vertical line width
height: 300,
// Tall vertical line
rotation: 0,
tint: shouldShowBeatIndicator ? 0x00FF00 : 0xFFFF00,
alpha: shouldShowBeatIndicator ? 1.0 : 0.6
});
// Add beat indicator for special lines
if (shouldShowBeatIndicator) {
self.beatIndicator = self.attachAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
width: 60,
height: 60,
tint: 0x00FF00,
alpha: 0.8,
x: 0,
y: 0
});
}
self.x = startX;
self.y = y; // Use passed Y
self.targetX = targetX;
self.duration = duration;
self.isDone = false;
self.canTap = false;
self.shouldShowBeatIndicator = shouldShowBeatIndicator;
// Move horizontally towards beat indicator
tween(self, {
x: targetX
}, {
duration: duration,
easing: tween.linear,
onFinish: function onFinish() {
self.isDone = true;
}
});
// Enable tapping when close to beat indicator
var tapWindow = 80;
var checkTapWindow = function checkTapWindow() {
if (!self.isDone && Math.abs(self.x - targetX) <= tapWindow) {
self.canTap = true;
if (self.shouldShowBeatIndicator) {
self.line.tint = 0x00FF00; // Bright green when in tap window
}
}
};
self.tapTimer = LK.setInterval(checkTapWindow, 16);
self.destroy = function () {
if (self.tapTimer) {
LK.clearInterval(self.tapTimer);
}
Container.prototype.destroy.call(self);
};
// New properties for long notes
self.isLongNote = false;
self.durationBeats = 1; // Default to 1 beat if it's a long note but duration not specified
self.isBeingHeld = false;
self.holdStartTime = 0;
self.beatNumber = 0; // The beat this note is intended for
// Call this after setting isLongNote and durationBeats
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
}
};
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;
});
// Customer class
var Customer = Container.expand(function (tableIndex, orderRecipe) {
var self = Container.call(this);
self.tableIndex = tableIndex;
self.orderRecipe = orderRecipe;
self.orderTime = LK.ticks * (1000 / 60);
self.patience = 45000; // 45 seconds patience
self.served = false;
// Simple customer graphic (placeholder)
var customerGfx = self.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
width: 60,
height: 60,
tint: 0x4CAF50,
// Green customer
alpha: 0.8
}));
// Order bubble
var orderBubble = self.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 1,
x: 0,
y: -40,
// Adjusted Y to be above customer head
width: 200,
height: 80,
color: 0xFFFFFF,
alpha: 0.9
}));
orderBubble.pivot.y = orderBubble.height; // Pivot at bottom center of bubble
var orderText = self.addChild(new Text2(RestaurantState.recipes[orderRecipe].name, {
size: 28,
fill: 0x000000,
align: 'center',
wordWrap: true,
wordWrapWidth: 180
}));
orderText.anchor.set(0.5, 0.5);
// Position text relative to orderBubble's new pivot and position
orderText.x = orderBubble.x;
orderText.y = orderBubble.y - orderBubble.height / 2;
// Patience timer ring (simple version)
var timerRing = self.addChild(LK.getAsset('button', {
// Using button as placeholder shape
anchorX: 0.5,
anchorY: 0.5,
width: 80,
height: 80,
tint: 0xFF9800,
// Orange
alpha: 0.3
}));
self.update = function () {
if (self.destroyed || !RestaurantState.sessionActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60);
var timeLeft = self.patience - (currentTime - self.orderTime);
var patiencePercent = Math.max(0, timeLeft / self.patience);
// Update timer ring color based on patience
if (patiencePercent > 0.6) {
timerRing.tint = 0x4CAF50; // Green
} else if (patiencePercent > 0.3) {
timerRing.tint = 0xFF9800; // Orange
} else {
timerRing.tint = 0xF44336; // Red
}
timerRing.alpha = 0.3 + 0.4 * (1 - patiencePercent); // Ring becomes more opaque as patience runs out
// Customer leaves if patience runs out
if (timeLeft <= 0 && !self.served) {
self.leaveRestaurant();
}
};
self.leaveRestaurant = function () {
if (self.destroyed) {
return;
}
// Remove from customers array
var index = RestaurantState.currentCustomers.indexOf(self);
if (index > -1) {
RestaurantState.currentCustomers.splice(index, 1);
}
// Animate leaving
tween(self, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
if (!self.destroyed) {
self.destroy();
}
}
});
};
self.serveCustomer = function (stars) {
// Assuming stars might be used later for quality
if (self.destroyed || self.served) {
return;
}
self.served = true;
var recipe = RestaurantState.recipes[self.orderRecipe];
var earnings = Math.floor(recipe.baseValue * recipe.multiplier * (stars || 1.0)); // Default stars to 1.0
RestaurantState.sessionEarnings += earnings;
GameState.money += earnings;
RestaurantState.customersServed++;
// Show earnings popup
var earningsText = new Text2('+$' + earnings, {
size: 60,
fill: 0x4CAF50,
// Green for earnings
stroke: 0x000000,
strokeThickness: 2
});
earningsText.anchor.set(0.5, 0.5);
earningsText.x = self.x;
earningsText.y = self.y - 100; // Start above customer
if (restaurantScreen && !restaurantScreen.destroyed) {
restaurantScreen.addChild(earningsText);
}
tween(earningsText, {
y: earningsText.y - 150,
// Move up
alpha: 0
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (earningsText && !earningsText.destroyed) {
earningsText.destroy();
}
}
});
self.leaveRestaurant(); // Customer leaves after being served
};
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);
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;
};
// Existing asset selection logic
var assetName = type + 'Fish';
if (type === 'shallow') {
var currentSongConfig = null;
if (typeof GameState !== 'undefined' && GameState && typeof GameState.getCurrentSongConfig === 'function') {
currentSongConfig = GameState.getCurrentSongConfig();
}
if (currentSongConfig && currentSongConfig.name === "Gentle Waves") {
var randGentle = Math.random();
if (randGentle < 0.60) {
assetName = 'anchovy';
} else if (randGentle < 0.90) {
assetName = 'sardine';
} else {
assetName = 'mackerel';
}
} else if (currentSongConfig && currentSongConfig.name === "Morning Tide") {
var randMorning = Math.random();
if (randMorning < 0.40) {
assetName = 'anchovy';
} else if (randMorning < 0.80) {
assetName = 'sardine';
} else {
assetName = 'mackerel';
}
} else if (currentSongConfig && currentSongConfig.name === "Sunny Afternoon") {
var randSunny = Math.random();
if (randSunny < 0.30) {
assetName = 'anchovy';
} else if (randSunny < 0.60) {
assetName = 'sardine';
} else {
assetName = 'mackerel';
}
} else {
var shallowFishAssets = ['sardine', 'anchovy', 'mackerel'];
assetName = shallowFishAssets[Math.floor(Math.random() * shallowFishAssets.length)];
}
}
// ADD THIS: Store the actual asset name for inventory tracking
self.actualAssetName = assetName;
self.fishGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
if (speed > 0) {
self.fishGraphics.scaleX = -1;
}
// Existing properties
self.type = type;
self.value = value;
self.speed = speed;
self.lane = lane; // This is the initial 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;
// 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 = self.actualAssetName; // 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
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;
}
}
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.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;
// The fish will move to its currently planned lane.
var laneToMoveToForThisPushback = self.nextPlannedMoveLane;
var laneFishWasInBeforeThisMove = self.lane; // Store where the fish was.
// Update fish's internal lane state FOR THIS MOVEMENT
// If nextPlannedMoveLane is somehow invalid (-1), keep current lane.
if (laneToMoveToForThisPushback !== -1) {
self.lane = laneToMoveToForThisPushback;
}
// else: fish stays in its current lane if no valid planned move (shouldn't happen with current logic)
var targetY = GAME_CONFIG.LANES[self.lane].y; // Target Y for the tween
self.baseY = targetY; // Update base Y for swim animation once it settles
// Determine the NEXT planned move for the SUBSEQUENT pushback (if any).
// This will be used for updating the arrow on the counter.
var tapsRemainingIncludingThisOne = self.maxTaps - self.currentTaps;
if (tapsRemainingIncludingThisOne > 1) {
// If there will be at least one more pushback *after* this current one completes
self.nextPlannedMoveLane = self.determineSubsequentPlannedLane(self.lane, laneFishWasInBeforeThisMove);
} else {
self.nextPlannedMoveLane = -1; // No more moves planned after this current one
}
if (self.isMultiTapFish && self.tapCounter && !self.tapCounter.destroyed) {
var tapsStillNeededForDisplay = self.maxTaps - self.currentTaps; // Taps needed for counter (current one is still active)
var counterText = tapsStillNeededForDisplay.toString();
// Arrow logic: Show arrow if more taps are needed *after* this current pushback resolves
// (i.e., tapsStillNeededForDisplay > 1) AND a next move is planned.
if (tapsStillNeededForDisplay > 1 && self.nextPlannedMoveLane !== -1 && self.nextPlannedMoveLane !== self.lane) {
// Arrow points from the fish's NEW current lane (self.lane) to the NEXT planned move (self.nextPlannedMoveLane)
if (self.nextPlannedMoveLane < self.lane) {
counterText += "β";
} else if (self.nextPlannedMoveLane > self.lane) {
counterText += "β";
}
}
self.tapCounter.setText(counterText);
} // End of tapCounter update
// targetY is already set based on the new self.lane
self.baseY = targetY;
// Calculate timing for next approach
self.currentPatternIndex++;
var songConfig = GameState.getCurrentSongConfig();
var beatInterval = 60000 / songConfig.bpm;
var timeToNextTap = beatInterval; // Always full beat
// Use original fish speed (keep it steady)
var fishSpeed = Math.abs(self.originalSpeed);
var framesForNextApproach = timeToNextTap / (1000 / 60);
// Calculate how far back we need to push based on selected difficulty.
var pushbackBeatMultiplier = GAME_DIFFICULTY.pushbackMultiplier[GAME_DIFFICULTY.current] || 3; // Default to easy (3) if not found
var distanceNeededToReachHook = fishSpeed * framesForNextApproach * pushbackBeatMultiplier;
var pushBackX;
if (self.originalSpeed > 0) {
// Fish moves right, push it left of hook
pushBackX = GAME_CONFIG.SCREEN_CENTER_X - distanceNeededToReachHook;
} else {
// Fish moves left, push it right of hook
pushBackX = GAME_CONFIG.SCREEN_CENTER_X + distanceNeededToReachHook;
}
// One smooth motion: push back while moving to new lane
tween(self, {
x: pushBackX,
y: targetY
}, {
duration: 200,
// Quick pushback
easing: tween.easeOut,
onFinish: function onFinish() {
// Resume original steady speed
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.scheduleNextFish();
};
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.scheduleNextFish();
}
// ADD THIS: Determine fish type for inventory
var inventoryFishType = self.getInventoryFishType();
if (inventoryFishType) {
FishInventory.addFish(inventoryFishType, 1);
}
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() {
if (self && !self.destroyed) {
self.destroy();
}
}
});
}
});
};
self.getInventoryFishType = function () {
// Direct mapping based on stored asset name
var assetToInventoryMap = {
'anchovy': 'anchovy',
'sardine': 'sardine',
'mackerel': 'mackerel',
'rareFish': 'rareFish',
'mediumFish': 'mediumFish',
// Added
'deepFish': 'deepFish' // Added
};
return assetToInventoryMap[self.actualAssetName] || 'sardine'; // Fallback to sardine
};
return self;
});
var GarnishRing = Container.expand(function (centerX, centerY, initialRadius, duration) {
var self = Container.call(this);
self.ring = self.attachAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
width: initialRadius * 2,
height: initialRadius * 2,
tint: 0x4CAF50,
alpha: 0.6
});
self.x = centerX;
self.y = centerY;
self.initialRadius = initialRadius;
self.currentRadius = initialRadius;
self.duration = duration;
self.isDone = false;
self.canTap = false;
// Shrink towards center
tween(self.ring, {
width: 20,
height: 20
}, {
duration: duration,
easing: tween.linear,
onFinish: function onFinish() {
self.isDone = true;
}
});
// Enable tapping when close to center
var tapRadius = 30;
var checkTapWindow = function checkTapWindow() {
if (!self.isDone) {
self.currentRadius = self.ring.width / 2;
if (self.currentRadius <= tapRadius) {
self.canTap = true;
self.ring.tint = 0xFFD700; // Gold when in tap window
}
}
};
self.tapTimer = LK.setInterval(checkTapWindow, 16);
self.destroy = function () {
if (self.tapTimer) {
LK.clearInterval(self.tapTimer);
}
Container.prototype.destroy.call(self);
};
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);
}
// Beat Detection System
var BeatDetector = {
bpm: 120,
// Default BPM
beatInterval: 500,
// ms between beats
lastBeatTime: 0,
tolerance: 100,
// ms tolerance for "on beat"
init: function init(bpm) {
this.bpm = bpm || 120;
this.beatInterval = 60000 / this.bpm;
this.lastBeatTime = LK.ticks * (1000 / 60);
},
update: function update() {
var currentTime = LK.ticks * (1000 / 60);
var timeSinceLastBeat = (currentTime - this.lastBeatTime) % this.beatInterval;
// Update beat indicator visual
if (restaurantScreen.cookingOverlay && restaurantScreen.cookingOverlay.beatIndicator && !restaurantScreen.cookingOverlay.beatIndicator.destroyed) {
var beatProgress = timeSinceLastBeat / this.beatInterval;
var intensity = Math.sin(beatProgress * Math.PI * 2) * 0.5 + 0.5; // intensidade de 0 a 1
var scale = 0.8 + intensity * 0.4; // escala de 0.8 a 1.2
restaurantScreen.cookingOverlay.beatIndicator.scale.set(scale);
// Color pulse (verde para amarelo)
var redChannel = Math.floor(intensity * 100); // 0 a 100 para o canal vermelho (mais amarelo no pico)
var greenChannel = 102 + Math.floor(intensity * 153); // 102 (0x66) a 255 (0xFF) para o verde
var blueChannel = 0; // Sem azul por enquanto
var color = redChannel << 16 | greenChannel << 8 | blueChannel;
restaurantScreen.cookingOverlay.beatIndicator.tint = color;
}
return {
timeSinceLastBeat: timeSinceLastBeat,
beatProgress: timeSinceLastBeat / this.beatInterval,
isOnBeat: this.isOnBeat(currentTime)
};
},
isOnBeat: function isOnBeat(currentTime) {
var timeSinceLastBeat = (currentTime - this.lastBeatTime) % this.beatInterval;
return timeSinceLastBeat < this.tolerance || timeSinceLastBeat > this.beatInterval - this.tolerance;
}
};
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
};
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)
} else if (GameState.currentScreen === 'restaurant' && CookingState.active) {
var phaseName = ['prep', 'cook', 'plate'][CookingState.currentPhase];
if (phaseName === 'cook') {
handleCookingInput(x, y, 'up', obj); // Pass 'up' eventType
}
// For prep/plate, 'up' might not be explicitly handled or could be part of a 'tap' gesture completed on down.
// If prep/plate need distinct 'up' logic, it can be added here.
}
// 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,
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
****/
// 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', 'beat'],
// 2 taps, 2 lane changes
anchovy: ['beat', 'beat', 'beat'],
// 3 taps, 3 lane changes
mackerel: ['beat', 'beat', 'beat', 'beat'],
// 4 taps, 4 lane changes
rareFish: ['beat', 'beat', 'beat', 'beat', 'beat'] // 5 taps, 5 lane changes
};
var BATTLE_STATES = {
NONE: 'none',
ACTIVE: 'active'
};
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"
}],
//[FN#9T]
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"
}]
}
}
};
/****
* Fish Inventory System
****/
var FishInventory = {
// Add mediumFish and deepFish to track all types
fishTypes: ['anchovy', 'sardine', 'mackerel', 'mediumFish', 'deepFish', 'rareFish'],
init: function init() {
// Initialize fish inventory if not exists
this.fishTypes.forEach(function (fishType) {
var storageKey = 'fishInventory_' + fishType;
if (storage[storageKey] === undefined) {
storage[storageKey] = 0;
}
});
// Initialize restaurant unlock status
if (storage.restaurantUnlocked === undefined) {
storage.restaurantUnlocked = false;
}
},
addFish: function addFish(fishType, quantity) {
quantity = quantity || 1;
var storageKey = 'fishInventory_' + fishType;
if (storage[storageKey] !== undefined) {
storage[storageKey] += quantity;
this.checkRestaurantUnlock();
return true;
}
return false;
},
removeFish: function removeFish(fishType, quantity) {
quantity = quantity || 1;
var storageKey = 'fishInventory_' + fishType;
if (storage[storageKey] !== undefined && storage[storageKey] >= quantity) {
storage[storageKey] -= quantity;
return true;
}
return false;
},
getFishCount: function getFishCount(fishType) {
var storageKey = 'fishInventory_' + fishType;
return storage[storageKey] || 0;
},
getTotalFish: function getTotalFish() {
var total = 0;
this.fishTypes.forEach(function (fishType) {
total += FishInventory.getFishCount(fishType);
});
return total;
},
hasAnyFish: function hasAnyFish() {
return this.getTotalFish() > 0;
},
checkRestaurantUnlock: function checkRestaurantUnlock() {
if (!storage.restaurantUnlocked && this.hasAnyFish()) {
storage.restaurantUnlocked = true;
// Update map if we're on level select screen
if (GameState.currentScreen === 'levelSelect' && levelSelectElements && levelSelectElements.updateMapDisplay) {
levelSelectElements.updateMapDisplay();
}
}
},
getInventoryDisplay: function getInventoryDisplay() {
var display = [];
this.fishTypes.forEach(function (fishType) {
var count = FishInventory.getFishCount(fishType);
if (count > 0) {
var displayName = fishType.charAt(0).toUpperCase() + fishType.slice(1);
if (fishType === 'rareFish') {
displayName = 'Rare Fish';
}
if (fishType === 'mediumFish') {
displayName = 'Medium Fish';
}
if (fishType === 'deepFish') {
displayName = 'Deep Fish';
}
display.push({
type: fishType,
name: displayName,
count: count
});
}
});
return display;
}
};
// Initialize fish inventory system
FishInventory.init();
/****
* Restaurant Game State & Recipe System
****/
var RestaurantState = {
sessionActive: false,
sessionStartTime: 0,
sessionDuration: 180000,
// 3 minutes like fishing
currentCustomers: [],
selectedMenu: [],
// Stores keys of selected recipes
sessionEarnings: 0,
customersServed: 0,
// Recipe definitions
recipes: {
fishAndChips: {
name: "Fish & Chips",
requiredFish: [{
type: "any",
count: 1
}],
// e.g., 1 of any fish
baseValue: 15,
multiplier: 1.0,
// Could be adjusted by quality/minigame later
difficulty: "easy" // For potential minigames
},
fishTacos: {
name: "Fish Tacos",
requiredFish: [{
type: "medium+",
count: 1
}],
// e.g., 1 sardine, mackerel, mediumFish, deepFish, or rareFish
baseValue: 25,
multiplier: 1.5,
difficulty: "medium"
},
searedFishSpecial: {
name: "Seared Fish Special",
requiredFish: [{
type: "rareFish",
count: 1
}],
// e.g., 1 rare fish
baseValue: 50,
multiplier: 3.0,
difficulty: "hard"
}
// Add more recipes here later
},
reset: function reset() {
this.sessionActive = false;
this.sessionStartTime = 0;
this.currentCustomers.forEach(function (customer) {
if (customer && !customer.destroyed) {
customer.destroy();
}
});
this.currentCustomers = [];
this.selectedMenu = [];
this.sessionEarnings = 0;
this.customersServed = 0;
},
canMakeRecipe: function canMakeRecipe(recipeKey) {
var recipe = this.recipes[recipeKey];
if (!recipe) {
return false;
}
for (var i = 0; i < recipe.requiredFish.length; i++) {
var requirement = recipe.requiredFish[i];
if (requirement.type === "any") {
if (FishInventory.getTotalFish() < requirement.count) {
return false;
}
} else if (requirement.type === "medium+") {
// Check for any fish that is medium or rarer
var availableCount = FishInventory.getFishCount('sardine') + FishInventory.getFishCount('mackerel') + FishInventory.getFishCount('mediumFish') + FishInventory.getFishCount('deepFish') + FishInventory.getFishCount('rareFish');
if (availableCount < requirement.count) {
return false;
}
} else {
// Specific fish type
if (FishInventory.getFishCount(requirement.type) < requirement.count) {
return false;
}
}
}
return true;
}
};
// Pre-service menu selection screen
function showMenuSelection() {
var menuOverlay = restaurantScreen.addChild(new Container());
var overlayBg = menuOverlay.addChild(LK.getAsset('songCard', {
// Placeholder asset
x: 1024,
// Center X
y: 1366,
// Center Y
width: 1800,
// Large overlay
height: 1200,
color: 0x424242,
// Dark grey
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.95
}));
var titleText = menuOverlay.addChild(new Text2('SELECT MENU (Choose 3 dishes)', {
size: 80,
fill: 0xFFFFFF,
align: 'center'
}));
titleText.anchor.set(0.5, 0.5);
titleText.x = 1024;
titleText.y = overlayBg.y - overlayBg.height / 2 + 100; // Top of overlay
var selectedCount = 0;
var maxSelection = 3;
var selectedRecipes = []; // Stores recipe keys
// Recipe buttons
var recipeKeys = Object.keys(RestaurantState.recipes);
var buttonWidth = 350;
var buttonHeight = 180;
var buttonSpacingX = 50;
var buttonTotalWidth = recipeKeys.length * buttonWidth + (recipeKeys.length - 1) * buttonSpacingX;
var startButtonX = 1024 - buttonTotalWidth / 2 + buttonWidth / 2;
var buttonY = titleText.y + titleText.height + 150;
for (var i = 0; i < recipeKeys.length; i++) {
var recipeKey = recipeKeys[i];
var recipe = RestaurantState.recipes[recipeKey];
var canMake = RestaurantState.canMakeRecipe(recipeKey);
var currentButtonX = startButtonX + i * (buttonWidth + buttonSpacingX);
var recipeButton = menuOverlay.addChild(LK.getAsset('button', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5,
x: currentButtonX,
y: buttonY,
width: buttonWidth,
height: buttonHeight,
tint: canMake ? 0x4CAF50 : 0x666666 // Green if can make, grey otherwise
}));
recipeButton.recipeKey = recipeKey;
recipeButton.selected = false;
var recipeText = menuOverlay.addChild(new Text2(recipe.name + "\n(Req: " + recipe.requiredFish[0].count + " " + recipe.requiredFish[0].type + ")", {
size: 32,
fill: 0xFFFFFF,
align: 'center',
wordWrap: true,
wordWrapWidth: buttonWidth - 20
}));
recipeText.anchor.set(0.5, 0.5);
recipeText.x = currentButtonX;
recipeText.y = buttonY;
}
// Start service button
var startButton = menuOverlay.addChild(LK.getAsset('bigButton', {
// Placeholder asset
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: overlayBg.y + overlayBg.height / 2 - 100,
// Bottom of overlay
width: 400,
height: 100,
tint: 0x666666 // Disabled by default
}));
var startButtonText = menuOverlay.addChild(new Text2('START SERVICE', {
size: 50,
fill: 0xFFFFFF
}));
startButtonText.anchor.set(0.5, 0.5);
startButtonText.x = startButton.x;
startButtonText.y = startButton.y;
// Menu selection input handling (specific to this overlay)
function handleMenuSelectionInput(x, y) {
// Check recipe buttons
for (var i = 0; i < menuOverlay.children.length; i++) {
var child = menuOverlay.children[i];
if (child.recipeKey && RestaurantState.canMakeRecipe(child.recipeKey)) {
// Check if it's a recipe button and can be made
if (x >= child.x - child.width / 2 && x <= child.x + child.width / 2 && y >= child.y - child.height / 2 && y <= child.y + child.height / 2) {
if (!child.selected && selectedCount < maxSelection) {
child.selected = true;
child.tint = 0x1976D2; // Blue when selected
selectedRecipes.push(child.recipeKey);
selectedCount++;
} else if (child.selected) {
child.selected = false;
child.tint = 0x4CAF50; // Back to green (if can make)
var index = selectedRecipes.indexOf(child.recipeKey);
if (index > -1) {
selectedRecipes.splice(index, 1);
}
selectedCount--;
}
// Update start button
if (selectedCount === maxSelection) {
startButton.tint = 0x4CAF50; // Green (enabled)
} else {
startButton.tint = 0x666666; // Grey (disabled)
}
LK.getSound('buttonClick').play();
return;
}
}
}
// Check start button
if (selectedCount === maxSelection && x >= startButton.x - startButton.width / 2 && x <= startButton.x + startButton.width / 2 && y >= startButton.y - startButton.height / 2 && y <= startButton.y + startButton.height / 2) {
RestaurantState.selectedMenu = selectedRecipes.slice(); // Copy selected recipes
menuOverlay.destroy(); // This will also restore original input handler
startRestaurantSession();
LK.getSound('buttonClick').play();
}
}
// Temporarily override restaurant input handling
var originalHandleRestaurantInput = handleRestaurantInput;
handleRestaurantInput = handleMenuSelectionInput;
// Restore original handler when overlay is destroyed
var originalDestroy = menuOverlay.destroy;
menuOverlay.destroy = function () {
handleRestaurantInput = originalHandleRestaurantInput; // Restore the global function variable
if (originalDestroy) {
// Ensure originalDestroy is a function before calling
originalDestroy.call(this);
} else {
// Fallback if originalDestroy was not set (e.g. if container had no children or its destroy was already modified)
if (this.parent && typeof this.parent.removeChild === 'function') {
this.parent.removeChild(this);
}
// Further cleanup if necessary, like removing all children manually
this.removeChildren();
}
};
}
// Start restaurant session
function startRestaurantSession() {
var currentSelectedMenu = [];
if (RestaurantState.selectedMenu && RestaurantState.selectedMenu.length > 0) {
currentSelectedMenu = RestaurantState.selectedMenu.slice(); // Capture the menu set by showMenuSelection
}
RestaurantState.reset(); // This will clear RestaurantState.selectedMenu among other things
RestaurantState.selectedMenu = currentSelectedMenu; // Restore the captured menu
RestaurantState.sessionActive = true;
RestaurantState.sessionStartTime = LK.ticks * (1000 / 60);
if (restaurantElements && restaurantElements.moneyDisplay) {
restaurantElements.moneyDisplay.setText('$' + GameState.money);
}
// Spawn first customer immediately
spawnCustomer();
// Then schedule the next one
scheduleNextCustomer();
}
// Cooking Game State
var CookingState = {
active: false,
currentCustomer: null,
currentRecipe: null,
currentPhase: 0,
// 0=prep, 1=cook, 2=plate
phaseStartTime: 0,
totalScore: 0,
phaseScores: [],
beatsHit: 0,
totalBeats: 0,
transitionActive: false,
currentPhaseContainer: null,
nextPhaseContainer: null
};
// Cooking phases configuration
var COOKING_PHASES = {
prep: {
name: "PREP",
instructions: "Tap the green beat indicators as they reach the target zone!",
color: 0x4CAF50,
pattern: [true, false, true, true, false, true, false, true, true, false, true],
// Example pattern for 11 beats total, with 7 indicators
slideDirection: 'left' // Slides in from left
},
cook: {
name: "COOK",
instructions: "Tap short notes! Hold & Release long notes on beat!",
color: 0xFF9800,
// pattern will define the beats. `beatsRequired` will be calculated in setupPhase.
pattern: [{
type: 'tap',
beatIndicator: true,
beat: 1
}, {
type: 'hold',
beatIndicator: true,
durationBeats: 2,
beat: 3
},
// Starts on beat 3, hold for 2 beats
{
type: 'tap',
beatIndicator: true,
beat: 6
}, {
type: 'hold',
beatIndicator: true,
durationBeats: 1,
beat: 8
},
// Starts on beat 8, hold for 1 beat
{
type: 'tap',
beatIndicator: true,
beat: 10
}, {
type: 'hold',
beatIndicator: true,
durationBeats: 3,
beat: 12
} // Starts on beat 12, hold for 3 beats
],
slideDirection: 'right' // Slides in from right
},
plate: {
name: "PLATE",
instructions: "Tap to place garnishes on beat!",
color: 0x2196F3,
beatsRequired: 4,
slideDirection: 'left' // Slides in from left
}
};
// Modify CookingState to include prep pattern tracking
var OriginalCookingState = CookingState;
CookingState = _objectSpread(_objectSpread({}, OriginalCookingState), {}, {
prepPattern: [],
prepPatternCurrentIndex: 0,
// Tracks which beat indicator from the pattern is next to be spawned
prepTotalIndicatorLinesInPattern: 0,
// Total number of 'true' values in the loaded pattern
prepIndicatorLinesResolved: 0,
// Number of indicator lines that have been tapped or missed
lastPrepLineBeat: -1,
// Tracks the beat number for which the last general line was spawned in prep
prepPhasePatternComplete: false,
// Flag to indicate the defined pattern is finished and processed
cookPhasePatternComplete: false,
currentPhaseScore: 0 // Score accumulated within the current phase
});
function createCookingOverlay() {
// Remove any existing overlay
if (restaurantScreen.cookingOverlay && !restaurantScreen.cookingOverlay.destroyed) {
restaurantScreen.cookingOverlay.destroy();
}
var overlay = restaurantScreen.addChild(new Container());
overlay.name = "cookingOverlay";
restaurantScreen.cookingOverlay = overlay;
// Dark background (full screen)
var bg = overlay.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
width: 2048,
height: 2732,
color: 0x000000,
alpha: 0.8
}));
// Main cooking window (same size as menu selection)
var cookingWindow = overlay.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 1366,
width: 1700,
height: 900,
color: 0x424242,
alpha: 0.95
}));
overlay.cookingWindow = cookingWindow;
// Container for sliding phases (clips to window)
var phaseContainer = overlay.addChild(new Container());
phaseContainer.x = 1024 - 850; // Center the container (1700/2 = 850)
phaseContainer.y = 1366 - 450; // Center the container (900/2 = 450)
overlay.phaseContainer = phaseContainer;
// Fixed UI elements (outside the sliding area)
// Recipe title (top of window)
var recipeTitle = overlay.addChild(new Text2('Cooking: ' + (CookingState.currentRecipe ? CookingState.currentRecipe.name : 'Unknown'), {
size: 60,
fill: 0xFFFFFF,
align: 'center'
}));
recipeTitle.anchor.set(0.5, 0.5);
recipeTitle.x = 1024;
recipeTitle.y = 950; // Top of cooking window
overlay.recipeTitle = recipeTitle;
// Score display (top right)
var scoreText = overlay.addChild(new Text2('Score: 0', {
size: 50,
fill: 0xFFD700
}));
scoreText.anchor.set(1, 0);
scoreText.x = 1850; // Right edge of window
scoreText.y = 980;
overlay.scoreText = scoreText;
// Progress indicator (top left)
var progressText = overlay.addChild(new Text2('Phase 1/3', {
size: 50,
fill: 0x4FC3F7
}));
progressText.anchor.set(0, 0);
progressText.x = 200; // Left edge of window
progressText.y = 980;
overlay.progressText = progressText;
// Beat indicator (bottom center, outside window)
var beatIndicator = overlay.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2000,
// Below the cooking window
width: 150,
height: 150,
tint: 0x666666,
alpha: 0.8
}));
overlay.beatIndicator = beatIndicator;
// Beat indicator text
var beatText = overlay.addChild(new Text2('BEAT', {
size: 30,
fill: 0xFFFFFF,
align: 'center'
}));
beatText.anchor.set(0.5, 0.5);
beatText.x = beatIndicator.x;
beatText.y = beatIndicator.y;
overlay.beatText = beatText;
// Ensure cooking overlay is on top of everything
if (restaurantScreen && restaurantScreen.children.indexOf(overlay) !== -1) {
restaurantScreen.setChildIndex(overlay, restaurantScreen.children.length - 1);
}
return overlay;
}
function createPhaseContainer(phaseName, phaseConfig) {
var container = new Container();
var WINDOW_WIDTH = 1700;
var WINDOW_HEIGHT = 900;
// Phase background
var phaseBg = container.addChild(LK.getAsset('songCard', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
color: phaseConfig.color,
alpha: 0.3
}));
// Phase title (top center)
var phaseTitle = container.addChild(new Text2(phaseConfig.name + ' PHASE', {
size: 80,
fill: phaseConfig.color,
align: 'center',
stroke: 0x000000,
strokeThickness: 3
}));
phaseTitle.anchor.set(0.5, 0);
phaseTitle.x = WINDOW_WIDTH / 2;
phaseTitle.y = 40;
// Instructions (below title)
var instructions = container.addChild(new Text2(phaseConfig.instructions, {
size: 45,
fill: 0xFFFFFF,
align: 'center',
wordWrap: true,
wordWrapWidth: WINDOW_WIDTH - 100
}));
instructions.anchor.set(0.5, 0);
instructions.x = WINDOW_WIDTH / 2;
instructions.y = 140;
// Create phase-specific content
var contentArea = container.addChild(new Container());
contentArea.x = WINDOW_WIDTH / 2; // Center content
contentArea.y = 300; // Below instructions
container.contentArea = contentArea;
switch (phaseName) {
case 'prep':
createPrepContent(contentArea);
break;
case 'cook':
createCookContent(contentArea);
break;
case 'plate':
createPlateContent(contentArea);
break;
}
// Beat counter (bottom right)
var beatCounter = container.addChild(new Text2('0/' + phaseConfig.beatsRequired, {
size: 60,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 2
}));
beatCounter.anchor.set(1, 1);
beatCounter.x = WINDOW_WIDTH - 50;
beatCounter.y = WINDOW_HEIGHT - 50;
container.beatCounter = beatCounter;
return container;
}
function createPrepContent(contentArea) {
var WINDOW_WIDTH = 1700;
var WINDOW_HEIGHT = 900;
// Cutting board (centered)
var cuttingBoard = contentArea.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
width: 500,
height: 300,
color: 0xD2691E,
alpha: 0.9
}));
// Fish to be cut
var fish = contentArea.addChild(LK.getAsset('sardine', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
scaleX: 1.2,
scaleY: 1.2
}));
contentArea.fish = fish;
// Knife (shows when player taps) - STAYS IN THE PREP WINDOW
var beatTarget = contentArea.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 180,
// Bottom of prep area
width: 400,
height: 40,
tint: 0xFF9800,
alpha: 0.6
}));
contentArea.beatTarget = beatTarget;
// Target zone text
var targetText = contentArea.addChild(new Text2('TAP ZONE', {
size: 24,
fill: 0xFFFFFF,
align: 'center'
}));
targetText.anchor.set(0.5, 0.5);
targetText.x = 0;
targetText.y = 180;
contentArea.targetText = targetText;
// Container for moving lines
var linesContainer = contentArea.addChild(new Container());
contentArea.linesContainer = linesContainer;
contentArea.activeLines = [];
// Knife (shows when player taps)
var knife = contentArea.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
// Centered in prep window
y: 0,
// Centered in prep window with fish
scaleX: 0.8,
scaleY: 0.8,
rotation: Math.PI / 4,
alpha: 0
}));
contentArea.knife = knife;
}
function createCookContent(contentArea) {
// Keep existing cook content as-is
var pan = contentArea.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
width: 400,
height: 400,
tint: 0x2F2F2F,
alpha: 0.9
}));
var fishPiece1 = contentArea.addChild(LK.getAsset('sardine', {
anchorX: 0.5,
anchorY: 0.5,
x: -80,
y: -50,
scaleX: 0.6,
scaleY: 0.6,
tint: 0xF4A460
}));
var fishPiece2 = contentArea.addChild(LK.getAsset('sardine', {
anchorX: 0.5,
anchorY: 0.5,
x: 80,
y: 50,
scaleX: 0.6,
scaleY: 0.6,
tint: 0xF4A460
}));
contentArea.fishPieces = [fishPiece1, fishPiece2];
// Steam effects
for (var i = 0; i < 3; i++) {
var steam = contentArea.addChild(LK.getAsset('bubbles', {
anchorX: 0.5,
anchorY: 0.5,
x: (Math.random() - 0.5) * 200,
y: (Math.random() - 0.5) * 200,
scaleX: 0.3,
scaleY: 0.3,
alpha: 0.6
}));
}
// Add a beat target and lines container for the cook phase, similar to prep
var beatTargetCook = contentArea.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
// Centered horizontally in contentArea
y: 180,
// Positioned towards the bottom of the content area, like prep
width: 400,
height: 40,
tint: 0xFF9800,
// Orange, matching cook phase color
alpha: 0.6
}));
contentArea.beatTarget = beatTargetCook; // Store reference
var targetTextCook = contentArea.addChild(new Text2('TAP/HOLD ZONE', {
size: 24,
fill: 0xFFFFFF,
align: 'center'
}));
targetTextCook.anchor.set(0.5, 0.5);
targetTextCook.x = 0;
targetTextCook.y = 180;
contentArea.targetText = targetTextCook;
var linesContainerCook = contentArea.addChild(new Container());
// Position lines container relative to beatTarget, or centrally if lines are spawned with absolute contentArea Y
// For now, let lines be positioned absolutely when spawned.
contentArea.linesContainer = linesContainerCook;
contentArea.activeLines = []; // To store active ChopLines for the cook phase
}
function createPlateContent(contentArea) {
// Plate
var plate = contentArea.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
width: 350,
height: 350,
tint: 0xFFFFFF,
alpha: 0.9
}));
// Cooked fish (main dish)
var cookedFish = contentArea.addChild(LK.getAsset('sardine', {
anchorX: 0.5,
anchorY: 0.5,
x: 0,
y: 0,
scaleX: 0.8,
scaleY: 0.8,
tint: 0xCD853F
}));
contentArea.cookedFish = cookedFish;
// Garnish spots (where rings will collapse to)
var garnishSpots = [];
var spotPositions = [{
x: -120,
y: -80
},
// Top left//{kD} //{kE}
{
x: 120,
y: -80
},
// Top right//{kG} //{kH} //{kI}
{
x: -120,
y: 80
},
// Bottom left//{kK} //{kL} //{kM}
{
x: 120,
y: 80
} // Bottom right//{kO} //{kP}
];
for (var i = 0; i < spotPositions.length; i++) {
var pos = spotPositions[i];
var spot = contentArea.addChild(LK.getAsset('dottedLine', {
anchorX: 0.5,
anchorY: 0.5,
x: pos.x,
y: pos.y,
width: 30,
height: 30,
tint: 0x888888,
alpha: 0.5
}));
garnishSpots.push(spot);
}
contentArea.garnishSpots = garnishSpots;
contentArea.garnishes = [];
contentArea.activeRings = [];
contentArea.ringsContainer = contentArea.addChild(new Container());
// IMPORTANT: Always initialize garnishPattern
contentArea.garnishPattern = [{
spotIndex: 0,
beat: 1
}, {
spotIndex: 2,
beat: 3
}, {
spotIndex: 1,
beat: 5
}, {
spotIndex: 3,
beat: 7
}];
contentArea.nextPatternIndex = 0;
}
function setupPhase(phaseIndex) {
// Prevent multiple simultaneous phase setups
if (CookingState.transitionActive) {
console.log("Phase transition already in progress, ignoring setupPhase call");
return;
}
var phases = ['prep', 'cook', 'plate'];
var phaseName = phases[phaseIndex];
var phaseConfig = COOKING_PHASES[phaseName];
// Add safety check for phaseConfig
if (!phaseConfig) {
console.error("Invalid phase configuration for:", phaseName, "at index:", phaseIndex);
return;
}
console.log("Setting up phase:", phaseName, "at index:", phaseIndex);
CookingState.currentPhase = phaseIndex;
CookingState.phaseStartTime = LK.ticks * (1000 / 60);
CookingState.currentPhaseScore = 0; // Reset score for the new phase
CookingState.beatsHit = 0;
if (phaseName === 'prep') {
CookingState.prepPattern = phaseConfig.pattern || [];
CookingState.prepPatternCurrentIndex = 0;
CookingState.prepTotalIndicatorLinesInPattern = CookingState.prepPattern.filter(function (isIndicator) {
return isIndicator;
}).length;
CookingState.prepIndicatorLinesResolved = 0;
CookingState.lastPrepLineBeat = -1;
CookingState.prepPhasePatternComplete = false;
phaseConfig.beatsRequired = CookingState.prepTotalIndicatorLinesInPattern; // For UI counter
} else if (phaseName === 'cook') {
CookingState.cookPattern = phaseConfig.pattern || [];
CookingState.cookPatternCurrentIndex = 0;
// Total events in cook phase is simply the length of its pattern array
CookingState.cookPhaseTotalEvents = CookingState.cookPattern.length;
CookingState.cookPhaseEventsResolved = 0;
CookingState.lastCookLineBeat = -1; // Tracks beat for spawning cook lines
phaseConfig.beatsRequired = CookingState.cookPhaseTotalEvents; // For UI counter, total notes to hit
CookingState.activeHeldLine = null; // Ensure no held line from previous attempt
}
CookingState.totalBeats = phaseConfig.beatsRequired; // This will now use the dynamic value for prep or cook
CookingState.transitionActive = true; // Set this FIRST to prevent duplicate calls
var overlay = restaurantScreen.cookingOverlay;
if (!overlay) {
CookingState.transitionActive = false; // Reset if overlay doesn't exist
return;
}
// Update fixed UI elements
if (overlay.progressText) {
overlay.progressText.setText('Phase ' + (phaseIndex + 1) + '/3');
}
// Create new phase container
var newPhaseContainer = createPhaseContainer(phaseName, phaseConfig);
var WINDOW_WIDTH = 1700;
// Position new container off-screen based on slide direction
if (phaseConfig.slideDirection === 'left') {
newPhaseContainer.x = -WINDOW_WIDTH;
} else {
newPhaseContainer.x = WINDOW_WIDTH;
}
newPhaseContainer.y = 0;
overlay.phaseContainer.addChild(newPhaseContainer);
CookingState.nextPhaseContainer = newPhaseContainer;
// Slide out old container (if exists) and slide in new container
var slideDuration = 800;
if (CookingState.currentPhaseContainer) {
var slideOutTarget = phaseConfig.slideDirection === 'left' ? WINDOW_WIDTH : -WINDOW_WIDTH;
tween(CookingState.currentPhaseContainer, {
x: slideOutTarget
}, {
duration: slideDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
if (CookingState.currentPhaseContainer && !CookingState.currentPhaseContainer.destroyed) {
CookingState.currentPhaseContainer.destroy();
}
}
});
}
// Slide in new container
tween(newPhaseContainer, {
x: 0
}, {
duration: slideDuration,
easing: tween.easeInOut,
onFinish: function onFinish() {
CookingState.currentPhaseContainer = newPhaseContainer;
CookingState.nextPhaseContainer = null;
CookingState.transitionActive = false; // Only reset after transition is complete
console.log("Phase transition completed for:", phaseName);
}
});
// Play transition sound
LK.getSound('reel').play();
}
function completePhase() {
// This function is now typically called after feedback is shown (e.g., from showPrepFeedbackAndProceed)
// or by checkPhaseTimeout for non-prep phases / timeouts.
// Store the score for the just-completed phase
var phaseScoreForThisPhase = CookingState.currentPhaseScore;
CookingState.phaseScores.push(phaseScoreForThisPhase);
if (CookingState.currentPhase < 2) {
// Transition to the next phase.
// showPhaseComplete is more generic, let's ensure specific phase feedback (like prep's) has already run.
// setupPhase will set transitionActive = true and handle resetting it.
setupPhase(CookingState.currentPhase + 1);
} else {
// All phases complete - finish cooking
// finishCooking will call showCookingResults, which handles its own transition display.
finishCooking();
}
// transitionActive state is managed by the functions called (setupPhase, finishCooking/showCookingResults)
}
// Add this function to check for phase timeout and force progression
function checkPhaseTimeout() {
if (!CookingState.active || CookingState.transitionActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60);
var timeSincePhaseStart = currentTime - CookingState.phaseStartTime;
var phases = ['prep', 'cook', 'plate'];
var phaseName = phases[CookingState.currentPhase];
var phaseConfig = COOKING_PHASES[phaseName];
// Add safety check here too
if (!phaseConfig) {
console.error("Invalid phase config in checkPhaseTimeout for phase:", CookingState.currentPhase);
return;
}
// Force phase completion after reasonable time (e.g., 15 seconds)
var forceCompleteTime = 15000; // 15 seconds
if (phaseName === 'prep') {
if (CookingState.prepPhasePatternComplete) {
// If pattern is marked complete (e.g., by showPrepFeedbackAndProceed starting)
return; // Already being handled or completed
}
var allPatternIndicatorsSpawned = CookingState.prepPatternCurrentIndex >= CookingState.prepPattern.length;
var allPatternIndicatorsResolved = CookingState.prepIndicatorLinesResolved >= CookingState.prepTotalIndicatorLinesInPattern;
if (allPatternIndicatorsSpawned && allPatternIndicatorsResolved) {
if (!CookingState.prepPhasePatternComplete) {
// Check flag before setting and proceeding
CookingState.prepPhasePatternComplete = true; // Mark here
console.log("Prep pattern fully resolved. Resolved: " + CookingState.prepIndicatorLinesResolved + "/" + CookingState.prepTotalIndicatorLinesInPattern);
showPrepFeedbackAndProceed();
return;
}
}
// Fallthrough to timeout for prep if pattern not resolved in time
} else if (phaseName === 'cook') {
if (CookingState.cookPhaseEventsResolved >= CookingState.cookPhaseTotalEvents) {
if (!CookingState.transitionActive) {
// Ensure not already transitioning
console.log("Cook phase pattern fully resolved. Resolved: " + CookingState.cookPhaseEventsResolved + "/" + CookingState.cookPhaseTotalEvents);
completePhase(); // Directly complete, no separate feedback screen like prep needed for now
return;
}
}
// Fallthrough to timeout for cook if pattern not resolved in time
}
if (timeSincePhaseStart > forceCompleteTime) {
// Ensure we don't try to complete a phase that's already in transition or done
if (CookingState.transitionActive) return;
if (phaseName === 'prep') {
if (!CookingState.prepPhasePatternComplete) {
// Only if not already handled
console.log("Force completing PREP phase due to timeout.");
CookingState.prepPhasePatternComplete = true; // Mark as complete to avoid re-entry
showPrepFeedbackAndProceed();
return;
}
} else {
console.log("Force completing phase (non-prep) due to timeout:", phaseName);
completePhase();
return;
}
}
// For non-prep phases, check beatsHit. Prep phase completion is primarily driven by pattern logic or timeout.
if (phaseName !== 'prep' && CookingState.beatsHit >= phaseConfig.beatsRequired) {
if (CookingState.transitionActive) return; // Don't complete if already transitioning
console.log("Completing phase - all beats hit (non-prep):", phaseName);
completePhase();
}
}
function showPhaseComplete(completedPhase, callback) {
var phases = ['PREP', 'COOK', 'PLATE'];
var phaseName = phases[completedPhase];
var overlay = restaurantScreen.cookingOverlay;
if (!overlay) {
return;
}
var message = overlay.addChild(new Text2(phaseName + ' COMPLETE!', {
size: 70,
fill: 0xFFD700,
stroke: 0x000000,
strokeThickness: 3,
align: 'center'
}));
message.anchor.set(0.5, 0.5);
message.x = 1024;
message.y = 1366;
// Animate message
message.alpha = 0;
message.scale.set(0.3);
tween(message, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
LK.setTimeout(function () {
tween(message, {
alpha: 0,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 300,
onFinish: function onFinish() {
if (message && !message.destroyed) {
message.destroy();
}
if (callback) {
callback();
}
}
});
}, 800);
}
});
}
function finishCooking() {
// Calculate final rating (1-3 stars)
var maxScorePerPhase = [400, 900, 480]; // Max possible scores for prep, cook, plate
var totalMaxScore = maxScorePerPhase.reduce(function (sum, max) {
return sum + max;
}, 0);
var finalScore = CookingState.phaseScores.reduce(function (sum, score) {
return sum + score;
}, 0);
var percentage = finalScore / totalMaxScore;
var stars = 1;
if (percentage >= 0.85) {
stars = 3;
} else if (percentage >= 0.65) {
stars = 2;
}
// Calculate earnings
var recipe = CookingState.currentRecipe;
var earnings = 0;
if (recipe && typeof recipe.baseValue === "number" && typeof recipe.multiplier === "number") {
earnings = Math.floor(recipe.baseValue * recipe.multiplier * stars);
} else {
console.error("Invalid or missing recipe in finishCooking:", recipe);
earnings = 0;
}
// Show final results
showCookingResults(stars, earnings, function () {
// Serve customer after results
if (CookingState.currentCustomer && !CookingState.currentCustomer.destroyed) {
CookingState.currentCustomer.serveCustomer(stars);
}
endCookingSession();
});
}
function showCookingResults(stars, earnings, callback) {
// Prevent re-entry or concurrent transitions
if (CookingState.transitionActive) {
console.warn("showCookingResults called while another transition is active. Callback will be invoked to maintain flow.");
if (callback) {
// Call async to avoid deep call stacks if this somehow happens rapidly
LK.setTimeout(callback, 0);
}
return;
}
CookingState.transitionActive = true; // Mark that we are now in the results display transition
var overlay = restaurantScreen.cookingOverlay;
if (!overlay || overlay.destroyed) {
// Added destroyed check
console.warn("showCookingResults: cookingOverlay is missing or destroyed.");
CookingState.transitionActive = false; // Reset flag as we can't proceed
if (callback) {
LK.setTimeout(callback, 0);
}
return;
}
// Create results container that slides in
var resultsContainer = new Container();
var WINDOW_WIDTH = 1700;
var WINDOW_HEIGHT = 900;
// Results background
var resultsBg = resultsContainer.addChild(LK.getAsset('songCard', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
color: 0x000000,
alpha: 0.9
}));
// Title
var title = resultsContainer.addChild(new Text2('DISH COMPLETE!', {
size: 80,
fill: 0xFFD700,
align: 'center',
stroke: 0x000000,
strokeThickness: 3
}));
title.anchor.set(0.5, 0.5);
title.x = WINDOW_WIDTH / 2;
title.y = 200;
// Stars display
var starsText = resultsContainer.addChild(new Text2('β
'.repeat(stars) + 'β'.repeat(3 - stars), {
size: 120,
fill: 0xFFD700,
align: 'center'
}));
starsText.anchor.set(0.5, 0.5);
starsText.x = WINDOW_WIDTH / 2;
starsText.y = 350;
// Recipe name
var recipeDisplayName = CookingState.currentRecipe && CookingState.currentRecipe.name ? CookingState.currentRecipe.name : "Unknown Dish";
var recipeName = resultsContainer.addChild(new Text2(recipeDisplayName, {
size: 60,
fill: 0xFFFFFF,
align: 'center'
}));
recipeName.anchor.set(0.5, 0.5);
recipeName.x = WINDOW_WIDTH / 2;
recipeName.y = 500;
// Earnings
var earningsText = resultsContainer.addChild(new Text2('Earned: $' + earnings, {
size: 70,
fill: 0x4CAF50,
align: 'center',
stroke: 0x000000,
strokeThickness: 2
}));
earningsText.anchor.set(0.5, 0.5);
earningsText.x = WINDOW_WIDTH / 2;
earningsText.y = 650;
// Position off-screen and slide in
resultsContainer.x = WINDOW_WIDTH;
resultsContainer.y = 0;
overlay.phaseContainer.addChild(resultsContainer);
// Slide out current phase container
if (CookingState.currentPhaseContainer) {
tween(CookingState.currentPhaseContainer, {
x: -WINDOW_WIDTH
}, {
duration: 600,
easing: tween.easeInOut
});
}
// Slide in results
tween(resultsContainer, {
x: 0
}, {
duration: 600,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Show results for 3 seconds then callback
LK.setTimeout(function () {
if (callback) {
callback();
}
}, 3000);
}
});
}
function endCookingSession() {
CookingState.active = false;
CookingState.transitionActive = false; // Explicitly clear transitionActive flag
// Restore original restaurant input
if (CookingState.originalRestaurantInput) {
handleRestaurantInput = CookingState.originalRestaurantInput;
}
// Restore customer UI elements
RestaurantState.currentCustomers.forEach(function (customer) {
if (customer && !customer.destroyed) {
customer.alpha = 1.0; // Restore full visibility
}
});
// Remove cooking overlay with slide out animation
if (restaurantScreen.cookingOverlay) {
tween(restaurantScreen.cookingOverlay, {
alpha: 0
}, {
duration: 500,
onFinish: function onFinish() {
if (restaurantScreen.cookingOverlay) {
restaurantScreen.cookingOverlay.destroy();
restaurantScreen.cookingOverlay = null;
}
}
});
}
// Clear cooking state
CookingState.currentCustomer = null;
CookingState.currentRecipe = null;
CookingState.currentPhase = 0;
CookingState.totalScore = 0;
CookingState.phaseScores = [];
CookingState.currentPhaseContainer = null;
CookingState.nextPhaseContainer = null;
}
function initializeCookingSystem() {
// Initialize the beat detector with restaurant BPM
BeatDetector.init(90); // Changed from 120 to 90 BPM
// Reset cooking state
CookingState.active = false;
CookingState.currentCustomer = null;
CookingState.currentRecipe = null;
CookingState.currentPhase = 0;
CookingState.totalScore = 0;
CookingState.phaseScores = [];
CookingState.beatsHit = 0;
CookingState.totalBeats = 0;
CookingState.transitionActive = false;
CookingState.currentPhaseContainer = null;
CookingState.nextPhaseContainer = null;
}
// Start Cooking Session
function startCookingSession(customer, recipeKey) {
if (!customer || !recipeKey || !RestaurantState.recipes[recipeKey]) {
return;
}
// Hide customer UI elements during cooking
RestaurantState.currentCustomers.forEach(function (customerInstance) {
if (customerInstance && !customerInstance.destroyed) {
// Use a different variable name to avoid conflict with the function parameter 'customer'
customerInstance.alpha = 0.3; // Dim customers during cooking
}
});
CookingState.active = true;
CookingState.currentCustomer = customer;
CookingState.currentRecipe = RestaurantState.recipes[recipeKey];
CookingState.currentPhase = 0;
CookingState.phaseStartTime = LK.ticks * (1000 / 60);
CookingState.totalScore = 0;
CookingState.phaseScores = [];
CookingState.beatsHit = 0;
CookingState.totalBeats = 0;
// Create cooking overlay
createCookingOverlay();
// Initialize beat detector
BeatDetector.init(90); // Changed from 120 to 90 BPM
// Start first phase after brief delay
LK.setTimeout(function () {
setupPhase(0); // Start with prep phase
}, 500);
// Temporarily override restaurant input
CookingState.originalRestaurantInput = handleRestaurantInput;
handleRestaurantInput = handleCookingInput;
}
function showPrepFeedbackAndProceed() {
if (CookingState.transitionActive || !CookingState.active) return;
CookingState.transitionActive = true; // Prevent re-entry
var overlay = restaurantScreen.cookingOverlay;
if (!overlay || overlay.destroyed) {
CookingState.transitionActive = false;
return;
}
// Stop spawning new prep lines if not already stopped
CookingState.prepPhasePatternComplete = true;
var successRate = 0;
if (CookingState.prepTotalIndicatorLinesInPattern > 0) {
successRate = CookingState.beatsHit / CookingState.prepTotalIndicatorLinesInPattern;
} else if (CookingState.prepTotalIndicatorLinesInPattern === 0) {
successRate = 1; // No indicators, so perfect by default
}
var feedbackText = "Chopping Done!";
if (successRate >= 0.9) feedbackText = "Perfect Chops!";else if (successRate >= 0.6) feedbackText = "Good Rhythm!";else feedbackText = "Keep Practicing!";
var message = overlay.addChild(new Text2(feedbackText, {
size: 70,
fill: 0xFFD700,
stroke: 0x000000,
strokeThickness: 3,
align: 'center'
}));
message.anchor.set(0.5, 0.5);
message.x = 1024;
message.y = 1366; // Center of cooking window
message.alpha = 0;
message.scale.set(0.3);
tween(message, {
alpha: 1,
scaleX: 1,
scaleY: 1
}, {
duration: 400,
easing: tween.easeOut,
onFinish: function onFinish() {
LK.setTimeout(function () {
tween(message, {
alpha: 0,
scaleX: 1.2,
scaleY: 1.2
}, {
duration: 300,
onFinish: function onFinish() {
if (message && !message.destroyed) message.destroy();
CookingState.transitionActive = false; // Reset flag: feedback transition is complete
completePhase(); // This will handle storing score and moving to next phase
// Subsequent functions like setupPhase will manage their own transitionActive state.
}
});
}, 1500); // Show feedback for 1.5 seconds
}
});
}
function showCookingFeedback(feedbackType, x, y) {
if (!restaurantScreen.cookingOverlay) {
return;
}
// Create feedback text
var feedbackText = '';
var feedbackColor = 0xFFFFFF;
switch (feedbackType) {
case 'perfect':
feedbackText = 'PERFECT!';
feedbackColor = 0xFFD700; // Gold
break;
case 'good':
feedbackText = 'GOOD!';
feedbackColor = 0x4CAF50; // Green
break;
case 'miss':
feedbackText = 'MISS!';
feedbackColor = 0xF44336; // Red
break;
}
var feedback = restaurantScreen.cookingOverlay.addChild(new Text2(feedbackText, {
size: 60,
fill: feedbackColor,
stroke: 0x000000,
strokeThickness: 3,
align: 'center'
}));
feedback.anchor.set(0.5, 0.5);
feedback.x = x;
feedback.y = y;
// Animate feedback
tween(feedback, {
y: y - 100,
alpha: 0
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
if (feedback && !feedback.destroyed) {
feedback.destroy();
}
}
});
}
function handleCookingInput(x, y) {
if (!CookingState.active || CookingState.transitionActive) {
return;
}
var overlay = restaurantScreen.cookingOverlay;
var currentPhaseContainer = CookingState.currentPhaseContainer;
if (!overlay || !currentPhaseContainer) {
return;
}
var beatInfo = BeatDetector.update();
var phaseName = ['prep', 'cook', 'plate'][CookingState.currentPhase];
var phaseConfig = COOKING_PHASES[phaseName];
// Convert screen coordinates to phase container coordinates
// phaseContainer.x = 1024 - 850; phaseContainer.y = 1366 - 450;
var containerX = x - (1024 - 850);
var containerY = y - (1366 - 450);
var eventType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'tap';
var obj = arguments.length > 3 ? arguments[3] : undefined;
var points = 0;
var feedback = 'miss';
var success = false;
var beatInfo = BeatDetector.update(); // Get current beat info
switch (phaseName) {
case 'prep':
// Check if player tapped when a line is in the tap window
// Ensure we are using the correct array of lines from the overlay (restaurantScreen.cookingOverlay.rhythmLines)
if (overlay && overlay.rhythmLines) {
var lines = overlay.rhythmLines;
for (var i = lines.length - 1; i >= 0; i--) {
var line = lines[i];
// Check if line is in tap window and tappable
// Tap is considered on the line if it's in its `canTap` state (close to target)
// and `shouldShowBeatIndicator` is true (it's a green line).
// We also need to check if the tap location (containerX, containerY) is reasonably close to the line itself.
// Assuming lines are vertical and move horizontally, their `y` is 0 relative to linesContainer, which is in contentArea.
// Target line.x is where the tap should occur horizontally.
// Target line.y is effectively the vertical center of the contentArea for prep (where beatTarget is).
// A tap anywhere in the contentArea could trigger if line.canTap is true.
// For prep, let's assume tap anywhere within the phase container is fine if a tappable line exists.
if (line.canTap && line.shouldShowBeatIndicator) {
// Only allow tapping on green beat indicator lines
points = 150;
feedback = 'perfect';
success = true;
// Remove the line (it gets "absorbed")
line.destroy();
lines.splice(i, 1);
// Show knife animation
if (currentPhaseContainer.contentArea.knife) {
var knife = currentPhaseContainer.contentArea.knife;
knife.alpha = 1;
tween(knife, {
alpha: 0
}, {
duration: 200
});
}
// Damage fish visual
if (currentPhaseContainer.contentArea.fish) {
var fish = currentPhaseContainer.contentArea.fish;
fish.alpha = Math.max(0.3, fish.alpha - 0.25); // Increased damage
}
// Make beat button flash when hit
if (currentPhaseContainer.contentArea.beatTarget) {
var beatBtn = currentPhaseContainer.contentArea.beatTarget;
var originalTint = beatBtn.tint; // Store original tint
beatBtn.tint = 0xFFFFFF; // Flash white
LK.setTimeout(function () {
if (beatBtn && !beatBtn.destroyed) {
beatBtn.tint = originalTint; // Back to original color (e.g. 0xFF9800 or whatever it was)
}
}, 100);
}
break;
}
}
}
break;
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; // 0 when on beat, negative before, positive after
if (Math.abs(noteStartProgress * BeatDetector.beatInterval) < BeatDetector.tolerance * 1.5) {
// Slightly more tolerance for hold start
line.isBeingHeld = true;
line.holdStartTime = LK.ticks * (1000 / 60);
CookingState.activeHeldLine = line;
if (line.beatIndicator) line.beatIndicator.tint = 0x0088FF; // Brighter blue when held
// feedback = "hold_start_good"; // No immediate points, wait for release
success = true; // Mark as a successful interaction initiation
showCookingFeedback('good', line.targetX, line.y - 100); // Indicate good start
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 timeDifference = Math.abs(holdDurationMs - expectedHoldDurationMs);
// Check release timing relative to the note's *end* beat
var noteEndBeatExact = line.beatNumber + line.durationBeats;
var releaseProgress = currentBeatInPhaseExact - noteEndBeatExact; // 0 if released on end beat
if (Math.abs(releaseProgress * BeatDetector.beatInterval) < BeatDetector.tolerance * 1.5 && timeDifference < BeatDetector.beatInterval * 0.5) {
// Good hold duration and release
points = 200; // More points for successful hold
feedback = 'perfect';
} else if (Math.abs(releaseProgress * BeatDetector.beatInterval) < BeatDetector.tolerance * 2 && timeDifference < BeatDetector.beatInterval * 0.75) {
points = 100;
feedback = 'good';
} else {
points = 25; // Still some points for trying
feedback = 'miss'; // Or 'early/late release'
}
success = true;
line.wasTappedSuccessfully = true;
line.isBeingHeld = false;
CookingState.activeHeldLine = null;
if (line.beatIndicator) line.beatIndicator.tint = COOKING_PHASES.cook.color; // Reset tint
line.destroy(); // Destroy after interaction
var lineIndex = linesCook.indexOf(line);
if (lineIndex > -1) linesCook.splice(lineIndex, 1);
}
} else {
// 'tap' event
for (var i = linesCook.length - 1; i >= 0; i--) {
var line = linesCook[i];
if (!line.isLongNote && line.canTap) {
// Only tap short notes
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(); // Destroy after interaction
var lineIndexTap = linesCook.indexOf(line);
if (lineIndexTap > -1) linesCook.splice(lineIndexTap, 1);
break;
}
}
}
if (success && points > 0) {
// Only update score and fish visuals on actual scoring events
// Animate fish pieces in pan for any successful cook interaction (tap or hold release)
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;
case 'plate':
// Check if player tapped a collapsing ring
if (currentPhaseContainer.contentArea && currentPhaseContainer.contentArea.activeRings) {
var rings = currentPhaseContainer.contentArea.activeRings;
for (var i = rings.length - 1; i >= 0; i--) {
var ring = rings[i]; // ring.x, ring.y are relative to ringsContainer, which is child of contentArea
// ring.x, ring.y are relative to contentArea.ringsContainer.
// Assuming ringsContainer is at (0,0) within contentArea.
// Global X of ring center: phaseContainer.x + contentArea.x + ring.x
// Global Y of ring center: phaseContainer.y + contentArea.y + ring.y
// Tap X (global) is x, Tap Y (global) is y.
var ringGlobalX = 1024 - 850 + currentPhaseContainer.contentArea.x + ring.x;
var ringGlobalY = 1366 - 450 + currentPhaseContainer.contentArea.y + ring.y;
var distance = Math.sqrt(Math.pow(x - ringGlobalX, 2) + Math.pow(y - ringGlobalY, 2));
if (ring.canTap && distance < 40) {
// 40px tap radius for ring
points = 120;
feedback = 'perfect';
success = true;
// Add garnish at this spot (ring.x, ring.y are correct local coords for contentArea.ringsContainer)
var garnish = currentPhaseContainer.contentArea.ringsContainer.addChild(LK.getAsset('oceanbubbles', {
anchorX: 0.5,
anchorY: 0.5,
x: ring.x,
// Use ring's local coords as garnish is child of same container
y: ring.y,
scaleX: 0.4,
scaleY: 0.4,
tint: 0x4CAF50
}));
if (!currentPhaseContainer.contentArea.garnishes) {
currentPhaseContainer.contentArea.garnishes = [];
}
currentPhaseContainer.contentArea.garnishes.push(garnish); // Store reference if needed
// Remove the ring
ring.destroy();
rings.splice(i, 1);
break;
}
}
}
break;
}
if (success) {
CookingState.currentPhaseScore += points;
CookingState.totalScore += points; // Overall session score
if (phaseName === 'prep') {
CookingState.beatsHit++; // Correctly counts tapped indicators
// 'line' variable might be out of scope here if success was from a different part of prep.
// The original logic assumes 'line' is the successfully interacted prep line.
// This part needs to be careful if prep logic changes. For now, assume 'line' is valid from prep context.
if (typeof line !== 'undefined' && line && line.wasTapped !== undefined) line.wasTapped = true;
CookingState.prepIndicatorLinesResolved++;
if (currentPhaseContainer.beatCounter) {
currentPhaseContainer.beatCounter.setText(CookingState.beatsHit + '/' + CookingState.prepTotalIndicatorLinesInPattern);
}
} else if (phaseName === 'cook') {
if (points > 0) {
// Only count as a "hit" if scored positively
CookingState.cookPhaseEventsResolved++;
if (currentPhaseContainer.beatCounter) {
currentPhaseContainer.beatCounter.setText(CookingState.cookPhaseEventsResolved + '/' + CookingState.cookPhaseTotalEvents);
}
}
} else {
// For plate phase (original logic)
CookingState.beatsHit++;
if (currentPhaseContainer.beatCounter) {
currentPhaseContainer.beatCounter.setText(CookingState.beatsHit + '/' + phaseConfig.beatsRequired);
}
}
if (overlay.scoreText) {
overlay.scoreText.setText('Score: ' + CookingState.totalScore);
}
// Phase completion check
if (phaseName === 'cook' && CookingState.cookPhaseEventsResolved >= CookingState.cookPhaseTotalEvents) {
completePhase();
} else if (phaseName !== 'prep' && phaseName !== 'cook' && CookingState.beatsHit >= phaseConfig.beatsRequired) {
// Original for plate
completePhase();
}
// Prep phase completion is handled by its specific logic in checkPhaseTimeout or showPrepFeedbackAndProceed
if (points > 0) LK.getSound('catch').play(); //{qC} else LK.getSound('miss').play();
} else {
// Only play miss sound if no success was registered in this tap for this phase
// (e.g. prep phase might have multiple lines, one miss shouldn't play sound if another was hit)
// For now, simple else is fine as break; is used.
LK.getSound('miss').play();
}
// Show feedback (feedback text, global x, global y)
showCookingFeedback(feedback, x, y);
}
function updateCookingPhases() {
if (!CookingState.active || !CookingState.currentPhaseContainer) {
return;
}
// Check for phase timeout/completion
checkPhaseTimeout();
// If CookingState became inactive or is transitioning due to phase completion, return early.
// This prevents further processing for a phase that just ended or is changing.
if (!CookingState.active || CookingState.transitionActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60);
var phaseName = ['prep', 'cook', 'plate'][CookingState.currentPhase];
var contentArea = CookingState.currentPhaseContainer.contentArea;
var overlay = restaurantScreen.cookingOverlay; // Get the main overlay
if (phaseName === 'prep' && 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.rhythmLines) {
overlay.rhythmLines = [];
}
if (currentBeatNumber > CookingState.lastPrepLineBeat) {
if (!CookingState.prepPhasePatternComplete) {
// Stop spawning new lines if pattern is marked complete
spawnThisBeat = true;
CookingState.lastPrepLineBeat = currentBeatNumber;
}
}
if (spawnThisBeat) {
var shouldShowBeatIndicator = false;
// Only consult pattern if we haven't exhausted its defined indicators
if (CookingState.prepPatternCurrentIndex < CookingState.prepPattern.length) {
shouldShowBeatIndicator = CookingState.prepPattern[CookingState.prepPatternCurrentIndex];
} else {
// Pattern's defined indicators are exhausted, subsequent lines are non-indicators
// unless we want to loop the pattern, which is not the current requirement.
shouldShowBeatIndicator = false;
}
var startX = 1800;
var targetX = 1024;
var lineY = 2000;
var lineDuration = beatInterval * 1.2;
var newLine = new ChopLine(startX, targetX, lineY, lineDuration, shouldShowBeatIndicator);
if (shouldShowBeatIndicator) {
newLine.isIndicator = true;
newLine.wasTapped = false;
}
if (overlay && !overlay.destroyed) {
overlay.addChild(newLine);
overlay.rhythmLines.push(newLine);
}
// Advance pattern index if we are still within the bounds of the defined pattern.
// This happens for every line spawned according to the pattern, regardless of whether it's an indicator or not.
if (CookingState.prepPatternCurrentIndex < CookingState.prepPattern.length) {
CookingState.prepPatternCurrentIndex++;
}
}
// Clean up finished lines & check for missed indicators
if (overlay.rhythmLines) {
for (var i = overlay.rhythmLines.length - 1; i >= 0; i--) {
var line = overlay.rhythmLines[i];
if (line.isDone) {
if (line.isIndicator && !line.wasTapped) {
CookingState.prepIndicatorLinesResolved++;
// Optional: show "MISS" feedback for this specific line immediately
showCookingFeedback('miss', line.targetX, line.y - 150); // Show miss near target point
}
line.destroy();
overlay.rhythmLines.splice(i, 1);
}
}
}
} 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);
}
}
}
} else if (phaseName === 'plate' && contentArea) {
// Add safety checks for plate phase
if (!contentArea.garnishPattern) {
console.error("garnishPattern not initialized for plate phase");
return;
}
if (!contentArea.activeRings) {
contentArea.activeRings = [];
}
if (contentArea.nextPatternIndex === undefined) {
contentArea.nextPatternIndex = 0;
}
// Spawn garnish rings according to pattern
var beatInfo = BeatDetector.update();
var timeSincePhaseStart = currentTime - CookingState.phaseStartTime;
var beatInterval = BeatDetector.beatInterval;
var beatsElapsed = Math.floor(timeSincePhaseStart / beatInterval);
// Check if we should spawn a ring for this beat
if (contentArea.nextPatternIndex < contentArea.garnishPattern.length) {
var nextPattern = contentArea.garnishPattern[contentArea.nextPatternIndex];
if (beatsElapsed >= nextPattern.beat - 1) {
// Spawn one beat before target
var spot = contentArea.garnishSpots[nextPattern.spotIndex];
var initialRadius = 80;
var shrinkDuration = beatInterval * 1.5; // Time to shrink to center
var newRing = new GarnishRing(spot.x, spot.y, initialRadius, shrinkDuration);
if (contentArea.ringsContainer && !contentArea.ringsContainer.destroyed) {
contentArea.ringsContainer.addChild(newRing);
contentArea.activeRings.push(newRing);
}
contentArea.nextPatternIndex++;
}
}
// Clean up finished rings
for (var i = contentArea.activeRings.length - 1; i >= 0; i--) {
var ring = contentArea.activeRings[i];
if (ring.isDone) {
ring.destroy();
contentArea.activeRings.splice(i, 1);
}
}
}
}
// Customer spawning
function scheduleNextCustomer() {
if (!RestaurantState.sessionActive) {
return;
}
// Check if game is still on restaurant screen
if (GameState.currentScreen !== 'restaurant') {
RestaurantState.sessionActive = false; // Stop session if screen changed
return;
}
var delay = 3000 + Math.random() * 4000; // 3-7 seconds instead of 8-15
LK.setTimeout(function () {
if (!RestaurantState.sessionActive || GameState.currentScreen !== 'restaurant') {
return;
}
// Double-check session is still active before spawning and rescheduling
if (RestaurantState.sessionActive) {
spawnCustomer();
scheduleNextCustomer(); // Schedule the next one
}
}, delay);
}
function spawnCustomer() {
if (!RestaurantState.sessionActive || RestaurantState.currentCustomers.length >= 6 || RestaurantState.selectedMenu.length === 0) {
return;
}
// Find empty table
var occupiedTables = RestaurantState.currentCustomers.map(function (c) {
return c.tableIndex;
});
var availableTables = [];
for (var i = 0; i < 6; i++) {
// Assuming 6 tables
if (occupiedTables.indexOf(i) === -1) {
availableTables.push(i);
}
}
if (availableTables.length === 0) {
return;
}
var tableIndex = availableTables[Math.floor(Math.random() * availableTables.length)];
var randomRecipeKey = RestaurantState.selectedMenu[Math.floor(Math.random() * RestaurantState.selectedMenu.length)];
var customer = new Customer(tableIndex, randomRecipeKey);
// Position at table (based on createTables and restaurantFloor structure)
var tablesPerRow = 3;
var tableWidth = 200; // From createTables
var tableSpacingX = (2048 - tablesPerRow * tableWidth) / (tablesPerRow + 1); // From createTables
var rowSpacing = 300; // From createTables
var row = Math.floor(tableIndex / tablesPerRow);
var col = tableIndex % tablesPerRow;
// Customer X is center of table, customer Y is center of table
customer.x = tableSpacingX + col * (tableWidth + tableSpacingX) + tableWidth / 2;
// Y is relative to restaurantFloor/tableContainer, which starts at TOP_SECTION_HEIGHT
customer.y = GAME_CONFIG.WATER_SURFACE_Y + 150 + row * rowSpacing + 150 / 2; // 150 is table's Y offset in createTables, 150/2 is half table height
if (restaurantScreen && !restaurantScreen.destroyed) {
restaurantScreen.addChild(customer); // Add customer to main restaurant screen for updates
// Ensure cooking overlay (if active and exists on screen) remains on top of newly spawned customers
if (restaurantScreen.cookingOverlay && restaurantScreen.cookingOverlay.parent === restaurantScreen) {
restaurantScreen.setChildIndex(restaurantScreen.cookingOverlay, restaurantScreen.children.length - 1);
}
}
RestaurantState.currentCustomers.push(customer);
}
/****
* Game State Management
****/
var MULTI_BEAT_SPAWN_DELAY_MS = 250;
var TRIPLET_BEAT_SPAWN_DELAY_MS = 350;
var FISH_SPAWN_END_BUFFER_MS = 500;
// Modified ImprovedRhythmSpawner - completely rewrite the update function
var ImprovedRhythmSpawner = {
update: function update(currentTime) {
if (!GameState.gameActive || GameState.songStartTime === 0) {
return;
}
// Only spawn if no battle is active and no fish are on screen
if (GameState.battleState !== BATTLE_STATES.NONE || fishArray.length > 0) {
return;
}
// If we're waiting for next spawn time
if (GameState.nextFishSpawnTime > 0 && currentTime < GameState.nextFishSpawnTime) {
return;
}
var songConfig = GameState.getCurrentSongConfig();
if (!songConfig || !songConfig.bpm) {
// Safety check
return;
}
var beatInterval = 60000 / songConfig.bpm;
// Spawn immediately with normal speed
this.spawnSingleFish(currentTime, beatInterval);
},
spawnSingleFish: function spawnSingleFish(currentTime, beatInterval) {
var depthConfig = GameState.getCurrentDepthConfig();
if (!depthConfig) {
// Safety check
return;
}
// Use the original fish speed from config
var fishSpeed = depthConfig.fishSpeed; // This is the normal speed
var spawnSide = Math.random() < 0.5 ? -1 : 1; // -1 for left, 1 for right
var actualSpeed = Math.abs(fishSpeed) * (spawnSide === -1 ? 1 : -1); // Positive for left to right, negative for right to left
// Determine fish type
var laneIndex = PatternGenerator.getNextLane();
var targetLane = GAME_CONFIG.LANES[laneIndex];
var fishType, fishValue;
var rand = Math.random();
if (rand < 0.05) {
fishType = 'rare';
fishValue = Math.floor(depthConfig.fishValue * 4);
} else if (GameState.selectedDepth >= 2 && rand < 0.3) {
fishType = 'deep';
fishValue = Math.floor(depthConfig.fishValue * 2);
} else if (GameState.selectedDepth >= 1 && rand < 0.6) {
fishType = 'medium';
fishValue = Math.floor(depthConfig.fishValue * 1.5);
} else {
fishType = 'shallow';
fishValue = Math.floor(depthConfig.fishValue);
}
var newFish = new Fish(fishType, fishValue, actualSpeed, laneIndex);
// If actualSpeed is positive, fish moves L to R, starts from left.
// If actualSpeed is negative, fish moves R to L, starts from right.
newFish.x = actualSpeed > 0 ? -150 : 2048 + 150;
newFish.y = targetLane.y;
newFish.baseY = targetLane.y;
newFish.lastX = newFish.x; // Initialize lastX
fishArray.push(newFish);
if (fishingScreen && !fishingScreen.destroyed) {
fishingScreen.addChild(newFish);
}
GameState.sessionFishSpawned++;
},
scheduleNextFish: function scheduleNextFish() {
var songConfig = GameState.getCurrentSongConfig();
var beatInterval = 60000 / songConfig.bpm;
var currentTime = LK.ticks * (1000 / 60);
// Schedule next fish spawn for just a half beat later
var beatsDelay = 0.1; // Much shorter delay
GameState.nextFishSpawnTime = currentTime + beatInterval * beatsDelay;
GameState.battleState = BATTLE_STATES.WAITING_FOR_NEXT;
LK.setTimeout(function () {
if (GameState.battleState === BATTLE_STATES.WAITING_FOR_NEXT) {
GameState.battleState = BATTLE_STATES.NONE;
GameState.nextFishSpawnTime = 0;
}
}, beatInterval * beatsDelay);
},
// Adding a reset method, though not in the prompt, is good practice if replacing the whole object.
// However, the original prompt for ImprovedRhythmSpawner did not include this in the new code.
// Sticking to the prompt, so no explicit reset here unless it was part of the provided block.
// The new system doesn't require the complex reset of the old one (nextBeatToSchedule, scheduledBeats).
// An empty reset or a reset focusing on its new simpler state might be added later if needed.
// For now, the structure above replaces the old entirely.
reset: function reset() {
// This spawner's state is mostly in GameState (battleState, nextFishSpawnTime)
// No internal scheduledBeats or nextBeatToSchedule to reset in this new version.
// If GameState.nextFishSpawnTime needs reset on a full game reset, that should happen elsewhere.
}
};
/****
* 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]; // Tutorial always uses shallow depth config for simplicity
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
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.destroyed) {
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish.destroy();
}
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) {
fishingElements.startWaterSurfaceAnimation();
}
if (typeof fishingElements.startBoatAndFishermanAnimation === 'function' && GameState.tutorialStep < 9) {
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.
}
}
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'.");
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; // Paused for CONTINUE, but swipe is enabled by handleFishingInput
break;
case 2:
setTutorialText("Great! Fish swim in three lanes. Try SWIPING your hook to different lanes. When ready, 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; // Paused for CONTINUE, swipe enabled by handleFishingInput
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!");
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.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.35,
lane: swipeState.currentLane,
// Fish in the hook's current lane
fishAsset: 'sardine' // Force sardine for 2 taps
});
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!");
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.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.45,
lane: 0,
// Fish in top lane (index 0)
fishAsset: 'sardine'
});
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'.");
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:
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();
GameState.tutorialFish = null;
}
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
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);
}
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,
introPlaying: false,
musicNotesActive: false,
currentPlayingMusicId: 'rhythmTrack',
currentPlayingMusicInitialVolume: 0.8,
lastLevelSelectNodeKey: 'dock',
hookTargetLaneIndex: 1,
tutorialMode: false,
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({
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 restaurantScreen = game.addChild(new Container());
restaurantScreen.visible = false; // Initially hidden
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);
function createInventoryDropdown() {
inventoryDropdownContainer.removeChildren();
var inventory = FishInventory.getInventoryDisplay();
if (inventory.length === 0) {
// Show "No fish caught yet" message
var dropdownBaseY = 190 + 120 + 30; // new button top (190) + new button height (120) + new scaled padding (20*1.5=30)
var emptyBg = inventoryDropdownContainer.addChild(LK.getAsset('songCard', {
anchorX: 1,
anchorY: 0,
x: 1900 + 20,
y: dropdownBaseY,
// Adjusted y based on new button size
width: 600,
// 400 * 1.5
height: 150,
// 100 * 1.5
color: 0x424242,
alpha: 0.9
}));
var emptyText = inventoryDropdownContainer.addChild(new Text2('No fish caught yet', {
size: 68,
// 45 * 1.5, rounded
fill: 0xCCCCCC,
align: 'center'
}));
emptyText.anchor.set(0.5, 0.5);
emptyText.x = 1900 + 20 - 600 / 2; // Centered in new emptyBg
emptyText.y = dropdownBaseY + 150 / 2; // Centered in new emptyBg height
return;
}
// Calculate dropdown size
var itemHeight = 180; // 120 * 1.5
var dropdownBaseY = 190 + 120 + 30; // Matches emptyBg.y logic
var dropdownVerticalPadding = 30; // 20 * 1.5
var dropdownHeight = inventory.length * itemHeight + dropdownVerticalPadding;
var dropdownWidth = 675; // 450 * 1.5
// Background
var dropdownBg = inventoryDropdownContainer.addChild(LK.getAsset('songCard', {
anchorX: 1,
anchorY: 0,
x: 1900 + 20,
y: dropdownBaseY,
// Below inventory button
width: dropdownWidth,
height: dropdownHeight,
color: 0x424242,
alpha: 0.9
}));
// Fish items
var itemTopPadding = 15; // 10 * 1.5
var itemImageLeftPadding = 30; // 20 * 1.5
var itemImageYOffset = 15; // 10 * 1.5
var spaceAfterImage = 150; // 100 * 1.5 (assuming based on original fish asset width + padding)
var textYOffset = 23; // 15 * 1.5, rounded
var countTextRightPadding = 30; // 20 * 1.5
for (var i = 0; i < inventory.length; i++) {
var fishData = inventory[i];
var itemY = dropdownBaseY + itemTopPadding + i * itemHeight;
// Fish image
var fishImage = inventoryDropdownContainer.addChild(LK.getAsset(fishData.type, {
anchorX: 0,
anchorY: 0,
x: 1900 + 20 - dropdownWidth + itemImageLeftPadding,
y: itemY + itemImageYOffset,
scaleX: 0.6,
// 0.4 * 1.5
scaleY: 0.6 // 0.4 * 1.5
}));
// Fish name
var fishNameText = inventoryDropdownContainer.addChild(new Text2(fishData.name, {
size: 63,
// 42 * 1.5
fill: 0xFFFFFF
}));
fishNameText.anchor.set(0, 0);
// Calculate actual image width: original asset width (e.g. sardine 200) * new scale (0.6)
// This is complex as original asset width varies. Let's assume spaceAfterImage is relative to fishImage.x
var fishImageDisplayWidth = LK.getAsset(fishData.type, {}).width * 0.6; // Get scaled width
fishNameText.x = fishImage.x + fishImageDisplayWidth + 20 * 1.5; // Scaled space after image, 20px old padding
fishNameText.y = itemY + textYOffset;
// Fish count
var fishCountText = inventoryDropdownContainer.addChild(new Text2('x' + fishData.count, {
size: 63,
// 42 * 1.5
fill: 0xFFD700
}));
fishCountText.anchor.set(1, 0);
fishCountText.x = 1900 + 20 - itemImageLeftPadding; // Align with right edge using same padding as left
fishCountText.y = itemY + textYOffset;
}
}
function toggleInventoryDropdown() {
if (inventoryDropdownOpen) {
// Close dropdown
inventoryDropdownContainer.visible = false;
inventoryDropdownOpen = false;
} else {
// Open dropdown
createInventoryDropdown();
inventoryDropdownContainer.visible = true;
if (levelSelectScreen && inventoryDropdownContainer.parent === levelSelectScreen) {
levelSelectScreen.setChildIndex(inventoryDropdownContainer, levelSelectScreen.children.length - 1);
}
inventoryDropdownOpen = true;
}
}
var dottedLinesContainer = levelSelectScreen.addChild(new Container());
var nodesContainer = levelSelectScreen.addChild(new Container());
var boatContainer = levelSelectScreen.addChild(new Container());
var inventoryButtonBg = levelSelectScreen.addChild(LK.getAsset('songCard', {
anchorX: 1,
anchorY: 0,
x: 1900 + 20,
y: 200 - 10,
// Below money display
width: 375,
// 250 * 1.5
height: 120,
// 80 * 1.5
color: 0x424242,
alpha: 0.8
}));
var inventoryButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 1,
anchorY: 0,
x: 1900 + 20,
y: 200 - 10,
width: 375,
// 250 * 1.5
height: 120,
// 80 * 1.5
tint: 0x1976d2,
alpha: 0
}));
var inventoryButtonText = new Text2('INVENTORY', {
size: 57,
// 38 * 1.5
fill: 0xFFFFFF
});
inventoryButtonText.anchor.set(1, 0);
inventoryButtonText.x = 1900;
inventoryButtonText.y = 190 + (120 - 57) / 2; // Adjusted for new button height and text size, centered
levelSelectScreen.addChild(inventoryButtonText);
// Inventory Dropdown Container
var inventoryDropdownContainer = levelSelectScreen.addChild(new Container());
inventoryDropdownContainer.visible = false;
var inventoryDropdownOpen = false;
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,
difficultyEasyButton: null,
difficultyEasyButtonText: null,
difficultyMediumButton: null,
difficultyMediumButtonText: null,
difficultyHardButton: null,
difficultyHardButtonText: 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 = storage.restaurantUnlocked || false;
}
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());
// Determine the correct asset for the node
var nodeAsset = 'nodeLocked'; // Default to locked
if (node.unlocked) {
if (nodeKey === 'restaurant') {
nodeAsset = 'restaurantUnlocked'; // Use special asset for unlocked restaurant
} else {
nodeAsset = 'nodeUnlocked'; // Use generic unlocked asset for others
}
}
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;
// 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) {
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;
// 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;
}
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) {
// Check inventory button click first
// inventoryButton.width is now 375, inventoryButton.height is 120. These are used directly.
if (x >= inventoryButton.x - inventoryButton.width && x <= inventoryButton.x && y >= inventoryButton.y && y <= inventoryButton.y + inventoryButton.height) {
toggleInventoryDropdown();
LK.getSound('buttonClick').play();
return;
}
// Close inventory dropdown if clicking elsewhere
var dropdownBaseY = 190 + 120 + 30; // Same as in createInventoryDropdown
var newDropdownWidth = 675;
// Check if the click is outside the dropdown container's bounds
if (inventoryDropdownOpen) {
var dropdownRightX = 1900 + 20;
var dropdownLeftX = dropdownRightX - newDropdownWidth;
var dropdownTopY = dropdownBaseY;
var dropdownBottomY = dropdownTopY + inventoryDropdownContainer.height; // Use actual height of container
if (!(x >= dropdownLeftX && x <= dropdownRightX && y >= dropdownTopY && y <= dropdownBottomY)) {
// And also ensure the click was not on the inventory button itself (already handled above, but good for clarity if separated)
var buttonTopY = inventoryButton.y;
var buttonBottomY = inventoryButton.y + inventoryButton.height;
var buttonRightX = inventoryButton.x;
var buttonLeftX = inventoryButton.x - inventoryButton.width;
if (!(x >= buttonLeftX && x <= buttonRightX && y >= buttonTopY && y <= buttonBottomY)) {
toggleInventoryDropdown();
}
}
}
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;
}
// 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++;
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;
}
// Handle inventory button click
if (x >= inventoryButton.x - inventoryButton.width / 2 && x <= inventoryButton.x + inventoryButton.width / 2 && y >= inventoryButton.y - inventoryButton.height / 2 && y <= inventoryButton.y + inventoryButton.height / 2) {
toggleInventoryDropdown();
LK.getSound('buttonClick').play();
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) {
// ADD THIS: Handle restaurant navigation
if (nodeGfx.nodeKey === 'restaurant') {
showScreen('restaurant');
} else {
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,
inventoryButton: inventoryButton,
inventoryDropdownContainer: inventoryDropdownContainer,
toggleInventoryDropdown: toggleInventoryDropdown,
songOverlayOpen: function songOverlayOpen() {
return _songOverlayOpen || inventoryDropdownOpen;
},
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 restaurantElements; // For restaurant screen UI elements
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 (isDown) {
swipeState.startX = x;
swipeState.startY = y;
// No action on 'down' other than recording start position for swipe/tap detection.
} 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();
}
}
}
}
/****
* 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;
restaurantScreen.visible = false; // Add this to hide restaurant screen by default
GameState.currentScreen = screenName;
switch (screenName) {
case 'restaurant':
restaurantScreen.visible = true;
restaurantElements = createRestaurantScreen(); // Create/recreate elements
if (restaurantElements.moneyDisplay) {
// Update money on screen show
restaurantElements.moneyDisplay.setText('$' + GameState.money);
}
break;
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
});
// 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);
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;
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 {
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.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();
}
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;
}
// Modify the checkCatch function to use the current player lane instead of detecting from touch position
function checkCatch(playerLane) {
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.
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)
if (caughtType === 'perfect' || caughtType === 'good') {
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.");
} 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.");
}
}
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) {
var distance = Math.abs(fish.x - hookX);
var maxCatchDistance = GAME_CONFIG.MISS_WINDOW * 2;
if (distance < maxCatchDistance && distance < closestDistance) {
closestDistance = distance;
closestFishInLane = fish;
}
}
}
if (!closestFishInLane) {
// Miss - play sound and break combo
LK.getSound('miss').play();
GameState.combo = 0;
// 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;
var tintToRedDuration = 50;
var holdRedDuration = 100;
var tintToWhiteDuration = 150;
if (leftBracket && !leftBracket.destroyed) {
tween(leftBracket, {
tint: 0xFF0000
}, {
duration: tintToRedDuration,
easing: tween.linear,
onFinish: function onFinish() {
LK.setTimeout(function () {
if (leftBracket && !leftBracket.destroyed) {
tween(leftBracket, {
tint: 0xFFFFFF
}, {
duration: tintToWhiteDuration,
easing: tween.linear
});
}
}, holdRedDuration);
}
});
}
if (rightBracket && !rightBracket.destroyed) {
tween(rightBracket, {
tint: 0xFF0000
}, {
duration: tintToRedDuration,
easing: tween.linear,
onFinish: function onFinish() {
LK.setTimeout(function () {
if (rightBracket && !rightBracket.destroyed) {
tween(rightBracket, {
tint: 0xFFFFFF
}, {
duration: tintToWhiteDuration,
easing: tween.linear
});
}
}, holdRedDuration);
}
});
}
}
return;
}
// 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';
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) {
points = closestFishInLane.value * multiplier;
catchType = 'good';
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.MISS_WINDOW) {
points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier));
catchType = 'good';
GameState.combo++;
} else {
showFeedback('miss', playerLane);
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();
}
}
var catchSounds = ['catch', 'catch2', 'catch3', 'catch4'];
var randomCatchSound = catchSounds[Math.floor(Math.random() * catchSounds.length)];
LK.getSound(randomCatchSound).play();
animateHookCatch();
}
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') {
// 1. Check for "CONTINUE" button tap
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;
}
// 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();
}
GameState.tutorialFish = null; // Clear the reference
}
if (advanceToNextLogicalStep) {
GameState.tutorialStep++; // Advance to the next tutorial segment (e.g., 3 becomes 4)
}
// 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
}
// 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
}
// 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) {
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':
//{EO} // This case now only handles non-tutorial fishing presses
handleFishingInput(x, y, true);
break;
case 'restaurant':
if (CookingState.active) {
var phaseName = ['prep', 'cook', 'plate'][CookingState.currentPhase];
if (phaseName === 'cook') {
handleCookingInput(x, y, 'down', obj); // Pass 'down' eventType
} else {
handleCookingInput(x, y, 'tap', obj); // Default to 'tap' for prep/plate on down
}
} else {
handleRestaurantInput(x, y); // For non-cooking restaurant interactions
}
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();
}
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();
}
}
if (GameState.currentScreen === 'restaurant') {
// Update restaurant customers
for (var i = RestaurantState.currentCustomers.length - 1; i >= 0; i--) {
var customer = RestaurantState.currentCustomers[i];
if (customer && typeof customer.update === 'function') {
customer.update();
}
}
// Update cooking system if active
if (CookingState.active) {
BeatDetector.update();
updateCookingPhases();
}
}
// 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();
}
// Automatic hook movement removed; player swipe input (via handleFishingInput)
// and initial setup in runTutorialStep now control the hook position.
}
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);
// (Automatic hook following logic removed as player now controls lane position via swipe)
// updateLaneBracketsVisuals() is now called within handleFishingInput after a swipe.
// 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)) {
// 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);
}
}
// 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
});
}
}
});
}
}
}
// 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) {
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');
// 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
};
/****
* Restaurant Screen
****/
function createTables(container, floorStartY) {
// Create 6 tables in 2 rows of 3
var tableWidth = 200;
var tableHeight = 150;
var tablesPerRow = 3;
var tableSpacingX = (2048 - tablesPerRow * tableWidth) / (tablesPerRow + 1);
var rowSpacing = 300;
for (var row = 0; row < 2; row++) {
for (var col = 0; col < tablesPerRow; col++) {
var tableX = tableSpacingX + col * (tableWidth + tableSpacingX);
// Adjust table Y position to be relative to the container and floorStartY
var tableY = 150 + row * rowSpacing; // Relative to container start
// Table background
var table = container.addChild(LK.getAsset('songCard', {
x: tableX,
y: tableY,
width: tableWidth,
height: tableHeight,
color: 0x8D6E63,
// Brown table color
alpha: 0.9,
anchorX: 0,
anchorY: 0
}));
// Table number
var tableNumber = container.addChild(new Text2('Table ' + (row * tablesPerRow + col + 1), {
size: 35,
fill: 0xFFFFFF,
align: 'center'
}));
tableNumber.anchor.set(0.5, 0.5);
tableNumber.x = tableX + tableWidth / 2;
tableNumber.y = tableY + tableHeight / 2;
}
}
}
function createRestaurantUI() {
// Money display (top right)
var moneyBg = restaurantScreen.addChild(LK.getAsset('songCard', {
anchorX: 1,
anchorY: 0,
x: 2048 - 50,
y: 50,
width: 300,
height: 80,
color: 0x000000,
alpha: 0.7
}));
var moneyDisplay = restaurantScreen.addChild(new Text2('$' + GameState.money, {
size: 50,
fill: 0xFFD700
}));
moneyDisplay.anchor.set(1, 0.5); // Anchor to middle-right for better vertical alignment
moneyDisplay.x = 2048 - 70;
moneyDisplay.y = 50 + 80 / 2; // Vertically center in its background
// Back button (bottom center)
var backButton = restaurantScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2600,
width: 300,
height: 100,
tint: 0x757575 // Grey color for back button
}));
var backButtonText = restaurantScreen.addChild(new Text2('BACK', {
size: 50,
fill: 0xFFFFFF
}));
backButtonText.anchor.set(0.5, 0.5);
backButtonText.x = backButton.x;
backButtonText.y = backButton.y;
return {
moneyDisplay: moneyDisplay,
backButton: backButton,
backButtonText: backButtonText
};
}
function createRestaurantScreen() {
restaurantScreen.removeChildren();
// Background
var restaurantBg = restaurantScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 1,
width: 2048,
// Ensure full width
height: 2732,
color: 0x8D6E63 // Brown restaurant color
}));
// Define layout constants
var TOP_SECTION_HEIGHT = GAME_CONFIG.WATER_SURFACE_Y; // Same height as sky/boat area in fishing
var KITCHEN_WIDTH = 2048 * 2 / 3; // Right 2/3 of screen
var COUNTER_WIDTH = 2048 / 3; // Left 1/3 of screen
// Top Section - Kitchen (right 2/3)
var kitchenArea = restaurantScreen.addChild(LK.getAsset('songCard', {
// Using songCard as a placeholder asset
x: COUNTER_WIDTH,
y: 0,
width: KITCHEN_WIDTH,
height: TOP_SECTION_HEIGHT,
color: 0xFFFFFF,
// Light color for kitchen
alpha: 0.9,
anchorX: 0,
anchorY: 0
}));
// Kitchen label
var kitchenLabel = restaurantScreen.addChild(new Text2('KITCHEN', {
size: 60,
fill: 0x424242,
// Dark grey text
align: 'center'
}));
kitchenLabel.anchor.set(0.5, 0.5);
kitchenLabel.x = COUNTER_WIDTH + KITCHEN_WIDTH / 2;
kitchenLabel.y = TOP_SECTION_HEIGHT / 2;
// Top Section - Counter/Cashier (left 1/3)
var counterArea = restaurantScreen.addChild(LK.getAsset('songCard', {
// Using songCard as a placeholder asset
x: 0,
y: 0,
width: COUNTER_WIDTH,
height: TOP_SECTION_HEIGHT,
color: 0x795548,
// Darker brown for counter
alpha: 0.9,
anchorX: 0,
anchorY: 0
}));
// Counter label
var counterLabel = restaurantScreen.addChild(new Text2('COUNTER', {
size: 50,
fill: 0xFFFFFF,
// White text
align: 'center'
}));
counterLabel.anchor.set(0.5, 0.5);
counterLabel.x = COUNTER_WIDTH / 2;
counterLabel.y = TOP_SECTION_HEIGHT / 2;
// Bottom Section - Restaurant Floor
var restaurantFloor = restaurantScreen.addChild(LK.getAsset('songCard', {
// Using songCard as a placeholder asset
x: 0,
y: TOP_SECTION_HEIGHT,
width: 2048,
height: 2732 - TOP_SECTION_HEIGHT,
color: 0x6D4C41,
// Medium brown for floor
alpha: 0.8,
anchorX: 0,
anchorY: 0
}));
// Restaurant floor label
var floorLabel = restaurantScreen.addChild(new Text2('RESTAURANT FLOOR', {
size: 70,
fill: 0xFFFFFF,
// White text
align: 'center'
}));
floorLabel.anchor.set(0.5, 0.5);
floorLabel.x = 1024; // Center of screen
floorLabel.y = TOP_SECTION_HEIGHT + 200; // Offset from top of floor section
// Create tables (simple placeholders for now)
var tableContainer = restaurantScreen.addChild(new Container());
tableContainer.x = 0; // Position container at screen edge
tableContainer.y = TOP_SECTION_HEIGHT; // Position container at the start of the floor
createTables(tableContainer, TOP_SECTION_HEIGHT);
// UI Elements
var uiElements = createRestaurantUI(); // This will add children directly to restaurantScreen
// Show menu selection immediately after a brief delay
LK.setTimeout(function () {
// Ensure we are still on the restaurant screen and it's not destroyed
if (GameState.currentScreen === 'restaurant' && restaurantScreen && !restaurantScreen.destroyed) {
showMenuSelection();
}
}, 100);
return {
kitchenArea: kitchenArea,
counterArea: counterArea,
restaurantFloor: restaurantFloor,
tableContainer: tableContainer,
moneyDisplay: uiElements.moneyDisplay,
backButton: uiElements.backButton,
backButtonText: uiElements.backButtonText
};
initializeCookingSystem();
}
function handleRestaurantInput(x, y) {
// Check for customer clicks first
for (var i = 0; i < RestaurantState.currentCustomers.length; i++) {
var customer = RestaurantState.currentCustomers[i];
if (customer && !customer.served && !customer.destroyed) {
// Check if click is near customer
var distance = Math.sqrt(Math.pow(x - customer.x, 2) + Math.pow(y - customer.y, 2));
if (distance < 100) {
// 100px click radius
// Start cooking for this customer
startCookingSession(customer, customer.orderRecipe);
LK.getSound('buttonClick').play();
return;
}
}
}
// Existing back button handling
if (restaurantElements && restaurantElements.backButton) {
var backBtn = restaurantElements.backButton;
if (x >= backBtn.x - backBtn.width / 2 && x <= backBtn.x + backBtn.width / 2 && y >= backBtn.y - backBtn.height / 2 && y <= backBtn.y + backBtn.height / 2) {
showScreen('levelSelect');
LK.getSound('buttonClick').play();
return;
}
}
} ===================================================================
--- original.js
+++ change.js
@@ -114,19 +114,19 @@
self.isLongNote = isLong;
if (self.isLongNote) {
self.durationBeats = beats > 0 ? beats : 1;
if (self.beatIndicator) {
- // Make the beat indicator wider for long notes
- // Assuming base width of 60 for a single beat indicator
- self.beatIndicator.width = 40 + self.durationBeats * 50; // Base width + 50px per beat length
+ // 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; // Also tint the line itself for long notes
+ self.line.tint = indicatorTint || 0x00BFFF;
self.line.alpha = 0.8;
}
} else if (self.beatIndicator) {
- // Ensure short notes have their specific indicator tint if different
self.beatIndicator.tint = indicatorTint || 0x00FF00; // Default green for short tap indicators
}
};
return self;
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 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
Change the anchor to a knife and fork icon.
Top down beach sand background image with a light to dark gradient starting at the top.. In-Game asset. 2d. High contrast. No shadows
A moped courier riding a moped with a food carrying basket with the top open and no lid on the back of the moped. Top down view with the moped pointing fully sideways.
A kitchen knife. Side view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A large kitchen mixing bowl. Side view.
A fryer basket from a deep fryer. Side view.
An anchovy. 80s arcade machine graphics. Swimming Side profile. White background. In-Game asset. 2d. High contrast. No shadows
A raw fish steak. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
This fish steak with covered with raw batter on it.
Cooked fish and chips in a newspaper lined basket. Side view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast
A long rectangular wooden cutting board counter with different colored laminated wooden strips. Top down view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
a textured white rectangular kitchen counter top. overhead view. In-Game asset. 2d. High contrast. No shadows
A light blue starburst with βChop!β In the center. 80s arcade machine graphics
Change the text to βDip!β
Change the text to βFry!β
Inside the kitchen of a small wooden shack restaurant, looking out over the counter onto a beach view with long gradual gradients. No people inside or on the beach.
This man with no fishing rod and chefs clothes on, facing forward.
Change the sign to say βThe Fish Shackβ and make the window larger.
A background image view looking into a fry kitchen.
A young man standing, facing away with his back facing down. Top down view.
A boombox stereo. Top down view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A young woman with long hair and shorts. No hat. Pink shirt.
An old man with a cane and hat. Still facing straight forward with back facing straight down.
A young boy with a yellow Hawaiian style shirt and sandals. No hat. Blonde hair.
A small red crab. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
Top down view of a shallow wave water on a beach shore..80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
A long horizontal line of low bushes. Top down view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A outdoor wooden picnic bench. Top down view. Squared up.
A sea bass. 80s arcade machine graphics. Swimming Side profile. White background. In-Game asset. 2d. High contrast. No shadows
A shining golden mythical fish. Side profile, swimming. 80s arcade machine graphics. White background. In-Game asset. 2d. High contrast. No shadows
A cod. 80s arcade machine graphics. Swimming Side profile. White background. In-Game asset. 2d. High contrast. No shadows
A snapper. 80s arcade machine graphics. Swimming Side profile. White background. In-Game asset. 2d. High contrast. No shadows
Replace the knife and fork with a dollar symbol.
A meat grinder.
A set of spices shakers.
A large soup pot.
A blank sandwhich board. Top down view.
Fish cakes in a to go box.
Two fish tacos in a to go box.
A fish burger and fries in a basket lined with paper.
A bowl of fish curry.
A bowl of fish dumplings.
A bowl of fish meatballs in broth.
A ball of ground fish in a bowl.
Two uncooked fish cakes.
A raw ground fish patty.
This fish steak with spices added to.
Change the word to say βGrind!β
Change the word to say βSpice!β
Change the word to say βBoil!β
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
oceanCurrent
Music
deepFlow
Music
coralGroove
Music