finished main-menu so far

This commit is contained in:
2026-07-06 22:17:26 +02:00
parent 811d29a7b0
commit f7c09e98f2
7 changed files with 633 additions and 82 deletions
+6
View File
@@ -29,3 +29,9 @@ MonoBehaviour:
- {fileID: 11400000, guid: c1a50000000000000000000000000003, type: 2} - {fileID: 11400000, guid: c1a50000000000000000000000000003, type: 2}
- {fileID: 11400000, guid: c1a50000000000000000000000000004, type: 2} - {fileID: 11400000, guid: c1a50000000000000000000000000004, type: 2}
- {fileID: 11400000, guid: c1a50000000000000000000000000005, type: 2} - {fileID: 11400000, guid: c1a50000000000000000000000000005, type: 2}
- {fileID: 11400000, guid: a7d40000000000000000000000000001, type: 2}
- {fileID: 11400000, guid: a7d40000000000000000000000000002, type: 2}
- {fileID: 11400000, guid: a7d40000000000000000000000000003, type: 2}
- {fileID: 11400000, guid: a7d40000000000000000000000000004, type: 2}
- {fileID: 11400000, guid: a7d40000000000000000000000000005, type: 2}
- {fileID: 11400000, guid: a7d40000000000000000000000000006, type: 2}
+1
View File
@@ -307,4 +307,5 @@ MonoBehaviour:
motdBody: unstable userspace detected; keep a rollback shell open. motdBody: unstable userspace detected; keep a rollback shell open.
distroDatabase: {fileID: 11400000, guid: 4d88ca5446fa4d80b2fbd4b12697cb7c, type: 2} distroDatabase: {fileID: 11400000, guid: 4d88ca5446fa4d80b2fbd4b12697cb7c, type: 2}
cardDatabase: {fileID: 11400000, guid: 6a8a9f37c98a4a7a9e67ab15d2d22208, type: 2} cardDatabase: {fileID: 11400000, guid: 6a8a9f37c98a4a7a9e67ab15d2d22208, type: 2}
languageDeckDatabase: {fileID: 11400000, guid: a7d20000000000000000000000000001, type: 2}
featuredUnitPanel: {} featuredUnitPanel: {}
+2
View File
@@ -20,6 +20,7 @@ namespace KernelPanic.Data
[SerializeField] private bool distroExclusive; [SerializeField] private bool distroExclusive;
[SerializeField] private bool isToken; [SerializeField] private bool isToken;
[SerializeField, TextArea] private string designNotes; [SerializeField, TextArea] private string designNotes;
[SerializeField] private List<string> requiresSystem = new();
[SerializeField] private CardDefinition upgradeTarget; [SerializeField] private CardDefinition upgradeTarget;
[SerializeField] private List<CardEffectDefinition> effects = new(); [SerializeField] private List<CardEffectDefinition> effects = new();
@@ -34,6 +35,7 @@ namespace KernelPanic.Data
public bool DistroExclusive => distroExclusive; public bool DistroExclusive => distroExclusive;
public bool IsToken => isToken; public bool IsToken => isToken;
public string DesignNotes => designNotes; public string DesignNotes => designNotes;
public IReadOnlyList<string> RequiresSystem => requiresSystem;
public CardDefinition UpgradeTarget => upgradeTarget; public CardDefinition UpgradeTarget => upgradeTarget;
public IReadOnlyList<CardEffectDefinition> Effects => effects; public IReadOnlyList<CardEffectDefinition> Effects => effects;
} }
+379 -52
View File
@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using KernelPanic.Core;
using KernelPanic.Data; using KernelPanic.Data;
using KernelPanic.Meta;
using UnityEngine; using UnityEngine;
using UnityEngine.TextCore.Text; using UnityEngine.TextCore.Text;
using UnityEngine.UIElements; using UnityEngine.UIElements;
@@ -8,7 +10,7 @@ using UnityEngine.UIElements;
namespace KernelPanic.UI namespace KernelPanic.UI
{ {
/// <summary> /// <summary>
/// Renders the owned-units list and detail readout on the collection screen. /// Renders Collection tabs and the pushed read-only card subview.
/// </summary> /// </summary>
[Serializable] [Serializable]
public sealed class CollectionScreenController public sealed class CollectionScreenController
@@ -18,20 +20,116 @@ namespace KernelPanic.UI
private VisualElement list; private VisualElement list;
private VisualElement detail; private VisualElement detail;
private FontAsset monospaceFont; private FontAsset monospaceFont;
private LanguageDeckDatabase languageDeckDatabase;
private CardDatabase cardDatabase;
private PlayerCollection playerCollection;
private IReadOnlyList<DistroDefinition> units = Array.Empty<DistroDefinition>(); private IReadOnlyList<DistroDefinition> units = Array.Empty<DistroDefinition>();
private IReadOnlyList<LanguageCatalogEntry> languages = LanguageCatalog.All;
private IReadOnlyList<CardEntry> cards = Array.Empty<CardEntry>(); private IReadOnlyList<CardEntry> cards = Array.Empty<CardEntry>();
private int selectedIndex; private CollectionMode mode;
private CollectionMode returnMode;
private int selectedUnitIndex;
private int selectedLanguageIndex;
private int selectedCardIndex;
private string emptyCardMessage = "no cards installed";
public void Bind(VisualElement root, FontAsset artFont) public event Action ViewChanged;
public void Bind(VisualElement root, FontAsset artFont, LanguageDeckDatabase deckDatabase, CardDatabase cardsDatabase, PlayerCollection collection)
{ {
monospaceFont = artFont; monospaceFont = artFont;
languageDeckDatabase = deckDatabase;
cardDatabase = cardsDatabase;
playerCollection = collection;
list = root.Q<VisualElement>("CollectionList"); list = root.Q<VisualElement>("CollectionList");
detail = root.Q<VisualElement>("CollectionDetail"); detail = root.Q<VisualElement>("CollectionDetail");
} }
public bool IsCardSubview => mode == CollectionMode.CardSubview;
public string CurrentTitle { get; private set; } = "$ ls ~/collection";
public string CurrentHint { get; private set; } = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select";
public void RefreshUnits(IReadOnlyList<DistroDefinition> ownedUnits) public void RefreshUnits(IReadOnlyList<DistroDefinition> ownedUnits)
{ {
units = ownedUnits ?? Array.Empty<DistroDefinition>(); units = ownedUnits ?? Array.Empty<DistroDefinition>();
ShowUnits();
}
public void ShowUnits()
{
mode = CollectionMode.Units;
CurrentTitle = "$ ls ~/collection";
CurrentHint = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select";
RenderUnits();
ViewChanged?.Invoke();
}
public void ShowLanguages()
{
mode = CollectionMode.Languages;
CurrentTitle = "$ ls ~/collection/languages";
CurrentHint = "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select";
RenderLanguages();
ViewChanged?.Invoke();
}
public bool BackFromSubview()
{
if (!IsCardSubview)
{
return false;
}
if (returnMode == CollectionMode.Languages)
{
ShowLanguages();
}
else
{
ShowUnits();
}
return true;
}
public void SelectRelative(int delta)
{
if (mode == CollectionMode.Units)
{
SelectUnit(selectedUnitIndex + delta);
return;
}
if (mode == CollectionMode.Languages)
{
SelectLanguage(selectedLanguageIndex + delta);
return;
}
SelectCard(selectedCardIndex + delta);
}
public void ActivateSelected()
{
if (mode == CollectionMode.Units && units.Count > 0)
{
OpenUnitCards(units[selectedUnitIndex]);
return;
}
if (mode == CollectionMode.Languages && languages.Count > 0)
{
LanguageCatalogEntry language = languages[selectedLanguageIndex];
if (LanguageUnlock.IsUnlocked(language.Language, playerCollection))
{
OpenLanguageDeck(language);
}
}
}
private void RenderUnits()
{
if (list == null || detail == null) if (list == null || detail == null)
{ {
return; return;
@@ -48,7 +146,7 @@ namespace KernelPanic.UI
return; return;
} }
selectedIndex = Mathf.Clamp(selectedIndex, 0, units.Count - 1); selectedUnitIndex = Mathf.Clamp(selectedUnitIndex, 0, units.Count - 1);
for (int i = 0; i < units.Count; i++) for (int i = 0; i < units.Count; i++)
{ {
int index = i; int index = i;
@@ -62,21 +160,20 @@ namespace KernelPanic.UI
name.AddToClassList("collection-row-name"); name.AddToClassList("collection-row-name");
name.style.color = new StyleColor(unit.AccentColor); name.style.color = new StyleColor(unit.AccentColor);
Label languages = new(DistroPresentation.FormatLanguages(unit)); Label unitLanguages = new(DistroPresentation.FormatLanguages(unit));
languages.AddToClassList("collection-row-languages"); unitLanguages.AddToClassList("collection-row-languages");
row.Add(name); row.Add(name);
row.Add(languages); row.Add(unitLanguages);
list.Add(row); list.Add(row);
rows.Add(row); rows.Add(row);
} }
SelectUnit(selectedIndex); SelectUnit(selectedUnitIndex);
} }
public void RefreshCards(DistroDefinition featuredUnit) private void RenderLanguages()
{ {
cards = BuildCardEntries(featuredUnit);
if (list == null || detail == null) if (list == null || detail == null)
{ {
return; return;
@@ -84,43 +181,32 @@ namespace KernelPanic.UI
rows.Clear(); rows.Clear();
list.Clear(); list.Clear();
selectedLanguageIndex = Mathf.Clamp(selectedLanguageIndex, 0, languages.Count - 1);
if (cards.Count == 0) for (int i = 0; i < languages.Count; i++)
{
detail.Clear();
detail.Add(new Label("no cards installed") { name = "CollectionEmptyTitle" });
detail.Add(new Label("featured distro cards appear here") { name = "CollectionEmptyHint" });
return;
}
selectedIndex = Mathf.Clamp(selectedIndex, 0, cards.Count - 1);
for (int i = 0; i < cards.Count; i++)
{ {
int index = i; int index = i;
CardEntry entry = cards[i]; LanguageCatalogEntry language = languages[i];
bool unlocked = LanguageUnlock.IsUnlocked(language.Language, playerCollection);
VisualElement row = new(); VisualElement row = new();
row.AddToClassList("collection-row"); row.AddToClassList("collection-row");
row.RegisterCallback<PointerEnterEvent>(_ => SelectCard(index)); row.EnableInClassList("locked", !unlocked);
row.RegisterCallback<ClickEvent>(_ => SelectCard(index)); row.RegisterCallback<PointerEnterEvent>(_ => SelectLanguage(index));
row.RegisterCallback<ClickEvent>(_ => SelectLanguage(index));
Label name = new(GetCardDisplayName(entry.Card)); Label name = new(unlocked ? language.DisplayName : $"{language.DisplayName} [locked]");
name.AddToClassList("collection-row-name"); name.AddToClassList("collection-row-name");
Label meta = new($"{DistroPresentation.DisplayName(entry.Owner)} / {FormatCardMeta(entry.Card)}"); Label tag = new(language.IdentityTag);
meta.AddToClassList("collection-row-languages"); tag.AddToClassList("collection-row-languages");
row.Add(name); row.Add(name);
row.Add(meta); row.Add(tag);
list.Add(row); list.Add(row);
rows.Add(row); rows.Add(row);
} }
SelectCard(selectedIndex); SelectLanguage(selectedLanguageIndex);
}
public void Refresh(IReadOnlyList<DistroDefinition> ownedUnits)
{
RefreshUnits(ownedUnits);
} }
private void SelectUnit(int index) private void SelectUnit(int index)
@@ -130,13 +216,16 @@ namespace KernelPanic.UI
return; return;
} }
selectedIndex = Mathf.Clamp(index, 0, units.Count - 1); selectedUnitIndex = Mathf.Clamp(index, 0, units.Count - 1);
for (int i = 0; i < rows.Count; i++) ApplySelection(selectedUnitIndex);
{ RenderUnitDetail(units[selectedUnitIndex]);
rows[i].EnableInClassList("selected", i == selectedIndex); }
}
RenderDetail(units[selectedIndex]); private void SelectLanguage(int index)
{
selectedLanguageIndex = Mathf.Clamp(index, 0, languages.Count - 1);
ApplySelection(selectedLanguageIndex);
RenderLanguageDetail(languages[selectedLanguageIndex]);
} }
private void SelectCard(int index) private void SelectCard(int index)
@@ -146,16 +235,20 @@ namespace KernelPanic.UI
return; return;
} }
selectedIndex = Mathf.Clamp(index, 0, cards.Count - 1); selectedCardIndex = Mathf.Clamp(index, 0, cards.Count - 1);
ApplySelection(selectedCardIndex);
RenderCardDetail(cards[selectedCardIndex]);
}
private void ApplySelection(int selectedIndex)
{
for (int i = 0; i < rows.Count; i++) for (int i = 0; i < rows.Count; i++)
{ {
rows[i].EnableInClassList("selected", i == selectedIndex); rows[i].EnableInClassList("selected", i == selectedIndex);
} }
RenderCardDetail(cards[selectedIndex]);
} }
private void RenderDetail(DistroDefinition unit) private void RenderUnitDetail(DistroDefinition unit)
{ {
detail.Clear(); detail.Clear();
@@ -182,7 +275,6 @@ namespace KernelPanic.UI
VisualElement details = new(); VisualElement details = new();
details.AddToClassList("collection-detail-values"); details.AddToClassList("collection-detail-values");
details.Add(BuildDetailLine("lang", DistroPresentation.FormatLanguages(unit))); details.Add(BuildDetailLine("lang", DistroPresentation.FormatLanguages(unit)));
details.Add(BuildDetailLine("uptime", unit.BaseUptime.ToString())); details.Add(BuildDetailLine("uptime", unit.BaseUptime.ToString()));
details.Add(BuildDetailLine("ram", unit.BaseRam.ToString())); details.Add(BuildDetailLine("ram", unit.BaseRam.ToString()));
@@ -190,6 +282,118 @@ namespace KernelPanic.UI
readout.Add(details); readout.Add(details);
detail.Add(readout); detail.Add(readout);
detail.Add(BuildSubviewCommand($"> cat ~/units/{unit.Id}/cards", "exclusive packages", HasListableCards(unit), () => OpenUnitCards(unit), "no packages installed"));
}
private void RenderLanguageDetail(LanguageCatalogEntry language)
{
detail.Clear();
bool unlocked = LanguageUnlock.IsUnlocked(language.Language, playerCollection);
Label name = new(language.DisplayName);
name.AddToClassList("collection-detail-name");
detail.Add(name);
detail.Add(BuildDetailLine("track", language.ResolutionTrack.ToString()));
Label how = new(language.HowItWorks);
how.AddToClassList("collection-detail-description");
detail.Add(how);
if (!unlocked)
{
string hintText = string.IsNullOrWhiteSpace(language.UnlockHint)
? $"unlock: own a distro that speaks {language.DisplayName} ({FormatSupportingDistros(language)})"
: language.UnlockHint;
Label hint = new(hintText);
hint.AddToClassList("package-notice");
detail.Add(hint);
return;
}
detail.Add(BuildSubviewCommand($"> cat ~/lang/{GetLanguageId(language.Language)}/deck", "starter deck", true, () => OpenLanguageDeck(language), null));
}
private void OpenUnitCards(DistroDefinition unit)
{
if (!HasListableCards(unit))
{
return;
}
returnMode = CollectionMode.Units;
cards = BuildUnitCardEntries(unit);
emptyCardMessage = "no packages installed";
CurrentTitle = $"$ cat ~/units/{unit.Id}/cards";
CurrentHint = "[esc] back [arrows] navigate";
RenderCardSubview();
ViewChanged?.Invoke();
}
private void OpenLanguageDeck(LanguageCatalogEntry language)
{
returnMode = CollectionMode.Languages;
cards = BuildStarterDeckEntries(language);
emptyCardMessage = "starter deck not yet available";
CurrentTitle = $"$ cat ~/lang/{GetLanguageId(language.Language)}/deck";
CurrentHint = "[esc] back [arrows] navigate";
RenderCardSubview();
ViewChanged?.Invoke();
}
private void RenderCardSubview()
{
mode = CollectionMode.CardSubview;
rows.Clear();
list.Clear();
detail.Clear();
selectedCardIndex = Mathf.Clamp(selectedCardIndex, 0, Math.Max(0, cards.Count - 1));
if (cards.Count == 0)
{
detail.Add(new Label(emptyCardMessage) { name = "CollectionEmptyTitle" });
detail.Add(new Label("check back after deck data is installed") { name = "CollectionEmptyHint" });
return;
}
for (int i = 0; i < cards.Count; i++)
{
int index = i;
CardEntry entry = cards[i];
VisualElement row = BuildCardRow(entry);
row.RegisterCallback<PointerEnterEvent>(_ => SelectCard(index));
row.RegisterCallback<ClickEvent>(_ => SelectCard(index));
list.Add(row);
rows.Add(row);
}
SelectCard(selectedCardIndex);
}
private VisualElement BuildCardRow(CardEntry entry)
{
VisualElement row = new();
row.AddToClassList("package-row");
row.AddToClassList("collection-card-row");
VisualElement summary = new();
summary.AddToClassList("package-summary");
Label count = new(entry.Count > 1 ? $"x{entry.Count}" : string.Empty);
count.AddToClassList("package-marker");
Label name = new(GetCardDisplayName(entry.Card));
name.AddToClassList("package-name");
Label meta = new(FormatCardMeta(entry.Card));
meta.AddToClassList("package-meta");
summary.Add(count);
summary.Add(name);
summary.Add(meta);
row.Add(summary);
Label description = new(entry.Card == null || string.IsNullOrWhiteSpace(entry.Card.Description) ? "--" : entry.Card.Description);
description.AddToClassList("package-description");
row.Add(description);
return row;
} }
private void RenderCardDetail(CardEntry entry) private void RenderCardDetail(CardEntry entry)
@@ -204,6 +408,7 @@ namespace KernelPanic.UI
Label description = new(card == null || string.IsNullOrWhiteSpace(card.Description) ? "--" : card.Description); Label description = new(card == null || string.IsNullOrWhiteSpace(card.Description) ? "--" : card.Description);
description.AddToClassList("collection-detail-description"); description.AddToClassList("collection-detail-description");
detail.Add(description); detail.Add(description);
if (card != null && !string.IsNullOrWhiteSpace(card.FlavorText)) if (card != null && !string.IsNullOrWhiteSpace(card.FlavorText))
{ {
Label flavor = new(card.FlavorText); Label flavor = new(card.FlavorText);
@@ -211,11 +416,10 @@ namespace KernelPanic.UI
detail.Add(flavor); detail.Add(flavor);
} }
detail.Add(BuildDetailLine("owner", DistroPresentation.DisplayName(entry.Owner)));
detail.Add(BuildDetailLine("lang", card == null ? "--" : card.Language.ToString())); detail.Add(BuildDetailLine("lang", card == null ? "--" : card.Language.ToString()));
detail.Add(BuildDetailLine("rarity", card == null ? "--" : card.Rarity.ToString())); detail.Add(BuildDetailLine("rarity", card == null ? "--" : card.Rarity.ToString()));
detail.Add(BuildDetailLine("cost", card == null ? "--" : card.CycleCost.ToString())); detail.Add(BuildDetailLine("cost", card == null ? "--" : card.CycleCost.ToString()));
detail.Add(BuildDetailLine("type", card != null && card.IsToken ? "token" : "loadout")); detail.Add(BuildDetailLine("track", card == null ? "--" : card.ResolutionTrack.ToString()));
} }
private static VisualElement BuildDetailLine(string key, string value) private static VisualElement BuildDetailLine(string key, string value)
@@ -233,6 +437,32 @@ namespace KernelPanic.UI
return row; return row;
} }
private VisualElement BuildSubviewCommand(string command, string description, bool enabled, Action action, string disabledDescription)
{
VisualElement row = new();
row.AddToClassList("command-row");
row.AddToClassList("collection-command-row");
row.EnableInClassList("disabled-row", !enabled);
Label cursor = new(">");
cursor.AddToClassList("command-cursor");
Label commandLabel = new(command);
commandLabel.AddToClassList("command-text");
Label descriptionLabel = new(enabled ? description : disabledDescription);
descriptionLabel.AddToClassList("command-description");
row.Add(cursor);
row.Add(commandLabel);
row.Add(descriptionLabel);
if (enabled)
{
row.RegisterCallback<ClickEvent>(_ => action?.Invoke());
}
return row;
}
private static void AddPassiveDetails(VisualElement target, DistroDefinition unit) private static void AddPassiveDetails(VisualElement target, DistroDefinition unit)
{ {
if (unit.Passive == null) if (unit.Passive == null)
@@ -256,28 +486,105 @@ namespace KernelPanic.UI
} }
} }
private static IReadOnlyList<CardEntry> BuildCardEntries(DistroDefinition featuredUnit) private static IReadOnlyList<CardEntry> BuildUnitCardEntries(DistroDefinition unit)
{ {
List<CardEntry> entries = new(); List<CardEntry> entries = new();
if (featuredUnit == null) if (unit == null)
{ {
return entries; return entries;
} }
for (int cardIndex = 0; cardIndex < featuredUnit.ExclusiveCards.Count; cardIndex++) for (int cardIndex = 0; cardIndex < unit.ExclusiveCards.Count; cardIndex++)
{ {
CardDefinition card = featuredUnit.ExclusiveCards[cardIndex]; CardDefinition card = unit.ExclusiveCards[cardIndex];
if (card == null || card.IsToken)
{
continue;
}
entries.Add(new CardEntry(unit, card, 1));
}
return entries;
}
private IReadOnlyList<CardEntry> BuildStarterDeckEntries(LanguageCatalogEntry language)
{
LanguageDeckDefinition deck = languageDeckDatabase == null ? null : languageDeckDatabase.FindByLanguage(language.Language);
if (deck == null)
{
return BuildRegisteredStarterDeckEntries(language.Language);
}
List<CardEntry> entries = new();
for (int i = 0; i < deck.Entries.Count; i++)
{
LanguageDeckDefinition.LanguageDeckEntry deckEntry = deck.Entries[i];
if (deckEntry.Card == null || deckEntry.Count <= 0)
{
continue;
}
entries.Add(new CardEntry(null, deckEntry.Card, deckEntry.Count));
}
return entries;
}
private IReadOnlyList<CardEntry> BuildRegisteredStarterDeckEntries(Language language)
{
if (cardDatabase == null)
{
return Array.Empty<CardEntry>();
}
return language switch
{
Language.Python => BuildRegisteredStarterDeckEntries(
("lang_py_print", 2),
("lang_py_import_antigravity", 1),
("lang_py_for_loop", 1)),
Language.JavaScript => BuildRegisteredStarterDeckEntries(
("lang_js_console_log", 2),
("lang_js_fetch", 1),
("lang_js_typeof", 1)),
_ => Array.Empty<CardEntry>()
};
}
private IReadOnlyList<CardEntry> BuildRegisteredStarterDeckEntries(params (string Id, int Count)[] cardRefs)
{
List<CardEntry> entries = new();
for (int i = 0; i < cardRefs.Length; i++)
{
CardDefinition card = cardDatabase.FindById(cardRefs[i].Id);
if (card == null) if (card == null)
{ {
continue; continue;
} }
entries.Add(new CardEntry(featuredUnit, card)); entries.Add(new CardEntry(null, card, cardRefs[i].Count));
} }
return entries; return entries;
} }
private static bool HasListableCards(DistroDefinition unit)
{
return BuildUnitCardEntries(unit).Count > 0;
}
private string FormatSupportingDistros(LanguageCatalogEntry language)
{
List<string> names = new();
for (int i = 0; i < language.SupportingDistros.Count; i++)
{
names.Add(language.SupportingDistros[i].DisplayName);
}
return string.Join(", ", names);
}
private static string GetCardDisplayName(CardDefinition card) private static string GetCardDisplayName(CardDefinition card)
{ {
if (card == null) if (card == null)
@@ -293,16 +600,36 @@ namespace KernelPanic.UI
return card == null ? "--" : $"{card.Language} / {card.CycleCost}c"; return card == null ? "--" : $"{card.Language} / {card.CycleCost}c";
} }
private static string GetLanguageId(Language language)
{
return language switch
{
Language.CPlusPlus => "cpp",
Language.JavaScript => "javascript",
Language.TypeScript => "typescript",
_ => language.ToString().ToLowerInvariant()
};
}
private enum CollectionMode
{
Units,
Languages,
CardSubview
}
private readonly struct CardEntry private readonly struct CardEntry
{ {
public CardEntry(DistroDefinition owner, CardDefinition card) public CardEntry(DistroDefinition owner, CardDefinition card, int count)
{ {
Owner = owner; Owner = owner;
Card = card; Card = card;
Count = count;
} }
public DistroDefinition Owner { get; } public DistroDefinition Owner { get; }
public CardDefinition Card { get; } public CardDefinition Card { get; }
public int Count { get; }
} }
} }
} }
+221 -28
View File
@@ -38,6 +38,7 @@ namespace KernelPanic.UI
[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 CardDatabase cardDatabase; [SerializeField] private CardDatabase cardDatabase;
[SerializeField] private LanguageDeckDatabase languageDeckDatabase;
[SerializeField] private FeaturedUnitPanel featuredUnitPanel = new(); [SerializeField] private FeaturedUnitPanel featuredUnitPanel = new();
[SerializeField] private CollectionScreenController collectionScreen = new(); [SerializeField] private CollectionScreenController collectionScreen = new();
[SerializeField] private StarterSelectionController starterSelection = new(); [SerializeField] private StarterSelectionController starterSelection = new();
@@ -45,6 +46,7 @@ namespace KernelPanic.UI
private readonly List<CommandMenuEntry> commandEntries = new(); private readonly List<CommandMenuEntry> commandEntries = new();
private readonly List<VisualElement> packageRows = new(); private readonly List<VisualElement> packageRows = new();
private readonly List<VisualElement> loadoutRows = new(); private readonly List<VisualElement> loadoutRows = new();
private readonly List<Language> selectedRunLanguages = new();
private UIDocument document; private UIDocument document;
private VisualElement root; private VisualElement root;
private VisualElement shellRoot; private VisualElement shellRoot;
@@ -58,6 +60,7 @@ namespace KernelPanic.UI
private VisualElement backgroundLogLayer; private VisualElement backgroundLogLayer;
private ScrollView runSetupList; private ScrollView runSetupList;
private VisualElement runSetupDetail; private VisualElement runSetupDetail;
private ScrollView runSetupPackageScroll;
private Label appIdLabel; private Label appIdLabel;
private Label entropyLabel; private Label entropyLabel;
private Label pullTokensLabel; private Label pullTokensLabel;
@@ -67,7 +70,7 @@ namespace KernelPanic.UI
private Label motdBodyLabel; private Label motdBodyLabel;
private VisualElement motdBlock; private VisualElement motdBlock;
private Button collectionUnitsButton; private Button collectionUnitsButton;
private Button collectionCardsButton; private Button collectionLanguagesButton;
private readonly ScreenFrameController runSetupFrame = new(); private readonly ScreenFrameController runSetupFrame = new();
private readonly ScreenFrameController collectionFrame = new(); private readonly ScreenFrameController collectionFrame = new();
private readonly ScreenFrameController gachaFrame = new(); private readonly ScreenFrameController gachaFrame = new();
@@ -83,7 +86,7 @@ namespace KernelPanic.UI
private int selectedCommandIndex; private int selectedCommandIndex;
private int selectedPackageIndex; private int selectedPackageIndex;
private string runSetupNotice; private string runSetupNotice;
private bool collectionShowingCards; private bool collectionShowingLanguages;
private float bootIntroElapsed; private float bootIntroElapsed;
private int bootIntroCharacterCount; private int bootIntroCharacterCount;
private bool cursorVisible; private bool cursorVisible;
@@ -111,7 +114,7 @@ namespace KernelPanic.UI
BindCommandEntries(); BindCommandEntries();
RegisterCommandEntryCallbacks(); RegisterCommandEntryCallbacks();
featuredUnitPanel.Bind(root, monospaceFont); featuredUnitPanel.Bind(root, monospaceFont);
collectionScreen.Bind(root, monospaceFont); collectionScreen.Bind(root, monospaceFont, languageDeckDatabase, cardDatabase, playerCollection);
BindScreenFrames(); BindScreenFrames();
starterSelection.Bind(root, distroDatabase, HandleStarterConfirmed); starterSelection.Bind(root, distroDatabase, HandleStarterConfirmed);
LoadMetaState(); LoadMetaState();
@@ -171,13 +174,13 @@ namespace KernelPanic.UI
motdBodyLabel = root.Q<Label>("MotdBodyLabel"); motdBodyLabel = root.Q<Label>("MotdBodyLabel");
collectionUnitsButton = root.Q<Button>("CollectionUnitsButton"); collectionUnitsButton = root.Q<Button>("CollectionUnitsButton");
collectionCardsButton = root.Q<Button>("CollectionCardsButton"); collectionLanguagesButton = root.Q<Button>("CollectionLanguagesButton");
} }
private void BindScreenFrames() private void BindScreenFrames()
{ {
runSetupFrame.Bind(runSetupPanel, "$ ./start_run --configure", "[esc] back [arrows] navigate [enter] select [b] boot run", ShowMainMenu); runSetupFrame.Bind(runSetupPanel, "$ ./start_run --configure", "[esc] back [arrows] navigate [enter] select [b] boot run", ShowMainMenu);
collectionFrame.Bind(collectionPanel, "$ ls ~/collection", "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select", ShowMainMenu); collectionFrame.Bind(collectionPanel, "$ ls ~/collection", "[esc] back [left/right] tabs [tab] tabs [arrows] navigate [enter] select", HandleCollectionBack);
gachaFrame.Bind(gachaPanel, "$ curl gacha.sh | sh", "[esc] back", ShowMainMenu); gachaFrame.Bind(gachaPanel, "$ curl gacha.sh | sh", "[esc] back", ShowMainMenu);
settingsFrame.Bind(settingsPanel, "$ dpkg-reconfigure kernel-panic", "[esc] back", ShowMainMenu); settingsFrame.Bind(settingsPanel, "$ dpkg-reconfigure kernel-panic", "[esc] back", ShowMainMenu);
} }
@@ -206,16 +209,18 @@ namespace KernelPanic.UI
{ {
root.RegisterCallback<KeyDownEvent>(HandleKeyDown); root.RegisterCallback<KeyDownEvent>(HandleKeyDown);
root.RegisterCallback<PointerDownEvent>(HandlePointerDown); root.RegisterCallback<PointerDownEvent>(HandlePointerDown);
collectionScreen.ViewChanged += SyncCollectionFrame;
collectionUnitsButton.RegisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked); collectionUnitsButton.RegisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
collectionCardsButton.RegisterCallback<ClickEvent>(HandleCollectionCardsTabClicked); collectionLanguagesButton.RegisterCallback<ClickEvent>(HandleCollectionLanguagesTabClicked);
} }
private void UnregisterCallbacks() private void UnregisterCallbacks()
{ {
root.UnregisterCallback<KeyDownEvent>(HandleKeyDown); root.UnregisterCallback<KeyDownEvent>(HandleKeyDown);
root.UnregisterCallback<PointerDownEvent>(HandlePointerDown); root.UnregisterCallback<PointerDownEvent>(HandlePointerDown);
collectionScreen.ViewChanged -= SyncCollectionFrame;
collectionUnitsButton.UnregisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked); collectionUnitsButton.UnregisterCallback<ClickEvent>(HandleCollectionUnitsTabClicked);
collectionCardsButton.UnregisterCallback<ClickEvent>(HandleCollectionCardsTabClicked); collectionLanguagesButton.UnregisterCallback<ClickEvent>(HandleCollectionLanguagesTabClicked);
} }
private void HandleCollectionUnitsTabClicked(ClickEvent evt) private void HandleCollectionUnitsTabClicked(ClickEvent evt)
@@ -224,9 +229,9 @@ namespace KernelPanic.UI
evt.StopPropagation(); evt.StopPropagation();
} }
private void HandleCollectionCardsTabClicked(ClickEvent evt) private void HandleCollectionLanguagesTabClicked(ClickEvent evt)
{ {
ShowCollectionCards(); ShowCollectionLanguages();
evt.StopPropagation(); evt.StopPropagation();
} }
@@ -468,6 +473,13 @@ namespace KernelPanic.UI
return; return;
} }
if (evt.keyCode == KeyCode.Escape && IsCollectionVisible() && collectionScreen.BackFromSubview())
{
SyncCollectionFrame();
evt.StopPropagation();
return;
}
if (evt.keyCode == KeyCode.Escape && IsSubScreenVisible()) if (evt.keyCode == KeyCode.Escape && IsSubScreenVisible())
{ {
ShowMainMenu(); ShowMainMenu();
@@ -501,12 +513,35 @@ namespace KernelPanic.UI
if (IsCollectionVisible()) if (IsCollectionVisible())
{ {
if (evt.keyCode == KeyCode.LeftArrow || evt.keyCode == KeyCode.RightArrow || evt.keyCode == KeyCode.Tab) if (!collectionScreen.IsCardSubview &&
(evt.keyCode == KeyCode.LeftArrow || evt.keyCode == KeyCode.RightArrow || evt.keyCode == KeyCode.Tab))
{ {
ToggleCollectionTab(); ToggleCollectionTab();
evt.StopPropagation(); evt.StopPropagation();
return; return;
} }
if (evt.keyCode == KeyCode.UpArrow)
{
collectionScreen.SelectRelative(-1);
evt.StopPropagation();
return;
}
if (evt.keyCode == KeyCode.DownArrow)
{
collectionScreen.SelectRelative(1);
evt.StopPropagation();
return;
}
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
collectionScreen.ActivateSelected();
SyncCollectionFrame();
evt.StopPropagation();
return;
}
} }
if (evt.keyCode == KeyCode.UpArrow) if (evt.keyCode == KeyCode.UpArrow)
@@ -568,6 +603,7 @@ namespace KernelPanic.UI
{ {
packageRows.Clear(); packageRows.Clear();
loadoutRows.Clear(); loadoutRows.Clear();
selectedRunLanguages.Clear();
cardLoadout.ClearAll(); cardLoadout.ClearAll();
runSetupList?.Clear(); runSetupList?.Clear();
runSetupDetail?.Clear(); runSetupDetail?.Clear();
@@ -634,6 +670,8 @@ namespace KernelPanic.UI
selectedPackageIndex = (index + packageRows.Count) % packageRows.Count; selectedPackageIndex = (index + packageRows.Count) % packageRows.Count;
cardLoadout.ClearLoadout(playerCollection.OwnedUnits[selectedPackageIndex]); cardLoadout.ClearLoadout(playerCollection.OwnedUnits[selectedPackageIndex]);
selectedRunLanguages.Clear();
runSetupPackageScroll = null;
runSetupNotice = null; runSetupNotice = null;
RefreshPackageSelection(); RefreshPackageSelection();
} }
@@ -650,6 +688,7 @@ namespace KernelPanic.UI
private void RenderSelectedPackageDetail() private void RenderSelectedPackageDetail()
{ {
Vector2 previousScrollOffset = runSetupPackageScroll == null ? Vector2.zero : runSetupPackageScroll.scrollOffset;
runSetupDetail?.Clear(); runSetupDetail?.Clear();
loadoutRows.Clear(); loadoutRows.Clear();
if (runSetupDetail == null || selectedPackageIndex < 0 || selectedPackageIndex >= playerCollection.OwnedUnits.Count) if (runSetupDetail == null || selectedPackageIndex < 0 || selectedPackageIndex >= playerCollection.OwnedUnits.Count)
@@ -704,9 +743,31 @@ namespace KernelPanic.UI
packageList.ElementAt(packageList.childCount - 1).AddToClassList("package-empty"); packageList.ElementAt(packageList.childCount - 1).AddToClassList("package-empty");
} }
if (equippedCardIds.Count >= CardLoadout.MaxEquippedCards)
{
AddRunLanguageSelection(packageList);
}
else
{
selectedRunLanguages.Clear();
}
packageScroll.Add(packageList); packageScroll.Add(packageList);
runSetupDetail.Add(packageScroll); runSetupDetail.Add(packageScroll);
runSetupDetail.Add(BuildBootRow(unit, equippedCardIds.Count > 0)); runSetupDetail.Add(BuildBootRow(unit, CanBootRun(equippedCardIds)));
runSetupPackageScroll = packageScroll;
RestoreRunSetupPackageScroll(previousScrollOffset);
}
private void RestoreRunSetupPackageScroll(Vector2 offset)
{
if (runSetupPackageScroll == null)
{
return;
}
runSetupPackageScroll.scrollOffset = offset;
runSetupPackageScroll.schedule.Execute(() => runSetupPackageScroll.scrollOffset = offset).StartingIn(0);
} }
private VisualElement BuildRunSetupReadout(DistroDefinition unit) private VisualElement BuildRunSetupReadout(DistroDefinition unit)
@@ -815,6 +876,80 @@ namespace KernelPanic.UI
return row; return row;
} }
private void AddRunLanguageSelection(VisualElement packageList)
{
IReadOnlyList<LanguageCatalogEntry> availableLanguages = GetAvailableRunLanguages();
packageList.Add(new Label("programming languages") { name = "RunSetupLanguagesHeader" });
packageList.ElementAt(packageList.childCount - 1).AddToClassList("package-header");
packageList.ElementAt(packageList.childCount - 1).AddToClassList("run-language-header");
if (selectedRunLanguages.Count < 2)
{
packageList.Add(new Label($"select {2 - selectedRunLanguages.Count} more language(s) before booting") { name = "RunSetupLanguagesNotice" });
packageList.ElementAt(packageList.childCount - 1).AddToClassList("package-notice");
}
if (availableLanguages.Count == 0)
{
packageList.Add(new Label("no programming languages unlocked") { name = "RunSetupLanguagesEmpty" });
packageList.ElementAt(packageList.childCount - 1).AddToClassList("package-empty");
return;
}
for (int i = 0; i < availableLanguages.Count; i++)
{
LanguageCatalogEntry language = availableLanguages[i];
packageList.Add(BuildRunLanguageRow(language, IsRunLanguageSelected(language.Language)));
}
}
private VisualElement BuildRunLanguageRow(LanguageCatalogEntry language, bool selected)
{
VisualElement row = new();
row.AddToClassList("package-row");
row.AddToClassList("run-language-row");
row.EnableInClassList("equipped", selected);
row.RegisterCallback<ClickEvent>(_ => ToggleRunLanguage(language.Language));
VisualElement summary = new();
summary.AddToClassList("package-summary");
summary.Add(new Label(selected ? "[x]" : "[ ]") { name = "RunSetupLanguageMarker" });
summary.ElementAt(0).AddToClassList("package-marker");
summary.Add(new Label(language.DisplayName) { name = "RunSetupLanguageName" });
summary.ElementAt(1).AddToClassList("package-name");
summary.Add(new Label(language.ResolutionTrack.ToString()) { name = "RunSetupLanguageTrack" });
summary.ElementAt(2).AddToClassList("package-meta");
row.Add(summary);
row.Add(new Label(language.IdentityTag) { name = "RunSetupLanguageDescription" });
row.ElementAt(1).AddToClassList("package-description");
return row;
}
private void ToggleRunLanguage(Language language)
{
if (IsRunLanguageSelected(language))
{
selectedRunLanguages.Remove(language);
runSetupNotice = null;
RenderSelectedPackageDetail();
return;
}
if (selectedRunLanguages.Count >= 2)
{
runSetupNotice = "language limit reached";
RenderSelectedPackageDetail();
return;
}
selectedRunLanguages.Add(language);
runSetupNotice = null;
RenderSelectedPackageDetail();
}
private void ToggleCardInSelectedLoadout(DistroDefinition unit, CardDefinition card) private void ToggleCardInSelectedLoadout(DistroDefinition unit, CardDefinition card)
{ {
runSetupNotice = TryToggleLoadoutCard(unit, card); runSetupNotice = TryToggleLoadoutCard(unit, card);
@@ -851,6 +986,39 @@ namespace KernelPanic.UI
return false; return false;
} }
private IReadOnlyList<LanguageCatalogEntry> GetAvailableRunLanguages()
{
List<LanguageCatalogEntry> available = new();
IReadOnlyList<LanguageCatalogEntry> languages = LanguageCatalog.All;
for (int i = 0; i < languages.Count; i++)
{
if (LanguageUnlock.IsUnlocked(languages[i].Language, playerCollection))
{
available.Add(languages[i]);
}
}
return available;
}
private bool IsRunLanguageSelected(Language language)
{
for (int i = 0; i < selectedRunLanguages.Count; i++)
{
if (selectedRunLanguages[i] == language)
{
return true;
}
}
return false;
}
private bool CanBootRun(IReadOnlyList<string> equippedCardIds)
{
return equippedCardIds.Count >= CardLoadout.MaxEquippedCards && selectedRunLanguages.Count == 2;
}
private static string FormatLoadoutFailure(CardLoadoutFailureReason reason) private static string FormatLoadoutFailure(CardLoadoutFailureReason reason)
{ {
return reason switch return reason switch
@@ -890,9 +1058,18 @@ namespace KernelPanic.UI
DistroDefinition unit = playerCollection.OwnedUnits[selectedPackageIndex]; DistroDefinition unit = playerCollection.OwnedUnits[selectedPackageIndex];
IReadOnlyList<string> equippedCardIds = cardLoadout.GetEquippedCardIds(unit.Id); IReadOnlyList<string> equippedCardIds = cardLoadout.GetEquippedCardIds(unit.Id);
if (equippedCardIds.Count < 1) if (equippedCardIds.Count < CardLoadout.MaxEquippedCards)
{ {
runSetupNotice = "boot: no packages staged"; runSetupNotice = equippedCardIds.Count == 0
? "boot: no packages staged"
: $"boot: stage {CardLoadout.MaxEquippedCards} packages";
RenderSelectedPackageDetail();
return;
}
if (selectedRunLanguages.Count < 2)
{
runSetupNotice = "boot: select 2 programming languages";
RenderSelectedPackageDetail(); RenderSelectedPackageDetail();
return; return;
} }
@@ -950,6 +1127,7 @@ namespace KernelPanic.UI
private void ShowMainMenu() private void ShowMainMenu()
{ {
cardLoadout.ClearAll(); cardLoadout.ClearAll();
selectedRunLanguages.Clear();
runSetupNotice = null; runSetupNotice = null;
ShowPanel(mainMenuPanel); ShowPanel(mainMenuPanel);
root.Focus(); root.Focus();
@@ -961,35 +1139,50 @@ namespace KernelPanic.UI
ShowPanel(collectionPanel); ShowPanel(collectionPanel);
} }
private void ShowCollectionUnits() private void HandleCollectionBack()
{ {
collectionShowingCards = false; if (collectionScreen.BackFromSubview())
collectionUnitsButton.EnableInClassList(SelectedClassName, true); {
collectionCardsButton.EnableInClassList(SelectedClassName, false); SyncCollectionFrame();
collectionFrame.SetTitle("$ ls ~/collection"); return;
collectionScreen.RefreshUnits(playerCollection.OwnedUnits); }
ShowMainMenu();
} }
private void ShowCollectionCards() private void ShowCollectionUnits()
{ {
collectionShowingCards = true; collectionShowingLanguages = false;
collectionUnitsButton.EnableInClassList(SelectedClassName, true);
collectionLanguagesButton.EnableInClassList(SelectedClassName, false);
collectionScreen.RefreshUnits(playerCollection.OwnedUnits);
SyncCollectionFrame();
}
private void ShowCollectionLanguages()
{
collectionShowingLanguages = true;
collectionUnitsButton.EnableInClassList(SelectedClassName, false); collectionUnitsButton.EnableInClassList(SelectedClassName, false);
collectionCardsButton.EnableInClassList(SelectedClassName, true); collectionLanguagesButton.EnableInClassList(SelectedClassName, true);
DistroDefinition featuredUnit = playerCollection.FeaturedUnit; collectionScreen.ShowLanguages();
string distroId = featuredUnit == null ? "--" : featuredUnit.Id; SyncCollectionFrame();
collectionFrame.SetTitle($"$ dpkg -l --distro={distroId}");
collectionScreen.RefreshCards(playerCollection.FeaturedUnit);
} }
private void ToggleCollectionTab() private void ToggleCollectionTab()
{ {
if (collectionShowingCards) if (collectionShowingLanguages)
{ {
ShowCollectionUnits(); ShowCollectionUnits();
return; return;
} }
ShowCollectionCards(); ShowCollectionLanguages();
}
private void SyncCollectionFrame()
{
collectionFrame.SetTitle(collectionScreen.CurrentTitle);
collectionFrame.SetHint(collectionScreen.CurrentHint);
} }
private void ShowGacha() private void ShowGacha()
+1 -1
View File
@@ -7,7 +7,7 @@
<ui:VisualElement class="screen-frame-content"> <ui:VisualElement class="screen-frame-content">
<ui:VisualElement class="collection-actions"> <ui:VisualElement class="collection-actions">
<ui:Button name="CollectionUnitsButton" text="units" focusable="false" class="collection-tab" /> <ui:Button name="CollectionUnitsButton" text="units" focusable="false" class="collection-tab" />
<ui:Button name="CollectionCardsButton" text="cards" focusable="false" class="collection-tab" /> <ui:Button name="CollectionLanguagesButton" text="languages" focusable="false" class="collection-tab" />
</ui:VisualElement> </ui:VisualElement>
<ui:VisualElement name="CollectionContent" class="collection-shell"> <ui:VisualElement name="CollectionContent" class="collection-shell">
<ui:ScrollView name="CollectionList" class="collection-list" /> <ui:ScrollView name="CollectionList" class="collection-list" />
+23 -1
View File
@@ -528,9 +528,11 @@ Button {
} }
.collection-tab { .collection-tab {
width: 92px; min-width: 92px;
height: 30px; height: 30px;
margin-right: 10px; margin-right: 10px;
padding-left: 10px;
padding-right: 10px;
border-bottom-width: 2px; border-bottom-width: 2px;
border-bottom-color: transparent; border-bottom-color: transparent;
background-color: transparent; background-color: transparent;
@@ -612,6 +614,10 @@ Button {
border-left-color: var(--kp-primary); border-left-color: var(--kp-primary);
} }
.collection-row.locked {
opacity: 0.55;
}
.collection-row-name { .collection-row-name {
font-size: 16px; font-size: 16px;
-unity-font-style: bold; -unity-font-style: bold;
@@ -635,6 +641,18 @@ Button {
margin-bottom: 12px; margin-bottom: 12px;
} }
.collection-command-row {
margin-top: 18px;
}
.collection-command-row .command-description {
width: 190px;
}
.collection-card-row {
margin-top: 6px;
}
.detail-section-title { .detail-section-title {
color: var(--kp-primary); color: var(--kp-primary);
-unity-font-style: bold; -unity-font-style: bold;
@@ -667,6 +685,10 @@ Button {
margin-bottom: 6px; margin-bottom: 6px;
} }
.run-language-header {
margin-top: 14px;
}
.package-notice { .package-notice {
color: var(--kp-amber); color: var(--kp-amber);
margin-bottom: 8px; margin-bottom: 8px;