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 { /// /// The Staging-only permanent-upgrade shop (meta shop) — extracted from into its own /// client-only, observe-only presentation in . /// Owns its own runtime UIDocument sharing (sortingOrder 51). Recomputes /// its inputs locally: the local class from the replicated /// (), Aether from the buffer, siege from /// , and the Staging gate from . Row clicks enqueue through /// — the server re-validates everything. /// [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] [UpdateInGroup(typeof(PresentationSystemGroup))] public partial class MetaShopHudSystem : SystemBase { GameObject _go; UIDocument _doc; bool _built; VisualElement _metaPanel, _metaRowsHost; Label _metaShopTitle; bool _metaShopBuilt; int _metaShownFor; // last (class, tiers, aether) signature the shop rows were built for protected override void OnStartRunning() { if (_go != null) return; MenuUi.EnsureEventSystem(); _go = new GameObject("~HUDMetaShop"); _doc = _go.AddComponent(); _doc.panelSettings = MenuUi.LoadPanelSettings(); _doc.sortingOrder = 51; } 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; // Aether from the ledger (the sole meta-shop currency; last entry wins, matching the core loop). int aether = 0; if (SystemAPI.TryGetSingletonEntity(out var ledgerE)) { var buf = SystemAPI.GetBuffer(ledgerE); for (int i = 0; i < buf.Length; i++) if (buf[i].ItemId == ResourceId.Aether) aether = buf[i].Count; } // Local class derives from the replicated AbilityRef (tracks the dev class-switch; PlayerClass is // server-only); tiers from the replicated MetaTierState record on the director ghost. byte localClass = ClassTraits.WarriorClass; bool haveLocalPlayer = false; foreach (var ar in SystemAPI.Query>().WithAll()) { localClass = ClassTraits.ClassForAbility(ar.ValueRO.Id); haveLocalPlayer = true; break; } bool metaShow = false; BlobAssetReference metaPool = default; DynamicBuffer metaRecord = default; if (haveRun && runInfo.Lifecycle == RunLifecycle.Staging && haveLocalPlayer && !siege && SystemAPI.TryGetSingleton(out var metaCat) && metaCat.Value.IsCreated && SystemAPI.TryGetSingletonBuffer(out metaRecord, true)) { metaPool = metaCat.Value; metaShow = true; } UpdateMetaShop(metaShow, localClass, aether, metaPool, metaRecord); } void UpdateMetaShop(bool show, byte classId, int aether, BlobAssetReference pool, DynamicBuffer record) { if (!show) { if (_metaPanel != null) _metaPanel.style.display = DisplayStyle.None; _metaShownFor = 0; return; } var root = _doc != null ? _doc.rootVisualElement : null; if (root == null) return; if (!_metaShopBuilt) { BuildMetaShop(root); _metaShopBuilt = true; } // Rebuild the rows only when class / owned tiers / affordability actually change (Staging-only, <=8 rows). int sig = classId * 131 ^ aether * 31; for (int i = 0; i < record.Length; i++) sig ^= (record[i].ClassId * 7 + record[i].UpgradeId * 13 + record[i].Tier) * (i + 3); if (sig == 0) sig = 1; if (_metaShownFor != sig) { _metaRowsHost.Clear(); _metaShopTitle.text = (classId == ClassTraits.RangerClass ? "RANGER" : "WARRIOR") + " PERMANENT UPGRADES - AETHER " + aether; ref var defs = ref pool.Value; byte classBit = BoonMath.MaskFor(classId); for (int d = 0; d < defs.Defs.Length; d++) { if ((defs.Defs[d].ClassMask & classBit) == 0) continue; byte id = defs.Defs[d].Id; byte owned = MetaMath.TierOf(record, classId, id); if (owned > defs.Defs[d].MaxTier) owned = defs.Defs[d].MaxTier; // D-F5 display clamp (seed AND spend AND shop) bool maxed = owned >= defs.Defs[d].MaxTier; int cost = MetaMath.CostForTier(in defs.Defs[d], owned); string label = defs.Defs[d].Name.ToString() + (maxed ? " MAXED" : " - " + cost + " Aether") + "\n" + defs.Defs[d].Desc.ToString(); byte buyId = id; // closure copy, never the loop variable var row = MenuUi.Button(label, () => MetaSpendSendSystem.RequestPurchase(buyId)); row.style.width = 290; row.style.height = StyleKeyword.Auto; // two-line labels must grow the row (overlap fix) row.style.paddingTop = 6; row.style.paddingBottom = 6; row.style.marginBottom = 4; row.style.whiteSpace = WhiteSpace.Normal; row.style.unityTextAlign = TextAnchor.MiddleLeft; row.SetEnabled(!maxed && aether >= cost); // honest UI; the server re-validates everything anyway // Owned-tier pips (replaces the "[2/5]" text — reads at a glance). var pipRow = new VisualElement { pickingMode = PickingMode.Ignore }; pipRow.style.flexDirection = FlexDirection.Row; pipRow.style.marginTop = 3; for (int p = 0; p < defs.Defs[d].MaxTier; p++) { var tp = new VisualElement { pickingMode = PickingMode.Ignore }; tp.style.width = 9; tp.style.height = 9; tp.style.marginRight = 3; MenuUi.Round(tp, 4.5f); tp.style.backgroundColor = p < owned ? MenuUi.Accent : new Color(1f, 1f, 1f, 0.14f); pipRow.Add(tp); } row.Add(pipRow); _metaRowsHost.Add(row); } _metaShownFor = sig; } _metaPanel.style.display = DisplayStyle.Flex; } void BuildMetaShop(VisualElement root) { _metaPanel = new VisualElement { pickingMode = PickingMode.Ignore }; _metaPanel.style.position = Position.Absolute; _metaPanel.style.right = 12; _metaPanel.style.top = Length.Percent(22); _metaPanel.style.alignItems = Align.FlexEnd; _metaPanel.style.display = DisplayStyle.None; var box = new VisualElement(); box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.92f); box.style.borderTopLeftRadius = 10; box.style.borderTopRightRadius = 10; box.style.borderBottomLeftRadius = 10; box.style.borderBottomRightRadius = 10; box.style.paddingLeft = 12; box.style.paddingRight = 12; box.style.paddingTop = 10; box.style.paddingBottom = 10; _metaShopTitle = new Label("PERMANENT UPGRADES"); _metaShopTitle.style.color = MenuUi.Accent; _metaShopTitle.style.fontSize = 14; _metaShopTitle.style.unityFontStyleAndWeight = FontStyle.Bold; _metaShopTitle.style.marginBottom = 8; box.Add(_metaShopTitle); _metaRowsHost = new VisualElement(); box.Add(_metaRowsHost); _metaPanel.Add(box); root.Add(_metaPanel); } } }