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);
}
}
}
}