namespace Aeshnidae.Enlightenment; /// /// Every rule of enlightenment, in one file. /// /// Retail enlightenment is a single hardcoded bargain: reset to level 1 and lose /// society, luminance, auras, aetheria and your unspent xp, in exchange for +1 to /// all skills, +2 vitality and a title. Aeshnidae keeps the shape and changes the /// terms, so every clause of that bargain is a field here rather than a literal in /// ACE's Enlightenment.cs. /// /// The requirement gate is enforced here too, not in the world database. Retail /// encodes it as an emote chain on the Font of Enlightenment (wcid 53412) - a /// Goto/InqIntStat state machine with the numbers baked into two rows. That cannot /// express "275 plus one per enlightenment you already have", and editing it means /// a SQL migration every time a number moves. So the mod intercepts the chain at /// its first Goto and answers the whole question in C#. The world database is left /// exactly as it shipped. /// /// One band of the post-enlightenment passup table: from this level, this much of what vassals pass up is received. public class PassupBand { public int FromLevel { get; set; } /// 0 to 100. 100 is retail's full passup; 0 receives nothing. public int Percent { get; set; } } public class Settings { public const string FileName = "Settings.json"; /// /// Master switch. Off leaves ACE's retail enlightenment running untouched - /// the patches stay applied but hand every call straight back. /// public bool Enabled { get; set; } = true; // ---- who may enlighten -------------------------------------------------- /// Level needed for a first enlightenment. public int BaseLevelRequirement { get; set; } = 275; /// /// Added to for each enlightenment already /// held, so the second costs 276, the third 277, and so on. Retail charged a /// flat 275 every time. /// /// Note this is the reason the gate cannot live in the world database: the /// InqIntStat row that guards it holds one min and one max, and this is a /// function of player state. /// public int LevelRequirementPerEnlightenment { get; set; } = 1; /// /// Hard ceiling on enlightenments. 0 is uncapped, which is the Aeshnidae /// default - retail stopped at 5 because that is how many titles exist, and /// handles that separately. /// public int MaxEnlightenments { get; set; } = 0; /// Free slots needed in the main pack, for the dequip and the certificate. public int RequiredFreeInventorySlots { get; set; } = 25; /// Require Master rank in one of the three societies. public bool RequireSocietyMaster { get; set; } = true; /// /// Require the full 65 luminance aura credits (everything except the two skill /// credit auras). Since is on, this is bought /// once and satisfies every later enlightenment for free. /// public bool RequireAllLuminanceAuras { get; set; } = true; // ---- what survives ------------------------------------------------------ /// /// Keep society membership and rank. Retail wiped both and stamped an /// "Enlightened<Society>Master" flag so the promotions officer could hand /// the rank straight back - a re-grind that existed only to be skipped. /// public bool KeepSociety { get; set; } = true; /// Keep the LumAug* aura ratings. Retail zeroed all thirteen. public bool KeepLuminanceAuras { get; set; } = true; /// /// Keep the quest flags that permit earning luminance at all. Retail erased /// them, so an enlightened character could not gain luminance until level 200 /// and a re-run of Nalicana's Test. /// public bool KeepLuminanceAccess { get; set; } = true; /// /// Keep banked AvailableLuminance / MaximumLuminance. Not mentioned either way /// in the Aeshnidae design; kept because wiping the small unspent remainder /// while handing back all thirteen auras it buys would be noise, not cost. /// public bool KeepLuminanceBalance { get; set; } = true; /// /// Keep the unspent xp pool (AvailableExperience). Retail zeroed it. /// /// This is the clause that closes Aeshnidae.XpCurrency's open hole rather than /// patching it. While retail zeroed the pool, a player could /xp it to a friend, /// enlighten, and have it sent back - the transfer dodged a cost that this /// server no longer charges. With the pool surviving by design there is nothing /// to dodge, and the exploit stops being one. /// /// Turning this off reopens it. logs a warning /// at startup if you do while XpCurrency is loaded. /// /// Only the *unspent* pool survives. Xp already sunk into levels, attributes and /// skill ranks is destroyed by the reset below, and that is still the bulk of a /// 275's lifetime earnings - so the cost of enlightening is unchanged in /// practice. /// public bool KeepUnassignedExperience { get; set; } = true; /// /// Keep aetheria slots and the mana field flags that open them. Off, per the /// design: losing the use of aetheria above your level requirement is one of /// the two things enlightenment is supposed to cost. /// public bool KeepAetheria { get; set; } = false; // ---- what resets -------------------------------------------------------- /// Reset TotalExperience and Level to 1. This is what makes the rest bite. public bool ResetLevel { get; set; } = true; /// Reset skill ranks and recompute available skill credits. public bool ResetSkills { get; set; } = true; /// /// Reset attribute and vital ranks. Not named in the design's keep or lose /// list; on, because a level 1 character holding maxed attributes would make /// cosmetic. /// public bool ResetAttributes { get; set; } = true; /// /// Move everything equipped into the pack. The design's "lose the ability to /// use weapons and armour over your level requirement" is enforced by ACE's own /// wield requirements once the level drops; this is what stops you keeping the /// gear on in the meantime. /// public bool DequipAllItems { get; set; } = true; // ---- what you gain ------------------------------------------------------ /// /// Award a title for the first five enlightenments (Awakened, Enlightened, /// Illuminated, Transcended, Cosmic Conscious). There is no sixth title in the /// client's dats, so enlightenments past five are silently untitled. /// public bool GrantTitles { get; set; } = true; /// Hand over an attribute reset certificate, wcid 46421. public bool GrantAttributeResetCertificate { get; set; } = true; /// Announce each enlightenment on the world broadcast channel. public bool BroadcastToServer { get; set; } = true; /// /// The three-beat effect at the Font: a private white-out and chant, the /// augmentation burst, the fireworks. Purely visual, and off means the /// enlightenment happens with only the chat lines. /// public bool Ceremony { get; set; } = true; // ---- the emote gate ----------------------------------------------------- /// /// The Goto label on the Font of Enlightenment that starts its requirement /// chain. Intercepting it is what lets every setting above take effect without /// a SQL change - see EmoteGatePatch. /// public string GateLabel { get; set; } = "EnlightenmentCheck"; /// /// The Goto label of the confirmation prompt to jump to once the mod's own /// checks pass. On wcid 53412 this is the InqYesNo that leads to the /// Enlightenment emote (type 9001). /// public string ConfirmLabel { get; set; } = "AbleToEnlighten"; /// /// Seconds after an enlightenment during which the same character cannot start /// another. Belt and braces: the level reset already fails the gate on a second /// attempt, but this closes the window between two confirmations answered in /// the same tick, which is the shape most emote-chain double-fires take. /// public double ReentryGuardSeconds { get; set; } = 30.0; /// /// Require the player to still be standing at the Font when the grant fires, /// not merely when they asked. /// /// This is the clause that closes the exploit the NPC was meant to fix. The old /// /enlighten command reset the character and then sent them to their lifestone; /// run in portal space the teleport could not land, so the player kept their /// position and got a level 1 character inside high-tier content. Moving to an /// NPC removes the command but not the gap - the yes/no confirmation is /// asynchronous, so a player can open the prompt at the Font, recall away, and /// answer yes from somewhere else. /// /// Portal space and mid-teleport are refused regardless of this setting; it only /// governs the proximity test, which is the part with a tunable number in it. /// public bool RequireProximityToNpc { get; set; } = true; /// /// Metres the player may be from the Font when the grant fires. Generous - this /// is meant to catch recalls and portals, not to punish someone who stepped /// back while reading the prompt. /// public float ProximityRadius { get; set; } = 15.0f; // ---- passup after enlightenment ---------------------------------------- /// /// Scale the allegiance passup an enlightened character RECEIVES from their /// vassals by their own level. Tom, 2026-09-16: enlightenment should be hard, as /// on retail, and a freshly enlightened patron was being carried through the /// early levels on their vassals' experience. What vassals generate, and ACE's /// loyalty and leadership arithmetic, are untouched; every receiver up the tree /// is scaled by their own level. The unenlightened are not touched at all. /// public bool ScalePassupAfterEnlightenment { get; set; } = true; /// /// The bands, ascending by : a character at or /// above a band's level receives that band's percent, until the next band. A /// level below the first band receives nothing. The last band is the ceiling. /// public List PassupAfterEnlightenment { get; set; } = new() { new() { FromLevel = 0, Percent = 0 }, new() { FromLevel = 40, Percent = 20 }, new() { FromLevel = 80, Percent = 40 }, new() { FromLevel = 100, Percent = 60 }, new() { FromLevel = 120, Percent = 100 }, }; // ---- notes, not settings ------------------------------------------------ /// /// Two couplings this mod deliberately does not own, recorded where whoever /// edits the numbers above will see them. /// /// Aeshnidae.SkillMastery: mastery ranks survive enlightenment and retail ranks /// do not, so mastery priced below the retail rank it competes with makes /// everything on this page toothless - players would skip retail progression /// entirely and keep their whole build through the reset. That invariant is /// SkillMastery's to hold, in its own cost bands. Nothing here should try to /// preserve or restore mastery: it survives because it is stored outside the /// skill, not because anyone special-cases it, and this mod must never write /// into a CreatureSkill to "keep" anything - ResetSkill zeroes Ranks, InitLevel /// and ExperienceSpent, and six other ACE methods bare-assign InitLevel. /// /// The +1 to all skills and +2 vitality per enlightenment are not settings here /// because they are not this mod's to set: ACE applies them in CreatureSkill.Base /// and CreatureVital, derived from PropertyInt.Enlightenment. They already match /// the design. Making them tunable would mean a second postfix on /// CreatureSkill.Base, where SkillMastery already lives, and SkillMastery's /// client-display patches add their bonus to InitLevel independently rather than /// reading Base - so a delta added here would apply on the server and not show on /// the client. Left alone on purpose. /// [JsonIgnore] public string DesignNotes => "see Settings.cs"; 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); } } }