Base<->Expedition ties: portal rooms, class-at-base, prep spend + Health.Max, C3/C4/Spitter (DR-046)

Portal-gated rooms: new RunLifecycle.RoomExplore loot window; room+nodes
persist past the last kill; PortalInteractRequest -> server-only PortalCommand
advances (client derives the portal pos). RunDirectorSystem moves teardown off
the InRoom edge, relocates the boss-branch/route-gate into the RoomExplore exit,
and advances an empty expedition immediately (incl. a boss clear).

Class-at-base via a shared ClassSwapUtil (meta-band strip + per-class
MetaTierState replay, so the base RPC and the dev SetClass can't drift);
ClassSelectRequest Staging-gated. Per-run PREP buffs (PrepPurchaseRequest,
PrepCatalog): once-per-run == the row's prep StatModifier band is present,
TotalOf-before-Withdraw atomic, stripped on Returning beside the boon band.

Health.Max promoted to [GhostField] (the one ghost-hash re-bake) so the boss +
floating enemy HP bars read it directly. Feel: melee cone connect-thunk (C3),
hit-stop throttle so a horde wipe can't stutter (C4), Spitter cornered-hold.

456/456 EditMode; two adversarial reviews (pre-code 13-confirmed folded;
post-impl clean bar the RoomExplore boss-clear dead-time, fixed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 10:37:24 -07:00
parent 35357faa7a
commit 304ee8c2a7
35 changed files with 860 additions and 110 deletions
@@ -0,0 +1,12 @@
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Client → server: interact with the room-exit portal to leave (advance the run). Client-gated on proximity +
/// the RoomExplore lifecycle (both replicated/derivable client-side); the server honors it ONLY in RoomExplore
/// from an expedition player, setting <see cref="PortalCommand"/> for RunDirectorSystem (the sole RunInfo writer)
/// to consume. Empty payload. UNCONDITIONAL wire type.
/// </summary>
public struct PortalInteractRequest : IRpcCommand { }
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9d527637b76c7a545817f6f388533248
@@ -24,6 +24,11 @@ namespace ProjectM.Simulation
public const byte Returning = 4;
/// <summary>Boons picked; party choosing the next branch (no room materialized — the teardown gap).</summary>
public const byte RouteSelect = 5;
/// <summary>DR-046: room cleared + boon picked, but the room + resource NODES persist and a portal is up.
/// Party loots; interacting the portal (or a soft-timeout) tears the room down + advances (RouteSelect, or
/// Returning if the boss fell). Append-only byte value — no ghost re-mean.</summary>
public const byte RoomExplore = 6;
}
/// <summary>
@@ -48,6 +48,11 @@ namespace ProjectM.Simulation
/// <summary>Server tick the RouteSelect grace elapses → auto-pick lowest-index reachable (NonZero; IsNewerThan-compared).</summary>
public uint RouteGraceTick;
/// <summary>DR-046: RoomExplore soft-timeout (NonZero; auto-advance if nobody interacts the portal), so the
/// loot window can never softlock. Set on entering RoomExplore, compared via NetworkTick.IsNewerThan.</summary>
public uint ExploreGraceTick;
// ---- boons ----
/// <summary>Monotonic per-run boon-pick counter → distinct SourceIds in the run-scoped boon band; reset each run.</summary>
public uint BoonPickCounter;