- Validate: a hardpoint's anchor role is fixed by its type (thruster-emitter, radiator-mesh, else articulation); ShipModPlan's
duplicate role check is deleted.
- Loadouts.Commit refuses a preset naming a mod design (mod-hull:/mod-ship:), so the shipped catalog never references a mod.
- Compose quarantines each package: a bad package is excluded and named, a collision excludes every package involved; the
game boots on the rest (or the shipped catalog) and the main menu lists the exclusions.
- RequireDesigns refuses a save with a null item or lot instead of throwing NullReferenceException.
- Tests: per structural role, muzzle id ties, every slot/zone/lot in RequireDesigns, listing order, exclusion cases.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Unity generated them on import; without them the two scripts' GUIDs are
unpinned and differ per checkout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Boot opens the derived catalog when GameData/Mods holds packages
(ShipModCatalog.ResolveCatalog), preloads each hull's GLB into an assembled
inactive prototype (ShipModShips), and LoadEntity clones it when the hull names
a Visual. ShipModPlan decides which anchor fills which ShipInstance slot; a
ShipModTemplate prefab carries the shared effects. Continue refuses a run that
names a design the catalog no longer holds (RunSave.RequireDesigns).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A mod ship file now holds a HullData at mod-hull:<id> and the ShipAuthoring
visual package at mod-ship:<id>. HullData.Visual (key 32) is the typed ref
between them; ShipAuthoring.Hull (key 1) is retired.
- One joint validator over (hull, visual) is the only semantic check; Load
owns the file structure (one hull, one visual, deterministic keys).
- Compose copies the mod store's records verbatim, so the derived catalog
holds one hull per mod ship.
- ship_cc.py edits the hull record's slots directly; the body[HULL_SLOT]
indirection is gone.
- The package fixture is generated from ShipAuthoringTests.Fixture() (glTF
scene included) and exported when AETHERIA_SHIP_FIXTURE_DIR is set.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- ConsoleView appended the control characters text input delivers (\b, \r),
so Backspace inserted a glyph and Enter's \r got every command refused
since d503f076 made the parser refuse instead of strip. Control characters
are no longer text; Backspace deletes like Delete.
- give reports an unknown name or a full cargo bay. A ship hull is refused
unless docked, and when docked is commissioned through
ActionGameManager.CommissionShip, the primitive TradeMenu.Buy now uses: a
bare player ship moored at the docked entity, no docking bay. Non-ship
hulls are refused.
- Record play smoke results for steps 3-6 and the docking bay finding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regenerate AetheriaInput.cs from the committed Aetheria.inputactions so the
wrapper carries Cycle Target Item, and bind it through Input.Player like the
other target actions. The by-name FindAction lookup was a stopgap from when
the generator could not be run outside the editor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After its fade a round kept integrating Velocity through the BindingLifetime wait, then a Hit (which carries
no burst point) played HitEffect at the drifted position. The wait now holds still; a hit without a point
plays the effect at the round's target position, and is skipped when the round never knew one. OnEnable resets
BindingLifetime so a pooled split child, which is never bound, does not inherit the last round's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A burst whose every round is refused fires nothing and leaves no cooldown: the cooldown moves from
Trigger to the first round Execute does not refuse, and CanFire holds a triggered burst that has not
yet fired. ChargedWeapon.Activate asks FireControl before it starts charging, so a refused weapon pays
no charge heat or audio. InReach is deleted: Designated carries the one range test and the lock gate,
and Combat reads it. GuidedProjectile answers Resolve while alive and keeps its binding through the
shot's flight time, so a faded round still shows the simulation's detonation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling 2026-09-30: a no-lock round of any fuse stops at the first hull
on its aim line and detonates there (bomb fishing still bursts at max range on
a clear line). CommitBurst no longer special-cases Contact.
Soul's findings on 14f66634..7e620ff9:
- Arming (HIGH): a contact or delayed blast on a hull nearer the muzzle than the
blast radius detonated at the hull. Armed is now the one owner of the arming
rule; Solve plans its burst point through it and ApplyBlastHit pushes the hull
point through it.
- A refused beam never starts (ConstantWeapon.Activate asks Refuses), so
OnStartFiring no longer flashes visuals and audio for a beam that fires nothing.
- Step removes a shot from the queue before Apply runs damage observers, so a
throwing observer cannot make the blast repeat every tick.
- AgentFires consults Solve for a fused weapon: a refused weapon is never fired
and Combat's group pick no longer counts its damage.
- SingleAmmoBurst pays at its first unrefused round in Execute, judged with the
Range that round flies with, instead of at the trigger on the previous tick's.
- Inspect reads Designated from Solve (FireSolution.Designated) instead of
recomputing visible, in-range and locked; FireControlDiagnostic drops those
three fields and the debug HUD shows the one. Combat's group range test is
FireControl.InReach, the range half of Designated (a LockWeapon must be
activated to acquire its lock, so Combat cannot ask Designated itself).
Tests pin each rule, plus the two surviving gaps: a pushed-out targeted round's
arrival at the arming distance, and the AgentMinHitProbability boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The overshoot branch of GuidedProjectile.Update (out of range, or flown past
its target) instantiated HitEffect whatever the simulation had decided. A round
the simulation never detonated could therefore explode on screen. The branch now
fades the round out; Resolve stays the one place a HitEffect is shown for a
detonation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unity-only, compile-checked by reading.
* F5: GuidedProjectile.Binding is one owner: a new binding disposes the one it
replaces, Kill disposes it, and OnDestroy does too, so a projectile torn down
with its scene cannot leave an observer on the zone's subjects to touch a
destroyed transform.
* F6: Resolve takes whether the simulation detonated. A miss (a contact or
delayed round that missed, a target that left) fades out with no explosion.
* F7: BurstAt resets the previous target position and target velocity, so the
retarget jump does not feed the guidance a bogus target velocity.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operator rulings 2026-09-30.
* Combat and TurretController ask FireControl.AgentFires for each weapon. A
fused weapon fires whenever its target is designated (Designated, the one
visible / in-range / locked predicate PFire already used), whatever the arc;
an agent never fires one at nothing. A direct-hit weapon keeps the
AgentMinHitProbability threshold. The group's first weapon no longer speaks
for the group, and the turret's own range test is gone.
* FireControl.Solve is the one decision about what a round does, extracted
from Fire: Direct, Burst at a reach, or Refused. Fire, Inspect and the
weapons' refusal check all read it. Inspect gains Outcome and BurstReach and
the debug HUD prints them for fused weapons.
* A refused round costs nothing. InstantWeapon asks before its ammo and energy
spend (at the trigger and at each round), ConstantWeapon folds the refusal
into the gate its power request and Execute already share, so a refused beam
draws no power, wear, heat or visibility and stops.
* The arming distance is BlastRadius, measured from the shooter's position and
blind to hull size. HullAhead's exit, ArmingDistance and
PendingShot.ArmingDistance are deleted; HullAhead survives as HullEntry for
the contact fuse. A round fired at a valid target inside the radius is pushed
out along its line but keeps its target, so a departed target cancels it.
* Step settles a shot's queue state before publishing about it, so a throwing
presentation observer costs its own notification and cannot re-commit or
re-resolve the shot on every later tick.
Tests: FireControlPerWeaponTests (new; includes a fused-beam fixture), the
arming tests in FireControlCut124Tests rewritten to the literal distance,
the hull-inclusive probe premises deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- GuidedProjectileManager passed target.transform to OnFireGuided for a
no-target fused round: a NullReferenceException inside InstantWeapon.Execute
after the shot was queued, which skipped audio, wear, heat and visibility
and unwound Zone.Update. The tuple's transform is null-safe now, and the
trackmissile console command falls back to the missile.
- Presentation follows the commit rather than predicting it. The manager binds
each guided round to Zone.ShotCommitted (retarget to the burst point the
commit names, so a Contact round bursts at its hull and a Range-clamped
burst is not overshot) and ShotResolved (burst there, then). The binding is
disposed when the projectile dies, so a pooled round does not accumulate them.
- Beams and barrel-fired projectiles stay with F5.
Unity-only; compile-checked by reading.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EntityInstance handed effect managers the shooter's selected target whatever
FireControl decided. A fused round whose target was invalid or out of arc
flies at none, and GuidedProjectileManager dropped a launcher shot with no
target entirely, so a real burst had no missile.
- Zone.TryGetShot: a presentation reads the shot the simulation committed
(its target, its burst point) by the ShotId OnFire hands it.
- EntityInstance passes the shot's own target; a launcher round with none
flies to the shot's burst point.
Unity-only; compile-checked by reading.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operator rulings 2026-09-30 after Soul's pass on 8c002637:
- A no-lock round flies along Entity.LookDirection restricted by the mount's
arc (FireControl.AimDirection: inside InArc's cone as is, outside it turned
to the cone's edge; no aim falls back to the mount). The simulation had no
aim clamp before; the old code used the hull-fixed mount direction.
- An invalid target (out of range, out of arc, not visible, not locked) counts
as no lock for a fused round. PFire's gate is the one decider and now also
reports `bears`; Fire reads it. The round carries no target afterwards.
- A no-lock Contact round bursts at the first hull its aim line crosses
before max range, read through Lane's own cell admission (CellsOnLine,
split out of Lane). Proximity and Delayed keep the max-range burst. Such a
round commits at Fire, since it has no target facing to read late, and
arrives when it has flown to the burst point. The seam for mining chunks is
FireControl.ContactDistance.
- A round outlives its shooter: Step no longer cancels a shot whose source
left the zone. Resolution reads the zone and the target; the shooter is an
identity a shot carries. PendingShot.FireOrigin freezes where it left from.
- ShotOutcome.Result (Miss, Hit, Burst) replaces the stored Hit bool, which
is now derived. A burst without a target is no longer reported as a miss,
and the debug HUD labels it BURST.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FireControl.Apply now switches on the frozen shot.Fuse: a null fuse keeps
12.3's lane path (ApplyDirectHit), a proximity fuse detonates at arrival with
no roll, and a contact or delayed fuse detonates only on a committed hit
(ApplyBlastHit), locating P through Lane and the new Reach owner (moved
verbatim from Apply's own inline penetration clip; the delayed fuse point
reads the identical rule). Step has no damage branch left.
Detonate (FireControl.cs) is the one owner of blast damage: candidates by
centre-plus-half-diagonal, exact circle/cell overlap shares computed once,
every blast's shield decided live per Q12-9 = A (including the host of a
contact/delayed blast -- Commit now gates its own shield decision on
shot.Fuse == null), armour absorbed per cell, and each item's covered cells
pooled into one ItemAbsorb call. Entity.Absorb and FireControl.Splash are
deleted; Entity gains ToSchematicPoint/ToWorldPoint for the point conversion
Detonate and the delayed fuse point need. Mine.Explode now calls Detonate.
Existing Splash/Absorb-era tests across Cut4/5/6/11/123 are rewritten or
retargeted to the new public owners; behavioural coverage for the blast
model itself lands separately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deletes Entity.DamageSchematic (the even split over the whole hit shape)
and Entity.ApplyHit (the 0.5-step penetration march), replacing both with
Entity.Absorb (armour then item, per cell) and Entity.DamageHull, moved
verbatim from DamageSchematic's own body. FireControl.Apply now builds
2n+1 parallel lanes from the committed Bearing/Lateral (Q12-2 = A),
shares damage evenly over the lanes that meet metal, walks each lane in
order via the already-verified FireControl.Lane, absorbing cell by cell
up to the penetration depth, and sends every lane's own remainder to the
hull where it ends (Q12-3 = A). Splash is rewritten onto Absorb/DamageHull
(arithmetically identical to before) since DamageSchematic is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/mutation_tests_fire_control_cut6.sh, modelled on Cut 5's harness: a no-op
control, four mutations (6.1's shared-stream generator and dropped-ShotId seed;
6.2's double-application and unconditional-splash), byte-exact I/O with
sha256-verified restore after every mutation, and the two negative greps the
Cut 6 spec names. All five mutations behave as declared and the tree-clean
verdict is PASS.
Fixes two of Commit's and Projectile's own explanatory comments that quoted the
literal strings the negative greps check for (ItemManager.Random, Airburst),
which the first harness run caught as false failures -- reworded, no behavior
change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
6.1 (Soul finding 6): Commit builds a local CultMath.Random from (Zone.CombatSeed,
shot.ShotId) instead of reading and writing the shared ItemManager.Random stream.
Zone exposes CombatSeed (the same expression that already seeds its own _random),
so a shot's outcome is a pure function of zone identity and shot id -- immune to
whatever else drew from the shared stream in between.
6.2 (Soul finding 5): airburst is wired into the simulation instead of back into
Projectile.cs. PendingShot gains BurstPosition/BurstRadius, frozen at Fire time
from PredictedIntercept and the weapon's authored WeaponItemData.AirburstRange
when WeaponModifiers carries Airburst. FireControl.Step resolves such a shot via
Splash instead of Apply, never both. Projectile.cs/ProjectileManager.cs drop the
dead AirburstDistance/AirburstRange fields and write-back.
Tests: FireControlCut6Tests.cs pins both rules (208 baseline + 5 new, all green).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul's pass over the whole campaign confirmed eleven findings. Seven were code
defects of one shape -- a rule implemented on one path and left off the path
beside it -- and this is all seven.
5.1 Tracking falls back to GameplaySettings.UnaidedTracking (new, 10) instead of
zero, and Commit drops the zero branch that turned a Tracking-less shooter
into a hard wall. Unaided fire was meant to be really, really bad; it was
334 shots and no hits against a target drifting at 5 u/s.
5.2 A shot the shield cannot absorb now breaks it. The decision is frozen into
ShotOutcome.ShieldBroken at commit and performed in Apply, so a shield that
recharges mid-flight cannot retroactively survive. Splash already did this;
the discrete roll never did, which made the whole shield-break mechanic
unreachable for every weapon in the catalog.
5.3 ConstantWeapon gates on ArcAllowsFire alongside StanceAllowsFire. A
side-mounted beam was firing forward.
5.4 The point-blank bearing case moves out of Weapon.ArcAllowsFire and into
InArc, which used to normalize the zero vector into NaN. The trigger said
fire and the probability said impossible.
5.5 A destroyed item drops out of IsRevealed's ranking, so Precision stops
concentrating hits onto a dead subsystem's footprint.
5.6 Zone subscribes Entity.Death and removes the corpse itself. That removal
lived only in Unity's loot-drop subscription, so headless nothing ever left
the zone, shots landed on corpses and no fight could end.
5.7 PendingShot drops PredictedIntercept and FlightTime, written and read
nowhere.
208 tests pass (197 plus eleven new).
Known defect, recorded rather than hidden: the committed mutation harness is
unreliable. Across two runs it failed to restore FireControl.cs and Weapon.cs,
leaving live mutations in the working tree -- including its own 5.1 mutation,
which is how a green suite briefly reported two failures. Its verdicts are
therefore evidence only where re-run from a verified-clean tree. Two anchors
(Zone.cs's ObserveAdd line, Weapon.cs's InArc return) fail to match at all, and
5.3's ConstantWeaponObeysArc survives its own mutation, so that rule is written
but not pinned. Cut 5b owns the repair; nothing here is trusted on the harness's
word, and the suite result above was taken by hand from a hash-verified tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deletes the last live Physics.* hit queries and every remaining SendHit/
SendSplash call in Assets/Scripts/Gameplay/Weapons: ConstantLaser's
RaycastAll, ConstantLightning's SphereCastAll, ConstantParticleWeapon's
trigger-collider plumbing, HitscanEffect's RaycastAll, and Mine's blast
OverlapSphere. Each becomes presentation only, the same convention Cut 3
gave Projectile/GuidedProjectile/Laser/Lightning: ConstantLaser/
ConstantLightning/HitscanEffect draw to TargetTransform (or out to Range with
no target); ConstantParticleWeapon keeps only its visual emission.
ConstantWeapon.Execute (previous commit) already rolled the damage before any
of these ever draw.
Mine keeps its proximity-arming OverlapSphere (Q7: contact detection, not hit
detection) but Explode() now calls FireControl.Splash instead of walking
colliders and calling SendSplash itself.
HullCollider.Hit/Splash/SendHit/SendSplash and their event-arg classes are
deleted outright -- their last callers are gone. HullCollider is now purely a
physics surface for ship-on-ship collision impulse; no Physics.* query
decides damage anywhere in Assets/Scripts/Gameplay/Weapons except Mine's own
named exception.
Every manager (ConstantLaserManager, ConstantLightningManager,
ConstantParticleWeaponManager, HitscanManager) is updated to match: no more
Damage/Penetration/Spread/DamageType plumbing into a presentation component
that no longer decides anything with it, and HitscanManager now passes
ShotId and TargetTransform the way LaserManager already does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 3 landed 39 compile errors in GuidedProjectile: it uses ToCultMath
throughout and lost the using directive. Caught by batchmode on merge, not
by the pass that wrote it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A new "Cycle Target Item" action (hand-authored into Aetheria.inputactions;
no Unity editor here to regenerate AetheriaInput.cs, so ActionGameManager
looks it up by name through Input.asset.FindAction instead of a typed
accessor) cycles the local player's aim point among the current target's
revealed items via TrySelectTargetItem. SchematicDisplay's enemy view now
filters to what FireControl.IsRevealed actually resolves for the viewer,
and marks whichever row is the current ResolvedTargetItem through a new
SchematicListElement.SelectedIndicator (unwired in any prefab yet -- a
no-op until an editor pass assigns one).
Presentation only; decides nothing. Unverified here -- Unity batchmode
is unavailable in this environment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Entity.HardpointTransforms was written only by Unity (EntityInstance.cs),
so the simulation's notion of where a gun points was a readback from a
renderer that never runs headless -- Combat.cs threw KeyNotFoundException
on the first combat tick outside Unity. Deletes the field, its writer, and
the readback branch in Behavior.Direction, leaving the item-rotation path
(the same convention reaction thrusters use, R6) as the only source of
mount direction.
Adds FireControl (ServerShared), a static class owning whether a weapon
bears on a point: MountDirection (planar, from ItemRotation), ArcFor (the
per-hardpoint HardpointData.FiringArc override, falling back to the new
GameplaySettings.FiringArc default of 120), and InArc (the planar dot-cone
test, R7 -- target height never enters it, and an arc of 360+ passes
unconditionally so a turret hardpoint needs no new type, just a number).
Combat.cs and TurretController.cs now gate AI fire through InArc instead
of the old hardcoded 0.99 dot-product test against a live Unity transform.
Settings.cs drops AgentFiringMinDot, which was declared and read by
nothing.
docs/fire-control-cut.md, Cut 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
F4: a hit that breaks the shield usually also cooks it (TakeHit's own
AddHeat), which can knock the item thermally offline in the same tick. The
reserve's recharge and its Broken-clearing check lived inside Execute, which
Entity.Update only runs while Item.Active is true -- so a shield broken by a
hit that also shuts it down could never come back, no matter how long
RestoreDuration allows for. That logic now lives in Shield.Update
(IAlwaysUpdatedBehavior), which Entity.Update calls every tick unconditionally
(Cooldown.cs already relies on the same interface for the same reason).
F5: CanTakeHit broke the shield and drained the reserve as a side effect of
the query itself, on the premise that every caller queries it exactly once
per hit. That premise was false: RaycastAll/SphereCastAll/OverlapSphere hand
every caller under Assets/Scripts/Gameplay/Weapons both the shield collider
and the hull collider for one shot, so the same shield's CanTakeHit was
queried twice per hit. CanTakeHit is now a pure read (Broken + the reserve's
own CanSpend); the mutation moved to a new Shield.Break(), idempotent, called
by each weapon file exactly where it already decided to route a hit past the
shield instead of absorbing it.
Pinned in tests/Aetheria.Shared.Tests/ShieldReserveTests.cs:
RestorationProceedsWhileTheItemIsInactive (F4),
CanTakeHitNeverMutatesRegardlessOfHowManyTimesItIsCalled and
BreakIsIdempotentAcrossRepeatedCalls (F5). Updated every existing test that
relied on CanTakeHit's old mutating side effect to call Break() explicitly,
matching the new caller pattern.
Cannot compile-verify Assets/Scripts/Gameplay/Weapons/*.cs here (UnityEngine,
Unity batchmode unavailable in this environment) -- reviewed by hand for
brace/control-flow correctness; Self or a Unity batchmode pass should confirm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ShipInstance's thruster and aether-drive emission were the operator's
actual complaint (2026-09-19): thrust intent times a raw durability ratio
that ignored heat and power and duplicated simulation arithmetic in
presentation. Both now multiply intent by Thruster.Condition /
AetherDrive.Condition -- no durability/heat/power math left in
presentation. Unity-side; not covered by the shared test suite. Small and
obvious for Self's batchmode run on merge to check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The operator's play report on FieldShieldTest ("weak hits are very
janky, clicking outside the hex still makes it tremble and shatter,
pooling seems very rough") traced to two provable defects, not one
tuning constant:
- ShieldPanel.Hit had no bounds check at all. A point handed to it was
always enqueued and injected regardless of how far outside the
panel's own panelRadius it was.
- ShieldInterceptor.FindReusable decided reuse from a raw world-space
chord distance to the panel's stale spawn-time Position, using
PanelRadius*ReuseFraction (authored up to 2x) as the threshold. On a
non-uniformly scaled/curved envelope a chord across curvature is not
the panel's own in-plane extent, so a click clearly outside the
rendered tiling could still measure inside threshold and get routed
to Hit() -- which had nothing to reject it.
- Reusing a panel never refreshed its pooled DieAt, so a panel struck
again could still expire and get returned to the pool mid-fracture
("pooling seems very rough").
Fix: ShieldPanel.ContainsWorldPoint is now the one owner of "is this
point within my own radius" (panel-local in-plane distance vs.
panelRadius), used both as Hit's hard gate and as FindReusable's
candidacy test (with ReuseFraction, now capped at 1, narrowing but
never widening past the physical radius). Reuse now also extends
DieAt from the moment of the new hit.
Pinned in ShieldPanelCut3Verify.CheckHitOutsideRadiusDoesNotReachPanel:
a direct Hit() and a routed Absorb() outside a panel's own radius must
enqueue nothing and leave its state buffer untouched, and a genuinely
outside click opens its own panel. Deterministic (checks the pending
queue and no-op state, not temper drift) because breakBudgetFraction
paces fracture over many frames on purpose, so temper keeps drifting
from an earlier hit alone -- a drift-based check would have been
measuring that, not this rule (caught this against the check's own
first run).
All four ShieldPanelCut<N>Verify probes pass (batchmode, real GPU
device, no -nographics).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
Entity's constructor now takes a MessagePack round-trip copy of the settings
it is given, so no entity can alias GameplaySettings.DefaultEntitySettings.
That makes ActionGameManager.NewEntitySettings and EntitySerializer.Unpack's
null-coalescing copy redundant; both are deleted, and TradeMenu buys a ship
straight off the template (Entity's ctor copies it).
EntitiesDoNotShareDefaultEntitySettings pins the invariant: writing
ShutdownPerformance on one ship must not move the default for a sibling or
the template (settings-globals-cut.md, Cut 0).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DefaultOverworldSoundbank/DefaultCombatSoundbank/DefaultBossSoundbank/
AmbienceSoundBank and HullHitColor have no reader in Assets/Scripts
(settings-globals-cut.md 1.1).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ActionGameManager.Sensitivity was a Unity-serialized public float2, but
CultMath's float2 isn't Unity-serializable, so ARPG.unity's stored value
loaded as zero and mouse look went dead. Sensitivity is a player
preference, so it belongs in PlayerInputSettings (key 2) with the prior
default (-0.001, 0.001) as a field initializer, so a pre-existing player
store missing the key still deserializes to the working default instead
of zero. ActionGameManager now reads PlayerSettings.InputSettings.Sensitivity
directly; no cached copy on the MonoBehaviour. Adds a pinning test that
simulates an old store lacking key 2 and asserts the default, plus a
round-trip test for a non-default value.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FieldDriver now implements IAbsorbPresenter/IGrabPresenter/IMeleePresenter/
IThrustPresenter by delegating to its existing AddHit/GrabObject/Melee/Push
fields; it still decides nothing about capability rules, only how they render
(fork L, docs/headless-playground-cut.md). Its own click-to-hit wiring moves
out to whatever publishes AbsorbEvent (the test scene, next commit) since a
presenter should not be sourcing its own capability events.
The private nested GrabPhase enum is renamed to TendrilPhase so it doesn't
collide with the new global GrabPhase in CapabilityEvents.cs; this is the only
behavior-preserving rename, tendril timing is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
IAbsorbPresenter/IGrabPresenter/IMeleePresenter/IThrustPresenter let a Unity
component declare which capability events it visualizes without knowing about
CapabilityEvents itself. CapabilityPresentationBinder is the only thing that
subscribes a presenter to its matching stream, resolving a GrabEvent's plain
target handle to a live Transform (and initial velocity) through delegates its
owner supplies, since the engine-free GrabEvent can't carry a Unity reference.
Neither file knows about FieldDriver or any other concrete presentation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TargetReticle, TargetNext and TargetPrevious now consider all visible
entities except self (hostile and neutral), not just VisibleEnemies, so a
neutral wanderer can be selected. TargetNearest stays enemies-only (it drives
weapon lock) but now picks the nearest target: it was using MaxBy, which
picked the farthest.
Adds a "Toggle Stance" input action (bound to the N key; report names which)
that flips the player's own stance on the current target between hostile and
neutral via Entity.SetIff -- one button for the whole affordance, per the
operator's request. Also keeps the "iff hostile"/"iff neutral"/"iff clear"
console commands next to the existing ones, since they cost nothing extra
alongside the hotkey.
Fixes the HUD lock indicator's hostility check, which read
Target.IsHostileTo(CurrentEntity) instead of CurrentEntity.IsHostileTo(Target)
-- the same backwards direction as LockWeapon, fixed in the prior commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quality, Ingredients and Product leave CraftedItemInstance; a unit now
carries only a LotId into the run-scoped ProvenanceLedger. ItemManager
gains the one lot-minting path (CreateLot/CreateInstance(int)), GetLot,
and Brand(), which derives a maker and product from provenance instead
of a stored field. RunSave persists the ledger pruned to what the saved
zones' entities still reach; the live ledger is never pruned, so a
floor pickup collected after a save still resolves. CopyBuild is gone:
matching hardpoints now share the same lot instead of copying values.
PropertiesPanel shows the derived brand where the design's manufacturer
used to be. AetherDb's census flags duplicate (maker, design) products
and its save command checks every crafted instance's lot against a real
run store.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut A of docs/item-provenance-cut.md. TradeMenuDebug.cs was dead (its
script GUID appears in no scene/prefab/asset and no other .cs names the
type). ItemManager.Instantiate and CreateInstance(SimpleCommodityData,
int) re-rolled quality and dropped provenance on every load; nothing
outside EntitySerializer called them once the instantiate parameter is
gone. EntitySerializer.Unpack/Restore no longer thread an instantiate
flag, so Temperature and Armor are restored unconditionally like every
other caller already got. The starting ship and spawnturret now keep
what the generator built instead of re-minting.
Verification: Aetheria.Shared, tools/AetherDb build green against
CultLib a0813c6; tests/Aetheria.Shared.Tests 29 passed (unchanged); the
four negative greps in the map are empty. Unity compile is still owed
(the editor is open on this project; batchmode was not run per
instructions).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening the process cache's catalog writable in the editor was the wrong cut.
It made catalog read-only only by convention, so a stray catalog write in play
succeeded and a later flush could land play-mutated instances in Aetheria.cc.
- The process cache opens the catalog read-only in every build again, so the
missing-global check runs for it as it did before db83cd29. The writable
empty-seed exemption from ab4ced0f stays for AetherDb and fixtures.
- Loadouts.Commit(catalogPath, loadout, replace) is the only preset writer. It
opens its own short-lived cache over the catalog, writable, commits the one
preset conditionally on its key and disposes it. No other cache or instance
is touched. The running game sees the preset only after the catalog reloads.
CultLib at a0813c6 allows this: a read-only single-file store takes no lock
and holds no handle, and the writer's commit takes the exclusive sidecar
<path>.lock and replaces the file atomically.
- Capture refuses, writing nothing, when Aetheria.cc is missing (it never
creates the catalog), when a preset of that name exists under another key
(CultCache does not reject duplicate names on write), or when the key holds a
preset and replace is not given.
- The console refuses a command with any character outside letters, digits,
spaces and hyphens instead of stripping it, so "Mk.II" can no longer become
"MkII". capturepreset refuses a second argument other than replace, so an
unquoted capturepreset My Ship refuses instead of saving "My".
- The capture log line says the session sees the preset after a reload, and
that a Studio session opened earlier overwrites captured presets on save.
- Tests: capture while the process cache holds the catalog read-only leaves it
unchanged; a missing catalog refuses and creates nothing; a same name under a
Studio-minted key refuses, with or without replace; a game-shaped read-only
open missing a global throws. The existing preset tests now commit through
the catalog path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loadouts are ship presets (variants) for spawners, not a player feature.
- Route Loadout to the catalog; the player store no longer holds it.
- Materialize takes no credits: affordability, the charge and Loadouts.Price
are deleted, since only the player menu priced loadouts. Availability,
key-order product choice, all-or-nothing failures and the weapon-group
rules stay.
- Delete the InventoryPanel Save Loadout and Restore Loadout entries and
RestoreLoadout, which also removes its docking-bay overwrite.
- The editor opens the catalog writable in the one process cache, and the
editor-only console command capturepreset "<name>" [replace] captures the
piloted ship. Loadouts.Commit keys a preset by name, refuses a same-name
replace unless asked, and commits conditionally on that key. The single-file
store then lands only that record onto the file as it is on disk. An
unconditional commit would re-serialize every live catalog instance,
including any that play has mutated.
- Tests: capture -> commit -> fresh reopen round-trip, explicit replace with
no duplicate, and a preset commit that does not persist a mutated live
Faction. The money-only tests are removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner: ActionGameManager composes the process's one cache. Its CultCache
getter attaches the catalog, GameData/run.cc and GameData/player.cc on
first access, in either scene, and nothing reopens it.
- RunSave (SavedGame.cs) replaces the SavedGame constructor:
- Capture builds the live run as plain documents and writes nothing. Zone
contents are `zone.Contents?.PackZone() ?? zone.PackedContents`, so a
zone generated earlier but not loaded this session keeps its contents
and its orbit and body records instead of being erased by the next save.
- Commit is the only writer of SavedGame and SavedZone: one Commit with
zone i at `savedzone-{i}`, SavedGame.Zones set to those refs, and every
stored SavedZone outside that set removed. Orbits and bodies staged by
zone generation land in the same single-file write.
- Clear removes every run record in one Commit.
- Run begins: New Game's first statement is RunSave.Clear. Run saves:
ActionGameManager.SaveRun at wormhole arrival and quit. Run ends: Die
clears the run, then saves settings.
- Run exists only as the run store's SavedGame global: MainMenu enables
Continue from it (and logs when the run store holds records but no
SavedGame), and StartGame reads it once to pick the new-run or resume
branch.
- PlayerSettings: the getter returns the player store's global and, on
first launch, commits GetDefaultPlayerSettings; it is the only creator.
SavePlayerSettings commits the instance.
- InputDisplayLayout reads its InputLayout from the catalog by the layout
asset's name and throws when the catalog has none.
Tests: new RunSaveTests (5): RepeatedSavesKeepRecordCountConstant,
SaveRemovesZonesNoLongerInTheRun, SaveWritesOnlyTheRunStore,
ResumeThenSaveKeepsUnloadedZoneContents, ClearRemovesEveryRunRecord. Each
fails with its rule mutated out.
Builds: headless, 11 tests pass, Unity batchmode compile reports no error CS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The deletion line lands before RunSave, as the spec orders. Deleted, not
demoted:
- PlayerSettings.SavedRun (Key 1 stays unclaimed) and the SavedGame
constructor that upserted a fresh SavedZone per zone on every save.
- ActionGameManager: the PlayerSettings.msgpack getter, file path and
writer; the .loadout format (_loadoutPath, Loadouts, SaveLoadout and its
default-options serializer, the commented loader); OnApplicationQuit and
SaveState; SaveZone, which had no caller.
- MainMenu's SavedRun Continue branch and both `SavedRun = null` writes.
- InventoryPanel's Save Loadout and Restore Loadout bodies.
- InputDisplayLayout's KeyboardLayouts file reader, ParseJson,
AssociateInputKeys, SaveLayout and the usings they alone needed.
- AetherDb's Cut 9 importer (Import.cs, legacy-census, import).
- The three-gates note on loading writes; Cut 3 made loading non-writing.
- .gitignore ignores GameData/run.cc and GameData/player.cc instead of
PlayerSettings.msgpack.
DOES NOT BUILD in Unity: ActionGameManager, MainMenu and InputDisplayLayout
still reference the deleted members until the next commit wires RunSave,
the settings getter and the catalog layout read. Headless builds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 8b. The Unity project compiles with 0 errors in 6000.3.24f1 batchmode.
Packages: manifest.json pins org.gamecult.cultlib at cultlib-unity-v1.0.57
and org.gamecult.caching.unity at caching-unity-v1.2.0, next to the 8a
CultMath tag. Aetheria.Shared.Unity references GameCult.CultLib and keeps
noEngineReferences.
The Database Tools (Assets/Scripts/CultCache/Editor) are deleted in this
commit, alongside their five ported drawers in
Assets/Scripts/Editor/CultCacheDrawers.cs, each an
[CultInspectorDrawer] ICultInspectorDrawer:
- InspectableType: popup of the attribute type's [Union] member names,
writing the string.
- InspectableColor: float3 and float4 through ColorField.
- InspectableAnimationCurve: float4[] and BezierCurve through CurveField.
- InspectableTemperature: Kelvin, Celsius and Fahrenheit fields over one
value.
- InspectableSchematicShape: size and cell toggles. The old drawer also
drew the item's schematic texture and tinted hull hardpoints, which need
the parent document. The tagged Studio hands a drawer only the value, so
those wait on the Studio exposing the open record.
Unity call sites:
- ActionGameManager opens the catalog through AetheriaStores; Cut 10 adds
the run and player stores.
- Refs resolve through ItemManager.GetData or Get(ref); orbit, planet and
belt maps key by CultRecordKey; identity is compared by reference or key.
- The PlayerSettings.msgpack reader and writer, which Cut 10 deletes, pass
CultDocumentMessagePackSerialization.OptionsFor, since the embedded run
reaches CultMath values and refs. Keyboard layouts need no resolver and
keep MessagePack's defaults until Cut 10.
- The input map uses the nested action to binding shape.
- PropertiesPanel reads CultInspectorRangeAttribute.
- NameTools loses its legacy name-file writer.
- Slime compares serialized settings instead of the deleted SHA1 helper.
- FieldDriver drops InspectorHeader.
- .gitignore covers the test project's build output.
Builds: Aetheria.Shared, AetherDb, Aetheria.Shared.Tests (6/6), Unity
batchmode (0 errors).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AetheriaMath predates the CultMath swap and re-implemented helpers CultMath
now owns. Callers use CultMath directly: decay (Ship, AetherDrive, Entity),
damp (VelocityConversion, FieldTester, FieldDriver), first_order_intercept
(TurretController, Combat, GuidedProjectile), quadratic_bezier (FieldDriver)
and smootherstep (Ship). Parameter order matches in every case, and those
formulas have the same float evaluation order, so behaviour is unchanged.
Galaxy's commented-out segment check names distance_to_segment.
The other overloads had no callers and are dropped without a CultMath
counterpart being needed: the double Damp overloads, FindDistanceToSegment,
GetCatmullRomPosition, GetCubicSplinePosition, Smoothstep(x), and the
Deg2Rad/Rad2Deg constants.
AetherDb census, factions and hardpoint-fit are byte-identical to the
pre-cut captures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>