Fix: dying on an expedition soft-bricked the player — respawn now resets RegionTag to Base

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>
This commit is contained in:
2026-06-25 22:44:33 -07:00
parent 8596cc74b1
commit 3c1b5c44cd
2 changed files with 39 additions and 7 deletions
@@ -26,7 +26,6 @@ namespace ProjectM.Server
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<NetworkTime>();
state.RequireForUpdate<PlayerSpawner>();
}
[BurstCompile]
@@ -37,14 +36,20 @@ namespace ProjectM.Server
return;
uint now = serverTick.TickIndexForValidTick;
var spawner = SystemAPI.GetSingleton<PlayerSpawner>();
float3 center = spawner.SpawnPoint;
if (SystemAPI.TryGetSingleton<BaseAnchor>(out var baseAnchor))
// Resilient spawn reference: prefer the BaseAnchor plot center, fall back to the PlayerSpawner. NEVER
// hard-require PlayerSpawner (a transiently-missing singleton must not strand dead players downed forever).
bool haveSpawner = SystemAPI.TryGetSingleton<PlayerSpawner>(out var spawner);
bool haveAnchor = SystemAPI.TryGetSingleton<BaseAnchor>(out var baseAnchor);
if (!haveSpawner && !haveAnchor)
return; // no spawn reference at all this tick — re-try next tick rather than mis-place
float3 center = haveAnchor ? BaseGridMath.PlotCenter(baseAnchor) : spawner.SpawnPoint;
float ringRadius = haveSpawner ? spawner.SpawnRingRadius : 2f;
int ringSlots = haveSpawner ? spawner.RingSlots : 4;
center = BaseGridMath.PlotCenter(baseAnchor);
foreach (var (health, respawn, invuln, xform, owner, eff) in
foreach (var (health, respawn, invuln, xform, region, owner, eff) in
SystemAPI.Query<RefRW<Health>, RefRW<RespawnState>, RefRW<RespawnInvuln>, RefRW<LocalTransform>,
RefRO<GhostOwner>, RefRO<EffectiveCharacterStats>>()
RefRW<RegionTag>, RefRO<GhostOwner>, RefRO<EffectiveCharacterStats>>()
.WithAll<PlayerTag>())
{
if (health.ValueRO.Current > 0f)
@@ -66,8 +71,12 @@ namespace ProjectM.Server
health.ValueRW.Current = maxHealth;
float3 pos = center + PlayerSpawnMath.SpawnOffset(
owner.ValueRO.NetworkId, spawner.SpawnRingRadius, spawner.RingSlots);
owner.ValueRO.NetworkId, ringRadius, ringSlots);
xform.ValueRW.Position = pos;
// Death fix: respawn is at BASE, so the player's server-only RegionTag MUST return to Base too
// (every other mover flips RegionTag + Position together). Dying on an expedition otherwise leaves
// you at base coords still tagged Expedition -> RegionRelevancy hides all base ghosts (soft-brick).
region.ValueRW.Region = RegionId.Base;
// Grant brief post-respawn damage immunity so the swarm can't instantly re-kill.
invuln.ValueRW.UntilTick = TickUtil.NonZero(now + (uint)math.max(0, respawn.ValueRO.InvulnTicks));