Files
unity/Assets/Game/Scripts/UI/GameHud.cs
T
RCiBot 92623a1bc8 feat: establish mech extraction prototype with bilingual UI
Track Unity source, scenes, editable assets, licenses, tools and QA evidence. Exclude generated caches, Windows builds and raw runtime logs.
2026-11-12 12:28:40 +08:00

424 lines
19 KiB
C#

using System;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;
namespace Ironspore
{
public sealed class GameHud : MonoBehaviour
{
GameSession game;
VisualElement root;
Label mission, prompt, status, notice, telemetry;
ProgressBar armor, energy, cargo;
VisualElement cross;
enum View
{
Hangar, Field, Cargo, Part, Routes, Pause
}
View view;
int partSlot, partIndex;
PanelSettings panelSettings;
VisualElement languageControl, languageMenu, languageDismiss;
Button languageButton;
int lastLanguageActivationFrame = -1;
int lastLanguageNavigationFrame = -1;
Label hangarNotice;
static string L(string key, params object[] args) => GameLocalization.Text(key, args);
public void Initialize(GameSession session)
{
game = session;
var document = gameObject.AddComponent<UIDocument>();
var settings = panelSettings = Instantiate(game.uiPanel);
settings.scaleMode = PanelScaleMode.ScaleWithScreenSize;
settings.referenceResolution = new Vector2Int(1920, 1080);
document.panelSettings = settings;
root = document.rootVisualElement;
root.styleSheets.Add(game.uiStyle);
root.pickingMode = PickingMode.Ignore;
root.style.unityFontDefinition = FontDefinition.FromSDFFont(game.localization.uiFont);
root.RegisterCallback<KeyDownEvent>(HandleLanguageKey, TrickleDown.TrickleDown);
GameLocalization.Changed += RefreshLocale;
}
void HandleLanguageKey(KeyDownEvent evt)
{
if (view != View.Hangar)
return;
if (languageMenu != null && evt.keyCode == KeyCode.Escape)
{
CloseLanguageMenu();
evt.StopImmediatePropagation();
return;
}
if (languageMenu != null && evt.keyCode == KeyCode.Tab)
{
MoveLanguageFocus(evt.shiftKey);
evt.StopImmediatePropagation();
return;
}
if (evt.keyCode != KeyCode.Return && evt.keyCode != KeyCode.KeypadEnter && evt.keyCode != KeyCode.Space)
return;
if (ActivateFocusedLanguage())
evt.StopImmediatePropagation();
}
void MoveLanguageFocus(bool reverse)
{
if (lastLanguageNavigationFrame == Time.frameCount)
return;
lastLanguageNavigationFrame = Time.frameCount;
var options = languageMenu.Query<Button>(className: "language-option").ToList();
int current = options.IndexOf(root.focusController.focusedElement as Button);
int next = (current + (reverse ? -1 : 1) + options.Count) % options.Count;
options[next].Focus();
}
bool ActivateFocusedLanguage()
{
var focused = root.focusController.focusedElement as Button;
if (focused == languageButton)
ToggleLanguageMenu(languageControl.parent);
else if (languageMenu != null && focused != null && focused.ClassListContains("language-option"))
ChooseLocale((string)focused.userData);
else
return false;
return true;
}
VisualElement Element(VisualElement parent, string css)
{
var e = new VisualElement();
e.AddToClassList(css);
parent.Add(e);
return e;
}
Label Text(VisualElement parent, string text, string css = "body")
{
var l = new Label(text);
l.AddToClassList(css);
parent.Add(l);
return l;
}
Button Button(VisualElement parent, string text, Action action, string css = "button")
{
var b = new Button(action) { text = text };
foreach (string className in css.Split(' '))
b.AddToClassList(className);
parent.Add(b);
return b;
}
void Clear()
{
root.Clear();
mission = prompt = status = notice = telemetry = null;
armor = energy = cargo = null;
cross = null;
hangarNotice = null;
languageControl = languageMenu = languageDismiss = null;
languageButton = null;
}
void RefreshLocale()
{
switch (view)
{
case View.Hangar:
ShowHangar();
languageButton?.Focus();
break;
case View.Field:
ShowField();
break;
case View.Cargo:
ShowCargo();
break;
case View.Part:
ShowPart(partSlot, partIndex);
break;
case View.Routes:
ShowRoutes();
break;
case View.Pause:
ShowPause();
break;
}
}
void AddLanguageControl(VisualElement page)
{
languageControl = Element(page, "language-control");
var locale = Array.Find(GameLocalization.Catalog.locales, x => x.code == GameLocalization.CurrentCode);
languageButton = Button(languageControl, L("ui.home.language", locale.nativeName), () => ToggleLanguageMenu(page), "language-button");
languageButton.name = "language-toggle";
languageButton.tooltip = L("ui.home.languageHelp");
}
void ToggleLanguageMenu(VisualElement page)
{
if (lastLanguageActivationFrame == Time.frameCount)
return;
lastLanguageActivationFrame = Time.frameCount;
if (languageMenu != null)
CloseLanguageMenu();
else
OpenLanguageMenu(page);
}
void ChooseLocale(string code)
{
if (lastLanguageActivationFrame == Time.frameCount)
return;
lastLanguageActivationFrame = Time.frameCount;
CloseLanguageMenu();
GameLocalization.SelectLocale(code);
}
void OpenLanguageMenu(VisualElement page)
{
languageDismiss = Element(page, "language-dismiss");
languageDismiss.RegisterCallback<PointerDownEvent>(evt => { CloseLanguageMenu(); evt.StopPropagation(); });
languageControl.BringToFront();
languageMenu = Element(languageControl, "language-menu");
languageMenu.name = "language-menu";
Text(languageMenu, L("ui.home.languageHelp"), "language-heading");
Button selected = null;
foreach (var locale in GameLocalization.Catalog.locales)
{
string code = locale.code;
bool current = code == GameLocalization.CurrentCode;
var option = Button(languageMenu, current ? L("ui.language.current", locale.nativeName) : locale.nativeName, () => ChooseLocale(code), current ? "language-option selected" : "language-option");
option.name = "locale-" + code;
option.userData = code;
if (current)
selected = option;
}
selected?.Focus();
}
public void CloseLanguageMenu()
{
languageMenu?.RemoveFromHierarchy();
languageDismiss?.RemoveFromHierarchy();
languageMenu = languageDismiss = null;
languageButton?.Focus();
}
public void RefreshHangarNotice()
{
if (hangarNotice != null)
{
hangarNotice.text = game.Notice;
hangarNotice.style.display = string.IsNullOrEmpty(game.Notice) ? DisplayStyle.None : DisplayStyle.Flex;
}
}
public void ShowHangar()
{
Clear();
view = View.Hangar;
var page = Element(root, "hangar");
var scroll = new ScrollView(ScrollViewMode.Vertical) { horizontalScrollerVisibility = ScrollerVisibility.Hidden };
scroll.AddToClassList("hangar-scroll");
page.Add(scroll);
var content = Element(scroll, "hangar-content");
Text(content, "IRONSPORE", "title");
Text(content, L("ui.home.subtitle"), "subtitle");
Text(content, game.Report, "body");
var frames = Element(content, "row");
for (int i = 0; i < game.chassis.Length; i++)
{
int choice = i;
var frame = game.chassis[i];
Button(frames, frame.LocalizedName + "\n" + frame.LocalizedDescription, () => { game.SelectedChassis = choice; ShowHangar(); }, i == game.SelectedChassis ? "frame selected" : "frame");
}
Text(content, L("ui.home.loadout"), "section");
for (int slot = 0; slot < 4; slot++)
{
int s = slot;
var p = game.Part(s, game.InitialEquipment[s]);
Button(content, L("ui.home.part", L("ui.slot." + s), p.LocalizedName, p.mass, p.power), () => { game.InitialEquipment[s] = (game.InitialEquipment[s] + 1) % game.PartCount(s); ShowHangar(); }, "loadout");
}
float mass = 0, power = 0;
for (int i = 0; i < 4; i++)
{
var p = game.Part(i, game.InitialEquipment[i]);
mass += p.mass;
power += p.power;
}
var selected = game.chassis[game.SelectedChassis];
Text(content, L("ui.home.budget", mass, selected.massCapacity, power, selected.powerCapacity), game.Fits(game.SelectedChassis, game.InitialEquipment) ? "budget" : "warning");
var controls = Element(content, "row");
var launch = Button(controls, L("ui.home.deploy"), game.BeginSortie, "primary");
launch.SetEnabled(game.Fits(game.SelectedChassis, game.InitialEquipment) && game.StartingPartsUnlocked() && !game.Store.Data.run.active);
if (!game.StartingPartsUnlocked())
Text(content, L("ui.home.locked"), "warning");
Button(controls, L("ui.home.range"), game.StartTrial);
if (game.Store.Data.run.active)
Button(controls, L("ui.home.resume"), game.Resume, "primary");
Text(content, L("ui.home.moveHint"), "hint");
Text(content, L("ui.home.fireHint"), "hint");
var save = game.Store.Data;
Text(content, L("ui.home.inventory"), "section");
Text(content, L("ui.home.stock", save.alloy, save.cells, save.blueprints.Count, save.extractions, save.completed), "budget");
foreach (string blueprint in save.blueprints)
{
string id = blueprint;
bool unlocked = save.unlocked.Contains(id);
var b = Button(content, L(unlocked ? "ui.home.researched" : "ui.home.research", GameLocalization.ItemName(id)), () => game.Unlock(id), "loadout");
b.SetEnabled(!unlocked);
}
hangarNotice = Text(content, game.Notice, "warning");
RefreshHangarNotice();
Button(content, L("ui.home.quit"), () => Application.Quit(), "quiet");
AddLanguageControl(page);
}
public void ShowField()
{
Clear();
view = View.Field;
var field = Element(root, "field");
field.pickingMode = PickingMode.Ignore;
var top = Element(field, "topbar");
Text(top, "IRONSPORE", "brand");
status = Text(top, "", "status");
var goal = Element(field, "mission");
Text(goal, L("ui.hud.operation"), "section");
mission = Text(goal, "", "mission-text");
telemetry = Text(field, "", "telemetry");
cross = Element(field, "crosshair");
cross.style.position = Position.Absolute;
cross.style.left = Length.Percent(50);
cross.style.top = Length.Percent(50);
cross.style.translate = new Translate(-20, -20);
cross.style.width = 40;
cross.style.height = 40;
for (int i = 0; i < 4; i++)
{
var arm = Element(cross, "reticle-arm");
arm.style.position = Position.Absolute;
arm.style.backgroundColor = new Color(.64f, 1, .9f);
arm.style.left = i == 0 ? 0 : i == 1 ? 27 : 19;
arm.style.top = i == 2 ? 0 : i == 3 ? 27 : 19;
arm.style.width = i < 2 ? 13 : 2;
arm.style.height = i < 2 ? 2 : 13;
}
prompt = Text(field, "", "interact");
notice = Text(field, "", "notice");
var bottom = Element(field, "bottom");
armor = new ProgressBar { title = L("ui.hud.armor", 0), lowValue = 0, highValue = 100 };
armor.AddToClassList("armor");
bottom.Add(armor);
energy = new ProgressBar { title = L("ui.hud.energy", 0), lowValue = 0, highValue = 100 };
energy.AddToClassList("energy");
bottom.Add(energy);
cargo = new ProgressBar { title = L("ui.hud.podReady"), lowValue = 0, highValue = 3 };
cargo.AddToClassList("cargo-progress");
bottom.Add(cargo);
Text(bottom, L("ui.hud.hint"), "hint");
}
VisualElement Overlay(string heading, string body)
{
ShowField();
var shade = Element(root, "overlay");
var panel = Element(shade, "panel");
Text(panel, heading, "heading");
Text(panel, body, "body");
return panel;
}
public void ShowCargo()
{
var panel = Overlay(L("ui.cargo.title"), L(game.Run.podUsed ? "ui.cargo.used" : "ui.cargo.help"));
view = View.Cargo;
foreach (var stack in game.Run.cargo)
{
string uid = stack.uid;
var b = Button(panel, L("ui.cargo.stack", game.SelectedCargo.Contains(uid) ? L("ui.cargo.selected") : "[ ]", stack.Name, stack.count), () => game.ToggleCargo(uid), "loadout");
b.SetEnabled(!game.Run.podUsed);
}
Text(panel, L("ui.cargo.capacity", game.Run.cargo.Count, game.SelectedCargo.Count), "budget");
Button(panel, L("ui.return"), game.CloseMenu, "primary");
}
public void ShowPart(int slot, int index)
{
index = Mathf.Clamp(index, 0, game.PartCount(slot) - 1);
int partIndex = index;
this.partSlot = slot;
this.partIndex = index;
var old = game.Part(slot, game.Run.equipment[slot]);
var next = game.Part(slot, index);
var panel = Overlay(L("ui.refit.title"), L("ui.refit.help"));
view = View.Part;
Text(panel, L("ui.refit.equipped", old.LocalizedName, old.mass, old.power), "body");
Text(panel, L("ui.refit.recovered", next.LocalizedName, next.mass, next.power), "budget");
Button(panel, L("ui.refit.install"), () => game.Install(slot, partIndex), "primary");
Button(panel, L("ui.refit.keep"), game.CloseMenu);
}
public void ShowRoutes()
{
bool split = game.Run.stage == 1;
var panel = Overlay(L(split ? "ui.routes.split" : "ui.routes.title"), L(split ? "ui.routes.splitHelp" : "ui.routes.help"));
view = View.Routes;
Button(panel, L(split ? "ui.routes.boss" : "ui.routes.freight"), () => game.NextZone(0), "route");
Button(panel, L(split ? "ui.routes.escort" : "ui.routes.power"), () => game.NextZone(1), "route");
Button(panel, L("ui.routes.stay"), game.CloseMenu);
}
public void ShowPause()
{
var panel = Overlay(L("ui.pause.title"), L("ui.pause.help"));
view = View.Pause;
Button(panel, L("ui.pause.resume"), game.CloseMenu, "primary");
if (game.Trial)
Button(panel, L("ui.pause.range"), () => { game.CloseMenu(); game.EndSortie(true); });
Button(panel, L("ui.pause.quit"), () => Application.Quit());
}
void Update()
{
if (view == View.Hangar)
{
RefreshHangarNotice();
if (languageMenu != null && game.Action("Pause").WasPressedThisFrame())
CloseLanguageMenu();
// The project uses Input System without a uGUI EventSystem. Handle submit
// explicitly as well as UI Toolkit events; frame guards prevent double input.
var keys = Keyboard.current;
if (keys != null)
{
if (languageMenu != null && keys.tabKey.wasPressedThisFrame)
MoveLanguageFocus(keys.shiftKey.isPressed);
if (keys.enterKey.wasPressedThisFrame || keys.numpadEnterKey.wasPressedThisFrame || keys.spaceKey.wasPressedThisFrame)
ActivateFocusedLanguage();
}
}
if (!game.InField || !game.Pilot)
return;
var run = game.Run;
var p = game.Pilot;
if (status != null)
status.text = L("ui.hud.status", game.Trial ? L("ui.home.range") : L("ui.hud.zone", run.stage + 1), TimeSpan.FromSeconds(run.elapsed).ToString(@"mm\:ss"));
if (mission != null)
mission.text = L(run.routeCompleted ? "ui.hud.complete" : run.stage == 4 && !game.Trial ? run.branch == 0 ? "ui.hud.boss" : "ui.hud.escort" : run.objectiveDone ? "ui.hud.routeOpen" : "ui.hud.objective");
if (armor != null)
{
armor.value = p.Health / game.chassis[run.chassis].armor * 100;
armor.title = L("ui.hud.armor", Mathf.CeilToInt(p.Health));
}
if (energy != null)
{
energy.value = p.Energy;
energy.title = p.Guard > 0 ? L("ui.hud.shield") : L("ui.hud.energy", Mathf.CeilToInt(p.Energy));
}
if (cargo != null)
{
cargo.value = game.CargoProgress;
cargo.title = L(run.podUsed ? "ui.hud.podUsed" : game.CargoProgress > 0 ? "ui.hud.podPreparing" : "ui.hud.podReady");
}
if (prompt != null)
{
prompt.text = game.World.Focused ? game.World.Focused.Prompt : "";
prompt.style.display = game.World.Focused ? DisplayStyle.Flex : DisplayStyle.None;
}
if (notice != null)
notice.text = game.Notice;
if (telemetry != null)
telemetry.text = L("ui.hud.telemetry", game.chassis[run.chassis].LocalizedName, L(p.FirstPerson ? "ui.hud.cockpit" : "ui.hud.shoulder"), game.Part(0, run.equipment[0]).LocalizedName, run.cargo.Count, game.World.ExtractionRemaining >= 0 ? game.World.TransportReady ? L("ui.hud.transportReady") : L("ui.hud.transportEta", Mathf.CeilToInt(game.World.ExtractionRemaining)) : L("ui.hud.extractionAvailable"));
if (cross != null)
cross.style.opacity = p.RecentHit ? 1 : .65f;
}
void OnDestroy()
{
GameLocalization.Changed -= RefreshLocale;
if (panelSettings)
Destroy(panelSettings);
}
}
}