Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида духов и сюжетный финал через Чёрный портал. Локализация на 13 языках. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
101 lines
5.2 KiB
C#
101 lines
5.2 KiB
C#
using HarmonyLib;
|
|
using UnityEngine;
|
|
|
|
namespace NecromancerTome
|
|
{
|
|
/// <summary>
|
|
/// Necromancer's Knife (BACKLOG.md item 5, user request 2026-08-28): a zombie hit by the
|
|
/// knife carries buffNecroVictim (buffs.xml) until it dies. On death, it's guaranteed to drop
|
|
/// a green "Жертва" loot bag (EntityLootContainerVictim in entityclasses.xml) instead of
|
|
/// whatever the normal random loot roll would have given it.
|
|
///
|
|
/// Two patch points, found the hard way (2026-08-28, "лута всё ещё нет" after confirming via
|
|
/// the CharmPatch.cs-style AddBuff log that the debuff itself WAS being applied fine):
|
|
///
|
|
/// 1. Patch_EntityAlive_dropItemOnDeath_VictimBag - the actual gate. Confirmed by
|
|
/// decompiling EntityAlive.dropItemOnDeath(): it only calls DropBagServer() at all if
|
|
/// `lootDropProb > rand.RandomFloat` passes first - vanilla zombies have LootDropProb
|
|
/// around .04 (4%), so DropBagServer() simply never runs for ~96% of deaths. The first
|
|
/// version of this file only patched DropBagServer() itself, which was correct once
|
|
/// inside it but never got a chance to run for most kills - confirmed by the "AddBuff
|
|
/// Added" log firing repeatedly while the "zombie died" log from the DropBagServer patch
|
|
/// never fired even once for a real, logged kill. This Prefix on dropItemOnDeath() calls
|
|
/// DropBagServer() directly for a Victim-tagged zombie, bypassing the probability roll
|
|
/// entirely, then skips the rest of the original method (the only other thing it does -
|
|
/// dropping a non-AI entity's own inventory - never applies to a zombie anyway, hasAI is
|
|
/// always true for those).
|
|
/// 2. Patch_Entity_DropBagServer_VictimBag - decides WHICH bag. Confirmed by decompiling
|
|
/// Entity.DropBagServer(): it reads entityClass.lootDrops/LootDropPick(rand) - a STATIC
|
|
/// per-species value from the zombie's own entity_class (XML's LootDropEntityClass
|
|
/// property), not anything a live buff or CVar can influence declaratively (unlike the
|
|
/// knife's damage, which only needed a CVar the passive_effect system already reads
|
|
/// live) - this needed an actual Harmony patch, not an XML trick.
|
|
///
|
|
/// Both are Prefixes returning false: they fully replace what they intercept rather than
|
|
/// running alongside it.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(EntityAlive), "dropItemOnDeath")]
|
|
public static class Patch_EntityAlive_dropItemOnDeath_VictimBag
|
|
{
|
|
public static bool Prefix(EntityAlive __instance)
|
|
{
|
|
// Diagnostic-only, added 2026-08-28: unconditional, before any branching, to answer
|
|
// definitively whether Harmony is even entering this method at all - "лута всё ещё
|
|
// нет" after the first fix, but with zero sign of even the unconditional part of this
|
|
// Prefix ever running (not even a false-branch silently returning - literally no log
|
|
// line at all), which is otherwise unexplained since decompiling
|
|
// EntityAlive.OnEntityDeath() confirms it calls dropItemOnDeath() directly,
|
|
// unconditionally, right after the exact "Entity X killed by Y" line seen in the log.
|
|
Debug.Log("[NecromancerTome] VictimPatch: dropItemOnDeath Prefix entered for " + __instance.entityId + " (" + __instance.GetType().Name + "), hasVictimBuff=" + (__instance.Buffs != null && __instance.Buffs.HasBuff(Patch_Entity_DropBagServer_VictimBag.VictimBuffName)));
|
|
if (__instance.Buffs == null || !__instance.Buffs.HasBuff(Patch_Entity_DropBagServer_VictimBag.VictimBuffName))
|
|
{
|
|
return true;
|
|
}
|
|
Debug.Log("[NecromancerTome] VictimPatch: " + __instance.entityId + " died carrying buffNecroVictim - forcing guaranteed bag, bypassing LootDropProb roll");
|
|
__instance.DropBagServer();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
[HarmonyPatch(typeof(Entity), "DropBagServer")]
|
|
public static class Patch_Entity_DropBagServer_VictimBag
|
|
{
|
|
public const string VictimBuffName = "buffNecroVictim";
|
|
public const string VictimContainerClassName = "EntityLootContainerVictim";
|
|
|
|
public static bool Prefix(Entity __instance)
|
|
{
|
|
if (!(__instance is EntityAlive alive) || alive.Buffs == null || !alive.Buffs.HasBuff(VictimBuffName))
|
|
{
|
|
return true;
|
|
}
|
|
if (!SingletonMonoBehaviour<ConnectionManager>.Instance.IsServer || __instance is EntityLootContainer)
|
|
{
|
|
// Same guard the original method opens with - not our place to override these cases.
|
|
return true;
|
|
}
|
|
|
|
int containerClassId = EntityClass.GetId(VictimContainerClassName);
|
|
if (containerClassId == -1)
|
|
{
|
|
Debug.LogWarning("[NecromancerTome] VictimPatch: entity class '" + VictimContainerClassName + "' not found, falling back to normal loot");
|
|
return true;
|
|
}
|
|
|
|
Vector3 pos = __instance.GetPosition();
|
|
pos.y += 0.9f;
|
|
Entity spawned = EntityFactory.CreateEntity(containerClassId, pos, Vector3.zero);
|
|
if (spawned is EntityLootContainer lootContainer)
|
|
{
|
|
GameManager.Instance.World.SpawnEntityInWorld(lootContainer);
|
|
Debug.Log("[NecromancerTome] VictimPatch: " + __instance.entityId + " (victim) dropped guaranteed loot bag " + lootContainer.entityId);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("[NecromancerTome] VictimPatch: created entity for '" + VictimContainerClassName + "' wasn't an EntityLootContainer");
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|