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