Docs: DR-046 base<->expedition ties + session log; CLAUDE.md net-zero
DR-046 (portal-gated rooms, class-at-base via shared ClassSwapUtil, per-run prep spend, Health.Max GhostField, C3/C4/Spitter) + the session log. CLAUDE.md folds the batch into the core-loop bullet and corrects the now-wrong 'Health.Max unreplicated' line; paid net-zero (40921 B) by archiving the one-time Rukhanka-samples bullet + trims into the gotchas archive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
---
|
||||
id: DR-046
|
||||
title: Base↔Expedition Ties — Portal-Gated Rooms, Class-at-Base, Per-Run Prep Spend
|
||||
date: 2026-07-04
|
||||
status: locked
|
||||
tags:
|
||||
- decision
|
||||
- combat
|
||||
- demo
|
||||
- netcode
|
||||
- rpc
|
||||
- class
|
||||
- prep
|
||||
- portal
|
||||
supersedes: extends DR-044 (room traversal) + DR-045 (combat demo pass); adds RunLifecycle.RoomExplore
|
||||
permalink: gamevault/07-sessions/decisions/dr-046-base-expedition-ties-portal-class-prep
|
||||
---
|
||||
|
||||
# DR-046 — Base↔Expedition Ties (Portal Rooms + Class-at-Base + Prep Spend)
|
||||
|
||||
Operator brief (ultracode, batch 2 of the demo pass): *"do C3 & C4 + the Health.Max GhostField; there's no time for
|
||||
resource collection during an expedition — after enemies are cleared, spawn a portal/door that must be interacted with to
|
||||
go to the next room / bring up the choice UI; the ranged enemies are too hard (too fast, instant-flee on approach) — are
|
||||
projectiles i-frame dodgeable?; let the player choose their class as part of the expedition / get the loadout from base
|
||||
resources & crafting; it needs more to tie the base and expeditions together."* Full session log:
|
||||
[[2026-07-04_Base_Expedition_Ties_Portal_Class_Prep]]. Builds on [[DR-045_Combat_Demo_Feel_Boss_Fight]] +
|
||||
[[DR-044_Expedition_Redesign_Shipped_Demo_Polish]].
|
||||
|
||||
## 1. Operator-locked fork (AskUserQuestion)
|
||||
|
||||
**Class / loadout = "Per-run prep spend"** — at base the player freely picks a class (Staging UI, re-pickable every run)
|
||||
AND spends base Ore/Biomass/Aether on a per-run **prep panel** for temporary run buffs that are wiped on return. This
|
||||
ties the base economy to the expedition without a persistent-gear grind, and keeps a run self-contained (roguelite).
|
||||
|
||||
## 2. Portal-gated rooms + the loot window (`RunLifecycle.RoomExplore = 6`)
|
||||
|
||||
Rooms no longer auto-advance the instant the last enemy dies. New FSM beat between the reward and the next route:
|
||||
|
||||
- **`RunLifecycle.RoomExplore = 6`** appended to `RunInfo.cs` (byte value → no ghost re-bake). New server-only working
|
||||
fields: `RunRuntime.ExploreGraceTick` (soft-timeout so a stuck party can't softlock) — the party "explores"/loots the
|
||||
cleared room, then **interacts with a portal to advance**.
|
||||
- **`PortalCommand{byte HasInteract}`** (`Simulation/Meta/MetaComponents.cs`) — server-only working flag on the
|
||||
CycleDirector, added at spawn beside `RouteCommand` (`CycleDirectorSpawnSystem`). NOT a `[GhostField]` (a runtime-added
|
||||
replicated component wouldn't replicate anyway; the client derives the portal from replicated `RunInfo`).
|
||||
- **`PortalInteractRequest`** (empty `IRpcCommand`) → **`PortalInteractReceiveSystem`** (server,
|
||||
`[UpdateBefore(RunDirectorSystem)]`, RoomExplore-gated, **sender must be an expedition-region player**) sets
|
||||
`PortalCommand.HasInteract=1`, drain-destroys every request. Client **`PortalInteractSendSystem`** (static `Interact()`).
|
||||
- **`RunDirectorSystem` (still the SOLE `RunInfo` writer + teardown owner)**:
|
||||
- Room **teardown MOVED off** the `InRoom→RoomReward` edge — the room + nodes now persist through the loot window.
|
||||
- `RoomReward` exit → strip the boon band → arm `ExploreGraceTick` → **`RoomExplore`** (the boss-branch + route-gate
|
||||
logic RELOCATED out of here).
|
||||
- New `RoomExplore` case: **empty expedition → `Returning` immediately** (banks the win if boss-cleared, else aborts
|
||||
no-credit — the leftover room is swept by `RoomFieldSystem` at Staging); else on `PortalCommand.HasInteract` OR the
|
||||
ExploreGrace timeout → `DestroyRoom` + the relocated boss-branch (`LastTerminalCleared→Returning`) / route-gate
|
||||
(ReachableOptions → RouteSelect, or `optionCount==0 → Returning`); clears `PortalCommand`.
|
||||
- **Client portal cue (`HudSystem`)** — when `Lifecycle==RoomExplore` and the local player is within
|
||||
`Tuning.PortalInteractRange` of the client-derived portal position
|
||||
`RegionMath.ExpeditionRoomOrigin(PlotCenter(BaseAnchor), (byte)(CurrentRoom & 1)) + PortalOffsetZ`, show an **"E to
|
||||
LEAVE"** prompt; E near the portal → `PortalInteractSendSystem.Interact()`. Never reads `RunParticipant`/`RunRuntime`/
|
||||
`RegionTag` client-side. Tuning: `PortalOffsetZ=-5`, `PortalInteractRange=3.5`, `ExploreGraceTicks=1800`.
|
||||
|
||||
**Post-impl review fix (confirmed, medium):** the RoomExplore empty-abort guard first read `expeditionPlayers==0 &&
|
||||
LastTerminalCleared==0`, which SKIPPED a boss clear — so a post-boss total wipe/disconnect idled the run in RoomExplore
|
||||
for the full ~30 s ExploreGrace before the win banked (a promptness regression vs the old flow, self-healing but a bad
|
||||
dead-time on the game's win moment). **Fix: drop the boss condition** — `if (expeditionPlayers == 0)` advances an empty
|
||||
expedition NOW (boss→Returning banks the win; non-boss→abort no-credit). No explicit teardown needed (Staging sweep).
|
||||
|
||||
## 3. Class-at-base (shared swap helper — the bug-prone part extracted)
|
||||
|
||||
- **`ClassSwapUtil`** (`Simulation/Combat/`) — extracts the FULL class swap from the (dev-only) `SetClass` path so the
|
||||
base-selection and the debug tool can't drift: `Apply(rawClass, mods, haveMeta, metaCat, metaRecord, out newClass, out
|
||||
newAbilityId)` does `ClassTraits.Reapply` + the `AbilityRef` id + **the meta-band strip + per-class `MetaTierState`
|
||||
replay** (the part that, done wrong, drains Aether into the wrong class's meta or leaves stale HP). `HealClamp(ref
|
||||
Health, baseMaxHealth, mods)` refills to the recomputed effective max.
|
||||
- **`ClassSelectRequest{byte ClassId}`** (unconditional `IRpcCommand`) → **`ClassSelectReceiveSystem`** (server,
|
||||
`[UpdateBefore(RunDirectorSystem)]`, **Staging-gated**, sender-resolved) → `ClassSwapUtil.Apply` + set
|
||||
`PlayerClass`/`AbilityRef`/`AbilityCooldown=0` + `HealClamp`, drain-destroy. Client **`ClassSelectSendSystem`** (static
|
||||
`RequestClass(byte)`).
|
||||
- **`DebugCommandReceiveSystem` SetClass refactored to call `ClassSwapUtil`** (dedup, no `#if` drift).
|
||||
- **`HudSystem`** Staging: a Warrior/Ranger class row (re-pickable each run at base).
|
||||
|
||||
## 4. Per-run prep spend
|
||||
|
||||
- **`PrepCatalog`** (`Simulation/Combat/`) — static `PrepRow{byte Id, CostResId; int Cost; byte Target, Op; float Value}`
|
||||
+ `TryGet`. Rows: Id0 +30 MaxHealth (Ore 30) · Id1 +12% MoveSpeed (Biomass 40) · Id2 +20% MeleeDamage (Aether 25) ·
|
||||
Id3 +20% Damage (Aether 25).
|
||||
- **`PrepPurchaseRequest{byte OptionId}`** (`IRpcCommand`) → **`PrepPurchaseSystem`** (server,
|
||||
`[UpdateBefore(RunDirectorSystem)]`, **Staging-gated**): `TotalOf` pre-check **BEFORE** `Withdraw` (in-loop atomic),
|
||||
**once-per-run gated by the presence of the row's prep `StatModifier`** (elegant: "purchased" == the band is present;
|
||||
lifetime == the band, stripped on Returning — no extra per-player buffer needed), then append a run-scoped
|
||||
`StatModifier` in the prep SourceId band. Client **`PrepPurchaseSendSystem`** (static `RequestPrep(byte)`).
|
||||
- **Prep band** — `Tuning.PrepSourceIdBase=0x00D00000`, `PrepSourceIdSpan=0x10000` (disjoint from boon/class/meta/equip).
|
||||
- **`RunDirectorSystem` Returning** strips the prep band (`TimedModifierUtil.RemoveBySourceIdRange`) beside the boon band
|
||||
→ buffs cleared on return, matching the roguelite "self-contained run" rule.
|
||||
- **`HudSystem`** Staging: a prep panel (reuses the meta-shop row-builder pattern).
|
||||
|
||||
## 5. Health.Max GhostField + combat feel (C3/C4/Spitter)
|
||||
|
||||
- **`Health.Max` promoted to `[GhostField]`** — the ONLY ghost-hash change this batch (deliberate, re-baked). Replaces
|
||||
DR-045's client-side `bakedMax × multiplier` reconstruction so the boss bar AND the floating enemy HP bars read a
|
||||
correct fraction regardless of server-side Max mods (boss ×8, class/boon HP). Display-only; clamp/refill still use
|
||||
`EffectiveCharacterStats.MaxHealth`. The boss bar reverted to reading the replicated `Max` directly.
|
||||
- **C3 — melee cone connect-thunk** (`CombatFeedbackSystem`): the Warrior cone now lands a hit-thunk cue on connect.
|
||||
- **C4 — hit-stop throttle** (`CombatFeedbackSystem` + `PrototypeCameraRig.Hold`, `FeelConfig.HitStopFreezeEnabled=true`):
|
||||
a `_lastHoldTime` throttle so a kill / finisher / heavy-hit fires the freeze but a horde wipe can't stutter-storm it.
|
||||
- **Spitter readable-but-fair** (`EnemyAISystem` + `EnemySpitter.prefab`): MoveSpeed 2.4, AttackCooldownTicks 90,
|
||||
`CorneredRange` 6, ProjectileSpeed 8; a **cornered-hold** — within `CorneredRange` the Spitter stops fleeing
|
||||
(`bandVel=0`) so it can be closed on and killed, instead of instantly kiting on approach.
|
||||
- **Confirmed: Spitter projectiles ARE dash-i-frame dodgeable** — `HealthApplyDamageSystem` negates any `DamageEvent`
|
||||
whose `SourceTick` falls in the dash i-frame window. No change needed.
|
||||
|
||||
## 6. Validation
|
||||
|
||||
Clean compile (0 errors). 456/456 EditMode (5 `RunDirectorTraversalTests` updated to drive the new
|
||||
RoomReward→RoomExplore→portal→RouteSelect/Returning flow via a `PortalAdvance` helper + `typeof(PortalCommand)` fixture).
|
||||
Live Play netcode smoke: both worlds go in-game with the 3 new RPCs (RpcCollection hash matched), all 3 receiver systems
|
||||
created, `PortalCommand` on the director, `GoalProgress.Target=2` server==client, zero Play exceptions. Pre-code
|
||||
adversarial review (`wf_e0c9f916-ca1`, 13 confirmed folded into the plan — the class-swap-helper extraction, the portal
|
||||
teardown/branch relocation, and the reset-per-run prep model all came from it). Post-impl diff review
|
||||
(`wf_f0e6cecc-8b0`, clean run, 0 agent failures): 9 candidates, 2 confirmed (the same RoomExplore boss-clear dead-time,
|
||||
from two lenses — fixed §2); 3 hygiene items folded (Temp-ECB `Dispose` in the 3 receivers to match convention; the
|
||||
orphaned `_lastConeFireTick` comment reattached); the rest refuted.
|
||||
|
||||
## 7. Open / operator-side
|
||||
|
||||
Boss + onboarding fun-gate playtests, live-tuning, and the standalone 2-instance LAN smoke (DR-044 §5) remain the
|
||||
operator's. New feel knobs to check live: the Spitter cornered-hold range/speed, the prep buff values, and whether the
|
||||
portal loot window (`ExploreGraceTicks`) feels right vs. an instant advance.
|
||||
Reference in New Issue
Block a user