/****
* 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.