Full avatar skeleton on the rig + body anchoring #10

Closed
muse-lantern wants to merge 2 commits from muse-lantern/da-game:full-skeleton into main
Member

Stacked on PR #9 (which is stacked on #8); the diff to review is the single commit 9ae3df5.

The skeleton (Zach's rig requirement, game-planning #19): the rig now carries the full avatar-contract skeleton at all times, mesh or no mesh, so a conforming player model maps straight onto it later.

  • src/player/avatar_skeleton.gd builds it from a data table in code — the contract as text: diffable, and asserted exactly by tests. 51 bones: the 39 verified arms bones (names read from the shipped GLB) + Hips, Spine, Neck, Head + Upper_Leg/Lower_Leg/Foot/Toe per side (leg chain per Resonite's rig requirements). Rest pose standing, facing −Z.
  • Count note, in the open: our arms README records the full rig as 54; only 51 are derivable from the verified subset + documented remainder. Per Zach's call this builds with the 51 — the unaccounted 3 get added when identified (additions don't break the contract; renames would). The discrepancy is also documented in the file header.
  • Mock collisions for IK: an Area3D on physics layer 10 holding capsule/sphere volumes at the torso, head, arm and leg bone positions — root-level mocks today; bone-posed attachments arrive with the real IK work.

Anchoring (the other half of the 2026-10-21 headset findings): PlayerRig.body_floor_transform() — the player body's ground position, yaw only — now roots both the avatar and ArmsView every frame. Arms and skeleton live in the body root, not the tracking root: shoulders follow the body's physical position instead of hanging at the tracking origin. ArmsView's authored 180° yaw composes on top; its solver already converts controller world transforms through the skeleton's global transform, so the solving path is untouched.

Verified: skeleton_check PASS (34 assertions: exact names, parents, rest positions, sides, mocks, anchoring math) · smoke/arms/addon/save/rig_motion/pause PASS · bridge e2e PASS · real boot clean. CI list gains skeleton_check.

— Lantern

**Stacked on PR #9** (which is stacked on #8); the diff to review is the single commit `9ae3df5`. **The skeleton** (Zach's rig requirement, game-planning #19): the rig now carries the full avatar-contract skeleton at all times, mesh or no mesh, so a conforming player model maps straight onto it later. - `src/player/avatar_skeleton.gd` builds it from a data table in code — the contract as text: diffable, and asserted exactly by tests. 51 bones: the 39 verified arms bones (names read from the shipped GLB) + Hips, Spine, Neck, Head + Upper_Leg/Lower_Leg/Foot/Toe per side (leg chain per Resonite's rig requirements). Rest pose standing, facing −Z. - **Count note, in the open:** our arms README records the full rig as 54; only 51 are derivable from the verified subset + documented remainder. Per Zach's call this builds with the 51 — the unaccounted 3 get *added* when identified (additions don't break the contract; renames would). The discrepancy is also documented in the file header. - **Mock collisions for IK**: an Area3D on physics layer 10 holding capsule/sphere volumes at the torso, head, arm and leg bone positions — root-level mocks today; bone-posed attachments arrive with the real IK work. **Anchoring** (the other half of the 2026-10-21 headset findings): `PlayerRig.body_floor_transform()` — the player body's ground position, yaw only — now roots **both** the avatar and ArmsView every frame. Arms and skeleton live in the body root, not the tracking root: shoulders follow the body's physical position instead of hanging at the tracking origin. ArmsView's authored 180° yaw composes on top; its solver already converts controller world transforms through the skeleton's global transform, so the solving path is untouched. **Verified**: skeleton_check PASS (34 assertions: exact names, parents, rest positions, sides, mocks, anchoring math) · smoke/arms/addon/save/rig_motion/pause PASS · bridge e2e PASS · real boot clean. CI list gains skeleton_check. — Lantern
SaveGame gains the working file boundary the design always
planned: save_state/load_state/has_state — a state Dictionary as
human-readable JSON, backup-first (an overwrite copies the old
file to .bak before writing). The #28 full-snapshot skeleton stays
as the documented refinement of what goes inside the dict.

Game wiring:
- New Game writes the run slot (user://saves/run.json)
- Returning to the menu from a game re-dumps, rig still where the
  player stopped — Continue resumes from exactly there
- Continue reads the slot back: level (re)load if it differs, rig
  transform restored wholesale via PlayerRig.place_at_transform
- Continue button lights up whenever a save exists (boot + every
  return to menu refresh it)
- Sandbox never reads or writes the save (throwaway by design)

tests/save_check.gd: unit round-trip + backup contents, boot
no-op Continue, dump/restore pose round-trip through the real
Game flow, sandbox byte-isolation. SAVE CHECK PASS; smoke, arms,
addon suites and the bridge e2e still PASS.
CI: Forgejo Actions test workflow (push + PR, status only)
Some checks failed
tests / test (pull_request) Failing after 14s
1aa6b8be12
Runs everything automatable on the pinned Godot 4.6.3 standard
build, downloaded per run:
- project import
- the headless suites (smoke_menu, arms_check, addon_check,
  save_check; terrain_check joins automatically once it lands —
  branches without it skip it in the open). The suites exit 0
  either way, so the step greps their PASS/FAIL output
- the debug bridge e2e, under Xvfb, which exits non-zero itself

No artifacts are uploaded, nothing is published, no release job —
the workflow only reports test status.

Verified locally: YAML parses; the suite step's script runs all
four suites green and exits 0; a deliberate FAIL probe makes it
exit 1.
Placement negates velocity; strafe on; harness-proof the suites
Some checks failed
tests / test (pull_request) Failing after 19s
fb3998af7d
Zach's bug (found testing main): walk off the sandbox, fall, return
to the menu, New Game — the body's stored fall velocity punched the
player through the floor. Rule (game-planning #28): placement means
at rest, and velocity is never saved. PlayerRig.place_at and
place_at_transform now zero the body's velocity and ground control
via _reset_motion().

Also: MovementDirect's strafe flag was never set (library default
off = stick X discarded = forward/back only). Strafing is the
decided scheme (game-planning #19); strafe = true in the rig scene.

Test fallout, all harness-only (real boots verified clean): in
--script mode a script keeps its first compile, which runs before
autoload identifiers register — the rig's typed children stay Nil
in any suite loading the game scene, and a mid-body script error
once hung save_check by re-running its body on dirty state.
- save_check: completion latch (abort reports FAIL, never loops);
  strafe read from scene state; velocity assertions moved out
- tests/rig_motion_check.gd (new): clean-process unit check that
  loads rig+body scripts after the broken window and exercises the
  real place_at_transform — velocity and ground control zeroed
- CI suite list gains rig_motion_check

Verified: save_check PASS, rig_motion_check PASS, smoke/arms/addon
PASS, bridge e2e PASS.
Pause menu + control settings (hold by_button 2s)
Some checks failed
tests / test (pull_request) Failing after 19s
cfcb78988d
Implements Zach's 2026-10-22 control rulings (game-planning #19):

- HOLD the upper face button (by_button, B/Y) for 2s to pause; the
  press action stays unallocated. Controller menu button and desktop
  Esc open the same pause (they no longer teleport straight to the
  menu place — Return to Menu inside pause does that now).
- Pausing freezes the world (level subtree process mode) and
  anchors the rig in place; the pause panel — a second MenuPanel
  under the persistent Game root — is placed in front of the
  player's view. Buttons: Resume / Options / Save / Load / Return
  to Menu, all functional (no stubs needed).
- Save inside pause writes the run in place; Load restores it
  wholesale; both are GAME-mode only and Save also honours
  Game.save_blocked — the traversal rule that saving is impossible
  there (game-planning #28). The flag is the traversal layer's hook;
  nothing sets it yet.
- Options page = the control settings surface: movement-stick side
  (left/right), turn mode (snap/smooth), snap angle, smooth speed.
  Persisted by GameSettings to user://settings.cfg — their own
  file, separate from the run save. PlayerRig.apply_settings writes
  turn params onto the provider and re-homes the two providers
  between controllers for the stick side (providers re-resolve
  their controller on _enter_tree).
- MenuPanel generalised: its menu control is found by the
  action_selected signal, not by MainMenu type, so main and pause
  panels share the one panel implementation.

Verified: pause_check PASS (pause machine, save gate, settings
round-trip, provider re-homing), save/smoke/arms/addon/rig_motion
PASS, bridge e2e PASS, real boot clean.
CI: install xvfb before the bridge e2e
Some checks failed
tests / test (pull_request) Failing after 17s
ae114a5108
Same fix as on menu-save-v1: the runner image lacks xvfb-run,
which the bridge e2e step needs. Install when missing (sudo only
when not already root).
CI: fetch xvfb from the Debian archive (bullseye is EOL)
Some checks failed
tests / test (pull_request) Failing after 40s
3233031032
Same fix as on menu-save-v1 — archive.debian.org sources for the
xvfb install; see that commit for the run-log evidence.
Full avatar skeleton on the rig + body anchoring
Some checks failed
tests / test (pull_request) Failing after 32s
9ae3df5ade
Zach's rig requirement (game-planning #19): the rig carries the
full avatar-contract skeleton even with no mesh skinned to it,
with mock collisions for IK — one skeleton, arms now, body later.

- src/player/avatar_skeleton.gd: the 51-bone contract built from a
  data table in code (names are Resonite's ExampleAvatarRig
  verbatim, chest -> Chest; the 39 verified arms bones + Hips,
  Spine, Neck, Head + Upper_Leg/Lower_Leg/Foot/Toe per side — the
  arms README's count of 54 is noted in the file; 3 bones are
  unaccounted for and get added when identified, per Zach's
  build-anyways call). Rest pose: standing, facing -Z, +Y along
  each bone. Mock collision volumes (layer 10 Area3D, root-level
  mocks at bone rest positions) for the torso/head/limb bones.
- Rig scene gains an Avatar node carrying it.
- Anchoring: PlayerRig.body_floor_transform() (body ground
  position, yaw only) now roots BOTH the avatar and ArmsView every
  frame — the arms/skeleton live in the body root, not the
  tracking root. This is the fix for Zach's 2026-10-21 headset
  findings (shoulders at a fixed height; arms not following the
  player's physical position): ArmsView's authored 180-degree yaw
  is composed on top, and its solver already converts controller
  world transforms through the skeleton's global transform, so
  the solving itself is untouched.
- tests/skeleton_check.gd (new, clean-process): exact contract
  names/parents/rest positions/sides, mock collisions, anchoring
  math. Added to the CI suite list.

Verified: skeleton_check PASS (34 assertions); smoke/arms/addon/
save/rig_motion/pause PASS; bridge e2e PASS; real boot clean.
CI: dump the e2e game log on failure; add mesa DRI drivers
Some checks failed
tests / test (pull_request) Failing after 39s
9390ef9b27
Same change as on menu-save-v1 (see that commit for the run-log
evidence): print /tmp/da_game_e2e.log when the e2e fails, and
install mesa DRI/GLX with xvfb.
CI: install the X11 client libraries Godot's display server needs
Some checks failed
tests / test (pull_request) Failing after 45s
c15f83d7d7
Run 2990's dumped game log gives the actual root cause of the e2e
startup exit (not GL): 'libXcursor.so.1: cannot open shared object
file' -> the X11 display server fails, the Wayland fallback fails
with it, Godot exits 1 before the bridge ever starts. xvfb does
not pull the X client libraries in. Add libxcursor1 plus the rest
of Godot's usual X11 runtime set (xinerama, xrandr, xi, xkbcommon).
CI: isolated rerun (no code change)
All checks were successful
tests / test (pull_request) Successful in 37s
fc7133087b
muse-lantern force-pushed full-skeleton from fc7133087b
All checks were successful
tests / test (pull_request) Successful in 37s
to 71194b36fe
All checks were successful
tests / test (pull_request) Successful in 43s
2026-10-23 18:57:21 +00:00
Compare
docs/input: menu key + Esc pause on non-SteamVR is settled behavior
Some checks failed
tests / test (pull_request) Failing after 44s
db02f3347a
Zach's ruling (2026-10-23): on non-SteamVR setups the menu key
and desktop Esc opening the pause menu is permanent, intended
behavior — the input system will express it as the oculus/openxr
preset binding rather than a direct call. No runtime-gating in
game code: under SteamVR the menu press is reserved and never
reaches the app, so the scoping lives in the bindings.
muse-lantern force-pushed full-skeleton from db02f3347a
Some checks failed
tests / test (pull_request) Failing after 44s
to d2c181c21c
Some checks failed
tests / test (pull_request) Failing after 1m24s
2026-10-23 19:25:28 +00:00
Compare
Input: tab-indent the generated JSONs; doc the identity invariant + remappability
Some checks failed
tests / test (pull_request) Failing after 52s
1dc060dc4c
Formatting rule (Zach): JSON files we generate use TAB
indentation. Content is unchanged (input_check parses them
semantically).

docs/input-system.md gains two settled points from the ABXY
deep-dive: (1) contract identity is (hand, id) — a shared
positional name never merges physical buttons; A/B/X/Y stay four
distinct inputs because OpenXR state is queried per subaction
path and each controller node reads only its own hand;
(2) bindings are owned by each runtime's setup, 100% where
possible — the presets are suggested defaults, and game code
reads action/contract state only, so user remapping is honored
by construction.
muse-lantern force-pushed full-skeleton from 1dc060dc4c
Some checks failed
tests / test (pull_request) Failing after 52s
to c53df2ae66
All checks were successful
tests / test (pull_request) Successful in 39s
2026-10-23 20:22:11 +00:00
Compare
muse-lantern force-pushed full-skeleton from c53df2ae66
All checks were successful
tests / test (pull_request) Successful in 39s
to e6cf141b6d
All checks were successful
tests / test (pull_request) Successful in 39s
2026-10-23 20:50:01 +00:00
Compare
muse-lantern force-pushed full-skeleton from e6cf141b6d
All checks were successful
tests / test (pull_request) Successful in 39s
to 6b08864dd7
All checks were successful
tests / test (pull_request) Successful in 1m50s
2026-10-23 22:36:00 +00:00
Compare
muse-lantern force-pushed full-skeleton from 6b08864dd7
All checks were successful
tests / test (pull_request) Successful in 1m50s
to 34cbc0928c
Some checks failed
tests / test (pull_request) Failing after 1m1s
2026-10-25 00:45:36 +00:00
Compare
muse-lantern force-pushed full-skeleton from 34cbc0928c
Some checks failed
tests / test (pull_request) Failing after 1m1s
to a583392a83
Some checks failed
tests / test (pull_request) Failing after 58s
2026-10-25 00:52:49 +00:00
Compare
Owner

Remake this PR using the new avatar-humanoid branch for me.

Remake this PR using the new `avatar-humanoid` branch for me.
Author
Member

Superseded by #14 (avatar-humanoid rebuild, per Zach's comment above).

Superseded by #14 (avatar-humanoid rebuild, per Zach's comment above).
muse-lantern closed this pull request 2026-10-25 04:52:39 +00:00
Some checks failed
tests / test (pull_request) Failing after 58s

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dead-air/engine!10
No description provided.