# Track note: player body (Paragon Twinblast as the player's first-person body) Workplace: the one project `D:\HADAL - The Descent\TheDescentUE`. Written by the Coder, 2026-10-11. The user's words: "the player model gonna be ParagonTwinblast ... red armed one ... without the guns"; "put him in now, legs metal, use the twinblast arms and animations without the guns (for now for guns)". Backup of Source/Config/Tools before this work: `D:\HADAL - The Descent\_backups\TheDescentUE_code_2026-10-11_pre-twinblast.zip`. ## 2026-10-12, third task: a model that can be swapped, a real run, swim arms made in code (read this first) The user: "running animation sucks find better one or make it ... fix the swimming arms ... this model might be changed not decided yet so all those animations and stuff needs to be saved it would be easy to paste it over to the new models". Code backup before: `D:\HADAL - The Descent\_backups\TheDescentUE_code_2026-10-12_pre-run-swim-profile.zip`. **Results after the last source change:** build PASS 0 warnings; tests **84 of 84** (new: `Descent.Body.ProfileRunSwim`); scenarios PASS `player_body`, `body_view_probe`, `m2_walk`, `m2_dry`, `m8_biomass`, `m10_full_run`, `body_run_look`; 0 files changed in any pack or in the user's new folders since the backup. Sheet looked at: `Docs\status\player_body_2026-10-12_run_swim.png`. Guide: `Docs\PLAYER_MODEL_SWAP.md`. **1. Model-swappable.** One data file, `Config\DescentBody.json`, read by the game (`Player\Body\DescentBodyProfile.h/.cpp`, struct `FDescentBodyProfile`) and by the bake (`Tools\ue\assets_14_body_anims.py`, which replaces `assets_14_twinblast_anims.py`): the model's profile (mesh, yaw, scale, bones to hide, bone name per role, material overrides, gun slots, animation folder, its own legacy set) and the animation table (role -> source clip, `loop` / `root_lock` / `reversed` / `upper_body`). The bake writes `A_Body_` sequences and `BodyAnims.measured.json` into the profile's folder; the game loads by role. `DescentTune_Body.h` holds no model names any more; the code-made parts take their bones from the profile. Setting: `"model"` in the file, or `descent.Body.Model`. The old `TB_*` sequences are gone (rebaked under role names). A rig or retargeter of an earlier bake is reused, not deleted (deleting a referenced asset fails unattended: tried twice). **2. Running.** The bake now measures what the retargeted LEGS do (the planted foot against the body). It showed the cause of the bad run: the play rate was matched to the source's root travel, which the legs fall 27-33 % short of on Twinblast, so the feet skated. | At the sprint (478 cm/s), seen from outside (`body_run_look`, lines `FOOTSLIDE`) | planted foot over the ground | |---|---| | before: the jog clip, rate from the root travel (1.30) | **120.6 cm/s** | | the jog clip, rate from its legs (would need 1.73, capped 1.60) | 42.9 cm/s | | `A_RunFwd_Loop` (FreeSampleAnimationSet), rate 0.87 | 28.1 cm/s (a hard forward-leaning sprint; 40 cm/s uneven in the clip itself) | | **`MM_Run_Fwd` (SampleAnimationPack), rate 1.03: chosen** | **26.3 cm/s** (17 cm/s uneven in the clip itself) | * The run clip plays above 3.85 m/s (`RunAboveCmS`); the walk speed (2.9 m/s) stays on the jog clips, now also at the rate of their legs (1.04 instead of 0.77: that is the clip's own cadence, 3.9 footfalls a second). * **Sideways and diagonal:** the one run clip, pelvis and legs turned toward where he goes (up to 90 degrees), the first spine bone turned back by the same so the chest stays with the view (`LowerTurnDeg`, in the pose). **Backward** (more than 110 degrees off the view): the same clip in reverse with the legs turned to the opposite of where he goes; chosen over a backpedal because the packs have no backward run and the walk-back clip would need 5x its speed. Not compared in motion. * **Bob:** since the footfalls come faster, the camera's speed is held at what it was (`ViewBobWalkMaxCmS` 8.1, `ViewBobRunMaxCmS` 20.4): walking 0.66 cm peak to peak (was 0.90), 0.26 cm a frame (0.27); sprinting 1.40 cm (1.36), 0.47 cm a frame (0.68). * In the OWN view the planted foot moves 51 cm/s walking and 59 cm/s sprinting over the floor (`body_view_probe`): the body is shifted every frame to keep the neck under the camera (second task). Not measured before, so no comparison. The strafe number of that probe (410) is not a slide: in a side step the swinging foot stays as low as the planted one and the probe cannot tell them apart. * Debug: `descent.Body.DebugOldLoco 1` plays it as before; `descent.Body.DebugRunRole ` plays another baked role as the run. **3. Swim arms in code** (`descent.Body.SwimArms 1|0`). Two-bone IK on both arms (`BodyArmIK`), hand places from `FDescentBodyAnimState::SwimHandEyeCm` in the EYE's frame: forward a breaststroke cycle (glide with the hands together ahead, sweep out and back, recovery, push), 0.55 to 0.95 strokes a second with the swim speed; treading, sculling low. Swimming, the body hangs on the eye in every view (neck 15 cm straight under it) and turns with the view's pitch as one piece; the legs keep the swim clips' kick. | Acceptance | State | |---|---| | four stroke phases from his own eyes: both hands in at least three | MET: wrists on screen in all four (0.42/0.58, 0.42/0.58, 0.15/0.85, 0.37/0.63 of the width). In the picture: clear in 1, 2 and 4; in 3 (the sweep) they are at the sides outside the lamp's cone and nearly black | | treading: hands at the lower edge | MET by the numbers (0.22 / 0.78 of the width, 0.89 of the height); in the picture they are dark shapes on the bottom edge, easy to miss | | from outside it reads as a swimmer, no arm through the body | MET: prone, legs kicking, arms ahead, nothing through the body. But the stroke is small from outside: the arms stay ahead in all six frames (the hand places are compact so they stay in his picture) | | nothing fills or blocks the view | MET (no collar dome) | | looking down and the port unchanged | MET: same numbers (-60: knees 0.89 / 0.94; -80: feet 0.66 / 0.72), same pictures | **Still wrong:** while running, a grey lump of the coat stands out behind his shoulders in every outside picture (it was there before; the coat's bones have no animation; cause not found). The hands in the swim are large when they pass under the chin (stroke 4). The swim's arms are lit only by the lamp's cone. The backward run and the turned lower body were checked by state and one still, not in motion. A packaged build still needs `Config\DescentBody.json` and `BodyAnims.measured.json` staged (both are read as loose files). ## 2026-10-12, second task: the first-person view of the body The user: "Yes do it since this is fps game its more important". Code backup before: `D:\HADAL - The Descent\_backups\TheDescentUE_code_2026-10-12_pre-fp-view.zip`. **Results after the last source change:** build PASS 0 warnings; tests 83 of 83; scenarios PASS `player_body`, `body_view_probe`, `m2_walk`, `m2_dry`, `m8_biomass`, `m10_full_run`; 0 pack files changed. Sheet looked at: `Docs\status\player_body_2026-10-12_fpview.png`. **Diagnosis (`body_view_probe`, lines `LOOKDOWN`, before any change).** At -45 and -60 every joint was BELOW the lower edge of the picture (feet at 2.1 / 1.5 of its height), at -80 only the left foot (0.94) and the toes were in. Nothing was near-clipped (near plane 5 cm, first-person scale 0.55), hidden or occluded. The body stood 18-23 cm behind the camera and the feet 24 cm behind it; the picture is only 44.5 degrees tall, so an upright man under the eye cannot be in it at -60 wherever he is put. **What changed.** * **The body is placed under the camera inside the pose** (`BodyAnchorNeck` in `DescentBodyAnim.cpp`, own view only; the camera is not touched): the root is moved every frame so `neck_01` is 12 cm behind the eye; height stays the animation's on the ground (feet on the floor), anchored when swimming. Same frame as the pose, so no lag. `neck_01` is hidden with the head (no stump). * **Looking down leans the whole body back about its feet** (feet turned flat again): 0 above 20 degrees, 30 degrees at -60, 9 at the pitch limit; the neck goes 0.2 cm further back per degree. This is what brings the legs into the picture. It is a cheat of the own view only. * **Jack-in: two-bone IK on the right arm** (`BodyRightArmIK`, `AnimationCore::SolveTwoBoneIK`; module added to `TheDescent.Build.cs`), weight = the reach (in over the first second, out over the last 0.7 s; the 4.8 s are unchanged). Target: the port `StartJackVisual` is given; in the own view the point on the eye-to-port line 47 cm from the shoulder, so the hand is seen on the port even when it is out of reach. * Switches: `descent.Body.Anchor 1|0` (0 = the fixed offsets of the first task), `descent.Body.NeckBehindCm`, `descent.Body.LookDownLeanDeg`. **Acceptance, from the sheet and the numbers:** | Point | State | |---|---| | -60 standing: both thighs, both feet, lower torso at the bottom | **PARTLY.** Both feet and shins are in, at the bottom fifth (knees 0.89 / 0.94, ankles 0.83 / 0.90 of the height). Hip joints (1.16) and torso are still under the edge | | -80: both feet in the lower half | MET (ankles 0.66 / 0.72); thighs, belt and the top of the chest (shirt neckline) at the bottom | | walking / running looking down shows the legs stepping | MET in the frames seen (one shin and boot forward, the opposite hand) | | nothing clipped by the near plane | none seen in the sheet | | bob not worse | 0.90 / 1.36 cm, unchanged; chest side sway against the camera 4.1 -> 1.3 cm | | running looking ahead | clean picture, no forearms in it | | swimming: strokes in view, hands low when treading | **NOT MET.** Two placements failed (head at the eye: no arms; shoulders under the eye: the coat's collar filled the lower middle as a grey dome, still no arms). Left at the clean first one. Looking down while swimming is open water, not blocked | | port: the hand goes to the port and is seen | MET in the own view (forearm and hand to the middle of the picture, large). Only tested with a made-up port 70 cm ahead at eye height; `m8_biomass` passes but its picture was not looked at | **Still wrong:** -45 shows only floor. The lean is a cheat: at -60 the feet are about 45 cm ahead of the capsule (the first-person depth scale keeps them out of walls on screen, not in the world). From OUTSIDE the jack-in pose (`tp_jack_side`) looked contorted in the one tile seen (torso and coat thrown back): not understood, not fixed; it is never the player's view. The jack-in forearm is very large in the picture. ## 2026-10-12, first task: the user's Fab animations retargeted onto Twinblast, and a calm view (the sections below it are the state of 10-11) The user: "I have downloaded many animations can you just find for fit animations to the situations also walking and running camera is not proper too shaky and not good revise it". Code backup before: `D:\HADAL - The Descent\_backups\TheDescentUE_code_2026-10-12_pre-anim-retarget.zip`. **Results (all run after the last source change):** build PASS, 0 warnings; tests **83 of 83** (82 older + `Descent.Body.AnimSetAndView`); scenarios PASS: `player_body`, `m2_walk`, `m2_dry`, `arm_character`, `m8_biomass`, `m10_full_run`, `body_view_probe`. Nothing else was run. 0 files under `Content\ParagonTwinblast` and the five downloaded packs changed. **Retarget.** `Tools\ue\assets_14_twinblast_anims.py` (`Tools\import_assets.ps1 -Only twinblast_anims`, 23 s; `-Force` bakes again): an IK Rig per mesh by `IKRigController.apply_auto_generated_retarget_definition` (Twinblast: 21 chains, the mannequin: 29), an IK Retargeter per source pack (default 5 ops, chains mapped by name: 21 of 21, target pose auto-aligned), `IKRetargetBatchOperation.run_batch_retarget` (the 5.8 name; `duplicate_and_retarget` is deprecated). Each pack's sequences go through that pack's own `SKM_Manny_Simple` (same skeleton asset as the sequence). 21 baked sequences `Content\Player\Twinblast\Anims\TB_`, rigs in `...\Retarget\`. Root motion off and root locked in the assets; the C++ sampler also holds the root at the reference pose. Measurements of every clip: `Saved\ImportState\twinblast_anims.json`. | Situation | Clip (set 1) | Note | |---|---|---| | Idle, at the helm, frozen | `AS_M_Unarmed_Idle_01` | upright; mesh scale 0.922 with this set (eyes measured 165.1 cm) | | Walk fwd/back/left/right (below 2.0 m/s) | `AS_M_Unarmed_Walk_F/B/L/R_Loop` | 137 / 113 / 108 / 108 cm/s clips | | Jog and sprint fwd/left/right | `anim_Jog_Loop_Fwd`, `_Left_L`, `_Right_R` | 400 cm/s clip; our walk 2.9 m/s plays it at 0.77, sprint at 1.28 | | Jog backward | `anim_Jog_Loop_Fwd` **played in reverse** | the pack's `anim_Jog_Loop_Bwd` is byte-identical to `_Fwd` (it runs forward): unusable | | Jump / fall / land | `MM_Jump`, `MM_Fall_Loop`, `MM_Land` | the jump clip is 0.87 s, so a standing jump never reaches the fall loop; landing uses its first 0.4 s | | Swim still / fwd / left / right | `anim_SwimIdle`, `anim_Swim_Surface_Fwd/Left/Right` | the mesh is shifted so the head stays at the camera (`SwimShift*Cm`) | | Jack-in | `anim_Touch_R`, upper body only | out to its furthest reach in 1 s, held to 4.1 s, back the same way by 4.8 s; own view: right upper arm raised toward the view | | "Use" with a focus | `anim_PushButton_R`, upper body, 2x | hooked in `ADescentCharacter::UseFocus`; **not looked at in a picture** | | Ladder | **not wired, stays idle** | `anim_Ladder_Up/Down/Idle` are baked but the clip climbs 273 cm in the pelvis and stands 30 cm up: needs a travel strip and a shift | | Sleep | **skipped** | `UDescentBioSubsystem::Sleep` is instantaneous, there is no sleep state with a visible body | **Switches (console variables).** `descent.Body.AnimSet 1|0` (0 = the pack's own set of 10-11, scale 0.975); `descent.Body.ArmsUp 0..1` (default 0: arms hang; 1 = the old raised hands); `descent.View.Bob 0..1` (default 1); yesterday's `descent.Body.Guns`, `descent.Body.OldArm` unchanged. **Camera, measured** (`body_view_probe`: every drawn frame after the camera update, fixed 30 Hz; log lines `VIEWPROBE`): | | before | after | |---|---|---| | bob walking, peak to peak / largest step in a frame | 8.69 cm / 1.84 cm | 0.90 cm / 0.27 cm | | bob sprinting | 12.48 cm / 4.34 cm | 1.36 cm / 0.68 cm | | body yaw behind the view, turning 70 deg/s | 2.33 deg (one frame) | 0.00 | | chest against the camera, up and down, walking / sprinting | 13.1 / 20.0 cm | 2.9 / 2.4 cm | | hands on screen looking ahead, walking | left 48 of 48 frames swinging 17 % x 32 % of the picture, right flickering in and out | none (arms hang below the picture) | | side, forward, roll | 0 | 0 | What was done: (1) the bob is now `ViewBobWalkCm` 0.45 / `ViewBobRunCm` 0.70 times the pelvis wave of the locomotion clip one frame ahead (`FDescentBodyAnimState::ViewBob`, footfalls and phase measured per clip), vertical only; the pack's set bobs by distance as before with the new size. (2) `ApplyIntent` calls `FaceRotation` right after setting the control rotation: the actor took the yaw a frame late. (3) Own view on the ground: 55 % of the locomotion is taken out of spine, head and arms and out of the pelvis' height (`UpperBodyCalmOwnView`); the retargeted jog bounces the pelvis 10 cm, up to 5 cm in one frame. Other views get the whole animation. **What looks wrong (sheets `Docs\status\player_body_2026-10-12_stills.png`, `_strips.png`):** * **Looking down does NOT show legs and torso.** Two values of `ViewBackRetargetedCm` tried: +9 (kept) shows floor, at 80 degrees only the shoulders at the lower edge; -6 put the camera over the neck stump, which filled the picture. Probably: hide `neck_01` too, then near 0. * **Swimming forward, no arm is in the picture looking ahead** (one frame seen): the stroke passes beside the head. From outside the body is prone and the pose reads as swimming. `fp_swim_forward_look_down` is dark (open water). * **Jack-in:** from outside the right arm reaches forward at chest height; from his own eyes only the fingers show at the lower edge. The hand is not aimed at the port (no IK). * Running from the side is a proper run for the three frames before he reaches the pool rail in the bay (frames 4-6 stand). Feet sliding was not measured; play rate is matched to speed. The coat's bones are not animated (rest pose on the spine). Fingers are open, not fists; no broken fingers seen at sheet size. Backward running is a reversed forward run. * In the own view the feet can dip up to about 2.5 cm into the floor at the bottom of a jog step (the pelvis calm). Not seen in motion by anyone. | Step | State | |---|---| | 1. Inspect the pack by script (bones, slots, sections, materials, animations) | DONE (numbers below) | | 2. Body on `GetMesh()`, scale, guns hidden, machine-leg material | DONE, seen in pictures | | 3. C++ anim instance driving the pack's sequences | DONE, seen in pictures (stills, not motion) | | 4. First-person view, old arm behind a switch | DONE, seen in pictures | | 5. Scenario `player_body` (33 pictures), tests `Descent.Body.*` (4) | DONE, PASS | | 6. Whole suite + the scenarios through the character | see "Results" | | 7. STATUS.md section 22, line in arm.md | DONE | ## Results (run on this PC, 2026-10-11 evening, after the last source change unless said otherwise) | Step | Result | |---|---| | `Tools\build.ps1` | PASS, 0 warnings | | `Tools\import_assets.ps1 -Only twinblast` | PASS, 5 s (one material; nothing saved in the pack: 0 files under `Content\ParagonTwinblast` changed today) | | `Tools\run_tests.ps1` | **82 of 82 PASS** (78 older + 4 `Descent.Body.*`), run after the last source change | | `player_body` | PASS, 41 s (the very first run, with the pack's shaders and textures to build, took 53 s) | | Scenarios through the character | **PASS (20):** `m2_walk`, `m2_moving_base`, `look_rooms`, `m12_boot`, `m3_launch_dock`, `m3_swim`, `m3_cockpit`, `m3_crush`, `m3_ui`, `arm_character`, `arm_jack`, `arm_showcase`, `m5_core_loop`, `m6_helm_speed`, `m6_walk_moving`, `m7_build`, `m8_biomass`, `m8_tools`, `m10_full_run`, `player_body` | | `m2_dry` | **FAILED first** (interior region on 0.2106, off 0.1981): the hands of the Idle animation moved between its two compared shots. The scenario now holds the body still for the two shots; then PASS, interior on 0.1980 / off 0.1980, so the body's pixels ignore the water post-process too | | The rest of the project | **PASS (46):** `core_sim`, `kazah_closeups`, `kazah_showcase`, `look_bloom`, `look_calibrate`, `look_deep_dark`, `look_deep_lamp`, `look_dry_mask`, `look_near_wall`, `look_rock`, `look_sweep`, `look_throat`, `m10_bloom`, `m10_look`, `m10_perf`, `m10_ui_layout`, `m10_ui_windows`, `m1_carve`, `m1_commands`, `m1_terrain_tour`, `m1_world_slices`, `m2_exterior`, `m3_carousel`, `m3_no_walls`, `m3_rotor`, `m4_basalt`, `m4_cargo_display`, `m4_meters`, `m4_mine`, `m4_tool`, `m5_packer_full`, `m5_sell`, `m6_helm_mouse`, `m6_scrape`, `m7_arms`, `m7_craft`, `m7_inventory`, `m7_save`, `m8_hunger`, `m8_no_fauna_inside`, `m9_audio`, `m9_beacon`, `m9_ending`, `m9_hallucination`, `m9_heartbeat`, `m9_leviathan` | | **NOT RUN** since this work (the run was stopped to save cost) | `m9_sonar`, `smoke`, `ui_dash`, `ui_gallery`, `ui_hud_layout`, `ui_windows` (and the two self-tests that fail on purpose) | The 20 scenarios through the character ran before the last source change, which touched one file (`Scenarios_M2.cpp`, the `m2_dry` fix); `m2_dry`, the 46 others and the test suite ran after it. The last build is PASS with 0 warnings and no source changed after it. An `adb.exe` server started by the editor commandlet may still be running; it is harmless and was left alone. Measured by `player_body`: the mesh's `Eyes_Position` socket is **164.9 cm** above the feet in the Idle pose with scale 0.975 (camera 165.0 cm). Mid-jump (0.30 s in the air) the feet are 0.85 m above the floor and the motion is `jumping` on `Jump_Start`; the jump went through `jumping, falling, landing, idle`. Pressed against the galley wall (carrier-local y -9.498, speed 0) both hands are in the picture, not in the wall. ## Hard rules of this track * Nothing under `Content\ParagonTwinblast\` is edited, re-saved, moved or deleted. Ours: C++ in `Source\TheDescent\Player\Body\`, assets under `Content\Player\Twinblast\` made by `Tools\ue\assets_13_twinblast.py`. * The old arm (`Player\Arm\*`) stays, one switch away. ## Files New: `Core/Tuning/DescentTune_Body.h`; `Player/Body/DescentBodyAnim.h/.cpp` (sequence table, the pure animation state, the anim instance and its proxy); `Player/Body/DescentPlayerBodyComponent.h/.cpp` (mesh setup, view settings, switches, lighting, animation input, state section `body`); `Debug/DescentCommands_Body.cpp`; `Debug/Scenarios_Body.cpp`; `Tests/BodyTests.cpp`; `Tools/ue/assets_13_twinblast.py`; `Content/Player/Twinblast/M_Twinblast_LegsMetal.uasset` (generated). Changed: `Player/DescentCharacter.h/.cpp` (member `Body`, `bInTender`, `IsInTender`, `IsFrozen`, `StartJackVisual`; the mesh block of the constructor; the arm is up only with the old-arm switch); `Systems/DescentBio.cpp` (calls `StartJackVisual` instead of the arm directly); `Debug/Scenarios_Arm.cpp` (`arm_character` takes the switch first); `Tools/import_assets.ps1` (`WaitForExit` instead of `-Wait`: the editor leaves an `adb` server behind and `-Wait` waited for it for ever). ## Switches, commands, state * **Old arm back:** `descent.Body.OldArm 1` (console variable; default `Descent::Tune::Body::OldArmViewDefault` = 0) or `Descent.Body.View arm` (`body` to return). The body is then not drawn, the arm is up on foot and swimming and does the jack-in, as before. * **Guns back:** `descent.Body.Guns 1` (default `GunsShownDefault` = 0) or `Descent.Body.ShowGuns 1`. (A command cannot be called `Descent.Body.Guns`: console names ignore case and the variable has that name; the editor refuses to start with both.) * `Descent.Body.ThirdPerson 0|1`: debug, the whole man with his head even in his own view. * View numbers as console variables for trying things in a running game: `descent.Body.ViewBackCm`, `.ViewBackDownCm`, `.FpScale`, `.ArmRaiseL`, `.ArmInL`, `.ArmRaiseR`, `.ArmInR`. Defaults in `DescentTune_Body.h`. * State section `body`: `available old_arm_view guns_wanted mesh shown own_view head_hidden guns_shown pistol_section_shown leg_material in_dry_air scale mesh_back_cm eye_height_cm sequences_loaded motion speed_cm_s move_alpha action_alpha loco_clip loco_rate action_clip`. ## How it works * **Body.** `UDescentPlayerBodyComponent::Setup` (BeginPlay) puts the pack's default mesh `TwinBlast` on the character's own mesh component: feet on the capsule bottom, yaw -90 (the pack faces +Y), **scale 0.975**, no collision. * **Own view** (the view target is the character): the `head` bone and the four front collar bones are hidden (`HideBoneByName`; the coat's standing collar flaps are 15 cm under the eye and filled the picture as two dark horns when looking down); the mesh is a first-person primitive with the camera's first-person scale 0.55 (same place on screen, nearer in depth: the hands, 50 cm out, do not enter a wall the 34 cm capsule touches); it stands 3 cm behind the capsule axis and up to 15 cm when looking down (a head tilts forward over the chest, the fixed camera cannot; without it the chest hides the legs); the two upper arms are turned about their shoulders, left 15 up / 16 in, right 12 up / 18 in (degrees), a little more when looking up, fading to nothing between 12 and 45 degrees of down pitch. No shadow in the own view. * **Any other view** (scenario camera, the helm's cameras): the whole man with his head, the pack's own pose, at true depth, casting a shadow. * **Hidden** in a Tender (`IsInTender`) and with the old-arm switch. * **Animation.** `UDescentBodyAnimInstance` (no AnimBlueprint). `FDescentBodyAnimState` is a pure value stepped from the movement (velocity in the body's frame, in air, swimming, held): Idle (Idle_Relaxed at the helm) / one locomotion clip (Walk or Jog set, four directions, play rate = speed / the clip's own speed) / an action channel (Jump_Start > Jump_Apex > Jump_Fall_Loop, Jump_Land, the swim pose). The proxy samples the pack's sequences and blends them; then the arm turn. * **Light.** The old arm's scheme: dry air, lighting channel 2 only (the lamps of the room the player is in, the flashlight, `ArmRoomFill`); water, channels 0 and 1. Custom-depth stencil 1 ("dry"), like the arm. ## Decisions * **Animation: a C++ anim instance, not the pack's `Twinblast_AnimBlueprint` and no AnimBlueprint of ours.** Why not the pack's blueprint: it cannot be read or changed without an editor session, it has the level-start state and the firing and ability slots, and the first-person view needs what it cannot give (arms turned into the picture, play rate matched to our speeds, a state the tests can read). Sampling the sequences directly also means none of the pack's notifies fire (no Paragon footstep sounds or effects). * **One locomotion clip at a time** (crossfade 0.2 s when it changes), not a weighted blend of directions: the pack's clips hold different numbers of strides, so a blend of two smears the legs. A diagonal plays the clip of the larger component (with a margin against flicker). * **Jog set for our walking.** Measured from the feet: the pack's Walk is 1.38-1.49 m/s, Jog 3.27-3.60 m/s, Sprint 7.6-8.1 m/s. Ours: walk 2.9, carry 2.3, sprint 4.8 m/s. So everything above 2.0 m/s is the Jog clip with its play rate set to speed / 3.4 (0.85 walking, 0.68 carrying, 1.41 sprinting); below it the Walk clip. The pack's Sprint is not used (he bends 35 cm forward in it: the camera would be in his back). * **Swimming: `Jump_Fall_Loop` at 0.35 speed.** The pack has no swim; that clip (upright, legs hanging, arms out in front) is the nearest. * **At the helm: `Idle_Relaxed`** (arms down). In the ready stance his hands were in the console. * **Jack-in: nothing is played.** The pack has no reach-to-a-wall animation. He stands in Idle (held) for the 4.8 s. The mechanic and its timing are `UDescentBioSubsystem`'s own clock and did not change. With the old-arm switch the arm's sequence plays as before. * **Guns: the pistol material section is hidden** (`ShowMaterialSection`, every LOD). Both pistols are one section (slot `M02_Pistol`), so nothing of them is drawn in any pose, including the reference pose where the pistol bones lie at the feet. Not stripped. * **Machine legs: a material, not a shape.** `M_Twinblast_LegsMetal` on the component's `M04_LowerBody` slot: the pack's normal map is flattened to 22 % where it shows leather; plate seams (rings every 9 cm and four upright seams per leg, as dark grooves with a bright bevel) are drawn from the rest-pose position; gunmetal with a different tone per plate; the pack's bands, straps and shoe patches in the arms' red (the pack's `ArmColor`, linear 0.47 / 0.0136 / 0.0046). * **Scale 0.975** so the eyes of the pose he stands in are at the camera (the reference pose is 7 cm taller than his Idle stance). ## What looks wrong or unfinished (the Coder's own eye, stills only) * **The hands are fists around pistols that are not there** (accepted for now). In his own view they sit left and right in the lower third like a boxer's guard; the right hand is nearer and larger than the left (the pack's stance is staggered, left side forward). * **Looking down shows the chest of the coat, the belt and ONE leg** (the forward left one): the pack's Idle is a wide staggered stance, the right foot 27 cm behind him. Both legs show while walking. Below about 45 degrees of pitch the arms are out of the picture. * **The legs read as plated armour on a man's legs, not as machine limbs.** Steel plates with seams and red bands, in the same red as the arms. There are no joints, pistons or cables, the silhouette is trousers and boots, the red bands are still strap-shaped, and the plate grid is regular. A real machine leg needs a model. In dim rooms, from his own eyes, the legs are mostly dark. * **Swimming is a man standing upright in the water with his arms out** (the fall loop, slowed). From his own eyes only the fists show at the bottom of the picture, dimly (they are outside the lamp's cone). * **Jack-in:** no arm goes to the port any more; he stands with his fists up while the ring and the HUD do the telling. * **At the helm** the player's view is the carrier's bridge camera 1.6 m in front of where he stands, so he does not see himself; the chase and the other cameras were not looked at with him in the picture. * **Not seen moving.** Every check is a still or a number: no blend, stride or landing was watched as motion. Feet may slide (one clip per direction, rate matched only to speed), most on diagonals. * **Not done:** turning in place (the body turns with the view at once, the feet do not step); a death pose (he is held in Idle while the screen fades); the carry pose with an ore box (fists up, no box in the hands; the box is drawn by the cargo system as before); the body in a Tender seat; LOD and cost were not measured (61,640 vertices at LOD 0). * **A packaged build was not tried.** The mesh, the sequences and the leg material are loaded by path at run time (like the arm's meshes), so a cook would need `/Game/ParagonTwinblast` and `/Game/Player` in its list of directories to always cook. Every run here is the editor binary with `-game`. * The pack logs one warning at load (`Unable to load Paragon_Proto_Retarget ... class (Rig) does not exist`): an asset of the pack for an engine class that no longer exists. Harmless, not ours, left alone. * Third-person pictures use a plain white lamp so colours can be judged; `tp_front_room_light` is the one without it. ## Numbers from the pack (inspection of 2026-10-11, editor commandlet, read only) * Mesh `TwinBlast`: 237 bones, 5 LODs (61,640 / 32,860 / 20,323 / 12,563 / 6,653 vertices), bounds 189 cm high. It faces +Y; +X is his left. * Material slots: 0 `M00_Head` (head AND shirt), 1 `M01_cloth` (coat), 2 `M02_Pistol`, 3 `M03_Arms`, 4 `M04_LowerBody`, 5-12 tear line, eyes, teeth, hair, eyelash, tear corner, eye shadow, eyebrow. The pistol and lower-body sections exist in every LOD. * Eyes (bone `eyeball_l`) 176.4 cm up in the reference pose; the head bone is 7.2 cm lower in `Idle`. * `Idle`: a staggered ready stance (left foot 36 cm ahead, right 27 cm behind), hands at chest height (z 144-146), 34 cm to each side, left hand 54 cm ahead, right hand 30 cm ahead. From the eye that is 26 and 39 degrees below the horizon and 32 and 53 degrees to the sides: outside a 72 x 44 degree picture. Hence the upper arms are turned up and inward in the own view. * Lengths (s): Idle 10.0, Idle_Relaxed 16.1, Walk Fwd/Bwd/Left/Right 2.233/1.9/1.867/2.033, Jog 1.867/1.567/1.567/1.733, Jump_Start 0.3, Jump_Apex 0.267, Jump_Fall_Loop 1.833, Jump_Land 0.167. None has root motion; all are in place. * Lower-body textures: `_MASK` (A: large panels and belts, B: piping), `_MASK2` (A: bands, straps, shoe patches), `_RGB` (R wear, G edges, A ambient occlusion), `__normals`. ## The four tests (`Tests\BodyTests.cpp`) | Test | What it proves | |---|---| | `Descent.Body.AnimState` | the pure state: idle; 2.9 m/s forward is `Jog_Fwd` at rate 290/340; right, left, backward; a wobbling diagonal keeps one clip; 1.4 m/s is `Walk_Fwd`; 4.8 m/s is `Jog_Fwd` at 480/340; a jump goes Jump_Start > Jump_Apex > fall loop > Jump_Land > Idle; walking off an edge is a fall after 0.12 s with no jump start; a two-frame drop is nothing; swimming is the fall loop at 0.35; held is Idle, held at the helm Idle_Relaxed; weights sum to 1 | | `Descent.Body.Setup` | a character in a throw-away world: the mesh is the pack's default `TwinBlast`, feet on the capsule bottom, scale, yaw, no collision; our anim instance with all 14 sequences, lengths as measured, on the mesh's skeleton; pistol section hidden in every LOD by default (arms still drawn), drawn with the switch, hidden again; the leg override is our material on the lower-body slot, the pack's asset keeps its own, every other slot is the pack's | | `Descent.Body.SeatedAndSwitch` | drawn on foot; `SetSeatedIn(Tender)` hides it, release shows it; `SetHeldBy` (the helm) keeps it drawn, held and relaxed; frozen is held; the old-arm switch hides it and gives it back, the arm component is still there | | `Descent.Body.FollowsMovement` | the real path (movement component > `Refresh` > animation input > state): standing idle, walking forward `Jog_Fwd` with the movement's speed, strafing `Jog_Right` (velocity read in the body's frame), up in the air jumping, falling, landing, idle; no arm turn without a player's view | ## The scenario `player_body` (`Debug\Scenarios_Body.cpp`), pictures in `Saved\Shots\player_body_*.png` (all 33 looked at by the Coder: every one as a 640-pixel tile of a contact sheet, a handful at full size) First person: `fp_idle`, `fp_look_down_45`, `fp_look_down_80`, `fp_look_up_35`, `fp_walk`, `fp_walk_look_down`, `fp_jump`, `fp_jump_look_down`, `fp_wall`, `fp_wall_look_down`, `fp_idle_bridge`, `fp_look_down_bridge`, `helm_view`, `fp_swim`, `fp_swim_look_down`, `fp_swim_forward`, `tender_docked`, `fp_bay_after_tender`, `old_arm_view`, `fp_idle_again`. Third person: `tp_front_room_light`, `tp_front`, `tp_side_right`, `tp_side_left`, `tp_back`, `tp_legs`, `tp_feet`, `tp_arms`, `tp_front_guns_on`, `tp_walk_side`, `tp_jump_side`, `tp_helm`, `tp_swim_side`. Asserted on the way: set up, own-view settings, lighting channels dry and wet, eye height, each motion and clip, the guns switch both ways, helm (drawn, held, seen from outside), Tender (not drawn, back after leaving), the old-arm switch both ways.