namespace Aeshnidae.Awakens; /// /// Every term of an awakening, in one file. /// /// An awakening is the soft enlightenment (Tom, 2026-09-19): the character goes /// back to level 1 and keeps everything else - skills, attributes, credits, /// unassigned experience, society, luminance, auras, gear. Retail enlightenment is /// untouched and runs beside it. What an awakening buys is a harder, richer copy of /// every instanced dungeon: Aeshnidae.InstancesNoDat counts awakenings into its /// tier, +5% difficulty and +5% rewards each - and one more level of cap, so the /// climb back is a level longer each time and a character must top it to awaken again. /// /// Since 2026-10-13 this mod also owns RESOLVE, the dial a character buys with pyreals /// (PropertyInt 9053, Resolve.cs): the danger their own challenge runs are set to, bought as /// seals and taken back by erasers. It is a separate ladder from the awakening count and does /// not touch the awakened dungeons; it lives here because this mod already owns the /// character's progression property. Its settings are in their own block at the bottom. /// /// Later the same day the SELLING moved out of this mod entirely: Ismene Vale is a plain ACE /// vendor now, so the shop window and the money are ACE's, the seals' price is a row in the /// world database, and the escalation lives in how many seals a step takes. Nothing here /// prices anything any more - see the Resolve block for which numbers are the truth and which /// are this mod's copy of them. /// public class Settings { public const string FileName = "Settings.json"; public bool Enabled { get; set; } = true; /// /// Levels the character's cap rises by with each awakening. The cap starts at /// retail (275): 276 after the first awakening, 277 after the second. A character /// must be at its own cap to awaken. /// public int LevelsPerAwakening { get; set; } = 1; /// /// How many levels past retail the in-memory XP table is grown at startup - the /// room for awakenings. A character whose cap would pass the table's end stops /// at the table's end. 100 is a hundred awakenings at one level each. /// public int LevelCapHeadroom { get; set; } = 100; /// How many times a character may awaken. 0 is uncapped. public int MaxAwakenings { get; set; } = 0; /// /// The awakening also resets skills and attributes, exactly as retail enlightenment /// does (Tom, 2026-09-19, evening): every skill untrained with its credits refunded, /// skill credits back to the heritage base plus the quest credits, attribute and /// vital ranks to zero, unassigned experience to zero, everything worn to the pack. /// Kept: luminance, auras, society, titles, Radiance and mastery raises. Without /// this the climb back produces experience with nothing retail to buy; with it the /// climb is real, and the character goes through the tiers again to earn the next /// one. Off is the original soft awakening: level to 1, everything else kept. /// public bool ResetSkillsAndAttributes { get; set; } = true; /// /// Items worn at the moment of awakening whose wield requirement is a LEVEL go to /// the pack, because a level-1 character cannot wear them; everything else stays /// on. Off keeps even those on, which the client will show but the server will /// refuse to re-equip. /// public bool DequipLevelGatedItems { get; set; } = true; /// /// The climb back would hand out skill credits at every credit level a second /// time, and the character kept every credit it had. On: an awakened character /// earns no level credits. Off: they stack, which is not the design. /// Only meaningful when ResetSkillsAndAttributes is off - the reset takes the /// level credits away, so the climb has to pay them again and this is ignored. /// public bool NoLevelCreditsAfterAwakening { get; set; } = true; /// Seconds a /awaken confirm is good for after /awaken. public double ConfirmSeconds { get; set; } = 60; /// The burst and fanfare at the moment of awakening. public bool Effects { get; set; } = true; /// Tell the whole server who awakened, and for the how-manyeth time. public bool BroadcastToServer { get; set; } = true; // ------------------------------------------------------------------ Resolve and seals // // Resolve (2026-10-13) is the OTHER dial this mod owns: the danger a character sets // their own challenge runs to. It is bought as SEALS - you buy Seals of Resolve from // Ismene Vale, press one onto your Scroll of Resolve, and the seals the next step needs // are consumed as the character's Resolve goes up by one. An ERASER, bought and used the // same way, takes one step back and gives nothing back. Both take effect the instant they // are used. Seal and eraser are transient purchase tokens; the progression is the // character property and nothing else. // // WHERE THE PRICE LIVES, AND WHERE THE ESCALATION LIVES (changed 2026-10-13, evening) // ----------------------------------------------------------------------------------- // Ismene is a REGULAR VENDOR now: you use her, her shop window opens, and you buy with // money as you would from any vendor. There is no command, no quote and no confirmation. // That moved two things: // // * The PRICE of one seal is FLAT, and it is a row in the WORLD DATABASE - the seal // weenie's Value, read through the vendor's own AlternateCurrency and SellPrice. A // vendor row cannot express a curve, so the price stopped being one. // * The ESCALATION became HOW MANY SEALS A STEP TAKES. Raising Resolve from N-1 to N // consumes SealsForStep(N) = SealsForFirstStep x SealsPerStepGrowth ^ (N-1), rounded // to a whole seal. At 1 x 1.5 that is the SAME geometric curve the pyreal price used // to be, only denominated in seals - so the total cost of reaching any Resolve is // what it always was, to within rounding. Seals.cs has the table. // // SealPyreals and EraserPyreals below are this mod's COPY of the vendor's price, used // only to write the readout. The world database is the truth. `resolve-status vendor` // reads both and says so when they disagree, because a readout quoting a price the vendor // does not charge is exactly the kind of drift this repo has been bitten by before. // // Every term of a seal lives in the Seals list below, so a SECOND KIND of seal is a // settings entry and two weenies, not new code. Resolve.cs has the whole design. /// /// The dial at all. Off: /resolve says so, no seal can be applied, no scroll is written, /// and any Resolve already on a character sits inert in the biota - nothing is lost. /// (It does NOT close the shop: Ismene is a world-database vendor and keeps selling until /// her stock list is emptied by a ledger. Applying a seal is what this switch stops.) /// public bool ResolveEnabled { get; set; } = true; /// /// One kind of seal: what it raises, how many seals each step of it takes, and the two /// weenies that carry it. A new kind (a denser roster, no recall, no vendor healing) is /// one more entry here plus its two weenies in a ledger - see Resolve.cs. /// public class SealKind { /// What a player and a command call it. One kind today, so nothing has to name it. public string Key { get; set; } = "resolve"; /// The word the readout uses: "Resolve 7". public string Label { get; set; } = "Resolve"; /// One line saying what this kind sets, for the readout. public string What { get; set; } = "the danger your challenge runs are set to"; /// /// The PropertyInt on the character this kind counts into - the progression itself. /// 9053 for Resolve, and it MUST STAY 9053: that id is the contract the /// challenge-run system reads (Resolve.Property). A new kind takes the next free id /// from the repo's allocation, never an invented one. /// public int PropertyId { get; set; } = 9053; /// The seal's weenie: 3000138 `aelrynthsealofresolve` (ledger 2026-10-13_001). public uint SealWcid { get; set; } = 3000138; /// /// The eraser's weenie: 3000139 `aelrynthresolveeraser`. One eraser kind PER SEAL /// kind rather than one generic eraser, so using it needs no question and no dialog - /// an Eraser of Resolve can only mean the one thing. /// public uint EraserWcid { get; set; } = 3000139; /// /// The highest this kind may reach. 0 is NO MAXIMUM, and that is the design: the /// seals-per-step curve is the only wall. Set a number only to put a ceiling on a /// kind while it is young. /// public int Max { get; set; } = 0; /// /// How many seals the FIRST step takes - Resolve 0 to Resolve 1. One: the first step /// is one seal, so a player's first purchase is immediately a step and the grammar /// teaches itself. 0 or less is read as 1. /// public int SealsForFirstStep { get; set; } = 1; /// /// What each step multiplies the last step's seal count by. 1.5. /// /// This is the escalation, and it is the ONLY wall there is - seals stack without /// limit and their price is flat. It is deliberately the same 1.5 the pyreal price /// used to compound by, so the total cost of reaching a given Resolve is unchanged: /// the k-th step used to cost 1.5^(k-1) times the base PRICE and now takes 1.5^(k-1) /// times the base NUMBER OF SEALS, which is the same money. /// /// Rounded to whole seals, so the ladder reads: step 1 = 1 seal, 2 = 2, 3 = 2, 4 = 3, /// 5 = 5, 6 = 8, 7 = 11, 8 = 17, 9 = 26, 10 = 38, 11 = 58, 15 = 292, 20 = 2,217. /// `resolve-status steps` prints it from this setting rather than from that list. /// /// Values below 1.0 are read as 1.0 - a flat curve - rather than honoured, so a typo /// cannot make a deep step cheaper than a shallow one. A step whose seal count passes /// Seals.Uncountable saturates and is REFUSED by name rather than wrapping negative /// (Aeshnidae.SkillMastery, 2026-09-22, learned that the hard way). /// public double SealsPerStepGrowth { get; set; } = 1.5; /// /// What Ismene charges for one seal, in pyreals. DISPLAY ONLY - see the block comment /// above. The truth is the seal weenie's Value and her AlternateCurrency, both in the /// world database (ledger 2026-10-13_005): 8 Trade Notes of 250,000 = 2,000,000. /// /// Two million was the first seal's price under the old escalating purchase and it is /// unchanged here: a number a character who farms can reach in an evening but not in /// an hour, so the first seal is a decision rather than a formality. /// public long SealPyreals { get; set; } = 2_000_000; /// /// What Ismene charges for one eraser, in pyreals. DISPLAY ONLY, as above: the truth /// is 4 Trade Notes of 250,000 = 1,000,000 in the world database. /// /// Half a seal, flat, and it RETURNS NOTHING. Going back is always cheaper than going /// forward, and at deep Resolve it is very much cheaper than the step it undoes - /// which is the right asymmetry for a difficulty you chose on purpose: if it turns out /// to be too much, getting out again should not be a second grind. It cannot be a /// money pump in either direction, because nothing comes back. /// public long EraserPyreals { get; set; } = 1_000_000; } /// /// Every kind of seal on this server. One today: Resolve, one step a use, no maximum. /// public List Seals { get; set; } = new() { new SealKind() }; /// /// The scroll's weenie: 3000137 `aelrynthresolvescroll`, a parchment shaped on retail's /// own 365, attuned and bonded, two pages (ledger 2026-10-13_001). It is the READOUT, and /// the thing a seal is used ON - the Resolve itself lives on the character, so replacing /// a destroyed one costs nothing but a walk to the Keeper. 0 means no scroll on this /// server and /resolve scroll says so. /// public uint ResolveScrollWcid { get; set; } = 3000137; /// /// Only the Keeper writes a scroll, and only for someone standing at her. Off lets /// `/resolve scroll` be typed anywhere, which is for testing and for a server with no /// Keeper placed. It has nothing to do with BUYING any more: buying is her vendor window, /// and ACE's own UseRadius decides how close you have to be for that. /// public bool ResolveSellerRequired { get; set; } = true; /// /// Which weenies write scrolls - and, for `resolve-status vendor`, which weenie to read /// the shop out of. /// /// The seller is **Ismene Vale, Keeper of Seals** (3000143), who stands at the arrival spot /// of the **Challenge Hall** - the shared instanced copy of the retail Town Network, landblock /// 0007, which is the home of the challenge-run system (ledger 2026-10-13_003, /// `Mods/Content/tools/challenge_hall_settings.py`). Resolve is the challenge-run difficulty /// dial and nothing else, so it is bought where the runs start. /// /// It moved there on 2026-10-13, the day it shipped. For the first few hours of that day the /// sellers were the **Town Wardens** (3000134 `aelrynth-npc-townwarden`, the number /// Aeshnidae.BuffBot's own `Wardens` setting names) because the hall did not exist yet and the /// wardens already stood in twenty towns and already took pyreals. That was always meant to be /// an interim, and the move cost exactly what this comment said it would: one setting and one /// `resolve-reload`, no rebuild and no deploy. Put another wcid here and the scroll writer /// moves again - but the SHOP is now in her weenie, so a new seller also needs the vendor /// rows of ledger 2026-10-13_005 copied onto it. /// public List ResolveSellerWcids { get; set; } = new() { 3000143 }; /// Metres from the Keeper a scroll may be asked for. The buff bot's own range is 10. public double ResolveSellerRange { get; set; } = 10.0; /// /// How the seller is named to a player who is not standing at her - the refusal line, the /// `/resolve` hint, the line an eraser gives when it is used on the wrong thing. /// /// It is a SETTING for the same reason is: the seller moves, /// and when it does, every line that tells a player where to go has to move with it or start /// lying. It already happened once - "A Town Warden handles this, until the hall of runs is /// built" was in three messages on the morning of 2026-10-13 and was wrong by the afternoon. /// A sentence begins with it, so capitalise it. /// public string ResolveSellerDescription { get; set; } = "The Keeper of Seals in the Challenge Hall"; 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); } } }