namespace Aeshnidae.ResonanceAuras;
///
/// Where the ranks become real.
///
/// Seven of the auras are a rank count added to a Player property that ACE's own
/// formulas already read - the same properties the retail augmentation gems and
/// luminance auras write. The patch is a postfix on the typed getter, so the
/// stored property is never touched: the gem device and the luminance vendor check
/// their caps through GetProperty (raw) and Aeshnidae.Enlightenment checks its
/// prerequisites the same way, so our ranks neither block a retail purchase nor
/// count towards enlightenment. Enlightenment's reset writes 0 through the typed
/// setter, which removes the stored property and leaves ours alone.
///
/// The two with no retail counterpart - components and ammunition - are patched at
/// the moment ACE spends the thing.
///
/// Every patch guards itself: these sit on hot paths (a vital tick, every spell)
/// and ACE catches nothing on our behalf.
///
[HarmonyPatch]
public static class Patches
{
// ------------------------------------------------------------------ cache
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.PlayerEnterWorld))]
public static void PostPlayerEnterWorld(Player __instance)
{
try
{
Auras.LoadInto(__instance);
Auras.TellClientOnLogin(__instance);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] failed to load auras on login: {ex}", ModManager.LogLevel.Error);
}
}
// ------------------------------------------------- the property-backed seven
private static int Ranks(Player player, string key)
{
try
{
return Auras.RanksOf(player, key);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] {key} lookup failed: {ex.Message}", ModManager.LogLevel.Error);
return 0;
}
}
/// Lasting Enchantments: EnchantmentManager multiplies duration by 1 + 0.2 x this.
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.AugmentationIncreasedSpellDuration), MethodType.Getter)]
public static void PostSpellDuration(Player __instance, ref int __result) =>
__result += Ranks(__instance, Aura.SpellDuration);
/// Frugal Mana: Player_Tick scales item mana burn by GetNegativeRatingMod(5 x this).
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.LumAugItemManaUsage), MethodType.Getter)]
public static void PostItemManaUsage(Player __instance, ref int __result) =>
__result += Ranks(__instance, Aura.ItemMana);
/// Charmed Hands: RecipeManager adds 0.05 x this to the imbue success chance.
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.AugmentationBonusImbueChance), MethodType.Getter)]
public static void PostImbueChance(Player __instance, ref int __result) =>
__result += Ranks(__instance, Aura.Imbue);
/// Generous Stones: ManaStone scales the ration by GetPositiveRatingMod(5 x this).
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.LumAugItemManaGain), MethodType.Getter)]
public static void PostItemManaGain(Player __instance, ref int __result) =>
__result += Ranks(__instance, Aura.ManaStone);
/// Keen Salvager: Player_Crafting multiplies value by 1 + 0.25 x this and units through CalcNumUnits.
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.AugmentationBonusSalvage), MethodType.Getter)]
public static void PostSalvage(Player __instance, ref int __result) =>
__result += Ranks(__instance, Aura.Salvage);
/// Strong Back: capacity is 150 x strength + 30 x strength x this (see PostEncumbranceCapacity for the cap).
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.AugmentationIncreasedCarryingCapacity), MethodType.Getter)]
public static void PostCarryingCapacity(Player __instance, ref int __result) =>
__result += Ranks(__instance, Aura.Carry);
/// Deep Rest: Creature_Vitals adds this to the regen multiplier while lying down.
[HarmonyPostfix]
[HarmonyPatch(typeof(Player), nameof(Player.AugmentationFasterRegen), MethodType.Getter)]
public static void PostFasterRegen(Player __instance, ref int __result) =>
__result += Ranks(__instance, Aura.Regen);
///
/// The burden formula used for movement, jumping and the burden penalty caps the
/// augmentation bonus at 150 (five gems' worth), which would make Strong Back
/// worthless to anyone who owns the five retail gems. The pack-space check in
/// Player.GetEncumbranceCapacity has no such cap. Lift it by our maximum, so the
/// two agree: only a count above five can get here, and only our aura makes one.
///
[HarmonyPostfix]
[HarmonyPatch(typeof(ACE.Server.Physics.Common.EncumbranceSystem), nameof(ACE.Server.Physics.Common.EncumbranceSystem.EncumbranceCapacity))]
public static void PostEncumbranceCapacity(int strength, int numAugs, ref int __result)
{
try
{
if (strength <= 0 || numAugs <= 5)
return;
var ours = Aura.ByKey(Aura.Carry)?.MaxRanks ?? 0;
var bonusBurden = Math.Min(30 * numAugs, 150 + 30 * ours);
__result = 150 * strength + strength * bonusBurden;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] capacity cap failed: {ex.Message}", ModManager.LogLevel.Error);
}
}
// ------------------------------------------------------- components and ammo
///
/// Thrifty Caster. Spell.TryBurnComponents has already rolled which components
/// burn; each one gets a second roll to survive. Runs before Player.TryBurnComponents
/// takes them from the pack and names them in chat, so what the player is told
/// burned is what burned.
///
[HarmonyPostfix]
[HarmonyPatch(typeof(Spell), nameof(Spell.TryBurnComponents))]
public static void PostTryBurnComponents(Player player, ref List __result)
{
try
{
if (__result is null || __result.Count == 0)
return;
var chance = Ranks(player, Aura.Components) * Mod.Settings.ComponentSaveChancePerRank;
if (chance <= 0)
return;
for (var i = __result.Count - 1; i >= 0; i--)
if (ThreadSafeRandom.Next(0.0f, 1.0f) < chance)
__result.RemoveAt(i);
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] component save failed: {ex.Message}", ModManager.LogLevel.Error);
}
}
///
/// Steady Quiver. Player.UpdateAmmoAfterLaunch hides the held ammo and then
/// spends one; on a save it is only hidden, and the reload animation that
/// follows parents it back into the hand as it always does. The last one in the
/// stack is never saved: Player.LaunchProjectile has already decided from the
/// stack size that the player is out of ammunition, and a saved arrow the
/// client believes is gone would be worse than a spent one.
///
[HarmonyPrefix]
[HarmonyPatch(typeof(Player), nameof(Player.UpdateAmmoAfterLaunch))]
public static bool PreUpdateAmmoAfterLaunch(Player __instance, WorldObject ammo)
{
try
{
if (ammo is null || ammo.UnlimitedUse || ammo.StackSize is null || ammo.StackSize <= 1)
return true;
var chance = Ranks(__instance, Aura.Ammo) * Mod.Settings.AmmoSaveChancePerRank;
if (chance <= 0 || ThreadSafeRandom.Next(0.0f, 1.0f) >= chance)
return true;
// Saved: do the part of the original that is not the spending.
__instance.EnqueueBroadcast(new GameMessagePickupEvent(ammo));
return false;
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] ammo save failed: {ex.Message}", ModManager.LogLevel.Error);
return true;
}
}
}