Compare commits
3
Commits
d97f83e972
...
01aa054177
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01aa054177 | ||
|
|
98ddcd7bce | ||
|
|
b6a892cadd |
+7
-6
@@ -1023,10 +1023,12 @@
|
||||
</append>
|
||||
|
||||
<!-- "Свиток духа крысы": тот же свиток, что у Зомбоволка, но серее - указание 2026-09-18
|
||||
("выглядит как свиток призыва зомбоволка, но с более серым тинтом"). Иконка
|
||||
переиспользуется целиком (CustomIcon="SummonZombieWolf"), своей рисовать не пришлось;
|
||||
CustomIconTint обесцвечивает её, TintColor - 3D-модель в руке. Оба множатся на базовый
|
||||
цвет, как у Камня духов.
|
||||
("выглядит как свиток призыва зомбоволка, но с более серым тинтом"). TintColor красит
|
||||
3D-модель в руке (множится на базовый цвет, как у Камня духов).
|
||||
|
||||
ИКОНКА СВОЯ с 2026-09-21: пользователь нарисовал RatBook.png (книга в пурпурных узорах с
|
||||
крысой). До этого переиспользовалась волчья SummonZombieWolf с CustomIconTint 8a9a8a;
|
||||
тинт убран вместе с ней - он множится на иконку и погасил бы нарисованные цвета.
|
||||
|
||||
ДВА ДЕЙСТВИЯ, И ОНИ НЕ ТЕ ЖЕ, ЧТО У ОСТАЛЬНЫХ ПИТОМЦЕВ. Action1 (силовая атака) - отзыв,
|
||||
как везде. А вот Action0 у крысы ТРЁХзначный, и разбирает это HarmonySrc/PetCommandPatch.cs:
|
||||
@@ -1061,8 +1063,7 @@
|
||||
|
||||
У ОСТАЛЬНЫХ ШЕСТИ КНИГ ПРИЗЫВА ЭТОТ ЖЕ ЗНАЧОК ПОКА ОСТАЛСЯ - трогать их без
|
||||
указания не стал, но замечание к ним относится ровно так же. -->
|
||||
<property name="CustomIcon" value="SummonZombieWolf"/>
|
||||
<property name="CustomIconTint" value="8a9a8a"/>
|
||||
<property name="CustomIcon" value="RatBook"/>
|
||||
<property name="DescriptionKey" value="bookSummonRatSpiritDesc"/>
|
||||
<property name="DisplayType" value="ammoGrenadeFire"/>
|
||||
<property name="HoldType" value="21"/>
|
||||
|
||||
@@ -43,7 +43,17 @@ namespace NecromancerTome
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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,
|
||||
@@ -74,6 +84,27 @@ namespace NecromancerTome
|
||||
{
|
||||
"bedMadeNoFrame",
|
||||
"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",
|
||||
"cntCardboardBox",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
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>Сколько секунд лежит помеченный труп. У ванильного мешка с лутом 3600, и
|
||||
/// причин быть скромнее нет: труп и есть мешок.</summary>
|
||||
public const float CorpseSecondsAfterDeath = 3600f;
|
||||
|
||||
/// <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>Повторная попытка перекраски. Зовётся из общего тика раз в секунду.</summary>
|
||||
public static void RetryPendingTints(World _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);
|
||||
}
|
||||
}
|
||||
|
||||
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: <none>
|
||||
/// shader 'Game/Animal/Fur' ... tint property: <none>
|
||||
///
|
||||
/// Свойства цвета у них НЕТ ВООБЩЕ. `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;
|
||||
}
|
||||
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;
|
||||
for (int i = 0; i < instances.Length && i < shared.Length; i++)
|
||||
{
|
||||
Material material = instances[i];
|
||||
if (material == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string albedo = GhostTraderPatch.FindAlbedoProperty(material);
|
||||
if (albedo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Texture2D repainted = Tint(material.GetTexture(albedo));
|
||||
if (repainted == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
material.SetTexture(albedo, repainted);
|
||||
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
|
||||
{
|
||||
rt = RenderTexture.GetTemporary(_source.width, _source.height, 0,
|
||||
RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||||
Graphics.Blit(_source, rt);
|
||||
RenderTexture.active = rt;
|
||||
|
||||
painted = new Texture2D(_source.width, _source.height, TextureFormat.RGBA32, false);
|
||||
painted.ReadPixels(new Rect(0f, 0f, _source.width, _source.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);
|
||||
painted.Apply(false, false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("[NecromancerTome] CorpseLoot: не удалось перекрасить текстуру '" +
|
||||
_source.name + "': " + e.Message);
|
||||
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 + " с");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,9 @@ namespace NecromancerTome
|
||||
// that method at all - rules "patch never applied" in or out without waiting on
|
||||
// another in-game death.
|
||||
VerifyPrefixAttached(typeof(EntityAlive), "dropItemOnDeath");
|
||||
VerifyPrefixAttached(typeof(Entity), "DropBagServer");
|
||||
// Патч на Entity.DropBagServer снят 2026-09-18 вместе с переделкой мешка «Жертвы»:
|
||||
// он подменял собой ОБЫЧНЫЙ мешок, а тот теперь должен доставаться игроку как есть.
|
||||
// Проверять нечего - см. VictimPatch.cs.
|
||||
|
||||
// Добавлено 2026-09-17 вместе с правкой "подчинённые не дерутся между собой"
|
||||
// (CharmPatch.cs). Метод EAITarget.check в исходнике protected и назван со строчной
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace NecromancerTome
|
||||
return;
|
||||
}
|
||||
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);
|
||||
if (!isPortalChannel && !isVictim && !isCharm)
|
||||
{
|
||||
|
||||
@@ -322,13 +322,19 @@ namespace NecromancerTome
|
||||
return;
|
||||
}
|
||||
timer = 0f;
|
||||
if (TrackedPets.Count == 0)
|
||||
World world = GameManager.Instance != null ? GameManager.Instance.World : null;
|
||||
if (world == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
World world = GameManager.Instance != null ? GameManager.Instance.World : null;
|
||||
if (world == null)
|
||||
// Трупы, которые не удалось перекрасить в момент смерти (модель бывает ещё не
|
||||
// собрана) - добиваем здесь: тик всё равно уже идёт. См. CorpseLootPatch.cs.
|
||||
// ВЫШЕ проверки на пустой список питомцев намеренно: трупы помечает Нож некроманта,
|
||||
// и к тому, есть ли у игрока сейчас призванный питомец, они отношения не имеют.
|
||||
CorpseLoot.RetryPendingTints(world);
|
||||
|
||||
if (TrackedPets.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
+123
-59
@@ -4,97 +4,161 @@ using UnityEngine;
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// Necromancer's Knife (BACKLOG.md item 5, user request 2026-08-28): a zombie hit by the
|
||||
/// knife carries buffNecroVictim (buffs.xml) until it dies. On death, it's guaranteed to drop
|
||||
/// a green "Жертва" loot bag (EntityLootContainerVictim in entityclasses.xml) instead of
|
||||
/// whatever the normal random loot roll would have given it.
|
||||
/// Нож некроманта (BACKLOG.md, п.5): зомби, задетый ножом, носит buffNecroVictim (buffs.xml) до
|
||||
/// самой смерти. На смерти он роняет мешок «Жертва» (EntityLootContainerVictim в
|
||||
/// entityclasses.xml).
|
||||
///
|
||||
/// Two patch points, found the hard way (2026-08-28, "лута всё ещё нет" after confirming via
|
||||
/// the CharmPatch.cs-style AddBuff log that the debuff itself WAS being applied fine):
|
||||
/// ПОВЕДЕНИЕ ИСПРАВЛЕНО 2026-09-18, И ЭТО ВОЗВРАТ К ТОМУ, О ЧЁМ ДОГОВАРИВАЛИСЬ. Баг-репорт
|
||||
/// пользователя: «мы договаривались, что он выпадает только если не должен был выпасть мешок с
|
||||
/// лутом. Но похоже падает только мешок жертвы».
|
||||
///
|
||||
/// 1. Patch_EntityAlive_dropItemOnDeath_VictimBag - the actual gate. Confirmed by
|
||||
/// 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.
|
||||
/// Так и было. Прежняя версия состояла из двух префиксов, и оба возвращали false:
|
||||
///
|
||||
/// Both are Prefixes returning false: they fully replace what they intercept rather than
|
||||
/// running alongside it.
|
||||
/// 1. на dropItemOnDeath - при дебафе звала DropBagServer() НАПРЯМУЮ, минуя ванильный
|
||||
/// бросок вероятности, и обрывала оригинал;
|
||||
/// 2. на DropBagServer - при дебафе спавнила «Жертву» ВМЕСТО обычного мешка.
|
||||
///
|
||||
/// Вместе это означало: помеченный зомби роняет «Жертву» ВСЕГДА и обычный мешок НИКОГДА. То
|
||||
/// есть нож не добавлял добычу, а подменял её - и в редких 4% случаев, когда игроку и так
|
||||
/// причитался нормальный мешок, он его терял.
|
||||
///
|
||||
/// СТАЛО (уточнено пользователем в тот же день - «а можно сделать так, чтобы в случае когда
|
||||
/// положен лут, падало два мешка»): «Жертва» не заменяет обычный мешок и не ждёт его отсутствия,
|
||||
/// а ДОБАВЛЯЕТСЯ. Ванильный бросок делается как есть (формула скопирована из
|
||||
/// EntityAlive.dropItemOnDeath дословно, включая модификатор от оружия убийцы и множитель
|
||||
/// LootContainer.LootBagChance), и дальше:
|
||||
///
|
||||
/// бросок прошёл -> обычный мешок рядом И добыча в трупе;
|
||||
/// бросок не прошёл -> только добыча в трупе.
|
||||
///
|
||||
/// То есть помеченный ножом зомби никогда не оставляет игрока ни с чем, а в удачном случае
|
||||
/// даёт вдвое больше обычного. Это осознанно щедро: метку ещё надо поставить ножом, которым
|
||||
/// по зомби надо попасть.
|
||||
///
|
||||
/// УТОЧНЕНО В ТОТ ЖЕ ДЕНЬ: «Жертва» больше не падает мешком в момент смерти - она остаётся
|
||||
/// ВНУТРИ ТРУПА, который перекрашивается, перестаёт исчезать и получает команду «обыскать»
|
||||
/// (CorpseLootPatch.cs). Мешок появляется только по обыску. Сам бросок и вся арифметика ниже
|
||||
/// от этого не изменились: они по-прежнему решают судьбу ОБЫЧНОГО мешка.
|
||||
///
|
||||
/// Второй префикс (на DropBagServer) УДАЛЁН ЦЕЛИКОМ - он теперь вреден: подменял бы и тот
|
||||
/// обычный мешок, который мы как раз хотим оставить. Спавн «Жертвы» переехал сюда, в
|
||||
/// SpawnVictimBag.
|
||||
///
|
||||
/// ПОЧЕМУ ВООБЩЕ НУЖЕН ПРЕФИКС, А НЕ ПОСТФИКС. Узнать «выпал ли мешок» после оригинала нечем:
|
||||
/// dropItemOnDeath ничего не возвращает и следов не оставляет. Поэтому бросок повторяется
|
||||
/// здесь, а оригинал не запускается. Ветка оригинала, которая при этом теряется, - выброс
|
||||
/// собственного инвентаря для сущностей БЕЗ ИИ (`if (!hasAI)`), и к зомби она не относится
|
||||
/// никогда; на всякий случай она всё равно отдана оригиналу проверкой hasAI ниже.
|
||||
///
|
||||
/// LootContainer.NoLoot УВАЖАЕТСЯ: если в настройках мира мешки выключены, «Жертва» тоже не
|
||||
/// падает. Это мировая настройка игрока, а не место мода спорить с ней.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(EntityAlive), "dropItemOnDeath")]
|
||||
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 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Ванильный расчёт, скопированный из 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);
|
||||
if (containerClassId == -1)
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] VictimPatch: entity class '" + VictimContainerClassName + "' not found, falling back to normal loot");
|
||||
return true;
|
||||
Debug.LogWarning("[NecromancerTome] VictimPatch: класс '" + VictimContainerClassName +
|
||||
"' не найден, мешок не выпадет");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 pos = __instance.GetPosition();
|
||||
Vector3 pos = _victim.GetPosition();
|
||||
pos.y += 0.9f;
|
||||
if (_offsetAside)
|
||||
{
|
||||
// Полметра вбок - достаточно, чтобы мешки читались как два и оба брались
|
||||
// прицелом, и мало, чтобы «Жертва» уехала за стену или в соседнюю комнату.
|
||||
pos += _victim.qrotation * new Vector3(0.6f, 0f, 0f);
|
||||
}
|
||||
Entity spawned = EntityFactory.CreateEntity(containerClassId, pos, Vector3.zero);
|
||||
if (spawned is EntityLootContainer 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
|
||||
{
|
||||
Debug.LogWarning("[NecromancerTome] VictimPatch: created entity for '" + VictimContainerClassName + "' wasn't an EntityLootContainer");
|
||||
Debug.LogWarning("[NecromancerTome] VictimPatch: созданная сущность для '" +
|
||||
VictimContainerClassName + "' не оказалась EntityLootContainer");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, a bed should become your respawn point, like a bedroll. Not tested in game yet.
|
||||
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.
|
||||
@@ -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]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 an hour, 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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
*English version below — scroll past the Russian half.*
|
||||
|
||||
**Версия 1.3.0** — для 7 Days to Die 3.2. Автор: Alex Cube.
|
||||
**Версия 1.4.0** — для 7 Days to Die 3.2. Автор: Alex Cube.
|
||||
|
||||
- Страница мода: https://www.alexcube.ru/7-days-to-die-moi-mody/kniga-nekromanta-necromancer-s-tome/
|
||||
- Nexus Mods: https://www.nexusmods.com/7daystodie/mods/12547
|
||||
@@ -54,10 +54,11 @@ HUD за этим следят два индикатора: череп в ста
|
||||
тот переходит на вашу сторону и начинает атаковать других зомби вместо вас.
|
||||
- **Свиток девиации** — тот же эффект, но мощнее: при броске переманивает на вашу сторону сразу
|
||||
всех зомби в области взрыва, а не одного. Расходуется при использовании.
|
||||
- **Нож некроманта** — почерневший костяной клинок. Урон растёт вместе с навыком Некромантии:
|
||||
почти бесполезен в неопытных руках, смертоносен у прокачанного игрока. Лечит владельца на
|
||||
половину нанесённого урона и метит раненого зомби как Жертву — тот при смерти гарантированно
|
||||
оставляет особый мешок с ингредиентами.
|
||||
- **Нож некроманта** — клинок, выточенный из бедренной кости: прямое лезвие, как у ампутационного
|
||||
ножа, костяная гарда, рукоять в верёвочной обмотке и светящиеся руны. Урон растёт вместе с
|
||||
навыком Некромантии: почти бесполезен в неопытных руках, смертоносен у прокачанного игрока.
|
||||
Лечит владельца на половину нанесённого урона и метит раненого зомби как Жертву — труп такого
|
||||
зомби краснеет и лежит час; при обыске он исчезает, оставляя мешок Жертвы с ингредиентами.
|
||||
- **Шесть модов только для Ножа некроманта** — обычные моды для ножей в это оружие не встают, а
|
||||
эти не встают ни во что другое: **Слёзы мертвеца** (2 воды с каждого убитого клинком зомби),
|
||||
**Пир падальщика** (2 еды за труп), **Могильный покой** (защита от перегрева и переохлаждения,
|
||||
@@ -140,8 +141,8 @@ HUD за этим следят два индикатора: череп в ста
|
||||
## По мелочи
|
||||
|
||||
- Стартовая записка при открытии тоже ставит игру на паузу и проигрывает короткий флэшбек.
|
||||
- Некоторые декоративные блоки (кровати, кулеры, картонные коробки) можно разобрать удержанием,
|
||||
как верстак.
|
||||
- Некоторые блоки (кровати, кулеры, картонные коробки) можно разобрать удержанием, как верстак.
|
||||
Кровать, поставленная после этого своими руками, становится точкой возрождения, как спальник.
|
||||
- Все торговцы выглядят иначе: чёрно-белые, полупрозрачные и матовые. Некромант имеет дело с
|
||||
мёртвыми, и торгуют с ним те, кто уже не совсем жив.
|
||||
|
||||
@@ -161,6 +162,23 @@ HUD за этим следят два индикатора: череп в ста
|
||||
|
||||
## Статус
|
||||
|
||||
Версия 1.4.0 — **питомцы переписаны заново**, и первый из них теперь доступен рано: **Дух крысы**
|
||||
(Некромантия 3, 60 упокоенных зомби). Все питомцы идут за вами, сами ни на кого не нападают и
|
||||
ждут приказа: повторное применение свитка при живом питомце натравливает его на цель под
|
||||
прицелом. Убийства питомцев засчитываются вам. Пороги призыва разнесены по шкале — крыса 60,
|
||||
стервятник 500, пёс 1300, медведь 2000, волк 4000 упокоенных; Зомбогриф стал Могильным
|
||||
стервятником, «Жуки Властелина» — Роем фараона.
|
||||
|
||||
Нож некроманта получил **собственную модель** — прямой клинок из бедренной кости, костяную гарду,
|
||||
верёвочную обмотку и светящиеся руны — и новую иконку. Добыча Жертвы переехала **в сам труп**: он
|
||||
краснеет, лежит час, а при обыске исчезает и оставляет мешок Жертвы. Подбирать теперь можно и те кровати, которые становятся
|
||||
точкой возрождения. У Свитка духа крысы своя иконка.
|
||||
|
||||
И напоминание к шкале из 1.3.0: уровень Некромантии растёт **раз в 20 упокоенных зомби**, а
|
||||
продвижение внутри уровня видно на фиолетовой шкале рядом с полосой опыта. При переходе с версий
|
||||
до 1.3.0 уровень пересчитывается из числа упокоенных, поэтому становится в 20 раз меньше, — сами
|
||||
убийства при этом не теряются.
|
||||
|
||||
Версия 1.3.0 — **шкала Некромантии переведена на 20 упокоенных зомби за уровень** (максимум 250-й
|
||||
уровень, те же 5000 зомби). Это не косметика, а починка: игра хранит уровень любого навыка одним
|
||||
байтом, поэтому старая шкала «одно убийство — один уровень» при каждом сохранении откатывала
|
||||
@@ -202,12 +220,16 @@ HUD за этим следят два индикатора: череп в ста
|
||||
Версия 1.0.1 закрывала первый баг-репорт с Nexus: содержимое браслета больше не пропадает после
|
||||
выхода из игры (хранилище сохраняется в файле игрока, рядом с рюкзаком).
|
||||
|
||||
Весь заявленный контент реализован. Основное проверено в игре: заряд браслета и расход Кровавой
|
||||
сферы, счёт убийств Некромантии (включая зомби-зверей, кровотечение и огонь), призрачность
|
||||
торговцев, крик при создании Крови некроманта.
|
||||
Весь заявленный контент реализован. Основное проверено в игре: призыв, отзыв и следование всех
|
||||
питомцев, приказ атаковать, зачёт убийств питомцами, новая модель ножа в руке, красный труп
|
||||
Жертвы и мешок при его обыске, заряд браслета и
|
||||
расход Кровавой сферы, счёт убийств Некромантии (включая зомби-зверей, кровотечение и огонь),
|
||||
призрачность торговцев, крик при создании Крови некроманта.
|
||||
|
||||
> **Эта сборка проверена не полностью.** Вкладка испытаний, оба первых испытания и счёт убийств
|
||||
> в игре подтверждены. Ещё не проверены: **новая шкала уровней и оба индикатора** (череп с уровнем
|
||||
> **Эта сборка проверена не полностью.** Из новшеств 1.4.0 не проверены: **подбор кроватей**
|
||||
> (становится ли поднятая и заново поставленная кровать точкой возрождения) и поведение питомцев
|
||||
> **в кровавую луну и на транспорте**.
|
||||
> Из прошлых версий ещё не проверены: **новая шкала уровней и оба индикатора** (череп с уровнем
|
||||
> и фиолетовая шкала прогресса), **починка старых сейвов на загрузке**, **мир между подчинёнными
|
||||
> зомби**, **Кровавый камень** (нужен
|
||||
> уровень Некромантии 100, он же 2000 упокоенных, и химстанция), **новый порог Кровавой сферы**
|
||||
@@ -222,7 +244,7 @@ HUD за этим следят два индикатора: череп в ста
|
||||
|
||||
# Necromancer's Tome (English)
|
||||
|
||||
**Version 1.3.0** - for 7 Days to Die 3.2. By Alex Cube.
|
||||
**Version 1.4.0** - for 7 Days to Die 3.2. By Alex Cube.
|
||||
|
||||
- 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
|
||||
@@ -273,10 +295,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.
|
||||
- **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.
|
||||
- **Necromancer's Knife** - a blackened bone blade. Its damage grows 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 - on death it is guaranteed to leave a
|
||||
special bag of ingredients.
|
||||
- **Necromancer's Knife** - 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 grows 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 an hour; 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,
|
||||
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
|
||||
@@ -361,8 +384,8 @@ deliberately not retold here: a README gets read before the playthrough.
|
||||
## Small things
|
||||
|
||||
- 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
|
||||
key, like a workbench.
|
||||
- Some blocks (beds, water coolers, cardboard boxes) can be disassembled by holding the key, like
|
||||
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
|
||||
with the dead, and the only people still trading are not quite alive.
|
||||
|
||||
@@ -383,6 +406,23 @@ story scenes play without stopping time.
|
||||
|
||||
## Status
|
||||
|
||||
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,
|
||||
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.
|
||||
@@ -427,12 +467,16 @@ matte.
|
||||
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).
|
||||
|
||||
All the announced content is implemented. The core of it is confirmed in game: 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.
|
||||
All the announced content is implemented. The core of it is confirmed in game: summoning,
|
||||
dismissing and following for every pet, the attack order, pets' kills counting as yours, the new
|
||||
knife model in hand, the red Victim corpse and the bag it leaves when searched, 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
|
||||
> kill count are confirmed in game. Not yet checked: the **new level scale and both indicators**
|
||||
> **This build has not been fully tested.** Of what is new in 1.4.0, not yet checked: **picking up
|
||||
> beds** (whether a bed picked up and placed again really becomes a respawn point) and how pets
|
||||
> 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
|
||||
> 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
|
||||
|
||||
Binary file not shown.
@@ -15,7 +15,7 @@
|
||||
<ul>
|
||||
<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> и <strong>Прах зомби</strong> — падают с зомби, помеченного ножом как Жертва. Основа книг призыва и большинства некромантских рецептов.</li>
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
<ul>
|
||||
<li>Стартовая записка при открытии тоже ставит игру на паузу и проигрывает короткий флэшбек.</li>
|
||||
<li>Часть декоративных блоков (кровати, кулеры, картонные коробки) разбирается удержанием, как верстак.</li>
|
||||
<li>Часть блоков (кровати, кулеры, картонные коробки) разбирается удержанием, как верстак. Кровать, поставленная после этого своими руками, становится точкой возрождения, как спальник.</li>
|
||||
<li>Все торговцы выглядят иначе: чёрно-белые, полупрозрачные и матовые. Некромант имеет дело с мёртвыми, и торгуют с ним те, кто уже не совсем жив.</li>
|
||||
</ul>
|
||||
|
||||
@@ -93,7 +93,7 @@ A dedicated <strong>Necromancy</strong> skill levels not from perk points and no
|
||||
<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>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 an hour; 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>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>
|
||||
@@ -128,7 +128,7 @@ Activating it asks for confirmation, stops the game and unfolds a full-screen fi
|
||||
|
||||
<ul>
|
||||
<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>
|
||||
</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 |
Reference in New Issue
Block a user