using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
///
/// 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.
///
[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.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;
}
}
}