- Главы питомцев с чертежами (Fig. III-VI), команда necroobj для выгрузки моделей - Книги призыва питомцев переименованы в гримуары (13 языков) - Чертоги: второй этаж; таблички necroDoorNumber (только в редакторе) нумеруют двери, дверь с номером закрыта до события (SetDoorOpen, necropalace door N open|close), с начала открыты 2, 4, 5, 6 - Дух: плавный шаг по ступенькам, проход в проёмы с порогом - Стервятник: снижение только C, без Ctrl-переключателя Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
333 lines
13 KiB
C#
333 lines
13 KiB
C#
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.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
|
||
},
|
||
new Chapter
|
||
{
|
||
TitleKey = "necroTomeDogTitle",
|
||
PageKeys = new[] { "necroTomeDogP1", "necroTomeDogP2", "necroTomeDogP3" },
|
||
Image = "@modfolder(NecromancerTome):Textures/necroTomeDog.png",
|
||
ImageTop = true,
|
||
Unlocked = _p => true
|
||
},
|
||
new Chapter
|
||
{
|
||
TitleKey = "necroTomeVultureTitle",
|
||
PageKeys = new[] { "necroTomeVultureP1", "necroTomeVultureP2", "necroTomeVultureP3" },
|
||
Image = "@modfolder(NecromancerTome):Textures/necroTomeVulture.png",
|
||
ImageTop = true,
|
||
Unlocked = _p => true
|
||
},
|
||
new Chapter
|
||
{
|
||
TitleKey = "necroTomeWolfTitle",
|
||
PageKeys = new[] { "necroTomeWolfP1", "necroTomeWolfP2", "necroTomeWolfP3" },
|
||
Image = "@modfolder(NecromancerTome):Textures/necroTomeWolf.png",
|
||
ImageTop = true,
|
||
Unlocked = _p => true
|
||
},
|
||
new Chapter
|
||
{
|
||
TitleKey = "necroTomeBearTitle",
|
||
PageKeys = new[] { "necroTomeBearP1", "necroTomeBearP2", "necroTomeBearP3" },
|
||
Image = "@modfolder(NecromancerTome):Textures/necroTomeBear.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;
|
||
/// <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;
|
||
|
||
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();
|
||
}
|
||
|
||
/// <summary>Показать картинку или видео справа (или убрать обе). Видео задаётся только здесь,
|
||
/// а не в вёрстке: XUiV_Video сам запускает ролик при открытии окна, и в оглавлении он играл
|
||
/// бы невидимым. Пустой uri - XUiV_Video.startVideo сам освобождает свою текстуру.</summary>
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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);
|
||
}
|
||
}
|
||
}
|
||
}
|