Книга некроманта 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,134 @@
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NecromancerTome
|
||||
{
|
||||
/// <summary>
|
||||
/// The core "Deviator" effect: whichever zombie the buff named in CharmBuffName lands on
|
||||
/// (via the item's own XML - see Config/items.xml/buffs.xml) stops targeting the player
|
||||
/// and starts targeting other zombies instead.
|
||||
///
|
||||
/// Why this needs Harmony at all: a zombie's valid-target class list (who it's even allowed
|
||||
/// to look for/attack) is parsed once from its entity_class XML into per-instance AI task
|
||||
/// objects at spawn time (EAIApproachAndAttackTarget / EAISetNearestEntityAsTarget). There is
|
||||
/// no XML-level action to change it afterwards - the buff system alone cannot flip a live
|
||||
/// zombie's allegiance. This patch is the smallest hook that can: it reacts to our specific
|
||||
/// buff being added and then rewrites those same per-instance task objects directly, using
|
||||
/// the exact same fields the game's own EAIManager.SetTargetOnlyPlayers() helper uses for the
|
||||
/// mirror-image trick (restricting a task to players only). We do the same thing in reverse:
|
||||
/// restrict to zombies only.
|
||||
/// </summary>
|
||||
[HarmonyPatch(typeof(EntityBuffs), "AddBuff", new System.Type[] { typeof(string), typeof(Vector3i), typeof(int), typeof(bool), typeof(bool), typeof(float) })]
|
||||
public static class Patch_EntityBuffs_AddBuff_DeviatorCharm
|
||||
{
|
||||
public const string CharmBuffName = "buffNecroDeviatorCharm";
|
||||
|
||||
/// <summary>Diagnostic-only, added 2026-08-28 while chasing "the Knife's Victim debuff
|
||||
/// never sticks" - this patch already logs every AddBuff call for our other marker buff,
|
||||
/// so logging buffNecroVictim's calls here too (no CharmZombie() side effects for it,
|
||||
/// just visibility) answers the actual open question directly: does
|
||||
/// necroWpnBladeNecroKnife's onSelfAttackedOther trigger ever even call AddBuff at all,
|
||||
/// or does it call it but something downstream (target class mismatch, requirement gate,
|
||||
/// stacking) rejects it. No log line at all next time means the XML trigger itself isn't
|
||||
/// firing (likely because EntityDamage computes to 0 and a 0-damage swing doesn't count
|
||||
/// as a landed hit); a log line with a non-Added result narrows it further.</summary>
|
||||
public const string VictimBuffName = "buffNecroVictim";
|
||||
|
||||
public static void Postfix(EntityBuffs __instance, string _name, EntityBuffs.BuffStatus __result)
|
||||
{
|
||||
if (_name == CharmBuffName || _name == VictimBuffName)
|
||||
{
|
||||
// entityId added 2026-08-28 while chasing "the zombie that died didn't have the
|
||||
// buff even though AddBuff Added fired repeatedly" - logging just the TYPE name
|
||||
// couldn't tell whether it landed on the same zombie that later died or a
|
||||
// different one nearby (the knife swings a 90-degree arc, plausible it's hitting
|
||||
// more than one zombie in a group). This settles it directly against
|
||||
// VictimPatch's own "dropItemOnDeath Prefix entered for <id>" line.
|
||||
string parentId = __instance.parent != null ? __instance.parent.entityId.ToString() : "?";
|
||||
Debug.Log("[NecromancerTome] AddBuff(" + _name + ") result=" + __result + " parent=" + (__instance.parent != null ? __instance.parent.GetType().Name : "null") + " id=" + parentId);
|
||||
}
|
||||
if (__result != EntityBuffs.BuffStatus.Added)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (_name != CharmBuffName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// HUMANOID-ONLY IS A DELIBERATE DESIGN DECISION, NOT A GAP TO CLOSE (user, 2026-09-07:
|
||||
// "Камень духов не годится, он для зомбо-гуманоидов, и пусть так и остаётся").
|
||||
// Do not "fix" this by widening the type check.
|
||||
//
|
||||
// The user had noticed the behaviour in play first ("камень душ на зомбособаках не
|
||||
// работает, это я проверил однажды") and was right. The reason is this line: zombie
|
||||
// ANIMALS sit on a completely different branch of the C# hierarchy, so the pattern
|
||||
// match below is false for every one of them -
|
||||
// EntityZombie : EntityHuman <- the only charmable one
|
||||
// EntityZombieDog : EntityEnemyAnimal : EntityEnemy
|
||||
// EntityEnemyAnimal : EntityEnemy (bear, boar)
|
||||
// EntityVulture : EntityFlying (not even EntityEnemy)
|
||||
// - while the XML side still lets the buff land on them, because they DO carry the
|
||||
// "zombie" tag ("entity,animal,zombie,zombieAnimal,hostile,..."), which is what
|
||||
// EntityTagCompare gates on. So the buff is added, this check rejects it, and the log
|
||||
// line below fires. That log line is expected on zombie animals and is not an error.
|
||||
//
|
||||
// Note the same decision applies to the other half of this patch: CharmZombie() below
|
||||
// restricts targetClasses to typeof(EntityZombie), so a charmed humanoid also ignores
|
||||
// zombie animals as targets. Consistent with "the Spirit Stone is a humanoid tool".
|
||||
// Countering zombie animals/birds is meant to be a separate mechanic - open topic,
|
||||
// see BACKLOG.md 2026-09-07.
|
||||
if (!(__instance.parent is EntityZombie zombie) || zombie.aiManager == null)
|
||||
{
|
||||
Debug.Log("[NecromancerTome] charm buff added but parent is not a charmable EntityZombie (aiManager null or wrong type)");
|
||||
return;
|
||||
}
|
||||
CharmZombie(zombie);
|
||||
}
|
||||
|
||||
public static void CharmZombie(EntityZombie zombie)
|
||||
{
|
||||
// AITask: whichever task actually chases/melees the current attack target.
|
||||
// Restrict it to EntityZombie only, same shape as SetTargetOnlyPlayers() but reversed.
|
||||
List<EAIApproachAndAttackTarget> approachTasks = zombie.aiManager.GetTasks<EAIApproachAndAttackTarget>();
|
||||
Debug.Log("[NecromancerTome] CharmZombie: approachTasks=" + (approachTasks != null ? approachTasks.Count.ToString() : "null"));
|
||||
if (approachTasks != null)
|
||||
{
|
||||
foreach (EAIApproachAndAttackTarget task in approachTasks)
|
||||
{
|
||||
task.targetClasses.Clear();
|
||||
task.targetClasses.Add(new EAIApproachAndAttackTarget.TargetClass
|
||||
{
|
||||
type = typeof(EntityZombie),
|
||||
chaseTimeMax = 0f
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// AITarget: whichever task picks the nearest valid entity to go attack in the first
|
||||
// place. Same restriction here, or the zombie would never even pick a target to hand
|
||||
// off to the approach task above.
|
||||
List<EAISetNearestEntityAsTarget> targetTasks = zombie.aiManager.GetTargetTasks<EAISetNearestEntityAsTarget>();
|
||||
Debug.Log("[NecromancerTome] CharmZombie: targetTasks=" + (targetTasks != null ? targetTasks.Count.ToString() : "null"));
|
||||
if (targetTasks != null)
|
||||
{
|
||||
foreach (EAISetNearestEntityAsTarget task in targetTasks)
|
||||
{
|
||||
task.targetClasses.Clear();
|
||||
task.targetClasses.Add(new EAISetNearestEntityAsTarget.TargetClass
|
||||
{
|
||||
type = typeof(EntityZombie),
|
||||
hearDistMax = 50f,
|
||||
seeDistMax = 0f
|
||||
});
|
||||
task.playerTargetClassIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Drop whatever it was mid-attack on (almost certainly the player who just threw the
|
||||
// book at it) so the switch takes effect immediately instead of after its current target dies/despawns.
|
||||
zombie.SetAttackTarget(null, 0);
|
||||
Debug.Log("[NecromancerTome] CharmZombie: done, attack target cleared for " + zombie.EntityName);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user