Files
necromants-tome-7d2d-3-2/Config/item_modifiers.xml
T
AlexCubeandClaude Opus 5 e362c627e7 Кровь некроманта - топливо Пространственного браслета
Шесть указаний одного захода, которые сложились в одну механику: поглощение блока
больше не бесплатно. Браслет требует модификацию в слоте, кровь ею стала, и она
на это тратится.

СТАК ПО ОДНОЙ БАНКЕ. Наследуемый medicalBloodBag даёт Stacknumber 15, и это надо
перебивать явно - Extends копирует свойство целиком, а не "если не задано иначе".
Дорого и так задумано: Чёрный портал просит десять банок, то есть десять ячеек.

ПРОЧНОСТЬ 1000. Свойства с именем вроде Durability в игре нет; ручек две, и обе
обязательны. Число - пассивный эффект DegradationMax
(ItemValue.MaxUseTimesBase -> EffectManager.GetValue), полоска - отдельное
свойство ShowQuality (XUiC_ItemStack.ShowDurability -> ItemClass.ShowQualityBar).
Без первой прочность равна нулю, а полоска при MaxUseTimes == 0 рисуется ПОЛНОЙ,
то есть забытый эффект выглядит как "всё работает". tiered="false" обязателен:
ItemClass.HasQuality читается как Effects.IsOwnerTiered(), и тированная группа
превратила бы банку в предмет с качеством, с тирами и рамкой.

КРОВЬ ПЕРЕЕХАЛА В item_modifiers.xml. Это не настройка, а смена класса предмета:
XUiC_ItemPartStack.CanSwap открывается строкой
`if (!(stack.itemValue.ItemClass is ItemClassModifier ...)) return false;` - слот
модификации не смотрит ни на теги, ни на свойства, пока предмет не
ItemClassModifier, а этот класс создаётся только из <item_modifier>. Свойства
вида CanBeInstalled не существует; остаться ресурсом в items.xml и вставляться в
браслет физически нельзя. На старом месте оставлен комментарий-указатель.

Что при этом проверено, а не понадеялось: рецепты резолвятся (ItemClassModifier
наследует ItemClass, имена лежат в общем ItemClass.nameToItem, Recipe ищет через
GetItemClass по тому же словарю); сейв цел (ItemClass.assignIdsFromMapping берёт
айди из сохранённого name->id мэппинга, перестановка в конфигах предмет не
подменит); Harmony-патч и ключи локализации ходят по имени, имя не менялось.

ЛОВУШКА ПЕРЕЕЗДА: у модификации effect_group применяется к предмету, В КОТОРЫЙ её
вставили - прочность 1000 начала бы выдаваться БРАСЛЕТУ. Пассивка гейтована
tags="necroBloodFlask", тег добавлен в Tags флакона: MaxUseTimesBase зовёт
GetValue с ItemTags того предмета, для которого считает.

СЛОТОВ У БРАСЛЕТА 1, было 4. Слот из набора улучшений стал выбором.

ПУСТОЙ СЛОТ ОТКАЗЫВАЕТ. Проверка стоит первой строкой Begin, впереди всех
остальных отказов: прочие про ЦЕЛЬ (нет блока, не тот блок, хранилище полно), эта
про ИНСТРУМЕНТ, и сказать "здесь нет блока", когда пуст браслет, значит отправить
игрока искать не там. Тест - ItemValue.HasMods(), игровой собственный: обходит
только Modifications, пропуская null и IsEmpty, и не считает CosmeticMods, иначе
краска читалась бы как "браслет заряжен". Звук отказа достался бесплатно -
Deny() в этом файле уже играет ванильный ui_denied.

РАСХОД. Цена пула считается ОДИН раз, при старте, и едет в PickupJob.ChannelSeconds
вместе с самим браслетом. Не потому, что так короче: ChannelSecondsFor меряет луч
игрока, а за десять секунд игрок успевает отвернуться - второй вызов насчитал бы
цену за другой блок, а не за тот, который забрали. Браслет хранится экземпляром по
той же причине: моды живут на ItemValue, а колесо прокручивается.

Списывается в SpendBlood, ПОСЛЕ SetBlockRPC и после того, как предмет лёг в
хранилище: все отказы выходят раньше через return, так что кровь за отменённое
поглощение невозможна по построению. Имя предмета берётся из
NecromancerBloodPatch.BloodItemName, а не вторым литералом, чтобы не разъехались.
Мод, который не кровь, не платит ничего и поглощению не мешает - слот задуман под
другие вещи.

КОНЧИЛАСЬ - РАЗБИВАЕТСЯ. Правило именно "прочность 0 или меньше", а не "не хватило
на пул", и разница не косметическая: по второй формулировке флакон, которому
хватило впритык, остался бы в слоте с нулём, HasMods() видел бы "что-то вставлено",
и браслет работал бы бесплатно до конца света. Поэтому зажим по MaxUseTimes убран,
а слот обнуляется через ItemValue.None - это type 0, ровно то, что проверяет
IsEmpty(). Последнее поглощение проходит всегда, флакон его просто не переживает.
Звук - ванильный itembreak, тот же, что играет ItemAction.HandleItemBreak.

Защита: если MaxUseTimes окажется 0 (снесли passive_effect или тег), флакон НЕ
удаляется, а в лог идёт предупреждение с указанием, где чинить. Без этой ветки
ошибка в XML съедала бы игроку предмет на первом же поглощении, и выглядело бы это
багом механики.

Локализация: новый ключ braceletSpatialVaultNoMod в 13 языках, плюс описания
флакона и браслета - механика стала условной и платной, и оба текста без этого
стали бы враньём.

Известное и намеренное: кровавого камня, который обещает сообщение о пустом слоте,
ещё нет - он запланирован, разбор в BACKLOG.md. В игре ничего из этого не
проверено.

---

Necromancer's Blood is the Spatial Bracelet's fuel

Six instructions from one session that add up to one mechanic: pulling a block
into the vault is no longer free. The bracelet needs a mod in its slot, the blood
became that mod, and it is spent doing the work.

ONE JAR PER STACK. The inherited medicalBloodBag sets Stacknumber 15 and it has to
be overridden explicitly - Extends copies a property wholesale, not "unless set".
Expensive on purpose: the Black Portal asks for ten jars, so ten slots.

DURABILITY 1000. There is no property called anything like Durability; there are
two knobs and both are required. The number is a DegradationMax passive effect
(ItemValue.MaxUseTimesBase -> EffectManager.GetValue); the bar is a separate
ShowQuality property (XUiC_ItemStack.ShowDurability -> ItemClass.ShowQualityBar).
Without the first, durability is zero - and the bar at MaxUseTimes == 0 draws
FULL, so a forgotten effect looks exactly like success. tiered="false" is
mandatory: ItemClass.HasQuality is Effects.IsOwnerTiered(), and a tiered group
would have turned the jar into a quality item with tiers and a frame.

THE BLOOD MOVED TO item_modifiers.xml. Not a setting but a change of item class:
XUiC_ItemPartStack.CanSwap opens with
`if (!(stack.itemValue.ItemClass is ItemClassModifier ...)) return false;` - a mod
slot looks at neither tags nor properties until the item is an ItemClassModifier,
and that class is only created from <item_modifier>. No CanBeInstalled property
exists; staying a resource in items.xml and going into the bracelet is impossible.
A pointer comment was left where it used to live.

Checked rather than hoped: recipes still resolve (ItemClassModifier extends
ItemClass, names live in the shared ItemClass.nameToItem, Recipe looks them up
through GetItemClass); saves are safe (ItemClass.assignIdsFromMapping takes ids
from the stored name->id mapping, so shuffling configs cannot swap the item); the
Harmony patch and the localization keys go by name, and the name did not change.

THE TRAP IN MOVING IT: a modifier's effect_group applies to the item it is
INSTALLED IN - the 1000 durability would have been granted to the BRACELET. The
passive is gated with tags="necroBloodFlask" and the tag added to the flask's own
Tags: MaxUseTimesBase calls GetValue with the ItemTags of whatever it is
computing for.

THE BRACELET HAS 1 MOD SLOT, down from 4. The slot stopped being a set of
upgrades and became a choice.

AN EMPTY SLOT REFUSES. The check is the first line of Begin, ahead of every other
refusal: the others are about the TARGET (no block, wrong block, vault full), this
one is about the TOOL, and saying "no block there" when the real problem is an
empty bracelet sends the player looking in the wrong place. The test is
ItemValue.HasMods(), the game's own: it walks Modifications only, skipping nulls
and IsEmpty, and does not count CosmeticMods - a dye would otherwise have read as
"loaded". The refusal sound came free: Deny() in this file already plays vanilla's
ui_denied.

THE COST. The price of a pull is computed ONCE, at the start, and carried in
PickupJob.ChannelSeconds along with the bracelet itself. Not for brevity:
ChannelSecondsFor measures the player's ray, and ten seconds is long enough to
turn away - a second call would charge for a different block than the one taken.
The bracelet is kept as an instance for the same reason: mods live on the
ItemValue and the hotbar scrolls.

It is charged in SpendBlood, AFTER SetBlockRPC and after the item is in the vault:
every refusal returns earlier, so blood charged for a cancelled pull is impossible
by construction. The item name comes from NecromancerBloodPatch.BloodItemName
rather than a second literal, so the two cannot drift. A mod that is not blood
pays nothing and does not block the pull - the slot is meant for other things.

RUNS OUT, SHATTERS. The rule is "durability 0 or less", not "could not cover the
pull", and the difference is not cosmetic: under the second wording a flask with
exactly enough left would sit in the slot at zero, HasMods() would see "something
installed", and the bracelet would work for free forever. So the MaxUseTimes clamp
is gone and the slot is cleared with ItemValue.None - type 0, exactly what
IsEmpty() tests. The last pull always completes; the flask simply does not survive
it. The sound is vanilla's itembreak, the same cue ItemAction.HandleItemBreak
plays.

A guard: if MaxUseTimes comes out 0 (the passive effect or the tag removed), the
flask is NOT deleted and a warning naming the fix goes to the log. Without that
branch a config error would eat the player's item on the first pull and look like
a bug in the mechanic.

Localization: a new braceletSpatialVaultNoMod key in 13 languages, plus the flask
and bracelet descriptions - the mechanic became conditional and paid, and both
texts would have been lies without it.

Known and deliberate: the Blood Stone the empty-slot message promises does not
exist yet - it is planned, written up in BACKLOG.md. None of this is tested in
game.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MnwP2Dt1vk8bUPJ452EoVL
2026-09-15 22:32:59 +03:00

446 lines
38 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.
<config>
<!-- Модификации Ножа некроманта. Продиктовано 2026-09-07: "добавь в нож слоты для
модификаций. Но модификации там будут особые, именно для ножа некроманта, а не для
обычного ножа."
HOW THE EXCLUSIVITY WORKS (both halves verified against the decompiled assembly, not
guessed - see the long comment on the knife's Tags in items.xml for the full audit):
- installable_tags="necroKnife" is the positive half. The install UI checks
"InstallableTags.IsEmpty || itemClass.HasAnyTags(InstallableTags)"
(XUiC_ItemPartStack.CanSwap / XUiC_ItemStack / XUiM_AssembleItem all agree), matched
against the TARGET ITEM's Tags. necroWpnBladeNecroKnife is the only item anywhere -
vanilla or this mod - that carries "necroKnife", so these three fit it and nothing
else. Note the IsEmpty short-circuit above: a modifier with no installable_tags at all
goes into ANY item, so leaving it off would be the opposite of what was asked.
- The knife's own "noMods" tag is the negative half, blocking all 87 vanilla modifiers
from going the other way. That lives on the knife, not here.
WHY EACH ONE HAS ITS OWN modifier_tags: mods whose modifier_tags overlap are counted
against ItemClass.MaxModsAllowed, which defaults to 1 (ItemClass.cs line 376), and
CanSwap refuses once the count is reached. Giving all three a shared tag like
"necroKnifeMod" would therefore have let the player install exactly ONE of them at a time
- the four slots would be unusable. Distinct tags per mod is also what vanilla does
(damageBleed / barrelAttachment / droneArmor ...).
type="attachment" (not "mod") so they can be pulled back out and moved to another knife -
a type="mod" is permanent once installed.
ICONS UPDATED 2026-09-07: real generated art for all six, drawn by the user and dropped in
via c:\Exchange\exch. Each mod now points CustomIcon at its own sprite in
UIAtlases/ItemIconAtlas (TearsOfTheDead / ScavengersFeast / DeadMansGrip / GravesRepose /
DeadStorm / DarkSense, all 160x160 RGBA like the other 21). This replaced the old
reused-vanilla-sprite placeholders (drinkJarBoiledWater, foodShamSandwich,
modMeleeGraveDigger, modArmorInsulatedLinerT2, modMeleeStunBatonRepulsor,
modGunScopeSmall) - the same progression the Knife and Victim's Skin went through on
2026-08-29. CustomIconTint dropped from all six along with them: it existed only to stop
the borrowed vanilla sprites reading as the items they came from, and on purpose-drawn art
it would just darken the picture.
If a tint is ever needed again here, note the format trap that cost a round the first
time: CustomIconTint is HEX ("785AB4"), NOT the "R, G, B" triplet that TintColor takes on
the knife itself. They look interchangeable and are not - decompiled, ItemClass parses this
one with StringParsers.ParseHexColor while TintColor goes through the Color32 comma path,
and every vanilla CustomIconTint value is hex (FF00FF, 6441A5, C68C53...). -->
<append xpath="/item_modifiers">
<!-- "Слёзы мертвеца" - вода. Продиктовано 2026-09-07: "Если он вставлен в нож, то каждый
убитый зомби даёт 2 единицы воды."
IMPORTANT - this deliberately does NOT just add to $waterAmountAdd and stop there.
That CVar is only a QUEUE; on its own it hydrates nobody. Every vanilla drink
(drinkJarRiverWater, Data/Config/items.xml ~21744) pairs the add with
"AddBuff buffProcessConsumables", and it is buffProcessConsumables that adds
buffHealWaterMax (Data/Config/buffs.xml ~8527: AddBuff buffHealWaterMax requires
$waterAmountAdd GT 0), whose own onSelfBuffUpdate is what finally moves the number
into the water stat - .1 per .1s tick, so the 2 units land over about two seconds.
Missing that second line is exactly the bug that made the knife's lifesteal do
nothing at all for over a week (see items.xml), so it is spelled out here on purpose.
onSelfKilledOther is fired by ItemActionAttack (decompiled, lines ~798 and ~846) as
"entityAlive.FireEvent(MinEventTypes.onSelfKilledOther, flag4)", guarded by
"!wasAlreadyDead && entity.IsDead()" so it means a real kill, not a hit on a corpse.
flag4 is "the damaging item IS the held item", which is true for a melee swing - and
that same flag4 is what routes the event through Inventory -> ItemValue -> installed
Modifications. It is the identical argument the knife's already-working
onSelfAttackedOther effects ride on, so if those fire, this fires. (No vanilla
item_modifier happens to use onSelfKilledOther, but it is in item_modifiers.xml's own
documented TRIGGER LIST and the dispatch path above is unconditional.)
Gated to zombies via EntityTagCompare on "other" - killing a bear or a bird gives
nothing, matching how every other on-hit effect in this mod is gated. -->
<item_modifier name="necroModKnifeTearsOfTheDead" installable_tags="necroKnife" modifier_tags="necroKnifeWater" type="attachment">
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
<property name="CustomIcon" value="TearsOfTheDead"/>
<property name="EconomicValue" value="0"/>
<property name="SellableToTrader" value="false"/>
<effect_group tiered="false">
<requirement name="EntityTagCompare" target="other" tags="zombie"/>
<triggered_effect trigger="onSelfKilledOther" action="ModifyCVar" cvar="$waterAmountAdd" operation="add" value="2"/>
<triggered_effect trigger="onSelfKilledOther" action="AddBuff" buff="buffProcessConsumables"/>
</effect_group>
</item_modifier>
<!-- "Пир падальщика" - еда. Продиктовано 2026-09-07: "Следующий мод, на еду. Принцип тот
же." - same 2 units, same per-kill trigger, same zombie gate.
Food rides the mirror-image path of the water mod above: $foodAmountAdd is the queue,
buffProcessConsumables is what notices it (Data/Config/buffs.xml ~8525: AddBuff
buffHealFood requires $foodAmountAdd GT 0), buffHealFood is what actually feeds you.
One AddBuff covers both mods if they are installed together - buffProcessConsumables
checks water and food independently and hands out whichever buffs apply, so stacking
the two mods costs nothing extra and neither one cancels the other. -->
<item_modifier name="necroModKnifeScavengersFeast" installable_tags="necroKnife" modifier_tags="necroKnifeFood" type="attachment">
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
<property name="CustomIcon" value="ScavengersFeast"/>
<property name="EconomicValue" value="0"/>
<property name="SellableToTrader" value="false"/>
<effect_group tiered="false">
<requirement name="EntityTagCompare" target="other" tags="zombie"/>
<triggered_effect trigger="onSelfKilledOther" action="ModifyCVar" cvar="$foodAmountAdd" operation="add" value="2"/>
<triggered_effect trigger="onSelfKilledOther" action="AddBuff" buff="buffProcessConsumables"/>
</effect_group>
</item_modifier>
<!-- "Хватка мертвеца" - замедление. Chosen by the user from the proposed list 2026-09-07.
Reuses buffInjurySlow, the same vanilla debuff the mod's own Зомбособака already
applies on its bite (necroMeleeHandZombieDog in items.xml) - proven working in this
mod rather than a fresh guess, and the same EntityTagCompare zombie gate.
onSelfAttackedOther, not onSelfKilledOther: the point is to slow a zombie that is
still coming at you, so it has to land on the hit, not on the kill. -->
<item_modifier name="necroModKnifeDeadMansGrip" installable_tags="necroKnife" modifier_tags="necroKnifeSlow" type="attachment">
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
<property name="CustomIcon" value="DeadMansGrip"/>
<property name="EconomicValue" value="0"/>
<property name="SellableToTrader" value="false"/>
<effect_group tiered="false">
<requirement name="EntityTagCompare" target="other" tags="zombie"/>
<triggered_effect trigger="onSelfAttackedOther" action="AddBuff" target="other" buff="buffInjurySlow"/>
</effect_group>
</item_modifier>
<!-- "Могильный покой" - защита от переохлаждения и перегрева. Продиктовано 2026-09-07:
"Пусть защищает от переохлаждения и перегрева... Есть параметр устойчивости к холоду и
жаре. Он бывает на предметах одежды и на некоторых модах на одежду."
Найденные параметры - HypothermalResist (холод) и HyperthermalResist (жара). Живой
ванильный образец: modArmorInsulatedLinerT1/T2/T3 (Data/Config/item_modifiers.xml
~1873), они ставят ровно эту пару. Величина у них по тирам: T1 1->2.5, T2 2.8->4.3,
T3 4.6->6 на ОДИН элемент брони, а элементов четыре.
ЗНАЧЕНИЕ 5 -> 50, 2026-09-13, прямое указание пользователя ("по факту она поднимает
сопротивление всего на 5, а надо на 50"). Изначально стояло 5 - примерно уровень одной
детали брони с T3-подкладкой, и ровно то число, которым ваниль пользуется во
вкомментированных modArmorInsulatedLiner/modArmorCoolingMesh. Это было осознанно
скромно; пользователь хочет иначе, и его решение тут главнее моей балансной оценки.
ЧТО 50 ОЗНАЧАЕТ НА САМОМ ДЕЛЕ, раз единица - градусы, а не проценты (формула ниже):
любая уличная температура в пределах 50 градусов от комфортных 70 подтягивается К 70
ЦЕЛИКОМ, потому что там стоит min/max-ограничение. То есть от 20 до 120 по шкале игры
это не "сильная защита", а полный иммунитет: и снежная вершина, и пустынный полдень
перестают быть угрозой. Это примерно в 10 раз больше, чем даёт набор брони с
T3-подкладками на всех четырёх деталях. Записано не в укор, а чтобы через месяц не
пришлось гадать, почему термометр перестал что-либо значить.
ЕДИНИЦА ИЗМЕРЕНИЯ - градусы, на которые сдвигается уличная температура в сторону
комфортной, а не проценты (PlayerEntityStats, декомпиляция):
if (outsideTemperature < 70) { v = GetValue(HypothermalResist);
outsideTemperature = min(70, outsideTemperature + v); }
else { v = GetValue(HyperthermalResist);
outsideTemperature = max(70, outsideTemperature - v); }
ВАЖНО - РАБОТАЕТ ТОЛЬКО ПОКА НОЖ В РУКАХ. Это не оплошность, а то, как движок вообще
умеет учитывать не-броню, и проверено по всей цепочке, потому что термостойкость - это
обычно броневой стат, а нож не броня:
1. PlayerEntityStats зовёт EffectManager.GetValue(HypothermalResist, null, 0f, entity)
со всеми параметрами по умолчанию, а в сигнатуре GetValue значения по умолчанию -
calcHoldingItem: true и useMods: true. То есть предмет в руках и его моды учитываются.
2. Внутри GetValue ветка "else if (calcHoldingItem && ...)" зовёт Inventory.ModifyValue.
3. Inventory.ModifyValue пропускает предмет, если его теги попадают в
ignoreWhenHeld = FastTags.Parse("clothing,armor"). У ножа ни того, ни другого нет,
так что он проходит - а вот на реальной броне в руках это бы не сработало.
4. ItemValue.ModifyValue в конце обходит Modifications[j].ModifyValue под флагом
_useMods. Заметь: здесь, в отличие от FireEvent, НЕТ отсечки "if (!HasQuality)
return;" - пассивки модов считаются независимо от качества. Но нож всё равно уже
починен по tiered (см. items.xml), так что вопрос снят в обе стороны.
Убрал нож в рюкзак - защита пропала. Так и задумано, и так это описано в Localization.csv.
Пассивка, а не triggered_effect, поэтому ни onSelfKilledOther, ни гейта на зомби здесь
нет - эффект просто висит, пока нож в руке.
Иконка - спрайт настоящего ванильного мода-подкладки (у item_modifier без своего
CustomIcon спрайт называется как он сам), то есть по смыслу ровно та картинка. Тинт
тот же фиолетовый, что у остальных трёх, до появления собственной графики. -->
<item_modifier name="necroModKnifeGravesRepose" installable_tags="necroKnife" modifier_tags="necroKnifeThermal" type="attachment">
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
<property name="CustomIcon" value="GravesRepose"/>
<property name="EconomicValue" value="0"/>
<property name="SellableToTrader" value="false"/>
<effect_group tiered="false">
<passive_effect name="HypothermalResist" operation="base_add" value="50"/>
<passive_effect name="HyperthermalResist" operation="base_add" value="50"/>
</effect_group>
</item_modifier>
<!-- "Мёртвая буря" - переделывает силовую атаку. Продиктовано 2026-09-07: "-5 HP повышаем
до -10 HP. Эффект делаем AOE в радиусе 20 блоков (если это много, то скажи). Расход
стамины на силовую атаку увеличиваем вдвое. Добавляем дебаф кровотечения и искрения."
ФОРМА И РАЗМЕР ОБЛАСТИ. Пользователь потом уточнил: "Нужна именно сфера. Иначе птиц не
заденет." Сфера не нужна и недоступна - движок умеет ровно одну форму, и она уже
объёмная. MinEventActionTargetedBase, ветка otherAOE:
entsInRange = World.GetLivingEntitiesInBounds(_params.Self,
new Bounds(_params.Other.position, Vector3.one * (maxRange * 2f)));
Unity-шный Bounds(center, SIZE) берёт РАЗМЕР, а не extents, здесь size = range*2 по
каждой оси - то есть это КУБ, расходящийся на range блоков во все шесть сторон,
ВКЛЮЧАЯ вверх и вниз. Дальше World.GetLivingEntitiesInBounds обходит чанки по X/Z и
сверяет коробки; никакого доп. фильтра по дистанции после выборки нет (см. цикл сразу
за вызовом - только isValidTarget и singleTargetCheck, оба про теги, не про радиус).
Поэтому вертикаль покрыта уже сейчас, и куб для летящей цели даже ЛУЧШЕ сферы: сфера
радиуса R целиком помещается внутрь куба с полу-размером R.
Заодно из этого следует, что 20 - это очень много: куб 40x40x40 вокруг цели, сквозь
стены и перекрытия (проверки линии взгляда тут нет вовсе). Весь ванильный диапазон
AOE для сравнения: 1.1, 1.3, 1.4, 2.7, 3, 6 и ровно один случай 10
(buffRingOfFireEffect). Поставлено 6 - это куб 12x12x12, то есть 6 блоков вверх, чего
хватает на пикирующего стервятника, и при этом не выкашивает соседний этаж POI. Если
нужно доставать высоко кружащих птиц - поднимать; помнить, что то же число уходит и
в горизонталь. Менять - четыре атрибута range ниже.
ЧТО ЭТОТ МОД ДОБАВЛЯЕТ К БАЗОВОЙ СИЛОВОЙ. Мод умеет только ДОБАВЛЯТЬ эффекты, снять
собственные эффекты предмета он не может, поэтому "-5 HP -> -10 HP" сделано вторым
списанием на 5, а не переписыванием первого. Порядок детерминирован: в
ItemValue.FireEvent собственные эффекты предмета (itemClass.FireEvent) идут ДО цикла
по Modifications, так что сперва снимается базовая пятёрка, затем эта. Оба списания
несут свой порог "Health GT 5", поэтому убить себя силовой атакой по-прежнему нельзя;
следствие - при здоровье между 5 и 10 спишется только часть.
ПРО СТАМИНУ. База берётся в ItemActionDynamicMelee.cs:384:
_actionData.StaminaUsage = EffectManager.GetValue(PassiveEffects.StaminaLoss,
itemValue, 2f, holdingEntity, null, _actionData.ActionTags) * StaminaUsageMultiplier;
- значение по умолчанию 2 (своего StaminaLoss у этого ножа нет), и читается оно с
ActionTags, то есть "secondary" для силовой. GetValue возвращает
_originalValue * _perc_value, где _perc_value стартует с 1, а perc_add к нему
прибавляет - поэтому value="1" это ровно "вдвое", а не "+1".
ПРО КРОВОТЕЧЕНИЕ - две строки, а не одна, и это не перестраховка. buffInjuryBleeding
снимает сам себя, если у цели bleedCounter == 0:
<triggered_effect trigger="onSelfBuffUpdate" action="RemoveBuff" buff="buffInjuryBleeding">
<requirement name="CVarCompare" cvar="bleedCounter" operation="Equals" value="0"/>
а урон берёт как HealthChangeOT base_subtract @$bleedAmount, где $bleedAmount
выставляется из bleedCounter на старте баффа. Просто повесить бафф - он мгновенно
снимется, не сделав ничего. Поэтому сначала счётчик, потом бафф - как и у ванильного
modMeleeSerratedBlade. Счётчик задан через "set 2", а не "add 1", чтобы не зависеть от
$maxBleedCounter (его выставляют перки игрока, buffs.xml ~530) и чтобы эффект не
накапливался бесконечно от серии ударов. 2 -> 2 HP/сек в течение 20 секунд.
ПРО ИСКРЕНИЕ - это ванильный buffShocked, тот самый, что вешают электродубинка и
электрозабор. Своя частица p_electric_shock и звук electric_fence_impact уже внутри
баффа, подключать отдельно нечего. По умолчанию 4 секунды и -5 HP/сек через
HealthChangeOT. Его замедление лежит в ОТДЕЛЬНЫХ effect_group, гейтованных на
$shockDurationMax >= 4 - это важно для птиц, см. ниже.
СРАБОТАЕТ ЛИ НА ПТИЦ (прямой вопрос пользователя) - да, в той части, которая наносит
урон:
+ Стервятник попадает в выборку: коробка трёхмерная, вертикаль включена.
+ Тег есть: animalZombieVulture несёт "entity,animal,zombie,zombieAnimal,hostile,
vulture,special", а target_tags="zombie" гейтит именно по нему.
+ Кровотечение работает: HealthChangeOT применяется в EntityStats.cs:142, это общий
путь для любого EntityAlive, полёт ему безразличен.
+ Урон от искрения работает по той же причине.
- Замедление от buffShocked по птице НЕ отработает: оно давит на RunSpeed/WalkSpeed,
а EntityVulture.cs (~563/567) умножает на сырые поля moveSpeed/moveSpeedAggro и
геттеры GetMoveSpeed()/GetMoveSpeedAggro() - единственные, кто эти пассивки
применяет - не зовёт вовсе.
? Ragdoll по летящей цели - НЕ ПРОВЕРЕНО. Ни в EntityVulture, ни в EntityFlying нет
ни строчки про ragdoll, полёт-специфичной поддержки точно нет. Проверять в игре.
Итого по птице: сбить с ног скорее всего не выйдет, но кровотечение и разряд догрызут.
Иконка-заглушка - спрайт ванильного мода-репульсора электродубинки, тематически
ближайшее, что есть готового. -->
<item_modifier name="necroModKnifeDeadStorm" installable_tags="necroKnife" modifier_tags="necroKnifePower" type="attachment">
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
<property name="CustomIcon" value="DeadStorm"/>
<property name="EconomicValue" value="0"/>
<property name="SellableToTrader" value="false"/>
<effect_group tiered="false">
<!-- Вдвое дороже по стамине, только силовая. -->
<passive_effect name="StaminaLoss" operation="perc_add" value="1" tags="secondary"/>
<!-- Вторая половина платы: базовые 5 + эти 5 = 10 HP. -->
<triggered_effect trigger="onSelfSecondaryActionRayHit" action="ModifyStats" stat="Health" operation="subtract" value="5">
<requirement name="EntityTagCompare" target="other" tags="zombie"/>
<!-- "Health GT 0" на цели - тот же гейт на трупы, что у самого ножа, чтобы
силовой удар по трупу не брал 5 HP впустую; подробный разбор в items.xml
у лечения. AOE-строкам ниже он не нужен: GetLivingEntitiesInBounds сама
отсеивает мёртвых. -->
<requirement name="StatCompareCurrent" target="other" stat="Health" operation="GT" value="0"/>
<requirement name="StatCompareCurrent" stat="Health" operation="GT" value="5"/>
</triggered_effect>
<!-- AOE вокруг задетой цели. Базовое сбивание с ног у ножа одиночное, это его
расширяет; двойное попадание по самой цели безвредно - Ragdoll это действие,
а не стакающийся бафф. -->
<triggered_effect trigger="onSelfSecondaryActionRayHit" action="Ragdoll" target="otherAOE" range="6" target_tags="zombie" duration="1.5" force="150"/>
<!-- Кровотечение: сначала счётчик, затем бафф - см. большой комментарий выше. -->
<triggered_effect trigger="onSelfSecondaryActionRayHit" action="ModifyCVar" target="otherAOE" range="6" target_tags="zombie" cvar="bleedCounter" operation="set" value="2"/>
<triggered_effect trigger="onSelfSecondaryActionRayHit" action="AddBuff" target="otherAOE" range="6" target_tags="zombie" buff="buffInjuryBleeding"/>
<!-- Искрение. -->
<triggered_effect trigger="onSelfSecondaryActionRayHit" action="AddBuff" target="otherAOE" range="6" target_tags="zombie" buff="buffShocked"/>
</effect_group>
</item_modifier>
<!-- "Тёмное чутьё" - радар зомби, пока нож в руке. Вся механика и разбор, почему это
вообще выполнимо на XML, лежат в комментарии к buffNecroDarkSense в Config/buffs.xml -
здесь только выключатель.
onSelfEquipStart / onSelfEquipStop - ванильный паттерн для "работает, только пока
предмет в руках" (так сделаны эффекты кирок, Data/Config/items.xml ~158 и ~175).
Оба события шлёт Inventory на ItemValue держимого предмета (строки ~1226 и ~1245), а
ItemValue.FireEvent прокидывает их в установленные Modifications - при условии, что у
предмета есть качество, что у этого ножа теперь так (см. правку tiered в items.xml).
Бафф снимается и при смерти игрока: remove_on_death у него не выставлен, а по
умолчанию это true - то есть залипнуть после респавна он не может. -->
<item_modifier name="necroModKnifeDarkSense" installable_tags="necroKnife" modifier_tags="necroKnifeSense" type="attachment">
<property name="Extends" value="modGeneralMaster" param1="CustomIcon"/>
<property name="CustomIcon" value="DarkSense"/>
<property name="EconomicValue" value="0"/>
<property name="SellableToTrader" value="false"/>
<effect_group tiered="false">
<triggered_effect trigger="onSelfEquipStart" action="AddBuff" buff="buffNecroDarkSense"/>
<triggered_effect trigger="onSelfEquipStop" action="RemoveBuff" buff="buffNecroDarkSense"/>
</effect_group>
</item_modifier>
</append>
<!-- "Кровь некроманта" (Necromancer's Blood) - ПЕРЕЕХАЛА СЮДА ИЗ items.xml 2026-09-15.
Указание: «пусть флакон крови можно будет вставлять как модификацию для пространственного
хранилища» (уточнено: именно Крови некроманта).
ПОЧЕМУ ПЕРЕЕЗД, А НЕ НОВОЕ СВОЙСТВО. XUiC_ItemPartStack.CanSwap начинается с
if (!(stack.itemValue.ItemClass is ItemClassModifier itemClassModifier)) return false;
Слот модификации не смотрит ни на теги, ни на свойства, пока предмет не ItemClassModifier -
а этот класс создаётся только из <item_modifier>. Свойства вида "CanBeInstalled" в игре нет;
остаться ресурсом в items.xml и при этом вставляться в браслет физически нельзя.
ЧТО ПРИ ЭТОМ НЕ СЛОМАЛОСЬ - проверено по декомпилятору, а не понадеялись:
- Рецепты. ItemClassModifier наследует ItemClass, а имена регистрируются в ОДНОМ общем
ItemClass.nameToItem (ItemClass.cs:624). Recipe ищет ингредиент через
ItemClass.GetItemClass, то есть по тому же словарю - все четыре рецепта на крови
(10 на Чёрный портал, 3 на Пирамиду, два по одной) резолвятся как раньше.
- Свой рецепт самой крови и Harmony-патч NecromancerBloodPatch.cs - оба по имени
"resourceNecromancerBlood", имя не менялось.
- Сейв. Айди предметов раздаются при загрузке, но ItemClass.assignIdsFromMapping берёт их
из сохранённого в мире name->id мэппинга (nameIdMapping.GetIdForName), так что предмет в
старом сейве не превратится в другой из-за перестановки в конфигах.
- Локализация. Ключи "resourceNecromancerBlood"/"...Desc" те же, файл не трогали.
ЧЕГО НЕТ - у этой модификации пока НЕТ НИ ОДНОГО ЭФФЕКТА ДЛЯ БРАСЛЕТА, и это не
забывчивость: указание было про "вставлять", а что именно она даёт - не сказано.
effect_group ниже существует ради прочности самого флакона, а не ради браслета.
ЛОВУШКА, КОТОРУЮ ПРИШЛОСЬ ОБОЙТИ: у модификации effect_group применяется К ПРЕДМЕТУ, В
КОТОРЫЙ ЕЁ ВСТАВИЛИ. Прочность 1000 (сделана 2026-09-15, разбор в BACKLOG.md) живёт именно
в effect_group - и, переехав сюда, она стала бы выдавать 1000 прочности БРАСЛЕТУ. Поэтому
пассивка гейтована tags="necroBloodFlask", а сам тег добавлен в Tags флакона:
ItemValue.MaxUseTimesBase зовёт GetValue с ItemClass.ItemTags того предмета, для которого
считает, так что у флакона совпадение есть, а у браслета
(T0,weapon,attPerception,noMods,necroBracelet) - нет. Тот же механизм, что у "Мёртвой бури"
с tags="secondary", только фильтр не по действию, а по предмету.
installable_tags="necroBracelet" - положительная половина, ровно как требует комментарий к
Tags браслета в items.xml: без неё модификация лезла бы в ЛЮБОЙ предмет
(CanSwap короткозамыкается на InstallableTags.IsEmpty).
blocked_tags НЕ задан намеренно: у браслета в тегах есть "noMods", и объявить его здесь
значило бы заблокировать самому себе установку.
modifier_tags="necroBraceletBlood" - свой, ни с чем не пересекающийся: моды с общим
modifier_tags считаются против MaxModsAllowed (по умолчанию 1). Слот у браслета с
2026-09-15 всего один, так что слотов это больше не съедает - но тег всё равно должен быть
свой: общий сделал бы две модификации браслета взаимоисключающими и там, где интерфейс
этого никак не объясняет.
type="attachment" - флакон можно вынуть обратно; type="mod" вставляется навсегда. -->
<append xpath="/item_modifiers">
<item_modifier name="resourceNecromancerBlood" installable_tags="necroBracelet" modifier_tags="necroBraceletBlood" type="attachment">
<property name="Extends" value="medicalBloodBag"/>
<property name="DescriptionKey" value="resourceNecromancerBloodDesc"/>
<!-- Своя рисованная иконка, 2026-08-30. CustomIcon задаётся явно даже при Extends -
тот же урок, что у всех предметов мода: Extends сам по себе рабочей иконки не даёт. -->
<property name="CustomIcon" value="NecromantsBlood"/>
<!-- Своя банка с кровью, 2026-09-10 (указание: «берём чай из золотарника, и жёлтое
заменяем на кровавый цвет, с фиолетовыми оттенками»). Заодно чинилось расхождение
текста и модели: описание говорит «Банка, наполненная кровью», а наследуемый
medicalBloodBag показывал sackPrefab - обычный мешок.
ПОЧЕМУ НЕ ХВАТИЛО ТИНТА - ПРОВЕРЕНО В ИГРЕ. Сначала пробовали дёшево, без бандла:
ванильный префаб чая плюс TintColor. Банка осталась чаем из золотарника - тинт
предмета на этот меш НЕ ПОДЕЙСТВОВАЛ ВООБЩЕ: у шейдера Game_EntityTintMaskSSS
выигрывает собственный _Color материала. Поэтому TintColor здесь не задаётся
совсем - он ничего не даёт и только вводил бы в заблуждение.
И по сути: кровь отличается от чая не цветом, а тем, что она непрозрачная, тёмная
и густая, с плёнкой на стекле. Жидкость ПЕРЕРИСОВАНА по яркости, а не перекрашена
множителем - генератор _private/tools/make_necroblood_textures.py.
HoldType 3 - хват банки вместо 45 (мешок), Material Mglass - стекло вместо ткани
(звук удара и осколки при разбитии). -->
<property name="Meshfile" value="#@modfolder(NecromancerTome):Resources/necroblood?necroBloodPrefab.prefab"/>
<property name="HoldType" value="3"/>
<property name="Material" value="Mglass"/>
<!-- Теги medical/medicalSkill - те же, что давал medicalBloodBag через Extends;
перечислены явно, потому что своя строка Tags наследуемую перекрывает целиком.
necroBloodFlask добавлен ради гейта прочности - см. большой комментарий выше.
Внимание на будущее: ItemClassModifier ПЕРЕОПРЕДЕЛЯЕТ HasAnyTags на ModifierTags,
так что на вопрос "есть ли у этого предмета тег X" у модификации отвечают
modifier_tags, а не эта строка. Здешние теги читает только то, что лезет в
ItemTags напрямую - как раз GetValue с прочностью. -->
<property name="Tags" value="medical,medicalSkill,necroBloodFlask"/>
<!-- По одной банке на ячейку, указание 2026-09-15. Наследуемый medicalBloodBag даёт
15, и это надо перебивать явно: Extends копирует свойство целиком. Дорого и так
задумано - Чёрный портал просит десять банок, то есть десять ячеек. -->
<property name="Stacknumber" value="1"/>
<!-- Прочность 1000, указание 2026-09-15. Две разные ручки, обе обязательны:
ShowQuality рисует полоску (XUiC_ItemStack.ShowDurability => ItemClass.ShowQualityBar),
а само число идёт пассивным эффектом DegradationMax
(ItemValue.MaxUseTimesBase => EffectManager.GetValue(PassiveEffects.DegradationMax)).
Без эффекта прочность равна нулю, а полоска при MaxUseTimes == 0 рисуется ПОЛНОЙ -
то есть забытый эффект выглядит как "всё работает".
tiered="false" обязателен: ItemClass.HasQuality читается как Effects.IsOwnerTiered(),
и тированная группа превратила бы банку в предмет с качеством - тиры, рамка.
Прочность пока НЕ УБЫВАЕТ: UseTimes растёт только от ItemAction-ов, а у крови их
нет. Решение пользователя 2026-09-15: "прочность будет убавляться, но это
реализуем позже". -->
<property name="ShowQuality" value="true"/>
<!-- true: опустевшая банка должна исчезать, а не лежать "сломанной" в ожидании
ремонта, как топор. Ванильные инструменты ставят false именно потому, что их чинят. -->
<property name="DegradationBreaksAfter" value="true"/>
<property name="EconomicValue" value="0"/>
<effect_group name="resourceNecromancerBlood" tiered="false">
<passive_effect name="DegradationMax" operation="base_set" value="1000" tags="necroBloodFlask"/>
</effect_group>
</item_modifier>
</append>
</config>