Files
necromants-tome-7d2d-3-2/HarmonySrc/NecroTome.cs
T

412 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Scripting;
using UnityEngine.Video;
namespace NecromancerTome
{
/// <summary>
/// Книга с оглавлением (03.10.2026, BACKLOG «Книга с оглавлением и главами»). Окно necroTome
/// (Config/XUi_InGame/windows.xml): оглавление - кнопки глав (до восьми), глава - страницы
/// с «Назад/Далее», «Оглавление» возвращает к списку, «Закрыть» и Esc закрывают.
///
/// СОДЕРЖАНИЕ - список Chapters ниже: ключ названия, ключи страниц (Localization.csv,
/// necroTome*), условие открытия. Закрытая глава в оглавлении не показывается. Условия брать
/// из того, что игра уже хранит (уровень некромантии, рецепты, испытания), своё хранение - только
/// для чистых событий, по cvar на событие.
/// </summary>
public static class NecroTomeBook
{
public const string WindowGroup = "necroTome";
public class Chapter
{
public string TitleKey;
public string[] PageKeys;
public Func<EntityPlayerLocal, bool> Unlocked;
/// <summary>Иллюстрация справа от текста на всех страницах главы: картинка (путь вида
/// "@modfolder(NecromancerTome):Textures/x.png") или зацикленное видео без звука. Ни того,
/// ни другого - текст на всю ширину.</summary>
public string Image, Video;
/// <summary>Картинка полосой сверху во всю ширину, текст под ней (главы питомцев, указание 09.10:
/// «под картинкой - управление схематично, под ним - описание питомца»).</summary>
public bool ImageTop;
}
/// <summary>cvar «письмо архимага прочитано» - ставит записка noteDuke01 (Config/items.xml).</summary>
public const string LetterReadCVar = "necroTomeLetterRead";
public static readonly List<Chapter> Chapters = new List<Chapter>
{
// Глава 1 (указание 06.10): письмо архимага - та самая стартовая записка, текст берётся из её
// же ключа. Открывается ТОЛЬКО когда записку прочли («появляется в книге, когда прочитано
// письмо», 06.10): cvar ставит сама записка по «ОК» (Config/items.xml). Запасного условия по
// уровню нет - в старых сейвах, где записку прочли до 1.6.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
},
new Chapter
{
TitleKey = "necroTomeDogTitle",
PageKeys = new[] { "necroTomeDogP1", "necroTomeDogP2", "necroTomeDogP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeDog.png",
ImageTop = true,
Unlocked = _p => RecipeUnlocked(_p, "bookSummonZombieDog")
},
new Chapter
{
TitleKey = "necroTomeVultureTitle",
PageKeys = new[] { "necroTomeVultureP1", "necroTomeVultureP2", "necroTomeVultureP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeVulture.png",
ImageTop = true,
Unlocked = _p => RecipeUnlocked(_p, "bookSummonZombieGriffin")
},
new Chapter
{
TitleKey = "necroTomeWolfTitle",
PageKeys = new[] { "necroTomeWolfP1", "necroTomeWolfP2", "necroTomeWolfP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeWolf.png",
ImageTop = true,
Unlocked = _p => RecipeUnlocked(_p, "bookSummonZombieWolf")
},
new Chapter
{
TitleKey = "necroTomeBearTitle",
PageKeys = new[] { "necroTomeBearP1", "necroTomeBearP2", "necroTomeBearP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeBear.png",
ImageTop = true,
Unlocked = _p => RecipeUnlocked(_p, "bookSummonZombieBear")
},
// Кровавый прилив (указание 10.10): предупреждение - в ночь кровавой луны дух не выходит из тела
// (SnakeMode.IsBloodTide). Лист-схема: луна, горы, некропыль из земли; подписи на эсперанто.
new Chapter
{
TitleKey = "necroTomeTideTitle",
PageKeys = new[] { "necroTomeTideP1", "necroTomeTideP2", "necroTomeTideP3" },
Image = "@modfolder(NecromancerTome):Textures/necroTomeTide.png",
ImageTop = true,
Unlocked = _p => GameUtils.WorldTimeToDays(_p.world.worldTime) >= TideChapterDay
},
};
/// <summary>С какого дня видна глава о кровавом приливе. По замыслу - 5 (тогда же надпись на доске
/// чертогов, MindPalace.BoardLines); на время теста было 1, возвращено к релизу 1.6.0 (10.10).</summary>
public const int TideChapterDay = 5;
/// <summary>Глава питомца видна, когда игроку открыт рецепт его гримуара (указание 09.10, после проверки глав).
/// Крыса - учебная модель, её глава открыта сразу.</summary>
static bool RecipeUnlocked(EntityPlayerLocal _p, string _item)
{
Recipe r = CraftingManager.GetRecipe(_item);
return r != null && r.IsUnlocked(_p);
}
public static void Open(EntityPlayerLocal _player)
{
LocalPlayerUI ui = LocalPlayerUI.GetUIForPlayer(_player);
ui?.windowManager.Open(WindowGroup, true);
}
}
[Preserve]
public class XUiC_NecroTome : XUiController
{
/// <summary>Кнопки глав в оглавлении (btnCh1..20 в windows.xml). Глав будет много (указание 10.10): до Rows - один
/// столбец по центру, больше - два столбца по Rows («я бы их сделал в две колонки»), ещё больше - страницы,
/// листаются теми же «Назад/Далее», что и главы.</summary>
const int MaxButtons = 20, Rows = 10;
/// <summary>Места кнопок (windows.xml: ширина 560, шаг строк 60 от y -130): столбец по центру окна 1300 и два столбца.</summary>
const int ColCenterX = 370, ColLeftX = 60, ColRightX = 680, RowTopY = -130, RowStep = 60;
XUiV_Label _title, _text, _page;
XUiV_Texture _img;
XUiV_Video _vid;
/// <summary>Ширина текста без иллюстрации и с ней (windows.xml: lblText 1180, картинка с x 680).</summary>
const int TextWidthFull = 1180, TextWidthHalf = 590;
/// <summary>Раскладка «картинка сверху»: лист-чертёж 1180x400 (как Textures/necroTomeRat.png), текст ниже.</summary>
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];
/// <summary>Глава по кнопке оглавления (только открытые главы, по порядку).</summary>
readonly int[] _buttonChapter = new int[MaxButtons];
int _chapter = -1, _pageIndex;
/// <summary>Страница оглавления; «Оглавление» из главы возвращает на неё.</summary>
int _tocPage;
readonly List<int> _open = new List<int>();
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(_tocPage);
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(0);
}
void ShowToc(int _p)
{
_chapter = -1;
SetLabel(_title, Localization.Get("necroTomeToc"));
SetLabel(_text, "");
SetMedia(null, null);
EntityPlayerLocal player = xui.playerUI.entityPlayer;
// Открытые главы по порядку; на странице - MaxButtons из них.
_open.Clear();
for (int c = 0; c < NecroTomeBook.Chapters.Count; c++)
{
NecroTomeBook.Chapter ch = NecroTomeBook.Chapters[c];
if (ch.Unlocked == null || ch.Unlocked(player))
{
_open.Add(c);
}
}
bool twoCols = _open.Count > Rows;
int perPage = twoCols ? MaxButtons : Rows;
int pages = Mathf.Max(1, (_open.Count + perPage - 1) / perPage);
_tocPage = Mathf.Clamp(_p, 0, pages - 1);
int b = 0;
for (int i = _tocPage * perPage; i < _open.Count && b < perPage; i++)
{
int c = _open[i];
NecroTomeBook.Chapter ch = NecroTomeBook.Chapters[c];
_buttonChapter[b] = c;
if (_chapterButtons[b] != null)
{
// Сначала левый столбец сверху вниз, потом правый.
int x = !twoCols ? ColCenterX : b < Rows ? ColLeftX : ColRightX;
_chapterButtons[b].ViewComponent.Position = new Vector2i(x, RowTopY - RowStep * (b % Rows));
_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);
// Листать оглавление - только если страниц больше одной.
bool many = pages > 1;
SetLabel(_page, many ? (_tocPage + 1) + " / " + pages : "");
SetVisible(_prev, many);
SetVisible(_next, many);
if (_prev != null) _prev.Enabled = _tocPage > 0;
if (_next != null) _next.Enabled = _tocPage < pages - 1;
}
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();
}
/// <summary>Показать картинку или видео справа (или убрать обе). Видео задаётся только здесь,
/// а не в вёрстке: XUiV_Video сам запускает ролик при открытии окна, и в оглавлении он играл
/// бы невидимым. Пустой uri - XUiV_Video.startVideo сам освобождает свою текстуру.</summary>
/// <summary>Путь загруженной картинки (null - не загружена). Свой, а не _img.Path - см. ниже.</summary>
string _imgPath;
void ReleaseImage()
{
if (_img == null || _imgPath == null)
{
return;
}
Texture t = _img.Texture;
_img.Path = null; // TextureUris = пусто, Texture = null - без выгрузки (autounload выключен)
if (t != null)
{
UnityEngine.Object.Destroy(t);
}
_imgPath = null;
}
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). Сеттер сам сравнивает
// с текущим путём и не перезагружает тот же файл.
// Утечка (проверка 10.10): картинка из файла мода - новая текстура (XUiV_Texture.FetchWwwTexture, клон),
// и при смене пути старая сама не удаляется (autounload в вёрстке не включён - в этом режиме игра на
// загрузке стирает путь новой картинки). Каждая смена главы оставляла ~2 МБ. Удаляем сами: при смене
// картинки, у главы без картинки и при закрытии окна (SetMedia(null, null)).
if (_imgPath != _image)
{
ReleaseImage();
}
if (img && _imgPath == null)
{
_img.Path = _image;
_imgPath = _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);
}
else
{
ShowToc(_tocPage + _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;
}
}
}
/// <summary>necrotome - открыть книгу (проверка окна без чертогов, в том числе через почтовый ящик).</summary>
[Preserve]
public class ConsoleCmdNecroTome : ConsoleCmdAbstract
{
public override string[] getCommands() => new[] { "necrotome" };
public override string getDescription() => "NecromancerTome: открыть книгу с оглавлением";
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
{
EntityPlayerLocal player = GameManager.Instance.World?.GetPrimaryPlayer();
if (player != null)
{
NecroTomeBook.Open(player);
}
}
}
}