Files
Project-M/Assets/_Project/Scripts/Client/Presentation/RoomPortalBeaconSystem.cs
T
kronic 6379f5d897 Hygiene B5: split HudSystem + CombatFeedbackSystem god-objects
Extract 7 sibling PresentationSystemGroup systems (client-only, observe-only), faithfully relocating methods+fields so behavior is preserved by construction:
- HudSystem (2129->1588L): BoonModalHudSystem, RouteMapHudSystem, MetaShopHudSystem, ClassPrepPortalHudSystem — each owns its own runtime UIDocument (MenuUi.LoadPanelSettings + own sortingOrder + EnsureEventSystem), the proven EnemyMarkerSystem/OnboardingSystem pattern; no shared root, no new static bridge.
- CombatFeedbackSystem (1300->914L): RoomPortalBeaconSystem, EnemyHealthBarSystem, EnemyDangerTelegraphSystem — each owns its FX-root + mats (via FeedbackFx), self-queries enemies + self-detects its edge (health-bar LastHp; danger _prevWindup), prunes its caches each frame.

Verified: compiles clean (0 errors), 466/466 EditMode tests pass, Play world-creation clean (no ComponentSystemSorter cycle, no OnCreate exception, 0 console errors). NOTE: the final VISUAL smoke (panels appear at the right lifecycle; enemy health bars / danger telegraphs / portal beacon render; buttons live) needs a FOCUSED Play pass — the play-mode transition throttles while Unity is unfocused, so I could not drive live frames headlessly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 14:00:55 -07:00

103 lines
5.9 KiB
C#

using ProjectM.Simulation;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
using static ProjectM.Client.FeedbackFx;
namespace ProjectM.Client
{
/// <summary>
/// DR-046 — client-only, observe-only presentation of the room-exit PORTAL made visible. A managed
/// <see cref="SystemBase"/> in <see cref="PresentationSystemGroup"/> that OBSERVES replicated <see cref="RunInfo"/>
/// and never mutates the sim. During the <see cref="RunLifecycle.RoomExplore"/> loot window it shows a glowing cyan
/// pillar (or the authored <see cref="VFXConfig.Portal"/> effect when wired) at the client-derived portal position
/// so the player has an unmistakable "go here to continue" target; hidden whenever the run isn't in RoomExplore.
/// Position resolves through the SAME <see cref="RegionMath.ExpeditionPortalPos"/> authority the HUD prompt uses, so
/// the beacon and the "PRESS E" range always agree. Extracted from CombatFeedbackSystem (owns its own FX-root +
/// beacon material); no Entity-keyed cache.
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class RoomPortalBeaconSystem : SystemBase
{
Transform _fxRoot;
GameObject _portalFx; // Phase 1: authored portal effect (VFXConfig.Portal) replacing the procedural pillar when wired
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
protected override void OnStartRunning()
{
if (_fxRoot != null) return;
_fxRoot = new GameObject("~RoomPortalBeaconFX").transform;
_portalMat = MakeParticleMaterial();
_portalMat.name = "RoomPortal";
_portalMat.color = new Color(0.25f, 1.2f, 1.55f, 0.85f); // DR-046: HDR cyan portal glow (Phase 0: tamed — 2.6/3.4 bloomed to a white blob)
}
protected override void OnDestroy()
{
if (_fxRoot != null) Object.Destroy(_fxRoot.gameObject);
if (_portalMat != null) Object.Destroy(_portalMat);
}
protected override void OnUpdate()
{
UpdatePortalBeacon();
}
// 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);
if (_portalFx != null && _portalFx.activeSelf) _portalFx.SetActive(false);
return;
}
float3 pos = RegionMath.ExpeditionPortalPos(BaseGridMath.PlotCenter(anchor), (byte)(ri.CurrentRoom & 1));
// Phase 1: prefer the authored portal effect (VFXConfig.Portal, PolygonParticleFX) over the
// procedural pillar; the pillar remains the asset-free fallback.
var vfx = VFXConfig.Instance;
if (vfx != null && vfx.Portal != null)
{
if (_portalFx == null)
{
_portalFx = Object.Instantiate(vfx.Portal, _fxRoot, false);
_portalFx.name = "~RoomPortalFx";
}
_portalFx.transform.position = new Vector3(pos.x, 0f, pos.z); // terrain y=0 (pos.y is the capsule plane)
if (!_portalFx.activeSelf) _portalFx.SetActive(true);
if (_portalBeacon != null && _portalBeacon.activeSelf) _portalBeacon.SetActive(false);
return;
}
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=2.2 gives a 4.4u pillar; lift the centre so the base sits
// on the TERRAIN (y=0) — pos.y is the CC capsule-center plane (GridOrigin.y=1), 1 u above the ground.
tr.position = new Vector3(pos.x, 2.2f, pos.z);
tr.localScale = new Vector3(0.9f + 0.12f * breathe, 2.2f, 0.9f + 0.12f * breathe);
_portalMat.color = new Color(0.25f, 1.2f, 1.55f, 0.45f + 0.3f * breathe); // glow throb (beacon-only mat; Phase 0: tamed + slimmed — the fat 6u pillar bloomed to a white egg swallowing the prompt)
}
}
}