Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида духов и сюжетный финал через Чёрный портал. Локализация на 13 языках. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
172 lines
7.6 KiB
C#
172 lines
7.6 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;
|
|
}
|
|
|
|
public static readonly List<TrackedPet> TrackedPets = new List<TrackedPet>();
|
|
|
|
public static float timer;
|
|
|
|
public static void Init()
|
|
{
|
|
ModEvents.UnityUpdate.RegisterHandler(OnUnityUpdate);
|
|
}
|
|
|
|
/// <summary>Called from SummonPatch.cs right after a pet is created and owned.</summary>
|
|
public static void Register(EntityAlive owner, Entity pet)
|
|
{
|
|
TrackedPets.Add(new TrackedPet { OwnerEntityId = owner.entityId, PetEntityId = pet.entityId });
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|