using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// Нож некроманта (BACKLOG.md, п.5): зомби, задетый ножом, носит buffNecroVictim (buffs.xml) до /// самой смерти. На смерти он роняет мешок «Жертва» (EntityLootContainerVictim в /// entityclasses.xml). /// /// ПОВЕДЕНИЕ ИСПРАВЛЕНО 2026-09-18, И ЭТО ВОЗВРАТ К ТОМУ, О ЧЁМ ДОГОВАРИВАЛИСЬ. Баг-репорт /// пользователя: «мы договаривались, что он выпадает только если не должен был выпасть мешок с /// лутом. Но похоже падает только мешок жертвы». /// /// Так и было. Прежняя версия состояла из двух префиксов, и оба возвращали false: /// /// 1. на dropItemOnDeath - при дебафе звала DropBagServer() НАПРЯМУЮ, минуя ванильный /// бросок вероятности, и обрывала оригинал; /// 2. на DropBagServer - при дебафе спавнила «Жертву» ВМЕСТО обычного мешка. /// /// Вместе это означало: помеченный зомби роняет «Жертву» ВСЕГДА и обычный мешок НИКОГДА. То /// есть нож не добавлял добычу, а подменял её - и в редких 4% случаев, когда игроку и так /// причитался нормальный мешок, он его терял. /// /// СТАЛО (уточнено пользователем в тот же день - «а можно сделать так, чтобы в случае когда /// положен лут, падало два мешка»): «Жертва» не заменяет обычный мешок и не ждёт его отсутствия, /// а ДОБАВЛЯЕТСЯ. Ванильный бросок делается как есть (формула скопирована из /// EntityAlive.dropItemOnDeath дословно, включая модификатор от оружия убийцы и множитель /// LootContainer.LootBagChance), и дальше: /// /// бросок прошёл -> обычный мешок рядом И добыча в трупе; /// бросок не прошёл -> только добыча в трупе. /// /// То есть помеченный ножом зомби никогда не оставляет игрока ни с чем, а в удачном случае /// даёт вдвое больше обычного. Это осознанно щедро: метку ещё надо поставить ножом, которым /// по зомби надо попасть. /// /// УТОЧНЕНО В ТОТ ЖЕ ДЕНЬ: «Жертва» больше не падает мешком в момент смерти - она остаётся /// ВНУТРИ ТРУПА, который перекрашивается, перестаёт исчезать и получает команду «обыскать» /// (CorpseLootPatch.cs). Мешок появляется только по обыску. Сам бросок и вся арифметика ниже /// от этого не изменились: они по-прежнему решают судьбу ОБЫЧНОГО мешка. /// /// Второй префикс (на DropBagServer) УДАЛЁН ЦЕЛИКОМ - он теперь вреден: подменял бы и тот /// обычный мешок, который мы как раз хотим оставить. Спавн «Жертвы» переехал сюда, в /// SpawnVictimBag. /// /// ПОЧЕМУ ВООБЩЕ НУЖЕН ПРЕФИКС, А НЕ ПОСТФИКС. Узнать «выпал ли мешок» после оригинала нечем: /// dropItemOnDeath ничего не возвращает и следов не оставляет. Поэтому бросок повторяется /// здесь, а оригинал не запускается. Ветка оригинала, которая при этом теряется, - выброс /// собственного инвентаря для сущностей БЕЗ ИИ (`if (!hasAI)`), и к зомби она не относится /// никогда; на всякий случай она всё равно отдана оригиналу проверкой hasAI ниже. /// /// LootContainer.NoLoot УВАЖАЕТСЯ: если в настройках мира мешки выключены, «Жертва» тоже не /// падает. Это мировая настройка игрока, а не место мода спорить с ней. /// [HarmonyPatch(typeof(EntityAlive), "dropItemOnDeath")] public static class Patch_EntityAlive_dropItemOnDeath_VictimBag { public const string VictimBuffName = "buffNecroVictim"; public const string VictimContainerClassName = "EntityLootContainerVictim"; public static bool Prefix(EntityAlive __instance) { if (__instance.Buffs == null || !__instance.Buffs.HasBuff(VictimBuffName)) { return true; } if (!__instance.hasAI) { // Не наш случай: у оригинала для таких сущностей своя работа (выброс инвентаря), // и дублировать её здесь незачем. Зомби сюда не попадают. return true; } if (!SingletonMonoBehaviour.Instance.IsServer) { 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; } /// Спавнит мешок «Жертва». Раньше это жило в префиксе на Entity.DropBagServer и /// подменяло собой обычный мешок; теперь ничего не подменяет, поэтому тот патч снят. /// /// _offsetAside - сдвинуть в сторону, потому что рядом уже лёг обычный мешок. Ваниль /// кладёт свой ровно в GetPosition() + 0.9 по Y (декомпилировано, Entity.DropBagServer), /// то есть без сдвига два мешка встали бы в одну точку и выглядели как один. public static void SpawnVictimBag(EntityAlive _victim, bool _offsetAside) { int containerClassId = EntityClass.GetId(VictimContainerClassName); if (containerClassId == -1) { Debug.LogWarning("[NecromancerTome] VictimPatch: класс '" + VictimContainerClassName + "' не найден, мешок не выпадет"); return; } 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: " + _victim.entityId + " уронил «Жертву» " + lootContainer.entityId + (_offsetAside ? " (вторым мешком, со сдвигом)" : " (единственным мешком)")); } else { Debug.LogWarning("[NecromancerTome] VictimPatch: созданная сущность для '" + VictimContainerClassName + "' не оказалась EntityLootContainer"); } } } }