DA Mod Editor: staging scene, markers, rails, assembly, reference slots, scroll (issue #35) #41
Loading…
Reference in a new issue
No description provided.
Delete branch "muse-lantern/da-game:editor-addon"
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?
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 at1a008dd(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 positiongun_*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 tieredassemblydict through load so mods can read their own keywords.What's in it, by commit
Phase A — staging scene, markers, rails (
28dbba1,5d520d7)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_gripshows the computed primary grip pose; saving writes back the delta.item_editor_checksuite added.Phase B — create, manage, detect (
b8e065f)model_pathis rewritten to an absoluteworld_sceneon cross-module duplicate), delete with confirmation. Rename deliberately left out (id references would go stale).Dock (
75f5621,075ff2c)DOCK_SLOT_RIGHT_UL(a tab with Inspector/Signals/Groups). Cleanup + rename to "DA Mod Editor" (label only — addon diraddons/da_item_editor/, classesDAItemEditorDock/DAItemSchemaunchanged). "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)gun_muzzlemarker along −Z), MagBounds (cyan wireframe of the mag AABB), SlideForward/SlideBack/SlideTravel (yellow wireframes; travel fromgun_sliderail /slide_travelfield 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)chamberedbullet at a newgun_chambermarker +magwellmag at mag_well in the game's insert pose; mag →openingbullet, GLB long-axis corrected +90°. Per-slot dock dropdowns, TAG-GATED (magwell:magtag; opening/chambered:ammotag), 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.<item>.modtool_meta.jsonalongside<item>.json, auto-loaded when present, never read by the game; stored""= explicit none.gun_chamberdefault ingeneric_pistol.json= (0, 0.0109, −0.05) — a guess; merith to drag it to the real breech in the editor.slide_travelelse 0.05 m.Magwell coupling (
3272876, issue #38)mag_wellon load; end_a local (0,0,0), end_b localmag_well_b−mag_well); the mag reference ghost and cyan mag-bounds gizmo follow live via the existing_processhook; save writes stage-space end positions.Tiered assembly + magwell rotation (
a3db1b4)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 sanitizesObject.001toObject_001) +origin{pos, rot} + that part's values. Slidetravel= {pos, rot} relative to origin, applied as a % modifier: pose(t) = origin + t×travel.fire_select.modeskeys are exactly safe, semi, burst, auto; each mode is a {pos, rot} offset from origin.mag_wellaccepts a {pos, rot} object form (plain array still valid, rot defaults zero, mirroring magazineopening); the rail node's rotation IS the magwell rotation; mag ghost + cyan bounds box rotate with it as an oriented box; stage_processdirty-check includes rotation; collect writes {pos, rot} when rotated or already a dict, plain array otherwise; insert pose and 0.4 drop factor unchanged.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'sweapon.modesis["semi"]), mode offsets zeroed.mag_release) and write mod-side GDScript that reads them.WorldItem._pos()accepts both mag_well forms;firearm.gdmag_well_position()/_refresh_mag_visual()use it.Generic gun models (
fcdd0a0by merith,8e5f108,1103edb)fcdd0a0"Replace Test Gun with Generic Gun Model": Fab.com asset pack, listing 3ee0ae1f-b364-4951-afcf-0c705035e386 (credited inmodules/devkit/assets/README.md) — generic pistol GLB, generic pistol mag GLB, 9mm bullet GLB, knife texture only. Item JSONs usemodel_pathto 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.8e5f108fixed the fallout: mag gained themag.generic_pistoltag (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.importfiles deleted; single texture atmodules/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).1103edbpitched 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)ItemSpec.assembly+assembly_part());apply_dictreads it deep-duplicated,to_item_definitioncopies it,from_dictsets it and keeps it out oftype_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)_processdirty-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._bin 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).item_stage.tscnguard:item_json_pathwas 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)gun_mag_releaserail dropped (grep-proven: zero readers in game or editor, no item JSON carries it). Themag_releaseassembly part stays — the part in the model file a mod's GDScript drives, not the rail marker.gun_slideuntouched (merith's "drop gun_slide in favor of gun_bolt" is awaiting clarification of whatgun_boltis).712d664([A-Za-z0-9_]→[A-Za-z0-9_-.]); proven with suite checks this round.Verification (on tip
b792f03)--import: 0 ERROR both passes.tools/check.py: 6/6.ok:lines (item_editor_check185,modloader_check73,mag_filo_check43). SCRIPT ERROR grep: exactly the 3 known pre-existing lines (2×add_childon null, 1× nonexistentis_trigger_down).d922e2bfixed them). One failure remains: "bullet stays held in L after the trigger release" (eject flow), proven pre-existing on pured922e2b— its fix is part of issue #40's §3 verification gate.--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
git statusclean 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).gun_chambermarker to the real breech (current default is a guess).{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_slideuntouched until answered.)railpart; mag rail behavior rides with Phase 1 (decided — this PR's branch is where it lands).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.- 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)NOTE THIS WORK IS VOLATILE AS FUCK AND LIKELY TO CHANGE AT A RATZ SNEESE
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 onb05a384); 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).