Docs: base-mining cohesive-loop session log + DR-031; CLAUDE.md base-local loop

Session log + DR-031 (base-local mining, any-attack harvest, scheduled base sieges, Synty asset swap) capturing the diagnosis, locked operator forks, both adversarial reviews, and the tuning knobs. CLAUDE.md: base-local loop is now the model (BaseFieldSpawnSystem + harvest region-routing + ThreatDirector Schedule source); net-neutral condensation of M7/biome/HUD reference bullets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 15:00:30 -07:00
parent 11ff043d6a
commit 35d33f12c1
3 changed files with 141 additions and 6 deletions
@@ -0,0 +1,84 @@
---
date: 2026-06-11
type: session
tags:
- session
- economy
- combat
- harvesting
- netcode
- assets
- loop-cohesion
permalink: gamevault/07-sessions/2026/2026-06-11-base-mining-cohesive-loop
---
# Base-local mining — consolidate combat + economy into one cohesive playable loop
> Operator: "A lot of work has been done on combat & some on inventory & equipment; currently you cannot mine any resources with the base. Consolidate all the changes so it's a cohesive playable loop. Also get rid of any placeholder assets for ores/turrets/anything else — you have plenty of real SM to choose from." Full [[dots-dev]] Feature track under **ultracode** (pre-code adversarial design review + post-code adversarial review, both workflowed). Locked → [[DR-031_Base_Mining_Loop_Cohesion]].
## Diagnosis — why you couldn't mine at the base
The combat half and the economy half were **structurally divorced**, and the economy half was effectively unreachable:
- **Resource nodes only spawned in the Expedition region** (`RegionMath.ExpeditionOffsetX = 1000`u) and only while a player physically stood out there (`ExpeditionFieldSystem` keys off per-player presence). **Nothing was harvestable at the home base.**
- The **only** path to the Expedition is an unsignposted walk-in `ExpeditionGate` — and **nothing in client code sends `RegionTransitRequest`** (no button, no prompt).
- Harvest was **projectile-only**, and MC-4 ([[DR-030_MC4_Combo_Melee_Primary_Verb]]) demoted ranged to the *secondary* button.
- **The hidden loop-breaker:** the ONLY wired siege source was post-expedition retaliation (`ThreatDirectorSystem` arms only on `ThreatState.PendingReturns`, set only by `ExpeditionGateSystem` on a base return). In a base-only loop nobody ever returns → **no siege ever arms → zero waves → all the dash/charger/melee combat work never triggers.**
- Everything was rendered as **placeholder cubes**.
## Locked forks (operator)
1. **Mine at the base** (not the expedition round-trip). 2. **Any attack harvests** (melee cleave AND ranged). 3. **Natural/stone-frontier art**.
## What shipped
### A — Base-local mining (server-only, reuses the node ghost; NO new replicated state)
- New `BaseFieldSpawner`/`BaseFieldRuntime` components + `BaseFieldSpawnerAuthoring` (one in the Gameplay subscene, prefab = the existing `ResourceNode.prefab`).
- New `BaseFieldSpawnSystem` (plain server group): tops the live `RegionTag{Base}` node count up to `TargetCount` (10) on a `NetworkTick` cadence; scatters **uniform-in-radius** in the annulus `[InnerRadius, OuterRadius]` (23.527) around `BaseGridMath.PlotCenter`; overrides each instance via **`SetComponent` (not Add)** to `RegionTag{Base}` + `ResourceId.Ore` (Ore-only — the build currency, no round-robin). First pass fires immediately; RNG seeded from a monotonic `Epoch` (never the tick). Runtime-spawned → dodges the prespawn handshake.
### B — Any-attack harvest
- `MeleeComboSystem`'s **server-only** cleave block now also gathers `ResourceNode`/`BlightClutter` in the cone, deposits, writes `Remaining` back (so `WorldFeedbackSystem` chips fire on melee mining), destroys at-most-once. **Region routing mirrors the projectile path:** Base node → shared `ResourceLedger` directly; Expedition/un-tagged → the swinging player's PERSONAL `InventorySlot` (via an `OwnerId`→player map), spill-to-ledger. Only deplete if the yield actually landed (no zero-credit consume).
- `ResourceHarvestSystem` (projectile) routes Base-region nodes straight to the ledger via an **optional** `ComponentLookup<RegionTag>` (NOT a required query column — that would drop the un-tagged test fixtures + Expedition clutter); default un-tagged → owner-inventory path (preserves `InventoryHarvestTests`).
- **Why base→ledger:** the build economy spends EXCLUSIVELY from the ledger (`BuildPlaceSystem`/`BuildSendSystem`/HUD all read it). Routing base harvest there makes "mine → build" work with zero `G`-deposit friction; the expedition keeps the DR-026 personal-haul.
### F — Base siege cadence (the loop-closer)
- Activated the reserved **Schedule** source in `ThreatDirectorSystem`: arms a `SizeBase + ScheduleSizePerWave*WaveNumber` siege every `ScheduleIntervalTicks` with NO expedition trip; defers `NextScheduledTick` while a siege runs (guaranteed calm/build window even on a long siege). Composes with the post-expedition source (guarded by `Phase==Calm && PendingSiegeSize==0`). New `ThreatConfig.ScheduleSizePerWave`; `CycleDirectorAuthoring` bakes `ScheduleEnabled=true`, `Interval=2700` (45s), `PerWave=1`.
### Blocker fix — ExpeditionFieldSystem teardown
- Its occupied→empty teardown destroyed EVERY `ResourceNode` unfiltered → would wipe the new permanent base field the first time the expedition emptied. Region-filtered the node destroy to `Expedition`-only (clutter stays unfiltered — there is no base clutter).
### C — Real art (swap mesh+material; ghost/authoring/LinkedEntityGroup untouched)
Mechanism: `PrefabUtility.LoadPrefabContents` → strip the root placeholder `MeshFilter`+`MeshRenderer`, reset root `LocalScale` to 1 (Scale is a `[GhostField]` propagated by spawners/BuildPlaceSystem — cosmetic scale goes on the nested child), **nest the Synty model as a "Model" child**, strip its colliders, assign the pack atlas to bare FBXs (Synty FBX import gives a blank "Lit" material — the pre-made *prefabs* carry the atlas) → `SaveAsPrefabAsset`. Uniform nest works for single- and multi-mesh (the Ballista is 7 GameObjects → a root mesh-swap would render 1/7).
| Prefab | → Synty (PolygonFantasyKingdom) |
|---|---|
| ResourceNode | `SM_Prop_Crystal_01` (crystal, +atlas) |
| Turret | `SiegeEngines/SM_Wep_Ballista_Mounted_01` |
| Wall | `SM_Prop_Spike_Wall_01` (palisade) |
| Pylon | `SM_Prop_Crystal_01` (tall beacon) |
| Storage | `Furniture/SM_Prop_Chest_01` |
| BlightClutter | `Environments/SM_Env_Rock_Chunk_01` |
| UpgradePickup | `SM_Prop_Crystal_01` (gem, floating) |
Automation machines (Harvester/Fabricator/Conveyor) **trimmed from the build palette** (catalog refs nulled on the subscene `StructureCatalogAuthoring`) — code stays, no placeholder cube ever shown. Palette is now `[Turret, Wall, Pylon]`.
### HUD
- Replaced the stale "deploy through the gate" copy with phase-aware loop guidance: Calm → "MINE THE CRYSTALS — any attack harvests Ore, then BUILD"; Siege → "DEFEND THE BASE — hold the line".
## Adversarial reviews (both workflowed under ultracode)
- **Pre-code** (6 lenses → synthesis): caught all 3 blockers BEFORE coding — the ExpeditionFieldSystem teardown wipe, the missing base siege source, and base-harvest-must-credit-the-ledger — plus the optional-ComponentLookup trap, the SetComponent-not-Add override, the measured annulus radii, the nest-not-root-swap for the 7-part Ballista. **This is why the slice landed clean.**
- **Post-code** (3 lenses → adversarial verify → synthesis): found 2 real defects I'd introduced when I simplified the melee path: (1) MAJOR — melee deposited EXPEDITION yields into the shared base ledger (no region routing); (2) MINOR — a node was consumed even with no ledger (silent loss). **Both fixed** + two new regression tests.
## Validation
- **EditMode: 302/302** (294 prior + 8 new: BaseFieldSpawn ×3, ExpeditionTeardown ×1, Schedule ×2, MeleeHarvest base/expedition ×2). Zero console errors at every compile.
- **Play (live netcode, focused editor):** server world has **10 Ore crystal nodes**, `RegionTag.Base`, radii 23.726.1; `ThreatConfig.ScheduleEnabled=1`; catalog `[Turret,Wall,Pylon]`; crystals render textured (cyan, not magenta/white) in a reachable ring; schedule sieges fire (loop cycles calm↔siege); **0 console errors/exceptions**.
- **Operator hands-on feel test is OPEN:** walk to a crystal → swing → watch Ore climb → build a ballista → survive a wave.
## Tuning knobs surfaced (sensible defaults shipped)
- **Siege cadence** `ScheduleIntervalTicks=2700` (45s). Bump to 36005400 for 6090s calm/build windows if waves feel too frequent.
- **Ore-ring placement** sits at radius ~2426 (just inside the boundary) because the 32×32 build plot fills the centre and the boundary is ~28.7 — only that thin annulus is conflict-free. If it feels edge-stuck: shrink the plot, widen the boundary ring, or allow some nodes on the grass plot (placeable-on; occupancy only scans `PlacedStructure`).
- **Siege source** chose timed **Schedule**; **Heat** (`HeatPerHarvest` — mining draws the attack) is the reserved alternative that braids the halves harder.
## Files
- New: `Simulation/Economy/BaseFieldSpawner.cs`, `Authoring/Economy/BaseFieldSpawnerAuthoring.cs`, `Server/Economy/BaseFieldSpawnSystem.cs`; tests `BaseFieldSpawnSystemTests.cs`, `ExpeditionFieldTeardownTests.cs`.
- Modified: `Simulation/Player/MeleeComboSystem.cs` (server-only node harvest + region routing), `Server/Economy/ResourceHarvestSystem.cs` (region routing), `Server/World/ThreatDirectorSystem.cs` (schedule source), `Simulation/World/ThreatComponents.cs` (`ScheduleSizePerWave`), `Authoring/World/CycleDirectorAuthoring.cs` (schedule bake), `Server/Economy/ExpeditionFieldSystem.cs` (teardown region-filter), `Client/Presentation/HudSystem.cs` (loop copy); tests `ThreatDirectorSystemTests.cs`, `MeleeComboTests.cs`.
- Assets: 7 prefabs re-meshed to Synty; `Subscenes/Gameplay.unity` (BaseFieldSpawner placed, catalog trimmed); `Prefabs/CycleDirector.prefab` (schedule baked).
## Next-session intent
Operator runs the hands-on feel test (mine → build → survive). Tune the siege cadence + ore-ring to taste. If the loop feels good, the obvious next braids are **EB-1 (machines can die)** / **EB-2 (felt spend — turret ammo from harvest)** which give the mined Ore weight, or wiring `PolygonParticleFX` into the harvest/death VFX. The expedition path remains dormant-but-functional (gather code intact); reviving it would want a signposted gate + a `RegionTransitRequest` sender.
@@ -0,0 +1,51 @@
---
id: DR-031
title: Base-local mining — mine at the base, any attack harvests, scheduled base sieges close the loop
status: accepted
date: 2026-06-11
tags:
- decision
- design
- economy
- combat
- harvesting
- netcode
- loop-cohesion
permalink: gamevault/07-sessions/decisions/dr-031-base-mining-loop-cohesion
---
# DR-031 — Base-local mining + any-attack harvest + scheduled base sieges
## Context
Session [[2026-06-11_Base_Mining_Cohesive_Loop]] · economy foundation [[DR-026_Inventory_Equipment_Progression_Foundation]] · region split [[DR-013_M6_Aether_Cycle_Region_Split]] · melee verb [[DR-030_MC4_Combo_Melee_Primary_Verb]] · world art [[DR-025_World_Environment_Redo_Natural_Frontier]].
Operator: combat got a lot of work, inventory/equipment got some, **but you couldn't mine resources at the base** — consolidate it into a cohesive playable loop and replace placeholder cube assets. Diagnosis: the loop's two halves were divorced. Resource nodes only spawned in the far Expedition region (`RegionMath.ExpeditionOffsetX = 1000`u), only while a player physically stood there, reachable solely by an **unsignposted** walk-in gate (nothing sends `RegionTransitRequest`), harvested by the MC-4-demoted ranged weapon. And the **only** wired siege source was post-expedition retaliation — so a base-only player draws **zero waves**, and all the dash/charger/melee combat never fires. Ran the standing **pre-code adversarial review** (caught 3 blockers) and a **post-code adversarial review** (caught 2 real defects, fixed).
## Decision
**1. Mining lives AT THE BASE** (operator fork; expedition becomes a dormant optional layer). A server-only `BaseFieldSpawnSystem` keeps `RegionTag{Base}` Ore nodes topped up to `TargetCount` in a `[Inner,Outer]` annulus around `BaseGridMath.PlotCenter`. Reuses the existing `ResourceNode` ghost (NO new replicated state) — a **distinct** `BaseFieldSpawner` singleton (reusing `ResourceFieldSpawner` would throw multiple-instances vs `ExpeditionFieldSystem`). Override the baked `RegionTag{Expedition}`→Base via **`SetComponent` not `AddComponent`** (Add throws; a node left Expedition-tagged is hidden from base players by `RegionRelevancy`). **Ore-only** — do NOT copy the expedition's Aether/Ore/Biomass round-robin (Ore is the sole build currency; scattering it breaks palette-affordability legibility). Determinism: RNG seeded from a monotonic `Epoch` (never the tick), cadence via `TickUtil.NonZero`+`NetworkTick.IsNewerThan`, first pass fires immediately, **uniform-in-radius** scatter (not area-weighted `sqrt` — that piles nodes on the outer wall).
**2. ANY attack harvests** (operator fork). Both the melee cleave and the ranged projectile deplete nodes. Melee harvest lives **strictly inside `MeleeComboSystem`'s server-only post-loop cleave block** (NEVER the predicted per-player foreach — interpolated node ghosts aren't rolled back on the client; harvesting there is a determinism/ownership violation + ledger double-credit risk). Writes `Remaining` back via `ecb.SetComponent` so the `[GhostField]` replicates and `WorldFeedbackSystem` chips fire on melee mining too.
**3. Harvest routing by node region (both melee + projectile):** **Base node → the shared `ResourceLedger` directly**; **Expedition / un-tagged → the harvesting player's PERSONAL `InventorySlot`** (spill-to-ledger). The build economy spends EXCLUSIVELY from the ledger (`BuildPlaceSystem`/`BuildSendSystem`/HUD), so routing base harvest there makes "mine → build" work with **zero `G`-deposit friction**; the expedition keeps DR-026's personal-haul. Read `RegionTag` via an **optional `ComponentLookup` (default missing → owner-inventory path)**, NOT a required query column — a required column silently drops un-tagged test fixtures + Expedition clutter (the 2026-06-08 partial-archetype class of bug). Only deplete a target if its yield actually landed somewhere (no zero-credit consume when no ledger exists).
**4. A base-resident siege source closes the loop** (the hidden blocker). Activated the **reserved Schedule source** in `ThreatDirectorSystem`: arms a `SizeBase + ScheduleSizePerWave*WaveNumber` siege every `ScheduleIntervalTicks` with NO expedition trip, deferring `NextScheduledTick` while a siege runs (a guaranteed calm/build window even on a long siege). Composes with the post-expedition source (guarded `Phase==Calm && PendingSiegeSize==0`). Chosen over the **Heat** source (mining accrues heat → arms siege) for predictability + zero harvest-coupling; Heat stays reserved-but-inert as the harder-braided alternative. Defaults: `Interval=2700` (45s), `PerWave=1`, baked on `CycleDirectorAuthoring` — all tunable.
**5. `ExpeditionFieldSystem` teardown region-filtered** to Expedition-only nodes — its old unfiltered destroy would wipe the new permanent base field the first time the expedition emptied (clutter teardown stays unfiltered: there is no base clutter).
**6. Real art by NEST, not root mesh-swap.** Swap each ghost prefab's visual: strip the root placeholder `MeshFilter`+`MeshRenderer`, **reset root `LocalScale` to 1** (Scale is a `[GhostField]` propagated by spawners — cosmetic scale goes on the nested child), nest the Synty model as a `"Model"` child, strip its colliders, assign the pack atlas to bare FBXs (Synty FBX import yields a blank "Lit" material; the pre-made *prefabs* carry the atlas). Nesting is uniform for single- and multi-mesh (the Ballista is a 7-GameObject hierarchy → a root mesh-swap renders 1/7). GhostAuthoring stays root-only (no phantom ghost children); `LinkedEntityGroupAuthoring` auto-collects the child. PolygonFantasyKingdom throughout (one atlas). Automation machines (Harvester/Fabricator/Conveyor) **trimmed from the build palette** — code stays, no placeholder cube shown.
## Consequences
- **The loop closes on one screen:** mine Ore at the base (any attack) → ledger fills → build Turret/Wall/Pylon (real ballista/palisade/crystal) → survive a scheduled wave → repeat. The combat work (dash/charger/melee) now actually triggers at the base.
- 302/302 EditMode (8 new); live netcode Play clean (10 Ore crystals in a reachable ring, schedule sieges firing, 0 errors). **Operator hands-on feel-test is OPEN.**
- **Two tuning forks left to the operator:** siege cadence (45s default) and the ore-ring radius (sits at ~2426m, against the boundary, because the 32×32 build plot fills the centre — only that thin annulus is conflict-free; widen by shrinking the plot / widening the boundary / allowing nodes on the grass).
- Brings forward gather→spend economy work the roadmap had gated behind the combat fun-gates ([[DR-028_Combat_Primary_Verb_Depth_First]]/[[combat-first-depth-before-breadth]]) — the operator's request was the reprioritization; the natural follow-on braids are EB-1 (machines can die) / EB-2 (felt spend) which give the mined Ore weight.
## Alternatives considered (rejected)
- **Fix the expedition round-trip instead** (signpost the gate, add a `RegionTransitRequest` sender) — keeps the region-split design but leaves the loop two-screen + less cohesive. Operator chose base-local.
- **Heat siege source** instead of Schedule — more teachable (mining draws the attack) but unpredictable + can be gamed (don't mine → no waves) and needs harvest→heat plumbing. Reserved.
- **Generalize `ExpeditionFieldSystem` with a region param** rather than a new `BaseFieldSpawnSystem` — rejected: the cadence-top-up model differs from the presence-edge model, and a shared singleton collides.
- **A `HarvestMath.Deposit` helper** to dedup the two harvest paths — rejected as over-abstraction (the shared core is ~3 lines; the resolve-owner/stackMax differs per caller). Inlined.