Validate DistroDatabase and split MainMenuController's starter/collection logic out
- DistroDatabase.TryValidate() catches empty slots, blank ids, and duplicate ids at edit time (OnValidate) and runtime, instead of only surfacing as a runtime count check. - StarterSelectionController centralizes the "do we have a usable starter set" check (previously re-derived in five places) and shows a dismissible in-UI error when the database is misconfigured, rather than silently leaving the modal hidden with just a console warning. - Boot-intro's bootIntroPlayed flag now resets via RuntimeInitializeOnLoadMethod so it stays session-scoped even when the Editor's Reload Domain setting is off. - Extracted StarterSelectionController and CollectionScreenController out of MainMenuController (885 -> ~490 lines), following the same plain-class-plus-Bind() pattern already used by FeaturedUnitPanel. Shared DisplayName/FormatLanguages formatting and digit-key mapping moved into DistroPresentation/CommandKeyBindings to remove the duplication that split would otherwise have introduced. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01364Mm9NsPooiSfDHKr4FAx
This commit is contained in:
@@ -31,5 +31,52 @@ namespace KernelPanic.Data
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<string> 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Renders the owned-units list and detail readout on the collection screen.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public sealed class CollectionScreenController
|
||||||
|
{
|
||||||
|
private readonly List<VisualElement> rows = new();
|
||||||
|
|
||||||
|
private VisualElement list;
|
||||||
|
private VisualElement detail;
|
||||||
|
private FontAsset monospaceFont;
|
||||||
|
private IReadOnlyList<DistroDefinition> units = Array.Empty<DistroDefinition>();
|
||||||
|
private int selectedIndex;
|
||||||
|
|
||||||
|
public void Bind(VisualElement root, FontAsset artFont)
|
||||||
|
{
|
||||||
|
monospaceFont = artFont;
|
||||||
|
list = root.Q<VisualElement>("CollectionList");
|
||||||
|
detail = root.Q<VisualElement>("CollectionDetail");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh(IReadOnlyList<DistroDefinition> ownedUnits)
|
||||||
|
{
|
||||||
|
units = ownedUnits ?? Array.Empty<DistroDefinition>();
|
||||||
|
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<PointerEnterEvent>(_ => SelectUnit(index));
|
||||||
|
row.RegisterCallback<ClickEvent>(_ => 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace KernelPanic.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using KernelPanic.Data;
|
||||||
|
|
||||||
|
namespace KernelPanic.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Shared text formatting for distros, used by every screen that lists or details them.
|
||||||
|
/// </summary>
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,11 +100,10 @@ namespace KernelPanic.UI
|
|||||||
populatedState.RemoveFromClassList("hidden");
|
populatedState.RemoveFromClassList("hidden");
|
||||||
emptyState.AddToClassList("hidden");
|
emptyState.AddToClassList("hidden");
|
||||||
|
|
||||||
string displayName = string.IsNullOrWhiteSpace(unit.DisplayName) ? unit.name : unit.DisplayName;
|
|
||||||
titleLabel.text = "[ neofetch ]";
|
titleLabel.text = "[ neofetch ]";
|
||||||
asciiFitter.SetArt(DistroArtPresenter.Render(asciiLabel, asciiPlaceholder, unit));
|
asciiFitter.SetArt(DistroArtPresenter.Render(asciiLabel, asciiPlaceholder, unit));
|
||||||
unitNameLabel.text = displayName;
|
unitNameLabel.text = DistroPresentation.DisplayName(unit);
|
||||||
languagesLabel.text = $"{unit.PrimaryLanguage} / {unit.SecondaryLanguage}";
|
languagesLabel.text = DistroPresentation.FormatLanguages(unit);
|
||||||
passiveLabel.text = string.IsNullOrWhiteSpace(unit.PassiveName) ? "--" : unit.PassiveName;
|
passiveLabel.text = string.IsNullOrWhiteSpace(unit.PassiveName) ? "--" : unit.PassiveName;
|
||||||
bestWaveLabel.text = "--"; // TODO: Bind best-wave stats from SaveService when stats exist.
|
bestWaveLabel.text = "--"; // TODO: Bind best-wave stats from SaveService when stats exist.
|
||||||
SetAccent(ColorUtility.ToHtmlStringRGB(unit.AccentColor));
|
SetAccent(ColorUtility.ToHtmlStringRGB(unit.AccentColor));
|
||||||
|
|||||||
@@ -25,17 +25,23 @@ namespace KernelPanic.UI
|
|||||||
private const float BootIntroSeconds = 1.5f;
|
private const float BootIntroSeconds = 1.5f;
|
||||||
private static bool bootIntroPlayed;
|
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 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 string motdBody = "unstable userspace detected; keep a rollback shell open.";
|
||||||
[SerializeField] private DistroDatabase distroDatabase;
|
[SerializeField] private DistroDatabase distroDatabase;
|
||||||
[SerializeField] private FeaturedUnitPanel featuredUnitPanel = new();
|
[SerializeField] private FeaturedUnitPanel featuredUnitPanel = new();
|
||||||
|
[SerializeField] private CollectionScreenController collectionScreen = new();
|
||||||
|
[SerializeField] private StarterSelectionController starterSelection = new();
|
||||||
|
|
||||||
private readonly List<CommandMenuEntry> commandEntries = new();
|
private readonly List<CommandMenuEntry> commandEntries = new();
|
||||||
private readonly List<VisualElement> starterCards = new();
|
|
||||||
private readonly List<Label> starterNames = new();
|
|
||||||
private readonly List<Label> starterLanguages = new();
|
|
||||||
private readonly List<Label> starterDescriptions = new();
|
|
||||||
private readonly List<VisualElement> collectionRows = new();
|
|
||||||
private UIDocument document;
|
private UIDocument document;
|
||||||
private VisualElement root;
|
private VisualElement root;
|
||||||
private VisualElement shellRoot;
|
private VisualElement shellRoot;
|
||||||
@@ -46,9 +52,6 @@ namespace KernelPanic.UI
|
|||||||
private VisualElement settingsPanel;
|
private VisualElement settingsPanel;
|
||||||
private VisualElement eventBanner;
|
private VisualElement eventBanner;
|
||||||
private VisualElement backgroundLogLayer;
|
private VisualElement backgroundLogLayer;
|
||||||
private VisualElement starterModal;
|
|
||||||
private VisualElement collectionList;
|
|
||||||
private VisualElement collectionDetail;
|
|
||||||
private Label appIdLabel;
|
private Label appIdLabel;
|
||||||
private Label entropyLabel;
|
private Label entropyLabel;
|
||||||
private Label pullTokensLabel;
|
private Label pullTokensLabel;
|
||||||
@@ -56,7 +59,6 @@ namespace KernelPanic.UI
|
|||||||
private Label promptCursorLabel;
|
private Label promptCursorLabel;
|
||||||
private Label bootIntroLogLabel;
|
private Label bootIntroLogLabel;
|
||||||
private Label motdBodyLabel;
|
private Label motdBodyLabel;
|
||||||
private Label starterConfirmLabel;
|
|
||||||
private VisualElement motdBlock;
|
private VisualElement motdBlock;
|
||||||
private Button collectionBackButton;
|
private Button collectionBackButton;
|
||||||
private Button gachaBackButton;
|
private Button gachaBackButton;
|
||||||
@@ -69,19 +71,14 @@ namespace KernelPanic.UI
|
|||||||
private BackgroundLogRingBuffer backgroundLog;
|
private BackgroundLogRingBuffer backgroundLog;
|
||||||
private IEventBannerSource eventBannerSource;
|
private IEventBannerSource eventBannerSource;
|
||||||
private int selectedCommandIndex;
|
private int selectedCommandIndex;
|
||||||
private int selectedStarterIndex;
|
|
||||||
private int selectedCollectionIndex;
|
|
||||||
private float bootIntroElapsed;
|
private float bootIntroElapsed;
|
||||||
private int bootIntroCharacterCount;
|
private int bootIntroCharacterCount;
|
||||||
private bool cursorVisible;
|
private bool cursorVisible;
|
||||||
private bool suppressNextClick;
|
private bool suppressNextClick;
|
||||||
private bool starterModalActive;
|
|
||||||
private bool starterConfirming;
|
|
||||||
private bool warnedUnresolvedSaveId;
|
private bool warnedUnresolvedSaveId;
|
||||||
private string bootIntroCopy;
|
private string bootIntroCopy;
|
||||||
private IVisualElementScheduledItem blinkSchedule;
|
private IVisualElementScheduledItem blinkSchedule;
|
||||||
private IVisualElementScheduledItem bootIntroSchedule;
|
private IVisualElementScheduledItem bootIntroSchedule;
|
||||||
private IVisualElementScheduledItem starterCloseSchedule;
|
|
||||||
|
|
||||||
public void Initialize(EntropyWallet initializedWallet)
|
public void Initialize(EntropyWallet initializedWallet)
|
||||||
{
|
{
|
||||||
@@ -99,8 +96,9 @@ namespace KernelPanic.UI
|
|||||||
BindElements();
|
BindElements();
|
||||||
BindCommandEntries();
|
BindCommandEntries();
|
||||||
RegisterCommandEntryCallbacks();
|
RegisterCommandEntryCallbacks();
|
||||||
RegisterStarterCallbacks();
|
|
||||||
featuredUnitPanel.Bind(root, monospaceFont);
|
featuredUnitPanel.Bind(root, monospaceFont);
|
||||||
|
collectionScreen.Bind(root, monospaceFont);
|
||||||
|
starterSelection.Bind(root, distroDatabase, HandleStarterConfirmed);
|
||||||
LoadMetaState();
|
LoadMetaState();
|
||||||
playerCollection.Changed += HandleMetaStateChanged;
|
playerCollection.Changed += HandleMetaStateChanged;
|
||||||
gachaService.BannerPoolChanged += HandleMetaStateChanged;
|
gachaService.BannerPoolChanged += HandleMetaStateChanged;
|
||||||
@@ -116,7 +114,7 @@ namespace KernelPanic.UI
|
|||||||
RefreshCurrencyReadouts();
|
RefreshCurrencyReadouts();
|
||||||
RefreshEventBanner();
|
RefreshEventBanner();
|
||||||
featuredUnitPanel.Refresh(playerCollection.OwnedUnits);
|
featuredUnitPanel.Refresh(playerCollection.OwnedUnits);
|
||||||
RefreshCollection();
|
collectionScreen.Refresh(playerCollection.OwnedUnits);
|
||||||
SelectCommand(0);
|
SelectCommand(0);
|
||||||
ShowMainMenu();
|
ShowMainMenu();
|
||||||
StartAmbientSchedules();
|
StartAmbientSchedules();
|
||||||
@@ -129,7 +127,7 @@ namespace KernelPanic.UI
|
|||||||
blinkSchedule?.Pause();
|
blinkSchedule?.Pause();
|
||||||
backgroundLog?.Stop();
|
backgroundLog?.Stop();
|
||||||
bootIntroSchedule?.Pause();
|
bootIntroSchedule?.Pause();
|
||||||
starterCloseSchedule?.Pause();
|
starterSelection.PauseSchedules();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BindElements()
|
private void BindElements()
|
||||||
@@ -145,9 +143,6 @@ namespace KernelPanic.UI
|
|||||||
motdBlock = root.Q<VisualElement>("MotdBlock");
|
motdBlock = root.Q<VisualElement>("MotdBlock");
|
||||||
backgroundLogLayer = root.Q<VisualElement>("BackgroundLogLayer");
|
backgroundLogLayer = root.Q<VisualElement>("BackgroundLogLayer");
|
||||||
backgroundLog = new BackgroundLogRingBuffer(backgroundLogLayer, BootLogCopy.Lines);
|
backgroundLog = new BackgroundLogRingBuffer(backgroundLogLayer, BootLogCopy.Lines);
|
||||||
starterModal = root.Q<VisualElement>("StarterModal");
|
|
||||||
collectionList = root.Q<VisualElement>("CollectionList");
|
|
||||||
collectionDetail = root.Q<VisualElement>("CollectionDetail");
|
|
||||||
|
|
||||||
appIdLabel = root.Q<Label>("AppIdLabel");
|
appIdLabel = root.Q<Label>("AppIdLabel");
|
||||||
entropyLabel = root.Q<Label>("EntropyLabel");
|
entropyLabel = root.Q<Label>("EntropyLabel");
|
||||||
@@ -156,15 +151,6 @@ namespace KernelPanic.UI
|
|||||||
promptCursorLabel = root.Q<Label>("PromptCursorLabel");
|
promptCursorLabel = root.Q<Label>("PromptCursorLabel");
|
||||||
bootIntroLogLabel = root.Q<Label>("BootIntroLogLabel");
|
bootIntroLogLabel = root.Q<Label>("BootIntroLogLabel");
|
||||||
motdBodyLabel = root.Q<Label>("MotdBodyLabel");
|
motdBodyLabel = root.Q<Label>("MotdBodyLabel");
|
||||||
starterConfirmLabel = root.Q<Label>("StarterConfirmLabel");
|
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++)
|
|
||||||
{
|
|
||||||
starterCards.Add(root.Q<VisualElement>($"StarterOption{i}"));
|
|
||||||
starterNames.Add(root.Q<Label>($"StarterName{i}"));
|
|
||||||
starterLanguages.Add(root.Q<Label>($"StarterLanguages{i}"));
|
|
||||||
starterDescriptions.Add(root.Q<Label>($"StarterDescription{i}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
collectionBackButton = root.Q<Button>("CollectionBackButton");
|
collectionBackButton = root.Q<Button>("CollectionBackButton");
|
||||||
gachaBackButton = root.Q<Button>("GachaBackButton");
|
gachaBackButton = root.Q<Button>("GachaBackButton");
|
||||||
@@ -249,7 +235,21 @@ namespace KernelPanic.UI
|
|||||||
{
|
{
|
||||||
SaveCurrentState();
|
SaveCurrentState();
|
||||||
featuredUnitPanel.Refresh(playerCollection.OwnedUnits);
|
featuredUnitPanel.Refresh(playerCollection.OwnedUnits);
|
||||||
RefreshCollection();
|
collectionScreen.Refresh(playerCollection.OwnedUnits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleStarterConfirmed(DistroDefinition picked, IReadOnlyList<DistroDefinition> remaining)
|
||||||
|
{
|
||||||
|
playerCollection.Add(picked);
|
||||||
|
for (int i = 0; i < remaining.Count; i++)
|
||||||
|
{
|
||||||
|
gachaService.AddToBannerPool(remaining[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData.starterChosen = true;
|
||||||
|
SaveCurrentState();
|
||||||
|
featuredUnitPanel.Refresh(playerCollection.OwnedUnits);
|
||||||
|
collectionScreen.Refresh(playerCollection.OwnedUnits);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveCurrentState()
|
private void SaveCurrentState()
|
||||||
@@ -298,20 +298,6 @@ namespace KernelPanic.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterStarterCallbacks()
|
|
||||||
{
|
|
||||||
for (int i = 0; i < starterCards.Count; i++)
|
|
||||||
{
|
|
||||||
int index = i;
|
|
||||||
starterCards[i].RegisterCallback<PointerEnterEvent>(_ => SelectStarter(index));
|
|
||||||
starterCards[i].RegisterCallback<ClickEvent>(_ =>
|
|
||||||
{
|
|
||||||
SelectStarter(index);
|
|
||||||
ConfirmStarterSelectionIntent();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshCurrencyReadouts()
|
private void RefreshCurrencyReadouts()
|
||||||
{
|
{
|
||||||
if (entropyLabel == null || pullTokensLabel == null || gachaService == null || wallet == null)
|
if (entropyLabel == null || pullTokensLabel == null || gachaService == null || wallet == null)
|
||||||
@@ -338,131 +324,6 @@ namespace KernelPanic.UI
|
|||||||
: banner.Text;
|
: banner.Text;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshCollection()
|
|
||||||
{
|
|
||||||
if (collectionList == null || collectionDetail == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
collectionRows.Clear();
|
|
||||||
collectionList.Clear();
|
|
||||||
|
|
||||||
IReadOnlyList<DistroDefinition> units = playerCollection.OwnedUnits;
|
|
||||||
if (units.Count == 0)
|
|
||||||
{
|
|
||||||
collectionDetail.Clear();
|
|
||||||
collectionDetail.Add(new Label("no units installed") { name = "CollectionEmptyTitle" });
|
|
||||||
collectionDetail.Add(new Label("run: curl gacha.sh | sh") { name = "CollectionEmptyHint" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedCollectionIndex = Mathf.Clamp(selectedCollectionIndex, 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<PointerEnterEvent>(_ => SelectCollectionUnit(index));
|
|
||||||
row.RegisterCallback<ClickEvent>(_ => SelectCollectionUnit(index));
|
|
||||||
|
|
||||||
Label name = new(DisplayName(unit));
|
|
||||||
name.AddToClassList("collection-row-name");
|
|
||||||
name.style.color = new StyleColor(unit.AccentColor);
|
|
||||||
|
|
||||||
Label languages = new(FormatLanguages(unit));
|
|
||||||
languages.AddToClassList("collection-row-languages");
|
|
||||||
|
|
||||||
row.Add(name);
|
|
||||||
row.Add(languages);
|
|
||||||
collectionList.Add(row);
|
|
||||||
collectionRows.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectCollectionUnit(selectedCollectionIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectCollectionUnit(int index)
|
|
||||||
{
|
|
||||||
IReadOnlyList<DistroDefinition> units = playerCollection.OwnedUnits;
|
|
||||||
if (units.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedCollectionIndex = Mathf.Clamp(index, 0, units.Count - 1);
|
|
||||||
for (int i = 0; i < collectionRows.Count; i++)
|
|
||||||
{
|
|
||||||
collectionRows[i].EnableInClassList(SelectedClassName, i == selectedCollectionIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderCollectionDetail(units[selectedCollectionIndex]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RenderCollectionDetail(DistroDefinition unit)
|
|
||||||
{
|
|
||||||
collectionDetail.Clear();
|
|
||||||
|
|
||||||
// TODO: Unify this readout with FeaturedUnitPanel when unit presentation grows beyond labels.
|
|
||||||
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(DisplayName(unit));
|
|
||||||
name.AddToClassList("collection-detail-name");
|
|
||||||
name.style.color = new StyleColor(unit.AccentColor);
|
|
||||||
details.Add(name);
|
|
||||||
|
|
||||||
details.Add(BuildDetailLine("lang", 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);
|
|
||||||
collectionDetail.Add(readout);
|
|
||||||
|
|
||||||
Label description = new(string.IsNullOrWhiteSpace(unit.Description) ? "--" : unit.Description);
|
|
||||||
description.AddToClassList("collection-detail-description");
|
|
||||||
collectionDetail.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string DisplayName(DistroDefinition unit)
|
|
||||||
{
|
|
||||||
return string.IsNullOrWhiteSpace(unit.DisplayName) ? unit.name : unit.DisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatLanguages(DistroDefinition unit)
|
|
||||||
{
|
|
||||||
return $"{unit.PrimaryLanguage} / {unit.SecondaryLanguage}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartAmbientSchedules()
|
private void StartAmbientSchedules()
|
||||||
{
|
{
|
||||||
blinkSchedule?.Pause();
|
blinkSchedule?.Pause();
|
||||||
@@ -525,7 +386,7 @@ namespace KernelPanic.UI
|
|||||||
shellRoot.RemoveFromClassList("boot-hidden");
|
shellRoot.RemoveFromClassList("boot-hidden");
|
||||||
shellRoot.AddToClassList("boot-visible");
|
shellRoot.AddToClassList("boot-visible");
|
||||||
backgroundLog?.Start(UIPreferences.ReducedMotion);
|
backgroundLog?.Start(UIPreferences.ReducedMotion);
|
||||||
ShowStarterModalIfNeeded();
|
starterSelection.ShowIfNeeded(saveData == null || saveData.starterChosen);
|
||||||
root.Focus();
|
root.Focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,9 +409,9 @@ namespace KernelPanic.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (starterModalActive)
|
if (starterSelection.IsActive)
|
||||||
{
|
{
|
||||||
HandleStarterKeyDown(evt);
|
starterSelection.HandleKeyDown(evt);
|
||||||
evt.StopPropagation();
|
evt.StopPropagation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -583,7 +444,7 @@ namespace KernelPanic.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int digitIndex = GetDigitCommandIndex(evt.keyCode);
|
int digitIndex = CommandKeyBindings.GetDigitIndex(evt.keyCode);
|
||||||
if (digitIndex >= 0)
|
if (digitIndex >= 0)
|
||||||
{
|
{
|
||||||
SelectCommand(digitIndex);
|
SelectCommand(digitIndex);
|
||||||
@@ -598,194 +459,6 @@ namespace KernelPanic.UI
|
|||||||
suppressNextClick = SkipBootIntro();
|
suppressNextClick = SkipBootIntro();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowStarterModalIfNeeded()
|
|
||||||
{
|
|
||||||
if (saveData == null || saveData.starterChosen)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IReadOnlyList<DistroDefinition> starters = GetStarterDistros();
|
|
||||||
if (starters.Count < 3)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("Starter distro selection needs three distros in DistroDatabase.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
starterModalActive = true;
|
|
||||||
starterConfirming = false;
|
|
||||||
starterConfirmLabel.AddToClassList(HiddenClassName);
|
|
||||||
starterModal.RemoveFromClassList(HiddenClassName);
|
|
||||||
|
|
||||||
for (int i = 0; i < starterCards.Count; i++)
|
|
||||||
{
|
|
||||||
DistroDefinition unit = starters[i];
|
|
||||||
starterNames[i].text = DisplayName(unit);
|
|
||||||
starterNames[i].style.color = new StyleColor(unit.AccentColor);
|
|
||||||
starterLanguages[i].text = FormatLanguages(unit);
|
|
||||||
starterDescriptions[i].text = unit.Description;
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectStarter(0);
|
|
||||||
root.Focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private IReadOnlyList<DistroDefinition> GetStarterDistros()
|
|
||||||
{
|
|
||||||
return distroDatabase == null ? Array.Empty<DistroDefinition>() : distroDatabase.AllDistros;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectStarter(int index)
|
|
||||||
{
|
|
||||||
IReadOnlyList<DistroDefinition> starters = GetStarterDistros();
|
|
||||||
if (starters.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedStarterIndex = (index + Mathf.Min(starters.Count, starterCards.Count)) % Mathf.Min(starters.Count, starterCards.Count);
|
|
||||||
starterConfirming = false;
|
|
||||||
starterConfirmLabel.AddToClassList(HiddenClassName);
|
|
||||||
|
|
||||||
for (int i = 0; i < starterCards.Count; i++)
|
|
||||||
{
|
|
||||||
bool selected = i == selectedStarterIndex;
|
|
||||||
starterCards[i].EnableInClassList(SelectedClassName, selected);
|
|
||||||
if (i < starters.Count && selected)
|
|
||||||
{
|
|
||||||
StyleColor accent = new(starters[i].AccentColor);
|
|
||||||
starterCards[i].style.borderTopColor = accent;
|
|
||||||
starterCards[i].style.borderRightColor = accent;
|
|
||||||
starterCards[i].style.borderBottomColor = accent;
|
|
||||||
starterCards[i].style.borderLeftColor = accent;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
starterCards[i].style.borderTopColor = StyleKeyword.Null;
|
|
||||||
starterCards[i].style.borderRightColor = StyleKeyword.Null;
|
|
||||||
starterCards[i].style.borderBottomColor = StyleKeyword.Null;
|
|
||||||
starterCards[i].style.borderLeftColor = StyleKeyword.Null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleStarterKeyDown(KeyDownEvent evt)
|
|
||||||
{
|
|
||||||
if (evt.keyCode == KeyCode.LeftArrow)
|
|
||||||
{
|
|
||||||
SelectStarter(selectedStarterIndex - 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evt.keyCode == KeyCode.RightArrow)
|
|
||||||
{
|
|
||||||
SelectStarter(selectedStarterIndex + 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int digitIndex = GetDigitCommandIndex(evt.keyCode);
|
|
||||||
if (digitIndex >= 0 && digitIndex < 3)
|
|
||||||
{
|
|
||||||
SelectStarter(digitIndex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
|
|
||||||
{
|
|
||||||
if (starterConfirming)
|
|
||||||
{
|
|
||||||
ConfirmStarterSelection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConfirmStarterSelectionIntent();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evt.keyCode == KeyCode.Y && starterConfirming)
|
|
||||||
{
|
|
||||||
ConfirmStarterSelection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.N)
|
|
||||||
{
|
|
||||||
starterConfirming = false;
|
|
||||||
starterConfirmLabel.AddToClassList(HiddenClassName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfirmStarterSelectionIntent()
|
|
||||||
{
|
|
||||||
IReadOnlyList<DistroDefinition> starters = GetStarterDistros();
|
|
||||||
if (selectedStarterIndex >= starters.Count)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
starterConfirming = true;
|
|
||||||
starterConfirmLabel.text = $"install {DisplayName(starters[selectedStarterIndex])}? this cannot be undone [Y/n]";
|
|
||||||
starterConfirmLabel.RemoveFromClassList(HiddenClassName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfirmStarterSelection()
|
|
||||||
{
|
|
||||||
IReadOnlyList<DistroDefinition> starters = GetStarterDistros();
|
|
||||||
if (selectedStarterIndex >= starters.Count)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DistroDefinition picked = starters[selectedStarterIndex];
|
|
||||||
playerCollection.Add(picked);
|
|
||||||
for (int i = 0; i < starters.Count; i++)
|
|
||||||
{
|
|
||||||
if (i != selectedStarterIndex)
|
|
||||||
{
|
|
||||||
gachaService.AddToBannerPool(starters[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
saveData.starterChosen = true;
|
|
||||||
SaveCurrentState();
|
|
||||||
featuredUnitPanel.Refresh(playerCollection.OwnedUnits);
|
|
||||||
RefreshCollection();
|
|
||||||
|
|
||||||
starterConfirmLabel.text = $"installing {picked.Id} (1/1)... done";
|
|
||||||
starterConfirmLabel.RemoveFromClassList(HiddenClassName);
|
|
||||||
if (UIPreferences.ReducedMotion)
|
|
||||||
{
|
|
||||||
CloseStarterModal();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
starterCloseSchedule?.Pause();
|
|
||||||
starterCloseSchedule = root.schedule.Execute(CloseStarterModal).StartingIn(600);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseStarterModal()
|
|
||||||
{
|
|
||||||
starterCloseSchedule?.Pause();
|
|
||||||
starterCloseSchedule = null;
|
|
||||||
starterModalActive = false;
|
|
||||||
starterConfirming = false;
|
|
||||||
starterModal.AddToClassList(HiddenClassName);
|
|
||||||
root.Focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int GetDigitCommandIndex(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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectCommand(int index)
|
private void SelectCommand(int index)
|
||||||
{
|
{
|
||||||
selectedCommandIndex = (index + commandEntries.Count) % commandEntries.Count;
|
selectedCommandIndex = (index + commandEntries.Count) % commandEntries.Count;
|
||||||
@@ -833,7 +506,7 @@ namespace KernelPanic.UI
|
|||||||
|
|
||||||
private void ShowCollection()
|
private void ShowCollection()
|
||||||
{
|
{
|
||||||
RefreshCollection();
|
collectionScreen.Refresh(playerCollection.OwnedUnits);
|
||||||
ShowPanel(collectionPanel);
|
ShowPanel(collectionPanel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,299 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using KernelPanic.Data;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
|
namespace KernelPanic.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Drives the starter-distro selection modal: presents the choices, confirms the pick, and
|
||||||
|
/// - if the DistroDatabase isn't usable - shows a dismissible in-UI error instead of leaving
|
||||||
|
/// the modal silently hidden.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public sealed class StarterSelectionController
|
||||||
|
{
|
||||||
|
private const int StarterCount = 3;
|
||||||
|
|
||||||
|
private readonly List<VisualElement> cards = new();
|
||||||
|
private readonly List<Label> names = new();
|
||||||
|
private readonly List<Label> languages = new();
|
||||||
|
private readonly List<Label> descriptions = new();
|
||||||
|
|
||||||
|
private VisualElement root;
|
||||||
|
private VisualElement modal;
|
||||||
|
private VisualElement options;
|
||||||
|
private Label confirmLabel;
|
||||||
|
private Label errorLabel;
|
||||||
|
private DistroDatabase database;
|
||||||
|
private Action<DistroDefinition, IReadOnlyList<DistroDefinition>> onConfirmed;
|
||||||
|
|
||||||
|
private IReadOnlyList<DistroDefinition> activeStarters = Array.Empty<DistroDefinition>();
|
||||||
|
private int selectedIndex;
|
||||||
|
private bool active;
|
||||||
|
private bool showingError;
|
||||||
|
private bool confirming;
|
||||||
|
private IVisualElementScheduledItem closeSchedule;
|
||||||
|
|
||||||
|
public bool IsActive => active;
|
||||||
|
|
||||||
|
public void Bind(VisualElement documentRoot, DistroDatabase distroDatabase, Action<DistroDefinition, IReadOnlyList<DistroDefinition>> onStarterConfirmed)
|
||||||
|
{
|
||||||
|
root = documentRoot;
|
||||||
|
database = distroDatabase;
|
||||||
|
onConfirmed = onStarterConfirmed;
|
||||||
|
|
||||||
|
modal = root.Q<VisualElement>("StarterModal");
|
||||||
|
options = root.Q<VisualElement>("StarterOptions");
|
||||||
|
confirmLabel = root.Q<Label>("StarterConfirmLabel");
|
||||||
|
errorLabel = root.Q<Label>("StarterErrorLabel");
|
||||||
|
|
||||||
|
for (int i = 0; i < StarterCount; i++)
|
||||||
|
{
|
||||||
|
cards.Add(root.Q<VisualElement>($"StarterOption{i}"));
|
||||||
|
names.Add(root.Q<Label>($"StarterName{i}"));
|
||||||
|
languages.Add(root.Q<Label>($"StarterLanguages{i}"));
|
||||||
|
descriptions.Add(root.Q<Label>($"StarterDescription{i}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < cards.Count; i++)
|
||||||
|
{
|
||||||
|
int index = i;
|
||||||
|
cards[i].RegisterCallback<PointerEnterEvent>(_ => Select(index));
|
||||||
|
cards[i].RegisterCallback<ClickEvent>(_ =>
|
||||||
|
{
|
||||||
|
Select(index);
|
||||||
|
ConfirmIntent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowIfNeeded(bool starterAlreadyChosen)
|
||||||
|
{
|
||||||
|
if (starterAlreadyChosen)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetStarters(out IReadOnlyList<DistroDefinition> starters, out string error))
|
||||||
|
{
|
||||||
|
Debug.LogError($"Cannot show starter selection: {error}.");
|
||||||
|
ShowConfigError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeStarters = starters;
|
||||||
|
active = true;
|
||||||
|
showingError = false;
|
||||||
|
confirming = false;
|
||||||
|
errorLabel.AddToClassList("hidden");
|
||||||
|
options.RemoveFromClassList("hidden");
|
||||||
|
confirmLabel.AddToClassList("hidden");
|
||||||
|
modal.RemoveFromClassList("hidden");
|
||||||
|
|
||||||
|
for (int i = 0; i < cards.Count; i++)
|
||||||
|
{
|
||||||
|
DistroDefinition unit = starters[i];
|
||||||
|
names[i].text = DistroPresentation.DisplayName(unit);
|
||||||
|
names[i].style.color = new StyleColor(unit.AccentColor);
|
||||||
|
languages[i].text = DistroPresentation.FormatLanguages(unit);
|
||||||
|
descriptions[i].text = unit.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
Select(0);
|
||||||
|
root.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleKeyDown(KeyDownEvent evt)
|
||||||
|
{
|
||||||
|
if (showingError)
|
||||||
|
{
|
||||||
|
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter ||
|
||||||
|
evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.Space)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.keyCode == KeyCode.LeftArrow)
|
||||||
|
{
|
||||||
|
Select(selectedIndex - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.keyCode == KeyCode.RightArrow)
|
||||||
|
{
|
||||||
|
Select(selectedIndex + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int digitIndex = CommandKeyBindings.GetDigitIndex(evt.keyCode);
|
||||||
|
if (digitIndex >= 0 && digitIndex < StarterCount)
|
||||||
|
{
|
||||||
|
Select(digitIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
|
||||||
|
{
|
||||||
|
if (confirming)
|
||||||
|
{
|
||||||
|
Confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmIntent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.keyCode == KeyCode.Y && confirming)
|
||||||
|
{
|
||||||
|
Confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evt.keyCode == KeyCode.Escape || evt.keyCode == KeyCode.N)
|
||||||
|
{
|
||||||
|
confirming = false;
|
||||||
|
confirmLabel.AddToClassList("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PauseSchedules()
|
||||||
|
{
|
||||||
|
closeSchedule?.Pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetStarters(out IReadOnlyList<DistroDefinition> starters, out string error)
|
||||||
|
{
|
||||||
|
if (database == null)
|
||||||
|
{
|
||||||
|
starters = Array.Empty<DistroDefinition>();
|
||||||
|
error = "no DistroDatabase is assigned";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!database.TryValidate(out error))
|
||||||
|
{
|
||||||
|
starters = Array.Empty<DistroDefinition>();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<DistroDefinition> allDistros = database.AllDistros;
|
||||||
|
if (allDistros.Count < StarterCount)
|
||||||
|
{
|
||||||
|
starters = Array.Empty<DistroDefinition>();
|
||||||
|
error = $"DistroDatabase needs at least {StarterCount} distros (found {allDistros.Count})";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
starters = allDistros;
|
||||||
|
error = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowConfigError(string error)
|
||||||
|
{
|
||||||
|
active = true;
|
||||||
|
showingError = true;
|
||||||
|
confirming = false;
|
||||||
|
options.AddToClassList("hidden");
|
||||||
|
confirmLabel.AddToClassList("hidden");
|
||||||
|
errorLabel.text = $"starter selection unavailable: {error}. press enter to continue.";
|
||||||
|
errorLabel.RemoveFromClassList("hidden");
|
||||||
|
modal.RemoveFromClassList("hidden");
|
||||||
|
root.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Select(int index)
|
||||||
|
{
|
||||||
|
if (activeStarters.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int slotCount = Mathf.Min(activeStarters.Count, cards.Count);
|
||||||
|
selectedIndex = (index + slotCount) % slotCount;
|
||||||
|
confirming = false;
|
||||||
|
confirmLabel.AddToClassList("hidden");
|
||||||
|
|
||||||
|
for (int i = 0; i < cards.Count; i++)
|
||||||
|
{
|
||||||
|
bool selected = i == selectedIndex;
|
||||||
|
cards[i].EnableInClassList("selected", selected);
|
||||||
|
if (i < activeStarters.Count && selected)
|
||||||
|
{
|
||||||
|
StyleColor accent = new(activeStarters[i].AccentColor);
|
||||||
|
cards[i].style.borderTopColor = accent;
|
||||||
|
cards[i].style.borderRightColor = accent;
|
||||||
|
cards[i].style.borderBottomColor = accent;
|
||||||
|
cards[i].style.borderLeftColor = accent;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cards[i].style.borderTopColor = StyleKeyword.Null;
|
||||||
|
cards[i].style.borderRightColor = StyleKeyword.Null;
|
||||||
|
cards[i].style.borderBottomColor = StyleKeyword.Null;
|
||||||
|
cards[i].style.borderLeftColor = StyleKeyword.Null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfirmIntent()
|
||||||
|
{
|
||||||
|
if (selectedIndex >= activeStarters.Count)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirming = true;
|
||||||
|
confirmLabel.text = $"install {DistroPresentation.DisplayName(activeStarters[selectedIndex])}? this cannot be undone [Y/n]";
|
||||||
|
confirmLabel.RemoveFromClassList("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Confirm()
|
||||||
|
{
|
||||||
|
if (selectedIndex >= activeStarters.Count)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DistroDefinition picked = activeStarters[selectedIndex];
|
||||||
|
List<DistroDefinition> remaining = new();
|
||||||
|
for (int i = 0; i < activeStarters.Count; i++)
|
||||||
|
{
|
||||||
|
if (i != selectedIndex)
|
||||||
|
{
|
||||||
|
remaining.Add(activeStarters[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmed?.Invoke(picked, remaining);
|
||||||
|
|
||||||
|
confirmLabel.text = $"installing {picked.Id} (1/1)... done";
|
||||||
|
confirmLabel.RemoveFromClassList("hidden");
|
||||||
|
if (UIPreferences.ReducedMotion)
|
||||||
|
{
|
||||||
|
Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSchedule?.Pause();
|
||||||
|
closeSchedule = root.schedule.Execute(Close).StartingIn(600);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Close()
|
||||||
|
{
|
||||||
|
closeSchedule?.Pause();
|
||||||
|
closeSchedule = null;
|
||||||
|
active = false;
|
||||||
|
confirming = false;
|
||||||
|
showingError = false;
|
||||||
|
modal.AddToClassList("hidden");
|
||||||
|
root.Focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -634,6 +634,12 @@ Button {
|
|||||||
color: var(--kp-amber);
|
color: var(--kp-amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.starter-error {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
color: var(--kp-amber);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.terminal-button {
|
.terminal-button {
|
||||||
height: 42px;
|
height: 42px;
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
<ui:VisualElement class="starter-dialog">
|
<ui:VisualElement class="starter-dialog">
|
||||||
<ui:Label text="select starter distro" class="starter-title" />
|
<ui:Label text="select starter distro" class="starter-title" />
|
||||||
<ui:Label text="choose one base system to install into persistent userspace" class="starter-instruction" />
|
<ui:Label text="choose one base system to install into persistent userspace" class="starter-instruction" />
|
||||||
|
<ui:Label name="StarterErrorLabel" class="starter-error hidden" />
|
||||||
<ui:VisualElement name="StarterOptions" class="starter-options">
|
<ui:VisualElement name="StarterOptions" class="starter-options">
|
||||||
<ui:VisualElement name="StarterOption0" class="starter-card">
|
<ui:VisualElement name="StarterOption0" class="starter-card">
|
||||||
<ui:Label name="StarterName0" class="starter-name" />
|
<ui:Label name="StarterName0" class="starter-name" />
|
||||||
|
|||||||
Reference in New Issue
Block a user