feat: add animated net gathering

This commit is contained in:
Alexander Sellite 2026-08-17 17:50:30 -04:00
parent b252b2f20c
commit f0ddcccaad
38 changed files with 1298 additions and 76 deletions

View file

@ -1,4 +1,6 @@
[gd_scene load_steps=3 format=3]
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://player/tool_socket_attachment.gd" id="1_socket"]
[sub_resource type="CylinderMesh" id="FishingRodMesh"]
top_radius = 0.025
@ -11,6 +13,8 @@ roughness = 0.8
[node name="FishingRodAttachment" type="BoneAttachment3D"]
bone_name = "rod_socket"
script = ExtResource("1_socket")
aligned_mount_path = NodePath("FishingRod")
[node name="FishingRod" type="Node3D" parent="."]

88
player/net_attachment.gd Normal file
View file

@ -0,0 +1,88 @@
extends "res://player/tool_socket_attachment.gd"
const TextureSamplingPolicyScript := preload(
"res://main/texture_sampling_policy.gd"
)
const SPRING_ROOT_BONE: StringName = &"net_mid"
const SPRING_END_BONE: StringName = &"net_tip"
const IMPACT_BONE: StringName = &"net_rim"
@export_range(0.0, 10.0, 0.05) var spring_stiffness: float = 1.8
@export_range(0.0, 1.0, 0.01) var spring_drag: float = 0.68
@export_range(0.0, 10.0, 0.05) var spring_gravity: float = 0.3
@export_range(0.01, 0.5, 0.01) var spring_end_length: float = 0.1
@onready var _catching_net: Node3D = %CatchingNet
@onready var _net_model: Node3D = %NetModel
var _spring: SpringBoneSimulator3D
var _spring_was_visible: bool = false
var _net_skeleton: Skeleton3D
func _ready() -> void:
super._ready()
TextureSamplingPolicyScript.enforce_subtree(_net_model)
_configure_spring()
_sync_spring_activity()
func _process(_delta: float) -> void:
_sync_spring_activity()
func _configure_spring() -> void:
_net_skeleton = _net_model.find_child(
"Skeleton3D", true, false
) as Skeleton3D
if _net_skeleton == null:
push_error("Imported net model is missing its Skeleton3D.")
return
if _net_skeleton.find_bone(SPRING_ROOT_BONE) < 0:
push_error("Imported net model is missing bone %s." % SPRING_ROOT_BONE)
return
if _net_skeleton.find_bone(SPRING_END_BONE) < 0:
push_error("Imported net model is missing bone %s." % SPRING_END_BONE)
return
if _net_skeleton.find_bone(IMPACT_BONE) < 0:
push_error("Imported net model is missing bone %s." % IMPACT_BONE)
return
_spring = SpringBoneSimulator3D.new()
_spring.name = "NetSpring"
_spring.set_setting_count(1)
_spring.set_root_bone_name(0, SPRING_ROOT_BONE)
_spring.set_end_bone_name(0, SPRING_END_BONE)
_spring.set_extend_end_bone(0, true)
_spring.set_end_bone_direction(
0, SkeletonModifier3D.BONE_DIRECTION_FROM_PARENT
)
_spring.set_end_bone_length(0, spring_end_length)
_spring.set_stiffness(0, spring_stiffness)
_spring.set_drag(0, spring_drag)
_spring.set_gravity(0, spring_gravity)
_spring.set_gravity_direction(0, Vector3.DOWN)
_net_skeleton.add_child(_spring)
func get_impact_world_position() -> Vector3:
if _net_skeleton == null:
return _catching_net.global_position
var impact_bone_index := _net_skeleton.find_bone(IMPACT_BONE)
if impact_bone_index < 0:
return _catching_net.global_position
return _net_skeleton.global_transform * (
_net_skeleton.get_bone_global_pose(impact_bone_index).origin
)
func _sync_spring_activity() -> void:
if _spring == null:
return
var is_visible := _catching_net.is_visible_in_tree()
if is_visible and not _spring_was_visible:
_spring.active = true
_spring.reset()
elif not is_visible:
_spring.active = false
_spring_was_visible = is_visible

View file

@ -0,0 +1 @@
uid://cw5vc02hoelgm

View file

@ -1,55 +1,20 @@
[gd_scene load_steps=7 format=3]
[gd_scene load_steps=3 format=3]
[sub_resource type="StandardMaterial3D" id="HandleMaterial"]
albedo_color = Color(0.25, 0.13, 0.055, 1)
roughness = 0.9
[sub_resource type="CylinderMesh" id="HandleMesh"]
material = SubResource("HandleMaterial")
top_radius = 0.025
bottom_radius = 0.035
height = 1.25
[sub_resource type="StandardMaterial3D" id="HoopMaterial"]
albedo_color = Color(0.34, 0.39, 0.4, 1)
metallic = 0.25
roughness = 0.65
[sub_resource type="TorusMesh" id="HoopMesh"]
material = SubResource("HoopMaterial")
inner_radius = 0.24
outer_radius = 0.29
rings = 12
ring_segments = 18
[sub_resource type="StandardMaterial3D" id="NetMaterial"]
transparency = 1
albedo_color = Color(0.65, 0.78, 0.75, 0.42)
shading_mode = 0
cull_mode = 2
[sub_resource type="CylinderMesh" id="NetMesh"]
material = SubResource("NetMaterial")
top_radius = 0.24
bottom_radius = 0.08
height = 0.34
radial_segments = 12
rings = 1
[ext_resource type="Script" path="res://player/net_attachment.gd" id="1_script"]
[ext_resource type="PackedScene" path="res://art/exported/items/equipment/net.glb" id="2_model"]
[node name="NetAttachment" type="BoneAttachment3D"]
bone_name = "rod_socket"
script = ExtResource("1_script")
aligned_mount_path = NodePath("CatchingNet")
[node name="CatchingNet" type="Node3D" parent="."]
[node name="CatchingNet" type="Node3D" parent="." unique_id=947004727]
unique_name_in_owner = true
visible = false
[node name="Handle" type="MeshInstance3D" parent="CatchingNet"]
position = Vector3(0, 0.55, 0)
mesh = SubResource("HandleMesh")
[node name="ModelMount" type="Node3D" parent="CatchingNet"]
position = Vector3(-0.000217, -0.098636, -0.020218)
rotation = Vector3(0, 3.1415927, 0)
[node name="Hoop" type="MeshInstance3D" parent="CatchingNet"]
position = Vector3(0, 1.17, 0)
mesh = SubResource("HoopMesh")
[node name="Bag" type="MeshInstance3D" parent="CatchingNet"]
position = Vector3(0, 1.0, 0)
mesh = SubResource("NetMesh")
[node name="NetModel" parent="CatchingNet/ModelMount" unique_id=1627767240 instance=ExtResource("2_model")]
unique_name_in_owner = true

View file

@ -32,6 +32,7 @@ const FishingRodAttachmentScene = preload(
"res://player/fishing_rod_attachment.tscn"
)
const NetAttachmentScene = preload("res://player/net_attachment.tscn")
const NetAttachmentType = preload("res://player/net_attachment.gd")
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
const HeldItemAttachmentScene = preload(
"res://player/held_item_attachment.tscn"
@ -51,6 +52,8 @@ const CHARACTER_WALKING_ANIMATION: StringName = &"walking"
const CHARACTER_WALKING_SHOW_ANIMATION: StringName = &"walking_show"
const CHARACTER_RUNNING_ANIMATION: StringName = &"running"
const CHARACTER_RUNNING_SHOW_ANIMATION: StringName = &"running_show"
const CHARACTER_IDLE_SNEAK_ANIMATION: StringName = &"idle_sneak"
const CHARACTER_SNEAKING_ANIMATION: StringName = &"sneaking"
const CHARACTER_CASTING_ANIMATION: StringName = &"casting"
const CHARACTER_CASTING_SIT_ANIMATION: StringName = &"casting_sit"
const CHARACTER_RELEASE_ANIMATION: StringName = &"release"
@ -73,10 +76,16 @@ const CHARACTER_FISHING_ANIMATION: StringName = &"fishing"
const CHARACTER_FISHING_SIT_ANIMATION: StringName = &"fishing_sit"
const CHARACTER_FIGHTING_ANIMATION: StringName = &"fighting"
const CHARACTER_FIGHTING_SIT_ANIMATION: StringName = &"fighting_sit"
const CHARACTER_NET_DRAW_ANIMATION: StringName = &"draw"
const CHARACTER_NET_STRIKE_ANIMATION: StringName = &"strike"
# 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.
const NETWORK_ANIMATION_ACTION_IDS: Array[StringName] = []
const NETWORK_ANIMATION_ACTION_IDS: Array[StringName] = [
CHARACTER_CASTING_ANIMATION,
CHARACTER_NET_DRAW_ANIMATION,
CHARACTER_NET_STRIKE_ANIMATION,
]
enum FishingVisualPhase {
NONE,
@ -98,6 +107,7 @@ enum LocomotionState {
IDLE,
WALKING,
RUNNING,
SNEAKING,
}
const FIGHTING_EYES_ID: String = "alligator_eyes"
const BLINK_EYES_ID: String = "closed"
@ -342,6 +352,10 @@ class ShowcaseCameraSnapshot:
@export_range(0.5, 3.0, 0.05) var showcase_camera_target_height: float = 1.45
@export_range(0.01, 1.0, 0.01) var catch_presentation_base_scale: float = 0.10
@export_category("Gathering")
@export_flags_3d_physics var net_impact_collision_mask: int = 1
@export_range(0.02, 0.5, 0.01) var net_impact_radius: float = 0.16
@export_category("Camera")
@export var mouse_sensitivity: float = 0.005
@export var controller_camera_speed: float = 2.5
@ -443,6 +457,7 @@ var _network_target_locomotion_state: LocomotionState = LocomotionState.IDLE
var _network_target_animation_action_id: StringName = &""
var _network_target_animation_action_sequence: int = 0
var _network_target_animation_action_elapsed: float = 0.0
var _network_target_animation_action_paused: bool = false
var _network_snapshot_ready: bool = false
var _network_snapshot_age: float = 0.0
var _network_snapshot_jitter: float = 0.0
@ -451,8 +466,10 @@ var _local_network_jump_intent_sequence: int = -1
var _animation_action_id: StringName = &""
var _animation_action_sequence: int = 0
var _animation_action_elapsed: float = 0.0
var _animation_action_paused: bool = false
var _presented_animation_action_id: StringName = &""
var _presented_animation_action_sequence: int = -1
var _presented_animation_action_paused: bool = false
var _character_animation_name: StringName = &""
var _sitting: bool = false
var _sit_after_landing: bool = false
@ -483,6 +500,17 @@ var _fishing_after_release_pending: bool = false
var _retract_animation_completed: bool = true
var _fishing_rod: Node3D
var _catching_net: Node3D
var _catching_net_attachment: NetAttachmentType
var _net_strike_collision_armed: bool = false
var _net_strike_has_previous_sample: bool = false
var _net_strike_previous_position: Vector3
var _net_strike_previous_animation_position: float = 0.0
var _net_strike_contact_held: bool = false
var _net_strike_result_known: bool = false
var _net_strike_successful: bool = false
var _net_impact_shape := SphereShape3D.new()
var _pending_net_showcase_catch: FishCatchType
var _pending_net_showcase_remote: bool = false
var _fishing_rod_tip: Marker3D
var _fishing_rod_model_mount: Node3D
var _fishing_rod_fallback_visual: GeometryInstance3D
@ -584,6 +612,141 @@ func end_animation_action() -> void:
)
func begin_net_draw_visual() -> bool:
return begin_animation_action(
CHARACTER_NET_DRAW_ANIMATION
if is_sneaking()
else CHARACTER_CASTING_ANIMATION
)
func play_net_strike_visual() -> bool:
if begin_animation_action(CHARACTER_NET_STRIKE_ANIMATION):
_net_strike_collision_armed = local_control_enabled
_net_strike_has_previous_sample = false
_net_strike_contact_held = false
_net_strike_result_known = false
_net_strike_successful = false
return true
cancel_net_action_visual()
return false
func resolve_net_strike_visual(successful: bool) -> void:
if _animation_action_id != CHARACTER_NET_STRIKE_ANIMATION:
return
_net_strike_result_known = true
_net_strike_successful = successful
if successful:
_net_strike_collision_armed = false
if successful and _animation_action_paused:
_net_strike_contact_held = false
_set_animation_action_paused(false)
func is_net_strike_held() -> bool:
return (
_animation_action_id == CHARACTER_NET_STRIKE_ANIMATION
and _net_strike_contact_held
)
func release_net_strike_hold() -> bool:
if not is_net_strike_held():
return false
cancel_net_action_visual()
return true
func cancel_net_action_visual() -> void:
_clear_net_strike_state()
if _animation_action_id in [
CHARACTER_CASTING_ANIMATION,
CHARACTER_NET_DRAW_ANIMATION,
CHARACTER_NET_STRIKE_ANIMATION,
]:
end_animation_action()
func _clear_net_strike_state() -> void:
_net_strike_collision_armed = false
_net_strike_has_previous_sample = false
_net_strike_contact_held = false
_net_strike_result_known = false
_net_strike_successful = false
if _animation_action_paused:
_set_animation_action_paused(false)
func _set_animation_action_paused(paused: bool) -> void:
if _animation_action_id.is_empty() or _animation_action_paused == paused:
return
_apply_animation_action_state(
NetworkPlayerAnimationProtocol.make_action_state(
_animation_action_id,
_animation_action_sequence,
_animation_action_elapsed,
paused,
)
)
func _update_net_strike_contact() -> void:
if (
not local_control_enabled
or not _net_strike_collision_armed
or _animation_action_paused
or _animation_action_id != CHARACTER_NET_STRIKE_ANIMATION
or _catching_net_attachment == null
or _character_animation_player == null
or get_world_3d() == null
):
return
var current_position: Vector3 = (
_catching_net_attachment.get_impact_world_position()
)
var current_animation_position: float = (
_character_animation_player.current_animation_position
)
if not _net_strike_has_previous_sample:
_net_strike_previous_position = current_position
_net_strike_previous_animation_position = current_animation_position
_net_strike_has_previous_sample = true
return
var motion := current_position - _net_strike_previous_position
if motion.length_squared() <= 0.000001:
_net_strike_previous_animation_position = current_animation_position
return
_net_impact_shape.radius = net_impact_radius
var query := PhysicsShapeQueryParameters3D.new()
query.shape = _net_impact_shape
query.transform = Transform3D(
Basis.IDENTITY,
_net_strike_previous_position,
)
query.motion = motion
query.collision_mask = net_impact_collision_mask
query.collide_with_areas = false
query.collide_with_bodies = true
query.exclude = [get_rid()]
var fractions: PackedFloat32Array = (
get_world_3d().direct_space_state.cast_motion(query)
)
if fractions.size() >= 2 and float(fractions[1]) < 1.0:
var impact_fraction := clampf(float(fractions[0]), 0.0, 1.0)
_animation_action_elapsed = lerpf(
_net_strike_previous_animation_position,
current_animation_position,
impact_fraction,
)
_net_strike_collision_armed = false
_net_strike_contact_held = true
_set_animation_action_paused(true)
return
_net_strike_previous_position = current_position
_net_strike_previous_animation_position = current_animation_position
func _apply_animation_action_state(action_state: Dictionary) -> void:
if not NetworkPlayerAnimationProtocol.validate_action_state(action_state):
return
@ -597,10 +760,13 @@ func _apply_animation_action_state(action_state: Dictionary) -> void:
var action_changed: bool = (
action_id != _animation_action_id
or action_sequence != _animation_action_sequence
or bool(action_state.get("paused", false))
!= _animation_action_paused
)
_animation_action_id = action_id
_animation_action_sequence = action_sequence
_animation_action_elapsed = float(action_state["elapsed"])
_animation_action_paused = bool(action_state.get("paused", false))
if action_changed:
_character_animation_name = &""
_update_character_animation()
@ -637,11 +803,12 @@ func _initialize_catching_net() -> void:
if skeleton == null:
push_error("Player character skeleton is unavailable for the net.")
return
var attachment := NetAttachmentScene.instantiate() as BoneAttachment3D
var attachment := NetAttachmentScene.instantiate() as NetAttachmentType
if attachment == null:
push_error("Catching net attachment could not be instantiated.")
return
skeleton.add_child(attachment)
_catching_net_attachment = attachment
_catching_net = attachment.get_node("CatchingNet") as Node3D
if _catching_net != null:
_catching_net.visible = false
@ -760,6 +927,8 @@ func _physics_process(delta: float) -> void:
move_direction.y = 0.0
var input_strength: float = minf(input_vector.length(), 1.0)
move_direction = move_direction.normalized()
if input_strength > 0.05:
release_net_strike_hold()
var speed: float = _get_network_aware_speed()
if item_effects != null:
@ -779,13 +948,14 @@ func _physics_process(delta: float) -> void:
func _process(delta: float) -> void:
if not _animation_action_id.is_empty():
if not _animation_action_id.is_empty() and not _animation_action_paused:
_animation_action_elapsed = minf(
_animation_action_elapsed + delta,
NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS,
)
_update_blink(delta)
_update_character_animation()
_update_net_strike_contact()
_update_sprint_dust(delta)
# The hand bone supplies the attachment position, but its animated wrist
# rotation should not turn the flat fish/catch artwork edge-on. Keep each
@ -963,8 +1133,17 @@ func _update_character_animation() -> void:
)
var is_walking: bool = locomotion_state != LocomotionState.IDLE
var is_running: bool = locomotion_state == LocomotionState.RUNNING
var is_sneaking_pose: bool = (
locomotion_state == LocomotionState.SNEAKING
)
var is_moving_horizontally: bool = (
_is_presented_moving_horizontally()
)
var animation_action: Dictionary = _get_presented_animation_action()
var animation_action_id := StringName(str(animation_action.get("id", "")))
var animation_action_paused: bool = bool(
animation_action.get("paused", false)
)
var animation_action_available: bool = (
not animation_action_id.is_empty()
and supports_network_animation_action(animation_action_id)
@ -1075,6 +1254,19 @@ func _update_character_animation() -> void:
&"sitting",
&"idle_loop_sit",
]
elif is_sneaking_pose:
if is_moving_horizontally:
requested_animation = [
CHARACTER_SNEAKING_ANIMATION,
CHARACTER_WALKING_ANIMATION,
&"walking_loop",
]
else:
requested_animation = [
CHARACTER_IDLE_SNEAK_ANIMATION,
CHARACTER_IDLE_ANIMATION,
&"idle_loop",
]
elif is_running and held_show_item_visible:
requested_animation = [
CHARACTER_RUNNING_SHOW_ANIMATION,
@ -1124,11 +1316,14 @@ func _update_character_animation() -> void:
and (
animation_action_id != _presented_animation_action_id
or action_sequence != _presented_animation_action_sequence
or animation_action_paused
!= _presented_animation_action_paused
)
)
if not action_selected:
_presented_animation_action_id = &""
_presented_animation_action_sequence = -1
_presented_animation_action_paused = false
if _character_animation_name == next_animation and not action_changed:
return
_character_animation_player.play(next_animation)
@ -1136,22 +1331,58 @@ func _update_character_animation() -> void:
if action_selected:
_presented_animation_action_id = animation_action_id
_presented_animation_action_sequence = action_sequence
_presented_animation_action_paused = animation_action_paused
var elapsed: float = float(animation_action.get("elapsed", 0.0))
var animation: Animation = _character_animation_player.get_animation(
next_animation
)
if elapsed > 0.0 and animation != null and animation.length > 0.0:
if (
(elapsed > 0.0 or animation_action_paused)
and animation != null
and animation.length > 0.0
):
_character_animation_player.seek(
fposmod(elapsed, animation.length), true
)
if animation_action_paused:
_character_animation_player.pause()
func _on_character_animation_finished(animation_name: StringName) -> void:
if animation_name == CHARACTER_NET_STRIKE_ANIMATION:
if local_control_enabled:
if _net_strike_result_known and _net_strike_successful:
_clear_net_strike_state()
end_animation_action()
_begin_pending_net_showcase()
else:
var strike_animation := (
_character_animation_player.get_animation(
CHARACTER_NET_STRIKE_ANIMATION
)
)
if strike_animation != null:
_animation_action_elapsed = maxf(
strike_animation.length - 0.001,
0.0,
)
_net_strike_collision_armed = false
_net_strike_contact_held = true
_set_animation_action_paused(true)
return
if _pending_net_showcase_remote:
_begin_pending_net_showcase()
return
if (
local_control_enabled
and not _animation_action_id.is_empty()
and animation_name == _animation_action_id
):
if animation_name in [
CHARACTER_CASTING_ANIMATION,
CHARACTER_NET_DRAW_ANIMATION,
]:
return
end_animation_action()
return
if animation_name in [
@ -1551,6 +1782,7 @@ func configure_network_remote(authoritative_simulation: bool) -> void:
_network_target_animation_action_id = &""
_network_target_animation_action_sequence = 0
_network_target_animation_action_elapsed = 0.0
_network_target_animation_action_paused = false
_camera.current = false
@ -1665,6 +1897,7 @@ func make_network_snapshot(peer_id: int) -> Dictionary:
_animation_action_id,
_animation_action_sequence,
_animation_action_elapsed,
_animation_action_paused,
),
"sitting": _sitting,
"casting": _fishing_visual_phase == FishingVisualPhase.CASTING,
@ -1876,6 +2109,7 @@ func _make_animation_action_state() -> Dictionary:
_animation_action_id,
_animation_action_sequence,
_animation_action_elapsed,
_animation_action_paused,
)
@ -1890,6 +2124,9 @@ func _apply_network_target_animation_state(state: Dictionary) -> void:
_network_target_animation_action_id = StringName(str(action["id"]))
_network_target_animation_action_sequence = int(action["sequence"])
_network_target_animation_action_elapsed = float(action["elapsed"])
_network_target_animation_action_paused = bool(
action.get("paused", false)
)
func _get_authoritative_locomotion_state() -> LocomotionState:
@ -1898,14 +2135,17 @@ func _get_authoritative_locomotion_state() -> LocomotionState:
and _is_movement_input_enabled()
and not _water_recovery_active
and not _sitting
and _network_axis.length_squared() > 0.0025
):
if _network_sneak:
return LocomotionState.SNEAKING
if _network_axis.length_squared() <= 0.0025:
return LocomotionState.IDLE
return (
LocomotionState.RUNNING
if _network_sprint
else LocomotionState.WALKING
)
return _locomotion_state_from_velocity()
return _locomotion_state_from_velocity(is_sneaking())
func _get_authoritative_locomotion_id() -> StringName:
@ -1915,7 +2155,19 @@ func _get_authoritative_locomotion_id() -> StringName:
func _get_presented_locomotion_state() -> LocomotionState:
if _network_interpolation_enabled and _network_snapshot_ready:
return _network_target_locomotion_state
return _locomotion_state_from_velocity()
return _locomotion_state_from_velocity(is_sneaking())
func _is_presented_moving_horizontally() -> bool:
var presented_velocity: Vector3 = (
_network_target_velocity
if _network_interpolation_enabled and _network_snapshot_ready
else velocity
)
return Vector2(
presented_velocity.x,
presented_velocity.z,
).length_squared() > 0.0025
func _get_presented_animation_action() -> Dictionary:
@ -1924,15 +2176,23 @@ func _get_presented_animation_action() -> Dictionary:
_network_target_animation_action_id,
_network_target_animation_action_sequence,
minf(
_network_target_animation_action_elapsed + _network_snapshot_age,
_network_target_animation_action_elapsed
+ (
0.0
if _network_target_animation_action_paused
else _network_snapshot_age
),
NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS,
),
_network_target_animation_action_paused,
)
return _make_animation_action_state()
func _locomotion_id_from_state(state: LocomotionState) -> StringName:
match state:
LocomotionState.SNEAKING:
return NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING
LocomotionState.WALKING:
return NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING
LocomotionState.RUNNING:
@ -1943,6 +2203,8 @@ func _locomotion_id_from_state(state: LocomotionState) -> StringName:
func _locomotion_state_from_id(state_id: StringName) -> LocomotionState:
match state_id:
NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING:
return LocomotionState.SNEAKING
NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING:
return LocomotionState.WALKING
NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING:
@ -1955,7 +2217,11 @@ static func supports_network_animation_action(action_id: StringName) -> bool:
return action_id in NETWORK_ANIMATION_ACTION_IDS
func _locomotion_state_from_velocity() -> LocomotionState:
func _locomotion_state_from_velocity(
sneaking: bool = false,
) -> LocomotionState:
if sneaking:
return LocomotionState.SNEAKING
var horizontal_speed_squared: float = (
velocity.x * velocity.x + velocity.z * velocity.z
)
@ -2234,6 +2500,8 @@ func set_active_fishing_rod(
func set_active_catching_net(should_show: bool) -> void:
_active_item_is_net = should_show
if not should_show:
cancel_net_action_visual()
if _catching_net == null:
return
if _showcase_rod_state_stored:
@ -2602,6 +2870,14 @@ func get_cast_origin_position() -> Vector3:
func begin_catch_showcase(fish_catch: FishCatchType) -> void:
if fish_catch == null or not fish_catch.is_valid():
return
if _is_net_strike_visual_active():
_pending_net_showcase_catch = fish_catch
_pending_net_showcase_remote = false
return
_begin_catch_showcase_now(fish_catch)
func _begin_catch_showcase_now(fish_catch: FishCatchType) -> void:
if _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE:
_cancel_pocket_visual()
_showcase_animation_active = true
@ -2636,6 +2912,14 @@ func begin_catch_showcase(fish_catch: FishCatchType) -> void:
func begin_remote_catch_showcase(fish_catch: FishCatchType) -> void:
if local_control_enabled or fish_catch == null or not fish_catch.is_valid():
return
if _is_net_strike_visual_active():
_pending_net_showcase_catch = fish_catch
_pending_net_showcase_remote = true
return
_begin_remote_catch_showcase_now(fish_catch)
func _begin_remote_catch_showcase_now(fish_catch: FishCatchType) -> void:
end_catch_showcase(Callable(), true)
_showcase_animation_active = true
set_fishing_visual(false)
@ -2657,6 +2941,32 @@ func begin_remote_catch_showcase(fish_catch: FishCatchType) -> void:
_update_character_animation()
func _is_net_strike_visual_active() -> bool:
return (
_animation_action_id == CHARACTER_NET_STRIKE_ANIMATION
or _network_target_animation_action_id
== CHARACTER_NET_STRIKE_ANIMATION
or (
_character_animation_player != null
and _character_animation_player.current_animation
== CHARACTER_NET_STRIKE_ANIMATION
)
)
func _begin_pending_net_showcase() -> void:
var fish_catch := _pending_net_showcase_catch
var remote: bool = _pending_net_showcase_remote
_pending_net_showcase_catch = null
_pending_net_showcase_remote = false
if fish_catch == null or not fish_catch.is_valid():
return
if remote:
_begin_remote_catch_showcase_now(fish_catch)
else:
_begin_catch_showcase_now(fish_catch)
func end_catch_showcase(
restored_callback: Callable = Callable(),
immediate: bool = false,

View file

@ -0,0 +1,57 @@
class_name ToolSocketAttachment
extends BoneAttachment3D
@export var direction_guide_bone: StringName = &"rod_socket.001"
@export_node_path("Node3D") var aligned_mount_path: NodePath
func _ready() -> void:
_align_mount_to_direction_guide()
func _align_mount_to_direction_guide() -> void:
var skeleton := get_parent() as Skeleton3D
var mount := get_node_or_null(aligned_mount_path) as Node3D
if skeleton == null or mount == null:
push_error("Tool socket alignment requires a Skeleton3D parent and mount.")
return
var socket_index := skeleton.find_bone(bone_name)
var guide_index := skeleton.find_bone(direction_guide_bone)
if socket_index < 0 or guide_index < 0:
push_error(
"Tool socket alignment is missing %s or %s."
% [bone_name, direction_guide_bone]
)
return
if skeleton.get_bone_parent(guide_index) != socket_index:
push_error(
"Tool direction guide %s must be a child of %s."
% [direction_guide_bone, bone_name]
)
return
var socket_rest := skeleton.get_bone_global_rest(socket_index)
var guide_rest := skeleton.get_bone_global_rest(guide_index)
var guide_in_socket := socket_rest.affine_inverse() * guide_rest
var rear_direction := guide_in_socket.basis.y.normalized()
# rod_socket already aims local +Y from the hand toward the working end.
# Its child guide is perpendicular to that axis and identifies which face
# of the held tool points rearward. Projecting it onto the roll plane keeps
# imperfect authored guides from changing the tool's lengthwise direction.
rear_direction -= Vector3.UP * rear_direction.dot(Vector3.UP)
if rear_direction.is_zero_approx():
push_error("Tool direction guide has no usable direction.")
return
rear_direction = rear_direction.normalized()
var rear_roll := Vector3.BACK.signed_angle_to(
rear_direction,
Vector3.UP,
)
var mount_transform := mount.transform
mount_transform.basis = (
Basis(Vector3.UP, rear_roll).orthonormalized()
* mount_transform.basis
)
# The guide starts at the socket tip, but the held tool must remain anchored
# at the socket origin in the hand. Only its authored direction is applied.
mount_transform.origin = mount.transform.origin
mount.transform = mount_transform

View file

@ -0,0 +1 @@
uid://toolsocketalignment01