diff --git a/fish/fish_quality.gd b/fish/fish_quality.gd index 6ae4031..61534e0 100644 --- a/fish/fish_quality.gd +++ b/fish/fish_quality.gd @@ -23,16 +23,11 @@ const MINNOW_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0] const SARDINE_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0] const LUMINOUS_ROE_ROLL_WEIGHTS: Array[float] = [16.0, 26.0, 26.0, 20.0, 12.0] const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0] -# Legacy profile multiplier retained for serialized/profile compatibility. -# New encounters use the weighted quality/rarity/weight bands below. +# Legacy multiplier helpers remain available for compatibility. New encounters +# roll directly within the authored quality bands below. const BARRIER_HEALTH_MULTIPLIERS: Array[float] = [1.0, 1.25, 1.6, 2.2, 3.25] -# Barrier health is weighted 70% by quality, 20% by rarity, and 10% by the -# catch's position in its authored weight range. The upper bounds define the -# intended per-barrier challenge bands; a small seeded variance keeps barriers -# from feeling identical without crossing those bands. -const BARRIER_QUALITY_WEIGHT: float = 0.70 -const BARRIER_RARITY_WEIGHT: float = 0.20 -const BARRIER_WEIGHT_WEIGHT: float = 0.10 +# Inclusive per-barrier health bands authored for each quality tier. Encounter +# generation rolls uniformly across the selected tier's complete band. const BARRIER_HEALTH_MINIMUMS: Array[int] = [1, 10, 50, 100, 200] const BARRIER_HEALTH_MAXIMUMS: Array[int] = [9, 50, 100, 200, 400] const DISPLAY_NAMES: PackedStringArray = [ @@ -89,32 +84,26 @@ static func apply_barrier_health(base_health: int, quality: int) -> int: ) -static func barrier_health_for_catch( - quality: int, - rarity: int, - weight_percentile: float, - variance: float = 1.0, -) -> int: +static func barrier_health_range(quality: int) -> Vector2i: var safe_quality: int = ( quality if is_valid(quality) else Tier.BORING ) - var safe_rarity: float = clampf(float(rarity) / 4.0, 0.0, 1.0) - var safe_weight: float = clampf(weight_percentile, 0.0, 1.0) - var weighted_health: float = float(BARRIER_HEALTH_MAXIMUMS[safe_quality]) * ( - BARRIER_QUALITY_WEIGHT - + BARRIER_RARITY_WEIGHT * safe_rarity - + BARRIER_WEIGHT_WEIGHT * safe_weight - ) - var varied_health: int = roundi( - weighted_health * clampf(variance, 0.8, 1.2) - ) - return clampi( - varied_health, + return Vector2i( BARRIER_HEALTH_MINIMUMS[safe_quality], BARRIER_HEALTH_MAXIMUMS[safe_quality], ) +static func roll_barrier_health( + rng: RandomNumberGenerator, + quality: int, +) -> int: + var health_range: Vector2i = barrier_health_range(quality) + if rng == null: + return health_range.x + return rng.randi_range(health_range.x, health_range.y) + + static func roll( rng: RandomNumberGenerator, weight_multipliers: Array[float] = [], diff --git a/fishing/bass_catch_profile.tres b/fishing/bass_catch_profile.tres index 6385eec..24024a7 100644 --- a/fishing/bass_catch_profile.tres +++ b/fishing/bass_catch_profile.tres @@ -6,8 +6,6 @@ script = ExtResource("1_profile") barrier_count_min = 3 barrier_count_max = 4 -barrier_health_min = 4 -barrier_health_max = 7 first_barrier_margin = 0.14 final_barrier_margin = 0.1 minimum_barrier_spacing = 0.14 diff --git a/fishing/carp_catch_profile.tres b/fishing/carp_catch_profile.tres index 94a8470..e1e0efe 100644 --- a/fishing/carp_catch_profile.tres +++ b/fishing/carp_catch_profile.tres @@ -6,8 +6,6 @@ script = ExtResource("1_profile") barrier_count_min = 3 barrier_count_max = 4 -barrier_health_min = 5 -barrier_health_max = 8 first_barrier_margin = 0.13 final_barrier_margin = 0.09 minimum_barrier_spacing = 0.14 diff --git a/fishing/catch_controller.gd b/fishing/catch_controller.gd index 62769aa..7e75370 100644 --- a/fishing/catch_controller.gd +++ b/fishing/catch_controller.gd @@ -69,8 +69,6 @@ var _reel_input_held: bool = false var _reel_speed: float = 0.0 var _click_power: int = 1 var _fish_quality: int = FishQualityType.Tier.BORING -var _fish_rarity: int = 0 -var _fish_weight_percentile: float = 0.0 var _chase_delay_remaining: float = 0.0 var _failure_epsilon: float = 0.0001 var _auto_click_accumulator: float = 0.0 @@ -113,8 +111,6 @@ func start_encounter( reel_speed: float, click_power: int, fish_quality: int = FishQualityType.Tier.BORING, - fish_rarity: int = 0, - fish_weight_percentile: float = 0.0, ) -> void: reset() if profile == null: @@ -128,8 +124,6 @@ func start_encounter( if FishQualityType.is_valid(fish_quality) else FishQualityType.Tier.BORING ) - _fish_rarity = clampi(fish_rarity, 0, 4) - _fish_weight_percentile = clampf(fish_weight_percentile, 0.0, 1.0) _chase_delay_remaining = CHASE_START_DELAY chase_progress = -CHASE_START_OFFSET _seed_encounter_rng() @@ -145,8 +139,6 @@ func start_authoritative_encounter( click_power: int, encounter_seed_value: int, fish_quality: int = FishQualityType.Tier.BORING, - fish_rarity: int = 0, - fish_weight_percentile: float = 0.0, ) -> void: var previous_test_mode: bool = use_deterministic_test_seed var previous_seed: int = deterministic_test_seed @@ -157,8 +149,6 @@ func start_authoritative_encounter( reel_speed, click_power, fish_quality, - fish_rarity, - fish_weight_percentile, ) use_deterministic_test_seed = previous_test_mode deterministic_test_seed = previous_seed @@ -327,11 +317,8 @@ func _generate_barriers(profile: CatchDifficultyProfileType) -> void: + spacing * float(barrier_index) + random_offsets[barrier_index] * random_slack ) - var health: int = FishQualityType.barrier_health_for_catch( - _fish_quality, - _fish_rarity, - _fish_weight_percentile, - _rng.randf_range(0.9, 1.1), + var health: int = FishQualityType.roll_barrier_health( + _rng, _fish_quality ) _barriers.append(Barrier.new(position, health)) diff --git a/fishing/catch_difficulty_profile.gd b/fishing/catch_difficulty_profile.gd index bd2508c..60aa190 100644 --- a/fishing/catch_difficulty_profile.gd +++ b/fishing/catch_difficulty_profile.gd @@ -3,8 +3,6 @@ extends Resource @export_range(0, 12, 1) var barrier_count_min: int = 2 @export_range(0, 12, 1) var barrier_count_max: int = 4 -@export_range(1, 100, 1) var barrier_health_min: int = 3 -@export_range(1, 100, 1) var barrier_health_max: int = 7 @export_range(0.0, 0.45, 0.01) var first_barrier_margin: float = 0.15 @export_range(0.0, 0.45, 0.01) var final_barrier_margin: float = 0.10 @export_range(0.01, 1.0, 0.01) var minimum_barrier_spacing: float = 0.15 @@ -15,11 +13,6 @@ func get_barrier_count_range() -> Vector2i: return Vector2i(minimum, maxi(minimum, barrier_count_max)) -func get_barrier_health_range() -> Vector2i: - var minimum: int = maxi(1, barrier_health_min) - return Vector2i(minimum, maxi(minimum, barrier_health_max)) - - func get_generation_interval() -> Vector2: var start: float = clampf(first_barrier_margin, 0.0, 0.95) var finish: float = clampf(1.0 - final_barrier_margin, start, 1.0) diff --git a/fishing/common_catch_profile.tres b/fishing/common_catch_profile.tres index 4af3284..86c2b74 100644 --- a/fishing/common_catch_profile.tres +++ b/fishing/common_catch_profile.tres @@ -6,8 +6,6 @@ script = ExtResource("1_profile") barrier_count_min = 2 barrier_count_max = 4 -barrier_health_min = 3 -barrier_health_max = 7 first_barrier_margin = 0.15 final_barrier_margin = 0.1 minimum_barrier_spacing = 0.15 diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index e0db860..243a069 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -1045,8 +1045,6 @@ func _activate_bite(confirmation_override: bool = false) -> void: _get_effective_reel_speed(), _get_effective_barrier_damage(), _pending_catch.quality, - int(_pending_catch.fish.rarity), - _pending_catch.fish.get_weight_percentile(_pending_catch.weight_lb), ) _catch_controller.set_reel_input( Input.is_action_pressed("fish_primary") diff --git a/network/network_fishing_service.gd b/network/network_fishing_service.gd index 7303f12..a5a2154 100644 --- a/network/network_fishing_service.gd +++ b/network/network_fishing_service.gd @@ -548,8 +548,6 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void: attempt.barrier_damage, attempt.encounter_seed, fish_catch.quality, - int(fish.rarity), - fish.get_weight_percentile(fish_catch.weight_lb), ) var data: Dictionary = { "attempt_id": attempt.attempt_id, diff --git a/tests/fish_quality_validation.gd b/tests/fish_quality_validation.gd index 366128f..72ebecc 100644 --- a/tests/fish_quality_validation.gd +++ b/tests/fish_quality_validation.gd @@ -83,40 +83,68 @@ func _validate_barrier_challenge_curve() -> void: var expected_maximums: Array[int] = [9, 50, 100, 200, 400] var previous_health: int = 0 for quality: int in FishQualityType.TIER_COUNT: + var expected_range := Vector2i( + expected_minimums[quality], expected_maximums[quality] + ) + assert( + FishQualityType.barrier_health_range(quality) + == expected_range + ) + assert( + FishQualityType.roll_barrier_health(null, quality) + == expected_range.x + ) + var rng := RandomNumberGenerator.new() + rng.seed = 73013 + quality + var observed_counts: Dictionary[int, int] = {} + var observed_minimum: int = expected_range.y + var observed_maximum: int = expected_range.x + var observed_total: int = 0 + var band_size: int = expected_range.y - expected_range.x + 1 + var sample_count: int = maxi(10000, band_size * 250) + for _sample: int in sample_count: + var rolled_health: int = FishQualityType.roll_barrier_health( + rng, quality + ) + assert( + rolled_health >= expected_range.x + and rolled_health <= expected_range.y + ) + observed_counts[rolled_health] = ( + int(observed_counts.get(rolled_health, 0)) + 1 + ) + observed_minimum = mini(observed_minimum, rolled_health) + observed_maximum = maxi(observed_maximum, rolled_health) + observed_total += rolled_health + assert(observed_minimum == expected_range.x) + assert(observed_maximum == expected_range.y) + for authored_health: int in range( + expected_range.x, expected_range.y + 1 + ): + assert(observed_counts.has(authored_health)) + var observed_average: float = ( + float(observed_total) / float(sample_count) + ) + var authored_midpoint: float = ( + float(expected_range.x + expected_range.y) / 2.0 + ) + assert( + absf(observed_average - authored_midpoint) + < maxf(0.15, float(band_size) * 0.01) + ) var health: int = FishQualityType.apply_barrier_health(8, quality) assert(health > previous_health) previous_health = health assert(FishQualityType.apply_barrier_health(8, -1) == 8) assert(FishQualityType.apply_barrier_health(0, FishQualityType.Tier.SHINY) == 4) - var impressive_low_input: int = FishQualityType.barrier_health_for_catch( - FishQualityType.Tier.IMPRESSIVE, - 0, - 0.0, - 1.0, - ) - var impressive_high_input: int = FishQualityType.barrier_health_for_catch( - FishQualityType.Tier.IMPRESSIVE, - 4, - 1.0, - 1.0, - ) - assert(impressive_low_input >= 50 and impressive_low_input <= 100) - assert(impressive_high_input > impressive_low_input) - assert(impressive_high_input <= 100) assert( - FishQualityType.barrier_health_for_catch( - FishQualityType.Tier.BORING, - 4, - 1.0, - 1.0, - ) <= 9 + FishQualityType.barrier_health_range(-1) + == Vector2i(1, 9) ) var profile := CatchDifficultyProfile.new() profile.barrier_count_min = 1 profile.barrier_count_max = 1 - profile.barrier_health_min = 8 - profile.barrier_health_max = 8 profile.first_barrier_margin = 0.2 profile.final_barrier_margin = 0.2 profile.minimum_barrier_spacing = 0.1 @@ -141,12 +169,9 @@ func _validate_barrier_challenge_curve() -> void: assert(health <= expected_maximums[quality]) controller.queue_free() - var shiny_health: int = FishQualityType.barrier_health_for_catch( - FishQualityType.Tier.SHINY, - 4, - 1.0, - 1.0, - ) + var shiny_health: int = FishQualityType.barrier_health_range( + FishQualityType.Tier.SHINY + ).y var base_power_clicks: int = ceili(float(shiny_health) / 1.0) var max_power_clicks: int = ceili( float(shiny_health) diff --git a/tests/fishing_authority_validation.gd b/tests/fishing_authority_validation.gd index 52ab878..20f02e1 100644 --- a/tests/fishing_authority_validation.gd +++ b/tests/fishing_authority_validation.gd @@ -104,7 +104,7 @@ func _run() -> void: assert(not bobber.visible) # A private host rolls and retains the authoritative catch before the fight - # so its quality can scale the same barriers clients receive in snapshots. + # so its quality selects the barrier band clients receive in snapshots. fishing_spot.call("_begin_aiming", player) fishing_spot.set("_cast_charge", 0.32) fishing_spot.call("_update_cast_charge", 0.0) @@ -138,8 +138,6 @@ func _run() -> void: attempt.barrier_damage, attempt.encounter_seed, fish_catch.quality, - int(fish.rarity), - fish.get_weight_percentile(fish_catch.weight_lb), ) var quality_barriers: Array = attempt.controller.get("_barriers") var reference_barriers: Array = reference_controller.get("_barriers")