namespace Aeshnidae.Awakens; /// /// Resolve: the difficulty dial a character raises on purpose, and the contract the rest of /// the server reads it by. /// /// Resolve is the danger a **challenge run** is set to - the new dungeon type being /// designed: random but difficulty-matched monsters, no corpses, a boss and a chest, get in /// and get out. It is bought as SEALS from a vendor, applied to a scroll, and taken back by /// ERASERS; the whole of that is in . There is no entry fee for a dungeon /// and no toll at a portal: the cost is the seals. /// /// WHAT IT DOES TODAY: nothing yet, and the readout says so in those words. The challenge /// dungeons do not exist, and Resolve deliberately changes NOTHING about any dungeon that /// does. The awakened hub ladder and every personal copy still scale with the character's /// awakenings, +5% a tier, exactly as before - this mod does not touch /// Aeshnidae.InstancesNoDat's tier, and nothing here is wired into it. A character with /// Resolve 50 sees precisely the dungeons they saw with Resolve 0. /// /// Why build the dial before the dungeons: the number has to be bought, be permanent, /// survive a logout, resist being farmed cheaply and be readable by a sibling mod. That is /// the hard half, it is testable on its own, and the challenge-run build then reads one /// documented number instead of inventing its own currency. is that /// contract - see "FOR THE MOD THAT CONSUMES THIS" below. /// /// WHY IT IS STATE ON THE CHARACTER AND NOT AN ITEM (design settled 2026-10-13) /// ---------------------------------------------------------------------------- /// Progression that lives in an item is losable - dropped on death, left in a vault, /// eaten by a bug - and "my difficulty got deleted" is worse than any convenience an /// item would buy. So Resolve is , PropertyInt 9053 on the /// character: an id ACE's PropertyInt enum does not define, so it is never sent to a /// client, never shown, and simply rides in the biota - saved with the character, backed /// up with the character, and left alone by retail enlightenment and by the awakening /// reset, both of which touch only the properties they name. It follows the house /// pattern: 9040 is this mod's awakening count, 9041 the attribute-only creature marker, /// 9042/9043 AwakenedRewards' item stamps, 9044 Keep's copy tag, 9045 the PK restore, /// 9046-9050 Expedition's, 9051 the tier feed, 9052 Towns' "inside my town copy". /// 9053 and 9054 were free in code and in both databases, live and staging, on 2026-10-13. /// /// The seal, the eraser and the scroll are all REPLACEABLE THINGS. The scroll /// () is a readout and the surface a seal is used on; losing it costs a /// walk to the Keeper. Seals are purchase tokens; losing unused ones costs a purchase and /// never a step. Neither is ever the truth. /// /// FOR THE MOD THAT CONSUMES THIS /// ------------------------------ /// Two routes, both supported, in order of preference: /// /// 1. Read the property. `player.GetProperty((PropertyInt)9053) ?? 0`, clamped at 0. /// No binding, no reflection, nothing to break on a reload - exactly how /// InstancesNoDat reads 9040 today ("the two mods share a number and nothing else"). /// Take this one unless the mod needs the cost curve. /// 2. Bind reflectively, the sibling-mod pattern Aeshnidae.Towns and /// Aeshnidae.AwakenedRewards use (their Instances.cs explains it at length): every mod /// is loaded into its own collectible assembly context, so a project reference would /// pin a second copy of these types and cast nothing. , /// and are kept stable for that /// purpose - their signatures use only host types (ACE's IPlayer) and primitives: /// /// var container = ModManager.GetModContainerByName("Aeshnidae.Awakens", false); /// var type = container?.ModAssembly?.GetType("Aeshnidae.Awakens.Resolve"); /// var of = AccessTools.Method(type, "Of", new[] { typeof(IPlayer) }) /// .CreateDelegate<Func<IPlayer, int>>(); /// /// Re-bind whenever the assembly differs; a hot reload of this mod replaces it. /// /// Do NOT write 9053 from another mod. It is bought, and this mod is where it is applied. /// public static class Resolve { /// /// PropertyInt 9053: the Resolve in force for this character. THE contract - the /// challenge-run system reads this id, so the "resolve" seal kind's PropertyId setting /// must stay 9053. /// public const PropertyInt Property = (PropertyInt)9053; /// /// PropertyInt 9054, RETIRED on 2026-10-13 the same day it shipped. It counted how many /// Seals of Resolve a character had PAID FOR and not erased away, and it existed for one /// reason: the price escalated with every seal bought, so reading the price off the /// APPLIED count would have let a player buy ten seals before applying any and pay the /// first step's price ten times. /// /// The price is FLAT now (Ismene Vale is a plain vendor and a vendor's price is one fixed /// number per item - see ), so buying early costs exactly the same as /// buying late and there is nothing left to dodge. The count would be a number nothing /// reads and nothing can act on, which is worse than no number at all. /// /// This constant exists ONLY so can delete the row, and /// nothing reads its value. Both shard databases, live and staging, had zero rows of it /// when it was retired, so there was nothing to migrate; the sweep is there for a /// character restored from a backup older than that afternoon. /// public const PropertyInt RetiredPaidProperty = (PropertyInt)9054; /// /// A price no pack can hold, used as the saturating value for "beyond arithmetic". /// Not long.MaxValue, so that nothing built out of it - a total of several steps, say - /// can wrap either. is the same guard for seal counts, /// which is where the curve now lives. /// /// A compounding curve leaves the range of a long quickly, and an unchecked cast of a /// double that large wraps NEGATIVE - a negative price passes every affordability /// check and then pays the buyer. Aeshnidae.SkillMastery minted 7.19e18 Radiance that /// way on 2026-09-22 (its Mastery.PriceCeiling is the fix). Here a saturated number is /// simply refused by name: nobody has to be able to take the next step, so the curve is /// allowed to end. /// public const long Unaffordable = long.MaxValue / 4; /// /// This character's Resolve. THE public contract for every other mod (see the class /// comment); its signature takes only ACE's own IPlayer, so it binds across the mod /// boundary and works for an offline character as well as an online one. /// public static int Of(IPlayer? player) => Math.Max(0, player?.GetProperty(Property) ?? 0); /// This character's awakenings - the other, earned ladder. Not a Resolve input. public static int AwakeningsOf(IPlayer? player) => Math.Max(0, player?.GetProperty(Awakening.Property) ?? 0); /// The Resolve seal kind, or null when it has been configured away. public static Settings.SealKind? Kind => Seals.KindNamed("resolve") ?? Seals.Kinds.FirstOrDefault(); /// /// How many Seals of Resolve the -th step takes - the escalation, /// since 2026-10-13 evening. Saturates at . Kept as a /// stable name for a consuming mod; the curve itself lives in . /// public static int SealsForStep(int step) => Kind is { } kind ? Seals.SealsForStep(kind, step) : Seals.Uncountable; /// /// What the -th step costs in PYREALS, saturating at /// . /// /// NOTE FOR ANYONE WHO BOUND THIS BEFORE 2026-10-13 EVENING: it used to be the price of /// ONE seal, read off the paid count, and the escalation was in the price. It is now the /// seals that step takes times the flat seal price - the same money for the same step, but /// a different question. is the one to read if you want the /// curve itself. /// public static long PriceOfStep(int step) => Kind is { } kind ? Seals.PyrealsForStep(kind, step) : Unaffordable; /// /// Deletes the retired paid count off a character, if it is there. Called wherever this /// mod writes a character's Resolve, so a biota restored from before the retirement is /// cleaned the first time its owner touches a seal rather than carrying a number that /// means nothing for ever. Never throws, and never touches anything else. /// public static void RetirePaidCount(IPlayer? player) { try { if (player?.GetProperty(RetiredPaidProperty) is not null) { player.RemoveProperty(RetiredPaidProperty); ModManager.Log($"[{Mod.Name}] removed the retired seals-paid-for count (9054) from {player.Name}"); } } catch { } } /// /// What is applied and what the next step takes - the same lines for /resolve and /// for the scroll, so the two can never say different things. /// /// Written to be honest today and still true later: it says what Resolve sets (the /// danger of a challenge run), that challenge runs are not open yet, and that it /// changes nothing in the dungeons that are. Nothing here claims an effect this mod /// does not have - the percentages of the awakened copies belong to the awakening count /// and are not quoted at all. The prices come out of the WORLD DATABASE, so the readout /// quotes what the shop will really charge. /// /// is the online character whose pack is counted, when there /// is one; a scroll minted for an offline character from the console simply leaves the /// "you carry" line out rather than guessing. /// public static List Readout(IPlayer reader, Player? carrier = null) { var lines = new List(); foreach (var kind in Seals.Kinds) lines.AddRange(Seals.Lines(carrier, reader, kind)); if (lines.Count == 0) lines.Add("There is no seal of any kind on this server."); lines.Add(" this scroll : a readout, and the parchment you press a seal onto. Your Resolve is recorded on you, " + "not on it - burn it and you lose nothing but the reading; the Keeper will write another."); return lines; } }