/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var BubbleParticle = Container.expand(function (startX, startY) {
var self = Container.call(this);
self.gfx = self.attachAsset('bubbles', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 1 + Math.random() * 0.2,
// Start with some variation
scaleX: 1.2 + Math.random() * 0.25,
// Larger bubbles
// Bubbles are now 20% to 45% of original asset size
scaleY: this.scaleX // Keep aspect ratio initially, can be changed in update if desired
});
self.x = startX;
self.y = startY;
self.vx = (Math.random() - 0.5) * 0.4; // Even slower horizontal drift
self.vy = -(0.4 + Math.random() * 0.3); // Slower upward movement
self.life = 120 + Math.random() * 60; // Lifespan in frames (2 to 3 seconds)
self.age = 0;
self.isDone = false;
var initialAlpha = self.gfx.alpha;
var initialScale = self.gfx.scaleX; // Assuming scaleX and scaleY start the same
self.update = function () {
if (self.isDone) {
return;
}
self.age++;
self.x += self.vx;
self.y += self.vy;
// Fade out
self.gfx.alpha = Math.max(0, initialAlpha * (1 - self.age / self.life));
// Optionally shrink a bit more, or grow slightly then shrink
var scaleFactor = 1 - self.age / self.life; // Simple shrink
self.gfx.scaleX = initialScale * scaleFactor;
self.gfx.scaleY = initialScale * scaleFactor;
if (self.age >= self.life || self.gfx.alpha <= 0 || self.gfx.scaleX <= 0.01) {
self.isDone = true;
}
};
return self;
});
var CloudParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('cloud', {
anchorX: 0.5,
anchorY: 0.5
});
// Cloud spawn location - 30% chance to spawn on screen
var spawnOnScreen = Math.random() < 0.3;
if (spawnOnScreen) {
// Spawn randomly across the screen width
self.x = 200 + Math.random() * 1648; // Between 200 and 1848 to avoid edges
// Random horizontal drift direction
self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.1 + Math.random() * 0.2);
} else {
// Original off-screen spawn logic (70% of the time)
var spawnFromLeft = Math.random() < 0.5;
if (spawnFromLeft) {
self.x = -100; // Start off-screen left
self.vx = 0.1 + Math.random() * 0.2; // Slower drift right at 0.1-0.3 pixels/frame
} else {
self.x = 2048 + 100; // Start off-screen right
self.vx = -(0.1 + Math.random() * 0.2); // Slower drift left
}
}
// Spawn in sky area (above water surface)
var skyTop = -500; // Where sky background starts
var skyBottom = GAME_CONFIG.WATER_SURFACE_Y - 100; // Stay well above water
self.y = skyTop + Math.random() * (skyBottom - skyTop);
// Cloud properties
var baseScale = 0.8 + Math.random() * 0.6; // Scale: 0.8x to 1.4x
self.gfx.scale.set(baseScale);
// Subtle vertical drift
self.vy = (Math.random() - 0.5) * 0.02; // Even slower up/down drift
// Opacity for atmospheric effect
var targetAlpha = 0.4 + Math.random() * 0.3; // Alpha: 0.4 to 0.7
self.gfx.alpha = 0; // Start transparent
self.isDone = false;
self.fadingOut = false;
self.hasFadedIn = false; // Track if initial fade in completed
// Define screen boundaries for fade in/out
var fadeInStartX = 400; // Start fading in when cloud reaches this X
var fadeInEndX = 800; // Fully visible by this X
var fadeOutStartX = 1248; // Start fading out at this X (2048 - 800)
var fadeOutEndX = 1648; // Fully transparent by this X (2048 - 400)
self.update = function () {
if (self.isDone) {
return;
}
// Apply movement
self.x += self.vx;
self.y += self.vy;
// Handle mid-screen fade in/out based on position
var currentAlpha = self.gfx.alpha;
if (spawnFromLeft) {
// Moving right: fade in then fade out
if (!self.hasFadedIn && self.x >= fadeInStartX && self.x <= fadeInEndX) {
// Calculate fade in progress
var fadeInProgress = (self.x - fadeInStartX) / (fadeInEndX - fadeInStartX);
self.gfx.alpha = targetAlpha * fadeInProgress;
if (fadeInProgress >= 1) {
self.hasFadedIn = true;
}
} else if (self.hasFadedIn && self.x >= fadeOutStartX && self.x <= fadeOutEndX) {
// Calculate fade out progress
var fadeOutProgress = (self.x - fadeOutStartX) / (fadeOutEndX - fadeOutStartX);
self.gfx.alpha = targetAlpha * (1 - fadeOutProgress);
} else if (self.hasFadedIn && self.x > fadeInEndX && self.x < fadeOutStartX) {
// Maintain full opacity in middle section
self.gfx.alpha = targetAlpha;
}
} else {
// Moving left: fade in then fade out (reversed positions)
if (!self.hasFadedIn && self.x <= fadeOutEndX && self.x >= fadeOutStartX) {
// Calculate fade in progress (reversed)
var fadeInProgress = (fadeOutEndX - self.x) / (fadeOutEndX - fadeOutStartX);
self.gfx.alpha = targetAlpha * fadeInProgress;
if (fadeInProgress >= 1) {
self.hasFadedIn = true;
}
} else if (self.hasFadedIn && self.x <= fadeInEndX && self.x >= fadeInStartX) {
// Calculate fade out progress (reversed)
var fadeOutProgress = (fadeInEndX - self.x) / (fadeInEndX - fadeInStartX);
self.gfx.alpha = targetAlpha * (1 - fadeOutProgress);
} else if (self.hasFadedIn && self.x < fadeOutStartX && self.x > fadeInEndX) {
// Maintain full opacity in middle section
self.gfx.alpha = targetAlpha;
}
}
// Check if completely off-screen
var currentWidth = self.gfx.width * self.gfx.scale.x;
if (self.x < -currentWidth || self.x > 2048 + currentWidth) {
self.isDone = true;
}
};
return self;
});
var FeedbackIndicator = Container.expand(function (type) {
var self = Container.call(this);
var indicator = self.attachAsset(type + 'Indicator', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
self.show = function () {
indicator.alpha = 1;
indicator.scaleX = 0.5;
indicator.scaleY = 0.5;
tween(indicator, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 1400,
easing: tween.easeOut
});
};
return self;
});
/****
* Title Screen
****/
var Fish = Container.expand(function (type, value, speed, lane) {
var self = Container.call(this);
var assetName = type + 'Fish';
// Randomly pick from the three shallowFish assets if type is shallow
if (type === 'shallow') {
var shallowFishAssets = ['shallowFish', 'shallowFish2', 'shallowFish3'];
assetName = shallowFishAssets[Math.floor(Math.random() * shallowFishAssets.length)];
}
self.fishGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
if (speed > 0) {
// Moving right (coming from left), flip horizontally
self.fishGraphics.scaleX = -1;
}
self.type = type;
self.value = value;
self.speed = speed;
self.lane = lane; // Index of the lane (0, 1, or 2)
self.caught = false;
self.missed = false; // True if the fish passed the hook without being caught
self.lastX = 0; // Stores the x position from the previous frame for miss detection
self.isSpecial = type === 'rare'; // For shimmer effect
self.shimmerTime = 0;
// Bubble properties
self.lastBubbleSpawnTime = 0;
self.bubbleSpawnInterval = 120 + Math.random() * 80; // 120-200ms interval (fewer bubbles)
// Add properties for swimming animation
self.swimTime = Math.random() * Math.PI * 2; // Random starting phase for variety
self.baseY = self.y; // Store initial Y position
self.scaleTime = 0;
self.baseScale = 1;
self.update = function () {
if (!self.caught) {
// Horizontal movement
self.x += self.speed;
// Sine wave vertical movement
self.swimTime += 0.08; // Speed of sine wave oscillation
var swimAmplitude = 15; // Pixels of vertical movement
self.y = self.baseY + Math.sin(self.swimTime) * swimAmplitude;
// Beat-synchronized scale pulsing
if (GameState.gameActive && GameState.songStartTime > 0) {
var currentTime = LK.ticks * (1000 / 60);
var songConfig = GameState.getCurrentSongConfig();
var beatInterval = 60000 / songConfig.bpm;
var timeSinceLastBeat = (currentTime - GameState.songStartTime) % beatInterval;
var beatProgress = timeSinceLastBeat / beatInterval;
// Create a pulse effect that peaks at the beat
var scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15; // 15% scale variation
// Determine base scaleX considering direction
var baseScaleXDirection = (self.speed > 0 ? -1 : 1) * self.baseScale;
self.fishGraphics.scaleX = baseScaleXDirection * scalePulse;
self.fishGraphics.scaleY = scalePulse * self.baseScale;
}
if (self.isSpecial) {
// Shimmer effect for rare fish
self.shimmerTime += 0.1;
self.fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2;
} else {
// Reset alpha if not special
self.fishGraphics.alpha = 1.0;
}
}
};
self.catchFish = function () {
self.caught = true;
// Animation: Fish arcs up over the boat, then down into it.
var currentFishX = self.x;
var currentFishY = self.y;
var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X;
var boatLandingY = GAME_CONFIG.BOAT_Y; // Y-coordinate where the fish "lands" in the boat
// Define the peak of the arc - e.g., 150 pixels above the boat's landing spot
var peakArcY = boatLandingY - 150;
// Define the X coordinate at the peak of the arc - halfway between current X and boat center X
var peakArcX = currentFishX + (boatCenterX - currentFishX) * 0.5;
var durationPhase1 = 350; // Duration for the first part of the arc (upwards)
var durationPhase2 = 250; // Duration for the second part of the arc (downwards), total 600ms
// Phase 1: Arc upwards and towards the boat's horizontal center.
// The container 'self' is tweened for position, scale, and alpha.
// Initial self.scale.x and self.scale.y are expected to be 1 for the container.
tween(self, {
x: peakArcX,
y: peakArcY,
scaleX: 0.75,
// Scale container to 75% of its original size
scaleY: 0.75,
alpha: 0.8 // Partially fade
}, {
duration: durationPhase1,
easing: tween.easeOut,
// Ease out for the upward motion to the peak
onFinish: function onFinish() {
// Phase 2: Arc downwards into the boat and disappear.
tween(self, {
x: boatCenterX,
y: boatLandingY,
scaleX: 0.2,
// Shrink container further to 20% of its original size
scaleY: 0.2,
alpha: 0 // Fade out completely
}, {
duration: durationPhase2,
easing: tween.easeIn,
// Ease in for the downward motion into the boat
onFinish: function onFinish() {
self.destroy(); // Remove fish once animation is complete
}
});
}
});
};
return self;
});
var MusicNoteParticle = Container.expand(function (startX, startY) {
var self = Container.call(this);
var FADE_IN_DURATION_MS = 600; // Duration for the note to fade in
var TARGET_ALPHA = 0.6 + Math.random() * 0.4; // Target alpha between 0.6 and 1.0 for variety
self.gfx = self.attachAsset('musicnote', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
// Start invisible for fade-in
scaleX: 0.4 + Math.random() * 0.4 // Random initial scale (0.4x to 0.8x)
});
self.gfx.scaleY = self.gfx.scaleX; // Maintain aspect ratio
self.x = startX;
self.y = startY;
// Animation properties for lazy floating
self.vx = (Math.random() - 0.5) * 0.8; // Slow horizontal drift speed
self.vy = -(0.8 + Math.random() * 0.7); // Steady upward speed (0.8 to 1.5 pixels/frame)
self.rotationSpeed = (Math.random() - 0.5) * 0.008; // Very slow rotation
self.life = 240 + Math.random() * 120; // Lifespan in frames (4 to 6 seconds)
self.age = 0;
self.isDone = false;
// Initial fade-in tween
tween(self.gfx, {
alpha: TARGET_ALPHA
}, {
duration: FADE_IN_DURATION_MS,
easing: tween.easeOut
});
self.update = function () {
if (self.isDone) {
return;
}
self.age++;
self.x += self.vx;
self.y += self.vy;
self.gfx.rotation += self.rotationSpeed;
var FADE_IN_TICKS = FADE_IN_DURATION_MS / (1000 / 60); // Fade-in duration in ticks
// Only manage alpha manually after the fade-in tween is expected to be complete.
if (self.age > FADE_IN_TICKS) {
var lifePortionForFadeOut = 0.6; // Use last 60% of life for fade out
var fadeOutStartTimeTicks = self.life * (1 - lifePortionForFadeOut);
if (self.age >= fadeOutStartTimeTicks && self.life > fadeOutStartTimeTicks) {
// ensure self.life > fadeOutStartTimeTicks to avoid division by zero
var progressInFadeOut = (self.age - fadeOutStartTimeTicks) / (self.life * lifePortionForFadeOut);
self.gfx.alpha = TARGET_ALPHA * (1 - progressInFadeOut);
self.gfx.alpha = Math.max(0, self.gfx.alpha); // Clamp at 0
} else if (self.age <= fadeOutStartTimeTicks) {
// At this point, the initial fade-in tween to TARGET_ALPHA should have completed.
// The alpha value is expected to remain at TARGET_ALPHA (as set by the initial tween)
// until the fade-out logic (in the 'if (self.age >= fadeOutStartTimeTicks)' block) begins.
// The previous 'tween.isTweening' check was removed as the method does not exist in the plugin.
}
}
// Check if particle's life is over or it has faded out
if (self.age >= self.life || self.gfx.alpha !== undefined && self.gfx.alpha <= 0.01 && self.age > FADE_IN_TICKS) {
self.isDone = true;
}
};
return self;
});
var OceanBubbleParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('oceanbubbles', {
anchorX: 0.5,
anchorY: 0.5
});
self.initialX = Math.random() * 2048;
var waterTop = GAME_CONFIG.WATER_SURFACE_Y;
var waterBottom = 2732;
self.x = self.initialX;
// Allow bubbles to spawn anywhere in the water, not just below the bottom
self.y = waterTop + Math.random() * (waterBottom - waterTop);
var baseScale = 0.1 + Math.random() * 0.4; // Scale: 0.1x to 0.5x
self.gfx.scale.set(baseScale);
self.vy = -(0.25 + Math.random() * 0.5); // Upward speed: 0.25 to 0.75 pixels/frame (slower, less variance)
self.naturalVy = self.vy; // Store natural velocity for recovery
self.driftAmplitude = 20 + Math.random() * 40; // Sideways drift: 20px to 60px amplitude
self.naturalDriftAmplitude = self.driftAmplitude; // Store natural drift for recovery
self.driftFrequency = (0.005 + Math.random() * 0.015) * (Math.random() < 0.5 ? 1 : -1); // Sideways drift speed/direction
self.driftPhase = Math.random() * Math.PI * 2; // Initial phase for sine wave
self.rotationSpeed = (Math.random() - 0.5) * 0.01; // Slow random rotation
var targetAlpha = 0.2 + Math.random() * 0.3; // Max alpha: 0.2 to 0.5 (dimmer background bubbles)
self.gfx.alpha = 0; // Start transparent
self.isDone = false;
self.fadingOut = false;
tween(self.gfx, {
alpha: targetAlpha
}, {
duration: 1000 + Math.random() * 1000,
// Slow fade in: 1 to 2 seconds
easing: tween.easeIn
});
self.update = function () {
if (self.isDone) {
return;
}
self.y += self.vy;
// Increment age
self.age++;
// Check if lifespan exceeded
if (!self.fadingOut && self.age >= self.lifespan) {
self.fadingOut = true;
tween.stop(self.gfx);
tween(self.gfx, {
alpha: 0
}, {
duration: 600 + Math.random() * 400,
// 0.6-1 second fade
easing: tween.easeOut,
onFinish: function onFinish() {
self.isDone = true;
}
});
}
self.driftPhase += self.driftFrequency;
self.x = self.initialX + Math.sin(self.driftPhase) * self.driftAmplitude;
self.gfx.rotation += self.rotationSpeed;
// Recovery mechanism: gradually return to natural upward movement
var naturalVy = -(0.25 + Math.random() * 0.5); // Natural upward speed
var recoveryRate = 0.02; // How quickly bubble recovers (2% per frame)
// If bubble is moving slower than its natural speed or downward, recover
if (self.vy > naturalVy) {
self.vy = self.vy + (naturalVy - self.vy) * recoveryRate;
}
// Also gradually reduce excessive drift amplitude back to normal
var normalDriftAmplitude = 20 + Math.random() * 40;
if (self.driftAmplitude > normalDriftAmplitude) {
self.driftAmplitude = self.driftAmplitude + (normalDriftAmplitude - self.driftAmplitude) * recoveryRate;
}
// Check if bubble reached surface or went off-screen
// Use gfx.height * current scale for accurate boundary check
var currentHeight = self.gfx.height * self.gfx.scale.y;
var currentWidth = self.gfx.width * self.gfx.scale.x;
if (!self.fadingOut && self.y <= GAME_CONFIG.WATER_SURFACE_Y - currentHeight * 0.5) {
self.fadingOut = true;
tween.stop(self.gfx); // Stop fade-in if ongoing
tween(self.gfx, {
alpha: 0
}, {
duration: 300 + Math.random() * 200,
// Quick fade out
easing: tween.easeOut,
onFinish: function onFinish() {
self.isDone = true; // Mark as fully done for removal
}
});
} else if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth)) {
// Off screen top/sides before reaching surface and starting fade
self.isDone = true;
self.gfx.alpha = 0; // Disappear immediately
}
};
return self;
});
var SeaweedParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('kelp', {
anchorX: 0.5,
anchorY: 0.5
});
// Determine spawn location
var spawnType = Math.random();
var waterTop = GAME_CONFIG.WATER_SURFACE_Y;
var waterBottom = 2732;
if (spawnType < 0.4) {
// 40% spawn from bottom
self.x = Math.random() * 2048;
self.y = waterBottom + 50;
self.vx = (Math.random() - 0.5) * 0.3; // Slight horizontal drift
self.vy = -(0.4 + Math.random() * 0.3); // Upward movement
} else if (spawnType < 0.7) {
// 30% spawn from left
self.x = -50;
self.y = waterTop + Math.random() * (waterBottom - waterTop);
self.vx = 0.4 + Math.random() * 0.3; // Rightward movement
self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift
} else {
// 30% spawn from right
self.x = 2048 + 50;
self.y = waterTop + Math.random() * (waterBottom - waterTop);
self.vx = -(0.4 + Math.random() * 0.3); // Leftward movement
self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift
}
self.initialX = self.x;
self.naturalVx = self.vx; // Store natural velocity for recovery
self.naturalVy = self.vy;
// Seaweed properties
var baseScale = 0.6 + Math.random() * 0.6; // Scale: 0.6x to 1.2x
self.gfx.scale.set(baseScale);
self.swayAmplitude = 15 + Math.random() * 25; // Sway: 15px to 40px
self.swayFrequency = (0.003 + Math.random() * 0.007) * (Math.random() < 0.5 ? 1 : -1);
self.swayPhase = Math.random() * Math.PI * 2;
// Random initial rotation (full 360 degrees)
self.gfx.rotation = Math.random() * Math.PI * 2;
// Random continuous rotation speed (slower than ocean bubbles)
self.continuousRotationSpeed = (Math.random() - 0.5) * 0.003; // -0.0015 to 0.0015 radians per frame
var targetAlpha = 0.3 + Math.random() * 0.3; // Alpha: 0.3 to 0.6
self.gfx.alpha = 0; // Start transparent
self.isDone = false;
self.fadingOut = false;
self.reachedSurface = false;
// Add random lifespan (10-30 seconds)
self.lifespan = 600 + Math.random() * 1200; // 600-1800 frames (10-30 seconds at 60fps)
self.age = 0;
tween(self.gfx, {
alpha: targetAlpha
}, {
duration: 1500 + Math.random() * 1000,
easing: tween.easeIn
});
self.update = function () {
if (self.isDone) {
return;
}
// Apply movement
self.x += self.vx;
self.y += self.vy;
// Add sway effect
self.swayPhase += self.swayFrequency;
var swayOffset = Math.sin(self.swayPhase) * self.swayAmplitude;
// Apply continuous rotation plus sway-based rotation
self.gfx.rotation += self.continuousRotationSpeed + swayOffset * 0.0001; // Reduced sway rotation influence
// Recovery mechanism for velocity
var recoveryRate = 0.015;
if (self.vx !== self.naturalVx) {
self.vx = self.vx + (self.naturalVx - self.vx) * recoveryRate;
}
if (self.vy !== self.naturalVy) {
self.vy = self.vy + (self.naturalVy - self.vy) * recoveryRate;
}
// Check if reached surface
var currentHeight = self.gfx.height * self.gfx.scale.y;
var currentWidth = self.gfx.width * self.gfx.scale.x;
if (!self.reachedSurface && self.y <= GAME_CONFIG.WATER_SURFACE_Y + currentHeight * 0.3) {
self.reachedSurface = true;
// Change to horizontal drift at surface
self.vy = 0;
self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.5 + Math.random() * 0.5);
self.naturalVx = self.vx;
self.naturalVy = 0;
}
// Check if off-screen
if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth || self.y > waterBottom + currentHeight)) {
self.fadingOut = true;
tween.stop(self.gfx);
tween(self.gfx, {
alpha: 0
}, {
duration: 400 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isDone = true;
}
});
}
};
return self;
});
/****
* Initialize Game
****/
/****
* Screen Containers
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
// Constants for Title Screen Animation
var TITLE_ANIM_CONSTANTS = {
INITIAL_GROUP_ALPHA: 0,
FINAL_GROUP_ALPHA: 1,
INITIAL_UI_ALPHA: 0,
FINAL_UI_ALPHA: 1,
INITIAL_GROUP_SCALE: 3.5,
// Start with an extreme closeup
FINAL_GROUP_SCALE: 2.8,
// Zoom out slightly, staying closer
GROUP_ANIM_DURATION: 4000,
// Slower duration for group fade-in and zoom
TEXT_FADE_DURATION: 1000,
// Duration for title text fade-in
BUTTON_FADE_DURATION: 800,
// Duration for buttons fade-in
// Positioning constants relative to titleAnimationGroup's origin (0,0) which is boat's center
BOAT_ANCHOR_X: 0.5,
BOAT_ANCHOR_Y: 0.5,
FISHERMAN_ANCHOR_X: 0.5,
FISHERMAN_ANCHOR_Y: 0.9,
// Anchor at feet
FISHERMAN_X_OFFSET: -20,
// Relative to boat center
FISHERMAN_Y_OFFSET: -100,
// Relative to boat center, fisherman sits on boat
LINE_ANCHOR_X: 0.5,
LINE_ANCHOR_Y: 0,
// Anchor at top of line
LINE_X_OFFSET_FROM_FISHERMAN: 70,
// Rod tip X from fisherman center
LINE_Y_OFFSET_FROM_FISHERMAN: -130,
// Rod tip Y from fisherman center (fisherman height ~200, anchorY 0.9)
HOOK_ANCHOR_X: 0.5,
HOOK_ANCHOR_Y: 0.5,
HOOK_Y_DEPTH_FROM_LINE_START: 700,
// Slightly longer line for the closeup
// titleAnimationGroup positioning
GROUP_PIVOT_X: 0,
// Boat's X in the group
GROUP_PIVOT_Y: 0,
// Boat's Y in the group
GROUP_INITIAL_Y_SCREEN_OFFSET: -450 // Adjusted for closer initial zoom
};
// If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up.
/****
* Pattern Generation System
****/
var PatternGenerator = {
lastLane: -1,
minDistanceBetweenFish: 300,
// Used by spawnFish internal check
// Minimum X distance between fish for visual clarity on hook
lastActualSpawnTime: -100000,
// Time of the last actual fish spawn
getNextLane: function getNextLane() {
if (this.lastLane === -1) {
// First fish, start in middle lane
this.lastLane = 1;
return 1;
}
// Prefer staying in same lane or moving to adjacent lane
var possibleLanes = [this.lastLane];
// Add adjacent lanes
if (this.lastLane > 0) {
possibleLanes.push(this.lastLane - 1);
}
if (this.lastLane < 2) {
possibleLanes.push(this.lastLane + 1);
}
// 70% chance to stay in same/adjacent lane
if (Math.random() < 0.7) {
this.lastLane = possibleLanes[Math.floor(Math.random() * possibleLanes.length)];
} else {
// 30% chance for any lane
this.lastLane = Math.floor(Math.random() * 3);
}
return this.lastLane;
},
// New method: Checks if enough time has passed since the last spawn.
canSpawnFishOnBeat: function canSpawnFishOnBeat(currentTime, configuredSpawnInterval) {
var timeSinceLast = currentTime - this.lastActualSpawnTime;
var minRequiredGap = configuredSpawnInterval; // Default gap is the song's beat interval for fish
return timeSinceLast >= minRequiredGap;
},
// New method: Registers details of the fish that was just spawned.
registerFishSpawn: function registerFishSpawn(spawnTime) {
this.lastActualSpawnTime = spawnTime;
},
reset: function reset() {
this.lastLane = -1;
this.lastActualSpawnTime = -100000; // Set far in the past to allow first spawn
}
};
/****
* Game Configuration
****/
game.up = function (x, y, obj) {
// Note: We don't play buttonClick sound on 'up' typically, only on 'down'.
switch (GameState.currentScreen) {
case 'title':
// title screen up actions (if any)
break;
case 'levelSelect':
// level select screen up actions (if any, usually 'down' is enough for buttons)
break;
case 'fishing':
handleFishingInput(x, y, false); // false for isUp
break;
case 'results':
// results screen up actions (if any)
break;
}
};
var GAME_CONFIG = {
SCREEN_CENTER_X: 1024,
SCREEN_CENTER_Y: 900,
// Adjusted, though less critical with lanes
BOAT_Y: 710,
// Original 300 + 273 (10%) + 137 (5%) = 710
WATER_SURFACE_Y: 760,
// Original 350 + 273 (10%) + 137 (5%) = 760
// 3 Lane System
// Y-positions for each lane, adjusted downwards further
LANES: [{
y: 1133,
// Top lane Y: (723 + 273) + 137 = 996 + 137 = 1133
name: "shallow"
}, {
y: 1776,
// Middle lane Y: (996 + 643) + 137 = 1639 + 137 = 1776
name: "medium"
}, {
y: 2419,
// Bottom lane Y: (1639 + 643) + 137 = 2282 + 137 = 2419
name: "deep"
}],
// Timing windows
PERFECT_WINDOW: 40,
GOOD_WINDOW: 80,
MISS_WINDOW: 120,
// Depth levels - reduced money values!
DEPTHS: [{
level: 1,
name: "Shallow Waters",
fishSpeed: 6,
fishValue: 1,
upgradeCost: 0,
// Starting depth
songs: [{
name: "Gentle Waves",
bpm: 90,
duration: 202000,
// 3:22
pattern: "gentle_waves_custom",
cost: 0
}, {
name: "Morning Tide",
bpm: 90,
duration: 156827,
pattern: "morning_tide_custom",
cost: 0,
musicId: 'morningtide'
}, {
name: "Sunny Afternoon",
bpm: 97,
duration: 181800,
// 3:01.8
pattern: "sunny_afternoon_custom",
cost: 0,
musicId: 'sunnyafternoon'
}]
}, {
level: 2,
name: "Mid Waters",
fishSpeed: 7,
fishValue: 2,
upgradeCost: 100,
songs: [{
name: "Ocean Current",
bpm: 120,
duration: 90000,
pattern: "medium",
cost: 0
}, {
name: "Deep Flow",
bpm: 125,
duration: 100000,
pattern: "medium",
cost: 150
}]
}, {
level: 3,
name: "Deep Waters",
fishSpeed: 8,
fishValue: 3,
upgradeCost: 400,
songs: [{
name: "Storm Surge",
bpm: 140,
duration: 120000,
pattern: "complex",
cost: 0
}, {
name: "Whirlpool",
bpm: 150,
duration: 135000,
pattern: "complex",
cost: 300
}]
}, {
level: 4,
name: "Abyss",
fishSpeed: 9,
fishValue: 6,
upgradeCost: 1000,
songs: [{
name: "Leviathan",
bpm: 160,
duration: 150000,
pattern: "expert",
cost: 0
}, {
name: "Deep Trench",
bpm: 170,
duration: 180000,
pattern: "expert",
cost: 600
}]
}],
// Updated patterns
PATTERNS: {
simple: {
beatsPerFish: 2,
// Increased from 1 - fish every 2 beats
doubleSpawnChance: 0.10,
// 10% chance of a double beat in simple
rareSpawnChance: 0.02
},
medium: {
beatsPerFish: 1.5,
// Increased from 0.75
doubleSpawnChance: 0.15,
//{1R} // 15% chance of a double beat
rareSpawnChance: 0.05
},
complex: {
beatsPerFish: 1,
// Increased from 0.5
doubleSpawnChance: 0.25,
//{1U} // 25% chance of a double beat
rareSpawnChance: 0.08
},
expert: {
beatsPerFish: 0.75,
// Increased from 0.25
doubleSpawnChance: 0.35,
//{1X} // 35% chance of a double beat
tripletSpawnChance: 0.20,
// 20% chance a double beat becomes a triplet
rareSpawnChance: 0.12
},
gentle_waves_custom: {
beatsPerFish: 1.5,
// Slightly more frequent than default simple
doubleSpawnChance: 0.05,
// Very rare double beats for shallow
rareSpawnChance: 0.01,
// Almost no rare fish
// Custom timing sections based on the musical structure
sections: [
// Opening - Simple chord pattern (0-30 seconds)
{
startTime: 0,
endTime: 30000,
spawnModifier: 1.0,
// Normal spawn rate
description: "steady_chords"
},
// Melody Introduction (30-60 seconds)
{
startTime: 30000,
endTime: 60000,
spawnModifier: 0.9,
// Slightly fewer fish
description: "simple_melody"
},
// Development (60-120 seconds) - Gets a bit busier
{
startTime: 60000,
endTime: 120000,
spawnModifier: 1.1,
// Slightly more fish
description: "melody_development"
},
// Climax (120-180 seconds) - Busiest section but still shallow
{
startTime: 120000,
endTime: 180000,
spawnModifier: 1.3,
// More fish, but not overwhelming
description: "gentle_climax"
},
// Ending (180-202 seconds) - Calming down
{
startTime: 180000,
endTime: 202000,
spawnModifier: 0.8,
// Fewer fish for gentle ending
description: "peaceful_ending"
}]
},
morning_tide_custom: {
beatsPerFish: 1.2,
// More frequent than gentle_waves_custom (1.5) and much more than simple (2)
doubleSpawnChance: 0.12,
// Higher than simple (0.10)
rareSpawnChance: 0.03,
// Higher than simple (0.02)
// Custom timing sections based on musical structure
sections: [
// Gentle opening - ease into the song (0-25 seconds)
{
startTime: 0,
endTime: 25000,
spawnModifier: 0.9,
// Slightly reduced for intro
description: "calm_opening"
},
// Building energy - first wave (25-50 seconds)
{
startTime: 25000,
endTime: 50000,
spawnModifier: 1.2,
// More active
description: "first_wave"
},
// Peak intensity - morning rush (50-80 seconds)
{
startTime: 50000,
endTime: 80000,
spawnModifier: 1.5,
// Most intense section
description: "morning_rush"
},
// Sustained energy - second wave (80-110 seconds)
{
startTime: 80000,
endTime: 110000,
spawnModifier: 1.3,
// High but slightly less than peak
description: "second_wave"
},
// Climactic finish (110-140 seconds)
{
startTime: 110000,
endTime: 140000,
spawnModifier: 1.4,
// Building back up
description: "climactic_finish"
},
// Gentle fade out (140-156.8 seconds)
{
startTime: 140000,
endTime: 156827,
spawnModifier: 0.8,
// Calm ending
description: "peaceful_fade"
}]
},
sunny_afternoon_custom: {
beatsPerFish: 1.3,
// Slightly faster than morning_tide_custom but still beginner-friendly
doubleSpawnChance: 0.08,
// Moderate chance for variety
rareSpawnChance: 0.025,
// Slightly better rewards than gentle_waves
sections: [
// Gentle warm-up (0-20 seconds)
{
startTime: 0,
endTime: 20000,
spawnModifier: 0.8,
description: "warm_sunny_start"
},
// First activity burst (20-35 seconds)
{
startTime: 20000,
endTime: 35000,
spawnModifier: 1.4,
description: "first_sunny_burst"
},
// Breathing room (35-50 seconds)
{
startTime: 35000,
endTime: 50000,
spawnModifier: 0.7,
description: "sunny_breather_1"
},
// Second activity burst (50-70 seconds)
{
startTime: 50000,
endTime: 70000,
spawnModifier: 1.5,
description: "second_sunny_burst"
},
// Extended breathing room (70-90 seconds)
{
startTime: 70000,
endTime: 90000,
spawnModifier: 0.6,
description: "sunny_breather_2"
},
// Third activity burst (90-110 seconds)
{
startTime: 90000,
endTime: 110000,
spawnModifier: 1.3,
description: "third_sunny_burst"
},
// Breathing room (110-125 seconds)
{
startTime: 110000,
endTime: 125000,
spawnModifier: 0.8,
description: "sunny_breather_3"
},
// Final activity section (125-150 seconds)
{
startTime: 125000,
endTime: 150000,
spawnModifier: 1.2,
description: "sunny_finale_buildup"
},
// Wind down (150-181.8 seconds)
{
startTime: 150000,
endTime: 181800,
spawnModifier: 0.9,
description: "sunny_afternoon_fade"
}]
}
}
};
/****
* Game State Management
****/
var MULTI_BEAT_SPAWN_DELAY_MS = 250; // ms delay for sequential spawns in multi-beats (increased for more space)
var TRIPLET_BEAT_SPAWN_DELAY_MS = 350; // ms delay for third fish in a triplet (even more space)
var FISH_SPAWN_END_BUFFER_MS = 500; // Just 0.5 seconds buffer
var ImprovedRhythmSpawner = {
nextBeatToSchedule: 1,
scheduledBeats: [],
update: function update(currentTime) {
if (!GameState.gameActive || GameState.songStartTime === 0) {
return;
}
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
var beatInterval = 60000 / songConfig.bpm;
var spawnInterval = beatInterval * pattern.beatsPerFish;
// Calculate how far ahead to look (use original logic)
var depthConfig = GameState.getCurrentDepthConfig();
var fishSpeed = Math.abs(depthConfig.fishSpeed);
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
var travelTimeMs = distanceToHook / fishSpeed * (1000 / 60);
var beatsAhead = Math.ceil(travelTimeMs / spawnInterval) + 2;
// Schedule beats ahead
var songElapsed = currentTime - GameState.songStartTime;
var currentSongBeat = songElapsed / spawnInterval;
var maxBeatToSchedule = Math.floor(currentSongBeat) + beatsAhead;
while (this.nextBeatToSchedule <= maxBeatToSchedule) {
if (this.scheduledBeats.indexOf(this.nextBeatToSchedule) === -1) {
this.scheduleBeatFish(this.nextBeatToSchedule, spawnInterval, travelTimeMs);
this.scheduledBeats.push(this.nextBeatToSchedule);
}
this.nextBeatToSchedule++;
}
// NO FISH SYNCING - let them move naturally!
},
scheduleBeatFish: function scheduleBeatFish(beatNumber, spawnInterval, travelTimeMs) {
var targetArrivalTime = GameState.songStartTime + beatNumber * spawnInterval;
var songConfig = GameState.getCurrentSongConfig();
// FIXED: The buffer should be applied to when the song ACTUALLY ends,
// not when the fish arrives at the hook
var songEndTime = GameState.songStartTime + songConfig.duration;
var lastValidArrivalTime = songEndTime - FISH_SPAWN_END_BUFFER_MS;
if (songConfig && GameState.songStartTime > 0 && targetArrivalTime > lastValidArrivalTime) {
return; // Don't schedule this fish, it would arrive too close to song end
}
var spawnTime = targetArrivalTime - travelTimeMs;
var currentTime = LK.ticks * (1000 / 60);
// Rest of function stays the same...
if (spawnTime >= currentTime - 100) {
var delay = Math.max(0, spawnTime - currentTime);
var self = this;
LK.setTimeout(function () {
if (GameState.gameActive && GameState.songStartTime !== 0) {
self.spawnRhythmFish(beatNumber, targetArrivalTime);
}
}, delay);
}
},
spawnRhythmFish: function spawnRhythmFish(beatNumber, targetArrivalTime) {
if (!GameState.gameActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60);
var depthConfig = GameState.getCurrentDepthConfig();
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
// Calculate spawnInterval for this pattern
var beatInterval = 60000 / songConfig.bpm;
var spawnInterval = beatInterval * pattern.beatsPerFish;
// Apply section modifiers if pattern has sections
var spawnModifier = 1.0;
if (pattern.sections) {
var songElapsed = currentTime - GameState.songStartTime;
for (var s = 0; s < pattern.sections.length; s++) {
var section = pattern.sections[s];
if (songElapsed >= section.startTime && songElapsed <= section.endTime) {
spawnModifier = section.spawnModifier;
break;
}
}
}
// Check if we should spawn
if (!PatternGenerator.canSpawnFishOnBeat(currentTime, beatInterval)) {
return;
}
// Apply spawn modifier chance
if (Math.random() > spawnModifier) {
return;
}
var laneIndex = PatternGenerator.getNextLane();
var targetLane = GAME_CONFIG.LANES[laneIndex];
// Fish type selection (original logic)
var fishType, fishValue;
var rand = Math.random();
if (rand < pattern.rareSpawnChance) {
fishType = 'rare';
fishValue = Math.floor(depthConfig.fishValue * 4);
} else if (GameState.selectedDepth >= 2 && rand < 0.3) {
fishType = 'deep';
fishValue = Math.floor(depthConfig.fishValue * 2);
} else if (GameState.selectedDepth >= 1 && rand < 0.6) {
fishType = 'medium';
fishValue = Math.floor(depthConfig.fishValue * 1.5);
} else {
fishType = 'shallow';
fishValue = Math.floor(depthConfig.fishValue);
}
// Calculate precise speed to arrive exactly on beat
var timeRemainingMs = targetArrivalTime - currentTime;
if (timeRemainingMs <= 0) {
return;
}
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
var spawnSide = Math.random() < 0.5 ? -1 : 1;
// Calculate frames remaining and required speed per frame
var framesRemaining = timeRemainingMs / (1000 / 60);
if (framesRemaining <= 0) {
return;
}
var requiredSpeedPerFrame = distanceToHook / framesRemaining;
if (spawnSide === 1) {
requiredSpeedPerFrame *= -1; // Moving left
}
// Create fish with exact calculated speed - NO SYNC DATA
var newFish = new Fish(fishType, fishValue, requiredSpeedPerFrame, laneIndex);
newFish.spawnSide = spawnSide;
newFish.targetArrivalTime = targetArrivalTime;
newFish.x = requiredSpeedPerFrame > 0 ? -150 : 2048 + 150;
newFish.y = targetLane.y;
newFish.baseY = targetLane.y;
newFish.lastX = newFish.x; // Initialize lastX for miss detection
// newFish.missed is false by default from constructor
// Don't add any syncData - let fish move naturally!
fishArray.push(newFish);
fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTime);
// Handle multi-spawns
this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime);
return newFish;
},
handleMultiSpawns: function handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime) {
if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) {
var travelTimeMs = this.calculateTravelTime();
var nextFishBeat = beatNumber + 1;
if (this.scheduledBeats.indexOf(nextFishBeat) === -1) {
this.scheduleBeatFish(nextFishBeat, spawnInterval, travelTimeMs);
this.scheduledBeats.push(nextFishBeat);
}
// Handle triplets
if (pattern.tripletSpawnChance && pattern.tripletSpawnChance > 0 && Math.random() < pattern.tripletSpawnChance) {
var thirdFishBeat = beatNumber + 2;
if (this.scheduledBeats.indexOf(thirdFishBeat) === -1) {
this.scheduleBeatFish(thirdFishBeat, spawnInterval, travelTimeMs);
this.scheduledBeats.push(thirdFishBeat);
}
}
}
},
calculateTravelTime: function calculateTravelTime() {
var depthConfig = GameState.getCurrentDepthConfig();
var fishSpeed = Math.abs(depthConfig.fishSpeed);
if (fishSpeed === 0) {
return Infinity;
}
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
return distanceToHook / fishSpeed * (1000 / 60);
},
reset: function reset() {
this.nextBeatToSchedule = 1;
this.scheduledBeats = [];
},
getDebugInfo: function getDebugInfo() {
return {
nextBeat: this.nextBeatToSchedule,
scheduledCount: this.scheduledBeats.length,
scheduledBeatsPreview: this.scheduledBeats.slice(-10)
};
}
};
/****
* Tutorial System - Global Scope
****/
function updateLaneBracketsVisuals() {
if (laneBrackets && laneBrackets.length === GAME_CONFIG.LANES.length) {
for (var i = 0; i < laneBrackets.length; i++) {
var isActiveLane = i === GameState.hookTargetLaneIndex;
var targetAlpha = isActiveLane ? 0.9 : 0.5;
if (laneBrackets[i] && laneBrackets[i].left && !laneBrackets[i].left.destroyed) {
if (laneBrackets[i].left.alpha !== targetAlpha) {
laneBrackets[i].left.alpha = targetAlpha;
}
}
if (laneBrackets[i] && laneBrackets[i].right && !laneBrackets[i].right.destroyed) {
if (laneBrackets[i].right.alpha !== targetAlpha) {
laneBrackets[i].right.alpha = targetAlpha;
}
}
}
}
}
function createTutorialElements() {
tutorialOverlayContainer.removeChildren(); // Clear previous elements
tutorialTextBackground = tutorialOverlayContainer.addChild(LK.getAsset('screenBackground', {
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 2732 * 0.85,
// Position towards the bottom
width: 1800,
height: 450,
// Increased height
color: 0x000000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.75
}));
tutorialTextDisplay = tutorialOverlayContainer.addChild(new Text2('', {
size: 55,
// Increased font size
fill: 0xFFFFFF,
wordWrap: true,
wordWrapWidth: 1700,
// Keep wordWrapWidth, adjust if lines are too cramped
// Slightly less than background width
align: 'center',
lineHeight: 65 // Increased line height
}));
tutorialTextDisplay.anchor.set(0.5, 0.5);
tutorialTextDisplay.x = tutorialTextBackground.x;
tutorialTextDisplay.y = tutorialTextBackground.y - 50; // Adjusted Y for new background height and text size
tutorialContinueButton = tutorialOverlayContainer.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: tutorialTextBackground.x,
y: tutorialTextBackground.y + tutorialTextBackground.height / 2 - 55,
// Adjusted Y for new background height
// Positioned at the bottom of the text background
tint: 0x1976d2,
// Blue color
width: 350,
// Standard button size
height: 70
}));
tutorialContinueText = tutorialOverlayContainer.addChild(new Text2('CONTINUE', {
// Changed text
size: 34,
fill: 0xFFFFFF
}));
tutorialContinueText.anchor.set(0.5, 0.5);
tutorialContinueText.x = tutorialContinueButton.x;
tutorialContinueText.y = tutorialContinueButton.y;
tutorialOverlayContainer.visible = false; // Initially hidden
}
function setTutorialText(newText, showContinue) {
if (showContinue === undefined) {
showContinue = true;
}
if (!tutorialTextDisplay || !tutorialContinueButton || !tutorialContinueText) {
createTutorialElements(); // Ensure elements exist
}
tutorialTextDisplay.setText(newText);
tutorialContinueButton.visible = showContinue;
tutorialContinueText.visible = showContinue;
tutorialOverlayContainer.visible = true;
}
function spawnTutorialFishHelper(config) {
var fishType = config.type || 'shallow';
// Use a consistent depth config for tutorial fish, e.g., shallowest
var depthConfig = GAME_CONFIG.DEPTHS[0];
var fishValue = Math.floor(depthConfig.fishValue / 2); // Tutorial fish might be worth less or nothing
var baseSpeed = depthConfig.fishSpeed;
var speedMultiplier = config.speedMultiplier || 0.5; // Slower for tutorial
var laneIndex = config.lane !== undefined ? config.lane : 1;
var spawnSide = config.spawnSide !== undefined ? config.spawnSide : Math.random() < 0.5 ? -1 : 1;
var actualFishSpeed = Math.abs(baseSpeed) * speedMultiplier * spawnSide;
var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex);
newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen
newFish.y = GAME_CONFIG.LANES[laneIndex].y + (config.yOffset || 0);
newFish.baseY = newFish.y;
newFish.lastX = newFish.x;
newFish.tutorialFish = true; // Custom flag
fishArray.push(newFish); // Add to global fishArray for rendering by main loop
fishingScreen.addChild(newFish); // Add to fishingScreen for visibility
return newFish;
}
function runTutorialStep() {
GameState.tutorialPaused = false;
GameState.tutorialAwaitingTap = false; // Will be set by steps if needed for "tap screen"
// Clear lane highlights from previous step if any
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
if (tutorialContinueButton) {
tutorialContinueButton.visible = true;
} // Default to visible
if (tutorialContinueText) {
tutorialContinueText.visible = true;
}
// Clear previous tutorial fish unless current step needs it (now steps 3 and 4)
if (GameState.tutorialFish && GameState.tutorialStep !== 3 && GameState.tutorialStep !== 4) {
if (!GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish = null;
}
// Ensure fishing screen elements are visually active
// Adjusted max step for animations to 8 (new end tutorial step is 7)
if (fishingElements) {
if (typeof fishingElements.startWaterSurfaceAnimation === 'function' && GameState.tutorialStep < 8) {
fishingElements.startWaterSurfaceAnimation();
}
if (typeof fishingElements.startBoatAndFishermanAnimation === 'function' && GameState.tutorialStep < 8) {
fishingElements.startBoatAndFishermanAnimation();
}
if (fishingElements.hook && GameState.tutorialStep < 8) {
fishingElements.hook.y = GAME_CONFIG.LANES[1].y; // Reset hook
GameState.hookTargetLaneIndex = 1;
}
}
switch (GameState.tutorialStep) {
case 0:
// Welcome
setTutorialText("Welcome to Beat Fisher! Let's learn the basics. Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 1:
// The Hook Explanation
setTutorialText("This is your hook. It automatically moves to the lane with the closest approaching fish. Tap 'CONTINUE'.");
if (fishingElements.hook && !fishingElements.hook.destroyed) {
tween(fishingElements.hook.scale, {
x: 1.2,
y: 1.2
}, {
duration: 250,
onFinish: function onFinish() {
if (fishingElements.hook && !fishingElements.hook.destroyed) {
tween(fishingElements.hook.scale, {
x: 1,
y: 1
}, {
duration: 250
});
}
}
});
}
GameState.tutorialPaused = true;
break;
case 2:
// Lanes & Tapping Explanation with Overlay
setTutorialText("Fish swim in three lanes. When a fish is over the hook in its lane, TAP THE SCREEN in that lane to catch it. The lanes are highlighted. Tap 'CONTINUE'.");
// Add highlights behind the text box elements
for (var i = 0; i < GAME_CONFIG.LANES.length; i++) {
var laneYPos = GAME_CONFIG.LANES[i].y;
var highlight = LK.getAsset('laneHighlight', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: laneYPos,
alpha: 0.25,
// Semi-transparent
width: 1800,
height: 200
});
tutorialOverlayContainer.addChildAt(highlight, 0); // Add at the bottom of the container
tutorialLaneHighlights.push(highlight);
}
GameState.tutorialPaused = true;
break;
case 3:
// Was case 2
// Fish Approaching & First Catch Attempt
setTutorialText("A fish will now approach. Tap in its lane when it's under the hook!");
tutorialContinueButton.visible = false; // No continue button, action is tapping screen
tutorialContinueText.visible = false;
GameState.tutorialPaused = false; // Fish moves
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
GameState.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.35,
lane: 1
});
break;
case 4:
// Was case 3
// Perfect/Good Timing (after first successful catch)
setTutorialText("Great! Timing is key. 'Perfect' or 'Good' catches earn more. Try to catch this next fish!");
tutorialContinueButton.visible = false;
tutorialContinueText.visible = false;
GameState.tutorialPaused = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
GameState.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.45,
lane: 1
});
break;
case 5:
// Was case 4
// Combos
setTutorialText("Nice one! Catch fish consecutively to build a COMBO for bonus points! Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 6:
// Was case 5
// Music & Rhythm
setTutorialText("Fish will approach the hook on the beat with the music's rhythm. Listen to the beat! Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 7:
// Was case 6
// End Tutorial
setTutorialText("You're all set! Tap 'CONTINUE' to go to the fishing spots!");
GameState.tutorialPaused = true;
break;
default:
// Effectively step 8
// Tutorial finished
GameState.tutorialMode = false;
tutorialOverlayContainer.visible = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
var idxDefault = fishArray.indexOf(GameState.tutorialFish);
if (idxDefault > -1) {
fishArray.splice(idxDefault, 1);
}
GameState.tutorialFish = null;
}
// Clear lane highlights if any persist
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
// Clean up fishing screen elements explicitly if they were started by tutorial
if (fishingElements && fishingElements.boat && !fishingElements.boat.destroyed) {
tween.stop(fishingElements.boat);
}
if (fishingElements && fishingElements.fishermanContainer && !fishingElements.fishermanContainer.destroyed) {
tween.stop(fishingElements.fishermanContainer);
}
// Stop water surface animations
if (fishingElements && fishingElements.waterSurfaceSegments) {
fishingElements.waterSurfaceSegments.forEach(function (segment) {
if (segment && !segment.destroyed) {
tween.stop(segment);
}
});
}
showScreen('levelSelect');
break;
}
}
function startTutorial() {
GameState.tutorialMode = true;
GameState.tutorialStep = 0;
GameState.gameActive = false;
// Ensure fishing screen is visible and setup for tutorial
showScreen('fishing');
fishingScreen.alpha = 1;
// Clear any active game elements from a previous session
fishArray.forEach(function (f) {
if (f && !f.destroyed) {
f.destroy();
}
});
fishArray = [];
ImprovedRhythmSpawner.reset();
// Clear existing UI text
if (fishingElements.scoreText) {
fishingElements.scoreText.setText('');
}
if (fishingElements.fishText) {
fishingElements.fishText.setText('');
}
if (fishingElements.comboText) {
fishingElements.comboText.setText('');
}
if (fishingElements.progressText) {
fishingElements.progressText.setText('');
}
// CREATE LANE BRACKETS FOR TUTORIAL
var bracketAssetHeight = 150;
var bracketAssetWidth = 75;
// Clear any existing brackets first
if (laneBrackets && laneBrackets.length > 0) {
laneBrackets.forEach(function (bracketPair) {
if (bracketPair.left && !bracketPair.left.destroyed) {
bracketPair.left.destroy();
}
if (bracketPair.right && !bracketPair.right.destroyed) {
bracketPair.right.destroy();
}
});
}
laneBrackets = [];
if (fishingScreen && !fishingScreen.destroyed) {
for (var i = 0; i < GAME_CONFIG.LANES.length; i++) {
var laneY = GAME_CONFIG.LANES[i].y;
var leftBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
x: bracketAssetWidth / 2,
y: laneY,
height: bracketAssetHeight
}));
var rightBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
scaleX: -1,
x: 2048 - bracketAssetWidth / 2,
y: laneY,
height: bracketAssetHeight
}));
laneBrackets.push({
left: leftBracket,
right: rightBracket
});
}
}
// Don't create tutorial elements here - wait for intro to finish
// createTutorialElements() and runTutorialStep() will be called after intro
}
// This function is called from game.update when tutorialFish exists
function checkTutorialFishState() {
var fish = GameState.tutorialFish;
if (!fish || fish.destroyed || fish.caught || fish.missed) {
return;
}
var hookX = fishingElements.hook.x;
// Fish passes hook without interaction during catch steps (now steps 3 or 4)
if (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) {
var passedHook = fish.speed > 0 && fish.x > hookX + GAME_CONFIG.MISS_WINDOW + fish.fishGraphics.width / 2 ||
// fish right edge passed hook right boundary
fish.speed < 0 && fish.x < hookX - GAME_CONFIG.MISS_WINDOW - fish.fishGraphics.width / 2; // fish left edge passed hook left boundary
if (passedHook) {
fish.missed = true; // Mark to prevent further attempts on this specific fish instance
GameState.tutorialPaused = true; // Pause to show message
setTutorialText("It got away! Tap 'CONTINUE' to try that part again.");
// Next tap on "CONTINUE" will call runTutorialStep(), which will re-run current step.
}
}
// Generic off-screen removal for tutorial fish
if (fish.x < -250 || fish.x > 2048 + 250) {
var wasCriticalStep = GameState.tutorialStep === 3 || GameState.tutorialStep === 4; // Adjusted step numbers
var fishIndex = fishArray.indexOf(fish);
if (fishIndex > -1) {
fishArray.splice(fishIndex, 1);
}
fish.destroy();
GameState.tutorialFish = null;
if (wasCriticalStep && !fish.caught && !fish.missed) {
// If it just went off screen without resolution
GameState.tutorialPaused = true;
setTutorialText("The fish swam off. Tap 'CONTINUE' to try that part again.");
}
}
}
var GameState = {
// Game flow
currentScreen: 'title',
// 'title', 'levelSelect', 'fishing', 'results'
// Player progression
currentDepth: 0,
money: 0,
totalFishCaught: 0,
ownedSongs: [],
// Array of {depth, songIndex} objects
// Level selection
selectedDepth: 0,
selectedSong: 0,
// Current session
sessionScore: 0,
sessionFishCaught: 0,
sessionFishSpawned: 0,
combo: 0,
maxCombo: 0,
// Game state
gameActive: false,
songStartTime: 0,
lastBeatTime: 0,
beatCount: 0,
introPlaying: false,
// Tracks if the intro animation is currently playing
musicNotesActive: false,
// Tracks if music note particle system is active
currentPlayingMusicId: 'rhythmTrack',
// ID of the music track currently playing in a session
currentPlayingMusicInitialVolume: 0.8,
// Initial volume of the current music track for fade reference
hookTargetLaneIndex: 1,
// Start with hook targeting the middle lane (index 1)
// Tutorial State
tutorialMode: false,
// Is the tutorial currently active?
tutorialStep: 0,
// Current step in the tutorial sequence
tutorialPaused: false,
// Is the tutorial paused (e.g., for text display)?
tutorialAwaitingTap: false,
// Is the tutorial waiting for a generic tap to continue?
tutorialFish: null,
// Stores the fish object used in a tutorial step
// Initialize owned songs (first song of each unlocked depth is free)
initOwnedSongs: function initOwnedSongs() {
this.ownedSongs = [];
for (var i = 0; i <= this.currentDepth; i++) {
this.ownedSongs.push({
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
****/
/****
* Title Screen - FIXED VERSION
****/
function createTitleScreen() {
var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 1,
height: 2732,
color: 0x87CEEB
}));
// ANIMATED CONTAINER GROUP - This will contain ALL visual elements including backgrounds
var titleAnimationGroup = titleScreen.addChild(new Container());
// Sky background image - NOW INSIDE animation group
var titleSky = titleAnimationGroup.addChild(LK.getAsset('skybackground', {
x: 0,
y: -500
}));
// Water background image - NOW INSIDE animation group
var titleWater = titleAnimationGroup.addChild(LK.getAsset('water', {
x: 0,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: 2048,
height: 2732 - GAME_CONFIG.WATER_SURFACE_Y
}));
// Initialize containers for title screen ambient particles - INSIDE animation group
titleScreenOceanBubbleContainer = titleAnimationGroup.addChild(new Container());
titleScreenSeaweedContainer = titleAnimationGroup.addChild(new Container());
titleScreenCloudContainer = titleAnimationGroup.addChild(new Container());
// Create a single container for boat, fisherman, and line that all move together
var titleBoatGroup = titleAnimationGroup.addChild(new Container());
// Boat positioned at origin of the group
var titleBoat = titleBoatGroup.addChild(LK.getAsset('boat', {
anchorX: 0.5,
anchorY: 0.74,
x: 0,
// Relative to group
y: 0 // Relative to group
}));
// Fisherman positioned relative to boat within the same group
var titleFisherman = titleBoatGroup.addChild(LK.getAsset('fisherman', {
anchorX: 0.5,
anchorY: 1,
x: -100,
// Relative to boat position
y: -70 // Relative to boat position
}));
// Fishing line positioned relative to boat within the same group
var rodTipX = -100 + 85; // fisherman offset + rod offset from fisherman center
var rodTipY = -70 - 200; // fisherman y (feet) - fisherman height (to get to head area for rod)
// initialHookY needs to be relative to the group's origin (which is boat's origin at WATER_SURFACE_Y)
// GAME_CONFIG.LANES[1].y is an absolute world Y.
// titleBoatGroup.y is GAME_CONFIG.WATER_SURFACE_Y.
// So, hook's Y relative to group = absolute hook Y - group's absolute Y
var initialHookYInGroup = GAME_CONFIG.LANES[1].y - GAME_CONFIG.WATER_SURFACE_Y;
var titleLine = titleBoatGroup.addChild(LK.getAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0,
x: rodTipX,
y: rodTipY,
// Relative to group
width: 6,
height: initialHookYInGroup - rodTipY // Length from rod tip to hook, all relative to group
}));
var titleHook = titleBoatGroup.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
x: rodTipX,
// Hook X matches line X initially
y: initialHookYInGroup // Relative to group
}));
// Position the entire group in the world (within titleAnimationGroup)
titleBoatGroup.x = GAME_CONFIG.SCREEN_CENTER_X;
titleBoatGroup.y = GAME_CONFIG.WATER_SURFACE_Y;
// Store base position for wave animation
var boatGroupBaseY = titleBoatGroup.y; // This is the group's world Y
var boatWaveAmplitude = 10;
var boatWaveHalfCycleDuration = 2000;
var boatRotationAmplitude = 0.03;
var boatRotationDuration = 3000;
// Wave animation variables for line (used in updateTitleFishingLineWave)
var lineWaveAmplitude = 12;
var lineWaveSpeed = 0.03;
var linePhaseOffset = 0;
// Animated Water Surface segments - INSIDE animation group
var titleWaterSurfaceSegments = [];
var NUM_WAVE_SEGMENTS_TITLE = 32;
var SEGMENT_WIDTH_TITLE = 2048 / NUM_WAVE_SEGMENTS_TITLE;
var SEGMENT_HEIGHT_TITLE = 24;
var WAVE_AMPLITUDE_TITLE = 12;
var WAVE_HALF_PERIOD_MS_TITLE = 2500;
var PHASE_DELAY_MS_PER_SEGMENT_TITLE = WAVE_HALF_PERIOD_MS_TITLE * 2 / NUM_WAVE_SEGMENTS_TITLE;
// Create water surface segments
for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) {
var segment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH_TITLE,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: SEGMENT_WIDTH_TITLE + 1,
height: SEGMENT_HEIGHT_TITLE,
anchorX: 0,
anchorY: 0.5,
alpha: 0.8,
tint: 0x4fc3f7
});
segment.baseY = GAME_CONFIG.WATER_SURFACE_Y;
titleAnimationGroup.addChild(segment); // Water surface is part of main animation group, not boat group
titleWaterSurfaceSegments.push(segment);
}
for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) {
var whiteSegment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH_TITLE,
y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2,
width: SEGMENT_WIDTH_TITLE + 1,
height: SEGMENT_HEIGHT_TITLE / 2,
anchorX: 0,
anchorY: 0.5,
alpha: 0.6,
tint: 0xffffff
});
whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2;
titleAnimationGroup.addChild(whiteSegment); // Water surface is part of main animation group
titleWaterSurfaceSegments.push(whiteSegment);
}
// Animation group setup for zoom effect - PROPERLY CENTER THE BOAT ON SCREEN
var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; // 1024
var targetBoatScreenY = GAME_CONFIG.SCREEN_CENTER_Y + 300; // Change +100 to move boat lower/higher
var boatWorldY = GAME_CONFIG.WATER_SURFACE_Y; // 760 - where titleBoatGroup actually is
var pivotY = boatWorldY - (targetBoatScreenY - boatWorldY); // Calculate proper pivot offset
// Set pivot to calculated position and position group so boat ends up at screen center
titleAnimationGroup.pivot.set(boatCenterX, pivotY);
titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X; // 1024
titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // 1366 - this will center the boat at targetBoatScreenY
// Initial zoom state
var INITIAL_ZOOM_FACTOR = 3.0;
var FINAL_ZOOM_FACTOR = 1.8; // This matches existing logic if it's used in showScreen
titleAnimationGroup.scale.set(INITIAL_ZOOM_FACTOR);
titleAnimationGroup.alpha = 1; // Main animation group is always visible
// Single wave animation function - moves entire group together
var targetUpY = boatGroupBaseY - boatWaveAmplitude; // boatGroupBaseY is absolute world Y
var targetDownY = boatGroupBaseY + boatWaveAmplitude; // boatGroupBaseY is absolute world Y
function moveTitleBoatGroupUp() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
// We tween the group's world Y position
tween(titleBoatGroup, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveTitleBoatGroupDown
});
}
function moveTitleBoatGroupDown() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
tween(titleBoatGroup, {
y: targetDownY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveTitleBoatGroupUp
});
}
// Single rotation animation - rotates entire group together
function rockTitleBoatGroupLeft() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
tween(titleBoatGroup, {
rotation: -boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockTitleBoatGroupRight
});
}
function rockTitleBoatGroupRight() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
tween(titleBoatGroup, {
rotation: boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockTitleBoatGroupLeft
});
}
// Simplified line wave function - just the sway effect
function updateTitleFishingLineWave() {
// titleLine and titleHook are children of titleBoatGroup, their x/y are relative to it.
// rodTipX, rodTipY, initialHookYInGroup are also relative to titleBoatGroup.
if (!titleLine || titleLine.destroyed || !titleHook || titleHook.destroyed) {
return;
}
linePhaseOffset += lineWaveSpeed;
var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude;
// Only apply the wave sway to X, positions stay relative to group's origin
// rodTipX is the line's base X relative to the group.
titleLine.x = rodTipX + waveOffset * 0.3;
titleHook.x = rodTipX + waveOffset;
// Y positions of line and hook (titleLine.y and titleHook.y) are static relative to the group.
// Recalculate line rotation based on hook position relative to line's anchor
// All coordinates here are relative to titleBoatGroup.
var deltaX = titleHook.x - titleLine.x; // Difference in x relative to group
var deltaY = titleHook.y - titleLine.y; // Difference in y relative to group
var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
titleLine.height = actualLineLength;
if (actualLineLength > 0.001) {
titleLine.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2;
} else {
titleLine.rotation = 0;
}
titleHook.rotation = titleLine.rotation; // Hook rotation matches line's
}
// Water surface animation function
function startTitleWaterSurfaceAnimationFunc() {
for (var k = 0; k < titleWaterSurfaceSegments.length; k++) {
var segment = titleWaterSurfaceSegments[k];
if (!segment || segment.destroyed) {
continue;
}
var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS_TITLE;
(function (currentLocalSegment, currentLocalSegmentIndexForDelay) {
var animUp, animDown;
animDown = function animDown() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE
}, {
duration: WAVE_HALF_PERIOD_MS_TITLE,
easing: tween.easeInOut,
onFinish: animUp
});
};
animUp = function animUp() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY - WAVE_AMPLITUDE_TITLE
}, {
duration: WAVE_HALF_PERIOD_MS_TITLE,
easing: tween.easeInOut,
onFinish: animDown
});
};
LK.setTimeout(function () {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
// Start with DOWN movement first
tween(currentLocalSegment, {
y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE
}, {
duration: WAVE_HALF_PERIOD_MS_TITLE,
easing: tween.easeInOut,
onFinish: animUp // This should call animUp after going down
});
}, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT_TITLE);
})(segment, segmentIndexForDelay);
}
}
// BLACK OVERLAY for reveal effect - this goes OVER everything
var blackOverlay = titleScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
width: 2048,
height: 2732,
color: 0x000000,
// Pure black
alpha: 1 // Starts fully opaque
}));
// UI elements - OUTSIDE animation group so they don't zoom, ABOVE black overlay
var titleImage = titleScreen.addChild(LK.getAsset('titleimage', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 700,
// Positioned where the subtitle roughly was, adjust as needed
alpha: 0,
scaleX: 0.8,
// Adjust scale as needed for optimal size
scaleY: 0.8 // Adjust scale as needed for optimal size
}));
// Buttons - OUTSIDE animation group, ABOVE black overlay
// New Y positions: 1/3 from bottom of screen (2732 / 3 = 910.66. Y = 2732 - 910.66 = 1821.33)
var startButtonY = 2732 - 2732 / 3.5; // Approx 1821
var tutorialButtonY = startButtonY + 600; // Maintain 150px gap
var startButton = titleScreen.addChild(LK.getAsset('startbutton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: startButtonY,
alpha: 0
}));
var tutorialButton = titleScreen.addChild(LK.getAsset('tutorialbutton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: tutorialButtonY,
alpha: 0
}));
// No assignment here; assign tutorialButtonGfx after createTitleScreen returns
return {
startButton: startButton,
tutorialButton: tutorialButton,
// This is the graphics object
titleImage: titleImage,
titleAnimationGroup: titleAnimationGroup,
blackOverlay: blackOverlay,
// Return the group and its specific animation functions
titleBoatGroup: titleBoatGroup,
moveTitleBoatGroupUp: moveTitleBoatGroupUp,
rockTitleBoatGroupLeft: rockTitleBoatGroupLeft,
// Individual elements that might still be needed for direct access or other effects
titleSky: titleSky,
// Sky is not in titleBoatGroup
titleWater: titleWater,
// Water background is not in titleBoatGroup
titleWaterSurfaceSegments: titleWaterSurfaceSegments,
// Water surface segments are not in titleBoatGroup
// Line and hook are part of titleBoatGroup, but if direct reference is needed for some reason, they could be returned.
// However, following the goal's spirit of "Return the group instead of individual elements [that are part of it]"
// titleLine and titleHook might be omitted here unless specifically needed by other parts of the code.
// For now, including them if they were returned before and are still locally defined.
// updateTitleFishingLineWave accesses titleLine and titleHook locally.
titleLine: titleLine,
// Still defined locally, might be useful for direct reference
titleHook: titleHook,
// Still defined locally
startTitleWaterSurfaceAnimation: startTitleWaterSurfaceAnimationFunc,
updateTitleFishingLineWave: updateTitleFishingLineWave // The new simplified version
};
}
/****
* Level Select Screen
****/
function createLevelSelectScreen() {
var selectBg = levelSelectScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 0.8,
height: 2732
}));
// Top section - Title, Money, and Back button
var title = new Text2('SELECT FISHING SPOT', {
size: 90,
// Increased from 80
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.x = GAME_CONFIG.SCREEN_CENTER_X;
title.y = 180; // Moved up slightly
levelSelectScreen.addChild(title);
// Money display (keep at top)
var moneyDisplay = new Text2('Money: $0', {
size: 70,
// Increased from 60
fill: 0xFFD700
});
moneyDisplay.anchor.set(1, 0);
moneyDisplay.x = 1900;
moneyDisplay.y = 80; // Moved up slightly
levelSelectScreen.addChild(moneyDisplay);
// Back button (moved to bottom center)
var backButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 2732 - 300,
// Positioned 100px from the bottom edge (center of button)
tint: 0x757575
}));
var backButtonText = new Text2('BACK', {
size: 45,
// Increased from 40
fill: 0xFFFFFF
});
backButtonText.anchor.set(0.5, 0.5);
backButtonText.x = backButton.x;
backButtonText.y = backButton.y;
levelSelectScreen.addChild(backButtonText);
// Depth tabs (moved down and spaced out more)
var depthTabs = [];
// Song display area (moved to center of screen)
var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1100,
// Moved down significantly from 700
width: 900,
// Made wider
height: 300 // Made taller
}));
// Song navigation arrows (repositioned around the larger song card)
var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 350,
// Moved further left
y: 1100,
// Moved down with song card
tint: 0x666666,
width: 120,
// Made bigger
height: 120
}));
var leftArrowText = new Text2('<', {
size: 80,
// Increased from 60
fill: 0xFFFFFF
});
leftArrowText.anchor.set(0.5, 0.5);
leftArrowText.x = 350;
leftArrowText.y = 1100;
levelSelectScreen.addChild(leftArrowText);
var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 1698,
// Moved further right
y: 1100,
// Moved down with song card
tint: 0x666666,
width: 120,
// Made bigger
height: 120
}));
var rightArrowText = new Text2('>', {
size: 80,
// Increased from 60
fill: 0xFFFFFF
});
rightArrowText.anchor.set(0.5, 0.5);
rightArrowText.x = 1698;
rightArrowText.y = 1100;
levelSelectScreen.addChild(rightArrowText);
// Song info (repositioned within the larger song card area)
var songTitle = new Text2('Song Title', {
size: 60,
// Increased from 50
fill: 0xFFFFFF
});
songTitle.anchor.set(0.5, 0.5);
songTitle.x = GAME_CONFIG.SCREEN_CENTER_X;
songTitle.y = 1020; // Positioned above song card center
levelSelectScreen.addChild(songTitle);
var songInfo = new Text2('BPM: 120 | Duration: 2:00', {
size: 40,
// Increased from 30
fill: 0xCCCCCC
});
songInfo.anchor.set(0.5, 0.5);
songInfo.x = GAME_CONFIG.SCREEN_CENTER_X;
songInfo.y = 1100; // Centered in song card
levelSelectScreen.addChild(songInfo);
var songEarnings = new Text2('Potential Earnings: $50-100', {
size: 40,
// Increased from 30
fill: 0x4CAF50
});
songEarnings.anchor.set(0.5, 0.5);
songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X;
songEarnings.y = 1180; // Positioned below song card center
levelSelectScreen.addChild(songEarnings);
// Play/Buy button (moved down and made larger)
var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1400,
// Moved down significantly from 900
width: 500,
// Made wider
height: 130 // Made taller
}));
var playButtonText = new Text2('PLAY', {
size: 60,
// Increased from 50
fill: 0xFFFFFF
});
playButtonText.anchor.set(0.5, 0.5);
playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
playButtonText.y = 1400;
levelSelectScreen.addChild(playButtonText);
// Shop button (moved to bottom and made larger)
var shopButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1650,
// Moved down significantly from 1100
tint: 0x666666,
// Grayed out since it's not available
width: 450,
// Made wider
height: 120 // Made taller
}));
var shopButtonText = new Text2('UPGRADE ROD', {
size: 50,
// Increased from 40
fill: 0xFFFFFF
});
shopButtonText.anchor.set(0.5, 0.5);
shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
shopButtonText.y = 1650;
levelSelectScreen.addChild(shopButtonText);
// Add "Coming soon!" text underneath
var comingSoonText = new Text2('Coming soon!', {
size: 35,
fill: 0xFFD700 // Gold color to make it stand out
});
comingSoonText.anchor.set(0.5, 0.5);
comingSoonText.x = GAME_CONFIG.SCREEN_CENTER_X;
comingSoonText.y = 1730; // Position below the button
levelSelectScreen.addChild(comingSoonText);
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() {
// Sky background - should be added first to be behind everything else
var sky = fishingScreen.addChild(LK.getAsset('skybackground', {
x: 0,
y: -500
}));
// Water background
var water = fishingScreen.addChild(LK.getAsset('water', {
x: 0,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: 2048,
height: 2732 - GAME_CONFIG.WATER_SURFACE_Y
}));
// Create a container for ambient ocean bubbles (from bottom of screen)
// This should be layered above the 'water' background, but below fish, boat, etc.
globalOceanBubbleContainer = fishingScreen.addChild(new Container());
// Create a container for seaweed particles
globalSeaweedContainer = fishingScreen.addChild(new Container());
// Create a container for cloud particles (added early so clouds appear behind UI)
globalCloudContainer = fishingScreen.addChild(new Container());
// Create a container for bubbles to render them behind fish and other elements
bubbleContainer = fishingScreen.addChild(new Container());
// Create a container for music notes
musicNotesContainer = fishingScreen.addChild(new Container());
// Music notes should visually appear to come from the boat area, so their container
// should ideally be layered accordingly. Adding it here means it's on top of water,
// but if boat/fisherman are added later, notes might appear behind them if not managed.
// For now, notes will be added to this container, which itself is added to fishingScreen.
// Animated Water Surface segments code
var waterSurfaceSegments = []; // This will be populated for returning and cleanup
var waterSurfaceSegmentsBlueTemp = []; // Temporary array for blue segments
var waterSurfaceSegmentsWhiteTemp = []; // Temporary array for white segments
var NUM_WAVE_SEGMENTS = 32;
var SEGMENT_WIDTH = 2048 / NUM_WAVE_SEGMENTS;
var SEGMENT_HEIGHT = 24;
var WAVE_AMPLITUDE = 12;
var WAVE_HALF_PERIOD_MS = 2500;
var PHASE_DELAY_MS_PER_SEGMENT = WAVE_HALF_PERIOD_MS * 2 / NUM_WAVE_SEGMENTS;
// Create blue segments (assets only, not added to fishingScreen yet)
for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) {
var segment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: SEGMENT_WIDTH + 1,
height: SEGMENT_HEIGHT,
anchorX: 0,
anchorY: 0.5,
alpha: 0.8,
tint: 0x4fc3f7
});
segment.baseY = GAME_CONFIG.WATER_SURFACE_Y;
// Animation functions will be defined and started by startWaterSurfaceAnimationFunc
waterSurfaceSegmentsBlueTemp.push(segment);
}
// Create white segments (assets only, not added to fishingScreen yet)
for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) {
var whiteSegment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH,
y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2,
width: SEGMENT_WIDTH + 1,
height: SEGMENT_HEIGHT / 2,
anchorX: 0,
anchorY: 0.5,
alpha: 0.6,
tint: 0xffffff
});
whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2;
// Animation functions will be defined and started by startWaterSurfaceAnimationFunc
waterSurfaceSegmentsWhiteTemp.push(whiteSegment);
}
// Boat - Add this to fishingScreen first
var boat = fishingScreen.addChild(LK.getAsset('boat', {
anchorX: 0.5,
anchorY: 0.74,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: GAME_CONFIG.WATER_SURFACE_Y
}));
// Now add the water segments to fishingScreen, so they render on top of the boat
for (var i = 0; i < waterSurfaceSegmentsBlueTemp.length; i++) {
fishingScreen.addChild(waterSurfaceSegmentsBlueTemp[i]);
waterSurfaceSegments.push(waterSurfaceSegmentsBlueTemp[i]); // Also add to the main array for cleanup
}
for (var i = 0; i < waterSurfaceSegmentsWhiteTemp.length; i++) {
fishingScreen.addChild(waterSurfaceSegmentsWhiteTemp[i]);
waterSurfaceSegments.push(waterSurfaceSegmentsWhiteTemp[i]); // Also add to the main array for cleanup
}
// Create separate fisherman container that will sync with boat movement
var fishermanContainer = fishingScreen.addChild(new Container());
// Fisherman (now in its own container, positioned to match boat)
var fisherman = fishermanContainer.addChild(LK.getAsset('fisherman', {
anchorX: 0.5,
anchorY: 1,
x: GAME_CONFIG.SCREEN_CENTER_X - 100,
y: GAME_CONFIG.WATER_SURFACE_Y - 70
}));
// Store references for wave animation sync
var boatBaseY = boat.y;
var fishermanBaseY = fishermanContainer.y;
var boatWaveAmplitude = 10;
var boatWaveHalfCycleDuration = 2000;
// SINGLE ANIMATED FISHING LINE
var initialHookY = GAME_CONFIG.LANES[1].y;
var fishingLineStartY = -100;
var line = fishingScreen.addChild(LK.getAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY,
width: 6,
height: initialHookY - (GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY)
}));
var hook = fishingScreen.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: initialHookY
}));
hook.originalY = initialHookY;
var lineWaveAmplitude = 12;
var lineWaveSpeed = 0.03;
var linePhaseOffset = 0;
function updateFishingLineWave() {
linePhaseOffset += lineWaveSpeed;
var rodTipX = fishermanContainer.x + fisherman.x + 85;
var rodTipY = fishermanContainer.y + fisherman.y - fisherman.height;
var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude;
line.x = rodTipX + waveOffset * 0.3;
line.y = rodTipY;
hook.x = rodTipX + waveOffset;
var hookAttachX = hook.x;
var hookAttachY = hook.y - hook.height / 2;
var deltaX = hookAttachX - line.x;
var deltaY = hookAttachY - line.y;
var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
line.height = actualLineLength;
if (actualLineLength > 0.001) {
line.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2;
} else {
line.rotation = 0;
}
hook.rotation = line.rotation;
}
// Calculate target positions for boat wave animation
var targetUpY = boatBaseY - boatWaveAmplitude;
var targetDownY = boatBaseY + boatWaveAmplitude;
var fishermanTargetUpY = fishermanBaseY - boatWaveAmplitude;
var fishermanTargetDownY = fishermanBaseY + boatWaveAmplitude;
// Synchronized wave animation functions (defined here to be closured)
function moveBoatAndFishermanUp() {
if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) {
return;
}
tween(boat, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveBoatAndFishermanDown
});
tween(fishermanContainer, {
y: fishermanTargetUpY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut
});
}
function moveBoatAndFishermanDown() {
if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) {
return;
}
tween(boat, {
y: targetDownY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveBoatAndFishermanUp
});
tween(fishermanContainer, {
y: fishermanTargetDownY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut
});
}
var boatRotationAmplitude = 0.03;
var boatRotationDuration = 3000;
function rockBoatLeft() {
if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) {
return;
}
tween(boat, {
rotation: -boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockBoatRight
});
tween(fisherman, {
rotation: boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut
});
}
function rockBoatRight() {
if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) {
return;
}
tween(boat, {
rotation: boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockBoatLeft
});
tween(fisherman, {
rotation: -boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut
});
}
// Function to start/restart water surface animations
function startWaterSurfaceAnimationFunc() {
var allSegments = waterSurfaceSegments; // Use the populated array from fishingElements via closure
for (var k = 0; k < allSegments.length; k++) {
var segment = allSegments[k];
if (!segment || segment.destroyed) {
continue;
}
var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS;
(function (currentLocalSegment, currentLocalSegmentIndexForDelay) {
var animUp, animDown;
animDown = function animDown() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY + WAVE_AMPLITUDE
}, {
duration: WAVE_HALF_PERIOD_MS,
easing: tween.easeInOut,
onFinish: animUp
});
};
animUp = function animUp() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY - WAVE_AMPLITUDE
}, {
duration: WAVE_HALF_PERIOD_MS,
easing: tween.easeInOut,
onFinish: animDown
});
};
LK.setTimeout(function () {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY - WAVE_AMPLITUDE
}, {
// Initial move up
duration: WAVE_HALF_PERIOD_MS,
easing: tween.easeInOut,
onFinish: animDown
});
}, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT);
})(segment, segmentIndexForDelay);
}
}
// Function to start/restart boat and fisherman animations
function startBoatAndFishermanAnimationFunc() {
if (boat && !boat.destroyed && fishermanContainer && !fishermanContainer.destroyed) {
tween(boat, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration / 2,
easing: tween.easeOut,
onFinish: moveBoatAndFishermanDown
});
tween(fishermanContainer, {
y: fishermanTargetUpY
}, {
duration: boatWaveHalfCycleDuration / 2,
easing: tween.easeOut
});
rockBoatLeft();
}
}
// UI elements (from existing)
var scoreText = new Text2('Score: 0', {
size: 70,
fill: 0xFFFFFF
});
scoreText.anchor.set(1, 0);
scoreText.x = 2048 - 50;
scoreText.y = 50;
fishingScreen.addChild(scoreText);
var fishText = new Text2('Fish: 0/0', {
size: 55,
fill: 0xFFFFFF
});
fishText.anchor.set(1, 0);
fishText.x = 2048 - 50;
fishText.y = 140;
fishingScreen.addChild(fishText);
var comboText = new Text2('Combo: 0', {
size: 55,
fill: 0xFF9800
});
comboText.anchor.set(1, 0);
comboText.x = 2048 - 50;
comboText.y = 210;
fishingScreen.addChild(comboText);
var progressText = new Text2('0:00 / 0:00', {
size: 50,
fill: 0x4FC3F7
});
progressText.anchor.set(1, 0);
progressText.x = 2048 - 50;
progressText.y = 280;
fishingScreen.addChild(progressText);
return {
boat: boat,
fishermanContainer: fishermanContainer,
fisherman: fisherman,
hook: hook,
line: line,
updateFishingLineWave: updateFishingLineWave,
scoreText: scoreText,
fishText: fishText,
comboText: comboText,
progressText: progressText,
waterSurfaceSegments: waterSurfaceSegments,
bubbleContainer: bubbleContainer,
musicNotesContainer: musicNotesContainer,
startWaterSurfaceAnimation: startWaterSurfaceAnimationFunc,
startBoatAndFishermanAnimation: startBoatAndFishermanAnimationFunc
};
}
/****
* Initialize Screen Elements
****/
var titleElements = createTitleScreen();
titleElements.tutorialButtonGfx = titleElements.tutorialButton; // Store the graphical button for compatibility
var levelSelectElements = createLevelSelectScreen();
var fishingElements = createFishingScreen();
// Tutorial UI Elements - MUST be at global scope
var tutorialOverlayContainer = game.addChild(new Container());
tutorialOverlayContainer.visible = false;
var tutorialTextBackground;
var tutorialTextDisplay;
var tutorialContinueButton;
var tutorialContinueText;
var tutorialLaneHighlights = []; // To store lane highlight graphics for the tutorial
// Feedback indicators are now created on-demand by the showFeedback function.
// The global feedbackIndicators object is no longer needed.
// Game variables
var fishArray = [];
var bubblesArray = [];
var bubbleContainer; // Container for bubbles, initialized in createFishingScreen
var musicNotesArray = [];
var musicNotesContainer; // Container for music notes
var laneBrackets = []; // Stores the visual bracket pairs for each lane
var musicNoteSpawnCounter = 0;
var MUSIC_NOTE_SPAWN_INTERVAL_TICKS = 45; // Spawn a note roughly every 0.75 seconds
// Ocean Bubbles (ambient background)
var globalOceanBubblesArray = [];
var globalOceanBubbleContainer;
var globalOceanBubbleSpawnCounter = 0;
// Increase interval to reduce amount (higher = less frequent)
var OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS = 40; // Spawn new ocean bubbles roughly every 2/3 second (was 20)
// Seaweed particles (ambient background)
var globalSeaweedArray = [];
var globalSeaweedContainer;
var globalSeaweedSpawnCounter = 0;
var SEAWEED_SPAWN_INTERVAL_TICKS = 120; // Spawn seaweed less frequently than bubbles
var MAX_SEAWEED_COUNT = 8; // Maximum number of seaweed particles at once
// Cloud particles (ambient sky)
var globalCloudArray = [];
var globalCloudContainer;
var globalCloudSpawnCounter = 0;
var CLOUD_SPAWN_INTERVAL_TICKS = 180; // Spawn clouds less frequently than seaweed
var MAX_CLOUD_COUNT = 5; // Maximum number of cloud particles at once
// Title Screen Ambient Particle Systems
var titleScreenOceanBubblesArray = [];
var titleScreenOceanBubbleContainer; // Will be initialized in createTitleScreen
var titleScreenOceanBubbleSpawnCounter = 0;
var titleScreenSeaweedArray = [];
var titleScreenSeaweedContainer; // Will be initialized in createTitleScreen
var titleScreenSeaweedSpawnCounter = 0;
var titleScreenCloudArray = [];
var titleScreenCloudContainer; // Will be initialized in createTitleScreen
var titleScreenCloudSpawnCounter = 0;
// Timers for title screen ambient sounds
var titleSeagullSoundTimer = null;
var titleBoatSoundTimer = null;
/****
* Input State and Helpers for Fishing
****/
var inputState = {
touching: false,
// Is the screen currently being touched?
touchLane: -1,
// Which lane was the touch initiated in? (0, 1, 2)
touchStartTime: 0 // Timestamp of when the touch started (LK.ticks based)
};
// Helper function to determine which lane a Y coordinate falls into
function getTouchLane(y) {
// Define boundaries based on the midpoints between lane Y coordinates
// These are calculated from GAME_CONFIG.LANES[i].y values
// Lane 0: y = 723
// Lane 1: y = 1366
// Lane 2: y = 2009
var boundary_lane0_lane1 = (GAME_CONFIG.LANES[0].y + GAME_CONFIG.LANES[1].y) / 2; // Approx 1044.5
var boundary_lane1_lane2 = (GAME_CONFIG.LANES[1].y + GAME_CONFIG.LANES[2].y) / 2; // Approx 1687.5
if (y < boundary_lane0_lane1) {
return 0; // Top lane (e.g., shallow)
} else if (y < boundary_lane1_lane2) {
return 1; // Middle lane (e.g., medium)
} else {
return 2; // Bottom lane (e.g., deep)
}
}
// Shows feedback (perfect, good, miss) at the specified lane
// Shows feedback (perfect, good, miss) at the specified lane
function showFeedback(type, laneIndex) {
var feedbackY = GAME_CONFIG.LANES[laneIndex].y;
var indicator = new FeedbackIndicator(type); // Creates a new indicator e.g. FeedbackIndicator('perfect')
// Position feedback at the single hook's X coordinate and the fish's lane Y
indicator.x = fishingElements.hook.x; // Use the single hook's X
indicator.y = feedbackY; // Feedback appears at the fish's lane Y
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 in tutorial mode, and it's a 'down' event during an active catch step (now steps 3 or 4)
if (GameState.tutorialMode && isDown && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !GameState.tutorialPaused) {
if (GameState.tutorialFish && !GameState.tutorialFish.caught && !GameState.tutorialFish.missed) {
checkCatch(getTouchLane(y)); // Attempt catch
}
return; // Tutorial catch handled
}
// Existing game active check
if (!GameState.gameActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60); // Current time in ms
if (isDown) {
// Touch started
inputState.touching = true;
inputState.touchLane = getTouchLane(y);
inputState.touchStartTime = currentTime;
// A normal tap action will be processed on 'up'.
} else {
// Touch ended (isUp)
if (inputState.touching) {
// This was a normal tap.
checkCatch(inputState.touchLane);
}
inputState.touching = false;
}
}
/****
* Screen Management
****/
function showScreen(screenName) {
titleScreen.visible = false;
levelSelectScreen.visible = false;
fishingScreen.visible = false;
resultsScreen.visible = false;
// Stop any ongoing tweens and clear particles if switching FROM title screen
if (GameState.currentScreen === 'title' && titleElements) {
tween.stop(titleElements.titleAnimationGroup);
tween.stop(titleElements.blackOverlay);
if (titleElements.titleImage) {
tween.stop(titleElements.titleImage);
} // Stop titleImage tween
if (titleElements.logo) {
tween.stop(titleElements.logo);
} // Keep for safety if old code path
if (titleElements.subtitle) {
tween.stop(titleElements.subtitle);
} // Keep for safety
tween.stop(titleElements.startButton);
tween.stop(titleElements.tutorialButton);
// Clear title screen sound timers
if (titleSeagullSoundTimer) {
LK.clearTimeout(titleSeagullSoundTimer);
titleSeagullSoundTimer = null;
}
if (titleBoatSoundTimer) {
LK.clearTimeout(titleBoatSoundTimer);
titleBoatSoundTimer = null;
}
// Stop water surface animations for title screen
if (titleElements.titleWaterSurfaceSegments) {
titleElements.titleWaterSurfaceSegments.forEach(function (segment) {
if (segment && !segment.destroyed) {
tween.stop(segment);
}
});
}
// Clear title screen particles
if (titleScreenOceanBubbleContainer) {
titleScreenOceanBubbleContainer.removeChildren();
}
titleScreenOceanBubblesArray.forEach(function (p) {
if (p && !p.destroyed) {
p.destroy();
}
});
titleScreenOceanBubblesArray = [];
if (titleScreenSeaweedContainer) {
titleScreenSeaweedContainer.removeChildren();
}
titleScreenSeaweedArray.forEach(function (p) {
if (p && !p.destroyed) {
p.destroy();
}
});
titleScreenSeaweedArray = [];
if (titleScreenCloudContainer) {
titleScreenCloudContainer.removeChildren();
}
titleScreenCloudArray.forEach(function (p) {
if (p && !p.destroyed) {
p.destroy();
}
});
titleScreenCloudArray = [];
}
// Cleanup tutorial elements if switching away from fishing/tutorial
if (GameState.currentScreen === 'fishing' && GameState.tutorialMode && screenName !== 'fishing') {
tutorialOverlayContainer.visible = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish = null;
}
// Clear lane highlights if any exist when switching screen away from tutorial
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
GameState.tutorialMode = false; // Ensure tutorial mode is exited
}
GameState.currentScreen = screenName;
switch (screenName) {
case 'title':
// Title Screen Sounds
var _scheduleNextSeagullSound = function scheduleNextSeagullSound() {
if (GameState.currentScreen !== 'title') {
// If screen changed, ensure timer is stopped and not rescheduled
if (titleSeagullSoundTimer) {
LK.clearTimeout(titleSeagullSoundTimer);
titleSeagullSoundTimer = null;
}
return;
}
var randomDelay = 5000 + Math.random() * 10000; // 5-15 seconds
titleSeagullSoundTimer = LK.setTimeout(function () {
if (GameState.currentScreen !== 'title') {
return; // Don't play or reschedule if not on title screen
}
var seagullSounds = ['seagull1', 'seagull2', 'seagull3'];
var randomSoundId = seagullSounds[Math.floor(Math.random() * seagullSounds.length)];
LK.getSound(randomSoundId).play();
_scheduleNextSeagullSound(); // Reschedule
}, randomDelay);
};
var _scheduleNextBoatSound = function scheduleNextBoatSound() {
if (GameState.currentScreen !== 'title') {
// If screen changed, ensure timer is stopped and not rescheduled
if (titleBoatSoundTimer) {
LK.clearTimeout(titleBoatSoundTimer);
titleBoatSoundTimer = null;
}
return;
}
var fixedBoatSoundInterval = 6000; // Rhythmic interval: 8 seconds (reduced from 15)
titleBoatSoundTimer = LK.setTimeout(function () {
if (GameState.currentScreen !== 'title') {
return; // Don't play or reschedule if not on title screen
}
LK.getSound('boatsounds').play();
_scheduleNextBoatSound(); // Reschedule
}, fixedBoatSoundInterval);
}; // Play initial random seagull sound
titleScreen.visible = true;
// Start all animations like in fishing screen
if (titleElements.startTitleWaterSurfaceAnimation) {
titleElements.startTitleWaterSurfaceAnimation();
}
// Start boat group animations (single animations for the whole group)
if (titleElements.moveTitleBoatGroupUp) {
titleElements.moveTitleBoatGroupUp();
}
if (titleElements.rockTitleBoatGroupLeft) {
titleElements.rockTitleBoatGroupLeft();
}
var initialSeagullSounds = ['seagull1', 'seagull2', 'seagull3'];
var initialRandomSoundId = initialSeagullSounds[Math.floor(Math.random() * initialSeagullSounds.length)];
LK.getSound(initialRandomSoundId).play();
LK.getSound('boatsounds').play(); // Play boat sound immediately after initial seagull
// Start the timed sounds (seagulls random, subsequent boats rhythmic)
_scheduleNextSeagullSound();
_scheduleNextBoatSound(); // Schedules the *next* boat sound rhythmically
// Reset particle spawn counters
titleScreenOceanBubbleSpawnCounter = 0;
titleScreenSeaweedSpawnCounter = 0;
titleScreenCloudSpawnCounter = 0;
// Animation timing
var ZOOM_DURATION = 8000; // 8-second zoom
var OVERLAY_FADE_DELAY = 1000; // Black overlay starts fading after 1 second (was 2)
var OVERLAY_FADE_DURATION = 3000; // 3 seconds to fade out overlay
var TEXT_DELAY = 4000; // Text appears as overlay finishes fading (was 5500)
var BUTTON_DELAY = 5500; // Buttons appear sooner (was 7000)
// Reset states
titleElements.titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X;
titleElements.titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // Centers boat vertically!
titleElements.titleAnimationGroup.alpha = 1; // No alpha animation for main content
titleElements.titleAnimationGroup.scale.set(3.0);
// Reset black overlay to fully opaque
titleElements.blackOverlay.alpha = 1;
// Reset UI alphas
if (titleElements.titleImage) {
titleElements.titleImage.alpha = 0;
}
if (titleElements.logo) {
titleElements.logo.alpha = 0;
}
if (titleElements.subtitle) {
titleElements.subtitle.alpha = 0;
}
titleElements.startButton.alpha = 0;
titleElements.tutorialButton.alpha = 0;
// Main zoom animation (no alpha change)
tween(titleElements.titleAnimationGroup, {
scaleX: 1.8,
scaleY: 1.8
}, {
duration: ZOOM_DURATION,
easing: tween.easeInOut
});
// Black overlay fade out (the reveal effect)
LK.setTimeout(function () {
tween(titleElements.blackOverlay, {
alpha: 0
}, {
duration: OVERLAY_FADE_DURATION,
easing: tween.easeInOut
});
}, OVERLAY_FADE_DELAY);
// Fade in title image after overlay is mostly gone
LK.setTimeout(function () {
if (titleElements.titleImage) {
tween(titleElements.titleImage, {
alpha: 1
}, {
duration: 1200,
easing: tween.easeOut
});
} else if (titleElements.logo && titleElements.subtitle) {
// Fallback for old structure if needed
tween(titleElements.logo, {
alpha: 1
}, {
duration: 1200,
easing: tween.easeOut
});
tween(titleElements.subtitle, {
alpha: 1
}, {
duration: 1200,
easing: tween.easeOut
});
}
}, TEXT_DELAY);
// Fade in buttons near the end
LK.setTimeout(function () {
tween(titleElements.startButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Fade in tutorial button AFTER start button finishes
tween(titleElements.tutorialButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut
});
}
});
}, BUTTON_DELAY);
break;
case 'levelSelect':
levelSelectScreen.visible = true;
updateLevelSelectScreen();
break;
case 'fishing':
fishingScreen.visible = true;
playIntroAnimation(); // Play the intro sequence
break;
case 'results':
resultsScreen.visible = true;
break;
}
// Tutorial System functions moved to global scope.
}
/****
* Intro Animation
****/
function playIntroAnimation() {
GameState.introPlaying = true;
GameState.gameActive = false;
// Start animations for water, boat, and fisherman at the beginning of the intro
if (fishingElements) {
if (typeof fishingElements.startWaterSurfaceAnimation === 'function') {
fishingElements.startWaterSurfaceAnimation();
}
if (typeof fishingElements.startBoatAndFishermanAnimation === 'function') {
fishingElements.startBoatAndFishermanAnimation();
}
}
// Calculate rod tip position (relative to fishingScreen)
var fc = fishingElements.fishermanContainer;
var f = fishingElements.fisherman;
var rodTipCalculatedX = fc.x + f.x + 85;
var rodTipCalculatedY = fc.y + f.y - f.height;
var initialHookDangleY = rodTipCalculatedY + 50;
fishingElements.hook.y = initialHookDangleY;
// Setup initial zoom and camera position for fishingScreen
var INITIAL_ZOOM_FACTOR = 1.5;
// Pivot around the boat's visual center
var pivotX = fishingElements.boat.x;
var pivotY = fishingElements.boat.y - fishingElements.boat.height * (fishingElements.boat.anchor.y - 0.5);
fishingScreen.pivot.set(pivotX, pivotY);
// Position screen so the pivot appears at screen center when zoomed
var screenCenterX = 2048 / 2;
var screenCenterY = 2732 / 2;
fishingScreen.x = screenCenterX;
fishingScreen.y = screenCenterY;
fishingScreen.scale.set(INITIAL_ZOOM_FACTOR, INITIAL_ZOOM_FACTOR);
var introDuration = 2000;
// Tween for zoom out
tween(fishingScreen.scale, {
x: 1,
y: 1
}, {
duration: introDuration,
easing: tween.easeInOut
});
// Tween screen position to compensate for the zoom change
tween(fishingScreen, {
x: pivotX,
y: pivotY
}, {
duration: introDuration,
easing: tween.easeInOut
});
// Hook drop animation
var targetHookY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
// Play reel sound effect with 300ms delay during hook animation
LK.setTimeout(function () {
LK.getSound('reel').play();
}, 600);
tween(fishingElements.hook, {
y: targetHookY
}, {
duration: introDuration * 0.8,
delay: introDuration * 0.2,
easing: tween.easeOut,
onFinish: function onFinish() {
GameState.introPlaying = false;
// Reset to normal view
fishingScreen.pivot.set(0, 0);
fishingScreen.x = 0;
fishingScreen.y = 0;
// Check if we're in tutorial mode
if (GameState.tutorialMode) {
// Start the tutorial properly after intro
GameState.gameActive = false; // Keep game inactive for tutorial
createTutorialElements(); // Create tutorial UI
runTutorialStep(); // Start with step 0
} else {
// Normal fishing session
startFishingSession();
}
}
});
}
/****
* 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 (positioned in the middle area)
var tabStartY = 600; // Moved down from 400
var tabSpacing = 250; // Increased spacing between tabs
for (var i = 0; i <= GameState.currentDepth; i++) {
var depth = GAME_CONFIG.DEPTHS[i];
var isSelected = i === GameState.selectedDepth;
var tabContainer = levelSelectScreen.addChild(new Container());
var tab = tabContainer.addChild(LK.getAsset('depthTab', {
anchorX: 0.5,
anchorY: 0.5,
x: 200 + i * tabSpacing,
// Increased spacing
y: tabStartY,
tint: isSelected ? 0x1976d2 : 0x455a64,
width: 400,
// Made wider
height: 160 // Made taller
}));
var tabText = new Text2(depth.name.split(' ')[0], {
size: 40,
// Increased from 30
fill: 0xFFFFFF
});
tabText.anchor.set(0.5, 0.5);
tabText.x = 200 + i * tabSpacing;
tabText.y = tabStartY;
tabContainer.addChild(tabText);
levelSelectElements.depthTabs.push({
container: tabContainer,
tab: tab,
depthIndex: i
});
}
}
function updateSongDisplay() {
var elements = levelSelectElements;
var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth];
var song = depth.songs[GameState.selectedSong];
var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong);
// 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;
// Always show as disabled - coming soon
elements.shopButtonText.setText('UPGRADE ROD');
elements.shopButton.tint = 0x666666; // Always grayed out
}
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.tutorialMode = false; // Ensure tutorial mode is off
GameState.sessionScore = 0;
GameState.sessionFishCaught = 0;
GameState.sessionFishSpawned = 0;
GameState.combo = 0;
GameState.maxCombo = 0;
GameState.gameActive = true;
GameState.songStartTime = 0;
GameState.lastBeatTime = 0;
GameState.beatCount = 0;
GameState.musicNotesActive = true;
ImprovedRhythmSpawner.reset();
musicNotesArray = [];
if (fishingElements && fishingElements.musicNotesContainer) {
fishingElements.musicNotesContainer.removeChildren();
}
musicNoteSpawnCounter = 0;
// Reset ocean bubbles
globalOceanBubblesArray = [];
if (globalOceanBubbleContainer) {
globalOceanBubbleContainer.removeChildren();
}
globalOceanBubbleSpawnCounter = 0;
// Reset seaweed
globalSeaweedArray = [];
if (globalSeaweedContainer) {
globalSeaweedContainer.removeChildren();
}
globalSeaweedSpawnCounter = 0;
// Reset clouds
globalCloudArray = [];
if (globalCloudContainer) {
globalCloudContainer.removeChildren();
}
globalCloudSpawnCounter = 0;
// Animations for water, boat, and fisherman are now started in playIntroAnimation
// Clear any existing fish
fishArray.forEach(function (fish) {
fish.destroy();
});
fishArray = [];
// Reset pattern generator for new session
PatternGenerator.reset();
// Clear and create lane brackets
if (laneBrackets && laneBrackets.length > 0) {
laneBrackets.forEach(function (bracketPair) {
if (bracketPair.left && !bracketPair.left.destroyed) {
bracketPair.left.destroy();
}
if (bracketPair.right && !bracketPair.right.destroyed) {
bracketPair.right.destroy();
}
});
}
laneBrackets = [];
var bracketAssetHeight = 150; // Height of the lanebracket asset
var bracketAssetWidth = 75; // Width of the lanebracket asset
if (fishingScreen && !fishingScreen.destroyed) {
// Ensure fishingScreen is available
for (var i = 0; i < GAME_CONFIG.LANES.length; i++) {
var laneY = GAME_CONFIG.LANES[i].y;
var leftBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
x: bracketAssetWidth / 2,
// Position its center so left edge is at 0
y: laneY,
height: bracketAssetHeight
}));
var rightBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
scaleX: -1,
// Flipped horizontally
x: 2048 - bracketAssetWidth / 2,
// Position its center so right edge is at 2048
y: laneY,
height: bracketAssetHeight
}));
laneBrackets.push({
left: leftBracket,
right: rightBracket
});
}
}
// Start music
var songConfig = GameState.getCurrentSongConfig();
var musicIdToPlay = songConfig.musicId || 'rhythmTrack'; // Default to rhythmTrack if no specific id
GameState.currentPlayingMusicId = musicIdToPlay;
// Determine initial volume based on known assets for correct fade-out later
if (musicIdToPlay === 'morningtide') {
GameState.currentPlayingMusicInitialVolume = 1.0; // Volume defined in LK.init.music for 'morningtide'
} else {
// Default for 'rhythmTrack' or other unspecified tracks
GameState.currentPlayingMusicInitialVolume = 0.8; // Volume defined in LK.init.music for 'rhythmTrack'
}
LK.playMusic(GameState.currentPlayingMusicId); // Play the selected music track
}
function spawnFish(currentTimeForRegistration, options) {
options = options || {}; // Ensure options is an object
var depthConfig = GameState.getCurrentDepthConfig();
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
// Proximity check: Skip spawn if too close to existing fish.
// This check is generally for the first fish of a beat or non-forced spawns.
// For forced multi-beat spawns, this might prevent them if they are too close.
// Consider if this rule should be relaxed for forced multi-beat spawns if visual overlap is acceptable for quick succession.
// For now, keeping it as is. If a spawn is skipped, the multi-beat sequence might be shorter.
var isFirstFishOfBeat = !options.laneIndexToUse && !options.forcedSpawnSide;
if (isFirstFishOfBeat) {
// Apply stricter proximity for non-forced spawns
for (var i = 0; i < fishArray.length; i++) {
var existingFish = fishArray[i];
if (Math.abs(existingFish.x - GAME_CONFIG.SCREEN_CENTER_X) < PatternGenerator.minDistanceBetweenFish) {
return null; // Skip this spawn, do not register
}
}
}
var laneIndex;
if (options.laneIndexToUse !== undefined) {
laneIndex = options.laneIndexToUse;
PatternGenerator.lastLane = laneIndex; // Update generator's state if lane is forced
} else {
laneIndex = PatternGenerator.getNextLane();
}
var targetLane = GAME_CONFIG.LANES[laneIndex];
var fishType, fishValue;
var rand = Math.random();
if (rand < pattern.rareSpawnChance) {
fishType = 'rare';
fishValue = Math.floor(depthConfig.fishValue * 4);
} else if (GameState.selectedDepth >= 2 && rand < 0.3) {
fishType = 'deep';
fishValue = Math.floor(depthConfig.fishValue * 2);
} else if (GameState.selectedDepth >= 1 && rand < 0.6) {
fishType = 'medium';
fishValue = Math.floor(depthConfig.fishValue * 1.5);
} else {
fishType = 'shallow';
fishValue = Math.floor(depthConfig.fishValue);
}
var fishSpeedValue = depthConfig.fishSpeed;
var spawnSide; // -1 for left, 1 for right
var actualFishSpeed;
if (options.forcedSpawnSide !== undefined) {
spawnSide = options.forcedSpawnSide;
} else {
spawnSide = Math.random() < 0.5 ? -1 : 1;
}
actualFishSpeed = Math.abs(fishSpeedValue) * spawnSide;
var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex);
newFish.spawnSide = spawnSide; // Store the side it spawned from
newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen
newFish.y = targetLane.y;
newFish.baseY = targetLane.y; // Set baseY for swimming animation
fishArray.push(newFish);
fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTimeForRegistration);
return newFish;
}
function checkCatch(fishLane) {
var hookX = fishingElements.hook.x;
if (GameState.tutorialMode) {
var tutorialFish = GameState.tutorialFish;
if (!tutorialFish || tutorialFish.lane !== fishLane || tutorialFish.caught || tutorialFish.missed) {
// Check if it's a critical catch step (now steps 3 or 4)
if (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) {
setTutorialText("Oops! Make sure to tap when the fish is in the correct lane and over the hook. Tap 'CONTINUE' to try again.");
GameState.tutorialPaused = true;
}
LK.getSound('miss').play();
return;
}
var distance = Math.abs(tutorialFish.x - hookX);
var caughtType = null;
if (distance < GAME_CONFIG.PERFECT_WINDOW) {
caughtType = 'perfect';
} else if (distance < GAME_CONFIG.GOOD_WINDOW) {
caughtType = 'good';
} else if (distance < GAME_CONFIG.MISS_WINDOW) {
// For tutorial, make 'miss window' taps still count as 'good' to be more forgiving
caughtType = 'good';
} else {
caughtType = 'miss';
}
showFeedback(caughtType, fishLane); // Show visual feedback based on derived type
if (caughtType === 'perfect' || caughtType === 'good') {
tutorialFish.catchFish();
var fishIndex = fishArray.indexOf(tutorialFish);
if (fishIndex > -1) {
fishArray.splice(fishIndex, 1);
}
// No points/money in tutorial normally, but can add if desired.
LK.getSound('catch').play();
animateHookCatch();
GameState.tutorialPaused = true; // Pause to show message
if (GameState.tutorialStep === 3) {
// Was step 2
setTutorialText("Great catch! That's how you do it. Tap 'CONTINUE'.");
// GameState.tutorialStep = 4; // Advance to next conceptual phase -- Handled by game.down
} else if (GameState.tutorialStep === 4) {
// Was step 3
setTutorialText("Nice one! You're getting the hang of timing. Tap 'CONTINUE'.");
// GameState.tutorialStep = 5; // Advance to Combo explanation -- Handled by game.down
}
} else {
// Miss
// Feedback 'miss' was already shown
LK.getSound('miss').play();
tutorialFish.missed = true;
GameState.tutorialPaused = true;
setTutorialText("Almost! Try to tap when the fish is closer. Tap 'CONTINUE' to try this part again.");
// The runTutorialStep logic on "CONTINUE" will handle respawning for this step.
}
return; // Tutorial catch logic finished
}
var closestFishInLane = null;
var closestDistance = Infinity;
// This is a tap action, find the closest fish in the tapped lane.
for (var i = 0; i < fishArray.length; i++) {
var fish = fishArray[i];
// Ensure fish is not caught, not already missed, and in the correct lane
if (!fish.caught && !fish.missed && fish.lane === fishLane) {
var distance = Math.abs(fish.x - hookX);
if (distance < closestDistance) {
closestDistance = distance;
closestFishInLane = fish;
}
}
}
if (!closestFishInLane) {
// No fish found for tap
// Play miss sound
LK.getSound('miss').play();
// Tint incorrect lane indicators red briefly using tween
if (laneBrackets && laneBrackets[fishLane]) {
var leftBracket = laneBrackets[fishLane].left;
var rightBracket = laneBrackets[fishLane].right;
var tintToRedDuration = 50; // Duration to tween to red (ms)
var holdRedDuration = 100; // How long it stays fully red (ms)
var tintToWhiteDuration = 150; // Duration to tween back to white (ms)
if (leftBracket && !leftBracket.destroyed) {
// Tween to red
tween(leftBracket, {
tint: 0xFF0000
}, {
duration: tintToRedDuration,
easing: tween.linear,
onFinish: function onFinish() {
// After tinting to red, wait, then tween back to white
LK.setTimeout(function () {
if (leftBracket && !leftBracket.destroyed) {
tween(leftBracket, {
tint: 0xFFFFFF
}, {
duration: tintToWhiteDuration,
easing: tween.linear
});
}
}, holdRedDuration);
}
});
}
if (rightBracket && !rightBracket.destroyed) {
// Tween to red
tween(rightBracket, {
tint: 0xFF0000
}, {
duration: tintToRedDuration,
easing: tween.linear,
onFinish: function onFinish() {
// After tinting to red, wait, then tween back to white
LK.setTimeout(function () {
if (rightBracket && !rightBracket.destroyed) {
tween(rightBracket, {
tint: 0xFFFFFF
}, {
duration: tintToWhiteDuration,
easing: tween.linear
});
}
}, holdRedDuration);
}
});
}
}
GameState.combo = 0;
return;
}
// --- Normal Fish Catch Logic ---
var points = 0;
var multiplier = Math.max(1, Math.floor(GameState.combo / 10) + 1);
if (closestDistance < GAME_CONFIG.PERFECT_WINDOW) {
points = closestFishInLane.value * 2 * multiplier;
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;
// Mark the specific fish that was tapped but missed
if (closestFishInLane) {
closestFishInLane.missed = true;
}
return;
}
// Successfully caught fish
closestFishInLane.catchFish();
var fishIndex = fishArray.indexOf(closestFishInLane);
if (fishIndex > -1) {
fishArray.splice(fishIndex, 1);
}
GameState.sessionScore += points;
GameState.money += points;
GameState.sessionFishCaught++;
GameState.totalFishCaught++;
GameState.maxCombo = Math.max(GameState.maxCombo, GameState.combo);
// Play a random catch sound effect
var catchSounds = ['catch', 'catch2', 'catch3', 'catch4'];
var randomCatchSound = catchSounds[Math.floor(Math.random() * catchSounds.length)];
LK.getSound(randomCatchSound).play();
animateHookCatch(); // Call parameterless animateHookCatch for the single hook
// Score pop-up animation
if (points > 0) {
var scorePopupText = new Text2('+' + points, {
size: 140,
// Slightly larger for impact
fill: 0xFFD700,
// Gold color for score
align: 'center',
stroke: 0x000000,
// Black stroke
strokeThickness: 6 // Thickness of the stroke
});
scorePopupText.anchor.set(0.5, 0.5);
scorePopupText.x = GAME_CONFIG.SCREEN_CENTER_X; // Centered with the boat
scorePopupText.y = GAME_CONFIG.BOAT_Y - 70; // Start slightly above the boat's deck line
// Add to fishingScreen so it's part of the game view
if (fishingScreen && !fishingScreen.destroyed) {
fishingScreen.addChild(scorePopupText);
}
tween(scorePopupText, {
y: scorePopupText.y - 200,
// Float up by 200 pixels
alpha: 0
}, {
duration: 1800,
// Slightly longer duration for a nice float
easing: tween.easeOut,
onFinish: function onFinish() {
if (scorePopupText && !scorePopupText.destroyed) {
scorePopupText.destroy();
}
}
});
}
}
// 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;
GameState.tutorialMode = false; // Ensure tutorial mode is off
// Stop the boat's wave animation to prevent it from running after the session
if (fishingElements && fishingElements.boat) {
tween.stop(fishingElements.boat);
}
// Stop fisherman container animation
if (fishingElements && fishingElements.fishermanContainer) {
tween.stop(fishingElements.fishermanContainer);
}
// Stop fisherman rotation animation
if (fishingElements && fishingElements.fisherman) {
tween.stop(fishingElements.fisherman);
}
// Stop water surface wave animations
if (fishingElements && fishingElements.waterSurfaceSegments) {
fishingElements.waterSurfaceSegments.forEach(function (segment) {
if (segment && !segment.destroyed) {
tween.stop(segment);
}
});
}
// Stop music immediately
LK.stopMusic();
ImprovedRhythmSpawner.reset();
// Clear lane brackets
if (laneBrackets && laneBrackets.length > 0) {
laneBrackets.forEach(function (bracketPair) {
if (bracketPair.left && !bracketPair.left.destroyed) {
bracketPair.left.destroy();
}
if (bracketPair.right && !bracketPair.right.destroyed) {
bracketPair.right.destroy();
}
});
laneBrackets = [];
}
// Clear fish
fishArray.forEach(function (fish) {
fish.destroy();
});
fishArray = [];
GameState.musicNotesActive = false;
if (fishingElements && fishingElements.musicNotesContainer) {
fishingElements.musicNotesContainer.removeChildren();
}
// The MusicNoteParticle instances themselves will be garbage collected.
// Clearing the array is important.
musicNotesArray = [];
// Clear ocean bubbles
if (globalOceanBubbleContainer) {
globalOceanBubbleContainer.removeChildren();
}
globalOceanBubblesArray = [];
// Clear seaweed
if (globalSeaweedContainer) {
globalSeaweedContainer.removeChildren();
}
globalSeaweedArray = [];
// Clear clouds
if (globalCloudContainer) {
globalCloudContainer.removeChildren();
}
globalCloudArray = [];
// Create results screen
createResultsScreen();
showScreen('results');
}
function createResultsScreen() {
// Clear previous results
resultsScreen.removeChildren();
var resultsBg = resultsScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 0.9,
height: 2732
}));
var title = new Text2('Fishing Complete!', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.x = GAME_CONFIG.SCREEN_CENTER_X;
title.y = 400;
resultsScreen.addChild(title);
var scoreResult = new Text2('Score: ' + GameState.sessionScore, {
size: 70,
fill: 0xFFD700
});
scoreResult.anchor.set(0.5, 0.5);
scoreResult.x = GAME_CONFIG.SCREEN_CENTER_X;
scoreResult.y = 550;
resultsScreen.addChild(scoreResult);
var fishResult = new Text2('Fish Caught: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned, {
size: 50,
fill: 0xFFFFFF
});
fishResult.anchor.set(0.5, 0.5);
fishResult.x = GAME_CONFIG.SCREEN_CENTER_X;
fishResult.y = 650;
resultsScreen.addChild(fishResult);
var comboResult = new Text2('Max Combo: ' + GameState.maxCombo, {
size: 50,
fill: 0xFF9800
});
comboResult.anchor.set(0.5, 0.5);
comboResult.x = GAME_CONFIG.SCREEN_CENTER_X;
comboResult.y = 750;
resultsScreen.addChild(comboResult);
var moneyEarned = new Text2('Money Earned: $' + GameState.sessionScore, {
size: 50,
fill: 0x4CAF50
});
moneyEarned.anchor.set(0.5, 0.5);
moneyEarned.x = GAME_CONFIG.SCREEN_CENTER_X;
moneyEarned.y = 850;
resultsScreen.addChild(moneyEarned);
// 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();
var currentScreen = GameState.currentScreen;
// If tutorial mode is active, treat as 'tutorial' screen for input
if (GameState.tutorialMode && (currentScreen === 'fishing' || currentScreen === 'tutorial')) {
// New case for tutorial input
if (tutorialOverlayContainer.visible && tutorialContinueButton && tutorialContinueButton.visible && x >= tutorialContinueButton.x - tutorialContinueButton.width / 2 && x <= tutorialContinueButton.x + tutorialContinueButton.width / 2 && y >= tutorialContinueButton.y - tutorialContinueButton.height / 2 && y <= tutorialContinueButton.y + tutorialContinueButton.height / 2) {
LK.getSound('buttonClick').play();
// If tutorial is paused and it's a catch instruction step (3 or 4),
// clicking "CONTINUE" implies a retry of that step.
if (GameState.tutorialPaused && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4)) {
// Logic for catch steps 3 or 4 when "CONTINUE" is pressed.
// This covers scenarios where fish was caught, missed, passed hook, or swam off screen.
var advanceAfterCatch = false;
// First, check the state of the existing tutorial fish, if any.
if (GameState.tutorialFish && GameState.tutorialFish.caught) {
advanceAfterCatch = true;
}
// Regardless of success or failure, if there was a tutorial fish, clean it up.
// This handles the fish that was just caught OR missed/swam_off.
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish.destroy();
}
// Ensure GameState.tutorialFish is null before runTutorialStep,
// as runTutorialStep might spawn a new one for the current or next step.
GameState.tutorialFish = null; //{pq} // Ensure it's null before re-running step to spawn a new one.
if (advanceAfterCatch) {
// If the fish was caught, advance to the next tutorial step.
GameState.tutorialStep++;
runTutorialStep();
} else {
// If the fish was missed, swam off, or there was no fish to catch (e.g. error state)
// then retry the current step. runTutorialStep will handle re-spawning.
runTutorialStep(); // Re-runs current step to spawn new fish
}
} else {
// For any other tutorial step, or if not paused in a catch step, "CONTINUE" advances.
GameState.tutorialStep++;
runTutorialStep();
}
} else if ((GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !GameState.tutorialPaused) {
// Catch attempt steps are now 3 and 4
// Catch attempt by tapping screen, not the continue button
handleFishingInput(x, y, true); // True for isDown
}
return;
}
switch (currentScreen) {
case 'title':
// Check if click is within start button bounds
var startButton = titleElements.startButton;
if (x >= startButton.x - startButton.width / 2 && x <= startButton.x + startButton.width / 2 && y >= startButton.y - startButton.height / 2 && y <= startButton.y + startButton.height / 2) {
showScreen('levelSelect');
}
// Check if click is within tutorial button bounds
var tutorialButtonGfx = titleElements.tutorialButtonGfx || titleElements.tutorialButton; // Compatibility
if (x >= tutorialButtonGfx.x - tutorialButtonGfx.width / 2 && x <= tutorialButtonGfx.x + tutorialButtonGfx.width / 2 && y >= tutorialButtonGfx.y - tutorialButtonGfx.height / 2 && y <= tutorialButtonGfx.y + tutorialButtonGfx.height / 2) {
// Defensive: check if startTutorial is defined before calling
if (typeof startTutorial === "function") {
startTutorial();
}
}
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 - disabled for "coming soon"
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) {
// Do nothing - button is disabled
}
// 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 () {
// Always update fishing line wave visuals if on fishing screen and elements are ready.
// This needs to run even during the intro when gameActive might be false.
if (GameState.currentScreen === 'fishing' && fishingElements && fishingElements.updateFishingLineWave) {
fishingElements.updateFishingLineWave();
}
// Update title screen ambient particles if title screen is active
if (GameState.currentScreen === 'title') {
// Add this to the game.update function, in the title screen section:
if (GameState.currentScreen === 'title' && titleElements && titleElements.updateTitleFishingLineWave) {
titleElements.updateTitleFishingLineWave();
}
// Title Screen Ocean Bubbles
if (titleScreenOceanBubbleContainer) {
titleScreenOceanBubbleSpawnCounter++;
if (titleScreenOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) {
// Use same interval as fishing
titleScreenOceanBubbleSpawnCounter = 0;
var newOceanBubble = new OceanBubbleParticle(); // Assuming OceanBubbleParticle is general enough
titleScreenOceanBubbleContainer.addChild(newOceanBubble);
titleScreenOceanBubblesArray.push(newOceanBubble);
}
for (var obIdx = titleScreenOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) {
var oceanBubble = titleScreenOceanBubblesArray[obIdx];
if (oceanBubble) {
oceanBubble.update(); // No fish interaction on title screen
if (oceanBubble.isDone) {
oceanBubble.destroy();
titleScreenOceanBubblesArray.splice(obIdx, 1);
}
} else {
titleScreenOceanBubblesArray.splice(obIdx, 1);
}
}
}
// Title Screen Seaweed
if (titleScreenSeaweedContainer) {
titleScreenSeaweedSpawnCounter++;
if (titleScreenSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && titleScreenSeaweedArray.length < MAX_SEAWEED_COUNT) {
titleScreenSeaweedSpawnCounter = 0;
var newSeaweed = new SeaweedParticle();
titleScreenSeaweedContainer.addChild(newSeaweed);
titleScreenSeaweedArray.push(newSeaweed);
}
for (var swIdx = titleScreenSeaweedArray.length - 1; swIdx >= 0; swIdx--) {
var seaweed = titleScreenSeaweedArray[swIdx];
if (seaweed) {
seaweed.update(); // No fish interaction on title screen
if (seaweed.isDone) {
seaweed.destroy();
titleScreenSeaweedArray.splice(swIdx, 1);
}
} else {
titleScreenSeaweedArray.splice(swIdx, 1);
}
}
}
// Title Screen Clouds
if (titleScreenCloudContainer) {
titleScreenCloudSpawnCounter++;
if (titleScreenCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && titleScreenCloudArray.length < MAX_CLOUD_COUNT) {
titleScreenCloudSpawnCounter = 0;
var newCloud = new CloudParticle();
titleScreenCloudContainer.addChild(newCloud);
titleScreenCloudArray.push(newCloud);
}
for (var cldIdx = titleScreenCloudArray.length - 1; cldIdx >= 0; cldIdx--) {
var cloud = titleScreenCloudArray[cldIdx];
if (cloud) {
cloud.update();
if (cloud.isDone) {
cloud.destroy();
titleScreenCloudArray.splice(cldIdx, 1);
}
} else {
titleScreenCloudArray.splice(cldIdx, 1);
}
}
}
}
// Spawn and update ambient ocean bubbles during intro and gameplay (fishing screen)
if (GameState.currentScreen === 'fishing' && globalOceanBubbleContainer) {
// Spawn bubbles during intro and gameplay
globalOceanBubbleSpawnCounter++;
if (globalOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) {
globalOceanBubbleSpawnCounter = 0;
var numToSpawn = 1; // Always spawn only 1 bubble per interval (was 1 or 2)
for (var i = 0; i < numToSpawn; i++) {
var newOceanBubble = new OceanBubbleParticle();
globalOceanBubbleContainer.addChild(newOceanBubble);
globalOceanBubblesArray.push(newOceanBubble);
}
}
// Update existing ocean bubbles
for (var obIdx = globalOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) {
var oceanBubble = globalOceanBubblesArray[obIdx];
if (oceanBubble) {
// Apply fish physics to bubble
for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) {
var fish = fishArray[fishIdx];
if (fish && !fish.caught) {
var dx = oceanBubble.x - fish.x;
var dy = oceanBubble.y - fish.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var influenceRadius = 150; // Radius of fish influence on bubbles
var minDistance = 30; // Minimum distance to avoid division issues
if (distance < influenceRadius && distance > minDistance) {
// Calculate influence strength (stronger when closer)
var influence = 1 - distance / influenceRadius;
influence = influence * influence; // Square for more dramatic close-range effect
// Calculate normalized direction away from fish
var dirX = dx / distance;
var dirY = dy / distance;
// Apply force based on fish speed and direction
var fishSpeedFactor = Math.abs(fish.speed) * 0.15; // Scale down fish speed influence
var pushForce = fishSpeedFactor * influence;
// Add horizontal push (stronger in direction of fish movement)
oceanBubble.x += dirX * pushForce * 2; // Stronger horizontal push
// Add vertical component (bubbles get pushed up/down)
oceanBubble.vy += dirY * pushForce * 0.5; // Gentler vertical influence
// Add some swirl/turbulence to drift
oceanBubble.driftAmplitude = Math.min(80, oceanBubble.driftAmplitude + pushForce * 10);
oceanBubble.driftFrequency *= 1 + influence * 0.1; // Slightly increase oscillation when disturbed
}
}
}
oceanBubble.update();
if (oceanBubble.isDone) {
oceanBubble.destroy();
globalOceanBubblesArray.splice(obIdx, 1);
}
} else {
globalOceanBubblesArray.splice(obIdx, 1); // Safeguard for null entries
}
}
}
// Spawn and update seaweed particles during intro and gameplay
if (GameState.currentScreen === 'fishing' && globalSeaweedContainer) {
// Spawn seaweed during intro and gameplay
globalSeaweedSpawnCounter++;
if (globalSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && globalSeaweedArray.length < MAX_SEAWEED_COUNT) {
globalSeaweedSpawnCounter = 0;
var newSeaweed = new SeaweedParticle();
globalSeaweedContainer.addChild(newSeaweed);
globalSeaweedArray.push(newSeaweed);
}
// Update existing seaweed
for (var swIdx = globalSeaweedArray.length - 1; swIdx >= 0; swIdx--) {
var seaweed = globalSeaweedArray[swIdx];
if (seaweed) {
// Apply fish physics to seaweed
for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) {
var fish = fishArray[fishIdx];
if (fish && !fish.caught) {
var dx = seaweed.x - fish.x;
var dy = seaweed.y - fish.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var influenceRadius = 180; // Slightly larger influence for seaweed
var minDistance = 40;
if (distance < influenceRadius && distance > minDistance) {
// Calculate influence strength
var influence = 1 - distance / influenceRadius;
influence = influence * influence;
// Calculate normalized direction away from fish
var dirX = dx / distance;
var dirY = dy / distance;
// Apply force based on fish speed
var fishSpeedFactor = Math.abs(fish.speed) * 0.2; // Stronger influence on seaweed
var pushForce = fishSpeedFactor * influence;
// Add push forces
seaweed.vx += dirX * pushForce * 1.5; // Seaweed is affected more horizontally
seaweed.vy += dirY * pushForce * 0.8; // And moderately vertically
// Increase sway when disturbed
seaweed.swayAmplitude = Math.min(60, seaweed.swayAmplitude + pushForce * 15);
}
}
}
seaweed.update();
if (seaweed.isDone) {
seaweed.destroy();
globalSeaweedArray.splice(swIdx, 1);
}
} else {
globalSeaweedArray.splice(swIdx, 1);
}
}
}
// Spawn and update cloud particles during intro and gameplay
if (GameState.currentScreen === 'fishing' && globalCloudContainer) {
// Spawn clouds during intro and gameplay
globalCloudSpawnCounter++;
if (globalCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && globalCloudArray.length < MAX_CLOUD_COUNT) {
globalCloudSpawnCounter = 0;
var newCloud = new CloudParticle();
globalCloudContainer.addChild(newCloud);
globalCloudArray.push(newCloud);
}
// Update existing clouds
for (var cldIdx = globalCloudArray.length - 1; cldIdx >= 0; cldIdx--) {
var cloud = globalCloudArray[cldIdx];
if (cloud) {
cloud.update();
if (cloud.isDone) {
cloud.destroy();
globalCloudArray.splice(cldIdx, 1);
}
} else {
globalCloudArray.splice(cldIdx, 1);
}
}
}
// Standard game active check; if intro is playing, gameActive will be false.
// Tutorial mode has its own logic path
if (GameState.currentScreen === 'fishing' && GameState.tutorialMode) {
// Update essential non-paused elements like fishing line wave
if (fishingElements && fishingElements.updateFishingLineWave) {
fishingElements.updateFishingLineWave();
}
// Update ambient particles (clouds, background bubbles, seaweed can run if desired)
// ... (Can copy particle update logic here if they should be active in tutorial)
if (!GameState.tutorialPaused) {
// Update tutorial fish if one exists and is active
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) {
GameState.tutorialFish.update();
checkTutorialFishState(); // Check its state (missed, off-screen)
}
// Hook follows tutorial fish
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) {
if (GameState.hookTargetLaneIndex !== GameState.tutorialFish.lane) {
GameState.hookTargetLaneIndex = GameState.tutorialFish.lane;
}
var targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
if (fishingElements.hook && Math.abs(fishingElements.hook.y - targetLaneY) > 1) {
// Smoother threshold
// fishingElements.hook.y = targetLaneY; // Instant for tutorial responsiveness
tween(fishingElements.hook, {
y: targetLaneY
}, {
duration: 100,
easing: tween.linear
});
fishingElements.hook.originalY = targetLaneY;
}
} else if (fishingElements.hook) {
// If no tutorial fish, hook stays in middle or last targeted lane
var middleLaneY = GAME_CONFIG.LANES[1].y;
if (fishingElements.hook.y !== middleLaneY) {
// tween(fishingElements.hook, { y: middleLaneY }, { duration: 150, easing: tween.easeOut });
// fishingElements.hook.originalY = middleLaneY;
}
}
}
updateLaneBracketsVisuals();
return; // End tutorial update logic
}
if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) {
return;
}
// Note: The fishing line wave update was previously here, it's now moved up.
var currentTime = LK.ticks * (1000 / 60);
// Initialize game timer
if (GameState.songStartTime === 0) {
GameState.songStartTime = currentTime;
}
// Check song end
var songConfig = GameState.getCurrentSongConfig();
if (currentTime - GameState.songStartTime >= songConfig.duration) {
endFishingSession();
return;
}
// Use RhythmSpawner to handle fish spawning
ImprovedRhythmSpawner.update(currentTime);
// Dynamic Hook Movement Logic
var approachingFish = null;
var minDistanceToCenter = Infinity;
for (var i = 0; i < fishArray.length; i++) {
var f = fishArray[i];
if (!f.caught) {
var distanceToHookX = Math.abs(f.x - fishingElements.hook.x);
var isApproachingOrAtHook = f.speed > 0 && f.x < fishingElements.hook.x || f.speed < 0 && f.x > fishingElements.hook.x || distanceToHookX < GAME_CONFIG.MISS_WINDOW * 2;
if (isApproachingOrAtHook && distanceToHookX < minDistanceToCenter) {
minDistanceToCenter = distanceToHookX;
approachingFish = f;
}
}
}
var targetLaneY;
if (approachingFish) {
if (GameState.hookTargetLaneIndex !== approachingFish.lane) {
GameState.hookTargetLaneIndex = approachingFish.lane;
}
targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
} else {
targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
}
// Update hook Y position (X is handled by the wave animation)
if (Math.abs(fishingElements.hook.y - targetLaneY) > 5) {
// Only tween if significantly different
tween(fishingElements.hook, {
y: targetLaneY
}, {
duration: 150,
easing: tween.easeOut
});
fishingElements.hook.originalY = targetLaneY;
} //{bO} // Re-using last relevant ID from replaced block for context if appropriate
updateLaneBracketsVisuals();
// Update fish
for (var i = fishArray.length - 1; i >= 0; i--) {
var fish = fishArray[i];
var previousFrameX = fish.lastX; // X position from the end of the previous game tick
fish.update(); // Fish updates its own movement (fish.x) and appearance
var currentFrameX = fish.x; // X position after this tick's update
// Check for miss only if fish is active (not caught, not already missed)
if (!fish.caught && !fish.missed) {
var hookCenterX = fishingElements.hook.x;
// GAME_CONFIG.MISS_WINDOW is the distance from hook center that still counts as a "miss" tap.
// If fish center passes this boundary, it's considered a full pass.
var missCheckBoundary = GAME_CONFIG.MISS_WINDOW;
if (fish.speed > 0) {
// Moving Left to Right
// Fish's center (previousFrameX) was to the left of/at the hook's right miss boundary,
// and its center (currentFrameX) is now to the right of it.
if (previousFrameX <= hookCenterX + missCheckBoundary && currentFrameX > hookCenterX + missCheckBoundary) {
showFeedback('miss', fish.lane);
LK.getSound('miss').play();
GameState.combo = 0;
fish.missed = true; // Mark as missed
}
} else if (fish.speed < 0) {
// Moving Right to Left
// Fish's center (previousFrameX) was to the right of/at the hook's left miss boundary,
// and its center (currentFrameX) is now to the left of it.
if (previousFrameX >= hookCenterX - missCheckBoundary && currentFrameX < hookCenterX - missCheckBoundary) {
showFeedback('miss', fish.lane);
LK.getSound('miss').play();
GameState.combo = 0;
fish.missed = true; // Mark as missed
}
}
}
// Update lastX for the next frame, using the fish's position *after* its update this frame
fish.lastX = currentFrameX;
// Remove off-screen fish (if not caught).
// If a fish is `missed`, it's also `!caught`, so it will be removed by this logic.
if (!fish.caught && (fish.x < -250 || fish.x > 2048 + 250)) {
// Increased buffer slightly
fish.destroy();
fishArray.splice(i, 1);
}
}
// Update UI
updateFishingUI();
// Spawn and update music notes if active
if (GameState.musicNotesActive && fishingElements && fishingElements.hook && !fishingElements.hook.destroyed && musicNotesContainer) {
musicNoteSpawnCounter++;
if (musicNoteSpawnCounter >= MUSIC_NOTE_SPAWN_INTERVAL_TICKS) {
musicNoteSpawnCounter = 0;
// Spawn notes from the fishing hook's position
var spawnX = fishingElements.hook.x;
var spawnY = fishingElements.hook.y - 30; // Spawn slightly above the hook's center for better visual origin
var newNote = new MusicNoteParticle(spawnX, spawnY);
musicNotesContainer.addChild(newNote);
musicNotesArray.push(newNote);
// Add scale pulse to the hook, synced with BPM
if (fishingElements.hook && !fishingElements.hook.destroyed && fishingElements.hook.scale) {
var currentSongConfig = GameState.getCurrentSongConfig();
var bpm = currentSongConfig && currentSongConfig.bpm ? currentSongConfig.bpm : 90; // Default to 90 BPM
var beatDurationMs = 60000 / bpm;
var pulsePhaseDuration = Math.max(50, beatDurationMs / 2); // Each phase (up/down) is half a beat, min 50ms
var pulseScaleFactor = 1.2;
// Ensure we have valid original scales, defaulting to 1 if undefined
var originalScaleX = fishingElements.hook.scale.x !== undefined ? fishingElements.hook.scale.x : 1;
var originalScaleY = fishingElements.hook.scale.y !== undefined ? fishingElements.hook.scale.y : 1;
// Stop any previous scale tweens on the hook to prevent conflicts
tween.stop(fishingElements.hook.scale);
tween(fishingElements.hook.scale, {
x: originalScaleX * pulseScaleFactor,
y: originalScaleY * pulseScaleFactor
}, {
duration: pulsePhaseDuration,
easing: tween.easeOut,
onFinish: function onFinish() {
if (fishingElements.hook && !fishingElements.hook.destroyed && fishingElements.hook.scale) {
tween(fishingElements.hook.scale, {
x: originalScaleX,
y: originalScaleY
}, {
duration: pulsePhaseDuration,
easing: tween.easeIn // Or tween.easeOut for a softer return
});
}
}
});
}
}
}
// Update existing music notes
for (var mnIdx = musicNotesArray.length - 1; mnIdx >= 0; mnIdx--) {
var note = musicNotesArray[mnIdx];
if (note) {
note.update();
if (note.isDone) {
note.destroy();
musicNotesArray.splice(mnIdx, 1);
}
} else {
// Should not happen, but good to safeguard
musicNotesArray.splice(mnIdx, 1);
}
}
// Spawn bubbles for active fish
if (bubbleContainer) {
for (var f = 0; f < fishArray.length; f++) {
var fish = fishArray[f];
if (fish && !fish.caught && !fish.isHeld && fish.fishGraphics) {
if (currentTime - fish.lastBubbleSpawnTime > fish.bubbleSpawnInterval) {
fish.lastBubbleSpawnTime = currentTime;
// Calculate tail position based on fish direction and width
// fish.fishGraphics.width is the original asset width.
// fish.fishGraphics.scale.x might be negative, but width property itself is positive.
// The anchor is 0.5, so width/2 is distance from center to edge.
var tailOffsetDirection = Math.sign(fish.speed) * -1; // Bubbles appear opposite to movement direction
var bubbleX = fish.x + tailOffsetDirection * (fish.fishGraphics.width * Math.abs(fish.fishGraphics.scaleX) / 2) * 0.8; // 80% towards tail
var bubbleY = fish.y + (Math.random() - 0.5) * (fish.fishGraphics.height * Math.abs(fish.fishGraphics.scaleY) / 4); // Slight Y variance around fish center
var newBubble = new BubbleParticle(bubbleX, bubbleY);
bubbleContainer.addChild(newBubble);
bubblesArray.push(newBubble);
}
}
}
}
// Update and remove bubbles
for (var b = bubblesArray.length - 1; b >= 0; b--) {
var bubble = bubblesArray[b];
if (bubble) {
// Extra safety check
bubble.update();
if (bubble.isDone) {
bubble.destroy();
bubblesArray.splice(b, 1);
}
} else {
// If a null/undefined somehow got in
bubblesArray.splice(b, 1);
}
}
};
// Initialize game
showScreen('title'); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var BubbleParticle = Container.expand(function (startX, startY) {
var self = Container.call(this);
self.gfx = self.attachAsset('bubbles', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 1 + Math.random() * 0.2,
// Start with some variation
scaleX: 1.2 + Math.random() * 0.25,
// Larger bubbles
// Bubbles are now 20% to 45% of original asset size
scaleY: this.scaleX // Keep aspect ratio initially, can be changed in update if desired
});
self.x = startX;
self.y = startY;
self.vx = (Math.random() - 0.5) * 0.4; // Even slower horizontal drift
self.vy = -(0.4 + Math.random() * 0.3); // Slower upward movement
self.life = 120 + Math.random() * 60; // Lifespan in frames (2 to 3 seconds)
self.age = 0;
self.isDone = false;
var initialAlpha = self.gfx.alpha;
var initialScale = self.gfx.scaleX; // Assuming scaleX and scaleY start the same
self.update = function () {
if (self.isDone) {
return;
}
self.age++;
self.x += self.vx;
self.y += self.vy;
// Fade out
self.gfx.alpha = Math.max(0, initialAlpha * (1 - self.age / self.life));
// Optionally shrink a bit more, or grow slightly then shrink
var scaleFactor = 1 - self.age / self.life; // Simple shrink
self.gfx.scaleX = initialScale * scaleFactor;
self.gfx.scaleY = initialScale * scaleFactor;
if (self.age >= self.life || self.gfx.alpha <= 0 || self.gfx.scaleX <= 0.01) {
self.isDone = true;
}
};
return self;
});
var CloudParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('cloud', {
anchorX: 0.5,
anchorY: 0.5
});
// Cloud spawn location - 30% chance to spawn on screen
var spawnOnScreen = Math.random() < 0.3;
if (spawnOnScreen) {
// Spawn randomly across the screen width
self.x = 200 + Math.random() * 1648; // Between 200 and 1848 to avoid edges
// Random horizontal drift direction
self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.1 + Math.random() * 0.2);
} else {
// Original off-screen spawn logic (70% of the time)
var spawnFromLeft = Math.random() < 0.5;
if (spawnFromLeft) {
self.x = -100; // Start off-screen left
self.vx = 0.1 + Math.random() * 0.2; // Slower drift right at 0.1-0.3 pixels/frame
} else {
self.x = 2048 + 100; // Start off-screen right
self.vx = -(0.1 + Math.random() * 0.2); // Slower drift left
}
}
// Spawn in sky area (above water surface)
var skyTop = -500; // Where sky background starts
var skyBottom = GAME_CONFIG.WATER_SURFACE_Y - 100; // Stay well above water
self.y = skyTop + Math.random() * (skyBottom - skyTop);
// Cloud properties
var baseScale = 0.8 + Math.random() * 0.6; // Scale: 0.8x to 1.4x
self.gfx.scale.set(baseScale);
// Subtle vertical drift
self.vy = (Math.random() - 0.5) * 0.02; // Even slower up/down drift
// Opacity for atmospheric effect
var targetAlpha = 0.4 + Math.random() * 0.3; // Alpha: 0.4 to 0.7
self.gfx.alpha = 0; // Start transparent
self.isDone = false;
self.fadingOut = false;
self.hasFadedIn = false; // Track if initial fade in completed
// Define screen boundaries for fade in/out
var fadeInStartX = 400; // Start fading in when cloud reaches this X
var fadeInEndX = 800; // Fully visible by this X
var fadeOutStartX = 1248; // Start fading out at this X (2048 - 800)
var fadeOutEndX = 1648; // Fully transparent by this X (2048 - 400)
self.update = function () {
if (self.isDone) {
return;
}
// Apply movement
self.x += self.vx;
self.y += self.vy;
// Handle mid-screen fade in/out based on position
var currentAlpha = self.gfx.alpha;
if (spawnFromLeft) {
// Moving right: fade in then fade out
if (!self.hasFadedIn && self.x >= fadeInStartX && self.x <= fadeInEndX) {
// Calculate fade in progress
var fadeInProgress = (self.x - fadeInStartX) / (fadeInEndX - fadeInStartX);
self.gfx.alpha = targetAlpha * fadeInProgress;
if (fadeInProgress >= 1) {
self.hasFadedIn = true;
}
} else if (self.hasFadedIn && self.x >= fadeOutStartX && self.x <= fadeOutEndX) {
// Calculate fade out progress
var fadeOutProgress = (self.x - fadeOutStartX) / (fadeOutEndX - fadeOutStartX);
self.gfx.alpha = targetAlpha * (1 - fadeOutProgress);
} else if (self.hasFadedIn && self.x > fadeInEndX && self.x < fadeOutStartX) {
// Maintain full opacity in middle section
self.gfx.alpha = targetAlpha;
}
} else {
// Moving left: fade in then fade out (reversed positions)
if (!self.hasFadedIn && self.x <= fadeOutEndX && self.x >= fadeOutStartX) {
// Calculate fade in progress (reversed)
var fadeInProgress = (fadeOutEndX - self.x) / (fadeOutEndX - fadeOutStartX);
self.gfx.alpha = targetAlpha * fadeInProgress;
if (fadeInProgress >= 1) {
self.hasFadedIn = true;
}
} else if (self.hasFadedIn && self.x <= fadeInEndX && self.x >= fadeInStartX) {
// Calculate fade out progress (reversed)
var fadeOutProgress = (fadeInEndX - self.x) / (fadeInEndX - fadeInStartX);
self.gfx.alpha = targetAlpha * (1 - fadeOutProgress);
} else if (self.hasFadedIn && self.x < fadeOutStartX && self.x > fadeInEndX) {
// Maintain full opacity in middle section
self.gfx.alpha = targetAlpha;
}
}
// Check if completely off-screen
var currentWidth = self.gfx.width * self.gfx.scale.x;
if (self.x < -currentWidth || self.x > 2048 + currentWidth) {
self.isDone = true;
}
};
return self;
});
var FeedbackIndicator = Container.expand(function (type) {
var self = Container.call(this);
var indicator = self.attachAsset(type + 'Indicator', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0
});
self.show = function () {
indicator.alpha = 1;
indicator.scaleX = 0.5;
indicator.scaleY = 0.5;
tween(indicator, {
scaleX: 1.5,
scaleY: 1.5,
alpha: 0
}, {
duration: 1400,
easing: tween.easeOut
});
};
return self;
});
/****
* Title Screen
****/
var Fish = Container.expand(function (type, value, speed, lane) {
var self = Container.call(this);
var assetName = type + 'Fish';
// Randomly pick from the three shallowFish assets if type is shallow
if (type === 'shallow') {
var shallowFishAssets = ['shallowFish', 'shallowFish2', 'shallowFish3'];
assetName = shallowFishAssets[Math.floor(Math.random() * shallowFishAssets.length)];
}
self.fishGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
if (speed > 0) {
// Moving right (coming from left), flip horizontally
self.fishGraphics.scaleX = -1;
}
self.type = type;
self.value = value;
self.speed = speed;
self.lane = lane; // Index of the lane (0, 1, or 2)
self.caught = false;
self.missed = false; // True if the fish passed the hook without being caught
self.lastX = 0; // Stores the x position from the previous frame for miss detection
self.isSpecial = type === 'rare'; // For shimmer effect
self.shimmerTime = 0;
// Bubble properties
self.lastBubbleSpawnTime = 0;
self.bubbleSpawnInterval = 120 + Math.random() * 80; // 120-200ms interval (fewer bubbles)
// Add properties for swimming animation
self.swimTime = Math.random() * Math.PI * 2; // Random starting phase for variety
self.baseY = self.y; // Store initial Y position
self.scaleTime = 0;
self.baseScale = 1;
self.update = function () {
if (!self.caught) {
// Horizontal movement
self.x += self.speed;
// Sine wave vertical movement
self.swimTime += 0.08; // Speed of sine wave oscillation
var swimAmplitude = 15; // Pixels of vertical movement
self.y = self.baseY + Math.sin(self.swimTime) * swimAmplitude;
// Beat-synchronized scale pulsing
if (GameState.gameActive && GameState.songStartTime > 0) {
var currentTime = LK.ticks * (1000 / 60);
var songConfig = GameState.getCurrentSongConfig();
var beatInterval = 60000 / songConfig.bpm;
var timeSinceLastBeat = (currentTime - GameState.songStartTime) % beatInterval;
var beatProgress = timeSinceLastBeat / beatInterval;
// Create a pulse effect that peaks at the beat
var scalePulse = 1 + Math.sin(beatProgress * Math.PI) * 0.15; // 15% scale variation
// Determine base scaleX considering direction
var baseScaleXDirection = (self.speed > 0 ? -1 : 1) * self.baseScale;
self.fishGraphics.scaleX = baseScaleXDirection * scalePulse;
self.fishGraphics.scaleY = scalePulse * self.baseScale;
}
if (self.isSpecial) {
// Shimmer effect for rare fish
self.shimmerTime += 0.1;
self.fishGraphics.alpha = 0.8 + Math.sin(self.shimmerTime) * 0.2;
} else {
// Reset alpha if not special
self.fishGraphics.alpha = 1.0;
}
}
};
self.catchFish = function () {
self.caught = true;
// Animation: Fish arcs up over the boat, then down into it.
var currentFishX = self.x;
var currentFishY = self.y;
var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X;
var boatLandingY = GAME_CONFIG.BOAT_Y; // Y-coordinate where the fish "lands" in the boat
// Define the peak of the arc - e.g., 150 pixels above the boat's landing spot
var peakArcY = boatLandingY - 150;
// Define the X coordinate at the peak of the arc - halfway between current X and boat center X
var peakArcX = currentFishX + (boatCenterX - currentFishX) * 0.5;
var durationPhase1 = 350; // Duration for the first part of the arc (upwards)
var durationPhase2 = 250; // Duration for the second part of the arc (downwards), total 600ms
// Phase 1: Arc upwards and towards the boat's horizontal center.
// The container 'self' is tweened for position, scale, and alpha.
// Initial self.scale.x and self.scale.y are expected to be 1 for the container.
tween(self, {
x: peakArcX,
y: peakArcY,
scaleX: 0.75,
// Scale container to 75% of its original size
scaleY: 0.75,
alpha: 0.8 // Partially fade
}, {
duration: durationPhase1,
easing: tween.easeOut,
// Ease out for the upward motion to the peak
onFinish: function onFinish() {
// Phase 2: Arc downwards into the boat and disappear.
tween(self, {
x: boatCenterX,
y: boatLandingY,
scaleX: 0.2,
// Shrink container further to 20% of its original size
scaleY: 0.2,
alpha: 0 // Fade out completely
}, {
duration: durationPhase2,
easing: tween.easeIn,
// Ease in for the downward motion into the boat
onFinish: function onFinish() {
self.destroy(); // Remove fish once animation is complete
}
});
}
});
};
return self;
});
var MusicNoteParticle = Container.expand(function (startX, startY) {
var self = Container.call(this);
var FADE_IN_DURATION_MS = 600; // Duration for the note to fade in
var TARGET_ALPHA = 0.6 + Math.random() * 0.4; // Target alpha between 0.6 and 1.0 for variety
self.gfx = self.attachAsset('musicnote', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0,
// Start invisible for fade-in
scaleX: 0.4 + Math.random() * 0.4 // Random initial scale (0.4x to 0.8x)
});
self.gfx.scaleY = self.gfx.scaleX; // Maintain aspect ratio
self.x = startX;
self.y = startY;
// Animation properties for lazy floating
self.vx = (Math.random() - 0.5) * 0.8; // Slow horizontal drift speed
self.vy = -(0.8 + Math.random() * 0.7); // Steady upward speed (0.8 to 1.5 pixels/frame)
self.rotationSpeed = (Math.random() - 0.5) * 0.008; // Very slow rotation
self.life = 240 + Math.random() * 120; // Lifespan in frames (4 to 6 seconds)
self.age = 0;
self.isDone = false;
// Initial fade-in tween
tween(self.gfx, {
alpha: TARGET_ALPHA
}, {
duration: FADE_IN_DURATION_MS,
easing: tween.easeOut
});
self.update = function () {
if (self.isDone) {
return;
}
self.age++;
self.x += self.vx;
self.y += self.vy;
self.gfx.rotation += self.rotationSpeed;
var FADE_IN_TICKS = FADE_IN_DURATION_MS / (1000 / 60); // Fade-in duration in ticks
// Only manage alpha manually after the fade-in tween is expected to be complete.
if (self.age > FADE_IN_TICKS) {
var lifePortionForFadeOut = 0.6; // Use last 60% of life for fade out
var fadeOutStartTimeTicks = self.life * (1 - lifePortionForFadeOut);
if (self.age >= fadeOutStartTimeTicks && self.life > fadeOutStartTimeTicks) {
// ensure self.life > fadeOutStartTimeTicks to avoid division by zero
var progressInFadeOut = (self.age - fadeOutStartTimeTicks) / (self.life * lifePortionForFadeOut);
self.gfx.alpha = TARGET_ALPHA * (1 - progressInFadeOut);
self.gfx.alpha = Math.max(0, self.gfx.alpha); // Clamp at 0
} else if (self.age <= fadeOutStartTimeTicks) {
// At this point, the initial fade-in tween to TARGET_ALPHA should have completed.
// The alpha value is expected to remain at TARGET_ALPHA (as set by the initial tween)
// until the fade-out logic (in the 'if (self.age >= fadeOutStartTimeTicks)' block) begins.
// The previous 'tween.isTweening' check was removed as the method does not exist in the plugin.
}
}
// Check if particle's life is over or it has faded out
if (self.age >= self.life || self.gfx.alpha !== undefined && self.gfx.alpha <= 0.01 && self.age > FADE_IN_TICKS) {
self.isDone = true;
}
};
return self;
});
var OceanBubbleParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('oceanbubbles', {
anchorX: 0.5,
anchorY: 0.5
});
self.initialX = Math.random() * 2048;
var waterTop = GAME_CONFIG.WATER_SURFACE_Y;
var waterBottom = 2732;
self.x = self.initialX;
// Allow bubbles to spawn anywhere in the water, not just below the bottom
self.y = waterTop + Math.random() * (waterBottom - waterTop);
var baseScale = 0.1 + Math.random() * 0.4; // Scale: 0.1x to 0.5x
self.gfx.scale.set(baseScale);
self.vy = -(0.25 + Math.random() * 0.5); // Upward speed: 0.25 to 0.75 pixels/frame (slower, less variance)
self.naturalVy = self.vy; // Store natural velocity for recovery
self.driftAmplitude = 20 + Math.random() * 40; // Sideways drift: 20px to 60px amplitude
self.naturalDriftAmplitude = self.driftAmplitude; // Store natural drift for recovery
self.driftFrequency = (0.005 + Math.random() * 0.015) * (Math.random() < 0.5 ? 1 : -1); // Sideways drift speed/direction
self.driftPhase = Math.random() * Math.PI * 2; // Initial phase for sine wave
self.rotationSpeed = (Math.random() - 0.5) * 0.01; // Slow random rotation
var targetAlpha = 0.2 + Math.random() * 0.3; // Max alpha: 0.2 to 0.5 (dimmer background bubbles)
self.gfx.alpha = 0; // Start transparent
self.isDone = false;
self.fadingOut = false;
tween(self.gfx, {
alpha: targetAlpha
}, {
duration: 1000 + Math.random() * 1000,
// Slow fade in: 1 to 2 seconds
easing: tween.easeIn
});
self.update = function () {
if (self.isDone) {
return;
}
self.y += self.vy;
// Increment age
self.age++;
// Check if lifespan exceeded
if (!self.fadingOut && self.age >= self.lifespan) {
self.fadingOut = true;
tween.stop(self.gfx);
tween(self.gfx, {
alpha: 0
}, {
duration: 600 + Math.random() * 400,
// 0.6-1 second fade
easing: tween.easeOut,
onFinish: function onFinish() {
self.isDone = true;
}
});
}
self.driftPhase += self.driftFrequency;
self.x = self.initialX + Math.sin(self.driftPhase) * self.driftAmplitude;
self.gfx.rotation += self.rotationSpeed;
// Recovery mechanism: gradually return to natural upward movement
var naturalVy = -(0.25 + Math.random() * 0.5); // Natural upward speed
var recoveryRate = 0.02; // How quickly bubble recovers (2% per frame)
// If bubble is moving slower than its natural speed or downward, recover
if (self.vy > naturalVy) {
self.vy = self.vy + (naturalVy - self.vy) * recoveryRate;
}
// Also gradually reduce excessive drift amplitude back to normal
var normalDriftAmplitude = 20 + Math.random() * 40;
if (self.driftAmplitude > normalDriftAmplitude) {
self.driftAmplitude = self.driftAmplitude + (normalDriftAmplitude - self.driftAmplitude) * recoveryRate;
}
// Check if bubble reached surface or went off-screen
// Use gfx.height * current scale for accurate boundary check
var currentHeight = self.gfx.height * self.gfx.scale.y;
var currentWidth = self.gfx.width * self.gfx.scale.x;
if (!self.fadingOut && self.y <= GAME_CONFIG.WATER_SURFACE_Y - currentHeight * 0.5) {
self.fadingOut = true;
tween.stop(self.gfx); // Stop fade-in if ongoing
tween(self.gfx, {
alpha: 0
}, {
duration: 300 + Math.random() * 200,
// Quick fade out
easing: tween.easeOut,
onFinish: function onFinish() {
self.isDone = true; // Mark as fully done for removal
}
});
} else if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth)) {
// Off screen top/sides before reaching surface and starting fade
self.isDone = true;
self.gfx.alpha = 0; // Disappear immediately
}
};
return self;
});
var SeaweedParticle = Container.expand(function () {
var self = Container.call(this);
self.gfx = self.attachAsset('kelp', {
anchorX: 0.5,
anchorY: 0.5
});
// Determine spawn location
var spawnType = Math.random();
var waterTop = GAME_CONFIG.WATER_SURFACE_Y;
var waterBottom = 2732;
if (spawnType < 0.4) {
// 40% spawn from bottom
self.x = Math.random() * 2048;
self.y = waterBottom + 50;
self.vx = (Math.random() - 0.5) * 0.3; // Slight horizontal drift
self.vy = -(0.4 + Math.random() * 0.3); // Upward movement
} else if (spawnType < 0.7) {
// 30% spawn from left
self.x = -50;
self.y = waterTop + Math.random() * (waterBottom - waterTop);
self.vx = 0.4 + Math.random() * 0.3; // Rightward movement
self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift
} else {
// 30% spawn from right
self.x = 2048 + 50;
self.y = waterTop + Math.random() * (waterBottom - waterTop);
self.vx = -(0.4 + Math.random() * 0.3); // Leftward movement
self.vy = -(0.1 + Math.random() * 0.2); // Slight upward drift
}
self.initialX = self.x;
self.naturalVx = self.vx; // Store natural velocity for recovery
self.naturalVy = self.vy;
// Seaweed properties
var baseScale = 0.6 + Math.random() * 0.6; // Scale: 0.6x to 1.2x
self.gfx.scale.set(baseScale);
self.swayAmplitude = 15 + Math.random() * 25; // Sway: 15px to 40px
self.swayFrequency = (0.003 + Math.random() * 0.007) * (Math.random() < 0.5 ? 1 : -1);
self.swayPhase = Math.random() * Math.PI * 2;
// Random initial rotation (full 360 degrees)
self.gfx.rotation = Math.random() * Math.PI * 2;
// Random continuous rotation speed (slower than ocean bubbles)
self.continuousRotationSpeed = (Math.random() - 0.5) * 0.003; // -0.0015 to 0.0015 radians per frame
var targetAlpha = 0.3 + Math.random() * 0.3; // Alpha: 0.3 to 0.6
self.gfx.alpha = 0; // Start transparent
self.isDone = false;
self.fadingOut = false;
self.reachedSurface = false;
// Add random lifespan (10-30 seconds)
self.lifespan = 600 + Math.random() * 1200; // 600-1800 frames (10-30 seconds at 60fps)
self.age = 0;
tween(self.gfx, {
alpha: targetAlpha
}, {
duration: 1500 + Math.random() * 1000,
easing: tween.easeIn
});
self.update = function () {
if (self.isDone) {
return;
}
// Apply movement
self.x += self.vx;
self.y += self.vy;
// Add sway effect
self.swayPhase += self.swayFrequency;
var swayOffset = Math.sin(self.swayPhase) * self.swayAmplitude;
// Apply continuous rotation plus sway-based rotation
self.gfx.rotation += self.continuousRotationSpeed + swayOffset * 0.0001; // Reduced sway rotation influence
// Recovery mechanism for velocity
var recoveryRate = 0.015;
if (self.vx !== self.naturalVx) {
self.vx = self.vx + (self.naturalVx - self.vx) * recoveryRate;
}
if (self.vy !== self.naturalVy) {
self.vy = self.vy + (self.naturalVy - self.vy) * recoveryRate;
}
// Check if reached surface
var currentHeight = self.gfx.height * self.gfx.scale.y;
var currentWidth = self.gfx.width * self.gfx.scale.x;
if (!self.reachedSurface && self.y <= GAME_CONFIG.WATER_SURFACE_Y + currentHeight * 0.3) {
self.reachedSurface = true;
// Change to horizontal drift at surface
self.vy = 0;
self.vx = (Math.random() < 0.5 ? 1 : -1) * (0.5 + Math.random() * 0.5);
self.naturalVx = self.vx;
self.naturalVy = 0;
}
// Check if off-screen
if (!self.fadingOut && (self.y < -currentHeight || self.x < -currentWidth || self.x > 2048 + currentWidth || self.y > waterBottom + currentHeight)) {
self.fadingOut = true;
tween.stop(self.gfx);
tween(self.gfx, {
alpha: 0
}, {
duration: 400 + Math.random() * 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isDone = true;
}
});
}
};
return self;
});
/****
* Initialize Game
****/
/****
* Screen Containers
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
// Constants for Title Screen Animation
var TITLE_ANIM_CONSTANTS = {
INITIAL_GROUP_ALPHA: 0,
FINAL_GROUP_ALPHA: 1,
INITIAL_UI_ALPHA: 0,
FINAL_UI_ALPHA: 1,
INITIAL_GROUP_SCALE: 3.5,
// Start with an extreme closeup
FINAL_GROUP_SCALE: 2.8,
// Zoom out slightly, staying closer
GROUP_ANIM_DURATION: 4000,
// Slower duration for group fade-in and zoom
TEXT_FADE_DURATION: 1000,
// Duration for title text fade-in
BUTTON_FADE_DURATION: 800,
// Duration for buttons fade-in
// Positioning constants relative to titleAnimationGroup's origin (0,0) which is boat's center
BOAT_ANCHOR_X: 0.5,
BOAT_ANCHOR_Y: 0.5,
FISHERMAN_ANCHOR_X: 0.5,
FISHERMAN_ANCHOR_Y: 0.9,
// Anchor at feet
FISHERMAN_X_OFFSET: -20,
// Relative to boat center
FISHERMAN_Y_OFFSET: -100,
// Relative to boat center, fisherman sits on boat
LINE_ANCHOR_X: 0.5,
LINE_ANCHOR_Y: 0,
// Anchor at top of line
LINE_X_OFFSET_FROM_FISHERMAN: 70,
// Rod tip X from fisherman center
LINE_Y_OFFSET_FROM_FISHERMAN: -130,
// Rod tip Y from fisherman center (fisherman height ~200, anchorY 0.9)
HOOK_ANCHOR_X: 0.5,
HOOK_ANCHOR_Y: 0.5,
HOOK_Y_DEPTH_FROM_LINE_START: 700,
// Slightly longer line for the closeup
// titleAnimationGroup positioning
GROUP_PIVOT_X: 0,
// Boat's X in the group
GROUP_PIVOT_Y: 0,
// Boat's Y in the group
GROUP_INITIAL_Y_SCREEN_OFFSET: -450 // Adjusted for closer initial zoom
};
// If game.up already exists, integrate the 'fishing' case. Otherwise, this defines game.up.
/****
* Pattern Generation System
****/
var PatternGenerator = {
lastLane: -1,
minDistanceBetweenFish: 300,
// Used by spawnFish internal check
// Minimum X distance between fish for visual clarity on hook
lastActualSpawnTime: -100000,
// Time of the last actual fish spawn
getNextLane: function getNextLane() {
if (this.lastLane === -1) {
// First fish, start in middle lane
this.lastLane = 1;
return 1;
}
// Prefer staying in same lane or moving to adjacent lane
var possibleLanes = [this.lastLane];
// Add adjacent lanes
if (this.lastLane > 0) {
possibleLanes.push(this.lastLane - 1);
}
if (this.lastLane < 2) {
possibleLanes.push(this.lastLane + 1);
}
// 70% chance to stay in same/adjacent lane
if (Math.random() < 0.7) {
this.lastLane = possibleLanes[Math.floor(Math.random() * possibleLanes.length)];
} else {
// 30% chance for any lane
this.lastLane = Math.floor(Math.random() * 3);
}
return this.lastLane;
},
// New method: Checks if enough time has passed since the last spawn.
canSpawnFishOnBeat: function canSpawnFishOnBeat(currentTime, configuredSpawnInterval) {
var timeSinceLast = currentTime - this.lastActualSpawnTime;
var minRequiredGap = configuredSpawnInterval; // Default gap is the song's beat interval for fish
return timeSinceLast >= minRequiredGap;
},
// New method: Registers details of the fish that was just spawned.
registerFishSpawn: function registerFishSpawn(spawnTime) {
this.lastActualSpawnTime = spawnTime;
},
reset: function reset() {
this.lastLane = -1;
this.lastActualSpawnTime = -100000; // Set far in the past to allow first spawn
}
};
/****
* Game Configuration
****/
game.up = function (x, y, obj) {
// Note: We don't play buttonClick sound on 'up' typically, only on 'down'.
switch (GameState.currentScreen) {
case 'title':
// title screen up actions (if any)
break;
case 'levelSelect':
// level select screen up actions (if any, usually 'down' is enough for buttons)
break;
case 'fishing':
handleFishingInput(x, y, false); // false for isUp
break;
case 'results':
// results screen up actions (if any)
break;
}
};
var GAME_CONFIG = {
SCREEN_CENTER_X: 1024,
SCREEN_CENTER_Y: 900,
// Adjusted, though less critical with lanes
BOAT_Y: 710,
// Original 300 + 273 (10%) + 137 (5%) = 710
WATER_SURFACE_Y: 760,
// Original 350 + 273 (10%) + 137 (5%) = 760
// 3 Lane System
// Y-positions for each lane, adjusted downwards further
LANES: [{
y: 1133,
// Top lane Y: (723 + 273) + 137 = 996 + 137 = 1133
name: "shallow"
}, {
y: 1776,
// Middle lane Y: (996 + 643) + 137 = 1639 + 137 = 1776
name: "medium"
}, {
y: 2419,
// Bottom lane Y: (1639 + 643) + 137 = 2282 + 137 = 2419
name: "deep"
}],
// Timing windows
PERFECT_WINDOW: 40,
GOOD_WINDOW: 80,
MISS_WINDOW: 120,
// Depth levels - reduced money values!
DEPTHS: [{
level: 1,
name: "Shallow Waters",
fishSpeed: 6,
fishValue: 1,
upgradeCost: 0,
// Starting depth
songs: [{
name: "Gentle Waves",
bpm: 90,
duration: 202000,
// 3:22
pattern: "gentle_waves_custom",
cost: 0
}, {
name: "Morning Tide",
bpm: 90,
duration: 156827,
pattern: "morning_tide_custom",
cost: 0,
musicId: 'morningtide'
}, {
name: "Sunny Afternoon",
bpm: 97,
duration: 181800,
// 3:01.8
pattern: "sunny_afternoon_custom",
cost: 0,
musicId: 'sunnyafternoon'
}]
}, {
level: 2,
name: "Mid Waters",
fishSpeed: 7,
fishValue: 2,
upgradeCost: 100,
songs: [{
name: "Ocean Current",
bpm: 120,
duration: 90000,
pattern: "medium",
cost: 0
}, {
name: "Deep Flow",
bpm: 125,
duration: 100000,
pattern: "medium",
cost: 150
}]
}, {
level: 3,
name: "Deep Waters",
fishSpeed: 8,
fishValue: 3,
upgradeCost: 400,
songs: [{
name: "Storm Surge",
bpm: 140,
duration: 120000,
pattern: "complex",
cost: 0
}, {
name: "Whirlpool",
bpm: 150,
duration: 135000,
pattern: "complex",
cost: 300
}]
}, {
level: 4,
name: "Abyss",
fishSpeed: 9,
fishValue: 6,
upgradeCost: 1000,
songs: [{
name: "Leviathan",
bpm: 160,
duration: 150000,
pattern: "expert",
cost: 0
}, {
name: "Deep Trench",
bpm: 170,
duration: 180000,
pattern: "expert",
cost: 600
}]
}],
// Updated patterns
PATTERNS: {
simple: {
beatsPerFish: 2,
// Increased from 1 - fish every 2 beats
doubleSpawnChance: 0.10,
// 10% chance of a double beat in simple
rareSpawnChance: 0.02
},
medium: {
beatsPerFish: 1.5,
// Increased from 0.75
doubleSpawnChance: 0.15,
//{1R} // 15% chance of a double beat
rareSpawnChance: 0.05
},
complex: {
beatsPerFish: 1,
// Increased from 0.5
doubleSpawnChance: 0.25,
//{1U} // 25% chance of a double beat
rareSpawnChance: 0.08
},
expert: {
beatsPerFish: 0.75,
// Increased from 0.25
doubleSpawnChance: 0.35,
//{1X} // 35% chance of a double beat
tripletSpawnChance: 0.20,
// 20% chance a double beat becomes a triplet
rareSpawnChance: 0.12
},
gentle_waves_custom: {
beatsPerFish: 1.5,
// Slightly more frequent than default simple
doubleSpawnChance: 0.05,
// Very rare double beats for shallow
rareSpawnChance: 0.01,
// Almost no rare fish
// Custom timing sections based on the musical structure
sections: [
// Opening - Simple chord pattern (0-30 seconds)
{
startTime: 0,
endTime: 30000,
spawnModifier: 1.0,
// Normal spawn rate
description: "steady_chords"
},
// Melody Introduction (30-60 seconds)
{
startTime: 30000,
endTime: 60000,
spawnModifier: 0.9,
// Slightly fewer fish
description: "simple_melody"
},
// Development (60-120 seconds) - Gets a bit busier
{
startTime: 60000,
endTime: 120000,
spawnModifier: 1.1,
// Slightly more fish
description: "melody_development"
},
// Climax (120-180 seconds) - Busiest section but still shallow
{
startTime: 120000,
endTime: 180000,
spawnModifier: 1.3,
// More fish, but not overwhelming
description: "gentle_climax"
},
// Ending (180-202 seconds) - Calming down
{
startTime: 180000,
endTime: 202000,
spawnModifier: 0.8,
// Fewer fish for gentle ending
description: "peaceful_ending"
}]
},
morning_tide_custom: {
beatsPerFish: 1.2,
// More frequent than gentle_waves_custom (1.5) and much more than simple (2)
doubleSpawnChance: 0.12,
// Higher than simple (0.10)
rareSpawnChance: 0.03,
// Higher than simple (0.02)
// Custom timing sections based on musical structure
sections: [
// Gentle opening - ease into the song (0-25 seconds)
{
startTime: 0,
endTime: 25000,
spawnModifier: 0.9,
// Slightly reduced for intro
description: "calm_opening"
},
// Building energy - first wave (25-50 seconds)
{
startTime: 25000,
endTime: 50000,
spawnModifier: 1.2,
// More active
description: "first_wave"
},
// Peak intensity - morning rush (50-80 seconds)
{
startTime: 50000,
endTime: 80000,
spawnModifier: 1.5,
// Most intense section
description: "morning_rush"
},
// Sustained energy - second wave (80-110 seconds)
{
startTime: 80000,
endTime: 110000,
spawnModifier: 1.3,
// High but slightly less than peak
description: "second_wave"
},
// Climactic finish (110-140 seconds)
{
startTime: 110000,
endTime: 140000,
spawnModifier: 1.4,
// Building back up
description: "climactic_finish"
},
// Gentle fade out (140-156.8 seconds)
{
startTime: 140000,
endTime: 156827,
spawnModifier: 0.8,
// Calm ending
description: "peaceful_fade"
}]
},
sunny_afternoon_custom: {
beatsPerFish: 1.3,
// Slightly faster than morning_tide_custom but still beginner-friendly
doubleSpawnChance: 0.08,
// Moderate chance for variety
rareSpawnChance: 0.025,
// Slightly better rewards than gentle_waves
sections: [
// Gentle warm-up (0-20 seconds)
{
startTime: 0,
endTime: 20000,
spawnModifier: 0.8,
description: "warm_sunny_start"
},
// First activity burst (20-35 seconds)
{
startTime: 20000,
endTime: 35000,
spawnModifier: 1.4,
description: "first_sunny_burst"
},
// Breathing room (35-50 seconds)
{
startTime: 35000,
endTime: 50000,
spawnModifier: 0.7,
description: "sunny_breather_1"
},
// Second activity burst (50-70 seconds)
{
startTime: 50000,
endTime: 70000,
spawnModifier: 1.5,
description: "second_sunny_burst"
},
// Extended breathing room (70-90 seconds)
{
startTime: 70000,
endTime: 90000,
spawnModifier: 0.6,
description: "sunny_breather_2"
},
// Third activity burst (90-110 seconds)
{
startTime: 90000,
endTime: 110000,
spawnModifier: 1.3,
description: "third_sunny_burst"
},
// Breathing room (110-125 seconds)
{
startTime: 110000,
endTime: 125000,
spawnModifier: 0.8,
description: "sunny_breather_3"
},
// Final activity section (125-150 seconds)
{
startTime: 125000,
endTime: 150000,
spawnModifier: 1.2,
description: "sunny_finale_buildup"
},
// Wind down (150-181.8 seconds)
{
startTime: 150000,
endTime: 181800,
spawnModifier: 0.9,
description: "sunny_afternoon_fade"
}]
}
}
};
/****
* Game State Management
****/
var MULTI_BEAT_SPAWN_DELAY_MS = 250; // ms delay for sequential spawns in multi-beats (increased for more space)
var TRIPLET_BEAT_SPAWN_DELAY_MS = 350; // ms delay for third fish in a triplet (even more space)
var FISH_SPAWN_END_BUFFER_MS = 500; // Just 0.5 seconds buffer
var ImprovedRhythmSpawner = {
nextBeatToSchedule: 1,
scheduledBeats: [],
update: function update(currentTime) {
if (!GameState.gameActive || GameState.songStartTime === 0) {
return;
}
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
var beatInterval = 60000 / songConfig.bpm;
var spawnInterval = beatInterval * pattern.beatsPerFish;
// Calculate how far ahead to look (use original logic)
var depthConfig = GameState.getCurrentDepthConfig();
var fishSpeed = Math.abs(depthConfig.fishSpeed);
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
var travelTimeMs = distanceToHook / fishSpeed * (1000 / 60);
var beatsAhead = Math.ceil(travelTimeMs / spawnInterval) + 2;
// Schedule beats ahead
var songElapsed = currentTime - GameState.songStartTime;
var currentSongBeat = songElapsed / spawnInterval;
var maxBeatToSchedule = Math.floor(currentSongBeat) + beatsAhead;
while (this.nextBeatToSchedule <= maxBeatToSchedule) {
if (this.scheduledBeats.indexOf(this.nextBeatToSchedule) === -1) {
this.scheduleBeatFish(this.nextBeatToSchedule, spawnInterval, travelTimeMs);
this.scheduledBeats.push(this.nextBeatToSchedule);
}
this.nextBeatToSchedule++;
}
// NO FISH SYNCING - let them move naturally!
},
scheduleBeatFish: function scheduleBeatFish(beatNumber, spawnInterval, travelTimeMs) {
var targetArrivalTime = GameState.songStartTime + beatNumber * spawnInterval;
var songConfig = GameState.getCurrentSongConfig();
// FIXED: The buffer should be applied to when the song ACTUALLY ends,
// not when the fish arrives at the hook
var songEndTime = GameState.songStartTime + songConfig.duration;
var lastValidArrivalTime = songEndTime - FISH_SPAWN_END_BUFFER_MS;
if (songConfig && GameState.songStartTime > 0 && targetArrivalTime > lastValidArrivalTime) {
return; // Don't schedule this fish, it would arrive too close to song end
}
var spawnTime = targetArrivalTime - travelTimeMs;
var currentTime = LK.ticks * (1000 / 60);
// Rest of function stays the same...
if (spawnTime >= currentTime - 100) {
var delay = Math.max(0, spawnTime - currentTime);
var self = this;
LK.setTimeout(function () {
if (GameState.gameActive && GameState.songStartTime !== 0) {
self.spawnRhythmFish(beatNumber, targetArrivalTime);
}
}, delay);
}
},
spawnRhythmFish: function spawnRhythmFish(beatNumber, targetArrivalTime) {
if (!GameState.gameActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60);
var depthConfig = GameState.getCurrentDepthConfig();
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
// Calculate spawnInterval for this pattern
var beatInterval = 60000 / songConfig.bpm;
var spawnInterval = beatInterval * pattern.beatsPerFish;
// Apply section modifiers if pattern has sections
var spawnModifier = 1.0;
if (pattern.sections) {
var songElapsed = currentTime - GameState.songStartTime;
for (var s = 0; s < pattern.sections.length; s++) {
var section = pattern.sections[s];
if (songElapsed >= section.startTime && songElapsed <= section.endTime) {
spawnModifier = section.spawnModifier;
break;
}
}
}
// Check if we should spawn
if (!PatternGenerator.canSpawnFishOnBeat(currentTime, beatInterval)) {
return;
}
// Apply spawn modifier chance
if (Math.random() > spawnModifier) {
return;
}
var laneIndex = PatternGenerator.getNextLane();
var targetLane = GAME_CONFIG.LANES[laneIndex];
// Fish type selection (original logic)
var fishType, fishValue;
var rand = Math.random();
if (rand < pattern.rareSpawnChance) {
fishType = 'rare';
fishValue = Math.floor(depthConfig.fishValue * 4);
} else if (GameState.selectedDepth >= 2 && rand < 0.3) {
fishType = 'deep';
fishValue = Math.floor(depthConfig.fishValue * 2);
} else if (GameState.selectedDepth >= 1 && rand < 0.6) {
fishType = 'medium';
fishValue = Math.floor(depthConfig.fishValue * 1.5);
} else {
fishType = 'shallow';
fishValue = Math.floor(depthConfig.fishValue);
}
// Calculate precise speed to arrive exactly on beat
var timeRemainingMs = targetArrivalTime - currentTime;
if (timeRemainingMs <= 0) {
return;
}
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
var spawnSide = Math.random() < 0.5 ? -1 : 1;
// Calculate frames remaining and required speed per frame
var framesRemaining = timeRemainingMs / (1000 / 60);
if (framesRemaining <= 0) {
return;
}
var requiredSpeedPerFrame = distanceToHook / framesRemaining;
if (spawnSide === 1) {
requiredSpeedPerFrame *= -1; // Moving left
}
// Create fish with exact calculated speed - NO SYNC DATA
var newFish = new Fish(fishType, fishValue, requiredSpeedPerFrame, laneIndex);
newFish.spawnSide = spawnSide;
newFish.targetArrivalTime = targetArrivalTime;
newFish.x = requiredSpeedPerFrame > 0 ? -150 : 2048 + 150;
newFish.y = targetLane.y;
newFish.baseY = targetLane.y;
newFish.lastX = newFish.x; // Initialize lastX for miss detection
// newFish.missed is false by default from constructor
// Don't add any syncData - let fish move naturally!
fishArray.push(newFish);
fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTime);
// Handle multi-spawns
this.handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime);
return newFish;
},
handleMultiSpawns: function handleMultiSpawns(beatNumber, pattern, songConfig, spawnInterval, currentTime) {
if (pattern.doubleSpawnChance > 0 && Math.random() < pattern.doubleSpawnChance) {
var travelTimeMs = this.calculateTravelTime();
var nextFishBeat = beatNumber + 1;
if (this.scheduledBeats.indexOf(nextFishBeat) === -1) {
this.scheduleBeatFish(nextFishBeat, spawnInterval, travelTimeMs);
this.scheduledBeats.push(nextFishBeat);
}
// Handle triplets
if (pattern.tripletSpawnChance && pattern.tripletSpawnChance > 0 && Math.random() < pattern.tripletSpawnChance) {
var thirdFishBeat = beatNumber + 2;
if (this.scheduledBeats.indexOf(thirdFishBeat) === -1) {
this.scheduleBeatFish(thirdFishBeat, spawnInterval, travelTimeMs);
this.scheduledBeats.push(thirdFishBeat);
}
}
}
},
calculateTravelTime: function calculateTravelTime() {
var depthConfig = GameState.getCurrentDepthConfig();
var fishSpeed = Math.abs(depthConfig.fishSpeed);
if (fishSpeed === 0) {
return Infinity;
}
var distanceToHook = GAME_CONFIG.SCREEN_CENTER_X + 150;
return distanceToHook / fishSpeed * (1000 / 60);
},
reset: function reset() {
this.nextBeatToSchedule = 1;
this.scheduledBeats = [];
},
getDebugInfo: function getDebugInfo() {
return {
nextBeat: this.nextBeatToSchedule,
scheduledCount: this.scheduledBeats.length,
scheduledBeatsPreview: this.scheduledBeats.slice(-10)
};
}
};
/****
* Tutorial System - Global Scope
****/
function updateLaneBracketsVisuals() {
if (laneBrackets && laneBrackets.length === GAME_CONFIG.LANES.length) {
for (var i = 0; i < laneBrackets.length; i++) {
var isActiveLane = i === GameState.hookTargetLaneIndex;
var targetAlpha = isActiveLane ? 0.9 : 0.5;
if (laneBrackets[i] && laneBrackets[i].left && !laneBrackets[i].left.destroyed) {
if (laneBrackets[i].left.alpha !== targetAlpha) {
laneBrackets[i].left.alpha = targetAlpha;
}
}
if (laneBrackets[i] && laneBrackets[i].right && !laneBrackets[i].right.destroyed) {
if (laneBrackets[i].right.alpha !== targetAlpha) {
laneBrackets[i].right.alpha = targetAlpha;
}
}
}
}
}
function createTutorialElements() {
tutorialOverlayContainer.removeChildren(); // Clear previous elements
tutorialTextBackground = tutorialOverlayContainer.addChild(LK.getAsset('screenBackground', {
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 2732 * 0.85,
// Position towards the bottom
width: 1800,
height: 450,
// Increased height
color: 0x000000,
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.75
}));
tutorialTextDisplay = tutorialOverlayContainer.addChild(new Text2('', {
size: 55,
// Increased font size
fill: 0xFFFFFF,
wordWrap: true,
wordWrapWidth: 1700,
// Keep wordWrapWidth, adjust if lines are too cramped
// Slightly less than background width
align: 'center',
lineHeight: 65 // Increased line height
}));
tutorialTextDisplay.anchor.set(0.5, 0.5);
tutorialTextDisplay.x = tutorialTextBackground.x;
tutorialTextDisplay.y = tutorialTextBackground.y - 50; // Adjusted Y for new background height and text size
tutorialContinueButton = tutorialOverlayContainer.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: tutorialTextBackground.x,
y: tutorialTextBackground.y + tutorialTextBackground.height / 2 - 55,
// Adjusted Y for new background height
// Positioned at the bottom of the text background
tint: 0x1976d2,
// Blue color
width: 350,
// Standard button size
height: 70
}));
tutorialContinueText = tutorialOverlayContainer.addChild(new Text2('CONTINUE', {
// Changed text
size: 34,
fill: 0xFFFFFF
}));
tutorialContinueText.anchor.set(0.5, 0.5);
tutorialContinueText.x = tutorialContinueButton.x;
tutorialContinueText.y = tutorialContinueButton.y;
tutorialOverlayContainer.visible = false; // Initially hidden
}
function setTutorialText(newText, showContinue) {
if (showContinue === undefined) {
showContinue = true;
}
if (!tutorialTextDisplay || !tutorialContinueButton || !tutorialContinueText) {
createTutorialElements(); // Ensure elements exist
}
tutorialTextDisplay.setText(newText);
tutorialContinueButton.visible = showContinue;
tutorialContinueText.visible = showContinue;
tutorialOverlayContainer.visible = true;
}
function spawnTutorialFishHelper(config) {
var fishType = config.type || 'shallow';
// Use a consistent depth config for tutorial fish, e.g., shallowest
var depthConfig = GAME_CONFIG.DEPTHS[0];
var fishValue = Math.floor(depthConfig.fishValue / 2); // Tutorial fish might be worth less or nothing
var baseSpeed = depthConfig.fishSpeed;
var speedMultiplier = config.speedMultiplier || 0.5; // Slower for tutorial
var laneIndex = config.lane !== undefined ? config.lane : 1;
var spawnSide = config.spawnSide !== undefined ? config.spawnSide : Math.random() < 0.5 ? -1 : 1;
var actualFishSpeed = Math.abs(baseSpeed) * speedMultiplier * spawnSide;
var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex);
newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen
newFish.y = GAME_CONFIG.LANES[laneIndex].y + (config.yOffset || 0);
newFish.baseY = newFish.y;
newFish.lastX = newFish.x;
newFish.tutorialFish = true; // Custom flag
fishArray.push(newFish); // Add to global fishArray for rendering by main loop
fishingScreen.addChild(newFish); // Add to fishingScreen for visibility
return newFish;
}
function runTutorialStep() {
GameState.tutorialPaused = false;
GameState.tutorialAwaitingTap = false; // Will be set by steps if needed for "tap screen"
// Clear lane highlights from previous step if any
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
if (tutorialContinueButton) {
tutorialContinueButton.visible = true;
} // Default to visible
if (tutorialContinueText) {
tutorialContinueText.visible = true;
}
// Clear previous tutorial fish unless current step needs it (now steps 3 and 4)
if (GameState.tutorialFish && GameState.tutorialStep !== 3 && GameState.tutorialStep !== 4) {
if (!GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish = null;
}
// Ensure fishing screen elements are visually active
// Adjusted max step for animations to 8 (new end tutorial step is 7)
if (fishingElements) {
if (typeof fishingElements.startWaterSurfaceAnimation === 'function' && GameState.tutorialStep < 8) {
fishingElements.startWaterSurfaceAnimation();
}
if (typeof fishingElements.startBoatAndFishermanAnimation === 'function' && GameState.tutorialStep < 8) {
fishingElements.startBoatAndFishermanAnimation();
}
if (fishingElements.hook && GameState.tutorialStep < 8) {
fishingElements.hook.y = GAME_CONFIG.LANES[1].y; // Reset hook
GameState.hookTargetLaneIndex = 1;
}
}
switch (GameState.tutorialStep) {
case 0:
// Welcome
setTutorialText("Welcome to Beat Fisher! Let's learn the basics. Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 1:
// The Hook Explanation
setTutorialText("This is your hook. It automatically moves to the lane with the closest approaching fish. Tap 'CONTINUE'.");
if (fishingElements.hook && !fishingElements.hook.destroyed) {
tween(fishingElements.hook.scale, {
x: 1.2,
y: 1.2
}, {
duration: 250,
onFinish: function onFinish() {
if (fishingElements.hook && !fishingElements.hook.destroyed) {
tween(fishingElements.hook.scale, {
x: 1,
y: 1
}, {
duration: 250
});
}
}
});
}
GameState.tutorialPaused = true;
break;
case 2:
// Lanes & Tapping Explanation with Overlay
setTutorialText("Fish swim in three lanes. When a fish is over the hook in its lane, TAP THE SCREEN in that lane to catch it. The lanes are highlighted. Tap 'CONTINUE'.");
// Add highlights behind the text box elements
for (var i = 0; i < GAME_CONFIG.LANES.length; i++) {
var laneYPos = GAME_CONFIG.LANES[i].y;
var highlight = LK.getAsset('laneHighlight', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: laneYPos,
alpha: 0.25,
// Semi-transparent
width: 1800,
height: 200
});
tutorialOverlayContainer.addChildAt(highlight, 0); // Add at the bottom of the container
tutorialLaneHighlights.push(highlight);
}
GameState.tutorialPaused = true;
break;
case 3:
// Was case 2
// Fish Approaching & First Catch Attempt
setTutorialText("A fish will now approach. Tap in its lane when it's under the hook!");
tutorialContinueButton.visible = false; // No continue button, action is tapping screen
tutorialContinueText.visible = false;
GameState.tutorialPaused = false; // Fish moves
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
GameState.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.35,
lane: 1
});
break;
case 4:
// Was case 3
// Perfect/Good Timing (after first successful catch)
setTutorialText("Great! Timing is key. 'Perfect' or 'Good' catches earn more. Try to catch this next fish!");
tutorialContinueButton.visible = false;
tutorialContinueText.visible = false;
GameState.tutorialPaused = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
}
GameState.tutorialFish = spawnTutorialFishHelper({
type: 'shallow',
speedMultiplier: 0.45,
lane: 1
});
break;
case 5:
// Was case 4
// Combos
setTutorialText("Nice one! Catch fish consecutively to build a COMBO for bonus points! Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 6:
// Was case 5
// Music & Rhythm
setTutorialText("Fish will approach the hook on the beat with the music's rhythm. Listen to the beat! Tap 'CONTINUE'.");
GameState.tutorialPaused = true;
break;
case 7:
// Was case 6
// End Tutorial
setTutorialText("You're all set! Tap 'CONTINUE' to go to the fishing spots!");
GameState.tutorialPaused = true;
break;
default:
// Effectively step 8
// Tutorial finished
GameState.tutorialMode = false;
tutorialOverlayContainer.visible = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
var idxDefault = fishArray.indexOf(GameState.tutorialFish);
if (idxDefault > -1) {
fishArray.splice(idxDefault, 1);
}
GameState.tutorialFish = null;
}
// Clear lane highlights if any persist
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
// Clean up fishing screen elements explicitly if they were started by tutorial
if (fishingElements && fishingElements.boat && !fishingElements.boat.destroyed) {
tween.stop(fishingElements.boat);
}
if (fishingElements && fishingElements.fishermanContainer && !fishingElements.fishermanContainer.destroyed) {
tween.stop(fishingElements.fishermanContainer);
}
// Stop water surface animations
if (fishingElements && fishingElements.waterSurfaceSegments) {
fishingElements.waterSurfaceSegments.forEach(function (segment) {
if (segment && !segment.destroyed) {
tween.stop(segment);
}
});
}
showScreen('levelSelect');
break;
}
}
function startTutorial() {
GameState.tutorialMode = true;
GameState.tutorialStep = 0;
GameState.gameActive = false;
// Ensure fishing screen is visible and setup for tutorial
showScreen('fishing');
fishingScreen.alpha = 1;
// Clear any active game elements from a previous session
fishArray.forEach(function (f) {
if (f && !f.destroyed) {
f.destroy();
}
});
fishArray = [];
ImprovedRhythmSpawner.reset();
// Clear existing UI text
if (fishingElements.scoreText) {
fishingElements.scoreText.setText('');
}
if (fishingElements.fishText) {
fishingElements.fishText.setText('');
}
if (fishingElements.comboText) {
fishingElements.comboText.setText('');
}
if (fishingElements.progressText) {
fishingElements.progressText.setText('');
}
// CREATE LANE BRACKETS FOR TUTORIAL
var bracketAssetHeight = 150;
var bracketAssetWidth = 75;
// Clear any existing brackets first
if (laneBrackets && laneBrackets.length > 0) {
laneBrackets.forEach(function (bracketPair) {
if (bracketPair.left && !bracketPair.left.destroyed) {
bracketPair.left.destroy();
}
if (bracketPair.right && !bracketPair.right.destroyed) {
bracketPair.right.destroy();
}
});
}
laneBrackets = [];
if (fishingScreen && !fishingScreen.destroyed) {
for (var i = 0; i < GAME_CONFIG.LANES.length; i++) {
var laneY = GAME_CONFIG.LANES[i].y;
var leftBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
x: bracketAssetWidth / 2,
y: laneY,
height: bracketAssetHeight
}));
var rightBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
scaleX: -1,
x: 2048 - bracketAssetWidth / 2,
y: laneY,
height: bracketAssetHeight
}));
laneBrackets.push({
left: leftBracket,
right: rightBracket
});
}
}
// Don't create tutorial elements here - wait for intro to finish
// createTutorialElements() and runTutorialStep() will be called after intro
}
// This function is called from game.update when tutorialFish exists
function checkTutorialFishState() {
var fish = GameState.tutorialFish;
if (!fish || fish.destroyed || fish.caught || fish.missed) {
return;
}
var hookX = fishingElements.hook.x;
// Fish passes hook without interaction during catch steps (now steps 3 or 4)
if (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) {
var passedHook = fish.speed > 0 && fish.x > hookX + GAME_CONFIG.MISS_WINDOW + fish.fishGraphics.width / 2 ||
// fish right edge passed hook right boundary
fish.speed < 0 && fish.x < hookX - GAME_CONFIG.MISS_WINDOW - fish.fishGraphics.width / 2; // fish left edge passed hook left boundary
if (passedHook) {
fish.missed = true; // Mark to prevent further attempts on this specific fish instance
GameState.tutorialPaused = true; // Pause to show message
setTutorialText("It got away! Tap 'CONTINUE' to try that part again.");
// Next tap on "CONTINUE" will call runTutorialStep(), which will re-run current step.
}
}
// Generic off-screen removal for tutorial fish
if (fish.x < -250 || fish.x > 2048 + 250) {
var wasCriticalStep = GameState.tutorialStep === 3 || GameState.tutorialStep === 4; // Adjusted step numbers
var fishIndex = fishArray.indexOf(fish);
if (fishIndex > -1) {
fishArray.splice(fishIndex, 1);
}
fish.destroy();
GameState.tutorialFish = null;
if (wasCriticalStep && !fish.caught && !fish.missed) {
// If it just went off screen without resolution
GameState.tutorialPaused = true;
setTutorialText("The fish swam off. Tap 'CONTINUE' to try that part again.");
}
}
}
var GameState = {
// Game flow
currentScreen: 'title',
// 'title', 'levelSelect', 'fishing', 'results'
// Player progression
currentDepth: 0,
money: 0,
totalFishCaught: 0,
ownedSongs: [],
// Array of {depth, songIndex} objects
// Level selection
selectedDepth: 0,
selectedSong: 0,
// Current session
sessionScore: 0,
sessionFishCaught: 0,
sessionFishSpawned: 0,
combo: 0,
maxCombo: 0,
// Game state
gameActive: false,
songStartTime: 0,
lastBeatTime: 0,
beatCount: 0,
introPlaying: false,
// Tracks if the intro animation is currently playing
musicNotesActive: false,
// Tracks if music note particle system is active
currentPlayingMusicId: 'rhythmTrack',
// ID of the music track currently playing in a session
currentPlayingMusicInitialVolume: 0.8,
// Initial volume of the current music track for fade reference
hookTargetLaneIndex: 1,
// Start with hook targeting the middle lane (index 1)
// Tutorial State
tutorialMode: false,
// Is the tutorial currently active?
tutorialStep: 0,
// Current step in the tutorial sequence
tutorialPaused: false,
// Is the tutorial paused (e.g., for text display)?
tutorialAwaitingTap: false,
// Is the tutorial waiting for a generic tap to continue?
tutorialFish: null,
// Stores the fish object used in a tutorial step
// Initialize owned songs (first song of each unlocked depth is free)
initOwnedSongs: function initOwnedSongs() {
this.ownedSongs = [];
for (var i = 0; i <= this.currentDepth; i++) {
this.ownedSongs.push({
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
****/
/****
* Title Screen - FIXED VERSION
****/
function createTitleScreen() {
var titleBg = titleScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 1,
height: 2732,
color: 0x87CEEB
}));
// ANIMATED CONTAINER GROUP - This will contain ALL visual elements including backgrounds
var titleAnimationGroup = titleScreen.addChild(new Container());
// Sky background image - NOW INSIDE animation group
var titleSky = titleAnimationGroup.addChild(LK.getAsset('skybackground', {
x: 0,
y: -500
}));
// Water background image - NOW INSIDE animation group
var titleWater = titleAnimationGroup.addChild(LK.getAsset('water', {
x: 0,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: 2048,
height: 2732 - GAME_CONFIG.WATER_SURFACE_Y
}));
// Initialize containers for title screen ambient particles - INSIDE animation group
titleScreenOceanBubbleContainer = titleAnimationGroup.addChild(new Container());
titleScreenSeaweedContainer = titleAnimationGroup.addChild(new Container());
titleScreenCloudContainer = titleAnimationGroup.addChild(new Container());
// Create a single container for boat, fisherman, and line that all move together
var titleBoatGroup = titleAnimationGroup.addChild(new Container());
// Boat positioned at origin of the group
var titleBoat = titleBoatGroup.addChild(LK.getAsset('boat', {
anchorX: 0.5,
anchorY: 0.74,
x: 0,
// Relative to group
y: 0 // Relative to group
}));
// Fisherman positioned relative to boat within the same group
var titleFisherman = titleBoatGroup.addChild(LK.getAsset('fisherman', {
anchorX: 0.5,
anchorY: 1,
x: -100,
// Relative to boat position
y: -70 // Relative to boat position
}));
// Fishing line positioned relative to boat within the same group
var rodTipX = -100 + 85; // fisherman offset + rod offset from fisherman center
var rodTipY = -70 - 200; // fisherman y (feet) - fisherman height (to get to head area for rod)
// initialHookY needs to be relative to the group's origin (which is boat's origin at WATER_SURFACE_Y)
// GAME_CONFIG.LANES[1].y is an absolute world Y.
// titleBoatGroup.y is GAME_CONFIG.WATER_SURFACE_Y.
// So, hook's Y relative to group = absolute hook Y - group's absolute Y
var initialHookYInGroup = GAME_CONFIG.LANES[1].y - GAME_CONFIG.WATER_SURFACE_Y;
var titleLine = titleBoatGroup.addChild(LK.getAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0,
x: rodTipX,
y: rodTipY,
// Relative to group
width: 6,
height: initialHookYInGroup - rodTipY // Length from rod tip to hook, all relative to group
}));
var titleHook = titleBoatGroup.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
x: rodTipX,
// Hook X matches line X initially
y: initialHookYInGroup // Relative to group
}));
// Position the entire group in the world (within titleAnimationGroup)
titleBoatGroup.x = GAME_CONFIG.SCREEN_CENTER_X;
titleBoatGroup.y = GAME_CONFIG.WATER_SURFACE_Y;
// Store base position for wave animation
var boatGroupBaseY = titleBoatGroup.y; // This is the group's world Y
var boatWaveAmplitude = 10;
var boatWaveHalfCycleDuration = 2000;
var boatRotationAmplitude = 0.03;
var boatRotationDuration = 3000;
// Wave animation variables for line (used in updateTitleFishingLineWave)
var lineWaveAmplitude = 12;
var lineWaveSpeed = 0.03;
var linePhaseOffset = 0;
// Animated Water Surface segments - INSIDE animation group
var titleWaterSurfaceSegments = [];
var NUM_WAVE_SEGMENTS_TITLE = 32;
var SEGMENT_WIDTH_TITLE = 2048 / NUM_WAVE_SEGMENTS_TITLE;
var SEGMENT_HEIGHT_TITLE = 24;
var WAVE_AMPLITUDE_TITLE = 12;
var WAVE_HALF_PERIOD_MS_TITLE = 2500;
var PHASE_DELAY_MS_PER_SEGMENT_TITLE = WAVE_HALF_PERIOD_MS_TITLE * 2 / NUM_WAVE_SEGMENTS_TITLE;
// Create water surface segments
for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) {
var segment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH_TITLE,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: SEGMENT_WIDTH_TITLE + 1,
height: SEGMENT_HEIGHT_TITLE,
anchorX: 0,
anchorY: 0.5,
alpha: 0.8,
tint: 0x4fc3f7
});
segment.baseY = GAME_CONFIG.WATER_SURFACE_Y;
titleAnimationGroup.addChild(segment); // Water surface is part of main animation group, not boat group
titleWaterSurfaceSegments.push(segment);
}
for (var i = 0; i < NUM_WAVE_SEGMENTS_TITLE; i++) {
var whiteSegment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH_TITLE,
y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2,
width: SEGMENT_WIDTH_TITLE + 1,
height: SEGMENT_HEIGHT_TITLE / 2,
anchorX: 0,
anchorY: 0.5,
alpha: 0.6,
tint: 0xffffff
});
whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT_TITLE / 2;
titleAnimationGroup.addChild(whiteSegment); // Water surface is part of main animation group
titleWaterSurfaceSegments.push(whiteSegment);
}
// Animation group setup for zoom effect - PROPERLY CENTER THE BOAT ON SCREEN
var boatCenterX = GAME_CONFIG.SCREEN_CENTER_X; // 1024
var targetBoatScreenY = GAME_CONFIG.SCREEN_CENTER_Y + 300; // Change +100 to move boat lower/higher
var boatWorldY = GAME_CONFIG.WATER_SURFACE_Y; // 760 - where titleBoatGroup actually is
var pivotY = boatWorldY - (targetBoatScreenY - boatWorldY); // Calculate proper pivot offset
// Set pivot to calculated position and position group so boat ends up at screen center
titleAnimationGroup.pivot.set(boatCenterX, pivotY);
titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X; // 1024
titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // 1366 - this will center the boat at targetBoatScreenY
// Initial zoom state
var INITIAL_ZOOM_FACTOR = 3.0;
var FINAL_ZOOM_FACTOR = 1.8; // This matches existing logic if it's used in showScreen
titleAnimationGroup.scale.set(INITIAL_ZOOM_FACTOR);
titleAnimationGroup.alpha = 1; // Main animation group is always visible
// Single wave animation function - moves entire group together
var targetUpY = boatGroupBaseY - boatWaveAmplitude; // boatGroupBaseY is absolute world Y
var targetDownY = boatGroupBaseY + boatWaveAmplitude; // boatGroupBaseY is absolute world Y
function moveTitleBoatGroupUp() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
// We tween the group's world Y position
tween(titleBoatGroup, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveTitleBoatGroupDown
});
}
function moveTitleBoatGroupDown() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
tween(titleBoatGroup, {
y: targetDownY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveTitleBoatGroupUp
});
}
// Single rotation animation - rotates entire group together
function rockTitleBoatGroupLeft() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
tween(titleBoatGroup, {
rotation: -boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockTitleBoatGroupRight
});
}
function rockTitleBoatGroupRight() {
if (!titleBoatGroup || titleBoatGroup.destroyed) {
return;
}
tween(titleBoatGroup, {
rotation: boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockTitleBoatGroupLeft
});
}
// Simplified line wave function - just the sway effect
function updateTitleFishingLineWave() {
// titleLine and titleHook are children of titleBoatGroup, their x/y are relative to it.
// rodTipX, rodTipY, initialHookYInGroup are also relative to titleBoatGroup.
if (!titleLine || titleLine.destroyed || !titleHook || titleHook.destroyed) {
return;
}
linePhaseOffset += lineWaveSpeed;
var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude;
// Only apply the wave sway to X, positions stay relative to group's origin
// rodTipX is the line's base X relative to the group.
titleLine.x = rodTipX + waveOffset * 0.3;
titleHook.x = rodTipX + waveOffset;
// Y positions of line and hook (titleLine.y and titleHook.y) are static relative to the group.
// Recalculate line rotation based on hook position relative to line's anchor
// All coordinates here are relative to titleBoatGroup.
var deltaX = titleHook.x - titleLine.x; // Difference in x relative to group
var deltaY = titleHook.y - titleLine.y; // Difference in y relative to group
var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
titleLine.height = actualLineLength;
if (actualLineLength > 0.001) {
titleLine.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2;
} else {
titleLine.rotation = 0;
}
titleHook.rotation = titleLine.rotation; // Hook rotation matches line's
}
// Water surface animation function
function startTitleWaterSurfaceAnimationFunc() {
for (var k = 0; k < titleWaterSurfaceSegments.length; k++) {
var segment = titleWaterSurfaceSegments[k];
if (!segment || segment.destroyed) {
continue;
}
var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS_TITLE;
(function (currentLocalSegment, currentLocalSegmentIndexForDelay) {
var animUp, animDown;
animDown = function animDown() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE
}, {
duration: WAVE_HALF_PERIOD_MS_TITLE,
easing: tween.easeInOut,
onFinish: animUp
});
};
animUp = function animUp() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY - WAVE_AMPLITUDE_TITLE
}, {
duration: WAVE_HALF_PERIOD_MS_TITLE,
easing: tween.easeInOut,
onFinish: animDown
});
};
LK.setTimeout(function () {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
// Start with DOWN movement first
tween(currentLocalSegment, {
y: currentLocalSegment.baseY + WAVE_AMPLITUDE_TITLE
}, {
duration: WAVE_HALF_PERIOD_MS_TITLE,
easing: tween.easeInOut,
onFinish: animUp // This should call animUp after going down
});
}, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT_TITLE);
})(segment, segmentIndexForDelay);
}
}
// BLACK OVERLAY for reveal effect - this goes OVER everything
var blackOverlay = titleScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
width: 2048,
height: 2732,
color: 0x000000,
// Pure black
alpha: 1 // Starts fully opaque
}));
// UI elements - OUTSIDE animation group so they don't zoom, ABOVE black overlay
var titleImage = titleScreen.addChild(LK.getAsset('titleimage', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 700,
// Positioned where the subtitle roughly was, adjust as needed
alpha: 0,
scaleX: 0.8,
// Adjust scale as needed for optimal size
scaleY: 0.8 // Adjust scale as needed for optimal size
}));
// Buttons - OUTSIDE animation group, ABOVE black overlay
// New Y positions: 1/3 from bottom of screen (2732 / 3 = 910.66. Y = 2732 - 910.66 = 1821.33)
var startButtonY = 2732 - 2732 / 3.5; // Approx 1821
var tutorialButtonY = startButtonY + 600; // Maintain 150px gap
var startButton = titleScreen.addChild(LK.getAsset('startbutton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: startButtonY,
alpha: 0
}));
var tutorialButton = titleScreen.addChild(LK.getAsset('tutorialbutton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: tutorialButtonY,
alpha: 0
}));
// No assignment here; assign tutorialButtonGfx after createTitleScreen returns
return {
startButton: startButton,
tutorialButton: tutorialButton,
// This is the graphics object
titleImage: titleImage,
titleAnimationGroup: titleAnimationGroup,
blackOverlay: blackOverlay,
// Return the group and its specific animation functions
titleBoatGroup: titleBoatGroup,
moveTitleBoatGroupUp: moveTitleBoatGroupUp,
rockTitleBoatGroupLeft: rockTitleBoatGroupLeft,
// Individual elements that might still be needed for direct access or other effects
titleSky: titleSky,
// Sky is not in titleBoatGroup
titleWater: titleWater,
// Water background is not in titleBoatGroup
titleWaterSurfaceSegments: titleWaterSurfaceSegments,
// Water surface segments are not in titleBoatGroup
// Line and hook are part of titleBoatGroup, but if direct reference is needed for some reason, they could be returned.
// However, following the goal's spirit of "Return the group instead of individual elements [that are part of it]"
// titleLine and titleHook might be omitted here unless specifically needed by other parts of the code.
// For now, including them if they were returned before and are still locally defined.
// updateTitleFishingLineWave accesses titleLine and titleHook locally.
titleLine: titleLine,
// Still defined locally, might be useful for direct reference
titleHook: titleHook,
// Still defined locally
startTitleWaterSurfaceAnimation: startTitleWaterSurfaceAnimationFunc,
updateTitleFishingLineWave: updateTitleFishingLineWave // The new simplified version
};
}
/****
* Level Select Screen
****/
function createLevelSelectScreen() {
var selectBg = levelSelectScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 0.8,
height: 2732
}));
// Top section - Title, Money, and Back button
var title = new Text2('SELECT FISHING SPOT', {
size: 90,
// Increased from 80
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.x = GAME_CONFIG.SCREEN_CENTER_X;
title.y = 180; // Moved up slightly
levelSelectScreen.addChild(title);
// Money display (keep at top)
var moneyDisplay = new Text2('Money: $0', {
size: 70,
// Increased from 60
fill: 0xFFD700
});
moneyDisplay.anchor.set(1, 0);
moneyDisplay.x = 1900;
moneyDisplay.y = 80; // Moved up slightly
levelSelectScreen.addChild(moneyDisplay);
// Back button (moved to bottom center)
var backButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 2732 - 300,
// Positioned 100px from the bottom edge (center of button)
tint: 0x757575
}));
var backButtonText = new Text2('BACK', {
size: 45,
// Increased from 40
fill: 0xFFFFFF
});
backButtonText.anchor.set(0.5, 0.5);
backButtonText.x = backButton.x;
backButtonText.y = backButton.y;
levelSelectScreen.addChild(backButtonText);
// Depth tabs (moved down and spaced out more)
var depthTabs = [];
// Song display area (moved to center of screen)
var songCard = levelSelectScreen.addChild(LK.getAsset('songCard', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1100,
// Moved down significantly from 700
width: 900,
// Made wider
height: 300 // Made taller
}));
// Song navigation arrows (repositioned around the larger song card)
var leftArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 350,
// Moved further left
y: 1100,
// Moved down with song card
tint: 0x666666,
width: 120,
// Made bigger
height: 120
}));
var leftArrowText = new Text2('<', {
size: 80,
// Increased from 60
fill: 0xFFFFFF
});
leftArrowText.anchor.set(0.5, 0.5);
leftArrowText.x = 350;
leftArrowText.y = 1100;
levelSelectScreen.addChild(leftArrowText);
var rightArrow = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: 1698,
// Moved further right
y: 1100,
// Moved down with song card
tint: 0x666666,
width: 120,
// Made bigger
height: 120
}));
var rightArrowText = new Text2('>', {
size: 80,
// Increased from 60
fill: 0xFFFFFF
});
rightArrowText.anchor.set(0.5, 0.5);
rightArrowText.x = 1698;
rightArrowText.y = 1100;
levelSelectScreen.addChild(rightArrowText);
// Song info (repositioned within the larger song card area)
var songTitle = new Text2('Song Title', {
size: 60,
// Increased from 50
fill: 0xFFFFFF
});
songTitle.anchor.set(0.5, 0.5);
songTitle.x = GAME_CONFIG.SCREEN_CENTER_X;
songTitle.y = 1020; // Positioned above song card center
levelSelectScreen.addChild(songTitle);
var songInfo = new Text2('BPM: 120 | Duration: 2:00', {
size: 40,
// Increased from 30
fill: 0xCCCCCC
});
songInfo.anchor.set(0.5, 0.5);
songInfo.x = GAME_CONFIG.SCREEN_CENTER_X;
songInfo.y = 1100; // Centered in song card
levelSelectScreen.addChild(songInfo);
var songEarnings = new Text2('Potential Earnings: $50-100', {
size: 40,
// Increased from 30
fill: 0x4CAF50
});
songEarnings.anchor.set(0.5, 0.5);
songEarnings.x = GAME_CONFIG.SCREEN_CENTER_X;
songEarnings.y = 1180; // Positioned below song card center
levelSelectScreen.addChild(songEarnings);
// Play/Buy button (moved down and made larger)
var playButton = levelSelectScreen.addChild(LK.getAsset('bigButton', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1400,
// Moved down significantly from 900
width: 500,
// Made wider
height: 130 // Made taller
}));
var playButtonText = new Text2('PLAY', {
size: 60,
// Increased from 50
fill: 0xFFFFFF
});
playButtonText.anchor.set(0.5, 0.5);
playButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
playButtonText.y = 1400;
levelSelectScreen.addChild(playButtonText);
// Shop button (moved to bottom and made larger)
var shopButton = levelSelectScreen.addChild(LK.getAsset('button', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: 1650,
// Moved down significantly from 1100
tint: 0x666666,
// Grayed out since it's not available
width: 450,
// Made wider
height: 120 // Made taller
}));
var shopButtonText = new Text2('UPGRADE ROD', {
size: 50,
// Increased from 40
fill: 0xFFFFFF
});
shopButtonText.anchor.set(0.5, 0.5);
shopButtonText.x = GAME_CONFIG.SCREEN_CENTER_X;
shopButtonText.y = 1650;
levelSelectScreen.addChild(shopButtonText);
// Add "Coming soon!" text underneath
var comingSoonText = new Text2('Coming soon!', {
size: 35,
fill: 0xFFD700 // Gold color to make it stand out
});
comingSoonText.anchor.set(0.5, 0.5);
comingSoonText.x = GAME_CONFIG.SCREEN_CENTER_X;
comingSoonText.y = 1730; // Position below the button
levelSelectScreen.addChild(comingSoonText);
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() {
// Sky background - should be added first to be behind everything else
var sky = fishingScreen.addChild(LK.getAsset('skybackground', {
x: 0,
y: -500
}));
// Water background
var water = fishingScreen.addChild(LK.getAsset('water', {
x: 0,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: 2048,
height: 2732 - GAME_CONFIG.WATER_SURFACE_Y
}));
// Create a container for ambient ocean bubbles (from bottom of screen)
// This should be layered above the 'water' background, but below fish, boat, etc.
globalOceanBubbleContainer = fishingScreen.addChild(new Container());
// Create a container for seaweed particles
globalSeaweedContainer = fishingScreen.addChild(new Container());
// Create a container for cloud particles (added early so clouds appear behind UI)
globalCloudContainer = fishingScreen.addChild(new Container());
// Create a container for bubbles to render them behind fish and other elements
bubbleContainer = fishingScreen.addChild(new Container());
// Create a container for music notes
musicNotesContainer = fishingScreen.addChild(new Container());
// Music notes should visually appear to come from the boat area, so their container
// should ideally be layered accordingly. Adding it here means it's on top of water,
// but if boat/fisherman are added later, notes might appear behind them if not managed.
// For now, notes will be added to this container, which itself is added to fishingScreen.
// Animated Water Surface segments code
var waterSurfaceSegments = []; // This will be populated for returning and cleanup
var waterSurfaceSegmentsBlueTemp = []; // Temporary array for blue segments
var waterSurfaceSegmentsWhiteTemp = []; // Temporary array for white segments
var NUM_WAVE_SEGMENTS = 32;
var SEGMENT_WIDTH = 2048 / NUM_WAVE_SEGMENTS;
var SEGMENT_HEIGHT = 24;
var WAVE_AMPLITUDE = 12;
var WAVE_HALF_PERIOD_MS = 2500;
var PHASE_DELAY_MS_PER_SEGMENT = WAVE_HALF_PERIOD_MS * 2 / NUM_WAVE_SEGMENTS;
// Create blue segments (assets only, not added to fishingScreen yet)
for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) {
var segment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH,
y: GAME_CONFIG.WATER_SURFACE_Y,
width: SEGMENT_WIDTH + 1,
height: SEGMENT_HEIGHT,
anchorX: 0,
anchorY: 0.5,
alpha: 0.8,
tint: 0x4fc3f7
});
segment.baseY = GAME_CONFIG.WATER_SURFACE_Y;
// Animation functions will be defined and started by startWaterSurfaceAnimationFunc
waterSurfaceSegmentsBlueTemp.push(segment);
}
// Create white segments (assets only, not added to fishingScreen yet)
for (var i = 0; i < NUM_WAVE_SEGMENTS; i++) {
var whiteSegment = LK.getAsset('waterSurface', {
x: i * SEGMENT_WIDTH,
y: GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2,
width: SEGMENT_WIDTH + 1,
height: SEGMENT_HEIGHT / 2,
anchorX: 0,
anchorY: 0.5,
alpha: 0.6,
tint: 0xffffff
});
whiteSegment.baseY = GAME_CONFIG.WATER_SURFACE_Y - SEGMENT_HEIGHT / 2;
// Animation functions will be defined and started by startWaterSurfaceAnimationFunc
waterSurfaceSegmentsWhiteTemp.push(whiteSegment);
}
// Boat - Add this to fishingScreen first
var boat = fishingScreen.addChild(LK.getAsset('boat', {
anchorX: 0.5,
anchorY: 0.74,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: GAME_CONFIG.WATER_SURFACE_Y
}));
// Now add the water segments to fishingScreen, so they render on top of the boat
for (var i = 0; i < waterSurfaceSegmentsBlueTemp.length; i++) {
fishingScreen.addChild(waterSurfaceSegmentsBlueTemp[i]);
waterSurfaceSegments.push(waterSurfaceSegmentsBlueTemp[i]); // Also add to the main array for cleanup
}
for (var i = 0; i < waterSurfaceSegmentsWhiteTemp.length; i++) {
fishingScreen.addChild(waterSurfaceSegmentsWhiteTemp[i]);
waterSurfaceSegments.push(waterSurfaceSegmentsWhiteTemp[i]); // Also add to the main array for cleanup
}
// Create separate fisherman container that will sync with boat movement
var fishermanContainer = fishingScreen.addChild(new Container());
// Fisherman (now in its own container, positioned to match boat)
var fisherman = fishermanContainer.addChild(LK.getAsset('fisherman', {
anchorX: 0.5,
anchorY: 1,
x: GAME_CONFIG.SCREEN_CENTER_X - 100,
y: GAME_CONFIG.WATER_SURFACE_Y - 70
}));
// Store references for wave animation sync
var boatBaseY = boat.y;
var fishermanBaseY = fishermanContainer.y;
var boatWaveAmplitude = 10;
var boatWaveHalfCycleDuration = 2000;
// SINGLE ANIMATED FISHING LINE
var initialHookY = GAME_CONFIG.LANES[1].y;
var fishingLineStartY = -100;
var line = fishingScreen.addChild(LK.getAsset('fishingLine', {
anchorX: 0.5,
anchorY: 0,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY,
width: 6,
height: initialHookY - (GAME_CONFIG.WATER_SURFACE_Y + fishingLineStartY)
}));
var hook = fishingScreen.addChild(LK.getAsset('hook', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_CONFIG.SCREEN_CENTER_X,
y: initialHookY
}));
hook.originalY = initialHookY;
var lineWaveAmplitude = 12;
var lineWaveSpeed = 0.03;
var linePhaseOffset = 0;
function updateFishingLineWave() {
linePhaseOffset += lineWaveSpeed;
var rodTipX = fishermanContainer.x + fisherman.x + 85;
var rodTipY = fishermanContainer.y + fisherman.y - fisherman.height;
var waveOffset = Math.sin(linePhaseOffset) * lineWaveAmplitude;
line.x = rodTipX + waveOffset * 0.3;
line.y = rodTipY;
hook.x = rodTipX + waveOffset;
var hookAttachX = hook.x;
var hookAttachY = hook.y - hook.height / 2;
var deltaX = hookAttachX - line.x;
var deltaY = hookAttachY - line.y;
var actualLineLength = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
line.height = actualLineLength;
if (actualLineLength > 0.001) {
line.rotation = Math.atan2(deltaY, deltaX) - Math.PI / 2;
} else {
line.rotation = 0;
}
hook.rotation = line.rotation;
}
// Calculate target positions for boat wave animation
var targetUpY = boatBaseY - boatWaveAmplitude;
var targetDownY = boatBaseY + boatWaveAmplitude;
var fishermanTargetUpY = fishermanBaseY - boatWaveAmplitude;
var fishermanTargetDownY = fishermanBaseY + boatWaveAmplitude;
// Synchronized wave animation functions (defined here to be closured)
function moveBoatAndFishermanUp() {
if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) {
return;
}
tween(boat, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveBoatAndFishermanDown
});
tween(fishermanContainer, {
y: fishermanTargetUpY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut
});
}
function moveBoatAndFishermanDown() {
if (!boat || boat.destroyed || !fishermanContainer || fishermanContainer.destroyed) {
return;
}
tween(boat, {
y: targetDownY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut,
onFinish: moveBoatAndFishermanUp
});
tween(fishermanContainer, {
y: fishermanTargetDownY
}, {
duration: boatWaveHalfCycleDuration,
easing: tween.easeInOut
});
}
var boatRotationAmplitude = 0.03;
var boatRotationDuration = 3000;
function rockBoatLeft() {
if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) {
return;
}
tween(boat, {
rotation: -boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockBoatRight
});
tween(fisherman, {
rotation: boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut
});
}
function rockBoatRight() {
if (!boat || boat.destroyed || !fisherman || fisherman.destroyed) {
return;
}
tween(boat, {
rotation: boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut,
onFinish: rockBoatLeft
});
tween(fisherman, {
rotation: -boatRotationAmplitude
}, {
duration: boatRotationDuration,
easing: tween.easeInOut
});
}
// Function to start/restart water surface animations
function startWaterSurfaceAnimationFunc() {
var allSegments = waterSurfaceSegments; // Use the populated array from fishingElements via closure
for (var k = 0; k < allSegments.length; k++) {
var segment = allSegments[k];
if (!segment || segment.destroyed) {
continue;
}
var segmentIndexForDelay = k % NUM_WAVE_SEGMENTS;
(function (currentLocalSegment, currentLocalSegmentIndexForDelay) {
var animUp, animDown;
animDown = function animDown() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY + WAVE_AMPLITUDE
}, {
duration: WAVE_HALF_PERIOD_MS,
easing: tween.easeInOut,
onFinish: animUp
});
};
animUp = function animUp() {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY - WAVE_AMPLITUDE
}, {
duration: WAVE_HALF_PERIOD_MS,
easing: tween.easeInOut,
onFinish: animDown
});
};
LK.setTimeout(function () {
if (!currentLocalSegment || currentLocalSegment.destroyed) {
return;
}
tween(currentLocalSegment, {
y: currentLocalSegment.baseY - WAVE_AMPLITUDE
}, {
// Initial move up
duration: WAVE_HALF_PERIOD_MS,
easing: tween.easeInOut,
onFinish: animDown
});
}, currentLocalSegmentIndexForDelay * PHASE_DELAY_MS_PER_SEGMENT);
})(segment, segmentIndexForDelay);
}
}
// Function to start/restart boat and fisherman animations
function startBoatAndFishermanAnimationFunc() {
if (boat && !boat.destroyed && fishermanContainer && !fishermanContainer.destroyed) {
tween(boat, {
y: targetUpY
}, {
duration: boatWaveHalfCycleDuration / 2,
easing: tween.easeOut,
onFinish: moveBoatAndFishermanDown
});
tween(fishermanContainer, {
y: fishermanTargetUpY
}, {
duration: boatWaveHalfCycleDuration / 2,
easing: tween.easeOut
});
rockBoatLeft();
}
}
// UI elements (from existing)
var scoreText = new Text2('Score: 0', {
size: 70,
fill: 0xFFFFFF
});
scoreText.anchor.set(1, 0);
scoreText.x = 2048 - 50;
scoreText.y = 50;
fishingScreen.addChild(scoreText);
var fishText = new Text2('Fish: 0/0', {
size: 55,
fill: 0xFFFFFF
});
fishText.anchor.set(1, 0);
fishText.x = 2048 - 50;
fishText.y = 140;
fishingScreen.addChild(fishText);
var comboText = new Text2('Combo: 0', {
size: 55,
fill: 0xFF9800
});
comboText.anchor.set(1, 0);
comboText.x = 2048 - 50;
comboText.y = 210;
fishingScreen.addChild(comboText);
var progressText = new Text2('0:00 / 0:00', {
size: 50,
fill: 0x4FC3F7
});
progressText.anchor.set(1, 0);
progressText.x = 2048 - 50;
progressText.y = 280;
fishingScreen.addChild(progressText);
return {
boat: boat,
fishermanContainer: fishermanContainer,
fisherman: fisherman,
hook: hook,
line: line,
updateFishingLineWave: updateFishingLineWave,
scoreText: scoreText,
fishText: fishText,
comboText: comboText,
progressText: progressText,
waterSurfaceSegments: waterSurfaceSegments,
bubbleContainer: bubbleContainer,
musicNotesContainer: musicNotesContainer,
startWaterSurfaceAnimation: startWaterSurfaceAnimationFunc,
startBoatAndFishermanAnimation: startBoatAndFishermanAnimationFunc
};
}
/****
* Initialize Screen Elements
****/
var titleElements = createTitleScreen();
titleElements.tutorialButtonGfx = titleElements.tutorialButton; // Store the graphical button for compatibility
var levelSelectElements = createLevelSelectScreen();
var fishingElements = createFishingScreen();
// Tutorial UI Elements - MUST be at global scope
var tutorialOverlayContainer = game.addChild(new Container());
tutorialOverlayContainer.visible = false;
var tutorialTextBackground;
var tutorialTextDisplay;
var tutorialContinueButton;
var tutorialContinueText;
var tutorialLaneHighlights = []; // To store lane highlight graphics for the tutorial
// Feedback indicators are now created on-demand by the showFeedback function.
// The global feedbackIndicators object is no longer needed.
// Game variables
var fishArray = [];
var bubblesArray = [];
var bubbleContainer; // Container for bubbles, initialized in createFishingScreen
var musicNotesArray = [];
var musicNotesContainer; // Container for music notes
var laneBrackets = []; // Stores the visual bracket pairs for each lane
var musicNoteSpawnCounter = 0;
var MUSIC_NOTE_SPAWN_INTERVAL_TICKS = 45; // Spawn a note roughly every 0.75 seconds
// Ocean Bubbles (ambient background)
var globalOceanBubblesArray = [];
var globalOceanBubbleContainer;
var globalOceanBubbleSpawnCounter = 0;
// Increase interval to reduce amount (higher = less frequent)
var OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS = 40; // Spawn new ocean bubbles roughly every 2/3 second (was 20)
// Seaweed particles (ambient background)
var globalSeaweedArray = [];
var globalSeaweedContainer;
var globalSeaweedSpawnCounter = 0;
var SEAWEED_SPAWN_INTERVAL_TICKS = 120; // Spawn seaweed less frequently than bubbles
var MAX_SEAWEED_COUNT = 8; // Maximum number of seaweed particles at once
// Cloud particles (ambient sky)
var globalCloudArray = [];
var globalCloudContainer;
var globalCloudSpawnCounter = 0;
var CLOUD_SPAWN_INTERVAL_TICKS = 180; // Spawn clouds less frequently than seaweed
var MAX_CLOUD_COUNT = 5; // Maximum number of cloud particles at once
// Title Screen Ambient Particle Systems
var titleScreenOceanBubblesArray = [];
var titleScreenOceanBubbleContainer; // Will be initialized in createTitleScreen
var titleScreenOceanBubbleSpawnCounter = 0;
var titleScreenSeaweedArray = [];
var titleScreenSeaweedContainer; // Will be initialized in createTitleScreen
var titleScreenSeaweedSpawnCounter = 0;
var titleScreenCloudArray = [];
var titleScreenCloudContainer; // Will be initialized in createTitleScreen
var titleScreenCloudSpawnCounter = 0;
// Timers for title screen ambient sounds
var titleSeagullSoundTimer = null;
var titleBoatSoundTimer = null;
/****
* Input State and Helpers for Fishing
****/
var inputState = {
touching: false,
// Is the screen currently being touched?
touchLane: -1,
// Which lane was the touch initiated in? (0, 1, 2)
touchStartTime: 0 // Timestamp of when the touch started (LK.ticks based)
};
// Helper function to determine which lane a Y coordinate falls into
function getTouchLane(y) {
// Define boundaries based on the midpoints between lane Y coordinates
// These are calculated from GAME_CONFIG.LANES[i].y values
// Lane 0: y = 723
// Lane 1: y = 1366
// Lane 2: y = 2009
var boundary_lane0_lane1 = (GAME_CONFIG.LANES[0].y + GAME_CONFIG.LANES[1].y) / 2; // Approx 1044.5
var boundary_lane1_lane2 = (GAME_CONFIG.LANES[1].y + GAME_CONFIG.LANES[2].y) / 2; // Approx 1687.5
if (y < boundary_lane0_lane1) {
return 0; // Top lane (e.g., shallow)
} else if (y < boundary_lane1_lane2) {
return 1; // Middle lane (e.g., medium)
} else {
return 2; // Bottom lane (e.g., deep)
}
}
// Shows feedback (perfect, good, miss) at the specified lane
// Shows feedback (perfect, good, miss) at the specified lane
function showFeedback(type, laneIndex) {
var feedbackY = GAME_CONFIG.LANES[laneIndex].y;
var indicator = new FeedbackIndicator(type); // Creates a new indicator e.g. FeedbackIndicator('perfect')
// Position feedback at the single hook's X coordinate and the fish's lane Y
indicator.x = fishingElements.hook.x; // Use the single hook's X
indicator.y = feedbackY; // Feedback appears at the fish's lane Y
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 in tutorial mode, and it's a 'down' event during an active catch step (now steps 3 or 4)
if (GameState.tutorialMode && isDown && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !GameState.tutorialPaused) {
if (GameState.tutorialFish && !GameState.tutorialFish.caught && !GameState.tutorialFish.missed) {
checkCatch(getTouchLane(y)); // Attempt catch
}
return; // Tutorial catch handled
}
// Existing game active check
if (!GameState.gameActive) {
return;
}
var currentTime = LK.ticks * (1000 / 60); // Current time in ms
if (isDown) {
// Touch started
inputState.touching = true;
inputState.touchLane = getTouchLane(y);
inputState.touchStartTime = currentTime;
// A normal tap action will be processed on 'up'.
} else {
// Touch ended (isUp)
if (inputState.touching) {
// This was a normal tap.
checkCatch(inputState.touchLane);
}
inputState.touching = false;
}
}
/****
* Screen Management
****/
function showScreen(screenName) {
titleScreen.visible = false;
levelSelectScreen.visible = false;
fishingScreen.visible = false;
resultsScreen.visible = false;
// Stop any ongoing tweens and clear particles if switching FROM title screen
if (GameState.currentScreen === 'title' && titleElements) {
tween.stop(titleElements.titleAnimationGroup);
tween.stop(titleElements.blackOverlay);
if (titleElements.titleImage) {
tween.stop(titleElements.titleImage);
} // Stop titleImage tween
if (titleElements.logo) {
tween.stop(titleElements.logo);
} // Keep for safety if old code path
if (titleElements.subtitle) {
tween.stop(titleElements.subtitle);
} // Keep for safety
tween.stop(titleElements.startButton);
tween.stop(titleElements.tutorialButton);
// Clear title screen sound timers
if (titleSeagullSoundTimer) {
LK.clearTimeout(titleSeagullSoundTimer);
titleSeagullSoundTimer = null;
}
if (titleBoatSoundTimer) {
LK.clearTimeout(titleBoatSoundTimer);
titleBoatSoundTimer = null;
}
// Stop water surface animations for title screen
if (titleElements.titleWaterSurfaceSegments) {
titleElements.titleWaterSurfaceSegments.forEach(function (segment) {
if (segment && !segment.destroyed) {
tween.stop(segment);
}
});
}
// Clear title screen particles
if (titleScreenOceanBubbleContainer) {
titleScreenOceanBubbleContainer.removeChildren();
}
titleScreenOceanBubblesArray.forEach(function (p) {
if (p && !p.destroyed) {
p.destroy();
}
});
titleScreenOceanBubblesArray = [];
if (titleScreenSeaweedContainer) {
titleScreenSeaweedContainer.removeChildren();
}
titleScreenSeaweedArray.forEach(function (p) {
if (p && !p.destroyed) {
p.destroy();
}
});
titleScreenSeaweedArray = [];
if (titleScreenCloudContainer) {
titleScreenCloudContainer.removeChildren();
}
titleScreenCloudArray.forEach(function (p) {
if (p && !p.destroyed) {
p.destroy();
}
});
titleScreenCloudArray = [];
}
// Cleanup tutorial elements if switching away from fishing/tutorial
if (GameState.currentScreen === 'fishing' && GameState.tutorialMode && screenName !== 'fishing') {
tutorialOverlayContainer.visible = false;
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
GameState.tutorialFish.destroy();
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish = null;
}
// Clear lane highlights if any exist when switching screen away from tutorial
if (tutorialLaneHighlights.length > 0) {
tutorialLaneHighlights.forEach(function (overlay) {
if (overlay && !overlay.destroyed) {
overlay.destroy();
}
});
tutorialLaneHighlights = [];
}
GameState.tutorialMode = false; // Ensure tutorial mode is exited
}
GameState.currentScreen = screenName;
switch (screenName) {
case 'title':
// Title Screen Sounds
var _scheduleNextSeagullSound = function scheduleNextSeagullSound() {
if (GameState.currentScreen !== 'title') {
// If screen changed, ensure timer is stopped and not rescheduled
if (titleSeagullSoundTimer) {
LK.clearTimeout(titleSeagullSoundTimer);
titleSeagullSoundTimer = null;
}
return;
}
var randomDelay = 5000 + Math.random() * 10000; // 5-15 seconds
titleSeagullSoundTimer = LK.setTimeout(function () {
if (GameState.currentScreen !== 'title') {
return; // Don't play or reschedule if not on title screen
}
var seagullSounds = ['seagull1', 'seagull2', 'seagull3'];
var randomSoundId = seagullSounds[Math.floor(Math.random() * seagullSounds.length)];
LK.getSound(randomSoundId).play();
_scheduleNextSeagullSound(); // Reschedule
}, randomDelay);
};
var _scheduleNextBoatSound = function scheduleNextBoatSound() {
if (GameState.currentScreen !== 'title') {
// If screen changed, ensure timer is stopped and not rescheduled
if (titleBoatSoundTimer) {
LK.clearTimeout(titleBoatSoundTimer);
titleBoatSoundTimer = null;
}
return;
}
var fixedBoatSoundInterval = 6000; // Rhythmic interval: 8 seconds (reduced from 15)
titleBoatSoundTimer = LK.setTimeout(function () {
if (GameState.currentScreen !== 'title') {
return; // Don't play or reschedule if not on title screen
}
LK.getSound('boatsounds').play();
_scheduleNextBoatSound(); // Reschedule
}, fixedBoatSoundInterval);
}; // Play initial random seagull sound
titleScreen.visible = true;
// Start all animations like in fishing screen
if (titleElements.startTitleWaterSurfaceAnimation) {
titleElements.startTitleWaterSurfaceAnimation();
}
// Start boat group animations (single animations for the whole group)
if (titleElements.moveTitleBoatGroupUp) {
titleElements.moveTitleBoatGroupUp();
}
if (titleElements.rockTitleBoatGroupLeft) {
titleElements.rockTitleBoatGroupLeft();
}
var initialSeagullSounds = ['seagull1', 'seagull2', 'seagull3'];
var initialRandomSoundId = initialSeagullSounds[Math.floor(Math.random() * initialSeagullSounds.length)];
LK.getSound(initialRandomSoundId).play();
LK.getSound('boatsounds').play(); // Play boat sound immediately after initial seagull
// Start the timed sounds (seagulls random, subsequent boats rhythmic)
_scheduleNextSeagullSound();
_scheduleNextBoatSound(); // Schedules the *next* boat sound rhythmically
// Reset particle spawn counters
titleScreenOceanBubbleSpawnCounter = 0;
titleScreenSeaweedSpawnCounter = 0;
titleScreenCloudSpawnCounter = 0;
// Animation timing
var ZOOM_DURATION = 8000; // 8-second zoom
var OVERLAY_FADE_DELAY = 1000; // Black overlay starts fading after 1 second (was 2)
var OVERLAY_FADE_DURATION = 3000; // 3 seconds to fade out overlay
var TEXT_DELAY = 4000; // Text appears as overlay finishes fading (was 5500)
var BUTTON_DELAY = 5500; // Buttons appear sooner (was 7000)
// Reset states
titleElements.titleAnimationGroup.x = GAME_CONFIG.SCREEN_CENTER_X;
titleElements.titleAnimationGroup.y = GAME_CONFIG.SCREEN_CENTER_Y; // Centers boat vertically!
titleElements.titleAnimationGroup.alpha = 1; // No alpha animation for main content
titleElements.titleAnimationGroup.scale.set(3.0);
// Reset black overlay to fully opaque
titleElements.blackOverlay.alpha = 1;
// Reset UI alphas
if (titleElements.titleImage) {
titleElements.titleImage.alpha = 0;
}
if (titleElements.logo) {
titleElements.logo.alpha = 0;
}
if (titleElements.subtitle) {
titleElements.subtitle.alpha = 0;
}
titleElements.startButton.alpha = 0;
titleElements.tutorialButton.alpha = 0;
// Main zoom animation (no alpha change)
tween(titleElements.titleAnimationGroup, {
scaleX: 1.8,
scaleY: 1.8
}, {
duration: ZOOM_DURATION,
easing: tween.easeInOut
});
// Black overlay fade out (the reveal effect)
LK.setTimeout(function () {
tween(titleElements.blackOverlay, {
alpha: 0
}, {
duration: OVERLAY_FADE_DURATION,
easing: tween.easeInOut
});
}, OVERLAY_FADE_DELAY);
// Fade in title image after overlay is mostly gone
LK.setTimeout(function () {
if (titleElements.titleImage) {
tween(titleElements.titleImage, {
alpha: 1
}, {
duration: 1200,
easing: tween.easeOut
});
} else if (titleElements.logo && titleElements.subtitle) {
// Fallback for old structure if needed
tween(titleElements.logo, {
alpha: 1
}, {
duration: 1200,
easing: tween.easeOut
});
tween(titleElements.subtitle, {
alpha: 1
}, {
duration: 1200,
easing: tween.easeOut
});
}
}, TEXT_DELAY);
// Fade in buttons near the end
LK.setTimeout(function () {
tween(titleElements.startButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut,
onFinish: function onFinish() {
// Fade in tutorial button AFTER start button finishes
tween(titleElements.tutorialButton, {
alpha: 1
}, {
duration: 1000,
easing: tween.easeOut
});
}
});
}, BUTTON_DELAY);
break;
case 'levelSelect':
levelSelectScreen.visible = true;
updateLevelSelectScreen();
break;
case 'fishing':
fishingScreen.visible = true;
playIntroAnimation(); // Play the intro sequence
break;
case 'results':
resultsScreen.visible = true;
break;
}
// Tutorial System functions moved to global scope.
}
/****
* Intro Animation
****/
function playIntroAnimation() {
GameState.introPlaying = true;
GameState.gameActive = false;
// Start animations for water, boat, and fisherman at the beginning of the intro
if (fishingElements) {
if (typeof fishingElements.startWaterSurfaceAnimation === 'function') {
fishingElements.startWaterSurfaceAnimation();
}
if (typeof fishingElements.startBoatAndFishermanAnimation === 'function') {
fishingElements.startBoatAndFishermanAnimation();
}
}
// Calculate rod tip position (relative to fishingScreen)
var fc = fishingElements.fishermanContainer;
var f = fishingElements.fisherman;
var rodTipCalculatedX = fc.x + f.x + 85;
var rodTipCalculatedY = fc.y + f.y - f.height;
var initialHookDangleY = rodTipCalculatedY + 50;
fishingElements.hook.y = initialHookDangleY;
// Setup initial zoom and camera position for fishingScreen
var INITIAL_ZOOM_FACTOR = 1.5;
// Pivot around the boat's visual center
var pivotX = fishingElements.boat.x;
var pivotY = fishingElements.boat.y - fishingElements.boat.height * (fishingElements.boat.anchor.y - 0.5);
fishingScreen.pivot.set(pivotX, pivotY);
// Position screen so the pivot appears at screen center when zoomed
var screenCenterX = 2048 / 2;
var screenCenterY = 2732 / 2;
fishingScreen.x = screenCenterX;
fishingScreen.y = screenCenterY;
fishingScreen.scale.set(INITIAL_ZOOM_FACTOR, INITIAL_ZOOM_FACTOR);
var introDuration = 2000;
// Tween for zoom out
tween(fishingScreen.scale, {
x: 1,
y: 1
}, {
duration: introDuration,
easing: tween.easeInOut
});
// Tween screen position to compensate for the zoom change
tween(fishingScreen, {
x: pivotX,
y: pivotY
}, {
duration: introDuration,
easing: tween.easeInOut
});
// Hook drop animation
var targetHookY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
// Play reel sound effect with 300ms delay during hook animation
LK.setTimeout(function () {
LK.getSound('reel').play();
}, 600);
tween(fishingElements.hook, {
y: targetHookY
}, {
duration: introDuration * 0.8,
delay: introDuration * 0.2,
easing: tween.easeOut,
onFinish: function onFinish() {
GameState.introPlaying = false;
// Reset to normal view
fishingScreen.pivot.set(0, 0);
fishingScreen.x = 0;
fishingScreen.y = 0;
// Check if we're in tutorial mode
if (GameState.tutorialMode) {
// Start the tutorial properly after intro
GameState.gameActive = false; // Keep game inactive for tutorial
createTutorialElements(); // Create tutorial UI
runTutorialStep(); // Start with step 0
} else {
// Normal fishing session
startFishingSession();
}
}
});
}
/****
* 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 (positioned in the middle area)
var tabStartY = 600; // Moved down from 400
var tabSpacing = 250; // Increased spacing between tabs
for (var i = 0; i <= GameState.currentDepth; i++) {
var depth = GAME_CONFIG.DEPTHS[i];
var isSelected = i === GameState.selectedDepth;
var tabContainer = levelSelectScreen.addChild(new Container());
var tab = tabContainer.addChild(LK.getAsset('depthTab', {
anchorX: 0.5,
anchorY: 0.5,
x: 200 + i * tabSpacing,
// Increased spacing
y: tabStartY,
tint: isSelected ? 0x1976d2 : 0x455a64,
width: 400,
// Made wider
height: 160 // Made taller
}));
var tabText = new Text2(depth.name.split(' ')[0], {
size: 40,
// Increased from 30
fill: 0xFFFFFF
});
tabText.anchor.set(0.5, 0.5);
tabText.x = 200 + i * tabSpacing;
tabText.y = tabStartY;
tabContainer.addChild(tabText);
levelSelectElements.depthTabs.push({
container: tabContainer,
tab: tab,
depthIndex: i
});
}
}
function updateSongDisplay() {
var elements = levelSelectElements;
var depth = GAME_CONFIG.DEPTHS[GameState.selectedDepth];
var song = depth.songs[GameState.selectedSong];
var owned = GameState.hasSong(GameState.selectedDepth, GameState.selectedSong);
// 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;
// Always show as disabled - coming soon
elements.shopButtonText.setText('UPGRADE ROD');
elements.shopButton.tint = 0x666666; // Always grayed out
}
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.tutorialMode = false; // Ensure tutorial mode is off
GameState.sessionScore = 0;
GameState.sessionFishCaught = 0;
GameState.sessionFishSpawned = 0;
GameState.combo = 0;
GameState.maxCombo = 0;
GameState.gameActive = true;
GameState.songStartTime = 0;
GameState.lastBeatTime = 0;
GameState.beatCount = 0;
GameState.musicNotesActive = true;
ImprovedRhythmSpawner.reset();
musicNotesArray = [];
if (fishingElements && fishingElements.musicNotesContainer) {
fishingElements.musicNotesContainer.removeChildren();
}
musicNoteSpawnCounter = 0;
// Reset ocean bubbles
globalOceanBubblesArray = [];
if (globalOceanBubbleContainer) {
globalOceanBubbleContainer.removeChildren();
}
globalOceanBubbleSpawnCounter = 0;
// Reset seaweed
globalSeaweedArray = [];
if (globalSeaweedContainer) {
globalSeaweedContainer.removeChildren();
}
globalSeaweedSpawnCounter = 0;
// Reset clouds
globalCloudArray = [];
if (globalCloudContainer) {
globalCloudContainer.removeChildren();
}
globalCloudSpawnCounter = 0;
// Animations for water, boat, and fisherman are now started in playIntroAnimation
// Clear any existing fish
fishArray.forEach(function (fish) {
fish.destroy();
});
fishArray = [];
// Reset pattern generator for new session
PatternGenerator.reset();
// Clear and create lane brackets
if (laneBrackets && laneBrackets.length > 0) {
laneBrackets.forEach(function (bracketPair) {
if (bracketPair.left && !bracketPair.left.destroyed) {
bracketPair.left.destroy();
}
if (bracketPair.right && !bracketPair.right.destroyed) {
bracketPair.right.destroy();
}
});
}
laneBrackets = [];
var bracketAssetHeight = 150; // Height of the lanebracket asset
var bracketAssetWidth = 75; // Width of the lanebracket asset
if (fishingScreen && !fishingScreen.destroyed) {
// Ensure fishingScreen is available
for (var i = 0; i < GAME_CONFIG.LANES.length; i++) {
var laneY = GAME_CONFIG.LANES[i].y;
var leftBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
x: bracketAssetWidth / 2,
// Position its center so left edge is at 0
y: laneY,
height: bracketAssetHeight
}));
var rightBracket = fishingScreen.addChild(LK.getAsset('lanebracket', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5,
scaleX: -1,
// Flipped horizontally
x: 2048 - bracketAssetWidth / 2,
// Position its center so right edge is at 2048
y: laneY,
height: bracketAssetHeight
}));
laneBrackets.push({
left: leftBracket,
right: rightBracket
});
}
}
// Start music
var songConfig = GameState.getCurrentSongConfig();
var musicIdToPlay = songConfig.musicId || 'rhythmTrack'; // Default to rhythmTrack if no specific id
GameState.currentPlayingMusicId = musicIdToPlay;
// Determine initial volume based on known assets for correct fade-out later
if (musicIdToPlay === 'morningtide') {
GameState.currentPlayingMusicInitialVolume = 1.0; // Volume defined in LK.init.music for 'morningtide'
} else {
// Default for 'rhythmTrack' or other unspecified tracks
GameState.currentPlayingMusicInitialVolume = 0.8; // Volume defined in LK.init.music for 'rhythmTrack'
}
LK.playMusic(GameState.currentPlayingMusicId); // Play the selected music track
}
function spawnFish(currentTimeForRegistration, options) {
options = options || {}; // Ensure options is an object
var depthConfig = GameState.getCurrentDepthConfig();
var songConfig = GameState.getCurrentSongConfig();
var pattern = GAME_CONFIG.PATTERNS[songConfig.pattern];
// Proximity check: Skip spawn if too close to existing fish.
// This check is generally for the first fish of a beat or non-forced spawns.
// For forced multi-beat spawns, this might prevent them if they are too close.
// Consider if this rule should be relaxed for forced multi-beat spawns if visual overlap is acceptable for quick succession.
// For now, keeping it as is. If a spawn is skipped, the multi-beat sequence might be shorter.
var isFirstFishOfBeat = !options.laneIndexToUse && !options.forcedSpawnSide;
if (isFirstFishOfBeat) {
// Apply stricter proximity for non-forced spawns
for (var i = 0; i < fishArray.length; i++) {
var existingFish = fishArray[i];
if (Math.abs(existingFish.x - GAME_CONFIG.SCREEN_CENTER_X) < PatternGenerator.minDistanceBetweenFish) {
return null; // Skip this spawn, do not register
}
}
}
var laneIndex;
if (options.laneIndexToUse !== undefined) {
laneIndex = options.laneIndexToUse;
PatternGenerator.lastLane = laneIndex; // Update generator's state if lane is forced
} else {
laneIndex = PatternGenerator.getNextLane();
}
var targetLane = GAME_CONFIG.LANES[laneIndex];
var fishType, fishValue;
var rand = Math.random();
if (rand < pattern.rareSpawnChance) {
fishType = 'rare';
fishValue = Math.floor(depthConfig.fishValue * 4);
} else if (GameState.selectedDepth >= 2 && rand < 0.3) {
fishType = 'deep';
fishValue = Math.floor(depthConfig.fishValue * 2);
} else if (GameState.selectedDepth >= 1 && rand < 0.6) {
fishType = 'medium';
fishValue = Math.floor(depthConfig.fishValue * 1.5);
} else {
fishType = 'shallow';
fishValue = Math.floor(depthConfig.fishValue);
}
var fishSpeedValue = depthConfig.fishSpeed;
var spawnSide; // -1 for left, 1 for right
var actualFishSpeed;
if (options.forcedSpawnSide !== undefined) {
spawnSide = options.forcedSpawnSide;
} else {
spawnSide = Math.random() < 0.5 ? -1 : 1;
}
actualFishSpeed = Math.abs(fishSpeedValue) * spawnSide;
var newFish = new Fish(fishType, fishValue, actualFishSpeed, laneIndex);
newFish.spawnSide = spawnSide; // Store the side it spawned from
newFish.x = actualFishSpeed > 0 ? -150 : 2048 + 150; // Start off-screen
newFish.y = targetLane.y;
newFish.baseY = targetLane.y; // Set baseY for swimming animation
fishArray.push(newFish);
fishingScreen.addChild(newFish);
GameState.sessionFishSpawned++;
PatternGenerator.registerFishSpawn(currentTimeForRegistration);
return newFish;
}
function checkCatch(fishLane) {
var hookX = fishingElements.hook.x;
if (GameState.tutorialMode) {
var tutorialFish = GameState.tutorialFish;
if (!tutorialFish || tutorialFish.lane !== fishLane || tutorialFish.caught || tutorialFish.missed) {
// Check if it's a critical catch step (now steps 3 or 4)
if (GameState.tutorialStep === 3 || GameState.tutorialStep === 4) {
setTutorialText("Oops! Make sure to tap when the fish is in the correct lane and over the hook. Tap 'CONTINUE' to try again.");
GameState.tutorialPaused = true;
}
LK.getSound('miss').play();
return;
}
var distance = Math.abs(tutorialFish.x - hookX);
var caughtType = null;
if (distance < GAME_CONFIG.PERFECT_WINDOW) {
caughtType = 'perfect';
} else if (distance < GAME_CONFIG.GOOD_WINDOW) {
caughtType = 'good';
} else if (distance < GAME_CONFIG.MISS_WINDOW) {
// For tutorial, make 'miss window' taps still count as 'good' to be more forgiving
caughtType = 'good';
} else {
caughtType = 'miss';
}
showFeedback(caughtType, fishLane); // Show visual feedback based on derived type
if (caughtType === 'perfect' || caughtType === 'good') {
tutorialFish.catchFish();
var fishIndex = fishArray.indexOf(tutorialFish);
if (fishIndex > -1) {
fishArray.splice(fishIndex, 1);
}
// No points/money in tutorial normally, but can add if desired.
LK.getSound('catch').play();
animateHookCatch();
GameState.tutorialPaused = true; // Pause to show message
if (GameState.tutorialStep === 3) {
// Was step 2
setTutorialText("Great catch! That's how you do it. Tap 'CONTINUE'.");
// GameState.tutorialStep = 4; // Advance to next conceptual phase -- Handled by game.down
} else if (GameState.tutorialStep === 4) {
// Was step 3
setTutorialText("Nice one! You're getting the hang of timing. Tap 'CONTINUE'.");
// GameState.tutorialStep = 5; // Advance to Combo explanation -- Handled by game.down
}
} else {
// Miss
// Feedback 'miss' was already shown
LK.getSound('miss').play();
tutorialFish.missed = true;
GameState.tutorialPaused = true;
setTutorialText("Almost! Try to tap when the fish is closer. Tap 'CONTINUE' to try this part again.");
// The runTutorialStep logic on "CONTINUE" will handle respawning for this step.
}
return; // Tutorial catch logic finished
}
var closestFishInLane = null;
var closestDistance = Infinity;
// This is a tap action, find the closest fish in the tapped lane.
for (var i = 0; i < fishArray.length; i++) {
var fish = fishArray[i];
// Ensure fish is not caught, not already missed, and in the correct lane
if (!fish.caught && !fish.missed && fish.lane === fishLane) {
var distance = Math.abs(fish.x - hookX);
if (distance < closestDistance) {
closestDistance = distance;
closestFishInLane = fish;
}
}
}
if (!closestFishInLane) {
// No fish found for tap
// Play miss sound
LK.getSound('miss').play();
// Tint incorrect lane indicators red briefly using tween
if (laneBrackets && laneBrackets[fishLane]) {
var leftBracket = laneBrackets[fishLane].left;
var rightBracket = laneBrackets[fishLane].right;
var tintToRedDuration = 50; // Duration to tween to red (ms)
var holdRedDuration = 100; // How long it stays fully red (ms)
var tintToWhiteDuration = 150; // Duration to tween back to white (ms)
if (leftBracket && !leftBracket.destroyed) {
// Tween to red
tween(leftBracket, {
tint: 0xFF0000
}, {
duration: tintToRedDuration,
easing: tween.linear,
onFinish: function onFinish() {
// After tinting to red, wait, then tween back to white
LK.setTimeout(function () {
if (leftBracket && !leftBracket.destroyed) {
tween(leftBracket, {
tint: 0xFFFFFF
}, {
duration: tintToWhiteDuration,
easing: tween.linear
});
}
}, holdRedDuration);
}
});
}
if (rightBracket && !rightBracket.destroyed) {
// Tween to red
tween(rightBracket, {
tint: 0xFF0000
}, {
duration: tintToRedDuration,
easing: tween.linear,
onFinish: function onFinish() {
// After tinting to red, wait, then tween back to white
LK.setTimeout(function () {
if (rightBracket && !rightBracket.destroyed) {
tween(rightBracket, {
tint: 0xFFFFFF
}, {
duration: tintToWhiteDuration,
easing: tween.linear
});
}
}, holdRedDuration);
}
});
}
}
GameState.combo = 0;
return;
}
// --- Normal Fish Catch Logic ---
var points = 0;
var multiplier = Math.max(1, Math.floor(GameState.combo / 10) + 1);
if (closestDistance < GAME_CONFIG.PERFECT_WINDOW) {
points = closestFishInLane.value * 2 * multiplier;
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;
// Mark the specific fish that was tapped but missed
if (closestFishInLane) {
closestFishInLane.missed = true;
}
return;
}
// Successfully caught fish
closestFishInLane.catchFish();
var fishIndex = fishArray.indexOf(closestFishInLane);
if (fishIndex > -1) {
fishArray.splice(fishIndex, 1);
}
GameState.sessionScore += points;
GameState.money += points;
GameState.sessionFishCaught++;
GameState.totalFishCaught++;
GameState.maxCombo = Math.max(GameState.maxCombo, GameState.combo);
// Play a random catch sound effect
var catchSounds = ['catch', 'catch2', 'catch3', 'catch4'];
var randomCatchSound = catchSounds[Math.floor(Math.random() * catchSounds.length)];
LK.getSound(randomCatchSound).play();
animateHookCatch(); // Call parameterless animateHookCatch for the single hook
// Score pop-up animation
if (points > 0) {
var scorePopupText = new Text2('+' + points, {
size: 140,
// Slightly larger for impact
fill: 0xFFD700,
// Gold color for score
align: 'center',
stroke: 0x000000,
// Black stroke
strokeThickness: 6 // Thickness of the stroke
});
scorePopupText.anchor.set(0.5, 0.5);
scorePopupText.x = GAME_CONFIG.SCREEN_CENTER_X; // Centered with the boat
scorePopupText.y = GAME_CONFIG.BOAT_Y - 70; // Start slightly above the boat's deck line
// Add to fishingScreen so it's part of the game view
if (fishingScreen && !fishingScreen.destroyed) {
fishingScreen.addChild(scorePopupText);
}
tween(scorePopupText, {
y: scorePopupText.y - 200,
// Float up by 200 pixels
alpha: 0
}, {
duration: 1800,
// Slightly longer duration for a nice float
easing: tween.easeOut,
onFinish: function onFinish() {
if (scorePopupText && !scorePopupText.destroyed) {
scorePopupText.destroy();
}
}
});
}
}
// 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;
GameState.tutorialMode = false; // Ensure tutorial mode is off
// Stop the boat's wave animation to prevent it from running after the session
if (fishingElements && fishingElements.boat) {
tween.stop(fishingElements.boat);
}
// Stop fisherman container animation
if (fishingElements && fishingElements.fishermanContainer) {
tween.stop(fishingElements.fishermanContainer);
}
// Stop fisherman rotation animation
if (fishingElements && fishingElements.fisherman) {
tween.stop(fishingElements.fisherman);
}
// Stop water surface wave animations
if (fishingElements && fishingElements.waterSurfaceSegments) {
fishingElements.waterSurfaceSegments.forEach(function (segment) {
if (segment && !segment.destroyed) {
tween.stop(segment);
}
});
}
// Stop music immediately
LK.stopMusic();
ImprovedRhythmSpawner.reset();
// Clear lane brackets
if (laneBrackets && laneBrackets.length > 0) {
laneBrackets.forEach(function (bracketPair) {
if (bracketPair.left && !bracketPair.left.destroyed) {
bracketPair.left.destroy();
}
if (bracketPair.right && !bracketPair.right.destroyed) {
bracketPair.right.destroy();
}
});
laneBrackets = [];
}
// Clear fish
fishArray.forEach(function (fish) {
fish.destroy();
});
fishArray = [];
GameState.musicNotesActive = false;
if (fishingElements && fishingElements.musicNotesContainer) {
fishingElements.musicNotesContainer.removeChildren();
}
// The MusicNoteParticle instances themselves will be garbage collected.
// Clearing the array is important.
musicNotesArray = [];
// Clear ocean bubbles
if (globalOceanBubbleContainer) {
globalOceanBubbleContainer.removeChildren();
}
globalOceanBubblesArray = [];
// Clear seaweed
if (globalSeaweedContainer) {
globalSeaweedContainer.removeChildren();
}
globalSeaweedArray = [];
// Clear clouds
if (globalCloudContainer) {
globalCloudContainer.removeChildren();
}
globalCloudArray = [];
// Create results screen
createResultsScreen();
showScreen('results');
}
function createResultsScreen() {
// Clear previous results
resultsScreen.removeChildren();
var resultsBg = resultsScreen.addChild(LK.getAsset('screenBackground', {
x: 0,
y: 0,
alpha: 0.9,
height: 2732
}));
var title = new Text2('Fishing Complete!', {
size: 100,
fill: 0xFFFFFF
});
title.anchor.set(0.5, 0.5);
title.x = GAME_CONFIG.SCREEN_CENTER_X;
title.y = 400;
resultsScreen.addChild(title);
var scoreResult = new Text2('Score: ' + GameState.sessionScore, {
size: 70,
fill: 0xFFD700
});
scoreResult.anchor.set(0.5, 0.5);
scoreResult.x = GAME_CONFIG.SCREEN_CENTER_X;
scoreResult.y = 550;
resultsScreen.addChild(scoreResult);
var fishResult = new Text2('Fish Caught: ' + GameState.sessionFishCaught + '/' + GameState.sessionFishSpawned, {
size: 50,
fill: 0xFFFFFF
});
fishResult.anchor.set(0.5, 0.5);
fishResult.x = GAME_CONFIG.SCREEN_CENTER_X;
fishResult.y = 650;
resultsScreen.addChild(fishResult);
var comboResult = new Text2('Max Combo: ' + GameState.maxCombo, {
size: 50,
fill: 0xFF9800
});
comboResult.anchor.set(0.5, 0.5);
comboResult.x = GAME_CONFIG.SCREEN_CENTER_X;
comboResult.y = 750;
resultsScreen.addChild(comboResult);
var moneyEarned = new Text2('Money Earned: $' + GameState.sessionScore, {
size: 50,
fill: 0x4CAF50
});
moneyEarned.anchor.set(0.5, 0.5);
moneyEarned.x = GAME_CONFIG.SCREEN_CENTER_X;
moneyEarned.y = 850;
resultsScreen.addChild(moneyEarned);
// 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();
var currentScreen = GameState.currentScreen;
// If tutorial mode is active, treat as 'tutorial' screen for input
if (GameState.tutorialMode && (currentScreen === 'fishing' || currentScreen === 'tutorial')) {
// New case for tutorial input
if (tutorialOverlayContainer.visible && tutorialContinueButton && tutorialContinueButton.visible && x >= tutorialContinueButton.x - tutorialContinueButton.width / 2 && x <= tutorialContinueButton.x + tutorialContinueButton.width / 2 && y >= tutorialContinueButton.y - tutorialContinueButton.height / 2 && y <= tutorialContinueButton.y + tutorialContinueButton.height / 2) {
LK.getSound('buttonClick').play();
// If tutorial is paused and it's a catch instruction step (3 or 4),
// clicking "CONTINUE" implies a retry of that step.
if (GameState.tutorialPaused && (GameState.tutorialStep === 3 || GameState.tutorialStep === 4)) {
// Logic for catch steps 3 or 4 when "CONTINUE" is pressed.
// This covers scenarios where fish was caught, missed, passed hook, or swam off screen.
var advanceAfterCatch = false;
// First, check the state of the existing tutorial fish, if any.
if (GameState.tutorialFish && GameState.tutorialFish.caught) {
advanceAfterCatch = true;
}
// Regardless of success or failure, if there was a tutorial fish, clean it up.
// This handles the fish that was just caught OR missed/swam_off.
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed) {
var idx = fishArray.indexOf(GameState.tutorialFish);
if (idx > -1) {
fishArray.splice(idx, 1);
}
GameState.tutorialFish.destroy();
}
// Ensure GameState.tutorialFish is null before runTutorialStep,
// as runTutorialStep might spawn a new one for the current or next step.
GameState.tutorialFish = null; //{pq} // Ensure it's null before re-running step to spawn a new one.
if (advanceAfterCatch) {
// If the fish was caught, advance to the next tutorial step.
GameState.tutorialStep++;
runTutorialStep();
} else {
// If the fish was missed, swam off, or there was no fish to catch (e.g. error state)
// then retry the current step. runTutorialStep will handle re-spawning.
runTutorialStep(); // Re-runs current step to spawn new fish
}
} else {
// For any other tutorial step, or if not paused in a catch step, "CONTINUE" advances.
GameState.tutorialStep++;
runTutorialStep();
}
} else if ((GameState.tutorialStep === 3 || GameState.tutorialStep === 4) && !GameState.tutorialPaused) {
// Catch attempt steps are now 3 and 4
// Catch attempt by tapping screen, not the continue button
handleFishingInput(x, y, true); // True for isDown
}
return;
}
switch (currentScreen) {
case 'title':
// Check if click is within start button bounds
var startButton = titleElements.startButton;
if (x >= startButton.x - startButton.width / 2 && x <= startButton.x + startButton.width / 2 && y >= startButton.y - startButton.height / 2 && y <= startButton.y + startButton.height / 2) {
showScreen('levelSelect');
}
// Check if click is within tutorial button bounds
var tutorialButtonGfx = titleElements.tutorialButtonGfx || titleElements.tutorialButton; // Compatibility
if (x >= tutorialButtonGfx.x - tutorialButtonGfx.width / 2 && x <= tutorialButtonGfx.x + tutorialButtonGfx.width / 2 && y >= tutorialButtonGfx.y - tutorialButtonGfx.height / 2 && y <= tutorialButtonGfx.y + tutorialButtonGfx.height / 2) {
// Defensive: check if startTutorial is defined before calling
if (typeof startTutorial === "function") {
startTutorial();
}
}
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 - disabled for "coming soon"
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) {
// Do nothing - button is disabled
}
// 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 () {
// Always update fishing line wave visuals if on fishing screen and elements are ready.
// This needs to run even during the intro when gameActive might be false.
if (GameState.currentScreen === 'fishing' && fishingElements && fishingElements.updateFishingLineWave) {
fishingElements.updateFishingLineWave();
}
// Update title screen ambient particles if title screen is active
if (GameState.currentScreen === 'title') {
// Add this to the game.update function, in the title screen section:
if (GameState.currentScreen === 'title' && titleElements && titleElements.updateTitleFishingLineWave) {
titleElements.updateTitleFishingLineWave();
}
// Title Screen Ocean Bubbles
if (titleScreenOceanBubbleContainer) {
titleScreenOceanBubbleSpawnCounter++;
if (titleScreenOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) {
// Use same interval as fishing
titleScreenOceanBubbleSpawnCounter = 0;
var newOceanBubble = new OceanBubbleParticle(); // Assuming OceanBubbleParticle is general enough
titleScreenOceanBubbleContainer.addChild(newOceanBubble);
titleScreenOceanBubblesArray.push(newOceanBubble);
}
for (var obIdx = titleScreenOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) {
var oceanBubble = titleScreenOceanBubblesArray[obIdx];
if (oceanBubble) {
oceanBubble.update(); // No fish interaction on title screen
if (oceanBubble.isDone) {
oceanBubble.destroy();
titleScreenOceanBubblesArray.splice(obIdx, 1);
}
} else {
titleScreenOceanBubblesArray.splice(obIdx, 1);
}
}
}
// Title Screen Seaweed
if (titleScreenSeaweedContainer) {
titleScreenSeaweedSpawnCounter++;
if (titleScreenSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && titleScreenSeaweedArray.length < MAX_SEAWEED_COUNT) {
titleScreenSeaweedSpawnCounter = 0;
var newSeaweed = new SeaweedParticle();
titleScreenSeaweedContainer.addChild(newSeaweed);
titleScreenSeaweedArray.push(newSeaweed);
}
for (var swIdx = titleScreenSeaweedArray.length - 1; swIdx >= 0; swIdx--) {
var seaweed = titleScreenSeaweedArray[swIdx];
if (seaweed) {
seaweed.update(); // No fish interaction on title screen
if (seaweed.isDone) {
seaweed.destroy();
titleScreenSeaweedArray.splice(swIdx, 1);
}
} else {
titleScreenSeaweedArray.splice(swIdx, 1);
}
}
}
// Title Screen Clouds
if (titleScreenCloudContainer) {
titleScreenCloudSpawnCounter++;
if (titleScreenCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && titleScreenCloudArray.length < MAX_CLOUD_COUNT) {
titleScreenCloudSpawnCounter = 0;
var newCloud = new CloudParticle();
titleScreenCloudContainer.addChild(newCloud);
titleScreenCloudArray.push(newCloud);
}
for (var cldIdx = titleScreenCloudArray.length - 1; cldIdx >= 0; cldIdx--) {
var cloud = titleScreenCloudArray[cldIdx];
if (cloud) {
cloud.update();
if (cloud.isDone) {
cloud.destroy();
titleScreenCloudArray.splice(cldIdx, 1);
}
} else {
titleScreenCloudArray.splice(cldIdx, 1);
}
}
}
}
// Spawn and update ambient ocean bubbles during intro and gameplay (fishing screen)
if (GameState.currentScreen === 'fishing' && globalOceanBubbleContainer) {
// Spawn bubbles during intro and gameplay
globalOceanBubbleSpawnCounter++;
if (globalOceanBubbleSpawnCounter >= OCEAN_BUBBLE_SPAWN_INTERVAL_TICKS) {
globalOceanBubbleSpawnCounter = 0;
var numToSpawn = 1; // Always spawn only 1 bubble per interval (was 1 or 2)
for (var i = 0; i < numToSpawn; i++) {
var newOceanBubble = new OceanBubbleParticle();
globalOceanBubbleContainer.addChild(newOceanBubble);
globalOceanBubblesArray.push(newOceanBubble);
}
}
// Update existing ocean bubbles
for (var obIdx = globalOceanBubblesArray.length - 1; obIdx >= 0; obIdx--) {
var oceanBubble = globalOceanBubblesArray[obIdx];
if (oceanBubble) {
// Apply fish physics to bubble
for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) {
var fish = fishArray[fishIdx];
if (fish && !fish.caught) {
var dx = oceanBubble.x - fish.x;
var dy = oceanBubble.y - fish.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var influenceRadius = 150; // Radius of fish influence on bubbles
var minDistance = 30; // Minimum distance to avoid division issues
if (distance < influenceRadius && distance > minDistance) {
// Calculate influence strength (stronger when closer)
var influence = 1 - distance / influenceRadius;
influence = influence * influence; // Square for more dramatic close-range effect
// Calculate normalized direction away from fish
var dirX = dx / distance;
var dirY = dy / distance;
// Apply force based on fish speed and direction
var fishSpeedFactor = Math.abs(fish.speed) * 0.15; // Scale down fish speed influence
var pushForce = fishSpeedFactor * influence;
// Add horizontal push (stronger in direction of fish movement)
oceanBubble.x += dirX * pushForce * 2; // Stronger horizontal push
// Add vertical component (bubbles get pushed up/down)
oceanBubble.vy += dirY * pushForce * 0.5; // Gentler vertical influence
// Add some swirl/turbulence to drift
oceanBubble.driftAmplitude = Math.min(80, oceanBubble.driftAmplitude + pushForce * 10);
oceanBubble.driftFrequency *= 1 + influence * 0.1; // Slightly increase oscillation when disturbed
}
}
}
oceanBubble.update();
if (oceanBubble.isDone) {
oceanBubble.destroy();
globalOceanBubblesArray.splice(obIdx, 1);
}
} else {
globalOceanBubblesArray.splice(obIdx, 1); // Safeguard for null entries
}
}
}
// Spawn and update seaweed particles during intro and gameplay
if (GameState.currentScreen === 'fishing' && globalSeaweedContainer) {
// Spawn seaweed during intro and gameplay
globalSeaweedSpawnCounter++;
if (globalSeaweedSpawnCounter >= SEAWEED_SPAWN_INTERVAL_TICKS && globalSeaweedArray.length < MAX_SEAWEED_COUNT) {
globalSeaweedSpawnCounter = 0;
var newSeaweed = new SeaweedParticle();
globalSeaweedContainer.addChild(newSeaweed);
globalSeaweedArray.push(newSeaweed);
}
// Update existing seaweed
for (var swIdx = globalSeaweedArray.length - 1; swIdx >= 0; swIdx--) {
var seaweed = globalSeaweedArray[swIdx];
if (seaweed) {
// Apply fish physics to seaweed
for (var fishIdx = 0; fishIdx < fishArray.length; fishIdx++) {
var fish = fishArray[fishIdx];
if (fish && !fish.caught) {
var dx = seaweed.x - fish.x;
var dy = seaweed.y - fish.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var influenceRadius = 180; // Slightly larger influence for seaweed
var minDistance = 40;
if (distance < influenceRadius && distance > minDistance) {
// Calculate influence strength
var influence = 1 - distance / influenceRadius;
influence = influence * influence;
// Calculate normalized direction away from fish
var dirX = dx / distance;
var dirY = dy / distance;
// Apply force based on fish speed
var fishSpeedFactor = Math.abs(fish.speed) * 0.2; // Stronger influence on seaweed
var pushForce = fishSpeedFactor * influence;
// Add push forces
seaweed.vx += dirX * pushForce * 1.5; // Seaweed is affected more horizontally
seaweed.vy += dirY * pushForce * 0.8; // And moderately vertically
// Increase sway when disturbed
seaweed.swayAmplitude = Math.min(60, seaweed.swayAmplitude + pushForce * 15);
}
}
}
seaweed.update();
if (seaweed.isDone) {
seaweed.destroy();
globalSeaweedArray.splice(swIdx, 1);
}
} else {
globalSeaweedArray.splice(swIdx, 1);
}
}
}
// Spawn and update cloud particles during intro and gameplay
if (GameState.currentScreen === 'fishing' && globalCloudContainer) {
// Spawn clouds during intro and gameplay
globalCloudSpawnCounter++;
if (globalCloudSpawnCounter >= CLOUD_SPAWN_INTERVAL_TICKS && globalCloudArray.length < MAX_CLOUD_COUNT) {
globalCloudSpawnCounter = 0;
var newCloud = new CloudParticle();
globalCloudContainer.addChild(newCloud);
globalCloudArray.push(newCloud);
}
// Update existing clouds
for (var cldIdx = globalCloudArray.length - 1; cldIdx >= 0; cldIdx--) {
var cloud = globalCloudArray[cldIdx];
if (cloud) {
cloud.update();
if (cloud.isDone) {
cloud.destroy();
globalCloudArray.splice(cldIdx, 1);
}
} else {
globalCloudArray.splice(cldIdx, 1);
}
}
}
// Standard game active check; if intro is playing, gameActive will be false.
// Tutorial mode has its own logic path
if (GameState.currentScreen === 'fishing' && GameState.tutorialMode) {
// Update essential non-paused elements like fishing line wave
if (fishingElements && fishingElements.updateFishingLineWave) {
fishingElements.updateFishingLineWave();
}
// Update ambient particles (clouds, background bubbles, seaweed can run if desired)
// ... (Can copy particle update logic here if they should be active in tutorial)
if (!GameState.tutorialPaused) {
// Update tutorial fish if one exists and is active
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) {
GameState.tutorialFish.update();
checkTutorialFishState(); // Check its state (missed, off-screen)
}
// Hook follows tutorial fish
if (GameState.tutorialFish && !GameState.tutorialFish.destroyed && !GameState.tutorialFish.caught) {
if (GameState.hookTargetLaneIndex !== GameState.tutorialFish.lane) {
GameState.hookTargetLaneIndex = GameState.tutorialFish.lane;
}
var targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
if (fishingElements.hook && Math.abs(fishingElements.hook.y - targetLaneY) > 1) {
// Smoother threshold
// fishingElements.hook.y = targetLaneY; // Instant for tutorial responsiveness
tween(fishingElements.hook, {
y: targetLaneY
}, {
duration: 100,
easing: tween.linear
});
fishingElements.hook.originalY = targetLaneY;
}
} else if (fishingElements.hook) {
// If no tutorial fish, hook stays in middle or last targeted lane
var middleLaneY = GAME_CONFIG.LANES[1].y;
if (fishingElements.hook.y !== middleLaneY) {
// tween(fishingElements.hook, { y: middleLaneY }, { duration: 150, easing: tween.easeOut });
// fishingElements.hook.originalY = middleLaneY;
}
}
}
updateLaneBracketsVisuals();
return; // End tutorial update logic
}
if (GameState.currentScreen !== 'fishing' || !GameState.gameActive) {
return;
}
// Note: The fishing line wave update was previously here, it's now moved up.
var currentTime = LK.ticks * (1000 / 60);
// Initialize game timer
if (GameState.songStartTime === 0) {
GameState.songStartTime = currentTime;
}
// Check song end
var songConfig = GameState.getCurrentSongConfig();
if (currentTime - GameState.songStartTime >= songConfig.duration) {
endFishingSession();
return;
}
// Use RhythmSpawner to handle fish spawning
ImprovedRhythmSpawner.update(currentTime);
// Dynamic Hook Movement Logic
var approachingFish = null;
var minDistanceToCenter = Infinity;
for (var i = 0; i < fishArray.length; i++) {
var f = fishArray[i];
if (!f.caught) {
var distanceToHookX = Math.abs(f.x - fishingElements.hook.x);
var isApproachingOrAtHook = f.speed > 0 && f.x < fishingElements.hook.x || f.speed < 0 && f.x > fishingElements.hook.x || distanceToHookX < GAME_CONFIG.MISS_WINDOW * 2;
if (isApproachingOrAtHook && distanceToHookX < minDistanceToCenter) {
minDistanceToCenter = distanceToHookX;
approachingFish = f;
}
}
}
var targetLaneY;
if (approachingFish) {
if (GameState.hookTargetLaneIndex !== approachingFish.lane) {
GameState.hookTargetLaneIndex = approachingFish.lane;
}
targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
} else {
targetLaneY = GAME_CONFIG.LANES[GameState.hookTargetLaneIndex].y;
}
// Update hook Y position (X is handled by the wave animation)
if (Math.abs(fishingElements.hook.y - targetLaneY) > 5) {
// Only tween if significantly different
tween(fishingElements.hook, {
y: targetLaneY
}, {
duration: 150,
easing: tween.easeOut
});
fishingElements.hook.originalY = targetLaneY;
} //{bO} // Re-using last relevant ID from replaced block for context if appropriate
updateLaneBracketsVisuals();
// Update fish
for (var i = fishArray.length - 1; i >= 0; i--) {
var fish = fishArray[i];
var previousFrameX = fish.lastX; // X position from the end of the previous game tick
fish.update(); // Fish updates its own movement (fish.x) and appearance
var currentFrameX = fish.x; // X position after this tick's update
// Check for miss only if fish is active (not caught, not already missed)
if (!fish.caught && !fish.missed) {
var hookCenterX = fishingElements.hook.x;
// GAME_CONFIG.MISS_WINDOW is the distance from hook center that still counts as a "miss" tap.
// If fish center passes this boundary, it's considered a full pass.
var missCheckBoundary = GAME_CONFIG.MISS_WINDOW;
if (fish.speed > 0) {
// Moving Left to Right
// Fish's center (previousFrameX) was to the left of/at the hook's right miss boundary,
// and its center (currentFrameX) is now to the right of it.
if (previousFrameX <= hookCenterX + missCheckBoundary && currentFrameX > hookCenterX + missCheckBoundary) {
showFeedback('miss', fish.lane);
LK.getSound('miss').play();
GameState.combo = 0;
fish.missed = true; // Mark as missed
}
} else if (fish.speed < 0) {
// Moving Right to Left
// Fish's center (previousFrameX) was to the right of/at the hook's left miss boundary,
// and its center (currentFrameX) is now to the left of it.
if (previousFrameX >= hookCenterX - missCheckBoundary && currentFrameX < hookCenterX - missCheckBoundary) {
showFeedback('miss', fish.lane);
LK.getSound('miss').play();
GameState.combo = 0;
fish.missed = true; // Mark as missed
}
}
}
// Update lastX for the next frame, using the fish's position *after* its update this frame
fish.lastX = currentFrameX;
// Remove off-screen fish (if not caught).
// If a fish is `missed`, it's also `!caught`, so it will be removed by this logic.
if (!fish.caught && (fish.x < -250 || fish.x > 2048 + 250)) {
// Increased buffer slightly
fish.destroy();
fishArray.splice(i, 1);
}
}
// Update UI
updateFishingUI();
// Spawn and update music notes if active
if (GameState.musicNotesActive && fishingElements && fishingElements.hook && !fishingElements.hook.destroyed && musicNotesContainer) {
musicNoteSpawnCounter++;
if (musicNoteSpawnCounter >= MUSIC_NOTE_SPAWN_INTERVAL_TICKS) {
musicNoteSpawnCounter = 0;
// Spawn notes from the fishing hook's position
var spawnX = fishingElements.hook.x;
var spawnY = fishingElements.hook.y - 30; // Spawn slightly above the hook's center for better visual origin
var newNote = new MusicNoteParticle(spawnX, spawnY);
musicNotesContainer.addChild(newNote);
musicNotesArray.push(newNote);
// Add scale pulse to the hook, synced with BPM
if (fishingElements.hook && !fishingElements.hook.destroyed && fishingElements.hook.scale) {
var currentSongConfig = GameState.getCurrentSongConfig();
var bpm = currentSongConfig && currentSongConfig.bpm ? currentSongConfig.bpm : 90; // Default to 90 BPM
var beatDurationMs = 60000 / bpm;
var pulsePhaseDuration = Math.max(50, beatDurationMs / 2); // Each phase (up/down) is half a beat, min 50ms
var pulseScaleFactor = 1.2;
// Ensure we have valid original scales, defaulting to 1 if undefined
var originalScaleX = fishingElements.hook.scale.x !== undefined ? fishingElements.hook.scale.x : 1;
var originalScaleY = fishingElements.hook.scale.y !== undefined ? fishingElements.hook.scale.y : 1;
// Stop any previous scale tweens on the hook to prevent conflicts
tween.stop(fishingElements.hook.scale);
tween(fishingElements.hook.scale, {
x: originalScaleX * pulseScaleFactor,
y: originalScaleY * pulseScaleFactor
}, {
duration: pulsePhaseDuration,
easing: tween.easeOut,
onFinish: function onFinish() {
if (fishingElements.hook && !fishingElements.hook.destroyed && fishingElements.hook.scale) {
tween(fishingElements.hook.scale, {
x: originalScaleX,
y: originalScaleY
}, {
duration: pulsePhaseDuration,
easing: tween.easeIn // Or tween.easeOut for a softer return
});
}
}
});
}
}
}
// Update existing music notes
for (var mnIdx = musicNotesArray.length - 1; mnIdx >= 0; mnIdx--) {
var note = musicNotesArray[mnIdx];
if (note) {
note.update();
if (note.isDone) {
note.destroy();
musicNotesArray.splice(mnIdx, 1);
}
} else {
// Should not happen, but good to safeguard
musicNotesArray.splice(mnIdx, 1);
}
}
// Spawn bubbles for active fish
if (bubbleContainer) {
for (var f = 0; f < fishArray.length; f++) {
var fish = fishArray[f];
if (fish && !fish.caught && !fish.isHeld && fish.fishGraphics) {
if (currentTime - fish.lastBubbleSpawnTime > fish.bubbleSpawnInterval) {
fish.lastBubbleSpawnTime = currentTime;
// Calculate tail position based on fish direction and width
// fish.fishGraphics.width is the original asset width.
// fish.fishGraphics.scale.x might be negative, but width property itself is positive.
// The anchor is 0.5, so width/2 is distance from center to edge.
var tailOffsetDirection = Math.sign(fish.speed) * -1; // Bubbles appear opposite to movement direction
var bubbleX = fish.x + tailOffsetDirection * (fish.fishGraphics.width * Math.abs(fish.fishGraphics.scaleX) / 2) * 0.8; // 80% towards tail
var bubbleY = fish.y + (Math.random() - 0.5) * (fish.fishGraphics.height * Math.abs(fish.fishGraphics.scaleY) / 4); // Slight Y variance around fish center
var newBubble = new BubbleParticle(bubbleX, bubbleY);
bubbleContainer.addChild(newBubble);
bubblesArray.push(newBubble);
}
}
}
}
// Update and remove bubbles
for (var b = bubblesArray.length - 1; b >= 0; b--) {
var bubble = bubblesArray[b];
if (bubble) {
// Extra safety check
bubble.update();
if (bubble.isDone) {
bubble.destroy();
bubblesArray.splice(b, 1);
}
} else {
// If a null/undefined somehow got in
bubblesArray.splice(b, 1);
}
}
};
// 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 sardine. 80s arcade machine graphics. Swimming Side profile. In-Game asset. 2d. High contrast. No shadows
An anchovy. 80s arcade machine graphics. Swimming Side profile. In-Game asset. 2d. High contrast. No shadows
A mackerel. 80s arcade machine graphics. Swimming Side profile. 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.
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