#pragma once #include "CoreMinimal.h" #include "Animation/AnimInstance.h" #include "DescentBodyAnim.generated.h" class UAnimSequence; /** The sequences of the Paragon Twinblast pack the body plays (Content/ParagonTwinblast/.../Animations, read only). */ enum class EDescentBodySeq : uint8 { Idle, WalkFwd, WalkBwd, WalkLeft, WalkRight, JogFwd, JogBwd, JogLeft, JogRight, JumpStart, JumpApex, FallLoop, Land, IdleRelaxed, // 2026-10-12, the retargeted set only (in the pack's own set these fall back to Jump_Fall_Loop and Idle): SwimIdle, SwimFwd, SwimLeft, SwimRight, Touch, PushButton, RunFwd, // the sprint: one clip, the legs turned toward where he goes Count }; /** What the body is doing, as the tests and the state dump read it. */ enum class EDescentBodyMotion : uint8 { Idle, Moving, Jumping, Falling, Landing, Swimming, Held }; namespace Descent::Body { constexpr int32 SeqCount = static_cast(EDescentBodySeq::Count); /** Asset name of a sequence in the pack's Animations folder. */ THEDESCENT_API const TCHAR* SeqAssetName(EDescentBodySeq Seq); /** The role's name in the animation table of Config/DescentBody.json ("WalkFwd"); its baked sequence is A_Body_. */ THEDESCENT_API const TCHAR* SeqRoleName(EDescentBodySeq Seq); THEDESCENT_API const TCHAR* MotionName(EDescentBodyMotion Motion); /** One-shots (Jump_Start, Jump_Apex, Jump_Land) stop at their end; everything else loops. */ THEDESCENT_API bool SeqLoops(EDescentBodySeq Seq); } /** Play lengths of the sequences in seconds (from the assets; the tests use the measured numbers). */ struct FDescentBodyLengths { double Sec[Descent::Body::SeqCount]; // ---- what else the pure state needs to know of a set (2026-10-12) ---- int32 Set = 0; // 0 the pack's own sequences, 1 the retargeted set double SpeedCmS[Descent::Body::SeqCount]; // locomotion clips: the speed the clip walks at (scaled mesh), 0 otherwise bool bReversed[Descent::Body::SeqCount]; // played backward (the retargeted set has no backward jog: the forward one, reversed) int32 BobSteps[Descent::Body::SeqCount]; // locomotion clips: footfalls in the clip (0 = not measured: no animation-driven bob) double BobPhase[Descent::Body::SeqCount]; double RootSpeedCmS[Descent::Body::SeqCount]; // the source's root travel per second (scaled): what the rate was matched to before bool bRootLock[Descent::Body::SeqCount]; bool bOldLoco = false; // descent.Body.DebugOldLoco: no run clip, rates from the root travel (to measure "before") FDescentBodyLengths() : FDescentBodyLengths(0) {} // the pack's set as measured on 2026-10-11, so the pure logic runs without assets explicit FDescentBodyLengths(int32 InSet); /** Set 1: the table's flags and the script's measurements of the model's baked clips replace the stand-ins. */ void ApplyProfile(const struct FDescentBodyProfile& Profile); double operator[](EDescentBodySeq Seq) const { return Sec[static_cast(Seq)]; } }; /** What the animation is driven by: the real movement (UDescentCharacterMovement), read by the body component every frame. */ struct FDescentBodyAnimInput { FVector2D VelLocalCmS = FVector2D::ZeroVector; // X forward, Y right, in the body's frame, relative to the floor it stands on double VerticalCmS = 0.0; bool bInAir = false; // falling or jumping (the movement mode) bool bSwimming = false; bool bHeld = false; // at the helm, on the ladder, dead, jacked in: stands idle whatever the velocity says bool bRelaxed = false; // at the helm: the idle with the arms down (Idle_Relaxed) instead of the ready stance // the arms in the player's own view (degrees; all zero in any other view) double ArmRaiseLeftDeg = 0.0, ArmInLeftDeg = 0.0, ArmRaiseRightDeg = 0.0, ArmInRightDeg = 0.0; bool bOwnView = false; // the player's own view: the upper body takes a reduced share of the locomotion double JackSec = -1.0; // seconds into the nutrition-port jack-in, negative = none double UseSec = -1.0; // seconds since "use" was pressed on something, negative = none // the body under the camera (own view): the neck goes to AnchorTarget (the mesh component's space); see DescentTune_Body.h double AnchorWeight = 0.0; // 1 in the own view; while swimming in every view (so the code-made arms meet the body) bool bFlatFeet = true; // the look-down lean keeps the feet flat on the floor; the swim's pitch turns the whole body FVector AnchorTarget = FVector::ZeroVector; double AnchorVertical = 0.0; // 0 the height stays the animation's (feet on the floor), 1 the neck's height is anchored too (swimming) double LeanDeg = 0.0; // the whole body leaned back about its feet (looking down) // the jack-in: where the right hand goes (the mesh component's space) bool bJackTarget = false; FVector JackTarget = FVector::ZeroVector; // the swim arms made in code: the eye's frame (x ahead along the view, y right, z up; cm) to the mesh component's space, and the switch FTransform EyeToComponent = FTransform::Identity; double SwimArms = 0.0; // 0 the clip's own arms, 1 the stroke in view (descent.Body.SwimArms) }; /** One sampled sequence of a frame's pose. */ struct FDescentBodyClip { EDescentBodySeq Seq = EDescentBodySeq::Idle; double Time = 0.0; double Weight = 0.0; }; /** The sequences sampled for one frame. */ using FDescentBodyClipList = TArray>; /** A channel that plays one sequence and crossfades from the one before it. */ struct FDescentBodyFader { EDescentBodySeq Cur = EDescentBodySeq::Idle, Prev = EDescentBodySeq::Idle; double CurTime = 0.0, PrevTime = 0.0; double Fade = 1.0; // weight of Cur; the rest is Prev's double FadeSec = 0.0; void Play(EDescentBodySeq Seq, double StartTime, double InFadeSec); void Advance(double Dt, double Rate, const FDescentBodyLengths& Len); }; /** * The whole animation state as a value: a pure function of the inputs over time (no engine objects), so the tests step it by hand. * Three layers: Idle (or Idle_Relaxed at the helm); the locomotion clip (walk or jog set, one of four directions, play rate matched to the real speed), weighted by * MoveAlpha; and the action channel (jump start > apex > fall loop, the landing, the swim pose), weighted by ActionAlpha on top. */ struct THEDESCENT_API FDescentBodyAnimState { enum class EAction : uint8 { None, JumpStart, Apex, Fall, Land, Swim }; FDescentBodyFader Loco; FDescentBodyFader Act; double IdleTime = 0.0; double RelaxTime = 0.0; double RelaxAlpha = 0.0; // 0 the ready stance (Idle), 1 arms down (Idle_Relaxed) double MoveAlpha = 0.0; double ActionAlpha = 0.0; double LocoRate = 1.0; double AirTime = 0.0; double SpeedCmS = 0.0; EAction Action = EAction::None; bool bJog = false; bool bWasAir = false; bool bHeldNow = false; bool bMovingNow = false; // 2026-10-12 double SwimAlpha = 0.0; // 1 while a swim clip of the retargeted set is the pose double SwimMove = 0.0; // 0 treading water, 1 a stroke double UpperCalm = 0.0; // own view: share of the locomotion taken out of the upper body EDescentBodySeq OverSeq = EDescentBodySeq::Touch; // the upper-body overlay: the jack-in reach, or the button push of "use" double OverTime = 0.0, OverAlpha = 0.0; double JackReach = 0.0; // 0 the arm at rest, 1 the hand at the port (the IK's weight) bool bSwimStroke = false; // third task of 2026-10-12 bool bRun = false; // the run clip (sprint) bool bLocoBackward = false; // running backward: the run clip in reverse double LowerTurnDeg = 0.0; // pelvis and legs turned toward where he runs (+ = to his right); the spine is turned back by it double SwimStroke = 0.0; // 0..1, place in the breaststroke cycle double SwimScullSec = 0.0; // clock of the sculling hands while treading /** * Where a hand is in the swim made in code, in the EYE's frame (cm: ahead along the view, to the right, up), so it is in the * picture by construction. Move 0: sculling low near the lower corners. Move 1: the breaststroke at Stroke (0 hands together * reaching ahead, sweep out and back, recover under the chin, push forward again). */ static FVector SwimHandEyeCm(double Stroke, double Move, double ScullSec, bool bLeft); /** * The pelvis' up-and-down of the locomotion clip AheadSec from now, -1..1, already faded with moving on the ground. False when * the set has no measured footfalls (the pack's own set): the caller then bobs by distance walked. */ bool ViewBob(const FDescentBodyLengths& Len, double AheadSec, double& OutSignal, double* OutFootfallHz = nullptr) const; FDescentBodyAnimState() { Loco.Cur = Loco.Prev = EDescentBodySeq::WalkFwd; Act.Cur = Act.Prev = EDescentBodySeq::FallLoop; } void Step(const FDescentBodyAnimInput& In, double Dt, const FDescentBodyLengths& Len); /** The sequences to sample this frame; weights are positive and sum to 1. */ void BuildClips(FDescentBodyClipList& Out) const; EDescentBodyMotion Motion() const; }; /** * The animation instance of the player's body: no AnimBlueprint. It loads the pack's sequences, steps FDescentBodyAnimState on the * game thread and its proxy samples and blends the sequences itself (FAnimInstanceProxy::Evaluate), then turns the two upper arms * for the player's own view. The pack's Twinblast_AnimBlueprint is not used (see Docs/status/player_body.md for why). */ UCLASS(Transient, NotBlueprintable) class THEDESCENT_API UDescentBodyAnimInstance : public UAnimInstance { GENERATED_BODY() public: /** Loads the sequences of the wanted set (idempotent; again when descent.Body.AnimSet changed). False when one is missing. */ bool LoadSequences(); /** descent.Body.AnimSet: 0 the pack's own sequences, 1 the retargeted set (default). */ static int32 WantedSet(); static void SetWantedSet(int32 Set); /** The set that is loaded (the pack's own when a retargeted sequence is missing). */ int32 ActiveSet() const { return Lengths.Set; } /** Object path of a sequence in a set, from the active model's profile (set 1: its baked A_Body_; set 0: its own pack's). */ static FString SeqAssetPath(int32 Set, EDescentBodySeq Seq); int32 CountLoadedSequences() const; const UAnimSequence* GetSequence(EDescentBodySeq Seq) const; const FDescentBodyLengths& GetLengths() const { return Lengths; } /** Written by the body component before the mesh ticks. */ FDescentBodyAnimInput Input; const FDescentBodyAnimState& GetState() const { return State; } /** Steps the state without a tick (tests). */ void StepState(double Dt) { State.Step(Input, Dt, Lengths); } protected: virtual void NativeInitializeAnimation() override; virtual void NativeUpdateAnimation(float DeltaSeconds) override; virtual FAnimInstanceProxy* CreateAnimInstanceProxy() override; private: UPROPERTY(Transient) TArray> Sequences; FDescentBodyAnimState State; FDescentBodyLengths Lengths; int32 LoadedForSet = -1; FString LoadedRunRole, LoadedModel; bool bLoggedMissing = false; };