Code edit (1 edits merged)
Please save this source code
User prompt
Please fix the bug: 'undefined is not a constructor (evaluating 'new Set()')' in or related to this line: 'var processedPatterns = new Set(); // Track which patterns we've already spawned' Line Number: 988
User prompt
Update as needed with: Update with new system: GAME_CONFIG.PATTERNS.gentle_waves_easy = { beatsPerFish: 2.0, // Reduce regular spawning to make room for patterns // Define the melodic patterns melodicPatterns: [ { name: "chord_motif", pattern: [ {offset: 0, type: "big", lane: 1}, // Strong chord {offset: 400, type: "small", lane: 1}, // Quick note 1 {offset: 600, type: "small", lane: 2}, // Quick note 2 {offset: 1200, type: "medium", lane: 1} // Accent ], repeats: [ {startTime: 0}, // First time {startTime: 5263}, // Repeat {startTime: 10526}, // Repeat {startTime: 15789} // Repeat ] }, { name: "melody_phrase", pattern: [ {offset: 0, type: "medium", lane: 2}, // Melody start {offset: 800, type: "small", lane: 2}, // Step up {offset: 1200, type: "small", lane: 2}, // Quick 1 {offset: 1400, type: "small", lane: 2}, // Quick 2 {offset: 1600, type: "small", lane: 1} // Quick 3 (descent) ], repeats: [ {startTime: 20000}, // First melody entrance {startTime: 35000}, // Melody repeats {startTime: 65000}, // Development section {startTime: 95000} // Final statement ] }, { name: "bass_accompaniment", pattern: [ {offset: 0, type: "big", lane: 0}, // Bass note {offset: 1000, type: "medium", lane: 1} // Chord ], repeats: [ {startTime: 25000}, {startTime: 30000}, {startTime: 40000}, {startTime: 50000}, {startTime: 60000}, {startTime: 70000} ] } ] };// Add these global variables for pattern tracking var melodicPatternData = []; var processedPatterns = new Set(); // Track which patterns we've already spawned // Modify your startFishingSession function function startFishingSession() { // ... existing code ... // Initialize melodic patterns var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; if (pattern.melodicPatterns) { melodicPatternData = []; processedPatterns.clear(); // Flatten all pattern repeats into a single timeline pattern.melodicPatterns.forEach(function(melodicPattern) { melodicPattern.repeats.forEach(function(repeat) { melodicPattern.pattern.forEach(function(note) { melodicPatternData.push({ time: repeat.startTime + note.offset, type: note.type, lane: note.lane, patternName: melodicPattern.name, uniqueId: repeat.startTime + "_" + note.offset + "_" + melodicPattern.name }); }); }); }); // Sort by time melodicPatternData.sort(function(a, b) { return a.time - b.time; }); } // ... rest of existing code ... } ``` ## **2. Update the Spawning Logic** ```javascript // Modify your game.update function game.update = function () { if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } var currentTime = LK.ticks * (1000 / 60); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; if (GameState.songStartTime === 0) { GameState.songStartTime = currentTime; } var songElapsed = currentTime - GameState.songStartTime; if (songElapsed >= songConfig.duration) { endFishingSession(); return; } // 1. REGULAR BEAT-BASED SPAWNING (reduced frequency) var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; if (currentTime - GameState.lastBeatTime >= spawnInterval) { GameState.lastBeatTime = currentTime; GameState.beatCount++; if (PatternGenerator.canSpawnFishOnBeat(currentTime, spawnInterval)) { var firstFish = spawnFish(currentTime, {}); // Your existing spawn logic // Your existing double spawn logic here if needed... } } // 2. MELODIC PATTERN SPAWNING if (pattern.melodicPatterns && melodicPatternData.length > 0) { // Check for pattern fish to spawn melodicPatternData.forEach(function(patternFish) { if (!processedPatterns.has(patternFish.uniqueId) && songElapsed >= patternFish.time) { spawnPatternFish(patternFish); processedPatterns.add(patternFish.uniqueId); } }); } // ... rest of your existing update logic ... }; ``` ## **3. Add Pattern Fish Spawning Function** ```javascript // Add this new function for pattern-based fish function spawnPatternFish(patternFish) { var depthConfig = GameState.getCurrentDepthConfig(); var fishType = patternFish.type; var fishValue = depthConfig.fishValue; // Adjust value based on fish type switch(fishType) { case "big": fishValue *= 3; break; case "medium": fishValue *= 2; break; case "small": fishValue *= 1; break; } var targetLane = patternFish.lane; var newFish = new Fish(fishType === "big" ? "medium" : "shallow", fishValue, depthConfig.fishSpeed, targetLane, false); // Give pattern fish a slight visual distinction if (fishType === "big") { newFish.isSpecial = true; // Shimmer effect for emphasis } var spawnSide = Math.random() < 0.5 ? -1 : 1; newFish.x = spawnSide === -1 ? -150 : 2048 + 150; newFish.y = GAME_CONFIG.LANES[targetLane].y; newFish.speed = Math.abs(depthConfig.fishSpeed) * spawnSide; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; } ``` ## **4. Update Your Song Config** ```javascript // Update the Gentle Waves song to use the new pattern GAME_CONFIG.DEPTHS[0].songs[0] = { name: "Gentle Waves", bpm: 95, duration: 202000, // 3:22 pattern: "gentle_waves_easy", cost: 0 };
User prompt
Make sure that the gentle waves song properly uses gentle_waves_easy pattern for fish spawning. Right now only the beat fish are spawning.
User prompt
Make sure that the gentle waves song actually uses the gentle_waves_easy pattern when spawning fish.
User prompt
Update with new system: GAME_CONFIG.PATTERNS.gentle_waves_easy = { beatsPerFish: 2.0, // Reduce regular spawning to make room for patterns // Define the melodic patterns melodicPatterns: [ { name: "chord_motif", pattern: [ {offset: 0, type: "big", lane: 1}, // Strong chord {offset: 400, type: "small", lane: 1}, // Quick note 1 {offset: 600, type: "small", lane: 2}, // Quick note 2 {offset: 1200, type: "medium", lane: 1} // Accent ], repeats: [ {startTime: 0}, // First time {startTime: 5263}, // Repeat {startTime: 10526}, // Repeat {startTime: 15789} // Repeat ] }, { name: "melody_phrase", pattern: [ {offset: 0, type: "medium", lane: 2}, // Melody start {offset: 800, type: "small", lane: 2}, // Step up {offset: 1200, type: "small", lane: 2}, // Quick 1 {offset: 1400, type: "small", lane: 2}, // Quick 2 {offset: 1600, type: "small", lane: 1} // Quick 3 (descent) ], repeats: [ {startTime: 20000}, // First melody entrance {startTime: 35000}, // Melody repeats {startTime: 65000}, // Development section {startTime: 95000} // Final statement ] }, { name: "bass_accompaniment", pattern: [ {offset: 0, type: "big", lane: 0}, // Bass note {offset: 1000, type: "medium", lane: 1} // Chord ], repeats: [ {startTime: 25000}, {startTime: 30000}, {startTime: 40000}, {startTime: 50000}, {startTime: 60000}, {startTime: 70000} ] } ] };// Add these global variables for pattern tracking var melodicPatternData = []; var processedPatterns = new Set(); // Track which patterns we've already spawned // Modify your startFishingSession function function startFishingSession() { // ... existing code ... // Initialize melodic patterns var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; if (pattern.melodicPatterns) { melodicPatternData = []; processedPatterns.clear(); // Flatten all pattern repeats into a single timeline pattern.melodicPatterns.forEach(function(melodicPattern) { melodicPattern.repeats.forEach(function(repeat) { melodicPattern.pattern.forEach(function(note) { melodicPatternData.push({ time: repeat.startTime + note.offset, type: note.type, lane: note.lane, patternName: melodicPattern.name, uniqueId: repeat.startTime + "_" + note.offset + "_" + melodicPattern.name }); }); }); }); // Sort by time melodicPatternData.sort(function(a, b) { return a.time - b.time; }); } // ... rest of existing code ... } ``` ## **2. Update the Spawning Logic** ```javascript // Modify your game.update function game.update = function () { if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } var currentTime = LK.ticks * (1000 / 60); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; if (GameState.songStartTime === 0) { GameState.songStartTime = currentTime; } var songElapsed = currentTime - GameState.songStartTime; if (songElapsed >= songConfig.duration) { endFishingSession(); return; } // 1. REGULAR BEAT-BASED SPAWNING (reduced frequency) var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; if (currentTime - GameState.lastBeatTime >= spawnInterval) { GameState.lastBeatTime = currentTime; GameState.beatCount++; if (PatternGenerator.canSpawnFishOnBeat(currentTime, spawnInterval)) { var firstFish = spawnFish(currentTime, {}); // Your existing spawn logic // Your existing double spawn logic here if needed... } } // 2. MELODIC PATTERN SPAWNING if (pattern.melodicPatterns && melodicPatternData.length > 0) { // Check for pattern fish to spawn melodicPatternData.forEach(function(patternFish) { if (!processedPatterns.has(patternFish.uniqueId) && songElapsed >= patternFish.time) { spawnPatternFish(patternFish); processedPatterns.add(patternFish.uniqueId); } }); } // ... rest of your existing update logic ... }; ``` ## **3. Add Pattern Fish Spawning Function** ```javascript // Add this new function for pattern-based fish function spawnPatternFish(patternFish) { var depthConfig = GameState.getCurrentDepthConfig(); var fishType = patternFish.type; var fishValue = depthConfig.fishValue; // Adjust value based on fish type switch(fishType) { case "big": fishValue *= 3; break; case "medium": fishValue *= 2; break; case "small": fishValue *= 1; break; } var targetLane = patternFish.lane; var newFish = new Fish(fishType === "big" ? "medium" : "shallow", fishValue, depthConfig.fishSpeed, targetLane, false); // Give pattern fish a slight visual distinction if (fishType === "big") { newFish.isSpecial = true; // Shimmer effect for emphasis } var spawnSide = Math.random() < 0.5 ? -1 : 1; newFish.x = spawnSide === -1 ? -150 : 2048 + 150; newFish.y = GAME_CONFIG.LANES[targetLane].y; newFish.speed = Math.abs(depthConfig.fishSpeed) * spawnSide; fishArray.push(newFish); fishingScreen.addChild(newFish); GameState.sessionFishSpawned++; } ``` ## **4. Update Your Song Config** ```javascript // Update the Gentle Waves song to use the new pattern GAME_CONFIG.DEPTHS[0].songs[0] = { name: "Gentle Waves", bpm: 95, duration: 202000, // 3:22 pattern: "gentle_waves_easy", cost: 0 };
User prompt
Reduce dollar value of all fishes.
User prompt
Reduce the money earned from fishes considerably. A perfect run of gentle waves should only generate $50
User prompt
Update code as needed with: // Add this to your GAME_CONFIG.PATTERNS GAME_CONFIG.PATTERNS.gentle_waves_easy = { beatsPerFish: 2, // Keep original as fallback doubleSpawnChance: 0, rareSpawnChance: 0.02, holdFishChance: 0, // Note-specific spawn events noteEvents: [ // Opening chords - Big fish for strong moments {time: 0, type: "shallow", lane: 1, value: 3}, {time: 1263, type: "shallow", lane: 1, value: 2}, {time: 2526, type: "shallow", lane: 1, value: 2}, {time: 5052, type: "shallow", lane: 1, value: 3}, // Phrase end // Melody introduction - Mix of melody and bass {time: 10105, type: "medium", lane: 2, value: 2}, // Melody starts {time: 12631, type: "shallow", lane: 2, value: 1}, {time: 15157, type: "shallow", lane: 0, value: 2}, // Bass note {time: 20210, type: "medium", lane: 2, value: 3}, // Melody peak // Development - Key structural notes only {time: 26526, type: "shallow", lane: 2, value: 2}, {time: 31578, type: "shallow", lane: 1, value: 2}, {time: 37894, type: "medium", lane: 2, value: 3}, // Important melody {time: 42105, type: "shallow", lane: 0, value: 2}, // Busy section - Just the skeleton {time: 50526, type: "medium", lane: 1, value: 4}, // Major chord change {time: 56842, type: "shallow", lane: 2, value: 2}, {time: 63157, type: "shallow", lane: 0, value: 2}, {time: 69473, type: "medium", lane: 2, value: 4}, // Climax note // Climax - Strong structural moments {time: 75789, type: "medium", lane: 1, value: 4}, // Final melody statement {time: 82105, type: "shallow", lane: 1, value: 2}, {time: 88421, type: "shallow", lane: 2, value: 2}, {time: 94736, type: "medium", lane: 1, value: 4}, // Final chord // Fill notes {time: 25263, type: "shallow", lane: 2, value: 1}, {time: 47368, type: "shallow", lane: 1, value: 2}, {time: 72631, type: "shallow", lane: 0, value: 1}, {time: 91578, type: "shallow", lane: 2, value: 2} ] }; 2. Update Song Config
User prompt
Fade out and stop a song at the results screen. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
update code as needed with: // Add this to your GAME_CONFIG.PATTERNS GAME_CONFIG.PATTERNS.gentle_waves_custom = { beatsPerFish: 1.5, // Slightly more frequent than default simple doubleSpawnChance: 0.05, // Very rare double beats for shallow rareSpawnChance: 0.01, // Almost no rare fish holdFishChance: 0.02, // Very few hold fish for beginners // Custom timing sections based on the musical structure sections: [ // Opening - Simple chord pattern (0-30 seconds) { startTime: 0, endTime: 30000, spawnModifier: 1.0, // Normal spawn rate description: "steady_chords" }, // Melody Introduction (30-60 seconds) { startTime: 30000, endTime: 60000, spawnModifier: 0.9, // Slightly fewer fish description: "simple_melody" }, // Development (60-120 seconds) - Gets a bit busier { startTime: 60000, endTime: 120000, spawnModifier: 1.1, // Slightly more fish description: "melody_development" }, // Climax (120-180 seconds) - Busiest section but still shallow { startTime: 120000, endTime: 180000, spawnModifier: 1.3, // More fish, but not overwhelming description: "gentle_climax" }, // Ending (180-202 seconds) - Calming down { startTime: 180000, endTime: 202000, spawnModifier: 0.8, // Fewer fish for gentle ending description: "peaceful_ending" } ] }; // Update the song config GAME_CONFIG.DEPTHS[0].songs[0] = { name: "Gentle Waves", bpm: 95, duration: 202000, // 3:22 pattern: "gentle_waves_custom", cost: 0 }; Modified Spawning Logic Add this to your game update loop to handle the custom sections:
User prompt
Hold fish can’t be any part of multi fish patterns.
User prompt
Held fish can’t be part of multi fish patterns.
User prompt
Double and triple beats have to come from the same screen side if they are in the same lane. If they are in different lanes, each sequential fish needs to be in an adjacent lane, not two lanes over.
User prompt
Double and triple beats have to come in quick succession. Not at the same time. Multiple fish shouldn’t ever reach the catch area at the same time.
User prompt
I want an occasional chance for more complicated beats even in shallow water.
User prompt
Add some double beats in to song patterns and occasional more complicated patterns.
User prompt
Move it down another 5%
User prompt
Move the top lane down 10% and the other lanes to accommodate. Keep boat where it is.
User prompt
That is too much post hold fish spawn buffer, reduce.
User prompt
There has be enough space between held fish and the next fish to allow time to get to the next fish. Currently they’re too close together. Keep track of beat timing while fish are being held.
User prompt
Space out fish on beat to accommodate necessary hold lengths.
User prompt
The hold catch currently doesn’t work because the input is checking on the touch up handler instead of touch down. The hold fish also needs to stop moving when held on and provide metered feedback for when the hold is successful. ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Players still need to tap in corresponding lanes to catch the fish.
User prompt
Instead of three fishing lines have one line from the center of the boat that dynamically changes heights to be in position for the level of each fish as it comes.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ var FeedbackIndicator = Container.expand(function (type) { var self = Container.call(this); var indicator = self.attachAsset(type + 'Indicator', { anchorX: 0.5, anchorY: 0.5, alpha: 0 }); self.show = function () { indicator.alpha = 1; indicator.scaleX = 0.5; indicator.scaleY = 0.5; tween(indicator, { scaleX: 1.5, scaleY: 1.5, alpha: 0 }, { duration: 600, easing: tween.easeOut }); }; return self; }); /**** * Title Screen ****/ var Fish = Container.expand(function (type, value, speed, lane, isHoldFish) { var self = Container.call(this); var assetName = type + 'Fish'; var fishGraphics = self.attachAsset(assetName, { anchorX: 0.5, anchorY: 0.5 }); self.type = type; self.value = value; self.speed = speed; self.lane = lane; // Index of the lane (0, 1, or 2) self.isHoldFish = isHoldFish || false; self.caught = false; self.isSpecial = type === 'rare'; // For shimmer effect self.shimmerTime = 0; // Hold fish specific properties if (self.isHoldFish) { fishGraphics.tint = 0xFFD700; // Golden tint for hold fish self.holdStarted = false; // True if player successfully holds long enough (visual cue potential) self.requiredHoldTime = 800; // 800ms hold required to "prime" the catch for a hold fish } self.update = function () { if (!self.caught) { self.x += self.speed; if (self.isSpecial) { // Shimmer effect for rare fish self.shimmerTime += 0.1; fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2; } else if (!self.isHoldFish) { // Reset alpha if not special and not hold (hold has tint) fishGraphics.alpha = 1.0; } } }; self.catchFish = function () { self.caught = true; // Animation to boat tween(self, { y: GAME_CONFIG.BOAT_Y, // Target Y: boat position x: GAME_CONFIG.SCREEN_CENTER_X, //{r} // Target X: center of screen (boat position) scaleX: 0.5, scaleY: 0.5, alpha: 0 }, { duration: 600, easing: tween.easeOut, onFinish: function onFinish() { self.destroy(); } }); }; return self; }); /**** * Initialize Game ****/ /**** * Screen Containers ****/ var game = new LK.Game({ backgroundColor: 0x87CEEB }); /**** * Game Code ****/ /**** * Pattern Generation System ****/ // If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up. var PatternGenerator = { lastLane: -1, lastSpawnX: -1000, minDistanceBetweenFish: 300, // Minimum X distance between fish getNextLane: function getNextLane() { if (this.lastLane === -1) { // First fish, start in middle lane this.lastLane = 1; return 1; } // Prefer staying in same lane or moving to adjacent lane var possibleLanes = [this.lastLane]; // Add adjacent lanes if (this.lastLane > 0) possibleLanes.push(this.lastLane - 1); if (this.lastLane < 2) possibleLanes.push(this.lastLane + 1); // 70% chance to stay in same/adjacent lane if (Math.random() < 0.7) { this.lastLane = possibleLanes[Math.floor(Math.random() * possibleLanes.length)]; } else { // 30% chance for any lane this.lastLane = Math.floor(Math.random() * 3); } return this.lastLane; }, canSpawnFish: function canSpawnFish(fishX) { // Check if enough distance from last spawn var distance = Math.abs(fishX - this.lastSpawnX); return distance >= this.minDistanceBetweenFish; }, registerSpawn: function registerSpawn(fishX) { this.lastSpawnX = fishX; }, reset: function reset() { this.lastLane = -1; this.lastSpawnX = -1000; } }; /**** * Game Configuration ****/ game.up = function (x, y, obj) { // Note: We don't play buttonClick sound on 'up' typically, only on 'down'. switch (GameState.currentScreen) { case 'title': // title screen up actions (if any) break; case 'levelSelect': // level select screen up actions (if any, usually 'down' is enough for buttons) break; case 'fishing': handleFishingInput(x, y, false); // false for isUp break; case 'results': // results screen up actions (if any) break; } }; var GAME_CONFIG = { SCREEN_CENTER_X: 1024, SCREEN_CENTER_Y: 900, // Adjusted, though less critical with lanes BOAT_Y: 573, // Original 300 + 273 (maintains original offset from water surface) WATER_SURFACE_Y: 623, // Original 350 + 273 (273 is 10% of 2732 screen height, rounded down) // 3 Lane System // Vertically spread so that the second lowest hook (lane 1) is at the center of the screen LANES: [{ y: 723, // Top lane: center - 600 name: "shallow" }, { y: 1366, // Middle lane: center of screen (1366) name: "medium" }, { y: 2009, // Bottom lane: center + 643 name: "deep" }], // Timing windows PERFECT_WINDOW: 40, GOOD_WINDOW: 80, MISS_WINDOW: 120, // Depth levels - much lower money values! (Upgrade costs and song costs might need re-balancing with new mechanics) DEPTHS: [{ level: 1, name: "Shallow Waters", fishSpeed: 6, fishValue: 1, upgradeCost: 0, // Starting depth songs: [{ name: "Gentle Waves", bpm: 100, duration: 60000, pattern: "simple", cost: 0 }, { name: "Morning Tide", bpm: 110, duration: 75000, pattern: "simple", cost: 50 }] }, { level: 2, name: "Mid Waters", fishSpeed: 7, fishValue: 3, 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: 6, 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: 12, upgradeCost: 1000, songs: [{ name: "Leviathan", bpm: 160, duration: 150000, pattern: "expert", cost: 0 }, { name: "Deep Trench", bpm: 170, duration: 180000, pattern: "expert", cost: 600 }] }], // Updated patterns with hold fish - reduced frequency for playability PATTERNS: { simple: { beatsPerFish: 2, // Increased from 1 - fish every 2 beats doubleSpawnChance: 0, // No double spawns in simple rareSpawnChance: 0.02, holdFishChance: 0.05 // Reduced from 0.1 }, medium: { beatsPerFish: 1.5, // Increased from 0.75 doubleSpawnChance: 0, // No double spawns rareSpawnChance: 0.05, holdFishChance: 0.1 // Reduced from 0.2 }, complex: { beatsPerFish: 1, // Increased from 0.5 doubleSpawnChance: 0, // No double spawns rareSpawnChance: 0.08, holdFishChance: 0.15 // Reduced from 0.3 }, expert: { beatsPerFish: 0.75, // Increased from 0.25 doubleSpawnChance: 0, // No double spawns rareSpawnChance: 0.12, holdFishChance: 0.2 // Reduced from 0.4 } } }; /**** * Game State Management ****/ var GameState = { // Game flow currentScreen: 'title', // 'title', 'levelSelect', 'fishing', 'results' // Player progression currentDepth: 0, money: 0, totalFishCaught: 0, ownedSongs: [], // Array of {depth, songIndex} objects // Level selection selectedDepth: 0, selectedSong: 0, // Current session sessionScore: 0, sessionFishCaught: 0, sessionFishSpawned: 0, combo: 0, maxCombo: 0, // Game state gameActive: false, songStartTime: 0, lastBeatTime: 0, beatCount: 0, hookTargetLaneIndex: 1, // Start with hook targeting the middle lane (index 1) // Initialize owned songs (first song of each unlocked depth is free) initOwnedSongs: function initOwnedSongs() { this.ownedSongs = []; for (var i = 0; i <= this.currentDepth; i++) { this.ownedSongs.push({ depth: i, songIndex: 0 }); } }, hasSong: function hasSong(depth, songIndex) { return this.ownedSongs.some(function (song) { return song.depth === depth && song.songIndex === songIndex; }); }, buySong: function buySong(depth, songIndex) { var song = GAME_CONFIG.DEPTHS[depth].songs[songIndex]; if (this.money >= song.cost && !this.hasSong(depth, songIndex)) { this.money -= song.cost; this.ownedSongs.push({ depth: depth, songIndex: songIndex }); return true; } return false; }, getCurrentDepthConfig: function getCurrentDepthConfig() { return GAME_CONFIG.DEPTHS[this.selectedDepth]; }, getCurrentSongConfig: function getCurrentSongConfig() { return GAME_CONFIG.DEPTHS[this.selectedDepth].songs[this.selectedSong]; }, canUpgrade: function canUpgrade() { var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1]; return nextDepth && this.money >= nextDepth.upgradeCost; }, upgrade: function upgrade() { if (this.canUpgrade()) { var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1]; this.money -= nextDepth.upgradeCost; this.currentDepth++; // Give free first song of new depth this.ownedSongs.push({ depth: this.currentDepth, songIndex: 0 }); return true; } return false; } }; var titleScreen = game.addChild(new Container()); var levelSelectScreen = game.addChild(new Container()); var fishingScreen = game.addChild(new Container()); var resultsScreen = game.addChild(new Container()); // Initialize GameState.initOwnedSongs(); /**** * Title Screen ****/ function createTitleScreen() { var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.3 })); // Logo var logo = new Text2('BEAT FISHER', { size: 150, fill: 0xFFFFFF }); logo.anchor.set(0.5, 0.5); logo.x = GAME_CONFIG.SCREEN_CENTER_X; logo.y = 600; titleScreen.addChild(logo); var subtitle = new Text2('Rhythm Fishing Adventure', { size: 60, fill: 0x4FC3F7 }); subtitle.anchor.set(0.5, 0.5); subtitle.x = GAME_CONFIG.SCREEN_CENTER_X; subtitle.y = 700; titleScreen.addChild(subtitle); // Start button var startButton = titleScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1000 })); var startText = new Text2('START', { size: 50, fill: 0xFFFFFF }); startText.anchor.set(0.5, 0.5); startText.x = GAME_CONFIG.SCREEN_CENTER_X; startText.y = 1000; titleScreen.addChild(startText); // Tutorial button var tutorialButton = titleScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1150, tint: 0x757575 })); var tutorialText = new Text2('TUTORIAL', { size: 40, fill: 0xFFFFFF }); tutorialText.anchor.set(0.5, 0.5); tutorialText.x = GAME_CONFIG.SCREEN_CENTER_X; tutorialText.y = 1150; titleScreen.addChild(tutorialText); return { startButton: startButton, tutorialButton: tutorialButton }; } /**** * Level Select Screen ****/ function createLevelSelectScreen() { var selectBg = levelSelectScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.8 })); // Title var title = new Text2('SELECT FISHING SPOT', { size: 80, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.x = GAME_CONFIG.SCREEN_CENTER_X; title.y = 200; levelSelectScreen.addChild(title); // Money display var moneyDisplay = new Text2('Money: $0', { size: 60, fill: 0xFFD700 }); moneyDisplay.anchor.set(1, 0); moneyDisplay.x = 1900; moneyDisplay.y = 100; levelSelectScreen.addChild(moneyDisplay); // Depth tabs (will be created dynamically) var depthTabs = []; // Song display area var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 700 })); // Song navigation arrows var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 400, y: 700, tint: 0x666666 })); var leftArrowText = new Text2('<', { size: 60, fill: 0xFFFFFF }); leftArrowText.anchor.set(0.5, 0.5); leftArrowText.x = 400; leftArrowText.y = 700; levelSelectScreen.addChild(leftArrowText); var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 1648, y: 700, tint: 0x666666 })); var rightArrowText = new Text2('>', { size: 60, fill: 0xFFFFFF }); rightArrowText.anchor.set(0.5, 0.5); rightArrowText.x = 1648; rightArrowText.y = 700; levelSelectScreen.addChild(rightArrowText); // Song info (will be updated dynamically) var songTitle = new Text2('Song Title', { size: 50, fill: 0xFFFFFF }); songTitle.anchor.set(0.5, 0.5); songTitle.x = GAME_CONFIG.SCREEN_CENTER_X; songTitle.y = 650; levelSelectScreen.addChild(songTitle); var songInfo = new Text2('BPM: 120 | Duration: 2:00', { size: 30, fill: 0xCCCCCC }); songInfo.anchor.set(0.5, 0.5); songInfo.x = GAME_CONFIG.SCREEN_CENTER_X; songInfo.y = 700; levelSelectScreen.addChild(songInfo); var songEarnings = new Text2('Potential Earnings: $50-100', { size: 30, fill: 0x4CAF50 }); songEarnings.anchor.set(0.5, 0.5); songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X; songEarnings.y = 730; levelSelectScreen.addChild(songEarnings); // Play/Buy button var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 900 })); var playButtonText = new Text2('PLAY', { size: 50, fill: 0xFFFFFF }); playButtonText.anchor.set(0.5, 0.5); playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; playButtonText.y = 900; levelSelectScreen.addChild(playButtonText); // Shop button var shopButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1100, tint: 0x2e7d32 })); var shopButtonText = new Text2('UPGRADE ROD', { size: 40, fill: 0xFFFFFF }); shopButtonText.anchor.set(0.5, 0.5); shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X; shopButtonText.y = 1100; levelSelectScreen.addChild(shopButtonText); // Back button var backButton = levelSelectScreen.addChild(LK.getAsset('button', { anchorX: 0.5, anchorY: 0.5, x: 200, y: 200, tint: 0x757575 })); var backButtonText = new Text2('BACK', { size: 40, fill: 0xFFFFFF }); backButtonText.anchor.set(0.5, 0.5); backButtonText.x = 200; backButtonText.y = 200; levelSelectScreen.addChild(backButtonText); return { moneyDisplay: moneyDisplay, depthTabs: depthTabs, leftArrow: leftArrow, rightArrow: rightArrow, songTitle: songTitle, songInfo: songInfo, songEarnings: songEarnings, playButton: playButton, playButtonText: playButtonText, shopButton: shopButton, shopButtonText: shopButtonText, backButton: backButton }; } /**** * Fishing Screen ****/ function createFishingScreen() { // Water background var water = fishingScreen.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, //{3l} // Adjusted to new config if different width: 2048, // Ensure it covers screen width height: 2732 - GAME_CONFIG.WATER_SURFACE_Y, // Ensure it covers below surface alpha: 0.7 })); // Water surface var waterSurface = fishingScreen.addChild(LK.getAsset('waterSurface', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, width: 2048, alpha: 0.8 })); // Boat var boat = fishingScreen.addChild(LK.getAsset('boat', { anchorX: 0.5, anchorY: 1, // Anchor at bottom-middle of boat asset x: GAME_CONFIG.SCREEN_CENTER_X, y: GAME_CONFIG.WATER_SURFACE_Y // Boat sits on water surface })); // Single, centered hook and line var initialHookY = GAME_CONFIG.LANES[1].y; // Start hook in the middle lane // Fishing line var line = fishingScreen.addChild(LK.getAsset('fishingLine', { anchorX: 0.5, anchorY: 0, x: GAME_CONFIG.SCREEN_CENTER_X, y: GAME_CONFIG.WATER_SURFACE_Y, height: initialHookY - GAME_CONFIG.WATER_SURFACE_Y })); // Hook var hook = fishingScreen.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: initialHookY })); hook.originalY = initialHookY; // Store original Y for animation and reference // Lane labels are removed as the hook is dynamic. // UI elements var scoreText = new Text2('Score: 0', { size: 50, fill: 0xFFFFFF }); scoreText.anchor.set(0, 0); scoreText.x = 50; scoreText.y = 50; fishingScreen.addChild(scoreText); var fishText = new Text2('Fish: 0/0', { size: 40, fill: 0xFFFFFF }); fishText.anchor.set(0, 0); fishText.x = 50; fishText.y = 120; fishingScreen.addChild(fishText); var comboText = new Text2('Combo: 0', { size: 40, fill: 0xFF9800 }); comboText.anchor.set(0, 0); comboText.x = 50; comboText.y = 180; // Corrected typo from y: 180 fishingScreen.addChild(comboText); // Song progress - Re-added as it's used by updateFishingUI var progressText = new Text2('0:00 / 0:00', { size: 40, fill: 0x4FC3F7 }); progressText.anchor.set(1, 0); // Anchor top-right progressText.x = 2048 - 50; // Position on the top-right side progressText.y = 50; fishingScreen.addChild(progressText); // Hold instruction Text var holdText = new Text2('TAP: Normal Fish | HOLD: Golden Fish', { size: 35, fill: 0xFFD700, // Golden color for emphasis alpha: 0.8 }); holdText.anchor.set(0.5, 0); // Anchor top-center holdText.x = GAME_CONFIG.SCREEN_CENTER_X; holdText.y = GAME_CONFIG.LANES[GAME_CONFIG.LANES.length - 1].y + 100; // Below the last lane (now matches new vertical spread) fishingScreen.addChild(holdText); return { boat: boat, hook: hook, // Single hook line: line, // Single line scoreText: scoreText, fishText: fishText, comboText: comboText, progressText: progressText }; } /**** * Initialize Screen Elements ****/ var titleElements = createTitleScreen(); var levelSelectElements = createLevelSelectScreen(); var fishingElements = createFishingScreen(); // Feedback indicators are now created on-demand by the showFeedback function. // The global feedbackIndicators object is no longer needed. // Game variables var fishArray = []; /**** * Input State and Helpers for Fishing ****/ var inputState = { touching: false, // Is the screen currently being touched? touchLane: -1, // Which lane was the touch initiated in? (0, 1, 2) touchStartTime: 0, // Timestamp of when the touch started (LK.ticks based) holdFish: null // Reference to a fish if a hold action is being performed on it }; // getTouchLane function is removed as player input is now tied to the single dynamic hook, // not specific screen regions for lanes. // Shows feedback (perfect, good, miss) at the specified lane function showFeedback(type, laneIndex) { var feedbackY = GAME_CONFIG.LANES[laneIndex].y; var indicator = new FeedbackIndicator(type); // Creates a new indicator e.g. FeedbackIndicator('perfect') // Position feedback at the single hook's X coordinate and the fish's lane Y indicator.x = fishingElements.hook.x; // Use the single hook's X indicator.y = feedbackY; // Feedback appears at the fish's lane Y fishingScreen.addChild(indicator); indicator.show(); // Triggers the animation and self-destruction } // Animates the hook in a specific lane after a catch attempt // Animates the single hook after a catch attempt function animateHookCatch() { var hook = fishingElements.hook; // We need a stable originalY. The hook.originalY might change if we re-assign it during tweens. // Let's use the target Y of the current fish lane for the "resting" position after animation. var restingY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; // Quick bobbing animation for the single hook tween(hook, { y: restingY - 30 }, { duration: 150, easing: tween.easeOut, onFinish: function onFinish() { tween(hook, { y: restingY }, { duration: 150, easing: tween.easeIn, onFinish: function onFinish() { // Ensure originalY reflects the current target lane after animation. hook.originalY = restingY; } }); } }); } // Handles input specifically for the fishing screen (down and up events) function handleFishingInput(x, y, isDown) { if (!GameState.gameActive) return; // With a single dynamic hook, any touch interacts with the hook's current/target lane. // The y-coordinate of the touch is less important for lane determination. var interactionLane = GameState.hookTargetLaneIndex; // The lane the hook is currently targeting. var currentTime = LK.ticks * (1000 / 60); // Current time in ms if (isDown) { // Touch started inputState.touching = true; inputState.touchLane = interactionLane; // Store the lane the hook was targeting at touch start inputState.touchStartTime = currentTime; inputState.holdFish = null; // Reset any previous hold target // Check if this touch might be starting a hold on a holdable fish in the hook's target lane var hookX = fishingElements.hook.x; for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (fish.lane === interactionLane && fish.isHoldFish && !fish.caught) { var distanceToHook = Math.abs(fish.x - hookX); // Check if the fish is close enough to the hook to be considered for a hold if (distanceToHook < GAME_CONFIG.MISS_WINDOW + 20) { // Slightly larger window for initiating hold inputState.holdFish = fish; // Mark this fish as the potential target of a hold break; } } } } else { // Touch ended // Check if this 'up' corresponds to an active 'down' for the same target lane if (inputState.touching && inputState.touchLane === interactionLane) { var holdDuration = currentTime - inputState.touchStartTime; var isHoldAttempt = holdDuration > 200; // Minimum duration for a "hold" checkCatch(interactionLane, isHoldAttempt, holdDuration); } inputState.touching = false; // Reset touching state inputState.holdFish = null; // Reset hold target } } /**** * Screen Management ****/ function showScreen(screenName) { titleScreen.visible = false; levelSelectScreen.visible = false; fishingScreen.visible = false; resultsScreen.visible = false; GameState.currentScreen = screenName; switch (screenName) { case 'title': titleScreen.visible = true; break; case 'levelSelect': levelSelectScreen.visible = true; updateLevelSelectScreen(); break; case 'fishing': fishingScreen.visible = true; startFishingSession(); break; case 'results': resultsScreen.visible = true; break; } } /**** * Level Select Logic ****/ function updateLevelSelectScreen() { var elements = levelSelectElements; // Update money display elements.moneyDisplay.setText('Money: $' + GameState.money); // Create depth tabs createDepthTabs(); // Update song display updateSongDisplay(); // Update shop button updateShopButton(); } function createDepthTabs() { // Clear existing tabs levelSelectElements.depthTabs.forEach(function (tab) { if (tab.container) tab.container.destroy(); }); levelSelectElements.depthTabs = []; // Create tabs for unlocked depths for (var i = 0; i <= GameState.currentDepth; i++) { var depth = GAME_CONFIG.DEPTHS[i]; var isSelected = i === GameState.selectedDepth; var tabContainer = levelSelectScreen.addChild(new Container()); var tab = tabContainer.addChild(LK.getAsset('depthTab', { anchorX: 0.5, anchorY: 0.5, x: 300 + i * 220, y: 400, tint: isSelected ? 0x1976d2 : 0x455a64 })); var tabText = new Text2(depth.name.split(' ')[0], { size: 30, fill: 0xFFFFFF }); tabText.anchor.set(0.5, 0.5); tabText.x = 300 + i * 220; tabText.y = 400; tabContainer.addChild(tabText); levelSelectElements.depthTabs.push({ container: tabContainer, tab: tab, depthIndex: i }); } } function updateSongDisplay() { var elements = levelSelectElements; var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth]; var song = depth.songs[GameState.selectedSong]; var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong); // Update song info elements.songTitle.setText(song.name); elements.songInfo.setText('BPM: ' + song.bpm + ' | Duration: ' + formatTime(song.duration)); // Calculate potential earnings var minEarnings = Math.floor(depth.fishValue * 20); // Conservative estimate var maxEarnings = Math.floor(depth.fishValue * 60); // With combos and rare fish elements.songEarnings.setText('Potential Earnings: $' + minEarnings + '-$' + maxEarnings); // Update play/buy button if (owned) { elements.playButtonText.setText('PLAY'); elements.playButton.tint = 0x1976d2; } else { elements.playButtonText.setText('BUY ($' + song.cost + ')'); elements.playButton.tint = GameState.money >= song.cost ? 0x2e7d32 : 0x666666; } // Update arrow states elements.leftArrow.tint = GameState.selectedSong > 0 ? 0x1976d2 : 0x666666; elements.rightArrow.tint = GameState.selectedSong < depth.songs.length - 1 ? 0x1976d2 : 0x666666; } function updateShopButton() { var elements = levelSelectElements; var canUpgrade = GameState.canUpgrade(); var nextDepth = GAME_CONFIG.DEPTHS[GameState.currentDepth + 1]; if (nextDepth) { elements.shopButtonText.setText('UPGRADE ROD ($' + nextDepth.upgradeCost + ')'); elements.shopButton.tint = canUpgrade ? 0x2e7d32 : 0x666666; } else { elements.shopButtonText.setText('MAX DEPTH REACHED'); elements.shopButton.tint = 0x666666; } } function formatTime(ms) { var seconds = Math.floor(ms / 1000); var minutes = Math.floor(seconds / 60); seconds = seconds % 60; return minutes + ':' + (seconds < 10 ? '0' : '') + seconds; } /**** * Fishing Game Logic ****/ function startFishingSession() { // Reset session state GameState.sessionScore = 0; GameState.sessionFishCaught = 0; GameState.sessionFishSpawned = 0; GameState.combo = 0; GameState.maxCombo = 0; GameState.gameActive = true; GameState.songStartTime = 0; GameState.lastBeatTime = 0; GameState.beatCount = 0; // Clear any existing fish fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; // Reset pattern generator for new session PatternGenerator.reset(); // Start music LK.playMusic('rhythmTrack'); } function spawnFish() { var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Skip spawn if pattern says no double spawns if (pattern.doubleSpawnChance === 0) { // Check if any fish are too close to spawn position for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (Math.abs(fish.x - GAME_CONFIG.SCREEN_CENTER_X) < PatternGenerator.minDistanceBetweenFish) { return; // Skip this spawn } } } // Use pattern generator to choose lane var laneIndex = PatternGenerator.getNextLane(); var targetLane = GAME_CONFIG.LANES[laneIndex]; // Determine if this is a hold fish var isHoldFish = Math.random() < (pattern.holdFishChance || 0); // Determine fish type and value var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; // Rare fish are more valuable } else if (GameState.selectedDepth >= 2 && rand < 0.3) { // Example: deep fish appear more in deeper levels fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { // Example: medium fish fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; // Common fish fishValue = depthConfig.fishValue; } // Bonus value for hold fish if (isHoldFish) { fishValue = Math.floor(fishValue * 1.5); // Hold fish are worth more } var fishSpeed = Math.random() < 0.5 ? depthConfig.fishSpeed : -depthConfig.fishSpeed; // Create fish var fish = new Fish(fishType, fishValue, fishSpeed, laneIndex, // Pass lane index isHoldFish // Pass hold status ); fish.x = fish.speed > 0 ? -150 : 2048 + 150; // Start off-screen fish.y = targetLane.y; // Set Y to the chosen lane's Y fishArray.push(fish); fishingScreen.addChild(fish); GameState.sessionFishSpawned++; // Register spawn position for pattern tracking PatternGenerator.registerSpawn(GAME_CONFIG.SCREEN_CENTER_X); } function checkCatch(fishLane, isHoldAction, holdDurationMs) { var hookX = fishingElements.hook.x; // Use the single hook's X position var closestFishInLane = null; var closestDistance = Infinity; // Find the closest fish in the target fishLane that hasn't been caught for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (!fish.caught && fish.lane === fishLane) { var distance = Math.abs(fish.x - hookX); if (distance < closestDistance) { closestDistance = distance; closestFishInLane = fish; } } } if (!closestFishInLane) { showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // --- Hold Fish Logic --- if (closestFishInLane.isHoldFish) { if (!isHoldAction) { // Tapped a hold fish showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } if (holdDurationMs < closestFishInLane.requiredHoldTime) { // Held, but not long enough showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } } else { // Normal Fish if (isHoldAction) { // Held a normal fish (counts as miss/mistake) showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } } // --- End Hold Fish Logic --- var points = 0; var multiplier = Math.max(1, Math.floor(GameState.combo / 10) + 1); if (closestDistance < GAME_CONFIG.PERFECT_WINDOW) { points = closestFishInLane.value * 2 * multiplier; showFeedback('perfect', fishLane); GameState.combo++; } else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) { points = closestFishInLane.value * multiplier; showFeedback('good', fishLane); GameState.combo++; } else if (closestDistance < GAME_CONFIG.MISS_WINDOW) { points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier)); showFeedback('good', fishLane); GameState.combo++; } else { showFeedback('miss', fishLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // Successfully caught fish closestFishInLane.catchFish(); var fishIndex = fishArray.indexOf(closestFishInLane); if (fishIndex > -1) { fishArray.splice(fishIndex, 1); } GameState.sessionScore += points; GameState.money += points; GameState.sessionFishCaught++; GameState.totalFishCaught++; GameState.maxCombo = Math.max(GameState.maxCombo, GameState.combo); LK.getSound('catch').play(); animateHookCatch(); // Call parameterless animateHookCatch for the single hook } // Note: The old animateHookCatch function that was defined right after checkCatch // is now a global helper: animateHookCatch(laneIndex), defined earlier. // We remove the old local one if it existed here by not re-inserting it. function updateFishingUI() { var elements = fishingElements; elements.scoreText.setText('Score: ' + GameState.sessionScore); elements.fishText.setText('Fish: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned); elements.comboText.setText('Combo: ' + GameState.combo); // Update progress if (GameState.songStartTime > 0) { var currentTime = LK.ticks * (1000 / 60); var elapsed = currentTime - GameState.songStartTime; var songConfig = GameState.getCurrentSongConfig(); elements.progressText.setText(formatTime(elapsed) + ' / ' + formatTime(songConfig.duration)); } } function endFishingSession() { GameState.gameActive = false; // Clear fish fishArray.forEach(function (fish) { fish.destroy(); }); fishArray = []; // Create results screen createResultsScreen(); showScreen('results'); } function createResultsScreen() { // Clear previous results resultsScreen.removeChildren(); var resultsBg = resultsScreen.addChild(LK.getAsset('screenBackground', { x: 0, y: 0, alpha: 0.9 })); var title = new Text2('Fishing Complete!', { size: 100, fill: 0xFFFFFF }); title.anchor.set(0.5, 0.5); title.x = GAME_CONFIG.SCREEN_CENTER_X; title.y = 400; resultsScreen.addChild(title); var scoreResult = new Text2('Score: ' + GameState.sessionScore, { size: 70, fill: 0xFFD700 }); scoreResult.anchor.set(0.5, 0.5); scoreResult.x = GAME_CONFIG.SCREEN_CENTER_X; scoreResult.y = 550; resultsScreen.addChild(scoreResult); var fishResult = new Text2('Fish Caught: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned, { size: 50, fill: 0xFFFFFF }); fishResult.anchor.set(0.5, 0.5); fishResult.x = GAME_CONFIG.SCREEN_CENTER_X; fishResult.y = 650; resultsScreen.addChild(fishResult); var comboResult = new Text2('Max Combo: ' + GameState.maxCombo, { size: 50, fill: 0xFF9800 }); comboResult.anchor.set(0.5, 0.5); comboResult.x = GAME_CONFIG.SCREEN_CENTER_X; comboResult.y = 750; resultsScreen.addChild(comboResult); var moneyEarned = new Text2('Money Earned: $' + GameState.sessionScore, { size: 50, fill: 0x4CAF50 }); moneyEarned.anchor.set(0.5, 0.5); moneyEarned.x = GAME_CONFIG.SCREEN_CENTER_X; moneyEarned.y = 850; resultsScreen.addChild(moneyEarned); // Accuracy var accuracy = GameState.sessionFishSpawned > 0 ? Math.round(GameState.sessionFishCaught / GameState.sessionFishSpawned * 100) : 0; var accuracyResult = new Text2('Accuracy: ' + accuracy + '%', { size: 50, fill: 0x2196F3 }); accuracyResult.anchor.set(0.5, 0.5); accuracyResult.x = GAME_CONFIG.SCREEN_CENTER_X; accuracyResult.y = 950; resultsScreen.addChild(accuracyResult); // Continue button var continueButton = resultsScreen.addChild(LK.getAsset('bigButton', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: 1200 })); var continueText = new Text2('CONTINUE', { size: 50, fill: 0xFFFFFF }); continueText.anchor.set(0.5, 0.5); continueText.x = GAME_CONFIG.SCREEN_CENTER_X; continueText.y = 1200; resultsScreen.addChild(continueText); // Fade in resultsScreen.alpha = 0; tween(resultsScreen, { alpha: 1 }, { duration: 500, easing: tween.easeOut }); } /**** * Input Handling ****/ game.down = function (x, y, obj) { LK.getSound('buttonClick').play(); switch (GameState.currentScreen) { case 'title': // Check if click is within start button bounds var startButton = titleElements.startButton; if (x >= startButton.x - startButton.width / 2 && x <= startButton.x + startButton.width / 2 && y >= startButton.y - startButton.height / 2 && y <= startButton.y + startButton.height / 2) { showScreen('levelSelect'); } // Check if click is within tutorial button bounds var tutorialButton = titleElements.tutorialButton; if (x >= tutorialButton.x - tutorialButton.width / 2 && x <= tutorialButton.x + tutorialButton.width / 2 && y >= tutorialButton.y - tutorialButton.height / 2 && y <= tutorialButton.y + tutorialButton.height / 2) { // TODO: Show tutorial } break; case 'levelSelect': handleLevelSelectInput(x, y); break; case 'fishing': handleFishingInput(x, y, true); // true for isDown break; case 'results': showScreen('levelSelect'); break; } }; function handleLevelSelectInput(x, y) { var elements = levelSelectElements; // Check depth tabs elements.depthTabs.forEach(function (tab) { var tabAsset = tab.tab; if (x >= tabAsset.x - tabAsset.width / 2 && x <= tabAsset.x + tabAsset.width / 2 && y >= tabAsset.y - tabAsset.height / 2 && y <= tabAsset.y + tabAsset.height / 2) { GameState.selectedDepth = tab.depthIndex; GameState.selectedSong = 0; // Reset to first song updateLevelSelectScreen(); } }); // Check song navigation var leftArrow = elements.leftArrow; if (x >= leftArrow.x - leftArrow.width / 2 && x <= leftArrow.x + leftArrow.width / 2 && y >= leftArrow.y - leftArrow.height / 2 && y <= leftArrow.y + leftArrow.height / 2 && GameState.selectedSong > 0) { GameState.selectedSong--; updateSongDisplay(); } var rightArrow = elements.rightArrow; if (x >= rightArrow.x - rightArrow.width / 2 && x <= rightArrow.x + rightArrow.width / 2 && y >= rightArrow.y - rightArrow.height / 2 && y <= rightArrow.y + rightArrow.height / 2) { var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth]; if (GameState.selectedSong < depth.songs.length - 1) { GameState.selectedSong++; updateSongDisplay(); } } // Check play/buy button var playButton = elements.playButton; if (x >= playButton.x - playButton.width / 2 && x <= playButton.x + playButton.width / 2 && y >= playButton.y - playButton.height / 2 && y <= playButton.y + playButton.height / 2) { var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong); if (owned) { showScreen('fishing'); } else { // Try to buy song if (GameState.buySong(GameState.selectedDepth, GameState.selectedSong)) { updateLevelSelectScreen(); } } } // Check shop button var shopButton = elements.shopButton; if (x >= shopButton.x - shopButton.width / 2 && x <= shopButton.x + shopButton.width / 2 && y >= shopButton.y - shopButton.height / 2 && y <= shopButton.y + shopButton.height / 2) { if (GameState.upgrade()) { LK.getSound('upgrade').play(); updateLevelSelectScreen(); } } // Check back button var backButton = elements.backButton; if (x >= backButton.x - backButton.width / 2 && x <= backButton.x + backButton.width / 2 && y >= backButton.y - backButton.height / 2 && y <= backButton.y + backButton.height / 2) { showScreen('title'); } } /**** * Main Game Loop ****/ game.update = function () { if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) { return; } var currentTime = LK.ticks * (1000 / 60); // Initialize game timer if (GameState.songStartTime === 0) { GameState.songStartTime = currentTime; } var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; var beatInterval = 60000 / songConfig.bpm; var spawnInterval = beatInterval * pattern.beatsPerFish; // Check song end if (currentTime - GameState.songStartTime >= songConfig.duration) { endFishingSession(); return; } // Spawn fish on beat if (currentTime - GameState.lastBeatTime >= spawnInterval) { GameState.lastBeatTime = currentTime; GameState.beatCount++; spawnFish(); } // Dynamic Hook Movement Logic var approachingFish = null; var minDistanceToCenter = Infinity; for (var i = 0; i < fishArray.length; i++) { var f = fishArray[i]; if (!f.caught) { var distanceToHookX = Math.abs(f.x - fishingElements.hook.x); // Consider fish that are moving towards the hook OR are very close to it already var isApproachingOrAtHook = f.speed > 0 && f.x < fishingElements.hook.x || f.speed < 0 && f.x > fishingElements.hook.x || distanceToHookX < GAME_CONFIG.MISS_WINDOW * 2; if (isApproachingOrAtHook && distanceToHookX < minDistanceToCenter) { minDistanceToCenter = distanceToHookX; approachingFish = f; } } } var targetLaneY; if (approachingFish) { if (GameState.hookTargetLaneIndex !== approachingFish.lane) { GameState.hookTargetLaneIndex = approachingFish.lane; } targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; } else { // No fish approaching, hook returns/stays at middle lane (or last known target) // For simplicity, let's use the last known/default target index. targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y; } // Tween hook and line to the target Y if not already there or already tweening // A more robust solution would check if a tween is active on the hook. // For now, we'll just update if the current Y is different. if (fishingElements.hook.y !== targetLaneY) { tween(fishingElements.hook, { y: targetLaneY }, { duration: 150, easing: tween.easeOut }); tween(fishingElements.line, { height: targetLaneY - GAME_CONFIG.WATER_SURFACE_Y }, { duration: 150, easing: tween.easeOut }); fishingElements.hook.originalY = targetLaneY; // Update reference for animations } // Update fish for (var i = fishArray.length - 1; i >= 0; i--) { var fish = fishArray[i]; fish.update(); // Update hold fish visual state if being held if (fish.isHoldFish && inputState.holdFish === fish && inputState.touching) { var currentHoldDuration = currentTime - inputState.touchStartTime; if (currentHoldDuration >= fish.requiredHoldTime && !fish.holdStarted) { fish.holdStarted = true; } } else if (fish.isHoldFish && fish.holdStarted && !(inputState.holdFish === fish && inputState.touching)) { fish.holdStarted = false; } // Remove off-screen fish (if not caught) if (!fish.caught && (fish.x < -150 || fish.x > 2048 + 150)) { fish.destroy(); fishArray.splice(i, 1); } } // Update UI updateFishingUI(); }; // Initialize game showScreen('title');
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var FeedbackIndicator = Container.expand(function (type) {
var self = Container.call(this);
var indicator = self.attachAsset(type + 'Indicator', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
self.show = function () {
indicator.alpha = 1;
indicator.scaleX = 0.5;
indicator.scaleY = 0.5;
tween(indicator, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 600,
easing: tween.easeOut
});
};
return self;
});
/****
* Title Screen
****/
var Fish = Container.expand(function (type, value, speed, lane, isHoldFish) {
var self = Container.call(this);
var assetName = type + 'Fish';
var fishGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
self.type = type;
self.value = value;
self.speed = speed;
self.lane = lane; // Index of the lane (0, 1, or 2)
self.isHoldFish = isHoldFish || false;
self.caught = false;
self.isSpecial = type === 'rare'; // For shimmer effect
self.shimmerTime = 0;
// Hold fish specific properties
if (self.isHoldFish) {
fishGraphics.tint = 0xFFD700; // Golden tint for hold fish
self.holdStarted = false; // True if player successfully holds long enough (visual cue potential)
self.requiredHoldTime = 800; // 800ms hold required to "prime" the catch for a hold fish
}
self.update = function () {
if (!self.caught) {
self.x += self.speed;
if (self.isSpecial) {
// Shimmer effect for rare fish
self.shimmerTime += 0.1;
fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2;
} else if (!self.isHoldFish) {
// Reset alpha if not special and not hold (hold has tint)
fishGraphics.alpha = 1.0;
}
}
};
self.catchFish = function () {
self.caught = true;
// Animation to boat
tween(self, {
y: GAME_CONFIG.BOAT_Y,
// Target Y: boat position
x: GAME_CONFIG.SCREEN_CENTER_X,
//{r} // Target X: center of screen (boat position)
scaleX: 0.5,
scaleY: 0.5,
alpha: 0
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
self.destroy();
}
});
};
return self;
});
/****
* Initialize Game
****/
/****
* Screen Containers
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
/****
* Pattern Generation System
****/
// If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up.
var PatternGenerator = {
lastLane: -1,
lastSpawnX: -1000,
minDistanceBetweenFish: 300,
// Minimum X distance between fish
getNextLane: function getNextLane() {
if (this.lastLane === -1) {
// First fish, start in middle lane
this.lastLane = 1;
return 1;
}
// Prefer staying in same lane or moving to adjacent lane
var possibleLanes = [this.lastLane];
// Add adjacent lanes
if (this.lastLane > 0) possibleLanes.push(this.lastLane - 1);
if (this.lastLane < 2) possibleLanes.push(this.lastLane + 1);
// 70% chance to stay in same/adjacent lane
if (Math.random() < 0.7) {
this.lastLane = possibleLanes[Math.floor(Math.random() * possibleLanes.length)];
} else {
// 30% chance for any lane
this.lastLane = Math.floor(Math.random() * 3);
}
return this.lastLane;
},
canSpawnFish: function canSpawnFish(fishX) {
// Check if enough distance from last spawn
var distance = Math.abs(fishX - this.lastSpawnX);
return distance >= this.minDistanceBetweenFish;
},
registerSpawn: function registerSpawn(fishX) {
this.lastSpawnX = fishX;
},
reset: function reset() {
this.lastLane = -1;
this.lastSpawnX = -1000;
}
};
/****
* Game Configuration
****/
game.up = function (x, y, obj) {
// Note: We don't play buttonClick sound on 'up' typically, only on 'down'.
switch (GameState.currentScreen) {
case 'title':
// title screen up actions (if any)
break;
case 'levelSelect':
// level select screen up actions (if any, usually 'down' is enough for buttons)
break;
case 'fishing':
handleFishingInput(x, y, false); // false for isUp
break;
case 'results':
// results screen up actions (if any)
break;
}
};
var GAME_CONFIG = {
SCREEN_CENTER_X: 1024,
SCREEN_CENTER_Y: 900,
// Adjusted, though less critical with lanes
BOAT_Y: 573,
// Original 300 + 273 (maintains original offset from water surface)
WATER_SURFACE_Y: 623,
// Original 350 + 273 (273 is 10% of 2732 screen height, rounded down)
// 3 Lane System
// Vertically spread so that the second lowest hook (lane 1) is at the center of the screen
LANES: [{
y: 723,
// Top lane: center - 600
name: "shallow"
}, {
y: 1366,
// Middle lane: center of screen (1366)
name: "medium"
}, {
y: 2009,
// Bottom lane: center + 643
name: "deep"
}],
// Timing windows
PERFECT_WINDOW: 40,
GOOD_WINDOW: 80,
MISS_WINDOW: 120,
// Depth levels - much lower money values! (Upgrade costs and song costs might need re-balancing with new mechanics)
DEPTHS: [{
level: 1,
name: "Shallow Waters",
fishSpeed: 6,
fishValue: 1,
upgradeCost: 0,
// Starting depth
songs: [{
name: "Gentle Waves",
bpm: 100,
duration: 60000,
pattern: "simple",
cost: 0
}, {
name: "Morning Tide",
bpm: 110,
duration: 75000,
pattern: "simple",
cost: 50
}]
}, {
level: 2,
name: "Mid Waters",
fishSpeed: 7,
fishValue: 3,
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: 6,
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: 12,
upgradeCost: 1000,
songs: [{
name: "Leviathan",
bpm: 160,
duration: 150000,
pattern: "expert",
cost: 0
}, {
name: "Deep Trench",
bpm: 170,
duration: 180000,
pattern: "expert",
cost: 600
}]
}],
// Updated patterns with hold fish - reduced frequency for playability
PATTERNS: {
simple: {
beatsPerFish: 2,
// Increased from 1 - fish every 2 beats
doubleSpawnChance: 0,
// No double spawns in simple
rareSpawnChance: 0.02,
holdFishChance: 0.05 // Reduced from 0.1
},
medium: {
beatsPerFish: 1.5,
// Increased from 0.75
doubleSpawnChance: 0,
// No double spawns
rareSpawnChance: 0.05,
holdFishChance: 0.1 // Reduced from 0.2
},
complex: {
beatsPerFish: 1,
// Increased from 0.5
doubleSpawnChance: 0,
// No double spawns
rareSpawnChance: 0.08,
holdFishChance: 0.15 // Reduced from 0.3
},
expert: {
beatsPerFish: 0.75,
// Increased from 0.25
doubleSpawnChance: 0,
// No double spawns
rareSpawnChance: 0.12,
holdFishChance: 0.2 // Reduced from 0.4
}
}
};
/****
* Game State Management
****/
var GameState = {
// Game flow
currentScreen: 'title',
// 'title', 'levelSelect', 'fishing', 'results'
// Player progression
currentDepth: 0,
money: 0,
totalFishCaught: 0,
ownedSongs: [],
// Array of {depth, songIndex} objects
// Level selection
selectedDepth: 0,
selectedSong: 0,
// Current session
sessionScore: 0,
sessionFishCaught: 0,
sessionFishSpawned: 0,
combo: 0,
maxCombo: 0,
// Game state
gameActive: false,
songStartTime: 0,
lastBeatTime: 0,
beatCount: 0,
hookTargetLaneIndex: 1,
// Start with hook targeting the middle lane (index 1)
// Initialize owned songs (first song of each unlocked depth is free)
initOwnedSongs: function initOwnedSongs() {
this.ownedSongs = [];
for (var i = 0; i <= this.currentDepth; i++) {
this.ownedSongs.push({
depth: i,
songIndex: 0
});
}
},
hasSong: function hasSong(depth, songIndex) {
return this.ownedSongs.some(function (song) {
return song.depth === depth && song.songIndex === songIndex;
});
},
buySong: function buySong(depth, songIndex) {
var song = GAME_CONFIG.DEPTHS[depth].songs[songIndex];
if (this.money >= song.cost && !this.hasSong(depth, songIndex)) {
this.money -= song.cost;
this.ownedSongs.push({
depth: depth,
songIndex: songIndex
});
return true;
}
return false;
},
getCurrentDepthConfig: function getCurrentDepthConfig() {
return GAME_CONFIG.DEPTHS[this.selectedDepth];
},
getCurrentSongConfig: function getCurrentSongConfig() {
return GAME_CONFIG.DEPTHS[this.selectedDepth].songs[this.selectedSong];
},
canUpgrade: function canUpgrade() {
var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1];
return nextDepth && this.money >= nextDepth.upgradeCost;
},
upgrade: function upgrade() {
if (this.canUpgrade()) {
var nextDepth = GAME_CONFIG.DEPTHS[this.currentDepth + 1];
this.money -= nextDepth.upgradeCost;
this.currentDepth++;
// Give free first song of new depth
this.ownedSongs.push({
depth: this.currentDepth,
songIndex: 0
});
return true;
}
return false;
}
};
var titleScreen = game.addChild(new Container());
var levelSelectScreen = game.addChild(new Container());
var fishingScreen = game.addChild(new Container());
var resultsScreen = game.addChild(new Container());
// Initialize
GameState.initOwnedSongs();
/****
* Title Screen
****/
function createTitleScreen() {
var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 0.3
}));
// Logo
var logo = new Text2('BEAT FISHER', {
size: 150,
fill: 0xFFFFFF
});
logo.anchor.set(0.5, 0.5);
logo.x = GAME_CONFIG.SCREEN_CENTER_X;
logo.y = 600;
titleScreen.addChild(logo);
var subtitle = new Text2('Rhythm Fishing Adventure', {
size: 60,
fill: 0x4FC3F7
});
subtitle.anchor.set(0.5, 0.5);
subtitle.x = GAME_CONFIG.SCREEN_CENTER_X;
subtitle.y = 700;
titleScreen.addChild(subtitle);
// Start button
var startButton = titleScreen.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1000
}));
var startText = new Text2('START', {
size: 50,
fill: 0xFFFFFF
});
startText.anchor.set(0.5, 0.5);
startText.x = GAME_CONFIG.SCREEN_CENTER_X;
startText.y = 1000;
titleScreen.addChild(startText);
// Tutorial button
var tutorialButton = titleScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1150,
tint: 0x757575
}));
var tutorialText = new Text2('TUTORIAL', {
size: 40,
fill: 0xFFFFFF
});
tutorialText.anchor.set(0.5, 0.5);
tutorialText.x = GAME_CONFIG.SCREEN_CENTER_X;
tutorialText.y = 1150;
titleScreen.addChild(tutorialText);
return {
startButton: startButton,
tutorialButton: tutorialButton
};
}
/****
* Level Select Screen
****/
function createLevelSelectScreen() {
var selectBg = levelSelectScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 0.8
}));
// Title
var title = new Text2('SELECT FISHING SPOT', {
size: 80,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.x = GAME_CONFIG.SCREEN_CENTER_X;
title.y = 200;
levelSelectScreen.addChild(title);
// Money display
var moneyDisplay = new Text2('Money: $0', {
size: 60,
fill: 0xFFD700
});
moneyDisplay.anchor.set(1, 0);
moneyDisplay.x = 1900;
moneyDisplay.y = 100;
levelSelectScreen.addChild(moneyDisplay);
// Depth tabs (will be created dynamically)
var depthTabs = [];
// Song display area
var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 700
}));
// Song navigation arrows
var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 400,
y: 700,
tint: 0x666666
}));
var leftArrowText = new Text2('<', {
size: 60,
fill: 0xFFFFFF
});
leftArrowText.anchor.set(0.5, 0.5);
leftArrowText.x = 400;
leftArrowText.y = 700;
levelSelectScreen.addChild(leftArrowText);
var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 1648,
y: 700,
tint: 0x666666
}));
var rightArrowText = new Text2('>', {
size: 60,
fill: 0xFFFFFF
});
rightArrowText.anchor.set(0.5, 0.5);
rightArrowText.x = 1648;
rightArrowText.y = 700;
levelSelectScreen.addChild(rightArrowText);
// Song info (will be updated dynamically)
var songTitle = new Text2('Song Title', {
size: 50,
fill: 0xFFFFFF
});
songTitle.anchor.set(0.5, 0.5);
songTitle.x = GAME_CONFIG.SCREEN_CENTER_X;
songTitle.y = 650;
levelSelectScreen.addChild(songTitle);
var songInfo = new Text2('BPM: 120 | Duration: 2:00', {
size: 30,
fill: 0xCCCCCC
});
songInfo.anchor.set(0.5, 0.5);
songInfo.x = GAME_CONFIG.SCREEN_CENTER_X;
songInfo.y = 700;
levelSelectScreen.addChild(songInfo);
var songEarnings = new Text2('Potential Earnings: $50-100', {
size: 30,
fill: 0x4CAF50
});
songEarnings.anchor.set(0.5, 0.5);
songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X;
songEarnings.y = 730;
levelSelectScreen.addChild(songEarnings);
// Play/Buy button
var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 900
}));
var playButtonText = new Text2('PLAY', {
size: 50,
fill: 0xFFFFFF
});
playButtonText.anchor.set(0.5, 0.5);
playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
playButtonText.y = 900;
levelSelectScreen.addChild(playButtonText);
// Shop button
var shopButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1100,
tint: 0x2e7d32
}));
var shopButtonText = new Text2('UPGRADE ROD', {
size: 40,
fill: 0xFFFFFF
});
shopButtonText.anchor.set(0.5, 0.5);
shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
shopButtonText.y = 1100;
levelSelectScreen.addChild(shopButtonText);
// Back button
var backButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 200,
y: 200,
tint: 0x757575
}));
var backButtonText = new Text2('BACK', {
size: 40,
fill: 0xFFFFFF
});
backButtonText.anchor.set(0.5, 0.5);
backButtonText.x = 200;
backButtonText.y = 200;
levelSelectScreen.addChild(backButtonText);
return {
moneyDisplay: moneyDisplay,
depthTabs: depthTabs,
leftArrow: leftArrow,
rightArrow: rightArrow,
songTitle: songTitle,
songInfo: songInfo,
songEarnings: songEarnings,
playButton: playButton,
playButtonText: playButtonText,
shopButton: shopButton,
shopButtonText: shopButtonText,
backButton: backButton
};
}
/****
* Fishing Screen
****/
function createFishingScreen() {
// Water background
var water = fishingScreen.addChild(LK.getAsset('water', {
x: 0,
y: GAME_CONFIG.WATER_SURFACE_Y,
//{3l} // Adjusted to new config if different
width: 2048,
// Ensure it covers screen width
height: 2732 - GAME_CONFIG.WATER_SURFACE_Y,
// Ensure it covers below surface
alpha: 0.7
}));
// Water surface
var waterSurface = fishingScreen.addChild(LK.getAsset('waterSurface', {
x: 0,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: 2048,
alpha: 0.8
}));
// Boat
var boat = fishingScreen.addChild(LK.getAsset('boat', {
anchorX: 0.5,
anchorY: 1,
// Anchor at bottom-middle of boat asset
x: GAME_CONFIG.SCREEN_CENTER_X,
y: GAME_CONFIG.WATER_SURFACE_Y // Boat sits on water surface
}));
// Single, centered hook and line
var initialHookY = GAME_CONFIG.LANES[1].y; // Start hook in the middle lane
// Fishing line
var line = fishingScreen.addChild(LK.getAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: GAME_CONFIG.WATER_SURFACE_Y,
height: initialHookY - GAME_CONFIG.WATER_SURFACE_Y
}));
// Hook
var hook = fishingScreen.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: initialHookY
}));
hook.originalY = initialHookY; // Store original Y for animation and reference
// Lane labels are removed as the hook is dynamic.
// UI elements
var scoreText = new Text2('Score: 0', {
size: 50,
fill: 0xFFFFFF
});
scoreText.anchor.set(0, 0);
scoreText.x = 50;
scoreText.y = 50;
fishingScreen.addChild(scoreText);
var fishText = new Text2('Fish: 0/0', {
size: 40,
fill: 0xFFFFFF
});
fishText.anchor.set(0, 0);
fishText.x = 50;
fishText.y = 120;
fishingScreen.addChild(fishText);
var comboText = new Text2('Combo: 0', {
size: 40,
fill: 0xFF9800
});
comboText.anchor.set(0, 0);
comboText.x = 50;
comboText.y = 180; // Corrected typo from y: 180
fishingScreen.addChild(comboText);
// Song progress - Re-added as it's used by updateFishingUI
var progressText = new Text2('0:00 / 0:00', {
size: 40,
fill: 0x4FC3F7
});
progressText.anchor.set(1, 0); // Anchor top-right
progressText.x = 2048 - 50; // Position on the top-right side
progressText.y = 50;
fishingScreen.addChild(progressText);
// Hold instruction Text
var holdText = new Text2('TAP: Normal Fish | HOLD: Golden Fish', {
size: 35,
fill: 0xFFD700,
// Golden color for emphasis
alpha: 0.8
});
holdText.anchor.set(0.5, 0); // Anchor top-center
holdText.x = GAME_CONFIG.SCREEN_CENTER_X;
holdText.y = GAME_CONFIG.LANES[GAME_CONFIG.LANES.length - 1].y + 100; // Below the last lane (now matches new vertical spread)
fishingScreen.addChild(holdText);
return {
boat: boat,
hook: hook,
// Single hook
line: line,
// Single line
scoreText: scoreText,
fishText: fishText,
comboText: comboText,
progressText: progressText
};
}
/****
* Initialize Screen Elements
****/
var titleElements = createTitleScreen();
var levelSelectElements = createLevelSelectScreen();
var fishingElements = createFishingScreen();
// Feedback indicators are now created on-demand by the showFeedback function.
// The global feedbackIndicators object is no longer needed.
// Game variables
var fishArray = [];
/****
* Input State and Helpers for Fishing
****/
var inputState = {
touching: false,
// Is the screen currently being touched?
touchLane: -1,
// Which lane was the touch initiated in? (0, 1, 2)
touchStartTime: 0,
// Timestamp of when the touch started (LK.ticks based)
holdFish: null // Reference to a fish if a hold action is being performed on it
};
// getTouchLane function is removed as player input is now tied to the single dynamic hook,
// not specific screen regions for lanes.
// Shows feedback (perfect, good, miss) at the specified lane
function showFeedback(type, laneIndex) {
var feedbackY = GAME_CONFIG.LANES[laneIndex].y;
var indicator = new FeedbackIndicator(type); // Creates a new indicator e.g. FeedbackIndicator('perfect')
// Position feedback at the single hook's X coordinate and the fish's lane Y
indicator.x = fishingElements.hook.x; // Use the single hook's X
indicator.y = feedbackY; // Feedback appears at the fish's lane Y
fishingScreen.addChild(indicator);
indicator.show(); // Triggers the animation and self-destruction
}
// Animates the hook in a specific lane after a catch attempt
// Animates the single hook after a catch attempt
function animateHookCatch() {
var hook = fishingElements.hook;
// We need a stable originalY. The hook.originalY might change if we re-assign it during tweens.
// Let's use the target Y of the current fish lane for the "resting" position after animation.
var restingY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
// Quick bobbing animation for the single hook
tween(hook, {
y: restingY - 30
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(hook, {
y: restingY
}, {
duration: 150,
easing: tween.easeIn,
onFinish: function onFinish() {
// Ensure originalY reflects the current target lane after animation.
hook.originalY = restingY;
}
});
}
});
}
// Handles input specifically for the fishing screen (down and up events)
function handleFishingInput(x, y, isDown) {
if (!GameState.gameActive) return;
// With a single dynamic hook, any touch interacts with the hook's current/target lane.
// The y-coordinate of the touch is less important for lane determination.
var interactionLane = GameState.hookTargetLaneIndex; // The lane the hook is currently targeting.
var currentTime = LK.ticks * (1000 / 60); // Current time in ms
if (isDown) {
// Touch started
inputState.touching = true;
inputState.touchLane = interactionLane; // Store the lane the hook was targeting at touch start
inputState.touchStartTime = currentTime;
inputState.holdFish = null; // Reset any previous hold target
// Check if this touch might be starting a hold on a holdable fish in the hook's target lane
var hookX = fishingElements.hook.x;
for (var i = 0; i < fishArray.length; i++) {
var fish = fishArray[i];
if (fish.lane === interactionLane && fish.isHoldFish && !fish.caught) {
var distanceToHook = Math.abs(fish.x - hookX);
// Check if the fish is close enough to the hook to be considered for a hold
if (distanceToHook < GAME_CONFIG.MISS_WINDOW + 20) {
// Slightly larger window for initiating hold
inputState.holdFish = fish; // Mark this fish as the potential target of a hold
break;
}
}
}
} else {
// Touch ended
// Check if this 'up' corresponds to an active 'down' for the same target lane
if (inputState.touching && inputState.touchLane === interactionLane) {
var holdDuration = currentTime - inputState.touchStartTime;
var isHoldAttempt = holdDuration > 200; // Minimum duration for a "hold"
checkCatch(interactionLane, isHoldAttempt, holdDuration);
}
inputState.touching = false; // Reset touching state
inputState.holdFish = null; // Reset hold target
}
}
/****
* Screen Management
****/
function showScreen(screenName) {
titleScreen.visible = false;
levelSelectScreen.visible = false;
fishingScreen.visible = false;
resultsScreen.visible = false;
GameState.currentScreen = screenName;
switch (screenName) {
case 'title':
titleScreen.visible = true;
break;
case 'levelSelect':
levelSelectScreen.visible = true;
updateLevelSelectScreen();
break;
case 'fishing':
fishingScreen.visible = true;
startFishingSession();
break;
case 'results':
resultsScreen.visible = true;
break;
}
}
/****
* Level Select Logic
****/
function updateLevelSelectScreen() {
var elements = levelSelectElements;
// Update money display
elements.moneyDisplay.setText('Money: $' + GameState.money);
// Create depth tabs
createDepthTabs();
// Update song display
updateSongDisplay();
// Update shop button
updateShopButton();
}
function createDepthTabs() {
// Clear existing tabs
levelSelectElements.depthTabs.forEach(function (tab) {
if (tab.container) tab.container.destroy();
});
levelSelectElements.depthTabs = [];
// Create tabs for unlocked depths
for (var i = 0; i <= GameState.currentDepth; i++) {
var depth = GAME_CONFIG.DEPTHS[i];
var isSelected = i === GameState.selectedDepth;
var tabContainer = levelSelectScreen.addChild(new Container());
var tab = tabContainer.addChild(LK.getAsset('depthTab', {
anchorX: 0.5,
anchorY: 0.5,
x: 300 + i * 220,
y: 400,
tint: isSelected ? 0x1976d2 : 0x455a64
}));
var tabText = new Text2(depth.name.split(' ')[0], {
size: 30,
fill: 0xFFFFFF
});
tabText.anchor.set(0.5, 0.5);
tabText.x = 300 + i * 220;
tabText.y = 400;
tabContainer.addChild(tabText);
levelSelectElements.depthTabs.push({
container: tabContainer,
tab: tab,
depthIndex: i
});
}
}
function updateSongDisplay() {
var elements = levelSelectElements;
var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth];
var song = depth.songs[GameState.selectedSong];
var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong);
// Update song info
elements.songTitle.setText(song.name);
elements.songInfo.setText('BPM: ' + song.bpm + ' | Duration: ' + formatTime(song.duration));
// Calculate potential earnings
var minEarnings = Math.floor(depth.fishValue * 20); // Conservative estimate
var maxEarnings = Math.floor(depth.fishValue * 60); // With combos and rare fish
elements.songEarnings.setText('Potential Earnings: $' + minEarnings + '-$' + maxEarnings);
// Update play/buy button
if (owned) {
elements.playButtonText.setText('PLAY');
elements.playButton.tint = 0x1976d2;
} else {
elements.playButtonText.setText('BUY ($' + song.cost + ')');
elements.playButton.tint = GameState.money >= song.cost ? 0x2e7d32 : 0x666666;
}
// Update arrow states
elements.leftArrow.tint = GameState.selectedSong > 0 ? 0x1976d2 : 0x666666;
elements.rightArrow.tint = GameState.selectedSong < depth.songs.length - 1 ? 0x1976d2 : 0x666666;
}
function updateShopButton() {
var elements = levelSelectElements;
var canUpgrade = GameState.canUpgrade();
var nextDepth = GAME_CONFIG.DEPTHS[GameState.currentDepth + 1];
if (nextDepth) {
elements.shopButtonText.setText('UPGRADE ROD ($' + nextDepth.upgradeCost + ')');
elements.shopButton.tint = canUpgrade ? 0x2e7d32 : 0x666666;
} else {
elements.shopButtonText.setText('MAX DEPTH REACHED');
elements.shopButton.tint = 0x666666;
}
}
function formatTime(ms) {
var seconds = Math.floor(ms / 1000);
var minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
return minutes + ':' + (seconds < 10 ? '0' : '') + seconds;
}
/****
* Fishing Game Logic
****/
function startFishingSession() {
// Reset session state
GameState.sessionScore = 0;
GameState.sessionFishCaught = 0;
GameState.sessionFishSpawned = 0;
GameState.combo = 0;
GameState.maxCombo = 0;
GameState.gameActive = true;
GameState.songStartTime = 0;
GameState.lastBeatTime = 0;
GameState.beatCount = 0;
// Clear any existing fish
fishArray.forEach(function (fish) {
fish.destroy();
});
fishArray = [];
// Reset pattern generator for new session
PatternGenerator.reset();
// Start music
LK.playMusic('rhythmTrack');
}
function spawnFish() {
var depthConfig = GameState.getCurrentDepthConfig();
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
// Skip spawn if pattern says no double spawns
if (pattern.doubleSpawnChance === 0) {
// Check if any fish are too close to spawn position
for (var i = 0; i < fishArray.length; i++) {
var fish = fishArray[i];
if (Math.abs(fish.x - GAME_CONFIG.SCREEN_CENTER_X) < PatternGenerator.minDistanceBetweenFish) {
return; // Skip this spawn
}
}
}
// Use pattern generator to choose lane
var laneIndex = PatternGenerator.getNextLane();
var targetLane = GAME_CONFIG.LANES[laneIndex];
// Determine if this is a hold fish
var isHoldFish = Math.random() < (pattern.holdFishChance || 0);
// Determine fish type and value
var fishType, fishValue;
var rand = Math.random();
if (rand < pattern.rareSpawnChance) {
fishType = 'rare';
fishValue = depthConfig.fishValue * 4; // Rare fish are more valuable
} else if (GameState.selectedDepth >= 2 && rand < 0.3) {
// Example: deep fish appear more in deeper levels
fishType = 'deep';
fishValue = Math.floor(depthConfig.fishValue * 2);
} else if (GameState.selectedDepth >= 1 && rand < 0.6) {
// Example: medium fish
fishType = 'medium';
fishValue = Math.floor(depthConfig.fishValue * 1.5);
} else {
fishType = 'shallow'; // Common fish
fishValue = depthConfig.fishValue;
}
// Bonus value for hold fish
if (isHoldFish) {
fishValue = Math.floor(fishValue * 1.5); // Hold fish are worth more
}
var fishSpeed = Math.random() < 0.5 ? depthConfig.fishSpeed : -depthConfig.fishSpeed;
// Create fish
var fish = new Fish(fishType, fishValue, fishSpeed, laneIndex,
// Pass lane index
isHoldFish // Pass hold status
);
fish.x = fish.speed > 0 ? -150 : 2048 + 150; // Start off-screen
fish.y = targetLane.y; // Set Y to the chosen lane's Y
fishArray.push(fish);
fishingScreen.addChild(fish);
GameState.sessionFishSpawned++;
// Register spawn position for pattern tracking
PatternGenerator.registerSpawn(GAME_CONFIG.SCREEN_CENTER_X);
}
function checkCatch(fishLane, isHoldAction, holdDurationMs) {
var hookX = fishingElements.hook.x; // Use the single hook's X position
var closestFishInLane = null;
var closestDistance = Infinity;
// Find the closest fish in the target fishLane that hasn't been caught
for (var i = 0; i < fishArray.length; i++) {
var fish = fishArray[i];
if (!fish.caught && fish.lane === fishLane) {
var distance = Math.abs(fish.x - hookX);
if (distance < closestDistance) {
closestDistance = distance;
closestFishInLane = fish;
}
}
}
if (!closestFishInLane) {
showFeedback('miss', fishLane);
LK.getSound('miss').play();
GameState.combo = 0;
return;
}
// --- Hold Fish Logic ---
if (closestFishInLane.isHoldFish) {
if (!isHoldAction) {
// Tapped a hold fish
showFeedback('miss', fishLane);
LK.getSound('miss').play();
GameState.combo = 0;
return;
}
if (holdDurationMs < closestFishInLane.requiredHoldTime) {
// Held, but not long enough
showFeedback('miss', fishLane);
LK.getSound('miss').play();
GameState.combo = 0;
return;
}
} else {
// Normal Fish
if (isHoldAction) {
// Held a normal fish (counts as miss/mistake)
showFeedback('miss', fishLane);
LK.getSound('miss').play();
GameState.combo = 0;
return;
}
}
// --- End Hold Fish Logic ---
var points = 0;
var multiplier = Math.max(1, Math.floor(GameState.combo / 10) + 1);
if (closestDistance < GAME_CONFIG.PERFECT_WINDOW) {
points = closestFishInLane.value * 2 * multiplier;
showFeedback('perfect', fishLane);
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) {
points = closestFishInLane.value * multiplier;
showFeedback('good', fishLane);
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.MISS_WINDOW) {
points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier));
showFeedback('good', fishLane);
GameState.combo++;
} else {
showFeedback('miss', fishLane);
LK.getSound('miss').play();
GameState.combo = 0;
return;
}
// Successfully caught fish
closestFishInLane.catchFish();
var fishIndex = fishArray.indexOf(closestFishInLane);
if (fishIndex > -1) {
fishArray.splice(fishIndex, 1);
}
GameState.sessionScore += points;
GameState.money += points;
GameState.sessionFishCaught++;
GameState.totalFishCaught++;
GameState.maxCombo = Math.max(GameState.maxCombo, GameState.combo);
LK.getSound('catch').play();
animateHookCatch(); // Call parameterless animateHookCatch for the single hook
}
// Note: The old animateHookCatch function that was defined right after checkCatch
// is now a global helper: animateHookCatch(laneIndex), defined earlier.
// We remove the old local one if it existed here by not re-inserting it.
function updateFishingUI() {
var elements = fishingElements;
elements.scoreText.setText('Score: ' + GameState.sessionScore);
elements.fishText.setText('Fish: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned);
elements.comboText.setText('Combo: ' + GameState.combo);
// Update progress
if (GameState.songStartTime > 0) {
var currentTime = LK.ticks * (1000 / 60);
var elapsed = currentTime - GameState.songStartTime;
var songConfig = GameState.getCurrentSongConfig();
elements.progressText.setText(formatTime(elapsed) + ' / ' + formatTime(songConfig.duration));
}
}
function endFishingSession() {
GameState.gameActive = false;
// Clear fish
fishArray.forEach(function (fish) {
fish.destroy();
});
fishArray = [];
// Create results screen
createResultsScreen();
showScreen('results');
}
function createResultsScreen() {
// Clear previous results
resultsScreen.removeChildren();
var resultsBg = resultsScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 0.9
}));
var title = new Text2('Fishing Complete!', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.x = GAME_CONFIG.SCREEN_CENTER_X;
title.y = 400;
resultsScreen.addChild(title);
var scoreResult = new Text2('Score: ' + GameState.sessionScore, {
size: 70,
fill: 0xFFD700
});
scoreResult.anchor.set(0.5, 0.5);
scoreResult.x = GAME_CONFIG.SCREEN_CENTER_X;
scoreResult.y = 550;
resultsScreen.addChild(scoreResult);
var fishResult = new Text2('Fish Caught: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned, {
size: 50,
fill: 0xFFFFFF
});
fishResult.anchor.set(0.5, 0.5);
fishResult.x = GAME_CONFIG.SCREEN_CENTER_X;
fishResult.y = 650;
resultsScreen.addChild(fishResult);
var comboResult = new Text2('Max Combo: ' + GameState.maxCombo, {
size: 50,
fill: 0xFF9800
});
comboResult.anchor.set(0.5, 0.5);
comboResult.x = GAME_CONFIG.SCREEN_CENTER_X;
comboResult.y = 750;
resultsScreen.addChild(comboResult);
var moneyEarned = new Text2('Money Earned: $' + GameState.sessionScore, {
size: 50,
fill: 0x4CAF50
});
moneyEarned.anchor.set(0.5, 0.5);
moneyEarned.x = GAME_CONFIG.SCREEN_CENTER_X;
moneyEarned.y = 850;
resultsScreen.addChild(moneyEarned);
// Accuracy
var accuracy = GameState.sessionFishSpawned > 0 ? Math.round(GameState.sessionFishCaught / GameState.sessionFishSpawned * 100) : 0;
var accuracyResult = new Text2('Accuracy: ' + accuracy + '%', {
size: 50,
fill: 0x2196F3
});
accuracyResult.anchor.set(0.5, 0.5);
accuracyResult.x = GAME_CONFIG.SCREEN_CENTER_X;
accuracyResult.y = 950;
resultsScreen.addChild(accuracyResult);
// Continue button
var continueButton = resultsScreen.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1200
}));
var continueText = new Text2('CONTINUE', {
size: 50,
fill: 0xFFFFFF
});
continueText.anchor.set(0.5, 0.5);
continueText.x = GAME_CONFIG.SCREEN_CENTER_X;
continueText.y = 1200;
resultsScreen.addChild(continueText);
// Fade in
resultsScreen.alpha = 0;
tween(resultsScreen, {
alpha: 1
}, {
duration: 500,
easing: tween.easeOut
});
}
/****
* Input Handling
****/
game.down = function (x, y, obj) {
LK.getSound('buttonClick').play();
switch (GameState.currentScreen) {
case 'title':
// Check if click is within start button bounds
var startButton = titleElements.startButton;
if (x >= startButton.x - startButton.width / 2 && x <= startButton.x + startButton.width / 2 && y >= startButton.y - startButton.height / 2 && y <= startButton.y + startButton.height / 2) {
showScreen('levelSelect');
}
// Check if click is within tutorial button bounds
var tutorialButton = titleElements.tutorialButton;
if (x >= tutorialButton.x - tutorialButton.width / 2 && x <= tutorialButton.x + tutorialButton.width / 2 && y >= tutorialButton.y - tutorialButton.height / 2 && y <= tutorialButton.y + tutorialButton.height / 2) {
// TODO: Show tutorial
}
break;
case 'levelSelect':
handleLevelSelectInput(x, y);
break;
case 'fishing':
handleFishingInput(x, y, true); // true for isDown
break;
case 'results':
showScreen('levelSelect');
break;
}
};
function handleLevelSelectInput(x, y) {
var elements = levelSelectElements;
// Check depth tabs
elements.depthTabs.forEach(function (tab) {
var tabAsset = tab.tab;
if (x >= tabAsset.x - tabAsset.width / 2 && x <= tabAsset.x + tabAsset.width / 2 && y >= tabAsset.y - tabAsset.height / 2 && y <= tabAsset.y + tabAsset.height / 2) {
GameState.selectedDepth = tab.depthIndex;
GameState.selectedSong = 0; // Reset to first song
updateLevelSelectScreen();
}
});
// Check song navigation
var leftArrow = elements.leftArrow;
if (x >= leftArrow.x - leftArrow.width / 2 && x <= leftArrow.x + leftArrow.width / 2 && y >= leftArrow.y - leftArrow.height / 2 && y <= leftArrow.y + leftArrow.height / 2 && GameState.selectedSong > 0) {
GameState.selectedSong--;
updateSongDisplay();
}
var rightArrow = elements.rightArrow;
if (x >= rightArrow.x - rightArrow.width / 2 && x <= rightArrow.x + rightArrow.width / 2 && y >= rightArrow.y - rightArrow.height / 2 && y <= rightArrow.y + rightArrow.height / 2) {
var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth];
if (GameState.selectedSong < depth.songs.length - 1) {
GameState.selectedSong++;
updateSongDisplay();
}
}
// Check play/buy button
var playButton = elements.playButton;
if (x >= playButton.x - playButton.width / 2 && x <= playButton.x + playButton.width / 2 && y >= playButton.y - playButton.height / 2 && y <= playButton.y + playButton.height / 2) {
var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong);
if (owned) {
showScreen('fishing');
} else {
// Try to buy song
if (GameState.buySong(GameState.selectedDepth, GameState.selectedSong)) {
updateLevelSelectScreen();
}
}
}
// Check shop button
var shopButton = elements.shopButton;
if (x >= shopButton.x - shopButton.width / 2 && x <= shopButton.x + shopButton.width / 2 && y >= shopButton.y - shopButton.height / 2 && y <= shopButton.y + shopButton.height / 2) {
if (GameState.upgrade()) {
LK.getSound('upgrade').play();
updateLevelSelectScreen();
}
}
// Check back button
var backButton = elements.backButton;
if (x >= backButton.x - backButton.width / 2 && x <= backButton.x + backButton.width / 2 && y >= backButton.y - backButton.height / 2 && y <= backButton.y + backButton.height / 2) {
showScreen('title');
}
}
/****
* Main Game Loop
****/
game.update = function () {
if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60);
// Initialize game timer
if (GameState.songStartTime === 0) {
GameState.songStartTime = currentTime;
}
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
var beatInterval = 60000 / songConfig.bpm;
var spawnInterval = beatInterval * pattern.beatsPerFish;
// Check song end
if (currentTime - GameState.songStartTime >= songConfig.duration) {
endFishingSession();
return;
}
// Spawn fish on beat
if (currentTime - GameState.lastBeatTime >= spawnInterval) {
GameState.lastBeatTime = currentTime;
GameState.beatCount++;
spawnFish();
}
// Dynamic Hook Movement Logic
var approachingFish = null;
var minDistanceToCenter = Infinity;
for (var i = 0; i < fishArray.length; i++) {
var f = fishArray[i];
if (!f.caught) {
var distanceToHookX = Math.abs(f.x - fishingElements.hook.x);
// Consider fish that are moving towards the hook OR are very close to it already
var isApproachingOrAtHook = f.speed > 0 && f.x < fishingElements.hook.x || f.speed < 0 && f.x > fishingElements.hook.x || distanceToHookX < GAME_CONFIG.MISS_WINDOW * 2;
if (isApproachingOrAtHook && distanceToHookX < minDistanceToCenter) {
minDistanceToCenter = distanceToHookX;
approachingFish = f;
}
}
}
var targetLaneY;
if (approachingFish) {
if (GameState.hookTargetLaneIndex !== approachingFish.lane) {
GameState.hookTargetLaneIndex = approachingFish.lane;
}
targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
} else {
// No fish approaching, hook returns/stays at middle lane (or last known target)
// For simplicity, let's use the last known/default target index.
targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
}
// Tween hook and line to the target Y if not already there or already tweening
// A more robust solution would check if a tween is active on the hook.
// For now, we'll just update if the current Y is different.
if (fishingElements.hook.y !== targetLaneY) {
tween(fishingElements.hook, {
y: targetLaneY
}, {
duration: 150,
easing: tween.easeOut
});
tween(fishingElements.line, {
height: targetLaneY - GAME_CONFIG.WATER_SURFACE_Y
}, {
duration: 150,
easing: tween.easeOut
});
fishingElements.hook.originalY = targetLaneY; // Update reference for animations
}
// Update fish
for (var i = fishArray.length - 1; i >= 0; i--) {
var fish = fishArray[i];
fish.update();
// Update hold fish visual state if being held
if (fish.isHoldFish && inputState.holdFish === fish && inputState.touching) {
var currentHoldDuration = currentTime - inputState.touchStartTime;
if (currentHoldDuration >= fish.requiredHoldTime && !fish.holdStarted) {
fish.holdStarted = true;
}
} else if (fish.isHoldFish && fish.holdStarted && !(inputState.holdFish === fish && inputState.touching)) {
fish.holdStarted = false;
}
// Remove off-screen fish (if not caught)
if (!fish.caught && (fish.x < -150 || fish.x > 2048 + 150)) {
fish.destroy();
fishArray.splice(i, 1);
}
}
// Update UI
updateFishingUI();
};
// Initialize game
showScreen('title');
No background.
A music note. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A white bubble. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
Blue gradient background starting lighter blue at the top of the image and going to a darker blue at the bottom.. In-Game asset. 2d. High contrast. No shadows
A small single strand of loose floating seaweed. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A thin wispy white cloud. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
Game logo for the game ‘Beat Fisher’. High def 80’s color themed SVG of the word.. In-Game asset. 2d. High contrast. No shadows
A yellow star burst that says 'Perfect!' in the center. 80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
A red starburst with the word ‘Miss!’ In it. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A green starburst with the word ‘Good!’ In it. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
White stylized square bracket. Pixelated. In-Game asset. 2d. High contrast. No shadows
A golden fish. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A double sided fishing hook with a small speaker in the center. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
An SVG that says ‘Start’
Blue to green gradient on the word instead of pink.
Above water background image showing gradient of light to dark blue starting at the top. Looking straight down from high above.. In-Game asset. 2d. High contrast. No shadows
This boat, same perspective, boat facing down.
A small island centered with a large mountain taking up most of it with a waterfall on the south side and a fishing village just below it. Under the fishing village is a harbor with a single empty dock. The dock extends into a half open bay. 80s arcade machine inspire high definition graphics with 80s colored highlights. White background. Top down 3/4 view. In-Game asset. 2d. High contrast. No shadows
A small lock icon. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A seagull with wings spread straight out as if soaring. Top down view, looking down from above. 80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
A round button with an embossed edge with an anchor in the center. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A long fluffy white cloud seen from overhead. 80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
Much thinner border
The dark shadow silhouette of a fish. Top down view. In-Game asset. 2d. High contrast. No shadows
rhythmTrack
Music
morningtide
Music
miss
Sound effect
catch
Sound effect
catch2
Sound effect
catch3
Sound effect
catch4
Sound effect
seagull1
Sound effect
seagull2
Sound effect
seagull3
Sound effect
boatsounds
Sound effect
sunnyafternoon
Music
reel
Sound effect
sardineRiff
Sound effect
anchovyRiff
Sound effect