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
@@ -0,0 +1,51 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the home-base mining field (<see cref="BaseFieldSpawner"/>). Place ONE in the gameplay
/// subscene. <see cref="NodePrefab"/> = the SAME ResourceNode ghost prefab the expedition uses; the server
/// system overrides each instance to RegionTag{Base} + ResourceId.Ore and scatters them in the
/// [<see cref="InnerRadius"/>, <see cref="OuterRadius"/>] annulus around the base plot center. Defaults are
/// sized to the baked 32x32 plot (square corner reach ~22.6) inside the ~28.7 boundary ring, so nodes form a
/// reachable perimeter ring that never sits on a build cell.
/// </summary>
public class BaseFieldSpawnerAuthoring : MonoBehaviour
{
[Tooltip("Resource-node ghost prefab (ResourceNodeAuthoring + GhostAuthoring). Reuse the expedition node prefab.")]
public GameObject NodePrefab;
[Tooltip("Live base-node target; the field refills toward this each respawn pass.")]
[Min(1)] public int TargetCount = 10;
[Tooltip("Inner scatter radius — clears the build plot corner reach (~22.6) + spawn ring.")]
[Min(0f)] public float InnerRadius = 23.5f;
[Tooltip("Outer scatter radius — stays inside the walkable boundary ring (~28.7).")]
[Min(0f)] public float OuterRadius = 27f;
[Tooltip("Server ticks (@60) between top-up passes.")]
[Min(1)] public int RespawnIntervalTicks = 600;
private class BaseFieldSpawnerBaker : Baker<BaseFieldSpawnerAuthoring>
{
public override void Bake(BaseFieldSpawnerAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.None);
AddComponent(entity, new BaseFieldSpawner
{
Prefab = authoring.NodePrefab != null
? GetEntity(authoring.NodePrefab, TransformUsageFlags.Dynamic)
: Entity.Null,
TargetCount = authoring.TargetCount,
InnerRadius = authoring.InnerRadius,
OuterRadius = authoring.OuterRadius,
RespawnIntervalTicks = authoring.RespawnIntervalTicks,
});
AddComponent(entity, new BaseFieldRuntime { Epoch = 0, NextSpawnTick = 0u });
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c4055a6a779d06949ae23b16334b810e
@@ -30,6 +30,16 @@ namespace ProjectM.Authoring
[Tooltip("Max server ticks a siege may run before it auto-collapses (no soft-lock). 0 = no cap.")]
public uint SiegeTimeoutTicks = 3600;
[Header("Threat — scheduled base sieges")]
[Tooltip("A timed cadence arms a base siege even without an expedition trip (keeps the base loop stakeful).")]
public bool ScheduleEnabled = true;
[Tooltip("Server ticks (@60) between scheduled base sieges. First fire is one interval out (mine/build grace).")]
public uint ScheduleIntervalTicks = 2700;
[Tooltip("Extra Husks per surviving wave (siege size = SiegeSizeBase + this * WaveNumber). 0 = flat.")]
public int ScheduleSizePerWave = 1;
private class CycleDirectorBaker : Baker<CycleDirectorAuthoring>
{
@@ -53,6 +63,9 @@ namespace ProjectM.Authoring
SizePerExpeditionResource = authoring.SiegeSizePerResource,
StartCondition = ThreatStartCondition.Immediate,
SiegeTimeoutTicks = authoring.SiegeTimeoutTicks,
ScheduleEnabled = (byte)(authoring.ScheduleEnabled ? 1 : 0),
ScheduleIntervalTicks = authoring.ScheduleIntervalTicks,
ScheduleSizePerWave = authoring.ScheduleSizePerWave,
});
}
}