Книга некроманта 1.0 — первая публичная версия

Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых
зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида
духов и сюжетный финал через Чёрный портал. Локализация на 13 языках.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
This commit is contained in:
Alex Cube
2026-09-09 21:13:03 +03:00
co-authored by Claude Opus 5
commit e8f064f5ec
102 changed files with 7010 additions and 0 deletions
File diff suppressed because one or more lines are too long
+119
View File
@@ -0,0 +1,119 @@
<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>
</windows>
+45
View File
@@ -0,0 +1,45 @@
<xui>
<!-- Pairs with Config/XUi_InGame/windows.xml's new "videoPlayer" window - see that file's
comment for the full root-cause explanation. Copied verbatim from
Data/Config/XUi_Menu/xui.xml's own "videoPlayer" window_group (no controller= on the
group itself there - the controller lives on the window element instead, same as this
mod's copy). Without this entry the window from windows.xml exists but has no group to be
opened by name through (XUiC_VideoPlayer.PlayVideo/GetInstance both resolve via the GROUP
id, not the raw window). -->
<append xpath="/xui">
<window_group name="videoPlayer">
<window name="videoPlayer" />
</window_group>
</append>
<!-- Группы для шести финальных слайдов (см. XUi_InGame/windows.xml). Как и у videoPlayer,
контроллера на группе нет - окно чисто декоративное, открывается и закрывается по имени
из HarmonySrc/FinalSlides.cs. -->
<append xpath="/xui">
<window_group name="necroFinalSlide1">
<window name="necroFinalSlide1" />
</window_group>
<window_group name="necroFinalSlide2">
<window name="necroFinalSlide2" />
</window_group>
<window_group name="necroFinalSlide3">
<window name="necroFinalSlide3" />
</window_group>
<window_group name="necroFinalSlide4">
<window name="necroFinalSlide4" />
</window_group>
<window_group name="necroFinalSlide5">
<window name="necroFinalSlide5" />
</window_group>
<window_group name="necroFinalSlide6">
<window name="necroFinalSlide6" />
</window_group>
</append>
<!-- Группа для чёрного экрана эпилога (см. windows.xml). -->
<append xpath="/xui">
<window_group name="necroFinalBlack">
<window name="necroFinalBlack" />
</window_group>
</append>
</xui>
+90
View File
@@ -0,0 +1,90 @@
<config>
<!-- "Пирамида Ереси" (Pyramid of Heresy) - user request 2026-08-31: a placeable ward that
periodically applies the existing Deviator charm (see HarmonySrc/PyramidWardPatch.cs,
reusing HarmonySrc/CharmPatch.cs's buffNecroDeviatorCharm wholesale - no new charm logic
was written, this just triggers the same buff on a timer) to any un-charmed zombie that
wanders into its radius, turning it against other zombies instead of the player/base - a
"barrier" in effect, not in physics: it never pushes or physically blocks anything, it
flips allegiance.
SHAPE: Shape="New" + Model="@:Shapes/pyramid.fbx" - the same standalone pyramid mesh
vanilla itself ships and uses for imposterPyramid's own LOD model (Data/Config/blocks.xml,
"*** blockNextGen imposters" section) - a real, solid, always-loaded asset, not invented
for this mod. Checked the "tentBiohazardRoofXxx" tent-roof pieces first (they're the
nearest thing to a "block with a pyramid top" already in vanilla) but rejected them - all
of them are 3x1x1/3x2x3 multi-blocks meant as building-sized tent roofs, wrong shape/scale
for a single placeable ward. Red per the user's request via TintColor="8A4142" - the exact
hex vanilla's own tentBiohazardRoofRed/tentBiohazardRoofCenterRed use for their red tint,
reused here rather than inventing a new one.
FIXED 2026-08-31 (user report: placed fine but genuinely invisible - "нету префаба"):
missing `Texture` property. EVERY vanilla block using this exact Shape="New" + a custom
standalone Model="@:Shapes/....fbx" (imposterPyramid itself, imposterQuarter,
imposterCTREighth, poiFillerBlock - checked all of them directly) always pairs it with an
explicit `Texture="N"` atlas index; ours never had one, which is the real, simple root
cause. Also switched Material from plain "Msteel" to "Msteel_shapes" + Texture="356" -
the exact Material/Texture pair vanilla's own `steelShapes` block (the real "shapes=All"
steel building family) uses for this same "New" shape renderer - plain "Msteel" is meant
for non-shape steel blocks (doors, hatches) and was never a matched pair with Shape="New"
to begin with. TintColor="8A4142" still multiplies on top as before.
User specifically asked to reuse the "Пирамида 1 м" ("Pyramid 1m") shape/prefab -
genuinely tried, but that specific shape turned out to be a NAMED VARIANT of the engine's
procedural multi-shape building system (`shapes="All"` blocks like `steelShapes`/
`concreteShapes` - see those in vanilla blocks.xml), not a standalone asset with its own
file path at all: decompiled `BlockShapeNew` (the C# class behind every Shape="New" block)
directly and it contains NO "Pyramid" string anywhere - the mapping from a shape's display
name to its actual geometry lives in compiled/binary shape-node data this decompile
couldn't reach, not in anything grep-able or copyable as a `Model=` path. Reproducing that
exact shape would mean reverse-engineering that binary shape data, real extra work with an
uncertain payoff - what shipped instead is the closest already-proven-working equivalent:
the same "New"-shape pyramid mesh `imposterPyramid` itself uses, now actually configured
correctly (see the fix above) so it renders for real. If the proportions look wrong next to
the in-game "Pyramid 1m" (e.g. too tall/full-block instead of a short 1m cap), that's a
scale tweak once it's visible, not a rendering bug - say so and it can be resized.
REWRITTEN 2026-09-01, Class changed from none to Class="CompositeTileEntity" +
CompositeFeatures/TEFeaturePyramidWard - two direct user findings drove this:
1. "Навожу прицел, но подсказка про E не появляется" - the original plain-Block +
Harmony-patches-on-Block approach never showed the E-prompt at all, because a
SEPARATE gate method (Block.HasBlockActivationCommands) was never patched and always
said "nothing to activate" for a plain decorative block.
2. "Сделай TileEntity" - direct request for the effect/zone toggle state to actually
survive a save/reload, which the old static-Dictionary-in-Harmony-patch version could
not do at all (documented as a known caveat before, now actually fixed).
Same syntax vanilla's own keystoneBlock (Land Claim) already uses for this exact
Class="CompositeTileEntity" + CompositeFeatures pattern - see HarmonySrc/
PyramidWardPatch.cs's own class doc comment on TEFeaturePyramidWard for the full decompiled
reasoning (why this is the sanctioned extension point, not a hardcoded-switch hack; the
E-menu/persistence/per-tick mechanics all live there now, not in this XML file). -->
<append xpath="/blocks">
<block name="necroHeresyPyramid">
<property name="CreativeMode" value="Player" />
<property name="DescriptionKey" value="necroHeresyPyramidDesc" />
<property name="Class" value="CompositeTileEntity" />
<property class="CompositeFeatures">
<property class="TEFeaturePyramidWard" />
</property>
<property name="Material" value="Msteel_shapes" />
<property name="Shape" value="New" />
<property name="Model" value="@:Shapes/pyramid.fbx" />
<property name="Texture" value="356" />
<property name="TintColor" value="8A4142" />
<!-- Real generated art (exch/pyramidOfSpirit.png, copied to
UIAtlases/ItemIconAtlas/) added 2026-09-02, per direct instruction - no
CustomIconTint alongside it (same lesson as every other real icon in this mod,
e.g. resourceZombieAsh: a real drawn icon already has its own correct color, tinting
it on top would recolor it unintentionally). TintColor above is unaffected - that's
the 3D block MESH's tint, a completely separate thing from the 2D inventory icon. -->
<property name="CustomIcon" value="pyramidOfSpirit" />
<property name="Group" value="Decoration" />
<property name="FilterTags" value="MC_building,SC_decor" />
<property name="Collide" value="movement,melee,bullet,arrow,rocket" />
<property name="StabilitySupport" value="false" />
<property name="MaxDamage" value="2000" />
<property name="EconomicValue" value="0" />
<property name="SellableToTrader" value="false" />
<drop event="Destroy" name="resourceScrapIron" count="10,20" />
</block>
</append>
</config>
+188
View File
@@ -0,0 +1,188 @@
<config>
<!-- Step 1: lifetime zombie kill counter display.
buffStatusCheck01 is the vanilla hidden buff every player always has,
used here purely as a hook to (re)apply our display buff on game entry/respawn. -->
<append xpath="buffs/buff[@name='buffStatusCheck01']">
<effect_group name="necro zombie kill tracker">
<triggered_effect trigger="onSelfEnteredGame" action="AddBuff" buff="buffNecroZombieKillTrackerDisplay"/>
<triggered_effect trigger="onSelfRespawn" action="AddBuff" buff="buffNecroZombieKillTrackerDisplay"/>
</effect_group>
</append>
<append xpath="/buffs">
<!-- Never expires (duration 0): a persistent lifetime total, not a per-day/session counter. -->
<buff name="buffNecroZombieKillTrackerDisplay" icon="ui_game_symbol_zombie" icon_color="150,0,0" name_key="buffNecroZombieKillTrackerDisplayName" description_key="buffNecroZombieKillTrackerDisplayDesc">
<stack_type value="ignore"/>
<duration value="0"/>
<update_rate value=".1"/>
<display_value value="necroZombieKillsCVar"/>
<!-- Нож некроманта (BACKLOG.md item 5, user request 2026-08-28): "урон умножается на
скилл некроманта ... и делится на 10". Computed here (this buff already ticks
10x/sec on every player via update_rate) rather than on the knife item itself,
since a held item has no update tick of its own - reading a live CVar from a
passive_effect (confirmed working: vanilla's own $PlayerLevelBonus, computed in
buffs.xml and read elsewhere via passive_effect value="@$PlayerLevelBonus") needs
something that keeps recomputing it. $necroKnifeDamageCVar = necroZombieKillsCVar
/ 10, read by necroWpnBladeNecroKnife's own EntityDamage passive_effect in
items.xml. $necroKnifeHealCVar = half of that, read by the same item's lifesteal
trigger. Both "@cvar" (write) and "@$cvar" (write) forms exist in vanilla data for
plain vs. "$"-prefixed cvars - $-prefix here just follows the naming convention
vanilla uses for these player-effect scratch values, not a different mechanism.
BUG FIXED 2026-08-28 (knife dealt no damage, no lifesteal, victim debuff never
applied): these four triggered_effect elements were direct children of <buff>,
with no <effect_group> wrapper - unlike buffNecroDeviatorCharm below or vanilla's
own buffStatusCheck01 (Data/Config/buffs.xml line ~334), which both wrap every
triggered_effect in one. triggered_effect isn't valid straight under <buff> (only
things like display_value are) - the loader didn't throw on this (no "buffs.xml
failed to load" the way loot.xml did for its own ordering bug), it just silently
never ran them, so $necroKnifeDamageCVar/$necroKnifeHealCVar never got computed
and the knife's damage passive_effect read an undefined (0) value the whole time. -->
<effect_group name="necro knife damage tracking">
<triggered_effect trigger="onSelfBuffUpdate" action="ModifyCVar" cvar="$necroKnifeDamageCVar" operation="set" value="@necroZombieKillsCVar"/>
<triggered_effect trigger="onSelfBuffUpdate" action="ModifyCVar" cvar="$necroKnifeDamageCVar" operation="divide" value="10"/>
<triggered_effect trigger="onSelfBuffUpdate" action="ModifyCVar" cvar="$necroKnifeHealCVar" operation="set" value="@$necroKnifeDamageCVar"/>
<triggered_effect trigger="onSelfBuffUpdate" action="ModifyCVar" cvar="$necroKnifeHealCVar" operation="divide" value="2"/>
</effect_group>
</buff>
<!-- "Жертва" (Victim) marker - applied by the Necromancer's Knife on hit (see
necroWpnBladeNecroKnife in items.xml). HarmonySrc/VictimPatch.cs reacts to its
presence at the moment the zombie actually dies (patches Entity.DropBagServer) to
force-spawn the guaranteed green "Жертва" bag instead of the normal loot roll -
nothing in XML alone can make Entity.DropBagServer's LootDropEntityClass choice
conditional on a live buff, confirmed by decompiling it (that property is a static
per-species entity_class value, not CVar-driven like EntityDamage above).
BUG FIXED 2026-08-28 (buff kept vanishing before death-loot checked it, despite being
permanent - duration="0" confirmed via decompiling BuffClass to genuinely mean "never
expires", not the bug): missing remove_on_death="false". EntityAlive.ClientKill()
calls Buffs.OnDeath() BEFORE OnEntityDeath()/dropItemOnDeath() ever run (confirmed by
decompiling both) - EntityBuffs.OnDeath() strips any buff whose BuffClass.RemoveOnDeath
is true, which is the default when remove_on_death isn't specified at all. So the buff
really was being added, really was permanent under normal circumstances, and really
was still getting wiped out from under VictimPatch.cs's HasBuff check by the death
sequence itself, one step before that check ever ran. remove_on_death="false" is a
real, documented buff attribute - vanilla's own buffStatusCheck01 uses it for exactly
this "must survive death" reason.
Visual (user request 2026-08-28, "подсвети бафнутого зомби... фиолетовым"): same
RadiatedParticlesOnMesh glow as buffNecroDeviatorCharm below, reused rather than a
different particle - HarmonySrc/ParticlePatch.cs recolors it purple specifically for
THIS buff (checks which buff is present before deciding tint), since XML's
AttachParticleEffectToEntity has no color attribute at all (confirmed by decompiling
it - same reason the charm's own glow can't be XML-recolored either). Removed on
death too (onSelfDied) even though the buff FLAG itself survives death on purpose
(remove_on_death="false" above) - the glow is cosmetic and shouldn't linger on a
corpse; VictimPatch.cs's own HasBuff check doesn't care whether the particle is gone. -->
<buff name="buffNecroVictim" hidden="true" remove_on_death="false">
<stack_type value="ignore"/>
<duration value="0"/>
<effect_group>
<triggered_effect trigger="onSelfBuffStart" action="AttachParticleEffectToEntity" particle="RadiatedParticlesOnMesh" local_offset="0,0.75,0"/>
<triggered_effect trigger="onSelfBuffRemove" action="RemoveParticleEffectFromEntity" particle="RadiatedParticlesOnMesh"/>
<triggered_effect trigger="onSelfDied" action="RemoveParticleEffectFromEntity" particle="RadiatedParticlesOnMesh"/>
</effect_group>
</buff>
<!-- "Девиатор" charm marker. Purely a flag for HarmonySrc/CharmPatch.cs to react to -
the actual "fight for the player now" AI behavior happens in that Harmony patch, not
here. hidden/no duration: permanent for the charmed zombie's remaining lifetime.
The visible cue is the particle below - reuses the same mesh-hugging glow vanilla
radiated zombies wear (ParticleEffects/RadiatedParticlesOnMesh.prefab), the closest
thing to an actual recolor achievable from XML alone (no way to retint a live
entity's skin material via buff - AttachParticleEffectToEntity is as close as it gets).
It reads green/energy rather than "necromantic purple", but it's a real full-body
glow, not just a floating cloud; RemoveParticleEffectFromEntity on death/removal so it
doesn't linger on the corpse. -->
<buff name="buffNecroDeviatorCharm" hidden="true">
<stack_type value="ignore"/>
<duration value="0"/>
<effect_group>
<triggered_effect trigger="onSelfBuffStart" action="AttachParticleEffectToEntity" particle="RadiatedParticlesOnMesh" local_offset="0,0.75,0"/>
<triggered_effect trigger="onSelfBuffRemove" action="RemoveParticleEffectFromEntity" particle="RadiatedParticlesOnMesh"/>
<triggered_effect trigger="onSelfDied" action="RemoveParticleEffectFromEntity" particle="RadiatedParticlesOnMesh"/>
</effect_group>
</buff>
<!-- "Портальный вихрь" - BACKLOG.md item 6, user request 2026-08-29 ("пусть вокруг
персонажа летают частицы, лучше чёрные, как дым"). Purely a marker/particle-carrier
buff, same shape as buffNecroVictim/buffNecroDeviatorCharm above - NOT added/removed
by any trigger here at all (no onSelf* triggers reference it), because there's
nothing in the buff-trigger vocabulary that means "for the duration of an open
XUiC_Timer window" - HarmonySrc/PortalStonePatch.cs adds it directly
(player.Buffs.AddBuff) the instant the 10s channel opens and removes it
(player.Buffs.RemoveBuff) the instant the channel ends, however it ends (completed,
cancelled by damage, cancelled by power attack) - see that file.
Reuses the same RadiatedParticlesOnMesh glow the two zombie-facing buffs above
already use (same reasoning: no XML color attribute exists on
AttachParticleEffectToEntity, HarmonySrc/ParticlePatch.cs has to recolor it in code
regardless of which buff triggers it) - generalized ParticlePatch.cs to handle
EntityAlive broadly instead of EntityZombie-only, since this buff's target is the
PLAYER, not a zombie. Tinted near-black/smoke grey, per the user's own request. -->
<buff name="buffNecroPortalChannel" hidden="true">
<stack_type value="ignore"/>
<duration value="0"/>
<effect_group>
<triggered_effect trigger="onSelfBuffStart" action="AttachParticleEffectToEntity" particle="RadiatedParticlesOnMesh" local_offset="0,0.9,0"/>
<triggered_effect trigger="onSelfBuffRemove" action="RemoveParticleEffectFromEntity" particle="RadiatedParticlesOnMesh"/>
<triggered_effect trigger="onSelfDied" action="RemoveParticleEffectFromEntity" particle="RadiatedParticlesOnMesh"/>
</effect_group>
</buff>
<!-- "Тёмное чутьё" - постоянный радар зомби, пока Нож некроманта в руке и в нём стоит
модификатор necroModKnifeDarkSense. Продиктовано 2026-09-07: "В квестах на зачистку на
радаре показывает где находятся зомби. Нужен мод который включит подобный радар на
постоянку, пока мод в ноже, а нож в руке."
Да, реально, и целиком на XML - Harmony не нужен. Ключ в том, что метки на компасе и
карте ставятся через NavObject, а для них есть готовое XML-действие
MinEventActionSetNavObject (ваниль зовёт его в buffs.xml ~15281 для твич-эффектов):
entityAlive.AddNavObject(navObjectName, overrideSprite, overrideText)
и оно наследует MinEventActionTargetedBase, то есть умеет target="selfAOE" с range и
target_tags - ровно то, что нужно для "все зомби вокруг игрока".
ПОЧЕМУ nav_object="zombie", А НЕ "clear_sleeper". Квест на зачистку использует
clear_sleeper, но у того класса стоит <property name="requirement_type"
value="QuestBounds"/> - он показывается только внутри границ активного квеста, то есть
вне квеста не работал бы вообще. Класс "zombie" (nav_objects.xml ~431) - без такого
требования: красная пульсирующая иконка ui_game_symbol_zombie и на карте, и на компасе
(max_distance компаса 1024).
ПОЧЕМУ БАФФ, А НЕ ПРЯМО В МОДЕ. У модификатора нет своего тика. onSelfEquipUpdate
существует (Inventory.OnUpdate шлёт его на держимый предмет), но он идёт каждый
апдейт, а тут на каждом тике делается пространственная выборка сущностей - слишком
часто. Бафф даёт честный троттлинг через update_rate, поэтому мод только вешает и
снимает этот бафф по onSelfEquipStart/onSelfEquipStop (ванильный паттерн, так сделаны
эффекты кирок в items.xml ~158/175).
ПОЧЕМУ КАЖДЫЙ ТИК СНАЧАЛА СНИМАЕТ, ПОТОМ ВЕШАЕТ. Метка живёт на самой сущности
(RegisterNavObject(name, this) - она ездит за зомби), и сама собой не пропадает, когда
зомби отошёл. NavObjectManager.Update чистит только те, у которых TrackedEntity стал
null, то есть уже уничтоженный объект - мёртвый, но ещё не исчезнувший труп метку
сохраняет. Поэтому радиус снятия (60) заведомо больше радиуса добавления
(30): всё, что вышло из зоны показа, но ещё рядом, чистится само.
AddNavObjectClass дедуплицирует (NavObject.cs ~346: "if (!NavObjectClassList.Contains)"),
так что повторное добавление одному и тому же зомби безвредно.
ИЗВЕСТНЫЙ РИСК, ПРОВЕРИТЬ В ИГРЕ: снятие-и-добавление в одном тике для зомби, который
остаётся в зоне, технически пересоздаёт NavObject (RemoveNavObjectClass опустошает
список -> UnRegisterNavObject -> следующий Add регистрирует заново). Итоговое
состояние кадра верное, но пульсация иконки может подёргиваться раз в секунду. Если
будет заметно - убрать строку снятия из onSelfBuffUpdate и оставить чистку только на
onSelfBuffRemove, ценой залипших меток на ушедших зомби. -->
<buff name="buffNecroDarkSense" name_key="buffNecroDarkSenseName" description_key="buffNecroDarkSenseDesc" icon="ui_game_symbol_zombie" icon_color="150,0,255">
<stack_type value="replace"/>
<duration value="0"/>
<update_rate value="1"/>
<effect_group name="necro dark sense radar">
<triggered_effect trigger="onSelfBuffUpdate" action="SetNavObject" target="selfAOE" range="60" target_tags="zombie" nav_object="zombie" add="false"/>
<triggered_effect trigger="onSelfBuffUpdate" action="SetNavObject" target="selfAOE" range="30" target_tags="zombie" nav_object="zombie" add="true"/>
<triggered_effect trigger="onSelfBuffRemove" action="SetNavObject" target="selfAOE" range="60" target_tags="zombie" nav_object="zombie" add="false"/>
</effect_group>
</buff>
</append>
</config>
+284
View File
@@ -0,0 +1,284 @@
<config>
<!-- Step 1: lifetime zombie kill counter.
zombieTemplateMale is the root template every zombie entity_class extends
(directly, or indirectly via zombieTemplateShort), so patching it here covers
every zombie variant in the game without listing them individually. -->
<append xpath="/entity_classes/entity_class[@name='zombieTemplateMale']">
<effect_group>
<requirement name="EntityTagCompare" target="other" tags="player"/>
<triggered_effect trigger="onOtherKilledSelf" action="ModifyCVar" target="other" cvar="necroZombieKillsCVar" operation="add" value="1"/>
<!-- "Некромантия" skill: +1 level per zombie kill, capped by its own max_level.
See progression.xml for why this drives the skill instead of reading books. -->
<triggered_effect trigger="onOtherKilledSelf" action="AddProgressionLevel" target="other" progression_name="craftingNecroNecromancy" level="1"/>
</effect_group>
</append>
<!-- "Зомбособака" (Zombie Dog pet): BACKLOG.md item 3. Extends the vanilla hostile
animalZombieDog (same prefab/physics/sounds - a real zombie dog model, not a reskinned
wolf) but flips it to fight FOR the player instead of against them:
- EntityFlags drops "zombie": EntityAlive.DamageEntity has a hardcoded rule that two
entities BOTH flagged EntityFlags.Zombie can never damage each other (see
HarmonySrc/DamagePatch.cs for the same rule affecting charmed zombies) - keeping our
pet flagged "zombie" would make it literally unable to hurt real zombies. Dropping the
flag sidesteps that block entirely; no Harmony damage patch needed here, unlike the
charmed-zombie case (which can't drop the flag without breaking its own zombie-side
kill-counter/quest logic - our pet has none of that to worry about).
- Tags drops "zombie"/"hostile" (kept "entity,animal,dog"), IsEnemyEntity="false" - same
shape vanilla's own player-owned entityJunkDrone uses for "friendly to the player".
- AITask-3/AITarget-1/AITarget-4 retarget from EntityPlayer/EntityBandit to EntityZombie -
purely data-driven, same AI task types animalZombieDog already used, just pointed at a
different class. This is also exactly how HarmonySrc/CharmPatch.cs flips a charmed
zombie's allegiance at runtime - confirmed here that the underlying AI system doesn't
care whether the retarget happens via live Harmony rewrite or via XML at spawn time.
Spawned by HarmonySrc/SummonPatch.cs via item bookSummonZombieDog (items.xml) - see that
file for the ownership/one-pet-limit logic (mirrors vanilla's own drone: EntityDrone.
isValidForPlayer() blocks summoning a second one while you already own one, rather than
replacing the old one - SummonPatch.cs does the same check via ownedEntities). -->
<append xpath="/entity_classes">
<entity_class name="necroZombieDog" extends="animalZombieDog">
<property name="EntityFlags" value="animal"/>
<property name="Tags" value="entity,animal,dog"/>
<property name="IsEnemyEntity" value="false"/>
<property name="Faction" value="none"/>
<!-- User request 2026-08-28: bite applies buffInjurySlow, same debuff the Insect Swarm
originally had before its own rework - see necroMeleeHandZombieDog in items.xml. -->
<property name="HandItem" value="necroMeleeHandZombieDog"/>
<property name="AITask-3" value="ApproachAndAttackTarget" data="class=EntityZombie,20"/>
<property name="AITarget-1" value="SetAsTargetIfHurt" data="class=EntityZombie"/>
<property name="AITarget-4" value="SetNearestEntityAsTarget" data="class=EntityZombie,22,20"/>
</entity_class>
</append>
<!-- "Жуки Властелина" (insect swarm pet): user request 2026-08-28, alongside the Zombie Dog
bugfix (see items.xml). Extends vanilla's own animalInsectSwarm (same prefab/sounds/
tanky-vs-melee stats - PhysicalDamageResist 75% vs non-ranged inherits unchanged, a nice
free defensive fit for something meant to be swarmed by zombie melee).
BUG FIXED 2026-08-28 (swarm attacked the PLAYER, not zombies): the original version here
overrode AITask-1/AITarget-1, mirroring animalInsectSwarm's own choice to blank those two
indices - but animalInsectSwarm extends animalTemplateHostile, and re-reading THAT
template (Data/Config/entityclasses.xml) shows the actual player-attacking task/target
live at DIFFERENT indices: AITask-2 ("ApproachAndAttackTarget" class=EntityPlayer,...) and
AITarget-4 ("SetNearestEntityAsTarget" class=EntityPlayer,...). animalInsectSwarm's
AITask-1/AITarget-1 blanking only wipes animalTemplateHostile's harmless AITask-1
(BreakBlock) and AITarget-1 (SetAsTargetIfHurt, generic) - the real player-hunting
behavior at index 2/4 was never touched by that blanking, and my override at index 1
just sat next to it as a no-op extra task while the REAL one kept targeting players.
Fixed by overriding the actual indices (2 and 4) instead. AITask-1/AITarget-1 are left
alone now - they inherit animalInsectSwarm's own blank override, same as vanilla.
BEHAVIOR CHANGE 2026-08-28 (user request): no longer deals real damage + slows zombies -
it stings a zombie once and that zombie becomes a Deviator-charmed ally, same buff as the
Spirit Stone/Grimoire (buffNecroDeviatorCharm - see necroMeleeHandInsectSwarm in
items.xml). Search range widened (22/20 -> 40/30) per "ищут всех зомби в радиусе", though
that number turned out not to matter much either - see below.
TARGETING IS NOT ACTUALLY DRIVEN BY THESE AITask/AITarget PROPERTIES AT ALL, despite the
fix above (found out the hard way 2026-08-28, after that fix didn't stop the swarm from
attacking the player): animalInsectSwarm's own Class="EntitySwarm" extends EntityVulture,
which has entirely hardcoded C# target-finding (World.GetClosestPlayerSeen/GetClosestPlayer,
literally typed to EntityPlayer) - it never consults the declarative AITask/AITarget system
ground creatures like the Dog use. The AITask-2/AITarget-4 overrides above are harmless but
functionally dead for this entity_class. The real fix is HarmonySrc/SwarmTargetPatch.cs,
patching EntityAlive.SetAttackTarget to redirect player-targeting to the nearest zombie -
see that file. It also skips zombies that already carry buffNecroDeviatorCharm (added after
a second user report that the swarm would fly off in wide loops instead of moving straight
to an uncharmed zombie standing right next to the one it had just charmed - re-targeting
the same just-charmed zombie over and over, because it was still the nearest one, was
exactly what caused that).
EntityFlags/EntityType: parent sets EntityFlags="animal,zombie" and EntityType="Zombie" -
same DamageEntity zombie-vs-zombie block as the dog (see that entry above and
HarmonySrc/DamagePatch.cs) would otherwise stop it stinging real zombies at all, so both
get dropped to plain "animal". -->
<append xpath="/entity_classes">
<entity_class name="necroInsectSwarm" extends="animalInsectSwarm">
<property name="EntityFlags" value="animal"/>
<property name="EntityType" value="Animal"/>
<property name="Tags" value="entity,noHead"/>
<property name="IsEnemyEntity" value="false"/>
<property name="Faction" value="none"/>
<property name="HandItem" value="necroMeleeHandInsectSwarm"/>
<property name="AITask-2" value="ApproachAndAttackTarget" data="class=EntityZombie,20"/>
<property name="AITarget-4" value="SetNearestEntityAsTarget" data="class=EntityZombie,40,30"/>
</entity_class>
</append>
<!-- Three more summon-pet zombies, BACKLOG.md item 4a. REPLACED AGAIN 2026-08-29, THIRD
APPROACH, per the user's own direct suggestion after the second approach's Bear/Wolf
fixes still didn't work ("почему нельзя взять зомбособаку, но добавить её ХП и силы
атаки, и заменить ей модельку на медведя?"). Correct call - stop re-deriving AI/targeting
data from each different vanilla template's own quirky format (full pipe-string vs
indexed properties, different index numbering, etc.) and instead extend the Dog's OWN
entity_class (necroZombieDog, defined above) directly. necroZombieDog already has
EntityFlags/Tags/IsEnemyEntity/Faction AND the AITask-3/AITarget-1/AITarget-4 zombie-
targeting overrides all confirmed working correctly in-game across multiple test rounds -
none of that has to be re-verified or re-derived per creature any more, it's inherited
unchanged. Bear/Wolf below only override what's actually cosmetic/stat-related: Prefab
(swap the visible model), PhysicsBody/Mass (match the new model's real size so
collision/knockback look right), HandItem (reuse the REAL vanilla hand-item for that
creature - meleeHandAnimalZombieBear/meleeHandAnimalDireWolf, both real items with their
own already-tuned damage - 60 each, vs. the Dog's own 8), and HealthMax (bumped up to
match, see each entity's own comment for the exact vanilla reference number used).
Griffin NOT converted this way (left on the previous animalZombieVulture-based approach,
unchanged) - flagged as a real, separate risk, not just left out by oversight: Bear/Wolf
share the Dog's own quadruped body plan/animation rig, so swapping just the Prefab is
low-risk (same skeleton shape, same generic AvatarAnimalController). A vulture is a
bird - almost certainly a structurally different skeleton/rig - forcing that Prefab onto
necroZombieDog's own ground-quadruped C# class (EntityZombieDog, no flight code at all)
risks either a broken/distorted-looking model (mismatched bone names) or, best case, a
"walking bird" that never actually flies. Worth a real decision, not a silent guess -
see BACKLOG.md for the open question. -->
<append xpath="/entity_classes">
<entity_class name="necroZombieBear" extends="necroZombieDog">
<!-- Real vanilla animalZombieBear's own Prefab/PhysicsBody/Mass (the same asset a
genuine zombie bear uses) - not re-deriving AI from it any more, just borrowing
its look and physical size so collision/knockback scale correctly for a
bear-sized model sitting on the Dog's skeleton/rig. -->
<property name="Prefab" value="@:Entities/Animals/Bear/animalBearZombiePrefab.prefab"/>
<property name="PrefabCombined" value="true"/>
<property name="PhysicsBody" value="bear"/>
<property name="Mass" value="600"/>
<property name="Tags" value="entity,animal,bear"/>
<!-- FIXED 2026-08-30 (user report: "зомбомедведь лает" - swapping the Prefab only
changes the visible model, sound properties are a totally separate set of
properties and stayed inherited from necroZombieDog/animalZombieDog (dog barks)
the whole time - same oversight needed fixing on Wolf/Griffin below too, not
just this one. Real animalBear's own sound set (animalZombieBear itself doesn't
override any of these, confirmed by reading it directly - it inherits animalBear's). -->
<property name="SoundRandom" value="bearroam"/>
<property name="SoundAlert" value="bearalert"/>
<property name="SoundHurt" value="bearpain"/>
<property name="SoundDeath" value="beardeath"/>
<property name="SoundAttack" value="bearattack"/>
<property name="SoundSense" value="bearsense"/>
<property name="SoundGiveUp" value="beargiveup"/>
<property name="SoundStepType" value="animalhvystep"/>
<!-- Real vanilla hand item (claw damage 60, vs. the Dog's own bite at 8) - reused
as-is, not customized further (no slow debuff etc. - not asked for). -->
<property name="HandItem" value="meleeHandAnimalZombieBear"/>
<effect_group name="Base Effects">
<!-- 1500 is a guess between the Dog's 200 and real animalZombieBear's own 4000 -
a tough pet, not necessarily boss-tier tanky. Say if it should be higher/lower. -->
<passive_effect name="HealthMax" operation="base_set" value="1500"/>
</effect_group>
</entity_class>
</append>
<append xpath="/entity_classes">
<entity_class name="necroZombieWolf" extends="necroZombieDog">
<!-- Real vanilla animalDireWolf's own Prefab/PhysicsBody/Mass/SizeScale. -->
<property name="Prefab" value="@:Entities/Animals/DireWolf/animalDireWolfPrefab.prefab"/>
<property name="PrefabCombined" value="true"/>
<property name="PhysicsBody" value="AWolf"/>
<property name="Mass" value="180"/>
<property name="SizeScale" value="1.4"/>
<property name="Tags" value="entity,animal,wolf"/>
<!-- FIXED 2026-08-30, same oversight as the Bear above (see its comment) - real
animalDireWolf's own sound set. -->
<property name="SoundRandom" value="wolfdireroam"/>
<property name="SoundAlert" value="wolfdirealert"/>
<property name="SoundHurt" value="wolfdirepain"/>
<property name="SoundDeath" value="wolfdiredeath"/>
<property name="SoundAttack" value="wolfdireattack"/>
<property name="SoundSense" value="wolfdiresense"/>
<property name="SoundGiveUp" value="wolfdiregiveup"/>
<property name="SoundStepType" value="animalpawstep"/>
<!-- Real vanilla hand item (bite damage 60, vs. the Dog's own 8). -->
<property name="HandItem" value="meleeHandAnimalDireWolf"/>
<effect_group name="Base Effects">
<!-- 1200 is a guess between the Dog's 200 and real animalDireWolf's own 3000 -
slightly below the Bear, faster/leaner theme. Say if it should be different. -->
<passive_effect name="HealthMax" operation="base_set" value="1200"/>
</effect_group>
</entity_class>
</append>
<!-- Griffin CONVERTED 2026-08-29 to the same Dog-reskin trick as Bear/Wolf, after the user
confirmed live in-game that the animalZombieVulture+Harmony-redirect approach genuinely
doesn't work ("летает где-то в небе, и зомби его вообще не интересуют" - flies around
doing EntityVulture's own default Wander behavior, never engaging anything). Not worth
debugging the Harmony redirect further without another live test cycle - switched
straight to the proven-reliable pattern instead, same as Wolf (confirmed working) and
Bear (not yet confirmed, same trick).
No real griffin exists in this game and a bird's skeleton/rig was flagged as a real risk
for reusing necroZombieDog's own quadruped rig (see the removed comment this replaces) -
picked animalMountainLion as the substitute model instead of a bird: it EXTENDS
animalWolf (Data/Config/entityclasses.xml, confirmed by reading it directly) - the SAME
immediate parent animalZombieDog itself extends - almost certainly sharing the exact same
quadruped skeleton/rig family the Dog's own model already uses, the lowest-risk pick
available (lower risk than the Bear, which comes from a totally different
animalBear->animalTemplateHostile lineage). Not a literal griffin visually any more (a
mountain lion, not a bird/lion-eagle hybrid) - say if a different substitute or the name
itself should change; kept "Зомбогриф"/"Summon Zombie Griffin" as-is for now since
renaming would touch items.xml/recipes.xml/Localization.csv too and wasn't asked for. -->
<append xpath="/entity_classes">
<entity_class name="necroZombieGriffin" extends="necroZombieDog">
<!-- Real vanilla animalMountainLion's own Prefab/PhysicsBody/Mass/SizeScale. -->
<property name="Prefab" value="@:Entities/Animals/Cat/animalMountainLion.prefab"/>
<property name="PrefabCombined" value="true"/>
<property name="PhysicsBody" value="MountainLion"/>
<property name="Mass" value="125"/>
<property name="SizeScale" value="1"/>
<property name="Tags" value="entity,animal,cat"/>
<!-- FIXED 2026-08-30, same oversight as the Bear above (see its comment) - real
animalMountainLion's own sound set. -->
<property name="SoundRandom" value="mlionroam"/>
<property name="SoundAlert" value="mlionalert"/>
<property name="SoundHurt" value="mlionpain"/>
<property name="SoundDeath" value="mliondeath"/>
<property name="SoundAttack" value="mlionattack"/>
<property name="SoundSense" value="mlionsense"/>
<property name="SoundGiveUp" value="mliongiveup"/>
<property name="SoundStepType" value="animalpawstep"/>
<!-- Real vanilla hand item (claw damage 22 - lighter than the Bear/Wolf's 60, a
faster/leaner theme fitting a big cat rather than a heavyweight brawler). -->
<property name="HandItem" value="meleeHandAnimalMountainLion"/>
<effect_group name="Base Effects">
<!-- Real vanilla animalMountainLion's own HealthMax (750) - used as-is, not
scaled further, since this pet is meant to be the "fast/agile" one, not the
tankiest of the three. -->
<passive_effect name="HealthMax" operation="base_set" value="750"/>
</effect_group>
</entity_class>
</append>
<!-- "Жертва" loot bag - BACKLOG.md item 5, Necromancer's Knife. Same shape as vanilla's own
EntityLootContainerStrong ("BLUE")/EntityLootContainerBoss ("RED") right above these in
Data/Config/entityclasses.xml - only the Mesh and LootList differ.
BUG FIXED 2026-08-28 ("зомби погиб с бафом, гарантированный дроп запустился, но лута
нет"): Mesh was a guess, zpackGreenPrefab.prefab - confirmed by the in-game test itself
that it doesn't exist as a real asset. VictimPatch.cs's own warning fired: "created entity
for 'EntityLootContainerVictim' wasn't an EntityLootContainer" - EntityFactory.CreateEntity
didn't produce a usable entity at all once its mesh failed to resolve, so nothing ever
got spawned, guaranteed roll or not. Reverted to zpackPrefab.prefab (the default/yellow
mesh, confirmed to exist - every other loot container variant in vanilla config either
uses this one directly or one of the three OTHER confirmed variants, blue/red/gold) per
the fallback plan agreed on before this was ever guessed at - not visually distinct from a
normal zombie drop, but a real, working bag with the right guaranteed contents.
Force-spawned by HarmonySrc/VictimPatch.cs when a zombie with buffNecroVictim dies -
never rolled by the normal LootDropProb/LootDropEntityClass system (not referenced from
any zombie's own entity_class), so its own IsEnemyEntity/Faction/etc. just mirror the
other loot containers for consistency, not because anything reads them via that path. -->
<append xpath="/entity_classes">
<entity_class name="EntityLootContainerVictim">
<property name="Class" value="EntityLootContainer"/>
<property name="UserSpawnType" value="Console"/>
<property name="Mesh" value="@:Entities/LootContainers/zpackPrefab.prefab"/>
<property name="ModelType" value="Custom"/>
<property name="Prefab" value="Backpack"/>
<property name="Parent" value="Backpack"/>
<property name="IsEnemyEntity" value="false"/>
<property name="TimeStayAfterDeath" value="3600"/>
<property name="LootList" value="zPackVictim"/>
<property name="Faction" value="none"/>
</entity_class>
</append>
</config>
+317
View File
@@ -0,0 +1,317 @@
<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 - примерно уровень
одной детали брони с T3-подкладкой, и ровно то число, которое ваниль использовала во
вкомментированных modArmorInsulatedLiner/modArmorCoolingMesh (там 5 на холод и 5 на
жару, но двумя РАЗНЫМИ модами; здесь оба в одном, что щедрее - но это стоит слота из
четырёх и работает только с ножом в руках, см. ниже). Крутить это число - одна правка.
ЕДИНИЦА ИЗМЕРЕНИЯ - градусы, на которые сдвигается уличная температура в сторону
комфортной, а не проценты (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="5"/>
<passive_effect name="HyperthermalResist" operation="base_add" value="5"/>
</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>
</config>
+1346
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
<config>
<!-- "Жертва" (Victim) green loot bag - BACKLOG.md item 5, Necromancer's Knife. Guaranteed
drop off a zombie killed while carrying buffNecroVictim (see HarmonySrc/VictimPatch.cs,
which force-spawns EntityLootContainerVictim from entityclasses.xml on death, bypassing
the normal LootDropProb roll entirely).
Contents per user: random hides (usually one or none), rotten flesh, bones, and a chance
at "Кожа жертвы" (resourceVictimSkin, items.xml) - a crafting resource for summon books.
Exact counts/chance are a guess where the user didn't give numbers - count="0,1" for the
hide reads as "как правило одна или ноль" directly; flesh/bone ranges and the 15% skin
chance are otherwise reasonable defaults, not specified. Say if any of these should change.
UPDATED 2026-08-29 (BACKLOG.md item 8, dictated same day): foodRottingFlesh/resourceBone
removed per direct instruction - the mesh no longer drops the two generic zombie-corpse
staples. Replaced with a random chance at resourceZombieAsh ("Прах зомби") instead, a new
crafting resource fed into the other necromancy recipes (see recipes.xml - deliberately
NOT the Knife's own recipe, to avoid a craft-the-knife-to-get-ash-to-craft-the-knife loop).
Chance/count not specified by the user - guessed at the same shape as resourceVictimSkin's
own 15% (bumped the count up a little since ash is meant to actually get used as a
multi-unit crafting ingredient, unlike the skin). Say if this should change.
BUG FIXED 2026-08-28 (whole file failed to load, so NONE of this was active in-game -
"Loading and parsing 'loot.xml' failed" / "lootgroup 'groupZpackVictim' does not exist or
has not been defined before being referenced"): the lootgroup a lootcontainer's <item
group="..."/> points at must be defined EARLIER in load order, same file or not - vanilla
loot.xml itself always defines groupZpackReg etc. (~line 6589) well before the
lootcontainer that references it (~line 9971). Originally had the container's <append>
block first and the group's second; swapped so the group append comes first. Keep this
append-order intact when editing below - it's what makes the file load at all. -->
<append xpath="/lootcontainers">
<lootgroup name="groupZpackVictim" count="all">
<item name="resourceLeather" count="0,1"/>
<item name="resourceZombieAsh" count="1,4" prob="0.4"/>
<item name="resourceVictimSkin" count="1" prob="0.15"/>
</lootgroup>
</append>
<append xpath="/lootcontainers">
<lootcontainer name="zPackVictim" count="1" size="6,3" sound_open="UseActions/open_backpack" sound_close="UseActions/close_backpack" open_time="1" loot_quality_template="qualBaseTemplate">
<item group="groupZpackVictim"/>
</lootcontainer>
</append>
</config>
+194
View File
@@ -0,0 +1,194 @@
<config>
<!-- "Некромантия" (Necromancy) skill.
Reuses the vanilla crafting_skill mechanism (same class that powers e.g.
craftingHarvestingTools) parented to the vanilla "attCrafting" virtual
attribute — this makes it auto-level from a game action (not spent perk
points) and show up for free in the game's existing crafting-skills panel.
Originally this leveled from reading books (see git history / earlier
comments), but items.xml effect_group does NOT inherit through Extends in
this game version (confirmed by decompiling ItemClassesFromXml.parseItem -
it always parses effect_group from the item's own XML node, passing null as
the "parent node" argument to MinEffectController.ParseXml regardless of
Extends; entity_class does the equivalent WITH the parent node passed in,
which is why the zombie kill counter below always worked and reading books
never did). Patching every single one of the ~150 skill magazine/schematic
items individually to work around that was judged not worth it - so this
levels from zombie kills instead, via the same entityclasses.xml patch on
zombieTemplateMale that already drives necroZombieKillsCVar (see
entityclasses.xml) - one kill, one level, capped at max_level.
max_level=5000, with 5 recipe groups gated at fixed total-zombie-kills
thresholds (proportional to the original 1000-max version: 0.1% / 10% / 40%
/ 60% / 100%):
Group 1 "Адепт" - available from the start (level 1), but not all
of it - individual recipes within the group still
unlock at their own level as usual.
Group 2 "Подмастерье" - level 500
Group 3 "Ученик" - level 2000
Group 4 "Некромант" - level 3000
Group 5 "Мастер" - level 5000
Recipe unlocks: give a recipe Tags="necroNecromancyAdept" / "necroNecromancyJourneyman"
/ "necroNecromancyApprentice" / "necroNecromancyNecromancer" / "necroNecromancyMaster"
(whichever group it belongs to) and it will unlock automatically once the
player crosses that group's threshold - no further change needed here.
thrownStoneSpirit (Камень духов) carries none of these tags at all, since Group 1
"Адепт" is unlocked from level 1 anyway - a recipe with no unlock tag is just always
available, same effect, no need to spend a tag on it.
One genuine one-off exception: necroNecromancyLvl20 (level 20) - the Пространственный
браслет, per direct instruction 2026-08-30 ("нож, камень духов и хранилище - это база...
хранилище, когда убито минимум 20 зомби") - it belongs in the Group 1 "Адепт" display
bucket (see below) but needs its own slightly-later unlock level within that same group,
which is what unlock_tier is for (see display_entry below), not a reason to invent a
whole separate group.
UPDATED 2026-08-29: display_entry blocks added below now that real necromancer items/
recipes exist to point them at - per the user's own direct report ("до сих пор нету ни
одного рецепта" in the skill panel).
UPDATED 2026-08-30 (user report: "превью пустые" - the group icons in the skill panel
were blank). Root cause: `icon=` on display_entry needs a real ICON ATLAS SPRITE NAME
(the same string you'd give CustomIcon), not the item's own internal id - fixed by
pointing icon= at each group's actual CustomIcon sprite value.
REVERTED-AND-REDISTRIBUTED AGAIN 2026-08-30, two direct corrections in a row:
1. "Что ещё за 'уровень'? Была же система! А рецепту можно без группы задать уровень
скилла на котором он откроется." - an earlier edit had invented ad-hoc "Level
20/50/200" display groups, abandoning the real 5-tier flavor-name system this
comment already documents above (Адепт/Подмастерье/Ученик/Некромант/Мастер -
craftingNecroNecromancyTier1-5Name, all already localized, unused since). A recipe's
actual unlock level is controlled entirely by its own `tags=` + the
RecipeTagUnlocked effect_group below - display_entry is PURELY the skill-panel
preview widget, the two don't need to be 1:1. Confirmed against real vanilla
precedent (Data/Config/progression.xml's own explosives group): one display_entry
can list several items that reveal at different levels via a comma-list
unlock_level plus multiple unlock_entry elements at increasing unlock_tier.
2. "У тебя получается 5 групп, как и должно было быть. А вот уровень скилла ты
распределил идиотски." - the fix for #1 above had (wrongly) folded EVERYTHING except
the black stone into Group 1 alone (levels 1/20/50/200), leaving the three real
mid-game tiers (Подмастерье@500/Ученик@2000/Некромант@3000) completely empty and
unused - defeating the entire point of having 5 groups spread across the level
range. Fixed by actually spreading the non-base items across all 5 real tiers
instead of clustering them all near the bottom - see the effect_group tags below and
recipes.xml for the final per-item distribution:
Group 1 "Адепт" (level 1, +20 for the bracelet) - Spirit Stone, Knife,
Blue Portal Stone, Spatial Vault, Pyramid of Spirits,
and (since 2026-09-09) the four survival-flavoured knife
mods at +30 / +60 / +100 / +300
Group 2 "Подмастерье" (level 500) - Grimoire of Deviation, plus the two combat
knife mods at 1400 / 1700
Group 3 "Ученик" (level 2000) - Zombie Dog, Insect Swarm, Zombie Griffin
Group 4 "Некромант" (level 3000) - Zombie Bear, Zombie Wolf, Banshee's Scroll
Group 5 "Мастер" (level 5000) - Black Portal Stone
necroNecromancyLvl50/Lvl200 tags removed (no longer used by anything - Grimoire and
the summon books now use the real necroNecromancyJourneyman/Apprentice/Necromancer
tags instead). necroNecromancyLvl20 stays - still the one legitimate one-off (see
above). -->
<append xpath="/progression/crafting_skills">
<crafting_skill name="craftingNecroNecromancy" max_level="5000" parent="attCrafting" name_key="craftingNecroNecromancyName" desc_key="craftingNecroNecromancyDesc" long_desc_key="craftingNecroNecromancyLongDesc" icon="ui_game_symbol_zombie">
<!-- display_entry block kept in the SAME append as the crafting_skill itself (unlike
an earlier draft of this edit, which tried appending display_entry via a second,
separate xpath targeting crafting_skill[@name='craftingNecroNecromancy'] BEFORE
this element even exists in load order - same class of bug that already broke
loot.xml once, see that file's own load-order comment. Nesting it here avoids the
ordering question entirely, and matches how vanilla itself writes display_entry -
directly inside the crafting_skill tag, not as a separate append). -->
<display_entry icon="SpiritStone" name_key="craftingNecroNecromancyTier1Name" has_quality="false" unlock_level="1,20,30,60,100,300">
<!-- necroHeresyPyramid added here 2026-08-31 (user request: "добавь рецепт блока в
скиллы") - unlock_tier="1" alongside the other always-available Tier-1 items,
matching its recipe's own necroNecromancyAdept tag in recipes.xml (see that
file's comment - both express the same "available immediately" intent). -->
<unlock_entry item="thrownStoneSpirit,necroWpnBladeNecroKnife,thrownStonePortalBlue,necroHeresyPyramid" unlock_tier="1"/>
<unlock_entry item="braceletSpatialVault" unlock_tier="2"/>
<!-- ЧЕТЫРЕ МОДА НОЖА ПЕРЕЕХАЛИ СЮДА ИЗ ГРУППЫ 2, 2026-09-09. Продиктовано:
"Питьё важно в тот же день. Оно должно быть доступно после 30 убитых зомби.
Еда - 60. Это самые важные для начала выживания модификации. Модификация на
покой - 100 зомби. Модификацию на тёмное чутьё я бы сделал доступной после
300 убитых зомби."
ЧТО БЫЛО СЛОМАНО. Правка 2026-09-07 разложила все шесть модов ножа внутри
группы 2, то есть в диапазоне 500-1700 убийств. Это ошибка баланса, а не
кода (сама тройка "тег рецепта + RecipeTagUnlocked + unlock_tier" сходилась
по всем шести): голод и жажда - проблема ПЕРВЫХ ДНЕЙ, а к 500 убийствам у
игрока давно есть ферма, костёр, банки и фильтр, и +2 воды с трупа ему уже
не нужны. Два самых "выживальческих" мода открывались ровно тогда, когда
переставали быть нужны - мёртвый контент. Сам нож лежит в группе 1 и
доступен с уровня 1, поэтому моды на воду, еду и тепло теперь идут сразу за
ним, в той же группе.
Ступени группы 1 после правки: 1 - база, 20 - браслет, 30 - вода, 60 - еда,
100 - покой, 300 - чутьё. Все шесть порогов ниже 500, то есть на группу 2
они не заезжают. Хватка мертвеца (1400) и Мёртвая буря (1700) остались в
группе 2 - это чисто боевые моды, ранний доступ им не нужен ("дальше уже не
так принципиально").
Порядок вода -> еда не случаен и задан пользователем прямо: пить хочется в
тот же день, есть - позже. -->
<unlock_entry item="necroModKnifeTearsOfTheDead" unlock_tier="3"/>
<unlock_entry item="necroModKnifeScavengersFeast" unlock_tier="4"/>
<unlock_entry item="necroModKnifeGravesRepose" unlock_tier="5"/>
<unlock_entry item="necroModKnifeDarkSense" unlock_tier="6"/>
</display_entry>
<!-- Ступенчатая разблокировка внутри группы. ПЕРЕРАСПРЕДЕЛЕНО 2026-09-09: четыре из
шести модов ножа (вода/еда/покой/чутьё) уехали отсюда в группу 1 - см. большой
комментарий там. Здесь остались Гримуар и два боевых мода ножа.
КАК ЭТО ЧИТАЕТСЯ (разобрано по декомпиляции, потому что семантика неочевидная):
unlock_level - это список порогов (ProgressionClass.QualityStarts), а unlock_tier
в XML 1-based, при разборе из него вычитается единица
(ProgressionFromXml.cs:396 - "ParseSInt32(...) - 1"). Дальше
GetUnlockItemLocked = GetQualityLevel(level) <= UnlockTier
где GetQualityLevel возвращает индекс первого порога, который БОЛЬШЕ текущего
уровня. В сумме это даёт простое правило: запись с unlock_tier="N" выходит
из-под замка ровно на N-м значении unlock_level, считая с единицы. Поэтому здесь
tier 1 -> 500, tier 2 -> 1400, tier 3 -> 1700, а в группе 1 выше -
tier 1 -> 1, tier 2 -> 20, tier 3 -> 30, tier 4 -> 60, tier 5 -> 100,
tier 6 -> 300.
Заблокированная запись рисуется греем из АТЛАСА ItemIconAtlasGreyscale плюс
спрайт-замок ui_game_symbol_unlock поверх (XUi_InGame/windows.xml ~2523-2524,
привязки unlock_icon_atlasN / unlock_icon_lockedN). Именно поэтому у мода теперь
есть вторая папка UIAtlases/ItemIconAtlasGreyscale - без неё под замком у иконки
не было бы картинки вообще. -->
<display_entry icon="ScrollOfDeviation" name_key="craftingNecroNecromancyTier2Name" has_quality="false" unlock_level="500,1400,1700">
<unlock_entry item="thrownBookGrimoireDeviation" unlock_tier="1"/>
<unlock_entry item="necroModKnifeDeadMansGrip" unlock_tier="2"/>
<unlock_entry item="necroModKnifeDeadStorm" unlock_tier="3"/>
</display_entry>
<display_entry icon="SummonZombieDog" name_key="craftingNecroNecromancyTier3Name" has_quality="false" unlock_level="2000">
<unlock_entry item="bookSummonZombieDog,bookSummonInsectSwarm,bookSummonZombieGriffin" unlock_tier="1"/>
</display_entry>
<display_entry icon="SummonZombieBear" name_key="craftingNecroNecromancyTier4Name" has_quality="false" unlock_level="3000">
<unlock_entry item="bookSummonZombieBear,bookSummonZombieWolf,bookBanshee" unlock_tier="1"/>
</display_entry>
<display_entry icon="BlackPortalStone" name_key="craftingNecroNecromancyTier5Name" has_quality="false" unlock_level="5000">
<unlock_entry item="thrownStonePortalBlack" unlock_tier="1"/>
</display_entry>
<effect_group>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="1,5000" value="1" tags="necroNecromancyAdept"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="20,5000" value="1" tags="necroNecromancyLvl20"/>
<!-- Пороги модов ножа, 2026-09-09 (см. комментарий в recipes.xml и в группе 1
выше). Каждый уровень тут ОБЯЗАН совпадать с соответствующим значением в
unlock_level того display_entry, где лежит мод, иначе замок на панели скилла
разойдётся с реальной доступностью рецепта: display_entry рисует замок сам по
себе, по unlock_tier, и о тегах не знает.
Lvl800 и Lvl1100 удалены вместе с этой правкой - ими больше никто не
пользуется (Тёмное чутьё уехало на 300, Могильный покой на 100). -->
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="30,5000" value="1" tags="necroNecromancyLvl30"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="60,5000" value="1" tags="necroNecromancyLvl60"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="100,5000" value="1" tags="necroNecromancyLvl100"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="300,5000" value="1" tags="necroNecromancyLvl300"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="500,5000" value="1" tags="necroNecromancyJourneyman"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="1400,5000" value="1" tags="necroNecromancyLvl1400"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="1700,5000" value="1" tags="necroNecromancyLvl1700"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="2000,5000" value="1" tags="necroNecromancyApprentice"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="3000,5000" value="1" tags="necroNecromancyNecromancer"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="5000,5000" value="1" tags="necroNecromancyMaster"/>
</effect_group>
</crafting_skill>
</append>
</config>
+379
View File
@@ -0,0 +1,379 @@
<config>
<!-- Tin can water boiling - user request 2026-08-30 ("Вода в консервных банках должна
кипятиться на костре даже если там нету кастрюли... так было сделано в другом моём моде
'Энерголук'" = AC-EnergyBow, the same __NoMods reference the tin can items themselves were
ported from - see items.xml's own comment on tinCanEmpty/tinCanRiverWater/
tinCanBoiledWater).
Root cause this was missing at all: `Extends` copies an item's PROPERTIES, never its
RECIPES - recipes are separate top-level <recipe> nodes matched by name, with no
inheritance mechanism of their own (confirmed by grepping this mod's own recipes.xml -
zero "tinCan" hits before this entry). tinCanRiverWater/tinCanBoiledWater extending
drinkJarRiverWater/drinkJarBoiledWater therefore got NONE of the vanilla mason-jar-boiling
recipe (`<recipe name="drinkJarBoiledWater" craft_area="campfire"
craft_tool="toolCookingPot">`, Data/Config/recipes.xml) - boiling a tin can was never
possible at all until this recipe existed, not merely pot-gated.
No `craft_tool="toolCookingPot"` here BY DESIGN, per the direct request - a metal can can
sit right in a campfire's coals on its own, unlike a mason jar. Confirmed real vanilla
precedent for a pot-less campfire recipe existing at all: `foodBakedPotato`/
`foodCharredMeat` both use craft_area="campfire" with no craft_tool attribute at all - a
campfire recipe is not implicitly pot-gated, craft_tool is an opt-in extra requirement,
not something `craft_area="campfire"` implies on its own. -->
<append xpath="/recipes">
<recipe name="tinCanBoiledWater" count="1" craft_area="campfire">
<ingredient name="tinCanRiverWater" count="1"/>
</recipe>
</append>
<!-- "Камень духов" (Spirit Stone): craftable from the start, no perk/skill gate -
packMuleCrafting is a weight-while-crafting tag, not a gate (see vanilla
campfire/candle recipes for the same pattern). Per BACKLOG.md item 1: 1 rock +
20 grass fiber (resourceRockSmall doubles as both the crafted item's own mesh/icon
source and its craft ingredient here, same as vanilla's own resourceRockSmallBundle). -->
<append xpath="/recipes">
<!-- resourceZombieAsh added 2026-08-29 (BACKLOG.md item 8) - count is a guess, not
specified by the user, kept modest like the rest of this recipe's ingredients. -->
<!-- resourceZombieAsh REMOVED 2026-08-31, direct user correction: this is the starter
weapon, craftable before the player has the Knife at all - same circular-dependency
problem items.xml's own resourceZombieAsh comment already flags and deliberately avoids
for the Knife's own recipe ("the knife has to stay craftable from scratch, and ash only
exists because the knife already tagged a zombie as a Victim"). Ash got added here
anyway during the 2026-08-29 item-8 blanket rollout ("becomes a crafting ingredient
across the other necromancy recipes") without checking this one against that same rule
- missed it then, fixed now. Back to the original 2026-08-28 recipe (1 rock + 20
fibers, BACKLOG.md item 1), nothing else changed. -->
<recipe name="thrownStoneSpirit" count="1" tags="packMuleCrafting">
<ingredient name="resourceRockSmall" count="1"/>
<ingredient name="resourceYuccaFibers" count="20"/>
</recipe>
</append>
<!-- Two portal stones, BACKLOG.md item 6. Blue: no gate, simple early recipe by analogy with
the Spirit Stone (per that backlog note) - not specified exactly by the user. Black:
necroNecromancyMaster (level 5000, the existing "Мастер" tier, group 5) per direct
instruction. Both carry resourceZombieAsh like the other necromancy recipes
(BACKLOG.md item 8). -->
<append xpath="/recipes">
<!-- resourceZombieAsh bumped 5 -> 150, direct user correction 2026-08-31: unlike the Spirit
Stone (removed entirely, see that fix above), the Blue Portal Stone is NOT a starter
weapon - "он не является начальным оружием... больше про комфорт" - so the same
circular-dependency concern doesn't apply, and the user wants it to actually cost a
real chunk of ash rather than a token amount. -->
<recipe name="thrownStonePortalBlue" count="1" tags="packMuleCrafting">
<ingredient name="resourceRockSmall" count="1"/>
<ingredient name="drinkJarPureMineralWater" count="3"/>
<ingredient name="resourceYuccaFibers" count="15"/>
<ingredient name="resourceZombieAsh" count="150"/>
</recipe>
</append>
<!-- resourceNecromancerBlood x10 added 2026-08-30, direct instruction ("10 крови некроманта
нужно будет для рецепта чёрного портала"). ADDITIVE to the existing medicalBloodBag
ingredient, not a replacement - the user said "также" (also/additionally), not "instead
of"; the plain blood bag stays as the mundane-blood component, Necromancer's Blood as the
new, harder-to-get one. Say if this should replace medicalBloodBag instead. -->
<append xpath="/recipes">
<recipe name="thrownStonePortalBlack" count="1" tags="learnable,packMuleCrafting,necroNecromancyMaster">
<ingredient name="resourceRockSmall" count="1"/>
<ingredient name="medicalBloodBag" count="5"/>
<ingredient name="resourceNecromancerBlood" count="10"/>
<ingredient name="casinoCoin" count="20"/>
<ingredient name="resourceYuccaFibers" count="15"/>
<ingredient name="resourceZombieAsh" count="20"/>
</recipe>
</append>
<!-- "Кровь некроманта" (Necromancer's Blood): dictated 2026-08-30, see items.xml for the item
itself and HarmonySrc/NecromancerBloodPatch.cs for the knife-requirement/HP-cost mechanic
this recipe alone can't express. No level gate given by the user and none applied - same
"base" tier as the jar it's made from being a mundane always-available item; the real
gate on this ritual is the HP cost + knife requirement, not a skill level. drinkJarEmpty
confirmed as the real vanilla empty-jar item (Data/Config/items.xml) - not invented. -->
<append xpath="/recipes">
<recipe name="resourceNecromancerBlood" count="1" tags="packMuleCrafting">
<ingredient name="drinkJarEmpty" count="1"/>
</recipe>
</append>
<!-- "Пирамида Ереси" (Pyramid of Heresy): user request 2026-08-31, see blocks.xml/
HarmonySrc/PyramidWardPatch.cs for the block itself. "Рецепт доступен сразу" - no
"learnable" tag and no gate beyond necroNecromancyAdept (Group 1 "Адепт", unlocked from
level 1 - see progression.xml's own comment: a recipe with no unlock tag at all is already
always-available, the tag here is added anyway purely so the intent ("this is a Tier-1,
immediately-available necromancy recipe") is visible directly in this file, matching how
braceletSpatialVault documents itself with necroNecromancyLvl20 below rather than staying
unmarked). craft_area="workbench" (unlike the hand-craftable Knife/Spirit Stone) - this is
a heavy placeable structure, not a pocket item, matching braceletSpatialVault's own
workbench gate.
INGREDIENTS: resourceZombieAsh(1200)/resourceRockSmall/resourceYuccaFibers dictated
directly by the user ("прах зомби, камень, трава"). The rest chosen to fit a "necromantic
iron pylon" ("остальные ингредиенты добавь сам исходя из контекста"):
- resourceScrapIron: the block's own structural frame (real vanilla resource, matches
its own Destroy-drop in blocks.xml).
- resourceNecromancerBlood: the mod's existing magic-binding reagent (see above) - ties
the ward's charm magic to the same "blood" reagent already used elsewhere in this
mod's necromancy recipes, kept to a small count (3) since it costs the player 90% of
their current HP per unit to make (NecromancerBloodPatch.cs) - this recipe shouldn't
demand many. -->
<append xpath="/recipes">
<recipe name="necroHeresyPyramid" count="1" craft_area="workbench" tags="workbenchCrafting,necroNecromancyAdept">
<ingredient name="resourceZombieAsh" count="1200"/>
<ingredient name="resourceRockSmall" count="300"/>
<ingredient name="resourceYuccaFibers" count="200"/>
<ingredient name="resourceScrapIron" count="300"/>
<ingredient name="resourceNecromancerBlood" count="3"/>
</recipe>
</append>
<!-- "Гримуар девиации" (Grimoire of Deviation): BACKLOG.md item 2. Gated at
necroNecromancyJourneyman - group 2 "Подмастерье" (level 500), one of the 5 real skill
tiers (see progression.xml; REDISTRIBUTED 2026-08-30, was a one-off level-50 tag before -
direct user correction, "уровень скилла ты распределил идиотски"). -->
<append xpath="/recipes">
<recipe name="thrownBookGrimoireDeviation" count="1" tags="learnable,packMuleCrafting,necroNecromancyJourneyman">
<ingredient name="resourcePaper" count="20"/>
<ingredient name="resourceGlue" count="2"/>
<ingredient name="resourceCoal" count="5"/>
<ingredient name="resourceWood" count="10"/>
<ingredient name="resourceYuccaFibers" count="10"/>
<ingredient name="resourceZombieAsh" count="5"/>
</recipe>
</append>
<!-- "Призыв зомбособаки" (Summon Zombie Dog): BACKLOG.md item 3. Gated at
necroNecromancyApprentice - group 3 "Ученик" (level 2000), one of the 5 real skill tiers
(see progression.xml; REDISTRIBUTED 2026-08-30, was a one-off level-200 tag before -
direct user correction, "уровень скилла ты распределил идиотски"). Per user request,
crafted at a workbench specifically (craft_area="workbench"), same as advanced-tier weapons
(e.g. vanilla gunHandgunT3SMG5) - NOT available from the player's own personal crafting
menu like the Spirit Stone/Grimoire above (those have no craft_area, so default to
personal crafting). workbenchCrafting is just the same UI-categorization tag vanilla's
own workbench recipes carry alongside craft_area, not a separate gate. -->
<append xpath="/recipes">
<recipe name="bookSummonZombieDog" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyApprentice">
<ingredient name="foodRottingFlesh" count="50"/>
<ingredient name="medicalBloodBag" count="3"/>
<ingredient name="drinkJarBoiledWater" count="4"/>
<ingredient name="resourceYuccaFibers" count="50"/>
<ingredient name="resourceCloth" count="3"/>
<ingredient name="resourceLeather" count="10"/>
<ingredient name="resourceBone" count="40"/>
<ingredient name="casinoCoin" count="5"/>
<ingredient name="resourceZombieAsh" count="10"/>
</recipe>
</append>
<!-- "Жуки Властелина": user request 2026-08-28. Personal crafting (no craft_area) - unlike the
Dog, unconfirmed whether this should be workbench-gated too; see items.xml. Same
necroNecromancyApprentice gate as the Dog - group 3 "Ученик" (level 2000, see
progression.xml; REDISTRIBUTED 2026-08-30 like the others above). Ingredients per
explicit user instruction: 1 resourceQueenBee ("одна пчеломатка") is required; the rest
("сам додумай, но без фанатизма") is a modest hive/bait theme (honey to draw and bind the
swarm, wood for a hive, fiber to hold it together) - kept deliberately small, no rare
currency/high counts like the Dog's recipe. -->
<append xpath="/recipes">
<recipe name="bookSummonInsectSwarm" count="1" tags="learnable,necroNecromancyApprentice">
<ingredient name="resourceQueenBee" count="1"/>
<ingredient name="foodHoney" count="20"/>
<ingredient name="resourceWood" count="10"/>
<ingredient name="resourceYuccaFibers" count="15"/>
<ingredient name="resourceZombieAsh" count="5"/>
</recipe>
</append>
<!-- "Книга банши" (Banshee's Book): BACKLOG.md item 7. Gated at necroNecromancyNecromancer -
group 4 "Некромант" (level 3000), alongside Bear/Wolf (see progression.xml;
REDISTRIBUTED 2026-08-30, was necroNecromancyLvl200 before) - not specified by the user,
guessed consistent with the other "advanced ritual" summon items. -->
<append xpath="/recipes">
<recipe name="bookBanshee" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyNecromancer">
<ingredient name="resourcePaper" count="15"/>
<ingredient name="resourceGlue" count="3"/>
<ingredient name="foodRottingFlesh" count="10"/>
<ingredient name="resourceYuccaFibers" count="15"/>
<ingredient name="resourceZombieAsh" count="10"/>
</recipe>
</append>
<!-- "Нож некроманта" (Necromancer's Knife): BACKLOG.md item 5, user request 2026-08-28.
No recipe/gate specified beyond the item's own mechanics - guessed modest and available
from the start (personal crafting, no craft_area, no level tag), same tier as the Spirit
Stone, since its real power comes from playing the necromancer build over time (damage
scales with kill count), not an expensive unlock. Say if this should change.
resourceZombieAsh (BACKLOG.md item 8) deliberately NOT added here, unlike the other
necromancy recipes - per explicit user instruction 2026-08-29: ash only drops from
zombies the knife itself has already marked as "Жертва", so requiring ash to craft the
knife would make the knife uncraftable from a fresh start (circular dependency).
medicalBloodBag SWAPPED for resourceNecromancerBlood 2026-08-30, direct instruction
("для ножа некроманта пусть используется кровь некроманта"). NOT a circular dependency
like the ash case above, even though this item needs a knife to craft (see
NecromancerBloodPatch.cs) - resourceNecromancerBlood's own requirement is "ANY knife"
(ItemClass.DisplayType == "meleeKnife"), which vanilla's own starting/craftable knives
(e.g. the plain bone knife) already satisfy - it does NOT require this specific
necroWpnBladeNecroKnife to already exist. Count kept at 1, same as the medicalBloodBag it
replaces - not inflated further, since the blood itself is now a genuinely costly
ingredient (90% current HP + a knife + a jar per unit). -->
<append xpath="/recipes">
<recipe name="necroWpnBladeNecroKnife" count="1" tags="packMuleCrafting">
<ingredient name="resourceBone" count="5"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceYuccaFibers" count="10"/>
</recipe>
</append>
<!-- Three more summon-book recipes, BACKLOG.md item 4a. REPLACED 2026-08-29 - Griffin/Bear/
Wolf instead of Stripper/Cop/Soldier (see entityclasses.xml). Same workbench shape as the
Dog's own recipe; Griffin shares the Dog's gate (necroNecromancyApprentice, group 3
"Ученик", level 2000), Bear/Wolf are one tier higher (necroNecromancyNecromancer, group 4
"Некромант", level 3000, alongside the Banshee) - split REDISTRIBUTED 2026-08-30 across
the real 5 skill tiers instead of one shared one-off level-200 tag (see progression.xml).
Ingredient lists are a themed guess per creature (not specified by the user) -
resourceFeather/foodRawMeat both verified to exist in vanilla items.xml before use, same
lesson as the earlier wrong guesses. -->
<append xpath="/recipes">
<recipe name="bookSummonZombieGriffin" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyApprentice">
<ingredient name="resourceFeather" count="30"/>
<ingredient name="resourceBone" count="20"/>
<ingredient name="casinoCoin" count="5"/>
<ingredient name="resourceYuccaFibers" count="20"/>
<ingredient name="resourceZombieAsh" count="10"/>
</recipe>
</append>
<append xpath="/recipes">
<recipe name="bookSummonZombieBear" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyNecromancer">
<ingredient name="resourceLeather" count="20"/>
<ingredient name="foodRawMeat" count="10"/>
<ingredient name="resourceBone" count="20"/>
<ingredient name="resourceYuccaFibers" count="20"/>
<ingredient name="resourceZombieAsh" count="10"/>
</recipe>
</append>
<append xpath="/recipes">
<recipe name="bookSummonZombieWolf" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyNecromancer">
<ingredient name="resourceLeather" count="15"/>
<ingredient name="foodRawMeat" count="10"/>
<ingredient name="resourceBone" count="15"/>
<ingredient name="resourceYuccaFibers" count="20"/>
<ingredient name="resourceZombieAsh" count="10"/>
</recipe>
</append>
<!-- "Петля вора" (Thief's Loop) recipe REMOVED 2026-08-30 along with the item itself - see
items.xml for why. -->
<!-- "Пространственный браслет" (Spatial Bracelet) - gate moved 2026-08-30 from
necroNecromancyLvl200 to necroNecromancyLvl20 per direct instruction ("нож, камень духов
и хранилище - это база... хранилище, когда убито минимум 20 зомби"). craft_area/workbench
left as-is (not specified either way) - still an "advanced" build, just unlocked much
earlier than before. -->
<append xpath="/recipes">
<recipe name="braceletSpatialVault" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyLvl20">
<ingredient name="resourceLeather" count="10"/>
<ingredient name="resourceMechanicalParts" count="5"/>
<ingredient name="resourceScrapIron" count="10"/>
<ingredient name="resourceYuccaFibers" count="15"/>
<ingredient name="resourceZombieAsh" count="10"/>
</recipe>
</append>
<!-- СТУПЕНЧАТАЯ РАЗБЛОКИРОВКА МОДОВ НОЖА. Изначально (2026-09-07) продиктовано: "механика
скиллов даёт возможность сделать их доступными не сразу, а по мере набора количества
убитых зомби. Т.е. грейд открылся, а рецепт ещё нет, на нём замочек. Еда/вода должны
открываться на грейде сразу. Чутьё чуть позже, хватка ещё позже. Но всё в рамках грейда."
ПЕРЕСТАВЛЕНО 2026-09-09 - прямая правка баланса от пользователя: "Питьё важно в тот же
день. Оно должно быть доступно после 30 убитых зомби. Еда - 60. Это самые важные для
начала выживания модификации. Модификация на покой - 100 зомби. Модификацию на тёмное
чутьё я бы сделал доступной после 300 убитых зомби. Дальше уже не так принципиально."
ЧТО БЫЛО НЕ ТАК. Первая раскладка держала все шесть модов внутри группы 2, то есть в
диапазоне 500-1700 убийств. Ошибка не в коде (тег рецепта, RecipeTagUnlocked и
unlock_tier сходились по всем шести), а в балансе: вода и еда нужны в ПЕРВЫЕ ДНИ, а к
500 убийствам у игрока уже есть ферма, костёр, банки и фильтр - мод, дающий +2 воды с
трупа, к этому моменту бесполезен. Теперь порог у мода стоит там, где мод реально нужен,
а не там, где он "по силе" смотрится ровно.
Итоговая раскладка:
30 - Слёзы мертвеца (вода) группа 1 "Адепт"
60 - Пир падальщика (еда) группа 1
100 - Могильный покой (тепло/холод) группа 1
300 - Тёмное чутьё (радар) группа 1
1400 - Хватка мертвеца (замедление) группа 2 "Подмастерье"
1700 - Мёртвая буря (силовая) группа 2
Первые четыре - выживание и информация, они переехали в группу 1 к самому ножу (он там и
доступен с уровня 1). Последние два - чистый бой, остались в группе 2 на прежних порогах:
пользователь про них сказал "дальше уже не так принципиально".
Двигать - тройка "тег в рецепте + RecipeTagUnlocked в progression.xml + unlock_tier в
display_entry", все три должны совпадать, иначе замок на панели соврёт. -->
<!-- Модификации Ножа некроманта (см. Config/item_modifiers.xml), 2026-09-07. Пользователь
выбрал получение через "Крафт по скиллу Некромантии", поэтому все три идут обычными
рецептами с тегом группы, а не через лут.
Пороги у каждого свои, см. комментарий выше - изначально все шесть сидели в группе 2
("Подмастерье", 500 убийств), но 2026-09-09 четыре из них уехали в группу 1 к самому
ножу. Распределение остальных тиров скилла эта правка не трогает - его пользователь уже
правил вручную (см. progression.xml).
Ингредиенты - только из уже существующих ресурсов мода плюс ванильная база, как и просил
("Ингредиенты из уже существующих ресурсов мода"). Прах зомби и Кожа жертвы обе падают
из мешка "Жертва", то есть добываются этим же ножом - моды для ножа делаются из того, что
нож добыл. Крафт личный (без craft_area), как у Камня духов и самого ножа: моды дешевле
призывов и не должны требовать верстак. Тег learnable, как у остальных гейтованных
рецептов мода, чтобы рецепт не светился в меню до открытия группы. -->
<append xpath="/recipes">
<recipe name="necroModKnifeTearsOfTheDead" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl30">
<ingredient name="resourceZombieAsh" count="10"/>
<ingredient name="resourceVictimSkin" count="1"/>
<ingredient name="drinkJarEmpty" count="2"/>
<ingredient name="resourceYuccaFibers" count="10"/>
</recipe>
<recipe name="necroModKnifeScavengersFeast" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl60">
<ingredient name="resourceZombieAsh" count="10"/>
<ingredient name="resourceVictimSkin" count="1"/>
<ingredient name="foodRawMeat" count="5"/>
<ingredient name="resourceYuccaFibers" count="10"/>
</recipe>
<recipe name="necroModKnifeDeadMansGrip" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl1400">
<ingredient name="resourceZombieAsh" count="10"/>
<ingredient name="resourceBone" count="15"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceYuccaFibers" count="10"/>
</recipe>
<!-- "Могильный покой" (термозащита), добавлен 2026-09-07, порог 100 с 2026-09-09. Тот же
личный крафт, что у остальных модов ножа. Кожа жертвы тут не косметика: мод по смыслу - обмотка
рукояти, поэтому её взято 2 (больше всех), плюс перо как утеплитель - ванильный
resourceFeather, существование проверено. -->
<recipe name="necroModKnifeGravesRepose" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl100">
<ingredient name="resourceZombieAsh" count="10"/>
<ingredient name="resourceVictimSkin" count="2"/>
<ingredient name="resourceFeather" count="10"/>
<ingredient name="resourceYuccaFibers" count="10"/>
</recipe>
<!-- "Мёртвая буря" (переделка силовой атаки), 2026-09-07. Единственный мод ножа с
электрическими деталями в рецепте - искрение берётся из ванильного buffShocked, того
же, что у электродубинки, так что ингредиент тематически честный. Существование
resourceElectricParts проверено. -->
<!-- "Тёмное чутьё" (радар зомби), 2026-09-07. Глаз мертвеца как линза - отсюда кожа
жертвы и прах. Стекло взято resourceBrokenGlass (обычный лут), а НЕ resourceScopeLens:
та линза крафтится только в кузне и под перком perkAdvancedEngineering, то есть
утащила бы рецепт ножа в зависимость от чужой ветки прокачки. resourceGlass, которое
напрашивалось по названию, в игре не существует вовсе - проверено. -->
<recipe name="necroModKnifeDarkSense" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl300">
<ingredient name="resourceZombieAsh" count="15"/>
<ingredient name="resourceVictimSkin" count="1"/>
<ingredient name="resourceBrokenGlass" count="10"/>
<ingredient name="resourceYuccaFibers" count="10"/>
</recipe>
<recipe name="necroModKnifeDeadStorm" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl1700">
<ingredient name="resourceZombieAsh" count="15"/>
<ingredient name="resourceElectricParts" count="5"/>
<ingredient name="resourceNecromancerBlood" count="1"/>
<ingredient name="resourceYuccaFibers" count="10"/>
</recipe>
</append>
</config>