Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of EliteCreaturesReborn v3.0.0
plugins\EliteCreaturesReborn.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Charter; using ConfigReload; using EliteCreaturesReborn.Commands; using EliteCreaturesReborn.Config; using EliteCreaturesReborn.Display; using EliteCreaturesReborn.Mutations; using EliteCreaturesReborn.Rules; using EliteCreaturesReborn.Runtime; using EliteCreaturesReborn.Scaling; using EliteCreaturesReborn.Traits; using EliteCreaturesReborn.Util; using EliteCreaturesReborn.Visuals; using HarmonyLib; using Microsoft.CodeAnalysis; using PatchGuard; using UnityEngine; using UnityEngine.UI; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Core.Tokens; using YamlDotNet.Helpers; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; using YamlDotNet.Serialization.Callbacks; using YamlDotNet.Serialization.Converters; using YamlDotNet.Serialization.EventEmitters; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NodeDeserializers; using YamlDotNet.Serialization.NodeTypeResolvers; using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectGraphTraversalStrategies; using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.Schemas; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; using YamlDotNet.Serialization.Utilities; using YamlDotNet.Serialization.ValueDeserializers; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: IgnoresAccessChecksTo("assembly_utils")] [assembly: IgnoresAccessChecksTo("assembly_valheim")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("Gerald Glasgow")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) Gerald Glasgow. Licensed under the GNU General Public License v3.0.")] [assembly: AssemblyDescription("Elite Creatures Reborn: named, visible mutations that make every creature a different fight")] [assembly: AssemblyFileVersion("3.0.0.0")] [assembly: AssemblyInformationalVersion("3.0.0+0f503d56738d4608cf3a44a0c3267f2168470ee3")] [assembly: AssemblyProduct("Elite Creatures Reborn")] [assembly: AssemblyTitle("EliteCreaturesReborn")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("3.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace EliteCreaturesReborn { [BepInPlugin("gglasgow.elitecreaturesreborn", "Elite Creatures Reborn", "3.0.0")] public sealed class Plugin : BaseUnityPlugin { private void Awake() { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown //IL_0074: Expected O, but got Unknown Log.Bind(((BaseUnityPlugin)this).Logger); Configuration.BindAll(((BaseUnityPlugin)this).Config); RuleFile.Initialise(); ServerLock.Setup(((BaseUnityPlugin)this).Config, "gglasgow.elitecreaturesreborn", "Elite Creatures Reborn", "3.0.0"); Harmony val = new Harmony("gglasgow.elitecreaturesreborn"); val.PatchAll(typeof(Plugin).Assembly); ServerLock.Install(val); Guard.Install(val, ((BaseUnityPlugin)this).Logger, typeof(Plugin).Assembly); ConfigReloader.Setup(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger); RuleReload.Attach(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Elite Creatures Reborn 3.0.0 ready."); } } internal static class PluginInfo { public const string Guid = "gglasgow.elitecreaturesreborn"; public const string Name = "Elite Creatures Reborn"; public const string PluginVersion = "3.0.0"; public const string Author = "Gerald Glasgow"; public const string Licence = "GNU General Public License v3.0"; } } namespace EliteCreaturesReborn.Visuals { public static class CosmeticClone { private const float BaselineRadius = 4f; public static GameObject? Spawn(GameObject? prefab, Transform parent, Vector3 position, bool endless) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) float num = Density(); if ((Object)(object)prefab == (Object)null || num <= 0f) { return null; } GameObject obj = Instantiate(prefab, position); obj.transform.SetParent(parent, true); Strip(obj, endless); Thin(obj, num); return obj; } public static void Flash(GameObject? prefab, Vector3 position, float radius) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) float num = Density(); if (!((Object)(object)prefab == (Object)null) && !(num <= 0f)) { GameObject obj = Instantiate(prefab, position); Strip(obj, endless: false); Thin(obj, num); Transform transform = obj.transform; transform.localScale *= Mathf.Max(radius, 0.01f) / 4f; Object.Destroy((Object)(object)obj, Mathf.Max(radius, 3f)); } } private static GameObject Instantiate(GameObject prefab, Vector3 position) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) ZNetView.StartGhostInit(); GameObject result = Object.Instantiate<GameObject>(prefab, position, Quaternion.identity); ZNetView.FinishGhostInit(); return result; } private static void Strip(GameObject clone, bool endless) { Component[] componentsInChildren = (Component[])(object)clone.GetComponentsInChildren<Aoe>(true); Kill(componentsInChildren); componentsInChildren = (Component[])(object)clone.GetComponentsInChildren<Projectile>(true); Kill(componentsInChildren); componentsInChildren = (Component[])(object)clone.GetComponentsInChildren<ZSyncTransform>(true); Kill(componentsInChildren); componentsInChildren = (Component[])(object)clone.GetComponentsInChildren<ZNetView>(true); Kill(componentsInChildren); Collider[] componentsInChildren2 = clone.GetComponentsInChildren<Collider>(true); for (int i = 0; i < componentsInChildren2.Length; i++) { componentsInChildren2[i].enabled = false; } if (endless) { componentsInChildren = (Component[])(object)clone.GetComponentsInChildren<TimedDestruction>(true); Kill(componentsInChildren); } } private static void Kill(Component[] components) { for (int i = 0; i < components.Length; i++) { Object.Destroy((Object)(object)components[i]); } } private static void Thin(GameObject clone, float density) { Guard.Run("CosmeticClone.Thin", delegate { ThinParts(clone, density); }); } private static void ThinParts(GameObject clone, float density) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) ParticleSystem[] componentsInChildren = clone.GetComponentsInChildren<ParticleSystem>(true); for (int i = 0; i < componentsInChildren.Length; i++) { EmissionModule emission = componentsInChildren[i].emission; ((EmissionModule)(ref emission)).rateOverTimeMultiplier = ((EmissionModule)(ref emission)).rateOverTimeMultiplier * density; ((EmissionModule)(ref emission)).rateOverDistanceMultiplier = ((EmissionModule)(ref emission)).rateOverDistanceMultiplier * density; } Light[] componentsInChildren2 = clone.GetComponentsInChildren<Light>(true); foreach (Light obj in componentsInChildren2) { obj.intensity *= density; } } private static float Density() { return Mathf.Clamp01(Configuration.EffectDensity.Value); } } public static class EffectResolver { public static readonly string[] Cloud = new string[7] { "poison", "blob", "ooze", "puke", "gas", "spore", "smoke" }; public static readonly string[] Blast = new string[7] { "explos", "blast", "fire", "bomb", "death", "burst", "flame" }; public static readonly string[] Warning = new string[6] { "smoke", "fire", "burn", "charge", "glow", "spark" }; public static readonly string[] Reflect = new string[5] { "shield", "spark", "hit", "block", "staff" }; public static readonly string[] Devour = new string[6] { "death", "gore", "corpse", "destr", "blood", "hit" }; private static readonly Dictionary<string, GameObject?> Cache = new Dictionary<string, GameObject>(); public static GameObject? Resolve(string name, string[] keywords, string setting = "") { ZNetScene instance = ZNetScene.instance; if ((Object)(object)instance == (Object)null || string.IsNullOrEmpty(name)) { return null; } if (Cache.TryGetValue(name, out GameObject value)) { return value; } GameObject val = Find(instance, name, keywords, setting); Cache[name] = val; return val; } public static GameObject? ForRole(string role) { if (role == "reflect") { return ByKeyword(role, Reflect); } if (!(role == "devour")) { return null; } return ByKeyword(role, Devour); } public static GameObject? ByKeyword(string role, string[] keywords) { ZNetScene instance = ZNetScene.instance; if ((Object)(object)instance == (Object)null) { return null; } if (Cache.TryGetValue(role, out GameObject value)) { return value; } GameObject val = Nearest(instance, keywords); if ((Object)(object)val == (Object)null) { Log.Warn("no vanilla effect matched the " + role + " tell; it will not be shown"); } Cache[role] = val; return val; } private static GameObject? Find(ZNetScene scene, string name, string[] keywords, string setting) { GameObject val = Usable(scene.GetPrefab(name)); if ((Object)(object)val != (Object)null) { return val; } string text = (string.IsNullOrEmpty(setting) ? "" : (" (from setting '" + setting + "')")); GameObject val2 = Nearest(scene, keywords); Log.Warn(((Object)(object)val2 != (Object)null) ? ("effect prefab '" + name + "'" + text + " not found; falling back to '" + ((Object)val2).name + "'") : ("effect prefab '" + name + "'" + text + " not found and no fallback effect exists - this hazard stays invisible")); return val2; } private static GameObject? Nearest(ZNetScene scene, string[] keywords) { foreach (GameObject nonNetViewPrefab in scene.m_nonNetViewPrefabs) { if ((Object)(object)Usable(nonNetViewPrefab) != (Object)null && MatchesAny(((Object)nonNetViewPrefab).name, keywords)) { return nonNetViewPrefab; } } return null; } private static bool MatchesAny(string prefabName, string[] keywords) { string text = prefabName.ToLowerInvariant(); foreach (string value in keywords) { if (text.Contains(value)) { return true; } } return false; } private static GameObject? Usable(GameObject? prefab) { if ((Object)(object)prefab == (Object)null) { return null; } if (!((Object)(object)prefab.GetComponentInChildren<ParticleSystem>(true) != (Object)null) && !((Object)(object)prefab.GetComponentInChildren<Renderer>(true) != (Object)null)) { return null; } return prefab; } } public sealed class LingeringVisual : MonoBehaviour { private const float BaselineRadius = 4f; private const float FadeSeconds = 1f; private ParticleSystem[] _systems = Array.Empty<ParticleSystem>(); private Light[] _lights = Array.Empty<Light>(); private AudioSource[] _audio = Array.Empty<AudioSource>(); private float[] _lightBase = Array.Empty<float>(); private float[] _audioBase = Array.Empty<float>(); private float _life; private float _age; private bool _fading; public static void Attach(GameObject host, GameObject? prefab, float life, float radius) { LingeringVisual visual = host.AddComponent<LingeringVisual>(); visual._life = Mathf.Max(life, 0.01f); Guard.Run("LingeringVisual.Build", delegate { visual.Build(host, prefab, radius); }); } private void Build(GameObject host, GameObject? prefab, float radius) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) GameObject val = CosmeticClone.Spawn(prefab, host.transform, host.transform.position, endless: true); if ((Object)(object)val == (Object)null) { ((Behaviour)this).enabled = false; return; } Transform transform = val.transform; transform.localScale *= Mathf.Max(radius, 0.01f) / 4f; Loop(val); Capture(val); } private void Loop(GameObject clone) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) _systems = clone.GetComponentsInChildren<ParticleSystem>(true); ParticleSystem[] systems = _systems; foreach (ParticleSystem obj in systems) { MainModule main = obj.main; ((MainModule)(ref main)).loop = true; obj.Play(true); } } private void Capture(GameObject clone) { _lights = clone.GetComponentsInChildren<Light>(true); _audio = clone.GetComponentsInChildren<AudioSource>(true); _lightBase = Baseline(_lights, (Light l) => l.intensity); _audioBase = Baseline(_audio, (AudioSource a) => a.volume); } private static float[] Baseline<T>(T[] items, Func<T, float> read) { float[] array = new float[items.Length]; for (int i = 0; i < items.Length; i++) { array[i] = read(items[i]); } return array; } private void Update() { Guard.Run("LingeringVisual.Update", Step); } private void Step() { _age += Time.deltaTime; if (!_fading && _age >= _life - 1f) { BeginFade(); } if (_fading) { Dim(Mathf.Clamp01((_life - _age) / 1f)); } } private void BeginFade() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) _fading = true; ParticleSystem[] systems = _systems; for (int i = 0; i < systems.Length; i++) { EmissionModule emission = systems[i].emission; ((EmissionModule)(ref emission)).enabled = false; } } private void Dim(float factor) { for (int i = 0; i < _lights.Length; i++) { if ((Object)(object)_lights[i] != (Object)null) { _lights[i].intensity = _lightBase[i] * factor; } } for (int j = 0; j < _audio.Length; j++) { if ((Object)(object)_audio[j] != (Object)null) { _audio[j].volume = _audioBase[j] * factor; } } } } } namespace EliteCreaturesReborn.Util { internal static class ColorParse { public static Color Hex(string text, Color fallback) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) Color result = default(Color); if (!string.IsNullOrWhiteSpace(text) && ColorUtility.TryParseHtmlString(text.Trim(), ref result)) { return result; } return fallback; } } internal static class Dice { public static bool Percent(float chancePercent) { if (chancePercent <= 0f) { return false; } return Random.Range(0f, 100f) <= chancePercent; } public static float Draw() { return Random.Range(0f, 100f); } } internal static class Log { private static ManualLogSource? _log; public static bool Diagnostics; public static void Bind(ManualLogSource log) { _log = log; } public static void Info(string message) { ManualLogSource? log = _log; if (log != null) { log.LogInfo((object)message); } } public static void Warn(string message) { ManualLogSource? log = _log; if (log != null) { log.LogWarning((object)message); } } public static void Error(string message) { ManualLogSource? log = _log; if (log != null) { log.LogError((object)message); } } public static void Diag(string message) { if (Diagnostics) { ManualLogSource? log = _log; if (log != null) { log.LogInfo((object)("[diag] " + message)); } } } } public static class NetTime { public static long NowMs() { if (!((Object)(object)ZNet.instance != (Object)null)) { return 0L; } return (long)(ZNet.instance.GetTimeSeconds() * 1000.0); } public static float SecondsSince(long stampMs) { return (float)(NowMs() - stampMs) / 1000f; } } } namespace EliteCreaturesReborn.Traits { public sealed class CreatureTraits { public int Stars; public int Mask; public bool Any => Mask != 0; public int LargeGlyphs => Stars / 5; public int Count { get { int num = 0; foreach (Mutation item in Active()) { _ = item; num++; } return num; } } public CreatureTraits(int stars, int mask) { Stars = stars; Mask = mask; } public bool Has(Mutation mutation) { return (Mask & (1 << (int)mutation)) != 0; } public void Add(Mutation mutation) { Mask |= 1 << (int)mutation; } public void Remove(Mutation mutation) { Mask &= ~(1 << (int)mutation); } public bool OnLargeStar(Mutation mutation) { if (!Has(mutation)) { return false; } int num = 0; foreach (Mutation item in Active()) { if (item == mutation) { return num < LargeGlyphs; } num++; } return false; } public IEnumerable<Mutation> Active() { Mutation[] inOrder = MutationCatalog.InOrder; foreach (Mutation mutation in inOrder) { if (Has(mutation)) { yield return mutation; } } } } public enum Mutation { Mad, Bloated, Cloaked, Splintering, Leeching, Warding, Plated, Miasmic, Devouring } public static class MutationCatalog { public static readonly Mutation[] InOrder = new Mutation[9] { Mutation.Mad, Mutation.Bloated, Mutation.Cloaked, Mutation.Splintering, Mutation.Leeching, Mutation.Warding, Mutation.Plated, Mutation.Miasmic, Mutation.Devouring }; public static Mutation? FromName(string name) { Mutation[] inOrder = InOrder; foreach (Mutation mutation in inOrder) { if (string.Equals(Word(mutation), name, StringComparison.OrdinalIgnoreCase)) { return mutation; } } return null; } public static string Word(Mutation mutation) { return mutation switch { Mutation.Mad => "Mad", Mutation.Bloated => "Bloated", Mutation.Cloaked => "Cloaked", Mutation.Splintering => "Splintering", Mutation.Leeching => "Leeching", Mutation.Warding => "Warding", Mutation.Plated => "Plated", Mutation.Miasmic => "Miasmic", Mutation.Devouring => "Devouring", _ => "", }; } public static string DefaultColorHex(Mutation mutation) { return mutation switch { Mutation.Mad => "#E23030", Mutation.Bloated => "#8B5A2B", Mutation.Cloaked => "#4AA6FF", Mutation.Splintering => "#FFFFFF", Mutation.Leeching => "#33CC33", Mutation.Warding => "#101C50", Mutation.Plated => "#F2C40C", Mutation.Miasmic => "#1E6B2E", Mutation.Devouring => "#7A0F0F", _ => "#FFFFFF", }; } } public static class SplinterTable { private static readonly float[][] Weights = new float[5][] { new float[5] { 0f, 0f, 0f, 0f, 100f }, new float[5] { 15f, 0f, 0f, 0f, 85f }, new float[5] { 15f, 35f, 0f, 0f, 50f }, new float[5] { 15f, 35f, 30f, 0f, 20f }, new float[5] { 15f, 35f, 30f, 15f, 5f } }; public static int RollChildStars(int parentStars) { if (parentStars <= 0) { return 0; } float[] array = Weights[Mathf.Clamp(parentStars, 1, 5) - 1]; float num = Dice.Draw(); float num2 = 0f; for (int i = 0; i < 4; i++) { num2 += array[i]; if (num <= num2) { return Mathf.Max(0, parentStars - (i + 1)); } } return 0; } } public static class TraitKeys { public const string Resolved = "ecr_resolved"; public const string Stars = "ecr_stars"; public const string Mask = "ecr_mask"; public const string Biome = "ecr_biome"; public const string DevouredHealth = "ecr_dev_hp"; public const string DevouredDamage = "ecr_dev_dmg"; public const string Generation = "ecr_gen"; public const string CascadeRoot = "ecr_root"; public const string MiasmaTrail = "ecr_miasma"; public const string MiasmaSeq = "ecr_miasma_seq"; public const string DevourReadyAt = "ecr_dev_ready"; public const string DevouredBy = "ecr_devoured_by"; } public static class TraitRoller { public static CreatureTraits Roll(BiomeRules rules, int maxMutations) { int stars = RollStars(rules); int mask = RollMutations(rules, stars, maxMutations); return new CreatureTraits(stars, mask); } public static int RollStars(BiomeRules rules) { float[] starChances = rules.StarChances; float num = 0f; float[] array = starChances; foreach (float num2 in array) { num += Mathf.Max(0f, num2); } if (num <= 0f) { return 0; } return PickIndex(starChances, Random.Range(0f, num)); } private static int PickIndex(float[] chances, float draw) { float num = 0f; for (int i = 0; i < chances.Length; i++) { num += Mathf.Max(0f, chances[i]); if (draw < num) { return i; } } return chances.Length - 1; } private static int RollMutations(BiomeRules rules, int stars, int maxMutations) { List<Mutation> list = new List<Mutation>(); Mutation[] inOrder = MutationCatalog.InOrder; foreach (Mutation mutation in inOrder) { if (Dice.Percent(rules.ChanceOf(mutation, stars))) { list.Add(mutation); } } Cap(list, maxMutations); return Pack(list); } private static void Cap(List<Mutation> hits, int maxMutations) { if (maxMutations > 0 && hits.Count > maxMutations) { for (int num = hits.Count - 1; num > 0; num--) { int num2 = Random.Range(0, num + 1); int index = num; int index2 = num2; Mutation value = hits[num2]; Mutation value2 = hits[num]; hits[index] = value; hits[index2] = value2; } hits.RemoveRange(maxMutations, hits.Count - maxMutations); } } private static int Pack(List<Mutation> hits) { int num = 0; foreach (Mutation hit in hits) { num |= 1 << (int)hit; } return num; } } public static class TraitStore { public static bool IsResolved(ZDO zdo) { if (zdo != null) { return zdo.GetBool("ecr_resolved", false); } return false; } public static CreatureTraits Load(ZDO zdo) { int stars = zdo.GetInt("ecr_stars", 0); int mask = zdo.GetInt("ecr_mask", 0); return new CreatureTraits(stars, mask); } public static void Save(ZDO zdo, CreatureTraits traits) { zdo.Set("ecr_stars", traits.Stars); zdo.Set("ecr_mask", traits.Mask); zdo.Set("ecr_resolved", true); } public static void SetBiome(ZDO zdo, Biome biome) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected I4, but got Unknown zdo.Set("ecr_biome", (int)biome); } public static Biome GetBiome(ZDO zdo) { return (Biome)zdo.GetInt("ecr_biome", 0); } public static float GetDevouredHealth(ZDO zdo) { return zdo.GetFloat("ecr_dev_hp", 0f); } public static float GetDevouredDamage(ZDO zdo) { return zdo.GetFloat("ecr_dev_dmg", 0f); } public static void AddDevoured(ZDO zdo, float health, float damage) { zdo.Set("ecr_dev_hp", GetDevouredHealth(zdo) + health); zdo.Set("ecr_dev_dmg", GetDevouredDamage(zdo) + damage); } public static ZDOID GetDevouredBy(ZDO zdo) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return zdo.GetZDOID("ecr_devoured_by"); } public static void MarkDevouredBy(ZDO zdo, ZDOID devourer) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) zdo.Set("ecr_devoured_by", devourer); } public static long GetDevourReadyAt(ZDO zdo) { return zdo.GetLong("ecr_dev_ready", 0L); } public static void SetDevourReadyAt(ZDO zdo, long whenMs) { zdo.Set("ecr_dev_ready", whenMs); } } } namespace EliteCreaturesReborn.Scaling { public static class DamageMath { public static float OutgoingMultiplier(BiomeRules rules, CreatureTraits traits, float healthFraction) { float num = rules.Star.AttackAt(traits.Stars) - 1f; if (traits.Has(Mutation.Splintering)) { num += rules.PowerOf(Mutation.Splintering, "damage") - 1f; } if (traits.Has(Mutation.Plated)) { num += Enhance.Magnitude(rules, traits, Mutation.Plated, "damage") / 100f * (1f - healthFraction); } return Mathf.Max(0f, 1f + num); } public static float DevouredFlatDamage(CreatureTraits traits, ZDO zdo) { if (!traits.Has(Mutation.Devouring) || zdo == null) { return 0f; } return TraitStore.GetDevouredDamage(zdo); } public static float PlatedArmour(BiomeRules rules, CreatureTraits traits, float healthFraction) { if (!traits.Has(Mutation.Plated)) { return 0f; } return Enhance.Magnitude(rules, traits, Mutation.Plated, "armour") * Mathf.Clamp01(healthFraction); } } public static class Enhance { public static float Stat(BiomeRules rules, CreatureTraits traits, Mutation mutation, string field) { float num = rules.PowerOf(mutation, field); if (!traits.OnLargeStar(mutation)) { return num; } return 1f + (num - 1f) * rules.LargeStarPower; } public static float Magnitude(BiomeRules rules, CreatureTraits traits, Mutation mutation, string field) { float num = rules.PowerOf(mutation, field); if (!traits.OnLargeStar(mutation)) { return num; } return num * rules.LargeStarPower; } } public static class StatApplier { public static void ApplySize(Character character, BiomeRules rules, CreatureTraits traits) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) float num = StatMath.SizeMultiplier(rules, traits); if (!Mathf.Approximately(num, 1f)) { Transform transform = ((Component)character).transform; transform.localScale *= num; } } public static void ApplyHealth(Character character, BiomeRules rules, CreatureTraits traits, bool freshlyResolved) { ZDO zDO = ((Component)character).GetComponent<ZNetView>().GetZDO(); float num = ((zDO != null) ? TraitStore.GetDevouredHealth(zDO) : 0f); float num2 = character.GetMaxHealthBase() * StatMath.HealthMultiplier(rules, traits) + num; character.SetMaxHealth(num2); if (freshlyResolved) { character.SetHealth(num2); } } } public static class StatMath { public const float HealthFloor = 0.05f; public const float MoveFloor = 0.05f; public static float HealthMultiplier(BiomeRules rules, CreatureTraits traits) { float num = rules.Star.HpAt(traits.Stars) - 1f; if (traits.Has(Mutation.Mad)) { num += rules.PowerOf(Mutation.Mad, "health") - 1f; } if (traits.Has(Mutation.Bloated)) { num += Enhance.Stat(rules, traits, Mutation.Bloated, "health") - 1f; } return Mathf.Max(0.05f, 1f + num); } public static float SizeMultiplier(BiomeRules rules, CreatureTraits traits) { return 1f + rules.Star.GrowthAt(traits.Stars); } public static float MoveMultiplier(BiomeRules rules, CreatureTraits traits, float devourBonus) { float num = rules.Star.SpeedAt(traits.Stars) - 1f + devourBonus; if (traits.Has(Mutation.Mad)) { num += Enhance.Stat(rules, traits, Mutation.Mad, "move") - 1f; } return Mathf.Max(0.05f, 1f + num); } public static float SwingSpeedMultiplier(BiomeRules rules, CreatureTraits traits) { float num = rules.Star.SwingSpeedAt(traits.Stars) - 1f; if (traits.Has(Mutation.Mad)) { num += Enhance.Stat(rules, traits, Mutation.Mad, "attack speed") - 1f; } return Mathf.Max(0.05f, 1f + num); } } } namespace EliteCreaturesReborn.Runtime { public struct BaseSpeeds { private float _speed; private float _acceleration; private float _crouch; private float _walk; private float _run; private float _turn; private float _runTurn; private float _flySlow; private float _flyFast; private float _flyTurn; public float RunSpeed => _run; public static BaseSpeeds Capture(Character c) { return new BaseSpeeds { _speed = c.m_speed, _acceleration = c.m_acceleration, _crouch = c.m_crouchSpeed, _walk = c.m_walkSpeed, _run = c.m_runSpeed, _turn = c.m_turnSpeed, _runTurn = c.m_runTurnSpeed, _flySlow = c.m_flySlowSpeed, _flyFast = c.m_flyFastSpeed, _flyTurn = c.m_flyTurnSpeed }; } public void ApplyTo(Character c, float factor) { c.m_speed = _speed * factor; c.m_acceleration = _acceleration * factor; c.m_crouchSpeed = _crouch * factor; c.m_walkSpeed = _walk * factor; c.m_runSpeed = _run * factor; c.m_turnSpeed = _turn * factor; c.m_runTurnSpeed = _runTurn * factor; c.m_flySlowSpeed = _flySlow * factor; c.m_flyFastSpeed = _flyFast * factor; c.m_flyTurnSpeed = _flyTurn * factor; } } public static class BehaviourInstaller { public static void Install(EliteController controller) { CreatureTraits traits = controller.Traits; if (traits.Has(Mutation.Cloaked)) { ((Component)controller).gameObject.AddComponent<CloakBehaviour>(); } if (traits.Has(Mutation.Leeching)) { ((Component)controller).gameObject.AddComponent<LeechBehaviour>(); } if (traits.Has(Mutation.Miasmic)) { ((Component)controller).gameObject.AddComponent<MiasmaField>(); ((Component)controller).gameObject.AddComponent<MiasmaBody>(); } if (traits.Has(Mutation.Devouring)) { ((Component)controller).gameObject.AddComponent<DevourBehaviour>(); } } } public static class CreatureRpc { public const string Devour = "ecr_devour"; public const string Flash = "ecr_flash"; public static void Register(ZNetView nview, Character character, EliteController controller) { nview.Register<float, float>("ecr_devour", (Action<long, float, float>)delegate(long sender, float health, float damage) { ApplyAbsorb(character, controller, health, damage); }); nview.Register<Vector3, float, string>("ecr_flash", (Action<long, Vector3, float, string>)delegate(long sender, Vector3 pos, float radius, string role) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) DrawFlash(pos, radius, role); }); } public static void FireFlash(ZNetView creatureView, Vector3 pos, float radius, string role) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)creatureView != (Object)null && creatureView.IsValid()) { creatureView.InvokeRPC(0L, "ecr_flash", new object[3] { pos, radius, role ?? "" }); } } private static void DrawFlash(Vector3 pos, float radius, string role) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) Guard.Run("CreatureRpc.Flash", delegate { //IL_000c: Unknown result type (might be due to invalid IL or missing references) CosmeticClone.Flash(EffectResolver.ForRole(role), pos, radius); }); } public static void Absorb(Character killer, float health, float damage) { ZNetView component = ((Component)killer).GetComponent<ZNetView>(); if (!((Object)(object)component == (Object)null) && component.IsValid()) { if (component.IsOwner()) { ApplyAbsorb(killer, ((Component)killer).GetComponent<EliteController>(), health, damage); return; } component.InvokeRPC("ecr_devour", new object[2] { health, damage }); } } private static void ApplyAbsorb(Character killer, EliteController controller, float health, float damage) { Guard.Run("CreatureRpc.Absorb", delegate { Bank(killer, controller, health, damage); }); } private static void Bank(Character killer, EliteController controller, float health, float damage) { if (!((Object)(object)killer == (Object)null) && !((Object)(object)controller == (Object)null) && controller.IsOwner()) { ZDO zDO = controller.View.GetZDO(); TraitStore.AddDevoured(zDO, health, damage); StartCooldown(controller, zDO); controller.RefreshHealth(); killer.Heal(health, false); } } private static void StartCooldown(EliteController controller, ZDO zdo) { float num = controller.Rules.PowerOf(Mutation.Devouring, "devour cooldown"); double num2 = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.instance.GetTimeSeconds() : 0.0); TraitStore.SetDevourReadyAt(zdo, (long)((num2 + (double)num) * 1000.0)); } } public sealed class EliteController : MonoBehaviour { private Character _character; private ZNetView _nview; private BaseSpeeds _baseSpeeds; private bool _ready; private bool _pending; private bool _movementClamped; private CreatureTraits? _forced; private Biome _forcedBiome; public CreatureTraits Traits { get; private set; } = new CreatureTraits(0, 0); public BiomeRules Rules { get; private set; } = RuleState.Active.Defaults; public bool FreshlyResolved { get; private set; } public float SwingSpeedFactor { get; private set; } = 1f; public Character Creature => _character; public ZNetView View => _nview; public bool Ready => _ready; private void Start() { Guard.Run("EliteController.Start", Setup); } private void Setup() { _character = ((Component)this).GetComponent<Character>(); _nview = (((Object)(object)_character != (Object)null) ? ((Component)_character).GetComponent<ZNetView>() : null); if ((Object)(object)_character == (Object)null || (Object)(object)_nview == (Object)null || !_nview.IsValid() || _character.IsBoss() || _character.IsPlayer()) { ((Behaviour)this).enabled = false; return; } EliteRpc.EnsureRegistered(); CreatureRpc.Register(_nview, _character, this); if (!TryResolve()) { _pending = true; } else { Apply(); } } private void Update() { if (_pending) { Guard.Run("EliteController.Resolve", PollResolve); } } private void PollResolve() { if (!((Object)(object)_nview == (Object)null) && _nview.IsValid() && TryResolve()) { _pending = false; Apply(); } } private bool TryResolve() { //IL_004a: Unknown result type (might be due to invalid IL or missing references) ZDO zDO = _nview.GetZDO(); if (zDO == null) { return false; } if (_nview.IsOwner() && !TraitStore.IsResolved(zDO)) { RollFresh(zDO); } if (!TraitStore.IsResolved(zDO)) { return false; } Traits = TraitStore.Load(zDO); Rules = RuleState.Active.For(TraitStore.GetBiome(zDO)); return true; } private void RollFresh(ZDO zdo) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) Biome biome = ((_forced != null) ? _forcedBiome : Heightmap.FindBiome(((Component)_character).transform.position)); CreatureTraits traits = _forced ?? TraitRoller.Roll(RuleState.Active.For(biome), RuleState.Active.MaxMutations); TraitStore.Save(zdo, traits); TraitStore.SetBiome(zdo, biome); _character.SetLevel(1); FreshlyResolved = true; _forced = null; } public void ForceTraits(CreatureTraits traits, Biome biome) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) _forced = traits; _forcedBiome = biome; } private void Apply() { _baseSpeeds = BaseSpeeds.Capture(_character); SwingSpeedFactor = StatMath.SwingSpeedMultiplier(Rules, Traits); StatApplier.ApplySize(_character, Rules, Traits); RefreshMovement(0f); if (_nview.IsOwner()) { StatApplier.ApplyHealth(_character, Rules, Traits, FreshlyResolved); } BehaviourInstaller.Install(this); _ready = true; ((Behaviour)this).enabled = false; } public void RefreshMovement(float devourBonus) { if ((Object)(object)_character != (Object)null) { float factor = StatMath.MoveMultiplier(Rules, Traits, devourBonus); _baseSpeeds.ApplyTo(_character, ClampToPlayer(factor)); } } private float ClampToPlayer(float factor) { float runSpeed = _baseSpeeds.RunSpeed; float num = PlayerSpeed.Reference(); if (runSpeed <= 0f || runSpeed * factor <= num) { return factor; } if (!_movementClamped) { _movementClamped = true; Log.Info($"{((Object)this).name}: run speed clamped to the player's ({num:0.#}) so it stays outrunnable"); } return num / runSpeed; } public void RefreshHealth() { if ((Object)(object)_character != (Object)null && IsOwner()) { StatApplier.ApplyHealth(_character, Rules, Traits, freshlyResolved: false); } } public bool IsOwner() { if ((Object)(object)_nview != (Object)null && _nview.IsValid()) { return _nview.IsOwner(); } return false; } } public static class EliteRpc { public const string Bloat = "ecr_bloat"; public const string Blast = "ecr_blast"; private static ZRoutedRpc? _registeredOn; public static void EnsureRegistered() { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null && instance != _registeredOn) { _registeredOn = instance; instance.Register<ZPackage>("ecr_bloat", (Action<long, ZPackage>)OnBloat); instance.Register<ZPackage>("ecr_blast", (Action<long, ZPackage>)OnBlast); } } public static void FireBloat(Vector3 pos, float baseDamage, float radius, float delay, int stars, string warningEffect, string blastEffect) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Expected O, but got Unknown //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) EnsureRegistered(); if (ZRoutedRpc.instance != null) { ZPackage val = new ZPackage(); val.Write(pos.x); val.Write(pos.y); val.Write(pos.z); val.Write(baseDamage); val.Write(radius); val.Write(delay); val.Write(stars); val.Write(warningEffect ?? ""); val.Write(blastEffect ?? ""); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ecr_bloat", new object[1] { val }); } } public static void FireBlast(Vector3 pos, float baseDamage, float radius, int stars, string blastEffect) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Expected O, but got Unknown //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) EnsureRegistered(); if (ZRoutedRpc.instance != null) { ZPackage val = new ZPackage(); val.Write(pos.x); val.Write(pos.y); val.Write(pos.z); val.Write(baseDamage); val.Write(radius); val.Write(stars); val.Write(blastEffect ?? ""); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "ecr_blast", new object[1] { val }); } } private static void OnBloat(long sender, ZPackage pkg) { Guard.Run("EliteRpc.OnBloat", delegate { ReadBloat(sender, pkg); }); } private static void ReadBloat(long sender, ZPackage pkg) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) Vector3 deathPos = new Vector3(pkg.ReadSingle(), pkg.ReadSingle(), pkg.ReadSingle()); float damage = pkg.ReadSingle(); float radius = pkg.ReadSingle(); float delay = pkg.ReadSingle(); int stars = pkg.ReadInt(); string warningEffect = pkg.ReadString(); string blastEffect = pkg.ReadString(); BloatedCorpse.Spawn(deathPos, damage, radius, delay, stars, warningEffect, blastEffect, sender == ZNet.GetUID()); } private static void OnBlast(long sender, ZPackage pkg) { Guard.Run("EliteRpc.OnBlast", delegate { ReadBlast(sender, pkg); }); } private static void ReadBlast(long sender, ZPackage pkg) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) Vector3 pos = new Vector3(pkg.ReadSingle(), pkg.ReadSingle(), pkg.ReadSingle()); float baseDamage = pkg.ReadSingle(); float radius = pkg.ReadSingle(); int stars = pkg.ReadInt(); string blastEffect = pkg.ReadString(); BloatedBlast.Detonate(pos, baseDamage, radius, stars, blastEffect, sender == ZNet.GetUID()); } } internal static class PlayerReference { public const float FallbackHealth = 100f; private static float _health = 100f; public static float MaxHealth() { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && ((Character)localPlayer).GetMaxHealth() > 0f) { _health = ((Character)localPlayer).GetMaxHealth(); } return _health; } } internal static class PlayerSpeed { public const float Fallback = 8f; private static float _reference = 8f; public static float Reference() { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && ((Character)localPlayer).m_runSpeed > 0f) { _reference = ((Character)localPlayer).m_runSpeed; } return _reference; } } } namespace EliteCreaturesReborn.Rules { public sealed class BiomeRules { public float[] StarChances = new float[1] { 100f }; public StarPower Star = new StarPower(); public float LargeStarPower = 2f; public float[] MutationChance = new float[1]; public readonly Dictionary<Mutation, float[]> MutationChances = new Dictionary<Mutation, float[]>(); public readonly Dictionary<Mutation, Dictionary<string, float>> MutationPower = new Dictionary<Mutation, Dictionary<string, float>>(); public readonly Dictionary<Mutation, Dictionary<string, string>> MutationText = new Dictionary<Mutation, Dictionary<string, string>>(); public int StarCeiling => Mathf.Max(0, StarChances.Length - 1); public float ChanceOf(Mutation mutation, int stars) { float[] value; return Sample(MutationChances.TryGetValue(mutation, out value) ? value : MutationChance, stars); } public float PowerOf(Mutation mutation, string field) { if (MutationPower.TryGetValue(mutation, out Dictionary<string, float> value) && value.TryGetValue(field, out var value2)) { return value2; } return RuleDefaults.Power(mutation, field); } public string PrefabOf(Mutation mutation, string field) { if (MutationText.TryGetValue(mutation, out Dictionary<string, string> value) && value.TryGetValue(field, out var value2) && !string.IsNullOrEmpty(value2)) { return value2; } return RuleDefaults.Prefab(mutation, field); } private static float Sample(float[] curve, int stars) { if (curve == null || curve.Length == 0) { return 0f; } return curve[Mathf.Clamp(stars, 0, curve.Length - 1)]; } public BiomeRules Clone() { BiomeRules biomeRules = new BiomeRules { StarChances = (float[])StarChances.Clone(), Star = Star.Clone(), LargeStarPower = LargeStarPower, MutationChance = (float[])MutationChance.Clone() }; foreach (KeyValuePair<Mutation, float[]> mutationChance in MutationChances) { biomeRules.MutationChances[mutationChance.Key] = (float[])mutationChance.Value.Clone(); } foreach (KeyValuePair<Mutation, Dictionary<string, float>> item in MutationPower) { biomeRules.MutationPower[item.Key] = new Dictionary<string, float>(item.Value); } foreach (KeyValuePair<Mutation, Dictionary<string, string>> item2 in MutationText) { biomeRules.MutationText[item2.Key] = new Dictionary<string, string>(item2.Value); } return biomeRules; } } internal static class Fields { public const string Growth = "growth"; public const string Hp = "hp"; public const string Attack = "attack"; public const string SwingSpeed = "swing speed"; public const string Speed = "speed"; public const string Drops = "drops"; public const string Move = "move"; public const string AttackSpeed = "attack speed"; public const string Health = "health"; public const string Delay = "delay"; public const string Damage = "damage"; public const string Radius = "radius"; public const string BlastEffect = "blast effect"; public const string WarningEffect = "warning effect"; public const string RevealDistance = "reveal distance"; public const string FadeTime = "fade time"; public const string FadeMargin = "fade margin"; public const string MaxGenerations = "max generations"; public const string MaxDescendants = "max descendants"; public const string Regen = "regen"; public const string Lifesteal = "lifesteal"; public const string Reflect = "reflect"; public const string Knockback = "knockback"; public const string Armour = "armour"; public const string CloudLife = "cloud life"; public const string CloudDamage = "cloud damage"; public const string CloudsPerSecond = "clouds per second"; public const string CloudRadius = "cloud radius"; public const string CloudEffect = "cloud effect"; public const string BodyEffect = "body effect"; public const string AbsorbHealth = "absorb health"; public const string AbsorbDamage = "absorb damage"; public const string SlowPer100Health = "slow per 100 health"; public const string PlayerThreshold = "player threshold"; public const string DevourCooldown = "devour cooldown"; public static bool IsPrefabField(string field) { switch (field) { default: return field == "warning effect"; case "cloud effect": case "body effect": case "blast effect": return true; } } } internal static class RuleDefaults { private static readonly Dictionary<Mutation, Dictionary<string, float>> Table = BuildTable(); private static readonly Dictionary<Mutation, Dictionary<string, string>> PrefabTable = new Dictionary<Mutation, Dictionary<string, string>> { [Mutation.Bloated] = new Dictionary<string, string> { ["blast effect"] = "fx_barrel_destroyed", ["warning effect"] = "fx_Smoke" }, [Mutation.Miasmic] = new Dictionary<string, string> { ["cloud effect"] = "vfx_blob_death", ["body effect"] = "vfx_blob_death" } }; public static BiomeRules Baseline() { BiomeRules biomeRules = new BiomeRules(); biomeRules.StarChances = new float[6] { 73f, 10f, 10f, 5f, 1f, 1f }; biomeRules.Star = BaselineStarPower(); biomeRules.LargeStarPower = 2f; biomeRules.MutationChance = new float[6] { 2.5f, 3.5f, 5f, 6f, 7.5f, 10f }; BiomeRules biomeRules2 = biomeRules; biomeRules2.MutationChances[Mutation.Devouring] = new float[6] { 0.6f, 0.9f, 1.2f, 1.5f, 1.8f, 2.4f }; Mutation[] inOrder = MutationCatalog.InOrder; foreach (Mutation mutation in inOrder) { biomeRules2.MutationPower[mutation] = new Dictionary<string, float>(Power(mutation)); } return biomeRules2; } public static RuleSet BuildDefault() { return new RuleSet { Defaults = Baseline() }; } public static float Power(Mutation mutation, string field) { if (!Power(mutation).TryGetValue(field, out var value)) { return 1f; } return value; } public static string Prefab(Mutation mutation, string field) { if (!PrefabTable.TryGetValue(mutation, out Dictionary<string, string> value) || !value.TryGetValue(field, out var value2)) { return ""; } return value2; } private static StarPower BaselineStarPower() { StarPower starPower = new StarPower(); starPower.Growth = new float[6] { 0.06f, 0.1f, 0.15f, 0.2f, 0.25f, 0.3f }; starPower.Hp = new float[6] { 1f, 1.4f, 1.95f, 2.75f, 3.85f, 5.4f }; starPower.Attack = new float[6] { 1f, 1.2f, 1.45f, 1.75f, 2.1f, 2.5f }; starPower.SwingSpeed = new float[6] { 1f, 1.02f, 1.05f, 1.08f, 1.12f, 1.16f }; starPower.Speed = new float[6] { 1f, 1f, 1.03f, 1.06f, 1.1f, 1.15f }; starPower.Drops = new float[6] { 1f, 1f, 1.5f, 2f, 2.5f, 3f }; return starPower; } private static Dictionary<string, float> Power(Mutation mutation) { if (!Table.TryGetValue(mutation, out Dictionary<string, float> value)) { return new Dictionary<string, float>(); } return value; } private static Dictionary<Mutation, Dictionary<string, float>> BuildTable() { return new Dictionary<Mutation, Dictionary<string, float>> { [Mutation.Mad] = new Dictionary<string, float> { ["move"] = 1.6f, ["attack speed"] = 1.5f, ["health"] = 0.5f }, [Mutation.Bloated] = new Dictionary<string, float> { ["health"] = 2f, ["delay"] = 1f, ["damage"] = 40f, ["radius"] = 4f }, [Mutation.Cloaked] = new Dictionary<string, float> { ["reveal distance"] = 6f, ["fade time"] = 0.5f, ["fade margin"] = 1f }, [Mutation.Splintering] = new Dictionary<string, float> { ["damage"] = 0.6f, ["max generations"] = 0f, ["max descendants"] = 0f }, [Mutation.Leeching] = new Dictionary<string, float> { ["regen"] = 2f, ["lifesteal"] = 30f }, [Mutation.Warding] = new Dictionary<string, float> { ["reflect"] = 30f, ["knockback"] = 4f }, [Mutation.Plated] = new Dictionary<string, float> { ["armour"] = 100f, ["damage"] = 60f }, [Mutation.Miasmic] = new Dictionary<string, float> { ["cloud life"] = 6f, ["cloud damage"] = 5f, ["clouds per second"] = 1f, ["cloud radius"] = 4f }, [Mutation.Devouring] = new Dictionary<string, float> { ["absorb health"] = 100f, ["absorb damage"] = 100f, ["slow per 100 health"] = 2f, ["player threshold"] = 0.333f, ["devour cooldown"] = 10f } }; } } internal static class RuleFile { private static long _stampTicks; private static long _stampSize; public static string LocalText { get; private set; } = "# Elite Creatures Reborn - creature rules\n#\n# Percentages are 0-100. Anything a biome does not set falls back to `defaults`.\n# Biome names must match the game's own: Meadows, BlackForest, Swamp, Mountain,\n# Plains, Mistlands, AshLands, DeepNorth, Ocean.\n\n# When true, players on a server use the server's copy of this file and cannot\n# override it. Their own file is ignored while connected and restored when they\n# leave. Display preferences in the .cfg (colours, nameplate distance) are never\n# locked - they change only what that player sees.\nlock to server: true\n\n# The most mutations one creature may carry. 1 by default: a creature is Mad, or\n# it is Cloaked, but not both. Raise it for stacked monsters; 0 means no cap at\n# all. Where more roll than the cap allows, the survivors are picked at random.\nmax mutations: 1\n\ndefaults:\n # How much stronger a mutation is when it sits on a large star (worth 5 stars)\n # rather than a small one. Multiplies the mutation's BONUS, never its cost: a\n # Mad creature on a large star is much faster but still has half health. 5 -\n # matching the star's worth - is available but produces absurdities like a\n # creature outrunning the player, so the default is deliberately lower. A biome\n # may override it, so Ash Lands large stars can be ferocious and the Meadows not.\n large star power: 2\n\n # What a star is worth. One entry per star count, so index 0 is an unstarred\n # creature and index 5 a five-star one.\n # growth - Size bonus, added to 1. 0.20 at 3 stars means 20% larger.\n # hp - Max health multiplier. 2.75 at 3 stars means 2.75x.\n # attack - Damage multiplier, applied to everything it deals.\n # swing speed - Attack and animation speed multiplier. Deliberately gentle.\n # speed - Movement speed multiplier. Also gentle.\n # drops - Loot quantity multiplier. Stars do not pay until the second.\n star power:\n growth: [0.06, 0.10, 0.15, 0.20, 0.25, 0.30]\n hp: [1, 1.4, 1.95, 2.75, 3.85, 5.4]\n attack: [1, 1.2, 1.45, 1.75, 2.1, 2.5]\n swing speed: [1, 1.02, 1.05, 1.08, 1.12, 1.16]\n speed: [1, 1, 1.03, 1.06, 1.1, 1.15]\n drops: [1, 1, 1.5, 2, 2.5, 3]\n\n # The chance that ANY ONE mutation appears, indexed by the creature's star\n # count. Applied to every mutation that has no entry of its own below. Each\n # mutation is rolled separately, so these do not sum to anything.\n mutation chance: [2.5, 3.5, 5, 6, 7.5, 10]\n\n # Per-mutation overrides. Anything not listed here uses `mutation chance` above.\n # Devouring is deliberately rarer everywhere: one of them changes a whole area.\n mutation chances:\n Devouring: [0.6, 0.9, 1.2, 1.5, 1.8, 2.4]\n\n # How strong each mutation is. Named fields, not positional numbers. A field\n # marked (enhanced) is multiplied by `large star power` when the mutation sits\n # on a large star; a cost is never multiplied. Meaning:\n # Mad move - Movement speed multiplier. 1.6 = 60% faster. (enhanced)\n # Mad attack speed - Attack and animation speed multiplier. (enhanced)\n # Mad health - Max health multiplier. 0.5 = half. Its cost.\n # Bloated health - Max health multiplier. (enhanced)\n # Bloated delay - Seconds between death and the blast. The blast\n # goes off at the corpse's resting place, not the\n # spot of death; the warning rides the corpse.\n # Bloated damage - Blunt damage at 0 stars, times (1 + stars). (enhanced)\n # Bloated radius - Blast radius in metres. (enhanced)\n # Bloated blast effect - Vanilla prefab cloned for the explosion.\n # Bloated warning effect - Vanilla prefab played on the corpse during the delay.\n # Cloaked reveal distance - Metres at which it becomes visible. (enhanced)\n # Cloaked fade time - Seconds to phase in or out. 0 snaps.\n # Cloaked fade margin - Extra metres before it fades back out (stops strobing).\n # Splintering damage - Damage multiplier. 0.6 = 40% weaker.\n # Splintering max generations - Cascade-depth cap. 0 = unlimited.\n # Splintering max descendants - Live-descendant cap. 0 = unlimited.\n # Leeching regen - Percent of max health regained per second. (enhanced)\n # Leeching lifesteal - Percent of damage dealt returned as health. (enhanced)\n # Warding reflect - Percent of incoming damage returned. (enhanced)\n # Warding knockback - Force applied to a melee attacker. (enhanced)\n # Plated armour - Percent armour bonus at full health, to 0 hurt. (enhanced)\n # Plated damage - Percent damage bonus at zero health, to 0 full. (enhanced)\n # Miasmic cloud life - Seconds a dropped cloud lasts.\n # Miasmic cloud damage - Strength of the vanilla Poison applied to a player\n # it hits or who stands in a cloud - not direct\n # damage; the status effect does the harming. (enhanced)\n # Miasmic clouds per second - How often it drops one while moving. (enhanced)\n # Miasmic cloud radius - Metres. The visible cloud matches this exactly.\n # Miasmic cloud effect - Vanilla prefab cloned for a trail cloud.\n # Miasmic body effect - Vanilla poison visual worn on the creature at all\n # times. It looks poisoned; it is not, and takes no harm.\n # Devouring absorb health - Percent of a victim's max health it keeps, added to\n # its own permanently. An instant kill always lands the\n # killing blow, so the whole amount is kept. (enhanced)\n # Devouring absorb damage - Percent of a victim's damage it keeps, permanently. (enhanced)\n # Devouring slow per 100 health - Percent speed lost per 100 eaten health. Its cost.\n # Devouring player threshold - Fraction of a player's max health its per-hit\n # damage must pass before it hunts players for good.\n # Devouring devour cooldown - Seconds it must wait after a meal before it can eat\n # again. During it the creature fights as an ordinary one.\n mutation power:\n Mad: { move: 1.6, attack speed: 1.5, health: 0.5 }\n Bloated: { health: 2.0, delay: 1.0, damage: 40, radius: 4, blast effect: fx_barrel_destroyed, warning effect: fx_Smoke }\n Cloaked: { reveal distance: 6, fade time: 0.5, fade margin: 1 }\n Splintering: { damage: 0.6, max generations: 0, max descendants: 0 }\n Leeching: { regen: 2, lifesteal: 30 }\n Warding: { reflect: 30, knockback: 4 }\n Plated: { armour: 100, damage: 60 }\n Miasmic: { cloud life: 6, cloud damage: 5, clouds per second: 1, cloud radius: 4, cloud effect: vfx_blob_death, body effect: vfx_blob_death }\n Devouring: { absorb health: 100, absorb damage: 100, slow per 100 health: 2, player threshold: 0.333, devour cooldown: 10 }\n\n# Every biome below overrides only what it names. Delete a line to fall back to\n# `defaults`; delete a whole biome to make it behave like the defaults entirely.\n#\n# `star chances` is per-biome ONLY - there is deliberately no global default,\n# because how starry a biome is is the main thing that separates one from the\n# next. One entry per star count, and they should add up to 100. This is a\n# straight distribution, not a chain: [73, 10, 10, 5, 1, 1] means 73 creatures in\n# 100 have no stars, 10 have one, 10 have two, 5 have three, 1 has four and 1 has\n# five. Even the gentlest biome keeps a sliver at the top - that is the rare\n# large-star creature, and it should exist everywhere, just barely. Adding a\n# seventh entry raises that biome's ceiling to 6 stars, and so on. An unlisted or\n# modded biome takes the Meadows row for stars.\nbiomes:\n - match: Meadows\n star chances: [73, 10, 10, 5, 1, 1]\n mutation chance: [2.5, 3.5, 5, 6, 7.5, 10]\n\n - match: BlackForest\n star chances: [62, 15, 12, 6, 3, 2]\n mutation chance: [3.5, 4.5, 6, 8, 10, 12.5]\n\n - match: Swamp\n star chances: [52, 18, 14, 8, 5, 3]\n mutation chance: [4, 6, 8, 10, 12.5, 16]\n mutation chances:\n Miasmic: [11, 16, 22, 28, 34, 42] # rot belongs here\n Leeching: [8, 12, 16, 21, 25, 31]\n\n - match: Mountain\n star chances: [42, 20, 17, 10, 7, 4]\n mutation chance: [5, 7, 9, 12, 15, 18.5]\n mutation chances:\n Plated: [16, 22, 30, 38, 46, 56]\n Cloaked: [2, 3, 4, 5, 6, 7] # little cover up there\n\n - match: Plains\n star chances: [32, 22, 20, 13, 8, 5]\n mutation chance: [6, 8, 11, 14, 17, 21.5]\n mutation chances:\n Mad: [22, 30, 40, 50, 60, 72]\n Devouring: [1.5, 2, 3, 4, 5, 6]\n\n - match: Mistlands\n star chances: [22, 22, 22, 16, 11, 7]\n mutation chance: [6.5, 9, 12.5, 16, 20, 25]\n mutation chances:\n Cloaked: [30, 40, 52, 64, 76, 90] # the mist hides things already\n Devouring: [2, 3, 4, 5, 6, 8]\n\n - match: AshLands\n star chances: [12, 20, 24, 20, 15, 9]\n mutation chance: [7.5, 10.5, 14, 18, 22, 28]\n mutation chances:\n Bloated: [38, 50, 64, 78, 92, 100]\n Splintering: [28, 37, 48, 58, 69, 82]\n\n - match: DeepNorth\n star chances: [12, 20, 24, 20, 15, 9]\n mutation chance: [7.5, 10.5, 14, 18, 22, 28]\n mutation chances:\n Plated: [38, 50, 64, 78, 92, 100]\n\n - match: Ocean\n star chances: [68, 12, 10, 6, 3, 1]\n mutation chance: [3, 4, 5.5, 7, 8.5, 11]\n"; public static RuleSet Local { get; private set; } = RuleDefaults.BuildDefault(); private static string Path => System.IO.Path.Combine(Paths.ConfigPath, "creature_rules.yml"); public static void Initialise() { EnsureExists(); Load(); } private static void EnsureExists() { try { if (!File.Exists(Path)) { File.WriteAllText(Path, "# Elite Creatures Reborn - creature rules\n#\n# Percentages are 0-100. Anything a biome does not set falls back to `defaults`.\n# Biome names must match the game's own: Meadows, BlackForest, Swamp, Mountain,\n# Plains, Mistlands, AshLands, DeepNorth, Ocean.\n\n# When true, players on a server use the server's copy of this file and cannot\n# override it. Their own file is ignored while connected and restored when they\n# leave. Display preferences in the .cfg (colours, nameplate distance) are never\n# locked - they change only what that player sees.\nlock to server: true\n\n# The most mutations one creature may carry. 1 by default: a creature is Mad, or\n# it is Cloaked, but not both. Raise it for stacked monsters; 0 means no cap at\n# all. Where more roll than the cap allows, the survivors are picked at random.\nmax mutations: 1\n\ndefaults:\n # How much stronger a mutation is when it sits on a large star (worth 5 stars)\n # rather than a small one. Multiplies the mutation's BONUS, never its cost: a\n # Mad creature on a large star is much faster but still has half health. 5 -\n # matching the star's worth - is available but produces absurdities like a\n # creature outrunning the player, so the default is deliberately lower. A biome\n # may override it, so Ash Lands large stars can be ferocious and the Meadows not.\n large star power: 2\n\n # What a star is worth. One entry per star count, so index 0 is an unstarred\n # creature and index 5 a five-star one.\n # growth - Size bonus, added to 1. 0.20 at 3 stars means 20% larger.\n # hp - Max health multiplier. 2.75 at 3 stars means 2.75x.\n # attack - Damage multiplier, applied to everything it deals.\n # swing speed - Attack and animation speed multiplier. Deliberately gentle.\n # speed - Movement speed multiplier. Also gentle.\n # drops - Loot quantity multiplier. Stars do not pay until the second.\n star power:\n growth: [0.06, 0.10, 0.15, 0.20, 0.25, 0.30]\n hp: [1, 1.4, 1.95, 2.75, 3.85, 5.4]\n attack: [1, 1.2, 1.45, 1.75, 2.1, 2.5]\n swing speed: [1, 1.02, 1.05, 1.08, 1.12, 1.16]\n speed: [1, 1, 1.03, 1.06, 1.1, 1.15]\n drops: [1, 1, 1.5, 2, 2.5, 3]\n\n # The chance that ANY ONE mutation appears, indexed by the creature's star\n # count. Applied to every mutation that has no entry of its own below. Each\n # mutation is rolled separately, so these do not sum to anything.\n mutation chance: [2.5, 3.5, 5, 6, 7.5, 10]\n\n # Per-mutation overrides. Anything not listed here uses `mutation chance` above.\n # Devouring is deliberately rarer everywhere: one of them changes a whole area.\n mutation chances:\n Devouring: [0.6, 0.9, 1.2, 1.5, 1.8, 2.4]\n\n # How strong each mutation is. Named fields, not positional numbers. A field\n # marked (enhanced) is multiplied by `large star power` when the mutation sits\n # on a large star; a cost is never multiplied. Meaning:\n # Mad move - Movement speed multiplier. 1.6 = 60% faster. (enhanced)\n # Mad attack speed - Attack and animation speed multiplier. (enhanced)\n # Mad health - Max health multiplier. 0.5 = half. Its cost.\n # Bloated health - Max health multiplier. (enhanced)\n # Bloated delay - Seconds between death and the blast. The blast\n # goes off at the corpse's resting place, not the\n # spot of death; the warning rides the corpse.\n # Bloated damage - Blunt damage at 0 stars, times (1 + stars). (enhanced)\n # Bloated radius - Blast radius in metres. (enhanced)\n # Bloated blast effect - Vanilla prefab cloned for the explosion.\n # Bloated warning effect - Vanilla prefab played on the corpse during the delay.\n # Cloaked reveal distance - Metres at which it becomes visible. (enhanced)\n # Cloaked fade time - Seconds to phase in or out. 0 snaps.\n # Cloaked fade margin - Extra metres before it fades back out (stops strobing).\n # Splintering damage - Damage multiplier. 0.6 = 40% weaker.\n # Splintering max generations - Cascade-depth cap. 0 = unlimited.\n # Splintering max descendants - Live-descendant cap. 0 = unlimited.\n # Leeching regen - Percent of max health regained per second. (enhanced)\n # Leeching lifesteal - Percent of damage dealt returned as health. (enhanced)\n # Warding reflect - Percent of incoming damage returned. (enhanced)\n # Warding knockback - Force applied to a melee attacker. (enhanced)\n # Plated armour - Percent armour bonus at full health, to 0 hurt. (enhanced)\n # Plated damage - Percent damage bonus at zero health, to 0 full. (enhanced)\n # Miasmic cloud life - Seconds a dropped cloud lasts.\n # Miasmic cloud damage - Strength of the vanilla Poison applied to a player\n # it hits or who stands in a cloud - not direct\n # damage; the status effect does the harming. (enhanced)\n # Miasmic clouds per second - How often it drops one while moving. (enhanced)\n # Miasmic cloud radius - Metres. The visible cloud matches this exactly.\n # Miasmic cloud effect - Vanilla prefab cloned for a trail cloud.\n # Miasmic body effect - Vanilla poison visual worn on the creature at all\n # times. It looks poisoned; it is not, and takes no harm.\n # Devouring absorb health - Percent of a victim's max health it keeps, added to\n # its own permanently. An instant kill always lands the\n # killing blow, so the whole amount is kept. (enhanced)\n # Devouring absorb damage - Percent of a victim's damage it keeps, permanently. (enhanced)\n # Devouring slow per 100 health - Percent speed lost per 100 eaten health. Its cost.\n # Devouring player threshold - Fraction of a player's max health its per-hit\n # damage must pass before it hunts players for good.\n # Devouring devour cooldown - Seconds it must wait after a meal before it can eat\n # again. During it the creature fights as an ordinary one.\n mutation power:\n Mad: { move: 1.6, attack speed: 1.5, health: 0.5 }\n Bloated: { health: 2.0, delay: 1.0, damage: 40, radius: 4, blast effect: fx_barrel_destroyed, warning effect: fx_Smoke }\n Cloaked: { reveal distance: 6, fade time: 0.5, fade margin: 1 }\n Splintering: { damage: 0.6, max generations: 0, max descendants: 0 }\n Leeching: { regen: 2, lifesteal: 30 }\n Warding: { reflect: 30, knockback: 4 }\n Plated: { armour: 100, damage: 60 }\n Miasmic: { cloud life: 6, cloud damage: 5, clouds per second: 1, cloud radius: 4, cloud effect: vfx_blob_death, body effect: vfx_blob_death }\n Devouring: { absorb health: 100, absorb damage: 100, slow per 100 health: 2, player threshold: 0.333, devour cooldown: 10 }\n\n# Every biome below overrides only what it names. Delete a line to fall back to\n# `defaults`; delete a whole biome to make it behave like the defaults entirely.\n#\n# `star chances` is per-biome ONLY - there is deliberately no global default,\n# because how starry a biome is is the main thing that separates one from the\n# next. One entry per star count, and they should add up to 100. This is a\n# straight distribution, not a chain: [73, 10, 10, 5, 1, 1] means 73 creatures in\n# 100 have no stars, 10 have one, 10 have two, 5 have three, 1 has four and 1 has\n# five. Even the gentlest biome keeps a sliver at the top - that is the rare\n# large-star creature, and it should exist everywhere, just barely. Adding a\n# seventh entry raises that biome's ceiling to 6 stars, and so on. An unlisted or\n# modded biome takes the Meadows row for stars.\nbiomes:\n - match: Meadows\n star chances: [73, 10, 10, 5, 1, 1]\n mutation chance: [2.5, 3.5, 5, 6, 7.5, 10]\n\n - match: BlackForest\n star chances: [62, 15, 12, 6, 3, 2]\n mutation chance: [3.5, 4.5, 6, 8, 10, 12.5]\n\n - match: Swamp\n star chances: [52, 18, 14, 8, 5, 3]\n mutation chance: [4, 6, 8, 10, 12.5, 16]\n mutation chances:\n Miasmic: [11, 16, 22, 28, 34, 42] # rot belongs here\n Leeching: [8, 12, 16, 21, 25, 31]\n\n - match: Mountain\n star chances: [42, 20, 17, 10, 7, 4]\n mutation chance: [5, 7, 9, 12, 15, 18.5]\n mutation chances:\n Plated: [16, 22, 30, 38, 46, 56]\n Cloaked: [2, 3, 4, 5, 6, 7] # little cover up there\n\n - match: Plains\n star chances: [32, 22, 20, 13, 8, 5]\n mutation chance: [6, 8, 11, 14, 17, 21.5]\n mutation chances:\n Mad: [22, 30, 40, 50, 60, 72]\n Devouring: [1.5, 2, 3, 4, 5, 6]\n\n - match: Mistlands\n star chances: [22, 22, 22, 16, 11, 7]\n mutation chance: [6.5, 9, 12.5, 16, 20, 25]\n mutation chances:\n Cloaked: [30, 40, 52, 64, 76, 90] # the mist hides things already\n Devouring: [2, 3, 4, 5, 6, 8]\n\n - match: AshLands\n star chances: [12, 20, 24, 20, 15, 9]\n mutation chance: [7.5, 10.5, 14, 18, 22, 28]\n mutation chances:\n Bloated: [38, 50, 64, 78, 92, 100]\n Splintering: [28, 37, 48, 58, 69, 82]\n\n - match: DeepNorth\n star chances: [12, 20, 24, 20, 15, 9]\n mutation chance: [7.5, 10.5, 14, 18, 22, 28]\n mutation chances:\n Plated: [38, 50, 64, 78, 92, 100]\n\n - match: Ocean\n star chances: [68, 12, 10, 6, 3, 1]\n mutation chance: [3, 4, 5.5, 7, 8.5, 11]\n"); Log.Info("wrote a fresh rule file to " + Path); } } catch (Exception ex) { Log.Error("could not write the rule file: " + ex.Message); } } public static bool Load() { string text; try { text = File.ReadAllText(Path); } catch (Exception ex) { Log.Error("could not read the rule file: " + ex.Message); return false; } Stamp(); return Adopt(text); } private static bool Adopt(string text) { RuleParser.Result result = RuleParser.Parse(text); foreach (string warning in result.Warnings) { Log.Warn("rule file: " + warning); } if (result.Rules == null || result.Errors.Count > 0) { Report(result.Errors); return false; } LocalText = text; Local = result.Rules; return true; } private static void Report(List<string> errors) { Log.Error("rule file has errors; keeping the last good rules:"); foreach (string error in errors) { Log.Error(" " + error); } } public static bool ChangedOnDisk() { try { FileInfo fileInfo = new FileInfo(Path); return fileInfo.Exists && (fileInfo.LastWriteTimeUtc.Ticks != _stampTicks || fileInfo.Length != _stampSize); } catch (Exception) { return false; } } private static void Stamp() { try { FileInfo fileInfo = new FileInfo(Path); _stampTicks = fileInfo.LastWriteTimeUtc.Ticks; _stampSize = fileInfo.Length; } catch (Exception) { _stampTicks = 0L; _stampSize = 0L; } } } internal static class RuleOverlay { private const int MaxStarEntries = 51; public static void Apply(BiomeRules rules, YamlMappingNode block, List<string> errors, List<string> warnings) { StarChances(rules, block, errors, warnings); LargeStarPower(rules, block, errors); Star(rules, block, errors); MutationChance(rules, block, errors); MutationChances(rules, block, errors); MutationPower(rules, block, errors); } private static void StarChances(BiomeRules rules, YamlMappingNode block, List<string> e, List<string> w) { float[] array = ReadFloats(block, "star chances", e); if (array != null) { rules.StarChances = Normalise(Truncate(array, w), w); } } private static float[] Truncate(float[] values, List<string> warnings) { if (values.Length <= 51) { return values; } warnings.Add($"star chances lists {values.Length} entries, past the 50-star ceiling - truncating"); float[] array = new float[51]; Array.Copy(values, array, 51); return array; } private static void LargeStarPower(BiomeRules rules, YamlMappingNode block, List<string> errors) { YamlNode yamlNode = YamlRead.Child(block, "large star power"); if (yamlNode != null && YamlRead.TryFloat(yamlNode, out var value)) { rules.LargeStarPower = value; } else if (yamlNode != null) { YamlRead.AddError(errors, yamlNode, "'large star power' is not a number"); } } private static float[] Normalise(float[] values, List<string> warnings) { float num = 0f; foreach (float num2 in values) { num += Mathf.Max(0f, num2); } if (num <= 0f || Mathf.Abs(num - 100f) < 0.01f) { return values; } warnings.Add($"star chances sum to {num:0.##}, not 100 - normalising"); float[] array = new float[values.Length]; for (int j = 0; j < values.Length; j++) { array[j] = Mathf.Max(0f, values[j]) / num * 100f; } return array; } private static void Star(BiomeRules rules, YamlMappingNode block, List<string> errors) { if (YamlRead.Child(block, "star power") is YamlMappingNode map) { StarPower star = rules.Star; star.Growth = Line(map, "growth", star.Growth, errors); star.Hp = Line(map, "hp", star.Hp, errors); star.Attack = Line(map, "attack", star.Attack, errors); star.SwingSpeed = Line(map, "swing speed", star.SwingSpeed, errors); star.Speed = Line(map, "speed", star.Speed, errors); star.Drops = Line(map, "drops", star.Drops, errors); } } private static float[] Line(YamlMappingNode map, string key, float[] current, List<string> errors) { return ReadFloats(map, key, errors) ?? current; } private static void MutationChance(BiomeRules rules, YamlMappingNode block, List<string> errors) { float[] array = ReadFloats(block, "mutation chance", errors); if (array != null) { rules.MutationChance = array; } } private static void MutationChances(BiomeRules rules, YamlMappingNode block, List<string> errors) { if (!(YamlRead.Child(block, "mutation chances") is YamlMappingNode yamlMappingNode)) { return; } foreach (KeyValuePair<YamlNode, YamlNode> child in yamlMappingNode.Children) { Mutation? mutation = NamedMutation(child.Key, errors); float[] array = YamlRead.Floats(child.Value, errors, "a mutation chance"); if (mutation.HasValue && array != null) { rules.MutationChances[mutation.Value] = array; } } } private static void MutationPower(BiomeRules rules, YamlMappingNode block, List<string> errors) { if (!(YamlRead.Child(block, "mutation power") is YamlMappingNode yamlMappingNode)) { return; } foreach (KeyValuePair<YamlNode, YamlNode> child in yamlMappingNode.Children) { Mutation? mutation = NamedMutation(child.Key, errors); if (mutation.HasValue) { YamlMappingNode yamlMappingNode2 = YamlRead.Map(child.Value, errors, "a mutation's power"); if (yamlMappingNode2 != null) { MergeFields(rules, mutation.Value, yamlMappingNode2, errors); } } } } private static void MergeFields(BiomeRules rules, Mutation mutation, YamlMappingNode fields, List<string> errors) { foreach (KeyValuePair<YamlNode, YamlNode> child in fields.Children) { string text = (child.Key as YamlScalarNode)?.Value; if (text == null) { YamlRead.AddError(errors, child.Key, $"'{mutation}' has a power field with no name"); } else if (Fields.IsPrefabField(text)) { MergeText(rules, mutation, text, child.Value, errors); } else { MergeNumber(rules, mutation, text, child.Value, errors); } } } private static void MergeNumber(BiomeRules rules, Mutation m, string key, YamlNode node, List<string> errors) { if (!YamlRead.TryFloat(node, out var value)) { YamlRead.AddError(errors, node, $"'{m}' field '{key}' is not a number"); return; } if (!rules.MutationPower.TryGetValue(m, out Dictionary<string, float> value2)) { value2 = (rules.MutationPower[m] = new Dictionary<string, float>()); } value2[key] = value; } private static void MergeText(BiomeRules rules, Mutation m, string key, YamlNode node, List<string> errors) { string value = (node as YamlScalarNode)?.Value; if (string.IsNullOrEmpty(value)) { YamlRead.AddError(errors, node, $"'{m}' field '{key}' should be a prefab name"); return; } if (!rules.MutationText.TryGetValue(m, out Dictionary<string, string> value2)) { value2 = (rules.MutationText[m] = new Dictionary<string, string>()); } value2[key] = value; } private static Mutation? NamedMutation(YamlNode key, List<string> errors) { string text = (key as YamlScalarNode)?.Value ?? ""; Mutation? result = MutationCatalog.FromName(text); if (!result.HasValue) { YamlRead.AddError(errors, key, "'" + text + "' is not one of the nine mutations"); } return result; } private static float[]? ReadFloats(YamlMappingNode block, string key, List<string> errors) { YamlNode yamlNode = YamlRead.Child(block, key); if (yamlNode != null) { return YamlRead.Floats(yamlNode, errors, "'" + key + "'"); } return null; } } internal static class RuleParser { public sealed class Result { public RuleSet? Rules; public readonly List<string> Errors = new List<string>(); public readonly List<string> Warnings = new List<string>(); } public static Result Parse(string text) { Result result = new Result(); YamlMappingNode yamlMappingNode = LoadRoot(text, result.Errors); if (yamlMappingNode == null) { return result; } RuleSet ruleSet = new RuleSet { LockToServer = YamlRead.Bool(yamlMappingNode, "lock to server", fallback: true, result.Errors), MaxMutations = Math.Max(0, YamlRead.Int(yamlMappingNode, "max mutations", 1, result.Errors)) }; ReadDefaults(ruleSet, yamlMappingNode, result); ReadBiomes(ruleSet, yamlMappingNode, result); SeedStarFallback(ruleSet); result.Rules = ruleSet; return result; } private static void SeedStarFallback(RuleSet set) { if (set.Biomes.TryGetValue("Meadows", out BiomeRules value)) { set.Defaults.StarChances = (float[])value.StarChances.Clone(); } } private static YamlMappingNode? LoadRoot(string text, List<string> errors) { try { YamlStream yamlStream = new YamlStream(); yamlStream.Load(new StringReader(text)); if (yamlStream.Documents.Count > 0 && yamlStream.Documents[0].RootNode is YamlMappingNode result) { return result; } errors.Add("the file is empty or is not a set of keyed values"); } catch (YamlException ex) { errors.Add($"line {ex.Start.Line}: {ex.Message}"); } catch (Exception ex2) { errors.Add("could not read the file: " + ex2.Message); } return null; } private static void ReadDefaults(RuleSet set, YamlMappingNode root, Result result) { set.Defaults = RuleDefaults.Baseline(); if (YamlRead.Child(root, "defaults") is YamlMappingNode block) { RuleOverlay.Apply(set.Defaults, block, result.Errors, result.Warnings); } } private static void ReadBiomes(RuleSet set, YamlMappingNode root, Result result) { YamlNode yamlNode = YamlRead.Child(root, "biomes"); if (yamlNode == null) { return; } if (!(yamlNode is YamlSequenceNode yamlSequenceNode)) { YamlRead.AddError(result.Errors, yamlNode, "'biomes' should be a list of biome blocks"); return; } foreach (YamlNode child in yamlSequenceNode.Children) { ReadBiome(set, child, result); } } private static void ReadBiome(RuleSet set, YamlNode item, Result result) { if (!(item is YamlMappingNode yamlMappingNode)) { YamlRead.AddError(result.Errors, item, "a biome entry should be a block of keyed values"); return; } string text = YamlRead.Scalar(yamlMappingNode, "match"); if (string.IsNullOrEmpty(text)) { YamlRead.AddError(result.Errors, yamlMappingNode, "a biome entry has no 'match' name"); return; } BiomeRules biomeRules = set.Defaults.Clone(); RuleOverlay.Apply(biomeRules, yamlMappingNode, result.Errors, result.Warnings); set.Biomes[text] = biomeRules; } } public sealed class RuleReload : MonoBehaviour { private const float IntervalSeconds = 5f; private float _timer; public static void Attach() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown GameObject val = new GameObject("ecr_rule_reload"); Object.DontDestroyOnLoad((Object)val); val.AddComponent<RuleReload>(); } private void Update() { Guard.Run("RuleReload.Update", Poll); } private void Poll() { _timer += Time.deltaTime; if (!(_timer < 5f)) { _timer = 0f; if (RuleFile.ChangedOnDisk() && RuleFile.Load()) { ServerLock.OnLocalReloaded(); } } } } public sealed class RuleSet { public bool LockToServer = true; public int MaxMutations = 1; public BiomeRules Defaults = new BiomeRules(); public readonly Dictionary<string, BiomeRules> Biomes = new Dictionary<string, BiomeRules>(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _loggedUnlisted = new HashSet<string>(); public unsafe BiomeRules For(Biome biome) { string text = ((object)(*(Biome*)(&biome))/*cast due to .constrained prefix*/).ToString(); if (Biomes.TryGetValue(text, out BiomeRules value)) { return value; } if (_loggedUnlisted.Add(text)) { Log.Info("biome '" + text + "' has no rules entry - using defaults, and the Meadows row for stars"); } return Defaults; } } public static class RuleState { public static RuleSet Active { get; private set; } = RuleDefaults.BuildDefault(); public static event Action? Changed; public static void Adopt(RuleSet set) { if (set == null) { return; } Active = set; try { RuleState.Changed?.Invoke(); } catch (Exception arg) { Log.Error($"a rule-change handler threw: {arg}"); } } } internal static class RuleText { public const string FileName = "creature_rules.yml"; public const string Default = "# Elite Creatures Reborn - creature rules\n#\n# Percentages are 0-100. Anything a biome does not set falls back to `defaults`.\n# Biome names must match the game's own: Meadows, BlackForest, Swamp, Mountain,\n# Plains, Mistlands, AshLands, DeepNorth, Ocean.\n\n# When true, players on a server use the server's copy of this file and cannot\n# override it. Their own file is ignored while connected and restored when they\n# leave. Display preferences in the .cfg (colours, nameplate distance) are never\n# locked - they change only what that player sees.\nlock to server: true\n\n# The most mutations one creature may carry. 1 by default: a creature is Mad, or\n# it is Cloaked, but not both. Raise it for stacked monsters; 0 means no cap at\n# all. Where more roll than the cap allows, the survivors are picked at random.\nmax mutations: 1\n\ndefaults:\n # How much stronger a mutation is when it sits on a large star (worth 5 stars)\n # rather than a small one. Multiplies the mutation's BONUS, never its cost: a\n # Mad creature on a large star is much faster but still has half health. 5 -\n # matching the star's worth - is available but produces absurdities like a\n # creature outrunning the player, so the default is deliberately lower. A biome\n # may override it, so Ash Lands large stars can be ferocious and the Meadows not.\n large star power: 2\n\n # What a star is worth. One entry per star count, so index 0 is an unstarred\n # creature and index 5 a five-star one.\n # growth - Size bonus, added to 1. 0.20 at 3 stars means 20% larger.\n # hp - Max health multiplier. 2.75 at 3 stars means 2.75x.\n # attack - Damage multiplier, applied to everything it deals.\n # swing speed - Attack and animation speed multiplier. Deliberately gentle.\n # speed - Movement speed multiplier. Also gentle.\n # drops - Loot quantity multiplier. Stars do not pay until the second.\n star power:\n growth: [0.06, 0.10, 0.15, 0.20, 0.25, 0.30]\n hp: [1, 1.4, 1.95, 2.75, 3.85, 5.4]\n attack: [1, 1.2, 1.45, 1.75, 2.1, 2.5]\n swing speed: [1, 1.02, 1.05, 1.08, 1.12, 1.16]\n speed: [1, 1, 1.03, 1.06, 1.1, 1.15]\n drops: [1, 1, 1.5, 2, 2.5, 3]\n\n # The chance that ANY ONE mutation appears, indexed by the creature's star\n # count. Applied to every mutation that has no entry of its own below. Each\n # mutation is rolled separately, so these do not sum to anything.\n mutation chance: [2.5, 3.5, 5, 6, 7.5, 10]\n\n # Per-mutation overrides. Anything not listed here uses `mutation chance` above.\n # Devouring is deliberately rarer everywhere: one of them changes a whole area.\n mutation chances:\n Devouring: [0.6, 0.9, 1.2, 1.5, 1.8, 2.4]\n\n # How strong each mutation is. Named fields, not positional numbers. A field\n # marked (enhanced) is multiplied by `large star power` when the mutation sits\n # on a large star; a cost is never multiplied. Meaning:\n # Mad move - Movement speed multiplier. 1.6 = 60% faster. (enhanced)\n # Mad attack speed - Attack and animation speed multiplier. (enhanced)\n # Mad health - Max health multiplier. 0.5 = half. Its cost.\n # Bloated health - Max health multiplier. (enhanced)\n # Bloated delay - Seconds between death and the blast. The blast\n # goes off at the corpse's resting place, not the\n # spot of death; the warning rides the corpse.\n # Bloated damage - Blunt damage at 0 stars, times (1 + stars). (enhanced)\n # Bloated radius - Blast radius in metres. (enhanced)\n # Bloated blast effect - Vanilla prefab cloned for the explosion.\n # Bloated warning effect - Vanilla prefab played on the corpse during the delay.\n # Cloaked reveal distance - Metres at which it becomes visible. (enhanced)\n # Cloaked fade time - Seconds to phase in or out. 0 snaps.\n # Cloaked fade margin - Extra metres before it fades back out (stops strobing).\n # Splintering damage - Damage multiplier. 0.6 = 40% weaker.\n # Splintering max generations - Cascade-depth cap. 0 = unlimited.\n # Splintering max descendants - Live-descendant cap. 0 = unlimited.\n # Leeching regen - Percent of max health regained per second. (enhanced)\n # Leeching lifesteal - Percent of damage dealt returned as health. (enhanced)\n # Warding reflect - Percent of incoming damage returned. (enhanced)\n # Warding knockback - Force applied to a melee attacker. (enhanced)\n # Plated armour - Percent armour bonus at full health, to 0 hurt. (enhanced)\n # Plated damage - Percent damage bonus at zero health, to 0 full. (enhanced)\n # Miasmic cloud life - Seconds a dropped cloud lasts.\n # Miasmic cloud damage - Strength of the vanilla Poison applied to a player\n # it hits or who stands in a cloud - not direct\n # damage; the status effect does the harming. (enhanced)\n # Miasmic clouds per second - How often it drops one while moving. (enhanced)\n # Miasmic cloud radius - Metres. The visible cloud matches this exactly.\n # Miasmic cloud effect - Vanilla prefab cloned for a trail cloud.\n # Miasmic body effect - Vanilla poison visual worn on the creature at all\n # times. It looks poisoned; it is not, and takes no harm.\n # Devouring absorb health - Percent of a victim's max health it keeps, added to\n # its own permanently. An instant kill always lands the\n # killing blow, so the whole amount is kept. (enhanced)\n # Devouring absorb damage - Percent of a victim's damage it keeps, permanently. (enhanced)\n # Devouring slow per 100 health - Percent speed lost per 100 eaten health. Its cost.\n # Devouring player threshold - Fraction of a player's max health its per-hit\n # damage must pass before it hunts players for good.\n # Devouring devour cooldown - Seconds it must wait after a meal before it can eat\n # again. During it the creature fights as an ordinary one.\n mutation power:\n Mad: { move: 1.6, attack speed: 1.5, health: 0.5 }\n Bloated: { health: 2.0, delay: 1.0, damage: 40, radius: 4, blast effect: fx_barrel_destroyed, warning effect: fx_Smoke }\n Cloaked: { reveal distance: 6, fade time: 0.5, fade margin: 1 }\n Splintering: { damage: 0.6, max generations: 0, max descendants: 0 }\n Leeching: { regen: 2, lifesteal: 30 }\n Warding: { reflect: 30, knockback: 4 }\n Plated: { armour: 100, damage: 60 }\n Miasmic: { cloud life: 6, cloud damage: 5, clouds per second: 1, cloud radius: 4, cloud effect: vfx_blob_death, body effect: vfx_blob_death }\n Devouring: { absorb health: 100, absorb damage: 100, slow per 100 health: 2, player threshold: 0.333, devour cooldown: 10 }\n\n# Every biome below overrides only what it names. Delete a line to fall back to\n# `defaults`; delete a whole biome to make it behave like the defaults entirely.\n#\n# `star chances` is per-biome ONLY - there is deliberately no global default,\n# because how starry a biome is is the main thing that separates one from the\n# next. One entry per star count, and they should add up to 100. This is a\n# straight distribution, not a chain: [73, 10, 10, 5, 1, 1] means 73 creatures in\n# 100 have no stars, 10 have one, 10 have two, 5 have three, 1 has four and 1 has\n# five. Even the gentlest biome keeps a sliver at the top - that is the rare\n# large-star creature, and it should exist everywhere, just barely. Adding a\n# seventh entry raises that biome's ceiling to 6 stars, and so on. An unlisted or\n# modded biome takes the Meadows row for stars.\nbiomes:\n - match: Meadows\n star chances: [73, 10, 10, 5, 1, 1]\n mutation chance: [2.5, 3.5, 5, 6, 7.5, 10]\n\n - match: BlackForest\n star chances: [62, 15, 12, 6, 3, 2]\n mutation chance: [3.5, 4.5, 6, 8, 10, 12.5]\n\n - match: Swamp\n star chances: [52, 18, 14, 8, 5, 3]\n mutation chance: [4, 6, 8, 10, 12.5, 16]\n mutation chances:\n Miasmic: [11, 16, 22, 28, 34, 42] # rot belongs here\n Leeching: [8, 12, 16, 21, 25, 31]\n\n - match: Mountain\n star chances: [42, 20, 17, 10, 7, 4]\n mutation chance: [5, 7, 9, 12, 15, 18.5]\n mutation chances:\n Plated: [16, 22, 30, 38, 46, 56]\n Cloaked: [2, 3, 4, 5, 6, 7] # little cover up there\n\n - match: Plains\n star chances: [32, 22, 20, 13, 8, 5]\n mutation chance: [6, 8, 11, 14, 17, 21.5]\n mutation chances:\n Mad: [22, 30, 40, 50, 60, 72]\n Devouring: [1.5, 2, 3, 4, 5, 6]\n\n - match: Mistlands\n star chances: [22, 22, 22, 16, 11, 7]\n mutation chance: [6.5, 9, 12.5, 16, 20, 25]\n mutation chances:\n Cloaked: [30, 40, 52, 64, 76, 90] # the mist hides things already\n Devouring: [2, 3, 4, 5, 6, 8]\n\n - match: AshLands\n star chances: [12, 20, 24, 20, 15, 9]\n mutation chance: [7.5, 10.5, 14, 18, 22, 28]\n mutation chances:\n Bloated: [38, 50, 64, 78, 92, 100]\n Splintering: [28, 37, 48, 58, 69, 82]\n\n - match: DeepNorth\n star chances: [12, 20, 24, 20, 15, 9]\n mutation chance: [7.5, 10.5, 14, 18, 22, 28]\n mutation chances:\n Plated: [38, 50, 64, 78, 92, 100]\n\n - match: Ocean\n star chances: [68, 12, 10, 6, 3, 1]\n mutation chance: [3, 4, 5.5, 7, 8.5, 11]\n"; } internal static class ServerLock { private const string ArticleName = "creature_rules"; private static global::Charter.Charter _charter; private static Article<string> _rules; private static ConfigEntry<bool> _binding; public static void Setup(ConfigFile config, string guid, string title, string version) { _charter = new global::Charter.Charter(guid, title, version); _binding = config.Bind<bool>("1 - General", "Lock to server", true, "Mirrors the rule file's 'lock to server'. The server governs this; a player's own copy is ignored while bound."); _charter.Binding(_binding); _rules = new Article<string>(_charter, "creature_rules", RuleFile.LocalText); _rules.Changed += delegate { Guard("rule push", Recompute); }; _charter.BindingChanged += delegate { Guard("binding change", OnBindingChanged); }; MirrorBinding(); Publish(); Recompute(); } public static void Install(Harmony harmony) { global::Charter.Charter.Install(harmony); } public static void OnLocalReloaded() { MirrorBinding(); Publish(); Recompute(); } private static void OnBindingChanged() { if (_charter.IsAuthor) { RuleFile.Load(); } Recompute(); } private static void MirrorBinding() { if (_charter.IsAuthor) { _binding.Value = RuleFile.Local.LockToServer; } } private static void Publish() { if (_charter.IsAuthor) { _rules.Assign(RuleFile.LocalText); } } private static void Recompute() { if (_charter.IsAuthor) { RuleState.Adopt(RuleFile.Local); return; } RuleParser.Result result = RuleParser.Parse(_rules.Value); if (result.Rules != null && result.Errors.Count == 0) { RuleState.Adopt(result.Rules); } } private static void Guard(string what, Action action) { try { action(); } catch (Exception arg) { Log.Error($"server lock ({what}) threw: {arg}"); } } } public sealed class StarPower { public float[] Growth = new float[1]; public float[] Hp = new float[1] { 1f }; public float[] Attack = new float[1] { 1f }; public float[] SwingSpeed = new float[1] { 1f }; public float[] Speed = new float[1] { 1f }; public float[] Drops = new float[1] { 1f }; public float GrowthAt(int stars) { return At(Growth, stars); } public float HpAt(int stars) { return At(Hp, stars); } public float AttackAt(int stars) { return At(Attack, stars); } public float SwingSpeedAt(int stars) { return At(SwingSpeed, stars); } public float SpeedAt(int stars) { return At(Speed, stars); } public float DropsAt(int stars) { return At(Drops, stars); } private static float At(float[] line, int stars) { if (line == null || line.Length == 0) { return 1f; } int num = Mathf.Clamp(stars, 0, line.Length - 1); return line[num]; } public StarPower Clone() { return new StarPower { Growth = (float[])Growth.Clone(), Hp = (float[])Hp.Clone(), Attack = (float[])Attack.Clone(), SwingSpeed = (float[])SwingSpeed.Clone(), Speed = (float[])Speed.Clone(), Drops = (float[])Drops.Clone() }; } } internal static class YamlRead { public static YamlNode? Child(YamlMappingNode map, string key) { foreach (KeyValuePair<YamlNode, YamlNode> child in map.Children) { if (child.Key is YamlScalarNode yamlScalarNode && yamlScalarNode.Value == key) { return child.Value; } } return null; } public static YamlMappingNode? Map(YamlNode? node, List<string> errors, string what) { if (node is YamlMappingNode result) { return result; } AddError(errors, node, what + " should be a block of keyed values"); return null; } public static float[]? Floats(YamlNode? node, List<string> errors, string what) { if (!(node is YamlSequenceNode yamlSequenceNode)) { AddError(errors, node, what + " should be a list like [1, 2, 3]"); return null; } List<float> list = new List<float>(); foreach (YamlNode child in yamlSequenceNode.Children) { if (TryFloat(child, out var value)) { list.Add(value); } else { AddError(errors, child, what + " has a value that is not a number"); } } if (list.Count <= 0) { return null; } return list.ToArray(); } public static bool TryFloat(YamlNode? node, out float value) { value = 0f; if (node is YamlScalarNode yamlScalarNode) { return float.TryParse(yamlScalarNode.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out value); } return false; } public static bool Bool(YamlMappingNode map, string key, bool fallback, List<string> errors) { YamlNode yamlNode = Child(map, key); if (yamlNode == null) { return fallback; } if (yamlNode is YamlScalarNode yamlScalarNode && bool.TryParse(yamlScalarNode.Value, out var result)) { return result; } AddError(errors, yamlNode, "'" + key + "' should be true or false"); return fallback; } public static int Int(YamlMappingNode map, string key, int fallback, List<string> errors) { YamlNode yamlNode = Child(map, key); if (yamlNode == null) { return fallback; } if (TryFloat(yamlNode, out var value)) { return (int)value; } AddError(errors, yamlNode, "'" + key + "' should be a whole number"); return fallback; } public static string? Scalar(YamlMappingNode map, string key) { if (!(Child(map, key) is YamlScalarNode yamlSc