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 = "LoreV10";
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 readonly List GraveEntriesScratch =
new List(64);
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;
/// True when Lore itself paused idle; close should auto-resume.
private static bool _pausedIdleForLore;
private static GameObject _backBtn;
private static Text _backBtnLabel;
private static GameObject _focusKillerBtn;
private static Text _focusKillerLabel;
private static GameObject _keepGraveBtn;
private static Text _keepGraveLabel;
private static GameObject _deleteGraveBtn;
private static Text _deleteGraveLabel;
private static GameObject _detailPaneBar;
private static GameObject _detailActivityBtn;
private static GameObject _detailLifeBtn;
private static DetailPane _detailPane = DetailPane.Life;
/// Lore is browsing one tile's grave stack (from a skull click).
private static bool _graveStackActive;
private static int _graveTileX;
private static int _graveTileY;
private static bool _openingGraveStack;
private static float _ignoreDismissUntil;
/// 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;
/// True when Lore is scoped to a gravestone stack (skull open).
public static bool GraveStackScopeActive => _graveStackActive;
public static int GraveStackTileX => _graveTileX;
public static int GraveStackTileY => _graveTileY;
/// Filtered stack list rows from the last grave-stack rebuild (harness).
public static int GraveStackFilteredCount { get; private set; }
///
/// Grave stack search should span the tools row (Keep/Del live in the title band).
///
public static bool GraveSearchFillsTools
{
get
{
if (!_graveStackActive || _searchField == null || _charTools == null)
{
return false;
}
RectTransform searchRt = _searchField.GetComponent();
RectTransform toolsRt = _charTools.GetComponent();
if (searchRt == null || toolsRt == null || !searchRt.gameObject.activeInHierarchy)
{
return false;
}
// Full stretch within CharTools (no right inset reserved for Refresh/Fav).
return Mathf.Abs(searchRt.offsetMax.x) < 0.5f
&& searchRt.anchorMin.x <= 0.01f
&& searchRt.anchorMax.x >= 0.99f
&& toolsRt.rect.width > 1f
&& searchRt.rect.width >= toolsRt.rect.width - 1f;
}
}
/// Ignore outside-click dismiss briefly after Lore open / Keep.
public static float IgnoreDismissUntil => _ignoreDismissUntil;
/// Arm a short grace so the open click does not instantly dismiss Lore.
public static void ArmDismissGrace(float seconds = 0.4f)
{
_ignoreDismissUntil = Time.unscaledTime + Mathf.Max(0.05f, seconds);
}
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;
_focusKillerBtn = null;
_focusKillerLabel = null;
_keepGraveBtn = null;
_keepGraveLabel = null;
_deleteGraveBtn = null;
_deleteGraveLabel = null;
_detailPaneBar = null;
_detailActivityBtn = null;
_detailLifeBtn = null;
_detailUnitId = 0;
_followFocus = false;
_detailPane = DetailPane.Life;
ClearGraveStackScope();
}
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);
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 = HudTheme.Muted;
_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 = HudTheme.Name;
_titleText.fontStyle = FontStyle.Bold;
_titleText.raycastTarget = false;
_titleText.horizontalOverflow = HorizontalWrapMode.Overflow;
// Grave Keep/Del live in the title band so search can use the full tools row.
_keepGraveBtn = BuildTabButton(_root.transform, "KeepGrave", "Keep", ToggleKeepGrave);
_keepGraveLabel = _keepGraveBtn.transform.Find("Label")?.GetComponent();
PlaceTitleTool(_keepGraveBtn, -58f, 34f);
_keepGraveBtn.SetActive(false);
_deleteGraveBtn = BuildTabButton(
_root.transform, "DeleteGrave", "Del", () => { DeleteActiveGraveStack(); });
_deleteGraveLabel = _deleteGraveBtn.transform.Find("Label")?.GetComponent();
if (_deleteGraveLabel != null)
{
_deleteGraveLabel.color = HudTheme.Danger;
}
PlaceTitleTool(_deleteGraveBtn, -22f, 34f);
_deleteGraveBtn.SetActive(false);
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 = HudTheme.TabIdleFill;
bg.raycastTarget = true;
Button button = go.GetComponent