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 ModalOpenClassName = "modal-open"; private const int ModalTransitionMs = 160; 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 IVisualElementScheduledItem _rootCreditExchangeOpenSchedule; private IVisualElementScheduledItem _rootCreditExchangeHideSchedule; 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(); _root.EnableInClassList("reduced-motion", UIPreferences.ReducedMotion); _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