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);
}
}
}