Книга некроманта 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:
Alex Cube
2026-09-09 21:13:03 +03:00
co-authored by Claude Opus 5
commit e8f064f5ec
102 changed files with 7010 additions and 0 deletions
+171
View File
@@ -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);
}
}
}
}
}