Integrate per-save identity and interaction UI
This commit is contained in:
parent
ac82a28f3b
commit
4fda6e97f6
58 changed files with 3471 additions and 461 deletions
236
player/player.gd
236
player/player.gd
|
|
@ -388,6 +388,17 @@ class ShowcaseCameraSnapshot:
|
|||
var camera_dragging: bool
|
||||
|
||||
|
||||
class ConversationCameraSnapshot:
|
||||
extends RefCounted
|
||||
|
||||
var yaw_transform: Transform3D
|
||||
var yaw_rotation: Vector3
|
||||
var pitch_rotation: Vector3
|
||||
var spring_length: float
|
||||
var target_zoom: float
|
||||
var camera_dragging: bool
|
||||
|
||||
|
||||
@export_category("Movement")
|
||||
@export var walk_speed: float = 2.25
|
||||
@export var sprint_speed: float = 4.5
|
||||
|
|
@ -446,6 +457,11 @@ var _rv_previous_camera_environment: Environment
|
|||
@export_range(0.0, 1.0, 0.01) var controller_trigger_threshold: float = 0.55
|
||||
@export_range(1.0, 40.0, 0.5) var free_camera_speed: float = 10.0
|
||||
@export_range(1.0, 10.0, 0.5) var free_camera_sprint_multiplier: float = 3.0
|
||||
@export_category("Conversation Camera")
|
||||
@export_range(0.05, 1.0, 0.01) var conversation_camera_transition_seconds: float = 0.42
|
||||
@export_range(-45.0, 20.0, 1.0) var conversation_camera_pitch_degrees: float = -9.0
|
||||
@export_range(1.5, 8.0, 0.1, "suffix:m") var conversation_camera_distance: float = 3.65
|
||||
@export_range(0.5, 2.5, 0.05, "suffix:m") var conversation_camera_target_height: float = 1.3
|
||||
|
||||
@onready var _visuals: Node3D = %Visuals
|
||||
@onready var _character_rig: Node3D = get_node_or_null(
|
||||
|
|
@ -521,6 +537,9 @@ var _showcase_restore_tween: Tween
|
|||
var _showcase_camera_tween: Tween
|
||||
var _showcase_camera_snapshot: ShowcaseCameraSnapshot
|
||||
var _showcase_restore_generation: int = 0
|
||||
var _conversation_camera_tween: Tween
|
||||
var _conversation_camera_snapshot: ConversationCameraSnapshot
|
||||
var _conversation_camera_generation: int = 0
|
||||
var _network_peer_id: int = 0
|
||||
var _network_authoritative_simulation: bool = false
|
||||
var _network_interpolation_enabled: bool = false
|
||||
|
|
@ -1070,7 +1089,18 @@ func _simulate_movement_physics(delta: float) -> void:
|
|||
if jump_requested:
|
||||
_set_sitting(false, local_control_enabled)
|
||||
else:
|
||||
velocity = Vector3.ZERO
|
||||
# Keep the body participating in physics while seated. In particular,
|
||||
# move_and_slide() refreshes Godot's floor state, so a stale grounded
|
||||
# flag from prediction or a delayed packet cannot trap this early-return
|
||||
# path forever after the capsule has moved into the air.
|
||||
velocity.x = 0.0
|
||||
velocity.z = 0.0
|
||||
if not is_on_floor():
|
||||
velocity.y -= _gravity * fall_gravity_multiplier * delta
|
||||
move_and_slide()
|
||||
if not is_on_floor():
|
||||
_sit_after_landing = true
|
||||
_set_sitting(false)
|
||||
_network_jump_pending = false
|
||||
return
|
||||
if _water_recovery_active:
|
||||
|
|
@ -1726,6 +1756,10 @@ func toggle_sitting() -> void:
|
|||
return
|
||||
if not _can_begin_sitting():
|
||||
_sit_after_landing = true
|
||||
# Queue and repeat the intent until an owner snapshot acknowledges it.
|
||||
# Previously this branch returned before marking a local sitting intent,
|
||||
# so a delayed authoritative audit could race the deferred sit.
|
||||
_set_sitting(false, local_control_enabled)
|
||||
return
|
||||
_set_sitting(should_sit, local_control_enabled)
|
||||
|
||||
|
|
@ -1750,7 +1784,11 @@ func _set_sitting(
|
|||
return
|
||||
_sitting = should_sit
|
||||
if _sitting:
|
||||
velocity = Vector3.ZERO
|
||||
# Sitting stops horizontal locomotion, but vertical motion remains owned by
|
||||
# physics. Clearing Y here allowed a delayed seated state to repeatedly
|
||||
# cancel gravity in a high-latency session.
|
||||
velocity.x = 0.0
|
||||
velocity.z = 0.0
|
||||
_character_animation_name = &""
|
||||
_update_character_animation()
|
||||
|
||||
|
|
@ -1803,6 +1841,24 @@ func apply_authoritative_network_sitting_state(should_sit: bool) -> void:
|
|||
_set_sitting(should_sit)
|
||||
|
||||
|
||||
func _apply_local_prediction_sitting_state(should_sit: bool) -> void:
|
||||
# Owner audits can arrive several physics frames behind the locally predicted
|
||||
# jump. Treat an authoritative seated bit as an intent until this capsule is
|
||||
# actually grounded; applying it directly zeros vertical velocity and lets a
|
||||
# stream of high-latency snapshots hold the local player in mid-air.
|
||||
if _local_network_jump_intent_pending:
|
||||
_sit_after_landing = false
|
||||
_set_sitting(false)
|
||||
return
|
||||
if should_sit and not _can_begin_sitting():
|
||||
_sit_after_landing = true
|
||||
_set_sitting(false)
|
||||
return
|
||||
if should_sit:
|
||||
_sit_after_landing = false
|
||||
_set_sitting(should_sit)
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# Camera dragging must observe the complete mouse gesture before GUI controls
|
||||
# get a chance to consume one part of it. Do not claim the event here: the
|
||||
|
|
@ -2048,6 +2104,162 @@ func get_active_gameplay_camera() -> Camera3D:
|
|||
return _camera
|
||||
|
||||
|
||||
func begin_shop_conversation_camera(npc_world_position: Vector3) -> bool:
|
||||
if (
|
||||
not local_control_enabled
|
||||
or _camera_yaw == null
|
||||
or _camera_pitch == null
|
||||
or _spring_arm == null
|
||||
):
|
||||
return false
|
||||
if _free_camera_active:
|
||||
_set_free_camera_active(false)
|
||||
_kill_conversation_camera_tween()
|
||||
if _conversation_camera_snapshot == null:
|
||||
_conversation_camera_snapshot = ConversationCameraSnapshot.new()
|
||||
_conversation_camera_snapshot.yaw_transform = _camera_yaw.transform
|
||||
_conversation_camera_snapshot.yaw_rotation = _camera_yaw.rotation
|
||||
_conversation_camera_snapshot.pitch_rotation = _camera_pitch.rotation
|
||||
_conversation_camera_snapshot.spring_length = _spring_arm.spring_length
|
||||
_conversation_camera_snapshot.target_zoom = _target_zoom
|
||||
_conversation_camera_snapshot.camera_dragging = _camera_dragging
|
||||
_set_camera_dragging(false)
|
||||
|
||||
var pair_direction := npc_world_position - global_position
|
||||
pair_direction.y = 0.0
|
||||
if pair_direction.length_squared() <= 0.000001:
|
||||
pair_direction = Vector3.RIGHT
|
||||
else:
|
||||
pair_direction = pair_direction.normalized()
|
||||
# Frame the midpoint, with the camera's right axis running from the player
|
||||
# toward the NPC. This naturally places the player left and shopkeeper right.
|
||||
var target_world_yaw := atan2(-pair_direction.z, pair_direction.x)
|
||||
var target_local_yaw := target_world_yaw - global_rotation.y
|
||||
var shortest_target_yaw := (
|
||||
_camera_yaw.rotation.y
|
||||
+ wrapf(target_local_yaw - _camera_yaw.rotation.y, -PI, PI)
|
||||
)
|
||||
var pair_midpoint := (global_position + npc_world_position) * 0.5
|
||||
pair_midpoint.y = global_position.y + conversation_camera_target_height
|
||||
var target_position := to_local(pair_midpoint)
|
||||
var duration := maxf(conversation_camera_transition_seconds, 0.05)
|
||||
_conversation_camera_tween = create_tween()
|
||||
_conversation_camera_tween.set_parallel(true)
|
||||
_conversation_camera_tween.set_trans(Tween.TRANS_QUAD)
|
||||
_conversation_camera_tween.set_ease(Tween.EASE_IN_OUT)
|
||||
_conversation_camera_tween.tween_property(
|
||||
_camera_yaw, "rotation:y", shortest_target_yaw, duration
|
||||
)
|
||||
_conversation_camera_tween.tween_property(
|
||||
_camera_yaw, "position", target_position, duration
|
||||
)
|
||||
_conversation_camera_tween.tween_property(
|
||||
_camera_pitch,
|
||||
"rotation:x",
|
||||
deg_to_rad(conversation_camera_pitch_degrees),
|
||||
duration,
|
||||
)
|
||||
_conversation_camera_tween.tween_property(
|
||||
_spring_arm,
|
||||
"spring_length",
|
||||
maxf(conversation_camera_distance, 0.1),
|
||||
duration,
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
func restore_shop_conversation_camera(
|
||||
immediate: bool = false,
|
||||
restored_callback: Callable = Callable(),
|
||||
) -> void:
|
||||
_kill_conversation_camera_tween()
|
||||
if _conversation_camera_snapshot == null:
|
||||
if restored_callback.is_valid():
|
||||
restored_callback.call()
|
||||
return
|
||||
if immediate:
|
||||
_complete_conversation_camera_restore(restored_callback)
|
||||
return
|
||||
var yaw_target := _conversation_camera_snapshot.yaw_rotation
|
||||
yaw_target.y = (
|
||||
_camera_yaw.rotation.y
|
||||
+ wrapf(yaw_target.y - _camera_yaw.rotation.y, -PI, PI)
|
||||
)
|
||||
var generation := _conversation_camera_generation
|
||||
var duration := maxf(conversation_camera_transition_seconds, 0.05)
|
||||
_conversation_camera_tween = create_tween()
|
||||
_conversation_camera_tween.set_parallel(true)
|
||||
_conversation_camera_tween.set_trans(Tween.TRANS_QUAD)
|
||||
_conversation_camera_tween.set_ease(Tween.EASE_IN_OUT)
|
||||
_conversation_camera_tween.tween_property(
|
||||
_camera_yaw, "rotation", yaw_target, duration
|
||||
)
|
||||
_conversation_camera_tween.tween_property(
|
||||
_camera_yaw,
|
||||
"position",
|
||||
_conversation_camera_snapshot.yaw_transform.origin,
|
||||
duration,
|
||||
)
|
||||
_conversation_camera_tween.tween_property(
|
||||
_camera_pitch,
|
||||
"rotation",
|
||||
_conversation_camera_snapshot.pitch_rotation,
|
||||
duration,
|
||||
)
|
||||
_conversation_camera_tween.tween_property(
|
||||
_spring_arm,
|
||||
"spring_length",
|
||||
_conversation_camera_snapshot.spring_length,
|
||||
duration,
|
||||
)
|
||||
_conversation_camera_tween.finished.connect(
|
||||
_on_conversation_camera_restore_finished.bind(
|
||||
generation, restored_callback
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func is_shop_conversation_camera_active() -> bool:
|
||||
return _conversation_camera_snapshot != null
|
||||
|
||||
|
||||
func _kill_conversation_camera_tween() -> void:
|
||||
if (
|
||||
_conversation_camera_tween != null
|
||||
and _conversation_camera_tween.is_valid()
|
||||
):
|
||||
_conversation_camera_tween.kill()
|
||||
_conversation_camera_tween = null
|
||||
_conversation_camera_generation += 1
|
||||
|
||||
|
||||
func _on_conversation_camera_restore_finished(
|
||||
generation: int,
|
||||
restored_callback: Callable,
|
||||
) -> void:
|
||||
if generation != _conversation_camera_generation:
|
||||
return
|
||||
_conversation_camera_tween = null
|
||||
_complete_conversation_camera_restore(restored_callback)
|
||||
|
||||
|
||||
func _complete_conversation_camera_restore(
|
||||
restored_callback: Callable,
|
||||
) -> void:
|
||||
if _conversation_camera_snapshot != null:
|
||||
_camera_yaw.transform = _conversation_camera_snapshot.yaw_transform
|
||||
_camera_pitch.rotation = _conversation_camera_snapshot.pitch_rotation
|
||||
_spring_arm.spring_length = _conversation_camera_snapshot.spring_length
|
||||
_target_zoom = _conversation_camera_snapshot.target_zoom
|
||||
_set_camera_dragging(
|
||||
_conversation_camera_snapshot.camera_dragging
|
||||
and Input.is_action_pressed("camera_drag")
|
||||
)
|
||||
_conversation_camera_snapshot = null
|
||||
if restored_callback.is_valid():
|
||||
restored_callback.call()
|
||||
|
||||
|
||||
func _set_target_zoom(value: float) -> void:
|
||||
_target_zoom = clampf(
|
||||
value,
|
||||
|
|
@ -2526,7 +2738,7 @@ func apply_local_prediction_correction(
|
|||
):
|
||||
_clear_local_network_jump_intent()
|
||||
if not _sitting_intent_pending:
|
||||
_set_sitting(bool(parsed["sitting"]))
|
||||
_apply_local_prediction_sitting_state(bool(parsed["sitting"]))
|
||||
var authoritative_position: Vector3 = parsed["position"]
|
||||
var transit_seconds: float = resolve_local_prediction_transit_seconds(
|
||||
acknowledged_input,
|
||||
|
|
@ -3322,9 +3534,23 @@ func get_body_center_position() -> Vector3:
|
|||
|
||||
|
||||
func get_chat_anchor_position() -> Vector3:
|
||||
return get_nameplate_anchor_position()
|
||||
|
||||
|
||||
func get_nameplate_anchor_position() -> Vector3:
|
||||
# Character head meshes keep stable authored bounds while skeletal animation
|
||||
# plays. Restricting the anchor to them prevents rods, nets, held catches,
|
||||
# and showcase props from making overhead UI bounce during an action.
|
||||
var highest_y := -INF
|
||||
for node: Node in _visuals.find_children("*", "MeshInstance3D", true, false):
|
||||
var mesh_instance := node as MeshInstance3D
|
||||
var skeleton := get_node_or_null(
|
||||
"Visuals/CharacterRig/CharacterRig/Skeleton3D"
|
||||
) as Skeleton3D
|
||||
for head_name: String in ["head_round", "head_pointy"]:
|
||||
var mesh_instance := (
|
||||
skeleton.get_node_or_null(head_name) as MeshInstance3D
|
||||
if skeleton != null
|
||||
else null
|
||||
)
|
||||
if mesh_instance == null or not mesh_instance.is_visible_in_tree():
|
||||
continue
|
||||
var bounds := mesh_instance.get_aabb()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue