Files
necromants-tome-7d2d-3-2/HarmonySrc/ParticlePatch.cs
T
Alex CubeandClaude Opus 5 e8f064f5ec Книга некроманта 1.0 — первая публичная версия
Мод для 7 Days to Die 3.2: навык «Некромантия», растущий от счётчика убитых
зомби, тёмное оружие с шестью собственными модами, призывная нежить, пирамида
духов и сюжетный финал через Чёрный портал. Локализация на 13 языках.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaNro5hAGTzcQ7rJNN2tCX
2026-09-09 21:13:03 +03:00

172 lines
8.5 KiB
C#

using HarmonyLib;
using UnityEngine;
namespace NecromancerTome
{
/// <summary>
/// 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.
/// </summary>
[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;
/// <summary>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.</summary>
public float DensityFactor;
}
/// <summary>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.</summary>
public static readonly GlowConfig CharmGlow = new GlowConfig { Tint = new Color(0.2f, 1f, 0.3f), SizeFactor = 0.2f, AlphaFactor = 0.5f, DensityFactor = 1f };
/// <summary>Purple, per user request 2026-08-28 ("подсвети бафнутого зомби... фиолетовым").</summary>
public static readonly GlowConfig VictimGlow = new GlowConfig { Tint = new Color(0.55f, 0.05f, 0.85f), SizeFactor = 0.2f, AlphaFactor = 0.5f, DensityFactor = 1f };
/// <summary>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).</summary>
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<ParticleSystem>(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;
}
}
}
}