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.
User prompt
The fish have to come in pattern that are achievable by the player. No notes in different lanes in the same position. Prefer lanes next to the current lane when switching lanes in the pattern. Lower the number of fish.
User prompt
Now spread the lanes and hooks/lines out vertically so that the second lowest hook is at the center of the screen. Adjust lane touch detection to match.
User prompt
Okay, now instead of 3 hooks on the same line, split them into three different lines of different lengths that are spaced out equally along the boats length on the X axis.
User prompt
Bring the top of the water 10% lower and adjust the rest to match.
User prompt
Update with: var GAME_CONFIG = { SCREEN_CENTER_X: 1024, SCREEN_CENTER_Y: 900, BOAT_Y: 300, WATER_SURFACE_Y: 350, // 3 Lane System LANES: [ { y: 700, name: "shallow" }, // Top lane { y: 900, name: "medium" }, // Middle lane { y: 1100, name: "deep" } // Bottom lane ], // Timing windows PERFECT_WINDOW: 40, GOOD_WINDOW: 80, MISS_WINDOW: 120, // Rest of config stays the same... DEPTHS: [ { level: 1, name: "Shallow Waters", fishSpeed: 6, fishValue: 1, upgradeCost: 0, songs: [ { name: "Gentle Waves", bpm: 100, duration: 60000, pattern: "simple", cost: 0 }, { name: "Morning Tide", bpm: 110, duration: 75000, pattern: "simple", cost: 50 } ] }, // ... other depths ], // Updated patterns with hold fish PATTERNS: { simple: { beatsPerFish: 1, doubleSpawnChance: 0.05, rareSpawnChance: 0.02, holdFishChance: 0.1 }, medium: { beatsPerFish: 0.75, doubleSpawnChance: 0.1, rareSpawnChance: 0.05, holdFishChance: 0.2 }, complex: { beatsPerFish: 0.5, doubleSpawnChance: 0.15, rareSpawnChance: 0.08, holdFishChance: 0.3 }, expert: { beatsPerFish: 0.25, doubleSpawnChance: 0.2, rareSpawnChance: 0.12, holdFishChance: 0.4 } } }; /**** * Updated Fish Class with Hold Mechanics ****/ 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; self.isHoldFish = isHoldFish || false; self.caught = false; self.isSpecial = type === 'rare'; self.shimmerTime = 0; // Hold fish visual indicator if (self.isHoldFish) { fishGraphics.tint = 0xFFD700; // Golden tint for hold fish self.holdStarted = false; self.holdDuration = 0; self.requiredHoldTime = 800; // 800ms hold required } self.update = function() { if (!self.caught) { self.x += self.speed; if (self.isSpecial) { self.shimmerTime += 0.1; fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2; } } }; self.catchFish = function() { self.caught = true; tween(self, { y: GAME_CONFIG.BOAT_Y, x: GAME_CONFIG.SCREEN_CENTER_X, scaleX: 0.5, scaleY: 0.5, alpha: 0 }, { duration: 600, easing: tween.easeOut, onFinish: function() { self.destroy(); } }); }; return self; }); /**** * Updated Fishing Screen Creation with 3 Hooks ****/ function createFishingScreen() { // Water background var water = fishingScreen.addChild(LK.getAsset('water', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, alpha: 0.7 })); // Water surface var waterSurface = fishingScreen.addChild(LK.getAsset('waterSurface', { x: 0, y: GAME_CONFIG.WATER_SURFACE_Y, alpha: 0.8 })); // Boat var boat = fishingScreen.addChild(LK.getAsset('boat', { anchorX: 0.5, anchorY: 1, x: GAME_CONFIG.SCREEN_CENTER_X, y: GAME_CONFIG.WATER_SURFACE_Y })); // Create 3 fishing lines and hooks var hooks = []; var lines = []; for (var i = 0; i < 3; i++) { var lane = GAME_CONFIG.LANES[i]; // 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: lane.y - GAME_CONFIG.WATER_SURFACE_Y })); lines.push(line); // Hook var hook = fishingScreen.addChild(LK.getAsset('hook', { anchorX: 0.5, anchorY: 0.5, x: GAME_CONFIG.SCREEN_CENTER_X, y: lane.y })); hook.originalY = lane.y; hook.lane = i; hooks.push(hook); } // Lane indicators var laneLabels = []; var laneNames = ['SHALLOW', 'MEDIUM', 'DEEP']; for (var i = 0; i < 3; i++) { var label = new Text2(laneNames[i], { size: 30, fill: 0xFFFFFF, alpha: 0.7 }); label.anchor.set(0, 0.5); label.x = 50; label.y = GAME_CONFIG.LANES[i].y; fishingScreen.addChild(label); laneLabels.push(label); } // 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; fishingScreen.addChild(comboText); // Hold instruction var holdText = new Text2('TAP: Normal Fish | HOLD: Golden Fish', { size: 35, fill: 0xFFD700, alpha: 0.8 }); holdText.anchor.set(0.5, 0); holdText.x = GAME_CONFIG.SCREEN_CENTER_X; holdText.y = 1400; fishingScreen.addChild(holdText); return { boat: boat, hooks: hooks, lines: lines, laneLabels: laneLabels, scoreText: scoreText, fishText: fishText, comboText: comboText }; } /**** * Updated Fish Spawning with Lane System ****/ function spawnFish() { var depthConfig = GameState.getCurrentDepthConfig(); var songConfig = GameState.getCurrentSongConfig(); var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern]; // Choose random lane var laneIndex = Math.floor(Math.random() * 3); var lane = GAME_CONFIG.LANES[laneIndex]; // Determine if this is a hold fish var isHoldFish = Math.random() < pattern.holdFishChance; // Determine fish type and value var fishType, fishValue; var rand = Math.random(); if (rand < pattern.rareSpawnChance) { fishType = 'rare'; fishValue = depthConfig.fishValue * 4; } else if (GameState.selectedDepth >= 2 && rand < 0.3) { fishType = 'deep'; fishValue = Math.floor(depthConfig.fishValue * 2); } else if (GameState.selectedDepth >= 1 && rand < 0.6) { fishType = 'medium'; fishValue = Math.floor(depthConfig.fishValue * 1.5); } else { fishType = 'shallow'; fishValue = depthConfig.fishValue; } // Bonus value for hold fish if (isHoldFish) { fishValue = Math.floor(fishValue * 1.5); } // Create fish var fish = new Fish( fishType, fishValue, Math.random() < 0.5 ? depthConfig.fishSpeed : -depthConfig.fishSpeed, laneIndex, isHoldFish ); fish.x = fish.speed > 0 ? -100 : 2148; fish.y = lane.y; fishArray.push(fish); fishingScreen.addChild(fish); GameState.sessionFishSpawned++; } /**** * Updated Input Handling with Lane Detection and Hold Logic ****/ var inputState = { touching: false, touchLane: -1, touchStartTime: 0, holdFish: null }; function getTouchLane(y) { // Determine which lane the touch is in if (y < 800) return 0; // Shallow lane else if (y < 1000) return 1; // Medium lane else return 2; // Deep lane } function checkCatch(touchLane, isHold, holdDuration) { var hookX = GAME_CONFIG.SCREEN_CENTER_X; var targetY = GAME_CONFIG.LANES[touchLane].y; var closestFish = null; var closestDistance = Infinity; // Find closest fish in the touched lane for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (!fish.caught && fish.lane === touchLane) { var distance = Math.abs(fish.x - hookX); if (distance < closestDistance) { closestDistance = distance; closestFish = fish; } } } if (!closestFish) { showFeedback('miss', touchLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // Check if input type matches fish type if (closestFish.isHoldFish && !isHold) { // Hold fish tapped instead of held showFeedback('miss', touchLane); LK.getSound('miss').play(); GameState.combo = 0; return; } else if (!closestFish.isHoldFish && isHold) { // Normal fish held instead of tapped showFeedback('miss', touchLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // For hold fish, check if held long enough if (closestFish.isHoldFish && holdDuration < closestFish.requiredHoldTime) { showFeedback('miss', touchLane); LK.getSound('miss').play(); GameState.combo = 0; return; } var points = 0; var multiplier = Math.max(1, Math.floor(GameState.combo / 10) + 1); if (closestDistance < GAME_CONFIG.PERFECT_WINDOW) { points = closestFish.value * 2 * multiplier; showFeedback('perfect', touchLane); GameState.combo++; } else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) { points = closestFish.value * multiplier; showFeedback('good', touchLane); GameState.combo++; } else if (closestDistance < GAME_CONFIG.MISS_WINDOW) { points = Math.max(1, Math.floor(closestFish.value * 0.5 * multiplier)); showFeedback('good', touchLane); GameState.combo++; } else { showFeedback('miss', touchLane); LK.getSound('miss').play(); GameState.combo = 0; return; } // Successfully caught fish closestFish.catchFish(); fishArray.splice(fishArray.indexOf(closestFish), 1); GameState.sessionScore += points; GameState.money += points; GameState.sessionFishCaught++; GameState.totalFishCaught++; GameState.maxCombo = Math.max(GameState.maxCombo, GameState.combo); LK.getSound('catch').play(); // Hook animation for the specific lane animateHookCatch(touchLane); } function showFeedback(type, lane) { // Create feedback indicator at the specific lane var indicator = new FeedbackIndicator(type); indicator.x = GAME_CONFIG.SCREEN_CENTER_X; indicator.y = GAME_CONFIG.LANES[lane].y; fishingScreen.addChild(indicator); indicator.show(); } function animateHookCatch(laneIndex) { var hook = fishingElements.hooks[laneIndex]; var originalY = hook.originalY; tween(hook, { y: originalY - 30 }, { duration: 150, easing: tween.easeOut, onFinish: function() { tween(hook, { y: originalY }, { duration: 150, easing: tween.easeIn }); } }); } /**** * Updated Input System for Fishing Screen ****/ function handleFishingInput(x, y, isDown) { if (!GameState.gameActive) return; var touchLane = getTouchLane(y); var currentTime = LK.ticks * (1000 / 60); if (isDown) { // Touch started inputState.touching = true; inputState.touchLane = touchLane; inputState.touchStartTime = currentTime; inputState.holdFish = null; // Check for hold fish in this lane for (var i = 0; i < fishArray.length; i++) { var fish = fishArray[i]; if (fish.lane === touchLane && fish.isHoldFish && !fish.caught) { var distance = Math.abs(fish.x - GAME_CONFIG.SCREEN_CENTER_X); if (distance < GAME_CONFIG.MISS_WINDOW) { inputState.holdFish = fish; break; } } } } else { // Touch ended if (inputState.touching && inputState.touchLane === touchLane) { var holdDuration = currentTime - inputState.touchStartTime; var isHold = holdDuration > 200; // 200ms minimum for hold checkCatch(touchLane, isHold, holdDuration); } inputState.touching = false; inputState.holdFish = null; } } /**** * Updated Main Input Handler ****/ game.down = function(x, y, obj) { switch(GameState.currentScreen) { case 'fishing': handleFishingInput(x, y, true); break; // ... other screen handling stays the same } }; game.up = function(x, y, obj) { switch(GameState.currentScreen) { case 'fishing': handleFishingInput(x, y, false); break; } }; /**** * Updated Game Loop with Hold Fish Logic ****/ 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(); } // Update fish for (var i = fishArray.length - 1; i >= 0; i--) { var fish = fishArray[i]; fish.update(); // Update hold fish progress if (fish.isHoldFish && inputState.holdFish === fish && inputState.touching) { var holdDuration = currentTime - inputState.touchStartTime; // Visual feedback for holding progress could go here if (holdDuration >= fish.requiredHoldTime && !fish.holdStarted) { fish.holdStarted = true; // Could show visual indication that hold is successful } } // Remove off-screen fish if (fish.x < -150 || fish.x > 2198) { fish.destroy(); fishArray.splice(i, 1); } } // Update UI updateFishingUI(); }; ↪💡 Consider importing and using the following plugins: @upit/tween.v1
User prompt
Fix the touch handler for the start button.
Code edit (2 edits merged)
Please save this source code
User prompt
Please fix the bug: 'TypeError: setTimeout is not a function' in or related to this line: 'setTimeout(function () {' Line Number: 456
Code edit (1 edits merged)
Please save this source code
User prompt
Rhythm Reel: Beat Fishing
Initial prompt
Create a single-screen rhythm fishing game with these specifications: **Visual Layout:** Side-view with boat at top of screen, fishing line with hook at fixed depth, water background below. **Core Gameplay:** Fish swim horizontally (left-to-right or right-to-left) at hook level, arriving on musical beats. Player taps anywhere on screen when fish aligns with hook to catch it. Missed fish swim away. **Mechanics:** - Fish movement synced to 4/4 beat of background music - Perfect timing = catch fish (visual feedback: fish quickly pulled up to boat) - Hook returns to position immediately for next beat - Track caught fish count and display score **Basic Features:** - One fish type, simple alternating spawn directions - Basic tap detection when fish crosses hook position - Catch/miss visual and audio feedback - End-of-song results screen showing fish caught
/****
* 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
****/
/****
* Game Configuration
****/
// If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up.
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
LANES: [{
y: 973,
//{Z} // Original 700 + 273
name: "shallow"
},
// Top lane
{
y: 1173,
// Original 900 + 273
name: "medium"
},
// Middle lane
{
y: 1373,
//{14} // Original 1100 + 273
name: "deep"
} // Bottom lane
],
// 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
PATTERNS: {
simple: {
beatsPerFish: 1,
doubleSpawnChance: 0.05,
rareSpawnChance: 0.02,
holdFishChance: 0.1
},
medium: {
beatsPerFish: 0.75,
doubleSpawnChance: 0.1,
rareSpawnChance: 0.05,
holdFishChance: 0.2
},
complex: {
beatsPerFish: 0.5,
doubleSpawnChance: 0.15,
rareSpawnChance: 0.08,
holdFishChance: 0.3
},
expert: {
beatsPerFish: 0.25,
doubleSpawnChance: 0.2,
rareSpawnChance: 0.12,
holdFishChance: 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,
// 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
}));
// Calculate X positions for the 3 hooks, spaced along the boat's width
// Using boat.width / 3 as the offset from the center for the side hooks
var boatWidth = boat.width; // Actual width of the boat asset
var hookXPositions = [GAME_CONFIG.SCREEN_CENTER_X - boatWidth / 3, GAME_CONFIG.SCREEN_CENTER_X, GAME_CONFIG.SCREEN_CENTER_X + boatWidth / 3];
// Create 3 fishing lines and hooks
var hooks = [];
var lines = [];
for (var i = 0; i < 3; i++) {
var lane = GAME_CONFIG.LANES[i];
var currentHookX = hookXPositions[i];
// Fishing line
var line = fishingScreen.addChild(LK.getAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0,
// Anchor at top-middle of line asset
x: currentHookX,
// Set X position for this specific line
y: GAME_CONFIG.WATER_SURFACE_Y,
height: lane.y - GAME_CONFIG.WATER_SURFACE_Y // Line extends to hook depth (varying length)
}));
lines.push(line);
// Hook
var hook = fishingScreen.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
x: currentHookX,
// Set X position for this specific hook
y: lane.y // Hook Y is at the specific lane's depth
}));
hook.originalY = lane.y; // Store original Y for animation
hook.lane = i; // Store lane index on hook
hooks.push(hook);
}
// Lane indicators (simple text for now)
var laneLabels = [];
var laneNames = ['SHALLOW', 'MEDIUM', 'DEEP']; // Example names
for (var i = 0; i < 3; i++) {
var label = new Text2(laneNames[i], {
size: 30,
fill: 0xFFFFFF,
alpha: 0.7
});
label.anchor.set(0, 0.5); // Anchor left-middle
label.x = 50; // Position on the left side
label.y = GAME_CONFIG.LANES[i].y; // Align with lane Y
fishingScreen.addChild(label);
laneLabels.push(label);
}
// 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
fishingScreen.addChild(holdText);
return {
boat: boat,
hooks: hooks,
// Array of 3 hooks
lines: lines,
// Array of 3 lines
laneLabels: laneLabels,
scoreText: scoreText,
fishText: fishText,
comboText: comboText,
progressText: progressText // Ensure progressText is returned
};
}
/****
* 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
};
// Determines which lane a Y coordinate falls into
function getTouchLane(y) {
// Check from top lane downwards
if (y < (GAME_CONFIG.LANES[0].y + GAME_CONFIG.LANES[1].y) / 2) return 0; // Boundary between lane 0 and 1
else if (y < (GAME_CONFIG.LANES[1].y + GAME_CONFIG.LANES[2].y) / 2) return 1; // Boundary between lane 1 and 2
else return 2; // Assumed to be in lane 2 if below the mid-point of lane 1 and 2
// More robust: define lane height/boundaries if needed
}
// 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 X coordinate of the hook in the specified lane
indicator.x = fishingElements.hooks[laneIndex].x;
indicator.y = feedbackY; // Feedback appears at the lane's Y
fishingScreen.addChild(indicator);
indicator.show(); // Triggers the animation and self-destruction
}
// Animates the hook in a specific lane after a catch attempt
function animateHookCatch(laneIndex) {
if (laneIndex < 0 || laneIndex >= fishingElements.hooks.length) return; // Safety check
var hook = fishingElements.hooks[laneIndex];
var originalY = hook.originalY;
// Quick bobbing animation for the hook in the specified lane
tween(hook, {
y: originalY - 30
}, {
duration: 150,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(hook, {
y: originalY
}, {
duration: 150,
easing: tween.easeIn
});
}
});
}
// Handles input specifically for the fishing screen (down and up events)
function handleFishingInput(x, y, isDown) {
if (!GameState.gameActive) return;
var touchLane = getTouchLane(y); // Determine which lane the interaction is in
var currentTime = LK.ticks * (1000 / 60); // Current time in ms
if (isDown) {
// Touch started
inputState.touching = true;
inputState.touchLane = touchLane;
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 this lane
for (var i = 0; i < fishArray.length; i++) {
var fish = fishArray[i];
if (fish.lane === touchLane && fish.isHoldFish && !fish.caught) {
// Use the X position of the hook in the current touchLane for distance calculation
var hookXForLane = fishingElements.hooks[touchLane].x;
var distanceToHook = Math.abs(fish.x - hookXForLane);
// 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
// Visual cue for starting hold could be added here (e.g., hook wiggles)
break;
}
}
}
} else {
// Touch ended
if (inputState.touching && inputState.touchLane === touchLane) {
// Ensure this 'up' corresponds to an active 'down' in the same lane
var holdDuration = currentTime - inputState.touchStartTime;
// Determine if it was a tap or a hold based on duration
// Minimum duration for a gesture to be considered a "hold" attempt
var isHoldAttempt = holdDuration > 200;
checkCatch(touchLane, 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 = [];
// Start music
LK.playMusic('rhythmTrack');
}
function spawnFish() {
var depthConfig = GameState.getCurrentDepthConfig();
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
// Choose random lane (0, 1, or 2)
var laneIndex = Math.floor(Math.random() * GAME_CONFIG.LANES.length);
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++;
}
function checkCatch(touchLane, isHoldAction, holdDurationMs) {
// Get the X position of the hook in the specific lane the player interacted with
var hookX = fishingElements.hooks[touchLane].x;
var closestFishInLane = null;
var closestDistance = Infinity;
// Find the closest fish in the *touched lane* that hasn't been caught
for (var i = 0; i < fishArray.length; i++) {
var fish = fishArray[i];
if (!fish.caught && fish.lane === touchLane) {
var distance = Math.abs(fish.x - hookX);
if (distance < closestDistance) {
closestDistance = distance;
closestFishInLane = fish;
}
}
}
if (!closestFishInLane) {
showFeedback('miss', touchLane);
LK.getSound('miss').play();
GameState.combo = 0;
return;
}
// --- Hold Fish Logic ---
if (closestFishInLane.isHoldFish) {
if (!isHoldAction) {
// Tapped a hold fish
showFeedback('miss', touchLane);
LK.getSound('miss').play();
GameState.combo = 0;
return;
}
if (holdDurationMs < closestFishInLane.requiredHoldTime) {
// Held, but not long enough
showFeedback('miss', touchLane);
LK.getSound('miss').play();
GameState.combo = 0;
return;
}
} else {
// Normal Fish
if (isHoldAction) {
// Held a normal fish (counts as miss/mistake)
showFeedback('miss', touchLane);
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', touchLane);
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.GOOD_WINDOW) {
points = closestFishInLane.value * multiplier;
showFeedback('good', touchLane);
GameState.combo++;
} else if (closestDistance < GAME_CONFIG.MISS_WINDOW) {
// Forgiving 'good' for near misses if it's not a hold fish requiring precise timing
// or if it is a hold fish and they met the hold duration.
points = Math.max(1, Math.floor(closestFishInLane.value * 0.5 * multiplier));
showFeedback('good', touchLane); //{4Y}
GameState.combo++;
} else {
showFeedback('miss', touchLane);
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; // Player earns money from catches
GameState.sessionFishCaught++;
GameState.totalFishCaught++;
GameState.maxCombo = Math.max(GameState.maxCombo, GameState.combo);
LK.getSound('catch').play();
animateHookCatch(touchLane); // Animate the specific 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();
}
// 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;
// This is where you might add visual feedback for the fish being held
// e.g., fish struggles, progress bar fills, etc.
if (currentHoldDuration >= fish.requiredHoldTime && !fish.holdStarted) {
fish.holdStarted = true; // Mark that the hold duration requirement was met
// Potentially change fish graphic tint or add particle effect
// console.log("Hold requirement met for fish: " + fish.type);
}
} else if (fish.isHoldFish && fish.holdStarted && !(inputState.holdFish === fish && inputState.touching)) {
// If fish was previously 'holdStarted' but player released too early or switched targets
fish.holdStarted = false; // Reset if no longer actively being held correctly.
}
// Remove off-screen fish (if not caught)
if (!fish.caught && (fish.x < -150 || fish.x > 2048 + 150)) {
fish.destroy();
fishArray.splice(i, 1);
// Optional: Penalize for missed fish if it wasn't a hold fish that was let go by design
// if (!fish.isHoldFish) { GameState.combo = 0; }
}
}
// Update UI
updateFishingUI();
};
// Initialize game
showScreen('title'); ===================================================================
--- original.js
+++ change.js
@@ -589,30 +589,35 @@
// Anchor at bottom-middle of boat asset
x: GAME_CONFIG.SCREEN_CENTER_X,
y: GAME_CONFIG.WATER_SURFACE_Y // Boat sits on water surface
}));
+ // Calculate X positions for the 3 hooks, spaced along the boat's width
+ // Using boat.width / 3 as the offset from the center for the side hooks
+ var boatWidth = boat.width; // Actual width of the boat asset
+ var hookXPositions = [GAME_CONFIG.SCREEN_CENTER_X - boatWidth / 3, GAME_CONFIG.SCREEN_CENTER_X, GAME_CONFIG.SCREEN_CENTER_X + boatWidth / 3];
// Create 3 fishing lines and hooks
var hooks = [];
var lines = [];
for (var i = 0; i < 3; i++) {
var lane = GAME_CONFIG.LANES[i];
+ var currentHookX = hookXPositions[i];
// Fishing line
var line = fishingScreen.addChild(LK.getAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0,
// Anchor at top-middle of line asset
- x: GAME_CONFIG.SCREEN_CENTER_X,
- //{3v} // Lines hang from center boat
+ x: currentHookX,
+ // Set X position for this specific line
y: GAME_CONFIG.WATER_SURFACE_Y,
- height: lane.y - GAME_CONFIG.WATER_SURFACE_Y // Line extends to hook depth
+ height: lane.y - GAME_CONFIG.WATER_SURFACE_Y // Line extends to hook depth (varying length)
}));
lines.push(line);
// Hook
var hook = fishingScreen.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
- x: GAME_CONFIG.SCREEN_CENTER_X,
- //{3A} // Hook X is centered
+ x: currentHookX,
+ // Set X position for this specific hook
y: lane.y // Hook Y is at the specific lane's depth
}));
hook.originalY = lane.y; // Store original Y for animation
hook.lane = i; // Store lane index on hook
@@ -724,9 +729,10 @@
// 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')
- indicator.x = GAME_CONFIG.SCREEN_CENTER_X; // Feedback appears at hook's X
+ // Position feedback at the X coordinate of the hook in the specified lane
+ indicator.x = fishingElements.hooks[laneIndex].x;
indicator.y = feedbackY; // Feedback appears at the lane's Y
fishingScreen.addChild(indicator);
indicator.show(); // Triggers the animation and self-destruction
}
@@ -765,9 +771,11 @@
// Check if this touch might be starting a hold on a holdable fish in this lane
for (var i = 0; i < fishArray.length; i++) {
var fish = fishArray[i];
if (fish.lane === touchLane && fish.isHoldFish && !fish.caught) {
- var distanceToHook = Math.abs(fish.x - GAME_CONFIG.SCREEN_CENTER_X);
+ // Use the X position of the hook in the current touchLane for distance calculation
+ var hookXForLane = fishingElements.hooks[touchLane].x;
+ var distanceToHook = Math.abs(fish.x - hookXForLane);
// 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
@@ -970,9 +978,10 @@
fishingScreen.addChild(fish);
GameState.sessionFishSpawned++;
}
function checkCatch(touchLane, isHoldAction, holdDurationMs) {
- var hookX = GAME_CONFIG.SCREEN_CENTER_X; // Hooks are aligned vertically at SCREEN_CENTER_X
+ // Get the X position of the hook in the specific lane the player interacted with
+ var hookX = fishingElements.hooks[touchLane].x;
var closestFishInLane = null;
var closestDistance = Infinity;
// Find the closest fish in the *touched lane* that hasn't been caught
for (var i = 0; i < fishArray.length; i++) {
No background.
A music note. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A white bubble. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
Blue gradient background starting lighter blue at the top of the image and going to a darker blue at the bottom.. In-Game asset. 2d. High contrast. No shadows
A small single strand of loose floating seaweed. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A thin wispy white cloud. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
Game logo for the game ‘Beat Fisher’. High def 80’s color themed SVG of the word.. In-Game asset. 2d. High contrast. No shadows
A yellow star burst that says 'Perfect!' in the center. 80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
A red starburst with the word ‘Miss!’ In it. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A green starburst with the word ‘Good!’ In it. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
White stylized square bracket. Pixelated. In-Game asset. 2d. High contrast. No shadows
A double sided fishing hook with a small speaker in the center. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
An SVG that says ‘Start’
Blue to green gradient on the word instead of pink.
Above water background image showing gradient of light to dark blue starting at the top. Looking straight down from high above.. In-Game asset. 2d. High contrast. No shadows
This boat, same perspective, boat facing down.
A small island centered with a large mountain taking up most of it with a waterfall on the south side and a fishing village just below it. Under the fishing village is a harbor with a single empty dock. The dock extends into a half open bay. 80s arcade machine inspire high definition graphics with 80s colored highlights. White background. Top down 3/4 view. In-Game asset. 2d. High contrast. No shadows
A small lock icon. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A seagull with wings spread straight out as if soaring. Top down view, looking down from above. 80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
A round button with an embossed edge with an anchor in the center. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A long fluffy white cloud seen from overhead. 80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
Much thinner border
The dark shadow silhouette of a fish. Top down view. In-Game asset. 2d. High contrast. No shadows
Change the anchor to a knife and fork icon.
Top down beach sand background image with a light to dark gradient starting at the top.. In-Game asset. 2d. High contrast. No shadows
A moped courier riding a moped with a food carrying basket with the top open and no lid on the back of the moped. Top down view with the moped pointing fully sideways.
A kitchen knife. Side view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A large kitchen mixing bowl. Side view.
A fryer basket from a deep fryer. Side view.
An anchovy. 80s arcade machine graphics. Swimming Side profile. White background. In-Game asset. 2d. High contrast. No shadows
A raw fish steak. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
This fish steak with covered with raw batter on it.
Cooked fish and chips in a newspaper lined basket. Side view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast
A long rectangular wooden cutting board counter with different colored laminated wooden strips. Top down view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
a textured white rectangular kitchen counter top. overhead view. In-Game asset. 2d. High contrast. No shadows
A light blue starburst with “Chop!” In the center. 80s arcade machine graphics
Change the text to “Dip!”
Change the text to “Fry!”
Inside the kitchen of a small wooden shack restaurant, looking out over the counter onto a beach view with long gradual gradients. No people inside or on the beach.
This man with no fishing rod and chefs clothes on, facing forward.
Change the sign to say “The Fish Shack” and make the window larger.
A background image view looking into a fry kitchen.
A young man standing, facing away with his back facing down. Top down view.
A boombox stereo. Top down view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A young woman with long hair and shorts. No hat. Pink shirt.
An old man with a cane and hat. Still facing straight forward with back facing straight down.
A young boy with a yellow Hawaiian style shirt and sandals. No hat. Blonde hair.
A small red crab. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
Top down view of a shallow wave water on a beach shore..80s arcade machine graphics. In-Game asset. 2d. High contrast. No shadows
A long horizontal line of low bushes. Top down view. 80s arcade machine graphics.. In-Game asset. 2d. High contrast. No shadows
A outdoor wooden picnic bench. Top down view. Squared up.
A sea bass. 80s arcade machine graphics. Swimming Side profile. White background. In-Game asset. 2d. High contrast. No shadows
A shining golden mythical fish. Side profile, swimming. 80s arcade machine graphics. White background. In-Game asset. 2d. High contrast. No shadows
A cod. 80s arcade machine graphics. Swimming Side profile. White background. In-Game asset. 2d. High contrast. No shadows
A snapper. 80s arcade machine graphics. Swimming Side profile. White background. In-Game asset. 2d. High contrast. No shadows
Replace the knife and fork with a dollar symbol.
A meat grinder.
A set of spices shakers.
A large soup pot.
A blank sandwhich board. Top down view.
Fish cakes in a to go box.
Two fish tacos in a to go box.
A fish burger and fries in a basket lined with paper.
A bowl of fish curry.
A bowl of fish dumplings.
A bowl of fish meatballs in broth.
A ball of ground fish in a bowl.
Two uncooked fish cakes.
A raw ground fish patty.
This fish steak with spices added to.
Change the word to say “Grind!”
Change the word to say “Spice!”
Change the word to say “Boil!”
rhythmTrack
Music
morningtide
Music
miss
Sound effect
catch
Sound effect
catch2
Sound effect
catch3
Sound effect
catch4
Sound effect
seagull1
Sound effect
seagull2
Sound effect
seagull3
Sound effect
boatsounds
Sound effect
sunnyafternoon
Music
reel
Sound effect
sardineRiff
Sound effect
anchovyRiff
Sound effect
oceanCurrent
Music
deepFlow
Music
coralGroove
Music