using HarmonyLib; using UnityEngine; namespace NecromancerTome { /// /// Shrinks and recolors the "RadiatedParticlesOnMesh" glow that both buffNecroDeviatorCharm /// and buffNecroVictim attach to a zombie (see buffs.xml, action="AttachParticleEffectToEntity" /// - both buffs reuse the same particle prefab rather than needing two different ones). /// /// Why this needs Harmony: AttachParticleEffectToEntity's XML attributes are limited to /// particle/parent_transform/local_offset/local_rotation/oneshot/shape_mesh/sound - there is /// no scale or color/alpha attribute (confirmed by decompiling /// MinEventActionAttachParticleEffectToEntity.ParseXmlAttribute - that's the exhaustive list). /// The prefab always instantiates at its own authored size/color; nothing in XML can change /// that. So instead we let the vanilla action run as normal (Postfix, not Prefix - the /// particle GameObject has to already exist), then find the same child object it just created /// and adjust it directly - same lookup the engine itself uses internally: a child transform /// named "Ptl_" + the particle prefab's name, parented under the entity's mesh transform. /// /// SizeFactor dropped 0.5 -> 0.2 2026-08-28 (user: "выглядят как шар вне зомби" - even the /// original half-size shrink still read as a floating ball rather than a mesh-hugging glow). /// /// Color (2026-08-28): Deviator green and Victim purple are both explicit now (Deviator used /// to just be whatever RadiatedParticlesOnMesh's own baked-in color happens to be - reads /// "green/energy" on its own, never actually set). Explicit per user request: "если оба бафа, /// то пусть свечения смешиваются" - a zombie carrying both gets Color.Lerp(charm, victim, .5), /// not one color just overriding the other. /// /// Gated to only affect entities carrying at least one of OUR buffs (checked per-buff, not /// just "is this the right particle name") - not vanilla naturally-irradiated zombies that /// happen to reuse the same particle prefab elsewhere. /// /// GENERALIZED 2026-08-29 for buffNecroPortalChannel (BACKLOG.md item 6, user request: /// dense green-blue particles while a portal stone channels, thick enough to partially /// obscure the player) - was hard-gated to `EntityZombie` specifically (`_params.Self is /// EntityZombie`) since the two original buffs are both zombie-facing; this new one targets /// the PLAYER, so the check is now against the common `EntityAlive` base (where /// `.Buffs`/`.emodel` actually live) instead. Also needed its own size/alpha/DENSITY numbers /// separate from the zombie glow's - the first version reused the same SizeFactor/AlphaFactor /// constants for all three buffs, which the user confirmed reads as "редкие-редкие" (way too /// sparse) for a "should partly cover you" effect - see GlowConfig below, one per buff now /// instead of two shared constants. /// [HarmonyPatch(typeof(MinEventActionAttachParticleEffectToEntity), "Execute")] public static class Patch_AttachParticleEffectToEntity_ShrinkCharmGlow { public const string ParticleName = "RadiatedParticlesOnMesh"; public class GlowConfig { public Color Tint; public float SizeFactor; public float AlphaFactor; /// Multiplies both the emission rate (particles/second) AND maxParticles by /// this factor together - raising rate alone caps out silently once the system hits /// its authored maxParticles ceiling, so both have to move together to actually get a /// visibly denser cloud instead of the same particle count arriving faster. public float DensityFactor; } /// Unchanged from the original 2026-08-28 tuning - the zombie-facing glow was /// never asked to get denser/bigger, only the new portal-channel one was. public static readonly GlowConfig CharmGlow = new GlowConfig { Tint = new Color(0.2f, 1f, 0.3f), SizeFactor = 0.2f, AlphaFactor = 0.5f, DensityFactor = 1f }; /// Purple, per user request 2026-08-28 ("подсвети бафнутого зомби... фиолетовым"). public static readonly GlowConfig VictimGlow = new GlowConfig { Tint = new Color(0.55f, 0.05f, 0.85f), SizeFactor = 0.2f, AlphaFactor = 0.5f, DensityFactor = 1f }; /// RE-TUNED 2026-08-29 (user: "частицы есть, но они редкие-редкие. А надо чтобы /// прямо густо располагались... чтобы частично перекрывали внешний вид" + colour changed /// from the first version's near-black to green-blue/teal, "зелёноголубые"). SizeFactor /// bumped from a shrink (0.2, matching the mesh-hugging zombie glow) to just under full /// size (0.9) - a swirl meant to partly obscure the player needs to actually be /// body-sized, not a tight skin-hugging glow. AlphaFactor raised to near-opaque (0.9) for /// the same "obscures the view" reason - the zombie glow's own 0.5 was deliberately subtle, /// this one shouldn't be. DensityFactor=5 - the actual fix for "редкие-редкие", multiplies /// both emission rate and maxParticles together (see GlowConfig's own doc on why both). public static readonly GlowConfig PortalChannelGlow = new GlowConfig { Tint = new Color(0.1f, 0.85f, 0.8f), SizeFactor = 0.9f, AlphaFactor = 0.9f, DensityFactor = 5f }; public static void Postfix(MinEventActionAttachParticleEffectToEntity __instance, MinEventParams _params) { if (_params.Self == null || __instance.goToInstantiate == null) { return; } if (__instance.goToInstantiate.name != ParticleName) { return; } if (!(_params.Self is EntityAlive entity) || entity.Buffs == null) { return; } bool isPortalChannel = entity.Buffs.HasBuff(Patch_ItemActionEat_ExecuteAction_PortalStones.ChannelBuffName); bool isVictim = entity.Buffs.HasBuff(Patch_Entity_DropBagServer_VictimBag.VictimBuffName); bool isCharm = entity.Buffs.HasBuff(Patch_EntityBuffs_AddBuff_DeviatorCharm.CharmBuffName); if (!isPortalChannel && !isVictim && !isCharm) { return; } // Portal channel is player-only and never coexists with the zombie-facing buffs // below in practice, so it's kept as a simple separate branch rather than folded // into the same Lerp blend those two use with each other. GlowConfig config; if (isPortalChannel) { config = PortalChannelGlow; } else if (isVictim && isCharm) { config = new GlowConfig { Tint = Color.Lerp(CharmGlow.Tint, VictimGlow.Tint, 0.5f), SizeFactor = CharmGlow.SizeFactor, AlphaFactor = CharmGlow.AlphaFactor, DensityFactor = 1f }; } else { config = isVictim ? VictimGlow : CharmGlow; } Transform meshTransform = entity.emodel != null ? entity.emodel.meshTransform : null; if (meshTransform == null) { return; } Transform particleTransform = meshTransform.Find("Ptl_" + ParticleName); if (particleTransform == null) { return; } // Belt-and-suspenders for size: not every particle system's Scaling Mode respects // transform scale, but startSizeMultiplier always does regardless of that setting. particleTransform.localScale = Vector3.one * config.SizeFactor; ParticleSystem[] systems = particleTransform.GetComponentsInChildren(true); foreach (ParticleSystem ps in systems) { ParticleSystem.MainModule main = ps.main; main.startSizeMultiplier *= config.SizeFactor; main.maxParticles = Mathf.Max(1, Mathf.RoundToInt(main.maxParticles * config.DensityFactor)); if (config.DensityFactor != 1f) { ParticleSystem.EmissionModule emission = ps.emission; emission.rateOverTimeMultiplier *= config.DensityFactor; emission.rateOverDistanceMultiplier *= config.DensityFactor; } ParticleSystem.MinMaxGradient startColor = main.startColor; switch (startColor.mode) { case ParticleSystemGradientMode.Color: { Color c = config.Tint; c.a = startColor.color.a * config.AlphaFactor; startColor.color = c; break; } case ParticleSystemGradientMode.TwoColors: { Color min = config.Tint; Color max = config.Tint; min.a = startColor.colorMin.a * config.AlphaFactor; max.a = startColor.colorMax.a * config.AlphaFactor; startColor.colorMin = min; startColor.colorMax = max; break; } // Gradient/TwoGradients modes bake color+alpha into the gradient asset itself - // no generic way to override that from code, so those are left as-is. } main.startColor = startColor; } } } }