using System.Collections.Generic; using ProjectM.Simulation; using Unity.Entities; using Unity.NetCode; using Unity.Transforms; using Unity.Mathematics; using UnityEngine; using UnityEngine.UIElements; namespace ProjectM.Client { /// /// DR-046 base class-select + prep-loadout panels (Staging) + the room-exit portal prompt (RoomExplore) — /// extracted from into their own client-only, observe-only presentation /// in . Owns its own runtime UIDocument sharing /// (sortingOrder 54). Recomputes the local class + ore/bio/aether + the /// Staging gate + the RoomExplore portal proximity locally; clicks enqueue through /// / / . /// NOTE (behavior-preserving): the class/prep Staging gate reproduces the original's FULL condition — it also /// requires the + buffer to be present (the panels /// were gated on the same metaShow boolean as the meta shop). /// [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] [UpdateInGroup(typeof(PresentationSystemGroup))] public partial class ClassPrepPortalHudSystem : SystemBase { GameObject _go; UIDocument _doc; bool _built; VisualElement _classPanel, _prepPanel, _prepRowsHost; Label _classTitle, _prepTitle, _portalPrompt; Button _classWarBtn, _classRangerBtn; bool _classPanelBuilt, _prepPanelBuilt, _portalBuilt; int _classShownFor, _prepShownFor; protected override void OnStartRunning() { if (_go != null) return; MenuUi.EnsureEventSystem(); _go = new GameObject("~HUDClassPrepPortal"); _doc = _go.AddComponent(); _doc.panelSettings = MenuUi.LoadPanelSettings(); _doc.sortingOrder = 54; } protected override void OnDestroy() { if (_go != null) Object.Destroy(_go); } protected override void OnUpdate() { if (_doc == null) return; var root = _doc.rootVisualElement; if (root == null) return; // panel not initialised yet (next frame) if (!_built) { root.style.position = Position.Absolute; root.style.left = 0; root.style.right = 0; root.style.top = 0; root.style.bottom = 0; root.pickingMode = PickingMode.Ignore; // never eat game-world clicks _built = true; } bool haveRun = SystemAPI.TryGetSingleton(out var runInfo); bool haveCycle = SystemAPI.TryGetSingleton(out var cyc); bool siege = haveCycle && cyc.Phase == CyclePhase.Siege; // Resources from the ledger (last entry per type wins, matching the core loop). int aether = 0, ore = 0, bio = 0; if (SystemAPI.TryGetSingletonEntity(out var ledgerE)) { var buf = SystemAPI.GetBuffer(ledgerE); for (int i = 0; i < buf.Length; i++) { var en = buf[i]; if (en.ItemId == ResourceId.Aether) aether = en.Count; else if (en.ItemId == ResourceId.Ore) ore = en.Count; else if (en.ItemId == ResourceId.Biomass) bio = en.Count; } } // Local class from the replicated AbilityRef (tracks the dev class-switch; PlayerClass is server-only). byte localClass = ClassTraits.WarriorClass; bool haveLocalPlayer = false; foreach (var ar in SystemAPI.Query>().WithAll()) { localClass = ClassTraits.ClassForAbility(ar.ValueRO.Id); haveLocalPlayer = true; break; } // Faithful reproduction of the original `metaShow` gate: class/prep were shown on the SAME condition as // the meta shop, which requires the meta catalog + tier buffer to exist. DynamicBuffer metaRecord = default; bool metaShow = haveRun && runInfo.Lifecycle == RunLifecycle.Staging && haveLocalPlayer && !siege && SystemAPI.TryGetSingleton(out var metaCat) && metaCat.Value.IsCreated && SystemAPI.TryGetSingletonBuffer(out metaRecord, true); UpdateClassPanel(metaShow, localClass); // DR-046: base class pick (Staging) UpdatePrepPanel(metaShow, ore, bio, aether); // DR-046: base prep loadout (Staging) UpdatePortalPrompt(haveRun ? runInfo : default, haveRun); // DR-046: room-exit portal prompt (RoomExplore) } void UpdateClassPanel(bool show, byte classId) { if (!show) { if (_classPanel != null) _classPanel.style.display = DisplayStyle.None; _classShownFor = 0; return; } var root = _doc != null ? _doc.rootVisualElement : null; if (root == null) return; if (!_classPanelBuilt) { BuildClassPanel(root); _classPanelBuilt = true; } int sig = classId + 1; if (_classShownFor != sig) { bool ranger = classId == ClassTraits.RangerClass; _classWarBtn.text = ranger ? "WARRIOR" : "WARRIOR ✓"; _classRangerBtn.text = ranger ? "RANGER ✓" : "RANGER"; _classWarBtn.SetEnabled(ranger); _classRangerBtn.SetEnabled(!ranger); _classShownFor = sig; } _classPanel.style.display = DisplayStyle.Flex; } void BuildClassPanel(VisualElement root) { _classPanel = new VisualElement { pickingMode = PickingMode.Ignore }; _classPanel.style.position = Position.Absolute; _classPanel.style.left = 12; _classPanel.style.top = Length.Percent(22); _classPanel.style.display = DisplayStyle.None; var box = new VisualElement(); box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.92f); MenuUi.Round(box, 10); box.style.paddingLeft = 12; box.style.paddingRight = 12; box.style.paddingTop = 10; box.style.paddingBottom = 10; _classTitle = new Label("CLASS"); _classTitle.style.color = MenuUi.Accent; _classTitle.style.fontSize = 14; _classTitle.style.unityFontStyleAndWeight = FontStyle.Bold; _classTitle.style.marginBottom = 8; box.Add(_classTitle); _classWarBtn = MenuUi.Button("WARRIOR", () => ClassSelectSendSystem.RequestClass(ClassTraits.WarriorClass)); _classWarBtn.style.marginBottom = 4; box.Add(_classWarBtn); _classRangerBtn = MenuUi.Button("RANGER", () => ClassSelectSendSystem.RequestClass(ClassTraits.RangerClass)); box.Add(_classRangerBtn); _classPanel.Add(box); root.Add(_classPanel); } void UpdatePrepPanel(bool show, int ore, int bio, int aether) { if (!show) { if (_prepPanel != null) _prepPanel.style.display = DisplayStyle.None; _prepShownFor = 0; return; } var root = _doc != null ? _doc.rootVisualElement : null; if (root == null) return; if (!_prepPanelBuilt) { BuildPrepPanel(root); _prepPanelBuilt = true; } uint boughtMask = 0; foreach (var mods in SystemAPI.Query>().WithAll()) { for (int m = 0; m < mods.Length; m++) { uint sid = mods[m].SourceId; if (sid >= Tuning.PrepSourceIdBase && sid < Tuning.PrepSourceIdBase + Tuning.PrepSourceIdSpan) boughtMask |= (uint)(1 << (int)(sid - Tuning.PrepSourceIdBase)); } break; } int sig = ore * 7 ^ bio * 13 ^ aether * 31 ^ (int)boughtMask * 101; if (sig == 0) sig = 1; if (_prepShownFor != sig) { _prepRowsHost.Clear(); for (int i = 0; i < PrepCatalog.Count; i++) { var r = PrepCatalog.Rows[i]; int have = r.CostResId == ResourceId.Aether ? aether : r.CostResId == ResourceId.Biomass ? bio : ore; bool bought = (boughtMask & (uint)(1 << r.Id)) != 0; string resName = r.CostResId == ResourceId.Aether ? "Aether" : r.CostResId == ResourceId.Biomass ? "Biomass" : "Ore"; string label = PrepLabel(r.Id) + (bought ? " BOUGHT" : " - " + r.Cost + " " + resName); byte buyId = r.Id; var row = MenuUi.Button(label, () => PrepPurchaseSendSystem.RequestPrep(buyId)); row.style.width = 240; row.style.marginBottom = 4; row.style.whiteSpace = WhiteSpace.Normal; row.style.unityTextAlign = TextAnchor.MiddleLeft; row.SetEnabled(!bought && have >= r.Cost); _prepRowsHost.Add(row); } _prepShownFor = sig; } _prepPanel.style.display = DisplayStyle.Flex; } static string PrepLabel(byte id) => id == 0 ? "+30 Max HP" : id == 1 ? "+12% Move Speed" : id == 2 ? "+20% Melee Damage" : "+20% Ranged Damage"; void BuildPrepPanel(VisualElement root) { _prepPanel = new VisualElement { pickingMode = PickingMode.Ignore }; _prepPanel.style.position = Position.Absolute; _prepPanel.style.left = 12; _prepPanel.style.top = Length.Percent(45); _prepPanel.style.display = DisplayStyle.None; var box = new VisualElement(); box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.92f); MenuUi.Round(box, 10); box.style.paddingLeft = 12; box.style.paddingRight = 12; box.style.paddingTop = 10; box.style.paddingBottom = 10; _prepTitle = new Label("PREP LOADOUT (lasts the run)"); _prepTitle.style.color = MenuUi.Accent; _prepTitle.style.fontSize = 14; _prepTitle.style.unityFontStyleAndWeight = FontStyle.Bold; _prepTitle.style.marginBottom = 8; box.Add(_prepTitle); _prepRowsHost = new VisualElement(); box.Add(_prepRowsHost); _prepPanel.Add(box); root.Add(_prepPanel); } void UpdatePortalPrompt(RunInfo runInfo, bool haveRun) { var root = _doc != null ? _doc.rootVisualElement : null; if (root == null) return; if (!_portalBuilt) { BuildPortalPrompt(root); _portalBuilt = true; } bool show = false, inRange = false; if (haveRun && runInfo.Lifecycle == RunLifecycle.RoomExplore && SystemAPI.TryGetSingleton(out var anchor)) { 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()) { inRange = math.distance(lt.ValueRO.Position.xz, portalPos.xz) <= Tuning.PortalInteractRange; if (inRange) { 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; } void BuildPortalPrompt(VisualElement root) { _portalPrompt = HudUi.Display("PRESS E TO LEAVE — the haul comes home", 20, new Color(0.55f, 0.95f, 1f), TextAnchor.MiddleCenter); _portalPrompt.style.position = Position.Absolute; _portalPrompt.style.left = 0; _portalPrompt.style.right = 0; _portalPrompt.style.bottom = 240; _portalPrompt.pickingMode = PickingMode.Ignore; _portalPrompt.style.display = DisplayStyle.None; root.Add(_portalPrompt); } } }