using System; using System.Collections.Generic; namespace AcDream.Core.Spells; // ───────────────────────────────────────────────────────────────────── // Scaffold for R1 — spell system data model + cast state machine. // Full research: docs/research/deepdives/r01-spell-system.md // Wire evidence: holtburger fixtures + ACE Player_Spells.cs. // ───────────────────────────────────────────────────────────────────── /// /// Magic school. Each maps to a skill (LifeMagic, CreatureMagic, ItemMagic, WarMagic, VoidMagic). /// See r01 §1 + ACE.Entity.Enum.MagicSchool. /// public enum MagicSchool : uint { None = 0, WarMagic = 1, LifeMagic = 2, CreatureEnchantment = 3, ItemEnchantment = 4, PortalMagic = 5, // VoidMagic added in later retail revisions; uses LifeMagic skill. VoidMagic = 6, } /// /// Per-spell targeting category. Validates before cast. /// public enum SpellTargetType : uint { None = 0, Self = 1, Item = 2, Creature = 3, Object = 4, SelfOrItem = 5, Undef = 6, OtherItem = 7, // targeted item but not your own } /// /// Spell category for enchantment stacking. Retail rule: same category + higher /// "power" replaces lower; different categories stack additively. See r01 §5. /// public enum SpellCategory : uint { Undef = 0, // There are ~600 categories in the retail dat. We reference them by number // rather than port the whole enum here. } [Flags] public enum SpellFlags : uint { None = 0, Beneficial = 0x00000001, Resistable = 0x00000002, Projectile = 0x00000004, EnchantmentDispel = 0x00000008, PurgeOnReset = 0x00000010, NotIndoors = 0x00000020, Melee = 0x00000040, Missile = 0x00000080, // more flags in r01 §1 } /// /// Per-spell on-disk record from the SpellTable dat (0x2Fxxxxxx). /// 27 fields in retail; r01 §1 has the full layout. /// public sealed class SpellDatEntry { public uint SpellId { get; init; } public string Name { get; init; } = ""; public string Description { get; init; } = ""; public MagicSchool School { get; init; } public int Power { get; init; } // difficulty / mastery public float CastingTime { get; init; } // seconds public float Duration { get; init; } // for enchants public int BaseMana { get; init; } public int ManaMod { get; init; } // per extra target public int ManaConversionBase { get; init; } public float ManaConversionMod { get; init; } public SpellTargetType TargetType { get; init; } public SpellCategory Category { get; init; } public SpellFlags Flags { get; init; } public IReadOnlyList FormulaComponentIds { get; init; } = Array.Empty(); public int RangeConstant { get; init; } public int EconomyMod { get; init; } public uint Icon { get; init; } // 0x06xxxxxx public uint SpellStatModKey { get; init; } // what property it buffs public int SpellStatModVal { get; init; } } /// /// Spell component from the SpellComponentsTable dat (0x30xxxxxx). /// Scarabs, herbs, talismans, taper wax. Each has a base consumption rate. /// public sealed class SpellComponentEntry { public int ComponentId { get; init; } public string Name { get; init; } = ""; public uint Icon { get; init; } public double CdmBonus { get; init; } // component-destruction modifier public double ManaMod { get; init; } public int Type { get; init; } // 1=scarab, 2=herb, 3=talisman, 4=taper, 5=pwax } /// /// Runtime cast state — retail's model is a 4-phase state machine: /// Preparing → Casting (syllables) → Releasing → Idle (success) / Fizzled. /// See r01 §3 for the exact transitions and timing. /// public enum SpellCastPhase { Idle, Preparing, // server validating Casting, // syllables being played Releasing, // cast animation hit-frame → effect resolves Fizzled, Complete, } public sealed class SpellCastStateMachine { public SpellCastPhase Phase { get; private set; } = SpellCastPhase.Idle; public uint SpellId { get; private set; } public uint? TargetGuid { get; private set; } public double StartedAt { get; private set; } public double CastDuration { get; private set; } // Fired when the server confirms the cast started. public event Action? OnPhaseChanged; public void BeginCast(uint spellId, uint? targetGuid, double castDurationSec, double nowSec) { SpellId = spellId; TargetGuid = targetGuid; StartedAt = nowSec; CastDuration = castDurationSec; TransitionTo(SpellCastPhase.Preparing); } public void ServerAckCastingStart() => TransitionTo(SpellCastPhase.Casting); public void ServerReleaseCast() => TransitionTo(SpellCastPhase.Releasing); public void ServerCompleteCast() => TransitionTo(SpellCastPhase.Complete); public void ServerFizzle() => TransitionTo(SpellCastPhase.Fizzled); private void TransitionTo(SpellCastPhase next) { Phase = next; OnPhaseChanged?.Invoke(this); } } /// /// Active buff/debuff on a character. Retail stacking rules: /// • Same category, same caster: replace if higher power /// • Same category, different caster: max power wins, both tracked /// • Different category: stack additively /// See r01 §5. /// public sealed class ActiveBuff { public uint SpellId { get; init; } public uint CasterGuid { get; init; } public SpellCategory Category { get; init; } public int Power { get; init; } public double StartedAt { get; init; } public double Duration { get; init; } public double EndsAt => StartedAt + Duration; public int StatModKey { get; init; } public int StatModValue { get; init; } } /// /// Fizzle + mana math. Retail-faithful formulas — see r01 §4. /// public static class SpellMath { /// /// Sigmoid fizzle curve. Returns chance-to-succeed in [0, 1]. /// chance = 1 / (1 + e^(-0.07 × (skill - difficulty))) /// Hard floor: if skill < difficulty-50, return 0 (auto-fizzle). /// public static double ChanceOfSuccess(int skill, int difficulty) { if (skill < difficulty - 50) return 0.0; double x = -0.07 * (skill - difficulty); return 1.0 / (1.0 + Math.Exp(x)); } /// /// Computes actual mana cost. Two sigmoid rolls at difficulty/2 then /// difficulty, each can reduce cost. See r01 §4. /// public static int ComputeManaCost(SpellDatEntry spell, int numTargets, int manaConvSkill, Random rng) { double cost = spell.BaseMana + spell.ManaMod * Math.Max(0, numTargets - 1); // First reduction roll at half difficulty if (rng.NextDouble() < ChanceOfSuccess(manaConvSkill, spell.Power / 2)) cost *= 0.5; // Second reduction roll at full difficulty if (rng.NextDouble() < ChanceOfSuccess(manaConvSkill, spell.Power)) cost *= 0.5; return (int)Math.Max(1, Math.Round(cost)); } }