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>
This commit is contained in:
AlexCube
2026-09-17 23:24:25 +03:00
co-authored by Claude Opus 5
parent fbd58ad018
commit ed5fea5192
14 changed files with 613 additions and 145 deletions
+46
View File
@@ -116,4 +116,50 @@
<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>