#pragma once #include "CoreMinimal.h" /** * Constants of the player's body: the Paragon Twinblast character (the user's Fab pack, Content/ParagonTwinblast, never edited) as * the first-person body of ADescentCharacter. Centimetres, seconds, degrees. Details and the numbers behind the guesses: Docs/status/player_body.md. */ namespace Descent::Tune::Body { // ---- the switches ---------------------------------------------------------------------------------------------------------- /** 0 = the Twinblast body is the player's view (default). 1 = the old cybernetic arm viewmodel (Player/Arm), no body. cvar descent.Body.OldArm. */ constexpr int32 OldArmViewDefault = 0; /** 0 = the two pistols of the mesh are not drawn (default; the user: "without the guns ... for now"). 1 = drawn. cvar descent.Body.Guns. */ constexpr int32 GunsShownDefault = 0; // ---- the model --------------------------------------------------------------------------------------------------------------- // Since the third task of 2026-10-12 everything that belongs to ONE model (mesh, scale, yaw, bones to hide, bone names, material // overrides, gun slots, animation folders) is in Config/DescentBody.json, read through FDescentBodyProfile (Player/Body/ // DescentBodyProfile.h). What stays here is behaviour: how the body is placed, blended and timed, whatever the model. // (Twinblast, for the record: 0.975 for its own crouched Idle, 0.922 for the upright retargeted idle; head, neck and the four // front collar bones hidden in the own view; pistols one material section; our leg material on the lower-body slot.) // ---- size and place -------------------------------------------------------------------------------------------------------- /** * In the player's own view the mesh stands this far BEHIND the capsule axis, more when he looks down: the camera stays where it * was (the capsule axis, 1.65 m), and a head tilts forward over the chest when it looks down, which the fixed camera cannot do. * Without it the chest fills the picture and hides the legs. Other views get no shift. */ constexpr double ViewBackCm = 3.0; constexpr double ViewBackLookDownCm = 12.0; // added at the pitch limit, in proportion to the down pitch /** * Unreal's first-person scale for the body in the own view: drawn at the same place and size on screen but nearer the eye in * depth, so a hand held 50 cm out does not go into a wall the 34 cm capsule is pressed against. 1 = true depth. */ constexpr double FirstPersonScale = 0.55; constexpr bool CastShadowInOwnView = false; // a first-person primitive is drawn nearer than it stands: its shadow would not match constexpr bool CastShadowOtherViews = true; constexpr int32 DryStencilValue = 1; // DESIGN 7.4, the same mask the old arm carried // ---- the arms in the own view (the pack's poses hold the hands low and wide, outside a 72 degree picture) ---------------- /** Each upper arm is turned about its shoulder: up, and toward the middle. Forward kinematics only; the pack's motion stays on top. */ constexpr double ArmRaiseLeftDeg = 15.0, ArmInLeftDeg = 16.0; constexpr double ArmRaiseRightDeg = 12.0, ArmInRightDeg = 18.0; constexpr double ArmFollowLookUp = 0.6; // of the up pitch added to the raise, so the hands stay in the picture /** * Looking down, the turn fades out between these down pitches: the arms go back to the pack's own low, wide pose and leave the * middle of the picture to the torso and the legs (turned in, the right arm lay across the view). */ constexpr double ArmFadeFromDownDeg = 12.0, ArmFadeToDownDeg = 45.0; // ---- animation (the pack's sequences; natural speeds measured from the feet, see the note) --------------------------------- constexpr double WalkAnimSpeedCmS = 143.0; // Walk_Fwd/Bwd/Left/Right: 138-149 measured constexpr double JogAnimSpeedCmS = 340.0; // Jog_*: 327-360 measured constexpr double JogAboveCmS = 200.0; // the jog set above this speed, the walk set below (hysteresis below) constexpr double JogHysteresisCmS = 20.0; constexpr double MoveFromCmS = 10.0, MoveFullCmS = 60.0; // idle below, fully the locomotion clip above constexpr double MinPlayRate = 0.55, MaxPlayRate = 1.6; constexpr double MoveBlendK = 10.0; // damp constants, 1/s constexpr double ActionBlendK = 14.0; constexpr double RelaxBlendK = 5.0; // into and out of Idle_Relaxed (arms down) at the helm constexpr double DirectionFadeSec = 0.20; // crossfade when the direction clip changes constexpr double GaitFadeSec = 0.25; constexpr double JumpStartFadeSec = 0.06, ApexFadeSec = 0.10, FallFadeSec = 0.18, LandFadeSec = 0.05, SwimFadeSec = 0.35; constexpr double JumpMinUpCmS = 100.0; // leaving the ground faster than this upward is a jump, otherwise a fall constexpr double FallAfterSec = 0.12; // a step or a placement drop shorter than this shows no fall constexpr double LandAfterAirSec = 0.25; // the landing clip only after this long in the air /** Swimming: the pack has no swim. Jump_Fall_Loop (legs hanging, arms out in front) at this rate is the nearest thing. */ constexpr double SwimPlayRate = 0.35; // ---- 2026-10-12: the retargeted animation set (the user's Fab packs for the UE5 mannequin, baked onto Twinblast's skeleton by // Tools/ue/assets_14_twinblast_anims.py into /Game/Player/Twinblast/Anims; numbers from Saved/ImportState/twinblast_anims.json) ---- /** cvar descent.Body.AnimSet: 0 = the Twinblast pack's own sequences (the set of 2026-10-11), 1 = the retargeted set (default). */ constexpr int32 AnimSetDefault = 1; /** * Own view, added to ViewBackCm with the retargeted set. The pack's idle crouched with the chest forward and needed the mesh behind * the eye; this idle stands upright. NOT SOLVED (2026-10-12, two values tried): with +9 looking down shows the floor and only the * shoulders at the lower edge at 80 degrees (no legs); with -6 the camera is over the neck stump, which fills the picture. +9 is the * clean one and stays. Probably needs the neck bone hidden too and then a value near 0. Live: descent.Body.ViewBackCm. */ constexpr double ViewBackRetargetedCm = 9.0; /** cvar descent.Body.ArmsUp (0..1): how much of the old "upper arms turned up into the view" is applied. 0 = arms hang and swing naturally. */ constexpr double ArmsUpDefault = 0.0; /** Own view, on the ground: this share of the locomotion is taken OUT of the spine, head and arms (blended to the idle pose); the legs keep all of it. */ constexpr double UpperBodyCalmOwnView = 0.55; /** * Clip speeds of the retargeted set: walk fwd, bwd, left, right; the jog (all four); the run. ONLY THE STAND-INS for the pure logic and * for a model whose clips were not measured yet: the game takes the numbers of /BodyAnims.measured.json (the planted * foot against the body, times the profile's scale). These are Twinblast's, already scaled by its 0.922. * Until the third task the rate was matched to the SOURCE's root travel (137.5 / 400.5 unscaled), which the retargeted legs do not * cover: they are 27-33 % slower, so the feet skated by a quarter of the ground speed. */ constexpr double WalkSpeedSet1CmS[4] = { 100.0, 79.5, 81.3, 81.2 }; constexpr double JogSpeedSet1CmS = 277.8; constexpr double RunSpeedSet1CmS = 463.9; constexpr double RunLengthSet1Sec = 1.9; constexpr int32 RunBobStepsSet1 = 6; constexpr double RunBobPhaseSet1 = 2.584; /** The run clip (sprint) above this speed, the jog below (hysteresis). Our walk is 290 cm/s, our sprint 480. */ constexpr double RunAboveCmS = 385.0, RunHysteresisCmS = 25.0; /** * Running sideways or diagonally there is one run clip (forward): the pelvis and legs are turned toward where he goes (up to * LowerTurnMaxDeg) and the spine is turned back, so the chest stays with the view. Going backward (more than LowerBackFromDeg off * the view) the clip runs in reverse and the legs turn toward the opposite of where he goes. */ constexpr double LowerTurnMaxDeg = 90.0, LowerBackFromDeg = 110.0, LowerBackHysteresisDeg = 15.0; constexpr double LowerTurnK = 9.0; // 1/s, how fast the legs come round /** The pelvis' up-and-down in a retargeted locomotion clip: z ~ cos(2 pi * Steps * t / length - Phase). The view bob follows it. */ constexpr int32 WalkBobStepsSet1[4] = { 6, 4, 4, 4 }; constexpr double WalkBobPhaseSet1[4] = { -0.439, 0.352, 0.134, 0.135 }; constexpr int32 JogBobStepsSet1 = 4; constexpr double JogBobPhaseSet1 = -1.896; constexpr double JumpApexSecSet1 = 0.2667; // the set has no apex clip: this long after the start clip the fall loop takes over // swimming (retargeted set): tread water, or a head-up stroke forward / to a side constexpr double SwimMoveFromCmS = 45.0, SwimMoveBelowCmS = 25.0; // a stroke above, treading below (hysteresis) constexpr double SwimAnimSpeedCmS = 250.0; constexpr double SwimMinRate = 0.6, SwimMaxRate = 1.3; constexpr double SwimClipFadeSec = 0.35, SwimMoveBlendK = 4.0; /** * In the swim clips the body leans forward: the head is 38-44 cm ahead of where the idle's head is and 8-16 cm lower. The mesh is * moved by this (the character's frame: forward, right, up; cm, already scaled) so the camera stays where the head is. */ inline FVector SwimShiftIdleCm() { return FVector(-35.0, 0.0, 7.0); } inline FVector SwimShiftMoveCm() { return FVector(-40.0, 0.0, 15.0); } // the nutrition-port jack-in on the right arm (Touch_R): reach in ReachSec, hold through the feed, back the same way constexpr double JackReachSec = 1.0, JackHoldEndSec = 4.10, JackEndSec = 4.80; constexpr double TouchPeakSec = 1.318; // the clip's furthest reach (hand 68 cm ahead, 126 cm up) constexpr double JackArmBelowViewDeg = 12.0; // own view: the right upper arm is raised so the hand is this far below the middle of the picture constexpr double TouchHandBelowEyeDeg = 37.0; // where the clip alone holds the hand, seen from the eye constexpr double UsePlayRate = 2.0; // "use" on a console: PushButton_R at this rate, on the upper body constexpr double OverlayBlendK = 12.0; // ---- 2026-10-12, second task ("this is fps game its more important"): the body anchored under the camera in the own view ---- // The camera stays where gameplay puts it (capsule axis, 1.65 m, the small bob). The BODY is placed under it every frame, inside // the pose evaluation (no frame of lag): the root is moved so the neck bone sits NeckBehindCm behind the eye (and NeckBelowCm // under it when swimming; on the ground the height is the animation's, so the feet stay on the floor). Measured before // (body_view_probe, LOOKDOWN): at -60 every joint was below the bottom edge of the picture (feet at 1.49 / 1.60 of its height, // 24 cm BEHIND the camera); nothing was near-clipped, hidden or occluded. With a 44.5 degree tall picture an upright man under // the eye cannot be seen at -60 at all, so looking down also LEANS the body back about its feet (legs forward into the picture). /** cvar descent.Body.Anchor: 1 = this placement (default), 0 = the fixed offsets of before (ViewBackCm, SwimShift). */ constexpr int32 AnchorDefault = 1; constexpr double NeckBehindCm = 12.0; // cvar descent.Body.NeckBehindCm constexpr double NeckBehindPerLeanDeg = 0.2; // further back as he leans, so the chest stays just under the picture's lower edge /** cvar descent.Body.LookDownLeanDeg: the lean at LeanFullDownDeg of down pitch; none above LeanFromDownDeg; LeanAtLimitShare of it at the pitch limit. */ constexpr double LookDownLeanDeg = 30.0; // 26 put the knees at 0.94 / 0.99 of the picture's height at -60: on its very edge constexpr double LeanFromDownDeg = 20.0, LeanFullDownDeg = 60.0; constexpr double LeanAtLimitShare = 0.3; /** * Swimming: where the neck is from the eye (ahead, below). NOT SOLVED: the arm strokes are not in the picture. Two placements failed: * the head at the eye (first task: clean water, the stroke passes beside the head, outside a 72 degree picture) and the shoulders * under the eye (neck +15 ahead, 8 below: the back of the coat's collar filled the lower middle of the picture as a grey dome, * still no arms). These values are the first, clean one again (the neck 8 behind and 10 under the eye). */ constexpr double SwimNeckAheadCm = 0.0, SwimNeckBelowCm = 15.0; // Third task: the arms' stroke is made in code (two-bone IK on both arms, FDescentBodyAnimState::SwimHandEyeCm), the hands placed // in the EYE's frame, so they are in the picture whatever the clip does; the neck sits straight under the eye (0 ahead, 15 below: // the shoulders in reach of those places, the collar behind and under the picture), in the eye's frame too: the whole body turns // with the view's pitch, so it never comes into the picture when he looks down. The legs keep the swim clips' kick. /** cvar descent.Body.SwimArms: 1 = the code-made stroke (default), 0 = the clips' own arms. */ constexpr int32 SwimArmsDefault = 1; constexpr double SwimStrokeMinHz = 0.55, SwimStrokeMaxHz = 0.95; // strokes per second, slow to full swim speed constexpr double SwimStrokeFullCmS = 280.0; constexpr double SwimScullHz = 0.7; /** The jack-in: the right hand is taken to the port by a two-bone IK; own view: to the point on the eye-to-port line this far from the shoulder. */ constexpr double JackArmReachCm = 47.0; }