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