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:
|
func begin_water_recovery() -> void:
|
||||||
_external_input_blocked = true
|
_external_input_blocked = true
|
||||||
if state in [
|
if state == FishingState.SHOWING_CATCH:
|
||||||
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:
|
|
||||||
_secure_showcase_catch_for_recovery()
|
_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:
|
func end_water_recovery() -> void:
|
||||||
|
|
@ -538,6 +539,12 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
alternate_reel and state == FishingState.FIGHTING
|
alternate_reel and state == FishingState.FIGHTING
|
||||||
):
|
):
|
||||||
return
|
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()
|
var selected_item: ItemDataType = _get_active_item()
|
||||||
if (
|
if (
|
||||||
state == FishingState.READY
|
state == FishingState.READY
|
||||||
|
|
@ -948,7 +955,6 @@ func _on_cast_completed() -> void:
|
||||||
if _selected_fish == null:
|
if _selected_fish == null:
|
||||||
_cleanup_attempt("nothing is biting here.", &"invalid")
|
_cleanup_attempt("nothing is biting here.", &"invalid")
|
||||||
return
|
return
|
||||||
_consume_active_bait()
|
|
||||||
|
|
||||||
state = FishingState.WAITING_FOR_BITE
|
state = FishingState.WAITING_FOR_BITE
|
||||||
_active_player.set_fishing_visual(true)
|
_active_player.set_fishing_visual(true)
|
||||||
|
|
@ -975,11 +981,13 @@ func _on_cast_completed() -> void:
|
||||||
status_changed.emit("")
|
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():
|
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):
|
if _local_bag == null or not _local_bag.remove_item(_active_player.active_bait_id, 1):
|
||||||
_active_player.unequip_bait()
|
_active_player.unequip_bait()
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func roll_bite_wait_time() -> float:
|
func roll_bite_wait_time() -> float:
|
||||||
|
|
@ -1131,14 +1139,18 @@ func _activate_bite(confirmation_override: bool = false) -> void:
|
||||||
return
|
return
|
||||||
|
|
||||||
_set_bite_confirmation_pending(false)
|
_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)
|
_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
|
||||||
|
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()
|
_start_fight_audio()
|
||||||
bite_activated.emit()
|
bite_activated.emit()
|
||||||
status_changed.emit("fish on!")
|
status_changed.emit("fish on!")
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ var target: Vector3
|
||||||
var bobber_position: Vector3
|
var bobber_position: Vector3
|
||||||
var fish_id: StringName
|
var fish_id: StringName
|
||||||
var rod_id: StringName
|
var rod_id: StringName
|
||||||
|
var bait_id: StringName
|
||||||
var bait_tags: Array[StringName] = []
|
var bait_tags: Array[StringName] = []
|
||||||
var lure_effects: Array[StringName] = []
|
var lure_effects: Array[StringName] = []
|
||||||
var encounter_seed: int = 0
|
var encounter_seed: int = 0
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ func cancel_local_attempt(reason: String = "Fishing attempt ended.") -> void:
|
||||||
if _session.is_host():
|
if _session.is_host():
|
||||||
_cancel_attempt(peer_id, reason)
|
_cancel_attempt(peer_id, reason)
|
||||||
else:
|
else:
|
||||||
|
_pending_local_bait_by_request.erase(attempt.request_id)
|
||||||
submit_cancel_request.rpc_id(1, attempt.attempt_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
|
owns_authoritative_bag
|
||||||
and (
|
and (
|
||||||
avatar_bag == null
|
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.bobber_position = authoritative_target
|
||||||
attempt.fish_id = selected_fish.id
|
attempt.fish_id = selected_fish.id
|
||||||
attempt.rod_id = rod.item_id
|
attempt.rod_id = rod.item_id
|
||||||
|
attempt.bait_id = bait_id
|
||||||
attempt.bait_tags = _bait_tags_for_request(data)
|
attempt.bait_tags = _bait_tags_for_request(data)
|
||||||
attempt.lure_effects.clear()
|
attempt.lure_effects.clear()
|
||||||
if lure != null:
|
if lure != null:
|
||||||
|
|
@ -564,7 +566,6 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void:
|
||||||
_cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.")
|
_cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.")
|
||||||
return
|
return
|
||||||
attempt.bite_confirmation_pending = false
|
attempt.bite_confirmation_pending = false
|
||||||
attempt.phase = NetworkFishingAttempt.Phase.FIGHTING
|
|
||||||
attempt.encounter_seed = _new_seed()
|
attempt.encounter_seed = _new_seed()
|
||||||
var selector := FishSelectorType.new()
|
var selector := FishSelectorType.new()
|
||||||
selector.active_rod = rod
|
selector.active_rod = rod
|
||||||
|
|
@ -575,6 +576,17 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void:
|
||||||
if fish_catch == null or not fish_catch.is_valid():
|
if fish_catch == null or not fish_catch.is_valid():
|
||||||
_cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.")
|
_cancel_attempt(attempt.owner_peer_id, "Fishing attempt ended.")
|
||||||
return
|
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.catch_payload = fish_catch.to_network_dict()
|
||||||
attempt.controller.start_authoritative_encounter(
|
attempt.controller.start_authoritative_encounter(
|
||||||
fish.catch_profile,
|
fish.catch_profile,
|
||||||
|
|
@ -1031,6 +1043,13 @@ func _apply_public_outcome(data: Dictionary) -> void:
|
||||||
if peer_id == _session.get_local_peer_id():
|
if peer_id == _session.get_local_peer_id():
|
||||||
if str(data["outcome"]) != "catch":
|
if str(data["outcome"]) != "catch":
|
||||||
if not _session.is_host():
|
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)
|
_attempts.erase(peer_id)
|
||||||
local_attempt_ended.emit(
|
local_attempt_ended.emit(
|
||||||
StringName(str(data["outcome"])),
|
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.
|
# On the host this replaces the same authoritative value with itself.
|
||||||
if not _session.is_host():
|
if not _session.is_host():
|
||||||
_attempts[peer_id] = attempt
|
_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)
|
local_cast_accepted.emit(attempt.attempt_id, target)
|
||||||
else:
|
else:
|
||||||
var presentation := _get_remote_presentation(peer_id)
|
var presentation := _get_remote_presentation(peer_id)
|
||||||
|
|
@ -1140,6 +1145,22 @@ func _apply_bite_started(data: Dictionary) -> void:
|
||||||
return
|
return
|
||||||
var peer_id: int = data["owner_peer_id"]
|
var peer_id: int = data["owner_peer_id"]
|
||||||
if peer_id == _session.get_local_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"]))
|
local_bite_started.emit(str(data["attempt_id"]))
|
||||||
else:
|
else:
|
||||||
var presentation := _get_remote_presentation(peer_id)
|
var presentation := _get_remote_presentation(peer_id)
|
||||||
|
|
@ -1260,12 +1281,12 @@ func _dispose_attempt(peer_id: int) -> void:
|
||||||
attempt.controller.reset()
|
attempt.controller.reset()
|
||||||
attempt.controller.queue_free()
|
attempt.controller.queue_free()
|
||||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
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()
|
peer_id == _session.get_local_peer_id()
|
||||||
and _fishing_spot != null
|
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)
|
avatar.set_movement_enabled(true)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1307,6 +1328,7 @@ func _clear_all() -> void:
|
||||||
_result_acknowledgements.clear()
|
_result_acknowledgements.clear()
|
||||||
_last_cast_time.clear()
|
_last_cast_time.clear()
|
||||||
_last_input_time.clear()
|
_last_input_time.clear()
|
||||||
|
_pending_local_bait_by_request.clear()
|
||||||
_snapshot_accumulator = 0.0
|
_snapshot_accumulator = 0.0
|
||||||
_local_input_sequence = 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_REFERENCE_LONG_SIDE_PX: float = 1280.0
|
||||||
const CATCH_PRESENTATION_MIN_TEXTURE_SCALE: float = 0.1
|
const CATCH_PRESENTATION_MIN_TEXTURE_SCALE: float = 0.1
|
||||||
const CATCH_PRESENTATION_MAX_TEXTURE_SCALE: float = 20.0
|
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
|
# Add future networked emote animation IDs here. The protocol accepts unknown
|
||||||
# safe IDs so newer clients can extend it, but Player only presents actions
|
# safe IDs so newer clients can extend it, but Player only presents actions
|
||||||
# explicitly approved by this catalog.
|
# 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 BLINK_DURATION_SECONDS: float = 0.11
|
||||||
const CHARACTER_CALL_MOUTH_ID: String = "open_ah"
|
const CHARACTER_CALL_MOUTH_ID: String = "open_ah"
|
||||||
const CHARACTER_CALL_MOUTH_DURATION_SECONDS: float = 0.16
|
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 BASE_REEL_SPEED: float = 0.16
|
||||||
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
|
const LANDING_DUST_MIN_FALL_SPEED: float = 2.5
|
||||||
const NETWORK_EXTRAPOLATION_LIMIT_SECONDS: float = 0.25
|
const NETWORK_EXTRAPOLATION_LIMIT_SECONDS: float = 0.25
|
||||||
|
|
@ -227,6 +235,42 @@ func play_character_call_visual() -> void:
|
||||||
_apply_presented_appearance()
|
_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:
|
func set_fishing_visual(active: bool) -> void:
|
||||||
if (
|
if (
|
||||||
active
|
active
|
||||||
|
|
@ -295,6 +339,10 @@ func _apply_presented_appearance() -> void:
|
||||||
if _fighting_visual_active:
|
if _fighting_visual_active:
|
||||||
presented_appearance = appearance_snapshot.duplicate(true)
|
presented_appearance = appearance_snapshot.duplicate(true)
|
||||||
presented_appearance["eyes"] = FIGHTING_EYES_ID
|
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 _character_call_mouth_active:
|
||||||
if presented_appearance == appearance_snapshot:
|
if presented_appearance == appearance_snapshot:
|
||||||
presented_appearance = appearance_snapshot.duplicate(true)
|
presented_appearance = appearance_snapshot.duplicate(true)
|
||||||
|
|
@ -505,6 +553,9 @@ var _blink_seconds_remaining: float = 0.0
|
||||||
var _blink_rng := RandomNumberGenerator.new()
|
var _blink_rng := RandomNumberGenerator.new()
|
||||||
var _character_call_mouth_active: bool = false
|
var _character_call_mouth_active: bool = false
|
||||||
var _character_call_visual_generation: int = 0
|
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_active: bool = false
|
||||||
var _fishing_visual_phase: FishingVisualPhase = FishingVisualPhase.NONE
|
var _fishing_visual_phase: FishingVisualPhase = FishingVisualPhase.NONE
|
||||||
var _fishing_after_release_pending: bool = false
|
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:
|
if _free_camera == null or _free_camera_body == null:
|
||||||
_set_free_camera_active(false)
|
_set_free_camera_active(false)
|
||||||
return
|
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(
|
var input_vector: Vector2 = Input.get_vector(
|
||||||
"move_left",
|
"move_left",
|
||||||
"move_right",
|
"move_right",
|
||||||
|
|
@ -2981,6 +3036,8 @@ func _begin_catch_showcase_now(
|
||||||
) -> void:
|
) -> void:
|
||||||
if _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE:
|
if _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE:
|
||||||
_cancel_pocket_visual()
|
_cancel_pocket_visual()
|
||||||
|
if local_control_enabled:
|
||||||
|
set_local_input_suppressed(CATCH_SHOWCASE_INPUT_OWNER, true)
|
||||||
_showcase_animation_active = true
|
_showcase_animation_active = true
|
||||||
set_fishing_visual(false)
|
set_fishing_visual(false)
|
||||||
_kill_showcase_camera_tween()
|
_kill_showcase_camera_tween()
|
||||||
|
|
@ -3149,6 +3206,8 @@ func end_catch_showcase(
|
||||||
not _showcase_visual_rotation_stored
|
not _showcase_visual_rotation_stored
|
||||||
and _showcase_camera_snapshot == null
|
and _showcase_camera_snapshot == null
|
||||||
):
|
):
|
||||||
|
if local_control_enabled:
|
||||||
|
set_local_input_suppressed(CATCH_SHOWCASE_INPUT_OWNER, false)
|
||||||
if restored_callback.is_valid():
|
if restored_callback.is_valid():
|
||||||
restored_callback.call()
|
restored_callback.call()
|
||||||
return
|
return
|
||||||
|
|
@ -3310,6 +3369,8 @@ func _complete_showcase_restore(
|
||||||
and Input.is_action_pressed("camera_drag")
|
and Input.is_action_pressed("camera_drag")
|
||||||
)
|
)
|
||||||
_showcase_camera_snapshot = null
|
_showcase_camera_snapshot = null
|
||||||
|
if local_control_enabled:
|
||||||
|
set_local_input_suppressed(CATCH_SHOWCASE_INPUT_OWNER, false)
|
||||||
if restored_callback.is_valid():
|
if restored_callback.is_valid():
|
||||||
restored_callback.call()
|
restored_callback.call()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ func _run() -> void:
|
||||||
await physics_frame
|
await physics_frame
|
||||||
|
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
var sprint_dust := player.get_node("%SprintDust") as SprintDustTrail
|
var sprint_dust := player.get_node("%SprintDust") as SprintDustTrail
|
||||||
assert(sprint_dust != null)
|
assert(sprint_dust != null)
|
||||||
assert(sprint_dust.get_active_puff_count() == 0)
|
assert(sprint_dust.get_active_puff_count() == 0)
|
||||||
|
|
@ -310,6 +311,55 @@ func _run() -> void:
|
||||||
right_stick_motion.axis_value = 0.0
|
right_stick_motion.axis_value = 0.0
|
||||||
Input.parse_input_event(right_stick_motion)
|
Input.parse_input_event(right_stick_motion)
|
||||||
await process_frame
|
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")
|
game_ui.call("_on_quick_action_selected", &"freecam")
|
||||||
assert(not player.is_free_camera_active())
|
assert(not player.is_free_camera_active())
|
||||||
game_ui.call("_on_quick_action_selected", &"hud")
|
game_ui.call("_on_quick_action_selected", &"hud")
|
||||||
|
|
@ -754,7 +804,6 @@ func _run() -> void:
|
||||||
assert(not service.is_active() and not toolbar.visible)
|
assert(not service.is_active() and not toolbar.visible)
|
||||||
|
|
||||||
print("Art tools validation: PASS")
|
print("Art tools validation: PASS")
|
||||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
|
||||||
session.disconnect_session("")
|
session.disconnect_session("")
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
for _frame: int in 4:
|
for _frame: int in 4:
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,17 @@ func _run() -> void:
|
||||||
var fishing_status := game_ui.get_node("%StatusLabel") as Label
|
var fishing_status := game_ui.get_node("%StatusLabel") as Label
|
||||||
var fishing_panel := game_ui.get_node("%FishingPanel") as PanelContainer
|
var fishing_panel := game_ui.get_node("%FishingPanel") as PanelContainer
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
|
var item_catalog := main.get("item_catalog") as ItemCatalog
|
||||||
assert(session.is_host())
|
assert(session.is_host())
|
||||||
assert(not session.is_open_host())
|
assert(not session.is_open_host())
|
||||||
assert(service != null and fishing_spot != null and player != null)
|
assert(service != null and fishing_spot != null and player != null)
|
||||||
assert(player.hotbar.get_selected_item_id() == &"basic_fishing_rod")
|
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(
|
var fresh_root := main.get_node(
|
||||||
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
|
"TestWorld/Regions/GeneratedWorldRegion/WaterBodies/FreshWaterBodies"
|
||||||
) as Node3D
|
) as Node3D
|
||||||
|
|
@ -90,6 +97,7 @@ func _run() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
|
assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
|
||||||
assert(service.has_local_attempt())
|
assert(service.has_local_attempt())
|
||||||
|
assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
|
||||||
assert(fishing_status.text.is_empty())
|
assert(fishing_status.text.is_empty())
|
||||||
assert(not fishing_status.visible)
|
assert(not fishing_status.visible)
|
||||||
assert(not fishing_panel.visible)
|
assert(not fishing_panel.visible)
|
||||||
|
|
@ -124,6 +132,7 @@ func _run() -> void:
|
||||||
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
|
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
|
||||||
assert(player.is_movement_enabled())
|
assert(player.is_movement_enabled())
|
||||||
assert(not bobber.visible)
|
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
|
# A private host rolls and retains the authoritative catch before the fight
|
||||||
# so its quality selects the barrier band clients receive in snapshots.
|
# so its quality selects the barrier band clients receive in snapshots.
|
||||||
|
|
@ -141,9 +150,11 @@ func _run() -> void:
|
||||||
attempts = service.get("_attempts")
|
attempts = service.get("_attempts")
|
||||||
attempt = attempts.get(session.get_local_peer_id())
|
attempt = attempts.get(session.get_local_peer_id())
|
||||||
assert(attempt != null)
|
assert(attempt != null)
|
||||||
|
assert(player.bag.get_quantity(&"worms") == bait_quantity_before)
|
||||||
service.call("_start_bite", attempt)
|
service.call("_start_bite", attempt)
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(attempt.phase == NetworkFishingAttempt.Phase.FIGHTING)
|
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
|
var catalog: FishPool = main.get("fish_catalog") as FishPool
|
||||||
assert(catalog != null)
|
assert(catalog != null)
|
||||||
var fish: FishData = catalog.get_fish_by_id(attempt.fish_id)
|
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_change_hotbar_selection())
|
||||||
assert(fishing_spot.can_open_fishing_shop())
|
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")
|
print("Fishing authority validation: PASS")
|
||||||
session.disconnect_session("")
|
session.disconnect_session("")
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,9 @@ func _run_client() -> void:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert(service.has_local_attempt())
|
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
|
var fishing_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < fishing_deadline
|
Time.get_ticks_msec() < fishing_deadline
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,11 @@ func _run() -> void:
|
||||||
assert(fishing_spot.present_external_catch(crab_catch))
|
assert(fishing_spot.present_external_catch(crab_catch))
|
||||||
assert(fishing_spot.state == FishingSpot.FishingState.SHOWING_CATCH)
|
assert(fishing_spot.state == FishingSpot.FishingState.SHOWING_CATCH)
|
||||||
assert(not player.is_movement_enabled())
|
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")
|
Input.action_release("fish_primary")
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(not bool(fishing_spot.get("_put_away_press_armed")))
|
assert(not bool(fishing_spot.get("_put_away_press_armed")))
|
||||||
|
|
@ -85,6 +90,8 @@ func _run() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(fishing_spot.state == FishingSpot.FishingState.READY)
|
assert(fishing_spot.state == FishingSpot.FishingState.READY)
|
||||||
assert(player.is_movement_enabled())
|
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(player.inventory.contains_catch_id(crab_catch.catch_id))
|
||||||
assert(bool(player.get("_active_item_is_net")))
|
assert(bool(player.get("_active_item_is_net")))
|
||||||
print("Gathering showcase validation: PASS")
|
print("Gathering showcase validation: PASS")
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,20 @@ func _run() -> void:
|
||||||
catch_profile.barrier_count_min = 0
|
catch_profile.barrier_count_min = 0
|
||||||
catch_profile.barrier_count_max = 0
|
catch_profile.barrier_count_max = 0
|
||||||
catch_controller.start_encounter(catch_profile, 1.0, 1)
|
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()
|
var alternate_press := InputEventAction.new()
|
||||||
alternate_press.action = &"reel_alternate"
|
alternate_press.action = &"reel_alternate"
|
||||||
alternate_press.pressed = true
|
alternate_press.pressed = true
|
||||||
|
|
|
||||||
|
|
@ -1068,6 +1068,8 @@ func _show_speech_bubble(
|
||||||
var voice_profile_id: String = VoiceProfilesType.DEFAULT_ID
|
var voice_profile_id: String = VoiceProfilesType.DEFAULT_ID
|
||||||
var sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
|
var sample_set_id: String = VoiceProfilesType.DEFAULT_SAMPLE_SET_ID
|
||||||
var speaker_avatar := _spawn.get_avatar(peer_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():
|
if not requested_voice_profile_id.is_empty():
|
||||||
voice_profile_id = VoiceProfilesType.sanitized_id(
|
voice_profile_id = VoiceProfilesType.sanitized_id(
|
||||||
requested_voice_profile_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 or now >= float(state.get("expires", 0.0)):
|
||||||
if bubble != null:
|
if bubble != null:
|
||||||
bubble.queue_free()
|
bubble.queue_free()
|
||||||
|
var expired_avatar := _spawn.get_avatar(peer_id)
|
||||||
|
if expired_avatar != null:
|
||||||
|
expired_avatar.stop_speech_visual()
|
||||||
_speech.erase(peer_id)
|
_speech.erase(peer_id)
|
||||||
continue
|
continue
|
||||||
var avatar := _spawn.get_avatar(peer_id)
|
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:
|
if avatar == null or camera == null:
|
||||||
bubble.hide()
|
bubble.hide()
|
||||||
continue
|
continue
|
||||||
|
|
@ -1824,4 +1829,7 @@ func _on_peer_removed(peer_id: int) -> void:
|
||||||
var bubble := state.get("bubble") as PanelContainer
|
var bubble := state.get("bubble") as PanelContainer
|
||||||
if bubble != null:
|
if bubble != null:
|
||||||
bubble.queue_free()
|
bubble.queue_free()
|
||||||
|
var avatar := _spawn.get_avatar(peer_id)
|
||||||
|
if avatar != null:
|
||||||
|
avatar.stop_speech_visual()
|
||||||
_speech.erase(peer_id)
|
_speech.erase(peer_id)
|
||||||
|
|
|
||||||
|
|
@ -143,14 +143,17 @@ func _on_recovery_requested(
|
||||||
or not is_instance_valid(triggered_player)
|
or not is_instance_valid(triggered_player)
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
recovery_starting.emit()
|
|
||||||
_generation += 1
|
_generation += 1
|
||||||
_entry_position = _player.global_position
|
_entry_position = _player.global_position
|
||||||
_recovery_root_basis = _player.global_basis
|
_recovery_root_basis = _player.global_basis
|
||||||
_fishing_spot.begin_water_recovery()
|
_fishing_spot.begin_water_recovery()
|
||||||
_player.prepare_for_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_movement_enabled = _player.is_movement_enabled()
|
||||||
_prior_camera_input_enabled = _player.is_camera_input_enabled()
|
_prior_camera_input_enabled = _player.is_camera_input_enabled()
|
||||||
|
recovery_starting.emit()
|
||||||
_player.set_movement_enabled(false)
|
_player.set_movement_enabled(false)
|
||||||
_player.set_camera_input_enabled(false)
|
_player.set_camera_input_enabled(false)
|
||||||
_player.set_water_recovery_active(true)
|
_player.set_water_recovery_active(true)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue