ani
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
extends Node3D
|
||||
class_name SkinnedPlayerModel
|
||||
|
||||
const MECHA_KIT_SCRIPT := preload("res://characters/anime_mecha_kit.gd")
|
||||
|
||||
## A player model loaded from a game-ready GLB (produced by tools/pipeline.py)
|
||||
## with a Mixamo-compatible skeleton and the canonical animation set.
|
||||
##
|
||||
@@ -18,6 +20,7 @@ class_name SkinnedPlayerModel
|
||||
## - first_person_mode = false → full third-person model for other players.
|
||||
|
||||
@export var model_path: String = ""
|
||||
@export var mecha_theme: String = ""
|
||||
@export var first_person_mode: bool = false
|
||||
@export var shadows_only: bool = false
|
||||
@export var facing_flip: bool = true # glTF forward is +Z; players face -Z
|
||||
@@ -89,20 +92,21 @@ const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
|
||||
"RunForward", "RunBackward", "RunLeft", "RunRight", "Dance",
|
||||
"EmoteStretch", "EmoteCall", "EmoteYes", "EmoteNo", "PistolIdle"]
|
||||
|
||||
const BLEND_TIME := 0.32
|
||||
const BLEND_TIME := 0.36
|
||||
## Per-clip blend overrides. Reaction moves still need to read as instant, but
|
||||
## nothing cuts hard any more — every clip cross-fades. Locomotion gets the
|
||||
## longest fades because Idle<->Walk<->Run<->Sprint switch constantly as speed
|
||||
## drifts across their thresholds, and that is where hard cuts were most
|
||||
## visible.
|
||||
const BLEND_TIMES := {
|
||||
"Dash": 0.14, "Jump": 0.16, "Hit": 0.10, "Land": 0.16,
|
||||
"Slide": 0.22, "Death": 0.20, "Throw": 0.14, "PistolReload": 0.24,
|
||||
"Dash": 0.20, "Jump": 0.24, "Hit": 0.14, "Land": 0.28,
|
||||
"Slide": 0.30, "Death": 0.34, "Throw": 0.20, "PistolReload": 0.30,
|
||||
"Idle": 0.42, "PistolIdle": 0.42, "Walk": 0.40, "Run": 0.40, "Sprint": 0.40,
|
||||
"CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.28,
|
||||
"WallRunLeft": 0.18, "WallRunRight": 0.18,
|
||||
"WallCling": 0.28, "Grapple": 0.28,
|
||||
"CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.32,
|
||||
"WallRunLeft": 0.26, "WallRunRight": 0.26,
|
||||
"WallCling": 0.32, "Grapple": 0.32,
|
||||
}
|
||||
const GROUND_STATE_BLEND := 0.30
|
||||
|
||||
## Named gameplay actions -> (clip, lock seconds). Networked via the
|
||||
## controller's synced action counter.
|
||||
@@ -120,11 +124,14 @@ var loaded: bool = false
|
||||
## performance's forward axis with gameplay travel; it never rotates bones or
|
||||
## manufactures poses.
|
||||
var _motion_root: Node3D
|
||||
var _wall_run_velocity_world := Vector3.ZERO
|
||||
var _wall_glide_velocity_world := Vector3.ZERO
|
||||
var _motion_yaw := 0.0
|
||||
|
||||
var _resolved_clips: Dictionary = {} # canonical name -> actual clip name
|
||||
var _current_clip: String = ""
|
||||
## True only for the ignored local evaluation builds made from the copyrighted
|
||||
## pilot reference. Shipping characters retain the original jet-glide fallback.
|
||||
var _has_titanfall_motion_reference := false
|
||||
var _weapon_attachment: BoneAttachment3D
|
||||
var _grapple_attachment: BoneAttachment3D
|
||||
var _grapple_shoulder_attachment: BoneAttachment3D
|
||||
@@ -138,6 +145,7 @@ var _rig_info: Dictionary = {}
|
||||
## the same sidecar; drives the per-class cel look and answers `surfaces_of()`.
|
||||
var _surfaces: SkinSurfaces = null
|
||||
var _spring_mod: SpringBones
|
||||
var _mecha_kit: AnimeMechaKit
|
||||
var is_holding_weapon: bool = false
|
||||
## Which hold archetype the equipped weapon uses — see WeaponHoldProfiles. Read
|
||||
## by `_process` (a blade releases the off arm) and by the checks.
|
||||
@@ -189,6 +197,12 @@ var _target_strafe: float = 0.0
|
||||
var _target_fwd: float = 0.0
|
||||
var _target_ads: float = 0.0
|
||||
var _target_wall: float = 0.0
|
||||
var _loco_blend_target := Vector2.ZERO
|
||||
var _loco_blend_visual := Vector2.ZERO
|
||||
var _loco_scale_target := 1.0
|
||||
var _loco_scale_visual := 1.0
|
||||
var _crouch_blend_target := 0.0
|
||||
var _crouch_blend_visual := 0.0
|
||||
var _cur_ads: float = 0.0
|
||||
var _cur_wall: float = 0.0
|
||||
var _owner_visible: bool = false
|
||||
@@ -206,15 +220,23 @@ func load_model(path: String) -> void:
|
||||
skeleton = null
|
||||
animation_player = null
|
||||
_motion_root = null
|
||||
_wall_run_velocity_world = Vector3.ZERO
|
||||
_wall_glide_velocity_world = Vector3.ZERO
|
||||
_motion_yaw = 0.0
|
||||
_loco_blend_target = Vector2.ZERO
|
||||
_loco_blend_visual = Vector2.ZERO
|
||||
_loco_scale_target = 1.0
|
||||
_loco_scale_visual = 1.0
|
||||
_crouch_blend_target = 0.0
|
||||
_crouch_blend_visual = 0.0
|
||||
loaded = false
|
||||
_resolved_clips.clear()
|
||||
_current_clip = ""
|
||||
_has_titanfall_motion_reference = false
|
||||
_current_state_node = ""
|
||||
_weapon_attachment = null
|
||||
_grapple_attachment = null
|
||||
_grapple_shoulder_attachment = null
|
||||
_mecha_kit = null
|
||||
# Cleared, not left standing: a model with no skeleton never reaches the
|
||||
# branch that reloads these, and would otherwise be described by the LAST
|
||||
# character's sidecar.
|
||||
@@ -240,6 +262,7 @@ func load_model(path: String) -> void:
|
||||
else:
|
||||
_ensure_meshes_bound(scene)
|
||||
_rig_info = _load_rig_info(path)
|
||||
_apply_rig_animation_speeds()
|
||||
# `weights_authored` is MEASURED at build time, not inferred from which
|
||||
# pipeline branch ran: a model that arrives unrigged still gets a
|
||||
# sidecar, and its nearest-bone weights still need the repair below.
|
||||
@@ -303,6 +326,12 @@ func load_model(path: String) -> void:
|
||||
# what used to render a flat-coloured model as a black silhouette.
|
||||
_surfaces = SkinSurfaces.from_rig_info(_rig_info)
|
||||
LevelMaterials.apply_character_look(scene, _surfaces)
|
||||
if skeleton and mecha_theme != "":
|
||||
_apply_mecha_base_suit(scene)
|
||||
_mecha_kit = MECHA_KIT_SCRIPT.new()
|
||||
_mecha_kit.name = "AnimeMechaKit"
|
||||
add_child(_mecha_kit)
|
||||
_mecha_kit.setup(skeleton, _rig_info.get("roles", {}), mecha_theme)
|
||||
if animation_player:
|
||||
_index_animations()
|
||||
_setup_anim_tree(scene)
|
||||
@@ -319,6 +348,42 @@ func load_model(path: String) -> void:
|
||||
_play_ground_locomotion(0.0, false)
|
||||
|
||||
|
||||
## The imported garment is the actual zero-clearance synthetic skin. Recoloring
|
||||
## that authored, skinned surface is what makes the robotics follow every curve
|
||||
## and deformation; the procedural kit then only has to add seams and tools.
|
||||
func _apply_mecha_base_suit(scene: Node) -> void:
|
||||
if mecha_theme != "sakura" or _surfaces == null:
|
||||
return
|
||||
for mesh_node in scene.find_children("*", "MeshInstance3D", true, false):
|
||||
var mesh_instance := mesh_node as MeshInstance3D
|
||||
if mesh_instance == null or mesh_instance.mesh == null:
|
||||
continue
|
||||
for surface_index in mesh_instance.mesh.get_surface_count():
|
||||
var source := mesh_instance.mesh.surface_get_material(surface_index) as BaseMaterial3D
|
||||
if source == null:
|
||||
continue
|
||||
var resolved: Array = _surfaces.resolve(
|
||||
mesh_instance.name, surface_index, source)
|
||||
var surface_class := str(resolved[0])
|
||||
var is_garment := surface_class == SkinSurfaces.CLOTH
|
||||
# Sakura's import keeps the face on material "Head" and the remaining
|
||||
# anatomy on material "Body". Turning only that second surface into the
|
||||
# membrane closes the waist, thigh, arm and hand gaps without tinting her
|
||||
# face or eyes.
|
||||
var is_body_membrane := surface_class == SkinSurfaces.BODY \
|
||||
and source.resource_name.to_lower() == "body"
|
||||
if not is_garment and not is_body_membrane:
|
||||
continue
|
||||
var current := mesh_instance.get_surface_override_material(surface_index)
|
||||
if current is ShaderMaterial:
|
||||
var membrane := (current as ShaderMaterial).duplicate(true) as ShaderMaterial
|
||||
membrane.set_shader_parameter("albedo_color",
|
||||
Color("303852") if is_garment else Color("3b4563"))
|
||||
membrane.set_shader_parameter("shadow_color", Color("171b31"))
|
||||
membrane.set_shader_parameter("mid_tone", 0.86)
|
||||
membrane.set_shader_parameter("rim_strength", 0.13)
|
||||
mesh_instance.set_surface_override_material(surface_index, membrane)
|
||||
|
||||
## Collision hulls for the cloth solver, taken from the MESH rather than from
|
||||
## the sidecar: bone name -> the points that bone drives, in its own rest space.
|
||||
##
|
||||
@@ -414,6 +479,30 @@ func _cloth_hulls(scene: Node) -> Dictionary:
|
||||
return out
|
||||
|
||||
|
||||
## Character-specific authored gait speeds, measured from source root motion
|
||||
## before it is stripped for gameplay. This keeps native clips at their
|
||||
## original cadence instead of imposing the generic library's timing.
|
||||
func _apply_rig_animation_speeds() -> void:
|
||||
var speeds: Dictionary = _rig_info.get("animation_speeds", {})
|
||||
if speeds.is_empty():
|
||||
return
|
||||
walk_anim_reference_speed = maxf(
|
||||
float(speeds.get("walk", walk_anim_reference_speed)), 0.01)
|
||||
run_anim_reference_speed = maxf(
|
||||
float(speeds.get("run", run_anim_reference_speed)), 0.01)
|
||||
sprint_anim_reference_speed = maxf(
|
||||
float(speeds.get("sprint", sprint_anim_reference_speed)), 0.01)
|
||||
strafe_walk_anim_reference_speed = maxf(
|
||||
float(speeds.get("strafe_walk", strafe_walk_anim_reference_speed)), 0.01)
|
||||
strafe_run_anim_reference_speed = maxf(
|
||||
float(speeds.get("strafe_run", strafe_run_anim_reference_speed)), 0.01)
|
||||
print("SkinnedPlayerModel: native gait speeds walk=%.2f run=%.2f sprint=%.2f strafe=%.2f/%.2f m/s" % [
|
||||
walk_anim_reference_speed, run_anim_reference_speed,
|
||||
sprint_anim_reference_speed, strafe_walk_anim_reference_speed,
|
||||
strafe_run_anim_reference_speed,
|
||||
])
|
||||
|
||||
|
||||
## Read the rig sidecar that tools/retarget.py writes next to the GLB.
|
||||
##
|
||||
## Its presence is also the signal that this model kept its OWN skeleton and
|
||||
@@ -475,6 +564,8 @@ func _ensure_meshes_bound(scene: Node) -> void:
|
||||
## loop modes (glTF has no loop flag, so we set it here).
|
||||
func _index_animations() -> void:
|
||||
var available := animation_player.get_animation_list()
|
||||
_has_titanfall_motion_reference = \
|
||||
_find_clip(available, "ReviewWallHangFront") != ""
|
||||
for canonical in CLIP_FALLBACKS:
|
||||
for candidate in CLIP_FALLBACKS[canonical]:
|
||||
var match_name := _find_clip(available, candidate)
|
||||
@@ -576,8 +667,8 @@ func _setup_anim_tree(scene: Node) -> void:
|
||||
var upper := AnimationNodeOneShot.new()
|
||||
# The upper-body one-shot pops in and out over the locomotion clip, so its
|
||||
# own fades matter as much as the locomotion cross-fade.
|
||||
upper.fadein_time = 0.14
|
||||
upper.fadeout_time = 0.22
|
||||
upper.fadein_time = 0.20
|
||||
upper.fadeout_time = 0.30
|
||||
upper.filter_enabled = true
|
||||
_upper_anim = AnimationNodeAnimation.new()
|
||||
bt.add_node("upper_clip", _upper_anim, Vector2(0, 240))
|
||||
@@ -740,9 +831,13 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
||||
"slide":
|
||||
clip = "Slide"
|
||||
"wall_run":
|
||||
# Separate authored performances: the contact leg and balancing arm
|
||||
# are different on each side. This is never a renamed ground run.
|
||||
clip = "WallRunRight" if _target_wall > 0.0 else "WallRunLeft"
|
||||
# The jets still own propulsion, but the authored pilot pack supplies a
|
||||
# distinct compact wall-performance for each side. This is body motion,
|
||||
# not gameplay root motion or procedural feet planted against the wall.
|
||||
if _has_titanfall_motion_reference:
|
||||
clip = "WallRunRight" if _target_wall < 0.0 else "WallRunLeft"
|
||||
else:
|
||||
clip = "Grapple"
|
||||
"wall_cling":
|
||||
clip = "WallCling"
|
||||
"wall_climb":
|
||||
@@ -769,11 +864,9 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
||||
var scale := 1.0
|
||||
if clip == "Slide":
|
||||
scale = clampf(speed / run_anim_reference_speed, 0.75, 1.8)
|
||||
elif clip in ["WallRunLeft", "WallRunRight"]:
|
||||
scale = clampf(speed / wall_run_anim_reference_speed, 0.85, 1.65)
|
||||
elif clip == "Grapple":
|
||||
# Grapple is a held, authored flight pose. Its cadence does not
|
||||
# represent footsteps and must not accelerate with pull velocity.
|
||||
elif clip == "Grapple" or clip in ["WallRunLeft", "WallRunRight"]:
|
||||
# Grapple is held, and the wall clips are compact propulsion loops.
|
||||
# Neither should become a frantic treadmill at high traversal speed.
|
||||
scale = 1.0
|
||||
elif clip == "Dash":
|
||||
scale = clampf(speed / 13.0, 0.75, 1.8)
|
||||
@@ -790,22 +883,25 @@ func set_locomotion(strafe: float, fwd: float, ads: float) -> void:
|
||||
_target_ads = clampf(ads, 0.0, 1.0)
|
||||
|
||||
|
||||
## Wall side during a wall run: -1 wall on left, +1 wall on right, 0 none.
|
||||
## Used only to release the wall-side weapon hand; the clip owns body motion.
|
||||
## Wall side during a wall glide: -1 wall on left, +1 wall on right, 0 none.
|
||||
func set_wall_side(side: float) -> void:
|
||||
_target_wall = clampf(side, -1.0, 1.0)
|
||||
|
||||
|
||||
## Gameplay velocity for aligning the authored lateral wall-run performance.
|
||||
## Uisco supplies the body lean, contact leg, arm balance, and cadence. This
|
||||
## supplies only the wall-tangent coordinate frame that an offline clip cannot
|
||||
## know.
|
||||
## Gameplay velocity aligns the held glide silhouette with the live wall
|
||||
## tangent. The mechanic still reports `wall_run` internally for save/network
|
||||
## compatibility, but the character presentation is entirely propulsion-based.
|
||||
func set_wall_glide_motion(velocity_world: Vector3) -> void:
|
||||
_wall_glide_velocity_world = velocity_world
|
||||
|
||||
|
||||
## Compatibility entry point for older controller scenes and debug tools.
|
||||
func set_wall_run_motion(velocity_world: Vector3) -> void:
|
||||
_wall_run_velocity_world = velocity_world
|
||||
set_wall_glide_motion(velocity_world)
|
||||
|
||||
|
||||
## Regression telemetry: the authored model's forward direction in world space.
|
||||
func wall_run_forward_debug() -> Vector3:
|
||||
func wall_glide_forward_debug() -> Vector3:
|
||||
if not is_instance_valid(_motion_root):
|
||||
return Vector3.ZERO
|
||||
var source_forward := Vector3.FORWARD if facing_flip else Vector3.BACK
|
||||
@@ -814,6 +910,32 @@ func wall_run_forward_debug() -> Vector3:
|
||||
).normalized()
|
||||
|
||||
|
||||
func wall_run_forward_debug() -> Vector3:
|
||||
return wall_glide_forward_debug()
|
||||
|
||||
|
||||
func set_helmet_closed(closed: bool, immediate: bool = false) -> void:
|
||||
if is_instance_valid(_mecha_kit) and _mecha_kit.has_method("set_helmet_closed"):
|
||||
_mecha_kit.set_helmet_closed(closed, immediate)
|
||||
|
||||
|
||||
func toggle_helmet() -> void:
|
||||
if is_instance_valid(_mecha_kit) and _mecha_kit.has_method("toggle_helmet"):
|
||||
_mecha_kit.toggle_helmet()
|
||||
|
||||
|
||||
func helmet_closed_debug() -> bool:
|
||||
return _mecha_kit.helmet_closed() \
|
||||
if is_instance_valid(_mecha_kit) and _mecha_kit.has_method("helmet_closed") \
|
||||
else false
|
||||
|
||||
|
||||
func helmet_progress_debug() -> float:
|
||||
return _mecha_kit.helmet_progress_debug() \
|
||||
if is_instance_valid(_mecha_kit) and _mecha_kit.has_method("helmet_progress_debug") \
|
||||
else 0.0
|
||||
|
||||
|
||||
## Live grapple mechanics layered over the authored airborne performance.
|
||||
## The clip supplies weight and secondary motion; gameplay supplies the one
|
||||
## constraint an authored fixed pose cannot know: the actual cable direction.
|
||||
@@ -876,6 +998,8 @@ var _lod_timer: float = 0.0
|
||||
func _process(delta: float) -> void:
|
||||
_update_cloth_lod(delta)
|
||||
_update_motion_orientation(delta)
|
||||
_update_mecha_nozzles()
|
||||
_update_animation_smoothing(delta)
|
||||
if not _pose_mod:
|
||||
return
|
||||
_hold_weapon_still()
|
||||
@@ -926,11 +1050,9 @@ func _process(delta: float) -> void:
|
||||
"slide":
|
||||
hold_l = 0.0 # trailing arm braces the ground
|
||||
"wall_run":
|
||||
# The wall-side arm reaches for the wall.
|
||||
if _cur_wall > 0.05:
|
||||
hold_r = 0.0
|
||||
elif _cur_wall < -0.05:
|
||||
hold_l = 0.0
|
||||
# A glide is stabilized by the suit; neither hand needs to touch
|
||||
# the wall, so weapon posture can remain coherent.
|
||||
pass
|
||||
"grapple":
|
||||
hold_l = 0.0 # left hand rides the grapple line
|
||||
_pose_mod.hold_r_target = hold_r
|
||||
@@ -949,14 +1071,14 @@ func _update_motion_orientation(delta: float) -> void:
|
||||
var wanted_yaw := 0.0
|
||||
if _pose_mod and _pose_mod.state == "wall_run":
|
||||
var local_velocity := global_transform.basis.inverse() \
|
||||
* _wall_run_velocity_world
|
||||
* _wall_glide_velocity_world
|
||||
local_velocity.y = 0.0
|
||||
if local_velocity.length_squared() > 0.01:
|
||||
var source_forward := Vector3.FORWARD \
|
||||
if facing_flip else Vector3.BACK
|
||||
wanted_yaw = source_forward.signed_angle_to(
|
||||
local_velocity.normalized(), Vector3.UP)
|
||||
var blend := 1.0 - exp(-14.0 * delta)
|
||||
var blend := 1.0 - exp(-8.5 * delta)
|
||||
_motion_yaw = lerp_angle(_motion_yaw, wanted_yaw, blend)
|
||||
_motion_root.rotation.y = _motion_yaw
|
||||
|
||||
@@ -990,21 +1112,45 @@ func _update_cloth_lod(delta: float) -> void:
|
||||
## even though gameplay acceleration reached running speed in one physics frame.
|
||||
## That is the visible "sliding, then slow walk, then run" failure. Here the
|
||||
## BlendSpace position is physical metres/second on this call, so the feet react
|
||||
## on the same frame as the body.
|
||||
## immediately but ease over a few rendered frames instead of teleporting from
|
||||
## one gait/direction pose to another.
|
||||
func _update_animation_smoothing(delta: float) -> void:
|
||||
if not _anim_tree:
|
||||
return
|
||||
var current_speed := _loco_blend_visual.length()
|
||||
var target_speed := _loco_blend_target.length()
|
||||
var reversing := current_speed > 0.2 and target_speed > 0.2 \
|
||||
and _loco_blend_visual.dot(_loco_blend_target) < 0.0
|
||||
var response := 10.0 if target_speed > current_speed else 6.5
|
||||
if reversing:
|
||||
response = 5.5
|
||||
var position_weight := 1.0 - exp(-response * delta)
|
||||
_loco_blend_visual = _loco_blend_visual.lerp(
|
||||
_loco_blend_target, position_weight)
|
||||
_crouch_blend_visual = lerpf(_crouch_blend_visual,
|
||||
_crouch_blend_target, 1.0 - exp(-8.0 * delta))
|
||||
_loco_scale_visual = lerpf(_loco_scale_visual,
|
||||
_loco_scale_target, 1.0 - exp(-7.0 * delta))
|
||||
_anim_tree.set("parameters/loco_blend/blend_position", _loco_blend_visual)
|
||||
_anim_tree.set("parameters/crouch_blend/blend_position", _crouch_blend_visual)
|
||||
if _current_state_node in ["Locomotion", "CrouchLocomotion"]:
|
||||
_anim_tree.set("parameters/loco_scale/scale", _loco_scale_visual)
|
||||
|
||||
|
||||
func _play_ground_locomotion(speed: float, crouched: bool) -> void:
|
||||
if not _anim_tree:
|
||||
return
|
||||
var state_node := "CrouchLocomotion" if crouched else "Locomotion"
|
||||
if _current_state_node != state_node:
|
||||
_state_trans.xfade_time = 0.16
|
||||
_state_trans.xfade_time = GROUND_STATE_BLEND
|
||||
_anim_tree.set("parameters/state/transition_request", state_node)
|
||||
_current_state_node = state_node
|
||||
|
||||
if crouched:
|
||||
var authored_speed := minf(speed, crouch_anim_reference_speed)
|
||||
_anim_tree.set("parameters/crouch_blend/blend_position", authored_speed)
|
||||
_anim_tree.set("parameters/loco_scale/scale",
|
||||
maxf(1.0, speed / maxf(crouch_anim_reference_speed, 0.01)))
|
||||
_crouch_blend_target = authored_speed
|
||||
_loco_scale_target = maxf(1.0,
|
||||
speed / maxf(crouch_anim_reference_speed, 0.01))
|
||||
_current_clip = _resolved_clips.get(
|
||||
"CrouchWalk" if speed > 0.1 else "Crouch", "")
|
||||
return
|
||||
@@ -1028,10 +1174,9 @@ func _play_ground_locomotion(speed: float, crouched: bool) -> void:
|
||||
)
|
||||
var authored_limit := 1.0 / maxf(inverse_limit, 0.01)
|
||||
var authored_speed := minf(speed, authored_limit)
|
||||
_anim_tree.set("parameters/loco_blend/blend_position",
|
||||
direction * authored_speed)
|
||||
_anim_tree.set("parameters/loco_scale/scale",
|
||||
maxf(1.0, speed / maxf(authored_limit, 0.01)))
|
||||
_loco_blend_target = direction * authored_speed
|
||||
_loco_scale_target = maxf(1.0,
|
||||
speed / maxf(authored_limit, 0.01))
|
||||
|
||||
var canonical := "Idle"
|
||||
if speed > 0.1:
|
||||
@@ -1069,7 +1214,7 @@ func get_brake_debug() -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
## Physical velocity currently driving the authored ground BlendSpace.
|
||||
## Smoothed visual velocity currently driving the authored ground BlendSpace.
|
||||
func locomotion_blend_debug() -> Vector2:
|
||||
if not _anim_tree:
|
||||
return Vector2.ZERO
|
||||
@@ -1077,7 +1222,11 @@ func locomotion_blend_debug() -> Vector2:
|
||||
return value if value is Vector2 else Vector2.ZERO
|
||||
|
||||
|
||||
## Velocity represented by the authored stride after playback-rate calibration.
|
||||
func locomotion_blend_target_debug() -> Vector2:
|
||||
return _loco_blend_target
|
||||
|
||||
|
||||
## Visual velocity represented by the authored stride after playback-rate calibration.
|
||||
## The raw BlendSpace position stops at the source clip's measured velocity;
|
||||
## TimeScale covers faster gameplay without changing the pose or foot phase.
|
||||
func locomotion_effective_speed_debug() -> float:
|
||||
@@ -1091,7 +1240,7 @@ func locomotion_effective_speed_debug() -> float:
|
||||
## Animated world-space anchors for the two shoulder and two foot jets.
|
||||
## Offsets use the skeleton frame, not each limb's twisting frame, so the
|
||||
## nozzles stay on the character's back/heels through every authored clip.
|
||||
func get_jet_socket_world_positions() -> Array[Vector3]:
|
||||
func _animated_jet_socket_world_positions() -> Array[Vector3]:
|
||||
if not skeleton:
|
||||
return []
|
||||
var roles_and_fallbacks := [
|
||||
@@ -1118,6 +1267,31 @@ func get_jet_socket_world_positions() -> Array[Vector3]:
|
||||
return sockets
|
||||
|
||||
|
||||
func _update_mecha_nozzles() -> void:
|
||||
if not is_instance_valid(_mecha_kit) or not skeleton:
|
||||
return
|
||||
var sockets := _animated_jet_socket_world_positions()
|
||||
if sockets.size() == 4:
|
||||
_mecha_kit.update_nozzles(sockets, skeleton.global_transform.basis)
|
||||
|
||||
|
||||
func get_jet_socket_world_positions() -> Array[Vector3]:
|
||||
var sockets := _animated_jet_socket_world_positions()
|
||||
if is_instance_valid(_mecha_kit) and sockets.size() == 4:
|
||||
_mecha_kit.update_nozzles(sockets, skeleton.global_transform.basis)
|
||||
return _mecha_kit.nozzle_world_positions()
|
||||
return sockets
|
||||
|
||||
|
||||
func mecha_nozzle_count_debug() -> int:
|
||||
return _mecha_kit.nozzle_count() if is_instance_valid(_mecha_kit) else 0
|
||||
|
||||
|
||||
func mecha_detail_part_count_debug() -> int:
|
||||
return _mecha_kit.detail_part_count_debug() \
|
||||
if is_instance_valid(_mecha_kit) else 0
|
||||
|
||||
|
||||
## Every canonical clip this character resolved to something real.
|
||||
##
|
||||
## Canonical rather than raw, because the raw names differ per character — one
|
||||
@@ -1442,7 +1616,7 @@ func _role_bone(role: String, fallbacks: Array) -> int:
|
||||
var roles: Dictionary = _rig_info.get("roles", {})
|
||||
var actual := String(roles.get(role, ""))
|
||||
if actual != "" and skeleton:
|
||||
var b := skeleton.find_bone(actual)
|
||||
var b := RigRoles.find_imported_bone(skeleton, actual)
|
||||
if b >= 0:
|
||||
return b
|
||||
return _find_bone(fallbacks)
|
||||
@@ -1688,8 +1862,8 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
_hold_r = lerpf(_hold_r, hold_r_target, t)
|
||||
_hold_l = lerpf(_hold_l, hold_l_target, t)
|
||||
|
||||
# The authored clip owns cadence and weight. Grappling adds the physical
|
||||
# pendulum frame before the arm constraint is solved.
|
||||
# Authored clips own the full silhouette. Traversal adds only constraints
|
||||
# an offline clip cannot know, such as the live grapple cable.
|
||||
_apply_grapple_body(skel, delta)
|
||||
if absf(aim_pitch) > 0.01 and not grapple_active:
|
||||
_apply_aim_pitch(skel)
|
||||
|
||||
Reference in New Issue
Block a user