Skip to content

Commit f0a7dba

Browse files
committed
2 parents 1f1666b + ad2f632 commit f0a7dba

7 files changed

Lines changed: 105 additions & 12 deletions

File tree

CLAUDE.md

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,47 @@ Achievements registered in `Achievements.initAchievements()` with event-based co
4545
FmodConstants.hx is auto-generated from the FMOD Studio project in `fmod/`. Use `FmodManager.PlaySong()` / `PlaySoundOneShot()` / `StopSong()`. Constants are in FmodSongs and FmodSFX.
4646

4747
### Player (source/entities/Player.hx)
48-
Player extends FlxSprite (48x48 graphic, 16x16 hitbox). Takes an `FlxState` reference in its constructor so it can add/remove its own child sprites (reticle, power bar, cast bobber) from the scene. Movement speed is 150px/s (225 in hot mode). Uses `InputCalculator.getInputCardinal()` for input and tracks `lastInputDir` for facing direction. Has a `frozen` flag that suppresses movement during cast charging.
48+
Player extends FlxSprite (48x48 graphic, 16x16 hitbox). Takes an `FlxState` reference in its constructor so it can add/remove its own child sprites (reticle, power bar, cast bobber) from the scene. Movement speed is 100px/s (150 in hot mode). Uses `InputCalculator.getInputCardinal()` for input and tracks `lastInputDir` for facing direction. Has a `frozen` flag that suppresses movement during cast charging, catch animation, and retrieve (unfreezes only when the bobber is destroyed after reaching the player). Supports multiple skins (Q/E to cycle). Skin loading parses Aseprite JSON atlas (`loadSkin()`) and auto-detects one-shot animations by prefix (`cast_`, `throw_`, `catch_`).
4949

5050
### Fishing Cast System (source/entities/Player.hx)
51-
Cast mechanic uses a `CastState` enum (IDLE → CHARGING → CAST_ANIM → CASTING → LANDED → CATCH_ANIM → RETURNING). Press Z to start charging — a power bar pulses below the player. Press Z again to launch a bobber toward the reticle at a distance proportional to power (max 96px / 6 tiles). Press Z or move to retract the bobber at any point (mid-flight, landed). The bobber flies back to the player at 500px/s before being destroyed. The `CastState` enum is defined at module level in Player.hx.
51+
Cast mechanic uses a `CastState` enum (IDLE → CHARGING → CAST_ANIM → CASTING → LANDED → CATCH_ANIM → RETURNING). Press A (Z key) to start charging — a power bar pulses below the player. Press A again to launch a bobber toward the reticle at a distance proportional to power (max 96px / 6 tiles). Press A or move to retract the bobber at any point (mid-flight, landed). The `CastState` enum is defined at module level in Player.hx.
5252

53-
The bobber launches on frame 3 of the 5-frame cast animation (CAST_LAUNCH_FRAME) with velocity 300px/s. A dot-product overshoot check in CAST_ANIM clamps the bobber at the target if it arrives during the animation, preventing visual snap-back on short casts. The same dot-product check in CASTING transitions to LANDED. `Player.catchFish()` transitions to CATCH_ANIM, which plays a retract animation and returns the bobber.
53+
The bobber launches on frame 3 of the 5-frame cast animation (CAST_LAUNCH_FRAME) using a parabolic arc at 150px/s. The arc uses `updateCastArc()` with formula `arcHeight * 4 * t * (1-t)`. CAST_ANIM clamps the bobber at the target if it arrives during the animation. The same check in CASTING transitions to LANDED.
54+
55+
**Retrieve:** `Player.catchFish(hasFish)` transitions to CATCH_ANIM. The `retractHasFish` flag controls retrieve style: with a fish, the bobber/fish arcs back via `updateCastArc()` at 188px/s; without a fish, straight-line velocity at 188px/s. On catch, the bobber sprite swaps to `fish.png` showing the caught fish frame (`caughtFishSpriteIndex`). CATCH_ANIM → RETURNING keeps `frozen = true`; player unfreezes only when the bobber is destroyed after reaching them. Movement animation is suppressed during CAST_ANIM, CATCH_ANIM, and RETURNING to prevent moonwalking.
56+
57+
**Fish delivery callback:** `onFishDelivered` fires when the bobber/fish reaches the player. PlayState wires this to add the fish to inventory (or spawn a GroundFish if inventory is full). The callback is set before `catchFish()` and nulled after firing.
58+
59+
**Fishing line:** Drawn pixel-by-pixel each frame from rod tip to bobber center. Rod tip positions (`getRodTipPos()`) vary per cast direction and per animation frame. Left/right casts use cubic Bezier curves with downward sag; up/down use Bresenham line drawing.
60+
61+
**Rod tip positions:** Manually calibrated per direction per frame. The CATCH_ANIM/RETURNING branch has 3 frames per direction (frame 0 = cast position, frame 1 = mid-retract, frame 2 = final resting position).
5462

5563
### Fish System (source/entities/)
56-
**FishSpawner** (`FishSpawner.hx`) is a `FlxTypedGroup<WaterFish>` that flood-fills the FishSpawner IntGrid layer to find water bodies, then spawns `WaterFish` into each body. It handles separation (fleeing when fish are too close) and passes bobber references through to fish via `setBobber()`. Takes an `onCatch` callback in its constructor, wired to each fish at spawn time.
64+
**FishSpawner** (`FishSpawner.hx`) is a `FlxTypedGroup<WaterFish>` that flood-fills the FishSpawner IntGrid layer to find water bodies, then spawns `WaterFish` into each body. Each FishSpawner LDTK entity has a `numFish` field controlling how many fish spawn in that body. FishSpawner handles separation — when two fish are closer than `SEPARATION_DIST` (20px), **both** flee from each other (not just one). Passes bobber references through to fish via `setBobber()`. Takes an `onCatch` callback in its constructor, wired to each fish at spawn time.
65+
66+
**WaterFish** (`WaterFish.hx`) owns its own bobber-awareness logic using center-to-center distance (`x + width/2, y + height/2`). Each fish has a nullable `bobber` reference and an `onCatch` callback. `checkBobber()` is called when `bobber != null || attracted` — this handles the case where the bobber is retracted while a fish is swimming toward it. Distance thresholds: attract within 32px, catch within 4px. When a fish is attracted and the bobber becomes null (retracted), the fish flees in the opposite direction via `fleeFrom()` then resumes normal wandering. `fleeFrom()` returns immediately if the fish is attracted to a bobber (attraction overrides separation). Flee picks the farthest water tile in the away direction and immediately sets velocity (no pause). Fish fade in over 1 second when spawning/respawning. After being caught, fish respawn at a random water tile after 3 seconds. Fish use `fishShadow.png` sprite with `centerOffsets()` for proper hitbox alignment.
67+
68+
**GroundFish** (`GroundFish.hx`) — fish that land on the ground when the player's inventory is full. Arcs from the player's head position to a random non-water landing spot. Uses `fish.png` spritesheet (32x32 frames, 5 fish types). Has a `FISH_SIZES` lookup table with actual pixel dimensions per frame: `[8,8], [9,9], [12,12], [13,14], [15,16]` (top-left aligned within the 32x32 cell). Origin is set to the center of the actual fish content for proper rotation. While landing (`landing = true`), the fish arcs through the air and can't be picked up. After landing, it flops (sine-wave rotation) and can be picked up by walking over it.
69+
70+
**GroundFishGroup** (`GroundFishGroup.hx`) manages ground fish spawning and pickup. `addFish()` picks a random landing spot 16-32px away, trying up to 20 times to avoid water tiles (checked via the LDTK FishSpawner IntGrid layer). Prevents pickup during landing arc.
5771

58-
**WaterFish** (`WaterFish.hx`) owns its own bobber-awareness logic. Each fish has a nullable `bobber` reference and an `onCatch` callback. In `update()`, the fish checks its distance to the bobber and autonomously decides to attract (within 32px), get caught (within 4px), or ignore. Fish wander between random water tiles with pause/retarget timers, and flee from other fish that get too close.
72+
**PlayState wiring:** PlayState creates the spawner with an `onFishCaught` callback that sets `player.onFishDelivered` before calling `player.catchFish(true)`. The delivery callback adds fish to inventory; if full, spawns a GroundFish at the player's head (x+8, y-2) using the caught fish's sprite frame (`player.caughtFishSpriteIndex`). Each frame calls `fishSpawner.setBobber(player.isBobberLanded() ? player.castBobber : null)`, `rockGroup.checkPickup(player)`, and `groundFishGroup.checkPickup(player)`.
5973

60-
**PlayState wiring:** PlayState creates the spawner with `() -> player.catchFish()` as the catch callback, then each frame calls `fishSpawner.setBobber(player.isBobberLanded() ? player.castBobber : null)`.
74+
### Rock Throwing (source/entities/Player.hx, source/entities/Rock.hx)
75+
Press B to throw a rock from inventory toward the reticle (max 96px). The rock arcs via parabolic flight (`arcHeight * 4 * t * (1-t)`, max height = min(dist*0.5, 64)) at 200px/s. Player is frozen during the throw animation. The rock launches on frame 6 of the throw animation. `makeRock` factory is set by PlayState to create rocks that know about the spawner layer. After landing, `resolveThrow()` is called on the rock.
76+
77+
### Inventory (source/entities/Inventory.hx)
78+
Simple array-based inventory with `MAX_SLOTS = 4`. Supports `add()`, `remove()`, `has()`, `isFull()`, `count()`. Items are the `InventoryItem` enum: `Rock`, `Fish`. Fires `onChange` signal on add/remove. InventoryHUD displays current inventory state.
79+
80+
### Networking (source/net/NetworkManager.hx)
81+
Colyseus-based multiplayer. `NetworkManager` manages client connection, room joining, and message passing. Signals: `onJoined`, `onPlayerAdded`, `onPlayerChanged`, `onPlayerRemoved`, `onFishAdded`, `onFishMove`. `IS_HOST` determines whether this client spawns fish/rocks. The `sendMessage()` method has an optional `mute` parameter to suppress per-frame logging (used by `sendMove()`). In `-Dlocal` mode, all methods early-return as no-ops and `IS_HOST` defaults to `true`.
82+
83+
PlayState manages remote players (`remotePlayers` map) and remote fish (`remoteFish` map). Remote players are `Player` instances with `isRemote = true` that skip input processing and are driven by network events.
84+
85+
### Round/Game Management (source/managers/)
86+
**GameManager** (`GameManager.hx`) — singleton (`ME`) that holds the `NetworkManager`, `FishManager`, and orchestrates rounds. Constructed with an array of `Round` definitions. Calls `net.sendMessage("round_update", ...)` at round transitions (lobby, pre-round, post-round, end-game).
87+
88+
**RoundManager** (`RoundManager.hx`) — manages a single round's goals. Signals completion when all goals (or any goal, depending on `allGoalsRequired`) are met. `initialize(state)` is called after PlayState creates to set up round-specific behavior.
6189

6290
### Analytics & Storage (source/helpers/)
6391
Analytics.hx reports events to Bitlytics. Storage.hx handles local persistence for achievements and metrics.
@@ -81,6 +109,8 @@ Analytics.hx reports events to Bitlytics. Storage.hx handles local persistence f
81109
- `API_KEY` — analytics token for production
82110
- `dev_analytics` — dev mode analytics
83111
- `llm_bridge` — enable LLM debug bridge (`window.__debug` API for Playwright introspection)
112+
- `local` — fully offline mode: `NetworkManager.IS_HOST` defaults to `true`, `connect()`/`sendMove()`/`sendMessage()` are no-ops (early return). Fish/rocks spawn immediately (no 10s network delay). PlayState skips `setupNetwork()` and `fishSpawner.setNet()`. Call-site code does **not** need `#if !local` guards — NetworkManager handles it internally.
113+
- `rocks` — debug flag: fills player inventory with `MAX_SLOTS` rocks at construction time
84114

85115
## Conventions
86116

@@ -93,6 +123,13 @@ Analytics.hx reports events to Bitlytics. Storage.hx handles local persistence f
93123
- When making sprites visible, set their position before setting `visible = true` to avoid a one-frame flash at the previous location
94124
- Use `FlxPoint.get()`/`.put()` for pooled points; call `.put()` when done to return to pool
95125

126+
## Key Sprite Assets
127+
- `assets/aseprite/characters/playerA.json` (and playerB-H) — player skins, 48x48 frames, Aseprite JSON atlas with frame tags for animations
128+
- `assets/aseprite/characters/fishShadow.png` — water fish silhouette sprite
129+
- `assets/aseprite/fish.png` — caught/ground fish spritesheet, 32x32 frames (3 columns x 2 rows), 5 fish types of varying sizes within the cells
130+
- `assets/aseprite/bobber.png` — fishing bobber sprite
131+
- `assets/aseprite/aimingTarget.png` — reticle/aiming target, 8x8 frames, 4-frame animation
132+
96133
## Git Hooks
97134

98135
Pre-commit hook auto-exports changed Aseprite files and runs the formatter on staged files.

source/entities/FishSpawner.hx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ class FishSpawner extends FlxTypedGroup<WaterFish> {
134134
}
135135
}
136136

137+
public function scareFish(splashX:Float, splashY:Float, radius:Float = 80) {
138+
for (fish in members) {
139+
if (fish == null || !fish.alive)
140+
continue;
141+
var dx = fish.x - splashX;
142+
var dy = fish.y - splashY;
143+
if (dx * dx + dy * dy < radius * radius) {
144+
fish.scare(splashX, splashY);
145+
}
146+
}
147+
}
148+
137149
public function setBobber(bobber:FlxSprite) {
138150
for (fish in members) {
139151
if (fish == null || !fish.alive)

source/entities/Player.hx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@ class Player extends FlxSprite {
541541
function updateCast(elapsed:Float) {
542542
switch (castState) {
543543
case IDLE:
544-
if (SimpleController.just_pressed(A)) {
544+
if (!throwing && SimpleController.just_pressed(A)) {
545545
castState = CHARGING;
546546
frozen = true;
547547
castPower = 0;

source/entities/Rock.hx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,28 @@ import flixel.FlxSprite;
55
class Rock extends FlxSprite {
66
var waterLayer:ldtk.Layer_IntGrid;
77
var onAddToWorld:(Float, Float) -> Void;
8+
var onWaterSplash:(Float, Float) -> Void;
89

9-
public function new(x:Float, y:Float, ?waterLayer:ldtk.Layer_IntGrid, ?onAddToWorld:(Float, Float) -> Void) {
10+
public function new(x:Float, y:Float, ?waterLayer:ldtk.Layer_IntGrid, ?onAddToWorld:(Float, Float) -> Void, ?onWaterSplash:(Float, Float) -> Void) {
1011
super(x, y);
1112
this.waterLayer = waterLayer;
1213
this.onAddToWorld = onAddToWorld;
14+
this.onWaterSplash = onWaterSplash;
1315
loadGraphic(AssetPaths.rock__png);
1416
}
1517

1618
public function resolveThrow(landX:Float, landY:Float) {
17-
if (waterLayer == null || onAddToWorld == null)
19+
if (waterLayer == null)
1820
return;
1921
var grid = waterLayer.gridSize;
2022
var tileX = Std.int(landX / grid);
2123
var tileY = Std.int(landY / grid);
22-
if (waterLayer.getInt(tileX, tileY) != 1) {
23-
onAddToWorld(landX, landY);
24+
if (waterLayer.getInt(tileX, tileY) == 1) {
25+
if (onWaterSplash != null)
26+
onWaterSplash(landX, landY);
27+
} else {
28+
if (onAddToWorld != null)
29+
onAddToWorld(landX, landY);
2430
}
2531
}
2632
}

source/entities/WaterFish.hx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class WaterFish extends FlxSprite {
3333

3434
var respawnTimer:Float = 0;
3535
var fadeInTimer:Float = 0;
36+
var scaredTimer:Float = 0;
3637

3738
public function new(id:String, x:Float, y:Float, waterTiles:Array<FlxPoint> = null, isRemote = false) {
3839
super(x, y);
@@ -139,6 +140,18 @@ class WaterFish extends FlxSprite {
139140

140141
super.update(elapsed);
141142

143+
if (scaredTimer > 0) {
144+
scaredTimer -= elapsed;
145+
alpha = Math.max(0, scaredTimer / 0.5);
146+
if (scaredTimer <= 0) {
147+
alive = false;
148+
visible = false;
149+
velocity.set(0, 0);
150+
respawnTimer = 5.5;
151+
}
152+
return;
153+
}
154+
142155
if (bobber != null || attracted) {
143156
checkBobber();
144157
}
@@ -210,6 +223,13 @@ class WaterFish extends FlxSprite {
210223
}
211224
}
212225

226+
public function scare(fromX:Float, fromY:Float) {
227+
attracted = false;
228+
fleeFrom(fromX, fromY);
229+
velocity.scale(1.5);
230+
scaredTimer = 0.5;
231+
}
232+
213233
function stopAttract() {
214234
attracted = false;
215235
pickTarget();

source/net/NetworkManager.hx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class NetworkManager {
2828
public var onPlayerRemoved:SessionIdSignal = new SessionIdSignal();
2929
public var onFishMove:FishStateSignal = new FishStateSignal();
3030
public var onFishAdded = new FishStateSignal();
31+
public var onRockSplash = new FlxTypedSignal<Float->Float->Void>();
3132

3233
public static inline var roomName:String = "game_room";
3334

@@ -104,6 +105,13 @@ class NetworkManager {
104105
room.onMessage("cast_line", (message) -> {
105106
trace('[NetMan] cast_line => ${message.x}, ${message.y}');
106107
});
108+
109+
room.onMessage("rock_splash", (message:Dynamic) -> {
110+
var sx:Float = message.x;
111+
var sy:Float = message.y;
112+
trace('[NetMan] rock_splash => $sx, $sy');
113+
onRockSplash.dispatch(sx, sy);
114+
});
107115
});
108116
}
109117

source/states/PlayState.hx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class PlayState extends FlxTransitionableState {
106106
GameManager.ME.net.onPlayerAdded.add(onPlayerAdded);
107107
GameManager.ME.net.onPlayerRemoved.add(onPlayerRemoved);
108108
GameManager.ME.net.onFishAdded.add(onFishAdded);
109+
GameManager.ME.net.onRockSplash.add(onRemoteRockSplash);
109110
}
110111

111112
function onPlayerJoined(sessionId:String) {
@@ -150,6 +151,15 @@ class PlayState extends FlxTransitionableState {
150151
QLog.notice('fish post-add pos: ${newFish.x}, ${newFish.y}');
151152
}
152153

154+
function onRemoteRockSplash(x:Float, y:Float) {
155+
fishSpawner.scareFish(x, y);
156+
}
157+
158+
function onLocalRockSplash(x:Float, y:Float) {
159+
fishSpawner.scareFish(x, y);
160+
GameManager.ME.net.sendMessage("rock_splash", {x: x, y: y});
161+
}
162+
153163
function loadLevel(level:String) {
154164
unload();
155165

@@ -179,7 +189,7 @@ class PlayState extends FlxTransitionableState {
179189
#end
180190

181191
var spawnerLayer = level.fishSpawnerLayer;
182-
player.makeRock = (rx, ry) -> new Rock(rx, ry, spawnerLayer, rockGroup.addRock);
192+
player.makeRock = (rx, ry) -> new Rock(rx, ry, spawnerLayer, rockGroup.addRock, onLocalRockSplash);
183193
groundFishGroup.setWaterLayer(spawnerLayer);
184194

185195
shop = new Shop();

0 commit comments

Comments
 (0)