Files

158 lines
6.1 KiB
GDScript
Raw Permalink Normal View History

2026-09-12 11:01:37 +02:00
class_name ImportedOperator
extends Node3D
static var scenes: Dictionary = {}
var skeleton: Skeleton3D
var weapon_mount := Node3D.new()
var pose := OperatorPose.new()
var current := "pro_rifle__idle"
var clock := 0.0
var action := ""
var action_time := 0.0
var action_weight := 0.0
var previous_life := -1
var previous_shot := -1
var previous_grenades := -1
var previous_hp := 100.0
var previous_yaw := 0.0
var was_grounded := true
var was_reloading := false
var dead := false
var hand := -1
var socket := Transform3D.IDENTITY
var variant := "steve"
func setup(model: String, arms_only: bool = false) -> void:
variant = model if model in ["steve", "gas_mask", "pro_rifle", "basic_shooter", "slim_shooter"] else "steve"
var suffix := "_arms" if arms_only else ""
var key := variant + suffix
if not scenes.has(key): scenes[key] = load("res://scenes/operators/" + key + ".scn") as PackedScene
var packed: PackedScene = scenes[key]
var body := packed.instantiate() as Node3D
body.name = "Body"
body.rotation.y = PI
add_child(body)
skeleton = body.find_child("Skeleton3D", true, false) as Skeleton3D
pose.setup(skeleton)
weapon_mount.name = "WeaponMount"
add_child(weapon_mount)
hand = skeleton.find_bone("mixamorig_RightHand")
pose.apply("pro_rifle__idle_aiming", 0.2, true, "", 0, 0, 0, 0)
var grip := pose.global_poses[hand]
# Calibrate in model space once; thereafter the socket follows the hand,
# including reloads, recoil and death, instead of floating at chest height.
var gun_basis := Basis(Vector3.UP, PI)
var origin := grip.origin - gun_basis * Vector3(0, -0.09, 0.04)
socket = grip.affine_inverse() * Transform3D(gun_basis, origin)
update_socket()
func update_socket() -> void:
var body := skeleton.get_parent() as Node3D
weapon_mount.transform = body.transform * skeleton.transform * pose.global_poses[hand] * socket
func choose(key: String, restart: bool = false) -> void:
if key != current or restart:
current = key
clock = 0.0
func trigger(key: String) -> void:
action = key
action_time = 0
static func direction(velocity: Vector3) -> String:
var angle := atan2(velocity.x, -velocity.z)
var sector := posmod(roundi(angle / (PI / 4.0)), 8)
return ["forward", "forward_right", "right", "backward_right", "backward", "backward_left", "left", "forward_left"][sector]
func animate_state(delta: float, state: Dictionary) -> void:
var velocity: Vector3 = state.get("velocity", Vector3.ZERO)
var speed := Vector2(velocity.x, velocity.z).length()
var crouch := bool(state.get("crouch", false))
var aiming := bool(state.get("aim", false))
var grounded := bool(state.get("grounded", true))
var hp := float(state.get("hp", 100.0))
var life := int(state.get("life", 0))
var shot := int(state.get("shot", 0))
var grenades := int(state.get("grenades", 2)) + int(state.get("flashes", 1))
var reload_time := float(state.get("reload", 0.0))
var yaw := float(state.get("yaw", 0.0))
if life != previous_life:
dead = false
action = ""
action_weight = 0
previous_shot = shot
previous_grenades = grenades
previous_hp = hp
previous_yaw = yaw
was_reloading = false
choose("pro_rifle__idle", true)
if hp <= 0:
if not dead:
var deaths := ["death_from_the_front", "death_from_the_back", "death_from_right", "death_from_front_headshot", "death_from_back_headshot"]
var death_key: String = "crouch_death" if crouch else deaths[posmod(int(state.get("id", 0)) + life, deaths.size())]
choose("pro_rifle__" + death_key, true)
dead = true
action = ""
elif float(state.get("slide", 0)) > 0:
choose("slide__running_slide")
clock = (1.0 - clampf(float(state.slide) / 0.65, 0, 1)) * pose.clip(current).length
elif not grounded:
choose("pro_rifle__jump_up" if velocity.y > 1 else "pro_rifle__jump_loop")
elif not was_grounded:
choose("pro_rifle__jump_down", true)
elif current == "pro_rifle__jump_down" and clock < minf(0.25, pose.clip(current).length):
pass
elif speed > 0.2:
var gait := "walk_crouching_" if crouch else ("sprint_" if speed > 7 else ("walk_" if speed < 3.3 or aiming else "run_"))
choose("pro_rifle__" + gait + direction(velocity))
else:
var turn := wrapf(yaw - previous_yaw, -PI, PI)
if absf(turn) > delta * 1.1 and not aiming:
choose("pro_rifle__" + ("crouching_" if crouch else "") + "turn_90_" + ("left" if turn > 0 else "right"))
else:
choose("pro_rifle__idle" + ("_crouching" if crouch else "") + ("_aiming" if aiming else ""))
if not dead:
if reload_time > 0:
if not was_reloading: trigger("pro_rifle__reload")
var duration := maxf(0.1, float(state.get("reload_duration", 2.3)))
action_time = (1.0 - clampf(reload_time / duration, 0, 1)) * pose.clip(action).length
elif was_reloading:
action = ""
elif grenades < previous_grenades:
trigger("basic_shooter__toss_grenade")
elif hp < previous_hp:
trigger("basic_shooter__hit_reaction")
elif shot != previous_shot:
trigger("pro_rifle__firing_rifle_walking" if speed > 0.2 else "pro_rifle__firing_rifle_standing")
if not action.is_empty():
if reload_time <= 0: action_time += delta
if action_time >= pose.clip(action).length: action = ""
action_weight = move_toward(action_weight, 0.0 if action.is_empty() else 1.0, delta * 16)
clock += delta
var looping := not dead and not "jump" in current and not "slide" in current
pose.apply(current, clock, looping, action, action_time, action_weight, delta, 0 if dead else float(state.get("pitch", 0)))
update_socket()
if not dead and action.is_empty() and state.has("weapon"):
align_support(int(state.weapon))
previous_life = life
previous_shot = shot
previous_grenades = grenades
previous_hp = hp
previous_yaw = yaw
was_grounded = grounded
was_reloading = reload_time > 0
func preview(key: String, time: float) -> void:
pose.apply(key, time, false, "", 0, 0, 0, 0)
update_socket()
static func support_point(weapon: int) -> Vector3:
if Arsenal.FAMILIES[weapon] == 4: return Vector3(-0.045, -0.145, 0.08)
return Vector3(-0.055, -0.095, -0.17 if Arsenal.FAMILIES[weapon] == 1 else -0.255)
func align_support(weapon: int) -> void:
var body := skeleton.get_parent() as Node3D
var target := (body.transform * skeleton.transform).affine_inverse() * weapon_mount.transform * support_point(weapon)
pose.reach_arm("Left", target)