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