Map the stats authority and the power bus
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
acb2fef930
commit
f45fefef75
1 changed files with 851 additions and 0 deletions
851
docs/stats-and-power-cut.md
Normal file
851
docs/stats-and-power-cut.md
Normal file
|
|
@ -0,0 +1,851 @@
|
|||
# Stats and Power: Cut Map
|
||||
|
||||
Date: 2026-09-18
|
||||
|
||||
Status: Imagination pass, cut map. Nothing here has landed. Ends are owned by
|
||||
`docs/stats-and-power-target.md`; this document owns the means.
|
||||
|
||||
Anchors are against `codex/item-provenance` HEAD `acb2fef9`. Claims marked
|
||||
**(probe)** were measured headless against the real catalog `GameData/Aetheria.cc`
|
||||
(read-only) with a scratch console referencing `Aetheria.Shared`, CultLib a temporary
|
||||
detached worktree at `45c2f40`, built with `--artifacts-path` in scratch so this tree
|
||||
got no `bin`/`obj` writes. Both are removed. The probe source is not kept; §1.6 records
|
||||
what it did.
|
||||
|
||||
**Scope: stats and power only.** Fire control is the next campaign and gets its own map.
|
||||
§8 names the seam and what fire control will need from this authority.
|
||||
|
||||
---
|
||||
|
||||
## 0. What is there now
|
||||
|
||||
### 0.1 The stat, as it exists
|
||||
|
||||
`PerformanceStat` (`ItemData.cs:575-622`) is a `[MessagePackObject]` **value**, not a
|
||||
document. It has no `[CultDocument]`; every instance on disk lives nested two levels
|
||||
inside a catalog record (`ItemData` → `BehaviorData` union member → the stat field).
|
||||
Keys: `0 Min`, `1 Max`, `2 HeatExponentMultiplier`, `3 DurabilityExponentMultiplier`,
|
||||
`4 QualityExponent`, `5 FromRole`.
|
||||
|
||||
**(probe)** The catalog holds **348 reachable `PerformanceStat` instances** across 180
|
||||
documents:
|
||||
|
||||
| | count |
|
||||
|---|---|
|
||||
| `HeatExponentMultiplier != 0` | 275 |
|
||||
| `DurabilityExponentMultiplier != 0` | 145 |
|
||||
| `QualityExponent != 0` | 193 |
|
||||
| `FromRole` set | **0** |
|
||||
| `Min != Max` (actually authored, not a stub) | 188 |
|
||||
|
||||
By owner: `AutoWeaponData` 91, `ChargedWeaponData` 64, `GuidedWeaponData` 45,
|
||||
`LauncherData` 40, `InstantWeaponData` 26, `RadiatorData` 25, `ReactorData` 20,
|
||||
`SensorData` 12, `ThrusterData` 8, `AetherDriveData` 6, `EnergyDrawData` 3,
|
||||
`HeatData`/`CapacitorData`/`ReflectorData` 2 each, `StatModifierData` 1, `GearData` 1
|
||||
(an `AudioStat`).
|
||||
|
||||
**(probe)** Roles: 102 `CraftedItemData` designs, 51 equippable, 37 products. **0
|
||||
designs author roles; 0 products author role spreads.** `FromRole` therefore does
|
||||
nothing today in the shipped catalog — `Lot.QualityForRole` (`Provenance.cs:73-79`)
|
||||
falls back to `Lot.Quality` for every read.
|
||||
|
||||
### 0.2 Three evaluation bodies that disagree
|
||||
|
||||
| Site | Terms applied | Modifiers |
|
||||
|---|---|---|
|
||||
| `ItemManager.cs:75-90` `Evaluate(stat, item)` — **unequipped** | quality × durability | no |
|
||||
| `Entity.cs:1177-1186` `ConsumableItemEffect.Evaluate` | quality × **consumable effectiveness** | no |
|
||||
| `Entity.cs:1354-1371` `EquippedItem.Evaluate` | quality × durability × **heat** | yes |
|
||||
|
||||
Three authorities for one number. The unequipped path in the trade menu and the
|
||||
equipped path in the properties panel show different values for the same item on
|
||||
purpose in no design document; it falls out of which function the caller reached for.
|
||||
|
||||
### 0.3 The catalog object holds per-entity runtime state, and it leaks
|
||||
|
||||
`PerformanceStat` carries two `[IgnoreMember]` dictionaries (`ItemData.cs:596-622`):
|
||||
|
||||
```
|
||||
Dictionary<Entity, Dictionary<Behavior, float>> _scaleModifiers;
|
||||
Dictionary<Entity, Dictionary<Behavior, float>> _constantModifiers;
|
||||
```
|
||||
|
||||
`EquippedItem.Evaluate` calls `stat.GetScaleModifiers(Entity)` and
|
||||
`GetConstantModifiers(Entity)` on **every evaluation** (`Entity.cs:1361,1365`), and both
|
||||
accessors insert an entry for the entity when missing (`ItemData.cs:607-621`).
|
||||
`StatModifier.RemoveModifier` (`StatModifier.cs:100-103`) removes only the inner
|
||||
`[this]` key. **Nothing ever removes the outer `Entity` key.**
|
||||
|
||||
So a shared catalog document holds a strong reference to every `Entity` that has ever
|
||||
evaluated any stat, for the life of the process. Every destroyed ship, every zone left
|
||||
behind, stays reachable from `Aetheria.cc`'s in-memory records. This is the target
|
||||
doc's "authored catalog data that also stores runtime modifier state per entity" stated
|
||||
as a defect rather than a smell. It is the strongest reason for the resolver, stronger
|
||||
than the performance argument.
|
||||
|
||||
Not measured: whether the leak is visible in a real session's memory profile. That is
|
||||
an operator check (§7, O1), not a claim this map makes.
|
||||
|
||||
### 0.4 Evaluation cost, measured, and why it is not the motive
|
||||
|
||||
`Weapon.Execute` calls `UpdateStats()` unconditionally every tick (`Weapon.cs:127-131`),
|
||||
so an **idle** weapon still evaluates 13 stats per tick (10 in `Weapon.UpdateStats`
|
||||
`:113-125`, 3 in `InstantWeapon.UpdateStats` `:107-117`); a `ChargedWeapon` 16
|
||||
(`ChargedWeapon.cs:111-123`).
|
||||
|
||||
**(probe)** Real generated loadouts (`LoadoutGenerator`, seed 1, the three hulls that
|
||||
generate at HEAD):
|
||||
|
||||
| hull | items | behaviors | evaluations/tick |
|
||||
|---|---|---|---|
|
||||
| `LonginusX` (Ship) | 14 | 24 | **78** |
|
||||
| `Turret` | 9 | 15 | 20 |
|
||||
| `Zenith` (Station) | 8 | 10 | 17 |
|
||||
|
||||
78/tick is 4,680/s at 60 fps for one ship; a zone of 7 entities at the median is
|
||||
~8,400/s. Each evaluation is three `pow` calls plus two nested dictionary lookups and
|
||||
two dictionary iterations. That is around a millisecond per second per ship. **Caching
|
||||
buys almost nothing here.** Say so plainly rather than selling the resolver on speed:
|
||||
the resolver is bought by ownership, by the leak, and by making a power-supply term
|
||||
cheap enough that adding it is not a regression. The operator's "we don't wanna pay
|
||||
that cost for every stat evaluation" is satisfied because the power-supply source
|
||||
changes at most once per tick, not because 78 became 12.
|
||||
|
||||
### 0.5 The power path, end to end
|
||||
|
||||
**Draw.** Nine call sites, all inside `ServerShared`, none outside it:
|
||||
|
||||
| site | shape |
|
||||
|---|---|
|
||||
| `EnergyDraw.cs:46` | continuous or per-activation, `PerSecond` flag |
|
||||
| `Thruster.cs:77` | continuous, scaled by input axis |
|
||||
| `AetherDrive.cs:138` | continuous, three axes summed |
|
||||
| `Radiator.cs:74` | continuous, scaled by temperature ratio |
|
||||
| `ConstantWeapon.cs:87` | continuous while firing |
|
||||
| `InstantWeapon.cs:98` | **instant**, per burst |
|
||||
| `InstantWeapon.cs:178` | **instant**, per shot |
|
||||
| `Sensor.cs:85` | **instant**, per ping |
|
||||
| `Shield.cs:59` (+ `:54` `CanConsumeEnergy`) | **instant**, per hit taken |
|
||||
|
||||
**Supply.** `Entity.TryConsumeEnergy` (`Entity.cs:858-888`): drain every charged
|
||||
capacitor evenly in a `do/while`, then hand any remainder to every online reactor as
|
||||
`Reactor.ConsumeEnergy` → `Draw +=`. It returns `onlineReactors > 0`. **Every draw
|
||||
succeeds while one reactor is online**, which is the target doc's first complaint.
|
||||
|
||||
`Reactor.Execute` (`Reactor.cs:85-147`), running last because `Reactor` is the only
|
||||
`IOrderedBehavior` (`Order => 100`, `Reactor.cs:48`), subtracts its own generation from
|
||||
the accumulated `Draw`, and:
|
||||
- deficit → overload heat at `OverloadEfficiency`, then `Draw = 0`. Unmet demand is
|
||||
never refused, only taxed as heat.
|
||||
- surplus → fill capacitors evenly, then throttle.
|
||||
|
||||
**The hidden priority the target names.** `EquippedItem.SortPosition` is set from the
|
||||
**last** `IOrderedBehavior` found on the item (`Entity.cs:1347-1348`) and
|
||||
`_orderedEquipment = Equipment.OrderBy(SortPosition)` (`Entity.cs:586,810`). Only
|
||||
`Reactor` sets it, so reactor items sort last and **every other item sorts equal, in
|
||||
equip order**. Whoever was equipped first drains the capacitors first. That is the
|
||||
whole existing priority system: the order a player happened to fit their ship.
|
||||
|
||||
Within an item, `BehaviorGroups` run in `Group` key order and the chain breaks on the
|
||||
first `Execute` returning false (`Entity.cs:1404-1414`), so a failed draw silently
|
||||
stops everything authored after it in the group.
|
||||
|
||||
**Capacitors.** `Capacitor` (`Capacitor.cs:31-61`) is a bus capacitor: one shared pool,
|
||||
charge added by the reactor's surplus and drained by anyone. There is no input-only
|
||||
capacitor today.
|
||||
|
||||
### 0.6 What persists, and where
|
||||
|
||||
- **Catalog `Aetheria.cc`, read-only at runtime:** every `PerformanceStat`, every
|
||||
`BehaviorData`, `CraftedItemData.Roles`, `FactionProductData.Roles`. A schema change
|
||||
here is a migration with a census, like provenance Cut C.
|
||||
- **Run store `run.cc`, deleted on death:** `EquippableItem.Durability`
|
||||
(`ItemInstance.cs:53`), `OverrideShutdown` (`:54`), `Lot` and its `RoleFill`s
|
||||
(`Provenance.cs:63-88`), `EntityPack.Temperature`/`Armor`/`Settings`/`WeaponGroups`
|
||||
(`EntitySerializer.cs:179-188`).
|
||||
- **Runtime-only, never saved:** `Capacitor.Charge`, `Reactor.Draw`/`CurrentLoadRatio`,
|
||||
every `EquippedItem` derived performance value, the modifier dictionaries.
|
||||
- **No migration mechanism exists.** `[CultDocument(name, "1")]` is identity only;
|
||||
CultCache resolves by name and refuses ambiguity (`CultCache.cs:346-356`). The only
|
||||
compatibility is slot-driven: an extra persisted `[Key(n)]` is ignored (reported in
|
||||
`LastSchemaMigrationReports.IgnoredExtraSlots`), a missing one is defaulted, and any
|
||||
type change is a hard refusal. So the only safe moves are **append a new key** or
|
||||
**tombstone a key by comment and rewrite the catalog in one writable `Commit`**.
|
||||
- `EquippableItem`'s next free MessagePack key is **12** (0,1 base; 2,9,10 tombstoned;
|
||||
7,8,11 live).
|
||||
|
||||
### 0.7 Consumers outside `ServerShared`
|
||||
|
||||
- `UI/Properties Panel/PropertiesPanel.cs:407-419` (equipped, evaluated),
|
||||
`:553-569` (design, shows `Min`–`Max`), and `UI/Menu/TradeMenu.cs:273-284` (shows and
|
||||
**sorts on `.Max` only**, no evaluation). Three near-duplicate
|
||||
`RuntimeInspectable` → `float`/`int`/`PerformanceStat` switches that disagree.
|
||||
- `PropertiesPanel.cs:400-402` and `:545-550` are the only places
|
||||
`StatReference.Target`/`.Stat` are shown, as `SplitCamelCase()` strings.
|
||||
- `UI/HUD/SchematicDisplay.cs:91-92,176-184` is the power HUD: `_reactor.Draw`,
|
||||
`_capacitors.Sum(Charge)/Sum(Capacity)`.
|
||||
- `Editor/CultCacheDrawers.cs:17-29` is the `InspectableType` dropdown that writes a
|
||||
**bare type-name string** and silently resolves an unknown one to "None". It drives
|
||||
`StatModifier.cs:23` `RequireBehavior` and `StatModifier.cs:136` `StatReference.Target`.
|
||||
- `Gameplay/ActionGameManager.cs:899` is the only gameplay gate on power:
|
||||
`GetBehavior<Reactor>() == null` refuses undock.
|
||||
- Tests: `LoadoutTests.cs:491-533` `StatsReadTheLot`, `:554-571`
|
||||
`EvaluateDurabilityExponentReadsLotQuality`, `:575-598`
|
||||
`EquippedItemThermalExponentReadsLotQuality`; `IffAndCombatTests.cs:286-321` builds
|
||||
`LockWeaponData` stats. `tests/mutation_tests.py:134-196` mutates the stat path.
|
||||
- `tools/AetherDb` touches **no** stat or power type. Its `census`
|
||||
(`Program.cs:37-108`) is the before/after invariant a catalog rewrite is verified
|
||||
against, and `:85-90` is the role-authoring audit.
|
||||
- There is **no** `CustomPropertyDrawer` for `PerformanceStat`; in CultCache Studio it
|
||||
falls through to `inspector.DrawDefault`.
|
||||
- There is **no** headless stat or power probe harness in the tree.
|
||||
|
||||
---
|
||||
|
||||
## 0b. Identity, lifecycle, authority
|
||||
|
||||
One row per persistent or runtime kind this campaign touches. No cut below is mapped
|
||||
while a cell here is empty.
|
||||
|
||||
| Kind | What names it | What happens to it over time | Who decides it |
|
||||
|---|---|---|---|
|
||||
| **A stat** (`PerformanceStat`) | Its field on a `BehaviorData`/`EquippableItemData`, reached by reflection; on disk, the `[Key(n)]` slot inside its owning catalog record | Authored in the catalog; **immutable at runtime**; one instance shared by every entity equipping that design | The catalog author, through CultCache Studio or a console over `Aetheria.Shared` |
|
||||
| **A declared term** (`StatTerm`) | Its index in its stat's `Terms` list, plus its `Source` kind (and `Role` name for a quality term) | Authored with the stat; migrated once from the four fixed exponent fields; immutable at runtime | The catalog author. Validation refuses a role the design lacks, and refuses a power-supply term on a stat that decides a power request |
|
||||
| **A modifier term** | The pair (modifier `Behavior` instance, target stat entry) in the owning entity's resolver | Attaches when the modifier's item is equipped **and** its behaviour executes; detaches on `Dispose` or a tick without execution (`StatModifier.cs:117-124`); never outlives the entity | The `StatModifier` behaviour, writing into the entity's resolver. **Not** the catalog object |
|
||||
| **A resolver entry** | `(EquippedItem or ConsumableItemEffect, PerformanceStat reference)` | Created at equip / consumable start; invalidated when one of its term sources changes; destroyed at unequip / entity teardown | `StatResolver`, one per `Entity`, the sole owner of every resolved stat value |
|
||||
| **A bus capacitor** (`Capacitor`) | Its `Behavior` instance on an equipped item | Runtime-only; charge starts at 0 each session and is never persisted; charged only by the bus, drained only by the bus | The `PowerBus`. No behaviour drains it directly |
|
||||
| **An input capacitor** | Its `Behavior` instance on the instant-activation item that owns it | Runtime-only; filled continuously by the bus up to its rate; spent whole by one activation; never persisted | The bus fills; the owning behaviour spends. Two writers, disjoint signs, named |
|
||||
| **A power tier** | `EquippableItem.PowerTier` (new run-store key 12) on the *unit*, not the design | A player choice; persists with the run; survives save/Continue; resets only with the run | The player, through the schematic/inventory UI. Nothing else writes it |
|
||||
| **A power grant** | `(Entity, EquippedItem)` for one tick | Computed once per tick before equipment updates; overwritten next tick; never persisted | The `PowerBus`. Read-only everywhere else, including the HUD |
|
||||
| **A lot's per-role quality** (`RoleFill`) | `(LotId, role name)` | Minted with the lot and **immutable**; an upgrade mints a *new* lot and repoints the item, which the resolver sees as a quality-source change | `ItemManager.CreateLot` (`ItemManager.cs:110-133`), rolling the product's `ProductRole` spread |
|
||||
| **A design role** (`ItemRole`) | Its `Name` string on `CraftedItemData.Roles` | Catalog-authored; adding one to a shipped design changes what existing lots fall back on (`QualityForRole` returns `Lot.Quality` for an unfilled role) | The catalog author. Agents generate, the operator reviews |
|
||||
|
||||
Two rows deserve their consequence spelled out.
|
||||
|
||||
- **A stat is shared; a resolved value is not.** That single sentence is the whole
|
||||
ownership fix. Everything per-entity moves off the catalog object into the resolver.
|
||||
- **A lot is immutable, so quality is a rare invalidation.** An upgrade is a lot swap,
|
||||
not a mutation, so the resolver never needs to watch a quality number — only the item's
|
||||
`Lot` field.
|
||||
|
||||
---
|
||||
|
||||
## 1. Authority maps
|
||||
|
||||
### 1.1 Stat values
|
||||
|
||||
- **Owner:** `StatResolver`, one per `Entity`, constructed with it.
|
||||
- **Inputs:** the stat's `Min`/`Max` and declared `Terms`; the current value of each
|
||||
term's source (heat, durability, quality-for-role, power supply, consumable
|
||||
progress); the modifier terms attached to that entry.
|
||||
- **Outputs:** one `float` per `(item, stat)`, read by every behaviour and by the UI.
|
||||
- **Derived state:** every `Weapon.Damage`-style cached field on a behaviour becomes a
|
||||
read of the resolver, not an independent computation.
|
||||
- **Forbidden writers:** `PerformanceStat` may hold no per-entity state at all — the
|
||||
two dictionaries and their four accessors are deleted, not hidden. `ItemManager` and
|
||||
`ConsumableItemEffect` stop computing stat values. No behaviour writes into another
|
||||
behaviour's stat.
|
||||
- **Shared paths:** equipped items, consumable effects, the properties panel, the trade
|
||||
menu and any future headless balance harness resolve through the same entry point. The
|
||||
unequipped case (trade menu, an item on the floor) is the one path with no entity;
|
||||
it resolves against a null-entity context that has no heat, no power and no modifiers,
|
||||
and it says so in its own name rather than being a third quiet dialect.
|
||||
- **Deletion line:** `ItemData.cs:580-594` (the four fixed exponent fields),
|
||||
`ItemData.cs:596-622` (the dictionaries), `ItemManager.cs:75-90`, `Entity.cs:1177-1186`,
|
||||
`Entity.cs:1354-1371`, `StatModifier.cs:61-103` (the reflection target search and the
|
||||
writes into the catalog object) all go before the resolver's first read lands.
|
||||
|
||||
### 1.2 Power
|
||||
|
||||
- **Owner:** `PowerBus`, one per `Entity`, stepped once per tick from `Entity.Update`
|
||||
**before** `_orderedEquipment` updates.
|
||||
- **Inputs:** each reactor's resolved generation for the tick; each bus capacitor's
|
||||
stored charge; each consumer's request, which is a resolved stat multiplied by a
|
||||
behaviour-supplied scalar in [0,1]; each item's `PowerTier`.
|
||||
- **Outputs:** a grant ratio per equipped item in [0,1]; the reactor load ratio; the
|
||||
overload and throttling heat the reactor adds.
|
||||
- **Derived state:** `EquippedItem.PowerSupply` (the grant ratio) is display-only and
|
||||
stat-source-only. `Capacitor.Charge` is written only by the bus. `Reactor.Draw`
|
||||
becomes a **reported** total, not a sink for unmet demand.
|
||||
- **Forbidden writers:** `Entity.TryConsumeEnergy` and `Entity.CanConsumeEnergy` are
|
||||
deleted, so no behaviour can spend energy on its own. `Reactor.ConsumeEnergy` is
|
||||
deleted. `Capacitor.AddCharge` becomes internal to the bus and the owning behaviour.
|
||||
Behaviour execution order is no longer allowed to decide who gets power — it decides
|
||||
nothing about power at all.
|
||||
- **Shared paths:** player firing, AI firing, thrust, radiators, shields and sensor
|
||||
pings all request through the same path. There is no "the player's weapon is special"
|
||||
branch; fire control will inherit this (§8).
|
||||
- **Deletion line:** `Entity.cs:851-888` and `Reactor.cs:80-83,90-141` are cut before
|
||||
the bus's first grant is computed.
|
||||
|
||||
### 1.3 Priority tiers
|
||||
|
||||
- **Owner:** `PowerBus`'s allocation pass.
|
||||
- **Inputs:** the per-item tier and the per-item request.
|
||||
- **Outputs:** the grants.
|
||||
- **Derived state:** nothing. A tier is a stored player choice, not a derivation.
|
||||
- **Forbidden writers:** `EquippedItem.SortPosition` must no longer influence who is fed
|
||||
— and once the bus exists, `IOrderedBehavior` has exactly one remaining job (running
|
||||
the reactor's own heat accounting last) which the bus subsumes. `IOrderedBehavior`,
|
||||
`Reactor.Order` and the `OrderBy(SortPosition)` sorts are candidates for deletion in
|
||||
the same cut; the map assumes they go, and §7 O3 is where the operator confirms
|
||||
nothing else wanted that ordering.
|
||||
- **Shared paths:** the tier is set from the schematic UI and read by the bus. The AI
|
||||
sets tiers through the same field or not at all.
|
||||
- **Deletion line:** `Entity.cs:1347-1348`, `Entity.cs:586`, `Entity.cs:810`,
|
||||
`Reactor.cs:48`, `Behaviors.cs:127-130`.
|
||||
|
||||
---
|
||||
|
||||
## 2. The target shape, named
|
||||
|
||||
Types this campaign creates. Bodies are not transcribed; what matters is what each owns
|
||||
and what must die under its own mutation.
|
||||
|
||||
**`StatSource`** — an enum: `Heat`, `Durability`, `Quality`, `PowerSupply`,
|
||||
`ConsumableProgress`. Closed. Adding a member is a schema change with a census.
|
||||
|
||||
**`StatTerm`** — `[MessagePackObject]`: `Key(0) StatSource Source`, `Key(1) float
|
||||
Exponent`, `Key(2) string Role`. `Role` is meaningful only for `Quality`; validation
|
||||
refuses it elsewhere rather than ignoring it.
|
||||
|
||||
**`PerformanceStat`** — keeps `Key(0) Min`, `Key(1) Max`; gains `Key(6) List<StatTerm>
|
||||
Terms`; **tombstones keys 2, 3, 4, 5**. The name stays: renaming it would churn 98 call
|
||||
sites and buy no invariant.
|
||||
|
||||
**`StatResolver`** — per entity. Owns `(item, stat) → value`. Exposes resolve,
|
||||
invalidate-by-source, attach-modifier, detach-modifier, and dispose. The only type that
|
||||
may hold a resolved value.
|
||||
|
||||
**`StatContext`** — what a resolver entry reads: the item's heat performance, durability
|
||||
ratio, lot, power supply and consumable progress. Implemented by `EquippedItem`, by
|
||||
`ConsumableItemEffect`, and by a bare unequipped context whose heat, power and modifier
|
||||
sets are empty. This is the seam that makes the three dialects one function.
|
||||
|
||||
**`PowerBus`** — per entity. Owns supply, demand, tiers, grants, and every capacitor
|
||||
write.
|
||||
|
||||
**`IPowerConsumer`** — implemented by a behaviour that draws: it names *which*
|
||||
`PerformanceStat` is its request and supplies a scalar in [0,1] for this tick. It does
|
||||
**not** hand the bus a number it computed itself. That is deliberate: it gives the
|
||||
validator an exact, static set of request stats, so "a stat deciding a power request may
|
||||
not depend on power supply" is checkable at catalog load rather than by discipline. A
|
||||
consumer whose draw is genuinely multi-term (`AetherDrive`, three axes) expresses it as
|
||||
one stat times a computed scalar.
|
||||
|
||||
**`InputCapacitor`** — a behaviour on an instant-activation item. The bus fills it at a
|
||||
rate; one activation spends a whole shot's worth or does not fire. Its capacity and rate
|
||||
default from the item's existing `Energy` and `Cooldown` stats so that no catalog
|
||||
authoring is required for the first cut (§7 Q4).
|
||||
|
||||
**`StatValidation`** — the loud refusal. Runs at catalog load, at Studio save, and at
|
||||
equip. Names the items and the stat.
|
||||
|
||||
---
|
||||
|
||||
## 3. Cuts
|
||||
|
||||
Deletes come first inside each cut, and subtraction cuts are kept separate from
|
||||
behaviour cuts.
|
||||
|
||||
### Cut 0. One evaluation path
|
||||
|
||||
- **Repo/branch:** `Aetheria` `codex/item-provenance` from `acb2fef9`. Depends on
|
||||
nothing.
|
||||
- **First:** capture `tools/AetherDb` `census`, `factions`, `dangling`, `station-fit`,
|
||||
`loadout 1`, `hardpoint-fit` into scratch, byte-for-byte, same method both sides.
|
||||
Record the `dotnet test` baseline at HEAD (the headless map recorded 32 passed / 2
|
||||
failed at `1e647953`; re-measure, do not assume).
|
||||
- **Deletes first:**
|
||||
- `ItemManager.cs:75-90` (16 lines) — the unequipped body.
|
||||
- `Entity.cs:1177-1186` (10) — the consumable body.
|
||||
- `Entity.cs:1354-1371` (18) — the equipped body.
|
||||
- **Adds:** one evaluation function taking a `StatContext`, plus the three context
|
||||
implementations. `EquippedItem`, `ConsumableItemEffect` and the unequipped case
|
||||
implement it; each supplies only the sources it has.
|
||||
- **Per-file changes:**
|
||||
- `Behaviors.cs:60` `Evaluate(stat) => Item?.Evaluate(stat) ?? Consumable.Evaluate(stat)`
|
||||
becomes a resolve against the owning context. **The method name `Evaluate` and its
|
||||
signature stay**, so all 81 behaviour call sites are untouched. This is the one place
|
||||
in the campaign where preserving a name is the right call: it is a genuine facade,
|
||||
not a compatibility shim, and it delegates rather than deciding.
|
||||
- `PropertiesPanel.cs:529` and `:490` both route through the new contexts.
|
||||
- `TradeMenu.cs:273-284` keeps showing `.Max` **in this cut** — changing what the trade
|
||||
menu shows is a design change, and it belongs in Cut 8 with the operator's ruling
|
||||
(§7 Q6).
|
||||
- **Authority map:** §1.1, minus the resolver and minus the modifier move. This cut
|
||||
makes one function own the arithmetic; it does not yet move where modifier state
|
||||
lives.
|
||||
- **Verification:**
|
||||
- builds: `dotnet build Aetheria.Shared/Aetheria.Shared.csproj`,
|
||||
`dotnet build tools/AetherDb`, each with `-p:CultLibRoot=<worktree>`.
|
||||
- tests: `LoadoutTests.StatsReadTheLot` pins that a role-named stat reads the role and
|
||||
an unnamed one reads workmanship.
|
||||
`EvaluateDurabilityExponentReadsLotQuality` pins the durability path.
|
||||
**New:** a test pinning that the *unequipped* and the *equipped-at-full-health-and-
|
||||
optimal-temperature* paths now agree, which they do not at HEAD. That test is the
|
||||
cut.
|
||||
- negative: `rg "public float Evaluate\(PerformanceStat" Assets/` returns exactly one
|
||||
hit. Verified against legitimate names: `BezierCurve.Evaluate(float)`,
|
||||
`ExponentialCurve.Evaluate(float)`, `Environment.Evaluate(float2,…)` and
|
||||
`ZoneData.Evaluate(float)` all differ in parameter type, so the pattern does not
|
||||
collide.
|
||||
- mutation: restore heat to the unequipped path; the new agreement test must go red
|
||||
naming it.
|
||||
- operator: none. No visible change is intended, and if one appears it is a defect.
|
||||
|
||||
### Cut 1. Terms replace the four fixed exponents
|
||||
|
||||
- **Depends on:** Cut 0.
|
||||
- **First:** a read-only probe against a **copy** of `Aetheria.cc` answering: does the
|
||||
catalog still open read-only with keys 2-5 gone, and how many records report
|
||||
`IgnoredExtraSlots`? Record the file's sha1 and byte length. This is provenance Cut C's
|
||||
method (`docs/item-provenance-cut.md:179-196`) and it is not optional.
|
||||
- **Deletes first:** `ItemData.cs:580-594` — `HeatExponentMultiplier`,
|
||||
`DurabilityExponentMultiplier`, `QualityExponent`, `FromRole` (15 lines), replaced by a
|
||||
tombstone comment in the style of `ItemData.cs:281-282`: *keys 2, 3, 4 and 5 belonged
|
||||
to fixed exponent fields now expressed as terms; do not reuse them.*
|
||||
- **Adds:** `StatSource`, `StatTerm`, `PerformanceStat.Terms` at `Key(6)`.
|
||||
- **Per-file changes:** the single evaluation function from Cut 0 walks `Terms` instead
|
||||
of the four fields. `ConsumableItemEffect`'s `effectiveness` becomes a
|
||||
`ConsumableProgress` term rather than a hard-coded factor, which removes the last
|
||||
reason for that path to be different.
|
||||
- **The migration.** A scratch console, **deliberately not committed**, opening
|
||||
`AetheriaStores.Open(..., catalogWritable: true)` and re-upserting every stored
|
||||
`ItemData` under its own key in **one `Commit`**. Precondition: Unity and CultCache
|
||||
Studio closed. The committed binary is the artifact; the code is not kept. Per stat,
|
||||
the rewrite is mechanical:
|
||||
- `HeatExponentMultiplier != 0` → a `Heat` term with that exponent (275 stats).
|
||||
- `DurabilityExponentMultiplier != 0` → a `Durability` term (145).
|
||||
- `QualityExponent != 0` → a `Quality` term with `Role = FromRole` (193; `FromRole` is
|
||||
unset on all 348, so every migrated quality term is roleless).
|
||||
- A zero multiplier becomes **no term**, not a term with exponent 0. The difference is
|
||||
the point: a stat declares only the terms it uses.
|
||||
- **Authority map:** unchanged owner (the catalog author); what changes is that the
|
||||
stat's dependency set becomes data the validator can read instead of four fields that
|
||||
are always present and usually zero.
|
||||
- **Verification:**
|
||||
- Reopen shows **zero** reports with `IgnoredExtraSlots`, and the expected byte delta.
|
||||
- `AetherDb` `census`, `factions`, `dangling`, `station-fit`, `loadout 1`,
|
||||
`hardpoint-fit` captured before and after are **byte-identical**. None of them reads a
|
||||
stat value, so any difference is a defect.
|
||||
- A reflection dump of all 180 records before and after, comparing every *evaluated*
|
||||
stat at quality 0.5, durability 1.0 and optimal temperature: identical to float
|
||||
tolerance. This is the real proof; `census` only proves nothing unrelated moved.
|
||||
- tests: `StatsReadTheLot` and `EvaluateDurabilityExponentReadsLotQuality` rewritten to
|
||||
build `Terms`, still pinning the same rules.
|
||||
- negative: `rg "HeatExponentMultiplier|DurabilityExponentMultiplier|QualityExponent|FromRole" Assets/ tools/ tests/`
|
||||
returns only the tombstone comment. `FromRole` is checked separately because
|
||||
`ProductRole.Role` and `RoleFill.Role` are legitimate and must not be swept.
|
||||
- Unity batchmode, editor closed, no `error CS` (§6).
|
||||
- Two commits, as Cut C landed: code, then the catalog rewrite.
|
||||
|
||||
### Cut 2. The resolver owns every value
|
||||
|
||||
- **Depends on:** Cut 1.
|
||||
- **Deletes first:**
|
||||
- `ItemData.cs:596-622` (27 lines) — both dictionaries and all four accessors. **The
|
||||
leak in §0.3 dies here, structurally, not by cleanup.**
|
||||
- `StatModifier.cs:61-86` (26) — the reflection target search that matches by type
|
||||
name and grabs `PerformanceStat` references out of other designs.
|
||||
- `StatModifier.cs:88-103` — the writes into the catalog object.
|
||||
- **Adds:** `StatResolver`; `StatValidation` for self-dependency and modifier-chain
|
||||
cycles; `StatModifier` rewritten to attach and detach a term in the entity's resolver.
|
||||
- **Per-file changes:**
|
||||
- `Entity.cs` constructs a resolver with the entity and disposes it with it.
|
||||
- `EquippedItem` signals `Durability` and `Heat` invalidation from `UpdatePerformance`
|
||||
(`Entity.cs:1377-1393`), and `Quality` invalidation when its `Lot` changes.
|
||||
- `StatReference` (`StatModifier.cs:132-140`) keeps its `(type name, field name)` shape
|
||||
but is **resolved once at catalog load into a concrete field, and fails loudly when
|
||||
it does not resolve**. Today an unresolvable reference is a silent no-op
|
||||
(`StatModifier.cs:63-65` leaves `_stats` null and `ApplyModifier` would throw on
|
||||
first use). §7 Q2 is whether the operator wants a stronger identity than a name pair.
|
||||
- **Authority map:** §1.1 in full.
|
||||
- **Verification:**
|
||||
- tests: a test equipping the same design on **two** entities, attaching a modifier on
|
||||
one, and asserting the other's resolved value is unchanged. At HEAD this passes by
|
||||
accident because the dictionary is keyed by entity; after the cut it passes because
|
||||
there is nothing shared to get wrong. Mutation: make the resolver static; it goes red.
|
||||
- tests: a test that unequipping and destroying an entity leaves no reference to it
|
||||
reachable from the catalog record — a `WeakReference` to the entity, a forced
|
||||
collection, and an assertion that it is dead. **This test fails at HEAD.** It is the
|
||||
cut's headline and the only test here that proves the leak is gone.
|
||||
- tests: a stat whose term chain reaches itself through a modifier is refused at equip,
|
||||
naming both items and the stat.
|
||||
- negative: `rg "GetScaleModifiers|GetConstantModifiers"` returns nothing.
|
||||
- operator: O1 — play a session, open a few zones, kill some ships, and confirm memory
|
||||
does not climb the way it did.
|
||||
|
||||
### Cut 3. The power bus replaces `TryConsumeEnergy`
|
||||
|
||||
- **Depends on:** Cut 2 (the bus's grant is a resolver source).
|
||||
- **Deletes first:**
|
||||
- `Entity.cs:851-856` `CanConsumeEnergy` (6 lines, one caller: `Shield.cs:54`).
|
||||
- `Entity.cs:858-888` `TryConsumeEnergy` (31).
|
||||
- `Reactor.cs:80-83` `ConsumeEnergy` (4).
|
||||
- `Reactor.cs:90-141` — the deficit and capacitor-fill branches as a *demand*
|
||||
mechanism (~52 lines; the overload-heat and throttling arithmetic survives, moved
|
||||
under the bus's numbers).
|
||||
- **Adds:** `PowerBus`; `IPowerConsumer` on the nine draw sites; `EquippedItem.PowerSupply`.
|
||||
- **Per-file changes:** each of the nine sites in §0.5 stops calling a spend and starts
|
||||
declaring a request. The five continuous ones (`EnergyDraw`, `Thruster`, `AetherDrive`,
|
||||
`Radiator`, `ConstantWeapon`) convert directly. The four instant ones
|
||||
(`InstantWeapon.cs:98,178`, `Sensor.cs:85`, `Shield.cs:59`) are **out of scope for this
|
||||
cut** — they keep a direct capacitor spend, marked with a comment naming Cut 4 as their
|
||||
owner, and they are the one temporary split this campaign accepts. That split is named,
|
||||
bounded to one cut, and Cut 4's deletion line closes it.
|
||||
- **Authority map:** §1.2, with the instant sites listed under forbidden writers as
|
||||
*not yet*, which is the honest word.
|
||||
- **Verification:**
|
||||
- tests: a ship whose requests exceed generation gets a total grant equal to
|
||||
generation plus available capacitor charge, and **not** more. At HEAD the equivalent
|
||||
assertion is unwritable, because every draw succeeds.
|
||||
- tests: a refused request produces no effect at all — not a partial one, and not an
|
||||
overload-heat receipt. Mutation: make the bus grant the full request and heat the
|
||||
reactor; it goes red.
|
||||
- tests: two ships with identical loadouts fitted in **opposite equip order** get
|
||||
identical grants. That is the death of the hidden priority, and it fails at HEAD.
|
||||
- negative: `rg "TryConsumeEnergy|CanConsumeEnergy|ConsumeEnergy"` returns nothing.
|
||||
- operator: O2 — fly with a weak reactor and confirm that things starve visibly and
|
||||
legibly rather than cooking the reactor.
|
||||
|
||||
### Cut 4. All draw is continuous
|
||||
|
||||
- **Depends on:** Cut 3.
|
||||
- **Deletes first:** the four direct capacitor spends left standing by Cut 3
|
||||
(`InstantWeapon.cs:98,178`, `Sensor.cs:85`, `Shield.cs:59`), and with them the last
|
||||
path by which anything but the bus touches stored charge.
|
||||
- **Adds:** `InputCapacitor`, with capacity and rate defaulting from the owning item's
|
||||
`Energy` and `Cooldown` stats (§7 Q4).
|
||||
- **Per-file changes:** firing becomes a check against the input capacitor's charge.
|
||||
`ChargedWeapon` (`ChargedWeapon.cs:125-152`) is the interesting case: its charge cycle
|
||||
already is a buffer, and `ChargeEnergy` should become the fill rate rather than a
|
||||
separate concept. `Shield.cs:57-61` is the odd one out: a shield hit is not an
|
||||
activation the player chose, so its "instant" draw is really a continuous reserve. Name
|
||||
it as such rather than giving it an activation buffer.
|
||||
- **Verification:**
|
||||
- tests: a weapon whose input capacitor is partly filled does **not** fire a partial
|
||||
shot. The operator's ruling — "no item receives a fraction of a shot" — is the rule
|
||||
this test pins.
|
||||
- tests: halving the grant halves the refill rate and therefore the sustained rate of
|
||||
fire, without changing damage per shot.
|
||||
- negative: `rg "AddCharge" Assets/` hits only the bus and `InputCapacitor`.
|
||||
- operator: O4 — the feel of a weapon that stutters under brownout instead of firing
|
||||
at full rate and cooking the ship.
|
||||
|
||||
### Cut 5. Priority tiers
|
||||
|
||||
- **Depends on:** Cut 3.
|
||||
- **Deletes first:** `Entity.cs:1347-1348` (the `SortPosition` assignment),
|
||||
`Entity.cs:586` and `:810` (the `OrderBy(SortPosition)` sorts), `Reactor.cs:48`
|
||||
(`Order => 100`), `Behaviors.cs:127-130` (`IOrderedBehavior`) — **subject to O3**.
|
||||
- **Adds:** `EquippableItem.PowerTier` at `Key(12)`; the tiered allocation pass;
|
||||
a tier control in the schematic UI.
|
||||
- **Per-file changes:** `EntitySerializer` needs nothing — the field rides inside
|
||||
`EquippableItem`, which is already packed. Old run stores default the field, and death
|
||||
clears the run anyway.
|
||||
- **Authority map:** §1.3.
|
||||
- **Verification:**
|
||||
- tests: with generation short of total demand, tier 0 is satisfied in full before tier
|
||||
1 receives anything, and tier 1 divides what is left.
|
||||
- tests: within a tier, two consumers with equal requests get equal grants; with
|
||||
unequal requests, proportional ones.
|
||||
- tests: a tier change on an equipped item takes effect on the next tick with no
|
||||
re-equip.
|
||||
- negative: `rg "SortPosition|IOrderedBehavior"` returns nothing.
|
||||
- operator: O3 — confirm nothing depended on reactors executing last for a reason this
|
||||
map did not find. O5 — the tier UI is playable.
|
||||
|
||||
### Cut 6. The power supply term, brownout, and the request-independence rule
|
||||
|
||||
- **Depends on:** Cuts 2, 3, 5.
|
||||
- **Deletes first:** nothing. This is the one purely additive cut, and it buys the
|
||||
operator's named capability: a continuous consumer that degrades instead of stopping.
|
||||
- **Adds:** `StatSource.PowerSupply` handling in the resolver; the validation rule that a
|
||||
request stat may not depend on power supply, directly or through a modifier chain.
|
||||
- **Per-file changes:** the resolver invalidates `PowerSupply` entries once per tick from
|
||||
the bus. The validator runs at catalog load, at Studio save, and at equip; because
|
||||
`IPowerConsumer` names its request stat rather than computing a number, the set of
|
||||
request stats is exactly enumerable from the catalog, and the check is static.
|
||||
- **Verification:**
|
||||
- tests: a thruster at half grant produces less thrust, by the authored exponent.
|
||||
- tests: a catalog holding a thruster whose `EnergyUsage` carries a `PowerSupply` term
|
||||
is **refused at load**, naming the item and the stat. Mutation: drop the rule; the
|
||||
test goes red.
|
||||
- tests: the same refusal through a modifier chain, at equip time, naming both items.
|
||||
- tests: the resolver recomputes a power-termed stat at most once per tick. This is the
|
||||
operator's "don't pay that cost for every stat evaluation" stated as an assertion.
|
||||
- operator: O6 — brownout reads as degradation, not as breakage.
|
||||
|
||||
### Cut 7. Roles authored
|
||||
|
||||
- **Depends on:** Cut 1 (terms carry the role).
|
||||
- **Deletes first:** nothing. This is a content cut, and it is where the campaign stops
|
||||
being code.
|
||||
- **Adds:** roles on designs, per-role means and deviations on products, and
|
||||
`Role`-carrying quality terms on the stats that should read a part rather than
|
||||
workmanship. Agents generate; the operator reviews. Written through the catalog's own
|
||||
types — AetherDb or a scratch console over `Aetheria.Shared` — never a hand-copied
|
||||
schema.
|
||||
- **Note the sharp edge:** adding a role to a shipped design changes nothing for existing
|
||||
lots, because `QualityForRole` falls back to `Lot.Quality` for an unfilled role. But a
|
||||
*newly minted* lot from a product with no spread for that role rolls
|
||||
`new ProductRole()` — mean .5, and `ProductRole`'s default deviation
|
||||
(`FactionProduct.cs:33-48`). Authoring a role on a design without authoring the
|
||||
matching product spreads silently pulls every maker toward average. The census
|
||||
(`AetherDb Program.cs:85-90`) is where that is caught, and the two must land together.
|
||||
- **Verification:**
|
||||
- `AetherDb census` reports every design's roles filled by every product that sells it;
|
||||
zero designs with roles and no matching product spread.
|
||||
- `AetherDb loadout 1` before and after: the failure count must not rise.
|
||||
- operator: O7 — the review pass on generated role content. Only the operator can do
|
||||
this.
|
||||
|
||||
### Cut 8. UI reconciliation
|
||||
|
||||
- **Depends on:** Cuts 2, 3, 5.
|
||||
- **Deletes first:** `PropertiesPanel.cs:553-569` and `TradeMenu.cs:273-284`, the two
|
||||
divergent copies of the `RuntimeInspectable` switch (~35 lines together), collapsed
|
||||
into the survivor at `PropertiesPanel.cs:407-419`.
|
||||
- **Per-file changes:**
|
||||
- `PropertiesPanel.cs:400-402` and `:545-550` stop building a label from
|
||||
`SplitCamelCase()`d type and field names and use the identity the resolver already
|
||||
has.
|
||||
- `SchematicDisplay.cs:176-184` reads the bus's supply and grants instead of
|
||||
`_reactor.Draw` and a hand-summed capacitor pool, and gains the per-item tier and
|
||||
grant readout — which is the whole point of tiers being a player choice.
|
||||
- `Editor/CultCacheDrawers.cs:17-29`: the type-name dropdown keeps writing a name, but
|
||||
Cut 2's load-time resolution means an unresolvable one now fails loudly instead of
|
||||
reading as "None".
|
||||
- **Verification:**
|
||||
- operator only, and it is the largest operator surface in the campaign: the properties
|
||||
panel, the trade menu, the schematic HUD, the tier control, and a CultCache Studio
|
||||
click-through over a stat with terms. §7 O8.
|
||||
|
||||
---
|
||||
|
||||
## 4. Subtraction ledger, estimated
|
||||
|
||||
Line counts for deletes are exact against `acb2fef9`; adds are estimates and should be
|
||||
compared with what lands, in the postmortem.
|
||||
|
||||
| Cut | Removed | Added (est.) | Net | Structure |
|
||||
|---|---|---|---|---|
|
||||
| 0 | 44 | ~45 | ~+1 | 3 evaluation authorities → 1 |
|
||||
| 1 | 15 | ~40 | ~+25 | 4 fixed fields → a declared term list; 4 catalog keys retired; **catalog rewritten** |
|
||||
| 2 | 53 | ~180 | ~+127 | per-entity state leaves the catalog object; a leak dies; validation appears |
|
||||
| 3 | 93 | ~200 | ~+107 | 9 ad-hoc spends → 1 bus; 2 public entity methods deleted |
|
||||
| 4 | ~30 | ~90 | ~+60 | the last direct capacitor writers die |
|
||||
| 5 | ~12 | ~110 | ~+98 | `IOrderedBehavior` deleted; 1 new run-store key |
|
||||
| 6 | 0 | ~120 | ~+120 | the only purely additive cut; buys brownout and the request rule |
|
||||
| 7 | 0 | ~0 code | 0 | content; catalog data only |
|
||||
| 8 | ~35 | ~20 | ~-15 | 3 stat renderers → 1 |
|
||||
| **Total** | **~282** | **~805** | **~+523** | |
|
||||
|
||||
This campaign is net additive by roughly 500 lines and it should be. What it buys, named
|
||||
against the "every new abstraction must name its owner" rule:
|
||||
|
||||
- `StatResolver` — owner of every resolved value. Replaces per-entity state living on a
|
||||
shared catalog document, and the entity leak that shape produced.
|
||||
- `PowerBus` — owner of allocation. Replaces nine independent spend paths and an implicit
|
||||
priority nobody authored.
|
||||
- `StatTerm` — replaces four fixed fields with a declared set, which is what makes
|
||||
validation possible at all.
|
||||
- `InputCapacitor` — buys the operator's "all power draw is continuous" ruling.
|
||||
|
||||
Targets, packages, dependencies, formats: **none added, none removed**. One MessagePack
|
||||
key added to `EquippableItem` (run store), one added and four retired on
|
||||
`PerformanceStat` (catalog).
|
||||
|
||||
## 5. Build budget
|
||||
|
||||
- Packages: `Aetheria.Shared` (netstandard2.1), `tools/AetherDb` and
|
||||
`tests/Aetheria.Shared.Tests` (net10.0), Debug only. No new project, target, package or
|
||||
store.
|
||||
- CultLib at `45c2f40`, unmodified, as a detached worktree passed with
|
||||
`-p:CultLibRoot=`, removed after.
|
||||
- Headless commands use `--artifacts-path <scratch>` while another agent reads the tree.
|
||||
- **Build host is this Windows workstation, and it is also the target** for every
|
||||
headless check. `Assembly-CSharp` is proven only by Unity batchmode, editor closed
|
||||
(§6), which is also this host.
|
||||
- The catalog rewrite in Cut 1 is the one irreversible artifact. It runs once, with Unity
|
||||
and CultCache Studio closed, against a backed-up `Aetheria.cc`.
|
||||
|
||||
## 6. Unity batchmode recipe
|
||||
|
||||
Per cut that touches `Assembly-CSharp` (Cut 8, and any cut whose signature change reaches
|
||||
`Assets/Scripts/UI` or `Assets/Scripts/Gameplay` — Cuts 0, 2, 3, 5 all do):
|
||||
|
||||
1. Confirm no editor: `Get-Process Unity`, and `Test-Path F:\Projects\Aetheria\Temp\UnityLockfile`.
|
||||
If either says yes, stop and ask. Never two batchmode runs at once.
|
||||
2. `Start-Process "C:\Program Files\Unity\Hub\Editor\6000.3.24f1\Editor\Unity.exe" -ArgumentList '-batchmode -nographics -quit -projectPath F:\Projects\Aetheria -logFile <scratch>\unity-cut-N.log' -PassThru`
|
||||
3. `Select-String -Path <log> -Pattern "error CS|Exiting batchmode"`.
|
||||
|
||||
There is no stat or power probe harness in the tree and this map does not add one. The
|
||||
NUnit suite reaches everything in `ServerShared`, which is where all of this lives;
|
||||
batchmode is only the compile check for the UI edits.
|
||||
|
||||
## 7. Operator forks
|
||||
|
||||
Most blocking first.
|
||||
|
||||
**Q1 — Does the campaign land before or after fire control?**
|
||||
`docs/three-gates-scope.md` puts fire control at item 4 of the shipping list and
|
||||
`docs/headless-playground-cut.md` maps it as Cut 2. Fire control will roll hits over
|
||||
weapon and targeting stats, and those rolls want a resolver that can hand out a value
|
||||
without re-deriving it, and a power model where a weapon can be starved.
|
||||
**A:** stats and power first, fire control leans on the finished authority.
|
||||
**B:** fire control first, and it builds its rolls on `PerformanceStat` as it is today,
|
||||
then migrates.
|
||||
**Recommended: A**, because B means fire control is written twice — once against three
|
||||
disagreeing evaluation bodies and a stat that leaks entities, then again. But A delays
|
||||
the shipping list's item 4, and that is the operator's call, not this map's.
|
||||
|
||||
**Q2 — How does a modifier name the stat it targets?**
|
||||
Today it is `(type name string, field name string)`, matched by reflection, and an
|
||||
unresolvable pair is a silent no-op that would throw on first use. Exactly **one**
|
||||
modifier is authored in the whole catalog (**probe**: `CapacitorData.Capacity`,
|
||||
Multiplier, and it does resolve).
|
||||
**A:** keep the name pair, resolve it once at catalog load, fail loudly. Studio's
|
||||
existing `InspectableType` dropdown (`CultCacheDrawers.cs:17-29`) gains a second popup
|
||||
for the field so a name is never typed.
|
||||
**B:** give every stat a stable authored id and reference that.
|
||||
**Recommended: A.** One authored instance does not justify a new identity scheme, and B
|
||||
is a registry by another name — the thing doctrine says not to reach for. A converts the
|
||||
silent failure into a loud one, which is what the target doc actually asked for. If role
|
||||
authoring (Cut 7) multiplies modifiers into the dozens, revisit.
|
||||
|
||||
**Q3 — Heat changes every tick. What does "recompute only when a source changes" mean
|
||||
for it?**
|
||||
275 of 348 stats carry a heat term, and `ThermalPerformance` is recomputed every tick in
|
||||
`UpdatePerformance` (`Entity.cs:1377`). A literal dirty-flag on heat means recomputing
|
||||
almost everything every tick anyway, which is exactly what happens now.
|
||||
**A:** accept it. Heat-termed stats recompute per tick; the resolver's win is ownership
|
||||
and the fact that power supply, durability and quality — the sources that would otherwise
|
||||
multiply the cost — are cheap. Measured cost at HEAD is ~4,680 evaluations/s for the
|
||||
heaviest generated ship (**probe**), around a millisecond per second.
|
||||
**B:** quantize `ThermalPerformance` to a step (1/256, say) and invalidate only when it
|
||||
crosses one. Turns a per-tick recompute into a rare one at the price of a visible
|
||||
quantization in stat values and HUD bars.
|
||||
**Recommended: A.** The measurement says the cost is not real, and B pays gameplay
|
||||
visibility for a performance win nobody needs. B stays available if a future zone is
|
||||
much denser than 7 entities. This is the fork most likely to be wrong, because the probe
|
||||
measured three hulls and a real fight may be heavier.
|
||||
|
||||
**Q4 — Where does an input capacitor's size come from?**
|
||||
Every instant-activation item needs one, and **0 are authored**.
|
||||
**A:** derive it — capacity from the item's `Energy` stat, rate from `Energy / Cooldown` —
|
||||
so Cut 4 needs no catalog authoring and a ship at rest behaves as it does today.
|
||||
**B:** author `InputCapacitorData` on every instant item, which is a catalog content pass
|
||||
across 15 weapon-bearing designs before Cut 4 can land.
|
||||
**C:** A now, with an authored override that B can fill in per design later.
|
||||
**Recommended: C.** A alone makes burst capacity and sustained rate the same authored
|
||||
number, which flattens a real design lever — a weapon that holds three shots and refills
|
||||
slowly is a different weapon from one that holds one and refills fast. C keeps the cut
|
||||
landable and keeps the lever.
|
||||
|
||||
**Q5 — How many priority tiers, and who authors the default?**
|
||||
The target says critical first, each lower tier dividing what is left. It does not say
|
||||
how many, or what an item defaults to.
|
||||
**A:** three (critical / normal / auxiliary), defaulted per hardpoint type.
|
||||
**B:** four or five, defaulted per behaviour kind (life support and reactor critical,
|
||||
thrust normal, weapons normal, sensors auxiliary).
|
||||
**C:** a free integer, everything defaulting to the same tier, and the player sorts it.
|
||||
**Recommended: B.** A default that is already sensible is what makes the tier UI an
|
||||
option rather than a chore, and per-behaviour is where the knowledge is. C makes every
|
||||
new ship a configuration task before it flies.
|
||||
|
||||
**Q6 — What should the trade menu show for a stat?**
|
||||
It shows and sorts on `.Max` alone (`TradeMenu.cs:273-284`) — the value at perfect
|
||||
quality, perfect durability and optimal temperature, which no item ever has. The
|
||||
properties panel shows the evaluated value for an equipped item and `Min`–`Max` for a
|
||||
design.
|
||||
**A:** show the evaluated value for the unit on offer, which is what the player is buying.
|
||||
**B:** keep `.Max` as a comparable ceiling and add the evaluated value beside it.
|
||||
**Recommended: A** for what the player reads, **B** for what the column sorts on — a
|
||||
ceiling is the honest sort key when comparing designs, and the evaluated number is the
|
||||
honest display when comparing units. This is a feel question and the operator should see
|
||||
it before it lands.
|
||||
|
||||
**Q7 — Does the heat/negent work want anything from this campaign?**
|
||||
`docs/stats-and-power-target.md`'s negentropy section and
|
||||
`docs/negent-weapons-concept.md` both rest on "cooling is a change to a condition, not an
|
||||
effect on a stat", which this campaign's `StatSource.Heat` term delivers unchanged. The
|
||||
one thing negent needs that is not here is a **consumable charge gating the cooling**, and
|
||||
`StatSource.ConsumableProgress` (Cut 1) is the term for it.
|
||||
**A:** nothing more. Negent is content against this authority.
|
||||
**B:** add a negent-shaped source now.
|
||||
**Recommended: A.** The concept doc's own claim is "almost nothing new, which is the
|
||||
point"; adding a source ahead of a single authored item would be inventing a mechanism to
|
||||
match a document.
|
||||
|
||||
### Operator-only checks
|
||||
|
||||
- **O1 (Cut 2)** — play a session across several zones with kills, and confirm memory does
|
||||
not climb the way the leak in §0.3 predicts. The `WeakReference` test proves the
|
||||
reference is gone; only a played session proves it mattered.
|
||||
- **O2 (Cut 3)** — fly with a reactor too small for the loadout. Starvation should be
|
||||
legible.
|
||||
- **O3 (Cut 5)** — confirm nothing wanted reactors to execute last for a reason this map
|
||||
did not find, before `IOrderedBehavior` is deleted.
|
||||
- **O4 (Cut 4)** — the feel of a weapon refilling under brownout.
|
||||
- **O5 (Cut 5)** — the tier control is playable from the schematic screen.
|
||||
- **O6 (Cut 6)** — brownout reads as degradation.
|
||||
- **O7 (Cut 7)** — review the agent-generated role content. Nobody else can.
|
||||
- **O8 (Cut 8)** — properties panel, trade menu, schematic HUD, tier control, and a
|
||||
CultCache Studio click-through over a stat with terms.
|
||||
|
||||
---
|
||||
|
||||
## 8. The seam with fire control
|
||||
|
||||
Fire control is the next campaign and owns its own map. Where the two touch:
|
||||
|
||||
**What fire control will find already built.**
|
||||
- One resolved value per `(item, stat)`, from one owner, with no per-entity state on the
|
||||
catalog object. A roll over weapon and targeting stats reads the resolver; it does not
|
||||
evaluate.
|
||||
- Weapon and targeting stats that can carry a `PowerSupply` term, so a starved targeting
|
||||
system rolls worse rather than switching off. That is the brownout ruling applied to
|
||||
accuracy, and it is available the moment Cut 6 lands.
|
||||
- An input capacitor per instant weapon, which means "can this weapon fire right now" is
|
||||
a question with a cheap, local, authoritative answer — no bus round trip inside the
|
||||
roll.
|
||||
- A power request path shared by player and AI, so an AI ship cannot draw in a way a
|
||||
player ship cannot.
|
||||
|
||||
**What fire control must own, and this map does not touch.**
|
||||
- The hit roll itself, the target-data thresholds, subsystem selection, and
|
||||
`DamageSchematic` moving out of `Gameplay/EntityInstance.cs`
|
||||
(`docs/three-gates-scope.md:80-99`).
|
||||
- **The pre-impact commit window.** `docs/three-gates-scope.md:92-97` rules that the
|
||||
outcome commits a short fixed time before impact and that fire control owns the horizon
|
||||
as one authored setting; `docs/shield-presentation-contract.md` binds presentations to
|
||||
it. Nothing in this campaign reads or writes that horizon. The one thing to be careful
|
||||
of: a weapon's resolved stats at the moment of the roll and at the moment of impact may
|
||||
differ, because heat and power move in between. **Fire control must snapshot what it
|
||||
rolled over at commit time.** The resolver hands out a value; it does not promise that
|
||||
value is still current a second later. That is the seam, stated as the rule fire
|
||||
control has to keep.
|
||||
- Whether a starved weapon that fires anyway should roll worse, or simply not fire. This
|
||||
campaign makes it not fire (Cut 4: no fraction of a shot). If fire control wants a
|
||||
degraded shot instead, that is a ruling against this map's Q4/Cut 4, and it should be
|
||||
raised there rather than worked around downstream.
|
||||
|
||||
## 9. Known gaps in this map
|
||||
|
||||
Stated so Soul does not have to find them.
|
||||
|
||||
- Only **three** hulls generate a loadout at HEAD, so §0.4's per-ship measurement rests
|
||||
on a small sample. A real fight may be much heavier than 7 entities at 20
|
||||
evaluations/tick.
|
||||
- The entity leak in §0.3 is proven from the code, not from a memory profile. O1 is where
|
||||
it becomes a measured fact.
|
||||
- The per-tick evaluation table in §0.4 was counted from behaviour bodies by hand and
|
||||
applied to real loadouts by the probe. A behaviour whose `Execute` returns early most
|
||||
ticks is overcounted; `Thruster` and `Radiator` are the likely ones.
|
||||
- Cut 3 leaves four instant draw sites spending directly for exactly one cut. That is a
|
||||
named, bounded split of authority, and if Cut 4 does not follow closely it becomes the
|
||||
thing this doctrine forbids.
|
||||
- The add estimates in §4 are guesses for types that do not exist yet. They are there to
|
||||
be compared against reality in the postmortem, not to be trusted now.
|
||||
Loading…
Reference in a new issue