using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
///
/// "Книга банши" (Banshee's Book) - BACKLOG.md item 7 (dictated 2026-08-28, implemented
/// 2026-08-29 "без вопросов" per user request). On use, the book is consumed, plays a
/// screamer's own scream sound, and spawns a small HOSTILE horde near the player - hostile to
/// the player, unlike the Dog/Swarm/three new zombie pets, which are all summoned allies.
///
/// DELIBERATE SIMPLIFICATION, flagged rather than guessed past - read before touching this
/// file again: the backlog's own research pointed at AIScoutHordeSpawner (decompiled here,
/// see the class itself in Assembly-CSharp) as "the" mechanism a real zombieScreamer's scream
/// uses. Decompiling it in full shows it is NOT a simple "spawn N zombies now" call - it's a
/// whole per-tick simulation object (constructed with an EntitySpawner, driven by
/// AIDirector.CanSpawn()/EntitySpawner.CurrentWave/SpawnManually, tracking a scout zombie
/// that has to physically wander off, spot a player, and only THEN calls its own
/// spawnHordeNear near that scout) - built for the existing "distant scout triggers a
/// blood-moon-style horde" system, not for "an item makes a horde appear right now". Wiring a
/// real AIScoutHordeSpawner up from a one-shot item click would mean also owning a per-tick
/// driver for it (another ModEvents.UnityUpdate loop, same shape as PetFollowPatch.cs) and an
/// EntitySpawner instance to hand it, for a payoff (a scout that has to run off and get
/// spotted first) the user's own description doesn't ask for - they asked for the scream and
/// the horde appearing "poblizosti" (nearby), not a scout-fetch quest.
///
/// So instead: this directly builds a small set of ordinary hostile zombies near the player
/// using the exact same EntityFactory.CreateEntity -> SetSpawnerSource -> SpawnEntityInWorld
/// sequence ItemActionSpawnEntity.Spawn itself uses (decompiled to confirm, not guessed) -
/// the same primitive this mod's own pet summons are already built on
/// (HarmonySrc/SummonPatch.cs), just spawning several real vanilla zombie classes instead of
/// one tamed pet, and never touching their AI/flags at all (they're meant to be hostile,
/// which is what an untouched vanilla zombie already is by default - the OPPOSITE of the
/// pet-taming work the rest of this mod does).
///
/// Zombie pool (zombieArlene/zombieBoe/zombieYo/zombieJoe) is a hardcoded guess at "generic
/// early/mid walker" flavor, picked because all four are confirmed real vanilla entity_class
/// names (checked directly against Data/Config/entityclasses.xml) with no special
/// gimmick (no explosion, no ranged attack) - NOT gamestage-scaled or otherwise tied to the
/// real difficulty-scaling horde-selection system real hordes use. Spawn positions come from
/// World.GetMobRandomSpawnPosWithWater (decompiled from AIScoutHordeSpawner's own use of it) -
/// the same "find a valid ground spot near here" helper the real scout-horde system itself
/// calls, just pointed at the player directly instead of a scout zombie's position.
///
[HarmonyPatch(typeof(ItemActionEat))]
[HarmonyPatch("consume")]
public static class Patch_ItemActionEat_Consume_Banshee
{
public const string ItemName = "bookBanshee";
/// Real, confirmed vanilla entity_class names - not invented. Deliberately no
/// exploders/spitters/screamers-of-their-own in the pool (would either be anticlimactic -
/// a screamer summoning more screamers - or risk chain-reaction explosions on the caster).
public static readonly string[] ZombiePool = { "zombieArlene", "zombieBoe", "zombieYo", "zombieJoe" };
/// "Небольшая орда" - not specified by the user as an exact number, guessed
/// modest (comparable to a small blood-moon wave, not a screen-filling swarm).
public const int HordeSize = 5;
public static readonly System.Random Rand = new System.Random();
public static void Postfix(ItemActionData _actionData)
{
string itemName = _actionData?.invData?.itemValue?.ItemClass?.Name;
if (itemName != ItemName)
{
return;
}
EntityAlive holdingEntity = _actionData.invData.holdingEntity;
World world = holdingEntity?.world;
if (holdingEntity == null || world == null)
{
return;
}
Debug.Log("[NecromancerTome] BansheePatch: casting near owner=" + holdingEntity.entityId);
// Reuses the real screamer's own alert sound (zombiefemalescoutalert, see
// zombieScreamer in Data/Config/entityclasses.xml's SoundAlert) rather than inventing
// a new one - PlayOneShot(string) confirmed via ItemActionEat's own
// ExecuteInstantAction, same call shape used there.
holdingEntity.PlayOneShot("zombiefemalescoutalert");
Vector3 casterPos = holdingEntity.GetPosition();
for (int i = 0; i < HordeSize; i++)
{
string zombieClassName = ZombiePool[Rand.Next(ZombiePool.Length)];
int classId = EntityClass.GetId(zombieClassName);
if (classId == -1)
{
Debug.LogWarning("[NecromancerTome] BansheePatch: entity class '" + zombieClassName + "' not found");
continue;
}
// 15-30m out, never closer than 15m to the caster - same shape of call
// AIScoutHordeSpawner itself makes to place a zombie near a point without
// dropping it inside terrain/right on top of a player.
if (!world.GetMobRandomSpawnPosWithWater(casterPos, 15, 30, 15, true, out Vector3 spawnPos))
{
Debug.LogWarning("[NecromancerTome] BansheePatch: no valid spawn position found for " + zombieClassName);
continue;
}
Entity entity = EntityFactory.CreateEntity(classId, spawnPos, new Vector3(0f, holdingEntity.rotation.y, 0f));
entity.SetSpawnerSource(EnumSpawnerSource.StaticSpawner);
world.SpawnEntityInWorld(entity);
Debug.Log("[NecromancerTome] BansheePatch: spawned " + zombieClassName + " (" + entity.entityId + ") at " + spawnPos);
}
}
}
}