From 274cf3b79184af44e9b2462cbb5a57358f5e41ad Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Mon, 6 Jul 2026 12:20:04 -0700 Subject: [PATCH] Portal made visible: cyan beacon pillar + always-on RoomExplore steer prompt (DR-046 follow-up) The room-exit portal was invisible in the first playtest - players loitered out the 30s ExploreGrace timeout. Adds RegionMath.ExpeditionPortalPos as the single client-derivable portal-position authority (HUD prompt + beacon can't drift), a pooled breathing HDR pillar in CombatFeedbackSystem shown only during RoomExplore, and an in-range/out-of-range two-stage prompt. Also repairs the comment-swallowed _portalMat initialization (NRE in OnStartRunning killed ALL combat feedback each session and auto-paused the editor via Error Pause). 456/456 EditMode. Co-Authored-By: Claude Fable 5 --- .../Presentation/CombatFeedbackSystem.cs | 54 +++++++++++++++++-- .../Scripts/Client/Presentation/HudSystem.cs | 14 ++--- .../Simulation/World/RegionComponents.cs | 13 ++++- ...Expedition_Redesign_Shipped_Demo_Polish.md | 13 ++++- ..._Base_Expedition_Ties_Portal_Class_Prep.md | 11 ++++ 5 files changed, 92 insertions(+), 13 deletions(-) diff --git a/Assets/_Project/Scripts/Client/Presentation/CombatFeedbackSystem.cs b/Assets/_Project/Scripts/Client/Presentation/CombatFeedbackSystem.cs index 59c2560b6..444227777 100644 --- a/Assets/_Project/Scripts/Client/Presentation/CombatFeedbackSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/CombatFeedbackSystem.cs @@ -66,7 +66,9 @@ namespace ProjectM.Client double _lastHoldTime; // C4: last hit-stop hold time (throttle so a horde wipe doesn't stutter) bool _coneTickInit; Material _dangerMat; - readonly Dictionary _dangerZones = new(); + readonly Dictionary _dangerZones = new(); Material _portalMat; // DR-046: room-exit portal beacon glow (mutated for the pulse; beacon-only mat) + GameObject _portalBeacon; // DR-046: pooled world-space "go here" pillar, shown only during RoomExplore + readonly HashSet _dangerSeen = new(); readonly List _dangerStale = new(); // ---- Enemy health bars (Slice 1, Feature B) — one pooled world-space Canvas per live Husk ---- @@ -148,6 +150,10 @@ namespace ProjectM.Client _dangerMat = MakeParticleMaterial(); _dangerMat.name = "EnemyDanger"; _dangerMat.color = new Color(3.2f, 0.28f, 0.18f, 1f); // HDR red (per-zone intensity carried in vertex alpha) + _portalMat = MakeParticleMaterial(); + _portalMat.name = "RoomPortal"; + _portalMat.color = new Color(0.5f, 2.6f, 3.4f, 0.85f); // DR-046: HDR cyan portal glow (pushes past bloom) + // Health-bar materials (UI/Default = always-included URP-compatible UI shader; per-instance Image.color carries alpha). Shader uiShader = Shader.Find("UI/Default") ?? Shader.Find("Sprites/Default"); _barBgMat = new Material(uiShader) { name = "HealthBarBg" }; @@ -163,7 +169,8 @@ namespace ProjectM.Client Object.Destroy(_fxRoot.gameObject); if (_slashMesh != null) Object.Destroy(_slashMesh); if (_slashMat != null) Object.Destroy(_slashMat); - if (_dangerMat != null) Object.Destroy(_dangerMat); + if (_dangerMat != null) Object.Destroy(_dangerMat); if (_portalMat != null) Object.Destroy(_portalMat); + if (_barBgMat != null) Object.Destroy(_barBgMat); if (_barFillMat != null) Object.Destroy(_barFillMat); foreach (var kv in _dangerZones) @@ -500,7 +507,8 @@ namespace ProjectM.Client PruneVfx(); AnimateNumbers(dt, cam); UpdateSlash(dt); - UpdateEnemyDanger(localPos); + UpdateEnemyDanger(localPos); UpdatePortalBeacon(); + UpdateRemoteSwings(dt); UpdateHealthBars(dt, cam, localPos); } @@ -982,7 +990,45 @@ void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int return new RemoteSlash { Go = go, Mesh = mesh, Mr = mr, Mat = mat, Active = false, Init = false }; } - // Enemy attack TELEGRAPH (MC-4 clarity): while an enemy's AttackWindup counts down, paint a red ground danger + // DR-046: the room-exit PORTAL made VISIBLE. During the RoomExplore loot window a glowing cyan pillar marks the + // client-derived portal position so the player has an unmistakable "go here to continue" target — the HUD prompt + // alone left the exit invisible, so players waited out the ~30s grace timeout ("nothing happens for a while"). + // Client-only, observe-only; one pooled GameObject, hidden whenever the run isn't in RoomExplore. Position + // resolves through the SAME RegionMath.ExpeditionPortalPos authority the HUD prompt uses -> beacon + "PRESS E" + // range always agree. + void UpdatePortalBeacon() + { + if (_fxRoot == null || _portalMat == null) return; + bool inExplore = SystemAPI.TryGetSingleton(out var ri) && ri.Lifecycle == RunLifecycle.RoomExplore; + if (!inExplore || !SystemAPI.TryGetSingleton(out var anchor)) + { + if (_portalBeacon != null && _portalBeacon.activeSelf) _portalBeacon.SetActive(false); + return; + } + float3 pos = RegionMath.ExpeditionPortalPos(BaseGridMath.PlotCenter(anchor), (byte)(ri.CurrentRoom & 1)); + if (_portalBeacon == null) + { + _portalBeacon = GameObject.CreatePrimitive(PrimitiveType.Cylinder); + _portalBeacon.name = "~RoomPortalBeacon"; + var col = _portalBeacon.GetComponent(); if (col != null) Object.Destroy(col); // cosmetic only + _portalBeacon.transform.SetParent(_fxRoot, false); + var mr = _portalBeacon.GetComponent(); + mr.sharedMaterial = _portalMat; + mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off; + mr.receiveShadows = false; + } + if (!_portalBeacon.activeSelf) _portalBeacon.SetActive(true); + float t = (float)SystemAPI.Time.ElapsedTime; + float breathe = 0.5f + 0.5f * math.sin(t * 3.5f); + var tr = _portalBeacon.transform; + // Cylinder is 2u tall in local space -> scale.y=3 gives a 6u pillar; lift the centre so the base sits on the ground. + tr.position = new Vector3(pos.x, pos.y + 3f, pos.z); + tr.localScale = new Vector3(1.4f + 0.15f * breathe, 3f, 1.4f + 0.15f * breathe); + _portalMat.color = new Color(0.5f, 2.6f, 3.4f, 0.55f + 0.35f * breathe); // glow throb (beacon-only mat, safe to mutate) + } + + +// Enemy attack TELEGRAPH (MC-4 clarity): while an enemy's AttackWindup counts down, paint a red ground danger // cone in its facing out to its reach, brightening + scaling as the strike nears -> the player reads WHERE + // WHEN to dodge. Client-only, observe-only; one pooled mesh per winding-up enemy, pruned each frame. void UpdateEnemyDanger(float3 localPos) diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs index 597db399c..8a48474ff 100644 --- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs +++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs @@ -1949,23 +1949,25 @@ namespace ProjectM.Client { var root = _doc != null ? _doc.rootVisualElement : null; if (root == null) return; if (!_portalBuilt) { BuildPortalPrompt(root); _portalBuilt = true; } - bool show = false; + bool show = false, inRange = false; if (haveRun && runInfo.Lifecycle == RunLifecycle.RoomExplore && SystemAPI.TryGetSingleton(out var anchor)) { - float3 center = BaseGridMath.PlotCenter(anchor); - float3 portalPos = RegionMath.ExpeditionRoomOrigin(center, (byte)(runInfo.CurrentRoom & 1)); - portalPos.z += Tuning.PortalOffsetZ; + show = true; // room cleared -> ALWAYS steer the player to the (now visible) portal, not only when in range + float3 portalPos = RegionMath.ExpeditionPortalPos(BaseGridMath.PlotCenter(anchor), (byte)(runInfo.CurrentRoom & 1)); foreach (var lt in SystemAPI.Query>().WithAll()) { - if (math.distance(lt.ValueRO.Position.xz, portalPos.xz) <= Tuning.PortalInteractRange) + inRange = math.distance(lt.ValueRO.Position.xz, portalPos.xz) <= Tuning.PortalInteractRange; + if (inRange) { - show = true; var kb = UnityEngine.InputSystem.Keyboard.current; if (kb != null && kb.eKey.wasPressedThisFrame) PortalInteractSendSystem.Interact(); } break; } + _portalPrompt.text = inRange + ? "PRESS E TO LEAVE — the haul comes home" + : "ROOM CLEAR — reach the glowing portal to move on"; } _portalPrompt.style.display = show ? DisplayStyle.Flex : DisplayStyle.None; } diff --git a/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs b/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs index cc5f235ee..a8cfeb5b6 100644 --- a/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs +++ b/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs @@ -53,7 +53,18 @@ namespace ProjectM.Simulation return baseCenter + new float3(ExpeditionOffsetX + subSlot * RoomStrideX, 0f, 0f); } - /// World-space origin of , given the base center (BaseGridMath.PlotCenter). + /// World-space position of the room-exit PORTAL for sub-slot — the single + /// client-derivable authority the HUD prompt AND the presentation beacon both resolve through (DR-046), so they + /// can't drift. = the room origin nudged by in Z. + public static float3 ExpeditionPortalPos(float3 baseCenter, byte subSlot) + { + float3 p = ExpeditionRoomOrigin(baseCenter, subSlot); + p.z += Tuning.PortalOffsetZ; + return p; + } + + +/// World-space origin of , given the base center (BaseGridMath.PlotCenter). /// The expedition resolves to room sub-slot 0 (legacy call sites; room-aware systems pass the ACTIVE /// sub-slot to directly). public static float3 RegionOrigin(byte region, float3 baseCenter) diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-044_Expedition_Redesign_Shipped_Demo_Polish.md b/Docs/Vault/07_Sessions/_Decisions/DR-044_Expedition_Redesign_Shipped_Demo_Polish.md index bc8f07098..81acfe95e 100644 --- a/Docs/Vault/07_Sessions/_Decisions/DR-044_Expedition_Redesign_Shipped_Demo_Polish.md +++ b/Docs/Vault/07_Sessions/_Decisions/DR-044_Expedition_Redesign_Shipped_Demo_Polish.md @@ -3,8 +3,17 @@ id: DR-044 title: Expedition Redesign SHIPPED (steps 1–14) + Demo-Readiness Polish Pass date: 2026-07-04 status: locked -tags: [decision, expedition, roguelite, netcode, demo, polish, onboarding, audio] +tags: +- decision +- expedition +- roguelite +- netcode +- demo +- polish +- onboarding +- audio supersedes: parts of DR-031/DR-042 (base-mining loop text); completes the [[2026-06-29_Expedition_Redesign_Build_Spec]] +permalink: gamevault/07-sessions/decisions/dr-044-expedition-redesign-shipped-demo-polish --- # DR-044 — Expedition Redesign SHIPPED + Demo-Readiness Polish @@ -88,4 +97,4 @@ depth dots · meta shop). Console clean (the only remaining editor noise was the Standalone build + 2-instance LAN smoke (operator runs it; `-mhost`/`-mjoin`, watchdog log lines = evidence) · onboarding fun-gate playthrough (Force-Each-Launch toggle) · tick-batching perf re-measure in the build · HudTheme structure icons vs new silhouettes + projectile/pickup placeholder meshes (art curation — needs -operator eyes; explicitly deferred) · GoalProgress.Target demo pacing (currently 4 runs). +operator eyes; explicitly deferred) · GoalProgress.Target demo pacing (currently 4 runs). \ No newline at end of file diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-046_Base_Expedition_Ties_Portal_Class_Prep.md b/Docs/Vault/07_Sessions/_Decisions/DR-046_Base_Expedition_Ties_Portal_Class_Prep.md index 783c9f55e..d42d032b2 100644 --- a/Docs/Vault/07_Sessions/_Decisions/DR-046_Base_Expedition_Ties_Portal_Class_Prep.md +++ b/Docs/Vault/07_Sessions/_Decisions/DR-046_Base_Expedition_Ties_Portal_Class_Prep.md @@ -59,6 +59,17 @@ Rooms no longer auto-advance the instant the last enemy dies. New FSM beat betwe LEAVE"** prompt; E near the portal → `PortalInteractSendSystem.Interact()`. Never reads `RunParticipant`/`RunRuntime`/ `RegionTag` client-side. Tuning: `PortalOffsetZ=-5`, `PortalInteractRange=3.5`, `ExploreGraceTicks=1800`. +**Playtest fix (2026-07-05) — the portal was INVISIBLE.** The shipped cue was only a HUD line that appeared *when the +player was already within `PortalInteractRange` of an unmarked point*, so after picking a boon there was no world target +to walk to — players loitered until the ~30 s `ExploreGrace` timeout auto-advanced them ("nothing happens for a while +until the next room selection comes up"). Fix (client presentation only, no netcode/re-bake): (1) new +`RegionMath.ExpeditionPortalPos(baseCenter, subSlot)` = the single portal-position authority the HUD prompt AND the +beacon both resolve through (extract-before-second-caller, per this batch's own lesson); (2) `CombatFeedbackSystem` now +renders a glowing cyan HDR **portal-beacon pillar** at that position during `RoomExplore` (pooled GameObject, hidden +otherwise, breathing pulse; mirrors the danger-zone pattern); (3) the `HudSystem` prompt is now **always-on during +RoomExplore** — "ROOM CLEAR — reach the glowing portal to move on" out of range, "PRESS E TO LEAVE" in range. 456/456 +EditMode; the beacon is the operator's visual fun-gate. + **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