using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Scripting; using UnityEngine.Video; namespace NecromancerTome { /// /// Книга с оглавлением (03.10.2026, BACKLOG «Книга с оглавлением и главами»). Окно necroTome /// (Config/XUi_InGame/windows.xml): оглавление - кнопки глав (до восьми), глава - страницы /// с «Назад/Далее», «Оглавление» возвращает к списку, «Закрыть» и Esc закрывают. /// /// СОДЕРЖАНИЕ - список Chapters ниже: ключ названия, ключи страниц (Localization.csv, /// necroTome*), условие открытия. Закрытая глава в оглавлении не показывается. Условия брать /// из того, что игра уже хранит (уровень некромантии, рецепты, испытания), своё хранение - только /// для чистых событий, по cvar на событие. /// public static class NecroTomeBook { public const string WindowGroup = "necroTome"; public class Chapter { public string TitleKey; public string[] PageKeys; public Func Unlocked; /// Иллюстрация справа от текста на всех страницах главы: картинка (путь вида /// "@modfolder(NecromancerTome):Textures/x.png") или зацикленное видео без звука. Ни того, /// ни другого - текст на всю ширину. public string Image, Video; /// Картинка полосой сверху во всю ширину, текст под ней (главы питомцев, указание 09.10: /// «под картинкой - управление схематично, под ним - описание питомца»). public bool ImageTop; } /// cvar «письмо архимага прочитано» - ставит записка noteDuke01 (Config/items.xml). public const string LetterReadCVar = "necroTomeLetterRead"; public static readonly List Chapters = new List { // Глава 1 (указание 06.10): письмо архимага - та самая стартовая записка, текст берётся из её // же ключа. Открывается ТОЛЬКО когда записку прочли («появляется в книге, когда прочитано // письмо», 06.10): cvar ставит сама записка по «ОК» (Config/items.xml). Запасного условия по // уровню нет - в старых сейвах, где записку прочли до 1.7.0, главы не будет. new Chapter { TitleKey = "necroTomeLetterTitle", PageKeys = new[] { "noteDuke01Desc" }, Video = Patch_XUiC_MessageBoxWindowGroup_ShowOkCancel_NoteFlashback.NoteFlashbackVideoPath, Unlocked = _p => _p.Buffs.GetCustomVar(LetterReadCVar) > 0f }, // Глава 2 (указание 03.10): статья о некроэнергии, сокращённая - без примеров и бытовой речи. new Chapter { TitleKey = "necroTomeCh1Title", PageKeys = new[] { "necroTomeCh1P1", "necroTomeCh1P2", "necroTomeCh1P3", "necroTomeCh1P4" }, Unlocked = _p => true }, // Нож некроманта (указание 09.10): лист-чертёж ножа (тот же рецепт, что у крысы), управление ножом и духом // змеи, описание. Перед питомцами - «основной инструмент», с него всё начинается. new Chapter { TitleKey = "necroTomeKnifeTitle", PageKeys = new[] { "necroTomeKnifeP1", "necroTomeKnifeP2", "necroTomeKnifeP3" }, Image = "@modfolder(NecromancerTome):Textures/necroTomeKnife.png", ImageTop = true, Unlocked = _p => true }, // Главы питомцев (указание 09.10): лист-чертёж модели (три вида из Blender, подписи на эсперанто) сверху, на первой // странице - управление, дальше - описание. Крыса - учебная модель, глава открыта сразу. new Chapter { TitleKey = "necroTomeRatTitle", PageKeys = new[] { "necroTomeRatP1", "necroTomeRatP2", "necroTomeRatP3" }, Image = "@modfolder(NecromancerTome):Textures/necroTomeRat.png", ImageTop = true, Unlocked = _p => true }, }; public static void Open(EntityPlayerLocal _player) { LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(_player); ui?.windowManager.Open(WindowGroup, true); } } [Preserve] public class XUiC_NecroTome : XUiController { const int MaxButtons = 8; XUiV_Label _title, _text, _page; XUiV_Texture _img; XUiV_Video _vid; /// Ширина текста без иллюстрации и с ней (windows.xml: lblText 1180, картинка с x 680). const int TextWidthFull = 1180, TextWidthHalf = 590; /// Раскладка «картинка сверху»: лист-чертёж 1180x400 (как Textures/necroTomeRat.png), текст ниже. static readonly Vector2i ImgSidePos = new Vector2i(680, -104), ImgSideSize = new Vector2i(560, 640); static readonly Vector2i ImgTopPos = new Vector2i(60, -104), ImgTopSize = new Vector2i(1180, 400); static readonly Vector2i TextPos = new Vector2i(60, -104), TextTopPos = new Vector2i(60, -518); const int TextHeightFull = 640, TextHeightTop = 245; XUiC_SimpleButton _toc, _prev, _next, _close; readonly XUiC_SimpleButton[] _chapterButtons = new XUiC_SimpleButton[MaxButtons]; /// Глава по кнопке оглавления (только открытые главы, по порядку). readonly int[] _buttonChapter = new int[MaxButtons]; int _chapter = -1, _pageIndex; public override void Init() { base.Init(); _title = GetChildById("lblTitle")?.ViewComponent as XUiV_Label; _text = GetChildById("lblText")?.ViewComponent as XUiV_Label; _page = GetChildById("lblPage")?.ViewComponent as XUiV_Label; _img = GetChildById("imgPage")?.ViewComponent as XUiV_Texture; _vid = GetChildById("vidPage")?.ViewComponent as XUiV_Video; _toc = GetChildById("btnToc") as XUiC_SimpleButton; _prev = GetChildById("btnPrev") as XUiC_SimpleButton; _next = GetChildById("btnNext") as XUiC_SimpleButton; _close = GetChildById("btnClose") as XUiC_SimpleButton; if (_toc != null) _toc.OnPressed += (_s, _m) => ShowToc(); if (_prev != null) _prev.OnPressed += (_s, _m) => Turn(-1); if (_next != null) _next.OnPressed += (_s, _m) => Turn(1); if (_close != null) _close.OnPressed += (_s, _m) => xui.playerUI.windowManager.Close(NecroTomeBook.WindowGroup); for (int i = 0; i < MaxButtons; i++) { int idx = i; _chapterButtons[i] = GetChildById("btnCh" + (i + 1)) as XUiC_SimpleButton; if (_chapterButtons[i] != null) { _chapterButtons[i].OnPressed += (_s, _m) => ShowChapter(_buttonChapter[idx], 0); } } } public override void OnOpen() { base.OnOpen(); ShowToc(); } void ShowToc() { _chapter = -1; SetLabel(_title, Localization.Get("necroTomeToc")); SetLabel(_text, ""); SetLabel(_page, ""); SetMedia(null, null); EntityPlayerLocal player = xui.playerUI.entityPlayer; int b = 0; for (int c = 0; c < NecroTomeBook.Chapters.Count && b < MaxButtons; c++) { NecroTomeBook.Chapter ch = NecroTomeBook.Chapters[c]; if (ch.Unlocked != null && !ch.Unlocked(player)) { continue; } _buttonChapter[b] = c; if (_chapterButtons[b] != null) { _chapterButtons[b].Text = Localization.Get(ch.TitleKey); _chapterButtons[b].ViewComponent.IsVisible = true; } b++; } for (; b < MaxButtons; b++) { if (_chapterButtons[b] != null) { _chapterButtons[b].ViewComponent.IsVisible = false; } } SetVisible(_toc, false); SetVisible(_prev, false); SetVisible(_next, false); } void ShowChapter(int _c, int _p) { if (_c < 0 || _c >= NecroTomeBook.Chapters.Count) { return; } NecroTomeBook.Chapter ch = NecroTomeBook.Chapters[_c]; _chapter = _c; _pageIndex = Mathf.Clamp(_p, 0, ch.PageKeys.Length - 1); foreach (XUiC_SimpleButton btn in _chapterButtons) { SetVisible(btn, false); } SetLabel(_title, Localization.Get(ch.TitleKey)); SetLabel(_text, Localization.Get(ch.PageKeys[_pageIndex])); SetLabel(_page, (_pageIndex + 1) + " / " + ch.PageKeys.Length); SetMedia(ch.Image, ch.Video, ch.ImageTop); SetVisible(_toc, true); SetVisible(_prev, true); SetVisible(_next, true); if (_prev != null) _prev.Enabled = _pageIndex > 0; if (_next != null) _next.Enabled = _pageIndex < ch.PageKeys.Length - 1; } public override void OnClose() { SetMedia(null, null); base.OnClose(); } /// Показать картинку или видео справа (или убрать обе). Видео задаётся только здесь, /// а не в вёрстке: XUiV_Video сам запускает ролик при открытии окна, и в оглавлении он играл /// бы невидимым. Пустой uri - XUiV_Video.startVideo сам освобождает свою текстуру. void SetMedia(string _image, string _video, bool _top = false) { bool img = !string.IsNullOrEmpty(_image), vid = !img && !string.IsNullOrEmpty(_video); if (_img != null) { // Без чтения _img.Path: у пустой картинки (в вёрстке texture не задан) его геттер берёт // textureUris[0] из пустого массива - IndexOutOfRange (тест 09.10). Сеттер сам сравнивает // с текущим путём и не перезагружает тот же файл. if (img) { _img.Path = _image; } _img.IsVisible = img; } if (_vid != null) { string uri = vid ? _video : ""; if (_vid.VideoUri != uri) { _vid.VideoUri = uri; } if (_vid.videoPlayer != null) { _vid.videoPlayer.audioOutputMode = VideoAudioOutputMode.None; // Проверка на утечки 06.10: XUiV_Video гасит ролик при смене uri, только если тот уже // играет; ушли в оглавление во время подготовки - догрузился бы и играл невидимым до // закрытия окна. Останавливаем сами (Stop освобождает декодер). if (!vid) { _vid.videoPlayer.Stop(); } } _vid.IsVisible = vid; } if (_img != null) { _img.Position = _top ? ImgTopPos : ImgSidePos; _img.Size = _top ? ImgTopSize : ImgSideSize; } if (_text != null) { bool top = _top && img; _text.Position = top ? TextTopPos : TextPos; _text.Size = new Vector2i(top || !(img || vid) ? TextWidthFull : TextWidthHalf, top ? TextHeightTop : TextHeightFull); } } void Turn(int _d) { if (_chapter >= 0) { ShowChapter(_chapter, _pageIndex + _d); } } static void SetLabel(XUiV_Label _l, string _s) { if (_l != null) { _l.Text = _s; } } static void SetVisible(XUiController _c, bool _v) { if (_c?.ViewComponent != null) { _c.ViewComponent.IsVisible = _v; } } } /// necrotome - открыть книгу (проверка окна без чертогов, в том числе через почтовый ящик). [Preserve] public class ConsoleCmdNecroTome : ConsoleCmdAbstract { public override string[] getCommands() => new[] { "necrotome" }; public override string getDescription() => "NecromancerTome: открыть книгу с оглавлением"; public override void Execute(List _params, CommandSenderInfo _senderInfo) { EntityPlayerLocal player = GameManager.Instance.World?.GetPrimaryPlayer(); if (player != null) { NecroTomeBook.Open(player); } } } }