namespace Aeshnidae.AllegianceBounty; /// /// Every term of the allegiance bounty, in one file. /// /// The bounty (design decided 2026-09-20): when any member of an allegiance completes a /// listed quest, the allegiance gains a bonus entry for that quest for a day - a quest is /// worth +2%, a kill task +0.5%. The same quest completed again by anyone in the /// allegiance while its entry is active adds nothing; different quests stack; there is /// no cap unless MaxPercent says so. The bonus applies to the KILL experience every /// member receives, not to quest experience, so it cannot feed itself. Only the final /// completion of a quest counts - the stamp the last turn-in writes with the reward - /// never a chain flag or a kill task's counter. The list of quests and their flags is /// Quests.json beside this file; Quests.md is the same list for people. /// public class Settings { public const string FileName = "Settings.json"; public bool Enabled { get; set; } = true; /// /// What a completed quest is worth to the allegiance, in percent of kill experience, /// for entries in Quests.json that carry no percent of their own (the shipped file /// writes one out per entry, so a single quest can be tuned by hand; this is the /// default for a hand-added entry that leaves it out). /// public double QuestPercent { get; set; } = 2.0; /// The same for a kill task. A kill task is a fraction of an evening; a quest is the evening. public double KillTaskPercent { get; set; } = 0.5; /// How long an entry lives from the moment of completion. public double HoursActive { get; set; } = 24; /// /// A ceiling on the allegiance's raw total, in percent, before the size band is /// applied. 0 is no ceiling: an allegiance that does forty quests in a day has forty /// entries and the sum of them. /// public double MaxPercent { get; set; } = 0; /// /// The bigger the allegiance, the less each completed quest is worth (2026-09-20). /// A quest done by one of ten is a bigger deal than one of a hundred, and a huge /// allegiance would otherwise have every quest on the list active permanently - a /// standing bonus that nobody in it had to do anything for. The allegiance's raw /// total is multiplied by the band its TOTAL member count falls in (the number /// /allegiance info shows: everyone sworn under the monarch, online or not). Bands /// are read in order; the first whose UpToMembers is at or above the count wins, and /// the LAST band is the fallback for any allegiance past the table's end. An empty /// list is x1 for everyone. /// public List SizeBands { get; set; } = new() { new SizeBand { UpToMembers = 10, Multiplier = 1.0 }, new SizeBand { UpToMembers = 25, Multiplier = 0.75 }, new SizeBand { UpToMembers = 50, Multiplier = 0.5 }, new SizeBand { UpToMembers = 100, Multiplier = 0.33 }, new SizeBand { UpToMembers = 0, Multiplier = 0.25 }, // 0 = everything above the last bound }; /// /// Tell every online member when an entry is gained: " completed : the /// allegiance's bounty is now +X% for 24 hours". Nothing is said per kill - the kill /// line already shows the total that landed. /// public bool AnnounceToAllegiance { get; set; } = true; /// /// Off by design: a bonus on quest experience would pay for the quests that create /// it, and the allegiance would be farming its own multiplier. On, the bonus also /// applies to XpType.Quest. Left as a switch for an event. /// public bool ApplyToQuestExperience { get; set; } = false; /// /// A fellow's slice of a shared kill arrives as XpType.Fellowship - it is kill /// experience received, split by ACE before it lands. On, the slice is bonused by /// the receiving member's own allegiance exactly as their own kills are; off, only /// experience typed Kill is, so a fellowed member would be bonused on their own /// kills and not on their fellows', which is not how a fellowship feels from inside. /// public bool ApplyToFellowshipShares { get; set; } = true; /// The quest list, relative to the mod folder. Reloaded by /bounty-reload. public string QuestsFile { get; set; } = "Quests.json"; // ------------------------------------------------------------ the personal layer /// /// The personal layer (2026-09-20): on top of the allegiance's bounty, each character /// earns a kill-experience bonus of its own for the quest flags it stamped today - /// PersonalPercentPerStamp for every DISTINCT flag, up to PersonalMaxPercent. Any /// quest flag counts (PersonalAnyQuest): quest stamps are abundant by design and the /// per-stamp value is small, so the day's questing adds up rather than one big /// turn-in. Each flag counts ONCE per day, so re-stamping the same cheap flag is /// worth nothing - the guard against looping one stamp to the cap. /// public bool PersonalEnabled { get; set; } = true; /// Percent of kill experience per distinct quest flag stamped today. public double PersonalPercentPerStamp { get; set; } = 0.1; /// The ceiling on the personal bonus, in percent. 0 is no ceiling. public double PersonalMaxPercent { get; set; } = 20; /// /// The server-local hour the personal day resets, 0-23. 17 - five in the afternoon, /// when people get home from work - so an evening's questing counts toward that /// evening's hunting and is not cut in half by a midnight. The day is the /// reset-to-reset window: a stamp at 16:59 belongs to the day that began at 17:00 /// yesterday. /// public int PersonalResetHour { get; set; } = 17; /// True: every quest flag written counts. False: only the flags in Quests.json. public bool PersonalAnyQuest { get; set; } = true; /// /// Returns the multiplier for an allegiance of the given size from SizeBands. /// public double SizeMultiplier(int members) { if (SizeBands is null || SizeBands.Count == 0) return 1.0; foreach (var band in SizeBands) { if (band.UpToMembers > 0 && members <= band.UpToMembers) return band.Multiplier; } return SizeBands[^1].Multiplier; } /// The band a count falls in, for /bounty. public string SizeBandLabel(int members) { if (SizeBands is null || SizeBands.Count == 0) return "no bands"; for (var i = 0; i < SizeBands.Count; i++) { var band = SizeBands[i]; if (band.UpToMembers > 0 && members <= band.UpToMembers) return i == 0 ? $"up to {band.UpToMembers}" : $"{SizeBands[i - 1].UpToMembers + 1} to {band.UpToMembers}"; } var last = SizeBands.Count >= 2 ? SizeBands[^2].UpToMembers : 0; return $"above {last}"; } private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; public static Settings Load(string modPath) { var path = Path.Combine(modPath, FileName); try { if (File.Exists(path)) return JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions) ?? new Settings(); var defaults = new Settings(); defaults.Save(modPath); ModManager.Log($"[{Mod.Name}] wrote default settings to {path}"); return defaults; } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] could not read {path}, using defaults: {ex.Message}", ModManager.LogLevel.Warn); return new Settings(); } } public void Save(string modPath) { try { File.WriteAllText(Path.Combine(modPath, FileName), JsonSerializer.Serialize(this, JsonOptions)); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] could not save settings: {ex.Message}", ModManager.LogLevel.Error); } } } /// One row of the size table: allegiances of up to this many members get this multiplier. public class SizeBand { /// Upper bound on the member count, inclusive. 0 means "everything above the previous band". public int UpToMembers { get; set; } public double Multiplier { get; set; } = 1.0; }