6 Commits
Author SHA1 Message Date
AlexCubeandClaude Opus 5 d97f83e972 ИИ питомцев переписан, добавлен Дух крысы, пороги разнесены по шкале
Работа по указаниям 2026-09-18. В релиз пока не выходит.

НОВЫЙ ПИТОМЕЦ "Дух крысы" (necroRatSpirit), Некромантия 3 (60 убийств):
модель зомбоволка в масштабе 0.28, урон 5, кроличьи звуки. Сам не нападает
никогда, держится справа-сзади в двух блоках, упёршись в препятствие проходит
СКВОЗЬ него и блоков не ломает. Укус замедляет, оставляет метку духа (видна на
компасе, +25% получаемого урона) и рвёт жилу. Обгрызая труп, лечится.

СВОЯ ЗАДАЧА ИИ. Задачи "иди за сущностью" в игре нет вовсе - проверены все 32
типа EAI*. Написана NecroFollowOwnerTask по образцу EAIApproachSpot: сектор
"3-6 часов" от хозяина, FindPath с canBreak:false, проход сквозь препятствие
через Entity.IsStuck. Вешается в рантайме, минуя Type.GetType.

ПРИКАЗ АТАКОВАТЬ. Повторное применение свитка при живом питомце натравливает
его на цель под прицелом: EntityPlayerLocal.HitInfo + ItemActionAttack.
GetEntityFromHit. Торговцы и игроки отсеяны. Нет цели - "Нет цели для атаки".

ЗОМБОЖИВОТНЫЕ ПРИВЕДЕНЫ К ТОМУ ЖЕ ОБРАЗЦУ. Сняты BreakBlock, Territorial,
ApproachSpot, Wander, BlockingTargetTask и поедание трупов; цель они больше не
выбирают сами. Лестница урона 20/35/45/60 плюс расчленение у Пса, Медведя и
Волка. Кровотечение всем, метка и ослабление - только у крысы.

УБИЙСТВА ПИТОМЦЕМ ЗАСЧИТЫВАЮТСЯ ВЛАДЕЛЬЦУ, включая добивание кровотечением.
Префикс на AwardKillXPServer подменяет убийцу владельцем; для смерти от баффа
заведена память укусов, потому что в DamageSource от баффа нет того, кто его
наложил. Зомби под Камнем духов это не задело - решение от 17.09 в силе.

ГРИФ откачен на летающую ветку EntityVulture и переименован в Могильного
стервятника: модель наконец соответствует имени. Держится у игрока сам, через
собственный механизм "дома" (setHomeArea), на время погони дом отвязывается.
Попытка натянуть птичий префаб на наземный класс провалилась и записана - так
делать нельзя.

ПРОЧЕЕ: призрачный вид распространён с торговцев на питомцев (у Пса, Медведя и
Волка выключен по указанию), у Пса светятся фиолетовые глаза, белая иконка
книги снята со всех свитков призыва, "Жуки Властелина" переименованы в "Рой
фараона" на всех 13 языках.

ПОРОГИ: крыса 60, стервятник 500, пёс 1300, медведь 2000, волк 4000.

ИСПРАВЛЕНО ПО ХОДУ ИГРОВЫХ ПРОВЕРОК: питомцы не призывались обычным кликом
(AnimWait требовал удержания), отзыв срабатывал не с первого раза (автомат
состояний ItemActionSpawnEntity), крыса проваливалась сквозь мир (IsStuck
отключает и пол), питомец подбрасывал хозяина (коллайдеры разводились до
появления модели).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 20:51:42 +03:00
AlexCubeandClaude Opus 5 15c59dd37a Changelog переписан под формат Nexus: строка = один пункт
Пользователь: "У тебя там много переносов строк, и каждый перенос отображается
как отдельное изменение."

Так и есть, проверено по разметке живой записи: Nexus заворачивает КАЖДУЮ строку
в свой <li> внутри ul.arrowlist. Мой текст был разбит по ширине в 90 символов,
поэтому на странице получилось 62 пункта со стрелками - половина из них обрывки
фраз вроде "skill's level in a single byte, so everything above 255 was cut".

Текст переписан: 16 строк, каждая - законченный пункт, переносов по ширине нет.
Содержание сохранено, только плотнее: откат уровня и новая шкала, самопочинка
сейвов, два индикатора, мир между подчинёнными, счёт убийств с благодарностью
Derizor и Savion, непочиненная модель кинжала для Scarlettdruid, изменения
порогов и блок "если приходишь с 1.0.1".

Строки длинные (до 288 знаков) - это нормально, пункт переносится сам по ширине
колонки, оставаясь одним пунктом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 00:23:21 +03:00
AlexCubeandClaude Opus 5 f5f9c9b9ff Changelog: убрана первая строка с номером версии
Nexus рисует "Version 1.3.0" над записью сам, из отдельного поля, а текст
начинался с той же строки - на странице номер задваивался. Проверено на живой
записи: "Version 1.3.0 1.3.0 FIXED: ...".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 00:20:14 +03:00
AlexCubeandClaude Opus 5 e2046a1866 Тексты для Nexus в тех форматах, которые сайт на самом деле принимает
Пользователь: "Описание файлов не bb-код и всего 255 знаков."

Описание ФАЙЛА на Nexus - обычный текст с жёстким пределом 255 знаков, разметка
там не работает. Мой NEXUS_FILE_DESCRIPTION_1.3.0.bbcode был написан в неверном
формате и втрое длиннее предела, поэтому удалён, а не оставлен путаться под
рукой. Задним числом это объясняет и живую страницу: у файла 1.0.1 на сайте
висит короткая заметка, а подробный NEXUS_FILE_DESCRIPTION_1.0.1.bbcode из
репозитория туда никогда и не вставлялся - он физически не мог туда влезть.

Вместо него два файла по назначению:

  NEXUS_FILE_SUMMARY_1.3.0.txt  - 251 знак, в поле описания файла;
  NEXUS_CHANGELOG_1.3.0.txt     - полные заметки во вкладку Changelogs, тоже
                                  обычным текстом.

Содержание длинного текста сохранено целиком, включая поимённый ответ Derizor,
Savion и Scarlettdruid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 00:02:32 +03:00
AlexCubeandClaude Opus 5 7fa68618b2 Описание 1.3.0: поимённый ответ на репорты из комментариев Nexus
Страницу мода удалось прочитать впервые (Chrome со своим профилем и отладочным
портом - curl и WebFetch там получают 403 от Cloudflare). В комментариях висит
жалоба Derizor (14.09) и Savion (15.09): убийство не засчитывается, если добил не
игрок своим оружием. Это ровно тот баг, который закрыт 16.09 в 229b436 - завели
его по своему наблюдению, не зная про тред.

В описание версии добавлен блок с ответом обоим поимённо и честная строка про
непочиненное: модель кинжала (Scarlettdruid) ждёт своего 3D-префаба.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 23:51:38 +03:00
AlexCubeandClaude Opus 5 7da12f562a YouTube-канал: @alex_cube вместо неверного @alexcube
Пользователь: "Ссылка на ютуб у тебя неверная. Должно быть @alex_cube".
Правка во всех четырёх текстах, где ссылка встречалась (README RU+EN,
SITE_DESCRIPTION, NEXUS_DESCRIPTION и старое описание версии 1.0.1) - и в самом
URL, и в подписи. Заодно описание версии 1.3.0 получило тот же футер со
ссылками, что и 1.0.1: исходники, страница на сайте, канал.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 23:35:02 +03:00
25 changed files with 2524 additions and 232 deletions
+22 -10
View File
File diff suppressed because one or more lines are too long
+123
View File
@@ -200,5 +200,128 @@
</effect_group>
</buff>
<!-- "Путы духа крысы" - укус Духа крысы, 2026-09-18.
БЫЛО НОКДАУНОМ, СТАЛО ЗАМЕДЛЕНИЕМ (в тот же день, по игровой проверке: "ронять зомби
при каждом ударе смотрится скорее смешно"). Первая версия ставила из кода
bodyDamage.CurrentStun = Prone на 5 секунд - механизм рабочий, но зомби валился на
землю от каждого укуса мелкого духа, и выглядело это комично.
ПОЧЕМУ ТЕПЕРЬ ХВАТАЕТ ЧИСТОГО XML, А РАНЬШЕ НЕ ХВАТАЛО. Открытым был ровно один вопрос:
доходят ли RunSpeed/WalkSpeed до ИИ, или это чисто игроцкие величины. Ответ нашёлся в
EntityAlive:
public virtual float GetMoveSpeedAggro() {
if (IsBloodMoon || world.IsDark())
return EffectManager.GetValue(PassiveEffects.RunSpeed, null, moveSpeedAggroMax, this);
return EffectManager.GetValue(PassiveEffects.WalkSpeed, null, moveSpeedAggro, this);
}
Скорость зомби берётся через EffectManager, а EntityMoveHelper.SetMoveTo читает её
оттуда же. Значит обычный пассивный эффект работает, и весь Harmony-код (RatGripPatch.cs)
оказался не нужен - удалён.
ОБЕ ВЕЛИЧИНЫ ОБЯЗАТЕЛЬНЫ. Ветка в GetMoveSpeedAggro выбирается по времени суток: ночью и
в кровавую луну читается RunSpeed, днём - WalkSpeed. Прописать одну - значит получить
замедление, которое молча отваливается на полсуток. CrouchSpeed добавлен за компанию,
для крадущихся спящих.
ЗАТУХАНИЕ, А НЕ РОВНАЯ ПОЛКА: value="0.7,.1" с duration="0,5" - это -70% в момент укуса,
плавно сходящие к -10% за 5 секунд, тем же приёмом, что у ванильного buffInjurySlow.
stack_type replace: каждый следующий укус начинает отсчёт заново, поэтому пока крыса
грызёт, зомби так и держится возле -70%, а стоит ей отвлечься - он разгоняется обратно.
Кулдаун, который был нужен нокдауну (иначе один питомец держал бы зомби лежачим вечно),
здесь не нужен и удалён вместе с ним: замедление, обновляемое укусами, - это ровно то
поведение, которое и требовалось. -->
<buff name="buffNecroRatGrip" name_key="buffNecroRatGripName" description_key="buffNecroRatGripDesc" icon="ui_game_symbol_twitch_slow" icon_color="140,200,255">
<stack_type value="replace"/>
<duration value="5"/>
<effect_group>
<passive_effect name="RunSpeed" operation="perc_subtract" value="0.7,.1" duration="0,5"/>
<passive_effect name="WalkSpeed" operation="perc_subtract" value="0.7,.1" duration="0,5"/>
<passive_effect name="CrouchSpeed" operation="perc_subtract" value="0.7,.1" duration="0,5"/>
</effect_group>
</buff>
<!-- "Метка духа" - вторая половина укуса Духа крысы, 2026-09-18 (указания "предложи дебаф
поинтереснее" -> выбраны варианты 2 и 3, метка и гниль, вместе).
ДВА ЭФФЕКТА, ОДИН БАФФ, И ЖИВЁТ ОН ДОЛЬШЕ ЗАМЕДЛЕНИЯ. Замедление (buffNecroRatGrip
выше) осмысленно только пока крыса грызёт, поэтому оно короткое и обновляется каждым
укусом. Метка - наоборот: её ценность в том, что она ПЕРЕЖИВАЕТ драку. Тридцать секунд
достаточно, чтобы добежать до помеченного и добить его, и мало, чтобы карта превратилась
в ёлку.
1. ВИДНО. SetNavObject на самого носителя - тот же механизм, которым сделано Тёмное
чутьё (necroModKnifeDarkSense ниже), только там selfAOE по площади, а здесь
target="self": метится ровно тот, кого укусили. Класс метки свой и зелёный, не
красный ванильный, - иначе добыча крысы была бы неотличима от зомби, подсвеченных
чутьём (см. комментарий в nav_objects.xml).
Снимается тремя триггерами, а не одним: по истечении баффа, по смерти носителя и на
всякий случай - тем же способом, что RemoveParticleEffectFromEntity у девиации выше.
Метка на трупе - это метка, которая не уйдёт уже никогда.
2. ГНИЁТ. GeneralDamageResist в МИНУС - помеченный получает на четверть больше урона от
всего подряд. Это не выдумка и не побочный эффект: сопротивление читается со стороны
ЖЕРТВЫ, в EntityAlive.DamageEntity:
float num = Utils.FastMin(1f, EffectManager.GetValue(PassiveEffects.GeneralDamageResist, null, 0f, this));
float num2 = (float)_strength * num + accumulatedDamageResisted;
int num3 = Utils.FastMin(_strength, (int)num2);
_strength -= num3;
при отрицательном сопротивлении num3 отрицателен, и вычитание его УВЕЛИЧИВАЕТ урон.
Ограничение FastMin(1f, ...) режет только верх (100% сопротивления), низ открыт.
Вместе это и есть роль Духа крысы: он не убивает сам (урон 5), он находит добычу и
делает её уязвимой для хозяина. -->
<buff name="buffNecroRatMark" name_key="buffNecroRatMarkName" description_key="buffNecroRatMarkDesc" icon="ui_game_symbol_skull" icon_color="150,220,70">
<stack_type value="replace"/>
<duration value="30"/>
<effect_group>
<passive_effect name="GeneralDamageResist" operation="base_subtract" value="0.25"/>
<triggered_effect trigger="onSelfBuffStart" action="SetNavObject" target="self" nav_object="necroRatSpiritMark" add="true"/>
<triggered_effect trigger="onSelfBuffRemove" action="SetNavObject" target="self" nav_object="necroRatSpiritMark" add="false"/>
<triggered_effect trigger="onSelfDied" action="SetNavObject" target="self" nav_object="necroRatSpiritMark" add="false"/>
</effect_group>
</buff>
<!-- "Кровоточащая рана" - кровотечение от укуса Духа крысы, 2026-09-18.
ВАНИЛЬНОЕ buffInjuryBleeding НА ЗОМБИ НЕ РАБОТАЕТ ВООБЩЕ, и стояло оно тут ошибочно.
Замечено пользователем по игре ("при такой слабой атаке байкера она била минут 5"),
подтверждено чтением Data/Config/buffs.xml. Урон там берётся не из самого баффа, а из
переменной игрока:
<triggered_effect trigger="onSelfBuffStart" action="ModifyCVar" cvar="$bleedAmount"
operation="set" value="@bleedCounter"/>
<passive_effect name="HealthChangeOT" operation="base_subtract" value="@$bleedAmount"/>
<triggered_effect trigger="onSelfBuffUpdate" action="RemoveBuff" buff="buffInjuryBleeding">
<requirement name="CVarCompare" cvar="bleedCounter" operation="Equals" value="0"/>
</triggered_effect>
bleedCounter накручивают ДРУГИЕ баффы-обёртки (buffInjuryBleedingOne/Two,
buffInjuryBleedingBarbedWire), и это часть игроцкой системы травм. Повесить
buffInjuryBleeding напрямую на зомби - значит получить счётчик 0: урон ноль, и бафф
снимает сам себя на первом же обновлении. Ровно ноль эффекта, молча.
Поэтому здесь своё, без единой внешней переменной: сколько написано, столько и капает.
ЧИСЛА. 5 в секунду на 15 секунд. Укус крысы даёт 5 урона и проходит примерно раз в
секунду, то есть кровотечение РОВНО УДВАИВАЕТ её вклад, а stack_type replace означает,
что пока она грызёт, рана не заживает. Отвлеклась - зомби живёт ещё 15 секунд и
перестаёт течь. Если в игре окажется мало или много, править надо эти две цифры и
больше ничего. -->
<buff name="buffNecroRatBleed" name_key="buffNecroRatBleedName" description_key="buffNecroRatBleedDesc" icon="ui_game_symbol_critical" icon_color="200,40,40" icon_blink="true">
<damage_type value="bloodloss"/>
<damage_source value="Internal"/>
<stack_type value="replace"/>
<duration value="15"/>
<update_rate value="1"/>
<effect_group>
<passive_effect name="HealthChangeOT" operation="base_subtract" value="5"/>
</effect_group>
</buff>
</append>
</config>
+161 -48
View File
@@ -58,9 +58,41 @@
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"/>
<!-- ИИ ПЕРЕПИСАН 2026-09-18 ПОД ОБРАЗЕЦ ДУХА КРЫСЫ, прямое указание: "поведение
других призванных животных зомби тоже подгони под крысу. По сути это те же
крысы, но размер модели оригинальный, урон нарастает от зомбособаки до
зомбоволка". Правка стоит здесь, у necroZombieDog, и этого достаточно:
Зомбомедведь, Зомбоволк и Зомбогриф расширяют именно его и унаследуют всё.
БЫЛО (и разобрано в BACKLOG.md 18.09 как "питомцы унаследовали ИИ враждебной
твари"): AITask-3 переопределял только погоню, а от animalZombieDog оставались
BreakBlock (грыз базу игрока), Territorial (держался точки призыва, а не
хозяина), ApproachSpot и Wander (бродил сам по себе), BlockingTargetTask и
SetNearestCorpseAsTarget (бросал хозяина ради ближайшего трупа), а
SetNearestEntityAsTarget заставлял его самому лезть в драку в 22 метрах.
СТАЛО, ровно как у крысы:
- AITask-3 пустой обрывает перебор, поэтому BreakBlock/Territorial/ApproachSpot/
Wander не грузятся вовсе (CopyPropertiesFromEntityClass идёт по индексам и
встаёт на первом пустом);
- AITarget-2 пустой так же убирает BlockingTargetTask и поедание трупов;
- цель НЕ ВЫБИРАЕТСЯ самостоятельно никогда: SetNearestEntityAsTarget больше
нет. Цель приходит только приказом игрока (PetCommandPatch.cs);
- AITarget-1 остаётся только как ответ на удар, и список классов без
EntityPlayer: питомец огрызается на зомби и враждебных зверей, но никогда на
хозяина;
- class=EntityAlive в AITask-1 - это разрешение задаче подхватить ту цель,
которую ей ДАЛИ, а не "бей всё живое" (EAIApproachAndAttackTarget.CanExecute
сверяет тип цели со списком и без совпадения не запускается). Кого можно
назначать, решает PetCommandPatch.cs, и торговец там отсеян.
Задача следования (NecroFollowOwnerTask) вешается в рантайме из SummonPatch.cs -
всем, у кого UsesFollowTask, то есть теперь и этим четверым. -->
<property name="AITask-1" value="ApproachAndAttackTarget" data="class=EntityAlive,0"/>
<property name="AITask-2" value="Look"/>
<property name="AITask-3" value=""/>
<property name="AITarget-1" value="SetAsTargetIfHurt" data="class=EntityZombie,EntityEnemyAnimal"/>
<property name="AITarget-2" value=""/>
</entity_class>
</append>
@@ -156,6 +188,9 @@
<property name="PrefabCombined" value="true"/>
<property name="PhysicsBody" value="bear"/>
<property name="Mass" value="600"/>
<!-- 80% от ванильного, указание 2026-09-18. У animalZombieBear своего SizeScale нет,
то есть ванильный размер - единица, отсюда 0.8. -->
<property name="SizeScale" value="0.8"/>
<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
@@ -173,7 +208,7 @@
<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"/>
<property name="HandItem" value="necroMeleeHandZombieBear"/>
<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. -->
@@ -189,7 +224,8 @@
<property name="PrefabCombined" value="true"/>
<property name="PhysicsBody" value="AWolf"/>
<property name="Mass" value="180"/>
<property name="SizeScale" value="1.4"/>
<!-- 1.12 = 80% от ванильных 1.4 у animalDireWolf, указание 2026-09-18. -->
<property name="SizeScale" value="1.12"/>
<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. -->
@@ -202,7 +238,7 @@
<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"/>
<property name="HandItem" value="necroMeleeHandZombieWolf"/>
<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. -->
@@ -211,56 +247,133 @@
</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).
<!-- "Зомбогриф" ОТКАЧЕН 2026-09-18 К ЛЕТАЮЩЕЙ ВЕТКЕ.
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. -->
История у него длинная, и важно не потерять её конец. До 29.08 он расширял
animalZombieVulture и просто бестолково летал ("летает где-то в небе, и зомби его вообще
не интересуют") - тогда его перевели на necroZombieDog, то есть сделали наземным со
сменённой моделью. 18.09 пользователь заметил результат прямо: "зомбогриф у тебя это серая
пума". Попробовали натянуть на тот же наземный класс префаб стервятника - вышло хуже
всего: "выглядит жутко, прозрачное только туловище и голова, по земле он ползёт на
крыльях". Это и был ответ на висевший с 29.08 вопрос, можно ли подменить ОДИН префаб:
нельзя, птичья модель на аниматоре наземного квадрупа не работает.
Указание: "зомбогрифа откати, пусть ведёт себя как раньше и не делай его прозрачным.
Пусть пока бестолково летает как раньше, но с возможностью нацелить его на противника...
Потом сделаем грифа как надо. Но потом."
Поэтому здесь снова ванильная ЛЕТАЮЩАЯ ветка со всем, что к ней прилагается, и это
сознательный шаг назад до отдельной работы над ним (замысел разведдрона - BACKLOG.md).
ЧТО ПРИШЛОСЬ СНЯТЬ И ПОЧЕМУ:
- EntityFlags и EntityType уходят с "animal,zombie"/"Zombie" на чистое "animal"/"Animal".
Иначе EntityAlive.DamageEntity не даст ему бить зомби вовсе: там жёсткое правило, что
две сущности с флагом Zombie не могут повредить друг другу. Тот же приём и по той же
причине применён к Рою жуков выше.
- Tags теряют zombie/hostile, IsEnemyEntity=false, Faction=none - форма "свой игроку",
как у всех остальных питомцев.
- AITask/AITarget НЕ ЗАДАЮТСЯ ВОВСЕ, и это не упущение: Class="EntityVulture" не
использует систему AITask ни в каком виде, у него собственный захардкоженный поиск
целей на C# (см. HarmonySrc/SwarmTargetPatch.cs, там это разобрано целиком). Писать
сюда задачи - ровно та ошибка, которую уже совершали 28.08 с Роем.
ЧТОБЫ НЕ АТАКОВАЛ ХОЗЯИНА, он добавлен в SwarmTargetPatch.SpeciesByName: префикс на
EntityAlive.SetAttackTarget подменяет игрока ближайшим зомби. Это единственная точка, через
которую проходят все цели EntityVulture.
ПРИКАЗ АТАКОВАТЬ у него остаётся (CommandsAttack в SummonPatch.cs), но с оговоркой:
EntityVulture перебирает цели сам, своим циклом, и может сбросить наш приказ. Задачу
следования ему НЕ вешаем - она наземная и с полётом только подерётся. -->
<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"/>
<entity_class name="necroZombieGriffin" extends="animalZombieVulture">
<property name="EntityFlags" value="animal"/>
<property name="EntityType" value="Animal"/>
<property name="Tags" value="entity,animal,vulture"/>
<property name="IsEnemyEntity" value="false"/>
<property name="Faction" value="none"/>
<property name="HandItem" value="necroMeleeHandZombieGriffin"/>
<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. -->
<!-- У ванильного зомбостервятника 30 HP - для питомца, которого не отозвать до
следующего призыва, это ничто. 750 оставлено с прежней, наземной версии. -->
<passive_effect name="HealthMax" operation="base_set" value="750"/>
</effect_group>
</entity_class>
</append>
<!-- "Дух крысы" (Rat Spirit) - питомец начального уровня, указание 2026-09-18. Первый
питомец, у которого поведение задано НАМЕРЕННО, а не унаследовано: на нём и отлаживается
ИИ питомцев вообще (см. разбор в BACKLOG.md от 2026-09-18).
МОДЕЛЬ. Extends necroZombieWolf, то есть тот же самый ванильный префаб лютоволка, только
SizeScale с 1.4 на 0.28 - "уменьшить в 5 раз". Хитбокс отдельно задавать НЕ НАДО и брать
кроличий НЕЛЬЗЯ: коллайдеры в physicsbodies.xml привязаны к костям по имени пути, а риг
кролика с волчьим не совпадает (у кролика корпус Hips/LowerBack/Spine1 и голова под
Spine2/Neck/Head, у волка Hips/LowerBack/Spine/Spine1/Spine2 и голова под Neck/Neck1/Head)
- кроличье тело нашло бы на волчьей модели только таз и ноги, а корпус и голова остались
бы вообще без хитбокса. Унаследованный PhysicsBody="AWolf" при этом ужимается сам:
Entity.SetScale ставит ModelTransform.localScale, а костяные коллайдеры лежат внутри него.
ЗВУКИ - кроличьи ("он тоже грызун"). На АТАКУ поставлен rabbitpain: это буквально вопль
кролика от боли, и он же остаётся на SoundHurt - крыса вопит и когда кусает, и когда бьют
её. Остальные звуки ГЛУШАТСЯ ПУСТЫМИ НАМЕРЕННО: у animalRabbit нет ни roam, ни alert, ни
sense, ни giveup, и если их не погасить, крыса унаследует волчьи от necroZombieWolf -
ровно те грабли, что дали "зомбомедведь лает" (см. necroZombieBear выше). Дух рыщет молча.
ИИ. Здесь снято ВСЁ, что питомцы до сих пор тащили от враждебного зомбопса:
- нет BreakBlock и BlockingTargetTask - крыса физически не может грызть блоки, и это же
продублировано в коде (EAINecroFollowOwner зовёт FindPath с canBreak:false). Упёршись
в препятствие, дух проходит СКВОЗЬ него - см. Entity.IsStuck в PetFollowTask.cs;
- нет Territorial, ApproachSpot и Wander - крыса не бродит сама, она ходит за хозяином
(задача следования добавляется в рантайме из SummonPatch.cs, отдельным классом, потому
что задачи "иди за сущностью" в игре нет вовсе - все 32 типа EAI* проверены);
- нет SetNearestCorpseAsTarget - она не бросает хозяина ради ближайшего трупа;
- AITarget-1 оставлен ТОЛЬКО как ответ на удар, и список классов без EntityPlayer:
крыса огрызается на зомби и враждебных зверей, но никогда на хозяина, даже если он
заденет её в свалке (прямое указание 2026-09-18).
Сама она цель не ВЫБИРАЕТ никогда: SetNearestEntityAsTarget здесь нет, а AITarget-2
пустой обрывает перебор (CopyPropertiesFromEntityClass идёт по индексам и встаёт на первом
пустом - тем же приёмом ваниль глушит задачи у animalInsectSwarm). Цель приходит снаружи:
по команде игрока из PetCommandPatch.cs.
class=EntityAlive в AITask-1 - это НЕ "бей всё живое" само по себе, а разрешение задаче
подхватить ту цель, которую ей дали (EAIApproachAndAttackTarget.CanExecute сверяет тип
цели с этим списком и без совпадения просто не запускается). Кого можно назначить целью,
решает PetCommandPatch.cs, и торговец там отсеян. -->
<append xpath="/entity_classes">
<entity_class name="necroRatSpirit" extends="necroZombieWolf">
<property name="SizeScale" value="0.28"/>
<property name="Mass" value="33"/>
<property name="Weight" value="11"/>
<property name="Tags" value="entity,animal,rat"/>
<property name="HandItem" value="necroMeleeHandRatSpirit"/>
<property name="SoundRandom" value=""/>
<property name="SoundAlert" value=""/>
<property name="SoundSense" value=""/>
<property name="SoundGiveUp" value=""/>
<property name="SoundHurt" value="rabbitpain"/>
<property name="SoundDeath" value="rabbitdeath"/>
<property name="SoundAttack" value="rabbitpain"/>
<property name="SoundStepType" value="animallightstep"/>
<!-- Жёлтая метка на карте и компасе - указание 2026-09-18. Класс метки наш,
Config/nav_objects.xml; ванильные animaltracking_* не подошли, они завязаны
на перк следопыта, а не на прямое назначение. -->
<property name="NavObject" value="necroRatSpiritPet"/>
<property name="AITask-1" value="ApproachAndAttackTarget" data="class=EntityAlive,0"/>
<property name="AITask-2" value="Look"/>
<property name="AITask-3" value=""/>
<property name="AITarget-1" value="SetAsTargetIfHurt" data="class=EntityZombie,EntityEnemyAnimal"/>
<property name="AITarget-2" value=""/>
<effect_group name="Base Effects">
<!-- Дух-разведчик, а не танк: меньше собаки (200). Урон у неё и так 5. -->
<passive_effect name="HealthMax" operation="base_set" value="120"/>
</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.
+309 -13
View File
@@ -518,7 +518,10 @@
<append xpath="/items">
<item name="bookSummonZombieDog">
<property name="Tags" value="T0,weapon,attPerception"/>
<property name="ItemTypeIcon" value="book"/>
<!-- ItemTypeIcon="book" УБРАН 2026-09-18 со ВСЕХ свитков и книг призыва разом,
прямое указание: "свитки призыва и книги призыва не должны содержать белую
иконку книги поверх. Они ПРЕДМЕТЫ, а не рецепты." Полный разбор виджета - у
bookSummonRatSpirit ниже и у Пространственного браслета (07.09). -->
<!-- ICON REPLACED 2026-08-29: swapped the user's earlier hand-drawn
schematicDogSummon.png for the new AI-generated SummonZombieDog.png, to match
the rest of the item set's unified style (same prompt/generator as the other 12
@@ -546,7 +549,22 @@
<property class="Action0">
<property name="Class" value="SpawnEntity"/>
<property name="AnimType" value="4"/>
<property name="AnimWait" value="0.3"/>
<!-- 0.05, а не 0.3 - выровнено со Свитком духа крысы 2026-09-18, после того как
в игре не призвались НИ ГРИФ, НИ ВОЛК, НИ МЕДВЕДЬ, ни пёс, а крыса
призвалась. В логе у всех четверых было только "action index=1" (отзыв) и ни
одного index=0: до Spawn основной клик не доходил вовсе.
ItemActionSpawnEntity.OnHoldingUpdate копит stateTime по 0.05 за тик и
сравнивает с animWait, а ExecuteAction(_bReleased: true) при отпускании
кнопки сбрасывает состояние в None. То есть 0.3 означало "держать кнопку
треть секунды", шесть тиков подряд, и обычный клик до спавна не доживал -
молча, без ошибки и без строки в логе.
Раньше это сходило с рук: призыв делают раз за сессию и кнопку держали. Но
теперь на этом же слоте сидит КОМАНДА АТАКОВАТЬ, которую отдают быстро и
часто, - и требовать под неё удержание нельзя. У крысы это было исправлено
сразу, у остальных - забыто; теперь у всех одинаково. -->
<property name="AnimWait" value="0.05"/>
<property name="SoundWarn" value="zombiedogalert"/>
<property name="SoundAttack" value="zombiedogattack"/>
<property name="Entity" value="necroZombieDog"/>
@@ -587,15 +605,99 @@
since it doesn't mean much against a zombie, kept only the slow that was asked
for. Gated to zombies only, same EntityTagCompare pattern as everywhere else in
this mod that adds a buff on hit. -->
<!-- УРОН ПОДНЯТ С ВАНИЛЬНЫХ 8 ДО 35, 2026-09-18. Указание: зомбоживотные - это "по
сути те же крысы, но размер модели оригинальный, урон нарастает от зомбособаки до
зомбоволка... они уже реально убивают зомби эффективно", и следом уточнение
"зомбособаке урон 35 и шанс расчленения, как и волку, как и медведю".
Лестница целиком: Дух крысы 5 -> Зомбогриф 20 -> Зомбопёс 35 ->
Зомбомедведь 45 -> Зомбоволк 60. Верхняя ступень - ванильный урон лютоволка,
ниже неё всё выстроено вручную; ванильные значения были 8 / 22 / 60 / 60, то
есть лестницы не было вовсе (медведь и волк совпадали).
ГРИФ НАМЕРЕННО САМЫЙ СЛАБЫЙ ИЗ ЗВЕРЕЙ, и это не просчёт: "для атаки он не особо
подходит", роль у него будет другая - см. BACKLOG.md 18.09, замысел сделать из
него аналог разведдрона (запустить в небо и смотреть его глазами).
РАСЧЛЕНЕНИЕ - у Пса, Медведя и Волка. Ни у Грифа, ни у Духа крысы его нет.
Величина 0.25 одна на всех троих: конкретной цифры названо не было, а разводить
её по видам незачем - они уже разведены уроном. -->
<property class="Action0">
<property name="DamageEntity" value="35"/>
</property>
<effect_group name="necroMeleeHandZombieDog" tiered="false">
<passive_effect name="ModSlots" operation="base_set" value="0"/>
<passive_effect name="DismemberChance" operation="base_add" value="0.25"/>
<triggered_effect trigger="onSelfAttackedOther" action="AddBuff" target="other" buff="buffInjurySlow">
<requirement name="EntityTagCompare" target="other" tags="zombie"/>
</triggered_effect>
<!-- Кровотечение добавлено 2026-09-18 всем призванным зомбоживотным. Метку духа
и ослабление (buffNecroRatMark) они НЕ ставят по прямому указанию - это
остаётся особенностью крысы, у которой своего урона почти нет. Замедление
buffInjurySlow выше - НЕ то же самое, что "ослабление": оно стоит здесь с
28.08 по отдельной просьбе ("укус применяет buffInjurySlow") и не трогалось.
Скажите, если его тоже убрать. -->
<triggered_effect trigger="onSelfAttackedOther" action="AddBuff" target="other" buff="buffNecroRatBleed"/>
</effect_group>
</item>
</append>
<!-- Оружие ближнего боя трёх остальных зомбоживотных, 2026-09-18. До сих пор они брали
ванильные предметы напрямую (meleeHandAnimalMountainLion / meleeHandAnimalZombieBear /
meleeHandAnimalDireWolf), и своих эффектов у них не было вовсе. Теперь нужны свои: и
чтобы выстроить лестницу урона, и чтобы повесить кровотечение.
effect_group НЕ наследуется через Extends (см. заметку в шапке файла), поэтому у каждого
он свой, а не добавленный к родительскому. Всё остальное - задержка, радиус, звуки удара -
наследуется от ванильного предмета как есть. -->
<append xpath="/items">
<item name="necroMeleeHandZombieGriffin">
<property name="Extends" value="meleeHandAnimalZombieVulture"/>
<property name="CreativeMode" value="None"/>
<!-- 20, а не 35: понижено 18.09 по прямому указанию. Гриф - будущий разведчик, а не
боец (см. комментарий к лестнице выше). Расчленения у него тоже нет. -->
<property class="Action0">
<property name="DamageEntity" value="20"/>
</property>
<effect_group name="necroMeleeHandZombieGriffin" tiered="false">
<passive_effect name="ModSlots" operation="base_set" value="0"/>
<triggered_effect trigger="onSelfAttackedOther" action="AddBuff" target="other" buff="buffNecroRatBleed"/>
</effect_group>
</item>
</append>
<append xpath="/items">
<item name="necroMeleeHandZombieBear">
<property name="Extends" value="meleeHandAnimalZombieBear"/>
<property name="CreativeMode" value="None"/>
<property class="Action0">
<property name="DamageEntity" value="45"/>
</property>
<effect_group name="necroMeleeHandZombieBear" tiered="false">
<passive_effect name="ModSlots" operation="base_set" value="0"/>
<passive_effect name="DismemberChance" operation="base_add" value="0.25"/>
<triggered_effect trigger="onSelfAttackedOther" action="AddBuff" target="other" buff="buffNecroRatBleed"/>
</effect_group>
</item>
</append>
<append xpath="/items">
<item name="necroMeleeHandZombieWolf">
<property name="Extends" value="meleeHandAnimalDireWolf"/>
<property name="CreativeMode" value="None"/>
<!-- 60 - ванильный урон лютоволка, верхняя ступень лестницы. Оставлен как есть. -->
<property class="Action0">
<property name="DamageEntity" value="60"/>
</property>
<effect_group name="necroMeleeHandZombieWolf" tiered="false">
<passive_effect name="ModSlots" operation="base_set" value="0"/>
<passive_effect name="DismemberChance" operation="base_add" value="0.25"/>
<triggered_effect trigger="onSelfAttackedOther" action="AddBuff" target="other" buff="buffNecroRatBleed"/>
</effect_group>
</item>
</append>
<!-- "Жуки Властелина" (Insect Swarm summon): user request 2026-08-28, added alongside the
Zombie Dog bugfix above since it reuses the exact same summon plumbing (SpawnEntity,
HarmonySrc/SummonPatch.cs ownership/limit).
@@ -656,7 +758,10 @@
<append xpath="/items">
<item name="bookSummonInsectSwarm">
<property name="Tags" value="T0,weapon,attPerception"/>
<property name="ItemTypeIcon" value="book"/>
<!-- ItemTypeIcon="book" УБРАН 2026-09-18 со ВСЕХ свитков и книг призыва разом,
прямое указание: "свитки призыва и книги призыва не должны содержать белую
иконку книги поверх. Они ПРЕДМЕТЫ, а не рецепты." Полный разбор виджета - у
bookSummonRatSpirit ниже и у Пространственного браслета (07.09). -->
<!-- ICON REPLACED 2026-08-29: same reasoning as bookSummonZombieDog above - swapped
the earlier hand-drawn schematicInsectoSummon.png for BeetlesOfTheLord.png. -->
<property name="CustomIcon" value="BeetlesOfTheLord"/>
@@ -681,7 +786,22 @@
<property class="Action0">
<property name="Class" value="SpawnEntity"/>
<property name="AnimType" value="4"/>
<property name="AnimWait" value="0.3"/>
<!-- 0.05, а не 0.3 - выровнено со Свитком духа крысы 2026-09-18, после того как
в игре не призвались НИ ГРИФ, НИ ВОЛК, НИ МЕДВЕДЬ, ни пёс, а крыса
призвалась. В логе у всех четверых было только "action index=1" (отзыв) и ни
одного index=0: до Spawn основной клик не доходил вовсе.
ItemActionSpawnEntity.OnHoldingUpdate копит stateTime по 0.05 за тик и
сравнивает с animWait, а ExecuteAction(_bReleased: true) при отпускании
кнопки сбрасывает состояние в None. То есть 0.3 означало "держать кнопку
треть секунды", шесть тиков подряд, и обычный клик до спавна не доживал -
молча, без ошибки и без строки в логе.
Раньше это сходило с рук: призыв делают раз за сессию и кнопку держали. Но
теперь на этом же слоте сидит КОМАНДА АТАКОВАТЬ, которую отдают быстро и
часто, - и требовать под неё удержание нельзя. У крысы это было исправлено
сразу, у остальных - забыто; теперь у всех одинаково. -->
<property name="AnimWait" value="0.05"/>
<property name="SoundWarn" value="swarmalert"/>
<property name="SoundAttack" value="swarmattack"/>
<property name="Entity" value="necroInsectSwarm"/>
@@ -703,7 +823,10 @@
<append xpath="/items">
<item name="bookSummonZombieGriffin">
<property name="Tags" value="T0,weapon,attPerception"/>
<property name="ItemTypeIcon" value="book"/>
<!-- ItemTypeIcon="book" УБРАН 2026-09-18 со ВСЕХ свитков и книг призыва разом,
прямое указание: "свитки призыва и книги призыва не должны содержать белую
иконку книги поверх. Они ПРЕДМЕТЫ, а не рецепты." Полный разбор виджета - у
bookSummonRatSpirit ниже и у Пространственного браслета (07.09). -->
<!-- ICON ADDED 2026-08-29: real generated art (SummonZombieGriffin.png). -->
<property name="CustomIcon" value="SummonZombieGriffin"/>
<property name="DescriptionKey" value="bookSummonZombieGriffinDesc"/>
@@ -721,9 +844,28 @@
<property class="Action0">
<property name="Class" value="SpawnEntity"/>
<property name="AnimType" value="4"/>
<property name="AnimWait" value="0.3"/>
<property name="SoundWarn" value="mlionalert"/>
<property name="SoundAttack" value="mlionattack"/>
<!-- 0.05, а не 0.3 - выровнено со Свитком духа крысы 2026-09-18, после того как
в игре не призвались НИ ГРИФ, НИ ВОЛК, НИ МЕДВЕДЬ, ни пёс, а крыса
призвалась. В логе у всех четверых было только "action index=1" (отзыв) и ни
одного index=0: до Spawn основной клик не доходил вовсе.
ItemActionSpawnEntity.OnHoldingUpdate копит stateTime по 0.05 за тик и
сравнивает с animWait, а ExecuteAction(_bReleased: true) при отпускании
кнопки сбрасывает состояние в None. То есть 0.3 означало "держать кнопку
треть секунды", шесть тиков подряд, и обычный клик до спавна не доживал -
молча, без ошибки и без строки в логе.
Раньше это сходило с рук: призыв делают раз за сессию и кнопку держали. Но
теперь на этом же слоте сидит КОМАНДА АТАКОВАТЬ, которую отдают быстро и
часто, - и требовать под неё удержание нельзя. У крысы это было исправлено
сразу, у остальных - забыто; теперь у всех одинаково. -->
<property name="AnimWait" value="0.05"/>
<!-- Звуки призыва сменены 2026-09-18: "при появлении слышен крик льва, а не
грифа". Остались с той поры, когда Гриф был горным львом; модель откатили на
стервятника, а свиток забыли - те же грабли, что "зомбомедведь лает", только
теперь на предмете, а не на сущности. -->
<property name="SoundWarn" value="vulturealert"/>
<property name="SoundAttack" value="vultureattack"/>
<property name="Entity" value="necroZombieGriffin"/>
<property name="EntityOffset" value="0, -1, 2.5"/>
</property>
@@ -741,7 +883,10 @@
<append xpath="/items">
<item name="bookSummonZombieBear">
<property name="Tags" value="T0,weapon,attPerception"/>
<property name="ItemTypeIcon" value="book"/>
<!-- ItemTypeIcon="book" УБРАН 2026-09-18 со ВСЕХ свитков и книг призыва разом,
прямое указание: "свитки призыва и книги призыва не должны содержать белую
иконку книги поверх. Они ПРЕДМЕТЫ, а не рецепты." Полный разбор виджета - у
bookSummonRatSpirit ниже и у Пространственного браслета (07.09). -->
<!-- ICON ADDED 2026-08-29: real generated art (SummonZombieBear.png). -->
<property name="CustomIcon" value="SummonZombieBear"/>
<property name="DescriptionKey" value="bookSummonZombieBearDesc"/>
@@ -759,7 +904,22 @@
<property class="Action0">
<property name="Class" value="SpawnEntity"/>
<property name="AnimType" value="4"/>
<property name="AnimWait" value="0.3"/>
<!-- 0.05, а не 0.3 - выровнено со Свитком духа крысы 2026-09-18, после того как
в игре не призвались НИ ГРИФ, НИ ВОЛК, НИ МЕДВЕДЬ, ни пёс, а крыса
призвалась. В логе у всех четверых было только "action index=1" (отзыв) и ни
одного index=0: до Spawn основной клик не доходил вовсе.
ItemActionSpawnEntity.OnHoldingUpdate копит stateTime по 0.05 за тик и
сравнивает с animWait, а ExecuteAction(_bReleased: true) при отпускании
кнопки сбрасывает состояние в None. То есть 0.3 означало "держать кнопку
треть секунды", шесть тиков подряд, и обычный клик до спавна не доживал -
молча, без ошибки и без строки в логе.
Раньше это сходило с рук: призыв делают раз за сессию и кнопку держали. Но
теперь на этом же слоте сидит КОМАНДА АТАКОВАТЬ, которую отдают быстро и
часто, - и требовать под неё удержание нельзя. У крысы это было исправлено
сразу, у остальных - забыто; теперь у всех одинаково. -->
<property name="AnimWait" value="0.05"/>
<property name="SoundWarn" value="bearalert"/>
<property name="SoundAttack" value="bearattack"/>
<property name="Entity" value="necroZombieBear"/>
@@ -779,7 +939,10 @@
<append xpath="/items">
<item name="bookSummonZombieWolf">
<property name="Tags" value="T0,weapon,attPerception"/>
<property name="ItemTypeIcon" value="book"/>
<!-- ItemTypeIcon="book" УБРАН 2026-09-18 со ВСЕХ свитков и книг призыва разом,
прямое указание: "свитки призыва и книги призыва не должны содержать белую
иконку книги поверх. Они ПРЕДМЕТЫ, а не рецепты." Полный разбор виджета - у
bookSummonRatSpirit ниже и у Пространственного браслета (07.09). -->
<!-- ICON ADDED 2026-08-29: real generated art (SummonZombieWolf.png). -->
<property name="CustomIcon" value="SummonZombieWolf"/>
<property name="DescriptionKey" value="bookSummonZombieWolfDesc"/>
@@ -797,7 +960,22 @@
<property class="Action0">
<property name="Class" value="SpawnEntity"/>
<property name="AnimType" value="4"/>
<property name="AnimWait" value="0.3"/>
<!-- 0.05, а не 0.3 - выровнено со Свитком духа крысы 2026-09-18, после того как
в игре не призвались НИ ГРИФ, НИ ВОЛК, НИ МЕДВЕДЬ, ни пёс, а крыса
призвалась. В логе у всех четверых было только "action index=1" (отзыв) и ни
одного index=0: до Spawn основной клик не доходил вовсе.
ItemActionSpawnEntity.OnHoldingUpdate копит stateTime по 0.05 за тик и
сравнивает с animWait, а ExecuteAction(_bReleased: true) при отпускании
кнопки сбрасывает состояние в None. То есть 0.3 означало "держать кнопку
треть секунды", шесть тиков подряд, и обычный клик до спавна не доживал -
молча, без ошибки и без строки в логе.
Раньше это сходило с рук: призыв делают раз за сессию и кнопку держали. Но
теперь на этом же слоте сидит КОМАНДА АТАКОВАТЬ, которую отдают быстро и
часто, - и требовать под неё удержание нельзя. У крысы это было исправлено
сразу, у остальных - забыто; теперь у всех одинаково. -->
<property name="AnimWait" value="0.05"/>
<property name="SoundWarn" value="wolfdirealert"/>
<property name="SoundAttack" value="wolfdireattack"/>
<property name="Entity" value="necroZombieWolf"/>
@@ -814,6 +992,121 @@
</item>
</append>
<!-- "Дух крысы" (Rat Spirit): питомец начального уровня, указание 2026-09-18.
УРОН 5 - прямо по указанию, и это почти ничего (у волчьего укуса, от которого предмет
наследуется, 60). Крыса не оружие: она вешает дебафы. Их два, и оба БЕЗ гейта по тегу
"zombie", в отличие от укуса зомбопса выше - крысе разрешено атаковать любое существо,
значит и дебафы должны ложиться на то, что она укусила, а не только на зомби.
ЗАМЕДЛЕНИЕ СДЕЛАНО СВОЁ (buffNecroRatGrip, см. buffs.xml), а не взято из ванили: все
готовые "слоу" - это эффекты игроцкой системы травм, половина из них скрыта и написана
заодно под транспорт. Своё - это ровно RunSpeed/WalkSpeed/CrouchSpeed с затуханием, без
лишнего. Что эти величины вообще доходят до ИИ зомби, подтверждено декомпиляцией
EntityAlive.GetMoveSpeedAggro - разбор целиком в комментарии к самому баффу. -->
<append xpath="/items">
<item name="necroMeleeHandRatSpirit">
<property name="Extends" value="meleeHandAnimalDireWolf"/>
<property name="CreativeMode" value="None"/>
<property class="Action0">
<property name="DamageEntity" value="5"/>
</property>
<!-- effect_group НЕ наследуется через Extends (см. заметку в шапке файла) - триггеры
родителя пришлось бы переписывать целиком, и здесь они и не нужны: у крысы свои. -->
<effect_group name="necroMeleeHandRatSpirit" tiered="false">
<passive_effect name="ModSlots" operation="base_set" value="0"/>
<triggered_effect trigger="onSelfAttackedOther" action="AddBuff" target="other" buff="buffNecroRatGrip"/>
<triggered_effect trigger="onSelfAttackedOther" action="AddBuff" target="other" buff="buffNecroRatMark"/>
<triggered_effect trigger="onSelfAttackedOther" action="AddBuff" target="other" buff="buffNecroRatBleed"/>
</effect_group>
</item>
</append>
<!-- "Свиток духа крысы": тот же свиток, что у Зомбоволка, но серее - указание 2026-09-18
("выглядит как свиток призыва зомбоволка, но с более серым тинтом"). Иконка
переиспользуется целиком (CustomIcon="SummonZombieWolf"), своей рисовать не пришлось;
CustomIconTint обесцвечивает её, TintColor - 3D-модель в руке. Оба множатся на базовый
цвет, как у Камня духов.
ДВА ДЕЙСТВИЯ, И ОНИ НЕ ТЕ ЖЕ, ЧТО У ОСТАЛЬНЫХ ПИТОМЦЕВ. Action1 (силовая атака) - отзыв,
как везде. А вот Action0 у крысы ТРЁХзначный, и разбирает это HarmonySrc/PetCommandPatch.cs:
если крыса не призвана - призыв; если призвана - КОМАНДА АТАКОВАТЬ того, на кого смотрит
прицел; если под прицелом некого атаковать - "Нет цели для атаки" и больше ничего.
Class="SpawnEntity" в обоих слотах при этом обязателен: наш префикс висит именно на
ItemActionSpawnEntity.Spawn, и без него клик до нас не дойдёт.
Книга НЕ расходуется (ConsumesBook=false в SummonPatch.cs): иначе командовать атакой
означало бы тратить свиток на каждый приказ. -->
<append xpath="/items">
<item name="bookSummonRatSpirit">
<property name="Tags" value="T0,weapon,attPerception"/>
<!-- ItemTypeIcon="book" НЕ СТАВИТСЯ, хотя стоит у всех шести книг призыва выше.
Баг-репорт 2026-09-18: "у книги призыва есть ещё одна белая иконка книги поверх.
Такая обычно появляется на неизученных рецептах, но тут у нас не рецепт и не
книга для изучения. Это предмет."
Это тот же виджет, что уже разбирался 07.09 у Пространственного браслета (см.
его комментарий ниже, там полностью): значок 12x12 в ЛЕВОМ ВЕРХНЕМ углу поверх
иконки предмета, XUi_InGame/templates.xml, recipe_entry:
<sprite name="itemtypeicon" sprite="ui_game_symbol_{itemtypeicon}" .../>
Значение подставляется прямо в имя спрайта, поэтому "book" даёт белую книжку. У
ванили этот значок означает "куда/чем крафтится" (bundle, forge, campfire), и
единственный ванильный предмет с value="book" - это настоящая схема. Наш свиток
схемой не является: его не изучают, им пользуются. Значок вводил в заблуждение.
Убрано целиком, а не заменено: ItemClass.ItemTypeIcon по умолчанию "", виджет
закрыт гейтом {hasitemtypeicon}, и без свойства он просто не рисуется.
У ОСТАЛЬНЫХ ШЕСТИ КНИГ ПРИЗЫВА ЭТОТ ЖЕ ЗНАЧОК ПОКА ОСТАЛСЯ - трогать их без
указания не стал, но замечание к ним относится ровно так же. -->
<property name="CustomIcon" value="SummonZombieWolf"/>
<property name="CustomIconTint" value="8a9a8a"/>
<property name="DescriptionKey" value="bookSummonRatSpiritDesc"/>
<property name="DisplayType" value="ammoGrenadeFire"/>
<property name="HoldType" value="21"/>
<property name="Meshfile" value="@:Other/Items/Misc/bookPrefab.prefab"/>
<property name="Material" value="Mpaper"/>
<property name="TintColor" value="120, 135, 120"/>
<property name="Weight" value="0"/>
<property name="Stacknumber" value="10"/>
<property name="EconomicValue" value="0"/>
<property name="Group" value="Ammo/Weapons,Ammo"/>
<property name="SoundPickup" value="schematics_grab"/>
<property name="SoundPlace" value="schematics_place"/>
<property class="Action0">
<property name="Class" value="SpawnEntity"/>
<property name="AnimType" value="4"/>
<!-- 0.05, а НЕ 0.3, как у остальных призывных книг. Исправлено 2026-09-18 по первому же
игровому запуску: в логе шесть нажатий подряд и все с action index=1 (отзыв), ни
одного с index=0 - то есть до Spawn основной клик не доходил вовсе.
Причина в ItemActionSpawnEntity.OnHoldingUpdate: stateTime растёт по 0.05 за тик и
сравнивается с animWait, а ExecuteAction(_bReleased: true) при отпускании кнопки
сбрасывает состояние обратно в None. Значит 0.3 - это "держать кнопку треть
секунды", шесть тиков; обычный клик короче и не доходит до спавна, причём МОЛЧА -
ни ошибки, ни строки в логе.
Для остальных питомцев удержание терпимо (призыв делают раз в сессию), а у крысы
на этом же слоте сидит КОМАНДА АТАКОВАТЬ - приказ посреди боя, который отдают
быстро и часто. Требовать под него удержание нельзя. -->
<property name="AnimWait" value="0.05"/>
<property name="SoundWarn" value="rabbitpain"/>
<property name="Entity" value="necroRatSpirit"/>
<property name="EntityOffset" value="0, -1, 2.5"/>
</property>
<property class="Action1">
<property name="Class" value="SpawnEntity"/>
<property name="AnimType" value="4"/>
<property name="AnimWait" value="0.1"/>
<property name="SoundWarn" value="swoosh"/>
<property name="Entity" value="necroRatSpirit"/>
<property name="EntityOffset" value="0, -1, 2.5"/>
</property>
</item>
</append>
<!-- "Книга банши" (Banshee's Book): BACKLOG.md item 7 (dictated 2026-08-28, implemented
2026-08-29 "без вопросов" per user request). Consumed on use, plays a screamer's own
alert sound, spawns a small HOSTILE horde near the caster - see
@@ -836,7 +1129,10 @@
<append xpath="/items">
<item name="bookBanshee">
<property name="Tags" value="T0,weapon,attPerception"/>
<property name="ItemTypeIcon" value="book"/>
<!-- ItemTypeIcon="book" УБРАН 2026-09-18 со ВСЕХ свитков и книг призыва разом,
прямое указание: "свитки призыва и книги призыва не должны содержать белую
иконку книги поверх. Они ПРЕДМЕТЫ, а не рецепты." Полный разбор виджета - у
bookSummonRatSpirit ниже и у Пространственного браслета (07.09). -->
<!-- ICON UPDATED 2026-08-29: real generated art (BansheesScroll.png), no tint. -->
<property name="CustomIcon" value="BansheesScroll"/>
<property name="DescriptionKey" value="bookBansheeDesc"/>
+87
View File
@@ -0,0 +1,87 @@
<config>
<!-- Метки на карте и компасе для Духа крысы, 2026-09-18.
Две штуки, и это РАЗНЫЕ вещи: одна помечает саму крысу (чтобы игрок видел, где его
питомец), вторая - зомби, которого крыса укусила (метка духа).
Форма скопирована с ванильного nav_object_class "zombie" (Data/Config/nav_objects.xml):
это самый простой из существующих классов - у него НЕТ requirement_type, то есть метка
показывается безусловно, как только её кому-нибудь повесили. Классы с requirement_type
(IsPlayer, IsAlly, IsOwner, Tracking и ещё семь) сами решают, кому показываться, и нам
это только мешало бы: и питомец, и его добыча должны быть видны владельцу всегда.
Ванильные animaltracking_* сюда не годятся: они работают через систему отслеживания
животных (перк следопыта), а не через прямое назначение. -->
<append xpath="/nav_object_classes">
<!-- Сам питомец - ЖЁЛТЫМ, по прямому указанию.
СПРАЙТ: "воскрешение" (пробуем, указание 2026-09-18 - "для крысы попробуй иконку
воскрешения, проверим как смотрится"). Кроличий, стоявший тут сначала, не подошёл по
смыслу. Призрака, духа или души среди иконок игры НЕТ ВООБЩЕ - просмотрены все 359
имён вида ui_game_symbol_* из Data/Config и из самой сборки; ближайшее по названию
было specters_grace (бафф "Призрачная грация", перк Ловкости), но у воскрешения
смысл к некромантии ближе. Если в игре окажется невнятно, запасные - specters_grace
и skull (череп, он же значок самой Некромантии), меняется одна строка. -->
<nav_object_class name="necroRatSpiritPet">
<map_settings>
<property name="sprite_name" value="ui_game_symbol_twitch_resurrect"/>
<property name="min_distance" value="0"/>
<property name="max_distance" value="-1"/>
<property name="color" value="255,220,0,255"/>
<property name="has_pulse" value="false"/>
</map_settings>
<compass_settings>
<property name="sprite_name" value="ui_game_symbol_twitch_resurrect"/>
<property name="min_distance" value="0"/>
<property name="max_distance" value="1024"/>
<property name="max_scale_distance" value="32"/>
<property name="color" value="255,220,0,255"/>
<property name="has_pulse" value="false"/>
<property name="icon_clamped" value="false"/>
</compass_settings>
</nav_object_class>
</append>
<append xpath="/nav_object_classes">
<!-- Помеченная добыча. Спрайт зомбиный, как у ванильной метки, но цвет НЕ красный:
красным в игре уже отмечены зомби из Тёмного чутья (буфф necroModKnifeDarkSense
вешает ванильный nav_object "zombie"), и если бы метка крысы была такой же, две
разные механики стали бы неотличимы на карте.
ЦВЕТ ИСПРАВЛЕН 2026-09-18. Сначала здесь стоял зелёный (150,220,70) - ошибка, и
пользователь поймал её сразу: "игрок может решить, что цель дружелюбна". Зелёный в
интерфейсах значит "свой", а это помеченный ВРАГ.
Взят гнилостно-коричневый, а не ядовито-зелёный (предложены были оба). Причина
простая: сама крыса на карте жёлтая (255,220,0), а ядовито-зелёный - это
жёлто-зелёный, и на значке размером в десяток пикселей эти два цвета слились бы.
Коричневый не спорит ни с жёлтой крысой, ни с красными зомби Тёмного чутья и
читается как "гниёт" - ровно то, что метка и делает (см. GeneralDamageResist в
buffs.xml). Если в игре окажется тускло, ядовито-зелёный - это value="154,185,20",
одна строка.
has_pulse=true намеренно: метка живёт полминуты и должна быть заметна среди прочего,
это её единственная работа, а коричневый на карте сам по себе неяркий. -->
<nav_object_class name="necroRatSpiritMark">
<map_settings>
<property name="sprite_name" value="ui_game_symbol_zombie"/>
<property name="min_distance" value="0"/>
<property name="max_distance" value="-1"/>
<property name="color" value="165,100,35,255"/>
<property name="has_pulse" value="true"/>
</map_settings>
<compass_settings>
<property name="sprite_name" value="ui_game_symbol_zombie"/>
<property name="min_distance" value="0"/>
<property name="max_distance" value="1024"/>
<property name="max_scale_distance" value="32"/>
<property name="color" value="165,100,35,255"/>
<property name="has_pulse" value="true"/>
<property name="icon_clamped" value="false"/>
</compass_settings>
</nav_object_class>
</append>
</config>
+51 -9
View File
@@ -179,7 +179,7 @@
ур. 15 (300 убийств) - чутьё
Тег necroNecromancyLvl30 вместе с этим удалён - им больше никто не
пользуется, вода сидит на necroNecromancyLvl20. -->
<unlock_entry item="necroModKnifeScavengersFeast" unlock_tier="3"/>
<unlock_entry item="necroModKnifeScavengersFeast,bookSummonRatSpirit" unlock_tier="3"/>
<unlock_entry item="necroModKnifeGravesRepose" unlock_tier="4"/>
<unlock_entry item="necroModKnifeDarkSense" unlock_tier="5"/>
</display_entry>
@@ -210,12 +210,43 @@
привязки unlock_icon_atlasN / unlock_icon_lockedN). Именно поэтому у мода теперь
есть вторая папка UIAtlases/ItemIconAtlasGreyscale - без неё под замком у иконки
не было бы картинки вообще. -->
<display_entry icon="ScrollOfDeviation" name_key="craftingNecroNecromancyTier2Name" has_quality="false" unlock_level="25,70,85">
<unlock_entry item="thrownBookGrimoireDeviation" unlock_tier="1"/>
<unlock_entry item="necroModKnifeDeadMansGrip" unlock_tier="2"/>
<unlock_entry item="necroModKnifeDeadStorm" unlock_tier="3"/>
<!-- ПИТОМЦЫ РАЗНЕСЕНЫ ПО ШКАЛЕ 2026-09-18, прямое указание: "давай разнесём получение
питомцев по шкале скилла. Крыса ок, она доступна почти сразу. Собака пусть
открывается на Подмастерье. Медведь на Ученик. Лютоволк - на 4000 зомби
(200 уровень некромантии)."
До этого трое из четверых лежали в одной куче: Пёс, Жуки и Гриф на Ученике (100),
Медведь, Волк и Банши на Некроманте (150). Стало так:
Дух крысы ур. 3 (60 убийств) - тег necroNecromancyLvl60, Адепт
Зомбопёс ур. 25 (500) - necroNecromancyJourneyman, Подмастерье
Зомбомедведь ур. 100 (2000) - necroNecromancyApprentice, Ученик
Зомбоволк ур. 200 (4000) - necroNecromancyLvl4000, Некромант
ГРИФ И ПЁС ПОМЕНЯНЫ МЕСТАМИ 2026-09-18, после игровой проверки: "пожалуй, стоит
его поменять местами с собакой по скиллу. Он проще собаки, и при этом сейчас
стал полезнее". Гриф встал на 500 убийств (ур. 25), Пёс на 1300 (ур. 65).
Странность, из-за которой это и поменяли: у Грифа урон 20, у Пса 35, но Гриф
летает, сам возвращается к хозяину и разведывает - на практике он оказался
полезнее. Жуки на Ученике и Свиток банши на Некроманте не трогались.
ПРАВИЛО, КОТОРОЕ ЛЕГКО НАРУШИТЬ (уже наступали, см. комментарий про браслет выше):
уровень в теге рецепта и unlock_level того display_entry, где предмет нарисован,
ОБЯЗАНЫ совпадать. display_entry рисует замок сам по себе, по unlock_tier, и про
теги не знает - разойдутся, и на панели будет замок на доступном рецепте. -->
<display_entry icon="ScrollOfDeviation" name_key="craftingNecroNecromancyTier2Name" has_quality="false" unlock_level="25,65,70,85">
<unlock_entry item="thrownBookGrimoireDeviation,bookSummonZombieGriffin" unlock_tier="1"/>
<!-- Зомбогриф, 18.09: "гриф пусть тоже будет на подмастерье, но 1300 (65 уровень
некромантии)". 65 вставлен в unlock_level МЕЖДУ 25 и 70, поэтому unlock_tier
у двух модов ножа ниже сдвинулись на единицу (были 2 и 3, стали 3 и 4).
Список обязан идти по возрастанию: unlock_tier - это номер позиции в нём. -->
<unlock_entry item="bookSummonZombieDog" unlock_tier="2"/>
<unlock_entry item="necroModKnifeDeadMansGrip" unlock_tier="3"/>
<unlock_entry item="necroModKnifeDeadStorm" unlock_tier="4"/>
</display_entry>
<display_entry icon="SummonZombieDog" name_key="craftingNecroNecromancyTier3Name" has_quality="false" unlock_level="100">
<!-- Иконка сменена с SummonZombieDog на SummonZombieBear 18.09: Пёс уехал на
Подмастерье, и группу теперь возглавляет Медведь. -->
<display_entry icon="SummonZombieBear" name_key="craftingNecroNecromancyTier3Name" has_quality="false" unlock_level="100">
<!-- resourceBloodStone добавлен 2026-09-16 (указание «доступен на третьем грейде
некромантии, в самом его начале»). Тир 3 - это и есть третий грейд, группа
«Ученик», а его единственный unlock_level равен 100 уровням (это те же 2000
@@ -228,10 +259,14 @@
себе, по unlock_tier. Поэтому 100 здесь и 100 там ОБЯЗАНЫ совпадать -
иначе замок на панели разойдётся с реальной доступностью рецепта. Совпадают:
один порог, одно значение. -->
<unlock_entry item="bookSummonZombieDog,bookSummonInsectSwarm,bookSummonZombieGriffin,resourceBloodStone" unlock_tier="1"/>
<unlock_entry item="bookSummonZombieBear,bookSummonInsectSwarm,resourceBloodStone" unlock_tier="1"/>
</display_entry>
<display_entry icon="SummonZombieBear" name_key="craftingNecroNecromancyTier4Name" has_quality="false" unlock_level="150">
<unlock_entry item="bookSummonZombieBear,bookSummonZombieWolf,bookBanshee" unlock_tier="1"/>
<!-- Две ступени вместо одной: у группы появился второй уровень (200), потому что
Лютоволк теперь открывается позже Свитка банши. Иконка сменена на волчью -
Медведь уехал на Ученика. -->
<display_entry icon="SummonZombieWolf" name_key="craftingNecroNecromancyTier4Name" has_quality="false" unlock_level="150,200">
<unlock_entry item="bookBanshee" unlock_tier="1"/>
<unlock_entry item="bookSummonZombieWolf" unlock_tier="2"/>
</display_entry>
<display_entry icon="BlackPortalStone" name_key="craftingNecroNecromancyTier5Name" has_quality="false" unlock_level="250">
<unlock_entry item="thrownStonePortalBlack" unlock_tier="1"/>
@@ -257,10 +292,17 @@
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="5,250" value="1" tags="necroNecromancyLvl100"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="15,250" value="1" tags="necroNecromancyLvl300"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="25,250" value="1" tags="necroNecromancyJourneyman"/>
<!-- Зомбогриф, 2026-09-18. 1300 убийств = 65-й уровень. -->
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="65,250" value="1" tags="necroNecromancyLvl1300"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="70,250" value="1" tags="necroNecromancyLvl1400"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="85,250" value="1" tags="necroNecromancyLvl1700"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="100,250" value="1" tags="necroNecromancyApprentice"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="150,250" value="1" tags="necroNecromancyNecromancer"/>
<!-- Лютоволк, 2026-09-18. Имя тега В УБИЙСТВАХ (4000), уровень - в уровнях (200):
шкала 20 убийств = 1 уровень, см. шапку файла. Это одноразовый порог внутри
группы "Некромант", а не новая группа - ровно та же схема, что у
necroNecromancyLvl20 и Lvl60 выше. -->
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="200,250" value="1" tags="necroNecromancyLvl4000"/>
<passive_effect name="RecipeTagUnlocked" operation="base_set" level="250,250" value="1" tags="necroNecromancyMaster"/>
</effect_group>
</crafting_skill>
+19 -4
View File
@@ -151,7 +151,7 @@
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">
<recipe name="bookSummonZombieDog" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyLvl1300">
<ingredient name="foodRottingFlesh" count="50"/>
<ingredient name="medicalBloodBag" count="3"/>
<ingredient name="drinkJarBoiledWater" count="4"/>
@@ -234,7 +234,7 @@
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">
<recipe name="bookSummonZombieGriffin" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyJourneyman">
<ingredient name="resourceFeather" count="30"/>
<ingredient name="resourceBone" count="20"/>
<ingredient name="casinoCoin" count="5"/>
@@ -243,7 +243,7 @@
</recipe>
</append>
<append xpath="/recipes">
<recipe name="bookSummonZombieBear" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyNecromancer">
<recipe name="bookSummonZombieBear" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyApprentice">
<ingredient name="resourceLeather" count="20"/>
<ingredient name="foodRawMeat" count="10"/>
<ingredient name="resourceBone" count="20"/>
@@ -252,7 +252,7 @@
</recipe>
</append>
<append xpath="/recipes">
<recipe name="bookSummonZombieWolf" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyNecromancer">
<recipe name="bookSummonZombieWolf" count="1" craft_area="workbench" tags="learnable,workbenchCrafting,necroNecromancyLvl4000">
<ingredient name="resourceLeather" count="15"/>
<ingredient name="foodRawMeat" count="10"/>
<ingredient name="resourceBone" count="15"/>
@@ -344,6 +344,21 @@
<ingredient name="foodRawMeat" count="5"/>
<ingredient name="resourceYuccaFibers" count="10"/>
</recipe>
<!-- "Свиток духа крысы" - питомец начального уровня, указание 2026-09-18. Состав задан
дословно. craft_area НЕТ намеренно: свиток Зомбоволка требует верстак, но это питомец
ТРЕТЬЕГО уровня, а верстак в первые дни игрок не находит - ровно эта претензия пришла с
Nexus (Derizor, 14.09, про Пространственный браслет). Порог ставится туда, где предмет
нужен, значит и крафт должен быть доступен там же - в личном верстаке.
Тег necroNecromancyLvl60 - это "60 убийств", и он стоит на уровне 3 (progression.xml). -->
<recipe name="bookSummonRatSpirit" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl60">
<ingredient name="resourceCoal" count="5"/>
<ingredient name="resourceScrapIron" count="2"/>
<ingredient name="resourceVictimSkin" count="1"/>
<ingredient name="resourceZombieAsh" count="2"/>
<ingredient name="resourceCropCottonPlant" count="10"/>
<ingredient name="resourceBone" count="3"/>
<ingredient name="resourceWood" count="1"/>
</recipe>
<recipe name="necroModKnifeDeadMansGrip" count="1" tags="learnable,packMuleCrafting,necroNecromancyLvl1400">
<ingredient name="resourceZombieAsh" count="10"/>
<ingredient name="resourceBone" count="15"/>
+77 -7
View File
@@ -266,6 +266,61 @@ namespace NecromancerTome
timer = 0f;
}
/// <summary>Кого этот проход обращает в призрака.
///
/// РАСШИРЕНО 2026-09-18 С ТОРГОВЦЕВ НА ПИТОМЦЕВ, по указанию: "всех призванных существ
/// (зомбопёс, зомбомедведь, зомбоволк, зомбогриф) тоже делаем полупрозрачными как сейчас
/// торговцев". Плюс Дух крысы, который призраком и задуман.
///
/// Обобщение вышло механическим, и это не совпадение: ApplyGreyscale ниже никогда не
/// пользовался ничем трейдерским - только GetComponentsInChildren&lt;Renderer&gt;(),
/// EntityClass.entityClassName и entityId, а всё это есть у любой EntityAlive.
///
/// СПИСОК БЕРЁТСЯ ИЗ SummonPatch.LimitedPets, а не дублируется здесь: новый питомец
/// становится призраком автоматически, в одном месте, и разойтись эти два списка не могут.
/// Рой жуков в нём тоже есть, но он безвреден - его рендереры это системы частиц, а
/// ApplyGreyscale их намеренно пропускает (см. Convert), так что для него проход вхолостую.
///
/// Проверка по имени класса, а не по типу: все наземные питомцы мода - это C#-класс
/// EntityZombieDog (см. entityclasses.xml), то есть по типу их не отличить ни друг от
/// друга, ни от настоящего зомбопса из мира.</summary>
/// <summary>Исключения из призрачного вида. Зомбопёс - по прямому указанию 2026-09-18:
/// "зомбособаку не делай полупрозрачной или серой, пусть будет обычной".
///
/// Проверка идёт по ИМЕНИ КЛАССА, а не по типу, и это здесь важно: Дух крысы наследуется
/// от necroZombieWolf, а тот от necroZombieDog, то есть по типу они все EntityZombieDog и
/// исключение задело бы заодно и крысу, которая призраком как раз и задумана.</summary>
public static readonly HashSet<string> NotGhosted = new HashSet<string>
{
"necroZombieDog",
// Медведь и Волк добавлены 2026-09-18: "прозрачность им не нужна". Призрачными
// остаются Дух крысы, Зомбогриф и торговцы.
"necroZombieBear",
"necroZombieWolf",
// Гриф выведен 18.09 вместе с откатом: "не делай его прозрачным". На птичьей модели
// обесцвечивание к тому же брало не всё - прозрачными выходили только туловище и
// голова, что и выглядело жутко.
"necroZombieGriffin",
};
public static bool ShouldBeGhost(EntityAlive _entity)
{
if (_entity is EntityTrader)
{
return true;
}
EntityClass entityClass = EntityClass.list[_entity.entityClass];
if (entityClass == null || entityClass.entityClassName == null)
{
return false;
}
if (NotGhosted.Contains(entityClass.entityClassName))
{
return false;
}
return Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.ContainsKey(entityClass.entityClassName);
}
public static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
{
timer += Time.deltaTime;
@@ -284,11 +339,11 @@ namespace NecromancerTome
for (int i = 0; i < world.EntityAlives.Count; i++)
{
EntityAlive entity = world.EntityAlives[i];
if (!(entity is EntityTrader trader) || trader.IsDead())
if (entity == null || entity.IsDead() || !ShouldBeGhost(entity))
{
continue;
}
if (Ghosted.TryGetValue(trader.entityId, out GhostBody body))
if (Ghosted.TryGetValue(entity.entityId, out GhostBody body))
{
if (IsIntact(body))
{
@@ -297,14 +352,14 @@ namespace NecromancerTome
// His model was destroyed and rebuilt under him. Drop what is known about the
// old one before building the new, or Converted and TintedMaterials keep
// entries for renderers and materials that no longer exist.
Debug.Log("[NecromancerTome] GhostTraderPatch: entity " + trader.entityId +
Debug.Log("[NecromancerTome] GhostTraderPatch: entity " + entity.entityId +
" came back with a new model - ghosting him again");
Ghosted.Remove(trader.entityId);
Ghosted.Remove(entity.entityId);
Prune();
}
if (ApplyGreyscale(trader, out GhostBody fresh))
if (ApplyGreyscale(entity, out GhostBody fresh))
{
Ghosted[trader.entityId] = fresh;
Ghosted[entity.entityId] = fresh;
}
}
}
@@ -363,7 +418,7 @@ namespace NecromancerTome
/// leaves this trader unmarked and tries again on the next sweep. On true, _body carries
/// the renderers taken over, which is how the next sweep tells this trader from a rebuilt
/// one standing under the same entity id.</summary>
public static bool ApplyGreyscale(EntityTrader _trader, out GhostBody _body)
public static bool ApplyGreyscale(EntityAlive _trader, out GhostBody _body)
{
_body = default(GhostBody);
Renderer[] renderers = _trader.GetComponentsInChildren<Renderer>(true);
@@ -797,6 +852,21 @@ namespace NecromancerTome
break;
}
// НАЙДЕНО 2026-09-18, НО НЕ ИСПРАВЛЕНО - записано, чтобы не искать заново.
// Пользователь: "собака не полупрозрачная". По логу у неё для этого всё было -
// шейдер Standard с _Color, _SrcBlend, _DstBlend, "5 of 5 renderer(s) desaturated,
// 5 opacity lever(s) installed". То есть альфа проставлена, а прозрачности нет.
//
// Разница с трейдерами, у которых она работает: их тело идёт по ДРУГОЙ ветке -
// MakeBlendMaterial создаёт материал заново и ставит там _Mode = 2 (Fade). Здесь,
// на ветке "блендим на месте", _Mode и _ZWrite не трогаются вовсе, а материал,
// собранный как Opaque, приходит с _Mode = 0 и _ZWrite = 1. Юнити для перевода
// Standard в прозрачность требует всю шестёрку: _Mode, _SrcBlend, _DstBlend,
// _ZWrite, ключевые слова и очередь отрисовки - у нас выставлены четыре из шести.
//
// Собаку это чинить больше не нужно (она выведена из призраков совсем), но Медведь,
// Волк и Гриф идут по этой же ветке. Если и они окажутся непрозрачными - причина
// здесь, и лечится добавлением _Mode = 2 и _ZWrite = 0 рядом со строками ниже.
if (_material.HasProperty("_SrcBlend") && _material.HasProperty("_DstBlend"))
{
_material.SetFloat("_SrcBlend", (float)BlendMode.SrcAlpha);
+92
View File
@@ -0,0 +1,92 @@
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// КОМАНДА "АТАКУЙ ТО, НА ЧТО Я СМОТРЮ" - второй режим свитка Духа крысы, указание
/// 2026-09-18: "у свитка призыва есть два режима (призыв и отзыв). Если крыса уже призвана,
/// то призыв становится атакой. И крыса берёт целью атаки того, на кого указал прицел
/// игрока. Если в прицеле нету целей для атаки, то пишется надпись "Нет цели для атаки"".
///
/// Это не Harmony-заплатка, а helper: точка входа одна и та же для всех питомцев -
/// префикс на ItemActionSpawnEntity.Spawn в SummonPatch.cs. Второй патч на тот же метод
/// только запутал бы порядок исполнения.
///
/// ОТКУДА БЕРЁТСЯ ЦЕЛЬ ПОД ПРИЦЕЛОМ. EntityPlayerLocal.HitInfo - публичное поле типа
/// WorldRayHitInfo, которое PlayerMoveController перезаписывает КАЖДЫЙ КАДР результатом луча
/// из прицела (Voxel.Raycast по GetLookRay, длина Utils.FastMax(cDigAndBuildDistance,
/// cCollectItemDistance, 30f), то есть не меньше 30 метров). Ничего своего лучить не надо -
/// игра уже посчитала это для интерфейса. Из попадания сущность достаётся собственным
/// публичным методом игры ItemActionAttack.GetEntityFromHit(hitInfo), который внутри зовёт
/// GameUtils.GetHitRootEntity(tag, transform) - то же самое, чем пользуется обычный удар
/// оружием, а значит и попадание по конечности/голове разрешится в саму сущность.
///
/// Луч упирается в блоки - и это правильно: натравить крысу сквозь стену на то, чего игрок
/// не видит, нельзя.
///
/// КОГО НЕЛЬЗЯ НАЗНАЧИТЬ ЦЕЛЬЮ (указание "пусть атакует всех существ кроме NPC торговцев"):
/// - EntityTrader - прямо по указанию;
/// - EntityPlayer - сам хозяин и вообще игроки: крыса не оружие против людей;
/// - собственный хозяин и она сама - на всякий случай, отдельной проверкой;
/// - мёртвые.
/// Отсев сделан ЗДЕСЬ, а не в entityclasses.xml, потому что targetClasses у
/// EAIApproachAndAttackTarget умеет только РАЗРЕШАТЬ тип, запретить им нельзя. В XML у крысы
/// стоит class=EntityAlive - это разрешение задаче подхватить любую цель, которую ей дали;
/// решение "кого дать" целиком принимается тут.
/// </summary>
public static class PetAttackCommand
{
/// <summary>Сколько тиков держится приказ. attackTargetTime уменьшается на 1 за тик ИИ и
/// на нуле сбрасывает цель (EntityAlive: "if (attackTargetTime > 0) { attackTargetTime--;
/// ... }"), так что это по сути "пока не убьёт". 6000 - заведомо больше любой драки;
/// смысл не в таймере, а в том, чтобы приказ не висел вечно, если цель уйдёт из мира.</summary>
public const int OrderTicks = 6000;
/// <summary>Отдаёт питомцу приказ атаковать то, на что смотрит игрок. false - приказа не
/// вышло, и вызывающая сторона показывает "Нет цели для атаки".</summary>
public static bool TryOrderAttack(EntityAlive _owner, EntityAlive _pet)
{
if (_owner == null || _pet == null || _pet.IsDead())
{
return false;
}
EntityPlayerLocal local = _owner as EntityPlayerLocal;
if (local == null)
{
// Не локальный игрок - HitInfo просто неоткуда взять (у мода нет сетевого слоя
// нигде, см. SummonPatch.cs). Молча отказываем, как будто цели нет.
Debug.Log("[NecromancerTome] PetAttackCommand: owner is not the local player, no crosshair to read");
return false;
}
WorldRayHitInfo hit = local.HitInfo;
if (hit == null || !hit.bHitValid || hit.transform == null)
{
return false;
}
Entity hitEntity = ItemActionAttack.GetEntityFromHit(hit);
EntityAlive target = hitEntity as EntityAlive;
if (target == null || target.IsDead())
{
return false;
}
if (target == _pet || target == _owner)
{
return false;
}
if (target is EntityPlayer || target is EntityTrader)
{
return false;
}
// Месть сбрасывается вместе с приказом: иначе задача SetAsTargetIfHurt могла бы на
// следующем тике перебить наш приказ тем, кто ударил крысу последним.
_pet.SetRevengeTarget(null);
_pet.SetAttackTarget(target, OrderTicks);
Debug.Log("[NecromancerTome] PetAttackCommand: pet " + _pet.entityId + " ordered to attack " +
target.entityId + " (" + target.EntityClass.entityClassName + ")");
return true;
}
}
}
+235
View File
@@ -0,0 +1,235 @@
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// СВЕТЯЩИЕСЯ ГЛАЗА у призванных зомбоживотных - указание 2026-09-18: "псу и волку сделай,
/// глаза светлофиолетовые светящиеся".
///
/// ПОЧЕМУ ТОЛЬКО ДВОЕ, И ЭТО НЕ ВЫБОР, А ОГРАНИЧЕНИЕ МОДЕЛЕЙ. Покрасить глаза отдельно можно
/// ровно тогда, когда они лежат ОТДЕЛЬНЫМ материалом. Проба шейдеров, которую печатает
/// GhostTraderPatch, показала по питомцам такую картину:
///
/// necroZombieDog - 5 рендереров: FeralEye, NormalEye, LOD0..LOD2 - ГОДИТСЯ
/// necroZombieWolf - 2 рендерера: DireWolf + DireWolfHair - НЕ ГОДИТСЯ
/// necroZombieBear - 1 рендерер - НЕ ГОДИТСЯ
/// necroZombieGriffin - 1 рендерер - НЕ ГОДИТСЯ
///
/// ПО ВОЛКУ ОТВЕТ ОКОНЧАТЕЛЬНЫЙ И ОТРИЦАТЕЛЬНЫЙ, по дампу 18.09:
///
/// DireWolf[0] shader='Game/Animal/Fur' texture=dire_wolf_df _Color=False
/// DireWolfHair[0] shader='Game/SDCS/Hair' texture=Afro_normal _Color=False
///
/// Глазного рендерера у него НЕТ ВООБЩЕ - только шкура и шерсть, глаза запечены прямо в
/// dire_wolf_df. Ни отдельного материала, ни даже _Color, чтобы хоть что-то подкрасить.
/// Единственный оставшийся путь - править саму текстуру по пикселям, зная координаты глаз на
/// развёртке. Поэтому LitEyes у Волка снят: пусть флаг не обещает того, чего нет.
///
/// У Медведя и Грифа материал один на всю модель, и отдельно глаза там не достать ничем,
/// кроме правки самой текстуры по пикселям. Поэтому их в списке нет - см. PetInfo.LitEyes.
///
/// КРАСИТСЯ СВЕЧЕНИЕ, А НЕ ЦВЕТ. У зомби глаза светятся сами (_EmissionMap у этого материала
/// уже назначена), и именно свечение читается как "горящие глаза". Поэтому ставится
/// _EmissionColor, а _Color правится следом только чтобы радужка не спорила с ним при ярком
/// дневном свете. Ключевое слово _EMISSION включается явно: материал, собранный без эмиссии,
/// игнорирует _EmissionColor, пока оно выключено, - ровно тот же класс ловушки, что с
/// _SrcBlend/_Mode у прозрачности (см. ApplyTransparency в GhostTraderPatch.cs).
///
/// ПОЧЕМУ ЭТО ТИК, А НЕ ХУК НА ПРИЗЫВ. Материалы появляются вместе с моделью, а она
/// собирается ПОЗЖЕ создания сущности - это уже дважды стоило нам ошибок: сначала
/// GhostTraderPatch пришлось перевести на опрос из-за трейдеров, потом
/// TryIgnoreCollisionWithOwner из-за того, что коллайдеров в момент CreateEntity ещё нет.
/// Здесь та же природа, поэтому сразу попытка с повтором: раз в секунду, до первого успеха,
/// из общего тика в PetFollowPatch.
/// </summary>
public static class PetEyeGlow
{
/// <summary>Фиолетовый, НАСЫЩЕННЫЙ. Первая версия была светлой (0.72, 0.55, 1.0) и в игре
/// дала ровно то, о чём сказал пользователь: "у собаки глаза просто белые".
///
/// Причина арифметическая: _EmissionColor читается как HDR, цвет умножается на яркость.
/// При множителе 6 светлый фиолетовый превращался в (4.3, 3.3, 6.0) - все три канала
/// далеко за единицей, и тонемаппинг сводил их в белое пятно. Оттенок выживает только
/// когда каналы РАСХОДЯТСЯ: отсюда низкий зелёный и умеренная яркость ниже.</summary>
public static readonly Color EyeColour = new Color(0.55f, 0.18f, 1f);
/// <summary>ПОДНЯТО С 2.4 ДО 6 после первой проверки: "у собаки они вообще никак не
/// светятся". Цвет сменился (это видно), а свечения нет - значит либо _EmissionColor у
/// этого шейдера читается как HDR и 2.4 для него ничто, либо свечение вообще идёт не
/// отсюда. Шесть - это попытка проверить первое предположение, а чтобы проверить второе,
/// рядом добавлен разовый дамп всех материалов питомца (DumpMaterialsOnce).</summary>
/// <summary>1.6, а не 6: см. комментарий к EyeColour - на шести глаза выгорали в белый.
/// Итоговое свечение (0.88, 0.29, 1.6): синий вдвое выше красного и вшестеро выше
/// зелёного, так что фиолетовый читается, а не тонет в пересвете.</summary>
public const float EmissionIntensity = 1.6f;
/// <summary>Классы, по которым дамп уже печатался: он нужен один раз на вид, а не на
/// каждого призванного.</summary>
public static readonly System.Collections.Generic.HashSet<string> Dumped =
new System.Collections.Generic.HashSet<string>();
/// <summary>ДИАГНОСТИКА, добавлена 2026-09-18. Первая попытка зажечь глаза дала половину
/// результата: у Пса цвет сменился, но свечения нет, а у Волка не изменилось ничего -
/// значит материала с "eye" в имени текстуры у него просто не нашлось. Гадать дальше
/// дороже, чем один раз посмотреть: этот дамп печатает по каждому виду все рендереры, их
/// материалы, шейдеры, имена текстур и наличие свойств свечения. По нему и будет видно,
/// за что хвататься - и надо ли вообще.</summary>
public static void DumpMaterialsOnce(Entity _pet, string _className)
{
if (_className == null || !Dumped.Add(_className))
{
return;
}
Renderer[] renderers = _pet.GetComponentsInChildren<Renderer>(true);
Debug.Log("[NecromancerTome] PetEyeGlow: ДАМП материалов " + _className + " - " +
(renderers != null ? renderers.Length : 0) + " рендерер(ов)");
if (renderers == null)
{
return;
}
foreach (Renderer renderer in renderers)
{
if (renderer == null)
{
continue;
}
Material[] mats = renderer.sharedMaterials;
for (int i = 0; mats != null && i < mats.Length; i++)
{
Material m = mats[i];
if (m == null)
{
continue;
}
string albedo = GhostTraderPatch.FindAlbedoProperty(m);
Texture tex = albedo != null ? m.GetTexture(albedo) : null;
Debug.Log("[NecromancerTome] PetEyeGlow: " + renderer.name + "[" + i + "] shader='" +
(m.shader != null ? m.shader.name : "null") + "' albedo=" + (albedo ?? "-") +
" texture=" + (tex != null ? tex.name : "-") +
" _EmissionColor=" + m.HasProperty("_EmissionColor") +
" _EmissionMap=" + (m.HasProperty("_EmissionMap") && m.GetTexture("_EmissionMap") != null) +
" _EmissionIntensity=" + m.HasProperty("_EmissionIntensity") +
" _Color=" + m.HasProperty("_Color"));
}
}
}
/// <summary>ПЕРЕПИСАНО 2026-09-18 ПО ДАМПУ. Первая версия искала материал по имени
/// ТЕКСТУРЫ ("eye"), нашла у собаки ровно один и зажгла его - а глаза всё равно не
/// светились. Дамп показал, почему:
///
/// FeralEye[0] shader='Game/Autodesk' texture=feral_eye _EmissionMap=True
/// NormalEye[0] shader='Standard' texture=HD_ZombieDog _EmissionMap=False
/// LOD0..LOD2 shader='Standard' texture=HD_ZombieDog
///
/// Глаз у собаки ДВА РАЗНЫХ, отдельными рендерерами. Светящийся feral - тот, что горит у
/// разъярённой собаки, и в обычном состоянии он не показывается; именно его мы и зажгли,
/// не увидев ничего. А обычный глаз - NormalEye - берёт текстуру ТУШКИ (HD_ZombieDog),
/// поэтому поиск по имени текстуры его пропускал.
///
/// Поэтому теперь глаз опознаётся по имени РЕНДЕРЕРА, а имя текстуры оставлено как
/// запасной признак. Материал у NormalEye свой собственный, так что правка цвета и
/// свечения на нём затрагивает только глаз, хотя текстура и общая с телом.</summary>
public static bool IsEyeRenderer(Renderer _renderer)
{
if (_renderer == null || _renderer is ParticleSystemRenderer)
{
return false;
}
if (_renderer.name != null && _renderer.name.ToLower().Contains("eye"))
{
return true;
}
Material[] shared = _renderer.sharedMaterials;
if (shared == null)
{
return false;
}
foreach (Material m in shared)
{
if (IsEyeMaterial(m))
{
return true;
}
}
return false;
}
/// <summary>Запасной признак: текстура с "eye" в имени (у собаки это feral_eye).</summary>
public static bool IsEyeMaterial(Material _material)
{
if (_material == null)
{
return false;
}
string albedo = GhostTraderPatch.FindAlbedoProperty(_material);
if (albedo == null)
{
return false;
}
Texture texture = _material.GetTexture(albedo);
return texture != null && texture.name != null &&
texture.name.ToLower().Contains("eye");
}
/// <summary>true, только если глаза действительно нашлись и были подожжены. Пока false,
/// тик повторяет попытку - модель может быть ещё не собрана.</summary>
public static bool TryLightEyes(Entity _pet)
{
if (_pet == null)
{
return false;
}
Renderer[] renderers = _pet.GetComponentsInChildren<Renderer>(true);
if (renderers == null || renderers.Length == 0)
{
return false;
}
int lit = 0;
foreach (Renderer renderer in renderers)
{
if (!IsEyeRenderer(renderer))
{
continue;
}
// Рендерер целиком глазной, поэтому красим ВСЕ его материалы: мешу глаза больше
// ничего не принадлежит, а вот у собаки их два (обычный и feral), и попасть надо
// в оба - какой из них покажется, решает сама игра.
Material[] instances = renderer.materials;
foreach (Material eye in instances)
{
if (eye == null)
{
continue;
}
if (eye.HasProperty("_EmissionColor"))
{
eye.EnableKeyword("_EMISSION");
eye.globalIlluminationFlags = MaterialGlobalIlluminationFlags.RealtimeEmissive;
eye.SetColor("_EmissionColor", EyeColour * EmissionIntensity);
if (eye.HasProperty("_EmissionIntensity"))
{
eye.SetFloat("_EmissionIntensity", EmissionIntensity);
}
lit++;
}
if (eye.HasProperty("_Color"))
{
Color tint = eye.GetColor("_Color");
eye.SetColor("_Color", new Color(EyeColour.r, EyeColour.g, EyeColour.b, tint.a));
}
}
Debug.Log("[NecromancerTome] PetEyeGlow: рендерер глаз '" + renderer.name + "', материалов " +
instances.Length);
}
if (lit > 0)
{
Debug.Log("[NecromancerTome] PetEyeGlow: pet " + _pet.entityId + " - " + lit +
" eye material(s) lit");
}
return lit > 0;
}
}
}
+299 -2
View File
@@ -53,6 +53,27 @@ namespace NecromancerTome
{
public int OwnerEntityId;
public int PetEntityId;
/// <summary>У питомца своя задача следования (NecroFollowOwnerTask, 2026-09-18) -
/// значит телепорт-поводок ниже его НЕ трогает. Уборка владения при этом остаётся:
/// она к способу передвижения отношения не имеет.</summary>
public bool OwnFollowTask;
/// <summary>Удалось ли развести коллайдеры питомца с хозяйскими. Пока false, попытка
/// повторяется каждый тик - см. TryIgnoreCollisionWithOwner.</summary>
public bool CollisionIgnored;
/// <summary>Зажжены ли глаза (PetEyeGlowPatch.cs). Как и коллайдеры, с первого раза
/// обычно не получается - модели ещё нет; попытка повторяется до успеха.</summary>
public bool EyesLit;
/// <summary>Нужно ли им вообще заниматься: копия PetInfo.LitEyes, чтобы тик не лазил
/// в словарь на каждом питомце каждую секунду.</summary>
public bool WantsLitEyes;
/// <summary>Радиус "дома" для летающих (см. PetInfo.FlyingHomeRadius). Ноль -
/// питомец не летающий, дом ему не переставляем.</summary>
public int FlyingHomeRadius;
}
public static readonly List<TrackedPet> TrackedPets = new List<TrackedPet>();
@@ -62,12 +83,98 @@ namespace NecromancerTome
public static void Init()
{
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
ModEvents.WorldShuttingDown.RegisterHandler(OnWorldShuttingDown);
}
/// <summary>ДОБАВЛЕНО 2026-09-18. TrackedPets - статический список, он переживает выход в
/// меню, а вот мир, к которому относятся лежащие в нём entityId, - нет. Без этой уборки
/// на следующей загрузке первый же тик начинал разбирать чужие идентификаторы: в новом
/// мире тот же номер принадлежит совершенно другой сущности, и RemoveOwnedEntity уходил
/// бы неизвестно куда. GhostTraderPatch.cs подписан на то же событие ровно по той же
/// причине; здесь подписки не было - это был найденный, но не закрытый пробел из разбора
/// ИИ питомцев (BACKLOG.md, 2026-09-18, п. 6).</summary>
public static void OnWorldShuttingDown(ref ModEvents.SWorldShuttingDownData _data)
{
if (TrackedPets.Count > 0)
{
Debug.Log("[NecromancerTome] PetFollowPatch: world shutting down, forgetting " +
TrackedPets.Count + " tracked pet(s)");
TrackedPets.Clear();
}
timer = 0f;
}
/// <summary>Called from SummonPatch.cs right after a pet is created and owned.</summary>
public static void Register(EntityAlive owner, Entity pet)
public static void Register(EntityAlive owner, Entity pet, bool ownFollowTask, bool collisionIgnored,
bool wantsLitEyes, int flyingHomeRadius)
{
TrackedPets.Add(new TrackedPet { OwnerEntityId = owner.entityId, PetEntityId = pet.entityId });
TrackedPets.Add(new TrackedPet
{
OwnerEntityId = owner.entityId,
PetEntityId = pet.entityId,
OwnFollowTask = ownFollowTask,
CollisionIgnored = collisionIgnored,
WantsLitEyes = wantsLitEyes,
FlyingHomeRadius = flyingHomeRadius,
});
}
/// <summary>Разводит коллайдеры питомца и его хозяина, чтобы они проходили друг сквозь
/// друга. Возвращает true, только если РЕАЛЬНО что-то развела.
///
/// БАГ-РЕПОРТ 2026-09-18: "когда крыса оказывается под ногами, игрока подбрасывает".
/// Просьба сделать питомца нематериальным для игрока была ещё 28.08, и код для неё
/// написан тогда же (SummonPatch.IgnoreCollisionWithOwner) - но он вызывался ровно один
/// раз, из постфикса на EntityFactory.CreateEntity, а это СЛИШКОМ РАНО: там сущность
/// только создана и в мир ещё не добавлена (SpawnEntityInWorld идёт следующей строкой в
/// ItemActionSpawnEntity.Spawn), модель не собрана, и коллайдеров, привязанных к костям
/// через PhysicsBody, попросту ещё нет. GetComponentsInChildren возвращал пустой массив,
/// цикл не делал ни одной итерации, и никто этого не замечал - метод ничего не возвращал
/// и ничего не логировал. Ровно тот же урок про "модель собирается позже" уже записан в
/// GhostTraderPatch.cs, где из-за него пришлось опрашивать торговцев по таймеру.
///
/// Поэтому теперь попытка повторяется в тике раз в секунду, пока не удастся. Толкать
/// игрока питомец сможет в худшем случае одну секунду после призыва.
///
/// Разводятся ВСЕ пары коллайдеров, включая капсулы передвижения: CharacterController -
/// это тоже Collider, и именно на ней игрок и стоит, когда его подбрасывает.</summary>
public static bool TryIgnoreCollisionWithOwner(EntityAlive owner, Entity pet)
{
if (owner == null || pet == null)
{
return false;
}
Collider[] ownerColliders = owner.GetComponentsInChildren<Collider>(true);
Collider[] petColliders = pet.GetComponentsInChildren<Collider>(true);
if (ownerColliders == null || petColliders == null ||
ownerColliders.Length == 0 || petColliders.Length == 0)
{
return false;
}
int pairs = 0;
foreach (Collider oc in ownerColliders)
{
if (oc == null)
{
continue;
}
foreach (Collider pc in petColliders)
{
if (pc == null || pc == oc)
{
continue;
}
Physics.IgnoreCollision(oc, pc, true);
pairs++;
}
}
if (pairs > 0)
{
Debug.Log("[NecromancerTome] PetFollowPatch: pet " + pet.entityId + " passes through owner " +
owner.entityId + " now (" + ownerColliders.Length + "x" + petColliders.Length +
" collider pairs ignored)");
}
return pairs > 0;
}
/// <summary>Called from SummonPatch.cs's manual recall path so a recalled pet stops being
@@ -83,6 +190,130 @@ namespace NecromancerTome
}
}
/// <summary>ПИТАНИЕ ПАДАЛЬЮ, 2026-09-18. Наблюдение пользователя: "Дух крысы после того как
/// зомби погибает, начинает есть труп. Пусть тогда этот процесс восстанавливает ей здоровье".
///
/// Поедание тут не отдельное поведение, а побочный эффект приказа: команда атаковать ставит
/// цель на 6000 тиков (PetAttackCommand.OrderTicks), а EAIApproachAndAttackTarget.CanExecute
/// сверяет только ТИП цели и ничего не знает про её смерть - так что, добив зомби, крыса
/// продолжает грызть труп, пока тот не исчезнет. Само по себе это выглядело хорошо, поэтому
/// не чинится, а используется.
///
/// Лечение идёт отсюда, а не из triggered_effect в items.xml, по простой причине: в XML
/// нельзя спросить "цель мертва". Полный список классов Requirement* в Assembly-CSharp
/// просмотрен - там есть RequirementFullHealth, RequirementHasEntityTag, RequirementNearbyEntities
/// и ещё сорок, но ни одного про смерть цели. Значит гейт всё равно оказался бы в коде, а
/// тик раз в секунду тут уже есть и обходит ровно тех же питомцев.
///
/// Условий три, и все три обязательны: цель есть, цель МЕРТВА (иначе это лечение в бою, а
/// не питание), и крыса рядом с ней (иначе она лечилась бы, стоя в другом конце улицы и
/// только СОБИРАЯСЬ дойти до трупа).</summary>
public const float FeedRangeSq = 2.5f * 2.5f;
/// <summary>Здоровья за секунду поедания. У Духа крысы всего 120 HP, так что 5 - это полное
/// восстановление примерно за полминуты над одним трупом: заметно, но не бесплатно.</summary>
public const int FeedHealthPerSecond = 5;
/// <summary>ДЫРА, ЗАКРЫТАЯ 2026-09-18. Защита от падения живёт в NecroFollowOwnerTask, а
/// та НЕ ИСПОЛНЯЕТСЯ, пока у питомца есть цель (CanExecute возвращает false) - то есть
/// именно в бою, когда питомец и бегает по незнакомым местам, ловить его было нечем.
/// Этот тик работает всегда, поэтому проверка переехала сюда.
///
/// ПРОВЕРКИ ДВЕ, И ОНИ ПРО РАЗНОЕ - это важно не путать.
///
/// 1. АБСОЛЮТНАЯ ВЫСОТА. Ниже отметки 5 не бывает законных причин находиться: мир
/// кончается на нуле, и всё, что туда опустилось, игра удаляет молча
/// (Entity.onUpdate: position.y &lt; 0f -&gt; MarkToUnload). Двусмысленности нет никакой,
/// поэтому спасаем всегда, хоть в бою, хоть нет. Это и есть настоящий детектор
/// падения, и он один закрывает тот случай, на котором 18.09 потерялась крыса.
///
/// 2. НИЖЕ ЦЕЛИ (предложено пользователем). Это детектор НЕ падения, а
/// НЕДОСТИЖИМОСТИ: питомец, который на десять метров ниже того, кого ему велели
/// грызть, скорее всего не упал, а не может добраться - зомби на крыше, на этаже
/// выше, за проломом. Без этой проверки он будет ломиться туда вечно: приказ игрока
/// держится 6000 тиков и сам не истечёт.
///
/// Поэтому здесь цель ещё и СБРАСЫВАЕТСЯ. Вернуть питомца к хозяину, не сняв
/// приказ, значило бы получить маятник: прыжок наверх - бег вниз - прыжок наверх.
///
/// Обратная сторона второй проверки названа честно: зомби, стоящий двумя этажами выше,
/// теперь отменяет приказ вместо бесконечной беготни. По-моему это лучше, но если
/// окажется, что питомец сдаётся слишком рано - крутить UnreachableBelowTarget.</summary>
public const float UnreachableBelowTarget = 10f;
public static void RescueFallen(EntityAlive pet, EntityAlive owner)
{
if (pet == null || owner == null || pet.world == null)
{
return;
}
bool outOfWorld = pet.position.y < NecroFollowOwnerTask.WorldFloorGuard;
EntityAlive target = pet.GetAttackTarget();
bool unreachable = !outOfWorld && target != null && !target.IsDead() &&
pet.position.y < target.position.y - UnreachableBelowTarget;
if (!outOfWorld && !unreachable)
{
return;
}
pet.IsStuck = false;
pet.motion = Vector3.zero;
if (pet.moveHelper != null)
{
pet.moveHelper.Stop();
}
if (unreachable)
{
pet.SetAttackTarget(null, 0);
}
pet.SetPosition(NecroFollowOwnerTask.SlotPosition(owner, 0, pet.world), true);
Debug.Log("[NecromancerTome] PetFollowPatch: pet " + pet.entityId + " rescued to owner " +
owner.entityId + " - " + (outOfWorld
? "выпал из мира (высота " + pet.position.y.ToCultureInvariantString("0.0") + ")"
: "цель недостижима, она выше на " +
(target.position.y - pet.position.y).ToCultureInvariantString("0.0") + " м"));
}
/// <summary>Кому принадлежит этот питомец. null - значит не наш или уже не отслеживается.
/// Нужна PetKillCreditPatch.cs, чтобы зачесть убийство владельцу.</summary>
public static EntityPlayer FindOwnerOfPet(int petEntityId)
{
World world = GameManager.Instance != null ? GameManager.Instance.World : null;
if (world == null)
{
return null;
}
for (int i = 0; i < TrackedPets.Count; i++)
{
if (TrackedPets[i].PetEntityId == petEntityId)
{
return world.GetEntity(TrackedPets[i].OwnerEntityId) as EntityPlayer;
}
}
return null;
}
public static void FeedOnCorpse(EntityAlive pet)
{
EntityAlive target = pet.GetAttackTarget();
if (target == null || !target.IsDead())
{
return;
}
if ((target.position - pet.position).sqrMagnitude > FeedRangeSq)
{
return;
}
int before = pet.Health;
pet.AddHealth(FeedHealthPerSecond);
if (pet.Health != before)
{
Debug.Log("[NecromancerTome] PetFollowPatch: pet " + pet.entityId + " fed on corpse " +
target.entityId + ", health " + before + " -> " + pet.Health);
}
}
public static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
{
timer += Time.deltaTime;
@@ -147,6 +378,72 @@ namespace NecromancerTome
}
}
// Повторная попытка развести коллайдеры - до первого успеха. Стоит ДО всех
// проверок про бой и расстояние: "питомец подбрасывает хозяина" не зависит ни от
// того, ни от другого.
if (!tracked.CollisionIgnored)
{
tracked.CollisionIgnored = TryIgnoreCollisionWithOwner(owner, pet);
}
// СЛЕДОВАНИЕ ДЛЯ ЛЕТАЮЩИХ, 2026-09-18. Дом переставляется на хозяина каждую
// секунду, а возвращается питомец САМ, своим полётом: у EntityVulture проверка
// "не ушёл ли из дома" встроена и работает каждые 60 тиков. Ни телепорта, ни
// пафайндинга, ни своей задачи - см. PetInfo.FlyingHomeRadius.
//
// НА ВРЕМЯ АТАКИ ДОМ ОТВЯЗЫВАЕТСЯ СОВСЕМ (указание того же дня: "для атаки радиус
// нужно увеличить, пусть летит сколько надо... а вот после атаки пусть
// возвращается"). Иначе собственная проверка EntityVulture срывала бы его с цели
// на полпути: она прерывает атаку, как только питомец вышел за радиус.
//
// Отвязка - это detachHome(), то есть maximumHomeDistance = -1, и тогда
// isWithinHomeDistanceCurrentPosition() безусловно возвращает true. Ограничителем
// вместо радиуса остаётся сама дальность приказа: цель назначается только по
// прицелу, а луч прицела не длиннее 30 метров, так что отправить Грифа на другой
// край карты нельзя при всём желании.
//
// Как только цель пропала (убита, недостижима, приказ сброшен), дом на следующей
// же секунде встаёт обратно на хозяина - и питомец возвращается сам.
if (tracked.FlyingHomeRadius > 0)
{
if (pet.GetAttackTarget() != null)
{
if (pet.hasHome())
{
pet.detachHome();
Debug.Log("[NecromancerTome] PetFollowPatch: flying pet " + pet.entityId +
" is on a target - home detached for the chase");
}
}
else
{
if (!pet.hasHome())
{
Debug.Log("[NecromancerTome] PetFollowPatch: flying pet " + pet.entityId +
" finished the chase - home re-anchored to owner " + owner.entityId);
}
pet.setHomeArea(new Vector3i(owner.position), tracked.FlyingHomeRadius);
}
}
if (tracked.WantsLitEyes && !tracked.EyesLit)
{
PetEyeGlow.DumpMaterialsOnce(pet, EntityClass.list[pet.entityClass]?.entityClassName);
tracked.EyesLit = PetEyeGlow.TryLightEyes(pet);
}
if (tracked.OwnFollowTask)
{
FeedOnCorpse(pet);
RescueFallen(pet, owner);
// У этого питомца есть настоящая задача следования (NecroFollowOwnerTask):
// она сама держит его при хозяине, сама решает, когда пройти сквозь стену, и
// сама сажает его на опорный блок. Телепорт-поводок здесь только мешал бы -
// две системы дёргали бы питомца в разные стороны. Уборка владения выше при
// этом уже отработала, и она остаётся общей для всех.
continue;
}
if (pet.GetAttackTarget() != null)
{
// Mid-fight - let it finish rather than teleporting it away.
+408
View File
@@ -0,0 +1,408 @@
using GamePath;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// ПЕРВАЯ СВОЯ ЗАДАЧА ИИ В ЭТОМ МОДЕ. "Дух крысы" держится справа-сзади от хозяина, а не
/// бродит сам по себе - указание 2026-09-18 ("держится сбоку и позади хозяина на 2 блока...
/// если по циферблату часов, то на 3-6 часов (зависит от препятствий)").
///
/// ПОЧЕМУ ЭТО ПРИШЛОСЬ ПИСАТЬ, А НЕ НАСТРОИТЬ. Задачи "иди за сущностью" в игре НЕТ вообще -
/// проверено по списку типов в Assembly-CSharp: всего 32 класса EAI*, и ни одного
/// follow-подобного (EAIWander, EAITerritorial, EAIApproachSpot, EAIApproachAndAttackTarget,
/// EAISetNearestCorpseAsTarget и т.д.). Слежение дрона за хозяином - это захардкоженный C#
/// внутри EntityDrone, в XML его не выбрать. До сегодняшнего дня мод обходился телепорт-
/// поводком в PetFollowPatch.cs: раз в секунду, если питомец дальше 32 м, его просто
/// переставляли к игроку. Это и есть то, что игроки видели как "питомец не идёт следом".
///
/// ОБРАЗЕЦ ВЗЯТ С ВАНИЛЬНОГО EAIApproachSpot - он решает ровно ту же задачу "дойти до точки"
/// и показывает весь нужный обвес: theEntity.FindPath(...) -> theEntity.navigator.getPath()
/// -> moveHelper. Отличий от него три, и все три - требования пользователя:
///
/// 1. ТОЧКА ПЕРЕСЧИТЫВАЕТСЯ КАЖДЫЙ ТИК и привязана к хозяину, а не к месту на земле.
/// SlotAngles - сектор "3-6 часов" от направления взгляда хозяина, перебираемый от
/// середины к краям: сначала 135° (ровно между бортом и кормой), потом ближе к борту и к
/// корме. Первая точка, до которой нашёлся путь, и становится местом крысы - это и есть
/// "зависит от препятствий".
///
/// 2. canBreak: false в FindPath. Питомцы до сих пор носили ванильный AITask BreakBlock и
/// грызли всё на пути, включая базу игрока. Крыса не может грызть блоки НА УРОВНЕ
/// ПАФАЙНДЕРА: он просто не строит путей через разрушение. В entityclasses.xml у неё
/// заодно нет ни BreakBlock, ни BlockingTargetTask.
///
/// 3. ПРОХОД СКВОЗЬ БЛОКИ вместо разрушения - указание "технически, дух может проходить
/// сквозь блоки, если нужно пройти там где есть препятствие. Блоки ломать не нужно".
/// Для этого не понадобилось ничего изобретать: в Entity есть публичное поле IsStuck, и
/// единственное место, где игра его читает, - собственный шаг перемещения:
///
/// if (IsStuck) { PhysicsTransform.position += hitMove; } // мимо контроллера
/// else { collisionFlags = m_characterController.Move(hitMove); }
///
/// То есть это готовый выключатель столкновений, оставленный игрой себе на случай
/// застревания. Ни один из просмотренных классов (EntityMoveHelper, EntityZombie,
/// EntityEnemy, EntityAnimal, EntityAlive, EntityPlayerLocal) в него не ПИШЕТ - поле
/// фактически бесхозное, и мы можем владеть им единолично.
///
/// ОПАСНОСТЬ ЗДЕСЬ РЕАЛЬНАЯ, И ОНА УЖЕ СРАБОТАЛА: IsStuck отключает столкновения со
/// ВСЕМ, включая пол, и 18.09 крыса провалилась под мир ("fell off the world,
/// pos=(319.2, -0.4, 1367.2)"). Разбор и все пять страховок - в комментариях к
/// MaxDropBelowOwner и в UpdatePhase; коротко: гравитация гасится на каждом тике,
/// ниже цели питомец не опускается, проход не включается на дальней дистанции и
/// ограничен по времени, а падение ловится отдельной проверкой в начале Update.
///
/// ЗАДАЧА ДОБАВЛЯЕТСЯ В РАНТАЙМЕ (SummonPatch.cs), а не через XML. В XML это тоже возможно -
/// EAIManager.GetType() для незнакомого имени падает в Type.GetType("EAI" + имя), и
/// индексированные AITask-N берут значение свойства целиком, так что
/// AITask-N value="NecroFollowOwner,NecromancerHarmony" разрешилось бы в наш тип. Но
/// Type.GetType с именем сборки дёргает Assembly.Load, а как именно загрузчик модов кладёт
/// наши DLL - непроверено, и ставить работоспособность питомца в зависимость от этого нет
/// никакой нужды: aiManager.tasks.AddTask даёт тот же результат без единого допущения, да ещё
/// и позволяет передать владельца прямо в конструктор.
/// </summary>
public class NecroFollowOwnerTask : EAIBase
{
/// <summary>Сектор "3-6 часов" в градусах от направления взгляда хозяина: 90° - правый
/// борт, 180° - корма. Перебор идёт от середины сектора к его краям, чтобы обычное место
/// крысы было одно и то же, а края работали как запасные при препятствии.</summary>
public static readonly float[] SlotAngles = { 135f, 112f, 158f, 90f, 180f };
/// <summary>Расстояние по умолчанию - два блока от хозяина.</summary>
public const float DefaultSlotDistance = 2f;
/// <summary>Своё у каждого питомца: Медведю и Волку задан блок дальше остальных
/// (указание 2026-09-18 - "сделай место следования там же где и собаке, но на блок
/// дальше, они мощнее"). Сектор при этом общий, меняется только радиус, поэтому
/// крупные звери встают в тот же угол, но не наступают хозяину на пятки.
/// Значение приходит из SummonPatch.LimitedPets при создании задачи.</summary>
public float SlotDistance = DefaultSlotDistance;
/// <summary>Ближе этого крыса считает, что уже на месте, и останавливается. Квадрат
/// расстояния, как везде в этом коде - корень тут не нужен.</summary>
public const float ArrivedDistSq = 1.2f * 1.2f;
/// <summary>Столько секунд без сокращения расстояния - и точка считается недостижимой:
/// пробуем следующую, а когда кончатся все - проходим насквозь.
///
/// ПОДНЯТО С 1.5 ДО 3 СЕКУНД 2026-09-18, после того как крыса провалилась сквозь мир.
/// Полторы секунды - слишком мало: в помещении питомец столько обходит мебель, не
/// сокращая расстояния ПО ПРЯМОЙ, и проход включался там, где обычный обход дошёл бы
/// сам. Чем реже поднимается IsStuck, тем меньше поводов у всего, что с ним связано.</summary>
public const float NoProgressSeconds = 3f;
/// <summary>Метры в секунду на проходе сквозь препятствие. Медленнее бега: дух
/// просачивается, а не выстреливает.</summary>
public const float PhaseSpeed = 3.5f;
/// <summary>Предохранитель. Если за это время проход не закончился, он прекращается
/// принудительно - лучше крыса, стоящая не там, чем крыса, летящая сквозь мир.</summary>
public const float PhaseTimeout = 4f;
/// <summary>Дальше этого расстояния до хозяина ждать пафайндер бессмысленно - прыжок.
/// Та же цифра, что у старого поводка в PetFollowPatch.cs (и у ванильного DroneManager,
/// откуда она изначально и взята).
///
/// РАНЬШЕ ЗДЕСЬ БЫЛ ПРОХОД, И ЭТО БЫЛА ОШИБКА (исправлено 18.09, см. MaxDropBelowOwner):
/// проход задуман для препятствия в шаге, а не для перелёта через тридцать метров
/// незнакомой местности с отключёнными столкновениями.</summary>
public const float LeashDistance = 32f;
/// <summary>Аварийный предел по высоте: если питомец оказался ниже хозяина больше чем на
/// столько, разбираться уже некогда - прыжок к хозяину.
///
/// ЗАЧЕМ. 18.09 крыса пропала в игре, и лог сказал ровно что случилось:
/// WRN Entity [type=EntityZombieDog, name=necroRatSpirit, id=1459] fell off the world,
/// pos=(319.2, -0.4, 1367.2)
/// Высота -0.4: она провалилась под мир, а игра удаляет всё, что опустилось ниже нуля
/// (Entity.onUpdate: position.y &lt; 0f -&gt; MarkToUnload). Виноват IsStuck - он снимает
/// столкновения со ВСЕМ, включая пол, а гравитация никуда не девается: шаг перемещения
/// складывает hitMove с накопленным motion. Четыре секунды прохода над дыркой в полу -
/// и питомца нет.
///
/// Эта проверка работает независимо от прохода и ловит любое падение, а не только наше
/// собственное.</summary>
public const float MaxDropBelowOwner = 6f;
/// <summary>Ниже этой высоты вмешиваемся в любом случае: мир кончается на нуле, и пять
/// метров запаса нужны, чтобы успеть что-то сделать, а не констатировать.</summary>
public const float WorldFloorGuard = 5f;
public int OwnerEntityId = -1;
public Vector3 seekPos;
public int slotIndex;
public float noProgressTime;
public float lastDistSq;
public bool phasing;
public float phaseTime;
public int pathRecalculateTicks;
public override void Init(EntityAlive _theEntity)
{
base.Init(_theEntity);
// 3 - те же биты, что у EAIApproachSpot: это задача ПЕРЕМЕЩЕНИЯ, и она не должна
// исполняться одновременно с другой такой же (в первую очередь с погоней за целью).
MutexBits = 3;
executeDelay = 0.1f;
}
public EntityAlive Owner()
{
if (OwnerEntityId < 0 || theEntity == null || theEntity.world == null)
{
return null;
}
return theEntity.world.GetEntity(OwnerEntityId) as EntityAlive;
}
/// <summary>Точка для слота slotIndex: хозяин + поворот его собственного направления на
/// угол из SlotAngles, посаженный на опорный блок.</summary>
public Vector3 SlotPosition(EntityAlive _owner, int _slot)
{
return SlotPosition(_owner, _slot, theEntity.world, SlotDistance);
}
/// <summary>То же самое, но без экземпляра задачи - нужна PetFollowPatch.cs, который
/// спасает упавших питомцев во время боя, когда сама задача не исполняется. Там точный
/// радиус не важен (питомца просто возвращают к хозяину), поэтому берётся общий.</summary>
public static Vector3 SlotPosition(EntityAlive _owner, int _slot, World _world,
float _slotDistance = DefaultSlotDistance)
{
float angle = SlotAngles[_slot % SlotAngles.Length];
Vector3 dir = _owner.qrotation * (Quaternion.Euler(0f, angle, 0f) * Vector3.forward);
Vector3 raw = _owner.position + dir * _slotDistance;
return _world.FindSupportingBlockPos(raw);
}
public override bool CanExecute()
{
// Дерётся - не мешаем. Цель крысе даёт игрок (PetCommandPatch.cs), и пока она есть,
// место в строю подождёт.
if (theEntity.GetAttackTarget() != null)
{
return false;
}
if (theEntity.IsDead() || theEntity.IsSleeping)
{
return false;
}
EntityAlive owner = Owner();
if (owner == null)
{
return false;
}
seekPos = SlotPosition(owner, slotIndex);
return (seekPos - theEntity.position).sqrMagnitude > ArrivedDistSq;
}
public override void Start()
{
noProgressTime = 0f;
lastDistSq = float.MaxValue;
phasing = false;
phaseTime = 0f;
pathRecalculateTicks = 0;
}
public override bool Continue()
{
if (theEntity.GetAttackTarget() != null)
{
return false;
}
EntityAlive owner = Owner();
if (owner == null)
{
return false;
}
seekPos = SlotPosition(owner, slotIndex);
return (seekPos - theEntity.position).sqrMagnitude > ArrivedDistSq;
}
public override void Update()
{
EntityAlive owner = Owner();
if (owner == null)
{
return;
}
// ПЕРВОЙ СТРОКОЙ, ДО ВСЕГО ОСТАЛЬНОГО: не падает ли питомец из мира. Проверка стоит
// здесь, а не внутри прохода, намеренно - провалиться можно и без нашего участия
// (дыра в полу POI, взорванный блок), а последствие одно и то же: сущность ниже нуля
// игра удаляет молча, и питомец просто исчезает.
if (theEntity.position.y < WorldFloorGuard ||
theEntity.position.y < owner.position.y - MaxDropBelowOwner)
{
SnapToOwner(owner, "падение (высота " + theEntity.position.y.ToCultureInvariantString("0.0") + ")");
return;
}
Vector3 lookPosition = owner.position;
lookPosition.y += 0.8f;
theEntity.SetLookPosition(lookPosition);
if (phasing)
{
UpdatePhase();
return;
}
float distSq = (seekPos - theEntity.position).sqrMagnitude;
bool ownerFarAway = (owner.position - theEntity.position).sqrMagnitude > LeashDistance * LeashDistance;
// Сокращаем расстояние - значит путь есть и он работает; счётчик простоя сбрасывается.
if (distSq < lastDistSq - 0.05f)
{
noProgressTime = 0f;
}
else
{
noProgressTime += executeDelay;
}
lastDistSq = distSq;
// ХОЗЯИН ДАЛЕКО - ЭТО НЕ ПОВОД ПРОХОДИТЬ СКВОЗЬ СТЕНЫ, и раньше было именно так.
// Проход задуман для препятствия В ШАГЕ, а не для перелёта через тридцать метров
// незнакомой местности с отключёнными столкновениями: ровно такой проход 18.09 и
// уронил крысу под мир. На дальней дистанции работает обычный прыжок поводком - тот
// самый приём, которым ванильный DroneManager возвращает дрона, и он безопасен,
// потому что точка назначения известна заранее и стоит на земле.
if (ownerFarAway)
{
SnapToOwner(owner, "хозяин дальше " + LeashDistance + " м");
return;
}
if (noProgressTime >= NoProgressSeconds)
{
noProgressTime = 0f;
lastDistSq = float.MaxValue;
if (slotIndex >= SlotAngles.Length - 1)
{
// Перепробованы все точки сектора - значит дело в препятствии: сквозь.
slotIndex = 0;
BeginPhase();
return;
}
// Следующая точка сектора - "зависит от препятствий".
slotIndex++;
seekPos = SlotPosition(owner, slotIndex);
pathRecalculateTicks = 0;
}
if (theEntity.navigator.getPath() != null)
{
theEntity.moveHelper.CalcIfUnreachablePos();
}
if (--pathRecalculateTicks <= 0)
{
UpdatePath();
}
}
public void UpdatePath()
{
if (PathFinderThread.Instance.IsCalculatingPath(theEntity.entityId))
{
return;
}
pathRecalculateTicks = 8 + GetRandom(6);
// canBreak: false - крыса не грызёт блоки, это её главное отличие от всех остальных
// питомцев мода (см. шапку файла).
theEntity.FindPath(seekPos, theEntity.GetMoveSpeedAggro(), false, this);
}
public void BeginPhase()
{
phasing = true;
phaseTime = 0f;
theEntity.moveHelper.Stop();
theEntity.IsStuck = true;
}
public void EndPhase()
{
phasing = false;
phaseTime = 0f;
theEntity.IsStuck = false;
// ОБЯЗАТЕЛЬНО: пока IsStuck был поднят, пола для крысы не существовало. Сажаем её на
// опорный блок, иначе она останется висеть или продолжит падать.
theEntity.SetPosition(theEntity.world.FindSupportingBlockPos(theEntity.position), true);
lastDistSq = float.MaxValue;
noProgressTime = 0f;
pathRecalculateTicks = 0;
}
public void UpdatePhase()
{
phaseTime += executeDelay;
// ГРАВИТАЦИЯ ГАСИТСЯ КАЖДЫЙ ТИК, и это главная правка 18.09. Пока IsStuck поднят,
// шаг перемещения складывает накопленный motion прямо с позицией, минуя контроллер
// и пол, - то есть питомец продолжает "падать" между нашими SetPosition, причём
// ускоряясь. Обнуление motion означает: во время прохода питомца двигает ТОЛЬКО
// этот метод и ничто больше.
theEntity.motion = Vector3.zero;
Vector3 next = Vector3.MoveTowards(theEntity.position, seekPos, PhaseSpeed * executeDelay);
// И отдельно - высота. Даже с обнулённым motion спускаться ниже цели незачем:
// seekPos уже посажен на опорный блок (FindSupportingBlockPos), так что всё, что
// ниже него, - это пол, сквозь который мы проходить не собирались.
if (next.y < seekPos.y)
{
next.y = seekPos.y;
}
theEntity.SetPosition(next, true);
if (phaseTime >= PhaseTimeout || (next - seekPos).sqrMagnitude <= ArrivedDistSq)
{
EndPhase();
}
}
/// <summary>Прыжок к хозяину: аварийный выход и он же дальний поводок. Точка - тот же
/// слот в секторе, посаженный на опорный блок, а не позиция хозяина в упор (иначе
/// коллайдеры сложатся и питомец подбросит игрока - эти грабли уже разобраны в
/// PetFollowPatch.cs).</summary>
public void SnapToOwner(EntityAlive _owner, string _why)
{
if (phasing)
{
phasing = false;
phaseTime = 0f;
}
theEntity.IsStuck = false;
theEntity.motion = Vector3.zero;
theEntity.moveHelper.Stop();
slotIndex = 0;
lastDistSq = float.MaxValue;
noProgressTime = 0f;
pathRecalculateTicks = 0;
Vector3 dest = SlotPosition(_owner, 0);
theEntity.SetPosition(dest, true);
Debug.Log("[NecromancerTome] NecroFollowOwnerTask: pet " + theEntity.entityId +
" snapped back to owner " + _owner.entityId + " - " + _why);
}
public override void Reset()
{
if (phasing)
{
EndPhase();
}
theEntity.IsStuck = false;
theEntity.moveHelper.Stop();
theEntity.SetLookPosition(Vector3.zero);
}
public override string ToString()
{
return string.Format("{0}, slot{1}{2} dist{3}", base.ToString(), slotIndex,
phasing ? " PHASING" : "",
(theEntity.position - seekPos).magnitude.ToCultureInvariantString());
}
}
}
+197
View File
@@ -0,0 +1,197 @@
using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// УБИЙСТВА ПИТОМЦЕМ ТЕПЕРЬ ЗАСЧИТЫВАЮТСЯ ВЛАДЕЛЬЦУ - 2026-09-18.
///
/// ПОЧЕМУ РАНЬШЕ НЕ ЗАСЧИТЫВАЛИСЬ. Ваниль решает, чей это фраг, ровно в одном месте:
///
/// public void AwardKillXPServer(DamageSource _source, EntityAlive _killingEntity)
/// {
/// if (_source == null || _source.BuffClass != null) return; // (2)
/// EntityPlayer entityPlayer = _killingEntity as EntityPlayer; // (1)
/// if ((bool)entityPlayer &amp;&amp; !(entityPlayer == this) &amp;&amp; ...)
/// entityPlayer.AddKillXP(this, _source.AttackingItem, num);
/// }
///
/// (1) Убийца обязан БЫТЬ игроком. При укусе питомца _killingEntity - это сам питомец
/// (берётся как world.GetEntity(_dmResponse.Source.getEntityId())), приведение к
/// EntityPlayer даёт null, и AddKillXP не вызывается вовсе. А счётчик Некромантии висит
/// постфиксом именно на AddKillXP (NecromancyKillCreditPatch.cs) - значит не растут ни
/// уровень, ни урон Ножа некроманта, который от этого счётчика и считается.
///
/// ЧТО ДЕЛАЕТ ЭТОТ ПРЕФИКС. Подменяет _killingEntity на владельца, если убил его питомец.
/// Дальше всё едет по ванильному пути само: и опыт, и наш счётчик, и урон Ножа, и испытания -
/// потому что мод по-прежнему СЛЕДУЕТ решению ванили, а не дублирует его. Это то же правило,
/// ради которого счёт убийств 16.09 переехал из XML в постфикс на AddKillXP.
///
/// ПОЧЕМУ ЭТО ВООБЩЕ ПОТРЕБОВАЛОСЬ ОБСУЖДАТЬ. 17.09 пользователь решил обратное для зомби под
/// Камнем духов: "если зомби под контролем камня духов убивает другого, то в скилл это не
/// идёт, что в целом логично. Пока оставляем так". Подчинённые зомби в TrackedPets не лежат,
/// поэтому то решение остаётся в силе - здесь меняется только судьба ПРИЗВАННЫХ питомцев,
/// которых игрок крафтит, кормит и водит за собой.
///
/// ВТОРАЯ ПОЛОВИНА: ДОБИВАНИЕ КРОВОТЕЧЕНИЕМ. Строка (2) отсекает любой урон от баффа ДО
/// всякой проверки убийцы - то есть зомби, истёкший кровью, не даёт опыта никому и никогда,
/// даже если рану нанёс сам игрок. Для Духа крысы это половина всего урона (buffNecroRatBleed
/// капает 5 в секунду при укусе в 5), и без этой части починка была бы половинчатой.
///
/// Мешало одно: в DamageSource от баффа НЕТ идентификатора того, кто бафф наложил. Значит
/// помнить приходится самим - что и делает RecentPetBites ниже: каждый удар нашего питомца
/// записывает "эту жертву кусал питомец такого-то" со сроком годности чуть больше, чем живёт
/// кровотечение. Когда жертва умирает от баффа, запись и отвечает, кому зачесть.
///
/// Ванильный путь при этом НЕ ПРАВИТСЯ: мы не обнуляем _source.BuffClass (это общий объект,
/// его читают и дальше - PartyShareKillServer, разбор смерти игрока), а сами зовём
/// AddKillXP и отменяем оригинал, который всё равно вышел бы ни с чем.
/// </summary>
[HarmonyPatch(typeof(EntityAlive), "AwardKillXPServer")]
public static class Patch_EntityAlive_AwardKillXPServer_PetCredit
{
public static bool Prefix(EntityAlive __instance, DamageSource _source, ref EntityAlive _killingEntity)
{
if (_source == null)
{
return true;
}
// Смерть от баффа: ваниль выйдет по строке (2) ни с чем, поэтому зачисляем сами.
if (_source.BuffClass != null)
{
EntityPlayer bleedOwner = RecentPetBites.OwnerOf(__instance.entityId);
if (bleedOwner == null || bleedOwner == __instance || !EntityClass.list.ContainsKey(__instance.entityClass))
{
return true;
}
// KillXPScale у баффа обычно нулевой - это множитель "сколько опыта дать", и на
// нуле AddKillXP не дал бы ничего. Единица = обычное убийство.
float scale = _source.KillXPScale > 0f ? _source.KillXPScale : 1f;
bleedOwner.AddKillXP(__instance, _source.AttackingItem, scale);
RecentPetBites.Forget(__instance.entityId);
Debug.Log("[NecromancerTome] PetKillCredit: bleed kill of " + __instance.entityId +
" credited to owner " + bleedOwner.entityId);
return false;
}
if (_killingEntity == null || _killingEntity is EntityPlayer)
{
return true;
}
EntityPlayer owner = PetFollowPatch.FindOwnerOfPet(_killingEntity.entityId);
if (owner == null)
{
return true;
}
Debug.Log("[NecromancerTome] PetKillCredit: kill by pet " + _killingEntity.entityId +
" credited to owner " + owner.entityId);
_killingEntity = owner;
return true;
}
}
/// <summary>Кто кого кусал из питомцев и когда. Запись живёт чуть дольше кровотечения
/// (buffNecroRatBleed - 15 секунд), чтобы добивание точно попало в окно, и при этом не
/// превращалась в вечную память: зомби, укушенный минуту назад и умерший от чужой ловушки,
/// не должен приносить очки владельцу питомца.</summary>
public static class RecentPetBites
{
public const float MemorySeconds = 18f;
public struct Bite
{
public int OwnerEntityId;
public float ExpiresAt;
}
public static readonly Dictionary<int, Bite> Bites = new Dictionary<int, Bite>();
public static void Remember(int _victimEntityId, int _ownerEntityId)
{
Bites[_victimEntityId] = new Bite
{
OwnerEntityId = _ownerEntityId,
ExpiresAt = Time.time + MemorySeconds,
};
}
public static void Forget(int _victimEntityId)
{
Bites.Remove(_victimEntityId);
}
public static EntityPlayer OwnerOf(int _victimEntityId)
{
if (!Bites.TryGetValue(_victimEntityId, out Bite bite))
{
return null;
}
if (Time.time > bite.ExpiresAt)
{
Bites.Remove(_victimEntityId);
return null;
}
World world = GameManager.Instance != null ? GameManager.Instance.World : null;
return world != null ? world.GetEntity(bite.OwnerEntityId) as EntityPlayer : null;
}
}
/// <summary>Здесь и запоминается укус. Точка выбрана самая общая: любой урон, нанесённый
/// нашим питомцем, метит жертву - неважно, укус это, кровотечение от прошлого укуса или
/// что-то ещё, что мы добавим потом.
///
/// Постфикс, а не префикс: смысл только в том, чтобы записать факт, а вмешиваться в сам урон
/// незачем. Заодно здесь же подчищается таблица - редко и дёшево, без своего таймера.</summary>
[HarmonyPatch(typeof(EntityAlive), "DamageEntity")]
public static class Patch_EntityAlive_DamageEntity_RememberPetBite
{
public static int callsSincePrune;
public static void Postfix(EntityAlive __instance, DamageSource _damageSource)
{
if (_damageSource == null || __instance == null)
{
return;
}
int attackerId = _damageSource.getEntityId();
if (attackerId == -1 || attackerId == __instance.entityId)
{
return;
}
EntityPlayer owner = PetFollowPatch.FindOwnerOfPet(attackerId);
if (owner == null)
{
return;
}
RecentPetBites.Remember(__instance.entityId, owner.entityId);
if (++callsSincePrune >= 200)
{
callsSincePrune = 0;
Prune();
}
}
public static void Prune()
{
if (RecentPetBites.Bites.Count == 0)
{
return;
}
List<int> stale = new List<int>();
foreach (KeyValuePair<int, RecentPetBites.Bite> entry in RecentPetBites.Bites)
{
if (Time.time > entry.Value.ExpiresAt)
{
stale.Add(entry.Key);
}
}
foreach (int id in stale)
{
RecentPetBites.Bites.Remove(id);
}
}
}
}
+349 -56
View File
@@ -85,6 +85,54 @@ namespace NecromancerTome
/// fix the earlier "dog died, no way to get the book back" complaint: with nothing
/// ever taken, there's nothing to lose when the dog dies off-screen.</summary>
public bool ConsumesBook;
/// <summary>Указание 2026-09-18, пока только для Духа крысы: "если крыса уже
/// призвана, то призыв становится атакой". Для остальных питомцев Action0 при живом
/// питомце по-прежнему просто говорит "уже призван" (AlreadyActiveKey), как и было.
/// Разбор прицела - в PetCommandPatch.cs.</summary>
public bool CommandsAttack;
/// <summary>Что показать, когда команда атаки отдана, но под прицелом некого
/// атаковать. Читается только при CommandsAttack.</summary>
public string NoTargetKey;
/// <summary>Радиус "дома" для ЛЕТАЮЩИХ питомцев, в блоках. Ноль - выключено.
///
/// Указание 2026-09-18: "гриф улетает слишком далеко от игрока и срабатывает поводок.
/// Можем сделать ему следование, которое возвратит в радиус игрока, чтобы поводок
/// вообще не использовался?" Можем, и писать для этого ничего не пришлось: у
/// EntityVulture слежение за "домом" уже встроено. Каждые 60 тиков он проверяет
/// isWithinHomeDistanceCurrentPosition(), и если вышел за радиус - сам переходит в
/// State.Home и летит обратно СВОИМ полётом:
///
/// if (state != State.Home &amp;&amp; --homeCheckDelay &lt;= 0) {
/// homeCheckDelay = 60;
/// if (!isWithinHomeDistanceCurrentPosition()) SetState(State.AttackStop);
/// }
/// ... StartHome(getHomePosition().position.ToVector3());
///
/// Достаточно каждую секунду переставлять этот дом на позицию хозяина
/// (EntityAlive.setHomeArea), и получается настоящее следование за игроком, без
/// единого телепорта и без нашего пафайндинга.</summary>
public int FlyingHomeRadius;
/// <summary>Зажигать ли этому питомцу глаза (PetEyeGlowPatch.cs). Только у Пса и
/// Волка: у Медведя и Грифа модель - один материал на всё, и глаза там запечены в
/// текстуру шкуры, доставать их нечем.</summary>
public bool LitEyes;
/// <summary>За сколько блоков от хозяина держится этот питомец. Ноль означает "как
/// у всех" (NecroFollowOwnerTask.DefaultSlotDistance, два блока). Медведю и Волку
/// задан блок дальше - указание 2026-09-18, "они мощнее": сектор тот же, радиус
/// больше, чтобы туша не наступала хозяину на пятки.</summary>
public float FollowDistance;
/// <summary>Вешать ли этому питомцу собственную задачу следования
/// (NecroFollowOwnerTask): держаться справа-сзади, не бродить, не грызть блоки,
/// проходить сквозь препятствия. Старый телепорт-поводок из PetFollowPatch.cs для
/// таких питомцев выключается - две системы на одно и то же дёргали бы питомца в
/// разные стороны.</summary>
public bool UsesFollowTask;
}
public static readonly Dictionary<string, PetInfo> LimitedPets = new Dictionary<string, PetInfo>
@@ -98,6 +146,14 @@ namespace NecromancerTome
NothingToRecallKey = "necroZombieDogNothingToRecall",
SummonItemName = "bookSummonZombieDog",
ConsumesBook = false,
LitEyes = true,
// 2026-09-18: "поведение других призванных животных зомби тоже подгони под
// крысу". Те же три флага, что у неё: повторный Action0 - приказ атаковать
// цель под прицелом, своя задача следования вместо телепорт-поводка.
// AlreadyActiveKey у всех четверых теперь не читается никогда.
CommandsAttack = true,
NoTargetKey = "necroPetNoTarget",
UsesFollowTask = true,
}
},
{
@@ -131,6 +187,18 @@ namespace NecromancerTome
NothingToRecallKey = "necroZombieGriffinNothingToRecall",
SummonItemName = "bookSummonZombieGriffin",
ConsumesBook = false,
// 15 блоков: заметно меньше 32-метрового поводка, чтобы тот вообще не
// понадобился, и достаточно, чтобы Гриф не висел у игрока над головой.
FlyingHomeRadius = 15,
// UsesFollowTask у Грифа НЕТ с 18.09: он снова летающий (EntityVulture), а
// задача следования наземная - она гоняла бы его пафайндером по земле и
// дралась бы с его собственным полётом. Приказ атаковать оставлен.
// 2026-09-18: "поведение других призванных животных зомби тоже подгони под
// крысу". Те же три флага, что у неё: повторный Action0 - приказ атаковать
// цель под прицелом, своя задача следования вместо телепорт-поводка.
// AlreadyActiveKey у всех четверых теперь не читается никогда.
CommandsAttack = true,
NoTargetKey = "necroPetNoTarget",
}
},
{
@@ -142,6 +210,14 @@ namespace NecromancerTome
NothingToRecallKey = "necroZombieBearNothingToRecall",
SummonItemName = "bookSummonZombieBear",
ConsumesBook = false,
FollowDistance = 3f,
// 2026-09-18: "поведение других призванных животных зомби тоже подгони под
// крысу". Те же три флага, что у неё: повторный Action0 - приказ атаковать
// цель под прицелом, своя задача следования вместо телепорт-поводка.
// AlreadyActiveKey у всех четверых теперь не читается никогда.
CommandsAttack = true,
NoTargetKey = "necroPetNoTarget",
UsesFollowTask = true,
}
},
{
@@ -153,6 +229,34 @@ namespace NecromancerTome
NothingToRecallKey = "necroZombieWolfNothingToRecall",
SummonItemName = "bookSummonZombieWolf",
ConsumesBook = false,
FollowDistance = 3f,
// 2026-09-18: "поведение других призванных животных зомби тоже подгони под
// крысу". Те же три флага, что у неё: повторный Action0 - приказ атаковать
// цель под прицелом, своя задача следования вместо телепорт-поводка.
// AlreadyActiveKey у всех четверых теперь не читается никогда.
CommandsAttack = true,
NoTargetKey = "necroPetNoTarget",
UsesFollowTask = true,
}
},
// "Дух крысы", указание 2026-09-18 - питомец начального уровня (Некромантия 3) и
// первый, у кого поведение задано намеренно, а не унаследовано от зомбопса. На нём
// отлаживается ИИ питомцев вообще, поэтому он единственный, у кого стоят оба новых
// флага. AlreadyActiveKey у него НЕ ЧИТАЕТСЯ НИКОГДА: повторный Action0 - это не
// отказ "уже призван", а команда атаковать (CommandsAttack), так что вместо него
// показывается либо ничего (приказ принят), либо NoTargetKey.
{
"necroRatSpirit",
new PetInfo
{
AlreadyActiveKey = null,
RecalledKey = "necroRatSpiritRecalled",
NothingToRecallKey = "necroRatSpiritNothingToRecall",
SummonItemName = "bookSummonRatSpirit",
ConsumesBook = false,
CommandsAttack = true,
NoTargetKey = "necroPetNoTarget",
UsesFollowTask = true,
}
},
};
@@ -161,8 +265,17 @@ namespace NecromancerTome
/// summon books now declare Action1 with the same Class="SpawnEntity"/Entity as Action0.</summary>
public const int RecallActionIndex = 1;
/// <summary>Правда ровно между "префикс разрешил призыв" и концом этого же вызова Spawn.
/// Нужна постфиксу ниже, чтобы отличить НАСТОЯЩИЙ призыв от команды атаковать: обе идут
/// через один и тот же Action0, постфикс Harmony отрабатывает в обоих случаях (возврат
/// false из префикса его не отменяет), а делать им надо противоположное - в одном случае
/// снять цель со свежего питомца, в другом ни в коем случае её не трогать, её только что
/// поставил игрок.</summary>
public static bool summonAllowedThisCall;
public static bool Prefix(ItemActionSpawnEntity __instance, ItemActionData _actionData)
{
summonAllowedThisCall = false;
if (!LimitedPets.TryGetValue(__instance.entityToSpawn, out PetInfo tooltips))
{
return true;
@@ -207,15 +320,73 @@ namespace NecromancerTome
if (owned.Count > 0)
{
// Action0 при уже живом питомце. Для всех, кроме Духа крысы, это тупик "уже
// призван". Для него - команда атаковать то, на что смотрит игрок: см.
// PetCommandPatch.cs, там же и весь отсев (торговцы, игроки, мёртвые).
if (tooltips.CommandsAttack)
{
EntityAlive pet = holdingEntity.world != null
? holdingEntity.world.GetEntity(owned[0].Id) as EntityAlive
: null;
if (!PetAttackCommand.TryOrderAttack(holdingEntity, pet) && holdingEntity.world != null)
{
GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), tooltips.NoTargetKey);
}
return false;
}
if (holdingEntity.world != null)
{
GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), tooltips.AlreadyActiveKey);
}
return false;
}
summonAllowedThisCall = true;
return true;
}
/// <summary>ДОБАВЛЕНО 2026-09-18, после чтения ItemActionSpawnEntity.Spawn целиком.
/// Последней строкой ваниль делает вот что:
///
/// entityAlive.SetAttackTarget(holdingEntity.GetAttackTarget(), 600);
///
/// то есть свежепризванный питомец НАСЛЕДУЕТ ЦЕЛЬ ХОЗЯИНА. Для зомбопса это задумано и
/// полезно, а для Духа крысы прямо противоречит его единственному правилу - "сама не
/// атакует никогда". Призови её в драке, и она бросилась бы в бой сама, без приказа.
///
/// Почистить это в постфиксе на EntityFactory.CreateEntity нельзя: тот отрабатывает
/// ВНУТРИ Spawn, ещё до этой строки. Поэтому чистка здесь, после всего.
///
/// Флаг summonAllowedThisCall обязателен: без него постфикс сбрасывал бы цель и после
/// команды атаковать - то есть приказ гасил бы сам себя.</summary>
public static void Postfix(ItemActionSpawnEntity __instance)
{
if (!summonAllowedThisCall)
{
return;
}
summonAllowedThisCall = false;
if (!LimitedPets.TryGetValue(__instance.entityToSpawn, out PetInfo petInfo) || !petInfo.UsesFollowTask)
{
return;
}
World world = GameManager.Instance != null ? GameManager.Instance.World : null;
if (world == null || world.EntityAlives == null)
{
return;
}
int petClassId = EntityClass.GetId(__instance.entityToSpawn);
for (int i = world.EntityAlives.Count - 1; i >= 0; i--)
{
EntityAlive candidate = world.EntityAlives[i];
if (candidate != null && candidate.entityClass == petClassId && candidate.GetAttackTarget() != null)
{
candidate.SetAttackTarget(null, 0);
Debug.Log("[NecromancerTome] SummonPatch: cleared the target vanilla Spawn handed to fresh pet " +
candidate.entityId + " (this species never attacks on its own)");
}
}
}
/// <summary>summonItemName is null when this species doesn't consume its book on summon
/// (see PetInfo.ConsumesBook) - nothing was taken, so nothing is given back.</summary>
public static void RecallPet(EntityAlive owner, int petEntityId, string recalledTooltipKey, string summonItemName)
@@ -267,6 +438,140 @@ namespace NecromancerTome
}
}
/// <summary>
/// ОТЗЫВ СРАБАТЫВАЕТ СРАЗУ ПО НАЖАТИЮ - вторая правка того же бага, 2026-09-18. Первая
/// (Patch_..._IsActionRunning_FreeTheRecall ниже) сняла блокировку вторичного действия
/// основным, и этого оказалось мало: "отозвать собаку получилось далеко не с первого раза.
/// Это проблема всех питомцев".
///
/// ОСТАВШАЯСЯ ПРИЧИНА - ТА ЖЕ САМАЯ СЕМЬЯ, что и у призыва, только в другом слоте. Отзыв идёт
/// через ItemActionSpawnEntity.Spawn, а до него надо ДОЖИТЬ через автомат состояний:
///
/// OnHoldingUpdate: stateTime += 0.05
/// Anim: если stateTime >= animWait -> state = Spawn
/// Spawn: Spawn(data); state = End
/// ExecuteAction(_bReleased: true): state = None // отпустил - всё сбросилось
///
/// При animWait = 0.1 это ТРИ тика: 0.05 (мало), 0.10 (переход в Spawn), и только третий
/// вызывает сам Spawn. То есть кнопку надо продержать около 0.15 секунды. У призыва это
/// лечилось уменьшением animWait до 0.05, но там и оставалось два тика; меньше двух автомат
/// не умеет в принципе - один переход и одно исполнение.
///
/// Поэтому отзыв вынут из автомата совсем: он делается прямо на НАЖАТИИ, в префиксе
/// ExecuteAction, и оригинал не запускается вовсе. Никакого удержания, никакого ожидания.
///
/// ЗАЩЁЛКА ОБЯЗАТЕЛЬНА. ExecuteAction(_bReleased: false) зовётся не один раз за клик, а
/// КАЖДЫЙ КАДР, пока кнопка нажата (PlayerMoveController проверяет Secondary.IsPressed, а не
/// WasPressed), и вся защита от повторов у ванили держится на её же state == None, который мы
/// теперь не выставляем. Без задержки ниже одно удержание правой кнопки отозвало бы питомца и
/// следом залило экран надписью "не призван" по разу в кадр.
/// </summary>
[HarmonyPatch(typeof(ItemActionSpawnEntity), "ExecuteAction")]
public static class Patch_ItemActionSpawnEntity_ExecuteAction_InstantRecall
{
/// <summary>Секунды между двумя срабатываниями отзыва. Полсекунды заведомо больше любого
/// клика и заведомо меньше осмысленного повторного нажатия.</summary>
public const float RepeatGuardSeconds = 0.5f;
public static float lastRecallTime;
public static bool Prefix(ItemActionSpawnEntity __instance, ItemActionData _actionData, bool _bReleased)
{
if (_bReleased || _actionData == null ||
_actionData.indexInEntityOfAction != Patch_ItemActionSpawnEntity_Spawn_PetLimit.RecallActionIndex)
{
return true;
}
if (!Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.TryGetValue(
__instance.entityToSpawn, out Patch_ItemActionSpawnEntity_Spawn_PetLimit.PetInfo petInfo))
{
return true;
}
EntityAlive holdingEntity = _actionData.invData != null ? _actionData.invData.holdingEntity : null;
if (holdingEntity == null || holdingEntity.world == null)
{
return true;
}
if (Time.time - lastRecallTime < RepeatGuardSeconds)
{
// Кнопка всё ещё зажата с прошлого кадра - молча проглатываем.
return false;
}
int petClassId = EntityClass.GetId(__instance.entityToSpawn);
if (petClassId == -1)
{
return true;
}
lastRecallTime = Time.time;
List<OwnedEntityData> owned = holdingEntity.GetOwnedEntities(petClassId);
if (owned.Count > 0)
{
Patch_ItemActionSpawnEntity_Spawn_PetLimit.RecallPet(holdingEntity, owned[0].Id,
petInfo.RecalledKey, petInfo.ConsumesBook ? petInfo.SummonItemName : null);
holdingEntity.PlayOneShot(__instance.soundWarn);
}
else
{
GameManager.ShowTooltip(GameManager.Instance.World.GetPrimaryPlayer(), petInfo.NothingToRecallKey);
}
return false;
}
}
/// <summary>
/// "ОТЗЫВАЕТСЯ НЕ С ПЕРВОГО РАЗА" - баг-репорт пользователя 2026-09-18: "если прожал атаку
/// несколько раз, то при попытке отозвать крысу, она не отзывается с первого раза".
///
/// Причина не в нашем коде и не в питомце, а в гейте ввода. PlayerMoveController решает,
/// пускать ли вторичное действие, так:
///
/// bool flag16 = Actions[0].AllowConcurrentActions() || Actions[1].AllowConcurrentActions();
/// bool flag18 = Actions[0].IsActionRunning(actionData[0]); // основное ещё "идёт"
/// ...
/// if (flag12 &amp;&amp; flag15 &amp;&amp; (flag16 || !flag18)) // flag15 = Secondary.IsPressed
/// inventory.Execute(1, _bReleased: false, ...); // ВОТ ЭТО - отзыв
///
/// То есть пока основное действие считается идущим, вторичное просто не запускается.
/// А "идущим" оно считается вот по чему (ItemActionSpawnEntity):
///
/// public override bool IsActionRunning(ItemActionData _actionData)
/// =&gt; ((ItemActionDataSpawnEntity)_actionData).state != State.None;
///
/// Состояние проходит None -&gt; Anim -&gt; Spawn -&gt; **End**, и в None возвращается ТОЛЬКО из
/// ExecuteAction(_bReleased: true), то есть по отпусканию кнопки. End - это уже
/// отработавшее, законченное действие, но формально всё ещё "не None". Чем чаще игрок
/// щёлкает приказом атаковать, тем выше шанс, что в момент нажатия правой кнопки Action0
/// висит именно в End, - и первый отзыв уходит в никуда.
///
/// ЧИНИМ РОВНО ЭТО И НИЧЕГО БОЛЬШЕ: для наших книг призыва End больше не считается "идёт".
/// Соблазн был переписывать сам state в None - так делать НЕЛЬЗЯ: ExecuteAction стартует
/// новое действие как раз по условию state == None, и при зажатой кнопке призыв/приказ пошёл
/// бы на повтор каждые два тика, вместе с воплем кролика на каждый. Здесь же меняется только
/// ОТВЕТ на вопрос "идёт ли действие": вторичное разблокировано, а повторный старт
/// основного по-прежнему заперт настоящим полем state, которое так и осталось End.
/// </summary>
[HarmonyPatch(typeof(ItemActionSpawnEntity), "IsActionRunning")]
public static class Patch_ItemActionSpawnEntity_IsActionRunning_FreeTheRecall
{
public static void Postfix(ItemActionSpawnEntity __instance, ItemActionData _actionData, ref bool __result)
{
if (!__result || _actionData == null)
{
return;
}
if (!Patch_ItemActionSpawnEntity_Spawn_PetLimit.LimitedPets.ContainsKey(__instance.entityToSpawn))
{
return;
}
if (_actionData is ItemActionSpawnEntity.ItemActionDataSpawnEntity data
&& data.state == ItemActionSpawnEntity.ItemActionDataSpawnEntity.State.End)
{
__result = false;
}
}
}
[HarmonyPatch(typeof(EntityFactory), "CreateEntity", new System.Type[] { typeof(int), typeof(Vector3), typeof(Vector3) })]
public static class Patch_EntityFactory_CreateEntity_PetOwnership
{
@@ -323,69 +628,57 @@ namespace NecromancerTome
{
owner.inventory.DecHoldingItem(1);
}
PetFollowPatch.Register(owner, __result);
IgnoreCollisionWithOwner(owner, __result);
ApplyGhostlyTransparency(__result);
// Первая попытка развести коллайдеры - здесь же, чтобы в удачном случае не ждать
// секунду до тика. Удачной она, скорее всего, НЕ БУДЕТ: модель питомца на этом шаге
// ещё не собрана и коллайдеров у него нет (разбор - в TryIgnoreCollisionWithOwner).
// Поэтому результат передаётся в Register, и тик повторяет попытку до успеха.
bool collisionIgnored = PetFollowPatch.TryIgnoreCollisionWithOwner(owner, __result);
PetFollowPatch.Register(owner, __result, petInfo.UsesFollowTask, collisionIgnored, petInfo.LitEyes,
petInfo.FlyingHomeRadius);
AttachFollowTask(owner, __result, petInfo);
// Полупрозрачность больше НЕ делается здесь. Бывший ApplyGhostlyTransparency ставил
// альфу в material.color и не работал ни разу: шейдеры моделей мобов альфу не
// поддерживают (на Рое это выяснилось ещё 28.08 - он только засорял лог). Призрачный
// вид всем питомцам теперь даёт GhostTraderPatch.cs - та же система, что у торговцев,
// с клонированием материала, обесцвечиванием и подбором blend-шейдера.
Debug.Log("[NecromancerTome] SummonPatch: owner=" + owner.entityId + " now owns pet " + __result.entityId);
}
/// <summary>User request 2026-08-28 ("нематериальными") - makes the pet pass through its
/// own owner specifically, via Physics.IgnoreCollision on every collider pair between the
/// two, rather than stripping the pet's PhysicsBody entirely the way vanilla's own
/// animalInsectSwarm does for its "no physics body at all" look (confirmed by checking
/// entityclasses.xml). That works for a flying swarm; a ground-walking dog with no
/// collider at all would fall through the terrain. This keeps it solid against ground and
/// zombies - just not its owner - which also directly closes the last piece of the
/// spawn-launch bug documented in items.xml/PetFollowPatch.cs (nothing left to shove the
/// player if the two colliders can't touch in the first place).</summary>
public static void IgnoreCollisionWithOwner(EntityPlayer owner, Entity pet)
{
Collider[] ownerColliders = owner.GetComponentsInChildren<Collider>();
Collider[] petColliders = pet.GetComponentsInChildren<Collider>();
foreach (Collider oc in ownerColliders)
{
if (oc == null)
{
continue;
}
foreach (Collider pc in petColliders)
{
if (pc == null)
{
continue;
}
Physics.IgnoreCollision(oc, pc, true);
}
}
}
/// <summary>User request 2026-08-28 ("слегка прозрачными") - best effort only. Directly
/// sets renderer.material.color's alpha, the same technique ItemActionSpawnTurret uses for
/// its own placement-preview tint (confirmed by decompiling it), but that only visibly
/// shows up if the model's actual shader supports alpha blending - most opaque mob
/// shaders in this game don't, and there's no reliable XML/reflection-only way to swap a
/// live renderer's shader to a transparent variant without risking breaking how it's lit.
/// <summary>Вешает питомцу собственную задачу следования - указание 2026-09-18, пока
/// только Духу крысы (PetInfo.UsesFollowTask).
///
/// BUG FIXED 2026-08-28: the Insect Swarm's renderers use a particle shader
/// ("Game Particles/surfaceShader_masked_particleEnhanced") that has no "_Color" property
/// at all - setting .color on it doesn't throw, but Unity logs "doesn't have a color
/// property '_Color'" on every single access, once per renderer per spawn (confirmed in
/// output_log - this is what the user saw as "an error about colors"). HasProperty check
/// added so this silently skips any renderer whose shader doesn't support it instead of
/// spamming the log - the visual effect was never going to work on those anyway.</summary>
public static void ApplyGhostlyTransparency(Entity pet)
/// ЧЕРЕЗ КОД, А НЕ ЧЕРЕЗ XML - см. шапку PetFollowTask.cs: XML-путь существует, но упирается
/// в Type.GetType с именем сборки, а этот не упирается ни во что и заодно позволяет отдать
/// владельца прямо в поле задачи, без поиска по ownedEntities на каждом тике.
///
/// Приоритет 3: у крысы в entityclasses.xml объявлены AITask-1 (ApproachAndAttackTarget) и
/// AITask-2 (Look), а AITask-3 пустой - то есть следование встаёт ровно туда, где в XML
/// кончился список, и ниже погони за целью. Разводить их приоритетом при этом всё равно
/// недостаточно: задача следования сама отказывается работать, пока у питомца есть цель
/// (NecroFollowOwnerTask.CanExecute), и несёт те же MutexBits=3, что ванильный
/// EAIApproachSpot.</summary>
public static void AttachFollowTask(EntityPlayer owner, Entity pet, Patch_ItemActionSpawnEntity_Spawn_PetLimit.PetInfo petInfo)
{
Renderer[] renderers = pet.GetComponentsInChildren<Renderer>();
foreach (Renderer renderer in renderers)
if (!petInfo.UsesFollowTask)
{
if (renderer == null || renderer.material == null || !renderer.material.HasProperty("_Color"))
{
continue;
}
Color color = renderer.material.color;
color.a = 0.55f;
renderer.material.color = color;
return;
}
EntityAlive alive = pet as EntityAlive;
if (alive == null || alive.aiManager == null || alive.aiManager.tasks == null)
{
Debug.LogWarning("[NecromancerTome] SummonPatch: pet " + pet.entityId +
" has no aiManager - follow task NOT attached");
return;
}
NecroFollowOwnerTask task = new NecroFollowOwnerTask { OwnerEntityId = owner.entityId };
if (petInfo.FollowDistance > 0f)
{
task.SlotDistance = petInfo.FollowDistance;
}
task.Init(alive);
alive.aiManager.tasks.AddTask(3, task);
Debug.Log("[NecromancerTome] SummonPatch: follow task attached to pet " + pet.entityId +
" (owner " + owner.entityId + ")");
}
}
}
+10
View File
@@ -67,6 +67,16 @@ namespace NecromancerTome
public static readonly Dictionary<string, VultureBasedPetInfo> SpeciesByName = new Dictionary<string, VultureBasedPetInfo>
{
{ "necroInsectSwarm", new VultureBasedPetInfo { SkipAlreadyCharmedZombies = true } },
// Зомбогриф вернулся сюда 2026-09-18 вместе с откатом на летающую ветку. Он снова
// EntityVulture, то есть снова целится в игрока захардкоженным C#-кодом, и без этой
// строки кидался бы на хозяина. SkipAlreadyCharmedZombies=false: подчинять он не
// умеет, значит и обходить подчинённых ему незачем - пусть добивает.
//
// Честно: 29.08 этот же перехват Грифа НЕ СПАС - он просто летал и никого не
// трогал. Тогда причину искать не стали и ушли на наземную ветку. Сейчас мы вернулись
// к тому же месту, и если он снова будет безучастно кружить - копать надо здесь,
// в том, доходит ли до SetAttackTarget хоть что-нибудь.
{ "necroZombieGriffin", new VultureBasedPetInfo { SkipAlreadyCharmedZombies = false } },
};
public static readonly Dictionary<int, VultureBasedPetInfo> cachedClassIds = new Dictionary<int, VultureBasedPetInfo>();
+2 -2
View File
@@ -2,8 +2,8 @@
<xml>
<Name value="NecromancerTome" />
<DisplayName value="Necromancer's Tome" />
<Description value="A dark necromancy progression for 7 Days to Die 3.2: a kill-count-driven skill tree with cursed weapons, charm/deviation magic, summonable undead pets, base-defence wards, and a story-ending Black Portal ritual. Fully localized into 13 languages. Single-player; requires EAC off." />
<Description value="A dark necromancy progression for 7 Days to Die 3.2: a kill-count-driven skill tree with cursed weapons, charm/deviation magic, undead companions that keep station with you and attack what you point at, base-defence wards, and a story-ending Black Portal ritual. Fully localized into 13 languages. Single-player; requires EAC off." />
<Author value="Alex Cube" />
<Version value="1.3.0" />
<Version value="1.4.0" />
<Website value="https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/" />
</xml>
+16
View File
@@ -0,0 +1,16 @@
FIXED: the Necromancy level was rolling back on every save. The game stores a skill's level in a single byte, so anything above 255 was cut back by 256 - recipes you had already unlocked could lock themselves again, and the tiers at 500, 2000, 3000 and 5000 could never be reached at all.
Necromancy now levels once per 20 zombies, up to level 250 - the same 5000 zombies to the top, counted in a number the save can actually hold.
Old saves repair themselves on load. The count of zombies you put to rest was never damaged, and the level is now recomputed from it, so anything the rollback had stolen comes back the first time you load.
NEW: the skull in the status bar now shows your Necromancy level instead of the raw kill count.
NEW: a purple bar above the toolbelt shows the progress inside the level - it fills over 20 zombies, the level goes up, the bar resets.
FIXED: charmed zombies no longer fight each other. They treat each other as their own side and go for the next real enemy instead, and charming one of two zombies already fighting breaks the fight off. Ordinary zombies still attack the charmed ones - that is the point of them.
FIXED: kill credit now follows the game's own decision about who earned a kill. Bleeding, fire, traps, your own summoned undead and zombie animals all count. Thanks to Derizor and Savion for the reports - you were exactly right about the cause.
The Necromancer's Knife scales off that same counter, so it had been quietly weaker than intended too, and it is not any more.
Not fixed yet: the dagger's model still does not match its icon (thanks Scarlettdruid) - it needs a hand-made 3D prefab, which is the next thing to learn.
CHANGED: Tears of the Dead now unlocks at 20 zombies instead of 30, sharing that step with the Spatial Bracelet and the Blood Sphere. Thirst is a first-days problem, so the threshold moved down rather than up.
Every other threshold is unchanged in zombies: food at 60, Grave's Repose at 100, Dark Sense at 300, Dead Man's Grip at 1400, Dead Storm at 1700 - levels 3, 5, 15, 70 and 85.
The skill-up toast now arrives once per 20 zombies rather than on every kill.
COMING FROM 1.0.1: the Spatial Bracelet runs on a charge - the Blood Sphere is spent by pulling blocks and crumbles when empty, while the Blood Stone from the Apprentice tier takes the same slot and is never spent.
COMING FROM 1.0.1: the bracelet pulls blocks straight into the vault, with the world draining to black and white while you wait.
COMING FROM 1.0.1: a challenges tab of its own, traders rendered as ghosts, and an audible cry of pain when crafting Necromancer's Blood - it always cost 90% of your health, it was just silent about it.
Single-player. The mod ships Harmony libraries, so EasyAntiCheat must be off.
+3 -3
View File
@@ -23,7 +23,7 @@ To uninstall, delete the folder. The mod adds items and one block, so a save tha
[*][b]Adept[/b] (from the start, level 0) - Spirit Stone, Necromancer's Knife, Blue Portal Stone, Pyramid of Spirits
[*][b]Adept[/b] (20 / 60 / 100 / 300, levels 1 / 3 / 5 / 15) - Spatial Bracelet and Blood Sphere, then four knife mods: Tears of the Dead, Scavenger's Feast, Grave's Repose, Dark Sense
[*][b]Journeyman[/b] (500 / 1400 / 1700) - Scroll of Deviation, knife mods Dead Man's Grip and Dead Storm
[*][b]Apprentice[/b] (2000) - Summon Zombie Dog, Beetles of the Lord, Summon Zombie Griffin, Blood Stone
[*][b]Apprentice[/b] (2000) - Summon Zombie Dog, Pharaohs Swarm, Summon Grave Vulture, Blood Stone
[*][b]Necromancer[/b] (3000) - Summon Zombie Bear, Summon Zombie Wolf, Banshee's Scroll
[*][b]Master[/b] (5000) - Black Portal Stone
[/list]
@@ -32,7 +32,7 @@ To uninstall, delete the folder. The mod adds items and one block, so a save tha
[b]A blade that grows with you.[/b] The [b]Necromancer's Knife[/b] scales its damage with your Necromancy level - nearly useless in unskilled hands, lethal once you are levelled. It heals you for half the damage dealt and marks the wounded zombie as a Victim, guaranteeing a bag of necromantic ingredients on its death. [b]Six mods fit this knife and nothing else[/b]: water and food drawn straight out of corpses, protection from heat and cold, slowed victims, an area power-attack storm bought with your own health, and every nearby zombie marked on your compass and map.
[b]Undead that fight for you.[/b] Summoning books raise a [b]zombie dog, bear, wolf or griffin[/b] - permanent companions, one of each kind at a time, recalled into the book with a power attack. [b]Beetles of the Lord[/b] release a swarm that scatters wide and stings zombies onto your side. The [b]Banshee's Scroll[/b] screams up a small horde that is [i]not[/i] on your side at all - use it on purpose.
[b]Undead that fight for you.[/b] Summoning books raise a [b]zombie dog, bear, wolf or griffin[/b] - permanent companions, one of each kind at a time, recalled into the book with a power attack. [b]Pharaohs Swarm[/b] release a swarm that scatters wide and stings zombies onto your side. The [b]Banshee's Scroll[/b] screams up a small horde that is [i]not[/i] on your side at all - use it on purpose.
[b]Base defence that converts instead of killing.[/b] The [b]Pyramid of Spirits[/b] is a deployable block. While you stand in its radius it charms any uncharmed zombie nearby on its own and sets it alight with cold purple flame, turning it against the rest instead of your walls. Its block menu toggles the effect and shows the radius.
@@ -62,4 +62,4 @@ Thanks to [b]The Fun Pimps[/b] for a game that survives this much rewriting, and
Source code, full change history and issue tracker: [url=https://git.08h.ru/alex/necromants-tome-7d2d-3-2]git.08h.ru/alex/necromants-tome-7d2d-3-2[/url]
Mod page on my site: [url=https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/]alexcube.ru[/url]
My YouTube channel: [url=https://www.youtube.com/@alexcube]@alexcube[/url]
My YouTube channel: [url=https://www.youtube.com/@alex_cube]@alex_cube[/url]
+1 -1
View File
@@ -41,4 +41,4 @@ Unchanged from 1.0: [b]7 Days to Die 3.2[/b], [b]EasyAntiCheat off[/b] (the mod
Source code and full change history: [url=https://git.08h.ru/alex/necromants-tome-7d2d-3-2]git.08h.ru/alex/necromants-tome-7d2d-3-2[/url]
Mod page on my site: [url=https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/]alexcube.ru[/url]
My YouTube channel: [url=https://www.youtube.com/@alexcube]@alexcube[/url]
My YouTube channel: [url=https://www.youtube.com/@alex_cube]@alex_cube[/url]
-47
View File
@@ -1,47 +0,0 @@
[size=5][b]Necromancer's Tome 1.3.0[/b][/size]
Drop-in replacement: delete the old [b]NecromancerTome[/b] folder and unpack this one in its place. [b]Your save is fine[/b] - and if you played an earlier version, this release quietly repairs a number it had been breaking. Nothing to do by hand.
[size=5][b]Fixed: the Necromancy level was silently rolling back[/b][/size]
The skill used to gain one level per zombie, up to 5000. The game, however, stores a skill's level in a single byte, so everything above 255 was cut back by 256 every time the game saved. From the player's side it looked like recipes closing on their own: the skill panel would put the lock back on a knife mod you had already unlocked. Worse, it made the whole upper half of the tree unreachable - Journeyman at 500, Apprentice at 2000, Necromancer at 3000, Master at 5000 were numbers you could never actually hold.
[b]Necromancy now scales at 20 zombies per level, up to level 250[/b] - the same 5000 zombies to the top, counted in a number the save can hold.
[b]Your progress is not reset and not lost.[/b] The count of zombies you have put to rest was never damaged - only the level derived from it was, and that level is now recomputed from the count when you load, including on saves broken by the old version. If the wrap had stolen an unlock from you, you get it back on the first load.
[size=5][b]New: two indicators for the skill[/b][/size]
[list]
[*][b]The skull in the status bar[/b] now shows your Necromancy level. It used to show the raw kill count.
[*][b]A purple bar above the toolbelt[/b], next to the experience bar, shows the progress inside the current level: it fills over 20 zombies, the level goes up, the bar resets.
[/list]
[size=5][b]Fixed: charmed zombies fought each other[/b][/size]
A zombie under deviation is told to attack zombies - and another charmed zombie was, as far as the AI was concerned, a perfectly valid target. The more of them you charmed, the more of your retinue settled scores among themselves instead of fighting for you. [b]Charmed zombies now treat each other as their own side[/b] and go for the next real enemy instead. Charming one of two zombies already fighting also breaks the fight off, instead of it continuing as revenge.
This is deliberately one-sided: ordinary zombies still attack the charmed ones. That is the point of them.
[size=5][b]Changed[/b][/size]
[list]
[*][b]Tears of the Dead[/b] (water from corpses) now unlocks at [b]20 zombies[/b] instead of 30 - it shares that step with the Spatial Bracelet and the Blood Sphere. Thirst is a first-days problem, so the threshold moved down, not up.
[*]Every other threshold is unchanged in zombies: food at 60, Grave's Repose at 100, Dark Sense at 300, Dead Man's Grip at 1400, Dead Storm at 1700. In levels they now read 3, 5, 15, 70 and 85.
[*]The skill-up toast now arrives once per 20 zombies rather than on every kill.
[/list]
[size=5][b]Also in this release, if you are coming from 1.0.1[/b][/size]
[list]
[*][b]The Spatial Bracelet runs on a charge.[/b] Pulling blocks into the vault draws on what sits in its mod slot: the [b]Blood Sphere[/b] spends a point of durability per second of the pull and crumbles when empty; the [b]Blood Stone[/b], from the Apprentice tier, takes the same slot and is never spent. With an empty slot, the bracelet's regular attack refuses.
[*][b]The kill count now counts what it should.[/b] It used to rise only from kills by your own hand and only from humanoid zombies - zombie dogs, bears, boars and vultures never counted, and neither did traps, fire or bleeding. Now anything the game gives you XP for counts. The Necromancer's Knife scales off that same counter, so the knife stopped coming up short as well.
[*][b]The Spatial Bracelet takes blocks straight into the vault[/b] - hold its regular attack on a block and it is pulled in after ten seconds, plus one more per block of distance, with the world draining to black and white while you wait.
[*][b]A challenges tab of its own[/b], with four challenges and vanilla XP rewards.
[*][b]Traders look like ghosts[/b] - black and white, half-transparent, matte.
[*][b]Crafting Necromancer's Blood now hurts audibly.[/b] It always cost 90% of your health; it was just silent about it.
[/list]
[size=5][b]Note[/b][/size]
Single-player. The mod ships Harmony libraries, so [b]EasyAntiCheat must be off[/b].
+1
View File
@@ -0,0 +1 @@
Fixes the Necromancy level rolling back on every save: it now rises once per 20 zombies, up to 250, and old saves repair themselves on load. Bleed, fire, trap and pet kills count. Charmed zombies no longer fight each other. Drop-in; your save is fine.
Binary file not shown.
Binary file not shown.
+55 -23
View File
@@ -7,7 +7,7 @@
- Страница мода: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
- Репозиторий: https://git.08h.ru/alex/necromants-tome-7d2d-3-2
- YouTube-канал автора: https://www.youtube.com/@alexcube
- YouTube-канал автора: https://www.youtube.com/@alex_cube
Мод для 7 Days to Die о пути от обычного выжившего до некроманта — с собственной веткой
прогрессии, тёмным оружием, призываемыми существами и сюжетной концовкой.
@@ -39,11 +39,13 @@ HUD за этим следят два индикатора: череп в ста
|---|---|---|---|
| Адепт | сразу | 0 | Камень духов, Нож некроманта, Синий портальный камень, Пирамида духов |
| Адепт (доп.) | 20 зомби | 1 | Пространственный браслет, Кровавая сфера, мод ножа «Слёзы мертвеца» |
| Адепт (доп.) | 60 / 100 / 300 зомби | 3 / 5 / 15 | Моды ножа: Пир падальщика, Могильный покой, Тёмное чутьё |
| Подмастерье | 500 зомби | 25 | Свиток девиации |
| Адепт (доп.) | 60 / 100 / 300 зомби | 3 / 5 / 15 | Свиток духа крысы; моды ножа: Пир падальщика, Могильный покой, Тёмное чутьё |
| Подмастерье | 500 зомби | 25 | Свиток девиации, Призыв могильного стервятника |
| Подмастерье (доп.) | 1300 зомби | 65 | Призыв зомбособаки |
| Подмастерье (доп.) | 1400 / 1700 зомби | 70 / 85 | Моды ножа: Хватка мертвеца, Мёртвая буря |
| Ученик | 2000 зомби | 100 | Призыв зомбособаки, Жуки Властелина, Призыв зомбогрифа, Кровавый камень |
| Некромант | 3000 зомби | 150 | Призыв зомбомедведя, Призыв зомбоволка, Свиток банши |
| Ученик | 2000 зомби | 100 | Призыв зомбомедведя, Рой фараона, Кровавый камень |
| Некромант | 3000 зомби | 150 | Свиток банши |
| Некромант (доп.) | 4000 зомби | 200 | Призыв зомбоволка |
| Мастер | 5000 зомби | 250 | Чёрный портальный камень |
## Арсенал
@@ -91,14 +93,27 @@ HUD за этим следят два индикатора: череп в ста
## Питомцы
Призывные книги поднимают союзных существ, которые дерутся с зомби, а не с игроком. Питомец не
«следует» за игроком в строгом смысле — просто бродит сам по себе, а если отойдёт дальше
32 блоков и в этот момент не занят боем, его телепортирует обратно к владельцу:
Призывные книги поднимают союзных существ, которые дерутся с зомби, а не с игроком. Все призванные
существа выглядят выцветшими призраками — прозрачность настраивается консольной командой
`necroghost`, той же, что и у призрачных торговцев.
- **Зомбособака**, **Зомбомедведь**, **Зомбоволк**, **Зомбогриф** — постоянные спутники. Можно
держать по одному экземпляру каждого вида одновременно; отзываются обратно в книгу силовой
атакой.
- **Жуки Властелина** — разовый свиток, выпускающий рой. Жуки сами разлетаются по широкому
- **Дух крысы** — первый доступный питомец (Некромантия 3). Он ведёт себя не так, как остальные:
сам не нападает никогда, держится справа-сзади от вас в двух блоках, а упёршись в препятствие,
проходит сквозь него — блоки он не грызёт вовсе. Пока крыса призвана, обычное применение свитка
становится приказом: она бросается на того, на кого наведён ваш прицел (кроме торговцев); если
цели под прицелом нет, появится надпись «Нет цели для атаки». Силовая атака отзывает её. Урон у
крысы почти никакой (5), но её укус делает три вещи: сильно замедляет, рвёт жилу (кровотечение,
которое не заживает, пока крыса грызёт) и оставляет **метку духа** — помеченный зомби полминуты
виден на вашем компасе и карте и получает на четверть больше урона от всего, чем бы вы его ни
били. Саму крысу видно на карте жёлтым. Обгрызая труп добитого зомби, она восстанавливает себе
здоровье. Если ударить саму крысу, она огрызнётся — но никогда на хозяина.
- **Зомбособака**, **Зомбомедведь**, **Зомбоволк**, **Могильный стервятник** — постоянные спутники, и ведут
себя они так же, как Дух крысы: идут за вами, сами ни на кого не нападают и ждут приказа. Разница
в силе: урон растёт от могильного стервятника к зомбоволку, и в отличие от крысы эти убивают быстро — пёс,
медведь и волк вдобавок могут отрывать зомби конечности. Их укус вызывает кровотечение, но метку
духа не ставит. Можно держать по одному экземпляру каждого вида
одновременно; отзываются обратно в книгу силовой атакой.
- **Рой фараона** — разовый свиток, выпускающий рой. Насекомые сами разлетаются по широкому
радиусу и жалят зомби; ужаленный переходит на вашу сторону, как от Камня духов. Рой нельзя
отозвать обратно, активен может быть только один. Расходуется при использовании.
- **Свиток банши** — одноразовый: при открытии вопит голосом банши и поднимает рядом с игроком
@@ -212,7 +227,7 @@ HUD за этим следят два индикатора: череп в ста
- Mod page: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
- Repository: https://git.08h.ru/alex/necromants-tome-7d2d-3-2
- The author's YouTube channel: https://www.youtube.com/@alexcube
- The author's YouTube channel: https://www.youtube.com/@alex_cube
A 7 Days to Die mod about the road from ordinary survivor to necromancer - with its own
progression tree, cursed weapons, summonable creatures and a story ending.
@@ -243,11 +258,13 @@ Five tiers, each opening part of the arsenal (the table gives both the zombie co
|---|---|---|---|
| Adept | from the start | 0 | Spirit Stone, Necromancer's Knife, Blue Portal Stone, Pyramid of Spirits |
| Adept (extra) | 20 zombies | 1 | Spatial Bracelet, Blood Sphere, the Tears of the Dead knife mod |
| Adept (extra) | 60 / 100 / 300 zombies | 3 / 5 / 15 | Knife mods: Scavenger's Feast, Grave's Repose, Dark Sense |
| Journeyman | 500 zombies | 25 | Scroll of Deviation |
| Adept (extra) | 60 / 100 / 300 zombies | 3 / 5 / 15 | Scroll of the Rat Spirit; knife mods: Scavenger's Feast, Grave's Repose, Dark Sense |
| Journeyman | 500 zombies | 25 | Scroll of Deviation, Summon Grave Vulture |
| Journeyman (extra) | 1300 zombies | 65 | Summon Zombie Dog |
| Journeyman (extra) | 1400 / 1700 zombies | 70 / 85 | Knife mods: Dead Man's Grip, Dead Storm |
| Apprentice | 2000 zombies | 100 | Summon Zombie Dog, Beetles of the Lord, Summon Zombie Griffin, Blood Stone |
| Necromancer | 3000 zombies | 150 | Summon Zombie Bear, Summon Zombie Wolf, Banshee's Scroll |
| Apprentice | 2000 zombies | 100 | Summon Zombie Bear, Pharaohs Swarm, Blood Stone |
| Necromancer | 3000 zombies | 150 | Banshee's Scroll |
| Necromancer (extra) | 4000 zombies | 200 | Summon Zombie Wolf |
| Master | 5000 zombies | 250 | Black Portal Stone |
## Arsenal
@@ -295,13 +312,28 @@ Five tiers, each opening part of the arsenal (the table gives both the zombie co
## Pets
Summoning books raise allied creatures that fight zombies rather than the player. A pet does not
"follow" in any strict sense - it wanders on its own, and if it strays further than 32 blocks
while not in combat, it is teleported back to its owner:
Summoning books raise allied creatures that fight zombies rather than the player. Every summoned
creature looks like a washed-out ghost - the transparency is tuned with the `necroghost` console
command, the same one the ghost traders use.
- **Zombie Dog**, **Zombie Bear**, **Zombie Wolf**, **Zombie Griffin** - permanent companions. One
of each kind can be kept at a time; a power attack recalls them into the book.
- **Beetles of the Lord** - a one-shot scroll releasing a swarm. The beetles scatter over a wide
- **Rat Spirit** - the first pet available (Necromancy 3), and the only one that behaves
deliberately rather than by inheritance: it never attacks on its own, keeps two blocks to your
right and just behind you, and where an obstacle stands it passes straight through - it never
gnaws blocks. While it is out, an ordinary use of the scroll becomes an order: the rat goes for
whatever your crosshair rests on (traders excepted); with nothing to attack there, you get "No
target to attack". A power attack recalls it. Its damage is next to nothing (5), but its bite
does three things: it slows heavily, it tears a vein (a bleed that will not close while the rat
keeps at it), and it leaves a **spirit mark** - for half a minute the marked zombie shows on
your compass and map and takes a quarter more damage from everything, whatever you hit it with.
The rat itself shows on the map in yellow. Gnawing on the corpse of a zombie it finished off
heals it back up. Hitting the rat makes it bite back - never at its owner, though.
- **Zombie Dog**, **Zombie Bear**, **Zombie Wolf**, **Grave Vulture** - permanent companions that
behave exactly like the Rat Spirit: they follow you, attack nobody on their own and wait for an
order. What differs is force - the damage climbs from the griffin up to the wolf, and unlike the rat
these kill quickly; the dog, the bear and the wolf can also tear limbs off. Their bite draws blood
but leaves no spirit mark. One of each kind can be kept
at a time; a power attack recalls them into the book.
- **Pharaohs Swarm** - a one-shot scroll releasing a swarm. They scatter over a wide
radius on their own and sting zombies; a stung zombie switches to your side just like with the
Spirit Stone. The swarm cannot be recalled and only one can be active. Consumed on use.
- **Banshee's Scroll** - single use: on opening it screams with a banshee's voice and raises a
+7 -7
View File
@@ -8,7 +8,7 @@
Отдельный навык <strong>«Некромантия»</strong> качается не за очки перков и не за книги, а по счётчику упокоенных зомби: двадцать упокоенных — один уровень, до 250-го (5000 зомби). Уровень показывает череп в статус-баре, а фиолетовая шкала рядом с полосой опыта — сколько набрано внутри уровня. Пять тиров, каждый порог открывает часть арсенала (в таблице — зомби, в скобках уровень):
<table><thead><tr><th>Тир</th><th>Порог</th><th>Что открывается</th></tr></thead><tbody><tr><td>Адепт</td><td>сразу (0)</td><td>Камень духов, Нож некроманта, Синий портальный камень, Пирамида духов</td></tr><tr><td>Адепт</td><td>20 (1)</td><td>Пространственный браслет, Кровавая сфера, мод ножа «Слёзы мертвеца»</td></tr><tr><td>Адепт</td><td>60 / 100 / 300 (3 / 5 / 15)</td><td>Моды ножа: Пир падальщика, Могильный покой, Тёмное чутьё</td></tr><tr><td>Подмастерье</td><td>500 / 1400 / 1700 (25 / 70 / 85)</td><td>Свиток девиации, моды ножа Хватка мертвеца и Мёртвая буря</td></tr><tr><td>Ученик</td><td>2000 (100)</td><td>Призыв зомбособаки, Жуки Властелина, Призыв зомбогрифа, Кровавый камень</td></tr><tr><td>Некромант</td><td>3000 (150)</td><td>Призыв зомбомедведя, Призыв зомбоволка, Свиток банши</td></tr><tr><td>Мастер</td><td>5000 (250)</td><td>Чёрный портальный камень</td></tr></tbody></table>
<table><thead><tr><th>Тир</th><th>Порог</th><th>Что открывается</th></tr></thead><tbody><tr><td>Адепт</td><td>сразу (0)</td><td>Камень духов, Нож некроманта, Синий портальный камень, Пирамида духов</td></tr><tr><td>Адепт</td><td>20 (1)</td><td>Пространственный браслет, Кровавая сфера, мод ножа «Слёзы мертвеца»</td></tr><tr><td>Адепт</td><td>60 / 100 / 300 (3 / 5 / 15)</td><td>Моды ножа: Пир падальщика, Могильный покой, Тёмное чутьё</td></tr><tr><td>Подмастерье</td><td>500 / 1400 / 1700 (25 / 70 / 85)</td><td>Свиток девиации, моды ножа Хватка мертвеца и Мёртвая буря</td></tr><tr><td>Ученик</td><td>2000 (100)</td><td>Призыв зомбособаки, Рой фараона, Призыв могильного стервятника, Кровавый камень</td></tr><tr><td>Некромант</td><td>3000 (150)</td><td>Призыв зомбомедведя, Призыв зомбоволка, Свиток банши</td></tr><tr><td>Мастер</td><td>5000 (250)</td><td>Чёрный портальный камень</td></tr></tbody></table>
<h2>Арсенал</h2>
@@ -31,8 +31,8 @@
Призывные книги поднимают союзников, которые дерутся с зомби, а не с вами. Питомец бродит сам по себе, а если отойдёт дальше 32 блоков и не занят боем — телепортируется обратно к владельцу.
<ul>
<li><strong>Зомбособака, зомбомедведь, зомбоволк, зомбогриф</strong> — постоянные спутники, по одному экземпляру каждого вида одновременно. Отзываются обратно в книгу силовой атакой.</li>
<li><strong>Жуки Властелина</strong> — разовый свиток с роем. Жуки разлетаются по широкому радиусу и жалят зомби; ужаленный переходит на вашу сторону, как от Камня духов. Рой не отзывается, активен может быть только один.</li>
<li><strong>Зомбособака, зомбомедведь, зомбоволк, могильный стервятник</strong> — постоянные спутники, по одному экземпляру каждого вида одновременно. Отзываются обратно в книгу силовой атакой.</li>
<li><strong>Рой фараона</strong> — разовый свиток с роем. Жуки разлетаются по широкому радиусу и жалят зомби; ужаленный переходит на вашу сторону, как от Камня духов. Рой не отзывается, активен может быть только один.</li>
<li><strong>Свиток банши</strong> — одноразовый: вопит голосом банши и поднимает рядом небольшую орду. Это <strong>не</strong> союзники — эти зомби так же опасны для вас, как любые другие.</li>
</ul>
@@ -69,7 +69,7 @@
<ul>
<li>Скачать на Nexus Mods: <a href="https://www.nexusmods.com/7daystodie/mods/12547" target="_blank" rel="noreferrer noopener">nexusmods.com/7daystodie/mods/12547</a></li>
<li>Исходники: <a href="https://git.08h.ru/alex/necromants-tome-7d2d-3-2" target="_blank" rel="noreferrer noopener">git.08h.ru/alex/necromants-tome-7d2d-3-2</a></li>
<li>YouTube-канал автора: <a href="https://www.youtube.com/@alexcube" target="_blank" rel="noreferrer noopener">@alexcube</a></li>
<li>YouTube-канал автора: <a href="https://www.youtube.com/@alex_cube" target="_blank" rel="noreferrer noopener">@alex_cube</a></li>
</ul>
<hr>
@@ -86,7 +86,7 @@ Necromancy here is an answer to a curse, not a side branch of crafting. Instead
A dedicated <strong>Necromancy</strong> skill levels not from perk points and not from books, but from your zombie kill count: twenty zombies put to rest make one level, up to level 250 (5000 zombies). The skull in the status bar shows the level, and the purple bar beside the experience bar shows how far you are into it. Five tiers, each threshold opening part of the arsenal (the table gives zombies, with the level in brackets):
<table><thead><tr><th>Tier</th><th>Threshold</th><th>What unlocks</th></tr></thead><tbody><tr><td>Adept</td><td>from the start (0)</td><td>Spirit Stone, Necromancer's Knife, Blue Portal Stone, Pyramid of Spirits</td></tr><tr><td>Adept</td><td>20 (1)</td><td>Spatial Bracelet, Blood Sphere, the Tears of the Dead knife mod</td></tr><tr><td>Adept</td><td>60 / 100 / 300 (3 / 5 / 15)</td><td>Knife mods: Scavenger's Feast, Grave's Repose, Dark Sense</td></tr><tr><td>Journeyman</td><td>500 / 1400 / 1700 (25 / 70 / 85)</td><td>Scroll of Deviation, knife mods Dead Man's Grip and Dead Storm</td></tr><tr><td>Apprentice</td><td>2000 (100)</td><td>Summon Zombie Dog, Beetles of the Lord, Summon Zombie Griffin, Blood Stone</td></tr><tr><td>Necromancer</td><td>3000 (150)</td><td>Summon Zombie Bear, Summon Zombie Wolf, Banshee's Scroll</td></tr><tr><td>Master</td><td>5000 (250)</td><td>Black Portal Stone</td></tr></tbody></table>
<table><thead><tr><th>Tier</th><th>Threshold</th><th>What unlocks</th></tr></thead><tbody><tr><td>Adept</td><td>from the start (0)</td><td>Spirit Stone, Necromancer's Knife, Blue Portal Stone, Pyramid of Spirits</td></tr><tr><td>Adept</td><td>20 (1)</td><td>Spatial Bracelet, Blood Sphere, the Tears of the Dead knife mod</td></tr><tr><td>Adept</td><td>60 / 100 / 300 (3 / 5 / 15)</td><td>Knife mods: Scavenger's Feast, Grave's Repose, Dark Sense</td></tr><tr><td>Journeyman</td><td>500 / 1400 / 1700 (25 / 70 / 85)</td><td>Scroll of Deviation, knife mods Dead Man's Grip and Dead Storm</td></tr><tr><td>Apprentice</td><td>2000 (100)</td><td>Summon Zombie Dog, Pharaohs Swarm, Summon Grave Vulture, Blood Stone</td></tr><tr><td>Necromancer</td><td>3000 (150)</td><td>Summon Zombie Bear, Summon Zombie Wolf, Banshee's Scroll</td></tr><tr><td>Master</td><td>5000 (250)</td><td>Black Portal Stone</td></tr></tbody></table>
<h3>Arsenal</h3>
@@ -110,7 +110,7 @@ Summoning books raise allies that fight zombies, not you. A pet wanders on its o
<ul>
<li><strong>Zombie dog, bear, wolf and griffin</strong> — permanent companions, one of each kind at a time. A power attack recalls them into the book.</li>
<li><strong>Beetles of the Lord</strong> — a one-shot scroll releasing a swarm. The beetles scatter over a wide radius and sting zombies; a stung zombie switches to your side just like with the Spirit Stone. The swarm cannot be recalled, and only one can be active.</li>
<li><strong>Pharaohs Swarm</strong> — a one-shot scroll releasing a swarm. They scatter over a wide radius and sting zombies; a stung zombie switches to your side just like with the Spirit Stone. The swarm cannot be recalled, and only one can be active.</li>
<li><strong>Banshee's Scroll</strong> — single use: it screams with a banshee's voice and raises a small horde next to you. These are <strong>not</strong> allies — they are as dangerous to you as any other zombies.</li>
</ul>
@@ -147,5 +147,5 @@ Built for single-player. In multiplayer the vanilla pause does not apply, so the
<ul>
<li>Download on Nexus Mods: <a href="https://www.nexusmods.com/7daystodie/mods/12547" target="_blank" rel="noreferrer noopener">nexusmods.com/7daystodie/mods/12547</a></li>
<li>Source: <a href="https://git.08h.ru/alex/necromants-tome-7d2d-3-2" target="_blank" rel="noreferrer noopener">git.08h.ru/alex/necromants-tome-7d2d-3-2</a></li>
<li>The author's YouTube channel: <a href="https://www.youtube.com/@alexcube" target="_blank" rel="noreferrer noopener">@alexcube</a></li>
<li>The author's YouTube channel: <a href="https://www.youtube.com/@alex_cube" target="_blank" rel="noreferrer noopener">@alex_cube</a></li>
</ul>