straywild/tests/fishing_authority_validation.gd
Voyager 4575830857 Improve nearby water recovery and camera prompts
Recover players on generated worlds to the nearest safe grass or sand surface. Use the active gameplay camera when positioning shop and storage prompts.
2026-08-29 17:43:11 -04:00

286 lines
11 KiB
GDScript

extends SceneTree
const MainScene = preload("res://main/main.tscn")
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
root.size = Vector2i(1280, 720)
var main := MainScene.instantiate()
root.add_child(main)
for _frame: int in 4:
await process_frame
if not bool(main.get("_application_initialized")):
main.call("_activate_selected_data_path", "", true)
for _frame: int in 8:
await process_frame
assert(bool(main.get("_application_initialized")))
assert(bool(main.call(
"_apply_world",
WorldLayout.GENERATED,
PlayerSaveManager.DEFAULT_WORLD_SEED,
true,
)))
assert(bool(main.call("_prepare_private_host")))
var save_manager := main.get("_save_manager") as PlayerSaveManager
assert(save_manager.initialize_new_game())
main.call("_enter_gameplay")
for _frame: int in 8:
await process_frame
var session := main.get_node("%NetworkSession") as NetworkSession
var service := main.get_node(
"%NetworkFishingService"
) as NetworkFishingService
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
var game_ui := main.get_node("UIPresentation/UIViewport/GameUI")
var fishing_status := game_ui.get_node("%StatusLabel") as Label
var fishing_panel := game_ui.get_node("%FishingPanel") as PanelContainer
var player := main.get("_player") as Player
var item_catalog := main.get("item_catalog") as ItemCatalog
assert(session.is_host())
assert(not session.is_open_host())
assert(service != null and fishing_spot != null and player != null)
assert(player.hotbar.get_selected_item_id() == &"basic_fishing_rod")
var worms: ItemData = item_catalog.get_item_by_id(&"worms")
assert(worms != null)
if player.bag.get_quantity(&"worms") == 0:
assert(player.bag.add_item(&"worms", 2))
assert(player.equip_bait(worms))
var bait_quantity_before: int = player.bag.get_quantity(&"worms")
var fresh_root := main.get_node(
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
) as Node3D
var pond: WaterBodyAuthoring
for child: Node in fresh_root.get_children():
var candidate := child as WaterBodyAuthoring
if candidate != null and &"pond" in candidate.location_tags:
pond = candidate
break
assert(pond != null)
var pond_region := pond.get_node("FishingRegion") as FishableWaterRegion
assert(pond_region != null)
var pond_surface_y: float = pond_region.get_surface_height()
player.global_position = pond.global_transform * Vector3(
pond.surface_size.x * 0.5 + 0.8,
1.44,
0.0,
)
var pond_direction := pond.global_position - player.global_position
pond_direction.y = 0.0
pond_direction = pond_direction.normalized()
var visuals := player.get_node("Visuals") as Node3D
visuals.global_rotation.y = atan2(-pond_direction.x, -pond_direction.z)
for _frame: int in 4:
await physics_frame
assert(player.get_facing_direction().dot(pond_direction) > 0.99)
fishing_spot.call("_begin_aiming", player)
assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST)
assert(player.is_movement_enabled())
fishing_spot.set("_cast_charge", 0.32)
fishing_spot.call("_update_cast_charge", 0.0)
var aimed_target: Vector3 = fishing_spot.get("_cast_target")
var aimed_direction := aimed_target - player.global_position
aimed_direction.y = 0.0
assert(aimed_direction.normalized().dot(pond_direction) > 0.99)
assert(is_equal_approx(aimed_target.y, pond_surface_y))
assert(fishing_spot.is_target_fishable(aimed_target))
fishing_spot.call("_confirm_cast")
assert(fishing_spot.state == FishingSpotType.FishingState.CASTING)
assert(not player.is_movement_enabled())
var wait_deadline: int = Time.get_ticks_msec() + 4000
while (
Time.get_ticks_msec() < wait_deadline
and fishing_spot.state != FishingSpotType.FishingState.WAITING_FOR_BITE
):
await process_frame
assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
assert(service.has_local_attempt())
assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
assert(fishing_status.text.is_empty())
assert(not fishing_status.visible)
assert(not fishing_panel.visible)
var attempts: Dictionary = service.get("_attempts")
var attempt: NetworkFishingAttempt = attempts.get(session.get_local_peer_id())
assert(attempt != null)
assert(is_equal_approx(attempt.target.y, pond_surface_y))
assert(attempt.bobber_position.is_equal_approx(attempt.target))
fishing_spot.set("_withdrawal_input_held", true)
fishing_spot.set("_network_primary_input_held", true)
service.submit_local_input(true, true)
var return_deadline: int = Time.get_ticks_msec() + 4000
while (
Time.get_ticks_msec() < return_deadline
and fishing_spot.state != FishingSpotType.FishingState.RETURNING
):
await process_frame
assert(fishing_spot.state == FishingSpotType.FishingState.RETURNING)
assert(not service.has_local_attempt())
var bobber := fishing_spot.get_node(
"FishingPresentation/Bobber"
) as MeshInstance3D
assert(bobber.visible)
var ready_deadline: int = Time.get_ticks_msec() + 3000
while (
Time.get_ticks_msec() < ready_deadline
and fishing_spot.state != FishingSpotType.FishingState.READY
):
await process_frame
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
assert(player.is_movement_enabled())
assert(not bobber.visible)
assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
# A private host rolls and retains the authoritative catch before the fight
# 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)
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)
assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
var fishing_priority_transitions: Array[bool] = []
fishing_spot.fishing_input_priority_changed.connect(
func(active: bool) -> void:
fishing_priority_transitions.append(active)
)
service.call("_start_bite", attempt)
await process_frame
assert(attempt.phase == NetworkFishingAttempt.Phase.FIGHTING)
assert(fishing_spot.is_fishing_input_priority_active())
assert(fishing_priority_transitions == [true])
assert(player.bag.get_quantity(&"worms") == bait_quantity_before - 1)
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 reference_controller := CatchController.new()
root.add_child(reference_controller)
reference_controller.start_authoritative_encounter(
fish.catch_profile,
attempt.reel_speed,
attempt.barrier_damage,
attempt.encounter_seed,
fish_catch.quality,
)
var quality_barriers: Array = attempt.controller.get("_barriers")
var reference_barriers: Array = reference_controller.get("_barriers")
assert(quality_barriers.size() == reference_barriers.size())
for barrier_index: int in quality_barriers.size():
var quality_barrier: RefCounted = quality_barriers[barrier_index]
var reference_barrier: RefCounted = reference_barriers[barrier_index]
assert(
is_equal_approx(
float(quality_barrier.get("position")),
float(reference_barrier.get("position")),
)
)
assert(
int(quality_barrier.get("maximum_health"))
== int(reference_barrier.get("maximum_health"))
)
assert(
int(quality_barrier.get("maximum_health"))
>= FishQuality.BARRIER_HEALTH_MINIMUMS[fish_catch.quality]
)
assert(
int(quality_barrier.get("maximum_health"))
<= FishQuality.BARRIER_HEALTH_MAXIMUMS[fish_catch.quality]
)
reference_controller.queue_free()
service.call("_on_attempt_escaped", session.get_local_peer_id())
await process_frame
assert(not service.has_local_attempt())
assert(fishing_spot.is_fishing_input_priority_active())
assert(fishing_status.text.is_empty())
assert(not fishing_status.visible)
assert(not fishing_panel.visible)
var reset_test_deadline: int = Time.get_ticks_msec() + 3000
while (
Time.get_ticks_msec() < reset_test_deadline
and fishing_spot.state != FishingSpotType.FishingState.READY
):
await process_frame
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
assert(not fishing_spot.is_fishing_input_priority_active())
assert(fishing_priority_transitions == [true, false])
# Leaving a session during the cast presentation must release every local
# action and equipment lock. This is the same cleanup path used by an
# in-game server switch or a lost connection.
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")
assert(fishing_spot.state == FishingSpotType.FishingState.CASTING)
assert(not player.is_movement_enabled())
fishing_spot.call(
"_on_network_session_state_changed",
NetworkSession.State.DISCONNECTING,
)
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
assert(player.is_movement_enabled())
assert(fishing_spot.can_change_hotbar_selection())
assert(fishing_spot.can_open_fishing_shop())
# Escape can put the rod into RETURNING immediately before deep-water
# recovery begins. Recovery must finish that return before snapshotting its
# movement state, otherwise the respawn restores a permanent cast lock.
var water_recovery := main.get_node("%WaterRecovery") as WaterRecoveryController
assert(water_recovery != null)
var pause_menu: PauseMenu = game_ui.get_pause_menu()
pause_menu.open_menu()
assert(pause_menu.visible)
player.set_movement_enabled(false)
fishing_spot.set("_active_player", player)
fishing_spot.state = FishingSpotType.FishingState.RETURNING
water_recovery.call(
"_on_recovery_requested",
player,
player.global_position.y,
)
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
assert(bool(water_recovery.get("_prior_movement_enabled")))
assert(bool(water_recovery.get("_prior_camera_input_enabled")))
assert(not pause_menu.visible)
var test_world := main.get_node("TestWorld") as TestWorld
assert(test_world != null)
var recovery_entry := water_recovery.get("_entry_position") as Vector3
var resolved_recovery := water_recovery.call(
"_resolve_recovery_position"
) as Vector3
assert(resolved_recovery.is_equal_approx(
test_world.get_water_recovery_position(recovery_entry)
))
water_recovery.call("_finish_recovery")
assert(player.is_movement_enabled())
print("Fishing authority validation: PASS")
session.disconnect_session("")
main.queue_free()
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
quit()