Decompiled source of EliteCreaturesReborn v3.0.0

plugins\EliteCreaturesReborn.dll

Decompiled a day ago
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