using System; using System.Collections.Generic; using KernelPanic.Core; using KernelPanic.Data; using KernelPanic.Meta; using UnityEngine; using UnityEngine.TextCore.Text; using UnityEngine.UIElements; #if UNITY_EDITOR using UnityEditor; #endif namespace KernelPanic.UI { /// /// 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 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; // TODO: Assign a real monospace FontAsset when typography assets exist. [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(); 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 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 string bootIntroCopy; 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(); BindCommandEntries(); RegisterCommandEntryCallbacks(); featuredUnitPanel.Bind(root, monospaceFont); collectionScreen.Bind(root, monospaceFont, languageDeckDatabase, cardDatabase, playerCollection); BindScreenFrames(); starterSelection.Bind(root, distroDatabase, HandleStarterConfirmed); LoadMetaState(); playerCollection.Changed += HandleMetaStateChanged; gachaService.BannerPoolChanged += HandleMetaStateChanged; ApplyOptionalFont(); } 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); 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