Уровень навыка игра хранит ОДНИМ БАЙТОМ (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>
166 lines
17 KiB
XML
166 lines
17 KiB
XML
<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) >= 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>
|