namespace Aeshnidae.QuestBonus; public static class QuestBonusExtensions { /// /// Where a character's accumulated quest points are stored. /// /// This is a "fake" PropertyFloat: ACE's own PropertyFloat enum currently stops /// at 171, so 20002 cannot collide, and the shard DB persists unknown property /// ids just fine. 20002 is deliberately the same id Aquafir's ACE.Shared uses /// for FakeFloat.QuestBonus, so the data stays interchangeable with his mods. /// /// Changing this orphans every character's stored total (they resync on next /// login, but the old rows linger), so treat it as fixed. /// public const PropertyFloat QuestBonusProperty = (PropertyFloat)20002; /// Replaces ACE.Shared's QuestExtensions.HasSolves. public static bool HasSolves(this CharacterPropertiesQuestRegistry quest) => quest.NumTimesCompleted != 0; /// /// Points a quest is worth: an exact entry in QuestWeights, else the first matching /// QuestWeightPatterns entry, else Settings.DefaultPoints. The same answer serves /// the live increments and the login resync, so the two cannot disagree. /// public static double WeightOf(string questFormat) { var name = QuestManager.GetQuestName(questFormat); if (Mod.Settings.QuestWeights.TryGetValue(name, out var weight)) return weight; return Mod.Settings.PatternWeightOf(name) ?? Mod.Settings.DefaultPoints; } public static double GetQuestPoints(this Player player) => player.GetProperty(QuestBonusProperty) ?? 0; public static void SetQuestPoints(this Player player, double points) => player.SetProperty(QuestBonusProperty, Math.Max(0, points)); public static void AddQuestPoints(this Player player, double delta) => player.SetQuestPoints(player.GetQuestPoints() + delta); /// Sums the weights of every quest this character has actually solved. public static double CalculateQuestPoints(this Player player) { double total = 0; foreach (var quest in player.QuestManager.GetQuests()) { if (quest.HasSolves()) total += WeightOf(quest.QuestName); } return total; } /// /// Recomputes from scratch and stores the result. This is the authoritative /// path; the incremental Add/Subtract patches are just to keep it live. /// public static void ResyncQuestPoints(this Player player) => player.SetQuestPoints(player.CalculateQuestPoints()); /// /// The XP multiplier: 1 + points * BonusConversion, capped by MaxMultiplier. /// Always >= 1. /// public static double QuestBonusMultiplier(this Player player) { var multiplier = 1 + player.GetQuestPoints() * Mod.Settings.BonusConversion; if (Mod.Settings.MaxMultiplier > 0) multiplier = Math.Min(multiplier, Mod.Settings.MaxMultiplier); return Math.Max(1, multiplier); } /// "+4.20%" - the bonus on its own, which reads better than a raw multiplier. public static string QuestBonusText(this Player player) => $"+{(player.QuestBonusMultiplier() - 1) * 100:0.##}%"; }