feat: emotive animations + anime speed lines — hit flinch, dance emote (B)
- SkinnedPlayerModel: generic play_oneshot() (generalizes the Land lock), Hit and Dance in the clip table (Dance loops), restartable clips - Hit flinch: victims visibly react on every peer's screen via the existing damage broadcast — no new RPC - Dance emote on B: toggles while grounded and idle, breaks on any movement; synced to remotes via synced_is_dancing (all three spawner configs) - Anime radial speed-lines overlay (canvas shader): fades in past ~1.2x walk speed, spikes on dash, widescreen-corrected clear center - New input action 'emote' (B) in project.godot 11/11 FSM tests, spawn smoke 0 failures. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
37c15bed1a
commit
5bcbcfdc3f
@@ -44,10 +44,12 @@ const CLIP_FALLBACKS := {
|
||||
"Grapple": ["Grapple", "Fall", "Jump", "Idle"],
|
||||
"Dash": ["Dash", "Sprint", "Run", "Idle"],
|
||||
"Death": ["Death", "Fall"],
|
||||
"Hit": ["Hit", "Idle"],
|
||||
"Dance": ["Dance", "Idle"],
|
||||
}
|
||||
|
||||
const LOOPING_CLIPS := ["Idle", "Walk", "Run", "Sprint", "Fall", "Crouch",
|
||||
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple"]
|
||||
"CrouchIdle", "CrouchWalk", "Slide", "WallRun", "WallCling", "Grapple", "Dance"]
|
||||
|
||||
const BLEND_TIME := 0.15
|
||||
|
||||
@@ -191,7 +193,23 @@ func _set_shadow_mode_recursive(node: Node, mode: int) -> void:
|
||||
# ── Animation state ───────────────────────────────────────────────────────────
|
||||
|
||||
var _prev_state: String = ""
|
||||
var _land_lock: float = 0.0 # seconds left where the Land one-shot owns playback
|
||||
var _oneshot_lock: float = 0.0 # seconds left where a one-shot owns playback
|
||||
var _dancing: bool = false
|
||||
|
||||
|
||||
## Play a one-shot clip (Hit reaction, Land, ...) over locomotion for
|
||||
## `lock_time` seconds; locomotion resumes afterwards.
|
||||
func play_oneshot(canonical: String, lock_time: float = 0.35) -> void:
|
||||
if not loaded or not _resolved_clips.has(canonical):
|
||||
return
|
||||
_oneshot_lock = lock_time
|
||||
_play_clip(canonical, true)
|
||||
|
||||
|
||||
## Emote toggle (Dance). Shown while grounded and near-idle; any real
|
||||
## movement breaks it (the controller clears the flag too).
|
||||
func set_dancing(on: bool) -> void:
|
||||
_dancing = on
|
||||
|
||||
|
||||
## Same contract as HumanoidModel.update_state(). Called by the movement
|
||||
@@ -200,15 +218,16 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
||||
if not loaded or not animation_player:
|
||||
return
|
||||
|
||||
# A heavy landing plays the Land one-shot before locomotion resumes.
|
||||
if _land_lock > 0.0:
|
||||
_land_lock -= get_process_delta_time()
|
||||
if _land_lock > 0.0 and state in ["ground", "idle"]:
|
||||
# One-shots (Land, Hit) own playback briefly.
|
||||
if _oneshot_lock > 0.0:
|
||||
_oneshot_lock -= get_process_delta_time()
|
||||
if _oneshot_lock > 0.0:
|
||||
_prev_state = state
|
||||
return
|
||||
# A heavy landing plays the Land one-shot before locomotion resumes.
|
||||
if state in ["ground", "idle"] and _prev_state == "air" \
|
||||
and _vertical_speed() < -12.0 and _resolved_clips.has("Land"):
|
||||
_land_lock = 0.25
|
||||
_oneshot_lock = 0.25
|
||||
_play_clip("Land")
|
||||
_prev_state = state
|
||||
return
|
||||
@@ -217,7 +236,9 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
||||
var clip := "Idle"
|
||||
match state:
|
||||
"ground", "idle":
|
||||
if is_crouching:
|
||||
if _dancing and speed < 0.5 and not is_crouching:
|
||||
clip = "Dance"
|
||||
elif is_crouching:
|
||||
clip = "CrouchWalk" if speed > 0.5 else "Crouch"
|
||||
elif speed > run_anim_reference_speed * 1.35:
|
||||
clip = "Sprint"
|
||||
@@ -291,12 +312,14 @@ func _process(delta: float) -> void:
|
||||
_pose_mod.wall = _cur_wall
|
||||
|
||||
|
||||
func _play_clip(canonical: String) -> void:
|
||||
func _play_clip(canonical: String, restart: bool = false) -> void:
|
||||
if not animation_player or not _resolved_clips.has(canonical):
|
||||
return
|
||||
var clip_name: String = _resolved_clips[canonical]
|
||||
if _current_clip == clip_name and animation_player.is_playing():
|
||||
if not restart and _current_clip == clip_name and animation_player.is_playing():
|
||||
return
|
||||
if restart:
|
||||
animation_player.stop()
|
||||
animation_player.play(clip_name, BLEND_TIME)
|
||||
_current_clip = clip_name
|
||||
|
||||
|
||||
Reference in New Issue
Block a user