netfishing/fish/fish_selector.gd

162 lines
5.6 KiB
GDScript3
Raw Permalink Normal View History

class_name FishSelector
extends RefCounted
const CollectionLogType = preload("res://collection/collection_log.gd")
const FishCatchType = preload("res://fish/fish_catch.gd")
const FishDataType = preload("res://fish/fish_data.gd")
const FishPoolType = preload("res://fish/fish_pool.gd")
2026-08-02 17:44:11 -04:00
const FishQualityType = preload("res://fish/fish_quality.gd")
const FishingContextType = preload("res://fishing/fishing_context.gd")
2026-08-14 16:31:28 -04:00
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
var undiscovered_weight_multiplier: float = 1.5
var rarity_weight_multipliers: Array[float] = []
2026-08-02 17:44:11 -04:00
var quality_weight_multipliers: Array[float] = []
2026-08-14 16:31:28 -04:00
var active_rod: FishingRodDataType
var use_deterministic_test_seed: bool = false
var deterministic_test_seed: int = 24680
var selection_seed: int = 0
var _active_bait_tags: Array[StringName] = []
var _rng := RandomNumberGenerator.new()
func begin_roll() -> void:
if use_deterministic_test_seed:
selection_seed = deterministic_test_seed
_rng.seed = deterministic_test_seed
else:
_rng.randomize()
selection_seed = _rng.seed
func select_fish(
pool: FishPoolType,
context: FishingContextType,
collection_log: CollectionLogType,
) -> FishDataType:
if pool == null or context == null or collection_log == null:
return null
_active_bait_tags = context.active_bait_tags.duplicate()
var all_fish: Array[FishDataType] = []
var all_weights: Array[float] = []
var rarity_fish: Array[FishDataType] = []
var rarity_weights: Array[float] = []
var selected_rarity: int = _roll_rarity(context)
var weights: Array[float] = []
var total_weight: float = 0.0
for fish: FishDataType in pool.candidates:
if fish == null or not fish.is_selectable():
continue
if not fish.is_allowed_in_water(context.water_type):
continue
if fish.availability != null and not fish.availability.is_available(context):
continue
var final_weight: float = fish.base_catch_weight
# Species-specific preferred bait is deprecated. Bait now controls
# quality and rarity bands globally; required tags remain authoritative.
if not collection_log.has_discovered(fish.id):
final_weight *= maxf(undiscovered_weight_multiplier, 0.0)
2026-08-14 16:31:28 -04:00
if active_rod != null:
final_weight *= maxf(
active_rod.undiscovered_weight_multiplier,
0.0,
)
if fish.rarity >= 0 and fish.rarity < rarity_weight_multipliers.size():
final_weight *= maxf(rarity_weight_multipliers[fish.rarity], 0.0)
2026-08-14 16:31:28 -04:00
if active_rod != null:
final_weight *= active_rod.get_rarity_multiplier(fish.rarity)
final_weight *= active_rod.get_affinity_multiplier(fish, context)
if final_weight <= 0.0:
continue
all_fish.append(fish)
all_weights.append(final_weight)
if int(fish.rarity) == selected_rarity:
rarity_fish.append(fish)
rarity_weights.append(final_weight)
var eligible_fish: Array[FishDataType] = rarity_fish if not rarity_fish.is_empty() else all_fish
weights = rarity_weights if not rarity_weights.is_empty() else all_weights
for weight: float in weights:
total_weight += weight
if eligible_fish.is_empty() or total_weight <= 0.0:
return null
var roll: float = _rng.randf() * total_weight
var accumulated_weight: float = 0.0
for index: int in range(eligible_fish.size()):
accumulated_weight += weights[index]
if roll <= accumulated_weight:
return eligible_fish[index]
return eligible_fish.back()
func _roll_rarity(context: FishingContextType) -> int:
var weights: Array[float] = FishQualityType.rarity_weights_for_bait(
context.active_bait_tags
)
for index: int in range(weights.size()):
if index < rarity_weight_multipliers.size():
weights[index] *= maxf(rarity_weight_multipliers[index], 0.0)
2026-08-14 16:31:28 -04:00
if active_rod != null:
weights[index] *= active_rod.get_rarity_multiplier(index)
var total: float = 0.0
for weight: float in weights:
total += weight
if total <= 0.0:
return FishDataType.Rarity.COMMON
var roll: float = _rng.randf() * total
for index: int in range(weights.size()):
roll -= weights[index]
if roll <= 0.0:
return index
return FishDataType.Rarity.LEGENDARY
func create_catch(fish: FishDataType, bait_tags: Array[StringName] = []) -> FishCatchType:
if fish == null or not fish.is_selectable():
return null
var caught_fish := FishCatchType.new()
caught_fish.fish = fish
caught_fish.fish_id = fish.id
2026-07-25 16:02:34 -04:00
caught_fish.ensure_identity()
2026-08-14 16:31:28 -04:00
var weight_roll: float = _biased_unit_roll(
active_rod.weight_roll_bias if active_rod != null else 0.0
)
caught_fish.weight_lb = lerpf(
fish.get_minimum_weight(),
2026-08-14 16:31:28 -04:00
fish.get_maximum_weight(),
weight_roll,
)
caught_fish.display_scale = fish.get_display_scale_for_weight(
caught_fish.weight_lb
)
var effective_bait: Array[StringName] = bait_tags if not bait_tags.is_empty() else _active_bait_tags
var quality_multipliers: Array[float] = FishQualityType.roll_weights_for_bait(effective_bait)
for quality: int in range(mini(quality_multipliers.size(), quality_weight_multipliers.size())):
quality_multipliers[quality] *= maxf(quality_weight_multipliers[quality], 0.0)
2026-08-14 16:31:28 -04:00
for quality: int in range(quality_multipliers.size()):
if active_rod != null:
quality_multipliers[quality] *= active_rod.get_quality_multiplier(
quality
)
2026-08-02 17:44:11 -04:00
caught_fish.quality = FishQualityType.roll(
_rng,
quality_multipliers,
2026-08-02 17:44:11 -04:00
)
caught_fish.sale_value = FishQualityType.apply_sale_value(
fish.get_sale_value_for_weight(caught_fish.weight_lb),
caught_fish.quality,
)
return caught_fish
2026-08-14 16:31:28 -04:00
func _biased_unit_roll(bias: float) -> float:
var roll: float = _rng.randf()
var safe_bias: float = clampf(bias, -0.5, 0.5)
if is_zero_approx(safe_bias):
return roll
var exponent: float = 1.0 / (1.0 + absf(safe_bias) * 2.0)
if safe_bias > 0.0:
return pow(roll, exponent)
return 1.0 - pow(1.0 - roll, exponent)