namespace Aeshnidae.Fellowship; public class Settings { public const string FileName = "Settings.json"; public bool Enabled { get; set; } = true; /// /// How many people may be in one fellowship. Retail is 9. /// /// ACE keeps this in a public static field, so it is simply assigned rather than /// patched - but raising it without extending is a /// bug, not a feature: ACE's own share table stops at 9 and falls through to 1.0, /// which would hand every member of a larger fellowship a full share each. The two /// settings are coupled, and this mod keeps them consistent. /// public int MaxFellows { get; set; } = 9; /// /// What fraction of earned experience each member receives, by fellowship size. /// The key is the number of members; 1 means soloing in a fellowship of one. /// /// Retail's curve is a single steep step from 1 to 2 members and then a steady /// -0.05 per member: 1.0, .75, .6, .55, .5, .45, .4, .35, .3. Generated to /// MaxFellows on first run, continuing that decline past 9, and then edited by hand. /// /// Note this is share PER MEMBER, not a split of a fixed pool - a fellowship of /// nine at .3 each yields 2.7x the experience one player would have earned. That is /// retail behaviour, and it is why the number falls as the group grows rather than /// staying at 1.0. /// public Dictionary SharePercentages { get; set; } = new(); /// /// Share for a fellowship larger than the table covers. A floor rather than a /// fallthrough, because ACE's own fallthrough is 1.0 and that is the bug this /// setting exists to prevent. /// public double ShareBeyondTable { get; set; } = 0.10; /// Fills in the retail curve, extended to MaxFellows, on first run. public bool EnsureSharePercentages() { if (SharePercentages.Count > 0) return false; // Retail, verbatim, for the range retail covers. var retail = new Dictionary { [1] = 1.00, [2] = 0.75, [3] = 0.60, [4] = 0.55, [5] = 0.50, [6] = 0.45, [7] = 0.40, [8] = 0.35, [9] = 0.30, }; for (var count = 1; count <= Math.Max(MaxFellows, 9); count++) { if (retail.TryGetValue(count, out var pct)) SharePercentages[count] = pct; else // Continue retail's -0.05 per member, floored so it never reaches zero. SharePercentages[count] = Math.Max(ShareBeyondTable, 0.30 - 0.05 * (count - 9)); } return true; } /// Share for a given member count, from the table or the floor. public double ShareFor(int members) { if (members <= 0) return 1.0; return SharePercentages.TryGetValue(members, out var pct) ? pct : ShareBeyondTable; } private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, }; 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(); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] could not read {FileName}, 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 write {FileName}: {ex.Message}", ModManager.LogLevel.Warn); } } }