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
@@ -35,6 +35,7 @@ namespace ProjectM.Server
ComponentLookup<GhostOwner> m_GhostOwnerLookup;
BufferLookup<InventorySlot> m_InvLookup;
ComponentLookup<RegionTag> m_RegionLookup;
[BurstCompile]
public void OnCreate(ref SystemState state)
@@ -43,6 +44,7 @@ namespace ProjectM.Server
state.RequireForUpdate<ResourceLedger>();
m_GhostOwnerLookup = state.GetComponentLookup<GhostOwner>(true);
m_InvLookup = state.GetBufferLookup<InventorySlot>(false);
m_RegionLookup = state.GetComponentLookup<RegionTag>(true);
}
[BurstCompile]
@@ -56,6 +58,7 @@ namespace ProjectM.Server
// hoisted out of the per-hit sweep (invariant for the tick).
m_GhostOwnerLookup.Update(ref state);
m_InvLookup.Update(ref state);
m_RegionLookup.Update(ref state);
bool haveDb = SystemAPI.TryGetSingleton<ItemDatabase>(out var itemDb);
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
@@ -72,6 +75,7 @@ namespace ProjectM.Server
var tgtYieldPerHit = new NativeList<float>(Allocator.Temp);
var tgtVariant = new NativeList<byte>(Allocator.Temp);
var tgtIsClutter = new NativeList<bool>(Allocator.Temp);
var tgtToLedger = new NativeList<bool>(Allocator.Temp);
foreach (var (xform, hr, node, e) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<HitRadius>, RefRO<ResourceNode>>().WithEntityAccess())
@@ -84,6 +88,7 @@ namespace ProjectM.Server
tgtYieldPerHit.Add(node.ValueRO.HarvestPerHit);
tgtVariant.Add(0);
tgtIsClutter.Add(false);
tgtToLedger.Add(m_RegionLookup.HasComponent(e) && m_RegionLookup[e].Region == RegionId.Base);
}
foreach (var (xform, hr, clutter, e) in
@@ -97,6 +102,7 @@ namespace ProjectM.Server
tgtYieldPerHit.Add(clutter.ValueRO.ScrapPerHit);
tgtVariant.Add(clutter.ValueRO.Variant);
tgtIsClutter.Add(true);
tgtToLedger.Add(m_RegionLookup.HasComponent(e) && m_RegionLookup[e].Region == RegionId.Base);
}
var destroyed = new NativeArray<bool>(tgtEntity.Length, Allocator.Temp);
@@ -144,7 +150,10 @@ namespace ProjectM.Server
// firing player's GhostOwner (AbilityFireSystem); the owner is read OPTIONALLY (cached lookup) so
// an un-owned projectile (or a test projectile with no GhostOwner) falls through to the ledger.
int remainder = amount;
if (m_GhostOwnerLookup.HasComponent(projEntity)
// Base-region nodes credit the SHARED ledger DIRECTLY (the build currency pool); expedition / un-tagged
// nodes keep the personal-inventory reroute (spill-to-ledger). Untagged -> not Base -> inventory path.
if (!tgtToLedger[bestIdx]
&& m_GhostOwnerLookup.HasComponent(projEntity)
&& playerByConn.TryGetValue(m_GhostOwnerLookup[projEntity].NetworkId, out var player)
&& m_InvLookup.HasBuffer(player))
{
@@ -208,6 +217,7 @@ namespace ProjectM.Server
tgtYieldPerHit.Dispose();
tgtVariant.Dispose();
tgtIsClutter.Dispose();
tgtToLedger.Dispose();
}
}
}