Fix fishing and chat interaction regressions
This commit is contained in:
parent
66ae8c8be7
commit
e4ddf4e5a6
11 changed files with 256 additions and 43 deletions
|
|
@ -417,20 +417,21 @@ func configure_accessibility_auto_click(
|
|||
|
||||
func begin_water_recovery() -> void:
|
||||
_external_input_blocked = true
|
||||
if state in [
|
||||
FishingState.AIMING_CAST,
|
||||
FishingState.CASTING,
|
||||
FishingState.WAITING_FOR_BITE,
|
||||
FishingState.FIGHTING,
|
||||
]:
|
||||
_cancel_attempt()
|
||||
# Recovery replaces the normal rod-return presentation. Finish the local
|
||||
# cleanup now so its temporary movement lock is not captured and restored
|
||||
# after the player respawns.
|
||||
if state != FishingState.READY:
|
||||
_finalize_attempt_cleanup("")
|
||||
elif state == FishingState.SHOWING_CATCH:
|
||||
if state == FishingState.SHOWING_CATCH:
|
||||
_secure_showcase_catch_for_recovery()
|
||||
elif is_fishing_sequence_active():
|
||||
if state in [
|
||||
FishingState.AIMING_CAST,
|
||||
FishingState.CASTING,
|
||||
FishingState.WAITING_FOR_BITE,
|
||||
FishingState.FIGHTING,
|
||||
]:
|
||||
_cancel_attempt()
|
||||
# Recovery replaces every in-flight rod return. This includes RETURNING,
|
||||
# which can be entered by Escape a frame before the water trigger fires.
|
||||
# Finish cleanup now so recovery never snapshots the cast movement lock.
|
||||
if state != FishingState.READY or _active_player != null:
|
||||
_finalize_attempt_cleanup("")
|
||||
|
||||
|
||||
func end_water_recovery() -> void:
|
||||
|
|
@ -538,6 +539,12 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
alternate_reel and state == FishingState.FIGHTING
|
||||
):
|
||||
return
|
||||
# A held keyboard key emits echo events. Treat the alternate reel exactly
|
||||
# like a mouse button: one barrier hit per physical press, while the normal
|
||||
# held-input path continues driving free reeling between barriers.
|
||||
if event is InputEventKey and event.echo:
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var selected_item: ItemDataType = _get_active_item()
|
||||
if (
|
||||
state == FishingState.READY
|
||||
|
|
@ -948,7 +955,6 @@ func _on_cast_completed() -> void:
|
|||
if _selected_fish == null:
|
||||
_cleanup_attempt("nothing is biting here.", &"invalid")
|
||||
return
|
||||
_consume_active_bait()
|
||||
|
||||
state = FishingState.WAITING_FOR_BITE
|
||||
_active_player.set_fishing_visual(true)
|
||||
|
|
@ -975,11 +981,13 @@ func _on_cast_completed() -> void:
|
|||
status_changed.emit("")
|
||||
|
||||
|
||||
func _consume_active_bait() -> void:
|
||||
func _consume_active_bait() -> bool:
|
||||
if _active_player == null or _active_player.active_bait_id.is_empty():
|
||||
return
|
||||
return true
|
||||
if _local_bag == null or not _local_bag.remove_item(_active_player.active_bait_id, 1):
|
||||
_active_player.unequip_bait()
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func roll_bite_wait_time() -> float:
|
||||
|
|
@ -1131,14 +1139,18 @@ func _activate_bite(confirmation_override: bool = false) -> void:
|
|||
return
|
||||
|
||||
_set_bite_confirmation_pending(false)
|
||||
state = FishingState.FIGHTING
|
||||
_active_player.set_fighting_visual(true)
|
||||
_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
|
||||
if not _consume_active_bait():
|
||||
_pending_catch = null
|
||||
_cleanup_attempt("No bait available.", &"cancel")
|
||||
return
|
||||
state = FishingState.FIGHTING
|
||||
_active_player.set_fighting_visual(true)
|
||||
_state_time_remaining = 0.0
|
||||
_withdrawal_input_held = false
|
||||
_start_fight_audio()
|
||||
bite_activated.emit()
|
||||
status_changed.emit("fish on!")
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ var target: Vector3
|
|||
var bobber_position: Vector3
|
||||
var fish_id: StringName
|
||||
var rod_id: StringName
|
||||
var bait_id: StringName
|
||||
var bait_tags: Array[StringName] = []
|
||||
var lure_effects: Array[StringName] = []
|
||||
var encounter_seed: int = 0
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ func cancel_local_attempt(reason: String = "Fishing attempt ended.") -> void:
|
|||
if _session.is_host():
|
||||
_cancel_attempt(peer_id, reason)
|
||||
else:
|
||||
_pending_local_bait_by_request.erase(attempt.request_id)
|
||||
submit_cancel_request.rpc_id(1, attempt.attempt_id)
|
||||
|
||||
|
||||
|
|
@ -361,7 +362,7 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
|||
owns_authoritative_bag
|
||||
and (
|
||||
avatar_bag == null
|
||||
or not avatar_bag.remove_item(bait_id, 1)
|
||||
or not avatar_bag.owns_item(bait_id)
|
||||
)
|
||||
)
|
||||
):
|
||||
|
|
@ -378,6 +379,7 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
|||
attempt.bobber_position = authoritative_target
|
||||
attempt.fish_id = selected_fish.id
|
||||
attempt.rod_id = rod.item_id
|
||||
attempt.bait_id = bait_id
|
||||
attempt.bait_tags = _bait_tags_for_request(data)
|
||||
attempt.lure_effects.clear()
|
||||
if lure != null:
|
||||
|
|
@ -564,7 +566,6 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void:
|
|||
_cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.")
|
||||
return
|
||||
attempt.bite_confirmation_pending = false
|
||||
attempt.phase = NetworkFishingAttempt.Phase.FIGHTING
|
||||
attempt.encounter_seed = _new_seed()
|
||||
var selector := FishSelectorType.new()
|
||||
selector.active_rod = rod
|
||||
|
|
@ -575,6 +576,17 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void:
|
|||
if fish_catch == null or not fish_catch.is_valid():
|
||||
_cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.")
|
||||
return
|
||||
if (
|
||||
attempt.owner_peer_id == _session.get_local_peer_id()
|
||||
and not attempt.bait_id.is_empty()
|
||||
and (
|
||||
_local_bag == null
|
||||
or not _local_bag.remove_item(attempt.bait_id, 1)
|
||||
)
|
||||
):
|
||||
_cancel_attempt(attempt.owner_peer_id, "No bait available.")
|
||||
return
|
||||
attempt.phase = NetworkFishingAttempt.Phase.FIGHTING
|
||||
attempt.catch_payload = fish_catch.to_network_dict()
|
||||
attempt.controller.start_authoritative_encounter(
|
||||
fish.catch_profile,
|
||||
|
|
@ -1031,6 +1043,13 @@ func _apply_public_outcome(data: Dictionary) -> void:
|
|||
if peer_id == _session.get_local_peer_id():
|
||||
if str(data["outcome"]) != "catch":
|
||||
if not _session.is_host():
|
||||
var local_attempt: NetworkFishingAttempt = _attempts.get(
|
||||
peer_id
|
||||
)
|
||||
if local_attempt != null:
|
||||
_pending_local_bait_by_request.erase(
|
||||
local_attempt.request_id
|
||||
)
|
||||
_attempts.erase(peer_id)
|
||||
local_attempt_ended.emit(
|
||||
StringName(str(data["outcome"])),
|
||||
|
|
@ -1091,20 +1110,6 @@ func _apply_cast_accepted(data: Dictionary) -> void:
|
|||
# On the host this replaces the same authoritative value with itself.
|
||||
if not _session.is_host():
|
||||
_attempts[peer_id] = attempt
|
||||
var pending_bait_id: StringName = (
|
||||
_pending_local_bait_by_request.get(
|
||||
attempt.request_id,
|
||||
StringName(),
|
||||
)
|
||||
)
|
||||
_pending_local_bait_by_request.erase(attempt.request_id)
|
||||
if not pending_bait_id.is_empty() and (
|
||||
_local_bag == null
|
||||
or not _local_bag.remove_item(pending_bait_id, 1)
|
||||
):
|
||||
cancel_local_attempt("No bait available.")
|
||||
local_cast_rejected.emit("No bait available.")
|
||||
return
|
||||
local_cast_accepted.emit(attempt.attempt_id, target)
|
||||
else:
|
||||
var presentation := _get_remote_presentation(peer_id)
|
||||
|
|
@ -1140,6 +1145,22 @@ func _apply_bite_started(data: Dictionary) -> void:
|
|||
return
|
||||
var peer_id: int = data["owner_peer_id"]
|
||||
if peer_id == _session.get_local_peer_id():
|
||||
if not _session.is_host():
|
||||
var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
|
||||
var pending_bait_id := StringName()
|
||||
if attempt != null:
|
||||
pending_bait_id = _pending_local_bait_by_request.get(
|
||||
attempt.request_id,
|
||||
StringName(),
|
||||
)
|
||||
_pending_local_bait_by_request.erase(attempt.request_id)
|
||||
if not pending_bait_id.is_empty() and (
|
||||
_local_bag == null
|
||||
or not _local_bag.remove_item(pending_bait_id, 1)
|
||||
):
|
||||
cancel_local_attempt("No bait available.")
|
||||
local_cast_rejected.emit("No bait available.")
|
||||
return
|
||||
local_bite_started.emit(str(data["attempt_id"]))
|
||||
else:
|
||||
var presentation := _get_remote_presentation(peer_id)
|
||||
|
|
@ -1260,12 +1281,12 @@ func _dispose_attempt(peer_id: int) -> void:
|
|||
attempt.controller.reset()
|
||||
attempt.controller.queue_free()
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
var local_return_is_active: bool = (
|
||||
var local_fishing_sequence_is_active: bool = (
|
||||
peer_id == _session.get_local_peer_id()
|
||||
and _fishing_spot != null
|
||||
and _fishing_spot.is_returning()
|
||||
and _fishing_spot.is_fishing_sequence_active()
|
||||
)
|
||||
if avatar != null and not local_return_is_active:
|
||||
if avatar != null and not local_fishing_sequence_is_active:
|
||||
avatar.set_movement_enabled(true)
|
||||
|
||||
|
||||
|
|
@ -1307,6 +1328,7 @@ func _clear_all() -> void:
|
|||
_result_acknowledgements.clear()
|
||||
_last_cast_time.clear()
|
||||
_last_input_time.clear()
|
||||
_pending_local_bait_by_request.clear()
|
||||
_snapshot_accumulator = 0.0
|
||||
_local_input_sequence = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const CHARACTER_NET_STRIKE_ANIMATION: StringName = &"strike"
|
|||
const CATCH_PRESENTATION_REFERENCE_LONG_SIDE_PX: float = 1280.0
|
||||
const CATCH_PRESENTATION_MIN_TEXTURE_SCALE: float = 0.1
|
||||
const CATCH_PRESENTATION_MAX_TEXTURE_SCALE: float = 20.0
|
||||
const CATCH_SHOWCASE_INPUT_OWNER: StringName = &"catch_showcase"
|
||||
# Add future networked emote animation IDs here. The protocol accepts unknown
|
||||
# safe IDs so newer clients can extend it, but Player only presents actions
|
||||
# explicitly approved by this catalog.
|
||||
|
|
@ -122,6 +123,13 @@ const BLINK_INTERVAL_SECONDS := Vector2(2.8, 7.5)
|
|||
const BLINK_DURATION_SECONDS: float = 0.11
|
||||
const CHARACTER_CALL_MOUTH_ID: String = "open_ah"
|
||||
const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
|
||||
const SPEECH_MOUTH_IDS: Array[String] = [
|
||||
"open_ah",
|
||||
"open_oh",
|
||||
"open_small",
|
||||
"open_smile",
|
||||
]
|
||||
const SPEECH_MOUTH_FRAME_SECONDS: float = 0.09
|
||||
const BASE_REEL_SPEED: float = 0.16
|
||||
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
|
||||
const NETWORK_EXTRAPOLATION_LIMIT_SECONDS: float = 0.25
|
||||
|
|
@ -227,6 +235,42 @@ func play_character_call_visual() -> void:
|
|||
_apply_presented_appearance()
|
||||
|
||||
|
||||
func play_speech_visual(duration_seconds: float) -> void:
|
||||
stop_speech_visual()
|
||||
if duration_seconds <= 0.0 or SPEECH_MOUTH_IDS.is_empty():
|
||||
return
|
||||
var generation: int = _speech_visual_generation
|
||||
var elapsed: float = 0.0
|
||||
var frame_index: int = 0
|
||||
_speech_mouth_active = true
|
||||
while elapsed < duration_seconds:
|
||||
_speech_mouth_id = SPEECH_MOUTH_IDS[
|
||||
frame_index % SPEECH_MOUTH_IDS.size()
|
||||
]
|
||||
_apply_presented_appearance()
|
||||
var frame_duration := minf(
|
||||
SPEECH_MOUTH_FRAME_SECONDS,
|
||||
duration_seconds - elapsed,
|
||||
)
|
||||
await get_tree().create_timer(frame_duration).timeout
|
||||
if generation != _speech_visual_generation:
|
||||
return
|
||||
elapsed += frame_duration
|
||||
frame_index += 1
|
||||
_speech_mouth_active = false
|
||||
_speech_mouth_id = ""
|
||||
_apply_presented_appearance()
|
||||
|
||||
|
||||
func stop_speech_visual() -> void:
|
||||
_speech_visual_generation += 1
|
||||
if not _speech_mouth_active and _speech_mouth_id.is_empty():
|
||||
return
|
||||
_speech_mouth_active = false
|
||||
_speech_mouth_id = ""
|
||||
_apply_presented_appearance()
|
||||
|
||||
|
||||
func set_fishing_visual(active: bool) -> void:
|
||||
if (
|
||||
active
|
||||
|
|
@ -295,6 +339,10 @@ func _apply_presented_appearance() -> void:
|
|||
if _fighting_visual_active:
|
||||
presented_appearance = appearance_snapshot.duplicate(true)
|
||||
presented_appearance["eyes"] = FIGHTING_EYES_ID
|
||||
if _speech_mouth_active and not _speech_mouth_id.is_empty():
|
||||
if presented_appearance == appearance_snapshot:
|
||||
presented_appearance = appearance_snapshot.duplicate(true)
|
||||
presented_appearance["mouth"] = _speech_mouth_id
|
||||
if _character_call_mouth_active:
|
||||
if presented_appearance == appearance_snapshot:
|
||||
presented_appearance = appearance_snapshot.duplicate(true)
|
||||
|
|
@ -505,6 +553,9 @@ var _blink_seconds_remaining: float = 0.0
|
|||
var _blink_rng := RandomNumberGenerator.new()
|
||||
var _character_call_mouth_active: bool = false
|
||||
var _character_call_visual_generation: int = 0
|
||||
var _speech_mouth_active: bool = false
|
||||
var _speech_mouth_id: String = ""
|
||||
var _speech_visual_generation: int = 0
|
||||
var _fishing_visual_active: bool = false
|
||||
var _fishing_visual_phase: FishingVisualPhase = FishingVisualPhase.NONE
|
||||
var _fishing_after_release_pending: bool = false
|
||||
|
|
@ -1755,6 +1806,10 @@ func _update_free_camera_physics() -> void:
|
|||
if _free_camera == null or _free_camera_body == null:
|
||||
_set_free_camera_active(false)
|
||||
return
|
||||
if not _is_movement_input_enabled():
|
||||
_free_camera_body.velocity = Vector3.ZERO
|
||||
_free_camera_body.move_and_slide()
|
||||
return
|
||||
var input_vector: Vector2 = Input.get_vector(
|
||||
"move_left",
|
||||
"move_right",
|
||||
|
|
@ -2981,6 +3036,8 @@ func _begin_catch_showcase_now(
|
|||
) -> void:
|
||||
if _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE:
|
||||
_cancel_pocket_visual()
|
||||
if local_control_enabled:
|
||||
set_local_input_suppressed(CATCH_SHOWCASE_INPUT_OWNER, true)
|
||||
_showcase_animation_active = true
|
||||
set_fishing_visual(false)
|
||||
_kill_showcase_camera_tween()
|
||||
|
|
@ -3149,6 +3206,8 @@ func end_catch_showcase(
|
|||
not _showcase_visual_rotation_stored
|
||||
and _showcase_camera_snapshot == null
|
||||
):
|
||||
if local_control_enabled:
|
||||
set_local_input_suppressed(CATCH_SHOWCASE_INPUT_OWNER, false)
|
||||
if restored_callback.is_valid():
|
||||
restored_callback.call()
|
||||
return
|
||||
|
|
@ -3310,6 +3369,8 @@ func _complete_showcase_restore(
|
|||
and Input.is_action_pressed("camera_drag")
|
||||
)
|
||||
_showcase_camera_snapshot = null
|
||||
if local_control_enabled:
|
||||
set_local_input_suppressed(CATCH_SHOWCASE_INPUT_OWNER, false)
|
||||
if restored_callback.is_valid():
|
||||
restored_callback.call()
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ func _run() -> void:
|
|||
await physics_frame
|
||||
|
||||
var player := main.get("_player") as Player
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
var sprint_dust := player.get_node("%SprintDust") as SprintDustTrail
|
||||
assert(sprint_dust != null)
|
||||
assert(sprint_dust.get_active_puff_count() == 0)
|
||||
|
|
@ -310,6 +311,55 @@ func _run() -> void:
|
|||
right_stick_motion.axis_value = 0.0
|
||||
Input.parse_input_event(right_stick_motion)
|
||||
await process_frame
|
||||
|
||||
# Chat owns all movement while text entry is active, including developer
|
||||
# freecam movement which reads the same WASD actions as the player.
|
||||
var free_camera_body := player.get("_free_camera_body") as CharacterBody3D
|
||||
assert(free_camera_body != null)
|
||||
chat_ui.open_chat()
|
||||
assert(chat_ui.is_open())
|
||||
var free_camera_position_before: Vector3 = free_camera_body.global_position
|
||||
Input.action_press("move_forward")
|
||||
for _frame: int in 3:
|
||||
await physics_frame
|
||||
Input.action_release("move_forward")
|
||||
assert(free_camera_body.global_position.is_equal_approx(
|
||||
free_camera_position_before
|
||||
))
|
||||
chat_ui.refocus_gameplay()
|
||||
|
||||
# Typewriter speech cycles the shared open-mouth faceplates, then restores
|
||||
# the player's authored mouth. World speech also follows the active freecam,
|
||||
# not the inactive normal gameplay camera.
|
||||
var normal_camera := player.get_gameplay_camera()
|
||||
var normal_camera_transform: Transform3D = normal_camera.global_transform
|
||||
var chat_anchor: Vector3 = player.get_chat_anchor_position()
|
||||
normal_camera.look_at(
|
||||
normal_camera.global_position
|
||||
- (chat_anchor - normal_camera.global_position),
|
||||
Vector3.UP,
|
||||
)
|
||||
assert(normal_camera.is_position_behind(chat_anchor))
|
||||
assert(not free_camera.is_position_behind(chat_anchor))
|
||||
chat_ui.show_local_speech("hello there friend")
|
||||
await process_frame
|
||||
assert(bool(player.get("_speech_mouth_active")))
|
||||
var first_speech_mouth: String = str(player.get("_speech_mouth_id"))
|
||||
await create_timer(0.12).timeout
|
||||
var second_speech_mouth: String = str(player.get("_speech_mouth_id"))
|
||||
assert(not first_speech_mouth.is_empty())
|
||||
assert(not second_speech_mouth.is_empty())
|
||||
assert(first_speech_mouth != second_speech_mouth)
|
||||
chat_ui.call("_update_speech")
|
||||
var speech: Dictionary = chat_ui.get("_speech")
|
||||
var local_speech: Dictionary = speech.get(session.get_local_peer_id(), {})
|
||||
var speech_bubble := local_speech.get("bubble") as PanelContainer
|
||||
assert(speech_bubble != null and speech_bubble.visible)
|
||||
normal_camera.global_transform = normal_camera_transform
|
||||
await create_timer(0.6).timeout
|
||||
assert(not bool(player.get("_speech_mouth_active")))
|
||||
chat_ui.call("_on_peer_removed", session.get_local_peer_id())
|
||||
|
||||
game_ui.call("_on_quick_action_selected", &"freecam")
|
||||
assert(not player.is_free_camera_active())
|
||||
game_ui.call("_on_quick_action_selected", &"hud")
|
||||
|
|
@ -754,7 +804,6 @@ func _run() -> void:
|
|||
assert(not service.is_active() and not toolbar.visible)
|
||||
|
||||
print("Art tools validation: PASS")
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
for _frame: int in 4:
|
||||
|
|
|
|||
|
|
@ -35,10 +35,17 @@ func _run() -> void:
|
|||
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
|
||||
|
|
@ -90,6 +97,7 @@ func _run() -> void:
|
|||
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)
|
||||
|
|
@ -124,6 +132,7 @@ func _run() -> void:
|
|||
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.
|
||||
|
|
@ -141,9 +150,11 @@ func _run() -> void:
|
|||
attempts = service.get("_attempts")
|
||||
attempt = attempts.get(session.get_local_peer_id())
|
||||
assert(attempt != null)
|
||||
assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
|
||||
service.call("_start_bite", attempt)
|
||||
await process_frame
|
||||
assert(attempt.phase == NetworkFishingAttempt.Phase.FIGHTING)
|
||||
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)
|
||||
|
|
@ -218,6 +229,29 @@ func _run() -> void:
|
|||
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)
|
||||
water_recovery.call("_finish_recovery")
|
||||
assert(player.is_movement_enabled())
|
||||
|
||||
print("Fishing authority validation: PASS")
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
|
|
|
|||
|
|
@ -302,7 +302,9 @@ func _run_client() -> void:
|
|||
]
|
||||
)
|
||||
assert(service.has_local_attempt())
|
||||
assert(player.bag.get_quantity(&"worms") == worms_before_cast - 1)
|
||||
# Bait remains available while the bobber is waiting. It is consumed only
|
||||
# once the authoritative bite begins.
|
||||
assert(player.bag.get_quantity(&"worms") == worms_before_cast)
|
||||
var fishing_deadline: int = Time.get_ticks_msec() + 5000
|
||||
while (
|
||||
Time.get_ticks_msec() < fishing_deadline
|
||||
|
|
|
|||
|
|
@ -59,6 +59,11 @@ func _run() -> void:
|
|||
assert(fishing_spot.present_external_catch(crab_catch))
|
||||
assert(fishing_spot.state == FishingSpot.FishingState.SHOWING_CATCH)
|
||||
assert(not player.is_movement_enabled())
|
||||
# Network attempt cleanup may restore the base movement flag before the
|
||||
# presentation finishes. The showcase owns a separate local-input lock.
|
||||
player.set_movement_enabled(true)
|
||||
assert(not bool(player.call("_is_movement_input_enabled")))
|
||||
assert(not bool(player.call("_is_camera_input_enabled")))
|
||||
Input.action_release("fish_primary")
|
||||
await process_frame
|
||||
assert(not bool(fishing_spot.get("_put_away_press_armed")))
|
||||
|
|
@ -85,6 +90,8 @@ func _run() -> void:
|
|||
await process_frame
|
||||
assert(fishing_spot.state == FishingSpot.FishingState.READY)
|
||||
assert(player.is_movement_enabled())
|
||||
assert(bool(player.call("_is_movement_input_enabled")))
|
||||
assert(bool(player.call("_is_camera_input_enabled")))
|
||||
assert(player.inventory.contains_catch_id(crab_catch.catch_id))
|
||||
assert(bool(player.get("_active_item_is_net")))
|
||||
print("Gathering showcase validation: PASS")
|
||||
|
|
|
|||
|
|
@ -181,6 +181,20 @@ func _run() -> void:
|
|||
catch_profile.barrier_count_min = 0
|
||||
catch_profile.barrier_count_max = 0
|
||||
catch_controller.start_encounter(catch_profile, 1.0, 1)
|
||||
var alternate_echo := InputEventKey.new()
|
||||
alternate_echo.physical_keycode = KEY_QUOTELEFT
|
||||
alternate_echo.pressed = true
|
||||
alternate_echo.echo = true
|
||||
assert(alternate_echo.is_action(&"reel_alternate"))
|
||||
fishing_spot._unhandled_input(alternate_echo)
|
||||
assert(not bool(catch_controller.get("_reel_input_held")))
|
||||
var alternate_key_press := InputEventKey.new()
|
||||
alternate_key_press.physical_keycode = KEY_QUOTELEFT
|
||||
alternate_key_press.pressed = true
|
||||
assert(alternate_key_press.is_action(&"reel_alternate"))
|
||||
fishing_spot._unhandled_input(alternate_key_press)
|
||||
assert(bool(catch_controller.get("_reel_input_held")))
|
||||
catch_controller.set_reel_input(false)
|
||||
var alternate_press := InputEventAction.new()
|
||||
alternate_press.action = &"reel_alternate"
|
||||
alternate_press.pressed = true
|
||||
|
|
|
|||
|
|
@ -1068,6 +1068,8 @@ func _show_speech_bubble(
|
|||
var voice_profile_id: String = VoiceProfilesType.DEFAULT_ID
|
||||
var sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
|
||||
var speaker_avatar := _spawn.get_avatar(peer_id)
|
||||
if speaker_avatar != null:
|
||||
speaker_avatar.play_speech_visual(reveal_seconds)
|
||||
if not requested_voice_profile_id.is_empty():
|
||||
voice_profile_id = VoiceProfilesType.sanitized_id(
|
||||
requested_voice_profile_id
|
||||
|
|
@ -1360,10 +1362,13 @@ func _update_speech() -> void:
|
|||
if bubble == null or now >= float(state.get("expires", 0.0)):
|
||||
if bubble != null:
|
||||
bubble.queue_free()
|
||||
var expired_avatar := _spawn.get_avatar(peer_id)
|
||||
if expired_avatar != null:
|
||||
expired_avatar.stop_speech_visual()
|
||||
_speech.erase(peer_id)
|
||||
continue
|
||||
var avatar := _spawn.get_avatar(peer_id)
|
||||
var camera := _player.get_gameplay_camera()
|
||||
var camera := _player.get_active_gameplay_camera()
|
||||
if avatar == null or camera == null:
|
||||
bubble.hide()
|
||||
continue
|
||||
|
|
@ -1824,4 +1829,7 @@ func _on_peer_removed(peer_id: int) -> void:
|
|||
var bubble := state.get("bubble") as PanelContainer
|
||||
if bubble != null:
|
||||
bubble.queue_free()
|
||||
var avatar := _spawn.get_avatar(peer_id)
|
||||
if avatar != null:
|
||||
avatar.stop_speech_visual()
|
||||
_speech.erase(peer_id)
|
||||
|
|
|
|||
|
|
@ -143,14 +143,17 @@ func _on_recovery_requested(
|
|||
or not is_instance_valid(triggered_player)
|
||||
):
|
||||
return
|
||||
recovery_starting.emit()
|
||||
_generation += 1
|
||||
_entry_position = _player.global_position
|
||||
_recovery_root_basis = _player.global_basis
|
||||
_fishing_spot.begin_water_recovery()
|
||||
_player.prepare_for_water_recovery()
|
||||
# Snapshot the normalized gameplay state before recovery_starting closes
|
||||
# menus for the handoff. Those closures deliberately disable movement and
|
||||
# camera input, which are recovery-owned locks rather than states to restore.
|
||||
_prior_movement_enabled = _player.is_movement_enabled()
|
||||
_prior_camera_input_enabled = _player.is_camera_input_enabled()
|
||||
recovery_starting.emit()
|
||||
_player.set_movement_enabled(false)
|
||||
_player.set_camera_input_enabled(false)
|
||||
_player.set_water_recovery_active(true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue