Своё окно с ячейкой коллектора не принимало череп ни перетаскиванием, ни Shift+кликом. Теперь - XUiC_BagStorageWindowGroup (как сумка транспорта и волка) с сумкой 1x1: перенос - кодом игры; после каждого изменения в ячейке остаётся один череп, лишнее и чужое - в рюкзак. Своё окно и группа из XUi удалены. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
212 lines
22 KiB
XML
212 lines
22 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>
|
||
<!-- Книга с оглавлением (03.10.2026, BACKLOG «Книга с оглавлением и главами»): оглавление кнопками,
|
||
глава по страницам, «Назад/Далее», «Оглавление», «Закрыть». Содержание и условия открытия глав -
|
||
в HarmonySrc/NecroTome.cs, тексты - Localization.csv (necroTome*). Открывает книга памяти в
|
||
чертогах (BlockNecroMemoryBook.Open) и консольная команда necrotome. -->
|
||
<append xpath="/windows">
|
||
<window name="necroTome" anchor="Center" pos="-650,420" width="1300" height="840" cursor_area="true" depth="1001" lock_navigation="true" controller="NecromancerTome.NecroTome, NecromancerHarmony">
|
||
<sprite depth="0" name="bg" sprite="menu_empty" color="14,10,10,250" type="sliced" globalopacitymod="0" />
|
||
<sprite depth="1" name="frame" sprite="menu_empty2px" color="120,18,14,255" type="sliced" fillcenter="false" globalopacitymod="0" />
|
||
<label depth="2" name="lblTitle" pos="0,-22" width="1300" height="50" justify="center" font_size="38" color="205,40,30,255" effect="outline" upper_case="false" />
|
||
<sprite depth="1" name="rule" pos="200,-82" width="900" height="2" sprite="menu_empty" color="120,18,14,255" globalopacitymod="0" />
|
||
<label depth="2" name="lblText" pos="60,-104" width="1180" height="640" justify="left" font_size="27" color="215,205,190,255" overflow="shrinkcontent" upper_case="false" effect="outline" effect_color="0,0,0,255" />
|
||
<!-- Иллюстрация страницы справа (Chapter.Image / Chapter.Video в NecroTome.cs); при ней
|
||
текст сужается до левой половины. Видео - тот же XUiV_Video, что фон главного меню. -->
|
||
<!-- globalopacitymod="0" (09.10): иначе фон и иллюстрация берут настройку прозрачности интерфейса и
|
||
просвечивают обоями чертогов - красные заголовки сливались, картинка была полупрозрачной. -->
|
||
<texture depth="2" name="imgPage" pos="680,-104" width="560" height="640" visible="false" globalopacitymod="0" />
|
||
<video depth="2" name="vidPage" pos="680,-104" width="560" height="640" videoaspect="FitInside" loop="true" visible="false" />
|
||
<!-- Кнопки глав: до 10 - один столбец по центру, больше - два столбца по 10 (10.10); места ставит XUiC_NecroTome.ShowToc. -->
|
||
<simplebutton name="btnCh1" depth="3" pos="370,-130" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh2" depth="3" pos="370,-190" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh3" depth="3" pos="370,-250" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh4" depth="3" pos="370,-310" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh5" depth="3" pos="370,-370" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh6" depth="3" pos="370,-430" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh7" depth="3" pos="370,-490" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh8" depth="3" pos="370,-550" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh9" depth="3" pos="370,-610" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh10" depth="3" pos="370,-670" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh11" depth="3" pos="370,-130" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh12" depth="3" pos="370,-190" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh13" depth="3" pos="370,-250" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh14" depth="3" pos="370,-310" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh15" depth="3" pos="370,-370" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh16" depth="3" pos="370,-430" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh17" depth="3" pos="370,-490" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh18" depth="3" pos="370,-550" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh19" depth="3" pos="370,-610" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnCh20" depth="3" pos="370,-670" width="560" height="48" font_size="30" visible="false" />
|
||
<simplebutton name="btnToc" depth="3" pos="40,-790" width="260" height="46" font_size="26" caption_key="necroTomeToc" />
|
||
<simplebutton name="btnPrev" depth="3" pos="440,-790" width="160" height="46" font_size="26" caption_key="necroTomePrev" />
|
||
<label depth="2" name="lblPage" pos="600,-772" width="100" height="40" justify="center" font_size="24" color="160,150,140,255" />
|
||
<simplebutton name="btnNext" depth="3" pos="700,-790" width="160" height="46" font_size="26" caption_key="necroTomeNext" />
|
||
<simplebutton name="btnClose" depth="3" pos="1000,-790" width="260" height="46" font_size="26" caption_key="necroTomeClose" />
|
||
</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>
|