Skip to content

ItemFisherAsset — Fishing Rods and Catch Mechanics

Overview

ItemFisherAsset extends ItemAsset and defines fishing rods. It is one of the mechanically richest item assets in the SDK, supporting a two-mode fishing system: a passive legacy mode where the catch resolves automatically after a bite, and an active catch-challenge mode with a real-time physics-based minigame. The asset carries audio clips for the cast/reel/tug sound stages, a reward spawn table reference, experience rewards, an optional NPC quest reward list, and the full parameter set for the catch-challenge cursor physics.

The companion class FishingCatchableProperties defines per-catchable-item physics parameters that customize how each fish species behaves during the catch-challenge minigame. Water volumes and fishing zones reference these properties to give different fish types distinct movement patterns.

Inheritance Chain

Asset
  └── ItemAsset
        └── ItemFisherAsset

ItemFisherAsset does not introduce intermediate base classes. It branches directly from ItemAsset, gaining all shared item fields (id, rarity, size, slot, quality, exchangeability) without layering through ItemWeaponAsset, ItemClothingAsset, or ItemBarricadeAsset. Fishing rods are held as equipable items but do not fire projectiles, deal damage, or place structures.

The EFishingRewardMode Enum

ItemFisherAsset introduces EFishingRewardMode, a two-value enum controlling how catchable items are selected:

csharp
public enum EFishingRewardMode
{
    Rod,
    WaterVolumes,
}

Rod Mode (Default)

The fishing rod's own _rewardID spawn table determines all catchable items. Per-volume reward tables are ignored entirely. This is the backwards-compatible default — all existing fishing rods before the WaterVolumes mode was added behave this way.

In Rod mode, the catch resolution calls SpawnTableTool.Resolve(rewardID) once per successful catch. Every cast from the same rod draws from the same table regardless of where the player is standing.

WaterVolumes Mode

Each water volume (or the level itself if a volume doesn't specify rewards) defines its own reward tables. The game checks the volume the bobber landed in first; if that volume has no reward configuration, it falls back to the _rewardID on the rod. This allows biome-specific fishing: a lake in the forest yields different fish than a swamp pond, even with the same rod.

The fallback chain is:

  1. Check the specific water volume's catchable table.
  2. If the volume has no table, check the level's default fishing rewards.
  3. If the level has no default, fall back to the rod's _rewardID spawn table.
  4. If _rewardID is zero, nothing is catchable.

Selecting the Mode

The mode is set via the .dat key Fishing_Reward_Mode:

Fishing_Reward_Mode Rod
Fishing_Reward_Mode WaterVolumes

The parse in PopulateAsset defaults to Rod:

csharp
FishingRewardMode = p.data.ParseEnum("Fishing_Reward_Mode", EFishingRewardMode.Rod);

Audio Clips

Three AudioClip fields are loaded from the Unity asset bundle during PopulateAsset:

FieldBundle Asset NameTrigger
_cast"Cast"Line is cast from the rod
_reel"Reel"Reeling in progress (legacy mode) or during catch-challenge active input
_tug"Tug"Fish bites the line; bobber submerges

The load calls are straightforward Unity bundle loads:

csharp
_cast = p.bundle.load<AudioClip>("Cast");
_reel = p.bundle.load<AudioClip>("Reel");
_tug = p.bundle.load<AudioClip>("Tug");

If the bundle does not contain an asset with the expected name, load<T> returns null. The UseableFisher usage class should null-check before playing. The asset validation system does not flag missing audio as an error — rods without audio simply produce no sound at those stages.

Reward Configuration

Spawn Table Reference

The _rewardID field (type ushort) references a SpawnAsset by its ID. This spawn table defines the weighted list of items that can be caught. A value of zero means no items can be caught via the rod's own table (WaterVolumes mode may still provide rewards).

csharp
_rewardID = p.data.ParseUInt16("Reward_ID");

Experience Rewards

Two int fields define the experience range granted per successful catch:

csharp
rewardExperienceMin = p.data.ParseInt32("Reward_Experience_Min", defaultValue: 3);
rewardExperienceMax = p.data.ParseInt32("Reward_Experience_Max", defaultValue: 3);

Both default to 3. At runtime, the actual XP granted is:

csharp
int xp = Random.Range(rewardExperienceMin, rewardExperienceMax + 1);

Setting Reward_Experience_Min higher than Reward_Experience_Max produces a Random.Range call with a reversed range, which Unity clamps — the result is always Reward_Experience_Min.

NPC Quest Rewards

The rewardsList field (type NPCRewardsList, internal visibility) parses quest rewards from the .dat using the keys Quest_Rewards (count) and Quest_Reward_N (individual reward entries):

csharp
rewardsList.Parse(p.data, p.localization, this, "Quest_Rewards", "Quest_Reward_");

Each successful catch grants these quest rewards in addition to the spawn-table item and experience. This is how fishing quests (catch N fish for an NPC) track progress: each catch fires the quest reward logic, incrementing quest conditions.

Catch Challenge: The Minigame Physics System

When EnableCatchChallenge is true, the player must actively participate in a minigame after a fish bites. A cursor moves vertically within a bounded region; the player holds input to push the cursor up and releases to let it fall. A target window moves independently according to per-fish physics. The player must keep the cursor inside the target window to fill a capture progress bar.

Fixed-Point Arithmetic

All physics values use fixed-point integers scaled by FISHING_POINT_SCALE = 10_000. This ensures deterministic client/server simulation — two machines calculating the same initial conditions with the same inputs produce identical results, regardless of floating-point hardware differences.

The fixed-point convention:

  • Position, velocity, acceleration values: scaled by 10,000.
  • Restitution coefficients: scaled by 10,000.
  • Time values for capture/escape: scaled by TOCK_PER_SECOND * TIME_SCALE (where TIME_SCALE is also 10,000), making them tick-count values.

To convert a .dat float value to fixed-point:

csharp
int fixedValue = Mathf.RoundToInt(floatValue * 10_000);

To read back a fixed-point value as a float for display:

csharp
float displayValue = fixedValue / 10_000.0f;

EnableCatchChallenge Flag

Parsed from the .dat key CatchChallenge_Enabled:

csharp
EnableCatchChallenge = p.data.ParseBool("CatchChallenge_Enabled");

When false, the legacy catch path is used: the fish is automatically caught after the bite interval with no player input. This is the default for backwards compatibility.

Cursor Physics Parameters

All parameters use the FishingCatchableProperties.FIXED_POINT_SCALE multiplier. The parsed defaults are:

CursorSize

csharp
CatchChallengeCursorSize = Mathf.RoundToInt(
    p.data.ParseFloat("CatchChallenge_CursorSize", 0.2f) * FIXED_POINT_SCALE
);
// Default: 2,000 fixed-point (0.2 * 10,000)

The size of the target window the cursor must stay within. Smaller = harder.

Gravity

csharp
CatchChallengeGravity = Mathf.RoundToInt(
    Mathf.Abs(p.data.ParseFloat("CatchChallenge_Gravity", 1.0f)) * FIXED_POINT_SCALE
);
// Default: 10,000 (1.0 * 10,000)

Downward acceleration applied when the player is NOT holding the input. The Mathf.Abs call means negative values in the .dat are treated as positive — gravity always pulls down. The direction is handled in the simulation loop, not in the parameter.

Acceleration

csharp
CatchChallengeAcceleration = Mathf.RoundToInt(
    Mathf.Abs(p.data.ParseFloat("CatchChallenge_Acceleration", 1.0f)) * FIXED_POINT_SCALE
);
// Default: 10,000 (1.0 * 10,000)

Upward acceleration applied when the player IS holding the input. Also force-absoluted.

Upper and Lower Restitution

csharp
CatchChallengeUpperRestitution = Mathf.RoundToInt(
    Mathf.Abs(p.data.ParseFloat("CatchChallenge_UpperRestitution", 0.5f)) * FIXED_POINT_SCALE
);
// Default: 5,000 (0.5 * 10,000)

CatchChallengeLowerRestitution = Mathf.RoundToInt(
    Mathf.Abs(p.data.ParseFloat("CatchChallenge_LowerRestitution", 0.5f)) * FIXED_POINT_SCALE
);
// Default: 5,000 (0.5 * 10,000)

Velocity preservation on boundary collision. A value of 5,000 means 50% of velocity is preserved on bounce. Higher restitution makes the cursor bouncier, harder to control. The upper and lower values can differ — a common pattern is higher upper restitution (the cursor bounces hard off the top) and lower lower restitution (the cursor slows quickly at the bottom).

Capture and Escape Speed Multipliers

csharp
CatchChallengeCaptureSpeedMultiplier = p.data.ParseFloat(
    "CatchChallenge_CaptureSpeed", 1.0f
);
CatchChallengeEscapeSpeedMultiplier = p.data.ParseFloat(
    "CatchChallenge_EscapeSpeed", 1.0f
);

These are floats, not fixed-point — they multiply the per-fish captureTicks and escapeTicks values. A multiplier of 2.0 doubles the capture speed (halves the time needed). These are rod-level modifiers, distinct from the per-fish captureTicks/escapeTicks.

Animation Validation

When EnableCatchChallenge is true and asset validation is enabled (Assets.shouldValidateAssets), the asset checks that the equipped prefab has two required animations:

csharp
if (EnableCatchChallenge && Assets.shouldValidateAssets)
{
    ValidateEquipableHasAnimation("Catch_Loop");
    ValidateEquipableHasAnimation("Catch_Failure");
}
  • Catch_Loop: Plays while the challenge minigame is active — usually a struggle/reeling animation on the character.
  • Catch_Failure: Plays when the fish escapes — usually a snap-back or stumble animation.

If either animation is missing, ValidateEquipableHasAnimation logs an error during asset load. This is a development-time check; it does not prevent the asset from loading, but the warning is surfaced in the Unity Editor console.

Note that there is no Catch_Success animation validation — the success state typically transitions to the standard item-receive animation rather than a dedicated catch celebration.

Description UI (BuildDescription)

The BuildDescription override adds modifier lines to the item tooltip when values differ from their defaults:

csharp
public override void BuildDescription(ItemDescriptionBuilder builder, Item itemInstance)
{
    base.BuildDescription(builder, itemInstance);

    if (!builder.HasFlag(EItemDescriptionFlags.Uncategorized))
        return;

    if (FishBiteIntervalMultiplier != 1.0f)
        builder.Append(..., DescSort_Important + DescSort_LowerIsBeneficial(...));

    if (CatchChallengeCaptureSpeedMultiplier != 1.0f)
        builder.Append(..., DescSort_Important + DescSort_HigherIsBeneficial(...));

    if (CatchChallengeEscapeSpeedMultiplier != 1.0f)
        builder.Append(..., DescSort_Important + DescSort_LowerIsBeneficial(...));
}

Three rules:

  1. Only show a modifier line when the value differs from 1.0 (the neutral default).
  2. Use DescSort_LowerIsBeneficial for values where lower is better (bite interval, escape speed).
  3. Use DescSort_HigherIsBeneficial for values where higher is better (capture speed).

The DescSort_Important constant ensures these lines sort near the top of the tooltip, before less-critical item properties.

Lines appear only when the EItemDescriptionFlags.Uncategorized flag is set — this is the standard tooltip section for item-specific stats.

FishingCatchableProperties

FishingCatchableProperties is a separate class (not an Asset) that defines per-catchable-item physics behavior. Each fish species can override these properties to create distinct catch difficulties and movement patterns.

Fixed-Point Scale Constants

csharp
public const int FIXED_POINT_SCALE = 10_000;
public const int TIME_SCALE = 10_000;
  • FIXED_POINT_SCALE scales position/velocity/acceleration values.
  • TIME_SCALE scales time values (used with PlayerInput.TOCK_PER_SECOND to convert seconds to tick counts).

Property Table

PropertyType.dat KeyFloat DefaultDescription
minChangeTargetTicksintMin_Relocate_Interval1.5sMin ticks before target window repositions
maxChangeTargetTicksintMax_Relocate_Interval2.0sMax ticks before target window repositions
maxUpwardAccelerationintMax_Upward_Acceleration1.5Max upward accel for target movement
maxDownwardAccelerationintMax_Downward_Acceleration1.2Max downward accel for target movement
maxUpwardSpeedintMax_Upward_Speed0.6Max upward speed limit
maxDownwardSpeedintMax_Downward_Speed0.45Max downward speed limit
upperRestitutionintUpper_Restitution0.6Bounce preservation at top bound
lowerRestitutionintLower_Restitution0.4Bounce preservation at bottom bound
minTargetDeltaintMin_Target_Delta0.3Min distance when repositioning target
maxTargetDeltaintMax_Target_Delta0.4Max distance when repositioning target
minTargetPositionintMin_Target_Position0.1Lower bound of target window position
maxTargetPositionintMax_Target_Position0.9Upper bound of target window position
captureTicksintCapture_Duration2.0sTicks to fill capture bar while cursor in window
escapeTicksintEscape_Duration2.0sTicks to fill escape bar while cursor outside window
springStiffnessintSpring_Stiffness16.0Spring stiffness for target movement interpolation
springDampingintSpring_Damping4.0Spring damping for target movement interpolation

Parse Method

Each property reads with data.ParseFloat and converts:

csharp
public void Parse(IDatDictionary data)
{
    minChangeTargetTicks = Mathf.RoundToInt(
        data.ParseFloat("Min_Relocate_Interval", DEFAULT_MIN_CHANGE_TARGET_INTERVAL)
        * PlayerInput.TOCK_PER_SECOND
    );
    // Time values use TOCK_PER_SECOND only (not FIXED_POINT_SCALE)

    maxUpwardAcceleration = Mathf.RoundToInt(
        data.ParseFloat("Max_Upward_Acceleration", DEFAULT_MAX_UPWARD_ACCELERATION)
        * FIXED_POINT_SCALE
    );
    // Physics values use FIXED_POINT_SCALE

    captureTicks = Mathf.RoundToInt(
        data.ParseFloat("Capture_Duration", DEFAULT_CAPTURE_DURATION)
        * PlayerInput.TOCK_PER_SECOND * TIME_SCALE
    );
    // Capture/escape durations use BOTH TOCK_PER_SECOND and TIME_SCALE
}

The distinction matters:

  • Time intervals (minChangeTargetTicks, maxChangeTargetTicks): multiplied by TOCK_PER_SECOND only. These are used in a tick-counter loop comparing against the current tick.
  • Physics values (maxUpwardAcceleration, maxUpwardSpeed, restitutions, deltas, positions, spring constants): multiplied by FIXED_POINT_SCALE only. These are used in the spring-physics simulation that advances per tick.
  • Durations (captureTicks, escapeTicks): multiplied by TOCK_PER_SECOND * TIME_SCALE. The extra TIME_SCALE factor allows rod-level multipliers to scale the effective duration at a finer granularity than whole-tick adjustments.

Static Default

csharp
public static FishingCatchableProperties Default = new FishingCatchableProperties()
{
    minChangeTargetTicks = Mathf.RoundToInt(DEFAULT_MIN_CHANGE_TARGET_INTERVAL * PlayerInput.TOCK_PER_SECOND),
    maxChangeTargetTicks = Mathf.RoundToInt(DEFAULT_MAX_CHANGE_TARGET_INTERVAL * PlayerInput.TOCK_PER_SECOND),
    maxUpwardAcceleration = Mathf.RoundToInt(DEFAULT_MAX_UPWARD_ACCELERATION * FIXED_POINT_SCALE),
    // ... all properties initialized with their float defaults converted
};

The Default static instance provides fallback values for fish that don't have custom FishingCatchableProperties configured. Any water volume or fishing system that doesn't find a specific catchable properties set uses Default.

Spring Physics Model

The target window's movement uses a spring-physics simulation with the following rules per tick:

  1. Acceleration: A random acceleration is selected within the bounds [-maxDownwardAcceleration, +maxUpwardAcceleration]. The sign convention: positive = upward, negative = downward.
  2. Speed clamping: After applying acceleration, the velocity is clamped to [-maxDownwardSpeed, +maxUpwardSpeed].
  3. Position update: position += velocity * dt.
  4. Boundary collision: If position exceeds maxTargetPosition or falls below minTargetPosition, the velocity is reflected and multiplied by the appropriate restitution coefficient, and the position is clamped to the boundary.
  5. Spring force: A spring force pulls the target toward a randomly chosen equilibrium point: force = -springStiffness * (position - equilibrium) - springDamping * velocity. This creates smooth oscillation rather than random walk.
  6. Relocation: After a random interval in [minChangeTargetTicks, maxChangeTargetTicks], a new equilibrium point is chosen within [minTargetPosition, maxTargetPosition], at least minTargetDelta away from the current position and at most maxTargetDelta away.

The net effect is a target window that drifts and bounces within the vertical space with spring-damped smoothness, occasionally relocating to a new area. Different fish species configure different parameters: a fast fish has high acceleration and speed limits, a bouncy fish has high restitution, and a twitchy fish has frequent relocations (low minChangeTargetTicks).

Fishing Simulation Phases

The runtime fishing simulation (UseableFisher) operates in distinct phases:

Phase 1: Cast

  1. The player presses the use key with the rod equipped and facing water.
  2. The cast animation plays (from the equipped prefab's Animator).
  3. A line projectile is launched with a trajectory defined by the player's aim direction and a configurable cast distance.
  4. The _cast AudioClip plays.
  5. A bobber GameObject is instantiated at the landing point.
  6. The player transitions to the "waiting" state — they can look around but not move.

Phase 2: Bite Wait

  1. A timer starts with duration: baseBiteInterval * FishBiteIntervalMultiplier.
  2. The base bite interval is defined per water volume (or defaults to a game-mode config value if the volume doesn't specify one).
  3. During the wait, the bobber floats with a gentle bobbing animation.
  4. When the timer expires, a bite occurs:
    • The bobber submerges (animation or hide).
    • The _tug AudioClip plays.
    • A UI prompt appears (legacy: "reel in"; challenge: "hold to fight").

Phase 3a: Legacy Catch (EnableCatchChallenge = false)

  1. After the bite, a brief reeling animation plays.
  2. The _reel AudioClip plays.
  3. The reward is resolved: SpawnTableTool.Resolve(rewardID) selects a random item from the rod's spawn table.
  4. Experience is granted: Random.Range(rewardExperienceMin, rewardExperienceMax + 1).
  5. The rewardsList quest rewards are granted (advancing any active fishing quests).
  6. The caught item is force-added to the player's inventory via ItemTool.tryForceGiveItem.
  7. If inventory is full, the item drops at the player's feet.

Phase 3b: Challenge Catch (EnableCatchChallenge = true)

  1. A UI overlay appears showing a vertical bar with the cursor and target window.
  2. The cursor position initializes at the center of the bar.
  3. Each frame (every tick on the server):
    • Player input: If held, apply CatchChallengeAcceleration upward. If released, apply CatchChallengeGravity downward.
    • Velocity update: velocity += acceleration * dt (where acceleration is positive for input held, negative for gravity).
    • Boundary check: If position <= minPosition: position = minPosition, velocity *= -CatchChallengeLowerRestitution / FIXED_POINT_SCALE. If position >= maxPosition: position = maxPosition, velocity *= -CatchChallengeUpperRestitution / FIXED_POINT_SCALE.
    • Position update: position += velocity * dt.
    • Target movement: The target window updates its position using the spring-physics model defined by FishingCatchableProperties.
    • Capture/escape check: If the cursor is within CursorSize / 2 of the target center, captureProgress += dt * CatchChallengeCaptureSpeedMultiplier. Otherwise, escapeProgress += dt * CatchChallengeEscapeSpeedMultiplier.
    • Resolution: If captureProgress >= captureTicks: catch success! Proceed to reward distribution. If escapeProgress >= escapeTicks: the fish escapes.
  4. On success: reward distribution as in legacy mode.
  5. On failure: the Catch_Failure animation plays, no reward is granted, and the rod is ready for another cast.

Phase 4: Recast

After success or failure, the rod returns to the ready state. The player can cast again immediately (no cooldown by default; mods may add one).

Performance Considerations

Fixed-Point Math Overhead

Every tick of the catch-challenge performs integer arithmetic on the cursor and target positions. The fixed-point scale of 10,000 means position and velocity values are in the range [0, 10000]. Multiplication of two fixed-point values produces a result in the range [0, 100_000_000] which fits comfortably in a 32-bit int (max ~2.1 billion). The simulation avoids overflow even with extreme parameter values.

Per-Tick Processing

The catch-challenge runs on both client and server. The server is authoritative for capture/escape progress to prevent cheating. The client runs the same simulation in parallel for visual prediction (no latency on cursor movement), but the server's decision is final for the catch/escape resolution. Discrepancies between client and server simulation are possible if the client's tick rate differs from the server's, but the fixed-point arithmetic minimizes drift.

SpawnTableTool.Resolve

Each successful catch calls SpawnTableTool.Resolve(rewardID), which performs a weighted random selection from the spawn table. For tables with hundreds of entries, this is O(n) per call. Fishing rods with large spawn tables should ensure the entries are sorted by weight descending to allow early-out optimization if SpawnTableTool implements one.

Validation and Error States

Missing Animations

When EnableCatchChallenge is true and asset validation is enabled, ValidateEquipableHasAnimation checks the equipped prefab for Catch_Loop and Catch_Failure. If missing, an error is logged. The rod still loads but the catch-challenge will fail to play animations at runtime.

Zero Reward ID

If _rewardID is zero (the default for unset), SpawnTableTool.Resolve(0) typically returns null or a default result. The rod catches nothing. This is valid for rods intended only for quest rewards (where rewardsList handles all rewards) or for pure-sport fishing with no item gain.

Negative Experience

Setting Reward_Experience_Min higher than Reward_Experience_Max causes Random.Range to receive (high, low + 1). Unity's Random.Range(int, int) expects (minInclusive, maxExclusive) and clamps so max >= min. The result is always the min value. This is a common modder mistake.

Missing Audio

If the Unity bundle doesn't contain Cast, Reel, or Tug audio clips, the respective AudioClip fields are null. UseableFisher should null-check before calling Play(). Most implementations use AudioSource.PlayOneShot(clip) which no-ops on null.

Cargo Data Export

ItemFisherAsset does not define a custom BuildCargoData override. It inherits the base ItemAsset cargo export, which writes the shared item fields (GUID, ID, rarity, size, etc.) to the generic item cargo table. No fishing-specific fields are exported to Cargo tables. The catch challenge parameters, audio references, and reward configuration are not part of the public game data wiki tables.

Modding Guide

Creating a Fishing Rod

Minimum .dat for a functional legacy rod:

ID 58000
Rarity Common
Size_X 2
Size_Y 1
Slot Primary
Reward_ID 120
Reward_Experience_Min 5
Reward_Experience_Max 10

This creates a 2×1 common rod that catches items from spawn table 120 and grants 5-10 XP per catch.

Adding Catch Challenge

CatchChallenge_Enabled true
CatchChallenge_CursorSize 0.15
CatchChallenge_Gravity 1.2
CatchChallenge_Acceleration 1.3
CatchChallenge_UpperRestitution 0.4
CatchChallenge_LowerRestitution 0.3
CatchChallenge_CaptureSpeed 0.8
CatchChallenge_EscapeSpeed 1.2

This makes a harder rod: smaller cursor, stronger gravity, weaker acceleration, lower bounciness, slower capture, and faster escape. The equipped prefab MUST have Catch_Loop and Catch_Failure animation states.

Biome-Specific Fishing with WaterVolumes

Fishing_Reward_Mode WaterVolumes
Reward_ID 121

The rod defaults to spawn table 121 if the water volume has no custom rewards. Each water volume's .dat can specify its own Fisher_Reward_ID to override.

Custom Catchable Properties

A water volume's .dat can define per-fish catchable properties using the FishingCatchableProperties keys:

Fish_1_ID 81
Fish_1_Min_Relocate_Interval 0.8
Fish_1_Max_Relocate_Interval 1.2
Fish_1_Max_Upward_Acceleration 2.0
Fish_1_Max_Downward_Acceleration 1.5
Fish_1_Max_Upward_Speed 0.8
Fish_1_Max_Downward_Speed 0.6
Fish_1_Upper_Restitution 0.7
Fish_1_Lower_Restitution 0.5
Fish_1_Min_Target_Delta 0.2
Fish_1_Max_Target_Delta 0.5
Fish_1_Min_Target_Position 0.05
Fish_1_Max_Target_Position 0.95
Fish_1_Capture_Duration 3.0
Fish_1_Escape_Duration 1.5
Fish_1_Spring_Stiffness 20.0
Fish_1_Spring_Damping 5.0

This defines a fast, bouncy, aggressive fish (ID 81) with 3-second capture requirement and 1.5-second escape window, active across the full vertical bar range.

Common Pitfalls

  1. Forgetting animations: If EnableCatchChallenge is true and the prefab has no Catch_Loop/Catch_Failure, the editor logs errors. At runtime, the minigame still functions but the character stays in idle pose.

  2. One Reward_Experience_Max but not Min: The default for Min is 3 regardless of Max. If Max is set to 50 and Min is left unset, the range is 3-50, not 50-50.

  3. Fixed-point display mismatch: When debugging, remember to divide by 10,000. A CatchChallengeCursorSize of 2000 is 0.2 in float space.

  4. WaterVolume mode with no fallback: If a volume has no rewards and the rod's Reward_ID is 0, nothing can be caught from that volume. Always set a rod-level Reward_ID as fallback when using WaterVolumes mode.

  5. Quest rewards without active quests: The rewardsList grant is unconditional. If no quest references the reward, the grant is silently ignored. This is by design — it allows rods to grant quest credit without coupling to specific quests.