Vessel construction unification — Dolphin rig swap, measured audits, vestige retirement [flight 15: the boosters' roll floor removed] - #802
Draft
gradies wants to merge 40 commits into
Conversation
Re-runs the per-model and per-prefab survey VESSEL_CONSTRUCTION.md §1/§4 was measured from, and diffs it before anything is touched. Confirmed unchanged: §1's twelve-row renderer table exactly; §3's invariant (zero unreachable nested instances); §3.4's duplicate Manta-family renderers; §4's central finding (dolphin shapes real, rhino/urchan shapes empty at one indexed vertex and delta 0); §2's guid ownership. Corrected, and this is what re-derives the plan: - urchan_shapekey_with_animations.fbx is NOT a vestige. It has zero prefab references and supplies SEVEN LIVE ANIMATION CLIPS to nine vessels, through four AnimatorControllers. A model can be referenced by no prefab and still be load-bearing, because a controller references clips by the model's guid. - SparrowModel4.fbx is a ninth model §4 never listed, in the same position: it supplies the Missile Launch states to Sparrow and Scarab. - The mirror image: Assets/_Animations/MantaAnimatorController.controller is referenced by nothing. The live one is _Models/Animations/. A reference count is not a liveness measurement until each referrer is itself resolved. New evidence, added as §4.3/§4.4: - The Dolphin rig IS the shipped hull, with NO offset — bounding boxes agree on all six faces to three decimals, 8,311 of 12,583 vertices within 5.5e-5. Do not generalise the Rhino's 1.5545 to it. - The rig also carries 4,284 vertices the shipped hull does not: the six engine EXHAUST NOZZLES, which Dolphin_Test ships at localScale 0.01 — 0.00095 units in a 5.3-unit ship, so never drawn. Rendered and confirmed by eye. They are exactly where the six jets mount. - A rig's REST POSE is not the shipped pose. Rhino: fuselage is a rigid +1.5550 (all 29 lathe rings) but the wings are posed 1.38x wider. Urchin: a uniform 2.105x SCALE (2,500/2,500 match after rescaling, median residual 0.04%). Tooling: measure_vessel_models.py and measure_vessel_prefabs.py reproduce all of it; fbx_binary.py gains 64-bit (FBX 7500+) READ support so serpent.fbx can be measured instead of assumed — writing 7700 is refused loudly rather than emitting a 32-bit body under a 64-bit version stamp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
… copy of Answers, per file, the question the governing rule asks: what is the only copy of something here? Nothing is deleted. Split three ways rather than the original two, because Phase 0 found the third: 1a NOT vestiges (live, do not touch): urchan_shapekey_with_animations (7 clips on 9 vessels), SparrowModel4 (the Missile Launch states on Sparrow+Scarab), Vessel_Wedge_Scene (4) (Grizzly, plus TimeDandruff and ExplodableProjectile). 1b Carries something unique: the dolphin rig (a real morph, the 28-bone armature, 10 takes, and the six exhaust nozzles the shipped hull scales to 0.00095 units); the rhino rig (armature + 9 takes, shapes empty); RhinoModel (nothing of its own — it is Rhino_Test before the wings were subdivided — but BOTH placeholder .metas remap materials into it). 1c Cleared for Phase 5: Riptide (the Dolphin's low-poly ancestor, and where the name RiptideAnimation comes from), Dolphin_split and Hammerhead_split (137 and 116-vert fauna blockouts filed under Vessel Models), both placeholders, and Assets/_Animations/MantaAnimatorController (a duplicate of the live controller, referenced by nothing). Vessel_Placeholder_1's 288 takes are not 288 animations: 288 stacks x 288 layers x 864 curve-nodes is ~3 TRS channels each, the one-node-per-take artefact of a Sketchfab/Blender import. Both placeholders are third-party downloads, which is a licensing question flagged for Garrett rather than a cleanup decision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…umbers VesselRigSwapPlanner only ever reported, and its mapping table was authored by hand. This adds the writer and re-measures the table against the rig. Two of the brief's premises changed, both in the easier direction: - The rig is the shipped hull IN THE SAME PLACE (bounds agree on all six faces to three decimals), so NO collider is re-fitted — each gameplay volume is re-homed onto its bone with the world pose preserved. That does not carry to the Rhino (offset 1.5545 in z, wings re-posed) or the Urchin (uniform 2.105x), which is why only the Dolphin is enabled. - The jets do NOT belong on jetT/jetm/jetB. The skin weights settle it: those bones drive 538 verts each — an engine CASE — while jetint/jetinm/jetinb drive 712 each, which is exactly the six inner meshes the shipped hull carries at localScale 0.01 and therefore never draws. The rig restores them as real exhaust bells. Each jet is parked at the mouth centre measured off its own bone's rearmost lip band. The tool dry-runs, is idempotent, refuses and writes nothing if a single mapped bone or object is missing, records to the ledger and draws the ship panel. Verification status is honest: this session had no Unity CLI, so the tool is type-checked against transcribed stubs and nothing more. The type check was proven to FAIL on a wrong member, an undeclared identifier and a wrong arity before being trusted. The branch carries the TOOL, not a swapped prefab; the run-and-verify steps are filed in Docs/UNITY_VERIFICATION_CHECKLIST.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
DECISIONS (Garrett, 2026-08-26), recorded rather than taken silently: Rhino — swap for the PUPPETRY, morph honestly absent. Urchin — swap at 1/2.105 to PRESERVE THE SHIPPED SIZE. The empty shapes were challenged and re-verified exhaustively, and the first sweep would not have answered it: this clone is SHALLOW with 2 of 363 remote branches fetched, so "nothing found" was the confident-wrong answer waiting to happen. After fetching every branch blob-filtered, across 93 distinct FBX blobs in the project's whole history: rhino_shapekey_with_animations.fbx has exactly ONE blob ever, its four element shapes are 1 indexed vertex at delta 0.0000, and no other Rhino-named model has ever carried a blend shape. The Rhino has never had a hull morph anywhere. Measurement removed most of what this phase was expected to cost. Each rig instance is placed at a FITTED transform that lands its hull on the shipped hull — Rhino z -1.5545 (2000/2000 fuselage verts, median residual 0.00019), Urchin localScale 0.474905 (2000/2000, median 0.00010 in a 0.43-unit hull) — so every collider and FX mount keeps its world position. The eight Rhino body-jet mounts and five BoxColliders the brief said must be re-fitted do not move at all. The Rhino bone map is verified rather than inherited: each legacy collider contains 100% of its mapped bone's skinned geometry, Wing back L/R -> wing2.l/r included. New rule, learned by nearly mis-reporting the Urchin: an overlap score is meaningless when the baseline overlap is already ~0 — run the control against the SHIPPED asset before reading a low score as a regression. The Urchin's colliders bound the shipped hull just as poorly as the rig (3.58% vs 3.54%), so they were already loose and the swap is collider-neutral. Two weaker discriminators (nearest centroid, nearest skinned vertex) both collapse onto the body bone on a radially symmetric hull and flagged correct mappings as wrong. Accepted cost, stated: the Rhino rig's bind pose has the wings 1.38x wider, so its resting silhouette changes. The morph audit must KEEP reporting both vessels un-morphed — Phase 4 is what makes that honest instead of a false green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
The four checks whose absence let each VESSEL_CONSTRUCTION.md discrepancy
through. All asset-only. Together they are what stops the Rhino/Urchin swaps
from reporting a false green.
1. Audit Vessel Elemental Morphs now measures shape MAGNITUDE. Travel() reads
each shape's authored extreme frame and reports its farthest vertex travel as
a fraction of the mesh's own bounding-box diagonal; below threshold it says
INERT, and a vessel with only inert shapes reads "LABELLED BUT INERT — the
hull morphs by NOTHING".
The threshold is RELATIVE because an absolute epsilon is not enough: a
historical Sparrow Missile carried Charge and Time shapes indexing 243 and 309
vertices and moving them 4e-6 units — non-zero, and 0.00004% of the model.
Measured over every shipped vessel model the populations separate with nothing
between them: real shapes 2.46%..17.94% of the diagonal, inert ones 0.0000%.
0.001 is picked from inside that measured gap, 24x below the smallest real
shape.
Watched failing before being trusted: on the same data Rhino and Urchin now
report LABELLED BUT INERT where the old audit said "all four elements", and
the Dolphin reports mass 15.471% / time 17.372%.
2-4. New VesselConstructionAuditor:
- guid ownership, by the ^guid: line of each candidate's OWN .meta (currently
every vessel model is owned by exactly its own);
- nested-instance reachability for plain-Transform parents, with the
stripped-parent case exempt and counted separately (currently zero
unreachable, which is the invariant). Reads YAML on purpose: a loaded prefab
presents the merged hierarchy, so the defect is invisible there;
- duplicate coincident hull renderers (currently Manta/Falcon/Shrike/Termite,
one 3,351-vertex hull from two files each) — reported, not deleted, because
§3.4 is still an open question.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
Only what Phase 1 cleared, in its own commit so a revert of this is not a revert of the swapper or the audits, and with a FRESH reference check immediately before each deletion rather than trusting the Phase 1 table. Retired: Riptide.fbx (the Dolphin's low-poly ancestor, and where the class name RiptideAnimation comes from — recorded in the doc so the name keeps its explanation), Dolphin_split.fbx and Hammerhead_split.fbx (137- and 116-vert fauna blockouts filed under Vessel Models), both Vessel_Placeholder FBXs, their CC attribution file, the now-empty Placeholder/ folder, and Assets/_Animations/MantaAnimatorController.controller (a duplicate of the live _Models/Animations/ one, referenced by nothing). 60.0 -> 48.2 MB of vessel models, 20 -> 15 FBX. Every retired guid was swept across Assets afterwards: ZERO dangling references created. Held deliberately, with reasons in the doc: RhinoModel.fbx, which becomes unreferenced only as a consequence of this commit and is the only copy of the Rhino's pre-subdivision front wings (245 verts against the shipped 476); urchan_shapekey_with_animations.fbx and SparrowModel4.fbx, which are not vestiges at all and supply live animation clips to nine and two vessels. Also corrects Phase 1's licensing flag. It escalated the two placeholders as an open question "for Garrett, not a cleanup decision" — and the answer was in their own folder: CC_Attribution_For_Placeholder_Models.txt names both as CC-BY 4.0 Sketchfab models by VertaScan, properly attributed. No liability, only an attribution obligation that was being met. Look in the folder before escalating a licensing question about its contents. Audits before/after are identical on every check (renderer table, reachability 0 unreachable, the four duplicate hull draws, 8/12 morphing, guid ownership, conditional-compilation OK) — which is what a purely subtractive commit should produce. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
CLAUDE.md's rigged-model-swap section said "Rhino wires Vessel_Placeholder_1.fbx (a literal placeholder)". That is the project's canonical document repeating the exact false positive VESSEL_CONSTRUCTION.md §2 exists to warn about: the Rhino wires Rhino_Test.fbx, whose own .meta declares guid 4a58…, and the placeholder merely remapped its materials into it. Also refreshes the VESSEL_CONSTRUCTION.md index row, which described state this branch changed: "referenced by nothing" is measured against prefabs and misses animator CLIP references (two live models were nearly retired that way); the morph audit now measures magnitude rather than labels; the Dolphin rig is the shipped hull with NO offset plus six undrawn exhaust nozzles; and a rig's rest pose is not the shipped pose, which is why each instance gets a fitted transform instead of re-measuring every mount. Kept the placeholder example in §2 as written — the mechanism is what matters and it is unchanged — with a pointer to the commit that still has the files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…l-construction-unify-hkleam
…alse
Found by the /ship-deep D2 blast-radius and D6 doc-drift sweeps, not by reading.
D2: VesselRigSwapPlanner's own doc comment still said "Rhino wires
Vessel_Placeholder_1" — the same guid trap corrected in CLAUDE.md last commit,
and now naming a file the branch deleted. The tool a future session opens FIRST
was the one still carrying it.
D6, in CLAUDE.md's rigged-model-swap section:
- "Their *_shapekey_with_animations rigs are one skinned mesh on an armature
PLUS the four element shapes" — false for two of the three, and it is the
exact claim this branch spent Phase 0 and Phase 3 disproving.
- "The prefab half is a hands-on editor pass, not an automated one ... collider
volumes must be re-fitted by eye" — the branch shipped the writer, and the
fitted instance transform means no collider is re-fitted at all.
D6, in the /vessel skill:
- CONTRACT.md said a rig swap forces every mount to be RE-MEASURED. It does not
if you fit the instance transform, which is the cheaper and more exact move.
- CONTRACT.md said the morph audit "reports presence, not magnitude ... treat a
green audit as necessary, not sufficient, UNTIL it measures deltas". That
until-condition is now met; the note would otherwise have kept telling readers
to distrust a gate that works.
- SKILL.md's auditor list gains Audit Vessel Construction and Swap Vessel Rig.
D1 also found one real hazard and guarded it: every vessel model imports with
isReadable 0, and if GetBlendShapeFrameVertices ever returned zeros rather than
throwing, a REAL morph would report INERT — indistinguishable from an empty
shape. It is now wrapped so a throw becomes "CANNOT MEASURE" and is reported,
never silently converted into a verdict.
Skill capture (/ship §3.5), both into asset-surgery's trap list:
- "referenced by nothing" is measured against whatever you grepped, and an
AnimatorController references clips by the model's guid — resolve liveness in
two steps, and resolve each referring controller to the prefabs that use IT.
- an overlap score is meaningless when the baseline overlap is already ~0; run
the control against the shipped asset. Plus its companion, that a weak
discriminator collapses onto the dominant element.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
The /ship-deep §2.5 tool-output gate resolved to HOLD rather than merge tool-only. Recording that in the checklist rather than only in chat, so the blocker and its unblocking condition live in the repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
The swap ran and the morph audit reported green. This verifies that claim against the asset rather than against the tool's own log, differentially versus the pre-swap revision, and corrects every document that still described the Dolphin as un-swapped. Verified on the pushed prefab: - 31 -> 25 GameObjects: exactly the six `Engine Left/Right.N` sub-pixel nozzles, nothing else - MeshRenderer 18 -> 1, MeshFilter 19 -> 2 (legacy hull art stripped; the remainder is the skimmer sphere + crackle overlay, both built-in meshes) - 11 Box + 1 Sphere colliders UNCHANGED - the fitted identity transform meant nothing needed re-fitting, as measured - rig sourced from the guid OWNED by dolphin_shapekey_with_animations.fbx (resolved by declaration count), parented under OrientationHandle at identity; six jets on jetint/jetinm/jetinb x .l/.r, all six names present among the rig's 30 Model nodes - RiptideAnimation's 13 Transform fields cleared to bind by name; its SkinnedMeshRenderer bound; _shipGeometries 11 -> 1 - newly-serialized fields are exactly the C# initializers, not invented - 0 dangling fileIDs introduced (one pre-existing dangle cleared); missing script set identical before and after; §3 reachability clean - Dolphin ABSENT from the §3.4 duplicate-hull list: the old renderers were removed, not merely disabled. A swap that only disables them passes every functional check and silently doubles the vessel's draw calls. measure_vessel_prefabs.py: read the model through a nested prefab instance's m_SourcePrefab, and never report Unity's built-in-resources guid as a model. A stripped SkinnedMeshRenderer carries no m_Mesh, so the survey reported the swapped Dolphin as UNRESOLVED - and had been silently blind to Scarab, Serpent, Sparrow and Squirrel for the same reason all along. Phase 0 re-runs on a tool that cannot see the skinned family are worth little. VESSEL_CONSTRUCTION.md §4.5 (new): the in-editor audit and the offline mirror report the same four Dolphin shapes 1.283x apart - identical to four digits across all four, so it is the DENOMINATOR. mesh.bounds on a skinned mesh is the culling volume in root-bone space, not the raw vertex AABB; the same trap the occlusion corridor already records. Neither is wrong, the verdict is unaffected (the threshold gap is four orders of magnitude wide), and the two must not be reconciled - compare a magnitude only against its own tool. The Dolphin is the only vessel that has ever changed construction families; §1's table, its family paragraphs, and CLAUDE.md's three claims are updated to the measured post-swap state. The /ship-deep §2.5 PR hold is released; flying it remains open and human-only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…l-construction-unify-hkleam Resolves the CLAUDE.md index-row conflict with #801 (skyburst missile tail). The two sides touched adjacent rows and each left the other at the merge base, so the resolution is disjoint and lossless: - `VESSEL_CONSTRUCTION.md` row - only this branch changed it; kept ours (the Phase 0-5 rewrite: the Dolphin leaving the part-per-mesh family, the labelled-but-inert finding, the same-tool magnitude rule). - `VESSEL_TAIL_AND_JETS.md` row - only bleeding-edge changed it; kept theirs (a TAIL is defined by its PURPOSE, not its owner - the skyburst missile carries one). Both rows verified byte-identical to the side they came from. `Docs/VESSEL_TAIL_AND_JETS.md` itself auto-merged; this branch's placeholder correction and #801's missile section are both present. Re-validated against the new base: conditional-compilation OK (1767 files), all 8 retired assets still referenced by nothing, the prefab survey still resolves the swapped Dolphin, and the FBX codec regression (subdivide_sparrow_missile.py --check) still passes against #801's missile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
The checklist said the new editor tools were type-checked against stubs and nothing more. That understated it, and the reason generalises. unity-ci.yml's `unity` job is gated on `vars.UNITY_RUNNER_LABEL != ''`, which is unset - so it is skipped on every PR in this repo and nothing in CI compiles C#. But Unity compiles Assembly-CSharp-Editor as ONE unit, and there is no .asmdef under Assets/_Scripts/Editor/, so all four editor files this branch touches share that assembly. Garrett ran two of them: the brand-new Swap Vessel Rig menu item, and the morph audit reporting magnitude percentages only this branch's code can produce. A stale assembly could contain neither. The assembly therefore compiled, and VesselConstructionAuditor.cs - which nothing else has ever exercised - compiled with it. An assembly-wide compile is transitive evidence: a passing menu item vouches for its whole assembly, not just its own file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
Playtest found the swapped Dolphin puppeteering wrongly and wearing a yellow
left over from the Blender import. Both are defects in the swap, and both are
in things the swap did not touch rather than in what it moved: the geometry,
colliders, bones and morph it DID move all verified correct. Two independent
root causes.
1. MATERIALS. The fleet's convention is that a model's .meta REMAPS its DCC
material names onto three project assets (Body/Domain/Window) via
externalObjects - the Squirrel and Sparrow both do. The Dolphin rig's meta
had `externalObjects: {}`, so it imported its own Blender materials, and
`accent.001` - the material that should carry the DOMAIN colour - is
EmissiveColor (1.0, 0.395, 0.0) at EmissiveFactor 10. That is the yellow:
saturated orange, emissive 10x, on the accent.
Worse, and invisible without measuring: submesh ORDER is not a convention.
The rig emits [accent, BASE, windsheild, N], so the platform's default
`_domainMaterialSlot: 1` painted the team colour onto BASE - the BODY -
which VesselCustomization's own tooltip says never to do ("turns the whole
hull one uniform material and erases the two-tone read"). Fixed by remapping
the three roles AND switching the Dolphin to the order-independent path,
`_domainReplacesMaterials: [GreenAccentVesselMaterial]`, which resolves the
slot by material identity instead of by index.
2. REST POSITIONS. VesselAnimation grew rest-relative ROTATION when the first
rig landed (RotatePartFromRest) and never grew the position half, so
RiptideAnimation.AnimatePart still wrote `part.localPosition` absolutely.
That assumes a part is a direct child of the model root. On a rig it is
false twice over: a bone's local position is relative to its PARENT BONE,
and in Blender's convention it is (0, boneLength, 0). Measured on this rig -
every animated bone rests at (0, L, 0) with large rest rotations (jetT.l at
-2, 172, 57) - so writing (0, .15, -1.7) detached each of the six engines
and flung it 1.7 units along a DIFFERENT axis, and drove the chassis and
wings off their parents entirely.
Adds CaptureRestPositions / RestPositionOf / MovePartFromRest as the sibling
of the rotation trio. Two halves, both load-bearing: the ANCHOR resolves
through the part's HOME parent (so a part stays attached where it belongs),
while the OFFSET resolves through its CURRENT parent - because these parts
are re-parented onto a drift handle mid-flight and `parent =` preserves the
world pose, so a lerp toward a rest captured under the other parent would
teleport them, and the offset following the drift handle is the entire point
of that re-parenting.
Translating the constants surfaced that default and backward thruster
positions were the SAME vector - the thrusters never had a positional
animation at all, the constant only ever pinned them. The one real
positional effect in this puppetry is the wings sliding forward on a drift,
and it is now the only non-zero offset.
3. RECURRENCE. The Rhino and Urchin rigs have the same empty externalObjects,
so running either swap would reproduce the colour bug exactly. VesselRigSwapper
now REFUSES a swap whose rig wears none of the fleet's materials, naming the
importer's Remapped Materials list. It checks rather than fixes: which
material carries the domain is a decision, and one made silently is one
nobody can review.
Verification: the changed runtime code compiles (Roslyn, stubs transcribed
from real declarations, proven by two negative controls that fail correctly);
the editor tool compiles with the existing harness - which caught an invented
type name; every Unity API used is confirmed against independent in-repo usage;
prefab and meta both parse, 0 dangling fileIDs introduced. NOT verified: the
ship in motion. That needs a flight.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…lew broken
The Dolphin swap passed every structural check on the branch and was still
unusable in flight. That is the finding worth keeping, so it is recorded where
the next rig swap will read it rather than buried in a fix commit.
- UNITY_VERIFICATION_CHECKLIST.md: the entry goes back to RED with a table of
what the flight found, what caused it, and specifically WHY each static check
was blind to it. DO NOT MERGE until it is re-flown.
- VESSEL_CONSTRUCTION.md §4.6 (new): a rig swap is three jobs - geometry,
materials, animation - and passing the first says nothing about the other
two, because both of those only fail on screen. Carries the measured
cross-fleet submesh-order table showing that _domainMaterialSlot's default
of 1 is a coincidence, not a contract.
- CLAUDE.md: the same, compressed, in the VESSEL_CONSTRUCTION index row.
§4.6 is deliberately filed next to §4.4 ("a rig's REST POSE is not the shipped
pose"), because the position bug IS §4.4 - the lesson had been applied to
rotation only, and the missing half sat latent until a rig actually bound to it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
gradies
marked this pull request as draft
August 26, 2026 03:48
Second playtest: colours fixed, puppetry still wrong - "roll and pitch are mixed, pitch is inverted, the wings fold up when drifting". That diagnosis was right, and it is one root cause with two halves. Quaternion.Euler(pitch, yaw, roll) assigned to localRotation turns about the PARENT's axes. On part-per-mesh art every animated part hangs off the model root, so those ARE the ship's axes. A bone's parent is another bone, pointing wherever the skeleton points, so the identical call turns about the wrong axes. Measured against this rig's own bone rest rotations, for a commanded PITCH: jetT.l -> (-0.541, 0.840, -0.043) mostly YAW, pitch inverted winghold.l -> (-1.000, 0.000, 0.000) the ship's pitch axis, NEGATED fuse/wing.l -> ( 1.000, 0.000, 0.000) correct - these rest ship-aligned winghold.l is the whole "pitch is inverted" report: it rests at (77.08, 0, -180) and that -180 roll flips X, so the wings pitched backwards while the engines yawed. The second half is the drift. These parts are re-parented onto the drift handle in flight and `parent =` preserves the world pose, so a rest rotation captured under a BONE and replayed under the handle lands somewhere the part never was: 164.50 degrees off for the wings, 152.38 for the engines, on drift entry, with no pilot input at all. That is the fold. RotatePartFromRestInFrame fixes both together - conjugate the turn into the frame the animation MEANT (the vessel, or the drift handle while drifting) and anchor the rest through the part's HOME parent. MovePartFromRest takes the same frame, because its offset had the identical defect: at rest its basis was the part's own parent, which is a bone. Deliberately a SEPARATE method from RotatePartFromRest rather than a change to it: that one is shared with ScarabAnimation, whose animation was authored and play-tested against its current behaviour, and silently re-framing it is a regression with no evidence behind it. The Scarab is untouched and may have the same latent bug - that is a separate, evidenced call. Verified: compiles (Roslyn, stubs from real declarations); and Tools/Build/verify_vessel_rig_puppetry_frames.py re-proves the math offline against the rig's measured bones, asserting the two invariants that keep it honest - a ship-aligned part is BIT-IDENTICAL under both formulas (fuse, wing.l: 0.000000000 deg), and entering a drift with no input moves nothing (0.00 deg). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…at re-aimed Third playtest: "much better", with three specifics. All three are real, and two of them were only VISIBLE once the frames were right - while every part turned about its own bone there was no consistent roll to be wrong about. 1. ROLL WAS INVERTED. Roll arrives as InputStatus.YDiff and now turns parts about the ship's +Z. Unity's positive Z rotation is counter-clockwise seen from IN FRONT of the ship, so a pilot rolling right got a left bank. Negated once at the top of PerformShipPuppetry, NOT at the call sites: the wings' roll term is (roll +/- pitch), and flipping the whole expression would invert the pitch DIFFERENTIAL - the aileron read - with it. 2. THE ENGINES NEVER MOVED. A drifting Dolphin swings its hull to aim while it keeps sliding, so the wings slide FORWARD and the engines slide BACK to open a gap the fuselage and jaws can turn through without clipping. The engines' "backward" and "default" offsets were the SAME vector, so half that clearance had never existed - and translating the constants faithfully preserved the omission. A degenerate constant pair is not a design; it is indistinguishable from a value nobody finished, and it is a question to ask rather than a fact to carry forward. Both magnitudes are now authored (driftWingForward / driftJetBackward), because how much room the jaws need is a feel question, not a fact about the model. 3. THE DRIFT RE-AIMED THE PARTS IT SHOULD HAVE MOVED. Wings and engines were re-parented onto a handle aimed along Course, so they swung WITH the aim - engines chasing the nose instead of clearing it. The frame is now always the vessel and the drift is purely translational, which is what "wings forward, engines back" asks for. That makes the re-parenting machinery inert, so it is deleted rather than left lying in a file that has now cost three playtests: CaptureHomeParents, DriftParts, ReparentToDrift, ReparentHome, SetParent and _homeParents, 45 lines. DriftHandle survives as an empty anchor (no children, nothing reads it) in case a course-aligned frame is wanted back; git carries the methods. verify_vessel_rig_puppetry_frames.py gains two checks: a positive roll input must drop the RIGHT wing tip (-0.1736 now, +0.1736 before - the inverted bank), and the engines must have non-zero backward clearance so the gap actually opens. Verified: compiles (Roslyn); the verifier passes all five checks; conditional-compilation OK. NOT verified: the ship in motion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…l aims Fourth playtest: "the jet models and wings both stayed with the fuselage when they are supposed to be signaling the course direction and letting only the fuselage and jaws aim." I had this backwards, and the previous commit made it worse by deleting the machinery that did it. A drift splits the Dolphin in two. The fuselage and jaws turn to AIM wherever the pilot points; the wings and engines stay lined up with COURSE - the direction the ship is actually travelling - so the hull reads as slewing across its own path. The appendages are the instrument telling everyone where the ship is really going. The clearance (wings forward, engines back) is the SECOND half of the manoeuvre, not the whole of it: I read the clearance statement as the entire intent and removed the signalling. That is the lesson worth keeping: the re-parenting onto a Course-aligned handle had no observable effect ONLY because the frames were still wrong, so "it does nothing, delete it" was a conclusion drawn from a broken build. Machinery that looks inert may be the feature seen through a bug. Restored as a FRAME rather than a re-parent, which is strictly better on a rig: RotatePartFromRestInFrame now expresses the part's rest pose in the VESSEL's frame and carries it onto whatever frame it is handed, so handing it the Course-aligned drift handle puts the part on Course holding its own shape, with nothing moved in the hierarchy and no bone rest pose replayed under a foreign parent (the 164-degree wing fold). Handed the vessel - the default, and what the chassis and jaws get - it reduces exactly to the previous behaviour, which the verifier asserts bit-identically. verify_vessel_rig_puppetry_frames.py gains check 4: with the hull slewed 40 degrees off Course, a course-framed part must sit exactly 40 degrees off its hull-framed pose. Measured 40.00 for both a wing and an engine. STILL OPEN: roll left/right. Reported inverted, negated, still reported swapped - so the negation is not the lever and I am not going to guess a third time. Asking rather than shipping another sign flip. Verified: compiles (Roslyn); verifier passes all six checks; conditional-compilation OK (1767 files). NOT verified: the ship in motion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…e wings
Roll was reported inverted, negated inside RiptideAnimation, then reported
STILL swapped. A pure sign flip is one of the few edits whose effect is
certain, so a symptom that survives one is evidence about WHERE the defect is,
not about which way it points. Put to the pilot, who answered in a sentence:
it's the whole ship, not the wings.
It is VesselTransformer.Roll(), which banks the hull about transform.forward
from InputStatus.YDiff. Measured, not assumed:
* this branch has touched ZERO files under Controller/Vessel/;
* Roll() is byte-identical to origin/bleeding-edge;
* that transformer is shared by SIX vessels - Manta, Squirrel, Falcon,
Rhino, Shrike and the Dolphin.
So the bank direction is pre-existing, fleet-wide, and nothing to do with the
rig swap. Negating inside the animation only made the wings disagree with the
hull they are bolted to, which is strictly worse than both being wrong
together. Reverted: roll passes through untouched and the puppetry follows the
flight model wherever it goes.
Flipping it for real is a flight-model change across six shipped vessels. That
is a deliberate decision, not a side effect of a vessel-construction pass, so
it is left alone and written up instead (VESSEL_CONSTRUCTION.md 4.6).
The verifier's roll-sign check went with the negation it was asserting; the
other five checks are unchanged and still pass.
Verified: compiles (Roslyn); verifier passes; conditional-compilation OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…rable Fifth playtest: body reads right, wings bank opposite it, and the wings and engines keep responding to the stick mid-drift when they should be nothing but a course indicator. 1. APPENDAGES GO QUIET WHILE DRIFTING. Holding Course is the whole of their job during a drift - only the fuselage and jaws answer the stick. The wings and all six engines now take zero puppetry input while IsDrifting, so they sit on Course with the clearance gap open and say nothing else. Leaving them responsive read as the ship still flying while it is supposed to be sliding. 2. THE WING BANK IS NOW A PARAMETER, NOT A GUESS. The conjugation turns every part about the ship's +Z, so the wings' delta is provably identical in axis AND sign to the chassis's - measured, 25/75 degrees about (0,0,1) for both - and yet on screen they read opposite. I cannot derive that, and three sign guesses have now cost three flights, so it stops being a constant: wingRollResponse (default -1) scales the wings' roll contribution, +1 with the hull, -1 against, 0 not at all. That is honest authoring rather than a flag - how hard the wings bank relative to the hull is a feel parameter - and it makes the question answerable in the editor instead of by round-trip. When the math and the screen disagree and the math is verified, the answer is a parameter. Also corrects my own reading: "the rings" was the WINGS. The engine cases were measured first and are fine - their delta is the same ship-space turn as everything else's. Verified: compiles (Roslyn); puppetry verifier passes; conditional-compilation OK (1767 files). NOT verified: the ship in motion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
… YAW
Sixth playtest, two findings.
1. THE COURSE FRAME WAS TWISTING. "The wings and thrusters look like they are
trying to keep their up vector in a position relative to the camera instead
of just letting the camera rotate about them" - which names the mechanism
exactly. SafeLookRotation(Course, transform.up) pins the frame's roll to the
hull's up, so as the hull aims away from Course that up swings and drags the
frame around the Course axis. The appendages rotated with ZERO pilot input.
Measured over a 0->50 degree aim sweep with Course held fixed:
LookRotation(Course, hull.up) 19.77 deg <- what shipped
rebuild from the hull's current nose 7.03 deg
rotation-minimizing (now) 0.000024 deg
The middle row is the trap and I nearly shipped it: it is the obvious fix,
it is 3x better, and it is still wrong, because the shortest arc from a
MOVING nose changes as the nose moves. The frame is now seeded from the hull
at drift entry and thereafter turned by the shortest arc from ITS OWN
previous forward onto Course - every step a pure swing, no twist by
construction.
"Point this at that" does not define a frame. A direction leaves the roll
about it free, and every look-at helper fills that in from an up-vector that
is usually moving.
2. IT WAS THE WINGS' YAW, NOT THEIR ROLL. Last round's wingRollResponse = -1
flipped an axis that was already right, so the wings ended up wrong on two.
Roll returns to +1 (the hull's bank is correct and the wings follow it) and
yaw goes to -1. All three axes are now exposed - wingPitchResponse,
wingYawResponse, wingRollResponse - so which axis reads backwards on a rig is
an editor question rather than a code guess.
SafeLookRotation was this file's only user of CosmicShore.Utility; the using
goes with it.
Verified: compiles - and the harness was extended to actually INCLUDE
PerformShipPuppetry first, because the previous extraction stopped at the field
block and was reporting green on code it never saw; a negative control now
breaks on the drift-frame line itself. verify_vessel_rig_puppetry_frames.py
gains check 5 (no twist with no input), which fails correctly at 7.03 deg when
fed the old construction. conditional-compilation OK (1767 files).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
Seventh playtest, with a photo: the wings no longer cross each other in plane - one folds up and one folds down, each out of the plane - and the wings' roll still reads backwards while the vessel rolls. Three sign guesses across three flights never touched the real cause. The two wings' arguments differ in exactly two places: `pitch` bleeds into their ROLL and `throttle` into their YAW, one wing plus and the other minus. That antisymmetry is what takes them out of a common plane. Measured: roll 1.0 pitch 0.0 right -25.0 left -25.0 split 0 deg coplanar bank roll 1.0 pitch 0.3 right -17.5 left -32.5 split 15 deg FOLDED roll 0.5 pitch 0.6 right +2.5 left -27.5 split 30 deg FOLDED A bank is both wings turning by the SAME angle - the wing plane tilts and stays a plane. Unequal angles is the fold in the photo. On the legacy art the coupling read as wing sweep; on the rig, with the wings resting under 'winghold' bones at 77 / -102.9 degrees, it reads as the wings coming apart. The coupling is now `wingDifferential`, DEFAULT 0, so the wings bank together and the authored sweep is opt-in. `wingRollResponse` goes to -1, which is the sign this flight actually reported. A symptom that survives every sign you can flip is not a sign. Signs are cheap to try and that is what makes them a trap - three flights went into flipping roll, then yaw, then roll again, while the thing breaking the silhouette was a coupling nobody had looked at. When flipping stops paying, enumerate what makes the two sides DIFFER rather than what makes them wrong. Also corrects a bad measurement of my own along the way: I first tested the fold by comparing the two wing TIPS' vertical motion, which is opposite for a correct bank too, so it reported "folded" in every case including the fixed one. The invariant is equality of the two ROTATIONS, not opposition of the two tips. verify_vessel_rig_puppetry_frames.py gains check 6: with the coupling off the worst wing-to-wing roll split must be 0 (it is; at 1 it is 30 degrees). Verified: compiles, with the new code confirmed present in the compiled unit; verifier passes all seven checks; conditional-compilation OK (1767 files). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
Three passes of per-axis sign scalers on the wings were all wrong on playtest, because the defect was never a sign. Measured from the FBX and from the pre-swap prefab: * The legacy art parented every animated part under the chassis (LeftWing / RightWing.001 / Engine case L|R.1-3 all hung off `Dolphin_Test`, which is what `Chassis` resolved to), so each inherited the chassis's own (25*pitch, 25*yaw, 25*roll) turn and composed its own on top. On the rig the wings hang off `winghold.l|r` and the engines off `jetholdT|m|B.l|r` - a sibling branch of `fuse` - so that term is gone. The wings' own pitch input is Brake(throttle), zero unless braking, so they went dead on that axis: "fixed in their x axes at all times". * The drift's Course frame was `DriftHandle`, a Transform parented under the vessel. A child's world rotation is stored as a local one, so the hull's aiming carried it between one frame's write and the next read; re-pointing only its forward axis left the twist. Measured: 24.6 degrees accumulated on parts commanded to hold still, which is what "keeping their up vector relative to the camera" was. So: every per-axis response scaler and the cross-coupling knob are gone and the wing/thruster terms are the ones this ship has always used, with the chassis turn composed in by hand (composed, never Euler-added - 6.43 degrees apart on an ordinary stick pose). The drift frame is a quaternion captured once at entry - no parent, nothing can perturb it - and the appendages take no puppetry at all while drifting, only the clearance offsets. `RotatePartFromRestInFrame` and `MovePartFromRest` therefore take quaternions rather than Transforms; both are Dolphin-only, and the shared `RotatePartFromRest` is untouched. verify_vessel_rig_puppetry_frames.py grows three checks: the composed turn matches the legacy hierarchy to one ULP, pitch reaches the wings again, and a parented handle twists 24.6 degrees where the frozen quaternion twists 0.000. Both source files compile as shipped (Roslyn, with negative controls proving the harness sees the edited regions). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…ere they live
Two things from the eighth flight.
THE DRIFT FRAME. Freezing the hull's orientation at drift entry is stable but
holds where the ship WAS, not where it is going - so as the pilot rolls, the
appendages stop matching the hull and the camera (measured 8.20 degrees adrift
over an ordinary aim). The frame is now the hull RE-AIMED onto Course by the
shortest arc, rebuilt from scratch every frame:
FromToRotation(transform.forward, Course) * transform.rotation
The appendages then read as a dolphin flying straight down Course; their up
stays the hull's, which is roughly the camera's, because FromToRotation is a
pure swing about an axis perpendicular to the nose and injects no roll of its
own (verified as zero residual component ALONG the nose - comparing up-vectors
is not that test, and an earlier version of the check measured the aim's
legitimate tilt and called it roll); and being a pure function of (hull, Course)
with no state it cannot accumulate, which a parented Transform provably does.
POSITION IS RESOLVED IN A DIFFERENT FRAME FROM ROTATION, deliberately. The
clearance offsets are read in the VESSEL's frame - a clearance is measured
against the hull, and reading it in the Course frame would translate the parts
as the hull aims.
THE ENGINES. The rig's jet bones rest at z -1.90; the legacy art authored its
engine cases at z -2.047. Both models are unit-1 (UnitScaleFactor 1.0,
useFileScale 1, globalScale 1) with every scale in both prefab chains at 1, so
those are directly comparable world units and the rig sits its engines 0.15
FORWARD of the ship it replaced. `jetRestBackward` (0.15, authorable) puts them
back. It is a resting offset, not clearance - the drift gap adds on top.
verify_vessel_rig_puppetry_frames.py check 6 rewritten around the new frame
(forward exactly on Course, zero injected roll, bit-identical on rebuild) with
both rejected alternatives kept as controls; check 8 now asserts the resting
setback lands on the legacy engine z. Compiles as shipped, with a negative
control proving the harness sees the new frame line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…l-construction-unify-hkleam
Reported as "the jet and wing positions look like their z value in the vessel
frame remained near zero". It is not the offsets and not the frame:
if (InputStatus.Idle) Idle(); // ResetAnimation writes ROTATION only
else PerformShipPuppetry(...); // the only caller of MovePartFromRest
`VesselAnimation.Idle` relaxes rotations and nothing else, which is complete for
every other vessel because RiptideAnimation is the only animation in the fleet
that POSITIONS its parts. So the whole positional layer lived on a path an idle
vessel does not take: the engines' resting setback applied only while the stick
was off centre and froze wherever it had reached when the pilot let go.
`Idle()` is now overridden to settle positions through the same
ApplyRestingLayout() the puppetry path uses, so the two cannot describe a
different ship.
Three plausible causes were measured and ruled out first, all recorded in
VESSEL_CONSTRUCTION.md 4.6.3: an Animator stomping the write in the animation
update (there is no Animator on the prefab and no script assigns a controller);
soft skin weights smearing the mesh instead of sliding the part (every bone owns
its verts at weight 1.000 with zero sharing - wing.l 294 verts, jetT.l 538);
and the offsets being too small to see (the hull collider is 3.448 units long,
so driftWingForward 2.3 is 67% of it).
That last one also settles a scare from the same pass: the rig FBX root carries
Lcl Scaling (100,100,100), which reads as a 100x discrepancy. It is normalised
away on import - the mesh's raw extent 5.287 equals the wingspan the shipped
colliders describe, and the camera sits 20 units back - so bone positions
measured off the FBX are world units.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
The engines still sat too far forward and neither the wings nor the jets moved on a drift. The offsets were never reaching the screen in a readable size, and the reason is a fact neither model states: legacy Dolphin_Test.fbx root 'Chassis' Lcl Scaling: NONE rig dolphin_...fbx root 'dolphin' Lcl Scaling: (100, 100, 100) Both declare UnitScaleFactor 1.0 and useFileScale 1. Whether Unity bakes that 100 into the bone rest poses or leaves it on the imported root decides whether this hull is ~3.4 units long or ~340 - and the same absolute 2.3 is then either 67% of the ship or 0.67% of it. Simulating the shipped MovePartFromRest arithmetic against the real bone chain reproduces the report exactly under the second reading, down to the local value: wing.l goes from (0, 0.994, 0) to (0, 0.999, -0.022) - "their z value in the vessel frame remained near zero". Rather than pick a side offline, the offsets are now FRACTIONS of the hull's own circumscribing radius, measured once at Initialize through PrismOcclusionCorridor.MeasureCircumscribedRadius - the fleet's existing world-unit hull measurement, reused rather than duplicated. Correct at either reading, at any model scale, for any future re-export, with nothing re-authored; and the numbers finally mean something (0.25 = a quarter of the hull radius). The fields are not serialized on the prefab, so the new defaults apply with no asset edit. This also RETRACTS the previous commit's derivation of jetRestBackward = 0.15 from the legacy engine z. That rested on "both models are unit-1 so those are comparable world units", which is exactly the false clause above. Three causes were measured and ruled out on the way, each individually correct and collectively misleading: nothing else on the prefab writes a transform position (32 of 37 scripts checked, none writes localPosition; no Animator); the skin binding is rigid, so a bone translation slides its part cleanly (every cluster owns its verts at weight 1.000, all 12595 accounted for, and each engine's nozzle is a CHILD of its case so it follows); and the arithmetic is exact. Recorded in VESSEL_CONSTRUCTION.md 4.6.4. verify_vessel_rig_puppetry_frames.py check 8 now asserts the offsets are scale-free - the same fraction of the hull at both candidate readings (23.9% either way), against the absolute 2.3's 81.3% vs 0.81%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…space
"Now it was scaling things in wild ways" - my previous commit's fault, and the
useful half of it.
Re-basing the drift clearance on PrismOcclusionCorridor.MeasureCircumscribedRadius
was meant to escape the two Dolphin models' disagreement about import scale. It
did not remove that ambiguity, it IMPORTED it. For a skinned hull that helper
measures
var space = skinned.rootBone != null ? skinned.rootBone : skinned.transform;
Accumulate(skinned.localBounds, space, origin, ref maxSqr);
- localBounds in ROOT-BONE space, carried to world through the ROOT BONE's
transform. The root bone is exactly where the disputed Lcl Scaling (100,100,100)
lives. Multiplying a bone-space offset by a root-bone-space measurement can be
off by that factor either way, and at 100x it throws the wings and engines clear
of the ship. VESSEL_CONSTRUCTION.md 4.5 already records this trap costing the
Sparrow a ~5x oversized corridor off a 0.2 armature; I walked into it.
The basis is now the farthest positioned part's OWN rest distance from the
vessel origin, measured once at Initialize (nothing has displaced them yet, so
their position is their rest position). Whatever units those bones are in, the
offset is in the same ones by construction - no armature factor, no renderer
bounds, no unrelated child (a tail, a skimmer, a HUD canvas) can contaminate it.
Wings +0.35 reach forward on a drift, engines -0.35, -0.08 at rest.
And the offsets are CLAMPED to +-1x that reach. A clearance is a nudge, not a
launch, so the family is bounded against exactly this failure rather than
trusting the next basis to be right. The clamp tunes nothing; it refuses the
absurd.
Two rules, in VESSEL_CONSTRUCTION.md 4.6.4 and CLAUDE.md:
* a scale-free quantity is only scale-free if the value and its BASIS are
measured in the same space - "divide by a measurement of the ship" is not
sufficient when that measurement comes through a different transform chain;
* when you cannot resolve which of two readings is real, prefer a basis you
can derive from the thing you are about to modify. It cannot disagree with
itself.
verify_vessel_rig_puppetry_frames.py check 8 rewritten: the offsets are the same
fraction at both scale readings (35.0% either way), the mismatched-basis control
reproduces the defect (50x the part's own reach), and the clamp bounds an
authored 5.0 or -12.0. Compiles as shipped with negative controls on both the
measurement and the clamp.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…ten exactly The import model is PINNED by measurement (a conjugation by 0.01*mirror-X; bones keep lossyScale 100 while their world poses land on the hull - fitted against the colliders the rig swap itself preserved, residual 3e-5), which retires the scale ambiguity that produced both prior offset failures and makes plain world units safe. Every number is now a measurement: - driftWingForward 2.2: the proven bleeding-edge lunge (+2.3 from ITS on-screen rest; the rig rests wings 0.10 ahead of that). The previous reach-fraction scheme could not express it: |fraction| <= 1 on a 1.96 basis tops out below the proven look. - jetRestBackward 0: the "jets pushed forward" report inverted on measurement - the OLD RUNTIME dragged its engine pivots from authored z -2.047 to -1.7 every frame; the rig, which retires that drag, already rests its drawn nozzles 0.40 BEHIND the old screen. - driftJetBackward 0.5: the one ask rather than parity - bleeding-edge never moved its engines on a drift (backward == default, same vector). Four defects fixed with the redesign: - the drift cage now holds the COURSE frame for POSITIONS as well as orientations (the old DriftHandle re-parent did both; holding only orientation swept the cage sideways with the aiming hull); - the cage pose is written EXACTLY in cage coordinates with the entry pose ADOPTED and blended to station (the re-parent's other half: a lerp toward a hull-independent target trails a 110 deg/s aim by ~53 deg / ~1.7 wu of visible drag - "the wings and jets still appear to move as I am drifting and aiming"); - the rest anchor is a capture-time constant (a live read through an animated parent applies the chassis term twice on legacy chassis-child art - 27.8 deg at an ordinary stick pose; bit-identical on the rig); - FromToRotation is guarded at the antipode (aiming fully backwards whips the unguarded frame at 2x the wobble rate; the previous frame is held inside dot < -0.999). Also: Idle() reroutes to the drift layout while IsDrifting (touch-only exposure; the drifting branch never evaluates Brake), the three clearance fields are serialized into Dolphin.prefab deliberately (defaults changed meaning twice already), and m_UpdateWhenOffscreen=1 on the rig's skinned renderer (rest-pose culling bounds vs a 2.2 wu lunge). Verified: offline compile (Roslyn + stubs, negative-controlled); verify_vessel_rig_puppetry_frames.py extended to 12 checks, all green (world-unit round trip exact at chain scale 1 and 100, cage wander 0 vs 2.5 wu for the vessel-frame control, captured anchor 1.1e-16 vs 27.8 deg for the live-anchor control, antipode guard 0.00 vs 20 deg/step raw, lag control 53 deg vs 0 exact). Full derivation: VESSEL_CONSTRUCTION.md 4.6.5. NOT verified: the ship in motion - in-editor verification unavailable in this session; this commit is the subject of the next flight. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…l findings An adversarial four-lens review of the previous commit converged on one real defect and three honesty gaps, all fixed: - THE CAGE IS SLEW-LIMITED (360 deg/s, drift path only). The antiparallel hold cone (dot < -0.999, 2.56 deg) is an order of magnitude narrower than FromToRotation's ill-conditioned band (~2/sin(rho) churn amplification: 68 deg/FRAME measured 3 deg off a full-reverse aim), leaving the hold cone was a one-frame snap, and the exact-write cage removed the lerp low-pass that used to hide both. Legitimate cage motion is bounded by the hull ROLL rate (~1.8 deg/frame), so a 6 deg/frame cap never engages in ordinary flight and turns a reverse-aim sweep into a fast continuous swing. - The entry blend is EXPONENTIAL (Mathf.Lerp toward 1), not linear MoveTowards - the legacy re-parent's convergence class; a constant-velocity 2.2 wu lunge with a hard stop read mechanical. - _wasDrifting resets in OnEnable: a component disabled mid-drift and re-enabled inside a LATER drift no longer skips the entry adoption. - CLAUDE.md's VESSEL_CONSTRUCTION row contradicted itself: it still carried the superseded "position and rotation can want different frames" ruling this branch reversed. Rewritten to the measured ruling (one Course frame for the whole cage). - Verifier check 9 now runs the SHIPPED frame construction (it modelled the cage as a constant and mis-cited check 6): bit-still proven under single-axis aim, the 30-deg roll-following asserted as the designed up-tracking orbit (radius drift 2e-16), and check 11 gained the churn band (raw 20 deg/step vs shipped 6.00) and a hold-cone transit (0.00). - Docs 4.6.5: "bit-still" scoped to pitch/yaw aim; roll-following, the slew limit and the blend shape recorded. Verified: offline compile (negative-controlled), 12/12 verifier checks. NOT verified: the ship in motion - this commit is part of the next flight. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…t the boosters halfway Flight 8: "close to perfect. with only 2 minor adjustments." 1. The wings' and all six boosters' ROLL response is MIRRORED - the roll input is negated everywhere it reaches those parts (their own Euler term AND the chassis term composed into them), so their net roll deflection is the exact mirror of what it was. The chassis keeps the true roll, the aileron cross-coupling (the pitch component of the wings' z term) is untouched, and the drift cage still follows the hull's roll (up toward the camera - its own signed ask). A deliberate feel departure from legacy parity; the composition mechanism is unchanged. 2. The boosters' REST position moves to exactly halfway between the authored sculpt and the drift station: jetRestBackward 0 -> 0.25 and driftJetBackward 0.5 -> 0.25, so the drift STATION - called perfect - is byte-identical at rest+drift = 0.50. The verifier now pins the pair (station 0.50, rest = station/2) so retuning one field without re-balancing the other fails the build. Prefab serialization follows the code defaults; Docs 4.6.5 records both as flight-8-signed feel deltas. Verified: offline compile (Roslyn stubs), 12/12 verifier checks incl. the new station pin, conditional-compilation clean. NOT verified in-editor: no Unity CLI in this session - the change is two sign flips and two serialized floats on the flight-8-approved build. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…'s one ask Flight 9: "everything is perfect. just move the flying position of the boosters back further." jetRestBackward 0.25 -> 0.375 (half the remaining gap to the drift station), driftJetBackward 0.25 -> 0.125 - so the drift STATION, called perfect in both flights, stays byte-identical at rest + drift = 0.50. The visible drift slide shrinks to 0.125 as the geometric consequence; deepening the seat any further against the fixed station means moving the station (driftJetBackward), not the seat - the tooltip and Docs 4.6.5 now say so. Verifier's station pin re-signed to the shipped pair (the flight-8 "rest = station/2" clause is retired: the seat is a feel dial inside a signed station, not a fixed ratio). Verified: offline compile, 13/13 verifier checks, conditional-compilation clean. NOT verified in-editor (no Unity CLI here): two serialized floats and their matching code defaults on the flight-9-approved build. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…D anchor, not another halving Flight 10: "oddly this seemed to do nothing. the boosters are still right up next to the wings unless I'm drifting." It did nothing, and the code was never at fault. Measured from the rig's own skin clusters (vessel frame): the nozzles sculpt at z -2.290..-1.887 while the fuselage tail is at -2.471, so at any seat under 0.584 the boosters sit tucked ALONGSIDE the tail. Worse, flights 9 and 10 each moved the seat 0.125 wu - 3.6% of the 3.45 hull, a handful of pixels at chase distance. Both playtests were spent on steps below the threshold of visibility. The cause was a constraint I imposed: an earlier pass PINNED the drift station at 0.50 off one flight's sign-off, so every later "further back" had to come out of the shrinking gap to that ceiling and halved itself. That is backwards - what a drift must keep is the visible SLIDE, not an absolute station. jetRestBackward 0.375 -> 0.6 (clears the fuselage tail; 17.4% of hull) driftJetBackward 0.125 -> 0.25 (the slide restored to a visible 7.3%) station 0.50 -> 0.85, free to follow the seat The verifier's station pin is retired and replaced by a VISIBILITY FLOOR: 5% of hull on the seat, on the slide, and on any step away from a value a playtest could not see - so a sub-threshold tune fails the build rather than costing a flight. It also asserts the measured tail-clearance anchor. Verified: offline compile, 13/13 verifier checks, conditional-compilation clean. NOT verified in-editor: two serialized floats; jetRestBackward is a live inspector dial on Dolphin.prefab if the shipped value wants nudging. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…ads as behind the ship Flight 11: 'the wings don't travel far enough forward to get clearance while drifting, and the boosters still return to a position far to forward.' Both answered by measurement rather than increments: - driftWingForward 2.2 -> 3.5. 2.2 was old-game parity, and the old game interpenetrated: the aiming hull's jaw tip sweeps a 2.835-radius sphere about the vessel origin, and at 2.2 the wings' nearest vertex sat at |r| 1.76 - inside it. 3.5 rounds up the bisected L* = 3.492, the smallest lunge putting every wing vertex outside that sweep x1.05 gape margin, so the jaws cannot reach the wings at any drift aim angle. Within the +-4 clamp. - jetRestBackward 0.6 -> 1.0. 0.6 cleared the fuselage tail by only 0.017 and still read as tucked-in; 1.0 lands the nozzles' leading edge 0.42 wu behind the tail (29% of hull, a 0.4 step - both far above the 5%-of-hull floor). - driftJetBackward stays 0.25 (the motion the drift shows). Verifier: constants tracked (JET_REST 1.0, PREVIOUS_REST 0.6, lunge 3.5) and a new geometric clearance assertion added to check 8 - the binding wing vert at the shipped lunge must sit outside the jaw-tip sweep, so an under-clearing retune fails the build. All 13 checks green; conditional-compilation OK. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…e, seat it deeper Flight 12: 'everything is perfect now (even the wings and the drifting position of the boosters) except the home position of the boosters ... they continue to rest near the origin of the fuselage.' The drift position is confirmed perfect at driftJetBackwardTotal(effectively 1.25 before this commit). The rest seat (jetRestBackward) has now been called too far forward across three flights (10, 11's ask, 12) against three successively deeper, individually-measured seats (0.6, then 1.0) - each survives past the 5%-of-hull visibility floor (flight 10's actual defect) but still isn't enough, which is a different failure than flight 10's. Two changes: - DECOUPLE: BackwardThrusterOffset used to read jetRestBackward + driftJetBackward, so every rest-seat bump silently moved the drift clearance too - flight 11's 0.6->1.0 bump happened to land the drift total at 1.25, which flight 12 confirms reads as perfect. A further rest bump risked dragging that confirmed value along with it. driftJetBackwardTotal is now its own field, locked at 1.25, with no path back to jetRestBackward. - BOLD, not incremental: jetRestBackward 1.0 -> 1.8. Clears the fuselage tail by 1.22 wu (vs 1.0's 0.42), nearly matching the drift's own separation instead of trailing it by a marginal quarter-unit. Verifier restructured for the decoupling: JET_REST and DRIFT_TOTAL are asserted independently (visibility floor, tail clearance), with the coupled- field defect documented as the cause rather than re-derived. All 13 checks green; conditional-compilation OK. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…try, halve the booster flail Flight 13: 'I had to look at the scene view to realize the boosters were far more back than I realized. it is just getting puppeteer[ed] in a way that made me perceive them as much closer.' The seat was applying all along; no seat could ever have fixed the report. The Dolphin's chase camera is on-axis and level (followOffset 0,0,-20), so a part's station along the hull projects to almost nothing - the surviving cues are SIZE and OCCLUSION, and both invert: a booster seated 1.8 aft sits 3.7 wu nearer the lens than the wings, renders ~24% larger, and draws OVER the hull wherever silhouettes overlap. Every seat increase made the in-game read worse-or-same while the true geometry marched backwards - the flights 10-12 loop. On top, the puppetry flailed each case up to 100 deg at full stick (chassis 25 + the shared exaggerated 75), sweeping six enlarged occluding silhouettes across the wing region. Tune the read, not the depth: - jetRestBackward 1.8 -> 1.0: the true-geometry value (nozzle leading edge 0.42 wu behind the tail - engines at the back of the body), judged in the scene view where depth is honest. - NEW thrusterAnimationScaler = 25 for the boosters' own puppetry term (replaces the shared exaggerated 75 in thrusterTurn only; the wings keep their signed-off terms). Full-stick deflection halves 100 -> 50 deg. - driftJetBackwardTotal stays 1.25, driftWingForward stays 3.5 (signed off). Verifier: seat constants tracked, plus a SWING-ENVELOPE assertion - the most-forward z any jet-cluster skin vert reaches over the full input cube (measured about the six bone pivots) must stay behind the fuselage tail plane at the shipped seat+amplitude (margin 0.244 wu; control: the retired 75-deg term at the 0.6 seat crossed by 0.39). All 13 checks green; CC gate OK. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…own turn again "less separation between boosters and fuselage ... when turning the boosters should stay more pinned to the fuselage puppeteering. it should still separate, just closer to pinned than now." Read off the code rather than assumed: Chassis resolves to the `fuse` bone - the fuselage itself - and turns Euler(p*25, y*25, r*25), while the boosters compose appendageChassisTurn = Euler(p*25, y*25, -r*25) BEFORE their own term. So on pitch and yaw the boosters' chassis component tracks the fuselage exactly, and thrusterAnimationScaler is the WHOLE of what separates them from it - one number, with 0 meaning literally pinned. thrusterAnimationScaler 25 -> 12. Measured peak swing of a booster off a perfectly-pinned one, over the input cube at the farthest jet vertex (0.444 wu from its pivot): 75 -> 0.847 wu, 25 -> 0.345, 12 -> 0.165, 0 -> 0. Relative-to- fuselage angle on pitch/yaw falls 35.2 -> 17.0 deg: half the swing, still legibly separating. The flight-13 envelope invariant strengthens with it - the full-input booster envelope now clears the fuselage tail plane by 0.375 wu (was 0.244). On ROLL a 50-degree floor survives own=0 and this dial cannot reach it: the appendage chassis term MIRRORS roll (the signed-off legacy-parity ask) while the fuselage keeps true roll, so roll separation only falls 86.6 -> 65.3. Recorded as a named constant in the verifier and in the doc so a future "still too loose when I roll" goes to the mirror rather than to another amplitude cut that cannot reach it. Verifier check 8 gains a separation assertion (shipped swing strictly smaller than what it replaced AND strictly non-zero - "still separates" is the ask) plus the measured envelope row for own 12; all 13 checks green, negative control still crosses the tail by 0.390. Compiled via the offline Roslyn harness with a negative control (2 errors, restore clean); conditional-compilation gate OK. General rule recorded: when one term is shared with the body a part is measured against, it is not separation - separation is only what the part adds on top, so identify which terms cancel before tuning any of them. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
…wn turn to 5 "the boosters need to remain much closer to the back of the fuselage." Third ask in the same direction, and flight 14's own measurement said the dial could not deliver it. The boosters took appendageChassisTurn, whose roll is MIRRORED, while the fuselage (`fuse`, driven as Chassis) keeps true roll - so on roll the two ADDED where on pitch and yaw they cancel, and a pure roll pulled a booster 50 degrees off the body with thrusterAnimationScaler at ZERO. No amplitude cut could reach that. The boosters now compose a chassis term carrying the body's TRUE roll (thrusterChassisTurn) and keep the mirror in their OWN term only, so the chassis component cancels on all three axes and the dial bounds the whole separation. thrusterAnimationScaler 12 -> 5 on top of it. Measured over the input cube: worst-case relative-to-fuselage angle 65.3 -> 8.8 deg (7.4x), peak swing off a perfectly-pinned booster 0.165 -> 0.068 wu, flight-13 envelope margin behind the tail plane 0.375 -> 0.409 wu. The signed-off flight-9 mirror is PRESERVED, and by sign rather than by argument: relative roll is -5.00/+5.00 deg at full stick where it was -62.00/+62.00 - same direction, smaller magnitude. The verifier now asserts that sign, so a construction that cancelled the own term's mirror too would fail rather than quietly lose the ask; it also asserts the floor is gone (own=0 now separates by 0.0 deg on every axis). The WINGS are untouched and keep appendageChassisTurn - nobody has reported them and flight 12 called them perfect. All 13 verifier checks green; Roslyn compile clean with a negative control (2 errors, restore clean); conditional-compilation gate OK. General rule recorded: a term shared with the body cancels on the axes where it matches it and DOUBLES on any axis where it is negated - so mirroring an inherited term mirrors the part's motion on one axis and welds a permanent offset onto it on another. Mirror what a part CONTRIBUTES, never what it INHERITS. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
Both sides improved the SAME auditor for the SAME reason from different
directions, so the resolution is a UNION rather than a pick:
* ours measures MAGNITUDE - a labelled shape that moves less than the
measured threshold (real shapes travel 2.46%..17.94% of their mesh
diagonal, fake ones 0.0000%) is reported INERT rather than counted;
* theirs handles PROCEDURAL sources (IProceduralElementMorphSource - the
Scarab re-blends baked geometry deltas) and shapes living under a hidden
legacy model root, which draw nothing.
A shape now counts only if it MOVES the hull AND is actually DRAWN. The two
inert reasons are disjoint and both apply; procedural coverage contributes
elements on top, so the fleet line counts shape keys and procedural morphs
together. Reporting keeps ours' per-shape travel verdict and theirs' distinct
"hidden legacy model - never drawn" line, so a reader can tell the two failure
modes apart.
Docs resolved the same way: CLAUDE.md keeps our Dolphin fleet line (theirs was
written before the swap and still called the Dolphin shape-less) and gains
their Scarab procedural paragraph; the verification checklist keeps BOTH new
entries (different vessels, no overlap); VESSEL_CONSTRUCTION.md takes their
Scarab row and keeps ours' Serpent row, whose dagger anchors a footnote below
it.
Verified: the merged auditor COMPILES against transcribed stubs with two
negative controls (a typo in the merged path, and a wrong member name on the
procedural interface - 8 and 4 errors, restore clean); all 13 puppetry
verifier checks green; conditional-compilation gate OK; flight-15 tuning
survives the merge intact (thrusterAnimationScaler 5, jetRestBackward 1,
driftJetBackwardTotal 1.25, driftWingForward 3.5).
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The fleet is built two incompatible ways — a skinned family (one armature, one skinned hull, element shapes) and a part-per-mesh family (a static mesh per part, no armature, therefore no morph and no puppetry). This branch moves the Dolphin from the second to the first, which is the only time a vessel has ever changed families, and fixes the instruments that were reporting on it.
The governing rule was that nothing is deleted until what it carries has been measured and, if valuable, extracted. Phases 0 and 1 exist to honour that, and two "vestiges" turned out to be load-bearing.
What landed
Phase 0 — re-survey before touching anything. Two rows of
VESSEL_CONSTRUCTION.md§4 were wrong about liveness.urchan_shapekey_with_animations.fbx(7 clips → 9 vessels) andSparrowModel4.fbx(the Missile Launch states) have zero prefab references and are load-bearing — anAnimatorControllerreferences clips by the model's guid, so a sweep measured against prefabs and scenes cannot see them. Both were on the deletion list.Phase 1 — salvage inventory. Every candidate checked for shapes, bones, takes, referrers, and geometry not present elsewhere, written down before anything moved.
RhinoModel.fbxis held: it is the only copy of the Rhino's pre-subdivision wings.Phase 2 — the Dolphin rig swap.
dolphin_shapekey_with_animations.fbxcarries a real four-element morph, the 28-bone armatureRiptideAnimationnames, and six exhaust nozzles the shipped hull scaled to 0.00095 units and never drew. The rig is the shipped hull, in the same place, so each gameplay volume is re-homed onto its bone with the world pose preserved and no collider is re-fitted.Phase 3 — Rhino and Urchin, decided explicitly. Their rigs' element shapes are labelled but inert — one vertex, moved 0.0000 units. Challenged ("are we sure there are no rhino blend shapes on any model?") and re-verified across all 93 distinct FBX blobs in project history, after discovering this clone was shallow with 2 of 363 branches fetched. One blob ever, always empty. Decision: swap for the puppetry, leave the morph honestly absent.
Phase 4 — audits measure the thing, not its label. The morph audit compared shape names and would have reported the Rhino green after its swap. It now measures travel relative to the mesh's own diagonal and reports
LABELLED BUT INERT.Phase 5 — retire the cleared vestiges. 11.7 MB, zero dangling references.
Verification history — the part worth reading
Flight 1 — every asset check passed; the ship flew broken. The rig's
.metahadexternalObjects: {}, so it imported its own DCC materials — andaccent.001, the material that should carry the DOMAIN colour, isEmissiveColor (1.0, 0.395, 0.0)atEmissiveFactor 10. Worse, submesh order is not a convention (Sparrow puts Domain at 1, the Manta family at 2, this rig's body at 1), so the default_domainMaterialSlot: 1painted the team colour over the whole body — what that field's own tooltip forbids. Fixed by remapping the three roles and moving to the order-independent_domainReplacesMaterials.Flight 2 — colours fixed, puppetry wrong. "Roll and pitch are mixed, pitch is inverted, the wings fold up when drifting", with the hypothesis that the models had different coordinate systems. Correct.
Quaternion.Euler(...)onlocalRotationturns about the parent's axes — the ship's on root-parented art, a bone's on a rig. Measured, for a commanded pitch:jetT.lturned about(−0.541, 0.840, −0.043)(mostly yaw),winghold.labout(−1.000, 0, 0)— the ship's pitch axis exactly negated, which is the inverted pitch, because that bone rests at(77.08, 0, −180). Separately, parts re-parented onto the drift handle replayed a bone-captured rest under a foreign parent: 164.50° (wings) and 152.38° (engines) off, on drift entry, with no input. That is the fold.Flight 3 — roll still wrong, engines never moved, drift re-aimed. The engines' "backward" and "default" offsets were the same vector, so half the drift clearance had never existed — and translating the constants faithfully preserved the omission. A degenerate constant pair is not a design.
Flight 4 — the drift was backwards, and that one was mine. A drift splits the ship: the fuselage and jaws aim, while the wings and engines hold Course so the hull reads as slewing across its own path. I had read the clearance statement as the whole intent and deleted the Course re-parenting as dead machinery. It wasn't dead — it looked inert only because the frames were still wrong. Restored as a frame rather than a re-parent, which is strictly better on a rig.
Flight 5 — roll is the HULL, not the wings. Reported inverted, negated, reported still swapped. A pure sign flip is one of the few edits whose effect is certain, so a symptom surviving one is evidence about where the defect is. It is
VesselTransformer.Roll()— byte-identical tobleeding-edge, shared by six vessels, and untouched by this branch. My negation was reverted. Separately the wings bank against the hull in a way the math says is impossible (their delta is provably identical to the chassis's), so that is now an authored parameter rather than a fourth guess.Flight 6 — "the best yet" on base puppetry; the drift asked to be redesigned, not restored. Three asks: jets further back at rest, appendages holding Course orientation during a drift (not frozen at entry), wings forward / jets back. The frozen-at-entry frame became a stateless one — the hull re-aimed onto Course by the shortest arc, rebuilt every frame (§4.6.2: a frame that must hold still cannot be a Transform parented under the thing that moves; the parented handle accumulated 24.6° of twist). The wings' own pitch is
Brake(throttle)— zero unless braking — so their whole pitch response was the chassis term the old hierarchy delivered free; it is composed in explicitly, bit-identical to the legacy hierarchy at one ULP (§4.6.1).Flight 7 — the offsets did not read, then read 100× too big. The positional layer ran only on the puppetry path (
Idle()writes rotations only — §4.6.3), and the offsets were absolute units whose scale neither model states. Rebasing them on the occlusion corridor's hull measure imported the very ambiguity it fled — that helper measures through the root bone, exactly where the disputed armature factor lives — and threw the parts off the ship (§4.6.4).Flight 8 — PASSED: the measured rebuild. "This is close to perfect. with only 2 minor adjustments." Instead of an eighth guess, a four-agent hunt measured the ground truth (§4.6.5). The import model is now PINNED — fitted against the colliders the swap itself preserved (residual 3e-5): Unity's import is a 0.01·mirror-X conjugation; the bones keep
lossyScale 100while their world poses land exactly on the hull. Both prior readings were half-right, and everything re-derives from it:driftWingForward 2.2= the proven bleeding-edge lunge (its own constants, mapped through the rig's rest — the reach-fraction scheme's|f|≤1clamp on a 1.96 basis could not express it at any authoring).jetRestBackward 0— the "jets pushed forward" report inverted on measurement: the old runtime dragged its engine pivots from authoredz −2.047to−1.7every frame; the rig, which retires that drag, already rests its drawn nozzles 0.40 behind the old screen.driftJetBackward 0.5is the one ask rather than parity (bleeding-edge's "backward" and "default" engine constants were the same vector — its engines never moved).DriftHandlere-parent did both; holding only orientation swept the cage sideways with the aiming hull), and the pose is written exactly in cage coordinates with the entry pose adopted and blended to station: a lerp toward a hull-independent target trails a 110°/s aim by ~53°/1.7 wu of visible drag — the standing "the wings and jets still appear to move as I am drifting and aiming".FromToRotation's antipode churn is 68°/frame at 3° off a reverse aim, and the exact write removed the lerp that used to hide it); the rest anchor is a capture-time constant (a live read applies the chassis term twice on legacy art — 27.8°);updateWhenOffscreenon the skinned renderer (rest-pose culling bounds vs a 2.2 wu lunge); the entry blend is exponential (legacy feel); and the three clearance fields are serialized into the prefab deliberately.Flight 9 — PASSED ("everything is perfect. just move the flying position of the boosters back further.") — the two flight-8 feel asks (
050b7eee2): the wings' and all six boosters' ROLL response is mirrored (roll negated everywhere it reaches those parts — own term and composed chassis term; the chassis keeps the true roll, the aileron cross-coupling and the drift cage's roll-follow are untouched), and the boosters' rest seat moves to exactly halfway to the drift station (0.25 + 0.25) with the station itself — called perfect — byte-identical at 0.50 and pinned by the verifier as a balanced pair — that pin turned out to be the mistake flight 10 uncovered; it is retired below. Deliberate deltas from bleeding-edge on record in §4.6.5: the deeper engine rest, engines moving on a drift at all, and the mirrored appendage roll. (Flight 15 revisits the boosters' half of that mirror — direction preserved, see below.)Flight 10 — "oddly this seemed to do nothing", and it was exactly right (
f38a4e777). The code was never at fault: a tuning step below the threshold of visibility is indistinguishable from a change that never applied. Flights 9 and 10 each moved the booster seat 0.125 wu — 3.6% of the 3.45 hull, a handful of pixels at chase distance — so two playtests bought no information. The cause was a constraint this branch imposed on itself: an earlier pass PINNED the drift station at 0.50 off one flight's sign-off, so every later "further back" had to be paid out of the shrinking gap to that ceiling and halved its own step.Fixed by measuring the ship instead of guessing. From the rig's own skin clusters (vessel frame): nozzles sculpt at z −2.290…−1.887, fuselage tail at −2.471 — so at any seat under 0.584 the boosters sit tucked alongside the tail, which is precisely the report. Shipped
jetRestBackward 0.375 → 0.6(clears the tail; the cluster reads as engines behind the body),driftJetBackward 0.125 → 0.25(the drift SLIDE restored to a visible 7.3%), station free to follow at 0.85. Pin the motion, not the absolute station. The verifier's station pin is retired for a 5%-of-hull visibility floor — on the seat, on the slide, and on any step away from a value a playtest could not see — so a sub-threshold tune fails the build rather than costing a flight.Flight 11 — "the wings don't travel far enough forward to get clearance while drifting, and the boosters still return to a position far to forward" (
55afcd933). Both answered geometrically. The wing lunge stops being old-game parity and becomes a clearance: the aiming hull's jaw tip sweeps a 2.835-radius sphere about the vessel origin (max |r| over the jaw skin verts), and at 2.2 the wings' nearest vertex sat at |r| 1.76 — inside it, exactly as reported (the old game interpenetrated too; parity preserved the defect). ShippeddriftWingForward 2.2 → 3.5, rounding up the bisected L* = 3.492 — the smallest lunge putting every wing vertex outside that sweep ×1.05 gape margin, so the jaws cannot reach the wings at any drift aim angle (within the ±4 clamp; binding vert the wing root at (−0.760, 0, −0.614)). The booster seat goes bold after three escalating asks:jetRestBackward 0.6 → 1.0— 0.6 cleared the tail by a sub-visible 0.017; 1.0 lands the nozzles' leading edge 0.42 wu behind the tail (29% of hull, a 0.4 step, both far above the floor).driftJetBackwardstays 0.25. The verifier gains a geometric clearance assertion in check 8 (binding vert vs jaw sweep), so an under-clearing retune fails the build.Flight 12 — "everything is perfect now (even the wings and the drifting position of the boosters) except the home position of the boosters ... they continue to rest near the origin of the fuselage" (
50d568fab). Drift is now locked in as correct. The rest seat had been called too far forward across three flights (10, 11's ask, 12) against three successively deeper, individually-measured seats (0.6, then 1.0) — each one clear of the 5%-of-hull visibility floor (flight 10's actual defect) but still not enough, which is a different failure than flight 10's. Two changes: decouple —BackwardThrusterOffsetused to readjetRestBackward + driftJetBackward, so every rest-seat bump silently moved the drift clearance too; flight 11's0.6 → 1.0bump happened to land the drift total at 1.25, and flight 12 confirms that reads as perfect, so a further rest bump risked dragging a value nobody had asked to move.driftJetBackwardTotalis now its own field, locked at 1.25, with no path back tojetRestBackward. The seat was then pushed to 1.8 on the theory that three above-floor increases surviving the same verdict meant "not enough" — flight 13 reversed that; the decoupling stands and is what made the reversal a one-field change.Flight 13 — "I had to look at the scene view to realize the boosters were far more back than I realized. it is just getting puppeteer[ed] in a way that made me perceive them as much closer" (
62677def8). The seat was applying all along, and no seat could ever have fixed the report: the Dolphin's chase camera is on-axis and level (followOffset (0, 0, −20)), so a part's station along the hull projects to almost nothing on screen — the surviving depth cues are SIZE and OCCLUSION, and both invert the intent. A booster seated 1.8 aft sits 3.7 wu nearer the lens than the wings, renders ~24% larger, and draws over the hull wherever silhouettes overlap — occlusion, the strongest monocular cue, says "in front of the wings". Every seat increase made the in-game read worse-or-same while the true geometry marched backwards, which is the loop flights 10→12 lived in. On top of that, the puppetry flailed each case up to 100° at full stick (chassis 25° composed with the shared exaggerated 75°), sweeping six enlarged, hull-occluding silhouettes across the wing region. The fix tunes the read, not the depth:jetRestBackward 1.8 → 1.0(the true-geometry value — nozzle leading edge 0.42 wu behind the tail, judged in the scene view where depth is honest) and a newthrusterAnimationScaler= 25 for the boosters' own puppetry term (full-stick deflection halves to 50°; the wings keep their signed-off terms). The verifier gains a swing-envelope assertion: the most-forward z any of the 7,500 jet-cluster skin verts reaches over the full input cube, rotated about the six measured bone pivots, must stay behind the fuselage tail plane at the shipped seat + amplitude (margin 0.244 wu; the retired 75° term at the 0.6 seat crossed by 0.39). General rule: on an on-axis chase camera, moving a part aft moves it toward the lens — answer a "too far forward" report by tuning silhouette separation and motion amplitude, never by more depth.Flight 14 — "less separation between boosters and fuselage … when turning the boosters should stay more pinned to the fuselage puppeteering. it should still separate, just closer to pinned than now" (
4b7cdbac5). Read off the code rather than assumed:Chassisresolves to thefusebone — the fuselage itself — and turnsEuler(p·25, y·25, r·25), while the boosters composeappendageChassisTurn = Euler(p·25, y·25, −r·25)before their own term. So on pitch and yaw the boosters' chassis component tracks the fuselage exactly andthrusterAnimationScaleris the whole of what separates them from it — one number, with 0 meaning literally pinned. Shipped 25 → 12: measured peak swing of a booster off a perfectly-pinned one (over the input cube, at the farthest jet vertex, 0.444 wu from its pivot) 75 → 0.847 wu, 25 → 0.345, 12 → 0.165, 0 → 0, and relative-to-fuselage angle on pitch/yaw 35.2° → 17.0° — half the swing, still legibly separating. The flight-13 envelope invariant strengthens with it (margin 0.24 → 0.38 wu behind the tail plane). On ROLL a 50° floor survivesown = 0and this dial cannot reach it: the appendage chassis term mirrors roll (the signed-off legacy-parity ask) while the fuselage keeps true roll, so roll separation only falls 86.6° → 65.3° — recorded as a named constant in the verifier so a future "still too loose when I roll" goes to the mirror, not to another amplitude cut that cannot reach it. Check 8 gains a separation assertion (shipped swing strictly smaller than what it replaced and strictly non-zero — "still separates" is the ask). General rule: when one term is shared with the body a part is measured against, it is not separation — separation is only what the part adds on top, so identify which terms cancel before tuning any of them.Flight 15 — "the boosters need to remain much closer to the back of the fuselage" (
903017d0a). Third ask in the same direction, and flight 14's own measurement said the dial could not deliver it: atthrusterAnimationScaler = 0a pure roll still separated a booster from the body by 50°, because the boosters tookappendageChassisTurn— whose roll is mirrored — while the fuselage keeps true roll, so on that axis the two added where on pitch and yaw they cancel. The boosters now compose a chassis term carrying the body's true roll (thrusterChassisTurn) and keep the mirror in their own term only, so the chassis component cancels on all three axes and the dial bounds the whole separation;thrusterAnimationScaler12 → 5 on top of that. Measured over the input cube: worst-case relative-to-fuselage angle 65.3° → 8.8° (7.4×), peak swing off a perfectly-pinned booster 0.165 → 0.068 wu, flight-13 envelope margin 0.375 → 0.409 wu. The signed-off flight-9 mirror is preserved, and by sign rather than by argument: relative roll is−5.00°/+5.00°at full stick where it was−62.00°/+62.00°— same direction, smaller magnitude — and the verifier now asserts that sign, so a construction that cancelled the own term's mirror too would fail rather than quietly lose the ask. The wings are untouched (nobody has reported them; flight 12 called them perfect). General rule: a term shared with the body cancels on the axes where it matches it and DOUBLES on any axis where it is negated — mirror what a part CONTRIBUTES, never what it INHERITS.Verification status
Audit Vessel Elemental Morphsreports the Dolphin at all four elements — Mass 12.056%, Charge 4.314%, Space 3.140%, Time 13.538% of the hull diagonal. Fleet line: 9/12 vessels ship element shapes that move the hull (shape keys; the merged auditor now counts procedural morphs alongside them — see the merge note).EngineGameObjects removed and nothing else · MeshRenderer 18→1, MeshFilter 19→2 · 11 Box + 1 Sphere colliders unchanged · 0 dangling fileIDs introduced · §3 reachability clean · Dolphin absent from the §3.4 duplicate-hull list, proving the old renderers were removed rather than merely disabled.SwapSpec; it isRigSwap). Noteunity-ci.yml'sunityjob is gated on an unsetUNITY_RUNNER_LABEL, so nothing in CI compiles C# on any PR here.Tools/Build/verify_vessel_rig_puppetry_frames.py— now 13 checks against the rig's measured bones: the two original invariants (ship-aligned part bit-identical, 0.000000000°; zero-input drift entry moves nothing, 0.00°), plus the world-unit round trip exact at chain scale 1 and 100, the cage bit-still under single-axis aim with the vessel-frame control reproducing the 2.5 wu sweep defect, the 30° roll-following orbit at radius drift 2e-16, legacy-art single chassis application (1.1e-16 vs the 27.8° live-anchor control), the antipode guard + slew limit (raw 20°/step vs shipped 6.00), the lag control (a lerped cage trails a full-rate aim by 53°; the exact write, 0), and the visibility floor — 5% of hull on the booster seat, on the drift slide, and on any step away from a value a playtest could not see, plus the measured tail-clearance anchor — the wing clearance assertion (the binding wing vert at the shipped lunge must sit outside the jaw-tip sweep ×1.05) — the swing envelope, the separation assertion, and flight 15's floor-removed + mirror-direction assertions — and, since flight 12 decoupled them, an independent tail-clearance + visibility-floor check on the drift total and the rest seat.−2.047034reproduced to six decimals and the rig's jaw centroids equal to the old nose geometry to four). The old game's on-screen part positions were reconstructed from bleeding-edge's own constants — the drift numbers are that record, not feel values.2db1a87ed.903017d0a) — awaiting its flight. All four dials are live inspector fields onDolphin.prefab(RiptideAnimation.driftWingForward/jetRestBackward/driftJetBackwardTotal/thrusterAnimationScaler), draggable in play mode.Assets/_Scripts/Controller/Animation/, so it now ships code.What a reviewer should know
A rig swap is three jobs — geometry, materials, animation — and passing the first says nothing about the other two, because both of those only fail on screen (
VESSEL_CONSTRUCTION.md§4.6).VesselRigSwappernow refuses a rig whose hull wears none of the fleet's materials; the Rhino and Urchin rigs have the same emptyexternalObjectsand would have reproduced flight 1 exactly.Guid ownership. An FBX's
.metacan carry a material remap into another FBX, sogrep -rl | head -1returns a plausible false positive — it had put two passes of Rhino jets on a placeholder hull, andCLAUDE.mditself repeated the wrong answer. Resolve withgrep -c "^guid: $g"per candidate.A magnitude is only comparable to numbers from the same tool. The in-editor audit and the offline mirror report the same four shapes 1.283× apart, identical to four digits — the denominator, not a discrepancy:
mesh.boundson a skinned mesh is the culling volume in root-bone space. Do not reconcile them (§4.5).Also fixed:
measure_vessel_prefabs.pyread the model only fromm_Mesh, which a stripped renderer does not carry — it reported the swapped Dolphin asUNRESOLVEDand had been blind to Scarab, Serpent, Sparrow and Squirrel all along.Merge note
Conflicted with #801 (skyburst missile tail) on two adjacent
CLAUDE.mdindex rows. Each side changed exactly one row and left the other at the merge base, so the resolution is disjoint and lossless: ours forVESSEL_CONSTRUCTION.md, theirs forVESSEL_TAIL_AND_JETS.md. Both verified byte-identical to the side they came from.Second merge (
a2f492665), and this one was substantive.bleeding-edgeabsorbed the Scarab pass (#821), which improved the same auditor for the same reason from a different direction — so the resolution is a union, not a pick:IProceduralElementMorphSource— the Scarab re-blends baked geometry deltas) and shapes under a hidden legacy model root, which draw nothing.A shape now counts only if it moves the hull AND is actually drawn; the two inert reasons are disjoint and both apply, and procedural coverage contributes elements on top. Reporting keeps ours' per-shape travel verdict and theirs' distinct "hidden legacy model — never drawn" line, so the two failure modes stay tellable apart. Docs resolved the same way:
CLAUDE.mdkeeps our Dolphin fleet line (theirs predates the swap and still calls the Dolphin shape-less) and gains their Scarab paragraph; the verification checklist keeps both new entries;VESSEL_CONSTRUCTION.mdtakes their Scarab row and keeps ours' Serpent row, whose dagger anchors a footnote below it.The merged auditor was compiled against transcribed stubs with two negative controls — a typo in the merged path, and a wrong member name on the procedural interface (8 and 4 errors, restore clean) — since a merge resolution of a C# file that nothing in CI compiles is exactly where a silent syntax error would land.
🤖 Generated with Claude Code
https://claude.ai/code/session_01HVDmUarLeSWQa8kRBnubqw