Книга некроманта 1.0 — первая публичная версия
Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида духов и сюжетный финал через Чёрный портал. Локализация на 13 языках. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user