Files
unity/Assets/Game/Scripts/UI/GameHud.cs

475 lines
22 KiB
C#

using System;
using System.Linq;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;
namespace Ironspore
{
public sealed partial class GameHud : MonoBehaviour
{
GameSession game;
VisualElement root;
Label mission, prompt, status, notice, telemetry;
ProgressBar armor, energy, cargo;
VisualElement cross, controlsHint;
CombatInstruments instruments;
enum View
{
Hangar, Field, Cargo, Part, Routes, Pause, Settings
}
View view;
int partSlot, partIndex;
PanelSettings panelSettings;
VisualElement languageControl, languageMenu, languageDismiss;
Button languageButton;
int lastLanguageActivationFrame = -1;
int lastLanguageNavigationFrame = -1;
Label hangarNotice;
SettingsScreen settingsScreen;
int menuInputFrame = -1;
public bool SettingsOpen => view == View.Settings;
public bool MenuInputConsumedThisFrame => menuInputFrame == Time.frameCount;
string K(string action) => game.Settings.Bindings.Display(action);
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<PointerMoveEvent>(DragMove, TrickleDown.TrickleDown);
root.RegisterCallback<PointerUpEvent>(DragEnd, TrickleDown.TrickleDown);
root.RegisterCallback<PointerCancelEvent>(_ => ResetDrag(), TrickleDown.TrickleDown);
root.RegisterCallback<KeyDownEvent>(HandleLanguageKey, TrickleDown.TrickleDown);
GameLocalization.Changed += RefreshLocale;
settingsScreen = new SettingsScreen(game.Settings, () => game.Action("Pause").WasPressedThisFrame());
}
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()
{
ResetDrag();
settingsScreen?.Close();
root.Clear();
mission = prompt = status = notice = telemetry = null;
armor = energy = cargo = null;
cross = controlsHint = null;
instruments = 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;
case View.Settings:
settingsScreen.RefreshLocale();
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.SelectedInstances[s] = null; 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);
Button(controls, L("settings.title"), ShowSettings).name = "home-settings";
if (game.Store.Data.run.active)
Button(controls, L("ui.home.resume"), game.Resume, "primary");
Text(content, L("ui.home.moveHint", K("Move"), K("View"), K("Boost"), K("Jump")), "hint");
Text(content, L("ui.home.fireHint", K("Fire"), K("Aim"), K("Secondary"), K("Guard"), K("Interact")), "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 (var item in save.warehouse)
{
int slot = InventoryGrid.Slot(item.itemId);
Button(content, L("ui.home.chooseItem", item.Name, item.uid.Substring(0, Math.Min(6, item.uid.Length))), () =>
{
game.SelectedInstances[slot] = item.uid;
game.InitialEquipment[slot] = InventoryGrid.Index(slot, item.itemId);
ShowHangar();
}, game.SelectedInstances[slot] == item.uid ? "loadout selected" : "loadout");
}
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);
var recipe = InventoryGrid.Recipe(id);
if (unlocked && recipe.item != null)
Button(content, L("ui.home.manufacture", L("equipment." + recipe.item + ".name"), recipe.alloy, recipe.cells),
() => { if (!game.Store.Manufacture(id)) game.Notify("notice.saveFailed"); ShowHangar(); }, "loadout")
.SetEnabled(!save.run.active && save.alloy >= recipe.alloy && save.cells >= recipe.cells);
}
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(goal, "", "combat-extraction");
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");
instruments = new CombatInstruments(field, bottom, game);
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", K("Inventory")), lowValue = 0, highValue = 3 };
cargo.AddToClassList("cargo-progress");
bottom.Add(cargo);
controlsHint = Text(bottom, L("ui.hud.hint", K("View"), K("Boost"), K("Jump"), K("Secondary"), K("Guard"), K("Interact"), K("Inventory"), K("Cargo"), K("Pause")), "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 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 ?? 0, old?.power ?? 0), "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");
Button(panel, L("settings.title"), ShowSettings).name = "pause-settings";
if (game.Trial)
Button(panel, L("ui.pause.range"), () => { game.CloseMenu(); game.EndSortie(true); });
Button(panel, L("ui.pause.quit"), () => Application.Quit());
}
public void ShowSettings()
{
bool returnToPause = view == View.Pause;
Clear();
view = View.Settings;
menuInputFrame = Time.frameCount;
settingsScreen.Open(root, () =>
{
if (returnToPause) ShowPause();
else ShowHangar();
root.Q<Button>(returnToPause ? "pause-settings" : "home-settings")?.Focus();
});
}
public void CancelSettingsInput() => settingsScreen.CancelInput();
void Update()
{
if (SettingsOpen)
{
menuInputFrame = Time.frameCount;
settingsScreen.Tick();
return;
}
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 (view == View.Cargo && selectedUid != null && Keyboard.current?.rKey.wasPressedThisFrame == true) RotateSelection();
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 = game.World.Course ? game.World.Course.Instruction :
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", K("Inventory"));
}
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 = game.World.ExtractionRemaining < 0 ? L("ui.hud.extractionAvailable") :
game.World.TransportReady ? L("ui.hud.transportReady") : L("ui.hud.transportEta", Mathf.CeilToInt(game.World.ExtractionRemaining));
if (instruments != null)
{
instruments.Refresh();
controlsHint.style.display = game.Action("Aim").IsPressed() ? DisplayStyle.None : DisplayStyle.Flex;
}
if (cross != null)
cross.style.opacity = p.RecentHit ? 1 : .65f;
}
void OnDestroy()
{
settingsScreen?.Close();
GameLocalization.Changed -= RefreshLocale;
if (panelSettings)
Destroy(panelSettings);
}
}
}