Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Assets/Tests/EditMode/Simulation/ConstructionSystemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,14 +1032,14 @@ public void SellStorage_CapsRefundThenLoweredCapacityDrivesExcessDecay()
Is.EqualTo(EconomySystem.HqBaseCapacityAE + EconomySystem.StorageCapacityBonusAE));

Assert.That(f.Construction.SellBuilding(UnitCommandStateView.ToRawEntityId(storage)), Is.True);
Assert.That(f.Economy.GetPlayerEconomy(0).AetheriumCredits, Is.EqualTo(4000L),
"only 100 of the 150 AE sale refund fit before the Storage leaves the stock");
Assert.That(f.Economy.GetPlayerEconomy(0).AetheriumCredits, Is.EqualTo(4050L),
"the whole 150 AE sale refund now fits before the Storage leaves the stock; it only partially fit while the HQ ceiling was 2.000 (#131 raised it to 3.000)");
Assert.That(f.Economy.CapacityFor(0), Is.EqualTo(EconomySystem.HqBaseCapacityAE),
"selling the Storage immediately lowers the derived ceiling");

f.Step(EconomySystem.ExcessDecayIntervalTicks);
Assert.That(f.Economy.GetPlayerEconomy(0).AetheriumCredits, Is.EqualTo(3500L),
"tick 10 removes 25% of the 2,000 AE excess");
Assert.That(f.Economy.GetPlayerEconomy(0).AetheriumCredits, Is.EqualTo(3788L),
"tick 10 removes 25% of the 1.050 AE excess (4.050 over the 3.000 HQ ceiling that #131 raised from 2.000; the excess used to be 2.000)");
}

[Test]
Expand Down
200 changes: 185 additions & 15 deletions Assets/Tests/EditMode/Simulation/EconomySystemTests.cs

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions Assets/Tests/EditMode/Simulation/MatchFingerprintV1Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,14 @@ public void RulesRevisionOneAndTwo_GoldenHashesRemainByteStable()
ulong revisionOne = MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV1);
ulong revisionTwo = MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV2);

Assert.That(revisionOne, Is.EqualTo(0x531CE8F614A16CB5UL), "revision 1 canonical stream is frozen");
Assert.That(revisionTwo, Is.EqualTo(0x07725EA26668C9F8UL), "revision 2 canonical stream is frozen");
// NOT actually frozen — see issue #138. ComputeRulesHash64 writes
// TODAY's constants into every revision, so raising
// HqBaseCapacityAE (#131) moved the revision-1 hash too. Re-pinning
// here restores green; it does NOT restore the guarantee the name
// claims. Do not read this assertion as proof of anything until #138
// is decided.
Assert.That(revisionOne, Is.EqualTo(0x1163158B8146B168UL), "revision 1 canonical stream is frozen");
Assert.That(revisionTwo, Is.EqualTo(0x259E6977B751109BUL), "revision 2 canonical stream is frozen");
}

[Test]
Expand All @@ -96,7 +102,7 @@ public void CurrentRulesHash_MovesPastRevisionTwo_ForD104PlacementAndRepair()

Assert.That(MatchFingerprint.CurrentRulesRevision, Is.EqualTo(MatchFingerprint.RulesRevisionV3));
Assert.That(current, Is.EqualTo(MatchFingerprint.ComputeRulesHash64(MatchFingerprint.RulesRevisionV3)));
Assert.That(current, Is.EqualTo(0x05CCA8475789AD4AUL), "revision 3 canonical stream is frozen");
Assert.That(current, Is.EqualTo(0xD1B683837D12FCEFUL), "revision 3 canonical stream is frozen");
Assert.That(current, Is.Not.EqualTo(revisionTwo),
"D-104 placement and repair behavior must not share revision 2's rules identity");
}
Expand Down
59 changes: 49 additions & 10 deletions Assets/_Project/Scripts/Simulation/Economy/EconomySystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,18 @@ namespace Nova.Simulation.Economy
/// <see cref="UnitState.HarvestFieldId"/> order whose grid cell is the
/// field's cell or adjacent (Chebyshev distance &lt;= 1, documented
/// reach rule) gathers <see cref="HarvestRateAE"/> per tick, bounded by
/// the field's remaining reserve and the free cargo space. A FULL cargo
/// the field's remaining reserve and the free cargo space — but ONLY
/// while the owner account has room under the derived storage ceiling.
/// An account at or above its ceiling holds the harvester in place with
/// its order kept (#136, owner decision E-1 of 2026-08-31): every AE
/// gathered into a full account would be forfeited at the deposit while
/// the FINITE field (G2 reservation, D-010) still drains — the reserve
/// burns in front of a player who reads it as progress. The brake sits
/// at the pickup, not the deposit: what was never mined is never lost,
/// and the D-024 forfeit itself stays exactly as decided. The check
/// re-reads the live balance and the derived ceiling on every tick and
/// stores nothing, so the harvester resumes BY ITSELF the moment room
/// exists — credits spent or a storage completed. A FULL cargo
/// no longer resolves the order: the harvester flips to
/// <see cref="UnitState.IsReturningCargo"/> and KEEPS its
/// <see cref="UnitState.HarvestFieldId"/>, so after the deposit it
Expand All @@ -53,7 +64,8 @@ namespace Nova.Simulation.Economy
/// <para>
/// Storage ceiling (16.4, #53, D-024/D-096/D-106): the AE account has a
/// derived upper bound — one or more completed HQs provide the single
/// 2.000 AE account base,
/// 3.000 AE account base (E-2 of 2026-08-31, #131: raised from 2.000 so
/// the base carries the D-077 opening buffer),
/// every completed Storage adds 2.000, scanned from the living building
/// stock on every read and NEVER stored (a stored cap would be a state
/// field and a format break). All income and refunds route through
Expand Down Expand Up @@ -150,12 +162,16 @@ public sealed class EconomySystem : IStatefulSimSystem, ISlotFactionLookup
/// <summary>
/// 16.4 (#53, D-024/D-096/D-106): one-time AE account base while the
/// slot owns at least one completed HQ. Additional HQs do not stack.
/// Deliberately below the canonical start balance (3.000 AE, D-077):
/// the start stock stays (existing balances only decay), but fresh
/// income forfeits until the player builds storage — the D-024 silo
/// pressure from the first minute.
/// E-2 of 2026-08-31 (#131): raised from 2.000 to 3.000, so the base
/// carries the canonical start balance (D-077,
/// <see cref="CanonicalMatchStartingCreditsAE"/>) instead of silently
/// overriding it — the 3.000 AE opening buffer predates the ceiling,
/// and with a 2.000 base the first minutes decayed a third of it
/// away before the first harvester could deliver. Consequence,
/// deliberate: a completed Storage is worth slightly less in the
/// opening than it was under the 2.000 base.
/// </summary>
public const long HqBaseCapacityAE = 2000L;
public const long HqBaseCapacityAE = 3000L;

/// <summary>16.4 (#53, D-024): AE capacity bonus per completed Storage.</summary>
public const long StorageCapacityBonusAE = 2000L;
Expand Down Expand Up @@ -242,8 +258,8 @@ public void BindSiteLookup(Func<EntityId, bool> isSiteLookup)
/// 16.4 (#53, D-024/D-096/D-106): the slot's AE ceiling, DERIVED from the
/// living building stock on every read — never stored (a stored cap
/// would be a state-field and format break). One or more completed HQs
/// provide the single 2.000 AE account base; every completed Storage
/// adds 2.000.
/// provide the single 3.000 AE account base (E-2 of 2026-08-31, #131);
/// every completed Storage adds 2.000.
/// Sites are excluded via the bound lookup: a half-built silo holds
/// nothing. Without the lookup (construction-free rigs) every
/// building-role entity counts. Integer scan in ascending entity
Expand Down Expand Up @@ -569,7 +585,20 @@ private void ExecuteHarvest()
/// <summary>
/// One harvest order: in reach, bounded by reserve and free cargo;
/// starts the return leg on a full cargo (field id retained), resolves
/// to idle on an exhausted field, holds out of reach.
/// to idle on an exhausted field, holds out of reach — and HOLDS
/// WITHOUT GATHERING while the owner account has no room under the
/// derived storage ceiling (#136, owner decision E-1 of 2026-08-31).
/// The brake sits at the pickup, deliberately not at the deposit:
/// what was never mined is never lost, whereas a deposit-side brake
/// would let a finite field drain into loads the capped deposit then
/// forfeits in full. "Full" means NO room at all — a single free AE
/// keeps the harvester working; how much of a delivered cargo then
/// fits remains the deposit's D-024 concern, unchanged. The check
/// re-reads the live balance and the derived ceiling on every tick
/// and stores nothing, so the harvester resumes by itself the moment
/// room exists (credits spent, storage completed) — and a harvester
/// whose cargo is already full holds its load instead of starting a
/// return leg that would bank zero and wipe the cargo anyway.
/// </summary>
private void ExecuteHarvestOrder(ref UnitState unit)
{
Expand All @@ -592,6 +621,16 @@ private void ExecuteHarvestOrder(ref UnitState unit)

if (!IsInReach(in unit, field.GridPos)) return; // held, not dropped

// #136 (E-1): the account-full hold. Gathering now would drain a
// FINITE reserve into a deposit that banks nothing (D-024 caps
// the deposit, and the deposit clears the cargo regardless), so
// the harvester stands still with order and cargo kept. The room
// read is live and stateless: the moment the balance drops below
// the derived ceiling or the ceiling rises, this same order
// gathers again without any command.
long accountRoom = CapacityFor(unit.PlayerId) - _players[unit.PlayerId].AetheriumCredits;
if (accountRoom <= 0) return; // held, not dropped — nothing is mined into the void

// The cargo ceiling is the OWNER FACTION's, not a flat constant:
// an Alliance harvester loads 330, a Legion one 300.
int cargoCapacity = CargoCapacityByFaction[(int)_players[unit.PlayerId].Faction];
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,26 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
bei 2 AE/Tick, bis eine gespielte Balance-Kalibrierung belastbare Werte gibt

### Behoben
- **Sammler ernten nicht mehr weiter, wenn das Lager voll ist (#136).** Aus der
Proberunde vom 31.08.2026: „Die ernten ab, bringen das zur Raffinerie, aber
das erhöht den Kontostand nicht. Das vernichtet Material." Genau so war es —
und seit die Vorkommen endlich sind (#80), ging dabei nicht nur die Fahrt
verloren, sondern der Rohstoff selbst. Gebremst wird jetzt beim **Aufnehmen**,
nicht beim Abliefern: was nie abgebaut wurde, ist nicht verloren. Der Sammler
hält mit Auftrag und Ladung inne und nimmt die Arbeit von selbst wieder auf,
sobald Platz ist — durch ein neues Lager oder durch Ausgeben. Drei Tests
pinnen Halt, beide Wege der Wiederaufnahme und die Grenze „1 AE Platz heißt
arbeiten". **Dass der Überschuss verfällt, bleibt unangetastet** — das ist
D-024; geändert hat sich nur, ob überhaupt noch abgebaut wird
- **Das Startguthaben passt wieder unter die Decke (#131).**
`EconomySystem.HqBaseCapacityAE` steigt von 2.000 auf 3.000 AE. Die 3.000 AE
Startguthaben kamen mit **D-077** als bewusst gesetzter Eröffnungspuffer, die
2.000er-Decke erst später mit **D-024 / Sprint 16.4** — die zweite
Entscheidung hob die erste auf, ohne dass es jemandem auffiel, und ein Drittel
des Startguthabens verdampfte in den ersten fünfzehn Sekunden. Bewusst in Kauf
genommen: das Lagergebäude wird früh weniger wertvoll. Der eigentliche Ertrag
ist der Test, den es bis heute nicht gab — **keiner sah Startguthaben und
Decke zusammen**, und genau deshalb blieb der Widerspruch drei Wochen stehen
- **Eine einseitige Änderung der Geländetabelle blieb in der CI unsichtbar.**
Die kanonische Tabelle steht notgedrungen zweimal im Repo — `GlutrinneTerrainMap`
auf der Unity-Seite und ein handgespiegelter Ausdruck in
Expand Down
Loading
Loading