using System;
using System.Collections.Generic;
using KernelPanic.Core;
using KernelPanic.Data;
using KernelPanic.Meta;
using KernelPanic.Run;
using UnityEngine;
using UnityEngine.TextCore.Text;
using UnityEngine.UIElements;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace KernelPanic.UI
{
internal static class TerminalFontResolver
{
private static readonly string[] PreferredMonospaceFontFamilies =
{
"Consolas",
"Cascadia Mono",
"Menlo",
"Monaco",
"DejaVu Sans Mono",
"Liberation Mono",
"Ubuntu Mono",
"Courier New"
};
private static FontAsset runtimeMonospaceFont;
private static bool warnedMissingFont;
public static FontAsset Resolve(FontAsset configuredFont)
{
if (configuredFont != null)
{
return configuredFont;
}
if (runtimeMonospaceFont != null)
{
return runtimeMonospaceFont;
}
Font dynamicFont = Font.CreateDynamicFontFromOSFont(PreferredMonospaceFontFamilies, 16);
if (dynamicFont == null)
{
if (!warnedMissingFont)
{
Debug.LogWarning("No monospace OS font found; terminal UI will use Unity's default font.");
warnedMissingFont = true;
}
return null;
}
runtimeMonospaceFont = FontAsset.CreateFontAsset(dynamicFont);
return runtimeMonospaceFont;
}
}
///
/// Binds the main menu terminal UI document and routes command activation between panels.
///
[RequireComponent(typeof(UIDocument))]
public sealed class MainMenuController : MonoBehaviour
{
private const string HiddenClassName = "hidden";
private const string SelectedClassName = "selected";
private const string CursorOnClassName = "cursor-on";
private const string SharedScrollbarStyleResourcePath = "TerminalScrollbars";
private const float BootIntroSeconds = 1.5f;
private static bool _bootIntroPlayed;
// Static fields don't reset between Play sessions when the Editor's "Reload Domain"
// option is disabled. RuntimeInitializeOnLoadMethod runs once per player/Play-mode
// startup regardless of that setting, so this keeps bootIntroPlayed session-scoped.
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ResetSessionState()
{
_bootIntroPlayed = false;
}
[SerializeField] private FontAsset monospaceFont;
[SerializeField] private string motdBody = "unstable userspace detected; keep a rollback shell open.";
[SerializeField] private DistroDatabase distroDatabase;
[SerializeField] private CardDatabase cardDatabase;
[SerializeField] private LanguageDeckDatabase languageDeckDatabase;
[SerializeField] private FeaturedUnitPanel featuredUnitPanel = new();
[SerializeField] private CollectionScreenController collectionScreen = new();
[SerializeField] private StarterSelectionController starterSelection = new();
[SerializeField] private GachaScreenController gachaScreen = new();
private readonly List _commandEntries = new();
private readonly List _packageRows = new();
private readonly List _loadoutRows = new();
private readonly List _selectedRunLanguages = new();
private UIDocument _document;
private VisualElement _root;
private VisualElement _shellRoot;
private VisualElement _bootIntroPanel;
private VisualElement _mainMenuPanel;
private VisualElement _collectionPanel;
private VisualElement _runSetupPanel;
private VisualElement _gachaPanel;
private VisualElement _settingsPanel;
private VisualElement _eventBanner;
private VisualElement _backgroundLogLayer;
private ScrollView _runSetupList;
private VisualElement _runSetupDetail;
private ScrollView _runSetupPackageScroll;
private Label _appIdLabel;
private Label _entropyLabel;
private Label _pullTokensLabel;
private Label _titleCursorLabel;
private Label _promptCursorLabel;
private Label _bootIntroLogLabel;
private Label _motdBodyLabel;
private VisualElement _motdBlock;
private Button _rootCreditsToEntropyButton;
private VisualElement _rootCreditExchangeModal;
private Label _rootCreditExchangeBalanceLabel;
private Label _rootCreditExchangePreviewLabel;
private Label _rootCreditExchangeMessageLabel;
private TextField _rootCreditExchangeInput;
private Button _rootCreditExchangeMinusHundredButton;
private Button _rootCreditExchangeMinusOneButton;
private Button _rootCreditExchangeNoneButton;
private Button _rootCreditExchangePlusOneButton;
private Button _rootCreditExchangePlusHundredButton;
private Button _rootCreditExchangeMaxButton;
private Button _rootCreditExchangeConfirmButton;
private Button _rootCreditExchangeCancelButton;
private Button _rootCreditExchangeCloseButton;
private Button _collectionUnitsButton;
private Button _collectionLanguagesButton;
private readonly ScreenFrameController _runSetupFrame = new();
private readonly ScreenFrameController _collectionFrame = new();
private readonly ScreenFrameController _gachaFrame = new();
private readonly ScreenFrameController _settingsFrame = new();
private SaveService _saveService;
private SaveData _saveData;
private EntropyWallet _wallet;
private GachaService _gachaService;
private PlayerCollection _playerCollection;
private CardLoadout _cardLoadout;
private BackgroundLogRingBuffer _backgroundLog;
private IEventBannerSource _eventBannerSource;
private int _selectedCommandIndex;
private int _selectedPackageIndex;
private string _runSetupNotice;
private bool _collectionShowingLanguages;
private float _bootIntroElapsed;
private int _bootIntroCharacterCount;
private bool _cursorVisible;
private bool _suppressNextClick;
private bool _warnedUnresolvedSaveId;
private bool _rootCreditExchangeOpen;
private string _bootIntroCopy;
private Action _rootCreditExchangeMinusHundredClicked;
private Action _rootCreditExchangeMinusOneClicked;
private Action _rootCreditExchangePlusOneClicked;
private Action _rootCreditExchangePlusHundredClicked;
private IVisualElementScheduledItem _blinkSchedule;
private IVisualElementScheduledItem _bootIntroSchedule;
public void Initialize(EntropyWallet initializedWallet)
{
_wallet = initializedWallet ?? new EntropyWallet();
RefreshCurrencyReadouts();
}
private void Awake()
{
_document = GetComponent();
_saveService = new SaveService();
_gachaService = new GachaService();
_playerCollection = new PlayerCollection(); // TODO: Replace with persistent player-collection service composition.
_cardLoadout = new CardLoadout(_playerCollection.OwnedUnits);
Initialize(new EntropyWallet()); // TODO: Replace with persistent wallet service composition.
BindElements();
LoadSharedStyles();
BindCommandEntries();
RegisterCommandEntryCallbacks();
BindScreenFrames();
starterSelection.Bind(_root, distroDatabase, HandleStarterConfirmed);
FontAsset resolvedFont = ResolveMonospaceFont();
featuredUnitPanel.Bind(_root, resolvedFont);
collectionScreen.Bind(_root, resolvedFont, languageDeckDatabase, cardDatabase, _playerCollection, GetMergesBalance, UpgradeCollectionUnit);
gachaScreen.Bind(_root, distroDatabase, resolvedFont, _gachaService, _playerCollection, _wallet, ResolvePulledDistros, HandleMetaStateChanged, OpenRootCreditExchange);
LoadMetaState();
_playerCollection.Changed += HandleMetaStateChanged;
_gachaService.Changed += HandleMetaStateChanged;
ApplyOptionalFont(resolvedFont);
}
private void OnEnable()
{
RegisterCallbacks();
_root.Focus();
_shellRoot.EnableInClassList("reduced-motion", UIPreferences.ReducedMotion);
RefreshStaticText();
RefreshCurrencyReadouts();
RefreshEventBanner();
featuredUnitPanel.Refresh(_playerCollection.OwnedUnits, _playerCollection.FeaturedUnit);
collectionScreen.RefreshUnits(_playerCollection.OwnedUnits);
gachaScreen.Refresh();
SelectCommand(0);
ShowMainMenu();
StartAmbientSchedules();
PlayBootIntroIfNeeded();
}
private void OnDisable()
{
UnregisterCallbacks();
_blinkSchedule?.Pause();
_backgroundLog?.Stop();
_bootIntroSchedule?.Pause();
starterSelection.PauseSchedules();
}
private void BindElements()
{
_root = _document.rootVisualElement;
_shellRoot = _root.Q("ShellRoot");
_bootIntroPanel = _root.Q("BootIntroPanel");
_mainMenuPanel = _root.Q("MainMenuPanel");
_collectionPanel = _root.Q("CollectionPanel");
_runSetupPanel = _root.Q("RunSetupPanel");
_gachaPanel = _root.Q("GachaPanel");
_settingsPanel = _root.Q("SettingsPanel");
_eventBanner = _root.Q("EventBanner");
_motdBlock = _root.Q("MotdBlock");
_backgroundLogLayer = _root.Q("BackgroundLogLayer");
_runSetupList = _root.Q("RunSetupList");
_runSetupDetail = _root.Q("RunSetupDetail");
_backgroundLog = new BackgroundLogRingBuffer(_backgroundLogLayer, BootLogCopy.Lines);
_appIdLabel = _root.Q