Operator delegated the placement call. Applied standard top-down-arena practice
rather than the first-pass ring scatter:
clear core (5u) the landing spot and dash room; props there cramp spawn
and cause dash collisions the player never asked for
band 6-13u solids live in KITING range, which is also roughly where
the diver's travelling light reaches. A prop outside the
lit radius reads as an invisible wall - the worst failure
mode in a dark game, so cover is pulled inward to where
it can be SEEN before it is hit
min spacing 2.6u enforced across clutter AND cover on ONE shared occupancy
list. This is a NAVIGATION guarantee, not an aesthetic:
enemies have no pathfinding, so gaps must stay wider than
a body. Rejection-sampled; a piece that cannot fit is
DROPPED rather than jammed into a gap movers can't pass
explosives >8u a barrel near the landing spot chains into you on spawn
Measured live, all three invariants hold:
17 solids, radius 7.89..12.95, min gap 2.79 (policy >=2.60), 0 explosives <8u
Nav re-validated on a CLEAN run: closest approach 1.40u, two enemies in melee.
Enemies cross the inward cover band and reach the player.
Correction worth recording: three intermediate readings showed "frozen movers"
and were WRONG. Teleporting the player around a long-running Play session
desyncs the predicted character controller and leaves stale AI state - the
server had the player back at (2.5,0) while I was measuring against (0,0), and
an earlier "all four frozen" reading was simply enemies that had converged and
were attacking. Only the clean, unmutated run is valid. Do not mutate server
state and then trust a movement measurement in the same session.
304/304 EditMode green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Operator at the A0 gate: "the world is very static." Part of that was a
regression from today's purge, and the audit's own signature pattern for the
third time: the system exists, the authoring is placed, and it silently
produces nothing.
Server/Economy/RoomFieldSystem.cs was deleted with the run/room FSM in
62e48a3b0. That was correct for the FSM, but the system was also the only
consumer of the ClutterFieldSpawner / CoverFieldSpawner / GeyserFieldSpawner
singletons. Those kept baking into Gameplay.unity and nothing read them:
before: Geyser 0 BlightClutter 0 (spawners configured for 14 + 2 + 3)
after: Geyser 2 BlightClutter 17
So the arena lost 14 destructible clutter pieces, 3 cover pieces and 2
periodically erupting geysers — a large share of everything that moved or
could be interacted with.
The old system could not be revived: it keyed every spawn off RunInfo,
RunRuntime, RoomPlan, RunMapMath, RegionMath and RoomTag teardown, all gone.
ArenaFieldSystem is a gym-scoped replacement that seeds ONCE at world start
and never tears down — Phase 1 is "no world, a gym", and everything
room-shaped belongs to the Phase-2 pocket generator, not here.
Contracts carried over deliberately, each load-bearing:
- baked.WithPosition, never FromPosition (FromPosition resets Scale, a GhostField)
- Geyser NextEruptTick born-correct off the live ServerTick via TickUtil.NonZero,
staggered 60 ticks per instance so eruptions desync; 0 stays "not ready"
- a 6 u keep-out ring around the arena origin (the player's landing spot)
- clutter variant 3 = the explosive hazard, 0-2 inert dressing
- a fixed arena seed: a gym wants the same arena every session
Verified live: geysers stamped 306 / 366 (staggered, Scale 1.00 preserved).
Navigation re-validated per the standing rule that adding Environment cover
can freeze movers, since enemies have NO pathfinding. First measurement showed
all four "frozen" — that was my error: they had already converged and were
attacking. Re-tested by moving the player across the arena: all four closed
23.5->20.4, 24.2->21.2, 23.9->20.9, 22.8->19.7 u through the cover field. No
soft-lock.
304/304 EditMode green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
M1 — two systems timed INTERPOLATED ghosts against the PREDICTED tick,
the exact hazard CLAUDE.md documents and the one that is invisible on
loopback:
- EnemyDangerTelegraphSystem timed the red danger cone off nt.ServerTick,
so over a real connection the dodge tell finished ~RTT/2 + interp
buffer EARLY. The cue lied.
- PlayerAnimationDriveSystem fed the same predicted tick to RemoteDriveJob
([WithDisabled(GhostOwnerIsLocal)] — i.e. interpolated teammates), so a
teammate's swing animation desynced from their damage.
Both now use the ZoneTelegraphSystem idiom. The LOCAL drive job keeps
ServerTick: the owning player really is predicted.
M12 — the RPC leak I reproduced live during the audit. Every receiver in
this project gates on RequireForUpdate over a scene-baked singleton; in a
scene without it the receiver never runs and the request entity is never
destroyed. Netcode's WarnAboutStaleRpcSystem Consume()s but never
destroys, and is compiled out of player builds — so these accumulated
silently, and worst in a shipped build.
New StaleRpcReaperSystem (server, OrderLast, no RequireForUpdate) destroys
any unconsumed request that outlived its receiving frame. Consumed
requests are left to their owner. Verified live: a planted unconsumed
request is gone within a few frames. Three regression tests pin both
halves of the contract.
Also: HealthApplyDamageSystem and ProjectileDamageSystem now filter
.WithAll<Simulate>(). They are ServerSimulation-only so it was not a bug,
but the audit's "all predicted systems filter Simulate" reassurance was
false until now — the rule is unconditional again.
The five undisposed Allocator.Temp ECBs the audit flagged all lived in
systems the purge deleted; none remain.
298/298 green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
H2 was the audit's sharpest finding: in Game.unity every player spawned
with four ability sockets pointing at SparkIds the baked AbilityDatabase
did not contain, so all four resolved to Damage=0 Range=0 Cooldown=0.
Melee and dash were the only working combat verbs in the built game.
Cause: the 5 LANTERN Sparks were added to GymSub.unity and never to
Gameplay.unity.
- Gameplay.unity's AbilityDatabaseAuthoring now carries all 9 defs (the
4 legacy ids keep their numbers; Sparks are 5-9) with their effect
prefabs. Live-verified in Play: sockets now read
Vortex 8dmg/6range/420cd, Blink 20range/1cd, Hook & Pull
15dmg/25range/120cd, Light Zone 6dmg/5range/480cd.
- Removed 6 orphaned authoring GameObjects the purge left behind in the
subscene (StorageSpawner, StructureCatalog, ItemDatabase,
SpitterProjectileConfig, BoonCatalog, MetaCatalog) and the RoomDressing
object in Game.unity — the latter is what scattered 47 Synty desert
props into the seabed murk.
- Deleted 4 now-unreferenced prefabs: EnemySpit, Pylon, Storage, Wall.
- Frame rename (M4): FrameKind.Warrior/Ranger -> Bathynaut/Harpooner,
93 identifier sites. Byte values pinned (2/3), so no ghost-hash or save
impact. The menu said "Warrior"/"Ranger" to players three weeks after
the frames were renamed in design.
- Menu now reads LANTERN / "Light is territory — co-op descent" instead
of "PROJECT M" / "Frontier colony — co-op" (the Awakening-Engine
tagline, two directions stale).
- Removed the "Replay Tutorial" button and the Settings ONBOARDING
section: both drove coach-marks DR-051 deleted. The settings fields
still round-trip so existing settings files load unchanged.
All four project scenes + all prefabs verified free of missing scripts.
Server measured at 59.6 ticks/s against a 60 Hz target. 295/295 green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 2026-08-06 audit found the shipping scene was still the abandoned
co-op-Hades game with LANTERN combat bolted on, and that a third of the
codebase was live code for a direction abandoned on 2026-07-13. Operator
chose deletion over freezing: "everything is saved in source control if
needed. I want the project to be clean."
DELETED (~140 source files, Scripts 335->231, Tests 77->43):
- Enemy variants + boss (H3). ChargerAuthoring / SpitterAuthoring /
SwarmerAuthoring were attached to ZERO prefabs, so LungeState /
SpitterState / SwarmerTag were never baked: ~272 lines of Bursted AI
passes, BossAISystem (261 lines) and the whole MixBands escalation
curve could not match a single chunk at runtime, while 734 lines of
green tests certified them. Both shipping enemy prefabs were already
byte-identical in stats.
- Run/room lifecycle: RunDirector FSM, RunInfo/RunMap/RoomPlan/RoomTag,
route select, portal interact, ready-check, room field/teardown.
- Meta shop, prep loadout, boons (incl. KillRewardSystem and
DashTrailDamageSystem, which existed only to serve boon flags).
- Build palette + structures, shared storage, inventory/equipment
(already recorded PAUSED in CLAUDE.md).
- The HUD panels driving all of the above (HudSystem 1168 -> 610).
KEPT deliberately: BaseGridMath + BaseAnchor (8 systems use PlotCenter
for spawn rings, respawn and dynamic light), the resource ledger +
StorageMath, the save system, region/relevancy. Three of these were in
the delete set until I checked their consumers — worth remembering that
the file-level manifest was wrong about them.
Also folds in audit finding M5: PlayerClass was a second, server-only
copy of the byte FrameId already replicates. It existed for the meta
shop; with that gone, FrameId is the single frame identity.
Harvest is now single-sink (ledger). HarvestMath keeps its shape so
LANTERN's carried-vs-banked cargo split lands in one place, not two.
295/295 EditMode green, zero compile errors. Subscene re-bake and Play
validation follow in the next commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GymSub bakes no WaveDirector (why sandbox enemies never spawned); the
sandbox's combat target is now a TargetDummyTag'd enemy ghost all five
EnemyAISystem passes exclude (planted; knockback stamps inert), spawned
at player+3m with 400 HP by an editor-only scene-gated server system,
dying through the normal Dying path and respawning 1.5s after the corpse.
EnemyTag-reuse audited: sandbox-only by design (cleared-checks would
count it elsewhere).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enum renamed in StatIds.cs (byte values unchanged - serialized definitions/saves
never re-mean); all call sites + doc mentions swept (ClassTraits, PlayerAuthoring,
CharacterStatsDefinition field type, menu/UI, tests). CharacterStatsDefinition SO
CLASS name kept (asset-binding risk; deferred per plan). 390 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deletes the M7 production systems, automation components/math, authoring, 3
machine prefabs, and 6 test files (-43 tests, 459 green). Trims the automation
paths out of BaseRestoreSystem/SaveStructureScan/BuildPlaceSystem/BuildSendSystem/
HudSystem/HudTheme/StructureCatalogAuthoring/Tuning. RuntimePlacedTag (save
marker, a keeper) re-homed into StructureComponents.cs. StructureType byte codes
stay reserved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per connection, marks every ENEMY ghost OUTSIDE the player's lamp radius
IRRELEVANT (monsters in the dark aren't replicated — the gamma test). Modelled
on the proven RegionRelevancySystem: SetIsIrrelevant so players/global ghosts
stay relevant for free. Shared-set discipline — RegionRelevancySystem is the SOLE
clearer (runs every tick); LightRelevancySystem is [UpdateAfter] it and only ADDs
light hides via TryAdd (no clear; defensive against a region+light overlap dup).
Gated on GymTag (LANTERN-only; legacy region relevancy untouched). Pure decision
in LightRelevancyMath (+3 EditMode tests, 500 green). Per-player dim/bright
(variable radius) + the L3 gamma-test verification are follow-ups; the ★adversarial
netcode review is recommended before this ships.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FrameId (replicated frame/class signal) now WRITTEN server-side at spawn for all
players; the two class-HUD readers (ClassPrepPortal, MetaShop) read FrameId (with
an AbilityRef fallback for pre-FrameId players) instead of ClassForAbility. The
gym seeds a PER-FRAME default Spark loadout via ClassTraits.FrameLoadout —
Harpooner (Ranger slot): reel/mobility/skillshots; Bathynaut (Warrior slot):
pull-in/dash/zone-control. FrameId uses ecb.AddComponent (baked on the real
player; absent on the minimal MetaSeeding test prefab). 497 EditMode green.
Deferred to a focused pass (risky wholesale refactor, "still works" via the
transition): full removal of legacy AbilityRef/AbilityCooldown/EffectiveAbility
Stats (woven through equip/class/meta/StatRecompute + tests) and the CharacterId
Bathynaut/Harpooner rename.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GymTag + GymEnemyRoster (+ GymRosterAuthoring) bake the gym singletons. New
DebugOp.SpawnEnemy (client SpawnEnemy wrapper + server receive case) spawns a
chosen enemy KIND (Drowner/Grindylow) from the baked roster near the sender —
replacing the wave-roster hack per the roadmap's Enemy-test GYM direction.
GoInGameServerSystem takes a GymTag branch: bypass the meta-catalog spawn guard
(a fresh gym has no CycleDirector) + seed a default Spark loadout on keys 1-4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Aoe/Hitscan archetype dispatch in AbilityFireSystem (both-worlds cooldown;
server-only ecb.Instantiate — the client-never-instantiates invariant; ZoneEffect
vs DecoyTag chosen by prefab membership). Harpooner REEL as a homing state
(ReelState + ReelSystem re-aim the hit Husk's KnockbackState toward the caster's
live position each tick; EnemyAISystem stays sole Position writer), stamped by
ProjectileDamageSystem on a Reel-flag hit. ZonePulseSystem (enemy-only periodic
AoE, caster attribution, no friendly fire, Vortex pull, born-0 lazy-stamp).
Decoy aggro in EnemyAISystem + DecoySystem despawn. AbilityDefBlob gains
EffectFlags (Reel) + DurationTicks. +3 ZonePulseSystem EditMode tests (497 green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review-first netcode slice (design review wf_900e9965-8f0 -> 11 folded;
post-impl wf_5c8299f8-299 clean). A PERMANENT periodic telegraphed AoE in
Blight-biome rooms only; one new [GhostField] uint NextEruptTick.
- Geyser component + GeyserEruptSystem (server): inverted invalid-tick guard
(a baked-0 tick must NEVER erupt -> lazy-stamps born-correct instead of the
party-wiping per-tick barrage), inline both-sides gather (SourceNetworkId=-1),
reschedule = now + period (never +=), never destroyed.
- GeyserTelegraphSystem (client): absolute-tick growing warning disc +
erupt burst on the >0->=<0 crossing, latched per NextEruptTick + arm-guard
(never edge-detects the replicated field -> no phantom on 0->stamp /
relevancy re-entry). Reuses ScorchDecalSystem + DynamicLightSystem.
- Seeded in RoomFieldSystem (Blight-gated on plan.Biome, born-correct staggered
stamp from live ServerTick), GeyserFieldSpawner + authoring wired into the
Gameplay subscene. Geyser.prefab duplicated from ResourceNode (no collider).
- BuildDisc promoted to FeedbackFx (shared with the barrel fuse ring).
- 474/474 EditMode incl. the unstamped-storm regression; live no-storm end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1.5 long-term #8, the LAST 1.5 item (design review wf_e14dd739-069:
both HIGHs + the distinct MEDs verified before the session limit ate the
duplicate verifiers; unverified leftovers reasoned-closed in the spec).
Cover = a BlightClutter ghost (Variant 4) with a baked Environment-layer
BoxCollider: the runtime ghost's collider joins BuildPhysicsWorld ->
blocks the player CC and the enemy sweep while alive, and the gap opens
the instant the entity dies. Both hit paths already break it (the
clutter chassis); 8 hits to carve.
Review folds:
- HIGH: the 07-07 anti-stuck nudge would phase enemies THROUGH cover
after ~1.5s (it exists to prevent room soft-locks). The nudge is now
COVER-AWARE: a SphereCast identifies the blocker; a live BlightClutter
ghost suppresses the phase (no soft-lock possible - the blocker is
destructible); static-geometry wedges still nudge.
- HIGH: yield==durability made tanky cover an economy faucet. Deposit
is DECOUPLED from the decrement at both hit sites: ScrapPerHit=0 =
breaks in Remaining hits, yields NOTHING; fractional POSITIVE yields
still credit >=1 (the B1 immortal-sink test pinned this - zero means
zero, positive floors to 1).
- MEDs: HitRadius 1.2 (visual core, not a shot-eating disc), NO cover
in Boss rooms (no boss depenetration backstop), scatter keep-out >=7u
from the room origin (player landing + portal), Variant=4 explicit
(the clutter block's explosive reroll cannot leak).
- Old static layer removed: 8 WorldColliders_RoomCover colliders
(subscene) + the RoomCover_Visuals root (Game.unity).
Verified live: 3 cover ghosts spawned with SOLID baked colliders;
BLOCK - NudgeUntilTick stayed 0 across 3.5s of forced zero-progress
(old code arms at 1.5s); DEPENETRATE - an enemy shoved inside a live
rock exited to 1.7u (backstop holds against ghost colliders); CARVE -
the rock died to the real sweep and the spot probed OPEN. 470/470
EditMode; console clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1.5 environmental hazard v1 (design-review wf_2cb10454-fdf: 13
findings confirmed + folded; Build Spec in the vault). ~25% of room
clutter seeds as EXPLOSIVE (Variant 3 on the existing replicated byte
- zero new GhostFields/prefabs/RPCs).
The FUSE is the review's fold - one mechanism closes both HIGHs:
- Pop sites (projectile sweep + isServer-gated melee harvest) do NOT
destroy an explosive: they zero the replicated Remaining + add a
server-only BarrelFuse (~36 ticks). The client sees Remaining=0 on a
still-alive barrel across snapshots -> unambiguous fuse cue, and
booms at despawn ONLY when cached Remaining<=0 - a teardown despawn
carries Remaining>0, so portal-advance teardowns can never fire
false booms (HIGH #1).
- HazardExplosionSystem (server, plain group, presence-gated on
BarrelFuse, never lifecycle-gated) detonates at ExplodeTick: radius
damage to LIVING enemies AND players (boss-slam player filter
verbatim; friendly fire = the bait mechanic), SourceTick stamped at
detonation = the authoring moment (HIGH #2: the authored-tick
contract dash i-frames negate against), SourceNetworkId=-1 (the
environment convention - a player id would consume Charger
whiff-punish windows). Fuse rides the RoomTag'd barrel -> teardown
cleans lit barrels free.
- Lit barrels are unhittable at both snapshot sites (no double-pop).
- Client: WorldFeedback caches Variant -> boom-vs-puff split (big
burst + light flash + boom SFX vs the old puff); DynamicLightSystem
gives Variant-3 barrels a red danger glow that STROBES once fused.
Verified: 470/470 EditMode (4 new: fused pop instead of destroy +
unhittable, both-sides damage w/ -1 source + authored tick, dead-player
+ radius filters, unelapsed-fuse inert); live smoke - seeded 3/8
explosive, real-sweep pop, CLIENT observed the fuse cue on the live
ghost, pinned bait enemy took exactly 26 (30->4), walk-out escape
confirmed; console clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The BlightClutter destructible-ghost chain (per-room seeding, unified
DR-018-safe sweep, melee + projectile breaking, personal-inventory
drops) already existed end-to-end - the roadmap item's gap was FEEL:
- Visual: prefab Model mesh swapped SM_Env_Rock_Chunk_01 (read as
scenery) -> SM_Prop_Barrel_01 (FantasyKingdom, same proven atlas
material). Base-pivot barrel offset -1 on the Model child (scatter
y=1 is the capsule plane - the base-pivot float gotcha).
- One-hit pop: prefab Remaining 8 -> 2 (both hit paths decrement by
max(1,(int)ScrapPerHit)=2, so one swing/shot shatters); authoring
default kept in sync.
- Density: MaxClutterPerRoom 6 -> 8.
Verified live: 8 barrels seeded in a combat room, grounded (screenshot);
an injected projectile through the real ResourceHarvestSystem sweep
popped one barrel in ONE hit (8 -> 7) and credited 2 Biomass; 466/466
EditMode, console clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New Server/PlayerResolve.TryResolve single-sources the RPC
SourceConnection -> NetworkId -> conn->player map resolve (3 sites:
ClassSelectReceive, PrepPurchase, DebugCommandReceive); EntityManager
reads keep it source-gen-safe from Bursted receivers.
- ecb.Dispose() after Playback in 9 Temp-ECB systems (explicit-lifetime
hygiene).
- The TuningConfig.GetOrDefault(ref state) variant of this tail was
REVERTED: state.GetEntityQuery in OnUpdate trips the Entities
"creates a query during OnUpdate" diagnostic per system per world
(caught in Play smoke) - the SystemAPI.TryGetSingleton idiom is
already source-gen-optimal, confirming the original B4 deferral.
Verified: 466/466 EditMode green on the final tree, console clean,
Play smoke 0 errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CycleDirectorSpawnSystem: drop the dangling <see cref="CyclePhase.ExpeditionTicks"/> (deleted const in B2) -> plain 'initial phase delay'.
- BuildSendSystem: remove the stale '(Conveyor uses s_ConveyorDir for facing)' comment (field + hotkey deleted in B2).
Comment-only; 459/459 EditMode tests pass. Adversarial verification round (2 agents over the full diff) found no behavior regressions and no false-confidence tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete (unreferenced in any scene, per operator decision):
- TrainingDummy* chain (authoring/spawner/system/components/prefab) + UpgradePickup* chain (authoring/spawner/systems/components/prefab); remove TrainingDummyTag from the 3 live combat queries (AbilityFire/Melee/HealthApplyDamage now key on EnemyTag) and repoint HealthApplyDamageSystemTests to EnemyTag.
- Dead RPC RegionTransitSystem + RegionTransitRequest (no sender, ungated) + its test.
- Heartbeat + HeartbeatSystem (no WorldSystemFilter, ran no-op every tick) + its test.
- BuildSendSystem dead conveyor/pylon dev hooks (s_ConveyorDir, [/] rotation, Place{Pylon,Harvester,Conveyor} statics).
- Deprecated CyclePhase alias consts (Expedition/Defend/Build/*Ticks); repoint WaveSystemTests to Siege/Calm.
- Unread FeelConfig fields (HitFlashDurationMs, RumbleHeavy) + dangling HudUi doc-comments + stale crefs.
451/451 EditMode tests pass. CLAUDE.md HeartbeatSystemTests example ref to be repointed in B7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Restore swallowed [Test] on EnemyAIMathTests.SlideVelocity_DegenerateNormal_DeflectsToTangent (dead regression guard for the shipped enemy-stuck-on-cover fix; now runs + passes).
- Extract shared HarvestMath.DepositYield used by ResourceHarvestSystem + MeleeComboSystem; fixes melee mining silently ignoring per-item StackMax (it hard-coded DefaultStackMax) and hoists the melee ledger-buffer fetch out of the per-target loop.
- StatMath.Apply switches on the raw byte (case (byte)ModOp.X) instead of casting to the enum inside the Bursted fold — removes the latent cross-assembly enum-in-Burst ICE trap.
- Add [Min] guards on CycleDirectorAuthoring loss-critical ints (CoreIntegrityMax>=1 so a mis-authored 0 can't bake an instant-loss core; siege sizes >=0).
456/456 EditMode tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
B1 SEPARATION: pairwise soft-collision pass INSIDE EnemyAISystem (sole
enemy-Position writer preserved; O(n^2) fine at MaxAlive<=14). Knocked/
lunging enemies keep committed motion but still push neighbours; the boss is
never pushed; enemies yield a small personal radius around players (below
melee range per review B1-1); displacement goes through the swept move so
nothing shoves through walls. SeparationMaxSpeed = live knob.
B2 POISE: the windup-cancel lived in the knockback CONTINUE branches (grunt/
charger/spitter). Threshold on the EXISTING KnockbackState.Speed channel:
light melee (6) nudges without interrupting; the finisher (10.8) and cone
(8) stagger as before. StaggerKnockbackSpeed = live knob (default 7).
B4 BOSS LUNGE: a telegraphed gap-closer on its own cooldown when the target
sits outside slam reach (repositioning - the slam stays the damage beat).
BossState.PendingAttack (server-only byte) disambiguates the shared windup
elapse (review-confirmed: naive reuse would SLAM on a lunge elapse);
LungeState.UntilTick spans windup+travel so the existing IsLunging ghost bit
replicates the tell, and the client draws a travel wedge instead of the slam
ring while it is set.
B5 PARTY HP SCALE: boss Health x(1 + 0.75/extra LIVING expedition player) at
spawn (not RunParticipant - dead-respawned members park at base). Max is a
GhostField so the bar stays truthful.
B6 RUN-FAILED BANNER: silent wipes used to land players home with zero
explanation. Client detects the (in-run)->Staging edge with a launch-cached
Charge (NEVER Returning - a 1-tick transient that precedes the bank by a
tick) and shows EXPEDITION FAILED for 6 s.
B7 FIRE ANIM: the class ability finally moves the body - a stateless window
from replicated AbilityCooldown.NextFireTick minus derived CooldownTicks
(works for predicted local + interpolated remotes, no cached edges).
456/456 EditMode; live Play smoke: launch -> 5 kills credit the room clear
through the corpse window -> boon window opens -> corpses expire clean, no
exceptions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Server: HealthApplyDamageSystem marks EnemyTag Dying{UntilTick} (TickUtil.
NonZero, ~54 ticks) on the lethal 0-crossing instead of instant destroy,
zeroes every live cue (replicated AttackWindup, LungeState + IsLunging bit,
KnockbackState), destroys on expiry; plain-world tests keep instant destroy
(no NetworkTime) so the suite's assertions stay meaningful.
Every consumer now ignores corpses (all confirmed entity-count/unfiltered by
the design review): EnemyAISystem all 4 passes, BossAISystem brain + summon
cap, RoomEnemyDirector room-clear + MaxAlive fit, WaveSystem cap + cleared,
CyclePhaseSystem breach wipe + DefendCleared, ThreatDirector timeout cull,
TurretFire targeting, CoreDamage drain, ProjectileDamage snapshot (corpses
are not shields), AbilityFire auto-aim candidates, debug cull + telemetry.
Wipe passes skip Dying to avoid cross-ECB double-destroys.
Client: EnemyAnimationDriveSystem finally drives the controller's IsDead
param (Health.Current<=0 is the replicated death read; corpse locomotion
zeroed); CombatFeedbackSystem moves the kill CRUNCH to the 0-crossing (the
prune-edge timing would land it ~1 s late) - the prune keeps a small
dissolve puff for corpses and the legacy full read for alive-vanish culls.
456/456 EditMode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The center-top phase banner read the CyclePhase label ("AT BASE") inside
expedition rooms - now shows ON EXPEDITION whenever a run is past Staging.
- The location-line switch had no RoomExplore case, so "choose your boon"
stuck through the whole loot window - added the portal-steer line.
- Storage crate floated 1u: GridOrigin.y=1 is the CC capsule-CENTER plane;
CellToWorld returns it, and the crate's mesh pivot is at its base (Turret/
Wall pivots are centered, which is why structures look grounded). The
container now sits on the terrain; same grounding for the portal beacon,
whose pos.y was the capsule plane too.
- Beacon HDR 2.6/3.4 bloomed into a white egg that swallowed the prompt -
tamed to 1.2/1.55 + slimmed the pillar.
456/456 EditMode before and after; Play smoke: crate at y=0, banner correct
at Staging.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Portal-gated rooms: new RunLifecycle.RoomExplore loot window; room+nodes
persist past the last kill; PortalInteractRequest -> server-only PortalCommand
advances (client derives the portal pos). RunDirectorSystem moves teardown off
the InRoom edge, relocates the boss-branch/route-gate into the RoomExplore exit,
and advances an empty expedition immediately (incl. a boss clear).
Class-at-base via a shared ClassSwapUtil (meta-band strip + per-class
MetaTierState replay, so the base RPC and the dev SetClass can't drift);
ClassSelectRequest Staging-gated. Per-run PREP buffs (PrepPurchaseRequest,
PrepCatalog): once-per-run == the row's prep StatModifier band is present,
TotalOf-before-Withdraw atomic, stripped on Returning beside the boon band.
Health.Max promoted to [GhostField] (the one ghost-hash re-bake) so the boss +
floating enemy HP bars read it directly. Feel: melee cone connect-thunk (C3),
hit-stop throttle so a horde wipe can't stutter (C4), Spitter cornered-hold.
456/456 EditMode; two adversarial reviews (pre-code 13-confirmed folded;
post-impl clean bar the RoomExplore boss-clear dead-time, fixed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BossState (server-only) + BossAISystem: sole mover, seek -> telegraphed
radial slam (AttackWindup) -> phase-2 speed + swarmer summon. Excluded from
the EnemyAISystem Charger pass; knockback-immune at all 3 stamp sites;
arena-anchored spawn; scaled Health/HitRadius/AttackRange. Grunt windup now
commits in its last ~30% (whiffs out-of-range); Charger stagger roots; grunt
speed 4.2->5.2; swarmer telegraph synced. SweptMove + zone-enemy spawn-stamp
extracted to shared helpers. Zero new [GhostField].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Operator: turrets were "super duper cheap", spammable "unlimited", "spaced weirdly", "massive and not textured".
All four were real and independently rooted (placement grid was actually fine).
- Cost: TurretCostOre 10 -> 40 (authoring default + the serialized Gameplay subscene value, which overrides the
code default). A node yields 30 Ore, so a turret is now ~1.3 nodes instead of 1/3 of one.
- Cap: new Tuning.TurretCap=6, enforced server-authoritatively in BuildPlaceSystem (count live Base turrets while
building the occupancy set; reject placement at the cap, same-tick-safe). Was unlimited.
- Model: the 1.6x Synty ballista (~5m on a 1m cell, clipping neighbours) scaled to 0.8 to fit one cell; the C5
BoxCollider shrunk to match (0.8x1.2x0.8, center y 0.6); all 6 sub-renderers swapped off the flat untextured
teal Mat_StructureOwned_Cyan to the Synty atlas PolygonFantasyKingdom_Mat_01_A (textured). Play-verified
TurretCost=40 Ore / cap=6 baked; no exceptions.
Also fixes 3 EditMode tests that pinned the old dash knobs (the prior tuning commit changed iframe 12->14 /
cooldown 45->36 but I committed it without re-running tests): DashSystemTests now derives the expected dash speed
from TuningConfig.Defaults() (robust to future tuning) + asserts now+14/+36; TuningConfigTests pins the new
defaults. 390/390 EditMode green.
Investigation: wf_c6c87dc5-9c3 (turret lane). Operator fork: 40 Ore + cap 6 (stricter).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PlayerRespawnSystem teleported a recovered player to base coords but never reset its server-only RegionTag
(every other region-mover flips RegionTag + Position together). So dying ON an expedition left you at base
still tagged Expedition: GhostRelevancy hid all base ghosts from you, base enemies ignored you, and the
expedition field/zone-director kept counting you as "still out there" (waves never stopped). No self-recovery.
- PlayerRespawnSystem: add RefRW<RegionTag> to the recovery query + set Region=Base alongside the reposition.
- Harden: drop the hard RequireForUpdate<PlayerSpawner> (a transiently-missing spawner could strand dead
players downed forever) -> TryGetSingleton with a BaseAnchor fallback, early-return only if both are absent.
- PlayerRespawnSystemTests: add RegionTag to the harness + a regression (expedition death -> respawn at base
with RegionTag reset to Base). 390/390 EditMode.
Investigation: combat-overhaul workflow wf_c6c87dc5-9c3 (death lane). Base-death case was already correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Player-built structures now physically block enemies (husks walked straight through walls before). Dedicated
"Structure" physics layer (slot 9) so the player passes its own walls while enemies are stopped:
- New WorldCollisionConfig.StructureMask, baked from the "Structure" layer in WorldCollisionAuthoring (mirrors
EnvironmentMask). EnemyAISystem ORs it into the movement sweep filter (CollidesWith = envMask | structMask) —
no new system, same 1-2 SphereCasts per enemy.
- Wall/Turret/Pylon prefabs get a cell-sized BoxCollider on the Structure layer (Wall's existing one relayered).
- Physics matrix: Default x Structure unchecked, so the kinematic player CC (Default) passes its own walls while
the enemy's explicit cast still hits them. Despawn frees collision for free (collider dies with the entity).
Play-verified baked filters: StructMask=512; structure colliders BelongsTo=512, CollidesWith=0xFFFFFFFE
(includes Environment for the enemy cast, EXCLUDES bit 0 so the player passes). 389/389 EditMode, no exceptions.
Server-only/static colliders -> deterministic, no client divergence. SaveData stays v5.
Phase C complete (C5-C7). A visual fun-gate (husk stops at wall, player walks through) is the operator's eyes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scoping/design-gated (wf_7c5a555e-136). Fixes "the base reads as inert after Phase A":
- C7b objective readout: new replicated ExpeditionObjective{[GhostField] byte State, short Remaining} on the
untagged CycleDirector ghost (cross-region safe). Sole writer ZoneEnemyDirectorSystem, written ABOVE its
early-returns (snapshot-above-early-return) so the HUD never freezes stale. Play-verified it replicates
server->client.
- C7a gate prompt + C7b HUD readout: HudSystem shows "GO TO THE EXPEDITION GATE" / "EXPEDITION IN PROGRESS - N
remaining" / "CLEARED - return to claim", below the siege/overrun overrides.
- C6a Aether upgrade button: un-gated BuildSendSystem.UpgradeAbility (was #if UNITY_EDITOR); HudSystem adds a
MenuUi.Button with live affordability tint (the only Aether sink was U-key only).
- C6c cold-start seed: CycleDirectorSpawnSystem seeds Tuning.StartingOre (50) into the ledger on a NEW game only
(born-correct, pre-Playback), killing the silent turret-before-fabricator deadlock. Play-verified seededOre=50.
- C6b Biomass sink: Wall cost Ore->Biomass (the dead currency now has a home). Play-verified WallCostRes=Biomass.
- C6d palette declutter: hide dead Pylon/Harvester/Conveyor from the build palette + trimmed their dev hotkeys
(catalog/prefabs stay baked, code-intact per DR-020).
389/389 EditMode + clean netcode Play smoke (ghost re-hash OK, no exceptions). SaveData stays v5.
C5 (walls block enemies) is the remaining Phase C item, sequenced separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design-review-gated (wf_ebef4e81-dba, GREEN-WITH-CHANGES). The win-driver moves
from "survive N base sieges" to "clear N expeditions". The review overturned the
literal plan: credit on RETURN, not at the clear edge (clear-edge crediting arms
the undefended final base siege -> uncontestable terminal Loss).
- ExpeditionGateSystem: now the sole production writer of GoalProgress.Charge —
a clamped +1 per cleared expedition folded into the existing once-per-epoch
reward block, reusing the LastRewardedEpoch latch (Ore + Charge share fate) +
a SaveRequest checkpoint. No new latch, no new GhostField, no ordering change.
- CyclePhaseSystem: deleted the survived-siege +1 (the AFK win path). Victory
latch unchanged; GoalReached still arms the final base siege at cap.
- CycleDirectorAuthoring + CycleDirector.prefab: ScheduleEnabled baked OFF
(retaliation-only). A serialized prefab bool ignores the C# field initializer,
so the value is flipped in the prefab, not just the code default.
- Tests: re-pointed CyclePhaseSystemTests + EndgameWinLoseTests survived-siege
assertions; extended ExpeditionGateRewardTests (+1, no-double-credit, clamp).
389/389 EditMode green; clean netcode Play smoke (no sort-cycle, Schedule=0).
SaveData stays v5. Docs: DR-042 build record + forks resolved, CLAUDE.md
base-loop line, Backlog (A done).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the Combat Depth slice on top of the MC-2 server spine (56cf60cce):
MC-3 impact juice (client, observe-only):
- 7 FeelConfig fields + ResetDefaults; magnitude-scaled player-dealt-hit camera
PunchFov on the enemy-Health-decrease edge (camera-only hit-stop, never timeScale).
- Spitter Kind==2 aim-LANE telegraph (BuildLaneMesh) — reads baked SpitterState
client-side, falls back to a fixed length. True freeze + material flash deferred.
Content / wiring:
- SpitterProjectilePrefabAuthoring (the SpitterProjectilePrefab singleton).
- Both directors rebuilt to a 4-entry KIND-INDEXED roster [Grunt,Charger,Spitter,
Swarmer] + mix/MaxAlive config + the SpitterProjectileConfig singleton in the subscene.
- Real rigged models: EnemySpitter (re-skinned Kaiju, ranged poker) + EnemySwarmerUndead
(Undead-Werewolf, fast/low-HP); grunt/charger keep Werewolf/ChargerMuscle. EnemySpit =
ownerless interpolated ghost (no Health, no collider).
Post-impl adversarial review fixes (wf_febdcfdb-665):
- [MED] in-band fire gate: the Spitter committed its telegraph from ANY range (fired while
advancing from far). Now commits only when sInBand || sCornered (gives CorneredRange a
real read site) — a Spitter out-of-band holds fire and repositions.
- [LOW] EnemyProjectileDamageSystem early-returns on !ServerTick.IsValid (sibling parity).
- [LOW] EnemyAuthoring bake-time guard: errors if a prefab composes both Charger+Spitter
(would match zero AI passes -> never move).
- [LOW] tests: Spitter brain fires from Expedition (kills the Base==0 region false-green);
a direct partition-exclusion test replaces the order-masked claim; added out-of-band +
cornered negative tests.
388/388 EditMode green + two Play smokes (clean boot, fire, swept-hit, region, server==
client; rigged Kaiju spitter bakes + fires with zero console errors). Accepted as-is
(documented in DR-041): global spit soft-cap, co-op punch attribution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the server-authoritative mechanics for three new enemy archetypes on top of
the Grunt/Charger base, plus the weighted wave-composition that introduces them:
- Spitter: a ranged Husk variant (SpitterState) that holds a preferred range-band
(advance/retreat/hold via EnemyAIMath.BandVelocity) and fires a telegraphed,
dodgeable EnemyProjectile. New server EnemyProjectileMoveSystem (integrate +
store LastStep) + EnemyProjectileDamageSystem (region-filtered swept hit-test
rebuilt from LastStep — DR-018 anti-tunnelling; players use HitRadius, structures
a const radius; at-most-once destroy). Concurrent-spit soft cap, soft-fail retry.
- Swarmer: marker tag + deterministic cluster spawn (1 slot = 1 pack;
EnemyAIMath.ClusterOffset), MaxAlive counts ENTITIES so a pack defers if it
won't fit.
- 4-type weighted mix: MixBands -> ZoneEnemyMath.WaveSlots/KindForSlot/
PackSizeForSlot drives both the expedition director and (fork-4a) the base siege,
with a mandatory MaxAlive cap. Legacy WaveSize/IsChargerSlot kept + parity-tested.
- Discriminator stays component-presence (no enum in Bursted systems): query-
partition guards keep each enemy moved by exactly one EnemyAISystem pass
(sole-Position-writer). EnemyTelegraph.IsCharger -> Kind byte for the client cue.
New authoring (Spitter/Swarmer/EnemyProjectile) + expanded director authorings with
tunable mix/cluster defaults. 13 new EditMode tests (mix composition + legacy parity,
band/cluster math, projectile move + cross-region + swept anti-tunnelling regressions);
full suite green before commit.
Dormant until the prefab/subscene wiring lands (next): the new systems guard on
TryGetSingleton/RequireForUpdate, so with no prefabs wired the new types stay inert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reactivate the dormant Expedition region as a procedural combat venue.
v1 loop: walk the gate -> fight an epoch-seeded enemy wave in the
expedition -> clear -> return -> flat Ore reward (once per epoch) ->
escalated retaliation base siege.
- New sim types: ZoneEnemyTag, ZoneEnemyDirector (+ ZoneEnemyPrefab
buffer), ZoneEnemyState, ZoneEnemyMath (grunt->charger composition
by epoch). ZoneEnemyDirectorSystem (server, Burst): drip-spawns the
wave at a deterministic ring under a MaxAlive cap while a player is
out and the base is Calm; marks ClearedThisEpoch on a real clear.
[UpdateAfter(ExpeditionFieldSystem)] only (avoids a sort cycle).
- BLOCKER 1: EnemyAISystem region-filters target selection (player +
structure snapshots gain parallel region lists; no base structures /
no Core fallback for expedition husks).
- BLOCKER 3: WaveSystem, ThreatDirectorSystem timeout cull, and
CyclePhaseSystem DefendCleared + Core-breach cull all count/cull
RegionTag{Base} husks only (the breach cull was caught region-blind
by the post-impl review: a base breach wiped the live expedition
wave and spuriously paid the reward).
- BLOCKER 4: reward de-duped via CycleRuntime.LastRewardedEpoch +
ClearedThisEpoch; ExpeditionGateSystem deposits RewardOre once/epoch.
- ExpeditionFieldSystem teardown also culls zone enemies + region-
guards the clutter loop. Subscene wired with the director + roster.
368/368 EditMode green + clean netcode Play smoke. Docs: DR-040 ->
built, session log, CLAUDE.md cross-region tag-reaudit rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Editor-only class swap via the existing scalar dev-RPC family (new DebugOp.SetClass): F1/F2 keybind (ClassSwitchHotkeySystem), DebugOverlay '- Class -' buttons, and DebugCommandSendSystem.SetWarrior/SetRanger/SetClass statics. Server (DebugCommandReceiveSystem) swaps class in place on the spawned player: strips+re-seeds the ClassTraits StatModifier seeds, swaps the AbilityRef Fire slot, resets the ability cooldown, and heals a LIVING player to the new max (dead players skip the heal so respawn isn't raced). Server-authoritative + prediction-correct (same buffer-mutation path as GrantUpgrade); wire type unchanged so the RpcCollection hash is unaffected.
ClassTraits gains a shared Seeds core (spawn + swap can't drift), ClassSeedCount, IsClassSeed, a DynamicBuffer AppendSeeds overload, and Reapply. +3 EditMode tests (exact-count round-trip, value-equality fold, boundary/foreign-mod preservation); 351/351 green; Warrior<->Ranger round-trip Play-validated (server+client agree).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-player class travels on GoInGameRequest.ClassId (client reads a ClassSelection
singleton); GoInGameServerSystem seeds the class at spawn via ClassTraits (AbilityRef +
permanent trait StatModifiers on a reserved ClassSourceId; CharacterStatsRef stays Default
so the DRG-asymmetry deltas ride the replicated OwnerSendType.All buffer). AbilityFireSystem
gains the aim-directed Cone archetype: cooldown predicted both worlds, server-only cone
damage to living enemies (same-tick, SourceTick-stamped, like the melee cleave). 345/345.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Four playtest do-now wins:
- Enemy health bars: pooled world-space Canvas, on-damage-sticky + fade,
always-on <25% HP (CombatFeedbackSystem; no new replication).
- Telegraph fix: new baked client-safe EnemyTelegraph sizes the danger-cone ramp
per enemy (0->1 ending at impact, fixes the Charger plateau); windup 18->22;
a windup scale-pulse.
- Build-mode toggle: BuildPaletteState.PaletteOpen hides the palette by default,
Tab / gamepad-Y toggles, with a discovery chip (HudSystem/BuildSendSystem).
- Charger committed-lunge tell: [GhostEnabledBit] IsLunging derived once/tick from
LungeState (the Dead idiom); the danger cone persists through the lunge.
345/345 EditMode (+3 IsLunging derive tests); Play-validated: ghost-hash change
did not break the handshake, bake correct (telegraph on all enemies, IsLunging
baked-disabled on the Charger, replicated to client), no runtime errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
At GoalProgress.Charge>=Target a new server-only GoalReachedSystem arms a larger final siege (x live FinalSiegeMultiplier) and flips RunPhase=FinalDefense; CyclePhaseSystem latches a REPLICATED RunOutcome (Victory on clear / Loss on Core breach) and halts the director. RunOutcome is a [GhostField] byte on the global CycleDirector ghost (the client banner observes it); RunPhase stays server-only. ThreatDirector/CoreRestore/CoreDamage halt once decided; SiegeTimeout is off during the final siege. SaveData v5 persists the outcome so a won/lost run loads finished. GoalProgress.Target 10->4. Completes Path A's spine. See DR-036.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds CoreIntegrity{[GhostField] Current,Max,OverrunTick} on the GLOBAL
CycleDirector ghost (no new ghost/relevancy). CoreDamageSystem (server,
after EnemyAISystem): a Husk within ~3u of PlotCenter drains + is consumed;
CoreRestoreSystem regenerates only in Calm. The SOFT-loss edge lives inside
CyclePhaseSystem (sole Phase writer): Current<=0 in Siege flips to Calm with
NO goal reward, StorageMath.DrainFraction drains the shared ledger, all Husks
despawn, and OverrunTick is stamped (a transient HUD-flash pulse, not a
latching outcome - the Victory latch is END-2's). EnemyAISystem treats the
Core as a FALLBACK target so an undefended base is overrun instead of idling.
SaveData -> v4 persists CoreCurrent (0 -> born full, the EB-1 HP sentinel);
3 live TuningConfig knobs + a red HUD Core bar. Soft-loss + targeting +
breach-resolution forks operator-locked.
See DR-034.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mined Ore now has an ongoing sink: a ledger-fed Fabricator converts Ore->Charge
(1 Ore -> 3 Charge / 30t) and turrets spend Charge per shot, soft-failing (no
shot, no cooldown burn) when the shared pool runs dry.
- ResourceId.Charge=4 rides the existing [GhostField] StorageEntry ledger (no new wire).
- TurretFireSystem: single ledger resolve + atomic spend / soft-fail / partial-refund.
- Fabricator.InputFromLedger (byte, server-only) feeds input from the shared ledger,
read live in-loop so two machines split a finite pool; both modes deposit to ledger.
- HudSystem: violet Charge chip + global quiet-turret cue when siege && Charge==0.
- StorageMath.TotalOf backs the affordability read; catalog re-enables the Fabricator (4 entries).
See DR-033.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Structures (Turret/Wall/Pylon) reuse the combat spine: authoring bakes Health(GhostField)+DamageEvent buffer+a Destructible tag (no HitRadius -> no friendly projectile fire; no EffectiveCharacterStats -> clamp-to-0). HealthApplyDamageSystem destroys a Destructible at 0 (occupancy auto-frees). EnemyAISystem fortress-targets the weighted-nearest of players+structures via the shared EnemyAIMath.PickWeightedNearest (StructureAggroWeight TuningConfig knob, <1 prefers structures, squared factor; snapshot above the early-return so an undefended base is razed). Persistence v3: per-structure HP threaded through 5 sites (SaveData/PendingStructure/scan-guarded/BaseRestore same-ECB born-correct/WorldLauncher via SaveApply.ToPending); SaveService floor-gate [2,3] loads old saves. Loss feedback: proximity-gated StructureFeedbackSystem; CombatFeedbackSystem suppressed for structures. Pre-code review caught the DamageEvent-buffer crash blocker + 8 majors; post-code review clean. See DR-032.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate the divorced combat + economy halves into one base-local loop. BaseFieldSpawnSystem tops up RegionTag{Base} Ore nodes around the plot; harvest routes Base->shared ledger and Expedition/untagged->personal inventory for BOTH the projectile (ResourceHarvestSystem) and melee (MeleeComboSystem server-only block, writes Remaining back for VFX). Activate the reserved Schedule source in ThreatDirectorSystem so base sieges arm WITHOUT an expedition trip (the loop-closer: previously zero waves ever attacked a base-only player). Region-filter the ExpeditionFieldSystem teardown so it no longer wipes the permanent base field. HudSystem shows phase-aware loop copy. See DR-031.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>