using System.Collections.Generic;
using HarmonyLib;
using UnityEngine;
using UnityEngine.Scripting;
namespace NecromancerTome
{
///
/// Чёрный разрыв: зомби на смерти рассыпается облаком чёрных частиц со звуком «gore_head», и
/// трупа не остаётся. Свой аналог ванильного Twitch-эффекта «Celebrate» (конфетти), продиктовано
/// 28.09.2026 как заготовка под дистанционную атаку (BACKLOG: «рывок и взрыв зомби»).
///
/// КАК УСТРОЕН ОРИГИНАЛ (EntityAlive.ClientKill, игра 3.3). Уже после выпадения добычи
/// (OnEntityDeath) и начисления опыта (AwardKillXPServer), если убийца - игрок, а на нём
/// пассивка CelebrationKill, сервер делает три вещи: SpawnParticleEffectServer("confetti"),
/// Manager.BroadcastPlay("twitch_celebrate") и World.RemoveEntity(Killed) - вместо рэгдолла.
/// Имена частиц и звука там зашиты строками, поэтому ванильный путь не переиспользуется: мы
/// повторяем те же три вызова своим постфиксом на ClientKill для помеченных зомби.
///
/// ПОЧЕМУ ЧАСТИЦЫ - КОПИЯ, А НЕ ПЕРЕКРАСКА. Цвет из ParticleEffect игра применяет только к
/// рендерерам БЕЗ ParticleSystem (ParticleEffect.SpawnParticleEffect), то есть конфетти он
/// не красит. Поэтому при первом обращении клонируем префаб «confetti», красим в клоне сами
/// системы частиц и кладём его в реестр игры (ParticleEffect.loadedTs) под своим именем.
/// Ванильные конфетти остаются цветными. Клон строится лениво в GetDynamicTransform - это та
/// точка, через которую идёт любой спавн частиц и на сервере, и на клиенте.
///
/// Счёт Некромантии и добыча не страдают: к моменту постфикса они уже отработали. А вот
/// красный труп Жертвы с разрывом несовместим - тело удаляется (решать при дистанционной атаке).
///
public static class NecroBurst
{
public const string ParticleName = "necroblackburst";
public const string SourceParticleName = "confetti";
public const string SoundName = "gore_head";
/// Почти чёрный, а не 0,0,0: у чистого чёрного пропадает объём частиц.
public static readonly Color Tint = new Color(0.04f, 0.035f, 0.045f, 1f);
/// Зомби, которые на ближайшей смерти должны разорваться. Сервер.
public static readonly HashSet Pending = new HashSet();
/// Разрыв случился: жертва и Transform частиц (может быть null).
public static System.Action OnBurst;
static GameObject _holder;
public static int ParticleId => ParticleEffect.ToId(ParticleName);
/// Строит чёрный клон конфетти и регистрирует его. true - клон есть.
public static bool EnsureRegistered()
{
int id = ParticleId;
if (ParticleEffect.loadedTs.ContainsKey(id))
{
return true;
}
if (!ParticleEffect.loadedTs.TryGetValue(ParticleEffect.ToId(SourceParticleName), out Transform src) || src == null)
{
Debug.LogWarning("[NecromancerTome] NecroBurst: ванильный эффект '" + SourceParticleName + "' не найден");
return false;
}
// Неактивный держатель: клон под ним не играет сам, а копии, которые игра делает из
// клона через Instantiate, выходят активными (активность берётся у самого клона).
if (_holder == null)
{
_holder = new GameObject("NecroBurstPrefabs");
_holder.SetActive(false);
Object.DontDestroyOnLoad(_holder);
}
Transform clone = Object.Instantiate(src, _holder.transform);
clone.name = ParticleEffect.prefix + ParticleName;
foreach (ParticleSystem ps in clone.GetComponentsInChildren(true))
{
ParticleSystem.MainModule main = ps.main;
main.startColor = new ParticleSystem.MinMaxGradient(Tint);
ParticleSystem.ColorOverLifetimeModule col = ps.colorOverLifetime;
if (col.enabled)
{
// Прозрачность по времени жизни оставляем как есть, цвет - в чёрный.
Gradient g = new Gradient();
GradientAlphaKey[] alpha = col.color.gradient != null
? col.color.gradient.alphaKeys
: new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) };
g.SetKeys(new[] { new GradientColorKey(Tint, 0f), new GradientColorKey(Tint, 1f) }, alpha);
col.color = new ParticleSystem.MinMaxGradient(g);
}
ParticleSystemRenderer r = ps.GetComponent();
if (r != null && r.sharedMaterial != null)
{
// Печатаем шейдер: если он аддитивный, чёрный на нём невидим, и это сразу видно
// в логе, а не гаданием по пустому месту на экране.
Debug.Log("[NecromancerTome] NecroBurst: система '" + ps.name + "', шейдер '" +
r.sharedMaterial.shader.name + "', материал '" + r.sharedMaterial.name + "'");
}
}
ParticleEffect.loadedTs[id] = clone;
Debug.Log("[NecromancerTome] NecroBurst: зарегистрирован эффект '" + ParticleName + "'");
return true;
}
/// Разорвать уже мёртвого зомби: частицы, звук, удаление тела. Только сервер.
public static void Burst(EntityAlive _victim, Entity _soundSource)
{
if (!SingletonMonoBehaviour.Instance.IsServer)
{
return;
}
World world = GameManager.Instance.World;
Vector3 pos = _victim.position;
Transform effect = null;
if (EnsureRegistered())
{
float light = world.GetLightBrightness(_victim.GetBlockPosition());
ParticleEffect pe = new ParticleEffect(ParticleName, pos, light, Color.white, null, null, _OLDCreateColliders: false);
// Как GameManager.SpawnParticleEffectServer, но с Transform на руках - режиму змеи
// нужно вести частицы за собой (OnBurst).
effect = ParticleEffect.SpawnParticleEffect(pe, _victim.entityId, false, true);
SingletonMonoBehaviour.Instance.SendPackage(
NetPackageManager.GetPackage().Setup(pe, _victim.entityId, false, true),
_onlyClientsAttachedToAnEntity: false, -1, _victim.entityId);
}
OnBurst?.Invoke(_victim, effect);
Audio.Manager.BroadcastPlay(_soundSource, pos, SoundName);
world.RemoveEntity(_victim.entityId, EnumRemoveEntityReason.Killed);
}
}
/// Ленивая регистрация клона: любой спавн по нашему id сначала строит его.
[HarmonyPatch(typeof(ParticleEffect), nameof(ParticleEffect.GetDynamicTransform))]
public static class Patch_ParticleEffect_GetDynamicTransform_NecroBurst
{
public static void Prefix(int _particleId)
{
if (_particleId == NecroBurst.ParticleId)
{
NecroBurst.EnsureRegistered();
}
}
}
/// Помеченный зомби умер - разрываем. Постфикс: добыча и опыт уже выданы.
[HarmonyPatch(typeof(EntityAlive), nameof(EntityAlive.ClientKill))]
public static class Patch_EntityAlive_ClientKill_NecroBurst
{
public static void Postfix(EntityAlive __instance)
{
if (!NecroBurst.Pending.Remove(__instance.entityId) || !__instance.IsDead())
{
return;
}
NecroBurst.Burst(__instance, __instance.entityThatKilledMe != null ? (Entity)__instance.entityThatKilledMe : __instance);
}
}
///
/// necroburst - убить чёрным разрывом зомби, на которого смотришь (до 30 м). Проверка эффекта,
/// пока дистанционной атаки нет. Убийство засчитывается игроку как обычное.
///
[Preserve]
public class ConsoleCmdNecroBurst : ConsoleCmdAbstract
{
public override bool IsExecuteOnClient => true;
public override bool AllowedInMainMenu => false;
public override string[] getCommands()
{
return new string[] { "necroburst" };
}
public override string getDescription()
{
return "Necromancer's Tome: kill the zombie you look at with the black burst (effect test).";
}
public override void Execute(List _params, CommandSenderInfo _senderInfo)
{
World world = GameManager.Instance.World;
EntityPlayerLocal player = world?.GetPrimaryPlayer();
if (player == null)
{
return;
}
Ray look = player.GetLookRay();
EntityAlive best = null;
float bestAngle = 15f;
foreach (Entity e in world.Entities.list)
{
if (!(e is EntityZombie z) || z.IsDead())
{
continue;
}
Vector3 to = z.getChestPosition() - look.origin - Origin.position;
if (to.magnitude > 30f)
{
continue;
}
float angle = Vector3.Angle(look.direction, to);
if (angle < bestAngle)
{
bestAngle = angle;
best = z;
}
}
if (best == null)
{
SingletonMonoBehaviour.Instance.Output("necroburst: не вижу зомби перед собой (до 30 м)");
return;
}
NecroBurst.Pending.Add(best.entityId);
best.DamageEntity(new DamageSourceEntity(EnumDamageSource.External, EnumDamageTypes.Piercing, player.entityId),
99999, false);
SingletonMonoBehaviour.Instance.Output("necroburst: " + best.EntityName + " (" + best.entityId + ")");
}
}
}