UITK HUD rework + build palette (click-to-place ghost)

Rebuild the in-game HUD on UI Toolkit (HudUi/HudSystem, Aether palette) consistent with the menu; build-palette bar (BuildPaletteState) drives cursor->cell ground-ghost preview (green/red via BuildPreviewMath), left-click place / right-click cancel / rotate; fire suppressed in build mode; combat juice restyle. +4 BuildPreviewMath EditMode tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 15:05:49 -07:00
parent f31ffe910b
commit a4edf7a03b
13 changed files with 579 additions and 219 deletions
@@ -343,6 +343,7 @@ namespace ProjectM.Client
tm.anchor = TextAnchor.MiddleCenter;
tm.alignment = TextAlignment.Center;
tm.color = Color.white;
tm.fontStyle = FontStyle.Bold;
go.SetActive(false);
return new FloatingNumber { Tm = tm, Tr = go.transform, Active = false };
}
@@ -358,7 +359,7 @@ namespace ProjectM.Client
fn.Age = 0f;
fn.Life = 0.7f;
fn.Tm.text = Mathf.Max(1, Mathf.RoundToInt(amount)).ToString();
fn.BaseColor = isLocalPlayer ? new Color(1f, 0.32f, 0.26f) : new Color(1f, 0.92f, 0.45f);
fn.BaseColor = isLocalPlayer ? new Color(1f, 0.5f, 0.22f) : new Color(0.45f, 0.92f, 1f); // Blight orange (hurt) / Aether cyan (you hit)
fn.Tm.color = fn.BaseColor;
fn.Tr.position = worldPos + Vector3.up * 1.4f + new Vector3(UnityEngine.Random.Range(-0.25f, 0.25f), 0f, 0f);
fn.Vel = new Vector3(0f, 2.2f, 0f);
@@ -478,7 +479,7 @@ namespace ProjectM.Client
static void PlayClip(AudioClip clip, Vector3 pos, float vol)
{
if (clip == null) return;
AudioSource.PlayClipAtPoint(clip, pos, vol);
AudioSource.PlayClipAtPoint(clip, pos, vol * GameVolume.Sfx);
}
}
}
@@ -1,37 +1,41 @@
using System.Collections.Generic;
using ProjectM.Simulation;
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.UIElements;
namespace ProjectM.Client
{
/// <summary>
/// Client-only screen HUD. A managed presentation SystemBase (<see cref="PresentationSystemGroup"/>) that builds
/// a uGUI overlay canvas in code and drives it from the LOCAL player ghost each frame: a health bar
/// (Health / EffectiveCharacterStats.MaxHealth), an ability-cooldown bar (AbilityCooldown vs NetworkTime
/// ServerTick + EffectiveAbilityStats.CooldownTicks), a live Husk threat count, and a DOWNED/RESPAWNING overlay
/// (the derived <see cref="Dead"/> gate). Presentation only — no simulation, client world only. Bars are
/// RawImages over <c>Texture2D.whiteTexture</c> (always available; the fill width is the RectTransform's
/// anchorMax.x), so the HUD needs no sprite assets — only a resolved builtin font for the labels.
/// Client-only screen HUD, rebuilt on UI Toolkit so it reads in the same Aether-cyan visual language as the
/// menu / pause / settings screens (<see cref="MenuUi"/> / <see cref="HudUi"/>). A managed presentation
/// SystemBase (<see cref="PresentationSystemGroup"/>) that OBSERVES the local player ghost + the global
/// cycle / ledger / goal each frame and pushes values into a runtime UIDocument (shared PanelSettings,
/// sortingOrder 50 so it sits BEHIND the pause overlay's 100). The root is <c>pickingMode = Ignore</c> so the
/// HUD never eats clicks meant for the game world — only the build-palette buttons pick. Presentation only
/// (client world, no simulation). The palette buttons set <see cref="BuildPaletteState"/> (client-local UI
/// state), which BuildSendSystem turns into a ground ghost + click-to-place. The visual tree is built on the
/// first OnUpdate where the UIDocument's root exists (giving the panel a frame to initialise its PanelSettings).
/// </summary>
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class HudSystem : SystemBase
{
Canvas _canvas;
RectTransform _healthFill;
RectTransform _cooldownFill;
Text _healthText;
Text _threatText;
Text _phaseText;
Text _resourceText;
Text _locationText;
RectTransform _goalFill;
Text _goalText;
GameObject _respawnOverlay;
GameObject _hudGo;
UIDocument _doc;
bool _built;
VisualElement _healthFill, _cooldownFill, _goalFill, _downed, _paletteRow;
Label _healthText, _threatText, _phaseText, _resourceText, _locationText, _goalText;
bool _paletteBuilt;
readonly Dictionary<byte, PaletteItem> _palette = new();
EntityQuery _huskQuery;
struct PaletteItem { public VisualElement Root; public Label Cost; public int CostAmount; }
protected override void OnCreate()
{
_huskQuery = GetEntityQuery(ComponentType.ReadOnly<EnemyTag>());
@@ -39,76 +43,88 @@ namespace ProjectM.Client
protected override void OnStartRunning()
{
if (_canvas == null) BuildHud();
if (_hudGo != null) return;
MenuUi.EnsureEventSystem();
_hudGo = new GameObject("~HUD");
_doc = _hudGo.AddComponent<UIDocument>();
_doc.panelSettings = MenuUi.LoadPanelSettings();
_doc.sortingOrder = 50; // behind the pause overlay (100)
}
protected override void OnDestroy()
{
if (_canvas != null) Object.Destroy(_canvas.gameObject);
if (_hudGo != null) Object.Destroy(_hudGo);
}
protected override void OnUpdate()
{
if (_canvas == null) return;
if (_doc == null) return;
if (!_built)
{
var r = _doc.rootVisualElement;
if (r == null) return; // panel not initialised yet (next frame)
BuildTree(r);
_built = true;
}
bool haveTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt);
int huskCount = _huskQuery.CalculateEntityCount();
// Macro-loop HUD (phase + cycle + countdown + location), read before the per-player early-out so it persists pre-spawn.
// ---- Macro loop: phase + cycle + countdown ----
bool haveCycle = SystemAPI.TryGetSingleton<CycleState>(out var cyc);
if (_phaseText != null && haveCycle)
if (haveCycle)
{
var endTick = new NetworkTick(cyc.PhaseEndTick);
string detail;
if (cyc.Phase == CyclePhase.Siege)
detail = "WAVE " + cyc.WaveNumber + " - " + _huskQuery.CalculateEntityCount() + " HUSKS";
detail = "WAVE " + cyc.WaveNumber + " - " + huskCount + " HUSKS";
else if (haveTick && cyc.PhaseEndTick != 0 && endTick.IsValid && endTick.IsNewerThan(nt.ServerTick))
detail = "INCURSION IN " + (endTick.TicksSince(nt.ServerTick) / 60 + 1) + "s";
else
detail = "";
_phaseText.text = PhaseLabel(cyc.Phase) + (detail.Length > 0 ? " - " + detail : "");
_phaseText.color = PhaseColor(cyc.Phase);
_phaseText.style.color = PhaseColor(cyc.Phase);
}
else if (_phaseText != null)
else
{
_phaseText.text = "";
}
if (_locationText != null)
{
var cam = Camera.main;
bool onExpedition = cam != null && cam.transform.position.x > 500f;
_locationText.text = onExpedition
? "ON EXPEDITION - return through the gate"
: "AT BASE - deploy through the gate when you're ready";
_locationText.color = onExpedition ? new Color(1f, 0.8f, 0.4f) : new Color(0.6f, 0.85f, 1f);
}
// ---- Location + gate hint ----
var cam = Camera.main;
bool onExpedition = cam != null && cam.transform.position.x > 500f;
_locationText.text = onExpedition
? "ON EXPEDITION - return through the gate"
: "AT BASE - deploy through the gate when you're ready";
_locationText.style.color = onExpedition ? new Color(1f, 0.8f, 0.4f) : new Color(0.6f, 0.85f, 1f);
if (_goalFill != null && SystemAPI.TryGetSingleton<GoalProgress>(out var goal))
// ---- Goal ----
if (SystemAPI.TryGetSingleton<GoalProgress>(out var goal))
{
float gfrac = goal.Target > 0 ? Mathf.Clamp01(goal.Charge / (float)goal.Target) : 0f;
SetFill(_goalFill, gfrac);
if (_goalText != null) _goalText.text = "GOAL " + goal.Charge + " / " + goal.Target;
HudUi.SetFill(_goalFill, gfrac);
_goalText.text = "GOAL " + goal.Charge + " / " + goal.Target;
}
if (_resourceText != null)
// ---- Resources (Ore feeds the palette affordability) ----
int aether = 0, ore = 0, bio = 0;
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
{
string res = "";
if (SystemAPI.TryGetSingletonEntity<ResourceLedger>(out var ledgerE))
var buf = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
for (int i = 0; i < buf.Length; i++)
{
var buf = SystemAPI.GetBuffer<StorageEntry>(ledgerE);
int aether = 0, ore = 0, bio = 0;
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;
}
res = "AETHER " + aether + " ORE " + ore + " BIO " + bio;
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;
}
_resourceText.text = res;
}
_resourceText.text = "AETHER " + aether + " ORE " + ore + " BIO " + bio;
// ---- Build palette (lazy-built once the catalog has streamed; hidden off-base) ----
UpdatePalette(ore, onExpedition);
// ---- Per-player vitals ----
bool found = false;
float hp = 0f, maxHp = 1f, cdFrac = 1f;
bool dead = false, shielded = false;
@@ -123,8 +139,6 @@ namespace ProjectM.Client
maxHp = effChar.ValueRO.MaxHealth > 0f ? effChar.ValueRO.MaxHealth : health.ValueRO.Max;
dead = SystemAPI.IsComponentEnabled<Dead>(entity);
// Cooldown fraction via wrap-safe NetworkTick compare (raw uint subtraction is unsafe across
// tick wraparound — the project convention, mirroring AbilityFireSystem/EnemyAISystem).
uint nextFire = cd.ValueRO.NextFireTick;
int cdTicks = effAbility.ValueRO.CooldownTicks;
var nextTick = new NetworkTick(nextFire);
@@ -138,166 +152,144 @@ namespace ProjectM.Client
break;
}
_canvas.enabled = found || haveCycle;
if (!found) return;
_doc.rootVisualElement.style.display = (found || haveCycle) ? DisplayStyle.Flex : DisplayStyle.None;
if (!found) { _downed.style.display = DisplayStyle.None; return; }
float frac = maxHp > 0f ? Mathf.Clamp01(hp / maxHp) : 0f;
SetFill(_healthFill, frac);
var hc = _healthFill.GetComponent<RawImage>();
if (hc != null)
hc.color = shielded
? new Color(0.45f, 0.85f, 1f)
: Color.Lerp(new Color(0.92f, 0.16f, 0.16f), new Color(0.25f, 0.9f, 0.5f), frac);
if (_healthText != null)
_healthText.text = Mathf.CeilToInt(Mathf.Max(0f, hp)) + " / " + Mathf.CeilToInt(maxHp) + (shielded ? " SHIELDED" : "");
HudUi.SetFill(_healthFill, frac);
_healthFill.style.backgroundColor = shielded
? new Color(0.45f, 0.85f, 1f)
: Color.Lerp(new Color(0.92f, 0.16f, 0.16f), new Color(0.25f, 0.9f, 0.5f), frac);
_healthText.text = Mathf.CeilToInt(Mathf.Max(0f, hp)) + " / " + Mathf.CeilToInt(maxHp) + (shielded ? " SHIELDED" : "");
SetFill(_cooldownFill, cdFrac);
if (_threatText != null)
_threatText.text = "HUSKS " + _huskQuery.CalculateEntityCount();
_respawnOverlay.SetActive(dead);
HudUi.SetFill(_cooldownFill, cdFrac);
_threatText.text = "HUSKS " + huskCount;
_downed.style.display = dead ? DisplayStyle.Flex : DisplayStyle.None;
}
static void SetFill(RectTransform fill, float frac)
void UpdatePalette(int ore, bool onExpedition)
{
if (fill == null) return;
var max = fill.anchorMax;
max.x = Mathf.Clamp01(frac);
fill.anchorMax = max;
if (!_paletteBuilt && SystemAPI.TryGetSingletonEntity<StructureCatalog>(out var catE))
{
var cat = SystemAPI.GetBuffer<StructureCatalogEntry>(catE);
for (int i = 0; i < cat.Length; i++)
AddPaletteItem(cat[i].Type, cat[i].CostAmount);
_paletteBuilt = true;
}
if (!_paletteBuilt) { _paletteRow.style.display = DisplayStyle.None; return; }
_paletteRow.style.display = onExpedition ? DisplayStyle.None : DisplayStyle.Flex;
foreach (var kv in _palette)
{
var item = kv.Value;
bool affordable = ore >= item.CostAmount;
bool selected = BuildPaletteState.Selected == kv.Key;
item.Root.style.opacity = affordable ? 1f : 0.45f;
item.Cost.style.color = affordable ? new Color(0.7f, 0.95f, 0.8f) : new Color(1f, 0.5f, 0.4f);
MenuUi.Border(item.Root, selected ? MenuUi.Accent : new Color(1f, 1f, 1f, 0.08f), selected ? 2 : 1);
item.Root.style.backgroundColor = selected
? new Color(0.16f, 0.26f, 0.32f, 0.95f)
: new Color(0.09f, 0.11f, 0.15f, 0.92f);
}
}
// ---- uGUI construction (code-built; no prefab/sprite assets) ----
void BuildHud()
void AddPaletteItem(byte type, int cost)
{
var go = new GameObject("~HUD");
_canvas = go.AddComponent<Canvas>();
_canvas.renderMode = RenderMode.ScreenSpaceOverlay;
_canvas.sortingOrder = 100;
var scaler = go.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
go.AddComponent<GraphicRaycaster>();
if (type == 0 || _palette.ContainsKey(type)) return;
var root = new VisualElement();
root.style.width = 92;
root.style.marginLeft = 4; root.style.marginRight = 4;
root.style.paddingTop = 6; root.style.paddingBottom = 6;
root.style.alignItems = Align.Center;
root.style.backgroundColor = new Color(0.09f, 0.11f, 0.15f, 0.92f);
root.pickingMode = PickingMode.Position;
MenuUi.Round(root, 6);
MenuUi.Border(root, new Color(1f, 1f, 1f, 0.08f), 1);
var font = GetFont();
var nameLabel = HudUi.Text(StructureName(type), 13, MenuUi.TextCol, TextAnchor.MiddleCenter);
var costLabel = HudUi.Text(cost + " ORE", 12, new Color(0.7f, 0.95f, 0.8f), TextAnchor.MiddleCenter);
costLabel.style.marginTop = 2;
root.Add(nameLabel);
root.Add(costLabel);
// Health bar (bottom-left).
var hpBg = MakeBar("HealthBg", _canvas.transform, new Color(0f, 0f, 0f, 0.6f),
new Vector2(40, 46), new Vector2(440, 40));
_healthFill = MakeFill("HealthFill", hpBg, new Color(0.25f, 0.9f, 0.5f));
_healthText = MakeText("HealthText", hpBg, "100 / 100", 24, TextAnchor.MiddleCenter, Color.white, font);
byte t = type;
root.RegisterCallback<ClickEvent>(_ =>
BuildPaletteState.Select(BuildPaletteState.Selected == t ? (byte)0 : t));
// Cooldown bar (just above health).
var cdBg = MakeBar("CooldownBg", _canvas.transform, new Color(0f, 0f, 0f, 0.55f),
new Vector2(40, 92), new Vector2(440, 14));
_cooldownFill = MakeFill("CooldownFill", cdBg, new Color(0.4f, 0.8f, 1f));
// Threat count (top-right).
_threatText = MakeText("ThreatText", _canvas.transform, "HUSKS 0", 30, TextAnchor.UpperRight,
new Color(1f, 0.62f, 0.4f), font);
var trt = _threatText.rectTransform;
trt.anchorMin = new Vector2(1, 1); trt.anchorMax = new Vector2(1, 1); trt.pivot = new Vector2(1, 1);
trt.anchoredPosition = new Vector2(-40, -30); trt.sizeDelta = new Vector2(380, 50);
// Cycle phase + number (top-center).
_phaseText = MakeText("PhaseText", _canvas.transform, "EXPEDITION CYCLE 1", 34, TextAnchor.UpperCenter,
new Color(0.55f, 0.9f, 1f), font);
var prt = _phaseText.rectTransform;
prt.anchorMin = new Vector2(0.5f, 1f); prt.anchorMax = new Vector2(0.5f, 1f); prt.pivot = new Vector2(0.5f, 1f);
prt.anchoredPosition = new Vector2(0, -24); prt.sizeDelta = new Vector2(600, 50);
// Resource ledger counts (top-center, below phase).
_resourceText = MakeText("ResourceText", _canvas.transform, "", 24, TextAnchor.UpperCenter,
new Color(0.7f, 0.95f, 0.8f), font);
var rrt = _resourceText.rectTransform;
rrt.anchorMin = new Vector2(0.5f, 1f); rrt.anchorMax = new Vector2(0.5f, 1f); rrt.pivot = new Vector2(0.5f, 1f);
rrt.anchoredPosition = new Vector2(0, -64); rrt.sizeDelta = new Vector2(600, 40);
// Location + gate hint (top-center, below resources).
_locationText = MakeText("LocationText", _canvas.transform, "", 22, TextAnchor.UpperCenter,
new Color(0.6f, 0.85f, 1f), font);
var lrt = _locationText.rectTransform;
lrt.anchorMin = new Vector2(0.5f, 1f); lrt.anchorMax = new Vector2(0.5f, 1f); lrt.pivot = new Vector2(0.5f, 1f);
lrt.anchoredPosition = new Vector2(0, -96); lrt.sizeDelta = new Vector2(760, 36);
// Goal progress bar (top-center, below the location line).
var goalBg = MakeBar("GoalBg", _canvas.transform, new Color(0f, 0f, 0f, 0.55f), Vector2.zero, new Vector2(360, 16));
goalBg.anchorMin = new Vector2(0.5f, 1f); goalBg.anchorMax = new Vector2(0.5f, 1f); goalBg.pivot = new Vector2(0.5f, 1f);
goalBg.anchoredPosition = new Vector2(0, -126); goalBg.sizeDelta = new Vector2(360, 16);
_goalFill = MakeFill("GoalFill", goalBg, new Color(0.8f, 0.6f, 1f));
_goalText = MakeText("GoalText", goalBg, "GOAL 0 / 10", 15, TextAnchor.MiddleCenter, Color.white, font);
// Downed / respawning overlay (full screen, toggled by Dead).
_respawnOverlay = new GameObject("RespawnOverlay", typeof(RectTransform));
_respawnOverlay.transform.SetParent(_canvas.transform, false);
var ov = _respawnOverlay.AddComponent<RawImage>();
ov.texture = Texture2D.whiteTexture;
ov.color = new Color(0.35f, 0f, 0f, 0.35f);
ov.raycastTarget = false;
Stretch((RectTransform)_respawnOverlay.transform);
var rtext = MakeText("RespawnText", _respawnOverlay.transform, "DOWNED - RESPAWNING", 56,
TextAnchor.MiddleCenter, new Color(1f, 0.45f, 0.4f), font);
Stretch(rtext.rectTransform);
_respawnOverlay.SetActive(false);
_paletteRow.Add(root);
_palette[type] = new PaletteItem { Root = root, Cost = costLabel, CostAmount = cost };
}
static RectTransform MakeBar(string name, Transform parent, Color color, Vector2 anchoredPos, Vector2 size)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
var img = go.AddComponent<RawImage>();
img.texture = Texture2D.whiteTexture;
img.color = color;
img.raycastTarget = false;
var rt = (RectTransform)go.transform;
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.zero; rt.pivot = Vector2.zero;
rt.anchoredPosition = anchoredPos; rt.sizeDelta = size;
return rt;
}
// ---- UITK construction ----
static RectTransform MakeFill(string name, RectTransform parent, Color color)
void BuildTree(VisualElement root)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
var img = go.AddComponent<RawImage>();
img.texture = Texture2D.whiteTexture;
img.color = color;
img.raycastTarget = false;
var rt = (RectTransform)go.transform;
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.pivot = Vector2.zero;
rt.offsetMin = new Vector2(3, 3); rt.offsetMax = new Vector2(-3, -3);
return rt;
}
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
static Text MakeText(string name, Transform parent, string text, int size, TextAnchor anchor, Color color, Font font)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
var t = go.AddComponent<Text>();
t.text = text;
t.font = font;
t.fontSize = size;
t.alignment = anchor;
t.color = color;
t.horizontalOverflow = HorizontalWrapMode.Overflow;
t.verticalOverflow = VerticalWrapMode.Overflow;
t.raycastTarget = false;
Stretch(t.rectTransform);
return t;
}
// Health + cooldown (bottom-left).
var vitals = HudUi.Group();
vitals.style.position = Position.Absolute;
vitals.style.left = 40; vitals.style.bottom = 40;
var cdBar = HudUi.Bar(440, 12, new Color(0.4f, 0.8f, 1f), out _cooldownFill);
cdBar.style.marginBottom = 6;
var hpBar = HudUi.Bar(440, 40, new Color(0.25f, 0.9f, 0.5f), out _healthFill);
_healthText = HudUi.Text("100 / 100", 22, Color.white, TextAnchor.MiddleCenter);
_healthText.style.position = Position.Absolute;
_healthText.style.left = 0; _healthText.style.right = 0; _healthText.style.top = 0; _healthText.style.bottom = 0;
hpBar.Add(_healthText);
vitals.Add(cdBar);
vitals.Add(hpBar);
root.Add(vitals);
static void Stretch(RectTransform rt)
{
rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero;
}
// Threat (top-right).
_threatText = HudUi.Text("HUSKS 0", 30, new Color(1f, 0.62f, 0.4f), TextAnchor.UpperRight);
_threatText.style.position = Position.Absolute;
_threatText.style.right = 40; _threatText.style.top = 28; _threatText.style.width = 380;
root.Add(_threatText);
static Font GetFont()
{
Font f = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
if (f == null) f = Resources.GetBuiltinResource<Font>("Arial.ttf");
if (f == null) f = Font.CreateDynamicFontFromOSFont(new[] { "Arial", "Liberation Sans", "DejaVu Sans" }, 28);
return f;
// Macro stack (top-center).
var macro = HudUi.Group(Align.Center);
macro.style.position = Position.Absolute;
macro.style.top = 22; macro.style.left = 0; macro.style.right = 0;
_phaseText = HudUi.Text("", 30, new Color(0.55f, 0.9f, 1f), TextAnchor.MiddleCenter);
_resourceText = HudUi.Text("", 22, new Color(0.7f, 0.95f, 0.8f), TextAnchor.MiddleCenter);
_resourceText.style.marginTop = 4;
_locationText = HudUi.Text("", 18, new Color(0.6f, 0.85f, 1f), TextAnchor.MiddleCenter);
_locationText.style.marginTop = 4;
var goalBar = HudUi.Bar(360, 16, new Color(0.8f, 0.6f, 1f), out _goalFill);
goalBar.style.marginTop = 8;
_goalText = HudUi.Text("GOAL 0 / 10", 13, Color.white, TextAnchor.MiddleCenter);
_goalText.style.position = Position.Absolute;
_goalText.style.left = 0; _goalText.style.right = 0; _goalText.style.top = 0; _goalText.style.bottom = 0;
goalBar.Add(_goalText);
macro.Add(_phaseText);
macro.Add(_resourceText);
macro.Add(_locationText);
macro.Add(goalBar);
root.Add(macro);
// Build palette row (bottom-center). The row passes clicks through; its buttons pick.
_paletteRow = new VisualElement();
_paletteRow.style.position = Position.Absolute;
_paletteRow.style.bottom = 24; _paletteRow.style.left = 0; _paletteRow.style.right = 0;
_paletteRow.style.flexDirection = FlexDirection.Row;
_paletteRow.style.justifyContent = Justify.Center;
_paletteRow.pickingMode = PickingMode.Ignore;
root.Add(_paletteRow);
// Downed overlay.
_downed = new VisualElement();
_downed.style.position = Position.Absolute;
_downed.style.left = 0; _downed.style.right = 0; _downed.style.top = 0; _downed.style.bottom = 0;
_downed.style.backgroundColor = new Color(0.35f, 0f, 0f, 0.35f);
_downed.style.alignItems = Align.Center;
_downed.style.justifyContent = Justify.Center;
_downed.pickingMode = PickingMode.Ignore;
_downed.Add(HudUi.Text("DOWNED - RESPAWNING", 52, new Color(1f, 0.45f, 0.4f), TextAnchor.MiddleCenter));
_downed.style.display = DisplayStyle.None;
root.Add(_downed);
}
static Color PhaseColor(byte phase)
@@ -319,5 +311,19 @@ namespace ProjectM.Client
default: return "";
}
}
static string StructureName(byte type)
{
switch (type)
{
case StructureType.Turret: return "Turret";
case StructureType.Wall: return "Wall";
case StructureType.Pylon: return "Pylon";
case StructureType.Harvester: return "Harvester";
case StructureType.Fabricator: return "Fabricator";
case StructureType.Conveyor: return "Conveyor";
default: return "?";
}
}
}
}
@@ -220,7 +220,7 @@ namespace ProjectM.Client
static void PlayClip(AudioClip clip, Vector3 pos, float vol)
{
if (clip == null) return;
AudioSource.PlayClipAtPoint(clip, pos, vol);
AudioSource.PlayClipAtPoint(clip, pos, vol * GameVolume.Sfx);
}
}
}