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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Entity, GameObject> _dangerZones = new();
|
||||
readonly Dictionary<Entity, GameObject> _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<Entity> _dangerSeen = new();
|
||||
readonly List<Entity> _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<RunInfo>(out var ri) && ri.Lifecycle == RunLifecycle.RoomExplore;
|
||||
if (!inExplore || !SystemAPI.TryGetSingleton<BaseAnchor>(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<Collider>(); if (col != null) Object.Destroy(col); // cosmetic only
|
||||
_portalBeacon.transform.SetParent(_fxRoot, false);
|
||||
var mr = _portalBeacon.GetComponent<MeshRenderer>();
|
||||
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)
|
||||
|
||||
@@ -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<BaseAnchor>(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<RefRO<LocalTransform>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,18 @@ namespace ProjectM.Simulation
|
||||
return baseCenter + new float3(ExpeditionOffsetX + subSlot * RoomStrideX, 0f, 0f);
|
||||
}
|
||||
|
||||
/// <summary>World-space origin of <paramref name="region"/>, given the base center (BaseGridMath.PlotCenter).
|
||||
/// <summary>World-space position of the room-exit PORTAL for sub-slot <paramref name="subSlot"/> — 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 <see cref="Tuning.PortalOffsetZ"/> in Z.</summary>
|
||||
public static float3 ExpeditionPortalPos(float3 baseCenter, byte subSlot)
|
||||
{
|
||||
float3 p = ExpeditionRoomOrigin(baseCenter, subSlot);
|
||||
p.z += Tuning.PortalOffsetZ;
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>World-space origin of <paramref name="region"/>, 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 <see cref="ExpeditionRoomOrigin"/> directly).</summary>
|
||||
public static float3 RegionOrigin(byte region, float3 baseCenter)
|
||||
|
||||
+11
-2
@@ -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).
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user