This commit is contained in:
2026-07-04 16:57:40 -07:00
parent 16e396841e
commit c6b7890212
47 changed files with 1713 additions and 209 deletions
@@ -139,6 +139,16 @@ namespace ProjectM.Server
bool due = info.LaunchTick == 0u || !new NetworkTick(info.LaunchTick).IsNewerThan(serverTick);
if (due)
{
// Conscript the party: the launch roster is EVERYONE connected (N7 co-location — all at
// base, all ready). Room advances teleport ONLY RunParticipants, so a mid-run late joiner
// is never yanked into the fight; the tag is released on the Returning edge.
var conscript = new EntityCommandBuffer(Allocator.Temp);
foreach (var (_, playerE) in
SystemAPI.Query<RefRO<PlayerReady>>().WithAll<PlayerTag>().WithEntityAccess())
conscript.AddComponent<RunParticipant>(playerE);
conscript.Playback(state.EntityManager);
conscript.Dispose();
// Enter room 0 (the guaranteed Combat landing at column 0, sub-slot 0).
var map = RunMapMath.Generate(run.RunSeed);
EnterRoom(ref state, ref info, ref run, in map, layer: 0, col: 0, baseCenter, bumpEpoch: true);
@@ -187,15 +197,24 @@ namespace ProjectM.Server
// Exit gate: every SURVIVING player has picked (BoonOffer.Pending==0 — inert until Step 10)
// OR the grace elapsed (wrap-safe IsNewerThan, never raw uint — F4).
// Only EXPEDITION players hold the gate (BoonOfferSystem's documented contract): a player who
// died and respawned to base must not stall the party (post-impl review, confirmed major).
bool anyPending = false;
foreach (var offer in SystemAPI.Query<RefRO<BoonOffer>>().WithAll<PlayerTag>())
if (offer.ValueRO.Pending != 0) { anyPending = true; break; }
foreach (var (offer, pregion) in
SystemAPI.Query<RefRO<BoonOffer>, RefRO<RegionTag>>().WithAll<PlayerTag>())
if (pregion.ValueRO.Region == RegionId.Expedition && offer.ValueRO.Pending != 0)
{ anyPending = true; break; }
bool graceElapsed = run.RewardGraceTick == 0u
|| !new NetworkTick(run.RewardGraceTick).IsNewerThan(serverTick);
if (anyPending && !graceElapsed)
break;
run.RewardGraceTick = 0u;
// NO offer survives this gate: zero every straggler (a dead-respawned base player the
// auto-pick deliberately skips, a grace-expired AFK) so a stale Pending can never wedge the
// HUD modal open or stall a later reward gate (post-impl review, confirmed major).
foreach (var offer in SystemAPI.Query<RefRW<BoonOffer>>().WithAll<PlayerTag>())
offer.ValueRW = default;
if (run.LastTerminalCleared != 0)
{
info.Lifecycle = RunLifecycle.Returning; // boss cleared — go home a winner
@@ -279,18 +298,25 @@ namespace ProjectM.Server
case RunLifecycle.Returning:
{
// Party teleport HOME + region flip back to Base.
// PARTICIPANT teleport home + region flip + roster release. Only the launch roster comes
// home (a mid-run joiner already at base keeps its position); the tag removal re-opens the
// next run's conscription cleanly (post-impl review, confirmed medium).
int idx = 0;
foreach (var (region, xform) in
SystemAPI.Query<RefRW<RegionTag>, RefRW<LocalTransform>>().WithAll<PlayerTag>())
var homebound = new EntityCommandBuffer(Allocator.Temp);
foreach (var (region, xform, playerE) in
SystemAPI.Query<RefRW<RegionTag>, RefRW<LocalTransform>>()
.WithAll<PlayerTag, RunParticipant>().WithEntityAccess())
{
region.ValueRW.Region = RegionId.Base;
var p = baseCenter;
p.x += 1.5f * idx;
p.y = xform.ValueRO.Position.y;
xform.ValueRW.Position = p;
homebound.RemoveComponent<RunParticipant>(playerE);
idx++;
}
homebound.Playback(state.EntityManager);
homebound.Dispose();
// THE terminal bank — once per RunEpoch (equality latch, F7), CLEAR-GATED (D-F3).
if (run.LastBankedRunEpoch != run.RunEpoch)
@@ -393,7 +419,7 @@ namespace ProjectM.Server
float3 roomOrigin = RegionMath.ExpeditionRoomOrigin(baseCenter, run.ActiveSubSlot);
int idx = 0;
foreach (var (region, xform) in
SystemAPI.Query<RefRW<RegionTag>, RefRW<LocalTransform>>().WithAll<PlayerTag>())
SystemAPI.Query<RefRW<RegionTag>, RefRW<LocalTransform>>().WithAll<PlayerTag, RunParticipant>())
{
region.ValueRW.Region = RegionId.Expedition;
var p = roomOrigin;