ZoneRenderer adds an exit wormhole at RunGoal.ExitGatePosition; Interact asks RunGoal.ExitOpen and either
calls Win or names the boss that seals it. Die and Win share one EndRun.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mining owns belts, chunks and target picking; scenarios owns admission and run
staging; master owns ships and fire control (rule scenarios-cut.md S4).
- AetheriaStores.CatalogTypes: nine-type union, master's list plus FieldKindData, no Scenario.
- tools/AetherDb/Program.cs: both cases (ship-authoring, field-kinds), one usage string.
- Zone.cs: master's MineAsteroid dropped (mining deleted its inputs); scenarios' SettleBelts and its three calls deleted.
- RunStartTests: the two belt-task tests deleted (they read belt.Transforms and MineAsteroid).
- FireControlCut124Tests: master's strict Exact assertions; mining's CompatibleDrift allowance and its comment dropped.
- GameData/Aetheria.cc: master's pointer 70a7b0a9. This commit lacks the Asteroid field kind in the catalog, so
TheShippedCatalogHasAPickableFieldKind fails here by design; the next commit rebuilds it.
Builds: this commit builds (verified at the end with commit 2); the catalog pin test fails at this commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The four handlers in ActionGameManager call Entity.TargetUnderReticle, TargetNext,
TargetPrevious and TargetNearestEnemy. TargetCandidates, which built, sorted and
searched every visible ship and rock per press, is gone. Previous with no target
takes the farthest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Launch cleared the run store before generating, and a generation or staging failure only reached the log: the
Generating Galaxy dialog stayed up, StartGame threw, and the saved run was already gone.
RunSave.Replace runs a start with the saved run still stored: when it returns a staged run the saved run's records go,
and when it returns null or throws, only the records it wrote (the arena's orbits and bodies) go. StartGame's
new-run branch is StartScenario, chosen by PendingScenario rather than by the absence of a SavedGame; it mints into an
empty ledger, and a failed start clears CurrentGalaxy (so quitting saves nothing) and refuses on the main menu with
every failure. Launch no longer clears; a galaxy that throws is refused on the title menu. The menu offers Continue
whenever no run is live, which replaces MainMenu.InGame (cut, with its prefab value and scene override), so a
failed start can resume the saved run in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Scenario is an abstract C# class compiled with the game: Generate picks the galaxy through GalaxyStage (Main or
Prelude, at the scenario's seed, the background noise drawn from the same seed), Stage places the run's first
entities through ScenarioStage (Fit, Bare, Generated, Preset, Player, Place, Cargo), which collects named failures
and admits nothing. RunStart generates the galaxy, the arena under Ambient and admits everything through Zone.Admit,
all or nothing. The aetheria.scenario record, ScenarioShip, ScenarioEntity, RunStart.Check and the null-scenario
branch are cut.
New Game opens a submenu: Tutorial Galaxy and Main Galaxy, then (editor and development builds) Djinni Shakedown,
Starved Reactor, Arcs, Duel, Launcher Angles and Long Haul, each with its brief. MainMenu.Launch is the one way into a
new run; ActionGameManager.PendingScenario carries the scenario to StartGame once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IsTutorial duplicated Galaxy.IsPrelude; its readers read CurrentGalaxy.IsPrelude, and RunSave.Capture writes
SavedGame.IsTutorial from galaxy.IsPrelude with no isTutorial argument. TutorialPassed was never written and its only
reader was New Game's unreachable main-galaxy branch, which goes with it (S2 brings the main galaxy back as a
scenario). spawnturret's two identical generator branches collapse to one. Old player.cc files still load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one conflict, ShipInstance.cs's thruster emitter lookup, keeps the
branch's thruster.Item.Hardpoint.Transform and master's new
ThrusterHardpoint.WireExhaust(particles, thrusterHardpoint). Master's side
as written would have brought back the origin-cell hardpoint lookup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one conflict is EntityInstance's SoundBank block, which this branch had
moved onto item.Hardpoint and ship-mounts deleted: it stays deleted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emitter carries the invisible material, whose alpha is 0, so an exhaust
prefab left at Unity's default (Use Mesh Colors on) emitted nothing. The
shipped thruster prefabs only worked because theirs is off. ThrusterHardpoint
.WireExhaust now owns the wiring and turns it off; ShipInstance calls it. The
preview smoke wires a default particle system to each emitter on the clone and
requires every particle emitted and none transparent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PreloadAsync rethrew on the first failing hull, so one bad package faulted
Loading and no mod ship could spawn. Each hull now loads in its own try: a
failure destroys its partial visual, is logged with the hull's name, and the
rest still preload, as Compose skips a bad package. The play smoke installs the
package beside a broken sibling that composes but cannot assemble (its map icon
moved onto a meshless muzzle node) and proves the sibling has no prototype while
the good ship spawns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
forceRenderingOff is not serialized, so Instantiate and SaveAsPrefabAsset dropped
it and every cloned mod ship (and every FBX-built prefab) drew its collision hull.
The collider renderer now takes the template's invisible material, as the thruster
emitter does. The preview smoke checks the collider on the clone and the prefab
smoke checks it on the configured prefab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The placement rule (HardpointAt) already finds the one hardpoint an item lies
in at equip; TryEquip threw that answer away and eighteen readers recomputed it
from the item's origin cell, which need not lie in the hardpoint (Soul's
scenarios batch-4 P30). EquippedItem.Hardpoint now carries the answer, null for
Tool gear and the hull, and every origin-cell lookup reads it instead:
FireControl.ArcFor, the eleven weapon managers, EntityInstance, ShipInstance
and ActionGameManager's articulation grouping. Entity.Hardpoints[,] stays the
per-cell map for armour and display.
TryFindSpace's hardpoint search tries every origin the fit rule can accept,
including ones up and left of the hardpoint, so a design whose shape lacks its
origin cell is found wherever ItemFits would place it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one conflict is AetheriaStores.CatalogTypes: keep ShipAuthoring from the
ships lane and Scenario from the scenarios lane.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
forceRenderingOff is not serialized, so the prototype's clone drew the
emitter again; the preview smoke's clone check caught it. The emitter's
materials become the template's invisible material, which survives
Instantiate and which EntityInstance keeps as the renderer's own material.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ruling thrusters-radiators-are-meshes: thruster and radiator mounts are meshes;
the thruster's is not drawn and is the surface its exhaust emits from, the
radiator's is drawn. The assembler already required a mesh for both; it now
turns rendering off on the thruster emitter, and the preview smoke checks it.
The authoring doc states the mount rules and the mesh rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The array's only reader was the dead SoundBank block in EntityInstance, which
looked up a hardpoint transform and did nothing with it. ShipModPlan.Equipment,
ShipModShips' assignment, the preview check, the FBX builder's HP.Equipment
case and its smoke assertion all fed that one dead reader. An HP.Equipment node
is now refused by the builder as an unknown hardpoint kind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StartGame's new-run branch no longer generates, unpacks or admits the
player: RunStart generates the arena and stages the run, and StartGame
enters the zone and binds the returned player (docs/scenarios-cut.md, 2.2,
deletion line). PopulateLevel's warp arrival and the spawnturret console
command admit through Zone.Admit, unpiloted. The console commands stay (Q4);
give's hull route moors a ship at the docked entity and joins a zone only
through Entity.TryUndock, which already admits.
Needs the Unity batchmode compile; Assembly-CSharp is not built headless.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DontDestroyOnLoad throws outside play mode, so the edit-mode play smoke
failed before it reached assembly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>