Книга некроманта 1.0 — первая публичная версия

Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых
зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида
духов и сюжетный финал через Чёрный портал. Локализация на 13 языках.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
This commit is contained in:
Alex Cube
2026-09-09 21:13:03 +03:00
co-authored by Claude Opus 5
commit e8f064f5ec
102 changed files with 7010 additions and 0 deletions
+119
View File
@@ -0,0 +1,119 @@
<windows>
<!-- Black portal video (BACKLOG.md, "чёрный портал" dialog/video feature, 2026-08-30 follow-up
bugfix). Root cause of "пауза встала, видео не вижу": XUiC_VideoPlayer.PlayVideo opens a
window/group named "videoPlayer" - real and fully functional (it's exactly what plays the
TFP studio intro and the main-menu background loop), but that window is declared ONLY in
Data/Config/XUi_Menu/windows.xml (the MAIN MENU's own UI layout) - it does not exist at
all in Data/Config/XUi_InGame/windows.xml (confirmed by grepping both files directly), so
GUIWindowManager.GetWindow/Open logged "Window \"videoPlayer\" unknown!" the moment the
in-game code tried to open it. This appends the EXACT SAME window block vanilla's own menu
already uses (copied verbatim from XUi_Menu/windows.xml, not reinvented) into the in-game
UI instead, so the exact same controller="VideoPlayer" (XUiC_VideoPlayer, already compiled
into the base game, no Harmony needed) and the <video> element (a generic engine-level UI
primitive, not menu-specific) can be found by the in-game windowManager too. Needs a
matching window_group entry - see Config/XUi_InGame/xui.xml.
THIS ONE WINDOW IS SHARED - both the Black Portal video AND Duke's note flashback
(NoteFlashbackPatch.cs) call the same XUiC_VideoPlayer.PlayVideo, which always opens this
same "videoPlayer" window/group by id - one fix here covers both.
"видео полупрозрачное" fix (user report 2026-08-30, after first successful in-game test):
added globalopacitymod="0" to videoBackground/videoTexture/skipPrompt below. Root cause,
decompiled, not guessed: every XUiV_ImageBased-derived view (sprite AND video both are -
XUiV_Sprite/XUiV_TextureBased both extend it) has a globalOpacityModifier defaulting to 1,
meaning by default its final alpha gets multiplied by xui.BackgroundGlobalOpacity (unless
foregroundlayer="true", which uses a separate ForegroundGlobalOpacity instead) - this is a
REAL, already-live game option, decompiled straight from EnumGamePrefs.Options:
OptionsBackgroundGlobalOpacity defaults to 0.95, not 1.0, and is user-adjustable (further
down is common - players lower it to see more of the game world through menu backgrounds).
This window's copy (like vanilla's own menu copy) never set globalopacitymod, so both the
"opaque" black background AND the video picture itself were always getting dimmed by
whatever fraction of full opacity the user's own Background Opacity option is set to -
in the menu this is invisible (nothing behind the menu to bleed through), but in-game the
actual 3D world sits right behind this window and shows through the gap. The vanilla menu
copy has this exact same latent issue, just never exposed because it's never used with a
3D scene behind it - not something to also go "fix" in Data/Config, just worth knowing this
mod's copy needed the explicit opt-out that context actually requires. -->
<append xpath="/windows">
<window name="videoPlayer" controller="VideoPlayer" pos="0,0" depth="900" cursor_area="true">
<sprite name="videoBackground" depth="0" sprite="menu_empty" type="sliced" color="[black]" anchor_left="#cam,0,-10" anchor_right="#cam,1,10" anchor_bottom="#cam,0,-10" anchor_top="#cam,1,10" style="press" gamepad_selectable="false" globalopacitymod="0" />
<video name="videoTexture" depth="1" anchor_left="#cam,0,0" anchor_right="#cam,1,0" anchor_bottom="#cam,0,0" anchor_top="#cam,1,0" globalopacitymod="0"/>
<sprite name="skipPrompt" depth="5" sprite="ui_game_panel_header" color="[black]" anchor_left="#cam,1,-200" anchor_right="#cam,1,0" anchor_bottom="#cam,0,-0" anchor_top="#cam,0,60" globalopacitymod="0">
<label name="lblSkip" depth="6" text_key="Skip" justify="left" pivot="left" font_size="32" width="180" height="100" pos="20,-30" parse_actions="true" actions_default_format="( ### )"/>
</sprite>
</window>
</append>
<!-- ФИНАЛЬНЫЕ СЛАЙДЫ ЧЁРНОГО ПОРТАЛА, 2026-09-09 (BACKLOG.md "концовка серией диалоговых окон
вместо видео"). Шесть полноэкранных картинок, по одному окну на слайд; текст и кнопки
Назад/Дальше/выбор рисует поверх них ванильный messageBox - см. HarmonySrc/FinalSlides.cs,
там же вся логика листания.
ПОЧЕМУ ШЕСТЬ ОТДЕЛЬНЫХ ОКОН, А НЕ ОДНО С ПЕРЕКЛЮЧЕНИЕМ СПРАЙТА. Смена спрайта на лету
требует либо привязки ({binding}), либо своего XUiController - а в разведке (BACKLOG.md)
как раз осталось непроверенным, найдёт ли движок контроллер, объявленный в сборке мода.
Шесть окон с ЖЁСТКО прописанным sprite= обходят вопрос целиком: ни привязок, ни
контроллера, окно вообще без атрибута controller= (так объявлены и ванильные
HUDLeftStatBars/windowFuel, проверено в Data/Config/XUi_InGame/windows.xml). Листание -
это просто Close старого окна + Open нового из C#.
depth="900" - ниже messageBox (depth="1001" в шаблоне <messagebox>,
Data/Config/XUi_Common/templates.xml:143), так что текстовая коробка 1200x400 ложится
поверх картинки, а картинка видна вокруг неё. Открывать эти окна НЕ модально: в
GUIWindowManager.openInternal модальное открытие зовёт CloseAllOpenModalWindows(), то есть
модальный messageBox закрыл бы модальный слайд - а немодальный переживает это спокойно.
globalopacitymod="0" - та же правка, что у videoPlayer выше: без неё альфа умножается на
пользовательскую настройку Background Opacity (по умолчанию 0.95), и сквозь "финальную"
картинку просвечивал бы игровой мир.
ПРО ПРОПОРЦИИ: исходники 1536x1024 (3:2), а окно растягивается на весь экран (16:9). Чтобы
не плющить лица на крупных планах, картинки залиты чёрными полями по бокам до 1824x1024
(16:9) ЗАРАНЕЕ, при копировании в UIAtlases/NecroFinal - у XUiV_Sprite нет атрибута
сохранения пропорций (декомпилировано: он парсит только atlas/sprite/color/fill*/gradient*),
так что letterbox приходится запекать в сам PNG. Второй чёрный спрайт под картинкой -
страховка на неродных соотношениях сторон (ультраширокие мониторы).
Атлас NecroFinal - папка UIAtlases/NecroFinal, механизм тот же, что у ItemIconAtlas.
Шесть картинок 1824x1024 пакуются UIAtlasFromFolder.createUiAtlasFromTextures через
PackTextures(..., 2, 8192): суммарно ~11.2 Мпикс, влезает в один атлас 4096x4096. -->
<append xpath="/windows">
<window name="necroFinalSlide1" pos="0,0" depth="900">
<sprite name="slideLetterbox" depth="0" sprite="menu_empty" type="sliced" color="[black]" anchor_left="#cam,0,-10" anchor_right="#cam,1,10" anchor_bottom="#cam,0,-10" anchor_top="#cam,1,10" globalopacitymod="0"/>
<sprite name="slideArt" depth="1" atlas="NecroFinal" sprite="NecroFinal1" type="simple" anchor_left="#cam,0,0" anchor_right="#cam,1,0" anchor_bottom="#cam,0,0" anchor_top="#cam,1,0" globalopacitymod="0"/>
</window>
<window name="necroFinalSlide2" pos="0,0" depth="900">
<sprite name="slideLetterbox" depth="0" sprite="menu_empty" type="sliced" color="[black]" anchor_left="#cam,0,-10" anchor_right="#cam,1,10" anchor_bottom="#cam,0,-10" anchor_top="#cam,1,10" globalopacitymod="0"/>
<sprite name="slideArt" depth="1" atlas="NecroFinal" sprite="NecroFinal2" type="simple" anchor_left="#cam,0,0" anchor_right="#cam,1,0" anchor_bottom="#cam,0,0" anchor_top="#cam,1,0" globalopacitymod="0"/>
</window>
<window name="necroFinalSlide3" pos="0,0" depth="900">
<sprite name="slideLetterbox" depth="0" sprite="menu_empty" type="sliced" color="[black]" anchor_left="#cam,0,-10" anchor_right="#cam,1,10" anchor_bottom="#cam,0,-10" anchor_top="#cam,1,10" globalopacitymod="0"/>
<sprite name="slideArt" depth="1" atlas="NecroFinal" sprite="NecroFinal3" type="simple" anchor_left="#cam,0,0" anchor_right="#cam,1,0" anchor_bottom="#cam,0,0" anchor_top="#cam,1,0" globalopacitymod="0"/>
</window>
<window name="necroFinalSlide4" pos="0,0" depth="900">
<sprite name="slideLetterbox" depth="0" sprite="menu_empty" type="sliced" color="[black]" anchor_left="#cam,0,-10" anchor_right="#cam,1,10" anchor_bottom="#cam,0,-10" anchor_top="#cam,1,10" globalopacitymod="0"/>
<sprite name="slideArt" depth="1" atlas="NecroFinal" sprite="NecroFinal4" type="simple" anchor_left="#cam,0,0" anchor_right="#cam,1,0" anchor_bottom="#cam,0,0" anchor_top="#cam,1,0" globalopacitymod="0"/>
</window>
<window name="necroFinalSlide5" pos="0,0" depth="900">
<sprite name="slideLetterbox" depth="0" sprite="menu_empty" type="sliced" color="[black]" anchor_left="#cam,0,-10" anchor_right="#cam,1,10" anchor_bottom="#cam,0,-10" anchor_top="#cam,1,10" globalopacitymod="0"/>
<sprite name="slideArt" depth="1" atlas="NecroFinal" sprite="NecroFinal5" type="simple" anchor_left="#cam,0,0" anchor_right="#cam,1,0" anchor_bottom="#cam,0,0" anchor_top="#cam,1,0" globalopacitymod="0"/>
</window>
<window name="necroFinalSlide6" pos="0,0" depth="900">
<sprite name="slideLetterbox" depth="0" sprite="menu_empty" type="sliced" color="[black]" anchor_left="#cam,0,-10" anchor_right="#cam,1,10" anchor_bottom="#cam,0,-10" anchor_top="#cam,1,10" globalopacitymod="0"/>
<sprite name="slideArt" depth="1" atlas="NecroFinal" sprite="NecroFinal6" type="simple" anchor_left="#cam,0,0" anchor_right="#cam,1,0" anchor_bottom="#cam,0,0" anchor_top="#cam,1,0" globalopacitymod="0"/>
</window>
</append>
<!-- ЭПИЛОГ, 2026-09-09. После выбора на шестом слайде картинки заканчиваются и текст идёт по
чёрному - отсюда отдельное окно без спрайта из атласа, только чёрная заливка. Тот же
depth="900", что у слайдов, так что текстовая коробка (depth 1001) снова ложится сверху.
Заливка копирует приём videoPlayer'а выше: menu_empty, растянутый по #cam с запасом в
10px по каждой стороне, плюс globalopacitymod="0" - без него сквозь "чёрный экран"
просвечивал бы игровой мир на величину пользовательской настройки Background Opacity. -->
<append xpath="/windows">
<window name="necroFinalBlack" pos="0,0" depth="900">
<sprite name="blackout" depth="0" sprite="menu_empty" type="sliced" color="[black]" anchor_left="#cam,0,-10" anchor_right="#cam,1,10" anchor_bottom="#cam,0,-10" anchor_top="#cam,1,10" globalopacitymod="0"/>
</window>
</append>
</windows>
+45
View File
@@ -0,0 +1,45 @@
<xui>
<!-- Pairs with Config/XUi_InGame/windows.xml's new "videoPlayer" window - see that file's
comment for the full root-cause explanation. Copied verbatim from
Data/Config/XUi_Menu/xui.xml's own "videoPlayer" window_group (no controller= on the
group itself there - the controller lives on the window element instead, same as this
mod's copy). Without this entry the window from windows.xml exists but has no group to be
opened by name through (XUiC_VideoPlayer.PlayVideo/GetInstance both resolve via the GROUP
id, not the raw window). -->
<append xpath="/xui">
<window_group name="videoPlayer">
<window name="videoPlayer" />
</window_group>
</append>
<!-- Группы для шести финальных слайдов (см. XUi_InGame/windows.xml). Как и у videoPlayer,
контроллера на группе нет - окно чисто декоративное, открывается и закрывается по имени
из HarmonySrc/FinalSlides.cs. -->
<append xpath="/xui">
<window_group name="necroFinalSlide1">
<window name="necroFinalSlide1" />
</window_group>
<window_group name="necroFinalSlide2">
<window name="necroFinalSlide2" />
</window_group>
<window_group name="necroFinalSlide3">
<window name="necroFinalSlide3" />
</window_group>
<window_group name="necroFinalSlide4">
<window name="necroFinalSlide4" />
</window_group>
<window_group name="necroFinalSlide5">
<window name="necroFinalSlide5" />
</window_group>
<window_group name="necroFinalSlide6">
<window name="necroFinalSlide6" />
</window_group>
</append>
<!-- Группа для чёрного экрана эпилога (см. windows.xml). -->
<append xpath="/xui">
<window_group name="necroFinalBlack">
<window name="necroFinalBlack" />
</window_group>
</append>
</xui>