using System;
using System.Collections.Generic;
using KernelPanic.Core;
using KernelPanic.Data;
using KernelPanic.Meta;
using UnityEngine;
using UnityEngine.TextCore.Text;
using UnityEngine.UIElements;
namespace KernelPanic.UI
{
///
/// Renders Collection tabs and the pushed read-only card subview.
///
[Serializable]
public sealed class CollectionScreenController
{
private readonly List _rows = new();
private VisualElement _list;
private VisualElement _detail;
private FontAsset _monospaceFont;
private LanguageDeckDatabase _languageDeckDatabase;
private CardDatabase _cardDatabase;
private PlayerCollection _playerCollection;
private IReadOnlyList _units = Array.Empty();
private IReadOnlyList _languages = LanguageCatalog.All;
private IReadOnlyList _cards = Array.Empty();
private CollectionMode _mode;
private CollectionMode _returnMode;
private int _selectedUnitIndex;
private int _selectedLanguageIndex;
private int _selectedCardIndex;
private string _emptyCardMessage = "no cards installed";
public event Action ViewChanged;
public void Bind(VisualElement root, FontAsset artFont, LanguageDeckDatabase deckDatabase, CardDatabase cardsDatabase, PlayerCollection collection)
{
_monospaceFont = artFont;
_languageDeckDatabase = deckDatabase;
_cardDatabase = cardsDatabase;
_playerCollection = collection;
_list = root.Q("CollectionList");
_detail = root.Q("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 ownedUnits)
{
_units = ownedUnits ?? Array.Empty();
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)
{
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;
}
_selectedUnitIndex = Mathf.Clamp(_selectedUnitIndex, 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 unitLanguages = new(DistroPresentation.FormatLanguages(unit));
unitLanguages.AddToClassList("collection-row-languages");
row.Add(name);
row.Add(unitLanguages);
_list.Add(row);
_rows.Add(row);
}
SelectUnit(_selectedUnitIndex);
}
private void RenderLanguages()
{
if (_list == null || _detail == null)
{
return;
}
_rows.Clear();
_list.Clear();
_selectedLanguageIndex = Mathf.Clamp(_selectedLanguageIndex, 0, _languages.Count - 1);
for (int i = 0; i < _languages.Count; i++)
{
int index = i;
LanguageCatalogEntry language = _languages[i];
bool unlocked = LanguageUnlock.IsUnlocked(language.Language, _playerCollection);
VisualElement row = new();
row.AddToClassList("collection-row");
row.EnableInClassList("locked", !unlocked);
row.RegisterCallback(_ => SelectLanguage(index));
row.RegisterCallback(_ => SelectLanguage(index));
Label name = new(unlocked ? language.DisplayName : $"{language.DisplayName} [locked]");
name.AddToClassList("collection-row-name");
Label tag = new(language.IdentityTag);
tag.AddToClassList("collection-row-languages");
row.Add(name);
row.Add(tag);
_list.Add(row);
_rows.Add(row);
}
SelectLanguage(_selectedLanguageIndex);
}
private void SelectUnit(int index)
{
if (_units.Count == 0)
{
return;
}
_selectedUnitIndex = Mathf.Clamp(index, 0, _units.Count - 1);
ApplySelection(_selectedUnitIndex);
RenderUnitDetail(_units[_selectedUnitIndex]);
}
private void SelectLanguage(int index)
{
_selectedLanguageIndex = Mathf.Clamp(index, 0, _languages.Count - 1);
ApplySelection(_selectedLanguageIndex);
RenderLanguageDetail(_languages[_selectedLanguageIndex]);
}
private void SelectCard(int index)
{
if (_cards.Count == 0)
{
return;
}
_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++)
{
_rows[i].EnableInClassList("selected", i == selectedIndex);
}
}
private void RenderUnitDetail(DistroDefinition unit)
{
_detail.Clear();
Label name = new(DistroPresentation.DisplayName(unit));
name.AddToClassList("collection-detail-name");
name.style.color = new StyleColor(unit.AccentColor);
_detail.Add(name);
Label description = new(string.IsNullOrWhiteSpace(unit.Description) ? "--" : unit.Description);
description.AddToClassList("collection-detail-description");
_detail.Add(description);
AddPassiveDetails(_detail, unit);
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");
details.Add(BuildDetailLine("lang", DistroPresentation.FormatLanguages(unit)));
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);
_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(_ => SelectCard(index));
row.RegisterCallback(_ => 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)
{
_detail.Clear();
CardDefinition card = entry.Card;
Label name = new(GetCardDisplayName(card));
name.AddToClassList("collection-detail-name");
_detail.Add(name);
Label description = new(card == null || string.IsNullOrWhiteSpace(card.Description) ? "--" : card.Description);
description.AddToClassList("collection-detail-description");
_detail.Add(description);
if (card != null && !string.IsNullOrWhiteSpace(card.FlavorText))
{
Label flavor = new(card.FlavorText);
flavor.AddToClassList("flavor-text");
_detail.Add(flavor);
}
_detail.Add(BuildDetailLine("lang", card == null ? "--" : card.Language.ToString()));
_detail.Add(BuildDetailLine("rarity", card == null ? "--" : card.Rarity.ToString()));
_detail.Add(BuildDetailLine("cost", card == null ? "--" : card.CycleCost.ToString()));
_detail.Add(BuildDetailLine("track", card == null ? "--" : card.ResolutionTrack.ToString()));
}
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 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(_ => action?.Invoke());
}
return row;
}
private static void AddPassiveDetails(VisualElement target, DistroDefinition unit)
{
if (unit.Passive == null)
{
return;
}
Label passiveTitle = new(unit.Passive.Name);
passiveTitle.AddToClassList("detail-section-title");
target.Add(passiveTitle);
Label passiveRules = new(unit.Passive.RulesText);
passiveRules.AddToClassList("collection-detail-description");
target.Add(passiveRules);
if (!string.IsNullOrWhiteSpace(unit.Passive.FlavorText))
{
Label passiveFlavor = new(unit.Passive.FlavorText);
passiveFlavor.AddToClassList("flavor-text");
target.Add(passiveFlavor);
}
}
private static IReadOnlyList BuildUnitCardEntries(DistroDefinition unit)
{
List entries = new();
if (unit == null)
{
return entries;
}
for (int cardIndex = 0; cardIndex < unit.ExclusiveCards.Count; cardIndex++)
{
CardDefinition card = unit.ExclusiveCards[cardIndex];
if (card == null || card.IsToken || card.IsRunOnly)
{
continue;
}
entries.Add(new CardEntry(unit, card, 1));
}
return entries;
}
private IReadOnlyList BuildStarterDeckEntries(LanguageCatalogEntry language)
{
LanguageDeckDefinition deck = _languageDeckDatabase == null ? null : _languageDeckDatabase.FindByLanguage(language.Language);
if (deck == null)
{
return BuildRegisteredStarterDeckEntries(language.Language);
}
List entries = new();
for (int i = 0; i < deck.Entries.Count; i++)
{
LanguageDeckDefinition.LanguageDeckEntry deckEntry = deck.Entries[i];
if (deckEntry.Card == null || deckEntry.Card.IsRunOnly || deckEntry.Count <= 0)
{
continue;
}
entries.Add(new CardEntry(null, deckEntry.Card, deckEntry.Count));
}
return entries;
}
private IReadOnlyList BuildRegisteredStarterDeckEntries(Language language)
{
if (_cardDatabase == null)
{
return Array.Empty();
}
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()
};
}
private IReadOnlyList BuildRegisteredStarterDeckEntries(params (string Id, int Count)[] cardRefs)
{
List entries = new();
for (int i = 0; i < cardRefs.Length; i++)
{
CardDefinition card = _cardDatabase.FindById(cardRefs[i].Id);
if (card == null || card.IsRunOnly)
{
continue;
}
entries.Add(new CardEntry(null, card, cardRefs[i].Count));
}
return entries;
}
private static bool HasListableCards(DistroDefinition unit)
{
return BuildUnitCardEntries(unit).Count > 0;
}
private string FormatSupportingDistros(LanguageCatalogEntry language)
{
List 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)
{
if (card == null)
{
return "--";
}
return string.IsNullOrWhiteSpace(card.DisplayName) ? card.Id : card.DisplayName;
}
private static string FormatCardMeta(CardDefinition card)
{
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
{
public CardEntry(DistroDefinition owner, CardDefinition card, int count)
{
Owner = owner;
Card = card;
Count = count;
}
public DistroDefinition Owner { get; }
public CardDefinition Card { get; }
public int Count { get; }
}
}
}