Работа по указаниям 2026-09-18. В релиз пока не выходит. НОВЫЙ ПИТОМЕЦ "Дух крысы" (necroRatSpirit), Некромантия 3 (60 убийств): модель зомбоволка в масштабе 0.28, урон 5, кроличьи звуки. Сам не нападает никогда, держится справа-сзади в двух блоках, упёршись в препятствие проходит СКВОЗЬ него и блоков не ломает. Укус замедляет, оставляет метку духа (видна на компасе, +25% получаемого урона) и рвёт жилу. Обгрызая труп, лечится. СВОЯ ЗАДАЧА ИИ. Задачи "иди за сущностью" в игре нет вовсе - проверены все 32 типа EAI*. Написана NecroFollowOwnerTask по образцу EAIApproachSpot: сектор "3-6 часов" от хозяина, FindPath с canBreak:false, проход сквозь препятствие через Entity.IsStuck. Вешается в рантайме, минуя Type.GetType. ПРИКАЗ АТАКОВАТЬ. Повторное применение свитка при живом питомце натравливает его на цель под прицелом: EntityPlayerLocal.HitInfo + ItemActionAttack. GetEntityFromHit. Торговцы и игроки отсеяны. Нет цели - "Нет цели для атаки". ЗОМБОЖИВОТНЫЕ ПРИВЕДЕНЫ К ТОМУ ЖЕ ОБРАЗЦУ. Сняты BreakBlock, Territorial, ApproachSpot, Wander, BlockingTargetTask и поедание трупов; цель они больше не выбирают сами. Лестница урона 20/35/45/60 плюс расчленение у Пса, Медведя и Волка. Кровотечение всем, метка и ослабление - только у крысы. УБИЙСТВА ПИТОМЦЕМ ЗАСЧИТЫВАЮТСЯ ВЛАДЕЛЬЦУ, включая добивание кровотечением. Префикс на AwardKillXPServer подменяет убийцу владельцем; для смерти от баффа заведена память укусов, потому что в DamageSource от баффа нет того, кто его наложил. Зомби под Камнем духов это не задело - решение от 17.09 в силе. ГРИФ откачен на летающую ветку EntityVulture и переименован в Могильного стервятника: модель наконец соответствует имени. Держится у игрока сам, через собственный механизм "дома" (setHomeArea), на время погони дом отвязывается. Попытка натянуть птичий префаб на наземный класс провалилась и записана - так делать нельзя. ПРОЧЕЕ: призрачный вид распространён с торговцев на питомцев (у Пса, Медведя и Волка выключен по указанию), у Пса светятся фиолетовые глаза, белая иконка книги снята со всех свитков призыва, "Жуки Властелина" переименованы в "Рой фараона" на всех 13 языках. ПОРОГИ: крыса 60, стервятник 500, пёс 1300, медведь 2000, волк 4000. ИСПРАВЛЕНО ПО ХОДУ ИГРОВЫХ ПРОВЕРОК: питомцы не призывались обычным кликом (AnimWait требовал удержания), отзыв срабатывал не с первого раза (автомат состояний ItemActionSpawnEntity), крыса проваливалась сквозь мир (IsStuck отключает и пол), питомец подбрасывал хозяина (коллайдеры разводились до появления модели). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
198 lines
9.3 KiB
C#
198 lines
9.3 KiB
C#
using System.Collections.Generic;
|
|
using HarmonyLib;
|
|
using UnityEngine;
|
|
|
|
namespace NecromancerTome
|
|
{
|
|
/// <summary>
|
|
/// УБИЙСТВА ПИТОМЦЕМ ТЕПЕРЬ ЗАСЧИТЫВАЮТСЯ ВЛАДЕЛЬЦУ - 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 и отменяем оригинал, который всё равно вышел бы ни с чем.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Кто кого кусал из питомцев и когда. Запись живёт чуть дольше кровотечения
|
|
/// (buffNecroRatBleed - 15 секунд), чтобы добивание точно попало в окно, и при этом не
|
|
/// превращалась в вечную память: зомби, укушенный минуту назад и умерший от чужой ловушки,
|
|
/// не должен приносить очки владельцу питомца.</summary>
|
|
public static class RecentPetBites
|
|
{
|
|
public const float MemorySeconds = 18f;
|
|
|
|
public struct Bite
|
|
{
|
|
public int OwnerEntityId;
|
|
public float ExpiresAt;
|
|
}
|
|
|
|
public static readonly Dictionary<int, Bite> Bites = new Dictionary<int, Bite>();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Здесь и запоминается укус. Точка выбрана самая общая: любой урон, нанесённый
|
|
/// нашим питомцем, метит жертву - неважно, укус это, кровотечение от прошлого укуса или
|
|
/// что-то ещё, что мы добавим потом.
|
|
///
|
|
/// Постфикс, а не префикс: смысл только в том, чтобы записать факт, а вмешиваться в сам урон
|
|
/// незачем. Заодно здесь же подчищается таблица - редко и дёшево, без своего таймера.</summary>
|
|
[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<int> stale = new List<int>();
|
|
foreach (KeyValuePair<int, RecentPetBites.Bite> entry in RecentPetBites.Bites)
|
|
{
|
|
if (Time.time > entry.Value.ExpiresAt)
|
|
{
|
|
stale.Add(entry.Key);
|
|
}
|
|
}
|
|
foreach (int id in stale)
|
|
{
|
|
RecentPetBites.Bites.Remove(id);
|
|
}
|
|
}
|
|
}
|
|
}
|