DA Mod Editor: staging scene, markers, rails, assembly, reference slots, scroll (issue #35) #41

Merged
merith-tk merged 24 commits from muse-lantern/da-game:editor-addon into main 2026-11-12 01:48:16 +00:00
Member

The DA Mod Editor (issue #35) — the editor addon for authoring items — plus the generic gun models and the game-side assembly carry-through that came with it. Branch: muse-lantern/da-game:editor-addon, based on main at 1a008dd (PR #36 merge). 17 commits, 80 files, +6229/−311.

Merge is merith's call, as always. The gun-fix work (issue #40) is in progress on this same branch and its commits will land here as they push.

What this is

A Godot editor plugin (addons/da_item_editor/, "DA Mod Editor") that loads an item JSON + its model into a staging scene in the 3D viewport, lets you position gun_* markers and rails, bind model parts to roles, preview reference items and slide travel, and save the JSON back (tab indentation, line endings preserved). The game side carries the tiered assembly dict through load so mods can read their own keywords.

What's in it, by commit

Phase A — staging scene, markers, rails (28dbba1, 5d520d7)

  • Plugin scaffold + dock, staging scene (item_stage.tscn / item_stage.gd).
  • gun_* markers schema-driven by type tables (gun_muzzle, gun_mag_well.{a,b}, gun_grip, gun_foregrip, gun_slide, gun_chamber); rails = ref A/B at pos/rot, part slides on a virtual rail, visuals = two gizmos + holographic line; gun_grip shows the computed primary grip pose; saving writes back the delta.
  • JSON round-trip: tab indentation, line endings preserved. item_editor_check suite added.

Phase B — create, manage, detect (b8e065f)

  • Create (template picker Weapon/Magazine/Ammo/Consumable/Generic-primitive, module picker, validated new ids), manage (module-filtered item list, duplicate into another module — only the JSON copied, model reference stays pointing at the original module's assets; a relative model_path is rewritten to an absolute world_scene on cross-module duplicate), delete with confirmation. Rename deliberately left out (id references would go stale).
  • File detection: clicking an item JSON / model TSCN in the FileSystem dock loads it into the tool. (Still needs merith's in-editor check: single or double click?)

Dock (75f5621, 075ff2c)

  • Dock in DOCK_SLOT_RIGHT_UL (a tab with Inspector/Signals/Groups). Cleanup + rename to "DA Mod Editor" (label only — addon dir addons/da_item_editor/, classes DAItemEditorDock / DAItemSchema unchanged). "New item" collapsible, collapsed by default; "Browse…" removed; marker rows use a status dot (green = in JSON, amber = placed but unsaved, grey = not set).

Staging-scene gizmos (afec5f0, issue #35)

  • BarrelAxis (amber 0.5 m line from the gun_muzzle marker along −Z), MagBounds (cyan wireframe of the mag AABB), SlideForward/SlideBack/SlideTravel (yellow wireframes; travel from gun_slide rail / slide_travel field else 0.05 m default). Gizmos rebuild with the stage and follow live marker transforms.

Reference slots + editor metadata + slide simulation (3fb4103, b7c110d, issue #38)

  • Ghost reference items in named sockets: gun → chambered bullet at a new gun_chamber marker + magwell mag at mag_well in the game's insert pose; mag → opening bullet, GLB long-axis corrected +90°. Per-slot dock dropdowns, TAG-GATED (magwell: mag tag; opening/chambered: ammo tag), defaulting to the accepted item. CASCADING: a slot rendering an item surfaces that item's own slot options; tree walked via metadata files, max depth 4.
  • Editor metadata: <item>.modtool_meta.json alongside <item>.json, auto-loaded when present, never read by the game; stored "" = explicit none. gun_chamber default in generic_pistol.json = (0, 0.0109, −0.05) — a guess; merith to drag it to the real breech in the editor.
  • Slide simulation: dock slider 0–1, "Rack / fire" cycle (0→1 in 0.15 s, →0 in 0.25 s); travel from rail / slide_travel else 0.05 m.

Magwell coupling (3272876, issue #38)

  • The magwell rail node's transform IS the magwell (placed at JSON mag_well on load; end_a local (0,0,0), end_b local mag_well_b−mag_well); the mag reference ghost and cyan mag-bounds gizmo follow live via the existing _process hook; save writes stage-space end positions.

Tiered assembly + magwell rotation (a3db1b4)

  • Assembly schema, confirmed with merith: top-level assembly; next tier is the part (slide, bolt, rail, trigger, fire_select, whichever); inside each part: object (the object's name in the model file, e.g. Handgun_Upper — Godot sanitizes Object.001 to Object_001) + origin {pos, rot} + that part's values. Slide travel = {pos, rot} relative to origin, applied as a % modifier: pose(t) = origin + t×travel. fire_select.modes keys are exactly safe, semi, burst, auto; each mode is a {pos, rot} offset from origin.
  • Dock Components section (tree-picker Assign with name + path columns, Clear, Add part, ⚠ missing/ambiguous warning, fire_select mode dropdown that poses the selector); assembly-first resolution with name fallback everywhere (slide gizmos/sim, fire_select, mag_release); slide sim + gizmos read assembly travel.
  • mag_well accepts a {pos, rot} object form (plain array still valid, rot defaults zero, mirroring magazine opening); the rail node's rotation IS the magwell rotation; mag ghost + cyan bounds box rotate with it as an oriented box; stage _process dirty-check includes rotation; collect writes {pos, rot} when rotated or already a dict, plain array otherwise; insert pose and 0.4 drop factor unchanged.
  • Seeded generic_pistol.json (pure insertion): 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 only (the gun's weapon.modes is ["semi"]), mode offsets zeroed.
  • Assembly is extensible/arbitrary by design (merith, 2026-11-11): any part names, any keys; core systems handle only their own keywords; a modder can add new keywords (e.g. mag_release) and write mod-side GDScript that reads them.
  • Game-side in this commit: compat shim only — WorldItem._pos() accepts both mag_well forms; firearm.gd mag_well_position() / _refresh_mag_visual() use it.

Generic gun models (fcdd0a0 by merith, 8e5f108, 1103edb)

  • fcdd0a0 "Replace Test Gun with Generic Gun Model": Fab.com asset pack, listing 3ee0ae1f-b364-4951-afcf-0c705035e386 (credited in modules/devkit/assets/README.md) — generic pistol GLB, generic pistol mag GLB, 9mm bullet GLB, knife texture only. Item JSONs use model_path to the GLBs. Registry after the renames: devkit:ammo/bullet_9mm, devkit:ammo/bullet_9mm_tracer, devkit:mag/generic_pistol, devkit:weapon/gun/generic_pistol, devkit:weapon/gun/rifle_test.
  • 8e5f108 fixed the fallout: mag gained the mag.generic_pistol tag (gun now accepts its mag); all old-id references updated (9 suites, spawn_loaded, e2e, start_kit, example gun, bridge server, modding.md, MECHANICS regenerated); 4 orphaned .blend.import files deleted; single texture at modules/devkit/assets/textures/generic_color_map.png. The alongside PNGs stay (the GLBs do NOT embed the texture; true dedup needs the .blends relinked + GLBs re-exported — merith's Blender-side step).
  • 1103edb pitched the generic pistol up: grip_rotate [90, 0, 0] → [18, 0, 0]. Total grip pitch was 162° (base 72° from the 18° default rake + grip_rotate 90°), barrel 72° below the level aim beam; now total pitch is 90°, barrel level with the beam. Verified: the yellow tracer runs horizontally, following the level red beam.

Assembly carry-through (712d664)

  • The game loads and exposes the raw assembly dict (ItemSpec.assembly + assembly_part()); apply_dict reads it deep-duplicated, to_item_definition copies it, from_dict sets it and keeps it out of type_tables. Non-dict → {}, absent → {}. No game-side consumption yet — core reads only its known parts. This is the prerequisite for toolkit Phase 1, not Phase 1 itself.

Rotation propagation + tscn guard (02c2a6a)

  • Opening-rotation bug reproduced and fixed: rotating the opening marker now rotates the opening bullet ghost live (the stage _process dirty-check never observed the opening rotation, and the position update never re-applied orientation). One rotation source per anchor (_magwell_rot() / _opening_rot()) consumed by gizmos, references, and collect.
  • Two more rotation drops found while testing thoroughly, both fixed: rail-load drift (a _b in the JSON is a stage-space position but load took it into the rail frame with a plain subtraction, dropping the rotation — end_b 10.45 mm off, and collect wrote the drifted value back, so the end would walk one rotation per save/load cycle; load now converts stage→local with the rail transform) and bounds double-refit (measuring the orient subtree then applying the orient basis refit the AABB twice, inflating magwell bounds 0.1503 → 0.1553 on the first live update; new _orient_bounds() helper, one refit).
  • Game path verified correct and untouched: first-out bullet rotation/position match the opening rot/pos.
  • item_stage.tscn guard: item_json_path was an @export, so the dock assigning it marked the scene dirty and a save baked the pistol path into the template. It is now a plain var with the same setter — the live instance still rebuilds, but the value can never serialize. Committed tscn restored to the stable form the editor writes (uid, ext_resource uid, unique_id, no item path). Verified headlessly; a real editor open→use→save cycle still needs merith's check.

Dock scroll + mag-release rail drop (b792f03)

  • Dock content now lives in a ScrollContainer (vertical expand-fill, horizontal scroll disabled) holding a VBox scroll body; dialogs stay direct children of the dock root (popups, not scrolled content). Sections collapsible, mirroring "New item": Markers/rails, Components, References, Slide. Defaults: Markers + Components expanded, References + Slide collapsed. Each component row expands to show origin, travel, and modes (display only, nothing writes JSON).
  • gun_mag_release rail dropped (grep-proven: zero readers in game or editor, no item JSON carries it). The mag_release assembly part stays — the part in the model file a mod's GDScript drives, not the rail marker. gun_slide untouched (merith's "drop gun_slide in favor of gun_bolt" is awaiting clarification of what gun_bolt is).
  • Add-part charset already relaxed in 712d664 ([A-Za-z0-9_] → [A-Za-z0-9_-.]); proven with suite checks this round.

Verification (on tip b792f03)

  • Double --import: 0 ERROR both passes. tools/check.py: 6/6.
  • Full headless suites: 34/34 PASS, 1,518 ok: lines (item_editor_check 185, modloader_check 73, mag_filo_check 43). SCRIPT ERROR grep: exactly the 3 known pre-existing lines (2× add_child on null, 1× nonexistent is_trigger_down).
  • Bridge e2e: 118 ok. The 3 known mag-insert failures are GONE (merith's new mag model/opening in d922e2b fixed them). One failure remains: "bullet stays held in L after the trigger release" (eject flow), proven pre-existing on pure d922e2b — its fix is part of issue #40's §3 verification gate.
  • Renders verified on the VM (xvfb, no --headless): assembly baseline vs magwell-rotated, opening baseline vs rotated, magwell rotated with slide at full travel.

In flight on this branch (issue #40)

The gun fixes from merith's 2026-11-11 headset test — eject pose (mag_eject {pos, rot} field), real renders (contained object, no model-swap logic), magwell snap-slider (rail = point A + direction + distance; eject holds at 30% of rail length; collider authoring), grip preview (HandView, L/R + ghosted/solid toggles), hand-posing investigation-first — are being built on this branch now. Their commits will land here as they push; the PR updates automatically.

Open items / checks for merith

  • In-editor: Phase B file-detection wiring (click an item JSON / model TSCN in the FileSystem dock — does it load into the tool, single or double click?); the tscn open→use→save cycle (git status clean for the tscn); dock scroll feel (scroll to the bottom, reference slots + slide sim reachable, collapsed sections open cleanly); grip-rotate feel in-headset (barrel level by math).
  • Drag the gun_chamber marker to the real breech (current default is a guess).
  • Phase A design calls still open: staging = one reusable template vs per-item scene; rail JSON two-keys vs {a,b}; rail ends positions-only; moving the model itself does NOT write JSON; dock marker list doesn't live-update while dragging.
  • gun_bolt: what is it — straight rename of the slide rail, or a new marker for a bolt part? (gun_slide untouched until answered.)
  • Toolkit Phase 1 decisions: mode-pose convention confirmation; assembly-beats-rail precedence; whether the game needs the rail part; mag rail behavior rides with Phase 1 (decided — this PR's branch is where it lands).
  • The e2e eject-flow failure (see Verification above).
The DA Mod Editor (issue #35) — the editor addon for authoring items — plus the generic gun models and the game-side assembly carry-through that came with it. Branch: `muse-lantern/da-game:editor-addon`, based on main at `1a008dd` (PR #36 merge). 17 commits, 80 files, +6229/−311. Merge is merith's call, as always. The gun-fix work (issue #40) is in progress on this same branch and its commits will land here as they push. ## What this is A Godot editor plugin (`addons/da_item_editor/`, "DA Mod Editor") that loads an item JSON + its model into a staging scene in the 3D viewport, lets you position `gun_*` markers and rails, bind model parts to roles, preview reference items and slide travel, and save the JSON back (tab indentation, line endings preserved). The game side carries the tiered `assembly` dict through load so mods can read their own keywords. ## What's in it, by commit **Phase A — staging scene, markers, rails (`28dbba1`, `5d520d7`)** - Plugin scaffold + dock, staging scene (`item_stage.tscn` / `item_stage.gd`). - `gun_*` markers schema-driven by type tables (`gun_muzzle`, `gun_mag_well.{a,b}`, `gun_grip`, `gun_foregrip`, `gun_slide`, `gun_chamber`); rails = ref A/B at pos/rot, part slides on a virtual rail, visuals = two gizmos + holographic line; `gun_grip` shows the *computed* primary grip pose; saving writes back the delta. - JSON round-trip: tab indentation, line endings preserved. `item_editor_check` suite added. **Phase B — create, manage, detect (`b8e065f`)** - Create (template picker Weapon/Magazine/Ammo/Consumable/Generic-primitive, module picker, validated new ids), manage (module-filtered item list, duplicate into another module — only the JSON copied, model reference stays pointing at the original module's assets; a relative `model_path` is rewritten to an absolute `world_scene` on cross-module duplicate), delete with confirmation. Rename deliberately left out (id references would go stale). - File detection: clicking an item JSON / model TSCN in the FileSystem dock loads it into the tool. (Still needs merith's in-editor check: single or double click?) **Dock (`75f5621`, `075ff2c`)** - Dock in `DOCK_SLOT_RIGHT_UL` (a tab with Inspector/Signals/Groups). Cleanup + rename to "DA Mod Editor" (label only — addon dir `addons/da_item_editor/`, classes `DAItemEditorDock` / `DAItemSchema` unchanged). "New item" collapsible, collapsed by default; "Browse…" removed; marker rows use a status dot (green = in JSON, amber = placed but unsaved, grey = not set). **Staging-scene gizmos (`afec5f0`, issue #35)** - BarrelAxis (amber 0.5 m line from the `gun_muzzle` marker along −Z), MagBounds (cyan wireframe of the mag AABB), SlideForward/SlideBack/SlideTravel (yellow wireframes; travel from `gun_slide` rail / `slide_travel` field else 0.05 m default). Gizmos rebuild with the stage and follow live marker transforms. **Reference slots + editor metadata + slide simulation (`3fb4103`, `b7c110d`, issue #38)** - Ghost reference items in named sockets: gun → `chambered` bullet at a new `gun_chamber` marker + `magwell` mag at mag_well in the game's insert pose; mag → `opening` bullet, GLB long-axis corrected +90°. Per-slot dock dropdowns, TAG-GATED (magwell: `mag` tag; opening/chambered: `ammo` tag), defaulting to the accepted item. CASCADING: a slot rendering an item surfaces that item's own slot options; tree walked via metadata files, max depth 4. - Editor metadata: `<item>.modtool_meta.json` alongside `<item>.json`, auto-loaded when present, never read by the game; stored `""` = explicit none. `gun_chamber` default in `generic_pistol.json` = (0, 0.0109, −0.05) — a guess; merith to drag it to the real breech in the editor. - Slide simulation: dock slider 0–1, "Rack / fire" cycle (0→1 in 0.15 s, →0 in 0.25 s); travel from rail / `slide_travel` else 0.05 m. **Magwell coupling (`3272876`, issue #38)** - The magwell rail node's transform IS the magwell (placed at JSON `mag_well` on load; end_a local (0,0,0), end_b local `mag_well_b`−`mag_well`); the mag reference ghost and cyan mag-bounds gizmo follow live via the existing `_process` hook; save writes stage-space end positions. **Tiered assembly + magwell rotation (`a3db1b4`)** - Assembly schema, confirmed with merith: top-level `assembly`; next tier is the part (slide, bolt, rail, trigger, fire_select, whichever); inside each part: `object` (the object's name in the model file, e.g. `Handgun_Upper` — Godot sanitizes `Object.001` to `Object_001`) + `origin` {pos, rot} + that part's values. Slide `travel` = {pos, rot} relative to origin, applied as a % modifier: pose(t) = origin + t×travel. `fire_select.modes` keys are exactly safe, semi, burst, auto; each mode is a {pos, rot} offset from origin. - Dock Components section (tree-picker Assign with name + path columns, Clear, Add part, ⚠ missing/ambiguous warning, fire_select mode dropdown that poses the selector); assembly-first resolution with name fallback everywhere (slide gizmos/sim, fire_select, mag_release); slide sim + gizmos read assembly travel. - `mag_well` accepts a {pos, rot} object form (plain array still valid, rot defaults zero, mirroring magazine `opening`); the rail node's rotation IS the magwell rotation; mag ghost + cyan bounds box rotate with it as an oriented box; stage `_process` dirty-check includes rotation; collect writes {pos, rot} when rotated or already a dict, plain array otherwise; insert pose and 0.4 drop factor unchanged. - Seeded `generic_pistol.json` (pure insertion): 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 only (the gun's `weapon.modes` is `["semi"]`), mode offsets zeroed. - Assembly is extensible/arbitrary by design (merith, 2026-11-11): any part names, any keys; core systems handle only their own keywords; a modder can add new keywords (e.g. `mag_release`) and write mod-side GDScript that reads them. - Game-side in this commit: compat shim only — `WorldItem._pos()` accepts both mag_well forms; `firearm.gd` `mag_well_position()` / `_refresh_mag_visual()` use it. **Generic gun models (`fcdd0a0` by merith, `8e5f108`, `1103edb`)** - `fcdd0a0` "Replace Test Gun with Generic Gun Model": Fab.com asset pack, listing 3ee0ae1f-b364-4951-afcf-0c705035e386 (credited in `modules/devkit/assets/README.md`) — generic pistol GLB, generic pistol mag GLB, 9mm bullet GLB, knife texture only. Item JSONs use `model_path` to the GLBs. Registry after the renames: `devkit:ammo/bullet_9mm`, `devkit:ammo/bullet_9mm_tracer`, `devkit:mag/generic_pistol`, `devkit:weapon/gun/generic_pistol`, `devkit:weapon/gun/rifle_test`. - `8e5f108` fixed the fallout: mag gained the `mag.generic_pistol` tag (gun now accepts its mag); all old-id references updated (9 suites, spawn_loaded, e2e, start_kit, example gun, bridge server, modding.md, MECHANICS regenerated); 4 orphaned `.blend.import` files deleted; single texture at `modules/devkit/assets/textures/generic_color_map.png`. The alongside PNGs stay (the GLBs do NOT embed the texture; true dedup needs the `.blend`s relinked + GLBs re-exported — merith's Blender-side step). - `1103edb` pitched the generic pistol up: `grip_rotate` [90, 0, 0] → [18, 0, 0]. Total grip pitch was 162° (base 72° from the 18° default rake + grip_rotate 90°), barrel 72° below the level aim beam; now total pitch is 90°, barrel level with the beam. Verified: the yellow tracer runs horizontally, following the level red beam. **Assembly carry-through (`712d664`)** - The game loads and exposes the raw assembly dict (`ItemSpec.assembly` + `assembly_part()`); `apply_dict` reads it deep-duplicated, `to_item_definition` copies it, `from_dict` sets it and keeps it out of `type_tables`. Non-dict → `{}`, absent → `{}`. No game-side consumption yet — core reads only its known parts. This is the prerequisite for toolkit Phase 1, not Phase 1 itself. **Rotation propagation + tscn guard (`02c2a6a`)** - Opening-rotation bug reproduced and fixed: rotating the opening marker now rotates the opening bullet ghost live (the stage `_process` dirty-check never observed the opening rotation, and the position update never re-applied orientation). One rotation source per anchor (`_magwell_rot()` / `_opening_rot()`) consumed by gizmos, references, and collect. - Two more rotation drops found while testing thoroughly, both fixed: rail-load drift (a `_b` in the JSON is a stage-space position but load took it into the rail frame with a plain subtraction, dropping the rotation — end_b 10.45 mm off, and collect wrote the drifted value back, so the end would walk one rotation per save/load cycle; load now converts stage→local with the rail transform) and bounds double-refit (measuring the orient subtree then applying the orient basis refit the AABB twice, inflating magwell bounds 0.1503 → 0.1553 on the first live update; new `_orient_bounds()` helper, one refit). - Game path verified correct and untouched: first-out bullet rotation/position match the opening rot/pos. - `item_stage.tscn` guard: `item_json_path` was an `@export`, so the dock assigning it marked the scene dirty and a save baked the pistol path into the template. It is now a plain var with the same setter — the live instance still rebuilds, but the value can never serialize. Committed tscn restored to the stable form the editor writes (uid, ext_resource uid, `unique_id`, no item path). Verified headlessly; a real editor open→use→save cycle still needs merith's check. **Dock scroll + mag-release rail drop (`b792f03`)** - Dock content now lives in a ScrollContainer (vertical expand-fill, horizontal scroll disabled) holding a VBox scroll body; dialogs stay direct children of the dock root (popups, not scrolled content). Sections collapsible, mirroring "New item": Markers/rails, Components, References, Slide. Defaults: Markers + Components expanded, References + Slide collapsed. Each component row expands to show origin, travel, and modes (display only, nothing writes JSON). - `gun_mag_release` rail dropped (grep-proven: zero readers in game or editor, no item JSON carries it). The `mag_release` *assembly part* stays — the part in the model file a mod's GDScript drives, not the rail marker. `gun_slide` untouched (merith's "drop gun_slide in favor of gun_bolt" is awaiting clarification of what `gun_bolt` is). - Add-part charset already relaxed in `712d664` (`[A-Za-z0-9_]` → `[A-Za-z0-9_-.]`); proven with suite checks this round. ## Verification (on tip `b792f03`) - Double `--import`: 0 ERROR both passes. `tools/check.py`: 6/6. - Full headless suites: 34/34 PASS, 1,518 `ok:` lines (`item_editor_check` 185, `modloader_check` 73, `mag_filo_check` 43). SCRIPT ERROR grep: exactly the 3 known pre-existing lines (2× `add_child` on null, 1× nonexistent `is_trigger_down`). - Bridge e2e: 118 ok. The 3 known mag-insert failures are GONE (merith's new mag model/opening in `d922e2b` fixed them). One failure remains: "bullet stays held in L after the trigger release" (eject flow), proven pre-existing on pure `d922e2b` — its fix is part of issue #40's §3 verification gate. - Renders verified on the VM (xvfb, no `--headless`): assembly baseline vs magwell-rotated, opening baseline vs rotated, magwell rotated with slide at full travel. ## In flight on this branch (issue #40) The gun fixes from merith's 2026-11-11 headset test — eject pose (`mag_eject` {pos, rot} field), real renders (contained object, no model-swap logic), magwell snap-slider (rail = point A + direction + distance; eject holds at 30% of rail length; collider authoring), grip preview (HandView, L/R + ghosted/solid toggles), hand-posing investigation-first — are being built on this branch now. Their commits will land here as they push; the PR updates automatically. ## Open items / checks for merith - In-editor: Phase B file-detection wiring (click an item JSON / model TSCN in the FileSystem dock — does it load into the tool, single or double click?); the tscn open→use→save cycle (`git status` clean for the tscn); dock scroll feel (scroll to the bottom, reference slots + slide sim reachable, collapsed sections open cleanly); grip-rotate feel in-headset (barrel level by math). - Drag the `gun_chamber` marker to the real breech (current default is a guess). - Phase A design calls still open: staging = one reusable template vs per-item scene; rail JSON two-keys vs `{a,b}`; rail ends positions-only; moving the model itself does NOT write JSON; dock marker list doesn't live-update while dragging. - `gun_bolt`: what is it — straight rename of the slide rail, or a new marker for a bolt part? (`gun_slide` untouched until answered.) - Toolkit Phase 1 decisions: mode-pose convention confirmation; assembly-beats-rail precedence; whether the game needs the `rail` part; mag rail behavior rides with Phase 1 (decided — this PR's branch is where it lands). - The e2e eject-flow failure (see Verification above).
A Godot editor addon for building modded item JSONs. The author picks
an item, opens the staging scene (editor 3D viewport), and places
markers and rails visually with the gizmo; the addon writes the
positions back into the item JSON (tab-indented, line endings
preserved). The JSON is the shipped output; the staging scene is a
working file.

- Plugin scaffold (addons/da_item_editor/): plugin.cfg + EditorPlugin
  with a dock, following the da_debug_bridge pattern.
- Staging scene: loads a real model (world_scene / model_path) or the
  procedural stand-in (mirrors WorldItem._build_visual), positioned
  around the grip frame.
- Markers (gun_* namespace): gun_muzzle, gun_mag_well.{a,b},
  gun_grip, gun_foregrip, gun_slide, gun_mag_release.{a,b}, plus the
  mag opening. Which markers apply is decided by the item's type
  tables, not the item id. gun_grip shows the computed primary pose
  (the runtime default pose + grip_offset/grip_rotate) with an axes
  grip frame.
- Rails: two end markers + a holographic line (unshaded cyan),
  rebuilt when an end moves. The a end writes to the base field, the
  b end to base + _b (additive; the runtime ignores them until the
  mag-rail behavior lands).
- Save writes only fields that were in the JSON or whose marker was
  moved, so a defaulted rail end never invents a field.
- Enabled in project.godot. No runtime behavior: the game runs
  unchanged with the plugin enabled or disabled.

Phase B (colliders), C (sliders/states/events), D (simulation) are
separate later phases.
Headless suite for the DA Item Editor (46 checks): item scan, the
marker table, the procedural stand-in, the staging scene populated
from the test pistol and the pistol mag, the grip default pose, the
collect round-trip (only moved / in-JSON fields are written),
tab-indented JSON save with CRLF preservation, and that the dock
instantiates headless without touching the editor. Added to the CI
suite list. The dock appearing in the editor GUI needs a manual
editor check.
The dock becomes the tool's home: create new items from templates
(weapon / magazine / ammo / consumable / generic) into any module, a
module-filtered item list, duplicate (including into another module --
JSON only, model references stay pointing at the original's assets),
delete with confirmation, and detect the focused file -- the plugin's
_handles() / _edit() load an item JSON (or a .tscn an item uses as
its model) selected in the FileSystem dock.

Detection predicate lives in DAItemSchema (editable_resource_is_item /
item_path_for_resource / find_owner_item_for_scene) and is covered
headlessly by the new item_editor_manage_check suite (102 checks);
the _handles() / _edit() wiring itself is editor-side and needs an
in-editor check. Rename is deliberately left out: moving a file is
easy, but references to the id elsewhere (spawn_loaded) would go
stale.
The dock lived in DOCK_SLOT_LEFT_UR, crowding the left column and
hiding the FileSystem dock. Move it to DOCK_SLOT_RIGHT_UL so it sits
as a tab with Inspector / Signals / Groups, and the left column is
free for Scene + FileSystem.
- Remove the header line and the blurb paragraph from the dock; the
  tab carries the name and the paragraph now lives as a tooltip.
- List rows (and the Loaded status) show the path under items/ without
  the module prefix; the list is already filtered by module.
- The New item section is collapsible and collapsed by default.
- Remove the Browse button (the list + FileSystem detection cover it).
- Marker rows use a small status dot (green = in JSON, amber = placed
  but unsaved, grey = not set) instead of the repeated (in JSON) text.
- Rename the user-facing label to DA Mod Editor (dock tab + plugin
  name). Internal names (addon dir, classes, tests) are unchanged.
See readme for asset source.
Zach's fcdd0a0 renamed the devkit pistol/mag/ammo to the generic
names, breaking 9 suites and the start kit / test-gun spawn_loaded.
Update every old-id reference to the new ids, add the mag.generic_pistol
tag so the gun accepts its own mag, and restore the first-out bullet
for GLB mags (the GLB path in _build_visual returned before it was
built). Suites updated for the GLB reality: no base bands on GLB
items, the bullet's long axis is Z, the mag bounds centre is not at
the origin, and the opening rotation is -97.274 deg.

Texture dedup: the 3 textures/ copies are gone; the single source is
modules/devkit/assets/textures/generic_color_map.png. The alongside
GLB-extracted PNGs stay -- the imported .scn depends on them, so they
cannot be deleted until the .blends are relinked to the single source
and the GLBs re-exported (Zach's Blender step). The 4 orphaned
.blend.import files are gone, and each new asset group gets a
source/.gdignore for the .blend source.
Total grip pitch was 162 deg (base 72 from the 18 deg default rake +
grip_rotate 90), putting the barrel 72 deg below the level aim beam.
grip_rotate 18 makes the total 90 deg, barrel level with the beam.
- BarrelAxis: amber 0.5 m line from the gun_muzzle marker (else the
  weapon muzzle field) along the item -Z; shows where the gun points.
- MagBounds: cyan wireframe of the mag AABB. On a magazine, its own
  bounds; on a gun, the accepted mags bounds at the mag_well, hanging
  below it (default pistol-mag box when no mag resolves).
- SlideForward / SlideBack / SlideTravel: yellow wireframes around
  the slide node at forward and at max-back (+Z by the travel), plus
  a green line between their centres. The slide is found via the
  gun_slide assembly role or by name (Slide/Upper); travel comes
  from the gun_slide rail, a slide_travel field, else 0.05 m. No
  slide found -> the gizmo is skipped, no error.
- Gizmos rebuild with the stage and follow the live marker
  transforms in the editor (muzzle / magwell moves re-draw them).
- Suite: gun_grip rotation expectation updated (162,0,0) -> (90,0,0)
  to match the committed grip_rotate [18,0,0]; gizmo checks added
  for the pistol (barrel axis, magwell box, slide travel) and the
  mag (own bounds box, no barrel/slide gizmo).
Reference slots render a ghost of the occupying item in the staging
scene: chambered (bullet at the new gun_chamber marker), magwell
(mag at the mag_well marker, using the game insert pose), opening
(bullet at the mag opening marker, GLB long axis corrected +90 deg
to lie along the mag). Slots cascade: a mag in the magwell surfaces
its own opening-bullet dropdown. Dropdowns are tag-gated (magwell:
mag tag; opening/chambered: ammo tag) and default to the accepted
item. Slot choices persist per item in <item>.modtool_meta.json,
auto-loaded when present, defaults when absent; the game never
reads it and the item JSON gains no editor state. A dock slider
(0-1) drives the slide node along its axis and a Rack/fire button
plays the slide cycle. The gun_chamber marker defaults to the
breech (muzzle pulled back 0.06 m along +Z).
Extends item_editor_check with chamber marker, reference-slot
tag-gating and defaults, reference rendering (pistol: chambered +
magwell + cascaded opening; mag: opening), metadata round-trip
(missing file = defaults; save/load; explicit empty = none) and
slide travel checks. The metadata round-trip cleans up the pistol
metadata file it creates so the suite leaves no side effects.
The magwell rail sat at the stage origin (identity transform) with
the magwell position living only in its end markers, so selecting
the rail put the editor gizmo at the origin, detached from the
magwell, and the mag reference ghost never followed a moved
magwell. The rail node transform is now the magwell itself (end_a
at local origin, end_b at local mag_well_b - mag_well), so the
editor gizmo appears at the magwell and dragging the rail moves
it; _magwell_pos() reads the end's stage-space position, so the
mag reference and the cyan mag-bounds gizmo follow it live via
the _process hook. _collect_rail() writes stage-space end
positions and detects rail-node drags. JSON format unchanged.
item_editor_check: rail expectations updated to the new scheme,
plus a coupling check (move the rail, the mag reference and the
mag-bounds gizmo move with it, collect writes the moved ends).
- assembly: tiered part bindings (object/origin/travel/modes) with
  assembly-first, name-fallback resolution; manual assignment via
  the dock tree picker; dotted file names resolve through the
  dot-to-underscore import sanitization, object_path only as a
  disambiguator when a name repeats
- slide simulation + gizmos read the assembly slide
  (origin + t*travel); fire_select modes pose the selector
- magwell accepts the {pos, rot} form; the rail rotation drives
  the mag ghost + bounds gizmo; the game reads both forms via
  WorldItem._pos (rotation still editor-only)
- seed the generic pistol assembly (slide Handgun_Upper,
  fire_select Handgun_Safety, modes safe + semi only)
The assembly tree is arbitrary by design - any part names, any keys.
Core reads only its own keywords; a mod reads its own parts off the
definition (def.assembly / def.assembly_part(name)) and drives its
own behaviour. Until now GenericItem.apply_dict dropped the assembly
dict at load, so a mod had nothing to read.

- ItemSpec.assembly + assembly_part(part_name) (shared base, so both
  GenericItem and ItemDefinition carry it).
- GenericItem.apply_dict reads d.assembly deep-duplicated; non-dict
  reads as no assembly. to_item_definition copies it.
- ItemDefinition.from_dict sets def.assembly and keeps it out of
  the type_tables loop (assembly is not a type table).
- No game-side consumption yet: core still reads only its known
  parts. Behaviour is unchanged when assembly is absent or malformed.

Tests: modloader_check gains 10 assembly carry-through checks.
Three rotation drops, same class as the magwell bug:

- Opening: rotating the opening marker never rotated the opening
  bullet ghost. _process only observed positions, and
  _update_reference_positions never re-applied the opening
  orientation. Now _ref_opening_rot is tracked, the dirty
  predicates (_gizmos_dirty/_references_dirty, shared by the editor
  hook and the suites) observe rotations, and the update re-orients
  the ghost and re-measures its rotated bounds like the magwell.
- Bounds: measuring the orient subtree then applying the orient
  basis refits the AABB twice, inflating it (magwell 0.1503 to
  0.1553 on the first live update, moving the insert pose 2 mm
  with no marker move). _orient_bounds measures under orient but
  relative to the wrapper - one refit, the same source build and
  live updates use, and exact past cascaded children.
- Rail load: a _b in the JSON is a stage-space position, but load
  took it into the rail frame with a plain subtraction, dropping
  the rotation - the end drifted 10.5 mm per save/load cycle at
  the -12 deg magwell and a save wrote the drift back. Load now
  converts stage to local with the rail transform (defaults keep
  the plain subtraction: _rail_default_b builds a local offset).

- item_json_path is a plain var, not @export: it is live editor
  state, not scene data. The dock sets it on the live instance
  and the setter still rebuilds - but it can never serialize, so
  saving the staging scene can never bake a loaded item path into
  the template. The tscn is restored to the stable form the editor
  itself writes (scene uid, ext_resource uid, fixed node
  unique_id, no load_steps, no item path).
- Add-part charset relaxes to allow hyphen/dot part names (names
  are only dict keys and UI labels, never identifiers).
- Suites follow the d922e2b values (magwell -12 deg plus mag_well_b,
  chamber z, opening pos, re-exported mag bounds) and gain 21
  rotation checks (item_editor_check 133 to 156), 2 first-out
  orientation checks (mag_filo), and a local-position fix for the
  firearm dict-form magwell check.
DA Mod Editor: dock scrolls with collapsible sections; drop gun_mag_release rail
Some checks failed
tests / test (pull_request) Has been cancelled
b792f03a6f
tweak some jsons
Some checks failed
tests / test (pull_request) Failing after 1m36s
2696798eca
Owner

NOTE THIS WORK IS VOLATILE AS FUCK AND LIKELY TO CHANGE AT A RATZ SNEESE

NOTE THIS WORK IS VOLATILE AS FUCK AND LIKELY TO CHANGE AT A RATZ SNEESE
The ejected bullet floated with the opening rotation and no GLB
nose correction, so its tip pointed down into the mag. The mag
now carries an authored mag_eject pose (the opening pose raised
0.04 m, same rotation); the eject float reads it through
WorldItem.magazine_eject_local()/rotation()/basis(), which compose
the +90 deg nose correction, and the editor authors it like the
opening (mag_eject marker + eject reference slot, live rotation
follow-through + save/reload round-trip). The eject test nose
checks move to the GLB frame (local -Z, not the procedural +Y).

The corrected float also changed the e2e mag test: it used to pass
only because the floating bullet, when L grabbed it, was blocked
after 15 mm by the mag's opening-end face (the first-out bullet
protruding from the opening) and stayed 0.036 m from the opening,
so it went in on the grab -- the place_controller step never ran.
With the nose correction the bullet's collision box turns 90 deg
(29 mm axis from world -Z to world +Y), slips past that face, and
rides the L hand to the test's geometric estimate, which is ~0.13 m
off the real opening (not ~0.05 m as its comment claimed) -- outside
the 0.06 m insert radius, so the game correctly refused the insert.
The e2e now aims the BULLET at the real opening (opening + up*0.02,
like the headless mag_interaction_check), and list_items carries
opening_position/opening_up for magazines so callers do not have to
reconstruct the pose from the mag JSON + the hand basis.

Follow-up for Zach's 2696798 opening retune (pos (0, 0.075, 0.002),
rot -75 deg): the mag_eject seed follows the new opening (pos
(0, 0.115, 0.002), same rot), the suite values follow it, and the
nose-alignment thresholds drop to 0.95 (15 deg off forward, dot
0.966). The retune also changes the e2e's early-insert behaviour:
the round now goes back into the mag on the grab, so the release
check accepts held-in-L OR already-inserted (a dropped round
still fails it) and the bring-back placement only runs when the
round is actually still held.
The inserted mag was a procedural 30x100x40 dark box and the first-out
bullet a gold cylinder. Both now render the contained object itself --
no model-swap logic, the physical instance is still right there:

- Firearm._refresh_mag_visual instances the contained mag's own
  world_scene at the insert pose (magwell position dropped 0.4x the
  mag's measured height, magwell rotation applied); the visual's
  centre lands on the insert pose (the mag GLB's bounds centre is
  not at its origin), the way the editor's magwell ghost does. A mag
  with no model keeps the dark box as fallback, at the same pose.
- WorldItem._build_first_out_bullet renders the contained first-out
  round's own GLB (opening rotation + the +90 deg nose correction,
  shared as _bullet_glb_basis with magazine_eject_basis); a round
  with no model keeps the gold cylinder. _refresh_first_out_bullet
  rebuilds the visual when the first-out round's model changes (a
  mag loaded after spawn swaps the cylinder for the GLB).
- New magwell helpers (the section 1 snap-rail helper, built here):
  Firearm.mag_well_rotation / mag_well_basis / mag_well_transform
  and WorldItem._rot (a {pos, rot} object carries the rotation; a
  plain array is a position only). mag_well_position() is unchanged.
- The old visuals are freed immediately, not queue_free'd: a stale
  queued node made get_node_or_null find the old hidden visual
  before the new one in the same frame (caught by mag_filo).

Cyan-shape identification (issue #40 section 2): the "cyan slab" is
the hand-grab highlight on the old procedural mag box. The empty
hand's nearest_in_reach() returns the other hand's gun, set_hand_target
highlights it with the translucent cyan overlay (0.3, 0.9, 1.0, 0.35),
and _set_highlight only touches DIRECT MeshInstance3D children -- the
gun's nested GLB meshes are untouched, but the mag box was a direct
child, so it alone turned cyan. All four GLBs are white-albedo +
texture, no cyan material, no emission. The real GLB visual is
nested under a Node3D wrapper, so the highlight no longer reaches
it -- the slab is gone as a side effect of the real render. (The
highlight still only covers direct MeshInstance3D children, so a GLB
gun shows no hand-grab highlight at all -- a separate follow-up.)

Side effect: GLB visuals lose the procedural colored base ring.
Show-all stays parked (first-out only).

Verification: double --import 0 ERROR both passes; tools/check.py
6/6; full headless suites 34/34 PASS (1506 ok + 64 rig_chains PASS,
was 1498 on b05a384); SCRIPT ERROR = only the 3 known pre-existing
lines; bridge e2e 120 ok PASS (mag test green). Renders (xvfb):
generic_pistol_media/s2_inserted_mag{,_b}.png (real mag GLB below
the grip, -12 deg magwell lean) and s2_first_out_floor{,_top}.png
(real bullet GLB, brass tapered nose, half-proud at the opening).
§1: the magwell is a snap-slider, not a proximity insert.

- HandInteractor: _try_snap_magazine triggers on the mag's OPENING
  vs the rail's outside point A (SNAP_RADIUS 0.12); the mag stays
  held while the hand slides it along the rail A->B (t driven by
  the hand's grip projection onto the rail axis, relative to the
  snap-time projection, so the held orientation's ~78 deg offset
  cannot pre-drive t); at t=1 the existing insert_magazine +
  deparent runs, with the mag already at the insert pose so there
  is no jump. A point rail (no mag_well_b) inserts at once, the old
  behaviour. Collision exception between mag and gun while snapped,
  so the mag can enter the grip interior; the snap cancels when the
  grip pulls >0.18 m from the mag, the mag leaves the hand, or the
  gun is gone/loaded.
- Eject-hold (1d): pressing BUTTON_PRIMARY ejects the mag and holds
  it frozen at t=0.7 on the rail as a separate grabbable entity;
  it only drops when eject is released (or stays held if a hand
  grabbed it). Replaces _drop_magazine's free spawn.
- Firearm.mag_rail_for + mag_rail_origin/mag_rail_opening: the rail
  derived in code from the authored mag_well/mag_well_b (A=outside,
  B=inside), parameterised by the mag ORIGIN so t=1 is the insert
  pose exactly (insert centre = magwell dropped 0.4x the mag's
  measured height). WorldItem.packed_scene_aabb + magazine opening
  statics so the rail works from a definition (eject-hold) as well
  as from a WorldItem.
- Grabbable.follow_override: _follow uses it as the desired pose
  while a snap holds the mag on the rail.
- Collider authoring (1e): model.colliders replaces the default
  single shape - {"mesh": name} (convex hull from a model node) or
  {"shape": box|sphere|cylinder, pos, rot, size} primitives; an
  entry that cannot be built is skipped and the default applies
  when none build. Editor: schema helpers (colliders_of/set,
  summary, default), stage colliders()/set_colliders() + green
  wireframe gizmos (mesh entries draw the named node's AABB), dock
  Colliders section (collapsed by default) with add/remove/edit +
  Assign tree-picker for mesh entries + missing-node warning.
- Bridge: grab_state carries mag_rail {a, b} (world opening
  positions at the rail ends) + snap {active, t} for a held mag
  with a compatible empty gun in the other hand.
- New suite tests/mag_rail_check.gd (63 checks: rail math, snap,
  slide, eject-hold, point rail, colliders); item_editor_check
  gains the collider round-trip checks; CI lists mag_rail_check.
- E2E: mag-into-gun test (generic pistol, the real rail - the test
  gun is a point rail and inserts at once, so it can never
  exercise the snap) aims the opening at mag_rail.a, slides toward
  b, asserts still-held mid-slide with t rising, then inserted.
  The gun is raised to y=1.0 and settled before the rail is read,
  so the rail is stable (an unposed L grip drags the gun across
  the floor and the moving rail drives t on its own).
§4 (editor-only, basic core work): the staging scene shows a hand
on the grip, so the author can reference/check the hand grip pose.

- Stage: _build_grip_preview instances a HandView at the
  gun_grip marker's transform (fix shape A — the game's own
  alignment, the OPENXR correction that puts the fist centre on
  the grip origin, positions the glove; never the raw GLB, which
  would sit in uncorrected model orientation). The preview
  follows the grip marker when the author drags it (editor
  _process dirty-check, like the gizmos/references). It only
  builds when the item has a gun_grip marker (weapons).
- Dock "Grip preview" section (expanded by default): "Show hand
  on grip" toggle, Right/Left hand picker (both share the same
  pose/rot — purely a visual helper; the left is the same model
  mirrored, HandView's own mirroring), "Ghosted" toggle
  (translucent alpha 0.35 so the grip and the axes triad show
  through; solid clears the overrides back to the model's own
  materials). All three are preview-only: they live in the stage
  + dock toggles and are NEVER written to JSON or metadata.
- The HandView calibrates headlessly: it awaits one process frame
  in _ready, and the suite proves built + measured by frame 10.
  The preview hand stays at its rest curl (the per-finger pose
  API is the later iteration in issue #40, not this round).
- Suite: item_editor_check gains 11 grip preview checks (hand
  exists at the grip pose, right by default, model instanced,
  solid has no override, left rebuilds a left hand at the same
  pose, ghosted overrides translucent, disable removes it,
  settings never saved, calibrates headlessly).
- Renders (staging scene, xvfb + opengl3): the glove on the
  pistol grip, solid right + ghosted right + solid left.
Hand posing investigation: bridge readouts for the grip/held-item
All checks were successful
tests / test (pull_request) Successful in 2m20s
3bbec44635
transforms + live fingertips, and the measured verdicts (issue #40)

This is the investigation half of §5. It adds the read-only
measurement surface the protocol needed and records what the bridge
measured. It does NOT change any posing behaviour: the one cause the
measurements found (H5, the curl) needs a deeper calibration rework
than a sign/amount tweak, so no fix is bundled here.

Measurement surface (additive, read-only):
- debug_bridge grab_state now carries the grip controller's full
  transform (grip_transform) per hand, and _item_brief carries the
  held item's full transform, so the barrel, laser and glove bases
  can be compared numerically instead of eyeballed.
- HandView.debug_snapshot now carries live fingertip positions
  (fingertips, one entry per finger, last bone of each chain, grip
  space). The wrist/knuckle joint centres it already reported do
  not move with curl, so they cannot test the curl direction; the
  tip is the curl indicator.

Measured verdicts (bridge protocol, pistol force-grabbed in R,
angles computed numerically):
- H1 grip pitch: NOT the cause. Barrel is level with the beam
  (0.00 deg apart, one-handed); barrel pitch is 0.00 deg when the
  grip is pitched +90 deg about X, the natural pistol-grip
  orientation. The -90 deg barrel seen with an identity grip is the
  unposed bridge hand, not a game bug.
- H2 HandView correction: NOT the cause. Post-correction finger
  direction is 0.00 deg off target and post-correction palm is
  0.00 deg off target, both hands (computed from the correction
  basis vectors, not the euler, whose order does not round-trip).
- H3 mirror: NOT the cause. L/R measured frames are symmetric,
  thumb_dot / palm_flipped opposite as expected.
- H4 laser vs grip: NOT the cause. Laser and barrel agree at
  0.00 deg one-handed.
- H5 curl: MEASURED CAUSE. At full grip the fingertips move AWAY
  from the palm (dorsal, +X for the right hand) instead of toward
  it: every finger's fists-minus-open displacement has a negative
  dot with the palm normal (-0.018 to -0.077), while moving
  correctly toward the wrist (+Y, the finger shortens). The
  calibration cannot find a palmar curl for this glove: all six
  cardinal bone-local axis/sign combinations move the index tip
  dorsally or neutrally at full curl, and the best-scoring one
  (BACK/+1, chosen for its shortening term) still swings the tip
  dorsally at amount 1.0. Reworking the calibration to derive the
  flex axis (or test a wider axis set) is its own plan, not this
  commit.
- H6 two-handed _follow: CONFIRMED EFFECT, known pivot gap. With
  L also holding the pistol the barrel pitches to -73.92 deg (from
  -90 deg one-handed) and the laser/barrel disagree by 16.08 deg.
  That is the full-basis-alignment branch the pivot split is meant
  to replace; reported here, not built.

No behaviour change: grip preview, mag rail, eject pose and real
renders are untouched. The half-curl edit stays Zach's pending
call; gun_slide is untouched.
DA Mod Editor: one row per mesh collider (issue #40)
All checks were successful
tests / test (pull_request) Successful in 2m20s
92d814dd74
Each mesh collider rendered its name twice: the header summary
(mesh Handgun_Body_Lower) plus a second mesh: <name> sub-row with
the Assign button. Assign now lives on the header next to Remove,
the sub-row is gone, and the summary reads mesh (none) when nothing
is assigned yet. Primitive entries are unchanged. The dock Control
tree is not headless-testable; the summary half is covered by three
new item_editor_check checks.
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!41
No description provided.