using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// Нож некроманта (BACKLOG.md, п.5): зомби, задетый ножом, носит buffNecroVictim (buffs.xml) до /// самой смерти. На смерти его труп становится обыскиваемым контейнером с добычей «Жертвы» /// (CorpseLootPatch.cs; до 1.5.0 обыск превращал труп в мешок EntityLootContainerVictim). /// /// ПОВЕДЕНИЕ ИСПРАВЛЕНО 2026-09-18, И ЭТО ВОЗВРАТ К ТОМУ, О ЧЁМ ДОГОВАРИВАЛИСЬ. Баг-репорт /// пользователя: «мы договаривались, что он выпадает только если не должен был выпасть мешок с /// лутом. Но похоже падает только мешок жертвы». /// /// Так и было. Прежняя версия состояла из двух префиксов, и оба возвращали false: /// /// 1. на dropItemOnDeath - при дебафе звала DropBagServer() НАПРЯМУЮ, минуя ванильный /// бросок вероятности, и обрывала оригинал; /// 2. на DropBagServer - при дебафе спавнила «Жертву» ВМЕСТО обычного мешка. /// /// Вместе это означало: помеченный зомби роняет «Жертву» ВСЕГДА и обычный мешок НИКОГДА. То /// есть нож не добавлял добычу, а подменял её - и в редких 4% случаев, когда игроку и так /// причитался нормальный мешок, он его терял. /// /// СТАЛО (уточнено пользователем в тот же день - «а можно сделать так, чтобы в случае когда /// положен лут, падало два мешка»): «Жертва» не заменяет обычный мешок и не ждёт его отсутствия, /// а ДОБАВЛЯЕТСЯ. Ванильный бросок делается как есть (формула скопирована из /// EntityAlive.dropItemOnDeath дословно, включая модификатор от оружия убийцы и множитель /// LootContainer.LootBagChance), и дальше: /// /// бросок прошёл -> обычный мешок рядом И добыча в трупе; /// бросок не прошёл -> только добыча в трупе. /// /// То есть помеченный ножом зомби никогда не оставляет игрока ни с чем, а в удачном случае /// даёт вдвое больше обычного. Это осознанно щедро: метку ещё надо поставить ножом, которым /// по зомби надо попасть. /// /// УТОЧНЕНО В ТОТ ЖЕ ДЕНЬ: «Жертва» больше не падает мешком в момент смерти - она остаётся /// ВНУТРИ ТРУПА, который перекрашивается, перестаёт исчезать и получает команду «обыскать» /// (CorpseLootPatch.cs). Мешок появляется только по обыску. Сам бросок и вся арифметика ниже /// от этого не изменились: они по-прежнему решают судьбу ОБЫЧНОГО мешка. /// /// Второй префикс (на DropBagServer) УДАЛЁН ЦЕЛИКОМ - он теперь вреден: подменял бы и тот /// обычный мешок, который мы как раз хотим оставить. /// /// С 1.5.0 мешка «Жертва» нет совсем: сам труп открывается как ящик (CorpseLootPatch.cs), и /// метод SpawnVictimBag удалён. Класс EntityLootContainerVictim в entityclasses.xml ОСТАВЛЕН /// ради сейвов: в мирах игроков могут лежать уже выпавшие мешки, и без класса они не загрузятся. /// /// ПОЧЕМУ ВООБЩЕ НУЖЕН ПРЕФИКС, А НЕ ПОСТФИКС. Узнать «выпал ли мешок» после оригинала нечем: /// dropItemOnDeath ничего не возвращает и следов не оставляет. Поэтому бросок повторяется /// здесь, а оригинал не запускается. Ветка оригинала, которая при этом теряется, - выброс /// собственного инвентаря для сущностей БЕЗ ИИ (`if (!hasAI)`), и к зомби она не относится /// никогда; на всякий случай она всё равно отдана оригиналу проверкой hasAI ниже. /// /// LootContainer.NoLoot УВАЖАЕТСЯ: если в настройках мира мешки выключены, «Жертва» тоже не /// падает. Это мировая настройка игрока, а не место мода спорить с ней. /// [HarmonyPatch(typeof(EntityAlive), "dropItemOnDeath")] public static class Patch_EntityAlive_dropItemOnDeath_VictimBag { public const string VictimBuffName = "buffNecroVictim"; 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; } } }