b34945c2d2
Deletes CyclePhaseSystem, GoalReachedSystem, CoreDamage/CoreRestore, ThreatDirector, CoreIntegrity/GoalProgress/RunPhase/RunOutcome/ThreatState components, CoreVisualFeedbackSystem, and the whole Client/Onboarding slice (+6 test files). Keepers reworked: RunDirectorSystem (UpdateBefore attr + launch guard + goal/threat bank removed; sole SaveRequest raiser now), CycleDirectorSpawnSystem (ledger/meta host only), WaveSystem UNGATED (waves run wherever a WaveDirector is baked), EnemyAISystem core-fallback stripped, AmbientAudioSystem reworked (bed + run cues; no CycleState gate), MusicSystem RunInfo-only, HudSystem big trim (goal meter, core bar, siege banner, terminal banner, outcome flash, onboarding hook all gone), MetaShop/ClassPrep/AimReticle siege gates dropped, DebugOverlay/ops re-meant (SpawnWave=force next wave, EndSiege=quiet arena; SetCalm/AdvanceGoal/SetHeat retired, bytes reserved), TuningConfig Core knobs retired (ids 20-23 reserved), StorageMath.DrainFraction deleted, HowToPlay copy rewritten. Save epoch v7 (fresh epoch, operator-approved): SaveData drops goal/core/outcome + conveyor/machine-IO fields; MinLoadableVersion=7; PendingSave/PendingStructure trimmed; RollTerminalCampaignForward deleted; SaveStructureScan signature slimmed. 390 tests green; Play world-creation clean (player + waves live, no exceptions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
241 lines
13 KiB
C#
241 lines
13 KiB
C#
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>
|
|
/// DR-046 base class-select + prep-loadout panels (Staging) + the room-exit portal prompt (RoomExplore) —
|
|
/// extracted from <see cref="HudSystem"/> into their own client-only, observe-only presentation
|
|
/// <see cref="SystemBase"/> in <see cref="PresentationSystemGroup"/>. Owns its own runtime UIDocument sharing
|
|
/// <see cref="MenuUi.LoadPanelSettings"/> (sortingOrder 54). Recomputes the local class + ore/bio/aether + the
|
|
/// Staging gate + the RoomExplore portal proximity locally; clicks enqueue through
|
|
/// <see cref="ClassSelectSendSystem"/> / <see cref="PrepPurchaseSendSystem"/> / <see cref="PortalInteractSendSystem"/>.
|
|
/// NOTE (behavior-preserving): the class/prep Staging gate reproduces the original's FULL condition — it also
|
|
/// requires the <see cref="MetaUpgradeCatalog"/> + <see cref="MetaTierState"/> buffer to be present (the panels
|
|
/// were gated on the same <c>metaShow</c> boolean as the meta shop).
|
|
/// </summary>
|
|
[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<UIDocument>();
|
|
_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<RunInfo>(out var runInfo);
|
|
|
|
// Resources from the ledger (last entry per type wins, matching the core loop).
|
|
int aether = 0, ore = 0, bio = 0;
|
|
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
|
|
{
|
|
var buf = SystemAPI.GetBuffer<StorageEntry>(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 (fr, ar) in SystemAPI.Query<RefRO<FrameId>, RefRO<AbilityRef>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
|
|
{
|
|
localClass = fr.ValueRO.Value != 0 ? fr.ValueRO.Value : ClassTraits.ClassForAbility(ar.ValueRO.Id); // FrameId signal, AbilityRef fallback
|
|
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<MetaTierState> metaRecord = default;
|
|
bool metaShow = haveRun && runInfo.Lifecycle == RunLifecycle.Staging && haveLocalPlayer
|
|
&& SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out var metaCat) && metaCat.Value.IsCreated
|
|
&& SystemAPI.TryGetSingletonBuffer<MetaTierState>(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<DynamicBuffer<StatModifier>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
|
|
{
|
|
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<BaseAnchor>(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<RefRO<LocalTransform>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|