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.
|
# 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 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]
|
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 = [
|
const DISPLAY_NAMES: PackedStringArray = [
|
||||||
"boring",
|
"boring",
|
||||||
"average",
|
"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(
|
static func roll(
|
||||||
rng: RandomNumberGenerator,
|
rng: RandomNumberGenerator,
|
||||||
weight_multipliers: Array[float] = [],
|
weight_multipliers: Array[float] = [],
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,3 @@ barrier_health_max = 7
|
||||||
first_barrier_margin = 0.14
|
first_barrier_margin = 0.14
|
||||||
final_barrier_margin = 0.1
|
final_barrier_margin = 0.1
|
||||||
minimum_barrier_spacing = 0.14
|
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
|
first_barrier_margin = 0.13
|
||||||
final_barrier_margin = 0.09
|
final_barrier_margin = 0.09
|
||||||
minimum_barrier_spacing = 0.14
|
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(
|
const CatchDifficultyProfileType = preload(
|
||||||
"res://fishing/catch_difficulty_profile.gd"
|
"res://fishing/catch_difficulty_profile.gd"
|
||||||
)
|
)
|
||||||
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
signal encounter_updated(
|
signal encounter_updated(
|
||||||
progress: float,
|
progress: float,
|
||||||
|
|
@ -42,6 +43,13 @@ class Barrier:
|
||||||
maximum_health = barrier_health
|
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_category("Accessibility")
|
||||||
@export var auto_click_enabled: bool = false
|
@export var auto_click_enabled: bool = false
|
||||||
@export_range(0.05, 2.0, 0.01) var auto_click_interval: float = 0.20
|
@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_input_held: bool = false
|
||||||
var _reel_speed: float = 0.0
|
var _reel_speed: float = 0.0
|
||||||
var _click_power: int = 1
|
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_delay_remaining: float = 0.0
|
||||||
var _chase_speed: float = 0.0
|
|
||||||
var _failure_epsilon: float = 0.0001
|
var _failure_epsilon: float = 0.0001
|
||||||
var _auto_click_accumulator: float = 0.0
|
var _auto_click_accumulator: float = 0.0
|
||||||
var _rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
var _rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
||||||
|
|
@ -103,6 +110,7 @@ func start_encounter(
|
||||||
profile: CatchDifficultyProfileType,
|
profile: CatchDifficultyProfileType,
|
||||||
reel_speed: float,
|
reel_speed: float,
|
||||||
click_power: int,
|
click_power: int,
|
||||||
|
fish_quality: int = FishQualityType.Tier.BORING,
|
||||||
) -> void:
|
) -> void:
|
||||||
reset()
|
reset()
|
||||||
if profile == null:
|
if profile == null:
|
||||||
|
|
@ -111,10 +119,13 @@ func start_encounter(
|
||||||
|
|
||||||
_reel_speed = maxf(reel_speed, 0.0)
|
_reel_speed = maxf(reel_speed, 0.0)
|
||||||
_click_power = maxi(click_power, 1)
|
_click_power = maxi(click_power, 1)
|
||||||
_chase_start_delay = maxf(profile.chase_start_delay, 0.0)
|
_fish_quality = (
|
||||||
_chase_delay_remaining = _chase_start_delay
|
fish_quality
|
||||||
chase_progress = -maxf(profile.chase_start_offset, 0.01)
|
if FishQualityType.is_valid(fish_quality)
|
||||||
_chase_speed = maxf(profile.chase_speed, 0.0)
|
else FishQualityType.Tier.BORING
|
||||||
|
)
|
||||||
|
_chase_delay_remaining = CHASE_START_DELAY
|
||||||
|
chase_progress = -CHASE_START_OFFSET
|
||||||
_seed_encounter_rng()
|
_seed_encounter_rng()
|
||||||
_generate_barriers(profile)
|
_generate_barriers(profile)
|
||||||
progress = 0.0
|
progress = 0.0
|
||||||
|
|
@ -127,12 +138,13 @@ func start_authoritative_encounter(
|
||||||
reel_speed: float,
|
reel_speed: float,
|
||||||
click_power: int,
|
click_power: int,
|
||||||
seed: int,
|
seed: int,
|
||||||
|
fish_quality: int = FishQualityType.Tier.BORING,
|
||||||
) -> void:
|
) -> void:
|
||||||
var previous_test_mode: bool = use_deterministic_test_seed
|
var previous_test_mode: bool = use_deterministic_test_seed
|
||||||
var previous_seed: int = deterministic_test_seed
|
var previous_seed: int = deterministic_test_seed
|
||||||
use_deterministic_test_seed = true
|
use_deterministic_test_seed = true
|
||||||
deterministic_test_seed = seed
|
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
|
use_deterministic_test_seed = previous_test_mode
|
||||||
deterministic_test_seed = previous_seed
|
deterministic_test_seed = previous_seed
|
||||||
|
|
||||||
|
|
@ -168,9 +180,8 @@ func reset() -> void:
|
||||||
_reel_input_held = false
|
_reel_input_held = false
|
||||||
_reel_speed = 0.0
|
_reel_speed = 0.0
|
||||||
_click_power = 1
|
_click_power = 1
|
||||||
_chase_start_delay = 0.0
|
_fish_quality = FishQualityType.Tier.BORING
|
||||||
_chase_delay_remaining = 0.0
|
_chase_delay_remaining = 0.0
|
||||||
_chase_speed = 0.0
|
|
||||||
_auto_click_accumulator = 0.0
|
_auto_click_accumulator = 0.0
|
||||||
emit_signal(
|
emit_signal(
|
||||||
"encounter_updated",
|
"encounter_updated",
|
||||||
|
|
@ -215,7 +226,7 @@ func _update_chase(delta: float) -> void:
|
||||||
if active_delta <= 0.0:
|
if active_delta <= 0.0:
|
||||||
return
|
return
|
||||||
chase_progress = minf(
|
chase_progress = minf(
|
||||||
chase_progress + _chase_speed * active_delta,
|
chase_progress + CHASE_SPEED * active_delta,
|
||||||
1.0
|
1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -302,7 +313,14 @@ func _generate_barriers(profile: CatchDifficultyProfileType) -> void:
|
||||||
+ spacing * float(barrier_index)
|
+ spacing * float(barrier_index)
|
||||||
+ random_offsets[barrier_index] * random_slack
|
+ 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))
|
_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 first_barrier_margin: float = 0.15
|
||||||
@export_range(0.0, 0.45, 0.01) var final_barrier_margin: float = 0.10
|
@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.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:
|
func get_barrier_count_range() -> Vector2i:
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,3 @@ barrier_health_max = 7
|
||||||
first_barrier_margin = 0.15
|
first_barrier_margin = 0.15
|
||||||
final_barrier_margin = 0.1
|
final_barrier_margin = 0.1
|
||||||
minimum_barrier_spacing = 0.15
|
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 = FishingState.FIGHTING
|
||||||
_state_time_remaining = 0.0
|
_state_time_remaining = 0.0
|
||||||
_withdrawal_input_held = false
|
_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()
|
bite_activated.emit()
|
||||||
status_changed.emit("fish on!")
|
status_changed.emit("fish on!")
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||||
|
|
@ -926,7 +930,8 @@ func _activate_bite() -> void:
|
||||||
_catch_controller.start_encounter(
|
_catch_controller.start_encounter(
|
||||||
_selected_fish.catch_profile,
|
_selected_fish.catch_profile,
|
||||||
_get_effective_reel_speed(),
|
_get_effective_reel_speed(),
|
||||||
_get_effective_barrier_damage()
|
_get_effective_barrier_damage(),
|
||||||
|
_pending_catch.quality,
|
||||||
)
|
)
|
||||||
_catch_controller.set_reel_input(
|
_catch_controller.set_reel_input(
|
||||||
Input.is_action_pressed("fish_primary")
|
Input.is_action_pressed("fish_primary")
|
||||||
|
|
@ -993,7 +998,6 @@ func _on_catch_completed() -> void:
|
||||||
_cancel_attempt()
|
_cancel_attempt()
|
||||||
return
|
return
|
||||||
|
|
||||||
_pending_catch = _fish_selector.create_catch(_selected_fish)
|
|
||||||
if _pending_catch == null or not _pending_catch.is_valid():
|
if _pending_catch == null or not _pending_catch.is_valid():
|
||||||
_cancel_attempt()
|
_cancel_attempt()
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -429,11 +429,21 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void:
|
||||||
return
|
return
|
||||||
attempt.phase = NetworkFishingAttempt.Phase.FIGHTING
|
attempt.phase = NetworkFishingAttempt.Phase.FIGHTING
|
||||||
attempt.encounter_seed = _new_seed()
|
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(
|
attempt.controller.start_authoritative_encounter(
|
||||||
fish.catch_profile,
|
fish.catch_profile,
|
||||||
attempt.reel_speed,
|
attempt.reel_speed,
|
||||||
attempt.barrier_damage,
|
attempt.barrier_damage,
|
||||||
attempt.encounter_seed
|
attempt.encounter_seed,
|
||||||
|
fish_catch.quality,
|
||||||
)
|
)
|
||||||
var data: Dictionary = {
|
var data: Dictionary = {
|
||||||
"attempt_id": attempt.attempt_id,
|
"attempt_id": attempt.attempt_id,
|
||||||
|
|
@ -617,11 +627,10 @@ func _on_attempt_caught(peer_id: int) -> void:
|
||||||
if fish == null:
|
if fish == null:
|
||||||
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
||||||
return
|
return
|
||||||
var selector := FishSelectorType.new()
|
var fish_catch: FishCatch = FishCatchType.from_network_dict(
|
||||||
selector.use_deterministic_test_seed = true
|
attempt.catch_payload,
|
||||||
selector.deterministic_test_seed = attempt.encounter_seed ^ 0x5F3759DF
|
fish,
|
||||||
selector.begin_roll()
|
)
|
||||||
var fish_catch: FishCatch = selector.create_catch(fish)
|
|
||||||
if fish_catch == null or not fish_catch.is_valid():
|
if fish_catch == null or not fish_catch.is_valid():
|
||||||
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
_cancel_attempt(peer_id, "Fishing attempt ended.")
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const FishingRodAttachmentScene = preload(
|
||||||
const CHARACTER_IDLE_ANIMATION: StringName = &"idle"
|
const CHARACTER_IDLE_ANIMATION: StringName = &"idle"
|
||||||
const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
|
const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
|
||||||
const CHARACTER_SITTING_ANIMATION: StringName = &"sitting"
|
const CHARACTER_SITTING_ANIMATION: StringName = &"sitting"
|
||||||
|
const BASE_REEL_SPEED: float = 0.16
|
||||||
|
|
||||||
var appearance_snapshot: Dictionary = (
|
var appearance_snapshot: Dictionary = (
|
||||||
CharacterCustomizationCatalog.default_snapshot()
|
CharacterCustomizationCatalog.default_snapshot()
|
||||||
|
|
@ -69,7 +70,7 @@ class ShowcaseCameraSnapshot:
|
||||||
@export var local_control_enabled: bool = true
|
@export var local_control_enabled: bool = true
|
||||||
|
|
||||||
@export_category("Fishing Stats")
|
@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_range(1, 100, 1) var click_power: int = 1
|
||||||
|
|
||||||
@export_category("Showcase")
|
@export_category("Showcase")
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,60 @@ func _run() -> void:
|
||||||
assert(player.is_movement_enabled())
|
assert(player.is_movement_enabled())
|
||||||
assert(not bobber.visible)
|
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")
|
print("Fishing authority validation: PASS")
|
||||||
session.disconnect_session("")
|
session.disconnect_session("")
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue