using System.Collections.Generic; using NeoModLoader.services; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.UI; namespace IdleSpectator; /// /// Bottom-right Lore panel (L): World Memory + Living + Fallen tabs. /// Sized in canvas units (same scaler as the dossier) to stay clear of top-right. /// public static class ChronicleHud { public enum LoreTab { World = 0, Living = 1, Fallen = 2 } public enum DetailPane { Life = 0, Activity = 1 } private const float PanelWidth = 268f; private const float PanelHeight = 188f; private const float ScreenInset = 12f; private const float BottomClearance = 64f; private const int MaxVisibleRows = 24; private const float RowHeightMin = 20f; private const float ChapterHeight = 15f; private const float KindIconSize = 11f; private const float TitleIconSize = 13f; private const float TitleBandH = 22f; private const float TabH = 15f; private const float CharToolsH = 17f; private const float ScrollSensitivity = 55f; private const string ChromeMarker = "LoreV6"; private static GameObject _root; private static RectTransform _rootRt; private static RectTransform _content; private static RectTransform _scrollRt; private static ScrollRect _scroll; private static Text _titleText; private static Text _hotkeyText; private static Image _titleIcon; private static readonly List Rows = new List(); private static bool _visible; private static int _lastCount = -1; private static string _lastAgeId = ""; private static LoreTab _tab = LoreTab.World; private static GameObject _tabsBar; private static GameObject _worldTabBtn; private static GameObject _livingTabBtn; private static GameObject _fallenTabBtn; private static GameObject _charTools; private static InputField _searchField; private static Button _favFilterBtn; private static Image _favFilterIcon; private static Button _refreshBtn; private static Image _refreshBg; private static Text _refreshLabel; private static bool _favoritesOnly; private static string _searchText = ""; private static int _lastCharFingerprint = int.MinValue; private static int _builtAtRevision = int.MinValue; private static long _detailUnitId; private static bool _followFocus; private static GameObject _backBtn; private static Text _backBtnLabel; private static GameObject _detailPaneBar; private static GameObject _detailActivityBtn; private static GameObject _detailLifeBtn; private static DetailPane _detailPane = DetailPane.Life; /// Rows actually built in the last character detail rebuild (harness). public static int LastDetailHistoryRows { get; private set; } /// Activity rows in the last Activity-pane rebuild (harness). public static int LastDetailActivityRows { get; private set; } /// Active detail sub-pane when a unit history is open. public static DetailPane ActiveDetailPane => _detailPane; /// Title of the first row in the last Living/Fallen list rebuild (harness). public static string LastListTopTitle { get; private set; } = ""; /// True when Living/Fallen list is older than new chronicle events (needs Refresh). public static bool CastListStale => Visible && IsCastTab && _detailUnitId == 0 && Chronicle.HistoryRevision != _builtAtRevision; public static bool Visible => _visible && _root != null && _root.activeSelf; public static bool IsReady => _root != null; public static LoreTab ActiveTab => _tab; public static bool FavoritesOnly => _favoritesOnly; public static string SearchText => _searchText ?? ""; public static long DetailUnitId => _detailUnitId; private static bool IsCastTab => _tab == LoreTab.Living || _tab == LoreTab.Fallen; /// True when Lore detail was opened via L/books and should track idle focus. public static bool FollowFocus => _followFocus; public static bool IsTypingSearch => _detailUnitId == 0 && _searchField != null && _searchField.isFocused && _root != null && _root.activeInHierarchy; public static void EnsureBuilt() { DestroyOrphanHuds(); if (_root != null && (_root.transform.Find("Tabs") == null || _root.transform.Find(ChromeMarker) == null)) { try { Object.Destroy(_root); } catch { // ignore } _root = null; _rootRt = null; _content = null; _scrollRt = null; _scroll = null; _tabsBar = null; _searchField = null; _backBtn = null; _backBtnLabel = null; _detailPaneBar = null; _detailActivityBtn = null; _detailLifeBtn = null; _detailUnitId = 0; _followFocus = false; _detailPane = DetailPane.Life; } if (_root != null) { ApplyRootPlacement(); return; } if (!Config.game_loaded && CanvasMain.instance == null) { return; } Canvas canvas = HudCanvas.Resolve(); if (canvas == null) { return; } _root = new GameObject("IdleSpectatorWorldMemoryHud", typeof(RectTransform), typeof(Image), typeof(CanvasGroup)); _root.transform.SetParent(canvas.transform, false); new GameObject(ChromeMarker, typeof(RectTransform)).transform.SetParent(_root.transform, false); _rootRt = _root.GetComponent(); ApplyRootPlacement(); Image bg = _root.GetComponent(); bg.raycastTarget = true; HudCanvas.StylePanel(bg, _root.transform, new Color(0.07f, 0.08f, 0.1f, 0.92f)); CanvasGroup group = _root.GetComponent(); group.blocksRaycasts = true; group.interactable = true; _hotkeyText = HudCanvas.MakeText(_root.transform, "Hotkey", "L", 8); RectTransform hotRt = _hotkeyText.GetComponent(); hotRt.anchorMin = new Vector2(1f, 1f); hotRt.anchorMax = new Vector2(1f, 1f); hotRt.pivot = new Vector2(1f, 1f); hotRt.sizeDelta = new Vector2(16f, 12f); hotRt.anchoredPosition = new Vector2(-6f, -5f); _hotkeyText.alignment = TextAnchor.MiddleRight; _hotkeyText.color = new Color(0.5f, 0.53f, 0.58f, 0.95f); _hotkeyText.raycastTarget = false; _titleIcon = HudCanvas.MakeIcon(_root.transform, "TitleIcon", TitleIconSize); RectTransform titleIconRt = _titleIcon.GetComponent(); titleIconRt.anchorMin = new Vector2(0f, 1f); titleIconRt.anchorMax = new Vector2(0f, 1f); titleIconRt.pivot = new Vector2(0.5f, 0.5f); titleIconRt.anchoredPosition = new Vector2(13f, -TitleBandH * 0.5f - 1f); HudIcons.Apply(_titleIcon, HudIcons.FromUiIcon("iconBooks") ?? HudIcons.FromUiIcon("iconWar")); _titleText = HudCanvas.MakeText(_root.transform, "Title", "Lore", 9); RectTransform titleRt = _titleText.GetComponent(); titleRt.anchorMin = new Vector2(0f, 1f); titleRt.anchorMax = new Vector2(1f, 1f); titleRt.pivot = new Vector2(0.5f, 1f); titleRt.offsetMin = new Vector2(24f, -TitleBandH); titleRt.offsetMax = new Vector2(-22f, -2f); _titleText.alignment = TextAnchor.MiddleLeft; _titleText.color = new Color(0.9f, 0.91f, 0.93f, 1f); _titleText.fontStyle = FontStyle.Bold; _titleText.raycastTarget = false; _titleText.horizontalOverflow = HorizontalWrapMode.Overflow; BuildTabs(); BuildCharTools(); BuildScroll(); _root.SetActive(false); _visible = false; ApplyTabChrome(); LogService.LogInfo("[IdleSpectator] Lore HUD ready (L, World + Living + Fallen)"); } private static void BuildTabs() { _tabsBar = new GameObject("Tabs", typeof(RectTransform)); _tabsBar.transform.SetParent(_root.transform, false); RectTransform tabsRt = _tabsBar.GetComponent(); tabsRt.anchorMin = new Vector2(0f, 1f); tabsRt.anchorMax = new Vector2(1f, 1f); tabsRt.pivot = new Vector2(0.5f, 1f); tabsRt.anchoredPosition = new Vector2(0f, -TitleBandH); tabsRt.sizeDelta = new Vector2(-12f, TabH); _worldTabBtn = BuildTabButton(_tabsBar.transform, "WorldTab", "World", () => SetTab(LoreTab.World)); _livingTabBtn = BuildTabButton(_tabsBar.transform, "LivingTab", "Living", () => SetTab(LoreTab.Living)); _fallenTabBtn = BuildTabButton(_tabsBar.transform, "FallenTab", "Fallen", () => SetTab(LoreTab.Fallen)); PlaceTab(_worldTabBtn, 0f, 3f); PlaceTab(_livingTabBtn, 1f, 3f); PlaceTab(_fallenTabBtn, 2f, 3f); } private static GameObject BuildTabButton(Transform parent, string name, string label, UnityAction onClick) { GameObject go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(Button)); go.transform.SetParent(parent, false); Image bg = go.GetComponent(); bg.color = new Color(0.1f, 0.11f, 0.13f, 0.35f); bg.raycastTarget = true; Button button = go.GetComponent