Работа по указаниям 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>
469 lines
25 KiB
C#
469 lines
25 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Keeps a summoned pet (see SummonPatch.cs) from wandering off and getting permanently
|
||
/// lost/unsummonable - user report 2026-08-28: the Zombie Dog didn't follow like a drone
|
||
/// would, wandered off (heard, not seen), and a second summon attempt did nothing while the
|
||
/// first was presumably still out there somewhere.
|
||
///
|
||
/// Two separate problems, one fix:
|
||
///
|
||
/// 1. "Doesn't follow": there is no generic "follow a specific entity" AI task anywhere in
|
||
/// this game - confirmed by listing every EAI*-named type in Assembly-CSharp (EAIWander,
|
||
/// EAITerritorial, EAIApproachSpot, EAIApproachAndAttackTarget, etc. - nothing
|
||
/// follow-shaped). EntityDrone's follow behavior is hardcoded C# specific to that one
|
||
/// class, not something an XML entity_class can opt into. Writing a real custom AITask
|
||
/// (actual pathfinding, priority-tuned against the pet's existing Wander/Territorial/
|
||
/// ApproachSpot tasks) is real engineering the backlog didn't ask for - so this copies
|
||
/// vanilla's own fallback for the identical problem instead: DroneManager.Update()
|
||
/// teleports a drone back near its owner once it's more than 32m away (sqrMagnitude >
|
||
/// 1024) and not doing something else (OrderState != Stay) - confirmed by decompiling
|
||
/// it. Same threshold, same idea, generalized to any pet class instead of drones only,
|
||
/// and skipped while the pet has a live attack target (don't yank it out of a fight).
|
||
///
|
||
/// 2. "Doesn't reappear on retry": SummonPatch.cs's one-pet-per-species limit checks
|
||
/// EntityAlive.ownedEntities, but nothing was ever removing a pet from that list once it
|
||
/// died or its chunk unloaded - unlike the drone, where DroneManager's own death/unload
|
||
/// callbacks do that cleanup. A dead or vanished pet left the slot "occupied" forever.
|
||
/// The same periodic check below detects a tracked pet that's gone (world.GetEntity
|
||
/// returns null - dead, or its chunk unloaded and it despawned like any untracked
|
||
/// entity, per the known persistence gap documented in SummonPatch.cs) and clears
|
||
/// ownership so the player can summon a fresh one.
|
||
///
|
||
/// 3. (added 2026-08-28) Insect Swarm only: makes it move on to the next zombie once its
|
||
/// current target is already Deviator-charmed, instead of camping the same converted
|
||
/// zombie forever - see the inline comment below, right where it happens.
|
||
///
|
||
/// Hooked via ModEvents.UnityUpdate - the same supported per-frame mod event GameManager's own
|
||
/// gmUpdate() fires DroneManager.Update() from (confirmed by decompiling GameManager) - not a
|
||
/// Harmony patch, since this is a genuine public extension point, no reason to patch around
|
||
/// it. Throttled to run the real check once a second (CheckInterval): a distance/liveness
|
||
/// check on a handful of pets is cheap, but no reason to do it 60x/sec either.
|
||
/// </summary>
|
||
public static class PetFollowPatch
|
||
{
|
||
public const float LeashDistance = 32f; // matches DroneManager's own 32m leash
|
||
public const float LeashDistanceSq = LeashDistance * LeashDistance;
|
||
public const float CheckInterval = 1f;
|
||
|
||
public class TrackedPet
|
||
{
|
||
public int OwnerEntityId;
|
||
public int PetEntityId;
|
||
|
||
/// <summary>У питомца своя задача следования (NecroFollowOwnerTask, 2026-09-18) -
|
||
/// значит телепорт-поводок ниже его НЕ трогает. Уборка владения при этом остаётся:
|
||
/// она к способу передвижения отношения не имеет.</summary>
|
||
public bool OwnFollowTask;
|
||
|
||
/// <summary>Удалось ли развести коллайдеры питомца с хозяйскими. Пока false, попытка
|
||
/// повторяется каждый тик - см. TryIgnoreCollisionWithOwner.</summary>
|
||
public bool CollisionIgnored;
|
||
|
||
/// <summary>Зажжены ли глаза (PetEyeGlowPatch.cs). Как и коллайдеры, с первого раза
|
||
/// обычно не получается - модели ещё нет; попытка повторяется до успеха.</summary>
|
||
public bool EyesLit;
|
||
|
||
/// <summary>Нужно ли им вообще заниматься: копия PetInfo.LitEyes, чтобы тик не лазил
|
||
/// в словарь на каждом питомце каждую секунду.</summary>
|
||
public bool WantsLitEyes;
|
||
|
||
/// <summary>Радиус "дома" для летающих (см. PetInfo.FlyingHomeRadius). Ноль -
|
||
/// питомец не летающий, дом ему не переставляем.</summary>
|
||
public int FlyingHomeRadius;
|
||
}
|
||
|
||
public static readonly List<TrackedPet> TrackedPets = new List<TrackedPet>();
|
||
|
||
public static float timer;
|
||
|
||
public static void Init()
|
||
{
|
||
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
|
||
ModEvents.WorldShuttingDown.RegisterHandler(OnWorldShuttingDown);
|
||
}
|
||
|
||
/// <summary>ДОБАВЛЕНО 2026-09-18. TrackedPets - статический список, он переживает выход в
|
||
/// меню, а вот мир, к которому относятся лежащие в нём entityId, - нет. Без этой уборки
|
||
/// на следующей загрузке первый же тик начинал разбирать чужие идентификаторы: в новом
|
||
/// мире тот же номер принадлежит совершенно другой сущности, и RemoveOwnedEntity уходил
|
||
/// бы неизвестно куда. GhostTraderPatch.cs подписан на то же событие ровно по той же
|
||
/// причине; здесь подписки не было - это был найденный, но не закрытый пробел из разбора
|
||
/// ИИ питомцев (BACKLOG.md, 2026-09-18, п. 6).</summary>
|
||
public static void OnWorldShuttingDown(ref ModEvents.SWorldShuttingDownData _data)
|
||
{
|
||
if (TrackedPets.Count > 0)
|
||
{
|
||
Debug.Log("[NecromancerTome] PetFollowPatch: world shutting down, forgetting " +
|
||
TrackedPets.Count + " tracked pet(s)");
|
||
TrackedPets.Clear();
|
||
}
|
||
timer = 0f;
|
||
}
|
||
|
||
/// <summary>Called from SummonPatch.cs right after a pet is created and owned.</summary>
|
||
public static void Register(EntityAlive owner, Entity pet, bool ownFollowTask, bool collisionIgnored,
|
||
bool wantsLitEyes, int flyingHomeRadius)
|
||
{
|
||
TrackedPets.Add(new TrackedPet
|
||
{
|
||
OwnerEntityId = owner.entityId,
|
||
PetEntityId = pet.entityId,
|
||
OwnFollowTask = ownFollowTask,
|
||
CollisionIgnored = collisionIgnored,
|
||
WantsLitEyes = wantsLitEyes,
|
||
FlyingHomeRadius = flyingHomeRadius,
|
||
});
|
||
}
|
||
|
||
/// <summary>Разводит коллайдеры питомца и его хозяина, чтобы они проходили друг сквозь
|
||
/// друга. Возвращает true, только если РЕАЛЬНО что-то развела.
|
||
///
|
||
/// БАГ-РЕПОРТ 2026-09-18: "когда крыса оказывается под ногами, игрока подбрасывает".
|
||
/// Просьба сделать питомца нематериальным для игрока была ещё 28.08, и код для неё
|
||
/// написан тогда же (SummonPatch.IgnoreCollisionWithOwner) - но он вызывался ровно один
|
||
/// раз, из постфикса на EntityFactory.CreateEntity, а это СЛИШКОМ РАНО: там сущность
|
||
/// только создана и в мир ещё не добавлена (SpawnEntityInWorld идёт следующей строкой в
|
||
/// ItemActionSpawnEntity.Spawn), модель не собрана, и коллайдеров, привязанных к костям
|
||
/// через PhysicsBody, попросту ещё нет. GetComponentsInChildren возвращал пустой массив,
|
||
/// цикл не делал ни одной итерации, и никто этого не замечал - метод ничего не возвращал
|
||
/// и ничего не логировал. Ровно тот же урок про "модель собирается позже" уже записан в
|
||
/// GhostTraderPatch.cs, где из-за него пришлось опрашивать торговцев по таймеру.
|
||
///
|
||
/// Поэтому теперь попытка повторяется в тике раз в секунду, пока не удастся. Толкать
|
||
/// игрока питомец сможет в худшем случае одну секунду после призыва.
|
||
///
|
||
/// Разводятся ВСЕ пары коллайдеров, включая капсулы передвижения: CharacterController -
|
||
/// это тоже Collider, и именно на ней игрок и стоит, когда его подбрасывает.</summary>
|
||
public static bool TryIgnoreCollisionWithOwner(EntityAlive owner, Entity pet)
|
||
{
|
||
if (owner == null || pet == null)
|
||
{
|
||
return false;
|
||
}
|
||
Collider[] ownerColliders = owner.GetComponentsInChildren<Collider>(true);
|
||
Collider[] petColliders = pet.GetComponentsInChildren<Collider>(true);
|
||
if (ownerColliders == null || petColliders == null ||
|
||
ownerColliders.Length == 0 || petColliders.Length == 0)
|
||
{
|
||
return false;
|
||
}
|
||
int pairs = 0;
|
||
foreach (Collider oc in ownerColliders)
|
||
{
|
||
if (oc == null)
|
||
{
|
||
continue;
|
||
}
|
||
foreach (Collider pc in petColliders)
|
||
{
|
||
if (pc == null || pc == oc)
|
||
{
|
||
continue;
|
||
}
|
||
Physics.IgnoreCollision(oc, pc, true);
|
||
pairs++;
|
||
}
|
||
}
|
||
if (pairs > 0)
|
||
{
|
||
Debug.Log("[NecromancerTome] PetFollowPatch: pet " + pet.entityId + " passes through owner " +
|
||
owner.entityId + " now (" + ownerColliders.Length + "x" + petColliders.Length +
|
||
" collider pairs ignored)");
|
||
}
|
||
return pairs > 0;
|
||
}
|
||
|
||
/// <summary>Called from SummonPatch.cs's manual recall path so a recalled pet stops being
|
||
/// tracked immediately, instead of lingering until the next tick notices it's gone.</summary>
|
||
public static void Unregister(int petEntityId)
|
||
{
|
||
for (int i = TrackedPets.Count - 1; i >= 0; i--)
|
||
{
|
||
if (TrackedPets[i].PetEntityId == petEntityId)
|
||
{
|
||
TrackedPets.RemoveAt(i);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>ПИТАНИЕ ПАДАЛЬЮ, 2026-09-18. Наблюдение пользователя: "Дух крысы после того как
|
||
/// зомби погибает, начинает есть труп. Пусть тогда этот процесс восстанавливает ей здоровье".
|
||
///
|
||
/// Поедание тут не отдельное поведение, а побочный эффект приказа: команда атаковать ставит
|
||
/// цель на 6000 тиков (PetAttackCommand.OrderTicks), а EAIApproachAndAttackTarget.CanExecute
|
||
/// сверяет только ТИП цели и ничего не знает про её смерть - так что, добив зомби, крыса
|
||
/// продолжает грызть труп, пока тот не исчезнет. Само по себе это выглядело хорошо, поэтому
|
||
/// не чинится, а используется.
|
||
///
|
||
/// Лечение идёт отсюда, а не из triggered_effect в items.xml, по простой причине: в XML
|
||
/// нельзя спросить "цель мертва". Полный список классов Requirement* в Assembly-CSharp
|
||
/// просмотрен - там есть RequirementFullHealth, RequirementHasEntityTag, RequirementNearbyEntities
|
||
/// и ещё сорок, но ни одного про смерть цели. Значит гейт всё равно оказался бы в коде, а
|
||
/// тик раз в секунду тут уже есть и обходит ровно тех же питомцев.
|
||
///
|
||
/// Условий три, и все три обязательны: цель есть, цель МЕРТВА (иначе это лечение в бою, а
|
||
/// не питание), и крыса рядом с ней (иначе она лечилась бы, стоя в другом конце улицы и
|
||
/// только СОБИРАЯСЬ дойти до трупа).</summary>
|
||
public const float FeedRangeSq = 2.5f * 2.5f;
|
||
|
||
/// <summary>Здоровья за секунду поедания. У Духа крысы всего 120 HP, так что 5 - это полное
|
||
/// восстановление примерно за полминуты над одним трупом: заметно, но не бесплатно.</summary>
|
||
public const int FeedHealthPerSecond = 5;
|
||
|
||
/// <summary>ДЫРА, ЗАКРЫТАЯ 2026-09-18. Защита от падения живёт в NecroFollowOwnerTask, а
|
||
/// та НЕ ИСПОЛНЯЕТСЯ, пока у питомца есть цель (CanExecute возвращает false) - то есть
|
||
/// именно в бою, когда питомец и бегает по незнакомым местам, ловить его было нечем.
|
||
/// Этот тик работает всегда, поэтому проверка переехала сюда.
|
||
///
|
||
/// ПРОВЕРКИ ДВЕ, И ОНИ ПРО РАЗНОЕ - это важно не путать.
|
||
///
|
||
/// 1. АБСОЛЮТНАЯ ВЫСОТА. Ниже отметки 5 не бывает законных причин находиться: мир
|
||
/// кончается на нуле, и всё, что туда опустилось, игра удаляет молча
|
||
/// (Entity.onUpdate: position.y < 0f -> MarkToUnload). Двусмысленности нет никакой,
|
||
/// поэтому спасаем всегда, хоть в бою, хоть нет. Это и есть настоящий детектор
|
||
/// падения, и он один закрывает тот случай, на котором 18.09 потерялась крыса.
|
||
///
|
||
/// 2. НИЖЕ ЦЕЛИ (предложено пользователем). Это детектор НЕ падения, а
|
||
/// НЕДОСТИЖИМОСТИ: питомец, который на десять метров ниже того, кого ему велели
|
||
/// грызть, скорее всего не упал, а не может добраться - зомби на крыше, на этаже
|
||
/// выше, за проломом. Без этой проверки он будет ломиться туда вечно: приказ игрока
|
||
/// держится 6000 тиков и сам не истечёт.
|
||
///
|
||
/// Поэтому здесь цель ещё и СБРАСЫВАЕТСЯ. Вернуть питомца к хозяину, не сняв
|
||
/// приказ, значило бы получить маятник: прыжок наверх - бег вниз - прыжок наверх.
|
||
///
|
||
/// Обратная сторона второй проверки названа честно: зомби, стоящий двумя этажами выше,
|
||
/// теперь отменяет приказ вместо бесконечной беготни. По-моему это лучше, но если
|
||
/// окажется, что питомец сдаётся слишком рано - крутить UnreachableBelowTarget.</summary>
|
||
public const float UnreachableBelowTarget = 10f;
|
||
|
||
public static void RescueFallen(EntityAlive pet, EntityAlive owner)
|
||
{
|
||
if (pet == null || owner == null || pet.world == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
bool outOfWorld = pet.position.y < NecroFollowOwnerTask.WorldFloorGuard;
|
||
EntityAlive target = pet.GetAttackTarget();
|
||
bool unreachable = !outOfWorld && target != null && !target.IsDead() &&
|
||
pet.position.y < target.position.y - UnreachableBelowTarget;
|
||
if (!outOfWorld && !unreachable)
|
||
{
|
||
return;
|
||
}
|
||
|
||
pet.IsStuck = false;
|
||
pet.motion = Vector3.zero;
|
||
if (pet.moveHelper != null)
|
||
{
|
||
pet.moveHelper.Stop();
|
||
}
|
||
if (unreachable)
|
||
{
|
||
pet.SetAttackTarget(null, 0);
|
||
}
|
||
pet.SetPosition(NecroFollowOwnerTask.SlotPosition(owner, 0, pet.world), true);
|
||
Debug.Log("[NecromancerTome] PetFollowPatch: pet " + pet.entityId + " rescued to owner " +
|
||
owner.entityId + " - " + (outOfWorld
|
||
? "выпал из мира (высота " + pet.position.y.ToCultureInvariantString("0.0") + ")"
|
||
: "цель недостижима, она выше на " +
|
||
(target.position.y - pet.position.y).ToCultureInvariantString("0.0") + " м"));
|
||
}
|
||
|
||
/// <summary>Кому принадлежит этот питомец. null - значит не наш или уже не отслеживается.
|
||
/// Нужна PetKillCreditPatch.cs, чтобы зачесть убийство владельцу.</summary>
|
||
public static EntityPlayer FindOwnerOfPet(int petEntityId)
|
||
{
|
||
World world = GameManager.Instance != null ? GameManager.Instance.World : null;
|
||
if (world == null)
|
||
{
|
||
return null;
|
||
}
|
||
for (int i = 0; i < TrackedPets.Count; i++)
|
||
{
|
||
if (TrackedPets[i].PetEntityId == petEntityId)
|
||
{
|
||
return world.GetEntity(TrackedPets[i].OwnerEntityId) as EntityPlayer;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
public static void FeedOnCorpse(EntityAlive pet)
|
||
{
|
||
EntityAlive target = pet.GetAttackTarget();
|
||
if (target == null || !target.IsDead())
|
||
{
|
||
return;
|
||
}
|
||
if ((target.position - pet.position).sqrMagnitude > FeedRangeSq)
|
||
{
|
||
return;
|
||
}
|
||
int before = pet.Health;
|
||
pet.AddHealth(FeedHealthPerSecond);
|
||
if (pet.Health != before)
|
||
{
|
||
Debug.Log("[NecromancerTome] PetFollowPatch: pet " + pet.entityId + " fed on corpse " +
|
||
target.entityId + ", health " + before + " -> " + pet.Health);
|
||
}
|
||
}
|
||
|
||
public static void OnUnityUpdate(ref ModEvents.SUnityUpdateData _data)
|
||
{
|
||
timer += Time.deltaTime;
|
||
if (timer < CheckInterval)
|
||
{
|
||
return;
|
||
}
|
||
timer = 0f;
|
||
if (TrackedPets.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
World world = GameManager.Instance != null ? GameManager.Instance.World : null;
|
||
if (world == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
for (int i = TrackedPets.Count - 1; i >= 0; i--)
|
||
{
|
||
TrackedPet tracked = TrackedPets[i];
|
||
EntityAlive pet = world.GetEntity(tracked.PetEntityId) as EntityAlive;
|
||
if (pet == null || pet.IsDead())
|
||
{
|
||
EntityAlive ownerForCleanup = world.GetEntity(tracked.OwnerEntityId) as EntityAlive;
|
||
if (ownerForCleanup != null)
|
||
{
|
||
ownerForCleanup.RemoveOwnedEntity(tracked.PetEntityId);
|
||
Debug.Log("[NecromancerTome] PetFollowPatch: pet " + tracked.PetEntityId + " gone, cleared ownership for " + ownerForCleanup.entityId);
|
||
}
|
||
TrackedPets.RemoveAt(i);
|
||
continue;
|
||
}
|
||
|
||
EntityAlive owner = world.GetEntity(tracked.OwnerEntityId) as EntityAlive;
|
||
if (owner == null)
|
||
{
|
||
// Owner not currently loaded (e.g. disconnected) - leave the pet tracked,
|
||
// nothing useful to do until they're back.
|
||
continue;
|
||
}
|
||
|
||
// User request 2026-08-28: "рой будет заражать зомби девиацией и лететь к
|
||
// следующему незаражённому?" - not on its own, so this makes it one. EntityVulture
|
||
// (the Swarm's real base class - see SwarmTargetPatch.cs) has no notion of
|
||
// "this target is already converted, go find another" - once it has a live target
|
||
// it just keeps attacking it until that target dies, charmed or not (repeated
|
||
// AddBuff on an already-charmed zombie is a harmless no-op, so it wasn't wrong,
|
||
// just stuck). Clearing the attack target here when it's already charmed makes
|
||
// EntityVulture's own FindTarget()/SetAttackTarget cycle kick back in on its next
|
||
// pass (SwarmTargetPatch.cs redirects that straight to the nearest zombie again,
|
||
// same as any other retarget) - not instant (that cycle runs on its own ~2s timer,
|
||
// not driven by us), but converts-then-moves-on within a couple seconds.
|
||
if (pet.entityClass == Patch_EntityAlive_SetAttackTarget_SwarmRetarget.SwarmOnlyClassId())
|
||
{
|
||
EntityAlive currentTarget = pet.GetAttackTarget();
|
||
if (currentTarget != null && currentTarget.Buffs != null && currentTarget.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName))
|
||
{
|
||
pet.SetAttackTarget(null, 0);
|
||
Debug.Log("[NecromancerTome] PetFollowPatch: swarm " + pet.entityId + " dropped already-charmed target " + currentTarget.entityId);
|
||
}
|
||
}
|
||
|
||
// Повторная попытка развести коллайдеры - до первого успеха. Стоит ДО всех
|
||
// проверок про бой и расстояние: "питомец подбрасывает хозяина" не зависит ни от
|
||
// того, ни от другого.
|
||
if (!tracked.CollisionIgnored)
|
||
{
|
||
tracked.CollisionIgnored = TryIgnoreCollisionWithOwner(owner, pet);
|
||
}
|
||
|
||
// СЛЕДОВАНИЕ ДЛЯ ЛЕТАЮЩИХ, 2026-09-18. Дом переставляется на хозяина каждую
|
||
// секунду, а возвращается питомец САМ, своим полётом: у EntityVulture проверка
|
||
// "не ушёл ли из дома" встроена и работает каждые 60 тиков. Ни телепорта, ни
|
||
// пафайндинга, ни своей задачи - см. PetInfo.FlyingHomeRadius.
|
||
//
|
||
// НА ВРЕМЯ АТАКИ ДОМ ОТВЯЗЫВАЕТСЯ СОВСЕМ (указание того же дня: "для атаки радиус
|
||
// нужно увеличить, пусть летит сколько надо... а вот после атаки пусть
|
||
// возвращается"). Иначе собственная проверка EntityVulture срывала бы его с цели
|
||
// на полпути: она прерывает атаку, как только питомец вышел за радиус.
|
||
//
|
||
// Отвязка - это detachHome(), то есть maximumHomeDistance = -1, и тогда
|
||
// isWithinHomeDistanceCurrentPosition() безусловно возвращает true. Ограничителем
|
||
// вместо радиуса остаётся сама дальность приказа: цель назначается только по
|
||
// прицелу, а луч прицела не длиннее 30 метров, так что отправить Грифа на другой
|
||
// край карты нельзя при всём желании.
|
||
//
|
||
// Как только цель пропала (убита, недостижима, приказ сброшен), дом на следующей
|
||
// же секунде встаёт обратно на хозяина - и питомец возвращается сам.
|
||
if (tracked.FlyingHomeRadius > 0)
|
||
{
|
||
if (pet.GetAttackTarget() != null)
|
||
{
|
||
if (pet.hasHome())
|
||
{
|
||
pet.detachHome();
|
||
Debug.Log("[NecromancerTome] PetFollowPatch: flying pet " + pet.entityId +
|
||
" is on a target - home detached for the chase");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (!pet.hasHome())
|
||
{
|
||
Debug.Log("[NecromancerTome] PetFollowPatch: flying pet " + pet.entityId +
|
||
" finished the chase - home re-anchored to owner " + owner.entityId);
|
||
}
|
||
pet.setHomeArea(new Vector3i(owner.position), tracked.FlyingHomeRadius);
|
||
}
|
||
}
|
||
|
||
if (tracked.WantsLitEyes && !tracked.EyesLit)
|
||
{
|
||
PetEyeGlow.DumpMaterialsOnce(pet, EntityClass.list[pet.entityClass]?.entityClassName);
|
||
tracked.EyesLit = PetEyeGlow.TryLightEyes(pet);
|
||
}
|
||
|
||
if (tracked.OwnFollowTask)
|
||
{
|
||
FeedOnCorpse(pet);
|
||
RescueFallen(pet, owner);
|
||
// У этого питомца есть настоящая задача следования (NecroFollowOwnerTask):
|
||
// она сама держит его при хозяине, сама решает, когда пройти сквозь стену, и
|
||
// сама сажает его на опорный блок. Телепорт-поводок здесь только мешал бы -
|
||
// две системы дёргали бы питомца в разные стороны. Уборка владения выше при
|
||
// этом уже отработала, и она остаётся общей для всех.
|
||
continue;
|
||
}
|
||
|
||
if (pet.GetAttackTarget() != null)
|
||
{
|
||
// Mid-fight - let it finish rather than teleporting it away.
|
||
continue;
|
||
}
|
||
|
||
float distSq = (pet.position - owner.position).sqrMagnitude;
|
||
if (distSq > LeashDistanceSq)
|
||
{
|
||
// 2m in front of the owner, not exactly on top of them - landing right on the
|
||
// owner's own position stacks the pet's collider into the player's and shoves
|
||
// them (this is exactly what happened during the "<=0 vs -1" bug above: a pile
|
||
// of a dozen undespawned dogs all teleporting onto the same point as the player
|
||
// every second launched them into the air).
|
||
Vector3 dest = owner.position + owner.qrotation * new Vector3(0f, 0f, 2f);
|
||
pet.SetPosition(dest, true);
|
||
Debug.Log("[NecromancerTome] PetFollowPatch: teleported pet " + pet.entityId + " back to owner " + owner.entityId);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|