From 3c1b5c44cd72d3931f80f89028d457334e72c5f2 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Thu, 25 Jun 2026 22:44:33 -0700 Subject: [PATCH] =?UTF-8?q?Fix:=20dying=20on=20an=20expedition=20soft-bric?= =?UTF-8?q?ked=20the=20player=20=E2=80=94=20respawn=20now=20resets=20Regio?= =?UTF-8?q?nTag=20to=20Base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 to the recovery query + set Region=Base alongside the reposition. - Harden: drop the hard RequireForUpdate (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 --- .../Server/Combat/PlayerRespawnSystem.cs | 23 +++++++++++++------ .../EditMode/PlayerRespawnSystemTests.cs | 23 +++++++++++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/Assets/_Project/Scripts/Server/Combat/PlayerRespawnSystem.cs b/Assets/_Project/Scripts/Server/Combat/PlayerRespawnSystem.cs index e9342cb54..dfd5bc3ea 100644 --- a/Assets/_Project/Scripts/Server/Combat/PlayerRespawnSystem.cs +++ b/Assets/_Project/Scripts/Server/Combat/PlayerRespawnSystem.cs @@ -26,7 +26,6 @@ namespace ProjectM.Server public void OnCreate(ref SystemState state) { state.RequireForUpdate(); - state.RequireForUpdate(); } [BurstCompile] @@ -37,14 +36,20 @@ namespace ProjectM.Server return; uint now = serverTick.TickIndexForValidTick; - var spawner = SystemAPI.GetSingleton(); - float3 center = spawner.SpawnPoint; - if (SystemAPI.TryGetSingleton(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(out var spawner); + bool haveAnchor = SystemAPI.TryGetSingleton(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, RefRW, RefRW, - RefRO, RefRO>() + RefRW, RefRO, RefRO>() .WithAll()) { 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)); diff --git a/Assets/_Project/Tests/EditMode/PlayerRespawnSystemTests.cs b/Assets/_Project/Tests/EditMode/PlayerRespawnSystemTests.cs index c95e2639d..ce3e093e9 100644 --- a/Assets/_Project/Tests/EditMode/PlayerRespawnSystemTests.cs +++ b/Assets/_Project/Tests/EditMode/PlayerRespawnSystemTests.cs @@ -44,6 +44,7 @@ namespace ProjectM.Tests em.AddComponentData(e, new GhostOwner { NetworkId = networkId }); em.AddComponentData(e, new EffectiveCharacterStats { MaxHealth = maxHealth }); em.AddComponent(e); + em.AddComponentData(e, new RegionTag { Region = RegionId.Base }); return e; } @@ -103,5 +104,27 @@ namespace ProjectM.Tests Assert.AreEqual(50f, em.GetComponentData(player).Current, 1e-4f, "Alive health is untouched."); } } + + [Test] + public void Expedition_Death_Respawns_At_Base_And_Resets_RegionTag() + { + // Death-fix regression: a player who dies ON an expedition (RegionTag=Expedition) must respawn at base + // AND have its RegionTag reset to Base, or it soft-bricks (RegionRelevancy hides all base ghosts). + var (world, group) = MakeWorld("RespawnExpedition", serverTick: 200); + using (world) + { + var em = world.EntityManager; + var player = MakePlayer(em, health: 0f, maxHealth: 100f, respawnTick: 160, + delayTicks: 60, invulnTicks: 120, pos: new float3(1005, 1, 3), networkId: 1); + em.SetComponentData(player, new RegionTag { Region = RegionId.Expedition }); // died out on the sortie + + group.Update(); + + Assert.AreEqual(RegionId.Base, em.GetComponentData(player).Region, + "Death fix: respawn resets RegionTag to Base (else the player is stranded tagged Expedition)."); + Assert.Less(em.GetComponentData(player).Position.x, 100f, + "And is repositioned from the expedition (x~1005) back to the base ring."); + } + } } }