Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore unversioniert; letztere sind unter ../backups gesichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
5.5 KiB
GDScript
156 lines
5.5 KiB
GDScript
extends Node3D
|
||
## Imported, weighted character. The battle clock owns playback, including pause/2×.
|
||
var skeleton: Skeleton3D
|
||
var player: AnimationPlayer
|
||
var bounds: AABB
|
||
var clips: Dictionary = {}
|
||
var attack_cycle: Array[String] = ["attack_a", "attack_b"]
|
||
var current_clip: String = ""
|
||
var dead: bool = false
|
||
var _clock: float = 0.0
|
||
var _walk_clock: float = 0.0
|
||
var _event_left: float = 0.0
|
||
var _last_hits: int = 0
|
||
var _had_attack: bool = false
|
||
var _last_ability: float = 0.0
|
||
var _last_hit: float = 0.0
|
||
var _moving: bool = false
|
||
var _preview_time: float = 0.0
|
||
var _ability_time: float = -1.0
|
||
var _hit_time: float = -1.0
|
||
|
||
func setup(source: Node3D, box: AABB) -> void:
|
||
bounds = box
|
||
add_child(source)
|
||
for node: Node in source.find_children("*", "Skeleton3D", true, false):
|
||
skeleton = node as Skeleton3D
|
||
for node: Node in source.find_children("*", "AnimationPlayer", true, false):
|
||
player = node as AnimationPlayer
|
||
if player != null:
|
||
player.callback_mode_process = AnimationMixer.ANIMATION_CALLBACK_MODE_PROCESS_MANUAL
|
||
player.playback_default_blend_time = .12
|
||
for key: String in player.get_animation_list():
|
||
var short_name: String = key.get_slice("/",key.get_slice_count("/")-1)
|
||
clips[short_name] = key
|
||
|
||
func get_aabb() -> AABB:
|
||
return bounds
|
||
|
||
func clip_length(clip: String) -> float:
|
||
return player.get_animation(clips[clip]).length if clips.has(clip) else 0.0
|
||
|
||
func _sample(clip: String, at: float, delta: float = 0.0, blend: float = .12) -> void:
|
||
if player == null or not clips.has(clip) or not is_inside_tree():
|
||
return
|
||
if current_clip != clip:
|
||
player.play(clips[clip], blend)
|
||
current_clip = clip
|
||
player.advance(delta)
|
||
player.seek(clampf(at,0,clip_length(clip)),true)
|
||
|
||
func preview(clip: String, at: float) -> void:
|
||
_sample(clip, at, 0.0, 0.0)
|
||
|
||
func pose(time: float, moving: bool, attack: float, ability: float, hit: float) -> void:
|
||
# Legacy preview entry point, retained for model galleries and callers.
|
||
var dt: float = clampf(time-_preview_time,0,.1)
|
||
_preview_time = time
|
||
if attack > 0:
|
||
_sample("attack_a",.90+(.28-attack),dt)
|
||
elif ability > 0:
|
||
_sample("ability",fmod(time,clip_length("ability")),dt)
|
||
elif hit > 0:
|
||
_sample("hit",.18-hit,dt)
|
||
else:
|
||
var clip: String = "walk" if moving else "idle"
|
||
_sample(clip,fmod(time,clip_length(clip)),dt)
|
||
|
||
func update_combat(delta: float, distance: float, in_range: bool, progress: float, hits: int, ability: float, hit: float, living: bool) -> void:
|
||
if not living:
|
||
if not dead:
|
||
dead = true
|
||
_clock = 0.0
|
||
_clock += delta
|
||
_sample("death",_clock,delta,.06)
|
||
return
|
||
_clock += delta
|
||
var moving: bool = delta > 0 and distance / delta > .04
|
||
# Direct damage stays in AttackComponent. Pose reaches contact exactly on its event.
|
||
if in_range:
|
||
if hits > _last_hits:
|
||
_had_attack = true
|
||
_last_hits = hits
|
||
var recovering: bool = _had_attack and progress < .30
|
||
var index: int = maxi(0,hits-1) if recovering else hits
|
||
var clip: String = attack_cycle[index % attack_cycle.size()]
|
||
var at: float = .90+.35*progress/.30 if recovering else .90*maxf(0,progress-(.30 if _had_attack else 0.0))/(.70 if _had_attack else 1.0)
|
||
_sample(clip,at,delta,.055)
|
||
_event_left = 0
|
||
else:
|
||
_had_attack = false
|
||
_last_hits = hits
|
||
if ability > _last_ability + .05:
|
||
_event_left = clip_length("ability")
|
||
_clock = 0.0
|
||
_sample("ability",0,delta)
|
||
elif hit > _last_hit + .05 and _event_left <= 0.0:
|
||
_event_left = clip_length("hit")
|
||
_clock = 0.0
|
||
_sample("hit",0,delta,.045)
|
||
if _event_left > 0:
|
||
_event_left -= delta
|
||
_sample(current_clip,_clock,delta)
|
||
elif moving:
|
||
if not _moving:
|
||
_clock = 0.0
|
||
# One stride travels 0.52 metres. Separation and slow effects affect cadence.
|
||
_walk_clock += distance / .52 * clip_length("walk")
|
||
if _clock < clip_length("start"):
|
||
_sample("start",_clock,delta)
|
||
else:
|
||
_sample("walk",fmod(_walk_clock,clip_length("walk")),delta)
|
||
else:
|
||
if _moving:
|
||
_clock = 0.0
|
||
if current_clip in ["walk","start","stop"] and _clock < clip_length("stop"):
|
||
_sample("stop",_clock,delta)
|
||
else:
|
||
_sample("idle",fmod(_clock,clip_length("idle")),delta)
|
||
if ability > _last_ability + .05:
|
||
_ability_time = 0.0
|
||
if hit > _last_hit + .05:
|
||
_hit_time = 0.0
|
||
if _ability_time >= 0:
|
||
_ability_time += delta
|
||
if in_range:
|
||
_overlay("ability",_ability_time,.45,["Head","Forearm.L","Forearm.R"])
|
||
if _ability_time >= clip_length("ability"):
|
||
_ability_time = -1.0
|
||
if _hit_time >= 0:
|
||
_hit_time += delta
|
||
if in_range:
|
||
_overlay("hit",_hit_time,.65,["Spine","Head"])
|
||
if _hit_time >= clip_length("hit"):
|
||
_hit_time = -1.0
|
||
_last_ability = ability
|
||
_last_hit = hit
|
||
_moving = moving
|
||
|
||
func _overlay(clip: String, at: float, weight: float, bones: Array) -> void:
|
||
# Only selected joints react while the primary swing keeps its contact time.
|
||
if not is_inside_tree() or not clips.has(clip):
|
||
return
|
||
var animation: Animation = player.get_animation(clips[clip])
|
||
for track: int in range(animation.get_track_count()):
|
||
if animation.track_get_type(track) != Animation.TYPE_ROTATION_3D:
|
||
continue
|
||
var path: NodePath = animation.track_get_path(track)
|
||
if path.get_subname_count() == 0 or not str(path.get_subname(0)) in bones:
|
||
continue
|
||
var index: int = skeleton.find_bone(path.get_subname(0))
|
||
if index < 0:
|
||
continue
|
||
var rest: Quaternion = skeleton.get_bone_rest(index).basis.get_rotation_quaternion()
|
||
var value: Quaternion = animation.rotation_track_interpolate(track,minf(at,animation.length))
|
||
var offset: Quaternion = Quaternion.IDENTITY.slerp(rest.inverse()*value,weight)
|
||
skeleton.set_bone_pose_rotation(index,skeleton.get_bone_pose_rotation(index)*offset)
|