Economy: base-local mining loop (mine at base, any attack harvests, scheduled sieges)

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>
This commit is contained in:
2026-06-11 14:59:51 -07:00
parent 0d259fb68b
commit e1ed08a803
13 changed files with 386 additions and 6 deletions
@@ -66,6 +66,27 @@ namespace ProjectM.Server
threat.PendingReturns = 0; // consume regardless so returns can't pile up
}
// ---- SOURCE: scheduled base sieges. A timed cadence arms a siege even with NO expedition trip, so
// the base-defense loop has stakes on its own. The first fire is one full interval out (a mine/build
// grace window); size escalates by the live wave number. All ticks wrap-safe (TickUtil.NonZero). ----
if (config.ScheduleEnabled != 0 && config.ScheduleIntervalTicks > 0)
{
if (threat.NextScheduledTick == 0 || cycle.Phase != CyclePhase.Calm)
{
// Seed, and DEFER while a siege runs, so the next scheduled siege is always one full interval
// AFTER the current one resolves -> a guaranteed calm/build window even if a siege runs long.
threat.NextScheduledTick = TickUtil.NonZero(now + config.ScheduleIntervalTicks);
}
else if (cycle.Phase == CyclePhase.Calm && threat.PendingSiegeSize == 0
&& !new NetworkTick(threat.NextScheduledTick).IsNewerThan(serverTick))
{
int wave = SystemAPI.TryGetSingleton<WaveState>(out var ws) ? ws.WaveNumber : 0;
threat.PendingSiegeSize = math.max(1, config.SizeBase + config.ScheduleSizePerWave * wave);
threat.ArmTick = TickUtil.NonZero(now + config.PostExpeditionDelayTicks);
threat.NextScheduledTick = TickUtil.NonZero(now + config.ScheduleIntervalTicks);
}
}
// ---- BOUNDED RESOLUTION: a Siege can't drag forever. Record its start; after SiegeTimeoutTicks cull
// the remaining Husks + stop spawning so CyclePhaseSystem's DefendCleared returns the base to Calm. ----
if (cycle.Phase == CyclePhase.Siege)