ИИ питомцев переписан, добавлен Дух крысы, пороги разнесены по шкале
Работа по указаниям 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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
15c59dd37a
commit
d97f83e972
@@ -0,0 +1,197 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user