using System.Collections.Generic; using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// УБИЙСТВА ПИТОМЦЕМ ТЕПЕРЬ ЗАСЧИТЫВАЮТСЯ ВЛАДЕЛЬЦУ - 2026-09-18. /// /// ПОЧЕМУ РАНЬШЕ НЕ ЗАСЧИТЫВАЛИСЬ. Ваниль решает, чей это фраг, ровно в одном месте: /// /// public void AwardKillXPServer(DamageSource _source, EntityAlive _killingEntity) /// { /// if (_source == null || _source.BuffClass != null) return; // (2) /// EntityPlayer entityPlayer = _killingEntity as EntityPlayer; // (1) /// if ((bool)entityPlayer && !(entityPlayer == this) && ...) /// entityPlayer.AddKillXP(this, _source.AttackingItem, num); /// } /// /// (1) Убийца обязан БЫТЬ игроком. При укусе питомца _killingEntity - это сам питомец /// (берётся как world.GetEntity(_dmResponse.Source.getEntityId())), приведение к /// EntityPlayer даёт null, и AddKillXP не вызывается вовсе. А счётчик Некромантии висит /// постфиксом именно на AddKillXP (NecromancyKillCreditPatch.cs) - значит не растут ни /// уровень, ни урон Ножа некроманта, который от этого счётчика и считается. /// /// ЧТО ДЕЛАЕТ ЭТОТ ПРЕФИКС. Подменяет _killingEntity на владельца, если убил его питомец. /// Дальше всё едет по ванильному пути само: и опыт, и наш счётчик, и урон Ножа, и испытания - /// потому что мод по-прежнему СЛЕДУЕТ решению ванили, а не дублирует его. Это то же правило, /// ради которого счёт убийств 16.09 переехал из XML в постфикс на AddKillXP. /// /// ПОЧЕМУ ЭТО ВООБЩЕ ПОТРЕБОВАЛОСЬ ОБСУЖДАТЬ. 17.09 пользователь решил обратное для зомби под /// Камнем духов: "если зомби под контролем камня духов убивает другого, то в скилл это не /// идёт, что в целом логично. Пока оставляем так". Подчинённые зомби в TrackedPets не лежат, /// поэтому то решение остаётся в силе - здесь меняется только судьба ПРИЗВАННЫХ питомцев, /// которых игрок крафтит, кормит и водит за собой. /// /// ВТОРАЯ ПОЛОВИНА: ДОБИВАНИЕ КРОВОТЕЧЕНИЕМ. Строка (2) отсекает любой урон от баффа ДО /// всякой проверки убийцы - то есть зомби, истёкший кровью, не даёт опыта никому и никогда, /// даже если рану нанёс сам игрок. Для Духа крысы это половина всего урона (buffNecroRatBleed /// капает 5 в секунду при укусе в 5), и без этой части починка была бы половинчатой. /// /// Мешало одно: в DamageSource от баффа НЕТ идентификатора того, кто бафф наложил. Значит /// помнить приходится самим - что и делает RecentPetBites ниже: каждый удар нашего питомца /// записывает "эту жертву кусал питомец такого-то" со сроком годности чуть больше, чем живёт /// кровотечение. Когда жертва умирает от баффа, запись и отвечает, кому зачесть. /// /// Ванильный путь при этом НЕ ПРАВИТСЯ: мы не обнуляем _source.BuffClass (это общий объект, /// его читают и дальше - PartyShareKillServer, разбор смерти игрока), а сами зовём /// AddKillXP и отменяем оригинал, который всё равно вышел бы ни с чем. /// [HarmonyPatch(typeof(EntityAlive), "AwardKillXPServer")] public static class Patch_EntityAlive_AwardKillXPServer_PetCredit { public static bool Prefix(EntityAlive __instance, DamageSource _source, ref EntityAlive _killingEntity) { if (_source == null) { return true; } // Смерть от баффа: ваниль выйдет по строке (2) ни с чем, поэтому зачисляем сами. if (_source.BuffClass != null) { EntityPlayer bleedOwner = RecentPetBites.OwnerOf(__instance.entityId); if (bleedOwner == null || bleedOwner == __instance || !EntityClass.list.ContainsKey(__instance.entityClass)) { return true; } // KillXPScale у баффа обычно нулевой - это множитель "сколько опыта дать", и на // нуле AddKillXP не дал бы ничего. Единица = обычное убийство. float scale = _source.KillXPScale > 0f ? _source.KillXPScale : 1f; bleedOwner.AddKillXP(__instance, _source.AttackingItem, scale); RecentPetBites.Forget(__instance.entityId); Debug.Log("[NecromancerTome] PetKillCredit: bleed kill of " + __instance.entityId + " credited to owner " + bleedOwner.entityId); return false; } if (_killingEntity == null || _killingEntity is EntityPlayer) { return true; } EntityPlayer owner = PetFollowPatch.FindOwnerOfPet(_killingEntity.entityId); if (owner == null) { return true; } Debug.Log("[NecromancerTome] PetKillCredit: kill by pet " + _killingEntity.entityId + " credited to owner " + owner.entityId); _killingEntity = owner; return true; } } /// Кто кого кусал из питомцев и когда. Запись живёт чуть дольше кровотечения /// (buffNecroRatBleed - 15 секунд), чтобы добивание точно попало в окно, и при этом не /// превращалась в вечную память: зомби, укушенный минуту назад и умерший от чужой ловушки, /// не должен приносить очки владельцу питомца. public static class RecentPetBites { public const float MemorySeconds = 18f; public struct Bite { public int OwnerEntityId; public float ExpiresAt; } public static readonly Dictionary Bites = new Dictionary(); public static void Remember(int _victimEntityId, int _ownerEntityId) { Bites[_victimEntityId] = new Bite { OwnerEntityId = _ownerEntityId, ExpiresAt = Time.time + MemorySeconds, }; } public static void Forget(int _victimEntityId) { Bites.Remove(_victimEntityId); } public static EntityPlayer OwnerOf(int _victimEntityId) { if (!Bites.TryGetValue(_victimEntityId, out Bite bite)) { return null; } if (Time.time > bite.ExpiresAt) { Bites.Remove(_victimEntityId); return null; } World world = GameManager.Instance != null ? GameManager.Instance.World : null; return world != null ? world.GetEntity(bite.OwnerEntityId) as EntityPlayer : null; } } /// Здесь и запоминается укус. Точка выбрана самая общая: любой урон, нанесённый /// нашим питомцем, метит жертву - неважно, укус это, кровотечение от прошлого укуса или /// что-то ещё, что мы добавим потом. /// /// Постфикс, а не префикс: смысл только в том, чтобы записать факт, а вмешиваться в сам урон /// незачем. Заодно здесь же подчищается таблица - редко и дёшево, без своего таймера. [HarmonyPatch(typeof(EntityAlive), "DamageEntity")] public static class Patch_EntityAlive_DamageEntity_RememberPetBite { public static int callsSincePrune; public static void Postfix(EntityAlive __instance, DamageSource _damageSource) { if (_damageSource == null || __instance == null) { return; } int attackerId = _damageSource.getEntityId(); if (attackerId == -1 || attackerId == __instance.entityId) { return; } EntityPlayer owner = PetFollowPatch.FindOwnerOfPet(attackerId); if (owner == null) { return; } RecentPetBites.Remember(__instance.entityId, owner.entityId); if (++callsSincePrune >= 200) { callsSincePrune = 0; Prune(); } } public static void Prune() { if (RecentPetBites.Bites.Count == 0) { return; } List stale = new List(); foreach (KeyValuePair entry in RecentPetBites.Bites) { if (Time.time > entry.Value.ExpiresAt) { stale.Add(entry.Key); } } foreach (int id in stale) { RecentPetBites.Bites.Remove(id); } } } }