31 KiB
Shield Panel: GPU Fracture Simulation Cut Map
Date: 2026-09-18
Status: Imagination pass, cut map. Nothing here has landed. Repo anchors are against
codex/item-provenance HEAD 491811bd. The six incoming sources are not in the repo
yet; they are anchored as files/<name>:<line> against the operator's drop at
<scratch>/files/.
Mechanism claims marked (probe) were run, not reasoned. The probe was a throwaway
Unity 6000.3.24f1 project in scratch (<scratch>/probe), holding only the six files, a
built-in port of the shader, and one editor script driven by
-batchmode -executeMethod. Nothing was built in the Aetheria tree and no Unity session
was opened against it; Hands was concurrently editing Assets/Content,
Packages/manifest.json and the Addressables surfaces. Logs: <scratch>/probe{,2..6}.log.
The probe project is scratch and will be removed with the session; §1 records what it
proved so no one has to re-run it to trust the map.
0. Target
Operator, 2026-09-18: wire it up to real hits. The absorb capability in the
FieldShieldTest scene drives the fracture panel; no standalone click-to-hit harness is
built first.
Ends:
- A
ShieldPanelexists inFieldShieldTest, materialises on load, and fractures when the absorb capability publishes anAbsorbEvent. - The event is the only input to the simulation. Presentation converts; it decides
nothing about the rule, matching
Assets/Scripts/Gameplay/CapabilityPresenters.cs:8and the fork-L contract inAssets/Scripts/ServerShared/CapabilityEvents.cs:9-20. - The sources compile and render on the built-in render pipeline. Aetheria has no URP:
ProjectSettings/GraphicsSettings.asset:41ism_CustomRenderPipeline: {fileID: 0}andPackages/manifest.jsoncarries no render-pipeline package.
Invariants:
- One owner of the hit.
ShieldPanel.Hitis the only entry point into the simulation. The presenter is the only caller. Nothing else reaches the panel. - One event, one hit. An
AbsorbEventproduces exactly oneHit, carrying the event's own world position and direction unmodified. - Presentation owns only presentation. Damage-type-to-pattern and magnitude-to-energy are presentation mappings authored on the presenter. The absorb capability's data (the damage-type mask) stays out of scope, as ruled 2026-09-17.
- No second render pipeline. Nothing in this cut adds a package, a pipeline asset, or a URP include path.
- No Unity.Mathematics. The incoming sources use
UnityEngine.Vector2/Vector3only (verified: noUnity.Mathematicsorusing static Unityin any of the six). The presenter convertsCultMath.float3withCultMath.UnityBridge, the wayAssets/Scripts/Gameplay/FieldDriver.cs:125already does.
1. Probe record
All results from Unity 6000.3.24f1, Direct3D12, srp=builtin, batchmode with graphics.
-
The shader ports to built-in with three substitutions. Removing
"RenderPipeline"="UniversalPipeline"from the SubShader tags, replacing the URPCore.hlslinclude withUnityCG.cginc, and renaming the threeTransformWorldToHClip(calls toUnityWorldToClipPos(is sufficient:shader supported=True, messages=0(probe3 had one warning, see §1.7). No other URP symbol is used —_ScreenParams,noperspective,clip,saturateandStructuredBufferat#pragma target 4.5are all built-in surface. (probe) -
Indirect drawing works on built-in.
Graphics.RenderPrimitivesIndirectwithRenderParams, aMaterialPropertyBlockcarryingGraphicsBuffers, and an append buffer counter copied in byGraphicsBuffer.CopyCountrendered the panel:argsFill vertsPerInstance=24 instanceCount=253, 38036 of 65536 pixels lit. The cut does not have to change the drawing strategy. (probe) -
RenderParamshas noshaderPass. Full property list in 6000.3.24f1:layer renderingLayerMask rendererPriority instanceID entityId worldBounds camera motionVectorMode reflectionProbeUsage material matProps shadowCastingMode receiveShadows lightProbeUsage lightProbeProxyVolume overrideSceneCullingMask sceneCullingMask forceMeshLod meshLodSelectionBias.files/ShieldPanel.cs:410,415,420do not compile. (probe) -
A multi-pass material renders every pass from one call. A two-pass probe shader (red triangle in pass 0, green in pass 1) drawn once with
Graphics.RenderPrimitivesproducedredPixels=40992 greenPixels=40983. Pass selection is not available at this API, so the supplied "one args buffer per pass index" design cannot work as written. (probe) -
One args buffer sized to the widest pass is equivalent. Setting every args buffer to
MaxVerts * 2 * 6 = 96vertices per instance and issuing a singleRenderPrimitivesIndirectproduced pixel-identical output to the three-call version:singleCall=False litPixels=43929vssingleCall=True litPixels=43929. Each pass already culls its own surplus vertices (files/ShieldPanel.shader:238fill,:287outline,:347conduit), and additiveBlend One OnewithZWrite Offmakes pass order irrelevant. (probe) -
centroidis a reserved word in the compute path.ShieldSim.computefailed withError 18: syntax error: unexpected token 'centroid' at kernel KInit— theCellStatic.centroidmember atfiles/ShieldCommon.hlsl:18. Renaming it tocenterin the HLSL, compute and shader givescompute supported=True, messages=0. The fragment/vertex compiler tolerates it; the compute compiler does not. (probe) -
One live shader warning survives the port:
Warning 172: use of potentially uninitialized variable (BuildLine), Conduit vertex program. Warning only; the pass renders. Worth a look while touchingBuildLine, not worth a cut of its own. -
The supplied tuning never fractures anything. Hex tiling,
panelRadius 5,cellSize 0.35, 253 cells, a head-on Radial hit at the centre, 60 frames after:energy broken maxDamage maxPeakTension minTemper 3.5 (the supplied "strong" hit) 0/253 0 0.058 0.547 35 0/253 0.0012 0.579 0.505 60 0/253 0.0097 0.743 0.487 90 0/253 0 0.076 0.548 120 0/253 0 0 0.55 160 0/253 0 0.084 0.549 220 0/253 0 0 0.55 350 0/253 0 0 0.55 3500 0/253 0 0 0.55 A break needs
u > temper + tensileStrength*toughness ≈ 1.45anddamage >= 0.75. Peak tension is non-monotonic in energy and collapses above ~60: the field saturates into the compression clamp and never swings back into tension. Causes named in §5 (D9, D10). (probe) This is the reason Cut 4 exists and why it is the operator's first fork: wiring real hits to a simulation that cannot break is a pretty stress wave and nothing else.
Cut 1. Land the sources, compiling, on built-in
- Repo/branch: Aetheria
codex/item-provenancefrom491811bd. Depends on nothing. Touches no file Hands is holding (Packages/manifest.json,Assets/Content,ServerSharedasset-path fields,GameData/Aetheria.cc,ActionBarSlot.cs,UnityHelpers.cs). - First: confirm Hands' Addressables cuts have landed or are on files disjoint from
Assets/Shaders/. Nothing here goes nearAssets/Resources, which is mid-move toAssets/Content.
Location. All six files go to Assets/Shaders/Compute/ShieldField/, matching the
repo's own convention for compute-driven effects: Assets/Shaders/Compute/Lightning/
holds Lightning.compute, Lightning.shader, LightningVertex.cginc and
LightningCompute.cs together; Slime/ does the same. Colocation is also load-bearing —
#include "ShieldCommon.hlsl" resolves relative to the including file (probe). No import
settings are needed: .compute and .hlsl import with no configurable state.
Assembly. No asmdef. The files land in Assembly-CSharp with everything else in
Assets/Shaders/Compute/*.cs and Assets/Scripts/Gameplay/. The ShieldField namespace
collides with nothing (rg "class ShieldPanel\b|class Tilings\b|class Limits\b|enum TilingKind|class PolygonSoup|class VertexWelder|enum FracturePattern" over
Assets/Scripts and Assets/Shaders returns nothing). See Q7 for the testability cost.
Deletes first:
files/ShieldPanel.cs:450-487—ShieldPanelTester. It is the standalone click harness the operator ruled against, and it cannot run anyway:ProjectSettings/ProjectSettings.asset:797isactiveInputHandler: 1(new Input System only), soInput.GetMouseButtonDownandInput.mousePositionthrow at runtime. Delete it rather than port it; the absorb path replaces it.files/ShieldPanel.cs:90—argsOutline,argsConduitand theirMakeArgscalls at:172-173, plus thedrawFill/drawOutline/drawConduitsbools at:68-70and their branches at:408-422. Replaced by one args buffer and one draw (probe §1.5). Per-pass toggles come back as a fork (Q6) if the operator wants them, and then as shader uniforms, not as draw calls.
Per-file changes (against the supplied files):
ShieldCommon.hlsl:18—float2 centroid;→float2 center;. Every.centroidinShieldSim.computeandShieldPanel.shaderfollows. Required to compile (§1.6).ShieldCommon.hlsl:31— addfloat pad2;afterpad1, takingCellStaticto 16 floats.TilingBuilder.cs:27— addpublic float pad2;and initialise it at:242. Today the C# struct is 15 floats (60 bytes) whileStrideat:28claims4 * 16(64), andSetDatathrows: "One of C# data stride (60 bytes) and Buffer stride (64 bytes) should be multiple of other" (probe). Padding to 16 keeps the 16-byte-aligned stride the comment intends; the alternative,Stride = 4 * 15, also works and is smaller. Either fixes it — do not fix only one side.TilingBuilder.cs:214— rename the innerdisttonbrDist(and:221). CS0136: it shadows thedistdeclared at:225in the enclosing scope. The file does not compile as delivered (probe).ShieldPanel.shader:27— drop"RenderPipeline"="UniversalPipeline", add"IgnoreProjector"="True".ShieldPanel.shader:33—#include "UnityCG.cginc"in place of the URPCore.hlsl.ShieldPanel.shader:162,163,244—TransformWorldToHClip(→UnityWorldToClipPos(.ShieldPanel.cs:171-173— oneargsFill = MakeArgs((uint)(MaxV * 2 * 6));.ShieldPanel.cs:369-371— oneGraphicsBuffer.CopyCount(bVisible, argsFill, sizeof(uint));.ShieldPanel.cs:408-422— oneGraphics.RenderPrimitivesIndirect(rp, MeshTopology.Triangles, argsFill, 1, 0);, norp.shaderPass.ShieldPanel.cs:296-298— hoist thenew uint[] { 0u, cap }to a cached field. It allocates every frame today.ShieldPanel.cs:9— drop[ExecuteAlways](Q5). With it,Updateruns the whole simulation and three GPU dispatch groups every editor frame in every open scene view, andOnEnablerebuilds the tiling on every domain reload.- Defect fixes D3-D8 from §5, each named in the commit.
Authority map:
- Owner:
ShieldPanelowns the simulation and its GPU buffers.TilingBuilderowns adjacency, areas and the free-surface ghost weights.Tilingsowns geometry generation. - Inputs: authored fields on the component;
Hit(worldPos, worldDir, energy, pattern, radius)from outside. - Outputs: three indirect draws' worth of pixels, and GPU state readable for verification.
- Derived state:
worldBounds,groups,echoSlot,simTimeare derived from the data and the frame clock; none is authored. - Forbidden writers: nothing outside
ShieldPaneltouchesbState,bUA/bUB,bV,bEcho,bFlow,bVisible,bBudgetor the args buffer.ShieldPanelTesteris deleted rather than kept as a second hit source. - Shared paths: edit-mode and play-mode take the same
Updatebody (onceExecuteAlwaysis settled);Rebuildis the only path that allocates, and always releases first. - Deletion line:
ShieldPanelTester, the two surplus args buffers, the three draw bools, and therp.shaderPasswrites are gone before the presenter is written.
Verification:
- builds: Unity batchmode compile of the Aetheria project (
-quit -batchmode -projectPath . -logFile), zero errors.ShaderUtil.GetShaderMessagesonShieldPanel.shaderandGetComputeShaderMessagesonShieldSim.computereport zero errors (the §1.7 warning may remain). - test: none. The rule worth pinning is in Cut 2; there is nothing here a unit test would hold that the compiler does not.
- negative:
rg "UniversalPipeline|render-pipelines\.universal|TransformWorldToHClip" Assets/Shaders/Compute/ShieldFieldis empty.rg "shaderPass" Assetsis empty (verified: no legitimate use of that identifier in the tree today). - operator: none yet. Cut 3 is the first thing to look at.
Cut 2. The absorb presenter
- Repo/branch: same. Depends on Cut 1.
- Adds:
Assets/Scripts/Gameplay/ShieldPanelPresenter.cs, one component,MonoBehaviour, IAbsorbPresenter. - Deletes: nothing.
FieldDriver.Absorb(Assets/Scripts/Gameplay/FieldDriver.cs:158) stays or goes by operator ruling Q2; the binder already supports both presenters subscribing to the same stream (Assets/Scripts/Gameplay/CapabilityPresentationBinder.cs:32-36iterates everyIAbsorbPresenterin the children).
Shape:
public class ShieldPanelPresenter : MonoBehaviour, IAbsorbPresenter
{
public ShieldPanel Panel; // sibling object, not a child
public float EnergyScale = 1f; // AbsorbEvent.Magnitude -> panel energy
public float HitRadius = -1f; // -1 = the panel's own default
public FracturePattern[] PatternByDamageType; // indexed by (int)DamageType, length 6
public void Absorb(AbsorbEvent e)
{
if (Panel == null) return;
Panel.Hit(e.Position.ToUnity(), e.Direction.ToUnity(),
e.Magnitude * EnergyScale, PatternFor(e.DamageType), HitRadius);
}
}
AbsorbEvent.Position is the world hit point and Direction is the incoming ray
direction (Assets/Scripts/UI/FieldTester.cs:76 publishes hit.point and
ray.direction), which is exactly what Hit wants — files/ShieldPanel.cs:257-282
transforms both into panel space itself. No projection, no normalisation, no
reinterpretation on the presentation side: that is the "one event, one hit" invariant.
PatternFor reads the serialized table and falls back to FracturePattern.Radial when
the array is short or unset, so an unauthored component still works. The mapping itself is
Q3.
Authority map:
- Owner:
ShieldPanelPresenterowns the event-to-hit conversion, and nothing else. - Inputs: one
AbsorbEvent; three authored fields. - Outputs: one
ShieldPanel.Hitcall. - Derived state: none. The presenter holds no per-hit state, no queue, no timers — the
panel already queues hits (
files/ShieldPanel.cs:82). - Forbidden writers: the presenter never touches
CapabilityEvents; the binder owns subscription. It never readsFieldDriver. Nothing writesShieldPanelfields at runtime except its own inspector values. - Shared paths: the operator's click and any later real absorb capability reach the panel
through the same
Absorbmethod, because both go throughCapabilityEvents.Absorb. - Deletion line: none; this cut is purely additive, and buys the only wiring the operator asked for.
Verification:
- builds: project compiles.
- test: nothing in
Assets/Scripts/Testscan reachAssembly-CSharp(Tests.asmdefreferences onlyUnityEngine.TestRunner,UnityEditor.TestRunnerandAetheria.Shared.Unity, andautoReferenced: false). So the rule is pinned by a batchmode probe instead, and the probe checks it at the layer where it would break: build aCapabilityEvents, aCapabilityPresentationBinder, the presenter and a real panel; publish oneAbsorbEventat a known off-centre position; step the sim; readbStateback and assert the injected region is centred where the event said and that a second identical publish doubles it. A presenter withPanel == nullpublishes and does not throw. See Q7 for the alternative that buys NUnit coverage. - negative:
rg "ShieldPanel" Assets/Scripts --glob '!ShieldPanelPresenter.cs'finds no second caller ofHit.
Cut 3. The scene rig
- Repo/branch: same. Depends on Cuts 1-2.
- Touches:
Assets/Scenes/FieldShieldTest.unityonly.
Geometry. The existing field is Assets/Prefabs/cubesphere Variant.prefab, a unit
cubesphere scaled (10, 10, 20) at the origin, with the camera at (0, 0, -50) looking
+Z. ShieldPanel builds a flat disc in local XY with its normal on +Z. So the panel
becomes a top-level scene object at (0, 0, -10) with identity rotation and unit
scale, panelRadius = 10 — the near face of the field, same silhouette, undistorted. It
must not be parented to the field: the field's non-uniform (10,10,20) scale would ride
into _PanelToWorld and shear every shard.
Clicks already land on the field's MeshCollider and the absorb event carries that world
point, whose X/Y is what the panel consumes. The panel therefore needs no collider of its
own and no change to ClickableCollider or the click catcher.
Wiring. The presenter component goes on the field GameObject, next to
FieldDriver, ClickableCollider and CapabilityPresentationBinder, because the binder
finds presenters with GetComponentsInChildren on its own object
(CapabilityPresentationBinder.cs:32). Its Panel field points at the separate panel
object. One cross-reference, no scale contamination.
How the scene is changed. Hands cannot click, and hand-editing the YAML means
inventing fileIDs next to a prefab instance with an override list — the exact shape that
produced 1428ada3 ("Repair the Addressables map after a bad sed prefixed every line").
So: a throwaway editor script under Assets/Scripts/Editor/, run once by
-batchmode -executeMethod, that opens the scene, creates the panel object, adds and
configures ShieldPanel (assigning the shader and compute assets by
AssetDatabase.LoadAssetAtPath), adds the presenter to the field object, assigns the
reference, and EditorSceneManager.SaveScenes. The script is deleted in the same
commit; the scene diff is the artifact. Keeping it would mean a second authority over
the rig that nobody would remember to re-run. Q4 offers the alternatives.
Authority map:
- Owner: the scene owns the rig. The editor script is a one-shot actuator with no standing authority and no life after the commit.
- Inputs: the two prefab/asset GUIDs and the transform numbers above.
- Outputs: one new GameObject, one new component on the field object, one reference.
- Derived state: none.
- Forbidden writers: nothing may create a
ShieldPanelat runtime; the scene is the only source.FieldTesteris not modified — it already publishes the absorb events, and giving it knowledge of the panel would re-split the authority fork L just merged. - Shared paths: the scene load path and the editor script write the same objects, because the script is what writes the scene.
- Deletion line: the editor script, before the commit closes.
Verification:
- builds: project compiles; the scene opens with no missing-script warnings.
- test: a batchmode headless open of
FieldShieldTest.unityasserting that exactly oneShieldPanelexists, that itssimandpanelShaderare non-null, and that the field object's presenter resolves to it. - negative:
rg "ShieldPanelTester" Assetsis empty. - operator: enter play mode in
FieldShieldTest; the panel materialises as a lattice filling the field silhouette; clicking the field produces a visible stress wave travelling out from the click and reflecting off the rim; the properties panel is unchanged. Whether anything shatters is Cut 4.
Cut 4. Make a hit actually break the panel
- Repo/branch: same. Depends on Cut 3 and on ruling Q1.
- Blocked: this cut is a tuning and mechanism pass, not a wiring pass. §1.8 shows no energy fractures the panel at the supplied defaults. Do not tune by eye first; the two mechanism defects below bound what tuning can reach.
Per-file changes:
ShieldSim.compute:226-232—uis clamped to±_StressClampwhilevis not. A hard hit drivesvfar past what the clamp permits,upins at-_StressClamp, and the field never swings back into tension: that is the collapse above energy 60 in §1.8, and why 3500 behaves like 120. Clampvas well, or normalise the injection so energy maps to a bounded velocity.ShieldPanel.cs:301and:359—_Dtis set tofixedSubstepDtbefore the injection loop and not restored until afterKFracturehas been dispatched, soKFractureintegrates damage and temper erosion with a substepdtwhile running once per frame. Damage accumulates roughly six times slower than the wave it is reading (substeps = 6). Either dispatchKFractureinside the substep loop, or hand it the framedt. The first is physically honest and costs five more dispatches per frame; the second is one line. Pick one and say which in the commit — this is the "which clock owns fracture" question, not a constant.ShieldPanel.cs:34,53,56and the presenter'sEnergyScale— the tuning itself, once the two above are settled.
Verification:
- test: a batchmode probe that sweeps energy across the authored range, reads
bStateback, and asserts that the weak energy leavesbroken == 0while the strong energy breaks a bounded, non-zero fraction of cells — the same readback shape as §1.8, which is cheap and already written once. - negative: no energy in the authored range leaves
maxPeakTension == 0withminTemper == temperInit, the saturation signature. - operator: strong clicks dice the panel; repeated weak clicks eventually break through
(the temper mechanism at
ShieldSim.compute:286-298is the whole point of the design and is untested until a break is reachable).
5. Defects in the supplied code
Confirmed by compiling and running them, not by reading. D1-D4 are fatal; the cut does not exist without them.
| # | Where | Finding |
|---|---|---|
| D1 | files/TilingBuilder.cs:214 |
CS0136: dist shadows the dist at :225. The file does not compile. (probe) |
| D2 | files/ShieldPanel.cs:410,415,420 |
RenderParams.shaderPass does not exist in 6000.3.24f1. (probe) |
| D3 | files/TilingBuilder.cs:28 vs :12-27 |
Stride = 4 * 16 but the struct is 15 floats; SetData throws on the stride mismatch. (probe) |
| D4 | files/ShieldCommon.hlsl:18 |
centroid is reserved in the compute compiler; ShieldSim.compute does not compile. (probe) |
| D5 | files/ShieldPanel.cs:411-421 + probe §1.4 |
Pass selection does not exist at this API, so the three per-pass args buffers cannot work as designed, independent of D2. |
| D6 | files/ShieldSim.compute:38 |
Comment says _BreakBudget is a "single element"; the C# allocates two (ShieldPanel.cs:162) and :304 reads index 1. Not a bug — a lying comment. The earlier reading of this as an overrun is refuted. |
| D7 | files/Tilings.cs:302-333 |
VoronoiCells is O(n² log n): every site scans every other site and sorts the whole list, then Voronoi runs it lloydPasses + 1 times (4 for TilingKind.Voronoi). Confirmed by reading; not a blocker for the Hex/Penrose default, a real wait at a few thousand sites. Use a partial selection (nth_element-style) or reuse the grid ChaosGame already builds at :212. |
| D8 | files/Tilings.cs:286-288 |
Vector2.zero is both the "invalid cell" sentinel and a legal centroid, so a cell centred on the origin is silently refused relaxation. Use a nullable or a parallel validity flag. idx at :292,297 is dead. |
| D9 | files/ShieldSim.compute:226-232 |
u is clamped, v is not; hard hits pin the field in compression and no tension ever reflects. The measured collapse in §1.8. (probe) |
| D10 | files/ShieldPanel.cs:301,356,359 |
KFracture runs once per frame but integrates with the substep dt, so damage and temper erosion accrue ~6× slow. |
| D11 | files/ShieldSim.compute:326-330 |
_V[e.nbr] += from many threads is a non-atomic read-modify-write; the cascade is nondeterministic run to run. Acceptable for an effect, worth knowing before anyone tries to write a test that pins an exact shard count. |
| D12 | files/ShieldPanel.cs:296-298 |
A two-element uint[] is allocated every frame for the break budget. Cache it. |
| D13 | files/ShieldPanel.cs:9 |
[ExecuteAlways] runs the full simulation, all dispatches and all draws every editor frame, and rebuilds the tiling on every domain reload. See Q5. |
| D14 | files/ShieldPanel.cs:426-440 |
ReleaseAll does not clear pending, so hits queued before a rebuild are injected into the new tiling. Harmless today; wrong in principle. |
| D15 | files/ShieldPanel.shader Conduit pass |
Warning 172: use of potentially uninitialized variable (BuildLine). Warning only. (probe) |
| D16 | files/Tilings.cs:91-116 |
The TriHex interstitial triangles are placed by a separate polar construction rather than from the hex corners they should share, so welding is not guaranteed and the dual graph can come out with spurious free surfaces. Untested; Hex and PenroseP3 are the safe defaults until someone looks. |
Buffer lifetime, SetCounterValue and CopyCount were checked specifically and are
correct: bVisible is created Append | Structured
(files/ShieldPanel.cs:164), reset at :366 before the dispatch that appends, copied at
:369 into offset sizeof(uint) — which is instanceCount in
GraphicsBuffer.IndirectDrawArgs — and every buffer is released by ReleaseAll on both
OnDisable and the head of Rebuild. The probe's readback of
argsFill instanceCount=253 against cells=253 confirms the counter path end to end.
(probe)
Per-frame cost, measured shape: 253 cells → 1 inject dispatch per queued hit, 12 dispatches
for six wave substeps plus their echoes, 1 fracture, 1 update, 1 CopyCount, 1 buffer
upload, 1 indirect draw. At the scene's panel radius of 10 with cellSize 0.35 expect
roughly four times the cells and the same dispatch count. Nothing here is close to a
budget problem in a test scene; the only per-frame CPU waste is D12.
6. Subtraction ledger
| Cut | Removed | Added | Deps/targets |
|---|---|---|---|
| 1 | ShieldPanelTester (~38 lines), 2 args buffers, 3 draw bools, 2 draw calls, ExecuteAlways |
6 files (~5.4k lines) | none added; no package, no pipeline, no asmdef |
| 2 | — | 1 file (~35 lines) | none |
| 3 | the one-shot editor script, in the same commit | 1 scene object, 1 component instance | none |
| 4 | — | ~5 lines changed | none |
Net: strongly additive, and it should be. The capability bought is a simulation that does
not exist in the tree in any form; nothing currently in the repo can be deleted in exchange
for it, and the only existing machine it might replace — FieldDriver's absorb visual — is
the operator's call in Q2, not a subtraction this map may assume.
7. Operator questions
Most blocking first.
Q1 — Does the first landing have to fracture? §1.8 says no energy breaks a cell at the supplied defaults, and two mechanism defects (D9, D10) bound what tuning alone can reach. A: land Cuts 1-3 now, ship a visibly-materialising panel with a real stress wave and no fracture, and take Cut 4 as its own pass with its own probe. B: hold the whole thing until Cut 4 lands too, so the first thing seen is the effect as designed. Recommended: A. Wiring is the thing ruled on, it is verifiable on its own, and separating it means the tuning pass gets judged against a rig that already works instead of against itself. The cost is one operator look that ends in "nothing broke," which is the honest state.
Q2 — Does the panel replace the field shield's absorb visual or sit alongside it?
FieldDriver.Absorb (FieldDriver.cs:158) feeds the existing ellipsoid ripple, and the
binder subscribes every IAbsorbPresenter it finds, so both can run from the same event
with no code change. A: both, for comparison. B: panel only — remove IAbsorbPresenter
from FieldDriver and let its hit buffer machinery (:76,105-132,204-217) go with it.
Recommended: A for the first landing, then B as a deliberate deletion once the panel has
been looked at. Keeping both permanently would be two absorb visuals with no rule about
which owns the read; that is exactly the split this map should not create.
Q3 — Which fracture pattern does each damage type get, and where is it authored? Six
of each, no natural ordering. Proposed table: Kinetic→Radial, Optical→Lance,
Electric→Star, Thermal→Crystal, Corrosive→Fizzle, Ionizing→Spiral. Authoring: A: a
serialized FracturePattern[] on the presenter, indexed by (int)DamageType. B: a switch
in code. C: gameplay settings or item data. Recommended: A. It is presentation choice,
it belongs with the presentation, and the operator can retune it without a compile. C is
wrong today: the absorb capability owns no data yet, and CapabilityEvents.cs:17-20
explicitly parks that.
Q4 — How is the scene edited? A: a one-shot editor script run in batchmode and deleted
in the same commit. B: hand-edited scene YAML. C: the operator clicks it together in the
editor. Recommended: A. B is the failure mode that produced 1428ada3; C costs the
operator's hands for something deterministic. If the operator would rather keep the script
as a menu item, say so — that turns it into a standing second authority over the rig and
should be a deliberate choice, not a leftover.
Q5 — [ExecuteAlways]? A: drop it; the panel is inert until play mode. B: keep it, so
the lattice is visible while authoring the scene. Recommended: A, with Rebuild kept
on the context menu so a deliberate editor preview is still one click. Keeping it runs a
full GPU simulation in every open scene view forever, for authoring convenience that a
context menu already provides.
Q6 — Per-pass draw toggles? The supplied drawFill/drawOutline/drawConduits bools
die with the single-call draw (probe §1.5). A: gone. B: back as shader uniforms that zero
each pass's alpha. Recommended: A, until someone wants them; B is two minutes' work
the day that happens, and the toggles have no consumer today.
Q7 — Buy NUnit coverage with an asmdef? Tests.asmdef cannot reference
Assembly-CSharp, so no unit test can see the presenter as mapped. A: no asmdef; pin the
rule with the batchmode probe in Cut 2, which checks the whole path from published event
to GPU state. B: add Aetheria.ShieldField.asmdef over the six files plus the presenter,
referencing Aetheria.Shared.Unity, and let Tests reference it. Recommended: A. B
buys a test of the arithmetic in a one-line method while adding a compile boundary and a
reference edge to a project that has two test files total; the probe checks the thing that
would actually break, at the layer it would break.
Q8 — Which tiling is the default? The component ships PenroseP3 with
mergeRhombs. Hex is what the probe exercised and the only one whose adjacency was
verified. Voronoi costs D7 at init and TriHex carries D16. A: Penrose, as authored. B:
Hex for the first landing, Penrose once the rig is looked at. Recommended: B, then move
to whatever looks right — this is a look-at-it decision, not a correctness one.