namespace Aeshnidae.Enlightenment; /// /// Allegiance passup received after enlightenment, scaled by the receiver's level. /// /// The point is to stop an enlightened character speeding through the early levels /// on their vassals' experience. Vassals generate exactly what they always did, and /// ACE's loyalty and leadership arithmetic (AllegianceManager.DoPassXP) is untouched; /// what changes is how much of the result an enlightened patron actually receives /// while low. Every receiver up the tree is scaled by their own level, so a /// grandpatron at 200 is unaffected by a patron at 10. /// /// Player.AddAllegianceXP is where a patron takes delivery: DoPassXP adds the passup /// to AllegianceXPCached and calls it at once for an online patron, and the login /// handler calls it for what accumulated while they were away. Scaling the cached /// amount there, before the grant, keeps GrantXP, AllegianceXPReceived and the /// cache in agreement. The login line "you gain N xp" is printed before this runs /// and shows the unscaled figure, so a second line says what was kept and why. /// [HarmonyPatch] public static class Passup { /// /// Fraction of passed-up XP a character at this level receives, per the settings' /// bands; 1.0 for the unenlightened or with the feature off. /// public static double FactorFor(Player player) { if (player is null || !Mod.Settings.Enabled || !Mod.Settings.ScalePassupAfterEnlightenment) return 1.0; if (player.Enlightenment <= 0) return 1.0; var bands = Mod.Settings.PassupAfterEnlightenment; if (bands is null || bands.Count == 0) return 1.0; var level = player.Level ?? 1; var percent = 0; // below the first band: nothing foreach (var band in bands.OrderBy(b => b.FromLevel)) if (level >= band.FromLevel) percent = band.Percent; return Math.Clamp(percent, 0, 100) / 100.0; } /// The table as a player would read it, for /enlighten and the Codex. public static string Describe() { var bands = Mod.Settings.PassupAfterEnlightenment.OrderBy(b => b.FromLevel).ToList(); var parts = new List(); for (var i = 0; i < bands.Count; i++) { var to = i + 1 < bands.Count ? $"-{bands[i + 1].FromLevel - 1}" : "+"; parts.Add($"{bands[i].FromLevel}{to}: {bands[i].Percent}%"); } return string.Join(", ", parts); } private static readonly System.Collections.Concurrent.ConcurrentDictionary _lastTold = new(); private static readonly TimeSpan TellEvery = TimeSpan.FromMinutes(10); [HarmonyPrefix] [HarmonyPatch(typeof(Player), nameof(Player.AddAllegianceXP))] public static void PreAddAllegianceXP(Player __instance) { try { var cached = __instance.AllegianceXPCached; if (cached == 0) return; var factor = FactorFor(__instance); if (factor >= 1.0) return; var kept = (ulong)Math.Round(cached * factor); __instance.AllegianceXPCached = kept; // An online patron takes delivery on every vassal kill; say it once in a // while, not every time. The login delivery is the one that matters. var now = DateTime.UtcNow; if (_lastTold.TryGetValue(__instance.Guid.Full, out var last) && now - last < TellEvery) return; _lastTold[__instance.Guid.Full] = now; __instance.Session?.Network.EnqueueSend(new GameMessageSystemChat( kept == 0 ? $"Enlightened and level {__instance.Level ?? 1}: your vassals' experience does not reach you yet ({Describe()})." : $"Enlightened and level {__instance.Level ?? 1}: {factor * 100:N0}% of your vassals' experience reaches you - {kept:N0} of {cached:N0}.", ChatMessageType.Broadcast)); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] passup scaling failed for {__instance?.Name}: {ex.Message}", ModManager.LogLevel.Warn); } } }