namespace Aeshnidae.AwakenedRewards; /// /// The terms of the awakened rewards, in one file. /// /// Two rewards for awakening (Tom, 2026-09-19), both read off the character's /// awakening count - PropertyInt 9040, which Aeshnidae.Awakens owns and this mod /// only reads: /// /// 1. Loot rolled inside a scaled dungeon copy (Aeshnidae.InstancesNoDat, tier = /// the opener's awakenings) is better - a richer quality roll and, every few /// tiers, a higher loot tier - and it is GATED: every piece carries a wield /// requirement of "awakenings at least the copy's tier", so it cannot be handed /// down to the unawakened. A tier-0 copy and the retail landblocks roll retail. /// /// 2. An attuned quest item scales, once, to the awakenings of the character it /// first belongs to: re-earning a quest item at a higher awakening yields a /// better one. Attuned already binds the item to that character, so it needs no /// wield gate of its own - the binding is the gate. /// /// Neither touches spells, level or skill requirements, or anything on an item that /// is not from a scaled copy or not attuned. /// public class Settings { public const string FileName = "Settings.json"; public bool Enabled { get; set; } = true; // ------------------------------------------------------------ loot table additions /// /// Stamina kits in loot (2026-09-21). Retail dropped healing kits only; the stamina /// kits existed but came from vendors and quests. When ACE rolls a healing kit, /// this share of the time it becomes a stamina kit of the same tier instead - /// Lesser (44711, 50 uses) up to tier 3, Greater (44714) from tier 4. 0 = never. /// Lives here, not in a mod of its own, because a new mod is a server restart. /// public float StaminaKitShare { get; set; } = 0.33f; /// The tier from which the Greater Stamina Kit always drops instead of the Lesser. public int GreaterStaminaKitFromTier { get; set; } = 4; /// /// Below that tier, the chance a stamina kit is the Greater anyway. Stamina is the /// battle of the early game, so the good kit is findable at every level - rarely. /// public float GreaterStaminaKitChanceBelow { get; set; } = 0.15f; // ------------------------------------------------------------ loot from scaled copies /// /// The whole of the first reward. Off, corpses and chests in a scaled copy roll /// exactly what the retail landblock would. /// public bool ScaleCopyLoot { get; set; } = true; /// /// Percentage points added to the loot profile's LootQualityMod per tier of the /// copy the loot is rolled in. ACE's quality mod is a 0..1 skew on every roll a /// loot item makes - cantrips, workmanship, the number of spells - so +2 points a /// tier is a gentle, continuous "richer", capped at 1.0 where the factory's rolls /// saturate. Tier 10 is +20 points; a profile at 0.2 reaches 0.4. /// public double LootQualityPercentPerTier { get; set; } = 2.0; /// /// The ceiling on the loot quality mod (2026-09-22). ACE's ChanceTable.Roll does not /// weight a roll by this number - it DISCARDS every entry whose cumulative chance is /// below it, so at 1.0 every table returns its last and best row: maximum cantrips, /// legendary levels, the best of everything, every time. Retail's own profiles run /// 0.00 to 0.40 and 234 of 275 are 0.00, so 0.40 is "as good as the best chest in the /// game" and anything approaching 1.0 is not a skew but a guarantee. /// public double MaxLootQuality { get; set; } = 0.40; /// /// Every this many tiers, the profile's loot TIER goes up by one (capped at 8, /// ACE's top tier). The tier is the coarse lever - it changes which wield tables, /// materials and spell levels an item can roll - so it steps rather than creeps: /// at 5, tier-5 copies roll one loot tier up, tier-10 copies two. 0 disables the /// step and leaves only the quality mod. /// public int LootTierStepEveryTiers { get; set; } = 5; /// /// Every equippable item rolled in a scaled copy needs awakenings at least the /// copy's tier to wear (the 9043 stamp, enforced by a postfix on /// Player.CheckWieldRequirements; see Loot.cs for why not a wield requirement on /// the item). This is what keeps the /// richer loot with the awakened - it can be traded and sold, but not worn by a /// character who has not earned the tier. Off, the loot is better and free. /// public bool MinimumAwakeningsToWieldLoot { get; set; } = true; /// /// How far below the copy's tier the wield gate sits. 5: loot from a tier-10 copy /// needs 5 awakenings to wear (Tom, 2026-09-19, a player's suggestion to encourage /// trade: the best drops can go to people a few awakenings behind, who could not /// have found them yet, without reaching the unawakened). 0 is the gate at the tier /// itself. The stamp on the item is still the tier; the gate is derived. /// public int WieldGateBelowTier { get; set; } = 5; /// The awakenings a piece of tier-N copy loot needs to wear, under the settings in force. public int WieldGateFor(int tier) => Math.Max(0, tier - Math.Max(0, WieldGateBelowTier)); // ------------------------------------------------------------ attuned quest items /// /// The whole of the second reward. Off, attuned items are retail and are not /// stamped; items already scaled keep what they have. /// public bool ScaleAttunedItems { get; set; } = true; /// /// Copy loot's numbers grow too, not only its rolls (Tom, 2026-09-19: the damage /// on weapons, the attack and defence modifiers, need to be better, not just /// better-rolled). The same table Attuned items use - damage, armour, the weapon /// and damage modifiers, ratings - by the copy's tier x PercentPerAwakening, /// capped at MaxPercent, so loot and quest items sit on one curve. /// public bool ScaleCopyLootStats { get; set; } = true; /// /// Percent per awakening of the first owner that an attuned item's combat numbers /// grow by - damage, damage modifiers, armour, the ratings. Two per awakening, /// the same figure as LootQualityPercentPerTier, so the two rewards climb /// together (Tom, 2026-09-19: quest gear must stay consistent with loot). /// Multipliers above 1.0 (DamageMod, ElementalDamageMod, the weapon mods, mana /// conversion) scale only the part above 1.0, so a 1.20 becomes 1.22 at 10%, not /// 1.32. /// public double PercentPerAwakening { get; set; } = 2.0; /// /// The most an attuned item's bonus can ever be, in percent, however many times /// its first owner has awakened. Reached at 20 awakenings by default (40 / 2). /// /// Loot has a ceiling built in: the quality mod saturates at 1.0 and the loot tier /// at 8, so a tier-100 copy rolls the same as a tier-40 one. Quest items have no /// such ceiling in ACE - a percentage per awakening left uncapped would hand a /// hundred-times-awakened character a Sword of Lost Light three times its retail /// damage while the drops next to it stopped improving sixty awakenings ago. So /// the cap is written down here (Tom, 2026-09-19). The stamp on the item (9042, /// and the name) still records the owner's full count; only the bonus is clamped. /// 0 disables the cap. /// public double MaxPercent { get; set; } = 40.0; // ------------------------------------------------------------ both /// /// Append " (Awakened N)" to the name of every item either reward touched, N the /// tier or the count applied. The client has no way to show a custom property, so /// the name is how a player tells a tier-3 drop from a retail one at a glance - /// and how staff tell, in a trade dispute, which is which. Off, the stamps /// (PropertyInt 9042 on quest items, 9043 on loot) are still written; only the /// name is left alone. /// public bool StampNames { get; set; } = true; // ------------------------------------------------------------ the tier, for the player's client /// /// Tell each player's own client the difficulty tier where they stand: PropertyInt 9051 on /// the player, a private update only they receive, never saved (TierFeed.cs, 2026-10-03). /// Sent on login, on teleport, on arrival in a copy and whenever it changes; 0 = real /// Dereth or an unscaled copy. Client helpers (RynthAi, the damage panel) use it to keep a /// world-10 monster's numbers apart from the real one's. Off = nothing is sent; clients /// keep the last value they were told until they log out. /// public bool SendTierToClient { get; set; } = true; /// One log line per tier sent (diagnostic; off normally - it fires on every crossing). public bool LogTierFeed { get; set; } = false; 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); } } }