docs+feat: 13 retail-AC deep-dives (R1-R13) + C# port scaffolds + roadmap E-H
78,000 words of grounded, citation-backed research across 13 major AC
subsystems, produced by 13 parallel Opus-4.7 high-effort agents. Plus
compact C# port scaffolds for the top-5 systems and a phase-E-through-H
roadmap update sequencing the work.
Research (docs/research/deepdives/):
- 00-master-synthesis.md (navigation hub + dependency graph)
- r01-spell-system.md 5.4K words (fizzle sigmoid, 8 tabs, 0x004A wire)
- r02-combat-system.md 5.9K words (damage formula, crit, body table)
- r03-motion-animation.md 8.2K words (450+ commands, 27 hook types)
- r04-vfx-particles.md 5.8K words (13 ParticleType, PhysicsScript)
- r05-audio-sound.md 5.6K words (DirectSound 8, CPU falloff)
- r06-items-inventory.md 7.4K words (ItemType flags, EquipMask 31 slots)
- r07-character-creation.md 6.3K words (CharGen dat, 13 heritages)
- r08-network-protocol-atlas 9.7K words (63+149+94 opcodes mapped)
- r09-dungeon-portal-space.md 6.3K words (EnvCell, PlayerTeleport flow)
- r10-quest-dialogs.md 7.1K words (emote-script VM, 122 actions)
- r11-allegiance.md 5.4K words (tree + XP passup + 5 channels)
- r12-weather-daynight.md 4.5K words (deterministic client-side)
- r13-dynamic-lighting.md 4.9K words (8-light cap, hard Range cutoff)
Every claim cites a FUN_ address, ACE file path, DatReaderWriter type,
or holtburger/ACViewer reference. The master synthesis ties them into a
dependency graph and phase sequence.
Key architectural finding: of 94 GameEvents in the 0xF7B0 envelope,
ZERO are handled today — that's the largest network-protocol gap and
blocks F.2 (items) + F.5 (panels) + H.1 (chat).
C# scaffolds (src/AcDream.Core/):
- Items/ItemInstance.cs — ItemType/EquipMask enums, ItemInstance,
Container, PropertyBundle, BurdenMath
- Spells/SpellModel.cs — SpellDatEntry, SpellComponentEntry,
SpellCastStateMachine, ActiveBuff,
SpellMath (fizzle sigmoid + mana cost)
- Combat/CombatModel.cs — CombatMode/AttackType/DamageType/BodyPart,
DamageEvent record, CombatMath (hit-chance
sigmoids, power/accuracy mods, damage formula),
ArmorBuild
- Audio/AudioModel.cs — SoundId enum, SoundEntry, WaveData,
IAudioEngine / ISoundCache contracts,
AudioFalloff (inverse-square)
- Vfx/VfxModel.cs — 13 ParticleType integrators, EmitterDesc,
PhysicsScript + hooks, Particle struct,
ParticleEmitter, IParticleSystem contract
All Core-layer data models; platform-backed engines live in AcDream.App.
Compiles clean; 470 tests still pass.
Roadmap (docs/plans/2026-04-11-roadmap.md):
- Phase E — "Feel alive": motion-hooks + audio + VFX
- Phase F — Fight + cast + gear: GameEvent dispatch, inventory,
combat, spell, core panels
- Phase G — World systems: sky/weather, dynamic lighting, dungeons
- Phase H — Social + progression: chat, allegiance, quests, char creation
- Phase J — Long-tail (renumbered from old Phase E)
Quick-lookup table updated with 10+ new rows mapping observations to
new phase letters.
This commit is contained in:
parent
7230c1590f
commit
3f913f1999
20 changed files with 15312 additions and 17 deletions
216
src/AcDream.Core/Spells/SpellModel.cs
Normal file
216
src/AcDream.Core/Spells/SpellModel.cs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
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.
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Magic school. Each maps to a skill (LifeMagic, CreatureMagic, ItemMagic, WarMagic, VoidMagic).
|
||||
/// See r01 §1 + <c>ACE.Entity.Enum.MagicSchool</c>.
|
||||
/// </summary>
|
||||
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,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-spell targeting category. Validates before cast.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spell category for enchantment stacking. Retail rule: same category + higher
|
||||
/// "power" replaces lower; different categories stack additively. See r01 §5.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-spell on-disk record from the SpellTable dat (0x2Fxxxxxx).
|
||||
/// 27 fields in retail; r01 §1 has the full layout.
|
||||
/// </summary>
|
||||
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<int> FormulaComponentIds { get; init; } = Array.Empty<int>();
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spell component from the SpellComponentsTable dat (0x30xxxxxx).
|
||||
/// Scarabs, herbs, talismans, taper wax. Each has a base consumption rate.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<SpellCastStateMachine>? 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fizzle + mana math. Retail-faithful formulas — see r01 §4.
|
||||
/// </summary>
|
||||
public static class SpellMath
|
||||
{
|
||||
/// <summary>
|
||||
/// Sigmoid fizzle curve. Returns chance-to-succeed in [0, 1].
|
||||
/// <c>chance = 1 / (1 + e^(-0.07 × (skill - difficulty)))</c>
|
||||
/// Hard floor: if skill < difficulty-50, return 0 (auto-fizzle).
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes actual mana cost. Two sigmoid rolls at difficulty/2 then
|
||||
/// difficulty, each can reduce cost. See r01 §4.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue