diff --git a/Assets/Scripts/Data/DistroDatabase.cs b/Assets/Scripts/Data/DistroDatabase.cs index d388af3..aa325a1 100644 --- a/Assets/Scripts/Data/DistroDatabase.cs +++ b/Assets/Scripts/Data/DistroDatabase.cs @@ -31,5 +31,52 @@ namespace KernelPanic.Data return null; } + + /// + /// Checks that every entry is assigned and has a unique, non-blank id. Used at both + /// edit time (OnValidate) and runtime, so misconfiguration surfaces before it can + /// silently disable a feature that depends on this database. + /// + public bool TryValidate(out string error) + { + for (int i = 0; i < allDistros.Count; i++) + { + if (allDistros[i] == null) + { + error = $"entry {i} is empty"; + return false; + } + } + + HashSet seenIds = new(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < allDistros.Count; i++) + { + string id = allDistros[i].Id; + if (string.IsNullOrWhiteSpace(id)) + { + error = $"'{allDistros[i].name}' has a blank id"; + return false; + } + + if (!seenIds.Add(id)) + { + error = $"duplicate distro id '{id}'"; + return false; + } + } + + error = null; + return true; + } + +#if UNITY_EDITOR + private void OnValidate() + { + if (!TryValidate(out string error)) + { + Debug.LogError($"DistroDatabase '{name}': {error}.", this); + } + } +#endif } } diff --git a/Assets/Scripts/UI/CollectionScreenController.cs b/Assets/Scripts/UI/CollectionScreenController.cs new file mode 100644 index 0000000..f3b4214 --- /dev/null +++ b/Assets/Scripts/UI/CollectionScreenController.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using KernelPanic.Data; +using UnityEngine; +using UnityEngine.TextCore.Text; +using UnityEngine.UIElements; + +namespace KernelPanic.UI +{ + /// + /// Renders the owned-units list and detail readout on the collection screen. + /// + [Serializable] + public sealed class CollectionScreenController + { + private readonly List rows = new(); + + private VisualElement list; + private VisualElement detail; + private FontAsset monospaceFont; + private IReadOnlyList units = Array.Empty(); + private int selectedIndex; + + public void Bind(VisualElement root, FontAsset artFont) + { + monospaceFont = artFont; + list = root.Q("CollectionList"); + detail = root.Q("CollectionDetail"); + } + + public void Refresh(IReadOnlyList ownedUnits) + { + units = ownedUnits ?? Array.Empty(); + if (list == null || detail == null) + { + return; + } + + rows.Clear(); + list.Clear(); + + if (units.Count == 0) + { + detail.Clear(); + detail.Add(new Label("no units installed") { name = "CollectionEmptyTitle" }); + detail.Add(new Label("run: curl gacha.sh | sh") { name = "CollectionEmptyHint" }); + return; + } + + selectedIndex = Mathf.Clamp(selectedIndex, 0, units.Count - 1); + for (int i = 0; i < units.Count; i++) + { + int index = i; + DistroDefinition unit = units[i]; + VisualElement row = new(); + row.AddToClassList("collection-row"); + row.RegisterCallback(_ => SelectUnit(index)); + row.RegisterCallback(_ => SelectUnit(index)); + + Label name = new(DistroPresentation.DisplayName(unit)); + name.AddToClassList("collection-row-name"); + name.style.color = new StyleColor(unit.AccentColor); + + Label languages = new(DistroPresentation.FormatLanguages(unit)); + languages.AddToClassList("collection-row-languages"); + + row.Add(name); + row.Add(languages); + list.Add(row); + rows.Add(row); + } + + SelectUnit(selectedIndex); + } + + private void SelectUnit(int index) + { + if (units.Count == 0) + { + return; + } + + selectedIndex = Mathf.Clamp(index, 0, units.Count - 1); + for (int i = 0; i < rows.Count; i++) + { + rows[i].EnableInClassList("selected", i == selectedIndex); + } + + RenderDetail(units[selectedIndex]); + } + + private void RenderDetail(DistroDefinition unit) + { + detail.Clear(); + + VisualElement readout = new(); + readout.AddToClassList("collection-detail-readout"); + + Label artLabel = new(); + DistroArtPresenter.ConfigureArtLabel(artLabel, monospaceFont); + AsciiArtFitter artFitter = new(artLabel, monospaceFont); + VisualElement artPlaceholder = DistroArtPresenter.CreatePlaceholder(); + artFitter.SetArt(DistroArtPresenter.Render(artLabel, artPlaceholder, unit)); + readout.Add(artPlaceholder); + readout.Add(artLabel); + + VisualElement details = new(); + details.AddToClassList("collection-detail-values"); + + Label name = new(DistroPresentation.DisplayName(unit)); + name.AddToClassList("collection-detail-name"); + name.style.color = new StyleColor(unit.AccentColor); + details.Add(name); + + details.Add(BuildDetailLine("lang", DistroPresentation.FormatLanguages(unit))); + details.Add(BuildDetailLine("passive", string.IsNullOrWhiteSpace(unit.PassiveName) ? "--" : unit.PassiveName)); + details.Add(BuildDetailLine("uptime", unit.BaseUptime.ToString())); + details.Add(BuildDetailLine("ram", unit.BaseRam.ToString())); + details.Add(BuildDetailLine("cycles", unit.BaseCyclesPerTurn.ToString())); + + readout.Add(details); + detail.Add(readout); + + Label description = new(string.IsNullOrWhiteSpace(unit.Description) ? "--" : unit.Description); + description.AddToClassList("collection-detail-description"); + detail.Add(description); + } + + private static VisualElement BuildDetailLine(string key, string value) + { + VisualElement row = new(); + row.AddToClassList("kv-row"); + + Label keyLabel = new(key); + keyLabel.AddToClassList("kv-key"); + Label valueLabel = new(value); + valueLabel.AddToClassList("kv-value"); + + row.Add(keyLabel); + row.Add(valueLabel); + return row; + } + } +} diff --git a/Assets/Scripts/UI/CommandKeyBindings.cs b/Assets/Scripts/UI/CommandKeyBindings.cs new file mode 100644 index 0000000..221ac13 --- /dev/null +++ b/Assets/Scripts/UI/CommandKeyBindings.cs @@ -0,0 +1,24 @@ +using UnityEngine; + +namespace KernelPanic.UI +{ + /// + /// Maps number-row/keypad digits to menu slot indices. Shared by the command menu and the + /// starter-selection modal so both keep the same 1-5 shortcut behavior. + /// + internal static class CommandKeyBindings + { + public static int GetDigitIndex(KeyCode keyCode) + { + return keyCode switch + { + KeyCode.Alpha1 or KeyCode.Keypad1 => 0, + KeyCode.Alpha2 or KeyCode.Keypad2 => 1, + KeyCode.Alpha3 or KeyCode.Keypad3 => 2, + KeyCode.Alpha4 or KeyCode.Keypad4 => 3, + KeyCode.Alpha5 or KeyCode.Keypad5 => 4, + _ => -1 + }; + } + } +} diff --git a/Assets/Scripts/UI/DistroPresentation.cs b/Assets/Scripts/UI/DistroPresentation.cs new file mode 100644 index 0000000..d6665ec --- /dev/null +++ b/Assets/Scripts/UI/DistroPresentation.cs @@ -0,0 +1,20 @@ +using KernelPanic.Data; + +namespace KernelPanic.UI +{ + /// + /// Shared text formatting for distros, used by every screen that lists or details them. + /// + public static class DistroPresentation + { + public static string DisplayName(DistroDefinition unit) + { + return string.IsNullOrWhiteSpace(unit.DisplayName) ? unit.name : unit.DisplayName; + } + + public static string FormatLanguages(DistroDefinition unit) + { + return $"{unit.PrimaryLanguage} / {unit.SecondaryLanguage}"; + } + } +} diff --git a/Assets/Scripts/UI/FeaturedUnitPanel.cs b/Assets/Scripts/UI/FeaturedUnitPanel.cs index 0cbf9f1..b194212 100644 --- a/Assets/Scripts/UI/FeaturedUnitPanel.cs +++ b/Assets/Scripts/UI/FeaturedUnitPanel.cs @@ -100,11 +100,10 @@ namespace KernelPanic.UI populatedState.RemoveFromClassList("hidden"); emptyState.AddToClassList("hidden"); - string displayName = string.IsNullOrWhiteSpace(unit.DisplayName) ? unit.name : unit.DisplayName; titleLabel.text = "[ neofetch ]"; asciiFitter.SetArt(DistroArtPresenter.Render(asciiLabel, asciiPlaceholder, unit)); - unitNameLabel.text = displayName; - languagesLabel.text = $"{unit.PrimaryLanguage} / {unit.SecondaryLanguage}"; + unitNameLabel.text = DistroPresentation.DisplayName(unit); + languagesLabel.text = DistroPresentation.FormatLanguages(unit); passiveLabel.text = string.IsNullOrWhiteSpace(unit.PassiveName) ? "--" : unit.PassiveName; bestWaveLabel.text = "--"; // TODO: Bind best-wave stats from SaveService when stats exist. SetAccent(ColorUtility.ToHtmlStringRGB(unit.AccentColor)); diff --git a/Assets/Scripts/UI/MainMenuController.cs b/Assets/Scripts/UI/MainMenuController.cs index 049933d..19227b2 100644 --- a/Assets/Scripts/UI/MainMenuController.cs +++ b/Assets/Scripts/UI/MainMenuController.cs @@ -25,21 +25,24 @@ namespace KernelPanic.UI private const float BootIntroSeconds = 1.5f; private static bool bootIntroPlayed; + // Static fields don't reset between Play sessions when the Editor's "Reload Domain" + // option is disabled. RuntimeInitializeOnLoadMethod runs once per player/Play-mode + // startup regardless of that setting, so this keeps bootIntroPlayed session-scoped. + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetSessionState() + { + bootIntroPlayed = false; + } + [SerializeField] private FontAsset monospaceFont; // TODO: Assign a real monospace FontAsset when typography assets exist. [SerializeField] private string motdBody = "unstable userspace detected; keep a rollback shell open."; [SerializeField] private DistroDatabase distroDatabase; [SerializeField] private CardDatabase cardDatabase; [SerializeField] private FeaturedUnitPanel featuredUnitPanel = new(); + [SerializeField] private CollectionScreenController collectionScreen = new(); + [SerializeField] private StarterSelectionController starterSelection = new(); private readonly List commandEntries = new(); - private readonly List starterCards = new(); - private readonly List