Files
necromants-tome-7d2d-3-2/Config/XUi_InGame/windows.xml
T
AlexCubeandClaude Opus 5 ed5fea5192 1.3.0: шкала 20 убийств за уровень, два индикатора, мир между подчинёнными
Уровень навыка игра хранит ОДНИМ БАЙТОМ (ProgressionValue.Write/Read), поэтому
старая шкала "одно убийство - один уровень" при max_level=5000 на каждом
сохранении откатывала уровень на 256 назад. Снаружи это выглядело как
самопроизвольно закрывающиеся рецепты (жалоба со стрима: Слёзы мертвеца открыты,
Пир падальщика под замком при 250+ убитых), а тиры 500/2000/3000/5000 были
недостижимы в принципе. Подтверждено на двух живых сейвах: sezon8 - 384 убийства
при уровне 129, test8 - 303 при уровне 48.

Шкала переведена на 20 убийств = 1 уровень, max_level=250 - влезает в байт с
запасом. Уровень больше не накапливается, а вычисляется из necroZombieKillsCVar
(float, сохраняется честно) и на каждом убийстве, и постфиксом на
PlayerDataFile.ToPlayer - последнее чинит старые сейвы само, без команд и новой
игры. Открытое при этом не теряется: в старой шкале уровень всегда был не больше
счётчика, так что пересчёт может только вернуть украденное переполнением.
Отдельно закрыт случай "счётчик пуст, а уровень есть" - счётчик восстанавливается
из уровня по старой шкале.

Все пороги пересчитаны в уровни, числа убийств не тронуты, кроме воды: 30 на
сетку шагом 20 не ложится, по указанию пользователя мод переехал на 20 - туда же,
где браслет и Кровавая сфера. Тег necroNecromancyLvl30 удалён. Сходимость всех 18
рецептов (тег -> RecipeTagUnlocked -> unlock_tier) проверена скриптом.

Два индикатора: череп в статус-баре показывает уровень (раньше - общее число
убийств), фиолетовая шкала над полосой опыта - продвижение внутри уровня, 0..20.
Шкала сделана вёрсткой, а не баффом (бафф умеет число, но не полосу), заполнение
привязано через XUi-выражение cvar(). Закрывающая скобка у выражения - одна "}",
а не "%}": лишний "%" NCalc читает как остаток от деления и ждёт правый операнд,
на чём первая проверка в игре и споткнулась.

Третья правка: подчинённые зомби больше не дерутся между собой. Девиация выдаёт
приказ "бей зомби", а подчинённый сам EntityZombie, и в targetClasses выражается
только тип. Постфикс на EAITarget.check вычёркивает подчинённого из кандидатов
(зомби выбирает следующего, настоящего врага), префикс на
EntityAlive.SetAttackTarget гасит цель на путях мимо выбора - прежде всего месть,
когда подчиняют уже дерущихся.

Тексты на 13 языках, README (RU+EN), описания для сайта и Nexus приведены к новой
шкале.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 23:24:25 +03:00

166 lines
17 KiB
XML
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.
<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>
<!-- ФИОЛЕТОВАЯ ШКАЛА НЕКРОМАНТИИ, 2026-09-17. Продиктовано: "Есть индикатор опыта. Это шкала
до уровня. А мы сделаем ещё одну шкалу, которая заполняется каждые 20 зомби... Каждое
заполнение индикатора пусть добавляет уровень некромантии, а индикатор обнуляет."
Заполнение считает не вёрстка: necroNecromancyProgressCVar (0..19) пишет
HarmonySrc/NecromancyKillCreditPatch.cs на каждом убийстве и при загрузке игрока, деля
общее число упокоенных зомби на 20 с остатком. Шкала обнуляется сама собой - вместе с
переходом остатка через двадцатку растёт уровень Некромантии (череп в статус-баре).
ПОЧЕМУ ЭТО ВЁРСТКА, А НЕ ЕЩЁ ОДИН БАФФ. Бафф умеет показать ЧИСЛО (display_value), но не
полосу. Полоса - это sprite type="filled" с атрибутом fill 0..1, и живёт она в окне.
Взято ровно то окно и ровно тот приём, которым нарисована ванильная полоса опыта:
windowToolbelt, три спрайта друг на друге (серая подложка, чёрная рамка fillcenter="false",
заливка) - см. Data/Config/XUi_InGame/windows.xml, строки с fill="{xp}". Наша полоса
стоит на 12 пикселей выше опыта (pos 0,20 против 0,8) и той же ширины 750.
ПРИВЯЗКА. fill читает CVar через функцию выражений cvar() - она есть в самой игре
(BindingNcalcFunctions.cvar, регистрирует BindingInfoNcalc.VariableStateCVar, который
сам перечитывает значение, а не застывает на первом).
ЗАКРЫВАЮЩАЯ СКОБКА - ОДНА "}", А НЕ "%}". Это не стилистика, а единственная рабочая форма,
и первая же проверка в игре (17.09, лог 22-36-46) на этом и споткнулась:
ERR [XUi] Binding expression can not be evaluated. Binding fill="{% cvar(...) / 20 %}"
EXC no viable alternative at input '<EOF>' at line 1:44
Открывается выражение как "{%", а закрывается просто "}" - так во всех ванильных
примерах (Data/Config/XUi_InGame/windows.xml: visible="{% int(windowWidth) &gt;= 300 }",
text="{% dictvalue(sandboxvalues, '1') }"). Лишний "%" остаётся ВНУТРИ выражения, а в
NCalc это оператор остатка от деления, поэтому парсер честно ждёт после него правый
операнд и упирается в конец строки - позиция 44 в сообщении указывает ровно на него.
Ошибка разовая, не в каждый кадр: привязка, не сумев вычислиться, гаснет насовсем, то
есть полоса просто не работает молча.
Если полоса когда-нибудь опять не появится или застынет - запасной путь такой: писать из
C# готовую долю 0..1 в отдельный CVar и подставлять её без арифметики в вёрстке. Своего XUi-контроллера тут сознательно
нет: находит ли движок контроллер из сборки мода, в этом моде так и не проверено (см.
комментарий к финальным слайдам выше), а окно с контроллером было бы риском без нужды.
Цвет 150,60,220 - фиолетовый, как просили; альфа 200, чтобы полоса читалась поверх
тёмного HUD, но не спорила с опытом яркостью. -->
<append xpath="/windows/window[@name='windowToolbelt']/rect">
<sprite depth="1" pos="0,20" height="8" color="[mediumGrey]" sprite="menu_empty2px" globalopacitymod="0" type="sliced" />
<sprite depth="9" pos="0,20" height="8" color="[black]" sprite="menu_empty2px" globalopacitymod="0" type="sliced" fillcenter="false" />
<sprite depth="4" pos="0,20" height="8" width="750" color="150,60,220,200" globalopacitymod="0" type="filled" fill="{% cvar('necroNecromancyProgressCVar') / 20 }" />
</append>
</windows>