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>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// The choice-of-3 boon modal (RoomReward) — extracted from <see cref="HudSystem"/> into its own client-only,
|
||||
/// observe-only presentation <see cref="SystemBase"/> in <see cref="PresentationSystemGroup"/>. Owns its own
|
||||
/// runtime UIDocument sharing <see cref="MenuUi.LoadPanelSettings"/> (sortingOrder 55) so it composes into the
|
||||
/// same UITK panel + event dispatcher as the HUD (50) / markers (48) / onboarding (60). Reads the local player's
|
||||
/// replicated <see cref="BoonOffer"/> + the <see cref="BoonCatalog"/> blob; card clicks enqueue through
|
||||
/// <see cref="BoonSendSystem.PickBoon"/>. Built lazily on first show.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
[UpdateInGroup(typeof(PresentationSystemGroup))]
|
||||
public partial class BoonModalHudSystem : SystemBase
|
||||
{
|
||||
GameObject _go;
|
||||
UIDocument _doc;
|
||||
bool _built;
|
||||
|
||||
VisualElement _boonModal, _boonCardRow;
|
||||
int _boonShownFor; // last exact (Option0|Option1<<8|Option2<<16)+1 signature the modal was built for
|
||||
bool _boonModalBuilt;
|
||||
|
||||
protected override void OnStartRunning()
|
||||
{
|
||||
if (_go != null) return;
|
||||
MenuUi.EnsureEventSystem();
|
||||
_go = new GameObject("~HUDBoonModal");
|
||||
_doc = _go.AddComponent<UIDocument>();
|
||||
_doc.panelSettings = MenuUi.LoadPanelSettings();
|
||||
_doc.sortingOrder = 55;
|
||||
}
|
||||
|
||||
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<RunInfo>(out var runInfo);
|
||||
|
||||
BoonOffer localOffer = default;
|
||||
bool hasOffer = false;
|
||||
foreach (var off in SystemAPI.Query<RefRO<BoonOffer>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
|
||||
{
|
||||
localOffer = off.ValueRO;
|
||||
hasOffer = true;
|
||||
break;
|
||||
}
|
||||
BlobAssetReference<BoonCatalogBlob> boonPool = default;
|
||||
if (SystemAPI.TryGetSingleton<BoonCatalog>(out var bcat))
|
||||
boonPool = bcat.Value;
|
||||
// Lifecycle gate (post-impl review): even a stale replicated Pending never shows the modal outside
|
||||
// the reward window.
|
||||
UpdateBoonModal(localOffer, hasOffer && localOffer.Pending == 1
|
||||
&& haveRun && runInfo.Lifecycle == RunLifecycle.RoomReward, boonPool);
|
||||
}
|
||||
|
||||
void UpdateBoonModal(BoonOffer offer, bool show, BlobAssetReference<BoonCatalogBlob> pool)
|
||||
{
|
||||
if (!show || !pool.IsCreated)
|
||||
{
|
||||
if (_boonModal != null) _boonModal.style.display = DisplayStyle.None;
|
||||
_boonShownFor = 0;
|
||||
return;
|
||||
}
|
||||
var root = _doc != null ? _doc.rootVisualElement : null;
|
||||
if (root == null) return;
|
||||
if (!_boonModalBuilt)
|
||||
{
|
||||
BuildBoonModal(root);
|
||||
_boonModalBuilt = true;
|
||||
}
|
||||
|
||||
// Rebuild the three cards only when the offer actually changes (a new room's deal).
|
||||
// Exact signature (post-impl review): the old lossy byte XOR could collide across consecutive
|
||||
// rooms and leave stale card labels. +1 keeps 0 as the hidden/reset sentinel.
|
||||
int sig = 1 + (offer.Option0 | (offer.Option1 << 8) | (offer.Option2 << 16));
|
||||
if (_boonShownFor != sig)
|
||||
{
|
||||
_boonCardRow.Clear();
|
||||
ref var defs = ref pool.Value;
|
||||
for (byte k = 0; k < 3; k++)
|
||||
{
|
||||
byte id = k == 2 ? offer.Option2 : k == 1 ? offer.Option1 : offer.Option0;
|
||||
int idx = BoonMath.FindDef(ref defs, id);
|
||||
string title = idx >= 0 ? defs.Defs[idx].Name.ToString() : ("BOON " + id);
|
||||
string desc = idx >= 0 ? defs.Defs[idx].Desc.ToString() : "";
|
||||
byte weight = idx >= 0 ? defs.Defs[idx].Weight : (byte)100;
|
||||
byte pick = k; // capture a COPY into the closure, never the loop variable
|
||||
var card = MenuUi.Button(title + "\n" + desc, () => BoonSendSystem.PickBoon(pick));
|
||||
card.style.width = 200;
|
||||
card.style.height = StyleKeyword.Auto; // long descs grow the card
|
||||
card.style.minHeight = 96;
|
||||
// Rarity from the draw weight (100 common / 60 uncommon / 30 rare / 10 epic).
|
||||
var rare = weight <= 10 ? new Color(1f, 0.82f, 0.30f)
|
||||
: weight <= 30 ? new Color(0.65f, 0.50f, 1f)
|
||||
: weight <= 60 ? new Color(0.45f, 0.95f, 0.55f)
|
||||
: new Color(1f, 1f, 1f, 0.30f);
|
||||
MenuUi.Border(card, rare, weight <= 30 ? 2.5f : 1.5f);
|
||||
card.style.marginLeft = 8;
|
||||
card.style.marginRight = 8;
|
||||
card.style.whiteSpace = WhiteSpace.Normal;
|
||||
_boonCardRow.Add(card);
|
||||
}
|
||||
_boonShownFor = sig;
|
||||
}
|
||||
_boonModal.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
|
||||
void BuildBoonModal(VisualElement root)
|
||||
{
|
||||
_boonModal = new VisualElement { pickingMode = PickingMode.Ignore };
|
||||
_boonModal.style.position = Position.Absolute;
|
||||
_boonModal.style.left = 0; _boonModal.style.right = 0;
|
||||
_boonModal.style.top = 0; _boonModal.style.bottom = 0;
|
||||
_boonModal.style.alignItems = Align.Center;
|
||||
_boonModal.style.justifyContent = Justify.Center;
|
||||
_boonModal.style.display = DisplayStyle.None;
|
||||
|
||||
var box = new VisualElement();
|
||||
box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.96f);
|
||||
box.style.borderTopLeftRadius = 10; box.style.borderTopRightRadius = 10;
|
||||
box.style.borderBottomLeftRadius = 10; box.style.borderBottomRightRadius = 10;
|
||||
box.style.paddingLeft = 18; box.style.paddingRight = 18;
|
||||
box.style.paddingTop = 14; box.style.paddingBottom = 16;
|
||||
box.style.alignItems = Align.Center;
|
||||
|
||||
var title = new Label("ROOM CLEARED — CHOOSE A BOON");
|
||||
title.style.color = new Color(0.6f, 1f, 0.7f);
|
||||
title.style.fontSize = 18;
|
||||
title.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
title.style.marginBottom = 12;
|
||||
box.Add(title);
|
||||
|
||||
_boonCardRow = new VisualElement();
|
||||
_boonCardRow.style.flexDirection = FlexDirection.Row;
|
||||
box.Add(_boonCardRow);
|
||||
|
||||
_boonModal.Add(box);
|
||||
root.Add(_boonModal);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user