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>
198 lines
9.3 KiB
C#
198 lines
9.3 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>
|
|
/// The Staging-only permanent-upgrade shop (meta shop) — 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 51). Recomputes
|
|
/// its inputs locally: the local class from the replicated <see cref="AbilityRef"/>
|
|
/// (<see cref="ClassTraits.ClassForAbility"/>), Aether from the <see cref="ResourceLedger"/> buffer, siege from
|
|
/// <see cref="CycleState"/>, and the Staging gate from <see cref="RunInfo"/>. Row clicks enqueue through
|
|
/// <see cref="MetaSpendSendSystem.RequestPurchase"/> — the server re-validates everything.
|
|
/// </summary>
|
|
[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<UIDocument>();
|
|
_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<RunInfo>(out var runInfo);
|
|
|
|
// Aether from the ledger (the sole meta-shop currency; last entry wins, matching the core loop).
|
|
int aether = 0;
|
|
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
|
|
{
|
|
var buf = SystemAPI.GetBuffer<StorageEntry>(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 (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;
|
|
}
|
|
|
|
bool metaShow = false;
|
|
BlobAssetReference<MetaUpgradeCatalogBlob> metaPool = default;
|
|
DynamicBuffer<MetaTierState> metaRecord = default;
|
|
if (haveRun && runInfo.Lifecycle == RunLifecycle.Staging && haveLocalPlayer
|
|
&& SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out var metaCat) && metaCat.Value.IsCreated
|
|
&& SystemAPI.TryGetSingletonBuffer<MetaTierState>(out metaRecord, true))
|
|
{
|
|
metaPool = metaCat.Value;
|
|
metaShow = true;
|
|
}
|
|
UpdateMetaShop(metaShow, localClass, aether, metaPool, metaRecord);
|
|
}
|
|
|
|
void UpdateMetaShop(bool show, byte classId, int aether,
|
|
BlobAssetReference<MetaUpgradeCatalogBlob> pool, DynamicBuffer<MetaTierState> 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);
|
|
}
|
|
}
|
|
}
|