Commit graph

2,131 commits

Author SHA1 Message Date
Meta Vens
529a3b40c4 F5: name AetherDb's Dangling-apply validation gap; F6: note Quality's gap
F5: ItemData.cs's comment claimed every tool, test and migration writes
through CultRecordRefs.Upsert. tools/AetherDb/Program.cs's Dangling command
does not -- it commits repaired records through CultCache's own batch API,
keyed explicitly to preserve identity, bypassing the validating extension.
That path can land an EquippableItemData or ConsumableItemData with an
invalid heat response or an unresolvable stat modifier reference, refused
only the next time someone reopens the file. Named as a second exception
alongside Studio's already-documented one, since routing the fix through the
generic Upsert<T> extension would need a way to change an existing record's
key-preserving write path that this pass did not have visibility into --
correcting the comment is the honest option available now.

F6: nothing invalidates StatSource.Quality (no InvalidateSource call for it
anywhere). Harmless today because EquippedItem.Lot is set once in the
constructor and never reassigned. Sec 0b designs an upgrade as a lot swap the
resolver must see; recorded at the read site so the cut that adds the repoint
finds the obligation waiting for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:42:21 +02:00
Meta Vens
0a971ec4d8 Pin Gate 1, Gate 2 and F8's mutant fix with tests and mutations
StatResolverTests: UnequipReturnsTheResolversRetainedSetToBaseline and
ConsumableExpiryReturnsTheResolversRetainedSetToBaseline drive 20 equip/
evaluate/unequip and activate/evaluate/expire cycles and assert the
resolver's own entry counts return to baseline, pinning Gate 1 at the
retained-set level rather than only via one WeakReference.
ARefitTargetReattachesTheModifierAfterReactivation drives the exact refit
scenario Gate 2 fixes and asserts the replacement target ends up boosted.

tests/mutation_tests_stats_power_cut2.py: one mutation per Forget() call site
(each expected red under deletion of that call), one for Gate 2's detach line,
and the StatOwner mutant swapped for Entity to match its removal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:42:05 +02:00
Meta Vens
5581911fb3 Gate 2: Initialize owns detach/reattach; delete dead StatOwner (F8)
StatModifier.Initialize recomputed _targets fresh every (re)activation but
never reset _applied, so after deactivate -> unequip the target -> equip a
replacement -> activate, Update saw `_executed && _applied` (both still true
from the stale attachment) and neither branch of its latch fired again --
the modifier silently stopped reaching anything. Differential evidence: the
same scenario resolved 6 pre-cut and 1 at Cut 2's HEAD.

Chose the ownership fix over the one-line flag reset: Initialize is the
entity's own re-activation hook, called exactly once per (re)activation for
every StatModifier on it, so it is the one place that already knows a
transition is happening. Detaching whatever this modifier was attached to
before recomputing _targets removes the split authority between Initialize
(which decides targets) and Update (which was silently also deciding
attachment state) instead of leaving Update to infer a transition it was
never told about.

F8: Behaviors.cs's `StatOwner` property had no production caller -- it was
added only to make one mutation test's mutant expressible. Deleted; the
mutation test now uses `Entity` (a real symbol) as its wrong-owner stand-in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:41:54 +02:00
Meta Vens
b0d3354357 Gate 1: give the resolver a Forget(owner); correct the recompute-cost comment
StatResolver's _cache, _modifiers and _generations were keyed by owner and
nothing ever removed an entry, contradicting Sec 0b's promise that a resolver
entry is "destroyed at unequip / entity teardown." A ConsumableItemEffect that
expired still had a generations entry, and an unequipped EquippedItem's entries
lived on for the rest of the process -- the same shape of leak the catalog
object had before Cut 2, one level down.

Forget(owner) drops every entry for that owner across all three dictionaries;
Entity.TryUnequip and the consumable-expiry branch in Entity.Update now call it.
Also adds GenerationOwnerCount/CacheEntryCount/ModifierEntryCount, a test-only
surface a mutation test pins the retained-set claim against.

F3: InvalidateSource's comment claimed a stat "recomputes only when a term's
source changes." Entity.cs's UpdatePerformance calls it unconditionally every
tick for Heat and Durability, regardless of whether the value moved, so the
true invariant is "at most once per tick per (item, stat)." Corrected, and
noted Quality as the one source this scheme still meaningfully defends
against a real no-op tick (see F6 in ItemData.cs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:41:42 +02:00
Meta Vens
dbb8be39d9 Cut 2: give every entity its own StatResolver, delete the catalog leak
PerformanceStat kept two Dictionary<Entity, ...> fields for attached modifiers,
grown by one key per entity that ever evaluated the stat and never trimmed
(docs/stats-and-power-cut.md §0.3). StatResolver.cs is the new sole owner of
every resolved (item, stat) value and every attached modifier, one instance
per Entity and reachable only from it. EquippedItem and ConsumableItemEffect
now read and write through it instead of through the catalog object; the two
dictionaries and their four accessors are deleted outright, not hidden.

StatModifier no longer resolves its (type name, field name) reference by
runtime reflection search on every equip: StatValidation.ResolveStatField
does it once, memoized, and StatValidation.ValidateStatModifiers checks it at
catalog load and at Upsert, naming the record instead of failing with an
opaque NullReferenceException the first time something equips it. A modifier
whose own magnitude stat is (directly or transitively, through another
modifier on the same entity) one of the stats it writes is refused at equip:
resolving it would depend on a value computed from itself one tick late.

A resolved value now caches by the generation of the sources its declared
Terms actually read, invalidated per source by EquippedItem.UpdatePerformance
(Heat, Durability) and ConsumableItemEffect.Update (ConsumableProgress), so a
stat recomputes only when a term it declared moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 19:53:59 +02:00
Meta Vens
3461eec520 Rule the panel rim held, and record what play showed
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 19:04:06 +02:00
Meta Vens
336a3672a2 Say plainly that the shield Cut 3 probe needs a graphics device
Under -batchmode -nographics every compute kernel lookup fails with
"Kernel 'KInit' not found", which reads like a broken shader and is not
one: it cost one agent four reproductions and a wrong 'pre-existing
environmental regression' claim, and another agent a contradicting pass.
SystemInfo.supportsComputeShaders still reports true against the null
device, so the guard tests the device type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:58:11 +02:00
Meta Vens
eedd309321 Record Hands' fix-up pass over Soul's Cut 1 findings
S1/S2 re-authored (Tractor Beam plus 13 curve-less designs), S5/S6 validation gaps
closed, S7's four surviving mutants pinned, S8 confirmed already correct, and the
zero-ConsumableItemData answer to Cut 0's question recorded. Also names two designs
outside the operator's named 15 (Earp, The Bat) found while enforcing the new
plateau-clamp rule -- the catalog would not open without touching them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:45:44 +02:00
Meta Vens
7204259ac3 Fix a mutation accidentally captured in the previous commit
f4d5659d landed with StatValidation's OptimalTemperature-bounds check replaced by
`if (false)`: the mutation harness was running concurrently against this same file
and a `git add`/`git commit` caught it mid-mutation. Restores the real check. Caught
because the harness's own "not yet reached" scars in doctrine are what prompted a
by-hand recheck of the just-committed diff rather than trusting the terminal output
that said 80/80 green a moment earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:45:10 +02:00
Meta Vens
f4d5659d99 Cut 1: close S5/S6's validation gaps, pin S7's surviving mutants
StatValidation.ValidateHeatResponse now refuses NaN in any of the four heat-response
fields, a zero-span range (Minimum == Maximum -- Tractor Beam's exact bug), and a
plateau whose low or high edge pokes past its bounds, matching the R-heat ruling and
the Performance() comment that already claimed this was enforced. The writable-path
hole is closed for every path this repo owns: CultRecordRefs.Upsert (AetheriaStores.cs),
the one function every tool, test and migration script writes catalog documents
through, validates before a write reaches disk instead of waiting for the next Open().
CultCache Studio's generic editor bypasses Upsert and still has no per-document
validation hook -- that hole is CultLib's, not this repo's, and stays named in the code
rather than silently assumed closed.

EquippableItemData's four heat fields get non-degenerate defaults (0/100/50/20, the
same shape HeatResponseTests already hand-authors) because dozens of existing test
fixtures across the suite build designs that never cared about heat and defaulted into
the same zero-span trap Tractor Beam shipped with; enforcing the zero-span refusal
without this would have required touching every one of those fixtures instead of the
root cause.

Four new mutations pin S7's survivors, each with its own dedicated test rather than a
coincidentally-sensitive existing one: out-of-bounds semantics (return 1f instead of 0f
outside bounds), the plateau-clamp validation's two sides, and a Quality term ignoring
its own exponent (StatsReadTheLot's Exponent = 1 everywhere could not tell "reads the
exponent" from "always uses 1"; the new test uses Exponent = 2). Performance()'s own
runtime plateau clamp is separately confirmed dead code -- the outer bounds guard makes
it unobservable at every input, verified by an exhaustive numeric sweep -- and is
recorded as such rather than forced into a fake kill.

80 tests (9 new), 17 mutation cases in tests/mutation_tests_stats_power_cut1.py, all
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 18:42:10 +02:00
Meta Vens
14a4051f75 Record Soul's Cut 1 findings and the re-authoring ruling
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 17:53:35 +02:00
Meta Vens
38aec5ddf9 Cut 1: rewrite the catalog to terms and authored R-heat
Migrated by a two-phase scratch console (neither kept): phase A dumps the
four retired exponent fields plus a curve fit (peak, optimum, near-peak
band) from the pre-Cut-1 code; phase B applies them as declared StatTerms
and authored OptimalTemperature/PlateauWidth under the new schema, in one
Commit, refusing to write anything if a single record fails to resolve.

348/348 stats and 51/51 EquippableItemData records migrated; zero
failures. AetherDb's census/factions/dangling/station-fit/hardpoint-fit/
loadout-1 are byte-identical before and after (captured with the matching
pre/post binary against the matching pre/post catalog, since neither
binary can open the other's catalog shape -- the pre-migration catalog no
longer opens under the new code, confirmed and explained in the report).

37 items fitted from an authored curve; 14 had none and got a full-range
plateau (reproducing the old "always full performance" behavior exactly).
281 of 348 stats evaluate identically at quality .5/full durability/optimum;
67 diverge upward (toward Max), the intended balance change Cut 0 flagged --
max delta ~73 units on a 750000-1750000 stat. Full table:
docs/stats-power-cut1-migration.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 17:18:24 +02:00
Meta Vens
05b5a209db Cut 1: terms replace the four fixed exponents; R-heat
PerformanceStat's four fixed fields (HeatExponentMultiplier,
DurabilityExponentMultiplier, QualityExponent, FromRole) tombstone into a
declared List<StatTerm> (source + exponent + role), walked by the one
evaluation path Cut 0 unified. A stat with no terms resolves to Max (the
same identity a stat with all-zero exponents already produced), so the
migration is exact: a zero multiplier becomes no term, not a term with
exponent 0.

R-heat (operator, docs/stats-and-power-cut.md): EquippableItemData's
HeatPerformanceCurve (a BezierCurve, plus the 100-sample scan that inferred
OptimalTemperature and its cache) is retired in favor of an authored
minimum, maximum, optimum and plateau width, with linear falloff outside
the plateau. StatValidation refuses an optimum outside its bounds or a
negative plateau width, loudly, at catalog load. The plateau's consequence
rides along for free: Wear's thermal term is pow(ThermalPerformance, ...),
and Performance() now returns exactly 1 across the plateau, so a
well-managed item takes wear only from deltaTemp there -- the
operational-lifespan lever the operator asked for.

ConsumableItemEffect's progress-through-duration substitution for "heat"
becomes its own StatSource (ConsumableProgress) rather than an unconditional
override applied regardless of what a stat declares.

Tests: LoadoutTests' four PerformanceStat-shaped fixtures rebuilt against
Terms/authored heat fields; HeatResponseTests.cs (new) pins load
validation, the plateau shape, and the three wear-lever claims (held
inside takes no thermal wear, held outside does, a fast swing across still
wears) each under its own mutation in tests/mutation_tests_stats_power_cut1.py
(new, 10 rules including a no-op control; all pass).

64 pre-Cut-1 tests still pass unmodified; 71 total with the 7 new ones.
Unity batchmode: 0 error CS. ShieldPanelCut1Verify and Cut2Verify pass;
Cut3Verify fails with an unrelated pre-existing "Kernel 'KInit' not found"
compute-shader error, reproduced against Cut 0's own landing state with no
shader/compute file in this diff -- flagged for Soul, not fixed here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 17:18:06 +02:00
Meta Vens
41514bb571 Make plateau width the operational-lifespan lever
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:28:23 +02:00
Meta Vens
9d6cc50a9f Record Cut 0's landing and the at-optimum divergence R-heat must settle
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:21:13 +02:00
Meta Vens
67217ed97c Cut 0: pin the collapsed evaluation path's agreements and disagreements
Three new LoadoutTests, each a check the cut map requires (docs/stats-and-
power-cut.md Cut 0):

- UnequippedAgreesWithEquippedAtFullHealthAndOptimalTemperature: the
  agreement the map names as the cut's own test. Real catalog curves rarely
  sample to exactly 1.0 at their reported OptimalTemperature (a probe found
  12/30 heat-bearing stats disagreeing there), so this pins the design
  contract with a controlled flat-topped fixture rather than live data --
  heat curve semantics are Cut 1's R-heat ruling, not this cut's.
- UnequippedIgnoresHeatRegardlessOfTemperature and
  ConsumableEvaluateSubstitutesProgressForHeatUnconditionally: the two named
  disagreements the cut preserves rather than resolves.

tests/mutation_tests_stats_power_cut0.py mutates each context's identity
factor (the constant a context returns instead of a term it structurally
cannot have) and asserts the right test goes red; a no-op control proves the
harness itself is transparent. All four cases pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:19:33 +02:00
Meta Vens
e9b688de51 Cut 0: collapse the three disagreeing stat evaluate bodies into one
ItemManager.Evaluate (unequipped), ConsumableItemEffect.Evaluate and
EquippedItem.Evaluate each hand-computed the same quality/durability/heat/
modifier arithmetic with small, undocumented disagreements (docs/stats-and-
power-cut.md Sec 0.2). Collapse the arithmetic into one function,
PerformanceStat.Evaluate(IStatContext), fed by three IStatContext
implementations: EquippedItem (the only one with heat, live durability and
modifiers), ConsumableItemEffect (progress substitutes for heat,
unconditionally, matching its prior behaviour exactly) and a new
UnequippedStatContext in ItemManager (no heat, no modifiers, by name).

No behaviour change beyond the two disagreements the cut map names as
defects. Each context's original NaN handling is preserved exactly
(unequipped throws with diagnostic detail; equipped/consumable fall back to
Min). Behavior.Evaluate(PerformanceStat) keeps its name and signature, so
the ~81 existing call sites are untouched.

Verified: dotnet test tests/Aetheria.Shared.Tests unchanged at 61/61 before
new tests are added; a real-catalog probe over GameData/Aetheria.cc found
the new unequipped path reproduces the old formula exactly across 3123
(item, quality, durability, stat) cases; Unity batchmode compiles with 0
error CS and the Cut 1/2/3 shield probes still pass; AetherDb census,
factions, dangling, station-fit, loadout 1 and hardpoint-fit are
byte-identical before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:19:22 +02:00
Meta Vens
c9eafe1dfa Author heat response as min, max, optimum and plateau width
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:49:22 +02:00
Meta Vens
4f01eca5d1 Rule stats first, and replace the heat curve with a range and falloffs
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:43:57 +02:00
Meta Vens
f45fefef75 Map the stats authority and the power bus
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:07:19 +02:00
Meta Vens
acb2fef930 Record Cut 3's landing, the Penrose floor and the pooling trap
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:53:00 +02:00
Meta Vens
5ae5c518a2 Add the Cut 3 batchmode probe and its mutation harness
ShieldPanelCut3Verify.cs stands in for NUnit coverage the same way Cut 1 and
Cut 2's probes do (Tests.asmdef cannot see Assembly-CSharp). It builds an
envelope/prototype/interceptor rig purely in code and steps ShieldPanel's
private Update() by reflection, since Cut 1 removed [ExecuteAlways] and
Update() otherwise never runs in edit mode. It needs GPU access, so unlike
Cut 1/2's probes it runs without -nographics.

Checks: same-frame injection (D17), the gate staying open on the following
frame (D20), reuse striking the existing panel and eroding its minimum temper,
pool exhaustion holding the cap with no allocation once full, null-guarded
Absorb, no rebuild across a pool return/reuse cycle (driven directly via
OnEnable/OnDisable reflection, since Object.Instantiate's OnEnable is not
synchronous in every context this runs in), and the D21 Penrose/Hex density
ratio at radius 1.5/3/5/10.

mutation_tests_shield_panel_cut3.py mutates each rule Cut 3 establishes back
to its pre-fix or pre-rule shape and asserts the probe goes red naming it,
plus a no-op control. Kept separate from _cut1.py/_cut2.py (each owns its own
cut's rules) and from mutation_tests.py (the item-provenance cut's headless
dotnet runner, no path to a Unity batchmode check).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:51:44 +02:00
Meta Vens
0e2fe6dc11 Add ShieldInterceptor: the interceptor presenter and panel pool (Cut 3)
R2: the shield is an interceptor. ShieldInterceptor implements IAbsorbPresenter
and converts each AbsorbEvent into a pose (intercept point projected onto the
ShieldEnvelope, normal from SurfaceNormal, size from the item's PanelRadius)
and a strike, deciding nothing else. It reuses Assets/Scripts/Prototype.cs
rather than writing a second pool.

Reuse: a second hit landing within PanelRadius*ReuseFraction of a live panel
strikes that panel (Hit only, no ResetSim) instead of spawning a new one --
this is what makes ShieldSim.compute's temper erosion observable across
multiple hits.

Pool exhaustion: when MaxLivePanels is already live, the oldest live panel
(by spawn order) is recycled in place -- reposed and re-armed for the new
intercept point -- rather than dropping the strike or growing past the
§2.3 frame budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:51:26 +02:00
Meta Vens
0ef88419ff Fix D17/D20/D21 and the pool-rebuild trap in ShieldPanel (Cut 3)
D17: KInject/KWaveStep/KFracture gate on growth, which only KUpdate writes and
only after this same frame's injection dispatch, so a panel struck the instant
it spawns swallowed its own creating hit. ResetSim now seeds bState directly
from a precomputed per-cell arm state (backdated by one growDuration) instead
of leaving KInit's zero in place, so the strike centre reads as already grown.

D20: _PanelSpawnTime was hardcoded to 0f every frame; it now tracks each
panel's own backdated arm instant, so growth keeps advancing correctly on the
frames after spawn instead of collapsing back under the gate.

Pool-rebuild trap (not map-numbered, but load-bearing for the whole pool
argument in §2.4): OnEnable unconditionally rebuilt and OnDisable
unconditionally released, so every SetActive cycle a pooled panel goes through
paid the 1.3-21ms Rebuild cost the pool exists to avoid. OnEnable now only
rebuilds when not yet built; release moves to OnDestroy. ResetSim is also
self-healing (builds if not yet built) rather than depending on OnEnable's
timing, which a batchmode probe showed is not synchronous with
Object.Instantiate in every context this runs in.

D21: Penrose's iteration count was CeilToInt(...)+1, always overshooting the
requested cellSize by a full extra phi-subdivision. Rounding to the nearest
achievable iteration count instead brings Penrose within 2-5x of Hex's cell
count at the same radius/cellSize, down from an order of magnitude.

TilingBuilder.cs gains a CellState mirror struct (matching ShieldCommon.hlsl's
CellState field-for-field) so ResetSim can write growth/temper/breakTime
directly and the Cut 3 verification probe can read them back without a new
readback path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 13:51:11 +02:00
Meta Vens
4f84470d54 Record Cut 2's landing, the measured radii and the space mismatch
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:46:04 +02:00
Meta Vens
7848a8a86e Fix ShieldManager's missing-envelope fallback regression
ShieldManager's fallback (no ShieldEnvelope yet, e.g. every ship prefab
before the operator hand-rigs it per Q5) hard-failed to a fixed
Vector3.forward direction with a per-hit Debug.LogError. That silently
changed behaviour for every ship in the game until the operator edits
Longinus.prefab/Djinni.prefab on their own schedule -- worse than either
the deleted code or a loud one-time failure.

Extract ShieldEnvelope's local-direction primitive into two static,
no-component-required entry points -- SurfaceDirection(Transform, Vector3)
and LegacyLocalSurfacePoint(Transform, Vector3) -- so both FieldDriver's and
ShieldManager's fallbacks go through one owner instead of each re-deriving
the pre-Cut-2 formula by hand. ShieldManager's fallback now reproduces the
deleted normalize(shield.transform.InverseTransformPoint(point)) formula
exactly via the shared static, logging once (a warning, not a per-hit
error). FieldDriver's fallback is rewired onto the same static rather than
its own inline copy of the maths.

Documented, not silently assumed: FieldDriver's "envelope present" branch
(envelope.ProjectToSurface, world-space) and its fallback (the legacy local-
space formula) are only numerically identical when the carrier's transform
has zero position and identity rotation -- true of the one FieldDriver
carrier in the tree today, not true in general. This is a characteristic of
Cut 2 as scoped by the map (FieldDriver.cs:125 reads envelope.ProjectToSurface
directly, per the map's own text), not something this fix introduces or
resolves; flagged so nobody assumes the two branches are interchangeable for
a future non-origin, non-identity FieldDriver carrier.

ShieldPanelCut2Verify.CheckLegacyFallbackFormulas pins both static entry
points against an independent re-derivation of the exact pre-Cut-2 formulas,
on a carrier with a non-trivial transform (nonzero position, non-identity
rotation) so the check isn't vacuous. mutation_tests_shield_panel_cut2.py
gained two cases: SurfaceDirection collapsing back to a fixed Vector3.forward
(the exact regression fixed here), and LegacyLocalSurfacePoint collapsing to
the raw, untransformed world point -- both correctly fail the probe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:45:07 +02:00
Meta Vens
a454a5a77f Collapse the two implicit shield ellipsoids into ShieldEnvelope (Cut 2)
FieldDriver derived the shield ellipsoid from its own transform.localScale;
ShieldManager derived it independently from the Shield prefab instance's
transform, doing the same normalise-and-project by hand against a different
object. Add ShieldEnvelope as the one reader of a transform's centre, radii,
surface projection, and true outward normal (derived, not authored, per R6 --
no second copy of numbers the physical MeshCollider already owns), and switch
both consumers onto it, deleting their hand-rolled projections.

Radii read the attached collider mesh's own bounds.extents per axis rather
than assuming a flat 0.5 or 1.0: Shield.prefab's Icosphere collider measures
(0.0095106, 0.01, 0.01), not a uniform round number, and the cubesphere's
collider measures exactly (1,1,1) -- confirmed by a throwaway batchmode probe
against the actual mesh assets, not guessed.

SurfaceNormal is not normalize(p): a non-uniform ellipsoid's true normal is
the gradient of the implicit surface, transformed by rotation and the INVERSE
of lossyScale (a normal is a covector). The first implementation used
TransformDirection (rotation and forward scale) and the batchmode probe's
finite-difference gradient check caught it immediately -- exactly the
plausible-looking-wrong-answer failure mode the cut map called out by name.
Both original consumers keep normalize(p)-equivalent behaviour via the new
SurfaceDirection method, since they only ever wanted a direction.

FieldDriver falls back to its pre-cut formulas (logged once) when no
ShieldEnvelope is present, since it has no guaranteed carrier yet.
ShieldManager does not: every object that carries it gets an envelope in this
cut, so a missing one logs an error and degrades to a fixed direction rather
than recomputing a second projection.

No prefab or scene file is touched here (Q5: the operator rigs prefabs and
scenes by hand); ShieldPanelCut2Verify's batchmode probe instead builds
synthetic transforms carrying Longinus's and Djinni's actual override values
plus the real Icosphere mesh, and proves ProjectToSurface lands exactly on the
ellipsoid and SurfaceNormal matches a finite-difference gradient to 1e-3.
mutation_tests_shield_panel_cut2.py mutates SurfaceNormal three ways (back to
normalize(p), back to TransformDirection, and a flattened 0.5 extents factor)
and confirms the probe goes red for each, naming the reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:33:38 +02:00
Meta Vens
808692b002 Write the negent weapon concept: ammunition is the ship's heat
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:32:19 +02:00
Meta Vens
3423dfb354 Rule negentropy gear as consumable-fed, wearing through the existing thermal path
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:26:00 +02:00
Meta Vens
a463028a65 Record Cut 1's landing and the Penrose density question
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:10:39 +02:00
Meta Vens
ff8b2ff7b6 Add the Cut 1 batchmode probe and its mutation harness
Companion to a935b965: the actual verification tooling for the shield-panel
Cut 1 landing, split into its own commit since it's infrastructure rather
than the ported sources.

.gitignore: ignore tests/_scratch_* (the mutation harness's throwaway log
directory) alongside the existing scratch-* patterns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:08:24 +02:00
Meta Vens
a935b965c9 Land the six shield-panel sources, ported to built-in and compiling
Cut 1 of docs/shield-panel-cut.md: land ShieldPanel.cs, ShieldPanel.shader,
ShieldSim.compute, ShieldCommon.hlsl, TilingBuilder.cs and Tilings.cs under
Assets/Shaders/Compute/ShieldField/, alongside Lightning/ and Slime/.

Port (URP -> built-in, three substitutions per the map's §2.1 probe):
- drop the UniversalPipeline SubShader tag, add IgnoreProjector
- swap the URP Core.hlsl include for UnityCG.cginc
- TransformWorldToHClip -> UnityWorldToClipPos (3 call sites)

Drawing collapse (D2/D5): RenderParams has no shaderPass member in
6000.3.24f1, and a multi-pass material draws every SubShader pass from one
indirect call regardless, so the three per-pass args buffers and draw calls
collapse to one args buffer sized to the widest pass (Outline) and one
Graphics.RenderPrimitivesIndirect call. Deleted: argsOutline, argsConduit,
drawFill/drawOutline/drawConduits and their branches.

Also deleted: ShieldPanelTester (the click harness the operator ruled
against; it also can't run — activeInputHandler is New Input System only)
and [ExecuteAlways] (incompatible with a pool per D13; Rebuild stays on the
context menu).

Defects fixed:
- D1: TilingBuilder.cs's inner `dist` shadowed the outer one (CS0136);
  renamed to nbrDist.
- D3: CellStatic was 15 floats against a declared Stride of 16; added pad2
  on both the HLSL and C# sides.
- D4: `centroid` is reserved in the compute compiler; renamed the
  ShieldCommon.hlsl field to `center` (kept the C# field name as-is; it's
  not reserved there and the two sides don't need matching names, only
  matching layout).
- D6: fixed the comment on _BreakBudget — it's two elements (count + cap),
  not the "single element" the old comment claimed.
- D12: the two-element uint[] Update() allocated every frame is now a
  cached field, cheaper still under a pool of many live panels.
- D18 (R5 — operator ruling, not deferred to a follow-up): Penrose ignored
  panelRadius entirely — a fixed authored iteration count meant the tiling
  was just uniformly rescaled, not resampled, so cell count never grew with
  the panel. Every other tiling here uses cellSize to hold local cell size
  constant while radius extends coverage; Penrose now derives its iteration
  count the same way (each substitution divides edge length by phi, solved
  for the count that brings a unit edge to ~cellSize once scaled to
  radius). This retires the now-meaningless `penroseIterations` field and
  the `subdivisions` parameter it threaded through Tilings.Generate.
- Also removed a dead `idx` counter in Tilings.Voronoi (D8's mention of
  dead code; the Vector2.zero sentinel ambiguity itself is unfixed, same as
  D7/D19 — Voronoi and TriHex stay recorded defects, not fixed, per R5).

Not fixed here, per the map: D7 (Voronoi is unusably slow) and D19 (TriHex
has zero dual edges) are recorded, not fixed. D9/D10 (fracture tuning) are
Cut 5. D17 (growth gate blocks the spawning hit) and D20 (_PanelSpawnTime
hardcoded to 0) are Cut 3.

Repo-specific fix not in the map: Assets/Scripts/UI/ContextMenu.cs defines
a global `ContextMenu` MonoBehaviour that shadows UnityEngine.ContextMenu,
so `[ContextMenu("Rebuild")]` doesn't compile here (CS0616) even though it
did in the map's throwaway probe project. Used the fully-qualified
`[UnityEngine.ContextMenu(...)]` instead.

Adds Assets/Scripts/Editor/ShieldPanelCut1Verify.cs, a batchmode probe (no
NUnit path exists per the map's Q7 — Tests.asmdef can't see
Assembly-CSharp): asserts zero shader/compute compile errors and that
Penrose cell count actually scales with panelRadius (D18). Also adds
tests/mutation_tests_shield_panel_cut1.py, which mutates the D18 fix, the
D4 fix, and the shaderPass deletion in turn and asserts the probe goes red
each time, restoring the source after each case (own file per the
convention mutation_tests_addressables_cut.py established, so as not to
touch item-provenance's or the addressables cut's harnesses).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:08:04 +02:00
Meta Vens
e3c9eca2fd Record the pre-impact commit window in fire control's scope
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:05:29 +02:00
Meta Vens
545f81d58e Rule the commit window as the source of anticipation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:05:02 +02:00
Meta Vens
af67ee3466 Contract shield and pickup presentations, anticipation included
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 06:01:30 +02:00
Meta Vens
f3a02712e1 Rule the scene rig as operator-driven, with a batchmode check
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:50:03 +02:00
Meta Vens
70bcdf7617 Rule Penrose sizing into Cut 1 and the envelope as derived
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:43:34 +02:00
Meta Vens
1ff21f176f Remap the shield as an interceptor spawning panels on the hull envelope
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:39:25 +02:00
Meta Vens
482229d1e3 Record where the Addressables cuts landed, misattributed moves included
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:08:53 +02:00
Meta Vens
2d348d64ca Un-clobber the item-provenance mutation harness; own file for this cut
My prior commit overwrote tests/mutation_tests.py wholesale instead of
noticing it already belongs to docs/item-provenance-cut.md (this
branch's primary cut, run by a concurrent session sharing this working
tree). Restored it byte-for-byte from 1fdf8151 and moved the
addressables-cut mutation coverage to its own
tests/mutation_tests_addressables_cut.py, since that harness's
runner only knows how to drive `dotnet test` against a source-anchor
mutation and has no path to a Unity batchmode check, which
EngineAssetCheck requires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:07:23 +02:00
Meta Vens
67cbf34b25 Switch engine asset references from Resources paths to Addressables GUIDs
Cut 2 of docs/addressables-cut.md. The asset move (git mv
Resources/{Prefabs,Schematics,Sprites/Icons} -> Content/...) and the
UnityHelpers.cs deletion already landed in 1fdf8151, picked up by a
concurrent session sharing this working tree; this commit is the rest
of Cut 2 landing on top of that shared state, atomically:

- Bump CultLib to cultlib-unity-v1.0.60 / caching-unity-v1.4.0 (both
  45c2f40) in Packages/manifest.json, packages-lock.json and
  Directory.Build.props, together with the 8 ServerShared
  [CultInspectorAssetPath] usages becoming [CultInspectorAssetGuid] -
  the .NET and Unity sides move together or nothing compiles.
- Assets/Scripts/EngineAssets.cs replaces UnityHelpers: loads an
  Addressables key (bare GUID or guid[subAsset]) synchronously,
  loading the GameObject and taking the component for the four sites
  that load a component. ActionBarSlot, EntityInstance, ShipInstance
  and ZoneRenderer call it instead of Resources.Load/UnityHelpers; the
  inline "Assets/Resources/" prefix strips are gone with them.
  CultCacheDrawers' schematic preview goes through
  AssetDatabase.GUIDToAssetPath first.
- Assets/Scripts/Editor/EngineAssetCheck.cs validates every stored
  [CultInspectorAssetGuid] value: 32 lowercase hex, resolves to a file,
  is addressable, and satisfies its member's consumer-type contract
  (Texture2D for icons/logos/schematics, a GameObject with the right
  component for prefabs) - table-driven, so an attributed member with
  no row fails loudly instead of being skipped.
- GameData/Aetheria.cc: the scratch migration (Aetheria.Shared + the
  pinned CultLib revision, a throwaway console app, never landed)
  walked all 138 attributed field slots across 180 documents,
  rewriting the 30 non-empty Resources paths to their .meta GUIDs -
  108 stayed empty, 0 unresolved, matching the map's census exactly.

Verification: dotnet test tests/Aetheria.Shared.Tests 61/61 green
against CultLib 45c2f40; Unity 6000.3.24f1 batchmode compiles clean;
EngineAssetCheck.Run passes all 30 stored references; the map's
negative greps are clean; tests/mutation_tests.py's 5 mutations (wrong
type, non-addressable, random guid, an old path, and an untabled
attributed member) are all caught and named, the control case and the
post-revert recompile stay clean, and the catalog/source are restored
byte-identical afterward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 05:04:50 +02:00
Meta Vens
1fdf815111 Map the shield panel onto the built-in pipeline and the absorb event
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 04:47:36 +02:00
Meta Vens
491811bd3d Add Addressables as a direct dependency and settings, Content group entry
Cut 1 of docs/addressables-cut.md: com.unity.addressables becomes a
direct manifest dependency (was transitive via com.unity.localization),
Addressables settings are created with BuildWithPlayer forced instead of
left as a per-user preference, and Assets/Content is added to the
Default Local Group as the one folder entry Cut 2 will populate (Q1
ruling A). No code loads through Addressables yet and the folder is
empty, so this lands harmlessly on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 04:31:28 +02:00
Meta Vens
092b745b78 Rule the Aetheria site as the download page owner
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 22:21:19 +02:00
Meta Vens
2c2b92579f Map build delivery, and narrow the Idunn invariant to services
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 22:00:45 +02:00
Meta Vens
6dc1860770 Target a public build download served from Yggdrasil
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:42:17 +02:00
Meta Vens
1428ada3cd Repair the Addressables map after a bad sed prefixed every line
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:33:50 +02:00
Meta Vens
78fd617cdd Restore the escaped dot in the corrected grep
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:33:24 +02:00
Meta Vens
3744ee16da Fix a control character in the corrected grep
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:33:06 +02:00
Meta Vens
45406df974 Rule sub-asset references as Addressables guid[name] keys
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 19:32:53 +02:00