Gun fixes: magwell snap-slider, real renders, eject pose, grip preview, hand posing #40
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
DEAD AIR — Gun fix plan (headset test 2026-11-11)
Status: BUILD AUTHORIZED by Zach 2026-11-11 ("store this as an issue we can
track and then get to work!"), with the decisions in the Decisions section
below. Tracking issue: dead-air/engine #40
(#40) — the full text of this
plan is the issue body, per Zach's rule that tracking issues carry the full
depth, not a summary.
Source: Zach's in-headset test of the generic pistol, 2026-11-11 13:38
(video + image). Branch:
editor-addon, tipb792f03at time of writing.Code traces verified on that tip; line numbers below are from it.
Related docs:
dead-air-mod-toolkit-editor-plan.md(Phase 1 open decision (d)asks whether the mag-rail behavior rides with Phase 1 or stays in the rebuild;
this plan answers it as its first decision),
dead-air-rebuild-change-list.md(mag rail behavior + internal mag, Phase 2, design only, NOT built).
The five problems, as Zach reported them
bottom of the magwell, the mag snaps to that WHILE STILL HELD, slide it up
into the magwell, it "clicks in" and is deparented from the hand.
the old simple cylinder bullet, not the loaded bullet model; the gun still
renders the old simple box mag, not the mag GLB.
right, but the ejected bullet's tip points directly into the mag.
staging scene shows no hand at the grip.
Two standing notes. First, the real-mag-GLB render was HELD by Zach on
2026-11-10 (unfiled, not to be re-raised) — he has now raised it himself as
problem 2, so the hold is lifted by him; this plan records that and does not
re-litigate it. Second, problem 5 is investigation-first by rule: his photos
and video are user-reported symptoms only, never proof of live positions or
build state. Nothing in §5 asserts a cause from them; it lays out how to
reproduce and measure the fault through the bridge instead.
Decisions (Zach, 2026-11-11) — applied to this plan
open decision (d) is answered.
like it slides in at the motion of the hand. The disconnect between object
physics and hand physics (for now) should make this easy. He has plans to
fix that disconnect to a degree, but that is for LATER, not this plan.
Point A is outside (the collider check for the mag's opening reference
point); the vector direction points from A to point B, and the distance
gives the internal stop position. (This replaces the two-absolute-points
a/bframing in §1 below — same segment, parametric form.)you can grab it; the mag only drops when the eject button is released.
Holding eject drops the mag about 30% of the rail length (out along
the rail from the inside), where it holds as a grabbable entity.
collider, and there is no way in the editor to change that to a separate
collider mesh in the model (or assembled primitive meshes for the
colliders). The build must add a way to adjust the gun's colliders in the
editor — a collider mesh from the model, or assembled primitives. This is
needed for the rail's collider check at point A, so it rides with §1.
the snapper point — it is already a value that is there, the physical
object of the mag itself — so no model-swap logic is needed, since the
object instance is still right there. Likewise the first-out bullet uses
the model of the object that is first-out for the mag. That makes it
functional and important for the systems as a whole.
slab" is and suggests Lantern may be mistaken by the gun model or his hand
model. The plan's §2 unknown is reframed: the shape in the video frame is
most likely the gun model's or the glove's own geometry/material — the
identification step checks the GLB materials first, before treating it as
a mystery object. (Show-all decision (2b) is still open — he did not
answer it; first-out only stands until he says otherwise.)
mag_ejectfield containing pos androt, just like other elements — the eject pose is literally the
openingpose, raised a little bit in a specific direction, defined in the editor
just like other elements. The +90° pitch correction is baked into the
default so the tip points forward.
HandView).Toggle between left and right hand; both share the same pose/rot, so the
toggle is purely a visual helper.
setting).
The attached discussion with a different AI agent (Claude, "Per-finger
pose modding API design", 2026-11-09, file
~/workspace/user/files/Claude-Per-finger_pose_modding_API_design-20261111-1408.md)is close to what he wants: per-finger curl 0–1 (0 = hand model resting,
1 = fist), named base poses (
pistol_grip), curl + spread, thumbcurl + opposition, a separate hand-to-item offset, per-joint additive
escape hatch (not absolute), GDScript owning the per-joint curves and
left-hand mirroring, clamps in code, a pose preview with sliders that
exports the JSON, versioned schema. This is a LATER iteration — iterate
and discuss in more detail after the rest of the basic core work is done.
It is stored in the tracking issue, not built in this round.
Data the plan stands on
generic_pistol.json:mag_well{pos (0, 0, 0.06), rot (−12, 0, 0)},mag_well_b(0, −0.0489, 0.0704),chamber(0, 0.0109, 0.0382),muzzle(0, 0.0109, −0.113),grip_offset(0, −0.03, 0.01),grip_rotate[18, 0, 0]. Assembly: slide origin (0, 0, 0.00004)/(0, 90, 0),travel (0, 0, 0.05); fire_select origin
(0.00005, −0.006296, 0.010929)/(0, 90, 0), modes safe + semi, offsets zeroed.
generic_pistol.json:opening{pos (0, 0.07216, −0.02078),rot (−97.274, 0, 0)}, capacity 15,
model_pathgeneric_pistol_mag.glb.bullet_9mm.json:model_pathbullet_9mm.glb,grip_rotate[90, 0, 0].Handgun_Bullet, noderot (0, 89.98, 0); mesh-local long axis X, fat end (radius 5.5 mm) at low X,
tip (radius 2.3 mm) at high X — in the instantiated scene the bullet's tip
points along −Z, base at +Z. Mag is one mesh
Handgun_Magazine, node rot(85, 0, −90), mesh 0.065×0.025×0.137 m. Pistol is 5 nodes, all (0, 90, 0).
1. Mag-well snap-slider
Current state
There is no snap, no slide, and no held-while-sliding state anywhere
in-game. Insert is a proximity check run every physics frame while a mag is
held (
modules/deadair/src/interaction/hand_interactor.gd:269-271calls_try_insert_magazine):MAG_INSERT_RADIUS := 0.09(line 30). The check uses the mag WorldItem'sorigin, not its opening — the opening can be ~16 cm from the well when it
fires.
Firearm.insert_magazine(modules/deadair/src/weapons/firearm.gd: 114-120) is a pure data op (item().add_child_item(mag)) — no pose, noreparenting, no rail. Eject is the mirror image
(
hand_interactor.gd:355-361,_drop_magazine, on BUTTON_PRIMARY press at:255-256): the mag spawns 8 cm below the magwell and falls free; it is not a
grabbable entity held at any rail end.
Two more facts the fix must respect.
mag_well_bis read nowhere in-game— only the editor schema (
addons/da_item_editor/item_schema.gd:29) and thestage reference it. And
mag_well_position()(firearm.gd:110-111) usesWorldItem._pos(...), which extracts pos only and drops rot(
modules/deadair/src/items/world_item.gd:910-913) — so the −12° magwellrotation is ignored in-game today. There is no rail-following logic in-game:
grep -i railovermodules/deadair/src/returns only fence rails, andthere is no
slideat all infirearm.gdorhand_interactor.gd.The closest existing mechanism is
Snapper(
modules/deadair/src/inventory/snapper.gd:54-65):snap()releases the itemand re-grabs it with the snapper node as the "hand" (
wi.grabbable.grab(self, ...)), so the item hangs off the snapper by its primary grab point using thesame
Grabbablefollow. But it holds a fixed pose only — nothing moves anitem along a segment while held — and snapping happens on release
(
hand_interactor.gd:319-324checksBodySlots.slot_for_drop), not onapproach.
Fix shape
New magwell-snap state in
HandInteractor(or aMagwellSnappernode at railend
b, reusing theSnapper.grabpattern), in four pieces. The rail isdefined per Zach's decision (1c) as **point A + vector direction to point B
reference point), the direction points from A to B, and the distance gives
the internal stop position. (The existing
mag_well/mag_well_btwo-pointdata is the same segment in another form — A is the outside end, B the
inside end; the build settles the JSON shape, keeping Zach's authored values.)
mag-origin → magwell point. The check is the opening reference point
against the collider at A.
the rail segment A → B (project the hand position onto the segment; the
segment is ~5.0 cm gun-local,
a(0, 0, 0.06) tob(0, −0.0489, 0.0704)).Slide input is hand motion on the rail axis (decision 1b) — the mag looks
like it slides in at the motion of the hand.
t = 1(reached B), run the existinginsert_magazine+ deparentfrom the hand.
the rail length out along the rail, where it holds as a separate
grabbable entity so you can grab it; the mag only drops (falls free) when
the eject button is released. This replaces
_drop_magazine's free spawn.The mag visual must also start honoring the magwell rotation during the
slide (see §2 — today it doesn't). A shared magwell-basis helper belongs
here:
mag_well_position()drops rot, and both the slide and the visualneed the full basis.
Collider authoring, per decision (1e), rides with this section: the gun
still uses a bounding-box collider, and the editor has no way to change it
to a separate collider mesh in the model or assembled primitive meshes. The
build must add a way to adjust the gun's colliders in the editor — a
collider mesh from the model, or assembled primitives — since the rail's
collider check at point A needs a real collider on the gun, not just a
bounding box. (This is the toolkit plan's Phase 3 / old Phase C colliders
work, pulled forward into §1 because the snap trigger depends on it.)
Decisions needed from Zach
build to settle: the JSON shape for the A + direction + distance rail
(against the existing
mag_well/mag_well_bvalues), and the colliderauthoring UI shape (collider mesh from the model vs assembled primitives,
or both).
Verification
Headless: new rail-follow suite — snap at
bwhile still held → slide toa→ click-in deparents; eject holds atb— alongside existingtests/mag_interaction_check.gd. Bridge e2e: extend the mag test inaddons/da_debug_bridge/mcp_server/test_e2e.py(place_controllerdrivesthe mag opening to
b, then along the rail;grab_stateasserts still-heldmid-slide, then inserted). In-headset feel check.
2. Real renders for the snapped bullet and mag
Current state
The inserted mag is a procedural box,
firearm.gd:271-286(
_refresh_mag_visual):The mag JSON carries no
model.size, so it is always the default 30×100×40mm dark-gray box, with no rotation (magwell −12° ignored), at the magwell
dropped 40% of its height. The real mag GLB is 24.6×142.4×77.1 mm. Bullets
in a loaded mag are a procedural gold cylinder,
world_item.gd:625-641(
_build_first_out_bullet):_cyl(rs.x, rs.y, false)withMagazines.round_size→ pistol defaultVector2(0.0045, 0.029)(
modules/deadair/src/weapons/magazines.gd:44-51), posed at the opening withthe opening rotation, visible only while the mag holds ≥1 round
(
_refresh_first_out_bullet,world_item.gd:253-256). Only the first-outround renders — show-all stays parked (comment at
world_item.gd:620-624).The bullet GLB is never instanced for it.
What exists to build on:
model_pathis already resolved to a loadedPackedSceneon both the element and the definition(
modules/deadair/types/item.gd:102-103and:60-61), somag.definition().world_sceneis the mag GLB, available to_refresh_mag_visualtoday._build_visual(world_item.gd:486-500) alreadyinstantiates
world_scenefor the mag's own WorldItem — and still builds thefirst-out bullet for GLB mags (line 500). The editor already solved the
posing math the game would need: the magwell ghost uses orient basis =
magwell basis with the wrapper positioned by measured bounds
(
addons/da_item_editor/item_stage.gd:1508-1515).Fix shape
Per decision (2a): use what is contained in the snapper point. The mag
object instance is already there —
insert_magazinedoesitem().add_child_item(mag), so the physical mag is a child of the gun —so no model-swap logic is needed: the visual shows the contained mag
instance itself instead of building a procedural box. Likewise the first-out
bullet uses the model of the object that is first-out for the mag — the
contained bullet's model — which makes it functional and important for the
systems as a whole.
_refresh_mag_visual: show the contained mag instance (its ownworld_scene/visual) at the game's insert pose (magwell pos + the 0.4drop, with the magwell rotation applied — needs the magwell-basis
helper from §1), accounting for the GLB's own node rotation (mag mesh
node is rotated (85, 0, −90) in the file; mirror the editor's
orient/bounds math, don't re-derive it). Keep the box as fallback when
the contained mag has no model.
model (the bullet GLB) instead of the cylinder, applying the same +90°
pitch correction the editor uses (see §3) because the GLB's nose is at
−Z. Keep the cylinder as fallback when the bullet has no model.
ring (
world_item.gd:596-615, bullet band is procedural-path only; samewas true for the mag band at
:568-591).Unknown — the cyan slab (reframed by Zach)
The authored mag visual is dark gray (0.12, 0.12, 0.12), so it does not
explain the flat cyan shape on the side of the grip in the video's last
frame. Zach's correction (2026-11-11): he has zero clue what the "cyan
slab" is and suggests Lantern may be mistaken by the gun model or his hand
model. The identification step therefore checks the GLB materials first —
the pistol, mag, and glove models' own geometry and materials — before
treating it as a mystery object. If it is part of the gun or hand model,
there is nothing to fix here beyond the real renders above; if live bridge
identification (
list_items+ snapshot + a render of the held loaded gun)shows something else, that finding goes into the build. The only other cyan
in-game objects found are the
BoundsGizmowireframe(
world_item.gd:372-378,Color(0.2, 0.9, 1.0), built on every item butgizmos_enableddefaultsfalse, line 69 — and it is 1 px lines, not aslab) and the
VolumeContainer(
modules/deadair/src/inventory/volume_container.gd:37WIRE_COLORcyan(0, 0.8, 1.0),
:39SOLID_COLORteal (0, 0.35, 0.38)) — which is fixed inthe sandbox, not attached to the gun.
Decisions needed from Zach
Verification
Headless
mag_filo_check/firearm_check(visual node type + poseassertions); xvfb render of the inserted mag (GLB silhouette, not a box)
and the first-out bullet; bridge e2e (spawn loaded gun,
list_items+screenshot); in-headset check.
3. Bullet-eject visual pose
Current state, end to end
_eject_bullet(hand_interactor.gd:411-430):_float_position(:435-437) =magazine_opening_position() + mag_global_up * FLOAT_HEIGHT,FLOAT_HEIGHT := 0.04(line 35)._update_floating_bullet(:443-461) re-applies the same transform everyphysics frame while the trigger is held, and on release unfreezes the
bullet and clears tracking (
:456-460).magazine_opening_basis()(
world_item.gd:241-243) is justBasis.from_euler(opening rot)— for thismag,
R_x(−97.274°), with no GLB correction. The spawned round is a realWorldItem, so it renders the bullet GLB.Why the tip points into the mag — measured, not guessed. The editor ghost
applies
BULLET_OPENING_PITCH_CORRECTION := 90.0(
item_stage.gd:92: "The correction maps the GLB bullet's nose onto +Y. Theopening rotation is then composed on top") at
:1507and:1550. The gamepath never applies it. With the probe's finding (GLB nose at −Z): opening
rot
R_x(−97.274°)maps nose −Z → (0, −0.992, +0.127) in mag-local space —nose pointing down into the mag body, exactly the reported symptom. With
the editor's correction applied first (nose −Z → +Y), the same opening rot
maps +Y → (0, −0.127, −0.992) — nose forward (−Z), slightly down, matching
the first-out intent stated at
world_item.gd:202-204("base at the back(+Z), nose forward (−Z)"). The procedural first-out cylinder never showed
this because a Y-axis cylinder is symmetric — the bug only bites the GLB
bullet, i.e. exactly the eject path. Position is right because
_float_positionis correct; only the rotation is wrong.Fix shape
Per decision (3a), fix shape B is what Zach wants: a
mag_ejectfieldcontaining pos and rot, just like other elements. The eject pose is
literally the
openingpose, raised a little bit in a specific direction,defined in the editor just like other elements. So: a new magazine-table
field
mag_eject: {pos, rot}; the eject float pose(
_eject_bullet/_update_floating_bullet) uses it; the editor authorsit like
opening(a marker/ghost for the floating bullet's pose). Thedefault is the opening pose raised a little in the eject direction, with
the +90° pitch correction baked in so the tip points forward, not into the
mag. (The minimal (A) correction is subsumed: the correction is part of the
default, not a separate code path. The build must still check the callers
of
magazine_opening_basis()before touching the shared function — thefirst-out cylinder path uses the sibling
magazine_opening_rotation()andmust stay untouched.)
E2E link
The one remaining e2e failure, "bullet stays held in L after the trigger
release" (
test_e2e.py:467), exercises this same path: eject pops the bullet(
:455), L force-grabs it (:460), R releases the trigger (:464), checkL still holds (
:467). The release branch is_update_floating_bullet: 443-461: if L has grabbed it,is_held()clears tracking (:452-455);otherwise
not trigger_downunfreezes it and clears (:456-460). Thefailure means L's held is
Noneafter release. Candidate mechanisms,hypotheses only: the unfreeze-on-release firing while the grab is mid-flight;
force_grabracing the per-frame transform rewrite at:461while L's_followis also driving the bullet; frozen-body +move_and_collideinterplay while held. It needs a bridge run with
grab_statetraces todistinguish. The plan treats the eject-pose fix and this failure as the
same verification gate (re-run the e2e mag test), and if it still fails, it
is a separate grab/freeze bug, not the pose.
Decisions needed from Zach
magazine table (+ editor support)?
Verification
Headless
mag_interaction_check/mag_filo_check(eject pose assertion:bullet nose basis vs opening); bridge e2e mag test (the "eject pops a loose
bullet" + "stays held" checks); side render of the floating bullet showing
the tip forward, not into the mag.
4. Grip-pose preview in the editor
Current state
The stage's entire grip visualization is a 6 cm RGB axes triad.
_add_grip_marker(item_stage.gd:882-895) builds aMarker3Dnamedgun_gripatgrip_default pos + grip_offset, rotationdrot + grip_rotate, whose only child is_make_axes(GRIP_AXIS_LEN)(:1665-1686,GRIP_AXIS_LEN := 0.06, line 63) — unshaded ImmediateMesh lines. A grep forglove|HandView|hand_model|vr_gloveinitem_stage.gdreturns zerohits. The secondary grip is the same story: a
gun_foregripmarker exists(
:607, and the game buildsGrabSecondaryfromweapon.foregripatworld_item.gd:145-150— onlyrifle_test.jsoncarries it), but no hand isshown there either.
What exists to reuse: (a) the ghost/reference pattern:
_load_item_model_node(
item_stage.gd:1558-1570) loadsworld_scene, elsemodel_pathresolvedagainst the item JSON's directory, else the procedural stand-in;
_build_references_for(:1414-1450) wraps it in awrapper Node3D + orient Node3Dpair positioned by measured bounds (_position_reference,:1500-1515). A grip preview would instance a hand model the same way: awrapper at the
gun_gripmarker pose with an orient node. (b) The handasset:
modules/deadair/assets/player/hands/vr_glove.glb(Valve vr_glove,mono right-hand model;
HandView.model_pathdefault,hand_view.gd:50).(c) A curl driver already exists:
HandView.set_debug_curl(
hand_view.gd:783-786, via the bridge'sset_hand_curl).The catch: instancing the raw GLB is not the same as the game's hand.
HandView(modules/deadair/src/player/hand_view.gd) builds the model withits OPENXR alignment correction measured from the skeleton's rest geometry
(
_apply_model_correction,:481-544;_openxr_alignment,:576-602),which needs a processed frame to calibrate (
_build_and_calibrateawaitsone frame,
:119-125). A raw GLB in the stage would sit in uncorrectedmodel orientation. So the preview must either reuse
HandViewitself (itbuilds under any plain
Node3Dparent) or share the correction math — notre-derive it.
Fix shape
Per decisions (4a–4c), fix shape A is what Zach wants: the stage
instances a
HandViewat thegun_grippose — reusing the game's ownalignment, showing exactly what the hand looks like on the grip — with a
left/right toggle (both hands share the same pose/rot, so the toggle is
purely a visual helper) and a ghosted/solid toggle in the editor preview
(non-persistent setting, not saved to JSON or metadata).
Per-finger posing is a LATER iteration, not this build. Zach wants to be
able to pose the hand grip via this as well, and the attached Claude
discussion ("Per-finger pose modding API design", 2026-11-09) is close to
what he wants: per-finger curl 0–1 (0 = hand model resting, 1 = fist),
named base poses (
pistol_grip), curl + spread, thumb curl + opposition, aseparate hand-to-item offset, a per-joint additive escape hatch (not
absolute), GDScript owning the per-joint curves and left-hand mirroring,
clamps in code, a pose preview with sliders that exports the JSON, versioned
schema. Iterate and discuss in more detail after the rest of the basic core
work is done. It is stored in the tracking issue, not built in this round.
(The foregrip second-hand question from the old (4c) is folded into that
later iteration unless the basic preview makes it trivial.)
The pose the stage would show is the same one the game computes
(
world_item.gd:140-173uses the samegrip_default + grip_offset / grip_rotatevalues), so the preview's value is precisely seeingHandView'scorrection + curl against the authored grip.
Decisions needed from Zach
per-finger posing later). Remaining for the build: the toggle placement in
the dock (near the slide sim / reference slots area) and whether the
preview defaults on or off.
Verification
Editor render (glove on the pistol grip at the authored pose, fingers wrapped
vs floating); headless suite (stage builds a grip-preview node at the marker
transform; the dock already instantiates headless in
item_editor_check).5. Hand posing when holding a grip — investigation first
Nothing in this section is asserted from Zach's screenshots or video — they
are symptoms only. This is the code chain plus how to measure the fault.
The chain, with lines
LeftGrip/RightGripareXRController3Dwithpose="grip"(
modules/deadair/src/player/player_rig.gd:18-30,:83-86). Each carriesa
HandView(Valve vr_glove, mono model mirrored for the left).HandViewdefaults toAlignment.OPENXR(
hand_view.gd:73), correction measured from skeleton rest geometry in_apply_model_correction(:481-544) via_openxr_alignment(
:576-602);openxr_tune_*are zero by default (:76-77). TheLEGACY_TUNEpath withtune_euler_deg (45, −10, 100)(:88) exists butis not the default — and
tune_enabled := true(:83) only affectsthe LEGACY path (
:559-561). Target frame for OPENXR: finger direction →(0,−1,0), palm →(±1,0,0)(:583-584), fist centre placed a littlewrist-side of the middle knuckle (
:591-593).Grabbable._follow(
modules/deadair/src/interaction/grabbable.gd:319-351); single-handtarget =
h0.global_transform * p0.affine_inverse()(line 350), wherep0is theGrabPoint.grip_transform()(
modules/deadair/src/interaction/grab_point.gd:30-36). So the item hangsoff the hand by the inverse grip pose — the glove orientation is the
hand node's orientation.
world_item.gd:140-173— weapon base rotation(90 − rake, 0, 0)=(72, 0, 0)for the pistol (rake 18.0), authoredgrip_posesoverride (the pistol JSON has none), thengrip_offsetadded to position (
:167) andgrip_rotateadded torotation_degrees(
:173): total pitch 72 + 18 = 90°.FunctionPointeron the grip controllerwith base −90° about X (
player_rig.gd:100-109,_apply_pointer_angle:403-424). The beam is the pointer's −Z after its own base rotation;the glove follows the grip controller's basis. They only agree if both
derivations are right — consistent with the 2026-10-28 finding that the
laser was the most visibly misaligned element while the headless check
found the glove derivation correct (glove 0° off the Grip ring).
set_curl(hand_view.gd:765-777) — index = trigger amount, others= grip amount, thumb = grip × 0.7,
REST_CURL 0.12,JOINT_CURL [1.35, 1.75, 1.05, 0.30]; axis/sign auto-calibrated from rest geometry(
_calibrate_curl,:687+). No half-curl for non-thumb exists — theamount *= 0.5change is Zach's pending edit; do not propose applying it.Measurement surface (bridge, already built)
hand_debug(addons/da_debug_bridge/debug_bridge.gd:940-963→HandView.debug_snapshot,hand_view.gd:193-218) reports alignment, tunevalues,
curl_axis/curl_sign, measuredf_dir/palm,thumb_dot,palm_flipped,target_f/target_p, and the correction origin/euler. Plusgrab_state,force_grab,set_controller_input,list_items(
addons/da_debug_bridge/mcp_server/server.py:250-549),place_controller/place_rig, andset_pose("fists"/"open",debug_bridge.gd:985-1000, which drives the live HandViews viaset_debug_curl).Reproduction protocol (no photos)
force_grabin R (the e2e pattern,test_e2e.py:411-419).grab_state(hand pose + held item pose) andhand_debug(measured frame + correction).
barrel direction; angle between the laser beam and the barrel; glove basis
vs
GrabPointbasis. Expected: barrel level with the beam per thegrip_rotate [18,0,0]fix; glove 0° off the Grip ring per the 2026-10-28headless finding.
set_pose "fists"to isolate curl from orientation, andone-handed vs two-handed (see H6).
Hypotheses, each with its killing measurement
(90 − rake) + grip_rotate= 90° total assumes the Fab pistol's grip rake is 18°; if itsactual grip angle differs, the barrel won't sit level with the hand's
natural pointing direction. Measure: barrel direction vs
grip-controller forward with the gun force-grabbed (
grab_state+list_itemsposes), against the model's own geometry (probe: barrel along−Z after the (0, 90, 0) node rotation).
correction derives
f_dir/palmfrom skeleton rest positions; wrong bonemapping (
RigChainTypes) or a bad rest pose rotates the glove relative tothe grip frame. Measure:
hand_debugmeasuredf_dir/palmvstargets ((0, −1, 0) / (±1, 0, 0)) and the correction euler — is the
measured frame sane?
right-hand model (
hand_view.gd:44-48,_MIRROR_BASIS). Measure:hand_debugboth sides; comparethumb_dot/palm_flippedL vs R.The beam comes from the
FunctionPointerwith its own −90° X base whilethe glove follows the grip controller. Measure: pointer global basis vs
grip global basis vs muzzle basis, numerically via
grab_state+snapshot.
or the thumb opposes wrong. Measure:
set_hand_curlto full grip viabridge and read fingertip positions — note
hand_debugdoes notreport fingertip positions, so this may need a new bridge readout; flag
it.
_followbranch. For a two-handed hold,_follow(
grabbable.gd:334-348) does full basis alignment (primary→secondary axisonto the palm-to-palm line) — the known pivot gap (rifles = pitch/yaw-only
steering, pistols = secondary hand snaps to the gun; not yet built). If
Zach held the pistol two-handed, the twist could be this branch, not the
grip pose. Measure: reproduce one-handed vs two-handed via bridge and
compare glove/item bases.
Decisions needed from Zach
in R); whether to include the two-handed case; the half-curl call stays
his.
Verification
Bridge-driven numeric comparison (angles printed, not eyeballed) + debug-cam
renders (yaw 180 = front) showing glove vs grip ring vs laser; a headless
suite asserting the measured angles once the cause is found. Re-measure
rather than re-derive — the 2026-10-28 headless diagnosis found the
derivation correct, so the error may be in the model import, the mirror, the
pointer, or the two-hand branch.
Suggested sequencing
is verified, not guessed), and it shares its verification gate with the
one remaining e2e failure. If the e2e still fails after it, the failure is
a separate grab/freeze bug — split it out then, don't bundle it.
GLB materials first (Zach's correction), then live bridge identification
if needed, before changing the visual. Builds the magwell-basis helper
§1 also needs.
magwell-basis helper; biggest piece. Rail is A + direction + distance;
eject holds at 30% of rail length.
any of the above. Basic core work only (HandView + toggles); per-finger
posing is a later iteration.
after the cause is measured. Do not bundle its fix with §1–§4 — different
evidence, different risk.
Decisions needed from Zach (collected)
Answered 2026-11-11 (see Decisions section): (1a) rides with the toolkit;
(1b) hand motion on the rail axis; (1c) rail = point A + direction to B +
distance; (1d) eject holds at 30% of rail length, drops on release; (1e)
collider authoring rides with §1; (2a) contained object, no model-swap
logic; (3a)
mag_eject{pos, rot} field, shape B; (4a) HandView, L/Rtoggle (visual only); (4b) ghosted/solid toggle, non-persistent; (4c)
per-finger posing is a later iteration.
Still open: (2b) first-out only, or lift the parked show-all too? (5a)
hand-posing reproduction scope: one-handed R, two-handed too?
Unknowns the plan flags
gun model's or the glove's own geometry/material; check the GLB materials
first, then live bridge identification if needed, before changing the
visual.
the eject pose, but the failure is about held-state after release, not
pose; needs a bridge run with
grab_statetraces.magazine_opening_basis()callers (§3) — the eject path uses it, butthe callers were not exhaustively listed; the build must check them before
editing the shared function.
HandViewcalibrates headlessly (§4) — it awaits aprocess frame; the dock instantiates headlessly in
item_editor_check,but whether the editor
_processticks it enough to calibrate needs aprobe during the build.
the existing
mag_well/mag_well_bvalues; the build settles it, keepingZach's authored values.
adjacent): the rebuild doc already lists marker names as "
gun_slide(slide + bolt)" — i.e. the standing design treats
gun_slideas coveringboth, which bears on Zach's "drop gun_slide in favor of gun_bolt" line.
Out of scope / held
iteration, after the basic core work — stored in the tracking issue, not
built in this round.
is a known gap that H6 may implicate — it is NOT in this plan unless the
§5 measurement points at it; if it does, it becomes its own plan.
real-mag render hold was lifted by Zach raising problem 2 himself.
Appendix — Per-finger hold-pose modding API (later iteration, full design)
This is the later iteration from decision (4c): Zach wants to be able to
pose the hand grip via the grip preview as well. Source: a discussion with
a different AI agent (Claude), "Per-finger pose modding API design",
2026-11-09. The design below is reproduced in full so this issue is
self-contained; it is NOT part of the current build — iterate and discuss in
more detail after the rest of the basic core work is done.
The question (Zach, 2026-11-09)
For a modding API system where users can slap in a model, generate some
JSON, and boom have a new item/gun, I want to make it easy for people to
mod in IK poses for the items being held. I'm thinking a per-finger curl 0
to 1 float %, zero is hands model resting position, 1 is fist. Should I
allow them to do per-joint pos/rot/% sliding in the JSON? Or only expose
the 0 to 1 value in the JSON and have the per-joint be built into GDScript?
The answer: do both, in layers
Make the 0-1 curl the primary interface, and let the per-joint override be
an optional escape hatch that most modders never touch.
Why curl-first
the top," not "index proximal is -42 deg on X."
the hand rig, change bone orientations, or add a second hand model, every
existing mod still works.
per-joint rotations in JSON are tied to your rig's bone axes, so they
break the moment you touch the rig.
What GDScript owns
distributes across the proximal, intermediate and distal joints. Real
fingers don't curl evenly, and the distal joint usually follows the
intermediate one at roughly 2/3 ratio. A tuned curve looks much better
than linear interpolation.
also swings across the palm. Give it
curlplus anoppositionvalue,or just treat it as two floats.
What the JSON exposes
Three things in there are worth copying:
pose) that the game ships and the modder tweaks.Most items can just say
"pose": "pistol_grip"and be done, which is thereal "easy" path.
problems are placement, not finger angles.
The escape hatch
Add an optional
"joints"block that applies additive offsets in degreeson top of the curl result:
Make it additive instead of absolute. The curl still does 95% of the work,
and a modder can nudge one knuckle without re-authoring the whole finger.
If you let them set absolute joint rotations, they will, and then you can
never change the rig.
Practical notes
palm.
JSON. Authoring numbers blind is the real friction, more than the schema.
"schema": 1) so you can change theunderlying hand model later without breaking mods.
Closing: all five phases built and merged in PR #41 (merge commit
a5838000d4) — eject pose, real renders, snap-slider + eject-hold + collider authoring, grip preview, and the hand-posing investigation. The hands remainder (H5 curl calibration, H6 pivot split) now lives in #42, with merith's decoupled-hands direction folded into that issue.muse-lantern referenced this issue2026-11-14 04:45:07 +00:00