Decompiled source of AcolyteSacrificeHeal v0.2.1

plugins/AcolyteSacrificeHeal.dll

Decompiled 2 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using EntityStates.Acolyte;
using EntityStates.Acolyte.Backpack;
using EntityStates.Acolyte.Weapon;
using EntityStates.LilReaper;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using RoR2;
using RoR2.Orbs;
using RoR2.UI;
using UnityEngine;
using UnityEngine.Networking;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: AssemblyVersion("0.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[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 AcolyteSacrificeHeal
{
	internal sealed class AnimaBudget
	{
		private readonly HashSet<object> creditedVictims = new HashSet<object>();

		internal double GrantedSeconds { get; private set; }

		internal float Credit(object victim, float secondsPerKill, float maxSeconds)
		{
			if (victim == null || !Finite(secondsPerKill) || !Finite(maxSeconds) || secondsPerKill <= 0f || maxSeconds <= 0f)
			{
				return 0f;
			}
			double num = Math.Max(0.0, (double)maxSeconds - GrantedSeconds);
			if (num <= 0.0 || !creditedVictims.Add(victim))
			{
				return 0f;
			}
			double num2 = Math.Min(secondsPerKill, num);
			GrantedSeconds += num2;
			return (float)num2;
		}

		private static bool Finite(float value)
		{
			if (!float.IsNaN(value))
			{
				return !float.IsInfinity(value);
			}
			return false;
		}
	}
	internal sealed class AnimaExtension : IDisposable
	{
		private readonly ConfigEntry<bool> enabled;

		private readonly ConfigEntry<float> secondsPerKill;

		private readonly ConfigEntry<float> maxSeconds;

		private readonly ConfigEntry<bool> logExtensions;

		private readonly ManualLogSource logger;

		private readonly FieldInfo stopwatch;

		private readonly ConditionalWeakTable<ReaperActivate, AnimaBudget> budgets = new ConditionalWeakTable<ReaperActivate, AnimaBudget>();

		internal bool IsEnabled => enabled.Value;

		internal float SecondsPerKill => secondsPerKill.Value;

		internal float MaxBonusSeconds => maxSeconds.Value;

		internal AnimaExtension(ConfigFile config, ManualLogSource logger)
		{
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0068: Expected O, but got Unknown
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Expected O, but got Unknown
			this.logger = logger;
			enabled = config.Bind<bool>("Anima", "Enabled", true, "Enemy kills credited to the player in Anima Harvester form extend that transformation. Does not affect Anima Echo.");
			secondsPerKill = config.Bind<float>("Anima", "SecondsPerKill", 0.2f, new ConfigDescription("Seconds added per eligible enemy kill. Extracted souls, allied kills, and minion kills do not count.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>()));
			maxSeconds = config.Bind<float>("Anima", "MaxBonusSeconds", 3f, new ConfigDescription("Total bonus seconds available per transformation. Spending bonus time does not refill this allowance. Set to zero to disable.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 3f), Array.Empty<object>()));
			logExtensions = config.Bind<bool>("Debug", "LogAnimaExtensions", false, "Log bonus time per eligible kill and the total earned this transformation.");
			stopwatch = typeof(BaseBackpackState).GetField("stopwatch", BindingFlags.Instance | BindingFlags.NonPublic);
			if (stopwatch == null || stopwatch.FieldType != typeof(float))
			{
				throw new MissingFieldException("Anima Conviction drain timer changed; extension cannot initialize.");
			}
			GlobalEventManager.onCharacterDeathServer += OnDeath;
			logger.LogInfo((object)("Anima kill extension loaded: " + secondsPerKill.Value + "s per kill, maximum " + maxSeconds.Value + "s per transformation; enabled=" + enabled.Value));
		}

		private void OnDeath(DamageReport report)
		{
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008a: Unknown result type (might be due to invalid IL or missing references)
			if (!NetworkServer.active || !enabled.Value || report == null)
			{
				return;
			}
			CharacterBody attackerBody = report.attackerBody;
			HealthComponent victim = report.victim;
			if (!Object.op_Implicit((Object)(object)attackerBody) || !attackerBody.isPlayerControlled || !Object.op_Implicit((Object)(object)victim) || !Object.op_Implicit((Object)(object)report.victimBody) || !Object.op_Implicit((Object)(object)attackerBody.healthComponent) || !attackerBody.healthComponent.alive || !Object.op_Implicit((Object)(object)attackerBody.teamComponent))
			{
				return;
			}
			TeamMask enemyTeams = TeamMask.GetEnemyTeams(attackerBody.teamComponent.teamIndex);
			if (!((TeamMask)(ref enemyTeams)).HasTeam(report.victimTeamIndex) || Object.op_Implicit((Object)(object)((Component)victim).GetComponent<ExtractedSoulController>()) || Object.op_Implicit((Object)(object)((Component)report.victimBody).GetComponent<ExtractedSoulController>()))
			{
				return;
			}
			AcolyteFormController component = ((Component)attackerBody).GetComponent<AcolyteFormController>();
			if (!Object.op_Implicit((Object)(object)component) || !component.isReaperForm || component.souls <= 0)
			{
				return;
			}
			EntityStateMachine val = EntityStateMachine.FindByCustomName(((Component)attackerBody).gameObject, "Backpack");
			ReaperActivate val2 = (ReaperActivate)(Object.op_Implicit((Object)(object)val) ? /*isinst with value type is only supported in some contexts*/: null);
			if (val2 == null)
			{
				return;
			}
			EntityStateMachine[] components = ((Component)attackerBody).GetComponents<EntityStateMachine>();
			foreach (EntityStateMachine val3 in components)
			{
				if (val3.state is EnterBanish || val3.state is Return)
				{
					return;
				}
			}
			float num = (float)stopwatch.GetValue(val2);
			if (float.IsNaN(num) || float.IsInfinity(num))
			{
				return;
			}
			AnimaBudget value = budgets.GetValue(val2, (ReaperActivate _) => new AnimaBudget());
			float num2 = value.Credit(victim, secondsPerKill.Value, maxSeconds.Value);
			if (!(num2 <= 0f))
			{
				stopwatch.SetValue(val2, num - num2);
				if (logExtensions.Value)
				{
					logger.LogInfo((object)("Anima kill: +" + num2.ToString("0.###") + "s; total " + value.GrantedSeconds.ToString("0.###") + "/" + maxSeconds.Value + "s this transformation."));
				}
			}
		}

		public void Dispose()
		{
			GlobalEventManager.onCharacterDeathServer -= OnDeath;
		}
	}
	[BepInPlugin("local.acolyte.sacrificeheal", "Acolyte Sacrifice Heal", "0.2.1")]
	public sealed class Plugin : BaseUnityPlugin
	{
		private sealed class HitContext
		{
			internal GenericDamageOrb Orb;

			internal HealthComponent Victim;

			internal HitContext Previous;

			internal bool Confirmed;
		}

		public const string PluginGuid = "local.acolyte.sacrificeheal";

		internal static Plugin Instance;

		private ConfigEntry<float> recoveryPercent;

		private ConfigEntry<bool> diagnosticLogging;

		private Harmony harmony;

		private AnimaExtension animaExtension;

		private TooltipPresentation presentation;

		[ThreadStatic]
		private static HitContext currentHit;

		private void Awake()
		{
			//IL_0035: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Expected O, but got Unknown
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b3: Expected O, but got Unknown
			//IL_00ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_00df: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f5: Unknown result type (might be due to invalid IL or missing references)
			//IL_0100: Expected O, but got Unknown
			//IL_0100: Expected O, but got Unknown
			//IL_0100: Expected O, but got Unknown
			Instance = this;
			recoveryPercent = ((BaseUnityPlugin)this).Config.Bind<float>("Healing", "MaxHealthPercent", 5f, new ConfigDescription("Maximum percent of max HP transferred from existing recoverable health per successful normal-form Sacrifice hit. No reserve means no bonus heal.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 100f), Array.Empty<object>()));
			diagnosticLogging = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "LogHealing", false, "Log each successful reserve-to-HP transfer for testing.");
			try
			{
				MethodInfo methodInfo = typeof(Sacrifice).GetNestedType("SacrificeOrb", BindingFlags.Public | BindingFlags.NonPublic)?.GetMethod("OnArrival", BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				if (methodInfo == null)
				{
					throw new MissingMethodException("SacrificeOrb.OnArrival: game version unsupported.");
				}
				harmony = new Harmony("local.acolyte.sacrificeheal");
				harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(typeof(Plugin), "BeginHit", (Type[])null), new HarmonyMethod(typeof(Plugin), "CompleteHit", (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "EndHit", (Type[])null), (HarmonyMethod)null);
				GlobalEventManager.onServerDamageDealt += OnDamageDealt;
				try
				{
					animaExtension = new AnimaExtension(((BaseUnityPlugin)this).Config, ((BaseUnityPlugin)this).Logger);
				}
				catch (Exception ex)
				{
					((BaseUnityPlugin)this).Logger.LogError((object)("Anima extension disabled; R healing remains active: " + ex));
				}
				try
				{
					presentation = new TooltipPresentation(recoveryPercent, animaExtension, ((BaseUnityPlugin)this).Logger);
				}
				catch (Exception ex2)
				{
					((BaseUnityPlugin)this).Logger.LogError((object)("Ability descriptions unavailable; gameplay remains active: " + ex2));
				}
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Acolyte Sacrifice Heal 0.2.1 loaded. Normal-form R recovers up to " + recoveryPercent.Value + "% max HP from reserve."));
			}
			catch (Exception ex3)
			{
				Harmony obj = harmony;
				if (obj != null)
				{
					obj.UnpatchSelf();
				}
				((BaseUnityPlugin)this).Logger.LogError((object)("Acolyte Sacrifice Heal could not initialize: " + ex3));
				((Behaviour)this).enabled = false;
			}
		}

		private void OnDestroy()
		{
			presentation?.Dispose();
			animaExtension?.Dispose();
			GlobalEventManager.onServerDamageDealt -= OnDamageDealt;
			Harmony obj = harmony;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
		}

		private static void BeginHit(GenericDamageOrb __instance, out HitContext __state)
		{
			__state = new HitContext
			{
				Orb = __instance,
				Previous = currentHit,
				Victim = (Object.op_Implicit((Object)(object)((Orb)__instance).target) ? ((Orb)__instance).target.healthComponent : null)
			};
			currentHit = __state;
		}

		private static void OnDamageDealt(DamageReport report)
		{
			//IL_0087: Unknown result type (might be due to invalid IL or missing references)
			//IL_0099: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
			HitContext hitContext = currentHit;
			if (NetworkServer.active && hitContext != null && !hitContext.Confirmed && report?.damageInfo != null)
			{
				DamageInfo damageInfo = report.damageInfo;
				if (!((Object)(object)report.victim != (Object)(object)hitContext.Victim) && !((Object)(object)report.attacker != (Object)(object)hitContext.Orb.attacker) && !damageInfo.rejected && !(report.damageDealt <= 0f) && !Object.op_Implicit((Object)(object)damageInfo.inflictor) && ((ProcChainMask)(ref damageInfo.procChainMask)).Equals(hitContext.Orb.procChainMask) && damageInfo.damageType.damageSource == hitContext.Orb.damageType.damageSource)
				{
					hitContext.Confirmed = true;
				}
			}
		}

		private static void CompleteHit(HitContext __state)
		{
			//IL_009d: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: Invalid comparison between Unknown and I4
			//IL_00b6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Invalid comparison between Unknown and I4
			if (!NetworkServer.active || !__state.Confirmed || !Object.op_Implicit((Object)(object)Instance) || !Object.op_Implicit((Object)(object)__state.Orb.attacker))
			{
				return;
			}
			CharacterBody component = __state.Orb.attacker.GetComponent<CharacterBody>();
			AcolyteFormController component2 = __state.Orb.attacker.GetComponent<AcolyteFormController>();
			if (!Object.op_Implicit((Object)(object)component) || !Object.op_Implicit((Object)(object)component2) || component2.inTransformation || !Object.op_Implicit((Object)(object)component.healthComponent) || !component.healthComponent.alive)
			{
				return;
			}
			HealthComponent healthComponent = component.healthComponent;
			bool eclipsePenalty = Object.op_Implicit((Object)(object)component.teamComponent) && (int)component.teamComponent.teamIndex == 1 && Object.op_Implicit((Object)(object)Run.instance) && (int)Run.instance.selectedDifficulty >= 7;
			float num = RecoveryRules.Amount(healthComponent.fullHealth, healthComponent.health, healthComponent.regainableHealth, Instance.recoveryPercent.Value, component.HasBuff(Buffs.HealingDisabled), eclipsePenalty);
			if (!(num <= 0f))
			{
				float num2 = healthComponent.SpendRegainableHealthServer(num);
				healthComponent.Networkhealth = Math.Min(healthComponent.fullHealth, healthComponent.health + num2);
				if (Instance.diagnosticLogging.Value)
				{
					((BaseUnityPlugin)Instance).Logger.LogInfo((object)("Sacrifice restored " + num2.ToString("0.##") + " HP; reserve remaining " + healthComponent.regainableHealth.ToString("0.##")));
				}
			}
		}

		private static Exception EndHit(Exception __exception, HitContext __state)
		{
			if (__state != null)
			{
				currentHit = __state.Previous;
			}
			return __exception;
		}
	}
	internal static class RecoveryRules
	{
		internal static float Amount(float fullHealth, float health, float reserve, float percent, bool healingBlocked, bool eclipsePenalty)
		{
			if (healingBlocked || !Finite(fullHealth) || !Finite(health) || !Finite(reserve) || !Finite(percent) || fullHealth <= 0f || health <= 0f || reserve <= 0f)
			{
				return 0f;
			}
			float num = fullHealth * Math.Max(0f, Math.Min(100f, percent)) / 100f;
			if (eclipsePenalty)
			{
				num *= 0.5f;
			}
			return Math.Max(0f, Math.Min(num, Math.Min(reserve, fullHealth - health)));
		}

		private static bool Finite(float value)
		{
			if (!float.IsNaN(value))
			{
				return !float.IsInfinity(value);
			}
			return false;
		}
	}
	internal sealed class TooltipPresentation : IDisposable
	{
		private static TooltipPresentation active;

		private readonly ConfigEntry<float> heal;

		private readonly AnimaExtension anima;

		private readonly ManualLogSource logger;

		private readonly Harmony patches = new Harmony("local.acolyte.sacrificeheal.presentation");

		private Type rowType;

		private FieldInfo bodyField;

		private FieldInfo colorField;

		private Sprite icon;

		private Texture2D texture;

		private bool rowErrorLogged;

		internal TooltipPresentation(ConfigEntry<float> heal, AnimaExtension anima, ManualLogSource logger)
		{
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Expected O, but got Unknown
			//IL_01b4: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b9: Unknown result type (might be due to invalid IL or missing references)
			//IL_01c8: Expected O, but got Unknown
			//IL_01e1: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ee: Expected O, but got Unknown
			this.heal = heal;
			this.anima = anima;
			this.logger = logger;
			try
			{
				LoadIcon();
				Type typeFromHandle = typeof(CharacterSelectController);
				Type nestedType = typeFromHandle.GetNestedType("BodyInfo", BindingFlags.NonPublic);
				rowType = typeFromHandle.GetNestedType("StripDisplayData", BindingFlags.NonPublic);
				bodyField = nestedType?.GetField("bodyPrefabBodyComponent");
				colorField = nestedType?.GetField("bodyColor");
				if (rowType == null || bodyField == null || colorField == null)
				{
					throw new MissingMemberException("Character selection display format changed.");
				}
				string[] array = new string[7] { "enabled", "primaryColor", "icon", "titleString", "descriptionString", "keywordString", "actionName" };
				foreach (string text in array)
				{
					if (rowType.GetField(text) == null)
					{
						throw new MissingFieldException(rowType.FullName, text);
					}
				}
				MethodInfo methodInfo = AccessTools.Method(typeof(Language), "GetLocalizedStringByToken", new Type[1] { typeof(string) }, (Type[])null);
				MethodInfo methodInfo2 = AccessTools.Method(typeFromHandle, "BuildSkillStripDisplayData", (Type[])null, (Type[])null);
				if (methodInfo == null || methodInfo2 == null)
				{
					throw new MissingMethodException("Tooltip method changed.");
				}
				active = this;
				patches.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(TooltipPresentation), "Describe", (Type[])null)
				{
					priority = 0
				}, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				patches.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(TooltipPresentation), "AddCard", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				logger.LogInfo((object)"Ability descriptions and Lingering Anima information card loaded.");
			}
			catch
			{
				Dispose();
				throw;
			}
		}

		private void LoadIcon()
		{
			//IL_0030: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Expected O, but got Unknown
			//IL_00aa: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b9: Unknown result type (might be due to invalid IL or missing references)
			using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("AcolyteSacrificeHeal.Anima.jpg");
			using MemoryStream memoryStream = new MemoryStream();
			if (stream == null)
			{
				throw new InvalidOperationException("Missing Anima icon.");
			}
			stream.CopyTo(memoryStream);
			texture = new Texture2D(2, 2, (TextureFormat)4, false);
			((Object)texture).name = "Acolyte_LingeringAnima";
			((Texture)texture).wrapMode = (TextureWrapMode)1;
			((Texture)texture).filterMode = (FilterMode)1;
			if (!ImageConversion.LoadImage(texture, memoryStream.ToArray(), true))
			{
				throw new InvalidOperationException("Could not decode Anima icon.");
			}
			icon = Sprite.Create(texture, new Rect(0f, 0f, (float)((Texture)texture).width, (float)((Texture)texture).height), new Vector2(0.5f, 0.5f), 100f);
			((Object)icon).name = ((Object)texture).name;
		}

		private static void Describe(string __0, ref string __result)
		{
			TooltipPresentation tooltipPresentation = active;
			if (tooltipPresentation != null)
			{
				__result = TooltipText.Describe(__0, __result, tooltipPresentation.heal.Value, tooltipPresentation.anima != null && tooltipPresentation.anima.IsEnabled, tooltipPresentation.anima?.SecondsPerKill ?? 0f, tooltipPresentation.anima?.MaxBonusSeconds ?? 0f);
			}
		}

		private static void AddCard(object[] __args)
		{
			TooltipPresentation tooltipPresentation = active;
			if (tooltipPresentation == null || tooltipPresentation.anima == null)
			{
				return;
			}
			try
			{
				string text = TooltipText.Anima(tooltipPresentation.anima.IsEnabled, tooltipPresentation.anima.SecondsPerKill, tooltipPresentation.anima.MaxBonusSeconds);
				if (text.Length == 0 || __args == null || __args.Length != 3)
				{
					return;
				}
				object? value = tooltipPresentation.bodyField.GetValue(__args[1]);
				CharacterBody val = (CharacterBody)((value is CharacterBody) ? value : null);
				IList list = __args[2] as IList;
				if (!Object.op_Implicit((Object)(object)val) || val.baseNameToken != "ACOLYTE_BODY_NAME" || list == null)
				{
					return;
				}
				foreach (object item in list)
				{
					if ((string)tooltipPresentation.rowType.GetField("titleString").GetValue(item) == "Lingering Anima")
					{
						return;
					}
				}
				object obj = Activator.CreateInstance(tooltipPresentation.rowType);
				tooltipPresentation.Set(obj, "enabled", true);
				tooltipPresentation.Set(obj, "primaryColor", tooltipPresentation.colorField.GetValue(__args[1]));
				tooltipPresentation.Set(obj, "icon", tooltipPresentation.icon);
				tooltipPresentation.Set(obj, "titleString", "Lingering Anima");
				tooltipPresentation.Set(obj, "descriptionString", "<style=cIsUtility>Passive.</style> " + text);
				tooltipPresentation.Set(obj, "keywordString", "<style=cKeywordName>Lingering Anima</style><style=cSub>Only enemy kills credited to you count. Extracted souls and minion kills do not count. The bonus resets each transformation and cannot refill during it. Does not affect Anima Echo.</style>");
				tooltipPresentation.Set(obj, "actionName", "");
				list.Add(obj);
			}
			catch (Exception ex)
			{
				if (!tooltipPresentation.rowErrorLogged)
				{
					tooltipPresentation.rowErrorLogged = true;
					tooltipPresentation.logger.LogWarning((object)("Could not display Anima information card: " + ex));
				}
			}
		}

		private void Set(object row, string name, object value)
		{
			rowType.GetField(name).SetValue(row, value);
		}

		public void Dispose()
		{
			patches.UnpatchSelf();
			if (active == this)
			{
				active = null;
			}
			if (Object.op_Implicit((Object)(object)icon))
			{
				Object.Destroy((Object)(object)icon);
			}
			if (Object.op_Implicit((Object)(object)texture))
			{
				Object.Destroy((Object)(object)texture);
			}
		}
	}
	internal static class TooltipText
	{
		internal const string AnimaTitle = "Lingering Anima";

		internal const string AnimaDetails = "Only enemy kills credited to you count. Extracted souls and minion kills do not count. The bonus resets each transformation and cannot refill during it. Does not affect Anima Echo.";

		private static string Number(float value)
		{
			return value.ToString("0.###", CultureInfo.InvariantCulture);
		}

		private static bool Positive(float value)
		{
			if (value > 0f && !float.IsNaN(value))
			{
				return !float.IsInfinity(value);
			}
			return false;
		}

		internal static string Healing(float percent)
		{
			if (!Positive(percent))
			{
				return "";
			}
			return "Successful hits also restore up to <style=cIsHealing>" + Number(percent) + "% of maximum health</style> from your <style=cIsHealing>recoverable health</style>. Requires an existing reserve.";
		}

		internal static string Anima(bool enabled, float perKill, float cap)
		{
			if (!enabled || !Positive(perKill) || !Positive(cap))
			{
				return "";
			}
			return "While in <style=cIsDamage>Anima Harvester</style> form, your enemy kills extend the transformation by <style=cIsUtility>" + Number(perKill) + "s</style>, up to <style=cIsUtility>" + Number(cap) + " extra seconds per transformation</style>.";
		}

		internal static string Append(string original, string extra)
		{
			if (string.IsNullOrEmpty(extra) || (original != null && original.Contains(extra)))
			{
				return original;
			}
			if (!string.IsNullOrEmpty(original))
			{
				return original + "\n\n" + extra;
			}
			return extra;
		}

		internal static string Describe(string token, string original, float percent, bool enabled, float perKill, float cap)
		{
			switch (token)
			{
			case "ACOLYTE_SPECIAL_DESCRIPTION":
				return Append(original, Healing(percent));
			case "ACOLYTE_SPECIAL_TRANSFORM_DESCRIPTION":
				return Append(original, Anima(enabled, perKill, cap));
			case "KEYWORD_LITANY":
			{
				string text = Anima(enabled, perKill, cap);
				if (text.Length != 0)
				{
					return Append(original, "<style=cKeywordName>Lingering Anima</style><style=cSub>" + text + "\nOnly enemy kills credited to you count. Extracted souls and minion kills do not count. The bonus resets each transformation and cannot refill during it. Does not affect Anima Echo.</style>");
				}
				break;
			}
			}
			return original;
		}
	}
}