6 Commits
Author SHA1 Message Date
AlexCubeandClaude Opus 5.5 f4470b784f 1.4.2: проверка версии игры и хранилище, которое не стирается
Повод: 1.4.1 на игре 3.3.0 experimental не смогла прочитать Пространственное
хранилище (Bag переписан) и при сохранении записала пустой маркер - вещи
пропали навсегда.

GameVersionGuard: версия из Constants.cVersionInformation (не из const
cVersionMinor - та вшивается в DLL), поддерживается Major 3 и Minor/10 == 2,
то есть любая 3.2.x. На другой версии в главном меню окно «мод несовместим,
обновите» с единственной кнопкой «Выйти из игры» (решение пользователя), а
вместо PatchAll ставятся по отдельности только окно и четыре патча
сохранения хранилища. Всё, чем пользуется защита, сверено с декомпиляцией 3.3.

Хранилище: непрочитанные байты (чужая версия или упавший разбор) проходят
Read -> ToPlayer -> FromPlayer -> Write нетронутыми, ключ - entityId, сброс
при открытии главного меню (последнее сохранение игрока делается раньше,
внутри SaveAndCleanupWorld - проверено по коду игры). Пока байты не
прочитаны, хранилище не открывается и не пополняется, с подсказкой.

Проверено в игре на 3.2 через отладочные файлы FORCE_VAULT_UNREAD и
FORCE_VERSION_GUARD (в .gitignore, в архив не попадают): 603 байта
хранилища прошли сохранение и прочитались обратно совпадающими с исходными
(26 ячеек, 24 предмета); окно появляется, Esc его не закрывает, кнопка
закрывает игру. На настоящей 3.3 не проверено.

Localization: 4 новых ключа на 13 языках, всего 165; README - статус и
число ключей (было 149).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 00:09:01 +03:00
AlexCubeandClaude Opus 5.5 77b56be9ae 1.4.1: утечка видеопамяти от трупов «Жертвы», труп лежит 5 минут
Было: CorpseLootPatch перекрашивал труп через renderer.materials и Tint(),
и ничего за собой не убирал. Копии материалов Unity с объектом не удаляет,
перекрашенная текстура была несжатой RGBA32 (16-64 МБ) и читаемой (копия
ещё и в RAM), кеш только рос. Лог игры: Texture.currentTextureMemory
1,2 -> 7,4 ГБ за два часа при desired ~3,5 ГБ, FPS 60 -> 11. Те же жалобы
на Nexus (sanovskiy, Kriloko, Megaladon43).

Стало: каждый перекрашенный труп записан (рендереры, материалы, текстуры),
тик раз в секунду удаляет всё за трупом, которого нет в мире, текстура
удаляется со счётчиком пользователей. Текстура не больше 1024, DXT,
makeNoLongerReadable. Проверено в игре: обыск и истечение срока доводят
счёт до «трупов 0, текстур 0».

Необысканный труп лежит 300 с вместо 3600 (указание пользователя).
buffNecroZombieKillTrackerDisplay: update_rate .1 -> 1 (отзыв zomb1);
урон ножа растёт после убийства, проверено.

Документы: версия 1.4.1, README (статус + несовместимость с 3.3
experimental), описания Nexus и сайта (пять минут вместо часа),
NEXUS_CHANGELOG/FILE_SUMMARY_1.4.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:36:51 +03:00
AlexCubeandClaude Opus 5 67dd410358 1.4.0: кровати и нож проверены в игре - снята пометка «не проверено»
Пользователь проверил: поднятая и заново поставленная кровать становится точкой
возрождения, финальный вид ножа (гарда над кулаком, светящиеся руны) и иконки на месте.
README (обе половины): кровати перенесены в проверенное, во врезке из новшеств 1.4.0
осталось только поведение питомцев в кровавую луну и на транспорте. Changelog: «should
become ... Not tested in game yet» -> утверждение.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 00:01:58 +03:00
AlexCubeandClaude Opus 5 01aa054177 1.4.0: своя модель Ножа некроманта, иконки ножа и Свитка духа крысы, документы релиза
НОЖ ПОЛУЧИЛ СВОЮ ФОРМУ. До сих пор в бандле necroknife стояла ванильная геометрия
костяной заточки с нашей текстурой - на Nexus это заметили (Scarlettdruid: нож не похож
на свою иконку). Форма сделана в Blender поверх ванильного меша: головка и шейка бедра
срезаны, срез закрыт; лезвие выпрямлено в ампутационный нож с клиновым сечением;
костяная гарда стоит над кулаком (место выбрано по игре: девятый виток обмотки от
прежней гарды); рукоять обмотана верёвкой. Весь нож повёрнут на 180° вокруг оси -
режущая кромка смотрела не в ту сторону, "получается, что я обухом режу". Поворот, а
не зеркало: так руны на клинке не отражаются.

Текстура осталась от ножа некроманта. Верёвка нарисована в свободном углу её же атласа
(второй материал импорт в Unity отбрасывает), старые руны заменены светящимися
рублеными знаками, одинаковыми на обеих сторонах клинка. Меш 2823 вершины / 4360
треугольников. Сама цепочка сборки (скрипты Blender и сборки текстур) - в _private,
в репозиторий не входит.

ИКОНКИ нарисованы пользователем: NecromancersKnife.png - под новую модель, RatBook.png -
первая своя у Свитка духа крысы. У свитка снят CustomIconTint: он множится на иконку
и гасил бы нарисованные цвета, он был нужен только перекрашенной волчьей иконке.

ДОКУМЕНТЫ. README (обе половины), SITE_DESCRIPTION.html, NEXUS_DESCRIPTION.bbcode:
версия 1.4.0, новый вид ножа, добыча Жертвы в самом трупе (краснеет, лежит час, при
обыске оставляет мешок - проверено в игре), подбор кроватей-точек возрождения (в игре
НЕ проверено, так и написано). В "Статусе" - абзац 1.4.0 и напоминание о шкале 1.3.0:
на Nexus 21.09 игрок принял уровень "1 из 250" за застывший счётчик и решил, что
прогресс сбросился - уровень растёт раз в 20 убийств, а с 1.0.1 он пересчитывается и
становится в 20 раз меньше.

Тексты для Nexus в принятых сайтом форматах: NEXUS_FILE_SUMMARY_1.4.0.txt (254 знака
из 255) и NEXUS_CHANGELOG_1.4.0.txt (20 строк = 20 пунктов, без номера версии в начале).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 23:57:18 +03:00
AlexCubeandClaude Opus 5 98ddcd7bce Добыча «Жертвы» переехала в сам труп: он красится, лежит час и обыскивается
Указание: «зомби после упокоения оставляет свой труп. Можем ли мы подменять
этот труп точно таким же, но с другой текстурой и с функцией мешка жертвы?»

ПОДМЕНЯТЬ НЕ ПРИШЛОСЬ НИЧЕГО. Труп - не отдельная модель, а та же скелетная
модель зомби в позе смерти, так что «такой же, но другой текстуры» делается
перекраской уже лежащего трупа. Ни нового префаба, ни новой сущности.

Три части, каждая на своём ванильном механизме:

  НЕ ИСЧЕЗАЕТ. EntityAlive.timeStayAfterDeath - обычное публичное поле в тиках,
  у зомби 30 секунд, у ванильного мешка 3600. Пишем второе.

  ОБЫСКИВАЕТСЯ. У сущностей своя система команд, отдельная от блочной:
  InitLocalActivationCommands / AllowActivationCommand / OnEntityActivated.
  Объявляем ту же команду "search", что и ванильный мешок (EntityItem), поэтому
  подпись достаётся готовой и локализованной. Список команд кешируется при
  первом обращении - пока зомби жив, - поэтому на смерти он обнуляется.

  ВНУТРИ ЛУТ. Обход сознательный: хранилище живёт внутри EntityLootContainer
  вместе со своим сейвом и сетевой синхронизацией, и приделывать это трупу
  значит трогать сейвы. Поэтому обыск ПРЕВРАЩАЕТ труп в мешок «Жертва».

ПЕРЕКРАСКА ЧЕРЕЗ ТЕКСТУРУ, А НЕ ЧЕРЕЗ _Color. Первая версия не работала вовсе:
у шейдеров Game/Character и Game/Animal/Fur свойства цвета НЕТ (видно в пробе,
которую печатает GhostTraderPatch: "tint property: <none>"). Красится сама
текстура - прогоном через RenderTexture, как обесцвечиваются торговцы, только
вместо сведения к яркости умножение на цвет. Результат кешируется по исходной
текстуре. Если модель на кадре смерти не собрана, труп встаёт в очередь и
докрашивается односекундным тиком.

ПОДСКАЗКА ПРИ НАВЕДЕНИИ - отдельный механизм, и сначала я настроил не тот.
Иконка EntityActivationCommand рисуется в РАДИАЛЬНОМ меню, то есть на зажатии.
А то, что видно при простом наведении, приходит из Entity.GetActivationText(),
и у EntityAlive он для существа возвращает пустую строку. Теперь возвращает то
же, что ванильный лутаемый объект: строку lootTooltipNew со значком клавиши из
разметки [action:...]. Имя берётся у зомби, так что читается «обыскать Арлин».

ОБЫСК НЕ МГНОВЕННЫЙ: три секунды с круговым индикатором (XUiC_Timer, тот же,
что при снятии верстака). Поле activateTime у команды не подошло - по нему игра
дёргает игровое событие из entity_class, а не наш код.

Обычный мешок это не отменяет: бросок вероятности остался как был, и теперь он
печатается в лог целиком (шанс против броска) - чтобы «не повезло» отличалось
от «сломано» без гадания. У ванильного зомби шанс 4%, то есть ноль мешков на
два десятка убийств - исход с вероятностью около 40%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 23:23:15 +03:00
AlexCubeandClaude Opus 5 b6a892cadd Подбор кроватей: включены те, что становятся точкой возрождения
Исправление промаха по цели. Кровати класса SleepingBag (bed02*, bunkBed*) были
исключены из подбора НАМЕРЕННО, с формулировкой "это respawn-anchor блоки, не
декорация, трогать не просил" - и подбирались только декоративные
bedMadeNoFrame*/bedMessyNoFrame*.

Пользователь объяснил, что смысл подбора кроватей ровно обратный: "поглощённые
блоки сохраняют состояние, которое было до поглощения. Кровати до поглощения не
являются точкой возрождения. Точкой возрождения они могли бы стать при подборе".
То есть мы подбирали ровно те кровати, которые не нужны, и не подбирали те, ради
которых всё затевалось.

Проверено декомпиляцией BlockSleepingBag: PlaceBlock ставит игроку
selectedSpawnPointKey и запоминает BedrollPos - поставленная своими руками
кровать действительно становится точкой возрождения, а стоящая в POI нет.
Значит подбор и поглощение браслетом решают РАЗНЫЕ задачи.

Новых патчей не понадобилось, и это проверялось до правки: BlockSleepingBag
переопределяет только CanPlaceBlockAt, PlaceBlock, GetActivationText и
ЧЕТЫРЁХаргументный OnBlockActivated. Наши три патча висят на
HasBlockActivationCommands, GetBlockActivationCommands и ПЯТИаргументном
OnBlockActivated - ни одного из них он не перекрывает. С BlockCompositeTileEntity
(кулеры, коробки) в своё время так не вышло и понадобился отдельный набор.

Добавлены два префикса: bed02 и bunkBed. Полсотни цветовых вариантов наследуют
Class="SleepingBag" через Extends и покрываются ими же. bedroll НЕ включён:
собственный предмет игрока, ставится и снимается штатно, а bedrollPOI от него не
наследуется и точкой возрождения не является.

В ИГРЕ НЕ ПРОВЕРЕНО. Смотреть надо одно: становится ли поднятая и заново
поставленная кровать точкой возрождения на самом деле.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 21:21:36 +03:00
29 changed files with 1134 additions and 106 deletions
+2
View File
@@ -6,3 +6,5 @@ TEPersistenceSrc/bin/
*.user *.user
BACKLOG.md BACKLOG.md
FINAL_TEXT.md FINAL_TEXT.md
FORCE_VERSION_GUARD
FORCE_VAULT_UNREAD
+4
View File
@@ -160,3 +160,7 @@ buffNecroRatMarkName,buffs,Buff,,,,Spirit Mark,,Geistermal,Marca del espíritu,M
buffNecroRatMarkDesc,buffs,Buff,,,,"Marked by a rat spirit: visible on its master’s compass and map, and rotting - every wound cuts deeper than it should.",,"Von einem Rattengeist gezeichnet: auf Kompass und Karte seines Herrn sichtbar - und faulend, sodass jede Wunde tiefer geht, als sie sollte.","Marcado por un espíritu de rata: visible en la brújula y el mapa de su amo, y pudriéndose - cada herida cala más hondo de lo que debería.","Marqué par un esprit de rat : visible sur la boussole et la carte de son maître, et en putréfaction - chaque blessure creuse plus profond qu’elle ne devrait.","Marchiato da uno spirito di ratto: visibile sulla bussola e sulla mappa del suo padrone, e in putrefazione - ogni ferita affonda più del dovuto.",ネズミの霊に印をつけられた。その主のコンパスと地図に映り、腐ってゆく - どんな傷も本来より深く刻まれる。,"쥐의 영혼에게 표식이 찍혔습니다. 그 주인의 나침반과 지도에 드러나며, 썩어들어 모든 상처가 본래보다 깊게 패입니다.","Naznaczony przez ducha szczura: widoczny na kompasie i mapie jego pana, i gnijący - każda rana sięga głębiej, niż powinna.","Marcado por um espírito de rato: visível na bússola e no mapa de seu mestre, e apodrecendo - cada ferida penetra mais fundo do que deveria.","Помечен духом крысы: виден на компасе и карте его хозяина и гниёт — каждая рана заходит глубже, чем должна.",Bir sıçan ruhu tarafından işaretlendi: efendisinin pusulasında ve haritasında görünür ve çürür - her yara olması gerekenden derine iner.,被鼠灵标记:显现在其主人的罗盘与地图上,并且开始腐烂——任何伤口都比应有的更深。,被鼠靈標記:顯現在其主人的羅盤與地圖上,並且開始腐爛——任何傷口都比應有的更深。 buffNecroRatMarkDesc,buffs,Buff,,,,"Marked by a rat spirit: visible on its master’s compass and map, and rotting - every wound cuts deeper than it should.",,"Von einem Rattengeist gezeichnet: auf Kompass und Karte seines Herrn sichtbar - und faulend, sodass jede Wunde tiefer geht, als sie sollte.","Marcado por un espíritu de rata: visible en la brújula y el mapa de su amo, y pudriéndose - cada herida cala más hondo de lo que debería.","Marqué par un esprit de rat : visible sur la boussole et la carte de son maître, et en putréfaction - chaque blessure creuse plus profond qu’elle ne devrait.","Marchiato da uno spirito di ratto: visibile sulla bussola e sulla mappa del suo padrone, e in putrefazione - ogni ferita affonda più del dovuto.",ネズミの霊に印をつけられた。その主のコンパスと地図に映り、腐ってゆく - どんな傷も本来より深く刻まれる。,"쥐의 영혼에게 표식이 찍혔습니다. 그 주인의 나침반과 지도에 드러나며, 썩어들어 모든 상처가 본래보다 깊게 패입니다.","Naznaczony przez ducha szczura: widoczny na kompasie i mapie jego pana, i gnijący - każda rana sięga głębiej, niż powinna.","Marcado por um espírito de rato: visível na bússola e no mapa de seu mestre, e apodrecendo - cada ferida penetra mais fundo do que deveria.","Помечен духом крысы: виден на компасе и карте его хозяина и гниёт — каждая рана заходит глубже, чем должна.",Bir sıçan ruhu tarafından işaretlendi: efendisinin pusulasında ve haritasında görünür ve çürür - her yara olması gerekenden derine iner.,被鼠灵标记:显现在其主人的罗盘与地图上,并且开始腐烂——任何伤口都比应有的更深。,被鼠靈標記:顯現在其主人的羅盤與地圖上,並且開始腐爛——任何傷口都比應有的更深。
buffNecroRatBleedName,buffs,Buff,,,,Torn Open,,Aufgerissen,Desgarrado,Déchiré,Squarciato,裂傷,찢긴 상처,Rozszarpany,Dilacerado,Рваная рана,Parçalanmış,撕裂伤,撕裂傷 buffNecroRatBleedName,buffs,Buff,,,,Torn Open,,Aufgerissen,Desgarrado,Déchiré,Squarciato,裂傷,찢긴 상처,Rozszarpany,Dilacerado,Рваная рана,Parçalanmış,撕裂伤,撕裂傷
buffNecroRatBleedDesc,buffs,Buff,,,,Teeth found a vein. The wound will not close on its own while they keep worrying at it.,,"Zähne haben eine Ader gefunden. Die Wunde schließt sich nicht von selbst, solange sie daran weiterzerren.",Unos dientes dieron con una vena. La herida no se cerrará sola mientras sigan hurgando en ella.,Des dents ont trouvé une veine. La plaie ne se refermera pas d’elle-même tant qu’elles s’y acharneront.,I denti hanno trovato una vena. La ferita non si chiuderà da sola finché continuano ad accanirsi.,歯が血管を探り当てた。噛みつづけられているかぎり、傷はひとりでに塞がらない。,이빨이 혈관을 찾아냈습니다. 계속 물어뜯기는 동안 상처는 저절로 아물지 않습니다.,"Zęby trafiły na żyłę. Rana nie zamknie się sama, dopóki będą się w nią wgryzać.",Os dentes acharam uma veia. A ferida não vai fechar sozinha enquanto continuarem a remexer nela.,"Зубы нашли жилу. Пока в ней продолжают копаться, рана сама не затянется.",Dişler bir damar buldu. Onu kurcalamayı sürdürdükleri sürece yara kendiliğinden kapanmaz.,牙齿咬中了血管。只要还在撕扯,伤口就不会自行闭合。,牙齒咬中了血管。只要還在撕扯,傷口就不會自行閉合。 buffNecroRatBleedDesc,buffs,Buff,,,,Teeth found a vein. The wound will not close on its own while they keep worrying at it.,,"Zähne haben eine Ader gefunden. Die Wunde schließt sich nicht von selbst, solange sie daran weiterzerren.",Unos dientes dieron con una vena. La herida no se cerrará sola mientras sigan hurgando en ella.,Des dents ont trouvé une veine. La plaie ne se refermera pas d’elle-même tant qu’elles s’y acharneront.,I denti hanno trovato una vena. La ferita non si chiuderà da sola finché continuano ad accanirsi.,歯が血管を探り当てた。噛みつづけられているかぎり、傷はひとりでに塞がらない。,이빨이 혈관을 찾아냈습니다. 계속 물어뜯기는 동안 상처는 저절로 아물지 않습니다.,"Zęby trafiły na żyłę. Rana nie zamknie się sama, dopóki będą się w nią wgryzać.",Os dentes acharam uma veia. A ferida não vai fechar sozinha enquanto continuarem a remexer nela.,"Зубы нашли жилу. Пока в ней продолжают копаться, рана сама не затянется.",Dişler bir damar buldu. Onu kurcalamayı sürdürdükleri sürece yara kendiliğinden kapanmaz.,牙齿咬中了血管。只要还在撕扯,伤口就不会自行闭合。,牙齒咬中了血管。只要還在撕扯,傷口就不會自行閉合。
necroVersionGuardTitle,UI,XUi MainMenu,x,,,Necromancer's Tome: incompatible game version,,Necromancer's Tome: inkompatible Spielversion,Necromancer's Tome: versión del juego incompatible,Necromancer's Tome : version du jeu incompatible,Necromancer's Tome: versione del gioco incompatibile,Necromancer's Tome: 互換性のないゲームバージョン,Necromancer's Tome: 호환되지 않는 게임 버전,Necromancer's Tome: niezgodna wersja gry,Necromancer's Tome: versão do jogo incompatível,Книга некроманта: несовместимая версия игры,Necromancer's Tome: uyumsuz oyun sürümü,Necromancer's Tome:游戏版本不兼容,Necromancer's Tome:遊戲版本不相容
necroVersionGuardText,UI,XUi MainMenu,x,,,"Necromancer's Tome {0} is made for 7 Days to Die {1} and is not compatible with your game version ({2}). Playing with it would damage your saves. Please update the mod to the version made for your game, or remove it. The game will now close.",,Necromancer's Tome {0} ist für 7 Days to Die {1} gemacht und nicht mit deiner Spielversion ({2}) kompatibel. Damit zu spielen würde deine Spielstände beschädigen. Bitte aktualisiere die Mod auf die Version für dein Spiel oder entferne sie. Das Spiel wird jetzt beendet.,Necromancer's Tome {0} está hecho para 7 Days to Die {1} y no es compatible con tu versión del juego ({2}). Jugar con él dañaría tus partidas guardadas. Actualiza el mod a la versión para tu juego o elimínalo. El juego se cerrará ahora.,"Necromancer's Tome {0} est conçu pour 7 Days to Die {1} et n'est pas compatible avec ta version du jeu ({2}). Jouer avec endommagerait tes sauvegardes. Mets le mod à jour vers la version prévue pour ton jeu, ou retire-le. Le jeu va maintenant se fermer.",Necromancer's Tome {0} è fatto per 7 Days to Die {1} e non è compatibile con la tua versione del gioco ({2}). Giocarci danneggerebbe i tuoi salvataggi. Aggiorna la mod alla versione per il tuo gioco oppure rimuovila. Il gioco ora verrà chiuso.,Necromancer's Tome {0} は 7 Days to Die {1} 用に作られており、お使いのゲームバージョン({2})とは互換性がありません。このままプレイするとセーブデータが損傷します。お使いのゲーム用のバージョンにMODを更新するか、MODを削除してください。ゲームを終了します。,Necromancer's Tome {0}은(는) 7 Days to Die {1}용으로 만들어졌으며 현재 게임 버전({2})과 호환되지 않습니다. 이대로 플레이하면 저장 파일이 손상됩니다. 게임에 맞는 버전으로 모드를 업데이트하거나 모드를 제거하세요. 이제 게임이 종료됩니다.,Necromancer's Tome {0} jest przeznaczony dla 7 Days to Die {1} i nie jest zgodny z twoją wersją gry ({2}). Gra z nim uszkodziłaby twoje zapisy. Zaktualizuj moda do wersji dla twojej gry albo go usuń. Gra zostanie teraz zamknięta.,Necromancer's Tome {0} foi feito para 7 Days to Die {1} e não é compatível com a sua versão do jogo ({2}). Jogar com ele danificaria seus saves. Atualize o mod para a versão feita para o seu jogo ou remova-o. O jogo será fechado agora.,"Мод «Книга некроманта» {0} сделан для 7 Days to Die {1} и несовместим с вашей версией игры ({2}). Игра с ним повредит ваши сохранения. Пожалуйста, обновите мод до версии для вашей игры или удалите его. Сейчас игра закроется.","Necromancer's Tome {0}, 7 Days to Die {1} için yapıldı ve oyun sürümünüzle ({2}) uyumlu değil. Onunla oynamak kayıtlarınıza zarar verir. Lütfen modu oyununuza uygun sürüme güncelleyin veya kaldırın. Oyun şimdi kapanacak.",Necromancer's Tome {0} 为 7 Days to Die {1} 制作,与你的游戏版本({2})不兼容。继续使用会损坏你的存档。请将模组更新到适合你游戏的版本,或将其移除。游戏现在将关闭。,Necromancer's Tome {0} 為 7 Days to Die {1} 製作,與你的遊戲版本({2})不相容。繼續使用會損壞你的存檔。請將模組更新到適合你遊戲的版本,或將其移除。遊戲現在將關閉。
necroVersionGuardQuit,UI,XUi MainMenu,x,,,Quit game,,Spiel beenden,Salir del juego,Quitter le jeu,Esci dal gioco,ゲームを終了,게임 종료,Wyjdź z gry,Sair do jogo,Выйти из игры,Oyundan çık,退出游戏,退出遊戲
braceletSpatialVaultUnreadable,items,Item,,,,This storage rift was saved by another version of the mod. It is kept untouched until you install the mod made for your game version.,,"Dieser Lagerriss wurde von einer anderen Version der Mod gespeichert. Er bleibt unangetastet, bis du die Mod für deine Spielversion installierst.",Esta grieta de almacenamiento se guardó con otra versión del mod. Se conserva intacta hasta que instales el mod para tu versión del juego.,Cette faille de stockage a été enregistrée par une autre version du mod. Elle reste intacte jusqu'à ce que tu installes le mod prévu pour ta version du jeu.,Questa fenditura di deposito è stata salvata da un'altra versione della mod. Resta intatta finché non installi la mod per la tua versione del gioco.,この収納の裂け目は別のバージョンのMODで保存されました。お使いのゲームバージョン用のMODを入れるまで、手を付けずに保持されます。,이 보관 균열은 다른 버전의 모드로 저장되었습니다. 게임 버전에 맞는 모드를 설치할 때까지 그대로 보존됩니다.,"Ta szczelina magazynowa została zapisana przez inną wersję moda. Pozostaje nietknięta, dopóki nie zainstalujesz moda dla twojej wersji gry.",Esta fenda de armazenamento foi salva por outra versão do mod. Ela fica intacta até você instalar o mod feito para a sua versão do jogo.,"Этот разлом хранилища сохранён другой версией мода. Он останется нетронутым, пока вы не поставите мод для вашей версии игры.",Bu depolama yarığı modun başka bir sürümüyle kaydedildi. Oyun sürümünüze uygun modu kurana kadar dokunulmadan saklanır.,这个储物裂隙由另一个版本的模组保存。在你安装适合你游戏版本的模组之前,它会原样保留。,這個儲物裂隙由另一個版本的模組儲存。在你安裝適合你遊戲版本的模組之前,它會原樣保留。
1 Key File Type UsedInMainMenu NoTranslate KeepLoaded english Context / Alternate Text german spanish french italian japanese koreana polish brazilian russian turkish schinese tchinese
160 buffNecroRatMarkDesc buffs Buff Marked by a rat spirit: visible on its master’s compass and map, and rotting - every wound cuts deeper than it should. Von einem Rattengeist gezeichnet: auf Kompass und Karte seines Herrn sichtbar - und faulend, sodass jede Wunde tiefer geht, als sie sollte. Marcado por un espíritu de rata: visible en la brújula y el mapa de su amo, y pudriéndose - cada herida cala más hondo de lo que debería. Marqué par un esprit de rat : visible sur la boussole et la carte de son maître, et en putréfaction - chaque blessure creuse plus profond qu’elle ne devrait. Marchiato da uno spirito di ratto: visibile sulla bussola e sulla mappa del suo padrone, e in putrefazione - ogni ferita affonda più del dovuto. ネズミの霊に印をつけられた。その主のコンパスと地図に映り、腐ってゆく - どんな傷も本来より深く刻まれる。 쥐의 영혼에게 표식이 찍혔습니다. 그 주인의 나침반과 지도에 드러나며, 썩어들어 모든 상처가 본래보다 깊게 패입니다. Naznaczony przez ducha szczura: widoczny na kompasie i mapie jego pana, i gnijący - każda rana sięga głębiej, niż powinna. Marcado por um espírito de rato: visível na bússola e no mapa de seu mestre, e apodrecendo - cada ferida penetra mais fundo do que deveria. Помечен духом крысы: виден на компасе и карте его хозяина и гниёт — каждая рана заходит глубже, чем должна. Bir sıçan ruhu tarafından işaretlendi: efendisinin pusulasında ve haritasında görünür ve çürür - her yara olması gerekenden derine iner. 被鼠灵标记:显现在其主人的罗盘与地图上,并且开始腐烂——任何伤口都比应有的更深。 被鼠靈標記:顯現在其主人的羅盤與地圖上,並且開始腐爛——任何傷口都比應有的更深。
161 buffNecroRatBleedName buffs Buff Torn Open Aufgerissen Desgarrado Déchiré Squarciato 裂傷 찢긴 상처 Rozszarpany Dilacerado Рваная рана Parçalanmış 撕裂伤 撕裂傷
162 buffNecroRatBleedDesc buffs Buff Teeth found a vein. The wound will not close on its own while they keep worrying at it. Zähne haben eine Ader gefunden. Die Wunde schließt sich nicht von selbst, solange sie daran weiterzerren. Unos dientes dieron con una vena. La herida no se cerrará sola mientras sigan hurgando en ella. Des dents ont trouvé une veine. La plaie ne se refermera pas d’elle-même tant qu’elles s’y acharneront. I denti hanno trovato una vena. La ferita non si chiuderà da sola finché continuano ad accanirsi. 歯が血管を探り当てた。噛みつづけられているかぎり、傷はひとりでに塞がらない。 이빨이 혈관을 찾아냈습니다. 계속 물어뜯기는 동안 상처는 저절로 아물지 않습니다. Zęby trafiły na żyłę. Rana nie zamknie się sama, dopóki będą się w nią wgryzać. Os dentes acharam uma veia. A ferida não vai fechar sozinha enquanto continuarem a remexer nela. Зубы нашли жилу. Пока в ней продолжают копаться, рана сама не затянется. Dişler bir damar buldu. Onu kurcalamayı sürdürdükleri sürece yara kendiliğinden kapanmaz. 牙齿咬中了血管。只要还在撕扯,伤口就不会自行闭合。 牙齒咬中了血管。只要還在撕扯,傷口就不會自行閉合。
163 necroVersionGuardTitle UI XUi MainMenu x Necromancer's Tome: incompatible game version Necromancer's Tome: inkompatible Spielversion Necromancer's Tome: versión del juego incompatible Necromancer's Tome : version du jeu incompatible Necromancer's Tome: versione del gioco incompatibile Necromancer's Tome: 互換性のないゲームバージョン Necromancer's Tome: 호환되지 않는 게임 버전 Necromancer's Tome: niezgodna wersja gry Necromancer's Tome: versão do jogo incompatível Книга некроманта: несовместимая версия игры Necromancer's Tome: uyumsuz oyun sürümü Necromancer's Tome:游戏版本不兼容 Necromancer's Tome:遊戲版本不相容
164 necroVersionGuardText UI XUi MainMenu x Necromancer's Tome {0} is made for 7 Days to Die {1} and is not compatible with your game version ({2}). Playing with it would damage your saves. Please update the mod to the version made for your game, or remove it. The game will now close. Necromancer's Tome {0} ist für 7 Days to Die {1} gemacht und nicht mit deiner Spielversion ({2}) kompatibel. Damit zu spielen würde deine Spielstände beschädigen. Bitte aktualisiere die Mod auf die Version für dein Spiel oder entferne sie. Das Spiel wird jetzt beendet. Necromancer's Tome {0} está hecho para 7 Days to Die {1} y no es compatible con tu versión del juego ({2}). Jugar con él dañaría tus partidas guardadas. Actualiza el mod a la versión para tu juego o elimínalo. El juego se cerrará ahora. Necromancer's Tome {0} est conçu pour 7 Days to Die {1} et n'est pas compatible avec ta version du jeu ({2}). Jouer avec endommagerait tes sauvegardes. Mets le mod à jour vers la version prévue pour ton jeu, ou retire-le. Le jeu va maintenant se fermer. Necromancer's Tome {0} è fatto per 7 Days to Die {1} e non è compatibile con la tua versione del gioco ({2}). Giocarci danneggerebbe i tuoi salvataggi. Aggiorna la mod alla versione per il tuo gioco oppure rimuovila. Il gioco ora verrà chiuso. Necromancer's Tome {0} は 7 Days to Die {1} 用に作られており、お使いのゲームバージョン({2})とは互換性がありません。このままプレイするとセーブデータが損傷します。お使いのゲーム用のバージョンにMODを更新するか、MODを削除してください。ゲームを終了します。 Necromancer's Tome {0}은(는) 7 Days to Die {1}용으로 만들어졌으며 현재 게임 버전({2})과 호환되지 않습니다. 이대로 플레이하면 저장 파일이 손상됩니다. 게임에 맞는 버전으로 모드를 업데이트하거나 모드를 제거하세요. 이제 게임이 종료됩니다. Necromancer's Tome {0} jest przeznaczony dla 7 Days to Die {1} i nie jest zgodny z twoją wersją gry ({2}). Gra z nim uszkodziłaby twoje zapisy. Zaktualizuj moda do wersji dla twojej gry albo go usuń. Gra zostanie teraz zamknięta. Necromancer's Tome {0} foi feito para 7 Days to Die {1} e não é compatível com a sua versão do jogo ({2}). Jogar com ele danificaria seus saves. Atualize o mod para a versão feita para o seu jogo ou remova-o. O jogo será fechado agora. Мод «Книга некроманта» {0} сделан для 7 Days to Die {1} и несовместим с вашей версией игры ({2}). Игра с ним повредит ваши сохранения. Пожалуйста, обновите мод до версии для вашей игры или удалите его. Сейчас игра закроется. Necromancer's Tome {0}, 7 Days to Die {1} için yapıldı ve oyun sürümünüzle ({2}) uyumlu değil. Onunla oynamak kayıtlarınıza zarar verir. Lütfen modu oyununuza uygun sürüme güncelleyin veya kaldırın. Oyun şimdi kapanacak. Necromancer's Tome {0} 为 7 Days to Die {1} 制作,与你的游戏版本({2})不兼容。继续使用会损坏你的存档。请将模组更新到适合你游戏的版本,或将其移除。游戏现在将关闭。 Necromancer's Tome {0} 為 7 Days to Die {1} 製作,與你的遊戲版本({2})不相容。繼續使用會損壞你的存檔。請將模組更新到適合你遊戲的版本,或將其移除。遊戲現在將關閉。
165 necroVersionGuardQuit UI XUi MainMenu x Quit game Spiel beenden Salir del juego Quitter le jeu Esci dal gioco ゲームを終了 게임 종료 Wyjdź z gry Sair do jogo Выйти из игры Oyundan çık 退出游戏 退出遊戲
166 braceletSpatialVaultUnreadable items Item This storage rift was saved by another version of the mod. It is kept untouched until you install the mod made for your game version. Dieser Lagerriss wurde von einer anderen Version der Mod gespeichert. Er bleibt unangetastet, bis du die Mod für deine Spielversion installierst. Esta grieta de almacenamiento se guardó con otra versión del mod. Se conserva intacta hasta que instales el mod para tu versión del juego. Cette faille de stockage a été enregistrée par une autre version du mod. Elle reste intacte jusqu'à ce que tu installes le mod prévu pour ta version du jeu. Questa fenditura di deposito è stata salvata da un'altra versione della mod. Resta intatta finché non installi la mod per la tua versione del gioco. この収納の裂け目は別のバージョンのMODで保存されました。お使いのゲームバージョン用のMODを入れるまで、手を付けずに保持されます。 이 보관 균열은 다른 버전의 모드로 저장되었습니다. 게임 버전에 맞는 모드를 설치할 때까지 그대로 보존됩니다. Ta szczelina magazynowa została zapisana przez inną wersję moda. Pozostaje nietknięta, dopóki nie zainstalujesz moda dla twojej wersji gry. Esta fenda de armazenamento foi salva por outra versão do mod. Ela fica intacta até você instalar o mod feito para a sua versão do jogo. Этот разлом хранилища сохранён другой версией мода. Он останется нетронутым, пока вы не поставите мод для вашей версии игры. Bu depolama yarığı modun başka bir sürümüyle kaydedildi. Oyun sürümünüze uygun modu kurana kadar dokunulmadan saklanır. 这个储物裂隙由另一个版本的模组保存。在你安装适合你游戏版本的模组之前,它会原样保留。 這個儲物裂隙由另一個版本的模組儲存。在你安裝適合你遊戲版本的模組之前,它會原樣保留。
+6 -2
View File
@@ -30,12 +30,16 @@
<buff name="buffNecroZombieKillTrackerDisplay" icon="ui_game_symbol_skull" icon_color="150,0,0" name_key="buffNecroZombieKillTrackerDisplayName" description_key="buffNecroZombieKillTrackerDisplayDesc"> <buff name="buffNecroZombieKillTrackerDisplay" icon="ui_game_symbol_skull" icon_color="150,0,0" name_key="buffNecroZombieKillTrackerDisplayName" description_key="buffNecroZombieKillTrackerDisplayDesc">
<stack_type value="ignore"/> <stack_type value="ignore"/>
<duration value="0"/> <duration value="0"/>
<update_rate value=".1"/> <!-- Было ".1" (10 раз в секунду). 2026-09-24 стало "1" по отзыву zomb1 на Nexus
(подозревал в этом падение FPS): тик только пересчитывает урон ножа из числа
убийств, а оно меняется лишь при убийстве - после убийства урон догоняет не позже
чем через секунду. -->
<update_rate value="1"/>
<display_value value="necroNecromancyLevelCVar"/> <display_value value="necroNecromancyLevelCVar"/>
<!-- Нож некроманта (BACKLOG.md item 5, user request 2026-08-28): "урон умножается на <!-- Нож некроманта (BACKLOG.md item 5, user request 2026-08-28): "урон умножается на
скилл некроманта ... и делится на 10". Computed here (this buff already ticks скилл некроманта ... и делится на 10". Computed here (this buff already ticks
10x/sec on every player via update_rate) rather than on the knife item itself, once a second - 10x/sec before 1.4.1 - on every player via update_rate) rather than on the knife item itself,
since a held item has no update tick of its own - reading a live CVar from a since a held item has no update tick of its own - reading a live CVar from a
passive_effect (confirmed working: vanilla's own $PlayerLevelBonus, computed in passive_effect (confirmed working: vanilla's own $PlayerLevelBonus, computed in
buffs.xml and read elsewhere via passive_effect value="@$PlayerLevelBonus") needs buffs.xml and read elsewhere via passive_effect value="@$PlayerLevelBonus") needs
+7 -6
View File
@@ -1023,10 +1023,12 @@
</append> </append>
<!-- "Свиток духа крысы": тот же свиток, что у Зомбоволка, но серее - указание 2026-09-18 <!-- "Свиток духа крысы": тот же свиток, что у Зомбоволка, но серее - указание 2026-09-18
("выглядит как свиток призыва зомбоволка, но с более серым тинтом"). Иконка ("выглядит как свиток призыва зомбоволка, но с более серым тинтом"). TintColor красит
переиспользуется целиком (CustomIcon="SummonZombieWolf"), своей рисовать не пришлось; 3D-модель в руке (множится на базовый цвет, как у Камня духов).
CustomIconTint обесцвечивает её, TintColor - 3D-модель в руке. Оба множатся на базовый
цвет, как у Камня духов. ИКОНКА СВОЯ с 2026-09-21: пользователь нарисовал RatBook.png (книга в пурпурных узорах с
крысой). До этого переиспользовалась волчья SummonZombieWolf с CustomIconTint 8a9a8a;
тинт убран вместе с ней - он множится на иконку и погасил бы нарисованные цвета.
ДВА ДЕЙСТВИЯ, И ОНИ НЕ ТЕ ЖЕ, ЧТО У ОСТАЛЬНЫХ ПИТОМЦЕВ. Action1 (силовая атака) - отзыв, ДВА ДЕЙСТВИЯ, И ОНИ НЕ ТЕ ЖЕ, ЧТО У ОСТАЛЬНЫХ ПИТОМЦЕВ. Action1 (силовая атака) - отзыв,
как везде. А вот Action0 у крысы ТРЁХзначный, и разбирает это HarmonySrc/PetCommandPatch.cs: как везде. А вот Action0 у крысы ТРЁХзначный, и разбирает это HarmonySrc/PetCommandPatch.cs:
@@ -1061,8 +1063,7 @@
У ОСТАЛЬНЫХ ШЕСТИ КНИГ ПРИЗЫВА ЭТОТ ЖЕ ЗНАЧОК ПОКА ОСТАЛСЯ - трогать их без У ОСТАЛЬНЫХ ШЕСТИ КНИГ ПРИЗЫВА ЭТОТ ЖЕ ЗНАЧОК ПОКА ОСТАЛСЯ - трогать их без
указания не стал, но замечание к ним относится ровно так же. --> указания не стал, но замечание к ним относится ровно так же. -->
<property name="CustomIcon" value="SummonZombieWolf"/> <property name="CustomIcon" value="RatBook"/>
<property name="CustomIconTint" value="8a9a8a"/>
<property name="DescriptionKey" value="bookSummonRatSpiritDesc"/> <property name="DescriptionKey" value="bookSummonRatSpiritDesc"/>
<property name="DisplayType" value="ammoGrenadeFire"/> <property name="DisplayType" value="ammoGrenadeFire"/>
<property name="HoldType" value="21"/> <property name="HoldType" value="21"/>
+32 -1
View File
@@ -43,7 +43,17 @@ namespace NecromancerTome
/// Data/Config/blocks.xml block names (checked directly, matched by name PREFIX since most /// Data/Config/blocks.xml block names (checked directly, matched by name PREFIX since most
/// categories have many color/variant blocks) - tell me if any of these aren't what was /// categories have many color/variant blocks) - tell me if any of these aren't what was
/// meant, this is an interpretation, not a spec: /// meant, this is an interpretation, not a spec:
/// - "Кровати" (beds) -> bedMadeNoFrame*/bedMessyNoFrame* ONLY. Deliberately excludes /// - ОБНОВЛЕНО 2026-09-18: кровати класса SleepingBag (bed02*, bunkBed*) ТЕПЕРЬ ВКЛЮЧЕНЫ,
/// см. комментарий в TargetPrefixes. НОВЫХ ПАТЧЕЙ ДЛЯ НИХ НЕ ПОНАДОБИЛОСЬ, и это стоило
/// проверить: BlockSleepingBag переопределяет только CanPlaceBlockAt, PlaceBlock,
/// GetActivationText и ЧЕТЫРЁХаргументный OnBlockActivated. Наши три патча висят на
/// HasBlockActivationCommands, GetBlockActivationCommands и ПЯТИаргументном
/// OnBlockActivated - ни одного из них он не перекрывает, поэтому патчи базового Block
/// достают его как есть. С BlockCompositeTileEntity (кулеры, коробки) так не вышло -
/// там понадобился отдельный набор, см. ниже.
///
/// - Исходная формулировка, оставлена для истории:
/// "Кровати" (beds) -> bedMadeNoFrame*/bedMessyNoFrame* ONLY. Deliberately excludes
/// bed02*/bunkBedMade*/bunkBedMessy* even though they look like beds too - decompiling /// bed02*/bunkBedMade*/bunkBedMessy* even though they look like beds too - decompiling
/// showed those all use Class="SleepingBag" (they're actually functional sleeping-bag/ /// showed those all use Class="SleepingBag" (they're actually functional sleeping-bag/
/// respawn-anchor blocks, not pure decoration - same family as the player's own bedroll, /// respawn-anchor blocks, not pure decoration - same family as the player's own bedroll,
@@ -74,6 +84,27 @@ namespace NecromancerTome
{ {
"bedMadeNoFrame", "bedMadeNoFrame",
"bedMessyNoFrame", "bedMessyNoFrame",
// ДОБАВЛЕНО 2026-09-18, И ЭТО ИСПРАВЛЕНИЕ ПРОМАХА ПО ЦЕЛИ. Изначально эти кровати были
// исключены НАМЕРЕННО, с формулировкой "это respawn-anchor блоки, не декорация,
// трогать не просил". Оказалось ровно наоборот: пользователь объяснил, что подбор
// кроватей нужен ИМЕННО ради возрождения - "поглощённые блоки сохраняют состояние,
// которое было до поглощения; кровати до поглощения не являются точкой возрождения;
// точкой возрождения они могли бы стать при подборе".
//
// Проверено декомпиляцией BlockSleepingBag: PlaceBlock ставит игроку
// selectedSpawnPointKey и запоминает BedrollPos - то есть поставленная своими руками
// кровать действительно становится точкой возрождения, а стоящая в POI - нет.
//
// Один префикс покрывает все цветовые варианты: bed02White, bunkBedMessyBlue и прочие
// наследуют Class="SleepingBag" через Extends от bed02 / bunkBedMade.
// Вместе с bunkBed* попадают bunkBedFrame и bunkBedMattress - у них класса нет, они
// просто декор, и подбирать их не вредно.
//
// bedroll НЕ включён намеренно: это собственный предмет игрока, он и так ставится и
// снимается штатно, а bedrollPOI - отдельный декоративный блок, который от bedroll не
// наследуется и точкой возрождения не является.
"bed02",
"bunkBed",
"cntWaterCooler", "cntWaterCooler",
"cntCardboardBox", "cntCardboardBox",
}; };
+551
View File
@@ -0,0 +1,551 @@
using System;
using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// ТРУП ПОМЕЧЕННОГО ЗОМБИ САМ СТАНОВИТСЯ ДОБЫЧЕЙ — указание 2026-09-18: «зомби после упокоения
/// оставляет свой труп. Можем ли мы подменять этот труп в момент смерти точно таким же, но с
/// другой текстурой и с функцией мешка жертвы (не исчезает, можно забрать лут)?»
///
/// ПОДМЕНЯТЬ НЕ ПРИШЛОСЬ НИЧЕГО, и это главное, что стоит знать. Труп — не отдельная модель, а
/// та же скелетная модель зомби в позе смерти, поэтому «такой же, но с другой текстурой» — это
/// перекраска материалов уже лежащего трупа, тем же приёмом, что призрачные питомцы и глаза
/// собаки. Ни нового префаба, ни новой сущности.
///
/// ТРИ ЧАСТИ, И КАЖДАЯ ДЕРЖИТСЯ НА СВОЁМ ВАНИЛЬНОМ МЕХАНИЗМЕ:
///
/// 1. НЕ ИСЧЕЗАЕТ. `EntityAlive.timeStayAfterDeath` — обычное публичное поле экземпляра,
/// которое при спавне заполняется из entity_class (`TimeStayAfterDeath * 20`). У зомби
/// там 30 секунд, у ванильного мешка с лутом — 3600. Достаточно записать другое число.
///
/// 2. ЕГО МОЖНО ОБЫСКАТЬ. У сущностей есть своя система команд взаимодействия, отдельная от
/// блочной: `Entity.GetActivationCommands()` собирает список из виртуального
/// `InitLocalActivationCommands`, `AllowActivationCommand` решает, доступна ли команда
/// сейчас, `OnEntityActivated` её исполняет. Мешок с лутом объявляет свою так
/// (EntityItem, от которого он наследуется):
///
/// _addCallback(new EntityActivationCommand("search", "search"));
///
/// Мы объявляем ровно такую же — с тем же идентификатором и той же иконкой. Значит и
/// подпись на экране, и значок берутся готовые, локализованные на все языки игры, и
/// игроку это выглядит ровно как обыск мешка, потому что это и есть он.
///
/// 3. ВНУТРИ ЛУТ. А вот здесь сознательный обход. Само хранилище живёт внутри
/// `EntityLootContainer`: его `bag`, его `Read`/`Write` для сейва, его сетевая
/// синхронизация. Приделать всё это трупу зомби — работа масштаба Пространственного
/// хранилища, с выходом на сейвы, то есть на риск номер один этого проекта.
///
/// Поэтому обыск НЕ открывает труп, а ПРЕВРАЩАЕТ его в мешок «Жертва»: мешок спавнится
/// на месте трупа, труп убирается. Снаружи читается как «обыскал и нашёл», внутри —
/// обычный ванильный контейнер, который сам себя сохраняет, синхронизирует и открывает.
/// Ни одной новой сущности с состоянием.
///
/// ОБЫЧНЫЙ МЕШОК ЭТО НЕ ОТМЕНЯЕТ. Бросок вероятности в VictimPatch.cs остаётся как был: выпал
/// — обычный мешок лежит рядом, не выпал — только труп. «Жертва» переехала из «падает сразу» в
/// «лежит внутри трупа», и больше ничего не изменилось.
///
/// ПОЧЕМУ СПИСОК КОМАНД СБРАСЫВАЕТСЯ. `GetActivationCommands` кеширует результат в
/// `activationCommands` при первом обращении — а первое обращение случается, пока зомби ещё
/// жив и командой не обладает. Обнуление поля на смерти заставляет пересобрать список; тем же
/// приёмом пользуется сама игра в CopyPropertiesFromEntityClass.
/// </summary>
public static class CorpseLoot
{
/// <summary>Сколько секунд лежит помеченный труп. В 1.4.0 было 3600, как у ванильного мешка с
/// лутом; 2026-09-24 уменьшено до 5 минут по указанию пользователя: «если игрок не будет
/// убирать, у него FPS начнёт падать» - каждый лежащий труп это отрисовываемая модель со
/// своими копиями материалов, а необобранных за час набирается много.</summary>
public const float CorpseSecondsAfterDeath = 300f;
/// <summary>Во что красится труп. Тёмно-багровый: издалека видно, что этот не как все, и
/// при этом не выглядит подсветкой из другой игры.</summary>
public static readonly Color CorpseTint = new Color(0.55f, 0.12f, 0.16f);
/// <summary>Идентификатор берётся у ванильного мешка (EntityItem), чтобы подпись досталась
/// готовой и локализованной на все языки игры. ИКОНКА - "hand", а не "search", по указанию
/// 2026-09-18 («при наведении на зомби пиктограмма руки»); имена иконок к идентификатору
/// команды не привязаны, у ванильного "take" стоит ровно эта.</summary>
public const string SearchCommand = "search";
public const string SearchIcon = "hand";
/// <summary>Секунды удержания. Круговой индикатор рисует сама игра - XUiC_Timer, тот же,
/// что крутится при снятии верстака.</summary>
public const float SearchSeconds = 3f;
/// <summary>Трупы, которые ещё не обысканы. Ключ — entityId. Чистится сама: запись
/// снимается при обыске, а остальные уходят вместе с миром.</summary>
public static readonly HashSet<int> SearchableCorpses = new HashSet<int>();
/// <summary>Трупы, которые ещё не удалось перекрасить: модель бывает не собрана на том
/// кадре, когда сущность умирает. Добивается односекундным тиком PetFollowPatch.</summary>
public static readonly HashSet<int> PendingTint = new HashSet<int>();
/// <summary>УТЕЧКА ПАМЯТИ, НАЙДЕНА 2026-09-24 (жалоба: «память течёт, лечится только
/// перезапуском»). Версия 1.4.0 перекрашивала труп и НИЧЕГО за собой не убирала:
///
/// - `renderer.materials` создаёт копии материалов, и Unity НЕ удаляет их вместе с
/// объектом - они живут до перезапуска;
/// - перекрашенная текстура создавалась несжатой (RGBA32, 16-64 МБ на штуку) и оставалась
/// читаемой, то есть лежала дважды - в видеопамяти и копией в оперативной;
/// - кеш текстур только рос.
///
/// Лог игры (`60sec delta: ... current: ..., desired: ...` - это Texture.currentTextureMemory)
/// показал рост видеопамяти текстур с 1,2 до 7,4 ГБ за два часа при нужных игре ~3,5 ГБ, и
/// FPS упал с 60 до 11. За сессию было перекрашено 53 трупа по 4-10 материалов.
///
/// Теперь каждый перекрашенный труп записан здесь вместе с копиями материалов и текстурами,
/// а тик раз в секунду (ReleaseGoneCorpses) убирает за теми, кого уже нет. Текстура
/// удаляется, когда её не использует ни один труп.</summary>
public class TintedCorpse
{
public readonly List<Renderer> Renderers = new List<Renderer>();
public readonly List<Material> Materials = new List<Material>();
public readonly List<Texture2D> Textures = new List<Texture2D>();
}
public static readonly Dictionary<int, TintedCorpse> Tinted = new Dictionary<int, TintedCorpse>();
/// <summary>Сколько трупов сейчас носит каждую перекрашенную текстуру.</summary>
public static readonly Dictionary<Texture2D, int> TextureUsers = new Dictionary<Texture2D, int>();
/// <summary>Предел стороны перекрашенной текстуры. Исходные 2048-4096; труп рассматривают
/// вблизи, но 1024 со сжатием это ~1 МБ вместо 16-64 МБ, и разница на глаз незаметна.</summary>
public const int MaxTintSize = 1024;
/// <summary>Повторная попытка перекраски. Зовётся из общего тика раз в секунду.</summary>
public static void RetryPendingTints(World _world)
{
ReleaseGoneCorpses(_world);
if (PendingTint.Count == 0 || _world == null)
{
return;
}
List<int> done = new List<int>();
foreach (int id in PendingTint)
{
Entity corpse = _world.GetEntity(id);
if (corpse == null || TintCorpse(corpse))
{
done.Add(id);
}
}
foreach (int id in done)
{
PendingTint.Remove(id);
}
}
/// <summary>Уборка за исчезнувшими трупами. Трупом считается исчезнувшим, когда его нет в
/// мире И уничтожены все его рендереры: удалять материал, который ещё рисуется, нельзя.</summary>
public static void ReleaseGoneCorpses(World _world)
{
if (Tinted.Count == 0 || _world == null)
{
return;
}
List<int> gone = null;
foreach (KeyValuePair<int, TintedCorpse> pair in Tinted)
{
if (_world.GetEntity(pair.Key) != null)
{
continue;
}
bool rendered = false;
foreach (Renderer renderer in pair.Value.Renderers)
{
if (renderer != null)
{
rendered = true;
break;
}
}
if (rendered)
{
continue;
}
if (gone == null)
{
gone = new List<int>();
}
gone.Add(pair.Key);
}
if (gone == null)
{
return;
}
foreach (int id in gone)
{
TintedCorpse corpse = Tinted[id];
Tinted.Remove(id);
SearchableCorpses.Remove(id);
foreach (Material material in corpse.Materials)
{
if (material != null)
{
UnityEngine.Object.Destroy(material);
}
}
foreach (Texture2D texture in corpse.Textures)
{
ReleaseTexture(texture);
}
}
Debug.Log("[NecromancerTome] CorpseLoot: убрано за исчезнувшими трупами: " + gone.Count +
", осталось трупов " + Tinted.Count + ", текстур " + TextureUsers.Count);
}
public static void ReleaseTexture(Texture2D _texture)
{
if (_texture == null || !TextureUsers.TryGetValue(_texture, out int users))
{
return;
}
if (users > 1)
{
TextureUsers[_texture] = users - 1;
return;
}
TextureUsers.Remove(_texture);
List<Texture> keys = new List<Texture>();
foreach (KeyValuePair<Texture, Texture2D> pair in TintedTextures)
{
if (pair.Value == _texture)
{
keys.Add(pair.Key);
}
}
foreach (Texture key in keys)
{
TintedTextures.Remove(key);
}
UnityEngine.Object.Destroy(_texture);
}
public static bool IsSearchable(Entity _entity)
{
return _entity != null && SearchableCorpses.Contains(_entity.entityId);
}
/// <summary>Зовётся из VictimPatch, когда помеченный зомби умер.</summary>
public static void MarkCorpse(EntityAlive _victim)
{
if (_victim == null)
{
return;
}
SearchableCorpses.Add(_victim.entityId);
// timeStayAfterDeath хранится в ТИКАХ: игра заполняет его как TimeStayAfterDeath * 20
// (EntityAlive.CopyPropertiesFromEntityClass), то есть двадцать тиков на секунду.
_victim.timeStayAfterDeath = (int)(CorpseSecondsAfterDeath * 20f);
// Список команд уже собран и закеширован, пока зомби был жив и обыскивать его было
// нельзя. Обнуляем, чтобы собрался заново - уже с "search".
_victim.activationCommands = null;
if (!TintCorpse(_victim))
{
// Модель на этом кадре может быть ещё не готова - ровно тем же болели призрачные
// торговцы и разведение коллайдеров у питомцев. Кладём в очередь, тик добьёт.
PendingTint.Add(_victim.entityId);
}
Debug.Log("[NecromancerTome] CorpseLoot: труп " + _victim.entityId +
" помечен как обыскиваемый, лежит " + CorpseSecondsAfterDeath + " с");
}
/// <summary>Перекраска трупа. ПЕРЕПИСАНА 2026-09-18, первая версия не работала вообще:
/// пользователь сообщил «труп не перекрасился», и причина нашлась в нашем же логе.
///
/// Первая версия ставила `_Color` на материалах - ровно как сделано у глаз собаки. Но у
/// собаки глаз сидит на шейдере `Standard`, а тело зомби и шкура зверей - на собственных
/// шейдерах игры, и проба, которую печатает GhostTraderPatch, по ним говорит прямо:
///
/// shader 'Game/Character' ... tint property: &lt;none&gt;
/// shader 'Game/Animal/Fur' ... tint property: &lt;none&gt;
///
/// Свойства цвета у них НЕТ ВООБЩЕ. `HasProperty("_Color")` возвращал false, цикл не делал
/// ни одной итерации, и метод честно печатал «материалов 0» - которое никто не прочитал.
///
/// Поэтому красится САМА ТЕКСТУРА, тем же приёмом, которым мод обесцвечивает торговцев:
/// прогон через RenderTexture (игровые текстуры сжаты и с isReadable=false, напрямую
/// GetPixels по ним падает), затем попиксельное умножение на цвет. Разница с
/// обесцвечиванием ровно в одной строке арифметики, всё остальное - тот же путь, уже
/// проверенный на шести торговцах.
///
/// Результат кешируется по исходной текстуре: они 2048x2048 и больше, а помеченных трупов
/// за игру будет много - пересчитывать на каждый незачем.</summary>
public static readonly Dictionary<Texture, Texture2D> TintedTextures = new Dictionary<Texture, Texture2D>();
public static bool TintCorpse(Entity _corpse)
{
Renderer[] renderers = _corpse.GetComponentsInChildren<Renderer>(true);
if (renderers == null || renderers.Length == 0)
{
return false;
}
if (!Tinted.TryGetValue(_corpse.entityId, out TintedCorpse record))
{
record = new TintedCorpse();
Tinted[_corpse.entityId] = record;
}
int tinted = 0;
foreach (Renderer renderer in renderers)
{
if (renderer == null || renderer is ParticleSystemRenderer)
{
continue;
}
Material[] shared = renderer.sharedMaterials;
if (shared == null || shared.Length == 0)
{
continue;
}
// Копии материалов создаются этой строкой, и все они теперь наши - записываем
// каждую, даже ту, что перекрасить не удалось: удалять её тоже нам.
Material[] instances = renderer.materials;
if (!record.Renderers.Contains(renderer))
{
record.Renderers.Add(renderer);
}
for (int i = 0; i < instances.Length && i < shared.Length; i++)
{
Material material = instances[i];
if (material == null)
{
continue;
}
if (!record.Materials.Contains(material))
{
record.Materials.Add(material);
}
string albedo = GhostTraderPatch.FindAlbedoProperty(material);
if (albedo == null)
{
continue;
}
Texture2D repainted = Tint(material.GetTexture(albedo));
if (repainted == null)
{
continue;
}
material.SetTexture(albedo, repainted);
record.Textures.Add(repainted);
TextureUsers.TryGetValue(repainted, out int users);
TextureUsers[repainted] = users + 1;
tinted++;
}
}
Debug.Log("[NecromancerTome] CorpseLoot: труп " + _corpse.entityId + " перекрашен, материалов " + tinted);
return tinted > 0;
}
/// <summary>Копия GhostTraderPatch.Desaturate с одной изменённой строкой: вместо сведения
/// каналов к яркости - умножение на CorpseTint. Прогон через RenderTexture обязателен и там
/// и здесь по одной причине: игровые текстуры сжаты и не читаются процессором напрямую.</summary>
public static Texture2D Tint(Texture _source)
{
if (_source == null)
{
return null;
}
if (TintedTextures.TryGetValue(_source, out Texture2D cached))
{
return cached;
}
Texture2D painted = null;
RenderTexture rt = null;
RenderTexture previous = RenderTexture.active;
try
{
// Уменьшение идёт бесплатно, самим Blit в RenderTexture меньшего размера. Стороны
// кратны четырём - иначе Compress ниже откажется сжимать.
float scale = Mathf.Min(1f, (float)MaxTintSize / Mathf.Max(_source.width, _source.height));
int width = Mathf.Max(4, (int)(_source.width * scale) / 4 * 4);
int height = Mathf.Max(4, (int)(_source.height * scale) / 4 * 4);
rt = RenderTexture.GetTemporary(width, height, 0,
RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
Graphics.Blit(_source, rt);
RenderTexture.active = rt;
painted = new Texture2D(width, height, TextureFormat.RGBA32, false);
painted.ReadPixels(new Rect(0f, 0f, width, height), 0, 0);
Color32[] pixels = painted.GetPixels32();
for (int i = 0; i < pixels.Length; i++)
{
Color32 p = pixels[i];
p.r = (byte)(p.r * CorpseTint.r);
p.g = (byte)(p.g * CorpseTint.g);
p.b = (byte)(p.b * CorpseTint.b);
pixels[i] = p;
}
painted.SetPixels32(pixels);
// Сжатие в DXT (вчетверо-восьмеро меньше RGBA32) и makeNoLongerReadable=true: без
// него копия пикселей оставалась в оперативной памяти рядом с видеопамятью.
painted.Compress(false);
painted.Apply(false, true);
}
catch (Exception e)
{
Debug.LogError("[NecromancerTome] CorpseLoot: не удалось перекрасить текстуру '" +
_source.name + "': " + e.Message);
if (painted != null)
{
UnityEngine.Object.Destroy(painted);
}
painted = null;
}
finally
{
RenderTexture.active = previous;
if (rt != null)
{
RenderTexture.ReleaseTemporary(rt);
}
}
TintedTextures[_source] = painted;
return painted;
}
/// <summary>Обыск: труп превращается в мешок «Жертва» на своём же месте.</summary>
public static void SearchCorpse(EntityAlive _corpse)
{
SearchableCorpses.Remove(_corpse.entityId);
Patch_EntityAlive_dropItemOnDeath_VictimBag.SpawnVictimBag(_corpse, false);
Debug.Log("[NecromancerTome] CorpseLoot: труп " + _corpse.entityId + " обыскан, заменён мешком");
if (_corpse.world != null)
{
_corpse.world.RemoveEntity(_corpse.entityId, EnumRemoveEntityReason.Despawned);
}
}
}
/// <summary>Добавляет команду «обыскать» в список. Постфикс, а не префикс: ванильные команды
/// сущности (у EntityAlive это «подобрать» для тех, у кого задан PickupItem) должны остаться.</summary>
[HarmonyPatch(typeof(EntityAlive), "InitLocalActivationCommands")]
public static class Patch_EntityAlive_InitLocalActivationCommands_Corpse
{
public static void Postfix(EntityAlive __instance, Action<EntityActivationCommand> _addCallback)
{
if (!CorpseLoot.IsSearchable(__instance))
{
return;
}
_addCallback(new EntityActivationCommand(CorpseLoot.SearchCommand, CorpseLoot.SearchIcon));
}
}
/// <summary>Разрешает команду только помеченному трупу. Без этого «обыскать» досталось бы
/// любому существу, у которого список команд пересобрался бы по любой причине.</summary>
[HarmonyPatch(typeof(EntityAlive), "AllowActivationCommand")]
public static class Patch_EntityAlive_AllowActivationCommand_Corpse
{
public static void Postfix(EntityAlive __instance, ReadOnlySpan<char> _commandName, ref bool __result)
{
if (!MemoryExtensions.Equals(_commandName, CorpseLoot.SearchCommand.AsSpan(), StringComparison.Ordinal))
{
return;
}
__result = CorpseLoot.IsSearchable(__instance);
}
}
/// <summary>ПОДСКАЗКА ПРИ НАВЕДЕНИИ. Баг-репорт 2026-09-18: «при наведении пиктограмма руки так
/// и не появилась. А без неё интуитивно непонятно что тут можно лутать».
///
/// Оказалось, я настроил не то место. Иконка у EntityActivationCommand рисуется в РАДИАЛЬНОМ
/// МЕНЮ (XUiC_Radial.SetCurrentEntityData -> CreateRadialEntry со спрайтом
/// $"ui_game_symbol_{icon}"), то есть только когда игрок ЗАЖМЁТ кнопку взаимодействия. А
/// подпись, которая появляется просто при наведении, берётся совсем из другого места - из
/// виртуального Entity.GetActivationText(), и у EntityAlive он для обычного существа
/// возвращает пустую строку.
///
/// Здесь возвращается ровно то же, что у ванильного лутаемого объекта (EntityItem):
///
/// string arg = playerInput.Activate.GetBindingXuiMarkupString()
/// + playerInput.PermanentActions.Activate.GetBindingXuiMarkupString();
/// return string.Format(Localization.Get("lootTooltipNew"), arg, имя);
///
/// `lootTooltipNew` - штатная строка лутаемых объектов, «{0} Search {1} (Untouched)» /
/// «{0}: обыскать {1} (нетронуто)», уже переведённая на все языки игры. Значок кнопки
/// подставляется разметкой [action:...] из первого аргумента, поэтому у игрока он будет ровно
/// такой же, как на любом мешке, и подпишется его собственной раскладкой.
///
/// Имя берётся у самого зомби (LocalizedEntityName), так что подсказка читается как
/// «обыскать Арлин», а не «обыскать мешок».</summary>
[HarmonyPatch(typeof(EntityAlive), "GetActivationText")]
public static class Patch_EntityAlive_GetActivationText_Corpse
{
public static void Postfix(EntityAlive __instance, ref string __result)
{
if (!CorpseLoot.IsSearchable(__instance))
{
return;
}
EntityPlayerLocal player = GameManager.Instance?.World?.GetPrimaryPlayer();
if (player == null)
{
return;
}
PlayerActionsLocal input = player.playerInput;
string key = input.Activate.GetBindingXuiMarkupString() +
input.PermanentActions.Activate.GetBindingXuiMarkupString();
__result = string.Format(Localization.Get("lootTooltipNew"), key,
Localization.Get(__instance.LocalizedEntityName));
}
}
/// <summary>Запускает обыск - НЕ мгновенно, а с трёхсекундным круговым индикатором (указание
/// 2026-09-18). Индикатор рисует сама игра: XUiC_Timer.OpenTimer, тот же механизм, что крутится
/// при снятии верстака, и знакомый игроку без объяснений.
///
/// ПОЧЕМУ НЕ ЧЕРЕЗ activateTime У КОМАНДЫ. У EntityActivationCommand есть поле activateTime, и
/// Entity.ActivateEntityCommand действительно открывает по нему таймер - но по завершении он
/// дёргает ИГРОВОЕ СОБЫТИЕ из entity_class (`onActivateEvent`), а не наш код, и запускается
/// только если у команды заполнено eventName. Нам нужен свой обработчик в конце, поэтому таймер
/// открывается вручную, а не описанием команды.
///
/// CloseOnHit=true: если по игроку ударили посреди обыска, полоса закрывается и труп остаётся
/// нетронутым. Обыскивать труп посреди боя не должно быть бесплатно.</summary>
[HarmonyPatch(typeof(EntityAlive), "OnEntityActivated")]
public static class Patch_EntityAlive_OnEntityActivated_Corpse
{
public static void Postfix(EntityAlive __instance, EntityActivationCommand _command,
EntityPlayerLocal _playerFocusing)
{
if (_command.commandId != CorpseLoot.SearchCommand || !CorpseLoot.IsSearchable(__instance))
{
return;
}
if (_playerFocusing == null || _playerFocusing.PlayerUI == null)
{
CorpseLoot.SearchCorpse(__instance);
return;
}
EntityAlive corpse = __instance;
TimerEventData timer = new TimerEventData
{
Data = corpse,
CloseOnHit = true,
};
timer.FullTimeFinishEvent += delegate
{
// Повторная проверка на финише: за три секунды труп мог истлеть, а игрок - уйти.
if (CorpseLoot.IsSearchable(corpse))
{
CorpseLoot.SearchCorpse(corpse);
}
};
XUiC_Timer.OpenTimer(_playerFocusing.PlayerUI.xui, CorpseLoot.SearchSeconds, timer);
Debug.Log("[NecromancerTome] CorpseLoot: начат обыск трупа " + corpse.entityId +
", " + CorpseLoot.SearchSeconds + " с");
}
}
}
+151
View File
@@ -0,0 +1,151 @@
using System;
using System.IO;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// ПРОВЕРКА ВЕРСИИ ИГРЫ, 1.4.2 (2026-09-24). Повод: 1.4.1 на игре 3.3.0 experimental молча
/// потеряла содержимое Пространственного хранилища - разбор в BACKLOG.md, «Игра 3.3.0
/// experimental». Решение пользователя: при несовместимой версии мод показывает в главном меню
/// окно «мод несовместим, обновите» с ЕДИНСТВЕННОЙ кнопкой «Выйти из игры» - без «играть всё
/// равно».
///
/// ПОЧЕМУ ЧИТАЕТСЯ cVersionInformation, А НЕ cVersionMinor. cVersionMinor - const, компилятор
/// вписывает её значение (20) прямо в нашу DLL, и на 3.3 она всё равно «видела» бы 20.
/// cVersionInformation - static readonly, её значение берётся из игры при запуске.
///
/// ПОЧЕМУ Minor / 10. Игра хранит версию двузначным числом: 3.2.0 = 20, 3.2.1 = 21, 3.3.0 = 30
/// (VersionInformation печатает его как {Minor/10}.{Minor%10}). Проверка по десяткам пропускает
/// любые хотфиксы 3.2.x и не пропускает 3.3 и всё, что будет после.
///
/// ВСЁ, ЧЕМ ПОЛЬЗУЕТСЯ ЭТОТ ФАЙЛ, СВЕРЕНО С ДЕКОМПИЛЯЦИЕЙ 3.3.0 (build 25485021): Constants,
/// VersionInformation, XUiC_MainMenu.OnOpen, XUiC_MessageBoxWindowGroup (класс совпадает целиком),
/// Mod.VersionString. Иначе сборка под 3.2 упала бы на 3.3 раньше, чем показала окно.
///
/// ОТЛАДКА НА 3.2: если в папке мода лежит файл FORCE_VERSION_GUARD, мод ведёт себя так, будто
/// версия несовместима. В релизный архив он не попадает - состав архива задан явным списком.
/// </summary>
public static class GameVersionGuard
{
/// <summary>Под какую версию собран мод: Major и десяток Minor (3.2.x = 3 и 2).</summary>
public const int SupportedMajor = 3;
public const int SupportedMinorTens = 2;
public const string SupportedText = "3.2";
private static bool? supported;
public static bool IsSupported
{
get
{
if (supported == null)
{
supported = Check();
}
return supported.Value;
}
}
public static string GameVersionText
{
get
{
VersionInformation v = Constants.cVersionInformation;
return v != null ? v.LongStringNoBuild : "?";
}
}
private static bool Check()
{
if (ForcedByFile())
{
Debug.LogWarning("[NecromancerTome] GameVersionGuard: FORCE_VERSION_GUARD file present - acting as if the game version were unsupported");
return false;
}
VersionInformation v = Constants.cVersionInformation;
bool ok = v != null && v.Major == SupportedMajor && v.Minor / 10 == SupportedMinorTens;
Debug.Log("[NecromancerTome] GameVersionGuard: game " + GameVersionText + " (Major " + (v != null ? v.Major : -1) +
", Minor " + (v != null ? v.Minor : -1) + "), mod built for " + SupportedText + " -> " +
(ok ? "supported" : "NOT SUPPORTED"));
return ok;
}
private static bool ForcedByFile()
{
return DebugFileExists("FORCE_VERSION_GUARD");
}
/// <summary>Отладка сохранения хранилища байт в байт на 3.2: при файле FORCE_VAULT_UNREAD
/// в папке мода хранилище из сейва не разбирается, а проходит всю цепочку сохранения
/// нетронутым - ровно как на несовместимой версии, но без окна с выходом, так что в мир
/// можно зайти и сохраниться.</summary>
public static bool ForceVaultUnread
{
get { return DebugFileExists("FORCE_VAULT_UNREAD"); }
}
private static bool DebugFileExists(string _name)
{
try
{
string folder = ModEntry.Instance != null ? ModEntry.Instance.Path : null;
return folder != null && File.Exists(Path.Combine(folder, _name));
}
catch (Exception)
{
return false;
}
}
public static void ShowIncompatibleWindow(XUi _xui)
{
string modVersion = ModEntry.Instance != null ? ModEntry.Instance.VersionString : "?";
string text = string.Format(Localization.Get("necroVersionGuardText"), modVersion, SupportedText, GameVersionText);
XUiC_MessageBoxWindowGroup.ShowCustom(_xui,
Localization.Get("necroVersionGuardTitle"),
text,
"",
delegate(XUiC_MessageBoxWindowGroup _box)
{
_box.buttons[0].DefaultConfirm("necroVersionGuardQuit", delegate
{
Debug.Log("[NecromancerTome] GameVersionGuard: player pressed Quit");
Application.Quit();
});
},
_openMainMenuOnClose: false,
_modal: true,
_buttonOnOutsideClick: -1,
// Проверено в игре 2026-09-25: Esc модальное окно не закрывает - оно остаётся на
// экране, и уйти можно только кнопкой. Это и нужно. _buttonOnExternalClose: 0 - на
// случай, если игра всё же закроет окно извне: тогда это тоже выход.
_buttonOnExternalClose: 0);
Debug.LogWarning("[NecromancerTome] GameVersionGuard: incompatible game version, quit window shown");
}
}
/// <summary>Главное меню: здесь показывается окно, и здесь же сбрасываются непрочитанные байты
/// хранилища - возврат в меню значит, что прежний мир закрыт, и его данные не должны перейти к
/// персонажу следующего.</summary>
[HarmonyPatch(typeof(XUiC_MainMenu), "OnOpen")]
public static class Patch_XUiC_MainMenu_OnOpen_VersionGuard
{
public static void Postfix(XUiC_MainMenu __instance)
{
try
{
SpatialVaultPersistence.UnreadableByEntity.Clear();
if (!GameVersionGuard.IsSupported)
{
GameVersionGuard.ShowIncompatibleWindow(__instance.xui);
}
}
catch (Exception e)
{
Debug.LogError("[NecromancerTome] GameVersionGuard: main menu postfix failed: " + e);
}
}
}
}
+33 -1
View File
@@ -19,6 +19,36 @@ namespace NecromancerTome
{ {
Instance = _modInstance; Instance = _modInstance;
var harmony = new Harmony("necromancertome.harmony"); var harmony = new Harmony("necromancertome.harmony");
// НЕСОВМЕСТИМАЯ ВЕРСИЯ ИГРЫ (1.4.2, см. GameVersionGuard.cs): ставятся только окно в
// главном меню и сохранение хранилища байт в байт - каждое отдельно, чтобы отказ одного
// не помешал другим. Остальной код мода, написанный под другую версию, не запускается
// вовсе: игра всё равно закроется из главного меню.
if (!GameVersionGuard.IsSupported)
{
System.Type[] safetyPatches =
{
typeof(Patch_XUiC_MainMenu_OnOpen_VersionGuard),
typeof(Patch_PlayerDataFile_Read_SpatialVault),
typeof(Patch_PlayerDataFile_ToPlayer_SpatialVault),
typeof(Patch_PlayerDataFile_FromPlayer_SpatialVault),
typeof(Patch_PlayerDataFile_Write_SpatialVault),
};
foreach (System.Type patch in safetyPatches)
{
try
{
harmony.CreateClassProcessor(patch).Patch();
Debug.Log("[NecromancerTome] ModEntry: unsupported game version - safety patch " + patch.Name + " applied");
}
catch (System.Exception e)
{
Debug.LogError("[NecromancerTome] ModEntry: safety patch " + patch.Name + " failed: " + e);
}
}
return;
}
harmony.PatchAll(Assembly.GetExecutingAssembly()); harmony.PatchAll(Assembly.GetExecutingAssembly());
PetFollowPatch.Init(); PetFollowPatch.Init();
// SpatialVaultPersistence needs NO Init(): it is four Harmony postfixes that PatchAll // SpatialVaultPersistence needs NO Init(): it is four Harmony postfixes that PatchAll
@@ -41,7 +71,9 @@ namespace NecromancerTome
// that method at all - rules "patch never applied" in or out without waiting on // that method at all - rules "patch never applied" in or out without waiting on
// another in-game death. // another in-game death.
VerifyPrefixAttached(typeof(EntityAlive), "dropItemOnDeath"); VerifyPrefixAttached(typeof(EntityAlive), "dropItemOnDeath");
VerifyPrefixAttached(typeof(Entity), "DropBagServer"); // Патч на Entity.DropBagServer снят 2026-09-18 вместе с переделкой мешка «Жертвы»:
// он подменял собой ОБЫЧНЫЙ мешок, а тот теперь должен доставаться игроку как есть.
// Проверять нечего - см. VictimPatch.cs.
// Добавлено 2026-09-17 вместе с правкой "подчинённые не дерутся между собой" // Добавлено 2026-09-17 вместе с правкой "подчинённые не дерутся между собой"
// (CharmPatch.cs). Метод EAITarget.check в исходнике protected и назван со строчной // (CharmPatch.cs). Метод EAITarget.check в исходнике protected и назван со строчной
+1 -1
View File
@@ -90,7 +90,7 @@ namespace NecromancerTome
return; return;
} }
bool isPortalChannel = entity.Buffs.HasBuff(Patch_ItemActionEat_ExecuteAction_PortalStones.ChannelBuffName); bool isPortalChannel = entity.Buffs.HasBuff(Patch_ItemActionEat_ExecuteAction_PortalStones.ChannelBuffName);
bool isVictim = entity.Buffs.HasBuff(Patch_Entity_DropBagServer_VictimBag.VictimBuffName); bool isVictim = entity.Buffs.HasBuff(Patch_EntityAlive_dropItemOnDeath_VictimBag.VictimBuffName);
bool isCharm = entity.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName); bool isCharm = entity.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName);
if (!isPortalChannel && !isVictim && !isCharm) if (!isPortalChannel && !isVictim && !isCharm)
{ {
+9 -3
View File
@@ -322,13 +322,19 @@ namespace NecromancerTome
return; return;
} }
timer = 0f; timer = 0f;
if (TrackedPets.Count == 0) World world = GameManager.Instance != null ? GameManager.Instance.World : null;
if (world == null)
{ {
return; return;
} }
World world = GameManager.Instance != null ? GameManager.Instance.World : null; // Трупы, которые не удалось перекрасить в момент смерти (модель бывает ещё не
if (world == null) // собрана) - добиваем здесь: тик всё равно уже идёт. См. CorpseLootPatch.cs.
// ВЫШЕ проверки на пустой список питомцев намеренно: трупы помечает Нож некроманта,
// и к тому, есть ли у игрока сейчас призванный питомец, они отношения не имеют.
CorpseLoot.RetryPendingTints(world);
if (TrackedPets.Count == 0)
{ {
return; return;
} }
+7
View File
@@ -116,6 +116,13 @@ namespace NecromancerTome
public static void OpenVault(EntityPlayerLocal player) public static void OpenVault(EntityPlayerLocal player)
{ {
// Хранилище из сейва не прочитано - открывать нечего, а пустое новое затёрло бы его при
// сохранении. См. SpatialVaultPersistence.UnreadableByEntity.
if (SpatialVaultPersistence.IsLocked(player))
{
GameManager.ShowTooltip(player, "braceletSpatialVaultUnreadable");
return;
}
ProgressionValue progressionValue = player.Progression?.GetProgressionValue(NecromancySkillName); ProgressionValue progressionValue = player.Progression?.GetProgressionValue(NecromancySkillName);
int level = progressionValue != null ? progressionValue.Level : 0; int level = progressionValue != null ? progressionValue.Level : 0;
int slotCount = Mathf.RoundToInt(level / 10f); int slotCount = Mathf.RoundToInt(level / 10f);
+83 -1
View File
@@ -89,6 +89,37 @@ namespace NecromancerTome
public class VaultSlot public class VaultSlot
{ {
public Bag Bag; public Bag Bag;
/// <summary>Байты хранилища, которые эта сборка не прочитала: версия игры не та, под
/// которую собран мод, или разбор упал. Пишутся обратно в файл байт в байт.</summary>
public byte[] RawPayload;
}
/// <summary>ХРАНИЛИЩЕ НЕ ДОЛЖНО ТЕРЯТЬСЯ НА ЧУЖОЙ ВЕРСИИ ИГРЫ, 1.4.2 (2026-09-24). В 1.4.1 на
/// игре 3.3 разбор хранилища падал, и при сохранении мод писал вместо него ПУСТОЙ маркер -
/// вещи пропадали навсегда. Теперь непрочитанные байты проходят всю цепочку сохранения
/// нетронутыми: Read кладёт их в слот файла, ToPlayer переносит к игроку (сюда), FromPlayer
/// отдаёт новому файлу (игра на каждом сохранении создаёт НОВЫЙ PlayerDataFile), Write
/// пишет их как есть. Сборка под нужную версию потом прочтёт их обычным путём.
///
/// Ключ - entityId. Очищается при каждом открытии главного меню (GameVersionGuard.cs), чтобы
/// байты одного мира не перешли к персонажу другого.
///
/// Пока байты здесь, хранилище не открывается и не пополняется (SpatialVaultPatch.OpenVault,
/// SpatialVaultPickup.Begin): вещи, положенные в новое пустое хранилище, всё равно не
/// сохранились бы - запись отдаёт приоритет нетронутым байтам.</summary>
public static readonly System.Collections.Generic.Dictionary<int, byte[]> UnreadableByEntity =
new System.Collections.Generic.Dictionary<int, byte[]>();
public static bool IsLocked(EntityPlayer _player)
{
return _player != null && UnreadableByEntity.ContainsKey(_player.entityId);
}
public static void AttachRaw(PlayerDataFile _file, byte[] _raw)
{
AttachedVaults.Remove(_file);
AttachedVaults.Add(_file, new VaultSlot { RawPayload = _raw });
} }
/// <summary>Vault attached to a PlayerDataFile while it is being written, read or /// <summary>Vault attached to a PlayerDataFile while it is being written, read or
@@ -215,6 +246,17 @@ namespace NecromancerTome
{ {
return; return;
} }
if (SpatialVaultPersistence.UnreadableByEntity.TryGetValue(_player.entityId, out byte[] raw))
{
SpatialVaultPersistence.AttachRaw(__instance, raw);
string rawLine = "FromPlayer entity " + _player.entityId + " - carrying " + raw.Length + " unread byte(s) unchanged";
if (rawLine != SpatialVaultPersistence.lastSaveLogged)
{
SpatialVaultPersistence.lastSaveLogged = rawLine;
Debug.Log("[NecromancerTome] SpatialVaultPersistence: " + rawLine);
}
return;
}
Patch_ItemActionEat_ExecuteAction_SpatialVault.PlayerVaults.TryGetValue(_player.entityId, out Bag bag); Patch_ItemActionEat_ExecuteAction_SpatialVault.PlayerVaults.TryGetValue(_player.entityId, out Bag bag);
string source = bag != null ? "session cache" : null; string source = bag != null ? "session cache" : null;
if (bag == null && _player is EntityPlayerLocal && SpatialVaultPersistence.LastLoadedVault != null) if (bag == null && _player is EntityPlayerLocal && SpatialVaultPersistence.LastLoadedVault != null)
@@ -260,6 +302,21 @@ namespace NecromancerTome
string note; string note;
Bag bag; Bag bag;
if (SpatialVaultPersistence.AttachedVaults.TryGetValue(__instance, out SpatialVaultPersistence.VaultSlot rawSlot) &&
rawSlot.RawPayload != null)
{
SpatialVaultPersistence.UnreadableByEntity[_player.entityId] = rawSlot.RawPayload;
Patch_ItemActionEat_ExecuteAction_SpatialVault.PlayerVaults.Remove(_player.entityId);
if (isLocal)
{
SpatialVaultPersistence.LastLoadedVault = null;
}
Debug.LogWarning("[NecromancerTome] SpatialVaultPersistence: ToPlayer entity " + _player.entityId +
" - vault kept as " + rawSlot.RawPayload.Length + " unread byte(s); it stays locked and is saved unchanged");
return;
}
SpatialVaultPersistence.UnreadableByEntity.Remove(_player.entityId);
if (SpatialVaultPersistence.AttachedVaults.TryGetValue(__instance, out SpatialVaultPersistence.VaultSlot slot)) if (SpatialVaultPersistence.AttachedVaults.TryGetValue(__instance, out SpatialVaultPersistence.VaultSlot slot))
{ {
bag = slot.Bag; bag = slot.Bag;
@@ -309,6 +366,11 @@ namespace NecromancerTome
try try
{ {
SpatialVaultPersistence.AttachedVaults.TryGetValue(__instance, out SpatialVaultPersistence.VaultSlot slot); SpatialVaultPersistence.AttachedVaults.TryGetValue(__instance, out SpatialVaultPersistence.VaultSlot slot);
if (slot != null && slot.RawPayload != null)
{
SpatialVaultBlobIO.Write(_bw, slot.RawPayload);
return;
}
Bag bag = slot != null ? slot.Bag : null; Bag bag = slot != null ? slot.Bag : null;
SpatialVaultBlobIO.Write(_bw, SpatialVaultPersistence.BuildPayload(bag)); SpatialVaultBlobIO.Write(_bw, SpatialVaultPersistence.BuildPayload(bag));
if (bag == null) if (bag == null)
@@ -346,7 +408,27 @@ namespace NecromancerTome
return; return;
} }
Bag bag = SpatialVaultPersistence.ParsePayload(payload); if (!GameVersionGuard.IsSupported || GameVersionGuard.ForceVaultUnread)
{
SpatialVaultPersistence.AttachRaw(__instance, payload);
Debug.LogWarning("[NecromancerTome] SpatialVaultPersistence: Read - game version not supported, " +
payload.Length + " byte(s) kept unread and will be saved unchanged");
return;
}
Bag bag;
try
{
bag = SpatialVaultPersistence.ParsePayload(payload);
}
catch (Exception parseError)
{
// Упал сам разбор (на 3.3 это был MissingMethodException на Bag.Read). Байты не
// выбрасываем: они уйдут в файл как были.
SpatialVaultPersistence.AttachRaw(__instance, payload);
Debug.LogError("[NecromancerTome] SpatialVaultPersistence: Read - could not parse the vault, " +
payload.Length + " byte(s) kept unread and will be saved unchanged: " + parseError);
return;
}
// Attached even when null: a blob that says "no vault" IS an answer, and ToPlayer // Attached even when null: a blob that says "no vault" IS an answer, and ToPlayer
// uses it to clear a stale cache when a different save is loaded. // uses it to clear a stale cache when a different save is loaded.
SpatialVaultPersistence.Attach(__instance, bag); SpatialVaultPersistence.Attach(__instance, bag);
+5
View File
@@ -599,6 +599,11 @@ namespace NecromancerTome
/// the same way in both places.</summary> /// the same way in both places.</summary>
public static Bag GetVault(EntityPlayerLocal _player) public static Bag GetVault(EntityPlayerLocal _player)
{ {
if (SpatialVaultPersistence.IsLocked(_player))
{
Deny(_player, "braceletSpatialVaultUnreadable");
return null;
}
ProgressionValue progressionValue = _player.Progression?.GetProgressionValue( ProgressionValue progressionValue = _player.Progression?.GetProgressionValue(
Patch_ItemActionEat_ExecuteAction_SpatialVault.NecromancySkillName); Patch_ItemActionEat_ExecuteAction_SpatialVault.NecromancySkillName);
int level = progressionValue != null ? progressionValue.Level : 0; int level = progressionValue != null ? progressionValue.Level : 0;
+123 -59
View File
@@ -4,97 +4,161 @@ using UnityEngine;
namespace NecromancerTome namespace NecromancerTome
{ {
/// <summary> /// <summary>
/// Necromancer's Knife (BACKLOG.md item 5, user request 2026-08-28): a zombie hit by the /// Нож некроманта (BACKLOG.md, п.5): зомби, задетый ножом, носит buffNecroVictim (buffs.xml) до
/// knife carries buffNecroVictim (buffs.xml) until it dies. On death, it's guaranteed to drop /// самой смерти. На смерти он роняет мешок «Жертва» (EntityLootContainerVictim в
/// a green "Жертва" loot bag (EntityLootContainerVictim in entityclasses.xml) instead of /// entityclasses.xml).
/// whatever the normal random loot roll would have given it.
/// ///
/// Two patch points, found the hard way (2026-08-28, "лута всё ещё нет" after confirming via /// ПОВЕДЕНИЕ ИСПРАВЛЕНО 2026-09-18, И ЭТО ВОЗВРАТ К ТОМУ, О ЧЁМ ДОГОВАРИВАЛИСЬ. Баг-репорт
/// the CharmPatch.cs-style AddBuff log that the debuff itself WAS being applied fine): /// пользователя: «мы договаривались, что он выпадает только если не должен был выпасть мешок с
/// лутом. Но похоже падает только мешок жертвы».
/// ///
/// 1. Patch_EntityAlive_dropItemOnDeath_VictimBag - the actual gate. Confirmed by /// Так и было. Прежняя версия состояла из двух префиксов, и оба возвращали false:
/// decompiling EntityAlive.dropItemOnDeath(): it only calls DropBagServer() at all if
/// `lootDropProb > rand.RandomFloat` passes first - vanilla zombies have LootDropProb
/// around .04 (4%), so DropBagServer() simply never runs for ~96% of deaths. The first
/// version of this file only patched DropBagServer() itself, which was correct once
/// inside it but never got a chance to run for most kills - confirmed by the "AddBuff
/// Added" log firing repeatedly while the "zombie died" log from the DropBagServer patch
/// never fired even once for a real, logged kill. This Prefix on dropItemOnDeath() calls
/// DropBagServer() directly for a Victim-tagged zombie, bypassing the probability roll
/// entirely, then skips the rest of the original method (the only other thing it does -
/// dropping a non-AI entity's own inventory - never applies to a zombie anyway, hasAI is
/// always true for those).
/// 2. Patch_Entity_DropBagServer_VictimBag - decides WHICH bag. Confirmed by decompiling
/// Entity.DropBagServer(): it reads entityClass.lootDrops/LootDropPick(rand) - a STATIC
/// per-species value from the zombie's own entity_class (XML's LootDropEntityClass
/// property), not anything a live buff or CVar can influence declaratively (unlike the
/// knife's damage, which only needed a CVar the passive_effect system already reads
/// live) - this needed an actual Harmony patch, not an XML trick.
/// ///
/// Both are Prefixes returning false: they fully replace what they intercept rather than /// 1. на dropItemOnDeath - при дебафе звала DropBagServer() НАПРЯМУЮ, минуя ванильный
/// running alongside it. /// бросок вероятности, и обрывала оригинал;
/// 2. на DropBagServer - при дебафе спавнила «Жертву» ВМЕСТО обычного мешка.
///
/// Вместе это означало: помеченный зомби роняет «Жертву» ВСЕГДА и обычный мешок НИКОГДА. То
/// есть нож не добавлял добычу, а подменял её - и в редких 4% случаев, когда игроку и так
/// причитался нормальный мешок, он его терял.
///
/// СТАЛО (уточнено пользователем в тот же день - «а можно сделать так, чтобы в случае когда
/// положен лут, падало два мешка»): «Жертва» не заменяет обычный мешок и не ждёт его отсутствия,
/// а ДОБАВЛЯЕТСЯ. Ванильный бросок делается как есть (формула скопирована из
/// EntityAlive.dropItemOnDeath дословно, включая модификатор от оружия убийцы и множитель
/// LootContainer.LootBagChance), и дальше:
///
/// бросок прошёл -> обычный мешок рядом И добыча в трупе;
/// бросок не прошёл -> только добыча в трупе.
///
/// То есть помеченный ножом зомби никогда не оставляет игрока ни с чем, а в удачном случае
/// даёт вдвое больше обычного. Это осознанно щедро: метку ещё надо поставить ножом, которым
/// по зомби надо попасть.
///
/// УТОЧНЕНО В ТОТ ЖЕ ДЕНЬ: «Жертва» больше не падает мешком в момент смерти - она остаётся
/// ВНУТРИ ТРУПА, который перекрашивается, перестаёт исчезать и получает команду «обыскать»
/// (CorpseLootPatch.cs). Мешок появляется только по обыску. Сам бросок и вся арифметика ниже
/// от этого не изменились: они по-прежнему решают судьбу ОБЫЧНОГО мешка.
///
/// Второй префикс (на DropBagServer) УДАЛЁН ЦЕЛИКОМ - он теперь вреден: подменял бы и тот
/// обычный мешок, который мы как раз хотим оставить. Спавн «Жертвы» переехал сюда, в
/// SpawnVictimBag.
///
/// ПОЧЕМУ ВООБЩЕ НУЖЕН ПРЕФИКС, А НЕ ПОСТФИКС. Узнать «выпал ли мешок» после оригинала нечем:
/// dropItemOnDeath ничего не возвращает и следов не оставляет. Поэтому бросок повторяется
/// здесь, а оригинал не запускается. Ветка оригинала, которая при этом теряется, - выброс
/// собственного инвентаря для сущностей БЕЗ ИИ (`if (!hasAI)`), и к зомби она не относится
/// никогда; на всякий случай она всё равно отдана оригиналу проверкой hasAI ниже.
///
/// LootContainer.NoLoot УВАЖАЕТСЯ: если в настройках мира мешки выключены, «Жертва» тоже не
/// падает. Это мировая настройка игрока, а не место мода спорить с ней.
/// </summary> /// </summary>
[HarmonyPatch(typeof(EntityAlive), "dropItemOnDeath")] [HarmonyPatch(typeof(EntityAlive), "dropItemOnDeath")]
public static class Patch_EntityAlive_dropItemOnDeath_VictimBag public static class Patch_EntityAlive_dropItemOnDeath_VictimBag
{
public static bool Prefix(EntityAlive __instance)
{
// Diagnostic-only, added 2026-08-28: unconditional, before any branching, to answer
// definitively whether Harmony is even entering this method at all - "лута всё ещё
// нет" after the first fix, but with zero sign of even the unconditional part of this
// Prefix ever running (not even a false-branch silently returning - literally no log
// line at all), which is otherwise unexplained since decompiling
// EntityAlive.OnEntityDeath() confirms it calls dropItemOnDeath() directly,
// unconditionally, right after the exact "Entity X killed by Y" line seen in the log.
Debug.Log("[NecromancerTome] VictimPatch: dropItemOnDeath Prefix entered for " + __instance.entityId + " (" + __instance.GetType().Name + "), hasVictimBuff=" + (__instance.Buffs != null && __instance.Buffs.HasBuff(Patch_Entity_DropBagServer_VictimBag.VictimBuffName)));
if (__instance.Buffs == null || !__instance.Buffs.HasBuff(Patch_Entity_DropBagServer_VictimBag.VictimBuffName))
{
return true;
}
Debug.Log("[NecromancerTome] VictimPatch: " + __instance.entityId + " died carrying buffNecroVictim - forcing guaranteed bag, bypassing LootDropProb roll");
__instance.DropBagServer();
return false;
}
}
[HarmonyPatch(typeof(Entity), "DropBagServer")]
public static class Patch_Entity_DropBagServer_VictimBag
{ {
public const string VictimBuffName = "buffNecroVictim"; public const string VictimBuffName = "buffNecroVictim";
public const string VictimContainerClassName = "EntityLootContainerVictim"; public const string VictimContainerClassName = "EntityLootContainerVictim";
public static bool Prefix(Entity __instance) public static bool Prefix(EntityAlive __instance)
{ {
if (!(__instance is EntityAlive alive) || alive.Buffs == null || !alive.Buffs.HasBuff(VictimBuffName)) if (__instance.Buffs == null || !__instance.Buffs.HasBuff(VictimBuffName))
{ {
return true; return true;
} }
if (!SingletonMonoBehaviour<ConnectionManager>.Instance.IsServer || __instance is EntityLootContainer) if (!__instance.hasAI)
{
// Не наш случай: у оригинала для таких сущностей своя работа (выброс инвентаря),
// и дублировать её здесь незачем. Зомби сюда не попадают.
return true;
}
if (!SingletonMonoBehaviour<ConnectionManager>.Instance.IsServer)
{ {
// Same guard the original method opens with - not our place to override these cases.
return true; return true;
} }
// Ванильный расчёт, скопированный из EntityAlive.dropItemOnDeath дословно.
float prob = __instance.lootDropProb;
if ((bool)__instance.entityThatKilledMe)
{
prob = EffectManager.GetValue(PassiveEffects.LootDropProb,
__instance.entityThatKilledMe.inventory.holdingItemItemValue, prob,
__instance.entityThatKilledMe);
}
prob *= LootContainer.LootBagChance;
if (LootContainer.NoLoot)
{
Debug.Log("[NecromancerTome] VictimPatch: " + __instance.entityId +
" - мешки выключены настройками мира, «Жертва» не падает");
return false;
}
// Сам бросок печатается ВСЕГДА, и это не отладочный мусор, а ответ на вопрос, который
// уже задавали: «на 25 Арлин ни одного мешка с обычным лутом». У ванильного зомби
// LootDropProb = .04, то есть ноль мешков на два десятка убийств - исход с
// вероятностью около 40%, совершенно рядовой. Без этой строки отличить «не повезло»
// от «ролл сломан» можно только гаданием.
float roll = __instance.rand.RandomFloat;
Debug.Log("[NecromancerTome] VictimPatch: " + __instance.entityId + " - бросок на обычный мешок: шанс " +
prob.ToCultureInvariantString("0.000") + " против " + roll.ToCultureInvariantString("0.000") +
" -> " + (prob > roll ? "ВЫПАЛ" : "не выпал"));
if (prob > roll)
{
// Поле пишется ПЕРЕД вызовом намеренно, ровно как это делает сам оригинал:
// DropBagServer читает lootDropProb (`if (lootDropProb != 0f)`) и по нему решает,
// брать ли добычу из entityClass.lootDrops или из инвентаря. Посчитай мы в
// локальную и не запиши - редкий зомби с нулевым собственным lootDropProb, чей
// убийца поднял шанс оружием, пошёл бы не по той ветке.
__instance.lootDropProb = prob;
__instance.DropBagServer();
Debug.Log("[NecromancerTome] VictimPatch: " + __instance.entityId +
" - обычный мешок выпал по ролу; «Жертва» ждёт в трупе");
}
// «Жертва» БОЛЬШЕ НЕ ПАДАЕТ МЕШКОМ СРАЗУ (указание 2026-09-18): она остаётся внутри
// трупа, а труп становится обыскиваемым. Разбор целиком - в CorpseLootPatch.cs.
// Обычный мешок при этом по-прежнему выпадает по своему ролу и лежит рядом, так что
// прежняя договорённость «выпал рол - два источника добычи» сохраняется.
CorpseLoot.MarkCorpse(__instance);
return false;
}
/// <summary>Спавнит мешок «Жертва». Раньше это жило в префиксе на Entity.DropBagServer и
/// подменяло собой обычный мешок; теперь ничего не подменяет, поэтому тот патч снят.
///
/// _offsetAside - сдвинуть в сторону, потому что рядом уже лёг обычный мешок. Ваниль
/// кладёт свой ровно в GetPosition() + 0.9 по Y (декомпилировано, Entity.DropBagServer),
/// то есть без сдвига два мешка встали бы в одну точку и выглядели как один.</summary>
public static void SpawnVictimBag(EntityAlive _victim, bool _offsetAside)
{
int containerClassId = EntityClass.GetId(VictimContainerClassName); int containerClassId = EntityClass.GetId(VictimContainerClassName);
if (containerClassId == -1) if (containerClassId == -1)
{ {
Debug.LogWarning("[NecromancerTome] VictimPatch: entity class '" + VictimContainerClassName + "' not found, falling back to normal loot"); Debug.LogWarning("[NecromancerTome] VictimPatch: класс '" + VictimContainerClassName +
return true; "' не найден, мешок не выпадет");
return;
} }
Vector3 pos = __instance.GetPosition(); Vector3 pos = _victim.GetPosition();
pos.y += 0.9f; pos.y += 0.9f;
if (_offsetAside)
{
// Полметра вбок - достаточно, чтобы мешки читались как два и оба брались
// прицелом, и мало, чтобы «Жертва» уехала за стену или в соседнюю комнату.
pos += _victim.qrotation * new Vector3(0.6f, 0f, 0f);
}
Entity spawned = EntityFactory.CreateEntity(containerClassId, pos, Vector3.zero); Entity spawned = EntityFactory.CreateEntity(containerClassId, pos, Vector3.zero);
if (spawned is EntityLootContainer lootContainer) if (spawned is EntityLootContainer lootContainer)
{ {
GameManager.Instance.World.SpawnEntityInWorld(lootContainer); GameManager.Instance.World.SpawnEntityInWorld(lootContainer);
Debug.Log("[NecromancerTome] VictimPatch: " + __instance.entityId + " (victim) dropped guaranteed loot bag " + lootContainer.entityId); Debug.Log("[NecromancerTome] VictimPatch: " + _victim.entityId + " уронил «Жертву» " +
lootContainer.entityId + (_offsetAside ? " (вторым мешком, со сдвигом)" : " (единственным мешком)"));
} }
else else
{ {
Debug.LogWarning("[NecromancerTome] VictimPatch: created entity for '" + VictimContainerClassName + "' wasn't an EntityLootContainer"); Debug.LogWarning("[NecromancerTome] VictimPatch: созданная сущность для '" +
VictimContainerClassName + "' не оказалась EntityLootContainer");
} }
return false;
} }
} }
} }
+1 -1
View File
@@ -4,6 +4,6 @@
<DisplayName value="Necromancer's Tome" /> <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, 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." /> <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" /> <Author value="Alex Cube" />
<Version value="1.4.0" /> <Version value="1.4.2" />
<Website value="https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/" /> <Website value="https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/" />
</xml> </xml>
+20
View File
@@ -0,0 +1,20 @@
NEW: the Rat Spirit - a pet you can summon at Necromancy 3 (60 zombies put to rest), hours before anything else. It barely hurts (5 damage), but its bite slows the target, makes it bleed and puts a spirit mark on it: the marked zombie shows on your compass and map and takes 25% more damage from everything. It heals by gnawing on corpses. Thanks to Derizor for asking for an early summon.
NEW: pet orders. Use a pet's scroll again while the pet is alive and it attacks whatever is under your crosshair. Traders and players are never targeted.
CHANGED: all pets were rewritten around one behaviour: they follow you, pick no fights on their own and wait for your order. They no longer break blocks, wander off or eat corpses, and they walk through what they cannot path around instead of getting stuck.
NEW: your pets' kills count towards Necromancy, including kills finished by the Rat Spirit's bleeding.
NEW: pets show on the map and compass - yellow for your pet, brown for prey it has marked.
CHANGED: the summon thresholds are spread across the whole scale - Rat Spirit 60, Grave Vulture 500, Zombie Dog 1300, Zombie Bear 2000, Zombie Wolf 4000 zombies.
CHANGED: pet damage is a ladder now - vulture 20, dog 35, bear 45, wolf 60. The bear and the wolf are 80% of vanilla size and keep one block further back.
CHANGED: the Zombie Griff is now the Grave Vulture, a real flying vulture that stays near you. The Beetles of the Lord are now the Pharaoh's Swarm in all 13 languages - the old name read as something else in several of them. Item ids are unchanged, so nothing in your save is lost.
NEW: the Zombie Dog's eyes glow purple.
FIXED: pets needed a held click to summon and sometimes two tries to dismiss. Both now happen on a single press.
FIXED: a pet could launch its owner into the air by standing under them, and the Rat Spirit could fall through the world.
FIXED: the Grave Vulture roared like a lion - the sound was left over from when it was a mountain lion.
FIXED: the white book badge is gone from every summon scroll. It is the game's mark for a recipe to learn, and these scrolls are items you use.
NEW: the Necromancer's Knife has its own model at last - a straight amputation-knife blade carved from a thighbone, a bone guard, a rope-wrapped grip and glowing runes on both sides of the blade - and a new icon to match. Thanks to Scarlettdruid for pointing out that the old one did not look like its icon.
CHANGED: a Victim's loot moved into the corpse itself. A zombie that dies marked as a Victim leaves a red corpse that stays for an hour; search it and it turns into the Victim's bag.
NEW: the Scroll of the Rat Spirit has its own icon.
CHANGED: beds that become a respawn point (beds and bunk beds) can now be picked up like the other beds - placed again by your own hands, the bed becomes your respawn point, like a bedroll.
A NOTE ON THE 1.3.0 SCALE: Necromancy rises once every 20 zombies put to rest, so "1 of 250" in the skill panel is a level, not a kill count. The purple bar above the toolbelt shows the progress inside the level. Coming from 1.0.1, your level is recomputed from your kills and looks twenty times smaller - the kills themselves are all still there.
Not in this version: an option to keep vanilla traders (thanks zomb1 and sanovskiy) and a cap on zombies held by the Pyramid of Spirits (thanks Savion). Both are noted.
Single-player. The mod ships Harmony libraries, so EasyAntiCheat must be off.
+5
View File
@@ -0,0 +1,5 @@
FIXED: the frame rate drop that grew the longer you played (thanks sanovskiy, Kriloko and Megaladon43 for the reports). Every red Victim corpse painted its own full-size copy of the zombie's textures and never released them, together with copies of its materials - within a couple of hours that was gigabytes of video memory. The painted textures are now small and compressed, shared between corpses, and everything is released as soon as the corpse is gone.
CHANGED: an unsearched Victim corpse now stays for 5 minutes instead of an hour.
CHANGED: the Necromancy tracker refreshes the knife's damage once a second instead of 10 times a second (thanks zomb1 for pointing at it). Nothing changes in play: the damage only moves when you kill.
IMPORTANT: this version is for game version 3.2. It is NOT compatible with 3.3 experimental (latest_experimental): there the Spatial Vault stops saving its contents, so items in it are lost, and the Pyramid of Heresy fails to load. Please stay on 3.2 with this mod for now; a 3.3 version will be released separately.
Drop-in over 1.4.0, saves carry over. Single-player. The mod ships Harmony libraries, so EasyAntiCheat must be off.
+4
View File
@@ -0,0 +1,4 @@
NEW: the mod now checks the game version at start. This build is for 7 Days to Die 3.2.x; on any other version (3.3 experimental included) the main menu shows a window saying the mod is incompatible and asking you to update it, with a single Quit game button. This is to stop what happened with 1.4.1 on 3.3: the Spatial Vault could not be read there and was saved empty, so its items were lost.
NEW: the Spatial Vault can no longer be wiped by a version it cannot read. If its saved contents cannot be read, they are kept untouched and written back byte for byte on every save, and the vault stays closed with a message instead of opening empty. Installing the right build of the mod brings the items back.
The new window and the vault message are translated into all 13 languages of the mod.
Drop-in over 1.4.1, saves carry over. For game version 3.2 only; a 3.3 version will come separately. Single-player. The mod ships Harmony libraries, so EasyAntiCheat must be off.
+1 -1
View File
@@ -30,7 +30,7 @@ To uninstall, delete the folder. The mod adds items and one block, so a save tha
[b]Turn the horde on itself.[/b] The [b]Spirit Stone[/b] is thrown; the zombie it touches switches sides and starts tearing into the others instead of you. The [b]Scroll of Deviation[/b] does the same to everything in the blast radius at once. [b]Turn the horde on itself.[/b] The [b]Spirit Stone[/b] is thrown; the zombie it touches switches sides and starts tearing into the others instead of you. The [b]Scroll of Deviation[/b] does the same to everything in the blast radius at once.
[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]A blade that grows with you.[/b] The [b]Necromancer's Knife[/b] is carved from a thighbone - a straight amputation-knife edge, a bone guard, a rope-wrapped grip and runes that glow. It 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: its corpse turns red and stays for five minutes, and searching it leaves a Victim's bag of necromantic ingredients. [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]Pharaoh’s 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]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]Pharaoh’s 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.
+1
View File
@@ -0,0 +1 @@
Pets rewritten: they follow you, wait and attack on command, and their kills count. New early pet, the Rat Spirit (60 zombies). The Necromancer's Knife gets its own model and icon. Victim corpses turn red and are searched for the bag. Drop-in over 1.3.0.
+1
View File
@@ -0,0 +1 @@
Fixes the FPS drop: Victim corpses leaked video memory until restart. Unsearched corpses now stay 5 minutes. For game 3.2 only - NOT compatible with 3.3 experimental (Spatial Vault contents get lost). Drop-in over 1.4.0.
+1
View File
@@ -0,0 +1 @@
Safety update: on an incompatible game version (e.g. 3.3 experimental) the mod asks you to update it and quits, and the Spatial Vault is never wiped - unreadable contents are kept as they are. For game 3.2. Drop-in over 1.4.1.
Binary file not shown.
Binary file not shown.
+82 -26
View File
@@ -2,7 +2,7 @@
*English version below — scroll past the Russian half.* *English version below — scroll past the Russian half.*
**Версия 1.3.0** — для 7 Days to Die 3.2. Автор: Alex Cube. **Версия 1.4.2** — для 7 Days to Die 3.2 (с 3.3 experimental не совместима и сама об этом скажет). Автор: Alex Cube.
- Страница мода: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/ - Страница мода: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547 - Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
@@ -54,10 +54,11 @@ HUD за этим следят два индикатора: череп в ста
тот переходит на вашу сторону и начинает атаковать других зомби вместо вас. тот переходит на вашу сторону и начинает атаковать других зомби вместо вас.
- **Свиток девиации** — тот же эффект, но мощнее: при броске переманивает на вашу сторону сразу - **Свиток девиации** — тот же эффект, но мощнее: при броске переманивает на вашу сторону сразу
всех зомби в области взрыва, а не одного. Расходуется при использовании. всех зомби в области взрыва, а не одного. Расходуется при использовании.
- **Нож некроманта** — почерневший костяной клинок. Урон растёт вместе с навыком Некромантии: - **Нож некроманта** — клинок, выточенный из бедренной кости: прямое лезвие, как у ампутационного
почти бесполезен в неопытных руках, смертоносен у прокачанного игрока. Лечит владельца на ножа, костяная гарда, рукоять в верёвочной обмотке и светящиеся руны. Урон растёт вместе с
половину нанесённого урона и метит раненого зомби как Жертву — тот при смерти гарантированно навыком Некромантии: почти бесполезен в неопытных руках, смертоносен у прокачанного игрока.
оставляет особый мешок с ингредиентами. Лечит владельца на половину нанесённого урона и метит раненого зомби как Жертву — труп такого
зомби краснеет и лежит час; при обыске он исчезает, оставляя мешок Жертвы с ингредиентами.
- **Шесть модов только для Ножа некроманта** — обычные моды для ножей в это оружие не встают, а - **Шесть модов только для Ножа некроманта** — обычные моды для ножей в это оружие не встают, а
эти не встают ни во что другое: **Слёзы мертвеца** (2 воды с каждого убитого клинком зомби), эти не встают ни во что другое: **Слёзы мертвеца** (2 воды с каждого убитого клинком зомби),
**Пир падальщика** (2 еды за труп), **Могильный покой** (защита от перегрева и переохлаждения, **Пир падальщика** (2 еды за труп), **Могильный покой** (защита от перегрева и переохлаждения,
@@ -140,8 +141,8 @@ HUD за этим следят два индикатора: череп в ста
## По мелочи ## По мелочи
- Стартовая записка при открытии тоже ставит игру на паузу и проигрывает короткий флэшбек. - Стартовая записка при открытии тоже ставит игру на паузу и проигрывает короткий флэшбек.
- Некоторые декоративные блоки (кровати, кулеры, картонные коробки) можно разобрать удержанием, - Некоторые блоки (кровати, кулеры, картонные коробки) можно разобрать удержанием, как верстак.
как верстак. Кровать, поставленная после этого своими руками, становится точкой возрождения, как спальник.
- Все торговцы выглядят иначе: чёрно-белые, полупрозрачные и матовые. Некромант имеет дело с - Все торговцы выглядят иначе: чёрно-белые, полупрозрачные и матовые. Некромант имеет дело с
мёртвыми, и торгуют с ним те, кто уже не совсем жив. мёртвыми, и торгуют с ним те, кто уже не совсем жив.
@@ -149,7 +150,7 @@ HUD за этим следят два индикатора: череп в ста
**13 языков полностью:** русский, английский, немецкий, испанский, французский, итальянский, **13 языков полностью:** русский, английский, немецкий, испанский, французский, итальянский,
японский, корейский, польский, португальский (Бразилия), турецкий, китайский упрощённый и японский, корейский, польский, португальский (Бразилия), турецкий, китайский упрощённый и
традиционный. Все 149 ключей `Config/Localization.csv` заполнены, пустых ячеек нет. традиционный. Все 165 ключей `Config/Localization.csv` заполнены, пустых ячеек нет.
## Установка ## Установка
@@ -161,6 +162,29 @@ HUD за этим следят два индикатора: череп в ста
## Статус ## Статус
Версия 1.4.2 — **защита от несовместимой версии игры**. Если мод запущен не на той версии игры, под которую собран (эта сборка — для 3.2.x), в главном меню появляется окно «мод несовместим, обновите» с единственной кнопкой «Выйти из игры». Если игрок всё же откроет мир, сохранённое Пространственное хранилище не разбирается и записывается обратно байт в байт — вещи дождутся правильной сборки мода.
Версия 1.4.1 — **исправлено падение FPS**. Красный труп «Жертвы» оставлял в памяти свои текстуры и материалы и не отдавал их до перезапуска игры: за пару часов набегали гигабайты видеопамяти, и FPS падал в разы. Теперь всё убирается вместе с трупом, а сам труп лежит 5 минут, а не час. Урон ножа пересчитывается раз в секунду, а не 10 раз.
**Мод рассчитан на игру 3.2 и НЕ совместим с 3.3 experimental**: там перестаёт сохраняться содержимое Пространственного хранилища и ломается Пирамида Ереси. Версия под 3.3 выйдет отдельно.
Версия 1.4.0 — **питомцы переписаны заново**, и первый из них теперь доступен рано: **Дух крысы**
(Некромантия 3, 60 упокоенных зомби). Все питомцы идут за вами, сами ни на кого не нападают и
ждут приказа: повторное применение свитка при живом питомце натравливает его на цель под
прицелом. Убийства питомцев засчитываются вам. Пороги призыва разнесены по шкале — крыса 60,
стервятник 500, пёс 1300, медведь 2000, волк 4000 упокоенных; Зомбогриф стал Могильным
стервятником, «Жуки Властелина» — Роем фараона.
Нож некроманта получил **собственную модель** — прямой клинок из бедренной кости, костяную гарду,
верёвочную обмотку и светящиеся руны — и новую иконку. Добыча Жертвы переехала **в сам труп**: он
краснеет, лежит час, а при обыске исчезает и оставляет мешок Жертвы. Подбирать теперь можно и те кровати, которые становятся
точкой возрождения. У Свитка духа крысы своя иконка.
И напоминание к шкале из 1.3.0: уровень Некромантии растёт **раз в 20 упокоенных зомби**, а
продвижение внутри уровня видно на фиолетовой шкале рядом с полосой опыта. При переходе с версий
до 1.3.0 уровень пересчитывается из числа упокоенных, поэтому становится в 20 раз меньше, — сами
убийства при этом не теряются.
Версия 1.3.0 — **шкала Некромантии переведена на 20 упокоенных зомби за уровень** (максимум 250-й Версия 1.3.0 — **шкала Некромантии переведена на 20 упокоенных зомби за уровень** (максимум 250-й
уровень, те же 5000 зомби). Это не косметика, а починка: игра хранит уровень любого навыка одним уровень, те же 5000 зомби). Это не косметика, а починка: игра хранит уровень любого навыка одним
байтом, поэтому старая шкала «одно убийство — один уровень» при каждом сохранении откатывала байтом, поэтому старая шкала «одно убийство — один уровень» при каждом сохранении откатывала
@@ -202,12 +226,16 @@ HUD за этим следят два индикатора: череп в ста
Версия 1.0.1 закрывала первый баг-репорт с Nexus: содержимое браслета больше не пропадает после Версия 1.0.1 закрывала первый баг-репорт с Nexus: содержимое браслета больше не пропадает после
выхода из игры (хранилище сохраняется в файле игрока, рядом с рюкзаком). выхода из игры (хранилище сохраняется в файле игрока, рядом с рюкзаком).
Весь заявленный контент реализован. Основное проверено в игре: заряд браслета и расход Кровавой Весь заявленный контент реализован. Основное проверено в игре: призыв, отзыв и следование всех
сферы, счёт убийств Некромантии (включая зомби-зверей, кровотечение и огонь), призрачность питомцев, приказ атаковать, зачёт убийств питомцами, новая модель ножа в руке, красный труп
торговцев, крик при создании Крови некроманта. Жертвы и мешок при его обыске, подобранная и заново поставленная кровать как точка возрождения,
заряд браслета и
расход Кровавой сферы, счёт убийств Некромантии (включая зомби-зверей, кровотечение и огонь),
призрачность торговцев, крик при создании Крови некроманта.
> **Эта сборка проверена не полностью.** Вкладка испытаний, оба первых испытания и счёт убийств > **Эта сборка проверена не полностью.** Из новшеств 1.4.0 не проверено поведение питомцев
> в игре подтверждены. Ещё не проверены: **новая шкала уровней и оба индикатора** (череп с уровнем > **в кровавую луну и на транспорте**.
> Из прошлых версий ещё не проверены: **новая шкала уровней и оба индикатора** (череп с уровнем
> и фиолетовая шкала прогресса), **починка старых сейвов на загрузке**, **мир между подчинёнными > и фиолетовая шкала прогресса), **починка старых сейвов на загрузке**, **мир между подчинёнными
> зомби**, **Кровавый камень** (нужен > зомби**, **Кровавый камень** (нужен
> уровень Некромантии 100, он же 2000 упокоенных, и химстанция), **новый порог Кровавой сферы** > уровень Некромантии 100, он же 2000 упокоенных, и химстанция), **новый порог Кровавой сферы**
@@ -222,7 +250,7 @@ HUD за этим следят два индикатора: череп в ста
# Necromancer's Tome (English) # Necromancer's Tome (English)
**Version 1.3.0** - for 7 Days to Die 3.2. By Alex Cube. **Version 1.4.2** - for 7 Days to Die 3.2 (not compatible with 3.3 experimental, and says so itself). By Alex Cube.
- Mod page: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/ - 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 - Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
@@ -273,10 +301,11 @@ Five tiers, each opening part of the arsenal (the table gives both the zombie co
one switches to your side and starts attacking other zombies instead of you. one switches to your side and starts attacking other zombies instead of you.
- **Scroll of Deviation** - the same effect, but stronger: on impact it wins over every zombie in - **Scroll of Deviation** - the same effect, but stronger: on impact it wins over every zombie in
the blast area at once rather than one. Consumed on use. the blast area at once rather than one. Consumed on use.
- **Necromancer's Knife** - a blackened bone blade. Its damage grows with the Necromancy skill: - **Necromancer's Knife** - a blade carved from a thighbone: a straight edge like an amputation
nearly useless in unskilled hands, lethal for a levelled player. Heals the wielder for half the knife, a bone guard, a rope-wrapped grip and glowing runes. Its damage grows with the Necromancy
damage dealt and marks the wounded zombie as a Victim - on death it is guaranteed to leave a skill: nearly useless in unskilled hands, lethal for a levelled player. Heals the wielder for
special bag of ingredients. half the damage dealt and marks the wounded zombie as a Victim - its corpse turns red and stays
for five minutes; searching it makes it vanish and leave a Victim's bag of ingredients.
- **Six mods for the Necromancer's Knife only** - ordinary knife mods will not fit this weapon, - **Six mods for the Necromancer's Knife only** - ordinary knife mods will not fit this weapon,
and these will not fit any other: **Tears of the Dead** (2 water from every zombie killed with and these will not fit any other: **Tears of the Dead** (2 water from every zombie killed with
the blade), **Scavenger's Feast** (2 food per corpse), **Grave's Repose** (protection from heat the blade), **Scavenger's Feast** (2 food per corpse), **Grave's Repose** (protection from heat
@@ -361,15 +390,15 @@ deliberately not retold here: a README gets read before the playthrough.
## Small things ## Small things
- The opening note also pauses the game and plays a short flashback. - The opening note also pauses the game and plays a short flashback.
- Some decorative blocks (beds, water coolers, cardboard boxes) can be disassembled by holding the - Some blocks (beds, water coolers, cardboard boxes) can be disassembled by holding the key, like
key, like a workbench. a workbench. A bed you then place with your own hands becomes a respawn point, like a bedroll.
- Every trader looks different: black and white, half-transparent and matte. The necromancer deals - Every trader looks different: black and white, half-transparent and matte. The necromancer deals
with the dead, and the only people still trading are not quite alive. with the dead, and the only people still trading are not quite alive.
## Localization ## Localization
**13 languages, complete:** Russian, English, German, Spanish, French, Italian, Japanese, Korean, **13 languages, complete:** Russian, English, German, Spanish, French, Italian, Japanese, Korean,
Polish, Brazilian Portuguese, Turkish, Simplified and Traditional Chinese. All 149 keys in Polish, Brazilian Portuguese, Turkish, Simplified and Traditional Chinese. All 165 keys in
`Config/Localization.csv` are filled in, with no empty cells. `Config/Localization.csv` are filled in, with no empty cells.
## Installation ## Installation
@@ -383,6 +412,29 @@ story scenes play without stopping time.
## Status ## Status
Version 1.4.2 - **protection against an incompatible game version**. If the mod runs on a game version it was not built for (this build is for 3.2.x), the main menu shows a "mod is incompatible, please update" window with a single "Quit game" button. And if a world does get opened, the saved Spatial Vault is not parsed but written back byte for byte - its items wait for the right build of the mod.
Version 1.4.1 - **the FPS drop is fixed**. A red Victim corpse left its textures and materials in memory and never gave them back until the game was restarted: within a couple of hours that was gigabytes of video memory, and the frame rate fell several times over. Everything is now cleaned up with the corpse, and the corpse itself stays for 5 minutes instead of an hour. The knife's damage is refreshed once a second instead of 10 times.
**The mod is built for game version 3.2 and is NOT compatible with 3.3 experimental**: there the Spatial Vault stops saving its contents and the Pyramid of Heresy breaks. A 3.3 version will come separately.
Version 1.4.0 - **pets have been rewritten**, and the first of them now comes early: the **Rat
Spirit** (Necromancy 3, 60 zombies put to rest). Every pet follows you, attacks nobody on its own
and waits for an order: using the scroll again while the pet is alive sends it after whatever is
under your crosshair. Your pets' kills count as yours. The summon thresholds are spread across the
scale - rat 60, vulture 500, dog 1300, bear 2000, wolf 4000 zombies put to rest; the Zombie Griff
became the Grave Vulture, and the Beetles of the Lord became the Pharaoh's Swarm.
The Necromancer's Knife has **its own model** - a straight thighbone blade, a bone guard, a rope
grip and glowing runes - and a new icon. A Victim's loot has moved **into the corpse itself**: it
turns red, stays for an hour, and searching it makes it vanish and leave the Victim's bag. The beds that become a respawn point can
now be picked up too. The Scroll of the Rat Spirit has its own icon.
And a reminder about the 1.3.0 scale: Necromancy rises **once every 20 zombies put to rest**, and
the progress inside a level shows on the purple bar next to the experience bar. Coming from a
version before 1.3.0, the level is recalculated from the zombies put to rest, so it becomes twenty
times smaller - the kills themselves are not lost.
Version 1.3.0 - **Necromancy now scales at 20 zombies put to rest per level** (up to level 250, Version 1.3.0 - **Necromancy now scales at 20 zombies put to rest per level** (up to level 250,
the same 5000 zombies). This is a fix, not a facelift: the game stores any skill's level in a the same 5000 zombies). This is a fix, not a facelift: the game stores any skill's level in a
single byte, so the old "one kill, one level" scale rolled the level back by 256 on every save. single byte, so the old "one kill, one level" scale rolled the level back by 256 on every save.
@@ -427,12 +479,16 @@ matte.
Version 1.0.1 closed the first bug report from Nexus: the bracelet's contents no longer disappear Version 1.0.1 closed the first bug report from Nexus: the bracelet's contents no longer disappear
after leaving the game (the storage is saved in the player's own file, next to the backpack). after leaving the game (the storage is saved in the player's own file, next to the backpack).
All the announced content is implemented. The core of it is confirmed in game: the bracelet's All the announced content is implemented. The core of it is confirmed in game: summoning,
charge and the Blood Sphere's spending, Necromancy's kill count (zombie animals, bleeding and fire dismissing and following for every pet, the attack order, pets' kills counting as yours, the new
included), the traders' ghostly look, and the cry when Necromancer's Blood is made. knife model in hand, the red Victim corpse and the bag it leaves when searched, a bed picked up
and placed again becoming a respawn point, the bracelet's charge and the Blood Sphere's spending, Necromancy's kill count
(zombie animals, bleeding and fire included), the traders' ghostly look, and the cry when
Necromancer's Blood is made.
> **This build has not been fully tested.** The challenges tab, its first two challenges and the > **This build has not been fully tested.** Of what is new in 1.4.0, not yet checked: how pets
> kill count are confirmed in game. Not yet checked: the **new level scale and both indicators** > behave **during a blood moon and on vehicles**. From
> earlier versions, not yet checked: the **new level scale and both indicators**
> (the skull showing the level and the purple progress bar), the **self-repair of old saves on > (the skull showing the level and the purple progress bar), the **self-repair of old saves on
> load**, the **truce between charmed zombies**, the **Blood Stone** (it needs Necromancy level 100, i.e. 2000 zombies put to rest, and a > load**, the **truce between charmed zombies**, the **Blood Stone** (it needs Necromancy level 100, i.e. 2000 zombies put to rest, and a
> chemistry station), the **Blood Sphere's new threshold** (it moved to level 1, where the Spatial > chemistry station), the **Blood Sphere's new threshold** (it moved to level 1, where the Spatial
Binary file not shown.
+4 -4
View File
@@ -15,7 +15,7 @@
<ul> <ul>
<li><strong>Камень духов</strong> — брошенный камень, светящийся некромантской энергией. Задетый зомби переходит на вашу сторону и начинает рвать других зомби вместо вас.</li> <li><strong>Камень духов</strong> — брошенный камень, светящийся некромантской энергией. Задетый зомби переходит на вашу сторону и начинает рвать других зомби вместо вас.</li>
<li><strong>Свиток девиации</strong> — то же самое, но по площади: переманивает сразу всех зомби в радиусе взрыва. Расходуется при использовании.</li> <li><strong>Свиток девиации</strong> — то же самое, но по площади: переманивает сразу всех зомби в радиусе взрыва. Расходуется при использовании.</li>
<li><strong>Нож некроманта</strong> — почерневший костяной клинок. Урон растёт вместе с навыком Некромантии: почти бесполезен в неопытных руках, смертоносен у прокачанного игрока. Лечит владельца на половину нанесённого урона и метит раненого зомби как Жертву — тот гарантированно оставляет особый мешок с ингредиентами.</li> <li><strong>Нож некроманта</strong> — клинок, выточенный из бедренной кости: прямое лезвие, как у ампутационного ножа, костяная гарда, рукоять в верёвочной обмотке и светящиеся руны. Урон растёт вместе с навыком Некромантии: почти бесполезен в неопытных руках, смертоносен у прокачанного игрока. Лечит владельца на половину нанесённого урона и метит раненого зомби как Жертву — труп такого зомби краснеет и лежит пять минут; при обыске он исчезает, оставляя мешок Жертвы с ингредиентами.</li>
<li><strong>Шесть модов только для этого ножа</strong> — вода и еда прямо из трупов (Слёзы мертвеца, Пир падальщика), защита от жары и холода (Могильный покой), замедление раненых (Хватка мертвеца), силовая атака-буря по площади за двойную цену здоровья и выносливости (Мёртвая буря) и подсветка всех зомби поблизости на карте и компасе (Тёмное чутьё). Обычные моды в этот нож не встают, а эти не встают ни во что другое.</li> <li><strong>Шесть модов только для этого ножа</strong> — вода и еда прямо из трупов (Слёзы мертвеца, Пир падальщика), защита от жары и холода (Могильный покой), замедление раненых (Хватка мертвеца), силовая атака-буря по площади за двойную цену здоровья и выносливости (Мёртвая буря) и подсветка всех зомби поблизости на карте и компасе (Тёмное чутьё). Обычные моды в этот нож не встают, а эти не встают ни во что другое.</li>
<li><strong>Кровь некроманта</strong> — ритуальный ресурс: пустая банка, любой нож в руках и 90% текущего здоровья за одну порцию. Ингредиент для самых тёмных рецептов.</li> <li><strong>Кровь некроманта</strong> — ритуальный ресурс: пустая банка, любой нож в руках и 90% текущего здоровья за одну порцию. Ингредиент для самых тёмных рецептов.</li>
<li><strong>Кожа жертвы</strong> и <strong>Прах зомби</strong> — падают с зомби, помеченного ножом как Жертва. Основа книг призыва и большинства некромантских рецептов.</li> <li><strong>Кожа жертвы</strong> и <strong>Прах зомби</strong> — падают с зомби, помеченного ножом как Жертва. Основа книг призыва и большинства некромантских рецептов.</li>
@@ -50,7 +50,7 @@
<ul> <ul>
<li>Стартовая записка при открытии тоже ставит игру на паузу и проигрывает короткий флэшбек.</li> <li>Стартовая записка при открытии тоже ставит игру на паузу и проигрывает короткий флэшбек.</li>
<li>Часть декоративных блоков (кровати, кулеры, картонные коробки) разбирается удержанием, как верстак.</li> <li>Часть блоков (кровати, кулеры, картонные коробки) разбирается удержанием, как верстак. Кровать, поставленная после этого своими руками, становится точкой возрождения, как спальник.</li>
<li>Все торговцы выглядят иначе: чёрно-белые, полупрозрачные и матовые. Некромант имеет дело с мёртвыми, и торгуют с ним те, кто уже не совсем жив.</li> <li>Все торговцы выглядят иначе: чёрно-белые, полупрозрачные и матовые. Некромант имеет дело с мёртвыми, и торгуют с ним те, кто уже не совсем жив.</li>
</ul> </ul>
@@ -93,7 +93,7 @@ A dedicated <strong>Necromancy</strong> skill levels not from perk points and no
<ul> <ul>
<li><strong>Spirit Stone</strong> — a thrown stone lit with necromantic energy. The zombie it touches switches to your side and starts tearing into other zombies instead of you.</li> <li><strong>Spirit Stone</strong> — a thrown stone lit with necromantic energy. The zombie it touches switches to your side and starts tearing into other zombies instead of you.</li>
<li><strong>Scroll of Deviation</strong> — the same effect, but area-wide: it charms every zombie in the blast radius at once. Consumed on use.</li> <li><strong>Scroll of Deviation</strong> — the same effect, but area-wide: it charms every zombie in the blast radius at once. Consumed on use.</li>
<li><strong>Necromancer's Knife</strong> — a blackened bone blade. Its damage scales with the Necromancy skill: nearly useless in unskilled hands, lethal for a levelled player. Heals the wielder for half the damage dealt and marks the wounded zombie as a Victim — it is guaranteed to drop a special bag of ingredients.</li> <li><strong>Necromancer's Knife</strong> — a blade carved from a thighbone: a straight edge like an amputation knife, a bone guard, a rope-wrapped grip and glowing runes. Its damage scales with the Necromancy skill: nearly useless in unskilled hands, lethal for a levelled player. Heals the wielder for half the damage dealt and marks the wounded zombie as a Victim — its corpse turns red and stays for five minutes; searching it makes it vanish and leave a Victim's bag of ingredients.</li>
<li><strong>Six mods for this knife only</strong> — water and food straight out of corpses (Tears of the Dead, Scavenger's Feast), protection from heat and cold (Grave's Repose), slowed victims (Dead Man's Grip), an area power-attack storm at double the health and stamina cost (Dead Storm), and every nearby zombie marked on your map and compass (Dark Sense). Ordinary knife mods will not fit this weapon, and these will not fit any other.</li> <li><strong>Six mods for this knife only</strong> — water and food straight out of corpses (Tears of the Dead, Scavenger's Feast), protection from heat and cold (Grave's Repose), slowed victims (Dead Man's Grip), an area power-attack storm at double the health and stamina cost (Dead Storm), and every nearby zombie marked on your map and compass (Dark Sense). Ordinary knife mods will not fit this weapon, and these will not fit any other.</li>
<li><strong>Necromancer's Blood</strong> — a ritual resource: an empty jar, any knife in hand and 90% of your current health per portion. An ingredient for the darkest recipes.</li> <li><strong>Necromancer's Blood</strong> — a ritual resource: an empty jar, any knife in hand and 90% of your current health per portion. An ingredient for the darkest recipes.</li>
<li><strong>Victim's Skin</strong> and <strong>Zombie Ash</strong> — dropped by a zombie marked as a Victim. The basis of the summoning books and most necromantic recipes.</li> <li><strong>Victim's Skin</strong> and <strong>Zombie Ash</strong> — dropped by a zombie marked as a Victim. The basis of the summoning books and most necromantic recipes.</li>
@@ -128,7 +128,7 @@ Activating it asks for confirmation, stops the game and unfolds a full-screen fi
<ul> <ul>
<li>The opening note also pauses the game and plays a short flashback.</li> <li>The opening note also pauses the game and plays a short flashback.</li>
<li>Some decorative blocks (beds, water coolers, cardboard boxes) can be disassembled by holding the pick-up key, like a workbench.</li> <li>Some blocks (beds, water coolers, cardboard boxes) can be disassembled by holding the pick-up key, like a workbench. A bed you then place with your own hands becomes a respawn point, like a bedroll.</li>
<li>Every trader looks different: black and white, half-transparent and matte. The necromancer deals with the dead, and the only people still trading are not quite alive.</li> <li>Every trader looks different: black and white, half-transparent and matte. The necromancer deals with the dead, and the only people still trading are not quite alive.</li>
</ul> </ul>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB