using ProjectM.Simulation; using Unity.Entities; using Unity.Mathematics; using UnityEngine; using static ProjectM.Client.FeedbackFx; namespace ProjectM.Client { /// /// DR-046 — client-only, observe-only presentation of the room-exit PORTAL made visible. A managed /// in that OBSERVES replicated /// and never mutates the sim. During the loot window it shows a glowing cyan /// pillar (or the authored 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 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. /// [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(out var ri) && ri.Lifecycle == RunLifecycle.RoomExplore; if (!inExplore || !SystemAPI.TryGetSingleton(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(); 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=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) } } }