Make fishing challenge scale with catch quality
This commit is contained in:
parent
c806e7ca6f
commit
46df74361a
10 changed files with 128 additions and 32 deletions
|
|
@ -16,6 +16,10 @@ const ALL_TIERS_MASK: int = (1 << TIER_COUNT) - 1
|
|||
# multipliers later without changing catch serialization or tier identity.
|
||||
const BASE_ROLL_WEIGHTS: Array[float] = [40.0, 32.0, 18.0, 8.0, 2.0]
|
||||
const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0]
|
||||
# Provisional challenge curve. Fish profiles continue to own their baseline
|
||||
# barrier health; quality scales that authored baseline before player upgrades
|
||||
# apply damage. This keeps future rods, bait, and lures on one shared seam.
|
||||
const BARRIER_HEALTH_MULTIPLIERS: Array[float] = [1.0, 1.25, 1.6, 2.2, 3.25]
|
||||
const DISPLAY_NAMES: PackedStringArray = [
|
||||
"boring",
|
||||
"average",
|
||||
|
|
@ -52,6 +56,24 @@ static func apply_sale_value(base_value: int, quality: int) -> int:
|
|||
)
|
||||
|
||||
|
||||
static func barrier_health_multiplier(quality: int) -> float:
|
||||
return (
|
||||
BARRIER_HEALTH_MULTIPLIERS[quality]
|
||||
if is_valid(quality)
|
||||
else BARRIER_HEALTH_MULTIPLIERS[Tier.BORING]
|
||||
)
|
||||
|
||||
|
||||
static func apply_barrier_health(base_health: int, quality: int) -> int:
|
||||
return maxi(
|
||||
ceili(
|
||||
float(maxi(base_health, 1))
|
||||
* barrier_health_multiplier(quality)
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
static func roll(
|
||||
rng: RandomNumberGenerator,
|
||||
weight_multipliers: Array[float] = [],
|
||||
|
|
|
|||
|
|
@ -11,6 +11,3 @@ barrier_health_max = 7
|
|||
first_barrier_margin = 0.14
|
||||
final_barrier_margin = 0.1
|
||||
minimum_barrier_spacing = 0.14
|
||||
chase_start_delay = 1.0
|
||||
chase_start_offset = 0.16
|
||||
chase_speed = 0.145
|
||||
|
|
|
|||
|
|
@ -11,6 +11,3 @@ barrier_health_max = 8
|
|||
first_barrier_margin = 0.13
|
||||
final_barrier_margin = 0.09
|
||||
minimum_barrier_spacing = 0.14
|
||||
chase_start_delay = 1.1
|
||||
chase_start_offset = 0.18
|
||||
chase_speed = 0.15
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ extends Node
|
|||
const CatchDifficultyProfileType = preload(
|
||||
"res://fishing/catch_difficulty_profile.gd"
|
||||
)
|
||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||
|
||||
signal encounter_updated(
|
||||
progress: float,
|
||||
|
|
@ -42,6 +43,13 @@ class Barrier:
|
|||
maximum_health = barrier_health
|
||||
|
||||
|
||||
# Fight movement uses one shared baseline. Species and quality difficulty comes
|
||||
# from barrier placement and health, while reel upgrades affect only the
|
||||
# player's progress speed.
|
||||
const CHASE_START_DELAY: float = 0.5
|
||||
const CHASE_START_OFFSET: float = 0.04
|
||||
const CHASE_SPEED: float = 0.07
|
||||
|
||||
@export_category("Accessibility")
|
||||
@export var auto_click_enabled: bool = false
|
||||
@export_range(0.05, 2.0, 0.01) var auto_click_interval: float = 0.20
|
||||
|
|
@ -60,9 +68,8 @@ var _active_barrier_index: int = -1
|
|||
var _reel_input_held: bool = false
|
||||
var _reel_speed: float = 0.0
|
||||
var _click_power: int = 1
|
||||
var _chase_start_delay: float = 0.0
|
||||
var _fish_quality: int = FishQualityType.Tier.BORING
|
||||
var _chase_delay_remaining: float = 0.0
|
||||
var _chase_speed: float = 0.0
|
||||
var _failure_epsilon: float = 0.0001
|
||||
var _auto_click_accumulator: float = 0.0
|
||||
var _rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
||||
|
|
@ -103,6 +110,7 @@ func start_encounter(
|
|||
profile: CatchDifficultyProfileType,
|
||||
reel_speed: float,
|
||||
click_power: int,
|
||||
fish_quality: int = FishQualityType.Tier.BORING,
|
||||
) -> void:
|
||||
reset()
|
||||
if profile == null:
|
||||
|
|
@ -111,10 +119,13 @@ func start_encounter(
|
|||
|
||||
_reel_speed = maxf(reel_speed, 0.0)
|
||||
_click_power = maxi(click_power, 1)
|
||||
_chase_start_delay = maxf(profile.chase_start_delay, 0.0)
|
||||
_chase_delay_remaining = _chase_start_delay
|
||||
chase_progress = -maxf(profile.chase_start_offset, 0.01)
|
||||
_chase_speed = maxf(profile.chase_speed, 0.0)
|
||||
_fish_quality = (
|
||||
fish_quality
|
||||
if FishQualityType.is_valid(fish_quality)
|
||||
else FishQualityType.Tier.BORING
|
||||
)
|
||||
_chase_delay_remaining = CHASE_START_DELAY
|
||||
chase_progress = -CHASE_START_OFFSET
|
||||
_seed_encounter_rng()
|
||||
_generate_barriers(profile)
|
||||
progress = 0.0
|
||||
|
|
@ -127,12 +138,13 @@ func start_authoritative_encounter(
|
|||
reel_speed: float,
|
||||
click_power: int,
|
||||
seed: int,
|
||||
fish_quality: int = FishQualityType.Tier.BORING,
|
||||
) -> void:
|
||||
var previous_test_mode: bool = use_deterministic_test_seed
|
||||
var previous_seed: int = deterministic_test_seed
|
||||
use_deterministic_test_seed = true
|
||||
deterministic_test_seed = seed
|
||||
start_encounter(profile, reel_speed, click_power)
|
||||
start_encounter(profile, reel_speed, click_power, fish_quality)
|
||||
use_deterministic_test_seed = previous_test_mode
|
||||
deterministic_test_seed = previous_seed
|
||||
|
||||
|
|
@ -168,9 +180,8 @@ func reset() -> void:
|
|||
_reel_input_held = false
|
||||
_reel_speed = 0.0
|
||||
_click_power = 1
|
||||
_chase_start_delay = 0.0
|
||||
_fish_quality = FishQualityType.Tier.BORING
|
||||
_chase_delay_remaining = 0.0
|
||||
_chase_speed = 0.0
|
||||
_auto_click_accumulator = 0.0
|
||||
emit_signal(
|
||||
"encounter_updated",
|
||||
|
|
@ -215,7 +226,7 @@ func _update_chase(delta: float) -> void:
|
|||
if active_delta <= 0.0:
|
||||
return
|
||||
chase_progress = minf(
|
||||
chase_progress + _chase_speed * active_delta,
|
||||
chase_progress + CHASE_SPEED * active_delta,
|
||||
1.0
|
||||
)
|
||||
|
||||
|
|
@ -302,7 +313,14 @@ func _generate_barriers(profile: CatchDifficultyProfileType) -> void:
|
|||
+ spacing * float(barrier_index)
|
||||
+ random_offsets[barrier_index] * random_slack
|
||||
)
|
||||
var health: int = _rng.randi_range(health_range.x, health_range.y)
|
||||
var base_health: int = _rng.randi_range(
|
||||
health_range.x,
|
||||
health_range.y,
|
||||
)
|
||||
var health: int = FishQualityType.apply_barrier_health(
|
||||
base_health,
|
||||
_fish_quality,
|
||||
)
|
||||
_barriers.append(Barrier.new(position, health))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ extends Resource
|
|||
@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
|
||||
@export_range(0.0, 5.0, 0.05) var chase_start_delay: float = 1.0
|
||||
@export_range(0.01, 0.5, 0.01) var chase_start_offset: float = 0.15
|
||||
@export_range(0.0, 2.0, 0.01) var chase_speed: float = 0.14
|
||||
|
||||
|
||||
func get_barrier_count_range() -> Vector2i:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,3 @@ barrier_health_max = 7
|
|||
first_barrier_margin = 0.15
|
||||
final_barrier_margin = 0.1
|
||||
minimum_barrier_spacing = 0.15
|
||||
chase_start_delay = 1.0
|
||||
chase_start_offset = 0.15
|
||||
chase_speed = 0.14
|
||||
|
|
|
|||
|
|
@ -919,6 +919,10 @@ func _activate_bite() -> void:
|
|||
state = FishingState.FIGHTING
|
||||
_state_time_remaining = 0.0
|
||||
_withdrawal_input_held = false
|
||||
_pending_catch = _fish_selector.create_catch(_selected_fish)
|
||||
if _pending_catch == null or not _pending_catch.is_valid():
|
||||
_cancel_attempt()
|
||||
return
|
||||
bite_activated.emit()
|
||||
status_changed.emit("fish on!")
|
||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||
|
|
@ -926,7 +930,8 @@ func _activate_bite() -> void:
|
|||
_catch_controller.start_encounter(
|
||||
_selected_fish.catch_profile,
|
||||
_get_effective_reel_speed(),
|
||||
_get_effective_barrier_damage()
|
||||
_get_effective_barrier_damage(),
|
||||
_pending_catch.quality,
|
||||
)
|
||||
_catch_controller.set_reel_input(
|
||||
Input.is_action_pressed("fish_primary")
|
||||
|
|
@ -993,7 +998,6 @@ func _on_catch_completed() -> void:
|
|||
_cancel_attempt()
|
||||
return
|
||||
|
||||
_pending_catch = _fish_selector.create_catch(_selected_fish)
|
||||
if _pending_catch == null or not _pending_catch.is_valid():
|
||||
_cancel_attempt()
|
||||
return
|
||||
|
|
|
|||
|
|
@ -429,11 +429,21 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void:
|
|||
return
|
||||
attempt.phase = NetworkFishingAttempt.Phase.FIGHTING
|
||||
attempt.encounter_seed = _new_seed()
|
||||
var selector := FishSelectorType.new()
|
||||
selector.use_deterministic_test_seed = true
|
||||
selector.deterministic_test_seed = attempt.encounter_seed ^ 0x5F3759DF
|
||||
selector.begin_roll()
|
||||
var fish_catch: FishCatch = selector.create_catch(fish)
|
||||
if fish_catch == null or not fish_catch.is_valid():
|
||||
_cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.")
|
||||
return
|
||||
attempt.catch_payload = fish_catch.to_network_dict()
|
||||
attempt.controller.start_authoritative_encounter(
|
||||
fish.catch_profile,
|
||||
attempt.reel_speed,
|
||||
attempt.barrier_damage,
|
||||
attempt.encounter_seed
|
||||
attempt.encounter_seed,
|
||||
fish_catch.quality,
|
||||
)
|
||||
var data: Dictionary = {
|
||||
"attempt_id": attempt.attempt_id,
|
||||
|
|
@ -617,11 +627,10 @@ func _on_attempt_caught(peer_id: int) -> void:
|
|||
if fish == null:
|
||||
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
||||
return
|
||||
var selector := FishSelectorType.new()
|
||||
selector.use_deterministic_test_seed = true
|
||||
selector.deterministic_test_seed = attempt.encounter_seed ^ 0x5F3759DF
|
||||
selector.begin_roll()
|
||||
var fish_catch: FishCatch = selector.create_catch(fish)
|
||||
var fish_catch: FishCatch = FishCatchType.from_network_dict(
|
||||
attempt.catch_payload,
|
||||
fish,
|
||||
)
|
||||
if fish_catch == null or not fish_catch.is_valid():
|
||||
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const FishingRodAttachmentScene = preload(
|
|||
const CHARACTER_IDLE_ANIMATION: StringName = &"idle"
|
||||
const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
|
||||
const CHARACTER_SITTING_ANIMATION: StringName = &"sitting"
|
||||
const BASE_REEL_SPEED: float = 0.16
|
||||
|
||||
var appearance_snapshot: Dictionary = (
|
||||
CharacterCustomizationCatalog.default_snapshot()
|
||||
|
|
@ -69,7 +70,7 @@ class ShowcaseCameraSnapshot:
|
|||
@export var local_control_enabled: bool = true
|
||||
|
||||
@export_category("Fishing Stats")
|
||||
@export_range(0.01, 2.0, 0.01) var reel_speed: float = 0.32
|
||||
@export_range(0.01, 2.0, 0.01) var reel_speed: float = BASE_REEL_SPEED
|
||||
@export_range(1, 100, 1) var click_power: int = 1
|
||||
|
||||
@export_category("Showcase")
|
||||
|
|
|
|||
|
|
@ -97,6 +97,60 @@ func _run() -> void:
|
|||
assert(player.is_movement_enabled())
|
||||
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.
|
||||
fishing_spot.call("_begin_aiming", player)
|
||||
fishing_spot.set("_cast_charge", 0.32)
|
||||
fishing_spot.call("_update_cast_charge", 0.0)
|
||||
fishing_spot.call("_confirm_cast")
|
||||
var second_wait_deadline: int = Time.get_ticks_msec() + 4000
|
||||
while (
|
||||
Time.get_ticks_msec() < second_wait_deadline
|
||||
and fishing_spot.state != FishingSpotType.FishingState.WAITING_FOR_BITE
|
||||
):
|
||||
await process_frame
|
||||
assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
|
||||
attempts = service.get("_attempts")
|
||||
attempt = attempts.get(session.get_local_peer_id())
|
||||
assert(attempt != null)
|
||||
service.call("_start_bite", attempt)
|
||||
await process_frame
|
||||
assert(attempt.phase == NetworkFishingAttempt.Phase.FIGHTING)
|
||||
var catalog: FishPool = main.get("fish_catalog") as FishPool
|
||||
assert(catalog != null)
|
||||
var fish: FishData = catalog.get_fish_by_id(attempt.fish_id)
|
||||
var fish_catch := FishCatch.from_network_dict(
|
||||
attempt.catch_payload,
|
||||
fish,
|
||||
)
|
||||
assert(fish_catch != null and fish_catch.is_valid())
|
||||
var baseline_controller := CatchController.new()
|
||||
root.add_child(baseline_controller)
|
||||
baseline_controller.start_authoritative_encounter(
|
||||
fish.catch_profile,
|
||||
attempt.reel_speed,
|
||||
attempt.barrier_damage,
|
||||
attempt.encounter_seed,
|
||||
FishQuality.Tier.BORING,
|
||||
)
|
||||
var quality_barriers: Array = attempt.controller.get("_barriers")
|
||||
var baseline_barriers: Array = baseline_controller.get("_barriers")
|
||||
assert(quality_barriers.size() == baseline_barriers.size())
|
||||
for barrier_index: int in quality_barriers.size():
|
||||
var quality_barrier: RefCounted = quality_barriers[barrier_index]
|
||||
var baseline_barrier: RefCounted = baseline_barriers[barrier_index]
|
||||
assert(
|
||||
int(quality_barrier.get("maximum_health"))
|
||||
== FishQuality.apply_barrier_health(
|
||||
int(baseline_barrier.get("maximum_health")),
|
||||
fish_catch.quality,
|
||||
)
|
||||
)
|
||||
baseline_controller.queue_free()
|
||||
service.call("_cancel_attempt", session.get_local_peer_id(), "")
|
||||
await process_frame
|
||||
assert(not service.has_local_attempt())
|
||||
|
||||
print("Fishing authority validation: PASS")
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue